Setting Up a Socket in µC/TCP-IP (Part 2)

Part 2, TCP Server

The procedure for setting up a TCP server, though somewhat more complex than that needed when UDP is the underlying protocol, can be boiled down to five steps.

In the previous post in this series, we examined the function calls that would be used to set up a socket for a UDP server. UDP, as that post mentioned, is one of two transport-layer protocols found in the Internet, and we'll now consider the other protocol, TCP. The standard sequence followed in establishing TCP communication for a server begins with calls almost identical to those covered in the UDP post, but it then deviates with a few API functions that we've yet to discuss.

Perhaps the most notable difference between a server based on UDP and one that relies on TCP is that the latter typically necessitates at least two sockets, whereas the former, at a minimum, employs just one. The use of two sockets or more in TCP is a result of this protocol's more robust feature set; while UDP is often described as a "connection-less" protocol, TCP can be called "connection-oriented," meaning that something resembling a connection (essentially, state information for a sender and receiver) is maintained by two communicating devices. Thus, before a TCP client is able to communicate with a server, it must establish a connection. In application code, this process typically unfolds as illustrated by the below figure.

establish_a_connection

Following the approach taken in our previous post, our examination here of the steps involved in TCP server initialization will focus on proprietary, Micrium API calls. However, as the UDP post noted, µC/TCP-IP offers a BSD interface in addition to the proprietary API, and everything described herein could easily be accomplished with either set of functions. Example server code that is based on the proprietary API and that fills in some of the details omitted in the above diagram is provided below.

    NET_SOCK_ID         sock_listen; 
    NET_SOCK_ID         sock_req;
    NET_SOCK_ADDR_IPv4  server_sock_addr_ip;
    NET_SOCK_ADDR_IPv4  client_sock_addr_ip;
    NET_SOCK_ADDR_LEN   client_sock_addr_ip_size; 
    CPU_INT32U          addr_any;
    NET_ERR             err;

    addr_any = NET_IPv4_ADDR_ANY;    

    sock_listen = NetSock_Open(NET_SOCK_PROTOCOL_FAMILY_IP_V4,                              
                               NET_SOCK_TYPE_STREAM,
                               NET_SOCK_PROTOCOL_TCP,
                              &err);
    if (err != NET_SOCK_ERR_NONE) {
        /* Handle error */
    }  

    NetApp_SetSockAddr((NET_SOCK_ADDR *)&server_sock_addr_ip,
                                         NET_SOCK_ADDR_FAMILY_IP_V4,
                                         APP_SERVER_PORT_NBR,
                       (CPU_INT08U    *)&addr_any,
                                         NET_IPv4_ADDR_SIZE,
                                        &err);

    NetSock_Bind(sock_listen,                                     
                (NET_SOCK_ADDR *)&server_sock_addr_ip,
                (NET_SOCK_ADDR_LEN)NET_SOCK_ADDR_SIZE,
                &err);
    if (err != NET_SOCK_ERR_NONE) {
        /* Handle error */
    }

    NetSock_Listen(sock_listen,      
                   APP_TCP_Q_SIZE,
                  &err);
    if (err != NET_SOCK_ERR_NONE) {
        /* Handle error */
    } 

    do {

        client_sock_addr_ip_size = sizeof(client_sock_addr_ip);

        sock_req = NetSock_Accept(sock_listen,                     
                                 (NET_SOCK_ADDR *)&client_sock_addr_ip,
                                 &client_sock_addr_ip_size,
                                 &err);
        if ((err != NET_SOCK_ERR_NONE) && (err != NET_SOCK_ERR_CONN_SIGNAL_TIMEOUT)) {
            /* Handle error */
        }

    } while (sock_req < 0);

Step 1: Opening the Socket

The first function invoked in our TCP example was also found at the beginning of the last post's UDP code: NetSock_Open(). In both examples, the function returns a socket ID that can be used in subsequent API calls, but the arguments passed to NetSock_Open() for a TCP socket are slightly different than those used in a UDP application. The developer of a TCP server still has the ability to create a socket based on IPv4 or IPv6, by passing either NET_SOCK_PROTOCOL_FAMILY_IP_V4 or NET_SOCK_PROTOCOL_FAMILY_IP_V6 as the first argument to NetSock_Open(). For the second and third arguments of this function, however, developers planning to implement communication in TCP typically specify a socket type of NET_SOCK_TYPE_STREAM and the protocol identifier NET_SOCK_PROTOCOL_TCP, as opposed to the NET_SOCK_TYPE_DATAGRAM and NET_SOCK_PROTOCOL_UDP arguments that we saw in the UDP example.

Step 2: Initializing the Address Struct

