Network Application Mechanics

Summary

All network programs follow a simple network programming model. If you understand this mode, you will have a good understanding of the mechanics behind any networked application.

CLIENT / SERVER

By default, every network application has a communication end point. There are two types of endpoints, clients and servers. By definition, a client sends the first packet and the server responds to it.

It is important to realize that each pair of networked applications perform complementary network operations in sequence. The server executes first and waits to receive. The client executes second and sends the first packet. After the initial contact, either the client or the server is capable of sending and receiving.

CLIENT / SERVER ASSOCIATION

The information in an association identifies each packet and guides it through the network from one network application to another.

NETWORK PROGRAM MODEL

All network programs use the following pattern:

  1. Open a Socket.
  2. Name the Socket.
  3. Associate With Another Socket.
  4. Send and Receive Between Sockets.
  5. Close the Socket.

OPEN A SOCKET

A socket is an endpoint of communication. A socket is created in software, and is equivalent to a computer’s network card (hardware). While it is typical for a PC to have only one network card, there can be many sockets that all use the same network card simultaneously. There is a one-to-many association between sockets and the network card:



A socket is opened using the socket() function:

SOCKET socket( int AddressFamily, int Type, int Protocol);

socket() returns SOCKET, a descriptor that de-references the current socket. Just as a file handle describes a given file, and a Windows Handle to identify a given object. As a rule of thumb, after every WSA API failed call, call WSAGetLastError() to retrieve extended error information.

NAME THE SOCKET

The server application must name its socket in order for the client to access it. If a server application does not name its socket, the protocol stack will reject a client’s request to connect. To name a socket, three attributes must be specified: protocol, port number, and address. To enable communication, the client must reference these attributes.

sockaddr Structure

To name a socket, the server initializes the sockaddr socket address structure and calls bind(). The act of populating the sockaddr structure and calling bind() assigns location and identity to the socket. The sockaddr structure is the generic socket address structure. You never use this structure. You always use a redefinition of the socket address structure, specific to the current address family.

sockaddr_in Structure

For the TCP/IP address family (PF_INET), you always use the fields of sockaddr_in and never those of the generic sockaddr structure.

struct sockaddr_in
{
    short          sin_family;      // Always PF_INT
    unsigned short sin_port;        // IP Port
    struct addr_in sin_addr;        // IP Address
    char           sin_zero[8];     // Padding to make sizeof(sockaddr_in) same as sockaddr
};

While sin_family identifies the protocol stack (TCP/IP), sin_port identifies the protocol service to be used within the protocol stack. The observed convention is:

Port Number Notes
0 - 1023 Reserved for well-known services – FTP is 21, SMTP is 25, POP3 is 110.
1024 Reserved.
1025 - 5000 Typical range for user-defined services.

An alternative to hard coding the port number, is to get it from the services database using getservbyname() or WSAGetServByName().

Local IP Address

sin_addr can either refer to a local IP address or to a remote IP address. For the function bind(), you always initialize sin_addr with the IP address for the same host on which you are calling bind(). For most cases you can use INADDR_ANY to automatically assign a local IP address. You can still assign a specific IP address, such as in multi-homed hosts (more than one network interface card, and hence more than one IP address).

bind()

The bind() function names the local socket referenced by the SOCKET descriptor with the values in the sockaddr_in structure.

Client Socket Name is Optional

While a server must name its socket to allow the client to find it on the network, a client is not required to name its socket. However, naming a client socket with a specific port and address number can cause conflicts when you run more than once instance of the client application.

When you do not explicitly name a client socket with bind(), the protocol stack implicitly names it for you (by assigning the local IP address and an arbitrary port number). For a TCP socket, the protocol stack implicitly names it when connect() is called. For a UDP socket, it implicitly names the socket when connect() or sendto() is called

ASSOCIATE WITH ANOTHER SOCKET

So far, we have opened sockets in both the server and the client, and named the socket – at least in the server. Now the server needs to prepare its socket to receive, and the client needs to prepare its socket to send. When the client is successful, it will create an association between the client and server sockets. Recall an association has five parts (protocol, client port number, client IP address, server port number, and server IP address).

The combination of the two socket names creates an association. There are three steps to create an association (the details of each step are different for each socket type):

  1. Server prepares for an association.
  2. Client initiates the association.
  3. Sever competes the association.

Every association is unique on any inter (network). This is what makes each packet unique. The association guides each packet as it travels through the network.

How a Server Prepares for an Association

A datagram (UDP) server does nothing to prepare for an association with a client, since it creates an association when it receives data.

A stream (TCP) server must prepare to accept a connection attempt from a TCP client by 1) calling listen() and then 2) calling one of three functions, accept(), select(), or WSAAsyncSelect(). WSAAsyncSelect() is the function of choice for Windows Sockets Applications.

How a Client initiates an Association

For a datagram (UDP) client, there are different ways to initiate an association:

  1. Use the sendto() function (preferable). Use this method when the client UDP socket may send to different remote hosts from the same socket. You can change the target of each datagram you send.
  2. Use the connect() function followed by send(). Use this method when the client UDP socket will be sending to the same remote address.

