Skip to content

Instantly share code, notes, and snippets.

@define-private-public
Last active July 8, 2024 13:48
Show Gist options
  • Save define-private-public/cea29b56eebaf59714f6c858f26b46d0 to your computer and use it in GitHub Desktop.
Save define-private-public/cea29b56eebaf59714f6c858f26b46d0 to your computer and use it in GitHub Desktop.
Synchronous, single-threaded TCP Chat Server in C#
// Filename: TcpChatMessenger.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace TcpChatMessenger
{
class TcpChatMessenger
{
// Connection objects
public readonly string ServerAddress;
public readonly int Port;
private TcpClient _client;
public bool Running { get; private set; }
// Buffer & messaging
public readonly int BufferSize = 2 * 1024; // 2KB
private NetworkStream _msgStream = null;
// Personal data
public readonly string Name;
public TcpChatMessenger(string serverAddress, int port, string name)
{
// Create a non-connected TcpClient
_client = new TcpClient(); // Other constructors will start a connection
_client.SendBufferSize = BufferSize;
_client.ReceiveBufferSize = BufferSize;
Running = false;
// Set the other things
ServerAddress = serverAddress;
Port = port;
Name = name;
}
public void Connect()
{
// Try to connect
_client.Connect(ServerAddress, Port); // Will resolve DNS for us; blocks
EndPoint endPoint = _client.Client.RemoteEndPoint;
// Make sure we're connected
if (_client.Connected)
{
// Got in!
Console.WriteLine("Connected to the server at {0}.", endPoint);
// Tell them that we're a messenger
_msgStream = _client.GetStream();
byte[] msgBuffer = Encoding.UTF8.GetBytes(String.Format("name:{0}", Name));
_msgStream.Write(msgBuffer, 0, msgBuffer.Length); // Blocks
// If we're still connected after sending our name, that means the server accepts us
if (!_isDisconnected(_client))
Running = true;
else
{
// Name was probably taken...
_cleanupNetworkResources();
Console.WriteLine("The server rejected us; \"{0}\" is probably in use.", Name);
}
}
else
{
_cleanupNetworkResources();
Console.WriteLine("Wasn't able to connect to the server at {0}.", endPoint);
}
}
public void SendMessages()
{
bool wasRunning = Running;
while (Running)
{
// Poll for user input
Console.Write("{0}> ", Name);
string msg = Console.ReadLine();
// Quit or send a message
if ((msg.ToLower() == "quit") || (msg.ToLower() == "exit"))
{
// User wants to quit
Console.WriteLine("Disconnecting...");
Running = false;
}
else if (msg != string.Empty)
{
// Send the message
byte[] msgBuffer = Encoding.UTF8.GetBytes(msg);
_msgStream.Write(msgBuffer, 0, msgBuffer.Length); // Blocks
}
// Use less CPU
Thread.Sleep(10);
// Check the server didn't disconnect us
if (_isDisconnected(_client))
{
Running = false;
Console.WriteLine("Server has disconnected from us.\n:[");
}
}
_cleanupNetworkResources();
if (wasRunning)
Console.WriteLine("Disconnected.");
}
// Cleans any leftover network resources
private void _cleanupNetworkResources()
{
_msgStream?.Close();
_msgStream = null;
_client.Close();
}
// Checks if a socket has disconnected
// Adapted from -- http://stackoverflow.com/questions/722240/instantly-detect-client-disconnection-from-server-socket
private static bool _isDisconnected(TcpClient client)
{
try
{
Socket s = client.Client;
return s.Poll(10 * 1000, SelectMode.SelectRead) && (s.Available == 0);
}
catch(SocketException se)
{
// We got a socket error, assume it's disconnected
return true;
}
}
public static void Main(string[] args)
{
// Get a name
Console.Write("Enter a name to use: ");
string name = Console.ReadLine();
// Setup the Messenger
string host = "localhost";//args[0].Trim();
int port = 6000;//int.Parse(args[1].Trim());
TcpChatMessenger messenger = new TcpChatMessenger(host, port, name);
// connect and send messages
messenger.Connect();
messenger.SendMessages();
}
}
}
// Filename: TcpChatServer.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using System;
using System.Text;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace TcpChatServer
{
class TcpChatServer
{
// What listens in
private TcpListener _listener;
// types of clients connected
private List<TcpClient> _viewers = new List<TcpClient>();
private List<TcpClient> _messengers = new List<TcpClient>();
// Names that are taken by other messengers
private Dictionary<TcpClient, string> _names = new Dictionary<TcpClient, string>();
// Messages that need to be sent
private Queue<string> _messageQueue = new Queue<string>();
// Extra fun data
public readonly string ChatName;
public readonly int Port;
public bool Running { get; private set; }
// Buffer
public readonly int BufferSize = 2 * 1024; // 2KB
// Make a new TCP chat server, with our provided name
public TcpChatServer(string chatName, int port)
{
// Set the basic data
ChatName = chatName;
Port = port;
Running = false;
// Make the listener listen for connections on any network device
_listener = new TcpListener(IPAddress.Any, Port);
}
// If the server is running, this will shut down the server
public void Shutdown()
{
Running = false;
Console.WriteLine("Shutting down server");
}
// Start running the server. Will stop when `Shutdown()` has been called
public void Run()
{
// Some info
Console.WriteLine("Starting the \"{0}\" TCP Chat Server on port {1}.", ChatName, Port);
Console.WriteLine("Press Ctrl-C to shut down the server at any time.");
// Make the server run
_listener.Start(); // No backlog
Running = true;
// Main server loop
while (Running)
{
// Check for new clients
if (_listener.Pending())
_handleNewConnection();
// Do the rest
_checkForDisconnects();
_checkForNewMessages();
_sendMessages();
// Use less CPU
Thread.Sleep(10);
}
// Stop the server, and clean up any connected clients
foreach (TcpClient v in _viewers)
_cleanupClient(v);
foreach (TcpClient m in _messengers)
_cleanupClient(m);
_listener.Stop();
// Some info
Console.WriteLine("Server is shut down.");
}
private void _handleNewConnection()
{
// There is (at least) one, see what they want
bool good = false;
TcpClient newClient = _listener.AcceptTcpClient(); // Blocks
NetworkStream netStream = newClient.GetStream();
// Modify the default buffer sizes
newClient.SendBufferSize = BufferSize;
newClient.ReceiveBufferSize = BufferSize;
// Print some info
EndPoint endPoint = newClient.Client.RemoteEndPoint;
Console.WriteLine("Handling a new client from {0}...", endPoint);
// Let them identify themselves
byte[] msgBuffer = new byte[BufferSize];
int bytesRead = netStream.Read(msgBuffer, 0, msgBuffer.Length); // Blocks
//Console.WriteLine("Got {0} bytes.", bytesRead);
if (bytesRead > 0)
{
string msg = Encoding.UTF8.GetString(msgBuffer, 0, bytesRead);
if (msg == "viewer")
{
// They just want to watch
good = true;
_viewers.Add(newClient);
Console.WriteLine("{0} is a Viewer.", endPoint);
// Send them a "hello message"
msg = String.Format("Welcome to the \"{0}\" Chat Server!", ChatName);
msgBuffer = Encoding.UTF8.GetBytes(msg);
netStream.Write(msgBuffer, 0, msgBuffer.Length); // Blocks
}
else if (msg.StartsWith("name:"))
{
// Okay, so they might be a messenger
string name = msg.Substring(msg.IndexOf(':') + 1);
if ((name != string.Empty) && (!_names.ContainsValue(name)))
{
// They're new here, add them in
good = true;
_names.Add(newClient, name);
_messengers.Add(newClient);
Console.WriteLine("{0} is a Messenger with the name {1}.", endPoint, name);
// Tell the viewers we have a new messenger
_messageQueue.Enqueue(String.Format("{0} has joined the chat.", name));
}
}
else
{
// Wasn't either a viewer or messenger, clean up anyways.
Console.WriteLine("Wasn't able to identify {0} as a Viewer or Messenger.", endPoint);
_cleanupClient(newClient);
}
}
// Do we really want them?
if (!good)
newClient.Close();
}
// Sees if any of the clients have left the chat server
private void _checkForDisconnects()
{
// Check the viewers first
foreach (TcpClient v in _viewers.ToArray())
{
if (_isDisconnected(v))
{
Console.WriteLine("Viewer {0} has left.", v.Client.RemoteEndPoint);
// cleanup on our end
_viewers.Remove(v); // Remove from list
_cleanupClient(v);
}
}
// Check the messengers second
foreach (TcpClient m in _messengers.ToArray())
{
if (_isDisconnected(m))
{
// Get info about the messenger
string name = _names[m];
// Tell the viewers someone has left
Console.WriteLine("Messeger {0} has left.", name);
_messageQueue.Enqueue(String.Format("{0} has left the chat", name));
// clean up on our end
_messengers.Remove(m); // Remove from list
_names.Remove(m); // Remove taken name
_cleanupClient(m);
}
}
}
// See if any of our messengers have sent us a new message, put it in the queue
private void _checkForNewMessages()
{
foreach (TcpClient m in _messengers)
{
int messageLength = m.Available;
if (messageLength > 0)
{
// there is one! get it
byte[] msgBuffer = new byte[messageLength];
m.GetStream().Read(msgBuffer, 0, msgBuffer.Length); // Blocks
// Attach a name to it and shove it into the queue
string msg = String.Format("{0}: {1}", _names[m], Encoding.UTF8.GetString(msgBuffer));
_messageQueue.Enqueue(msg);
}
}
}
// Clears out the message queue (and sends it to all of the viewers
private void _sendMessages()
{
foreach (string msg in _messageQueue)
{
// Encode the message
byte[] msgBuffer = Encoding.UTF8.GetBytes(msg);
// Send the message to each viewer
foreach (TcpClient v in _viewers)
v.GetStream().Write(msgBuffer, 0, msgBuffer.Length); // Blocks
}
// clear out the queue
_messageQueue.Clear();
}
// Checks if a socket has disconnected
// Adapted from -- http://stackoverflow.com/questions/722240/instantly-detect-client-disconnection-from-server-socket
private static bool _isDisconnected(TcpClient client)
{
try
{
Socket s = client.Client;
return s.Poll(10 * 1000, SelectMode.SelectRead) && (s.Available == 0);
}
catch(SocketException se)
{
// We got a socket error, assume it's disconnected
return true;
}
}
// cleans up resources for a TcpClient
private static void _cleanupClient(TcpClient client)
{
client.GetStream().Close(); // Close network stream
client.Close(); // Close client
}
public static TcpChatServer chat;
protected static void InterruptHandler(object sender, ConsoleCancelEventArgs args)
{
chat.Shutdown();
args.Cancel = true;
}
public static void Main(string[] args)
{
// Create the server
string name = "Bad IRC";//args[0].Trim();
int port = 6000;//int.Parse(args[1].Trim());
chat = new TcpChatServer(name, port);
// Add a handler for a Ctrl-C press
Console.CancelKeyPress += InterruptHandler;
// run the chat server
chat.Run();
}
}
}
// Filename: TcpChatViewer.cs
// Author: Benjamin N. Summerton <define-private-public>
// License: Unlicense (http://unlicense.org/)
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace TcpChatViewer
{
class TcpChatViewer
{
// Connection objects
public readonly string ServerAddress;
public readonly int Port;
private TcpClient _client;
public bool Running { get; private set; }
private bool _disconnectRequested = false;
// Buffer & messaging
public readonly int BufferSize = 2 * 1024; // 2KB
private NetworkStream _msgStream = null;
public TcpChatViewer(string serverAddress, int port)
{
// Create a non-connected TcpClient
_client = new TcpClient(); // Other constructors will start a connection
_client.SendBufferSize = BufferSize;
_client.ReceiveBufferSize = BufferSize;
Running = false;
// Set the other things
ServerAddress = serverAddress;
Port = port;
}
// connects to the chat server
public void Connect()
{
// Now try to connect
_client.Connect(ServerAddress, Port); // Will resolve DNS for us; blocks
EndPoint endPoint = _client.Client.RemoteEndPoint;
// check that we're connected
if (_client.Connected)
{
// got in!
Console.WriteLine("Connected to the server at {0}.", endPoint);
// Send them the message that we're a viewer
_msgStream = _client.GetStream();
byte[] msgBuffer = Encoding.UTF8.GetBytes("viewer");
_msgStream.Write(msgBuffer, 0, msgBuffer.Length); // Blocks
// check that we're still connected, if the server has not kicked us, then we're in!
if (!_isDisconnected(_client))
{
Running = true;
Console.WriteLine("Press Ctrl-C to exit the Viewer at any time.");
}
else
{
// Server doens't see us as a viewer, cleanup
_cleanupNetworkResources();
Console.WriteLine("The server didn't recognise us as a Viewer.\n:[");
}
}
else
{
_cleanupNetworkResources();
Console.WriteLine("Wasn't able to connect to the server at {0}.", endPoint);
}
}
// Requests a disconnect
public void Disconnect()
{
Running = false;
_disconnectRequested = true;
Console.WriteLine("Disconnecting from the chat...");
}
// Main loop, listens and prints messages from the server
public void ListenForMessages()
{
bool wasRunning = Running;
// Listen for messages
while (Running)
{
// Do we have a new message?
int messageLength = _client.Available;
if (messageLength > 0)
{
//Console.WriteLine("New incoming message of {0} bytes", messageLength);
// Read the whole message
byte[] msgBuffer = new byte[messageLength];
_msgStream.Read(msgBuffer, 0, messageLength); // Blocks
// An alternative way of reading
//int bytesRead = 0;
//while (bytesRead < messageLength)
//{
// bytesRead += _msgStream.Read(_msgBuffer,
// bytesRead,
// _msgBuffer.Length - bytesRead);
// Thread.Sleep(1); // Use less CPU
//}
// Decode it and print it
string msg = Encoding.UTF8.GetString(msgBuffer);
Console.WriteLine(msg);
}
// Use less CPU
Thread.Sleep(10);
// Check the server didn't disconnect us
if (_isDisconnected(_client))
{
Running = false;
Console.WriteLine("Server has disconnected from us.\n:[");
}
// Check that a cancel has been requested by the user
Running &= !_disconnectRequested;
}
// Cleanup
_cleanupNetworkResources();
if (wasRunning)
Console.WriteLine("Disconnected.");
}
// Cleans any leftover network resources
private void _cleanupNetworkResources()
{
_msgStream?.Close();
_msgStream = null;
_client.Close();
}
// Checks if a socket has disconnected
// Adapted from -- http://stackoverflow.com/questions/722240/instantly-detect-client-disconnection-from-server-socket
private static bool _isDisconnected(TcpClient client)
{
try
{
Socket s = client.Client;
return s.Poll(10 * 1000, SelectMode.SelectRead) && (s.Available == 0);
}
catch(SocketException se)
{
// We got a socket error, assume it's disconnected
return true;
}
}
public static TcpChatViewer viewer;
protected static void InterruptHandler(object sender, ConsoleCancelEventArgs args)
{
viewer.Disconnect();
args.Cancel = true;
}
public static void Main(string[] args)
{
// Setup the Viewer
string host = "localhost";//args[0].Trim();
int port = 6000;//int.Parse(args[1].Trim());
viewer = new TcpChatViewer(host, port);
// Add a handler for a Ctrl-C press
Console.CancelKeyPress += InterruptHandler;
// Try to connect & view messages
viewer.Connect();
viewer.ListenForMessages();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment