Async TCP Socket Server/Client in C#
I wanted to program a server that can handle many clients at the same time, just like MMORPG servers do.
Many languages are capable of doing this, but most of them are optimized for web servers.
Another important aspect is the amount of data being sent over the stream. There shouldn’t be any limit to the packet size.
Code
Enough talking. Here are the complete code blocks for both the client and server sides.
I implemented a bit more code than in the tutorial above, but everything should be well explained in the comments.
Feel free to use this code and maybe program your own little MMORPG software.
Click here to see the GitHub repository.
Client
public class ATclient
{
public static int BUFFER_SIZE = 1024;
public static int PACKAGE_LENGTH_SIZE = 4;
private Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
private string ip;
private int port;
//synchronize connected attribute
private static Object Lock = new Object();
private bool _connected = false;
public bool connected
{
get
{
lock (Lock)
{
return _connected;
}
}
set
{
lock (Lock)
{
_connected = value;
}
if(value == false){
//fire disconnect event
disconnecting?.Invoke();
}
}
}
//disconnecting event
public delegate void standardHandler();
public event standardHandler disconnecting;
//events for logging messages
public delegate void consoleLog(string message);
public event consoleLog consoleLogging;
#region Connect
public void connect(string ip = "127.0.0.1", int port = 5000)
{
this.ip = ip;
this.port = port;
log("Connecting to server...");
try
{
clientSocket.BeginConnect(ip, port, new AsyncCallback(connectCallback), clientSocket);
}
catch
{
connected = false;
log("Failed to connect to Server.");
}
log($"Client set up to communicate with Server {ip} with Port {port}");
}
#endregion
#region Receive
private void connectCallback(IAsyncResult ar)
{
try
{
Socket tempS = ar.AsyncState as Socket;
tempS.EndConnect(ar);
log("Connected.");
connected = true;
packageState package = new packageState(tempS);
tempS.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(receiveCallback), package);
}
catch
{
connected = false;
log("Failed to connect to Server.");
}
}
private void receiveCallback(IAsyncResult ar)
{
try
{
packageState package = ar.AsyncState as packageState;
Socket clientS = package.socket;
int bytesRead = clientS.EndReceive(ar);
if (bytesRead > 0)
{
int size = ATclient.BUFFER_SIZE;
if (package.readOffset == -1)
//got new package
{
size = BitConverter.ToInt32(package.sizeBuffer, 0);
package.readBuffer = new byte[size];
package.readOffset = 0;
}
else
//received data belongs to old package
{
package.readOffset += bytesRead;
if (package.readOffset == package.readBuffer.Length)
//all data for this package is read
{
//clone array so the package can be disposed
byte[] temp = package.readBuffer.Clone() as byte[];
//IMPLEMENT HERE
//TEMP IS THE RECEIVED DATA
//HANDLE DATA HERE
//free memory
package.Dispose();
package = new packageState(clientS);
}
}
if (package.readBuffer == null)
//new package
{
package.socket.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(recieveCallback), package);
}
else
//read max buffer size or rest of the package size
{
int readsize = (ATclient.BUFFER_SIZE > size) ? size : ATclient.BUFFER_SIZE;
package.socket.BeginReceive(package.readBuffer, package.readOffset, readsize, SocketFlags.None, new AsyncCallback(recieveCallback), package);
}
}
else
{
connected = false;
log("Error receiving Message! ReadBytes < 0.");
}
}
catch
{
connected = false;
log("Error receiving Message!");
}
}
#endregion
#region Send
public void sendData(byte[] data)
{
if (connected)
{
try
{
//send sizeinfo
byte[] sizeInfo = BitConverter.GetBytes(data.Length);
clientSocket.BeginSend(sizeInfo, 0, sizeInfo.Length, SocketFlags.None, new AsyncCallback(sendCallback), clientSocket);
//send data
clientSocket.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback(sendCallback), clientSocket);
}
catch
{
connected = false;
}
}
else
{
log("Can't send Data. You are not connected to the Server.");
}
}
private void sendCallback(IAsyncResult ar)
{
try
{
Socket clientS = ar.AsyncState as Socket;
int sizeSend = clientS.EndSend(ar);
log($"Sent {sizeSend} Bytes to the Server.");
}
catch
{
connected = false;
log("Failed sending Message!");
}
}
#endregion
internal void log(string message)
{
consoleLogging?.Invoke(message);
}
}
internal class packageState : IDisposable
{
public Socket socket = null;
public byte[] sizeBuffer = new byte[ATclient.PACKAGE_LENGTH_SIZE];
public int readOffset = -1;
public byte[] readBuffer = null;
public packageState(Socket s)
{
this.socket = s;
}
#region IDisposable Support
//free memory
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
socket = null;
sizeBuffer = null;
readBuffer = null;
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
Server
public class ATserver
{
public static int MAX_PENDING_CONNECTIONS = 20;
public static int MAX_CLIENTS = 1000;
public static int BUFFER_SIZE = 1024;
public static int PACKAGE_LENGTH_SIZE = 4;
private Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
private List<client> clients = new List<client>();
//public events
public event clientHandler clientConnecting;
public event clientHandler clientDisconnecting;
public delegate void clientHandler(int clientID);
//log events
public event stringHandler consoleLogged;
public delegate void stringHandler(string message);
//receiving data handler
public delegate void clientByteHandler(int clientID, byte[] data);
#region Connect
public void start(int port = 5000)
{
serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
serverSocket.Listen(ATserver.MAX_PENDING_CONNECTIONS);
serverSocket.BeginAccept(new AsyncCallback(acceptCallback), serverSocket);
log($"Server listening on Port: {port}");
}
private void acceptCallback(IAsyncResult ar)
{
try
{
Socket serverS = ar.AsyncState as Socket;
Socket clientSocket = serverS.EndAccept(ar);
serverS.BeginAccept(new AsyncCallback(acceptCallback), serverS);
log($"Connection from {clientSocket.RemoteEndPoint.ToString()} received.");
if (clients.Count < ATserver.MAX_CLIENTS)
{
client c = new client(clientSocket);
clients.Add(c);
//connect all the events from the client
c.clientDisconnecting += clientDisconnected;
c.receivingData += receivedData;
c.consoleLogging += log;
//start the client
c.startClient();
//fire the event that a client is connected
clientConnecting?.Invoke(c.id);
}
else
{
clientSocket.Close();
log($"Max number of clients connected is reached. IP: {clientSocket.RemoteEndPoint.ToString()} was declined.");
}
}
catch
{
log("Error accepting connection!");
}
}
private void clientDisconnected(int clientID)
{
client client = clients.Find(x => x.id == clientID);
if (client != null)
{
clientDisconnecting?.Invoke(clientID);
clients.Remove(client);
client.clientDisconnecting -= clientDisconnected;
client.receivingData -= receivedData;
client.consoleLogging -= log;
}
}
private void receivedData(int clientID, byte[] data)
{
//HANDLE DATA FROM THE CLIENT HERE
}
#endregion
#region Send
public void sendDataTo(int id, byte[] data)
{
client c = clients.Find(x => x.id == id);
if (c != null)
{
c.sendData(data);
}
else
{
log($"Couldn't find client with ID: {id}");
}
}
#endregion
internal void log(string message)
{
consoleLogged?.Invoke(message);
}
}
internal class client
{
public int id;
private static List<int> idList = new List<int>();
private Socket socket = null;
public event clientByteHandler receivingData;
public event clientHandler clientDisconnecting;
public event stringHandler consoleLogging;
public client(Socket s)
{
id = 0;
while (idList.Contains(id))
{
id++;
}
this.socket = s;
}
#region Setup
public void startClient()
{
packageState package = new packageState(this.socket);
socket.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(receiveCallback), package);
log($"Client: {id} is set up.");
}
private void receiveCallback(IAsyncResult ar)
{
log($"Data from Client: {id} received.");
try
{
packageState package = ar.AsyncState as packageState;
Socket clientS = package.socket;
int bytesRead = clientS.EndReceive(ar);
if (bytesRead > 0)
{
int size = ATserver.BUFFER_SIZE;
if (package.readOffset == -1)
//new package
{
size = BitConverter.ToInt32(package.sizeBuffer, 0);
package.readBuffer = new byte[size];
package.readOffset = 0;
}
else
//add data to existing package
{
package.readOffset += bytesRead;
if (package.readOffset == package.readBuffer.Length)
//package fully read
{
//clone array so the package can be disposed
byte[] temp = package.readBuffer.Clone() as byte[];
//invoke the event
//temp is the received data!
receivingData?.Invoke(this.id, temp);
//free memory
package.Dispose();
package = new packageState(clientS);
}
}
if (package.readBuffer == null)
//new package
{
package.socket.BeginReceive(package.sizeBuffer, 0, package.sizeBuffer.Length, SocketFlags.None, new AsyncCallback(receiveCallback), package);
}
else
//read rest of the bytelength
{
int readsize = (ATserver.BUFFER_SIZE > size) ? size : ATserver.BUFFER_SIZE;
package.socket.BeginReceive(package.readBuffer, package.readOffset, readsize, SocketFlags.None, new AsyncCallback(receiveCallback), package);
}
}
else
{
log("Error receiving Message! ReadBytes < 0.");
closeClient();
}
}
catch
{
log("Error receiving Message!");
closeClient();
}
}
private void closeClient()
{
log($"Connection from {socket.RemoteEndPoint.ToString()} has been terminated. Client-ID: {id}");
socket.Close();
//Client Disconnected
clientDisconnecting(this.id);
}
#endregion
#region Send / Receive Data
public void sendData(byte[] data)
{
try
{
//send sizeinfo
byte[] sizeInfo = BitConverter.GetBytes(data.Length);
this.socket.BeginSend(sizeInfo, 0, sizeInfo.Length, SocketFlags.None, new AsyncCallback(sendCallback), this.socket);
//send data
this.socket.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback(sendCallback), this.socket);
}
catch
{
log("Error sending message to the Client: " + this.id);
closeClient();
}
}
private void sendCallback(IAsyncResult ar)
{
try
{
Socket clientS = ar.AsyncState as Socket;
int sizeSend = clientS.EndSend(ar);
log($"Sent {sizeSend} Bytes to the Server.");
}
catch
{
log("Failed sending message!");
closeClient();
}
}
#endregion
private void log(string message)
{
consoleLogging?.Invoke(message);
}
}
internal class packageState : IDisposable
{
public Socket socket = null;
public byte[] sizeBuffer = new byte[ATserver.PACKAGE_LENGTH_SIZE];
public int readOffset = -1;
public byte[] readBuffer = null;
public packageState(Socket s)
{
this.socket = s;
}
#region IDisposable Support
//free memory
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
socket = null;
sizeBuffer = null;
readBuffer = null;
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}