dinsdag 20 december 2016

TCP Server in C# (part 2)

How to write a TCP Server in C# - part 2

This is part two of a serie about implementing a decent tcp server through sockets. Part one can be found here.

Part one contains an overview of a TCP Server in C# based on the socket class. Now it's time to show the implementation.

Implementing the listener


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

namespace Yxorp
{
   public class ListenerSocket
   {
      private readonly Socket _wrappedSocket;

      public ListenerSocket(IPAddress ipAddress, int port)
      {
         _wrappedSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
         var ipEndPoint = new IPEndPoint(ipAddress, port);

         _wrappedSocket.Bind(ipEndPoint);
         _wrappedSocket.Listen(int.MaxValue);
         _wrappedSocket.BeginAccept(AcceptCallback, _wrappedSocket);
      }

      private static void AcceptCallback(IAsyncResult asyncResult)
      {
         var listenerSocket = (Socket) asyncResult.AsyncState;

         Socket connectionSocket;

         try
         {
            connectionSocket = listenerSocket.EndAccept(asyncResult);
         }
         catch (ObjectDisposedException e) // This exception occurs when the listening socket is closed.
         {
            return;
         }

         var httpSocket = new HttpSocket(connectionSocket);

         httpSocket.BeginRead();

         listenerSocket.BeginAccept(AcceptCallback, listenerSocket);
      }

      public void Close()
      {
         _wrappedSocket.Close();
      }
   }
}

Please not that the implementation of HttpSocket will be shown later in this blog.


Hosting the listener


So how are we going to host this listener in a Windows Service or Console application? As I've shown earlier it's really easy to create a Windows Service that can be run as a console application as well, by using TopShelf.

While the listener can be run by TopShelf directly, it might be a good idea to create a seperate service class that will be started by TopShelf. And then let that service class start the listener. This can be usefull if we need to close more than only the listener, for example also the worker sockets.

So let's create a Service class that can be run with TopShelf:


using System.Net;
using System.Threading;

namespace Yxorp
{
   public class MyService
   {
      private readonly IPAddress _ipAddress;
      private readonly int _port;
      private static ListenerSocket _listenerSocket;

      public MyService(IPAddress ipAddress, int port)
      {
         _ipAddress = ipAddress;
         _port = port;
      }

      public void Start()
      {
         _listenerSocket = new ListenerSocket(_ipAddress, _port);
      }

      public void Stop()
      {
         _listenerSocket.Close();
      }
   }
}



Start the service with TopShelf


Now we can start the service class with TopShelf, by creating a console application:

using System;
using System.Configuration;
using Topshelf;

namespace Yxorp
{
   public class Program
   {
      static void Main()
      {
         var ipAddress = IPAddress.Parse("127.0.0.1");
         var port = 8099;
            
         HostFactory.Run(hostConfigurator =>
         {
            hostConfigurator.Service<MyService>(serviceConfigurator =>
            {
               serviceConfigurator.ConstructUsing(() => new MyService(ipAddress, port));

               serviceConfigurator.WhenStarted(myService => myService.Start());
               serviceConfigurator.WhenStopped(myService => myService.Stop());
            });

            hostConfigurator.RunAsLocalSystem();
            hostConfigurator.SetDescription("Reverse proxy using Topshelf");
            hostConfigurator.SetDisplayName("Yxorp.Net");
            hostConfigurator.SetServiceName("Yxorp.Net");
         });
      }
   }
}



Implementing the worker sockets


We already implemented the socket that is listening for new connections. But that socket is not the one reading the bytes that the client sends. So we need to implement the code for the socket that reads from and writes to the client.


using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;

namespace Yxorp
{
   public class HttpSocket
   {
      private readonly Stream _generalStream;
      private readonly Socket _wrappedSocket;
      const string ServerHttpVersion = "HTTP/1.1";
      const string ServerHttpStatuscode = "200";
      private static readonly byte[] CrlfBytes = Encoding.ASCII.GetBytes("\r\n");
      private static readonly byte[] EndBytes = Encoding.ASCII.GetBytes("\r\n\r\n");
      private static readonly string ServerResponse = $"{ServerHttpVersion} {ServerHttpStatuscode}\r\nContent-Length: 0\r\n\r\n";
      private static readonly byte[] ServerResponseBytes = Encoding.UTF8.GetBytes(ServerResponse);
      private static readonly int ServerResponseBytesLength = ServerResponseBytes.Length;


