I'm working on an Asynchronous TCP Client/Server using the old BeginXXX
and EndXXX
Socket API.
The goal of this hobby project is to have a working multi user chat with file sharing capabilities.
Here's my code to Send the file
public class Client
{
public readonly AutoResetEvent SendSync = new AutoResetEvent(true);
public Action<Client, byte[]> OnReceive;
public Action<Client, double> OnSpeedMeasurement;
public Action<Client> OnConnect;
public Action<Client> OnDisconnect;
public readonly Socket Socket;
public IPEndPoint ServerEndpoint;
public volatile bool SocketConnected;
public readonly object Reference;
public readonly byte[] Buffer;
public int BytesInBuffer;
public Client(object arg)
{
if (arg is Socket)
Socket = (Socket)arg;
else
{
Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
Reference = arg;
}
Buffer = new byte[ushort.MaxValue];
}
public Task ConnectAsync(string ip, ushort port)
{
ServerEndpoint = new IPEndPoint(IPAddress.Parse(ip), port);
return Task.Factory.FromAsync(Socket.BeginConnect(ServerEndpoint, null, Reference), Connected);
}
private void Connected(IAsyncResult ar)
{
try
{
Socket.EndConnect(ar);
SocketConnected = true;
OnConnect?.Invoke(this);
Receive();
}
catch (Exception e)
{
Console.WriteLine(e);
Disconnect();
}
}
public void Receive()
{
try
{
if (!SocketConnected)
throw new SocketException((int)SocketError.NotConnected);
Socket.BeginReceive(Buffer, 0, Buffer.Length, SocketFlags.None, out SocketError error, Received, Reference);
if (error != SocketError.Success)
Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
Disconnect();
}
}
private void Received(IAsyncResult ar)
{
try
{
var size = Socket.EndReceive(ar, out SocketError error);
if (size == 0 || error != SocketError.Success)
{
Disconnect();
return;
}
var packet = new byte[size];
System.Buffer.BlockCopy(Buffer, 0, packet, 0, size);
OnReceive?.Invoke(this, packet);
Receive();
}
catch (Exception e)
{
Disconnect();
Console.WriteLine(e);
}
}
public void Send(byte[] packet)
{
SendSync.WaitOne();
Socket.BeginSend(packet, 0, packet.Length, SocketFlags.None, EndSend, null);
}
private void EndSend(IAsyncResult ar)
{
Socket.EndSend(ar);
SendSync.Set();
}
public void Disconnect()
{
if (Socket.Connected)
{
SocketConnected = false;
Socket.Close();
OnDisconnect?.Invoke(this);
}
}
/// <summary>
/// Sends a File to the Server
/// </summary>
/// <param name="path">Path to the file to send</param>
public void SendFile(string path)
{
using (var fileStream = File.Open(path, FileMode.Open, FileAccess.Read))
{
var fileName = Path.GetFileName(path);
var fileSize = fileStream.Length;
var lastUpdateTime = DateTime.Now;
var iterations = 0;
while (fileStream.Position != fileStream.Length)
{
using (var memoryStream = new MemoryStream())
{
using (var writer = new BinaryWriter(memoryStream))
{
// Make the chunks as big as possible, let the TCP Stack do the rest
// Preserve 128 + 24 bytes for Header information (128 bytes for filename, 24 bytes for header info).
var chunk = new byte[ushort.MaxValue - (128 + 24)];
// [Offset 0] Advance by 2 bytes, first two bytes are packet size, calculated at the end.
writer.Seek(2, SeekOrigin.Current);
// [Offset 2] Packet Id, 3000 = FileTransfer
writer.Write((ushort)3000);
// [Offset 4]
writer.Write(Path.GetFileName(fileName));
// [Offset 6] FileName to be Created (1) / Appended (0) (6-8 = Length Prefix, 9-N = string)
writer.Write(fileStream.Position == 0);
//Read the payload (file contents chunk) into a buffer
var readBytes = fileStream.Read(chunk, 0, chunk.Length);
//Resize the buffer to the correct size
Array.Resize(ref chunk, readBytes);
// [Offset 6 + FileName Length] Write the total file size
writer.Write(fileSize);
// [Offset 10 + FileName Length] Write size contained in this packet
writer.Write((ushort)chunk.Length);
// [Offset 12 + FileName Length] Write payload buffer
writer.Write(chunk, 0, chunk.Length);
var pos = writer.BaseStream.Position;
writer.Seek(0, SeekOrigin.Begin);
// [Offset 0] Write the full packet size to the first two bytes
writer.Write((ushort)pos);
// Get the complete packet out of the stream
var buffer = memoryStream.ToArray();
// Send it
Send(buffer);
// Calculate Transfer Speed
if (lastUpdateTime.AddSeconds(1) < DateTime.Now)
{
iterations++;
var interval = DateTime.Now - lastUpdateTime;
var downloadSpeed = (int)Math.Floor(fileStream.Position / interval.TotalSeconds) / iterations;
OnSpeedMeasurement?.Invoke(this, downloadSpeed);
lastUpdateTime = DateTime.Now;
}
}
}
}
}
}
/// <summary>
/// Receives a File
/// </summary>
/// <param name="packet">packet containing file data</param>
public void ReceiveFile(byte[] packet)
{
using (var reader = new BinaryReader(new MemoryStream(packet)))
{
var length = reader.ReadUInt16();
var id = reader.ReadUInt16();
var name = reader.ReadString();
var create = reader.ReadBoolean();
var dataSize = reader.ReadInt64();
var dataChunkSize = reader.ReadUInt16();
var data = new byte[dataChunkSize];
var read = reader.Read(data, 0, dataChunkSize);
using (var filestream = new FileStream(@"C:\Test\" + name, create ? FileMode.Create : FileMode.Append))
{
filestream.Write(data, 0, data.Length);
}
}
}
That's how the Server processes incoming packets:
public class ServerSocket
{
public static Action<Client, byte[]> OnPacket;
public uint LastUniqueId;
public Socket Socket;
public void Start(ushort port)
{
Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
Socket.Bind(new IPEndPoint(IPAddress.Any, port));
Socket.Listen(1);
Socket.BeginAccept(Accepted, null);
}
private void Accepted(IAsyncResult ar)
{
var clientSocket = Socket.EndAccept(ar);
Socket.BeginAccept(Accepted, null);
var c = new Client(clientSocket);
AlumniCollections.ConnectedClients.TryAdd(LastUniqueId++, c);
Receive(c);
}
private void Receive(Client c) => c.Socket.BeginReceive(c.Buffer, 0, c.Buffer.Length, SocketFlags.None, Received, c);
private void Received(IAsyncResult ar)
{
try
{
var client = (Client) ar.AsyncState;
var size = client.Socket.EndReceive(ar, out SocketError error);
client.BytesInBuffer += size;
if (size == 0 || error != SocketError.Success)
client.Disconnect();
ConstructPacket(client);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private void ConstructPacket(Client client)
{
if (client.BytesInBuffer > 2)
{
var packetSize = BitConverter.ToUInt16(client.Buffer, 0);
if (client.BytesInBuffer < packetSize)
client.Socket.BeginReceive(client.Buffer, client.BytesInBuffer, packetSize - client.BytesInBuffer, SocketFlags.None, Received, client);
if (client.BytesInBuffer == packetSize)
{
client.BytesInBuffer -= packetSize;
var packet = new byte[packetSize];
Buffer.BlockCopy(client.Buffer, 0, packet, 0, packetSize);
OnPacket?.Invoke(client, packet);
Receive(client);
}
if (client.BytesInBuffer > packetSize)
{
client.BytesInBuffer -= packetSize;
var packet = client.Buffer.Take(packetSize).ToArray();
OnPacket?.Invoke(client, packet);
var data = client.Buffer.Skip(packetSize).Take(client.Buffer.Length - packetSize).ToArray();
Array.Clear(client.Buffer, 0, client.Buffer.Length);
for (var i = 0; i < data.Length; i++)
client.Buffer[i] = data[i];
ConstructPacket(client);
}
}
else
{
client.Socket.BeginReceive(client.Buffer, client.BytesInBuffer, client.Buffer.Length - client.BytesInBuffer, SocketFlags.None, Received, client);
}
}
}
Might want to point out that the transfers are really slow for some reason.