For a stream (TCP) client, create an association by starting a connection. This is performed using connect(). This function requires the remote socket name with which the client is attempting to open a connection. Note that connect() will implicitly name a socket if bind() has not been already called.

How a Server completes an Association

For a datagram (UDP) server, it completes the association simply be reading the data it was sent to it using recv() or recvfrom().

A stream (TCP) server completes an association by detecting an incoming connection attempt – when accept() succeeds, or when select() indicates writability, or when the socket receives a WSAAsyncSocket() FD_ACCEPT Windows message. If you have used accept() to detect incoming connections, then you have already completed the association. For the last two functions, you now need to complete the association by calling accept(). When accept() succeeds, it returns the name of the remote socket that just completed the connection.

Note that the accept() function returns a new socket. The listening socket referenced in the call to accept() still listens for incoming connection after accept() succeeds. This means that server applications have more than one socket to close.

SEND/RECEIVE BETWEEN SOCKETS

At this point, we have established an association between client and server. In other words, the client found the server, and the server recognized the client. The two sockets are now peers and can send data in either direction.

Sending Data on a Connected Socket

An application sends data using the send() function. On a datagram UDP socket, you can call send() only after you have called connect() to establish an association. If the application will send data to different addresses, use sendto() instead of send(). On a stream TCP socket you can only call send() only after you have called connect() successfully.

On success, send() returns the number of bytes sent. Note that with a TCP socket, send() can be successful, even if it sent less than the number of bytes you have specified. It is the application’s responsibility to know how many bytes have been sent, how many more bytes to send, and to retry the operation until all bytes have been sent.

Also a successful return from send() does not mean that the data has appeared on the network. A successful return from send() means that the protocol stack had room to buffer the data. The protocol stack may delay transmission to achieve certain optimizations – the Nagle algorithm reduces trivial network traffic until either a full TCP segment is queued, or all data has been acknowledged.

Sending Data on an Unconnected Socket

Any UDP socket is a valid socket. Nothing is required to prepare a UDP socket to use sendto(). However, it is recommend that you call bind() or connect() before calling sendto(). For a TCP socket, the send() and sendto() functions are exactly the same.

Receiving Data

An application received data using recv() or recvfrom() functions. Much of what has been said about send() and sendto() functions also applies to recv() and recvfrom() functions, respectively. Similar to send(), recv() can only be called on a connected socket. To detect the availability of data, simply use select() or WSAAsynSelect(). The use of WSAAsynSelect() is the recommended method.

Association as Socket Demultiplexer

The socket descriptor never appears in any packet sent between client and server. So how does the system know to which socket to direct the incoming packet? The 5-tuple that defines each association (protocol, local port, local IP address, remote port, remote IP address) appears in every packet. When the protocol stack receives a packet from a remote host, it uses the association information to direct its data to the proper socket.

CLOSE THE SOCKET

A socket is closed with closesocket(). However, this is far from simple. For datagram (UDP) sockets, closesocket() returns immediately and returns the socket resources to the protocol stack. For stream (TCP) sockets, because TCP is connection oriented, closesocket() returns socket resources to the protocol stack and attempts to close down the connections.

By default closesocket() is non-blocking; it returns immediately, whether it succeeds or fails. The option SO_LINGER can be set with setsockopt() in order to set a timeout for closesocket() on a stream socket.

On a stream socket, you should call shutdown() before calling closesocket() to perform a partial shutdown. Calling shutdown() on a UDP socket is not recommended. It is recommended that you call shutdown() with the how parameters set to 1 (sends are disallowed). 

Calling shutdown() with how=1 has no effect on a UDP socket. On a TCP socket it tells the other side that you are done sending data but does not disallow the other side from sending data. This is the recommended method to shut down a TCP socket. After calling shutdown() with how = 1, call recv() until no more data is available before calling closesocket().

CLIENT & SERVER SKETCHES

The following diagrams summarize the structure of each WinSock application

Connection-Oriented (TCP) Network Applications
Client Server

socket()

socket()

Initialize sockaddr_in() structure with server (remote) socket name

Initialize sockaddr_in() structure with local socket name

 -

bind()

 -

listen()

connect() ------------------>

 

 

accept()

Association created (can now send and receive)

send()

recv()

recv()

send()

closesocket()

closesocket() (connected socket)
closesocket() (listening socket)

 

Connection-less (UDP) Network Applications – Set remote socket name once
Client Server

socket()

socket()

Initialize sockaddr_in() structure with server (remote) socket name 

Initialize sockaddr_in() structure with local socket name

 

bind()

connect()

send() -------------------> recv()

Association created (can now send and recv)

recv() <------------------  send()
closesocket()  closesocket()

 

Connection-less (UDP) Network Applications – Set remote socket name each datagram
Client Server

socket()

socket()

Initialize sockaddr_in() structure with server (remote) socket name  Initialize sockaddr_in() structure with local socket name
bind()
sendto() ------------------> recvfrom()
recvfrom() <-----------------  sendto()
closesocket()  closesocket()

In this last table, if the client also called bind(), the client and server would be peers. By definition, a peer is both a client and a server – it can initiate or receive initial communication). Also note that a client or server can use either send() / recv() or sendto() / recvfrom() regardless of what API the peer application is using.