Advanced TCP Server

[10/10/2023]

1. Introduction

This example illustrates a server application that uses non blocking and select functions. It means, that we will be able to simultaneously connect several client to one server. The table below, shows differences between typical network programming in C/C++ and CodeSys.

The main difference is that CodeSys doesn't provide a function to remove single descriptor from socket set. It rather destroys whole set with SysSockFdZero.

Important thing is to understand that SysSockSelect function clears out SOCKET_FD_SET structure from all descriptors that are not ready for the activity. It means that SOCKET_FD_SET must be rebuild before SysSockSelect is called. In order to rebuild SOCKET_FD_SET correctly we must somehow store the information about all descriptors used. We could create two structures of SOCKET_FD_SET and copy one to another but for clarity of this example, I am going to create custom structure where I indicate which descriptor is a server descriptor and which are clients descriptors. In either case, we need to create custom functions that manages the master socket set.

Let's take a Basic TCP Server and improve it.

2. Implementation


          PROGRAM TcpServer
          VAR
            serverStep : UINT := 10;
            socketList : TcpSocketList;
          
            serverAddr : SOCKADDRESS;
            rCreate : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
          
            nonBlock : DWORD := 1;
            rIoctl :SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
            
            rBind : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
            
            rListen : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
            
            SocketSet : SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
            selectTimout : SysSocket.SysSocket_Interfaces.SOCKET_TIMEVAL;
            socketsReady : DINT;
            
            rServerClose : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
          END_VAR
          
          CASE serverStep OF
          10:	// socket initialization
              SysSockInetAddr(SERVER.IP, ADR(serverAddr.sin_addr));
              serverAddr.sin_family := SOCKET_AF_INET;
              serverAddr.sin_port := SysSockHtons(SERVER.PORT); 
              
              socketList.server.handle := SysSockCreate(SOCKET_AF_INET, SOCKET_STREAM, SOCKET_IPPROTO_TCP, ADR(rCreate));
              IF socketList.server.handle <> SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE THEN
                serverStep := 20;
              END_IF
              
          20:	// unblock mode
              rIoctl := SysSockIoctl(socketList.server.handle, SOCKET_FIONBIO, ADR(nonBlock));
              IF rIoctl = CmpErrors.Errors.ERR_OK THEN
                serverStep := 30;
              END_IF
              
          30:	// socket binding
              rBind := SysSockBind(socketList.server.handle, ADR(serverAddr), SIZEOF(SOCKADDRESS));
              IF rBind = CmpErrors.Errors.ERR_OK THEN
                serverStep := 40;
              END_IF
              
          40:	// listen
              rListen := SysSockListen(socketList.server.handle, 1);
              IF rListen = CmpErrors.Errors.ERR_OK THEN
                serverStep := 45;
              END_IF
            
          45:	// select
              BuildSocketSet(ADR(SocketSet), ADR(socketList));
              SysSockSelect(SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE, ADR(SocketSet), 0, 0, ADR(selectTimout), ADR(socketsReady));
              serverStep := 50;
              
          50:     // accept or deny
              AcceptOrDenyTcpClient(ADR(SocketSet), ADR(socketList));
              serverStep := 60;
              
          60:	// exchange data: receive and send
              RespondToTcpClient(ADR(SocketSet), ADR(socketList));
              serverStep := 45;
            
          90:	// close server
              rServerClose := SysSockClose(socketList.server.handle);
              IF rServerClose = CmpErrors.Errors.ERR_OK THEN
                serverStep := 91;	
              END_IF
            
          END_CASE
        

3. Explanation

IP address and Port number were moved to separate file with Global Variables.

SysSockIoctl has been added in step 20. Executing that command will set some functions (not all of them, ex: SysSockRecv) in non blocking mode.

In step 45 the "SocketSet" is build/rebuild from "socketList". "socketList" already has one descriptor - server.handle from step 10. By calling SysSockSelect function, we get all sockets ready for activity. If a client is trying to establish communication, server.handle is going to remain in SocketSet after SysSockSelect execution. It be will handled in step 50 where client socket will be added to "socketList". Next time step 45 is called, "SocketSet" is rebuild and will contain two descriptors. Client can also be denied if the socket set is full. In current example "socketList" capacity is determined by "SocketSet" which is 64 descriptors (1 server and 63 clients).

Now, client is successfully connected and with every time it sends data, client[i].handle will remain in "SocketSet". In a result, server will echo data back to client in step 60.

When the client closes connection, client[i].handle will remain in "SocketSet" but SysSockRecv will be return an error. In that case, descriptor is removed from "socketList" so the next time "SocketSet" is rebuild, it doesn't contain disconnected client descriptor.

4. Details

4.1 Socket structures


          TYPE TcpSocket :
          STRUCT
            handle : SysSocket.SysSocket_Interfaces.RTS_IEC_HANDLE := SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE;
          END_STRUCT
          END_TYPE
        

          TYPE TcpSocketList :
          STRUCT
            server : TcpSocket;
            client : ARRAY[1..SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE] OF TcpSocket;
          END_STRUCT
          END_TYPE
        

All together, we get data type that holds server handler and 63 client handlers as MAX_SOCKET_FD_SETSIZE value is 63.

