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).