poniedziałek, 14 października 2019

C# - Biblioteka TCP-Server

W tym poście chciałbym opisać prostą bibliotekę do obsługi TCP Serwer.


[Źródło: https://docs.microsoft.com/en-us/dotnet/]

Biblioteka:

Na samym początku należy zdefiniować nową instancję klasy:

  1. tcpServer.TcpServer tcpServerConnection = new tcpServer.TcpServer();

Przy jej tworzeniu inicjalizowane są domyślnymi wartościami:

  1. private void SetStartParameters()
  2. {
  3.     connections = new List<TcpServerConnection>();
  4.     listener = null;
  5.     listenThread = null;
  6.     sendThread = null;
  7.     _port = -1;
  8.     _maxSendAttempts = 4;
  9.     _connectionOpenStatus = false;
  10.     _idleTime = 100;
  11.     _maxCallbackThreads = 100;
  12.     _verifyConnectionInterval = 100;
  13.     _encoding = Encoding.ASCII;
  14.     _semaphoreSlim = new SemaphoreSlim(0);
  15.     _waitingOperationFinish = false;
  16.     _activeThreads = 0;
  17. }

W celu utworzenie połączenia należy na zdefiniowaną instancję klasy wywołać następujące elementy:

  1. public void CreateServer(string connectionIP, int portNumber)
  2. {
  3.     tcpServerConnection.OnDataAvailable += tcpServer1_OnDataAvailable;
  4.     tcpServerConnection.selectedIpAddress = connectionIP;
  5.     tcpServerConnection.Port = portNumber;
  6.     tcpServerConnection.Close();
  7.     tcpServerConnection.Open();
  8. }

Powyżej umieściłem przykładową funkcję uruchamiającą takie elementy jak zapisanie zdarzenia wykonywającego operację odebrania danych, zapisanie adresu IP oraz portu, zamknięcie serwera jeśli jest jakiś aktualnie uruchomiony.

Funkcja OnDataAvailable odczytuje dane, gdy jakieś się pojawią. Następnie uruchamia delegata obsługującego odebraną ramkę (funkcja logData).

  1. void tcpServer1_OnDataAvailable(tcpServer.TcpServerConnection connection)
  2. {
  3.     byte[] data = readStream(connection.Socket);
  4.     if (data != null)
  5.     {
  6.         invokeDelegate del = () =>
  7.         {
  8.             logData(false, data);
  9.         };
  10.         del.Invoke();
  11.         data = null;
  12.     }
  13. }

Funkcja readStream odczytuje dane przesłane do serwera:

  1. static byte[] readStream(TcpClient client)
  2. {
  3.     NetworkStream stream = client.GetStream();
  4.    
  5.     if (stream.DataAvailable)
  6.     {
  7.         byte[] data = new byte[client.Available];
  8.         int bytesRead = 0;
  9.         try
  10.         {
  11.             bytesRead = stream.Read(data, 0, data.Length);
  12.         }
  13.         catch (IOException) { }
  14.         if (bytesRead < data.Length)
  15.         {
  16.             byte[] lastData = data;
  17.             data = new byte[bytesRead];
  18.             Array.ConstrainedCopy(lastData, 0, data, 0, bytesRead);
  19.         }
  20.         return data;
  21.     }
  22.     return null;
  23. }

Funkcja zamykająca utworzony serwer wygląda następująco:

  1. public void Close()
  2. {
  3.     if (!_connectionOpenStatus) { return; }
  4.     lock (this)
  5.     {
  6.         _connectionOpenStatus = false;
  7.         foreach (TcpServerConnection conn in connections)
  8.         {
  9.             conn.forceDisconnect();
  10.         }
  11.         try
  12.         {
  13.             if (listenThread.IsAlive)
  14.             {
  15.                 listenThread.Interrupt();
  16.                 Thread.Yield();
  17.                 if(listenThread.IsAlive)
  18.                 {
  19.                     listenThread.Abort();
  20.                 }
  21.             }
  22.         }
  23.         catch (System.Security.SecurityException) { }
  24.      
  25.         try
  26.         {
  27.             if (sendThread.IsAlive)
  28.             {
  29.                 sendThread.Interrupt();
  30.                 Thread.Yield();
  31.                 if(sendThread.IsAlive)
  32.                 {
  33.                     sendThread.Abort();
  34.                 }
  35.             }
  36.         }
  37.         catch (System.Security.SecurityException) { }
  38.     }
  39.     listener.Stop();
  40.     lock (connections)
  41.     {
  42.         connections.Clear();
  43.     }
  44.     listenThread = null;
  45.     sendThread = null;
  46.     GC.Collect();
  47. }

Funkcja otwierająca serwer:

  1. public void Open()
  2. {
  3.     lock (this)
  4.     {
  5.         if (_connectionOpenStatus) { return; }
  6.         if (_port < 0) { throw new Exception("Port wrong or not set "); }
  7.         try
  8.         {
  9.             if (listener == null)
  10.             {
  11.                 listener = new TcpListener(IPAddress.Parse(selectedIpAddress), _port);
  12.             }
  13.             else
  14.             {
  15.                 listener.Server.Bind(new IPEndPoint(IPAddress.Parse(selectedIpAddress), _port));
  16.             }
  17.             listener.Start(1);
  18.         }
  19.         catch (Exception)
  20.         {
  21.             listener.Stop();
  22.             listener = new TcpListener(IPAddress.Parse(selectedIpAddress), _port);
  23.             listener.Start(1);
  24.         }
  25.         _connectionOpenStatus = true;
  26.         listenThread = new Thread(new ThreadStart(Listener));
  27.         listenThread.Start();
  28.         sendThread = new Thread(new ThreadStart(Sender));
  29.         sendThread.Start();
  30.    }
  31. }

Funkcje umożliwiające przesłanie danych:

  1. public void Send(string data)
  2. {
  3.     lock(_semaphoreSlim)
  4.     {
  5.         foreach (TcpServerConnection connection in connections)
  6.         {
  7.             connection.sendData(data);
  8.         }
  9.         Thread.Yield();
  10.         if (_waitingOperationFinish)
  11.         {
  12.             _semaphoreSlim.Release();
  13.             _waitingOperationFinish = false;
  14.         }
  15.     }
  16. }
  17. public void Send(byte[] data)
  18. {
  19.     lock (_semaphoreSlim)
  20.     {
  21.         foreach (TcpServerConnection connection in connections)
  22.         {
  23.             connection.sendData(data);
  24.         }
  25.         Thread.Yield();
  26.         if (_waitingOperationFinish)
  27.         {
  28.             _semaphoreSlim.Release();
  29.             _waitingOperationFinish = false;
  30.         }
  31.      }
  32. }

Funkcje przesyłające dodają ramkę do listy z danymi do przesłania:

  1. public void sendData(string data)
  2. {
  3.     byte[] array = _encodingType.GetBytes(data);
  4.     lock (_messagesToSend)
  5.     {
  6.         _messagesToSend.Add(array);
  7.     }
  8. }
  9. public void sendData(byte[] arrayToSend)
  10. {
  11.     lock (_messagesToSend)
  12.     {
  13.         _messagesToSend.Add(arrayToSend);
  14.     }
  15. }

W wątku Sendera obsługiwane jest aktywne połączenie z serwerem.

  1. private void Sender()
  2. {
  3.     while (_connectionOpenStatus && _port >= 0)
  4.     {
  5.         try
  6.         {
  7.             bool operationStatus = false;
  8.             for (int i = 0; i < connections.Count; i++)
  9.             {
  10.                 if (connections[i].CallbackThread != null)
  11.                 {
  12.                     try
  13.                     {
  14.                         connections[i].CallbackThread = null;
  15.                         lock (activeThreadsLock)
  16.                         {
  17.                             _activeThreads--;
  18.                         }
  19.                     }
  20.                     catch (Exception) { }
  21.                 }
  22.                 if(connections[i].connected() &&      (connections[i].LastVerifyTime.AddMilliseconds(_verifyConnectionInterval) > DateTime.UtcNow || connections[i].verifyConnected()))
  23.                 {
  24.                     moreWork = moreWork || processConnection(connections[i]);
  25.                 }
  26.                 else
  27.                 {
  28.                     lock (connections)
  29.                     {
  30.                         connections.RemoveAt(i);
  31.                         i--;
  32.                     }
  33.                 }
  34.             }
  35.             if (!operationStatus)
  36.             {
  37.                 System.Threading.Thread.Yield();
  38.                 lock (_semaphoreSlim)
  39.                 {
  40.                     foreach (TcpServerConnection conn in connections)
  41.                     {
  42.                         if (conn.hasMoreWork())
  43.                         {
  44.                             operationStatus = true;
  45.                             break;
  46.                         }
  47.                     }
  48.                  }
  49.                  if (!moreWork)
  50.                  {
  51.                     _waitingOperationFinish = true;
  52.                     _semaphoreSlim.Wait(_idleTime);
  53.                     _waitingOperationFinish = false;
  54.                  }
  55.             }
  56.         }
  57.         catch (Exception e)
  58.         {
  59.             if (_connectionOpenStatus && OnError != null)
  60.             {
  61.                 OnError(this, e);
  62.             }
  63.         }
  64.     }
  65. }

W wątku listener następuje oczekiwanie na aktywne połączenie klienta z serwerem:

  1. private void Listener()
  2. {
  3.     while (_connectionOpenStatus && _port >= 0)
  4.     {
  5.         try
  6.         {
  7.             if (listener.Pending())
  8.             {
  9.                 TcpClient socket = listener.AcceptTcpClient();
  10.                 TcpServerConnection connection= new TcpServerConnection(socket, _encoding);
  11.                 if (OnConnect != null)
  12.                 {
  13.                     lock (activeThreadsLock)
  14.                     {
  15.                         _activeThreads++;
  16.                     }
  17.                    
  18.                     connection.CallbackThread = new Thread(() =>
  19.                     {
  20.                         OnConnect(connection);
  21.                     });
  22.                             conn.CallbackThread.Start();
  23.                 }
  24.                 lock (connections)
  25.                 {
  26.                     connections.Add(connection);
  27.                 }
  28.             }
  29.             else
  30.             {
  31.                 System.Threading.Thread.Sleep(_idleTime);
  32.             }
  33.         }
  34.         catch (Exception e)
  35.         {
  36.             if (_connectionOpenStatus && OnError != null)
  37.             {
  38.                 OnError(this, e);
  39.             }
  40.          }
  41.     }
  42. }

Po nawiązaniu połączenia zostaje otwarty nowy socket dla klienta.

Biblioteka jest do pobrania z dysku Google pod tym linkiem.