4.2 SocketSet building


          FUNCTION BuildSocketSet : BOOL
          VAR_INPUT
            socketSet : POINTER TO SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
            list : POINTER TO TcpSocketList;
          END_VAR
          VAR
            i : DINT;
          END_VAR
          
          SysSockFdZero(socketSet^);
          SysSockFdInit(list^.server.handle, socketSet^);
          FOR i := 1 TO SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE DO
            IF (list^.client[i].handle <> SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE) THEN
              SysSockFdInit(list^.client[i].handle, socketSet^);	
            END_IF	
          END_FOR
        

Every cycle, SocketSet is rebuild for new. SysSockZero clears SocketSet and then server descriptor is added followed by client descriptors.

4.3 Accepting / denying connection


          FUNCTION AcceptOrDenyTcpClient : BOOL
          VAR_INPUT
            socketSet : POINTER TO SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
            list : POINTER TO TcpSocketList;
          END_VAR
          VAR
            hResult : SysSocket.SysSocket_Interfaces.RTS_IEC_HANDLE;
            rAccept : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
            rClose  : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
            clientAddress : SOCKADDRESS;
            clientAddrSize : DINT := SIZEOF(clientAddress);
          END_VAR
          
          IF SysSockFdIsset(list^.server.handle, socketSet^) THEN
            hResult := SysSockAccept(list^.server.handle, ADR(clientAddress), ADR(clientAddrSize), ADR(rAccept));
            IF rAccept = CmpErrors.Errors.ERR_OK THEN
              IF AddSocketToTheList(hResult, ADR(list^)) <> 0 THEN
                rClose := SysSockClose(hResult);
              END_IF
            END_IF
          END_IF
        

If the result of SysSockAccept function if OK, client descriptor is added.

4.4 Adding socket to "socketList"


          FUNCTION AddSocketToTheList : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT
          VAR_INPUT
            handle : SysSocket.SysSocket_Interfaces.RTS_IEC_HANDLE;
            list : POINTER TO TcpSocketList;
          END_VAR
          VAR
            i : UINT;
          END_VAR
          
          i := 1;
          WHILE (list^.client[i].handle <> SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE) AND (i < SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE) DO
            i := i + 1;
          END_WHILE

          IF i < SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE THEN
            list^.client[i].handle := handle;
            AddSocketToTheList := CmpErrors.Errors.ERR_OK;
          ELSE
            AddSocketToTheList := CmpErrors.Errors.ERR_FAILED;
          END_IF
        

Client descriptor is added at the first available spot of socketSet.

4.5 Echoing data back to the client


          FUNCTION RespondToTcpClient : BOOL
          VAR_INPUT
            socketSet : POINTER TO SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
            list : POINTER TO TcpSocketList;
          END_VAR
          VAR
            i : DINT;
            bytesReceived : __XINT;
            dataReceived : ARRAY[1..32] OF BYTE;
            rRecv : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
            rSend : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
            rClose : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;	
          END_VAR
          
          FOR i := 1 TO SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE DO
            IF list^.client[i].handle <> SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE THEN
              IF SysSockFdIsset(list^.client[i].handle, socketSet^) THEN
                bytesReceived := SysSockRecv(list^.client[i].handle, ADR(dataReceived), SIZEOF(dataReceived), 0, ADR(rRecv));
                IF rRecv = 0 THEN
                  SysSockSend(list^.client[i].handle, ADR(dataReceived), bytesReceived, 0, ADR(rSend));
                ELSE
                  rClose := SysSockClose(list^.client[i].handle);
                  RemoveSocketFromTheList(list^.client[i].handle, list);
                END_IF
              END_IF				
            END_IF
          END_FOR
        

This function loops through all valid clients and checks if client descriptor appears in SocketSet. If so, data are received and echoed back to client. If SysSockRecv results an error, descriptor is removed from socketList and the socket is closed.

4.6 Closing client socket


          FUNCTION RemoveSocketFromTheList : BOOL
          VAR_INPUT
            handle : SysSocket.SysSocket_Interfaces.RTS_IEC_HANDLE;
            list : POINTER TO TcpSocketList;
          END_VAR
          VAR
            i : UINT;
          END_VAR
          
          i := 1;
          WHILE list^.client[i].handle <> handle DO
            i := i + 1;
          END_WHILE
          list^.client[i].handle := SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE;
        

4.7 Global Variables


          VAR_GLOBAL CONSTANT
            IP : STRING(16) := '192.168.0.204';
            PORT : WORD := 52333;
          END_VAR
        

5. Issues

This example doesn't take into consideration physical disconnection of Ethernet cable. When that happens, socketList is not being cleared so eventually it may be filled with descriptors that are no more in use. There is a room for improvement:

  • Aggregate two physical port, use two Ethernet cables connected to two redundant switched,
  • Another way is to write additional code that monitors physical status the the port,
  • Implement timeout for each connection. If client is not active for certain amount of time, disconnect it.

6. Summary

Predefined Server may not have functionality we need. Although it is more complicated to write TCP Server from scratch, it gives flexibility that we may need.

Source code here (IP: 192.168.0.204, PORT: 52333).