complete lab3
This commit is contained in:
parent
a3134aa64f
commit
c88b6ac234
38
Lab3/.vscode/launch.json
vendored
Normal file
38
Lab3/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ChatRoom",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build-ChatRoom",
|
||||
"program": "${workspaceFolder}/ChatRoom/bin/Debug/net6.0/ChatRoom.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/ChatRoom",
|
||||
"console": "externalTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Moderator",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build-Moderator",
|
||||
"program": "${workspaceFolder}/Moderator/bin/Debug/net6.0/Moderator.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/Moderator",
|
||||
"console": "externalTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Participant",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build-Participant",
|
||||
"program": "${workspaceFolder}/Participant/bin/Debug/net6.0/Participant.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/Participant",
|
||||
"console": "externalTerminal",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
]
|
||||
}
|
41
Lab3/.vscode/tasks.json
vendored
Normal file
41
Lab3/.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build-ChatRoom",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/ChatRoom/ChatRoom.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build-Moderator",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/Moderator/Moderator.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build-Participant",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/Participant/Participant.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
20
Lab3/ChatRoom/ChatRoom.csproj
Normal file
20
Lab3/ChatRoom/ChatRoom.csproj
Normal file
@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChatRoomContract\ChatRoomContract.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
296
Lab3/ChatRoom/ChatRoomLogic.cs
Normal file
296
Lab3/ChatRoom/ChatRoomLogic.cs
Normal file
@ -0,0 +1,296 @@
|
||||
using NLog;
|
||||
|
||||
namespace ChatRoom;
|
||||
|
||||
/// <summary>
|
||||
/// Chat room service logic
|
||||
/// </summary>
|
||||
public class ChatRoomLogic
|
||||
{
|
||||
/// <summary>
|
||||
/// Background thread of deleting approved or rejected messages
|
||||
/// </summary>
|
||||
private Thread thread;
|
||||
/// <summary>
|
||||
/// Chat Room state
|
||||
/// </summary>
|
||||
private ChatRoomState state = new ChatRoomState();
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
private Logger log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Chat room logic constructor
|
||||
/// </summary>
|
||||
public ChatRoomLogic()
|
||||
{
|
||||
thread = new Thread(BackgroundTask);
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the next incrementing ID
|
||||
/// </summary>
|
||||
/// <returns>Unique ID</returns>
|
||||
int NextId()
|
||||
{
|
||||
int id = state.lastUniqueId;
|
||||
state.lastUniqueId++;
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a client
|
||||
/// </summary>
|
||||
/// <param name="name">Client name</param>
|
||||
/// <returns>Client ID</returns>
|
||||
public int RegisterClient(string name)
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
int clientId = NextId();
|
||||
state.clients.Add(new Client
|
||||
{
|
||||
id = clientId,
|
||||
name = name
|
||||
});
|
||||
log.Info($"Registered with client '{name}' with id {clientId}");
|
||||
return clientId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find a client by ID, can return NULL if not found
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <returns>Optional client object</returns>
|
||||
Client? FindClientById(int clientId)
|
||||
{
|
||||
foreach (var client in state.clients)
|
||||
{
|
||||
if (client.id == clientId)
|
||||
{
|
||||
return client;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find message by ID, can return NULL if not found
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message ID</param>
|
||||
/// <returns>Optional message object</returns>
|
||||
Message? FindMessageById(int messageId)
|
||||
{
|
||||
foreach (var message in state.messages)
|
||||
{
|
||||
if (message.id == messageId)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if client is still blocked, will clear `blockedUntil` if client became unblocked
|
||||
/// </summary>
|
||||
/// <param name="client">Client object</param>
|
||||
/// <returns>Is client blocked?</returns>
|
||||
bool GetAndUpdateBlockedState(Client client)
|
||||
{
|
||||
if (client.blockedUntil != null && DateTime.UtcNow >= client.blockedUntil)
|
||||
{
|
||||
client.blockedUntil = null;
|
||||
}
|
||||
|
||||
return client.blockedUntil != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a message
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <param name="contents">Message contents</param>
|
||||
/// <param name="needsToBeCensored">Does this message need to be censored?</param>
|
||||
/// <returns>Was sending the message successful, can fail if client is blocked</returns>
|
||||
public bool SendMessage(int clientId, string contents, bool needsToBeCensored)
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
var client = FindClientById(clientId);
|
||||
if (client == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetAndUpdateBlockedState(client))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var message = new Message
|
||||
{
|
||||
id = NextId(),
|
||||
clientId = clientId,
|
||||
contents = contents,
|
||||
needsToBeCensored = needsToBeCensored,
|
||||
status = MessageStatus.WaitingForModerator,
|
||||
sentAt = DateTime.UtcNow
|
||||
};
|
||||
state.messages.Add(message);
|
||||
log.Info($"Client '{client.name}' ({client.id}) sent message '{contents}' ({message.id}). Needs to censored: {needsToBeCensored}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get next message which isin't approved or rejected
|
||||
/// </summary>
|
||||
/// <returns>Optional message object</returns>
|
||||
public ChatRoomContract.Message? GetNewMessage()
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
foreach (var message in state.messages)
|
||||
{
|
||||
if (message.status != MessageStatus.WaitingForModerator) continue;
|
||||
|
||||
log.Info($"Message '{message.id}' given to moderator");
|
||||
message.status = MessageStatus.GivenToModerator;
|
||||
return new ChatRoomContract.Message
|
||||
{
|
||||
id = message.id,
|
||||
contents = message.contents,
|
||||
needsToBeCensored = message.needsToBeCensored
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Approve a message
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message ID</param>
|
||||
public void ApproveMessage(int messageId)
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
var message = FindMessageById(messageId);
|
||||
if (message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.status != MessageStatus.GivenToModerator)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
message.status = MessageStatus.Approved;
|
||||
log.Info($"Message {message.id} was approved");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reject a message
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message ID</param>
|
||||
public void RejectMessage(int messageId)
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
var message = FindMessageById(messageId);
|
||||
if (message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.status != MessageStatus.GivenToModerator)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
message.status = MessageStatus.Rejected;
|
||||
log.Info($"Message {message.id} was rejected");
|
||||
|
||||
var client = FindClientById(message.clientId);
|
||||
if (client != null && !GetAndUpdateBlockedState(client))
|
||||
{
|
||||
client.strikes++;
|
||||
|
||||
var rnd = new Random();
|
||||
if (client.strikes > rnd.Next(0, 10))
|
||||
{
|
||||
log.Info($"Client '{client.name}' ({client.id}) was blocked for {client.strikes}s");
|
||||
client.blockedUntil = DateTime.UtcNow.AddSeconds(client.strikes);
|
||||
client.strikes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get number of strikes a client has
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <returns>Number of strikes</returns>
|
||||
public int GetStrikes(int clientId)
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
var client = FindClientById(clientId);
|
||||
if (client == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return client.strikes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get timestamp until when the client is blocked
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <returns>Optional datetime object</returns>
|
||||
public DateTime? GetBlockedUntil(int clientId)
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
var client = FindClientById(clientId);
|
||||
if (client == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return client.blockedUntil;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main loop for background thread. Used for deleting approved or rejected messages.
|
||||
/// </summary>
|
||||
public void BackgroundTask()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
lock (state.accessLock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
int count = state.messages.RemoveAll(msg =>
|
||||
(now - msg.sentAt).TotalSeconds > 5 &&
|
||||
(msg.status == MessageStatus.Approved || msg.status == MessageStatus.Rejected)
|
||||
);
|
||||
log.Info($"Running periodic cleanup, removed {count} messages");
|
||||
}
|
||||
|
||||
Thread.Sleep(10 * 1000);
|
||||
}
|
||||
}
|
||||
}
|
156
Lab3/ChatRoom/ChatRoomService.cs
Normal file
156
Lab3/ChatRoom/ChatRoomService.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using RabbitMQ.Client;
|
||||
using Newtonsoft.Json;
|
||||
using RabbitMQ.Client.Events;
|
||||
using ChatRoomContract;
|
||||
using System.Text;
|
||||
using MessagePack;
|
||||
using System.Diagnostics;
|
||||
using NLog.LayoutRenderers.Wrappers;
|
||||
using NLog;
|
||||
|
||||
namespace ChatRoom;
|
||||
|
||||
internal class ChatRoomService
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger for this class.
|
||||
/// </summary>
|
||||
private Logger log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Communications channel to RabbitMQ message broker.
|
||||
/// </summary>
|
||||
private IModel rmqChannel;
|
||||
|
||||
/// <summary>
|
||||
/// Service logic.
|
||||
/// </summary>
|
||||
private ChatRoomLogic logic = new ChatRoomLogic();
|
||||
|
||||
public ChatRoomService(IConnection connection, string exchangeName, string serverQueueName)
|
||||
{
|
||||
//get channel, configure exchanges and request queue
|
||||
rmqChannel = connection.CreateModel();
|
||||
|
||||
rmqChannel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct);
|
||||
rmqChannel.QueueDeclare(queue: serverQueueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||
rmqChannel.QueueBind(queue: serverQueueName, exchange: exchangeName, routingKey: serverQueueName, arguments: null);
|
||||
|
||||
//connect to the queue as consumer
|
||||
//XXX: see https://www.rabbitmq.com/dotnet-api-guide.html#concurrency for threading issues
|
||||
var rmqConsumer = new EventingBasicConsumer(rmqChannel);
|
||||
rmqConsumer.Received += (consumer, delivery) => OnMessageReceived(((EventingBasicConsumer)consumer).Model, delivery);
|
||||
rmqChannel.BasicConsume(queue: serverQueueName, autoAck: true, consumer: rmqConsumer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is invoked to process messages received.
|
||||
/// </summary>
|
||||
/// <param name="channel">Related communications channel.</param>
|
||||
/// <param name="msgIn">Message deliver data.</param>
|
||||
private void OnMessageReceived(IModel channel, BasicDeliverEventArgs msgIn)
|
||||
{
|
||||
try
|
||||
{
|
||||
var msg = MessagePackSerializer.Deserialize<RPCMessage>(msgIn.Body);
|
||||
Debug.Assert(msg != null);
|
||||
|
||||
if (msg.isResponse == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasResponse = false;
|
||||
byte[]? response = null;
|
||||
|
||||
switch (msg.method)
|
||||
{
|
||||
case nameof(IChatRoomService.RegisterClient):
|
||||
{
|
||||
var name = MessagePackSerializer.Deserialize<string>(msg.args);
|
||||
var clientId = logic.RegisterClient(name);
|
||||
response = MessagePackSerializer.Serialize(clientId);
|
||||
hasResponse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case nameof(IChatRoomService.GetStrikes):
|
||||
{
|
||||
var clientId = MessagePackSerializer.Deserialize<int>(msg.args);
|
||||
var strikes = logic.GetStrikes(clientId);
|
||||
response = MessagePackSerializer.Serialize(strikes);
|
||||
hasResponse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case nameof(IChatRoomService.GetBlockedUntil):
|
||||
{
|
||||
var clientId = MessagePackSerializer.Deserialize<int>(msg.args);
|
||||
var blockedUntil = logic.GetBlockedUntil(clientId);
|
||||
response = MessagePackSerializer.Serialize(blockedUntil);
|
||||
hasResponse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case nameof(IChatRoomService.SendMessage):
|
||||
{
|
||||
var args = MessagePackSerializer.Deserialize<SendMessageArgs>(msg.args);
|
||||
var success = logic.SendMessage(args.clientId, args.contents, args.needsToBeCensored);
|
||||
response = MessagePackSerializer.Serialize(success);
|
||||
hasResponse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case nameof(IChatRoomService.GetNewMessage):
|
||||
{
|
||||
var newMessage = logic.GetNewMessage();
|
||||
response = MessagePackSerializer.Serialize(newMessage);
|
||||
hasResponse = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case nameof(IChatRoomService.RejectMessage):
|
||||
{
|
||||
var messageId = MessagePackSerializer.Deserialize<int>(msg.args);
|
||||
logic.ApproveMessage(messageId);
|
||||
break;
|
||||
}
|
||||
|
||||
case nameof(IChatRoomService.ApproveMessage):
|
||||
{
|
||||
var messageId = MessagePackSerializer.Deserialize<int>(msg.args);
|
||||
logic.ApproveMessage(messageId);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new Exception("Unknown RPC method");
|
||||
}
|
||||
}
|
||||
|
||||
if (hasResponse)
|
||||
{
|
||||
var responseMsg = new RPCMessage
|
||||
{
|
||||
isResponse = true,
|
||||
method = msg.method,
|
||||
args = response
|
||||
};
|
||||
|
||||
var properties = channel.CreateBasicProperties();
|
||||
properties.CorrelationId = msgIn.BasicProperties.CorrelationId;
|
||||
|
||||
channel.BasicPublish(
|
||||
exchange: msgIn.Exchange,
|
||||
routingKey: msgIn.BasicProperties.ReplyTo,
|
||||
basicProperties: properties,
|
||||
body: MessagePackSerializer.Serialize(responseMsg)
|
||||
);
|
||||
}
|
||||
} catch (Exception e)
|
||||
{
|
||||
log.Error(e, "Unhandled exception caught when processing a message. The message is now lost.");
|
||||
}
|
||||
}
|
||||
}
|
97
Lab3/ChatRoom/ChatRoomState.cs
Normal file
97
Lab3/ChatRoom/ChatRoomState.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ChatRoom;
|
||||
|
||||
/// <summary>
|
||||
/// Client information
|
||||
/// </summary>
|
||||
public class Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client ID
|
||||
/// </summary>
|
||||
public int id;
|
||||
/// <summary>
|
||||
/// Client name, can be the same between multiple clients
|
||||
/// </summary>
|
||||
public string name;
|
||||
/// <summary>
|
||||
/// Number of strikes
|
||||
/// </summary>
|
||||
public int strikes = 0;
|
||||
/// <summary>
|
||||
/// Until when is this client blocked from sending messages
|
||||
/// </summary>
|
||||
public DateTime? blockedUntil = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the messages status/stage
|
||||
/// </summary>
|
||||
public enum MessageStatus
|
||||
{
|
||||
WaitingForModerator,
|
||||
GivenToModerator,
|
||||
Approved,
|
||||
Rejected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message information
|
||||
/// </summary>
|
||||
public class Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Message ID
|
||||
/// </summary>
|
||||
public int id;
|
||||
/// <summary>
|
||||
/// Client ID
|
||||
/// </summary>
|
||||
public int clientId;
|
||||
/// <summary>
|
||||
/// Message contents
|
||||
/// </summary>
|
||||
public string contents;
|
||||
/// <summary>
|
||||
/// Does this message need to be censored?
|
||||
/// </summary>
|
||||
public bool needsToBeCensored;
|
||||
/// <summary>
|
||||
/// Message status/stage
|
||||
/// </summary>
|
||||
public MessageStatus status = MessageStatus.WaitingForModerator;
|
||||
|
||||
/// <summary>
|
||||
/// When was this message sent
|
||||
/// </summary>
|
||||
public DateTime sentAt;
|
||||
}
|
||||
|
||||
public class ChatRoomState
|
||||
{
|
||||
/// <summary>
|
||||
/// Access lock.
|
||||
/// </summary>
|
||||
public readonly object accessLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Last unique ID value generated.
|
||||
/// </summary>
|
||||
public int lastUniqueId;
|
||||
|
||||
/// <summary>
|
||||
/// List of all registered clients
|
||||
/// </summary>
|
||||
public List<Client> clients = new List<Client>();
|
||||
|
||||
/// <summary>
|
||||
/// List of messages
|
||||
/// </summary>
|
||||
public List<Message> messages = new List<Message>();
|
||||
}
|
||||
|
73
Lab3/ChatRoom/Server.cs
Normal file
73
Lab3/ChatRoom/Server.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using ChatRoomContract;
|
||||
using NLog;
|
||||
|
||||
namespace ChatRoom;
|
||||
|
||||
internal class Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger for this class.
|
||||
/// </summary>
|
||||
private Logger log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Configure loggin subsystem.
|
||||
/// </summary>
|
||||
private void ConfigureLogging()
|
||||
{
|
||||
var config = new NLog.Config.LoggingConfiguration();
|
||||
|
||||
var console =
|
||||
new NLog.Targets.ConsoleTarget("console")
|
||||
{
|
||||
Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}"
|
||||
};
|
||||
config.AddTarget(console);
|
||||
config.AddRuleForAllLevels(console);
|
||||
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Program body.
|
||||
/// </summary>
|
||||
private void Run()
|
||||
{
|
||||
//configure logging
|
||||
ConfigureLogging();
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var service = new ChatRoomService(Config.CreateConnection(), Config.ExchangeName, Config.ServerQueueName);
|
||||
|
||||
log.Info("Server has been started.");
|
||||
|
||||
//hang main thread
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//log exception
|
||||
log.Error(e, "Unhandled exception caught. Server will now restart.");
|
||||
|
||||
//prevent console spamming
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Program entry point.
|
||||
/// </summary>
|
||||
/// <param name="args">Command line arguments.</param>
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var self = new Server();
|
||||
self.Run();
|
||||
}
|
||||
}
|
241
Lab3/ChatRoomContract/ChatRoomClient.cs
Normal file
241
Lab3/ChatRoomContract/ChatRoomClient.cs
Normal file
@ -0,0 +1,241 @@
|
||||
using MessagePack;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
|
||||
namespace ChatRoomContract;
|
||||
|
||||
/// <summary>
|
||||
/// RabbitMQ chat room client
|
||||
/// </summary>
|
||||
public class ChatRoomClient : IChatRoomService
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
private string exchangeName;
|
||||
/// <summary>
|
||||
/// Client queue name
|
||||
/// </summary>
|
||||
private string clientQueueName;
|
||||
/// <summary>
|
||||
/// Server queue name
|
||||
/// </summary>
|
||||
private string serverQueueName;
|
||||
/// <summary>
|
||||
/// RabbitMQ channel
|
||||
/// </summary>
|
||||
private IModel rmqChannel;
|
||||
|
||||
/// <summary>
|
||||
/// Chat Room client constructor
|
||||
/// </summary>
|
||||
/// <param name="connection">RabbitMQ connection</param>
|
||||
/// <param name="exchangeName">Exchange name</param>
|
||||
/// <param name="clientQueueName">Client queue name</param>
|
||||
/// <param name="serverQueueName">Server queue name</param>
|
||||
public ChatRoomClient(IConnection connection, string exchangeName, string clientQueueName, string serverQueueName)
|
||||
{
|
||||
this.exchangeName = exchangeName;
|
||||
this.clientQueueName = clientQueueName;
|
||||
this.serverQueueName = serverQueueName;
|
||||
rmqChannel = connection.CreateModel();
|
||||
|
||||
rmqChannel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct);
|
||||
rmqChannel.QueueDeclare(queue: clientQueueName, durable: false, exclusive: true, autoDelete: false, arguments: null);
|
||||
rmqChannel.QueueBind(queue: clientQueueName, exchange: exchangeName, routingKey: clientQueueName, arguments: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send rpc message without waiting for a response
|
||||
/// </summary>
|
||||
/// <param name="method">Method name</param>
|
||||
/// <param name="args">Serialized arguments</param>
|
||||
/// <param name="correlationId">Optional correlation ID</param>
|
||||
private void CallVoid(string method, byte[] args, string? correlationId = null)
|
||||
{
|
||||
correlationId ??= Guid.NewGuid().ToString();
|
||||
|
||||
var requestProps = rmqChannel.CreateBasicProperties();
|
||||
requestProps.CorrelationId = correlationId;
|
||||
requestProps.ReplyTo = clientQueueName;
|
||||
|
||||
var msg = new RPCMessage{
|
||||
isResponse = false,
|
||||
method = method,
|
||||
args = args
|
||||
};
|
||||
|
||||
rmqChannel.BasicPublish(
|
||||
exchange: exchangeName,
|
||||
routingKey: serverQueueName,
|
||||
basicProperties: requestProps,
|
||||
body: MessagePackSerializer.Serialize(msg)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a rpc message and wait for a response
|
||||
/// </summary>
|
||||
/// <typeparam name="ResultType">Result type</typeparam>
|
||||
/// <param name="method">Method name</param>
|
||||
/// <param name="args">Serialized arguments</param>
|
||||
/// <returns>Result</returns>
|
||||
private ResultType Call<ResultType>(string method, byte[]? args)
|
||||
{
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var isResultReady = false;
|
||||
var resultReadySignal = new AutoResetEvent(false);
|
||||
|
||||
ResultType result = default;
|
||||
|
||||
//ensure contents of variables set in main thread, are loadable by receiver thread
|
||||
Thread.MemoryBarrier();
|
||||
|
||||
//create response message consumer
|
||||
var consumer = new EventingBasicConsumer(rmqChannel);
|
||||
consumer.Received +=
|
||||
(channel, delivery) => {
|
||||
//ensure contents of variables set in main thread are loaded into this thread
|
||||
Thread.MemoryBarrier();
|
||||
|
||||
if (isResultReady)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (delivery.BasicProperties.CorrelationId != correlationId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var msg = MessagePackSerializer.Deserialize<RPCMessage>(delivery.Body);
|
||||
if (msg.isResponse && msg.method == method)
|
||||
{
|
||||
if (msg.args != null)
|
||||
{
|
||||
result = MessagePackSerializer.Deserialize<ResultType>(msg.args);
|
||||
}
|
||||
|
||||
//indicate result has been received, ensure it is loadable by main thread
|
||||
isResultReady = true;
|
||||
Thread.MemoryBarrier();
|
||||
|
||||
//signal main thread that result has been received
|
||||
resultReadySignal.Set();
|
||||
}
|
||||
};
|
||||
|
||||
//attach message consumer to the response queue
|
||||
var consumerTag = rmqChannel.BasicConsume(clientQueueName, true, consumer);
|
||||
|
||||
CallVoid(method, args, correlationId);
|
||||
|
||||
//wait for the result to be ready
|
||||
resultReadySignal.WaitOne();
|
||||
|
||||
//ensure contents of variables set by the receiver are loaded into this thread
|
||||
Thread.MemoryBarrier();
|
||||
|
||||
//detach message consumer from the response queue
|
||||
rmqChannel.BasicCancel(consumerTag);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Approve a message
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message ID</param>
|
||||
public void ApproveMessage(int messageId)
|
||||
{
|
||||
CallVoid(
|
||||
nameof(IChatRoomService.ApproveMessage),
|
||||
MessagePackSerializer.Serialize(messageId)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get timestamp until when the client is blocked
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <returns>Optional datetime object</returns>
|
||||
public DateTime? GetBlockedUntil(int clientId)
|
||||
{
|
||||
return Call<DateTime?>(
|
||||
nameof(IChatRoomService.GetBlockedUntil),
|
||||
MessagePackSerializer.Serialize(clientId)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next message which hasn't been approved or rejected
|
||||
/// </summary>
|
||||
/// <returns>Message object. Returns null if there is no message</returns>
|
||||
public Message? GetNewMessage()
|
||||
{
|
||||
return Call<Message?>(
|
||||
nameof(IChatRoomService.GetNewMessage),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get number of strikes a participant has
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <returns>Number of strikes</returns>
|
||||
public int GetStrikes(int clientId)
|
||||
{
|
||||
return Call<int>(
|
||||
nameof(IChatRoomService.GetStrikes),
|
||||
MessagePackSerializer.Serialize(clientId)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register client with a name
|
||||
/// </summary>
|
||||
/// <param name="name">Name of client, can be duplicate between clients</param>
|
||||
/// <returns>Client ID</returns>
|
||||
public int RegisterClient(string name)
|
||||
{
|
||||
return Call<int>(
|
||||
nameof(IChatRoomService.RegisterClient),
|
||||
MessagePackSerializer.Serialize(name)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reject a message
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message ID</param>
|
||||
public void RejectMessage(int messageId)
|
||||
{
|
||||
CallVoid(
|
||||
nameof(IChatRoomService.RejectMessage),
|
||||
MessagePackSerializer.Serialize(messageId)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a message, will be given to a moderator to be approved
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <param name="contents">Message contents</param>
|
||||
/// <param name="needsToBeCensored">Does this message need to be censored?</param>
|
||||
/// <returns>Was sending successful, can fail if user is blocked</returns>
|
||||
public bool SendMessage(int clientId, string contents, bool needsToBeCensored)
|
||||
{
|
||||
var args = new SendMessageArgs {
|
||||
clientId = clientId,
|
||||
contents = contents,
|
||||
needsToBeCensored = needsToBeCensored
|
||||
};
|
||||
|
||||
return Call<bool>(
|
||||
nameof(IChatRoomService.SendMessage),
|
||||
MessagePackSerializer.Serialize(args)
|
||||
);
|
||||
}
|
||||
}
|
14
Lab3/ChatRoomContract/ChatRoomContract.csproj
Normal file
14
Lab3/ChatRoomContract/ChatRoomContract.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MessagePack" Version="2.5.192" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
30
Lab3/ChatRoomContract/Config.cs
Normal file
30
Lab3/ChatRoomContract/Config.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace ChatRoomContract;
|
||||
|
||||
public static class Config
|
||||
{
|
||||
public static string ExchangeName = "T120B180.ChatRoom.Exchange";
|
||||
public static string ServerQueueName = "T120B180.ChatRoom.Server";
|
||||
public static string ClientQueueNamePrefix = "T120B180.ChatRoom.Client_";
|
||||
public static string HostName = "localhost";
|
||||
public static string Username = "guest";
|
||||
public static string Password = "guest";
|
||||
|
||||
public static string CreateClientQueueName()
|
||||
{
|
||||
var ClientId = Guid.NewGuid().ToString();
|
||||
return ClientQueueNamePrefix + ClientId;
|
||||
}
|
||||
|
||||
public static IConnection CreateConnection()
|
||||
{
|
||||
var rmqConnectionFactory = new ConnectionFactory
|
||||
{
|
||||
HostName = HostName,
|
||||
UserName = Username,
|
||||
Password = Password
|
||||
};
|
||||
return rmqConnectionFactory.CreateConnection();
|
||||
}
|
||||
}
|
81
Lab3/ChatRoomContract/IChatRoomService.cs
Normal file
81
Lab3/ChatRoomContract/IChatRoomService.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace ChatRoomContract;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal message description
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class Message
|
||||
{
|
||||
/// <summary>
|
||||
/// Message ID
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public int id;
|
||||
/// <summary>
|
||||
/// Message contents
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
public string contents;
|
||||
/// <summary>
|
||||
/// Does this message need to be censored?
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
public bool needsToBeCensored;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chat room service contract
|
||||
/// </summary>
|
||||
public interface IChatRoomService
|
||||
{
|
||||
/// <summary>
|
||||
/// Register client with a name
|
||||
/// </summary>
|
||||
/// <param name="name">Name of client, can be duplicate between clients</param>
|
||||
/// <returns>Client ID</returns>
|
||||
int RegisterClient(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Get number of strikes a participant has
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <returns>Number of strikes</returns>
|
||||
int GetStrikes(int clientId);
|
||||
|
||||
/// <summary>
|
||||
/// Get timestamp until when the client is blocked
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <returns>Optional datetime object</returns>
|
||||
DateTime? GetBlockedUntil(int clientId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a message, will be given to a moderator to be approved
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID</param>
|
||||
/// <param name="contents">Message contents</param>
|
||||
/// <param name="needsToBeCensored">Does this message need to be censored?</param>
|
||||
/// <returns>Was sending successful, can fail if user is blocked</returns>
|
||||
bool SendMessage(int clientId, string contents, bool needsToBeCensored);
|
||||
|
||||
/// <summary>
|
||||
/// Get the next message which hasn't been approved or rejected
|
||||
/// </summary>
|
||||
/// <returns>Message object. Returns null if there is no message</returns>
|
||||
Message? GetNewMessage();
|
||||
|
||||
/// <summary>
|
||||
/// Reject a message
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message ID</param>
|
||||
void RejectMessage(int messageId);
|
||||
|
||||
/// <summary>
|
||||
/// Approve a message
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message ID</param>
|
||||
void ApproveMessage(int messageId);
|
||||
}
|
||||
|
49
Lab3/ChatRoomContract/RPCMessage.cs
Normal file
49
Lab3/ChatRoomContract/RPCMessage.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace ChatRoomContract;
|
||||
|
||||
/// <summary>
|
||||
/// RabbitMQ message
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class RPCMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this message a response
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public bool isResponse;
|
||||
/// <summary>
|
||||
/// Method name
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
public string method;
|
||||
/// <summary>
|
||||
/// Optional arguments
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
public byte[]? args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IChatRoomService.SendMessage arguments
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class SendMessageArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Client ID
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public int clientId { get; set; }
|
||||
/// <summary>
|
||||
/// Message contents
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
public string contents { get; set; }
|
||||
/// <summary>
|
||||
/// Does this message need to be censored?
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
public bool needsToBeCensored { get; set; }
|
||||
}
|
43
Lab3/Lab3.sln
Normal file
43
Lab3/Lab3.sln
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.11.35208.52
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoom", "ChatRoom\ChatRoom.csproj", "{BDE934D4-F8C9-484F-965F-FBBDE10818EE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatRoomContract", "ChatRoomContract\ChatRoomContract.csproj", "{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moderator", "Moderator\Moderator.csproj", "{CC693E3A-9508-4D87-8B5F-630DD268858F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Participant", "Participant\Participant.csproj", "{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BDE934D4-F8C9-484F-965F-FBBDE10818EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37F160C5-7D5A-4EF4-ADBB-C7F4065F2ECF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC693E3A-9508-4D87-8B5F-630DD268858F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F65D4958-FE80-4B62-ADAD-F27BC6B5C696}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B2EFC0A1-DB91-4736-B795-29ACE70DCBA4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
102
Lab3/Moderator/Moderator.cs
Normal file
102
Lab3/Moderator/Moderator.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using ChatRoomContract;
|
||||
using NLog;
|
||||
using Bogus;
|
||||
|
||||
namespace Moderator;
|
||||
|
||||
internal class Moderator
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger for this class.
|
||||
/// </summary>
|
||||
Logger log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Configures logging subsystem.
|
||||
/// </summary>
|
||||
private void ConfigureLogging()
|
||||
{
|
||||
var config = new NLog.Config.LoggingConfiguration();
|
||||
|
||||
var console =
|
||||
new NLog.Targets.ConsoleTarget("console")
|
||||
{
|
||||
Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}"
|
||||
};
|
||||
config.AddTarget(console);
|
||||
config.AddRuleForAllLevels(console);
|
||||
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run with a given service
|
||||
/// </summary>
|
||||
/// <param name="chatRoom">Chat room service</param>
|
||||
private void RunConnection(IChatRoomService chatRoom)
|
||||
{
|
||||
var faker = new Faker("en");
|
||||
|
||||
var name = faker.Name.FullName();
|
||||
int clientId = chatRoom.RegisterClient(name);
|
||||
log.Info($"Registered with client id {clientId}");
|
||||
|
||||
Console.Title = $"Moderator | {name} | {clientId}";
|
||||
|
||||
while (true)
|
||||
{
|
||||
var message = chatRoom.GetNewMessage();
|
||||
if (message != null)
|
||||
{
|
||||
log.Info($"Checking message ({message.id}): {message.contents}");
|
||||
Thread.Sleep(500);
|
||||
|
||||
if (message.needsToBeCensored)
|
||||
{
|
||||
chatRoom.RejectMessage(message.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatRoom.ApproveMessage(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(1 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main loop
|
||||
/// </summary>
|
||||
private void Run()
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var chatRoom = new ChatRoomClient(Config.CreateConnection(), Config.ExchangeName, Config.CreateClientQueueName(), Config.ServerQueueName);
|
||||
|
||||
try
|
||||
{
|
||||
RunConnection(chatRoom);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//log whatever exception to console
|
||||
log.Warn(e, "Unhandled exception caught. Will restart main loop.");
|
||||
|
||||
//prevent console spamming
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point
|
||||
/// </summary>
|
||||
static void Main()
|
||||
{
|
||||
var self = new Moderator();
|
||||
self.Run();
|
||||
}
|
||||
}
|
19
Lab3/Moderator/Moderator.csproj
Normal file
19
Lab3/Moderator/Moderator.csproj
Normal file
@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.1" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChatRoomContract\ChatRoomContract.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
107
Lab3/Participant/Participant.cs
Normal file
107
Lab3/Participant/Participant.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using ChatRoomContract;
|
||||
using NLog;
|
||||
using Bogus;
|
||||
|
||||
namespace Participant;
|
||||
|
||||
internal class Participant
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger for this class.
|
||||
/// </summary>
|
||||
Logger log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Configures logging subsystem.
|
||||
/// </summary>
|
||||
private void ConfigureLogging()
|
||||
{
|
||||
var config = new NLog.Config.LoggingConfiguration();
|
||||
|
||||
var console =
|
||||
new NLog.Targets.ConsoleTarget("console")
|
||||
{
|
||||
Layout = @"${date:format=HH\:mm\:ss}|${level}| ${message} ${exception}"
|
||||
};
|
||||
config.AddTarget(console);
|
||||
config.AddRuleForAllLevels(console);
|
||||
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run with a given service
|
||||
/// </summary>
|
||||
/// <param name="chatRoom">Chat room service</param>
|
||||
private void RunConnection(IChatRoomService chatRoom)
|
||||
{
|
||||
var faker = new Faker("en");
|
||||
var rnd = new Random();
|
||||
|
||||
var name = faker.Name.FullName();
|
||||
int clientId = chatRoom.RegisterClient(name);
|
||||
log.Info($"Registered with client id {clientId}");
|
||||
|
||||
while (true)
|
||||
{
|
||||
int strikes = chatRoom.GetStrikes(clientId);
|
||||
Console.Title = $"Participant | {name} | {clientId} | {strikes} Strikes";
|
||||
|
||||
var message = string.Join(" ", faker.Lorem.Words(5));
|
||||
bool needsToBeCensored = rnd.Next(0, 100) > 50;
|
||||
if (chatRoom.SendMessage(clientId, message, needsToBeCensored))
|
||||
{
|
||||
log.Info("Sent message");
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Info("Failed to send message, blocked");
|
||||
var blockedUntil = chatRoom.GetBlockedUntil(clientId);
|
||||
var now = DateTime.UtcNow;
|
||||
if (blockedUntil != null && blockedUntil > now)
|
||||
{
|
||||
var delta = blockedUntil.Value - now;
|
||||
log.Info($"Waiting {delta.TotalSeconds:F3}s until block expires");
|
||||
Thread.Sleep((int)delta.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(2 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main loop
|
||||
/// </summary>
|
||||
private void Run()
|
||||
{
|
||||
ConfigureLogging();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var chatRoom = new ChatRoomClient(Config.CreateConnection(), Config.ExchangeName, Config.CreateClientQueueName(), Config.ServerQueueName);
|
||||
|
||||
try
|
||||
{
|
||||
RunConnection(chatRoom);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//log whatever exception to console
|
||||
log.Warn(e, "Unhandled exception caught. Will restart main loop.");
|
||||
|
||||
//prevent console spamming
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point
|
||||
/// </summary>
|
||||
static void Main()
|
||||
{
|
||||
var self = new Participant();
|
||||
self.Run();
|
||||
}
|
||||
}
|
19
Lab3/Participant/Participant.csproj
Normal file
19
Lab3/Participant/Participant.csproj
Normal file
@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.1" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ChatRoomContract\ChatRoomContract.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
Loading…
Reference in New Issue
Block a user