For servers based on both UDP and TCP, a call to NetSock_Open() is typically followed by code to initialize a struct for the purpose of associating an address with the newly created socket. As in the UDP example, the TCP code provided here relies on NetApp_SetSockAddr() to automate the address initialization process. In either type of server, the struct provided to this function must be of type NET_SOCK_ADDR_IPv4 or NET_SOCK_ADDR_IPv6, depending on the underlying network layer, and application code needs to specify a port number and IP address through the function's third and fourth arguments, respectively. Following the same approach as the UDP code, the example TCP server uses the IP address NET_IPv4_ADDR_ANY, to allow packets from any interface to be received by the socket, as long as they have the correct destination port number.

Step 3: Binding an Address

Following the call to NetApp_SetSockAddr(), the example TCP server invokes NetSock_Bind() to assign an address to the new socket. The TCP server's bind call is practically identical to that contained in the UDP example. In each case, application code provides the TCP/IP stack with a socket id, an address struct, and a size value for the struct. (An error pointer, of course, is also passed to the bind function, and, as the previous post noted, the error codes returned through this type of pointer can be extremely helpful in diagnosing any problems that might occur during address binding and other stack operations.)

Step 4: Listening for New Connections

The TCP server's first call to an API function not appearing in the UDP example targets NetSock_Listen(). The need for this call in a TCP server reflects the aforementioned use of connections in TCP (or, at least, pseudo connections, since the routers between two communicating devices are not involved). Whereas a UDP server can simply create a socket and then wait for data to begin arriving on that socket, a TCP server must instead wait for connection requests. In terms of the underlying communication protocol, this means waiting on a SYN segment to initiate the sequence normally referred to as a "three-way handshake" in TCP.

NetSock_Listen(), which enables a server to begin receiving and processing connection requests, has a fairly simple parameter list. It begins with a socket ID that, as in other µC/TCP-IP API functions, identifies the socket involved in the operation. The third and final parameter is an error pointer that is, likewise, a staple of µC/TCP-IP's API functions. However, the second parameter warrants a bit of discussion. After a server invokes NetSock_Listen(), it might actually receive multiple connection requests before attempting to establish communication with any of the requesting clients. In order to allow a server to respond to all such requests, µC/TCP-IP is capable of storing them in a queue, the size of which is determined by the second parameter of NetSock_Listen().

Step 5: Accepting a Connection

How, in application code, does a server go about responding to connection requests? To answer this question, we must bring an additional API function into the picture, as well as an additional socket. The first socket created by a TCP server, via NetSock_Open(), is typically designated a "listen socket," and, after the call to NetSock_Listen(), remains open indefinitely, to allow the server to respond to various connection requests. Rather than using this socket to exchange data with requesting clients, the server will create a new socket for each request.

The API function that a server invokes to receive the ID of a socket created in response to a connection request is NetSock_Accept(). This is, by default, a blocking function, although it can be configured for non-blocking behavior. A task that calls NetSock_Accept() specifies a socket id (of a listening socket) as the function's first argument, and provides pointers to storage for an address struct and address length via the second and third arguments, respectively. If the standard, blocking behavior is enabled, the calling task is placed in a waiting, or pending, state until either a connection request is received or a timeout occurs. The default timeout is 5 seconds, meaning that NetSock_Accept() will return to the calling task with a socket ID of -1 and an error code of NET_SOCK_ERR_CONN_SIGNAL_TIMEOUT if a request is not received within this period. In the event that a request arrives prior to the timeout, the accept function will return a new, valid socket ID and supply address data for the connected client using the pointer passed as the function's second argument. The length of the address data is furnished via the third argument, which, interestingly enough, is, essentially, an input/output parameter: It points to the length of the address data upon the accept function's return, but it is expected to reference a storage capacity (for the address struct that will hold the data) when the function is called.

In the TCP server example, NetSock_Accept() is called within a do while loop, the condition for which will fail once the function returns a valid socket ID. With this arrangement, the server would be able to accept just a single connection from a client. In a server capable of simultaneously communicating with multiple clients, further calls to NetSock_Accept() would be needed following this function's first successful return. A developer implementing such a server might, for example, make calls to NetSock_Accept() as part of an infinite loop within a dedicated listening task's body. After a successful return from the function, the code in the loop would use a kernel call to create a separate task for communicating over the newly created socket.

A TCP server can easily grow to incorporate thousands of lines of code, and the details of implementing a server able to simultaneously communicate with multiple clients are beyond the scope of this particular post. However, the function calls described above can be viewed as part of an essential toolkit needed to develop any class of TCP server. For additional information on these functions, including a complete client-server example that was used as the basis for the code appearing here, you can consult Micrium's online documentation for µC/TCP-IP.

Tags: , ,

Questions or Comments?

Have a question or a suggestion for a future article?
Don't hesitate to contact us and let us know!
All comments and ideas are welcome.