      public HttpSocket(Socket socket)
      {
         _wrappedSocket = socket;
         int bufferSize = socket.ReceiveBufferSize;
         Buffer = new byte[bufferSize];
         AllData = new byte[0];
         RequestLine = new byte[0];

         _generalStream = new NetworkStream(socket);
      }

      public void Close()
      {
         _wrappedSocket.Disconnect(true);
         _generalStream.Close();
         _generalStream.Dispose();
      }

      public void EndWrite(IAsyncResult asyncResult)
      {
         _generalStream.EndWrite(asyncResult);
      }

      public void BeginRead()
      {
         _generalStream.BeginRead(Buffer, 0, Buffer.Length, ReadCallback, this);
      }

      public IntPtr Handle => _wrappedSocket.Handle;

      public byte[] Buffer { get; set; }

      /// <summary>
      /// This is a field and not a property, because Array.Resize can only handle fields.
      /// </summary>
      public byte[] AllData;

      public byte[] RequestLine { get; set; }

      private void BeginWrite()
      {
         _generalStream.BeginWrite(ServerResponseBytes, 0, ServerResponseBytesLength, WriteCallback, this);
      }

      private static void ReadCallback(IAsyncResult asyncResult)
      {
         HttpSocket httpSocket;

         try
         {
            httpSocket = (HttpSocket) asyncResult.AsyncState;
         }
         catch (Exception ex)
         {
            return;
         }

         int numberOfBytesRead;
         try
         {
            numberOfBytesRead = httpSocket.EndRead(asyncResult);
         }
         catch (Exception ex)
         {
            return;
         }

         // Reading Zero Bytes
         // Many stream-oriented objects (including sockets) will signal the end of the stream by
         //    returning 0 bytes in response to a Read operation.
         //    This means that the remote side of the connection has gracefully closed the connection,
         //    and the socket should be closed.
         // The zero-length read must be treated as a special case; if it is not,
         //    the receiving code usually enters an infinite loop attempting to read more data.
         //    A zero-length read is not an error condition; 
         //    it merely means that the socket has been disconnected.
         // source: http://blog.stephencleary.com/2009/06/using-socket-as-connected-socket.html
         if (numberOfBytesRead == 0)
         {
            httpSocket.Close();
            return;
         }

         bool endBytesAreSent = true;
         if (numberOfBytesRead >= 4)
         {
            for (int i = 0; i < 4; i++)
            {
               if (httpSocket.Buffer[numberOfBytesRead - 4 + i] == EndBytes[i])
                  continue;

               endBytesAreSent = false;
               break;
            }
         }
         else
         {
            endBytesAreSent = false;
         }

         httpSocket.Buffer.AddToEndOf(ref httpSocket.AllData, numberOfBytesRead);

         if (endBytesAreSent)
         {
            var request = Encoding.UTF8.GetString(httpSocket.AllData, 0, httpSocket.AllData.Length);

            httpSocket.BeginWrite();
         }
         else
         {
            httpSocket.BeginRead();
         }
      }

      private static void WriteCallback(IAsyncResult asyncResult)
      {
         var socketWrapper = (HttpSocket) asyncResult.AsyncState; 
         socketWrapper.EndWrite(asyncResult);

         socketWrapper.AllData = new byte[0];
         socketWrapper.RequestLine = new byte[0];
         socketWrapper.Buffer = new byte[1024];

         socketWrapper.BeginRead();
      }

      private int EndRead(IAsyncResult asyncResult)
      {
         return _generalStream.EndRead(asyncResult);
      }
   }
}

This workersocket reads all bytes the clients sends, and then returns an empty http response with status code 200.


How to test

If you run the code, and point your browser to http://127.0.0.1:8099 you can see the browser loading for a short while, and then see an empty screen. If you check with the developer tools of your browser, you can see that it makes a call to the TCP Server that we created, and that it get's a http status code 200.

Geen opmerkingen:

Een reactie plaatsen