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.
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.
The information in an association identifies each packet and guides it through the network from one network application to another.
All network programs use the following pattern:
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.
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.
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.
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().
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).
The bind() function names the local socket referenced by the SOCKET descriptor with the values in the sockaddr_in structure.
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
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):
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.
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.
For a datagram (UDP) client, there are different ways to initiate an association:
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.
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.
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.
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.
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.
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.
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.
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().
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) |
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.