Last active
July 8, 2024 13:48
-
-
Save define-private-public/cea29b56eebaf59714f6c858f26b46d0 to your computer and use it in GitHub Desktop.
Synchronous, single-threaded TCP Chat Server in C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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