C# JSON Client-Server Implementation

Background

A Socket is one end of a two way communication link. The major protocols for socket communication are TCP and UDP. The primary difference is that TCP guarantees data delivery and the order of data packets. UDP does not make such guarantees, but as a consequence it is faster. Developers typically default to TCP.

The C# socket library is found inthe System.Net package.

using System.Net;
using System.Net.Sockets;

IP Endpoint

An IPEndpoint is the pairing of an IPAddress and a port number. You can use DNS lookup to obtain an IP address.

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("google.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];
IPEndPoint ipEndPoint = new(ipAddress, 7000);

When creating the server endpoint, you can specify ‘any’ for the ip address.

IPEndPoint ipEndPoint = new(IPAddress.Any, 7000);

Terminate a Socket

Shutdown disables sends and receives on a Socket.

Close will terminate the Socket connection and releases all associated resources.

socket.Shutdown(SocketShutdown.Both);
socket.Close();

Server

A server must first listen for and accept connections. Then, when a connection is made, listen for data on a seperate Socket.

Creating a Socket

Bind associates a socket with an endpoint.

Listen causes a connection-oriented (server) Socket to listen for incoming connection attempts.

Socket socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(ipEndPoint);
socket.Listen(port);

Accepting Connections

Accept blocks until an incoming connection attempt is queued then it extracts the first pending request from a queue. It then creates and returns a new Socket. You can call the RemoteEndPoint method of the returned Socket to identify the remote host’s network address and port number.

See AcceptAsync for the asynchronous accept call.

Socket handler = this.listener.Accept();

Client

You create the client side socket in the same manner as the server. The difference being, instead of Bind and Listen, the client uses Connect.

IPAddress ipAdd = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPt = new IPEndPoint(ipAdd, port);
Socket socket = new Socket(ipAdd.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ipEndPt);

Reading & Writing

Reading and writing date on a socket is the same for both the server and client side socket. Once the actual connection is made, the difference between the two is arbitrary. Both the read and operations require a byte array to act as a buffer. So your data will need to be converted to and from an array of bytes.

Write

The Send method is used to write to a socket. While there are a number of different method flavours, the most common is to send the entire contents of a byte array.

byte[] msg = Encoding.ASCII.GetBytes(aString);
this.socket.Send(msg);

If you are using a connection-oriented protocol, Send will block until the requested number of bytes are sent, unless a time-out was set by using Socket.SendTimeout. If the time-out value was exceeded, the Send call will throw a SocketException

If the receiving socket (server) has not been started a SocketException will be thrown. If the server has been started but Accept has not been called reading and writing will hang. This can be remedied by setting a timeout (in ms).

Set Read/Write Timeouts

socket.ReceiveTimeout = 1000;
scoket.SentTimeout = 1000;

Read

If the remote host shuts down the Socket connection with the Shutdown method, and all available data has been received, the Receive method will complete immediately and return zero bytes. This allows you to detect a clean shutdown.

There are many flavours of the Receive method, but we will only concern ourselves with two of them. The first reads all bytes from a socket. It returns the number of bytes read. You will need to provide your own EOF indicator or wait until 0 bytes are read which means the socket has finished writing and closed. This is useful if you are only connecting the sockets for a single read-write operation.

byte[] bytes = new byte[BUFFER_SIZE];
socket.Receive(bytes);

The second, and the one we will be using, is to read a specific number of bytes from the socket.

int count = socket.Receive(bytes, nextReadSize, SocketFlags.None);

We will use this to first read the size of the data, then read the body of the data.

// read size
byte[] bytes = new byte[INT_BUFFER_SIZE];
socket.Receive(bytes, INT_BUFFER_SIZE, SocketFlags.None);
int size = BitConverter.ToInt32(bytes, 0);

// read message
byte[] bytes = new byte[size];
socket.Receive(bytes, size, SocketFlags.None)
string data = Encoding.ASCII.GetString(bytes, 0, count);

Implementation

JSON

The json encoding and decoding will be handled by the Newtonsoft Json.Net library.

dotnet add package Newtonsoft.Json --version 13.0.1

The include statements for this library are.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Layout

There are 3 source files in the project:

  • Server.cs: Manages new client connecting to the server and emits Connection objects.
  • Connection.cs: Wraps a socket providing the read-write capabilities.
  • Client.cs: A connection object that connects to its own socket.

Server

Most of the details for this class are provided in the above background section. The main detail here is the way new connections are managed. I have put the loop inside a IEnumerable method. This should be looped to hand off new connection objects. The following example does this in a new thread.

Thread thread = new Thread(new ThreadStart(()=>{    
    foreach(Connection connection in server.Connections()){
        connection.WriteString("ack");
        connection.Close();
    }
}));

Connection

The connection object reads and writes JSON objects or Strings in two parts. First it writes four bytes representing the integer size of the data. Next it writes the data it’s self.

Client client = new Client().Connect("127.0.0.1", 7000);
client.socket.ReceiveTimeout = 3000;
Console.WriteLine("> " + client.ReadString());
server.Stop();