Sockets

For a definitive text on sockets programming see Stevens, Fenner, and Rudoff’s, Unix Network Programming, Volume 1: The Sockets Networking API, 3/E.

The word server can be used to refer to

  1. a host machine that runs one or more services or
  2. a program that runs on a host machine and provides a service

When we refer to server, we’ll be referring to a program that runs on a host machine and provides a service.

TCP/IP

Though sockets can use many different protocols to exchange data between machines, we will consider only TCP/IP, a pair of protocols initially developed by Robert E. Kahn (DARPA) and Vinton Cerf (ARPANET) in 1973.

  • TCP -Transmission Control Protocol ensures integrity of packets and reliability.
  • IP – Inter-networking Protocol is the protocol that specifies how data is exchanged across network boundaries. It is designed to be efficient and fast, but does not ensure integrity or reliability.

Two primary principles of the TCP/IP architecture are:

  1. End-to-end principle:  Most of the maintenance of the state of the connections and overall intelligence is placed at the hosts (TCP).
  2. Robustness principle: A sender must send well-formed datagrams and accept any well-formed datagram it can interpret.

Network Ports

A host computer can have multiple servers (programs) running at the same time and each can receive connections from remote clients.  Each of the servers provide a service and communicate with their clients using a particular protocol.  For example, an Apache web server communicates with its clients using the Hyper-text Transport Protocol (HTTP).  Here we can say that Apache is providing the HTTP service.

In order for the os to direct traffic to the correct server, each service is assigned a port number.  On UNIX operating systems the assignment of port numbers to individual services like HTTP is defined in  /etc/services.   Internet Assigned Numbers Authority (IANA) has the responsibility of assigning port numbers.

You’re probably familiar with the following services.  The numbers in parenthesis are their assigned port numbers.

  • FTP (21)
  • SSH (22)
  • HTTP (80)
  • SFTP (115)

Network IPC: Sockets

A socket is an abstraction of a communication endpoint in a client or server that can be read from or written to.  Sockets are used by processes to communicate with one another using message passing.  But unlike shared memory, sockets can be used by processes residing on different hosts and on different networks.

POSIX defines a set of functions that allow clients and servers to connect to one another and communicate with one another via sockets.  The chart below describes some of these functions and lists them in the order in which they are called in both the server and client.

Server Function Client Function
Get the host machine’s name gethostname()    
Get host information getaddrinfo()    
Create socket file desc (fd) socket()    
Bind the socket fd to the server’s host address and a specific port bind()    
Free resources freeaddrinfo()    
Listen on the fd for connections listen()    
    Get host information getaddrinfo()
    Create socket fd socket()
    Attempt to connect the socket fd to the server, a random unused port on the client is used. connect()
    Free resources freeaddrinfo()
Accept connection from client, get new fd to read and write to. accept()    
Write data to client fd send()    
    Receive data from the server recv()
    Write data to server send()
Receive data from the client fd recv()    
Close client fd – not the socket fd used for listening close() Close socket fd close()

These functions use the following header files.

#include <sys/types.h>
#include <netdb.h>   
    getaddrinfo()
    freeaddrinfo()

#include <sys/socket.h>
    getaddrinfo()
    freeaddrinfo()
    socket()
    bind()
    listen()
    accept()
    send()
    recv()

#include <unistd.h>
    gethostname()    
    close()

Getting the Host’s Name

getaddrinfo() requires a host name as the first parameter.  In the code for the server, we can call gethostname() to get that information.  For the client however, if the server resides on a different machine, the client must obtain the domain name or IP address of the server that it wants to connect to.  It can so by

  • hard coding some well known domain name or IP address
  • requiring the user to enter it on command line argument when starting the client
  • require the user to enter it via some application prompt (as in a web browser)

Below is code that can be used in the server to obtain its own host name.

char* host_name = malloc(HOST_NAME_MAX);  // make sure to free this later!!!
memset(host_name, 0, HOST_NAME_MAX);

if (gethostname(host_name, HOST_NAME_MAX) == -1) {
    print_error("gethostname error");
    exit(1);
}

Getting the Port and Host IP address for a Specific Service

Next, both the client and server need to get a list of IP addresses and port numbers for the specific service they want to provide (server) or connect to (client).  They do this by calling getaddrinfo().  In the example below, the service provided (server) and requested (client) is the File Transport Protocol (FTP) service.

struct addrinfo *host_ai;
struct addrinfo hint;

hint.ai_flags = 0;                     /* customize behavior */
hint.ai_family = 0;                    /* protocol family for socket */
hint.ai_socktype = SOCK_STREAM;        /* socket type */
hint.ai_protocol = 0;                  /* protocol for socket */
hint.ai_canonname = NULL;              /* canonical name for service location */
hint.ai_addr = NULL;                   /* socket-address for socket */
hint.ai_addrlen = 0;                   /* length in bytes of socket address */
hint.ai_next = NULL;                   /* pointer to next addrinfo in list */

if ((getaddrinfo(host_name, "ftp", &hint, &host_ai)) != 0) {
    print_error("getaddrinfo error");
    exit(1);
}

The getaddrinfo() function has the following signature:

int getaddrinfo (const char *hostname, const char *servname, const struct addrinfo *hints, struct addrinfo **res);

As stated in the manual page for getaddrinfo(), “The hostname and servname arguments are either pointers to NUL-terminated strings or the null pointer.  An acceptable value for hostname is either a valid hostname or a numeric host address string consisting of a dotted decimal IPv4 address or an IPv6 address.  The servname is either a decimal port number or a service name listed in services(5).”

On the server, the hostname will be the name returned by gethostname().  On the client, the hostname will often be an IP address or domain name.

Since each host can have multiple NIC cards, thus multiple IP addresses, getaddrinfo() stores in host_ai a pointer to a linked list of struct addrinfo elements, each holding a pointer (ai_addr) to a struct that has a port number and IP address.  The variable hint is used as a filter.

The ai_addr member points to a struct sockaddr of length ai_addrlen.

struct sockaddr {
    sa_family_t sa_family;           /* address family (e.g. IPv4) */
    in_port_t sa_port;               /* port */
    struct in_addr sa_addr;          /* address */
};
struct in_addr {
    in_addr_t s_addr;               /* IPv4 address */
};

Creating a Socket

Both the client and server require sockets.  The socket() function uses the sa_family field in an ai_addr struct and the type of stream (below) to set up a socket.  The socket() function, if successful, returns a file descriptor. The server will listen for activity and accept connections using its file descriptor and the client will make a connection to the server using its file descriptor.

int host_fd;

if ((host_fd = socket(host_ai->ai_addr->sa_family, SOCK_STREAM, 0)) == -1) {
    print_error("unable to create socket");
    exit(1);
}

The socket() function has the following signature:

int socket (int domain, int type, int protocol);

Domains

  • AF_INET – IPv4 internet domain
  • AF_INET6 – IPv6 Internet domain

Type

  • SOCK_STREAM – sequenced, reliable, bidirectional, connection-oriented byte stream
  • SOCK_DGRAM – fixed length, connectionless, unreliable message

The protocol is usually set to 0 to select the default protocol for the given domain and socket type.  The default protocol for SOCK_STREAM in the AF_INET domain is IPPROTO_TCP (Transmission Control Protocol)  and the default protocol of a SOCK_DGRAM socket in the AF_INET domain is IPPROTO_UDP (User Datagram Protocol) .

Note:  When the client or server is finished using the file descriptor it should call close() to free up the file resources.

On the Server: Binding an Address to a Socket

We use the bind() function to associate an address and port with the server’s socket.

if (bind(host_fd, host_ai->ai_addr, host_ai->ai_addrlen) == -1) {
    print_error("unable to bind to socket");
    exit(1);
}

freeaddrinfo(host_ai);

The bind() function has the following signature:

int bind (int sockfd, const struct sockaddr *addr, socklen_t len);

Restrictions

  • address must belong to machine that calls bind
  • port number cannot be less than 1024 unless the process has root privilege

We can use the sockaddr returned from getaddrinfo() for addr.

For an internet domain, if we specify the IP address as INADDR_ANY the socket is bound to all the system’s network interfaces, which means we can receive packets from any of the network interface cards.

On the Server: Listening for Connections

A server announces that it is willing to accept connect request from a client by calling the listen() function.

if (listen(host_fd, QLEN) == -1) {
    print_error("listen failed");
    exit(1);
}

The listen function has the following signature

int listen (int sockfd, int backlog);

The backlog value specifies the maximum length for the queue of pending transactions.

On the Server: Accepting Client Connections

The accept() function attempts to create a session with a client.  If successful, a file descriptor is created that is used by the server to read and write data from and to the client respectively.

A server will often accept multiple incoming connections.  In the future we’ll learn how to service individual requests in individual threads of execution.  Without threads, the connection request must be processed one at a time in a for-loop.

struct sockaddr client_sockaddr;
socklen_t client_sockaddr_len = sizeof(client_sockaddr);

for (;;) {
    printf("waiting for connection ...\n");
    int clfd = accept(host_fd, &client_sockaddr, &client_sockaddr_len);

    if (clfd == -1) {
        print_error("accept error");
        exit(1);
    }

    printf("accepted connection, socket [%d]\n", clfd);

    ...

    // serve the client

}

The accept() function has the following signature:

int accept (int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);

The file descriptor returned by accept() is a socket descriptor that is connected to the client.  The server file descriptor passed to accept() remains open to receive additional connections.

On the Client: Connecting to a Server

The connect() function is used to create a connection between the client’s socket and the server.

printf("attempting Connection\n");

if (connect(sockfd, host_ai->ai_addr, host_ai->ai_addrlen) == -1) {
    printf("can't connect to %s\n", argv[1]);
    print_error("Error connecting to server");
}

printf("connection made...\n");
freeaddrinfo(host_ai);

The connect function has the following signature:

int connect (int sockfd, const struct sockaddr *addr, socklen_t len);

The address we specify is the address of the server.

The connection might fail for several reasons, for example, the server is not up and running, no room in the server’s pending queue, etc.  If successful, we can read and write to the socket file descriptor in order to communicate with the server.

Reading and Writing to Sockets

On the server we can write to the file descriptor created by accept().  On the client, we can read and write to the file descriptor created by socket().  We read and write using the send() and recv() methods as shown below.

int token = 1;
int len = send(clfd, &token, sizeof(token), 0);

if (len == -1) {
    print_error("error sending data");
    // act acordingly
}

The send function has the following signature:

size_t send (int socket, const void *buffer, size_t length, int flags);

The send() function returns the number of bytes that was sent.  If the message is too long to pass atomically through the underlying protocol, EMSGSIZE is stored in errno.  If sending data in a character array we might do this:

int len = send(clfd, buf, strlen(buf), 0);

To read data from the socket we use recv().

int token;
int len = recv(sockfd, &token, 4, 0);

if (len == -1) {
    print_error("recv error");
    // act accordingly - possibly terminate

} else {
    printf("Token from server [%d]\n", token);
    // act accordingly
}

The recv function has the following signature:

size_t recv (int socket,void *buffer,size_t length, int  flags);
  • The recv() function returns the length of the message on successful completion.
  • If the message is too long for the buffer, excess bytes may be discarded depending on the type of socket.
  • If no message is available at the socket, the receive call waits for a message to arrive unless the socket is non-blocking.
  • If no message is available to be received and the peer has performed an orderly shutdown, the value 0 is returned.
  • -1 is returned on error.

Byte order

Network protocols transfer content on a byte-by-byte basis.  So, if a server sends a client a string “hello”, since each character is represented as a byte, ‘h’ is sent first, followed by the ‘e’, the ‘l”, etc.   The client reads the ‘h’, then the ‘e’, etc.

Integers and other data types that require more than a single byte are stored in memory based on how the processor reads the bytes from memory.

Register [B1 B2 B3 B4] 11110000 00111100 00001111 00000000
               MSB           LSB     0xF0            0x3C          0x0F            0x00
       MSB                                                      LSB
Big-endian (first)  
(Store in memory starting at the MSB)  
Increasing addresses -> increasing addresses ->
[B1][B2][B3][B4]     0xF0            0x3C          0x0F            0x00
MSB is at lower address  
   
Little-endian (first)  
(Store in memory starting at the LSB)  
Increasing addresses -> increasing addresses ->
[B4][B3][B2][B1]      0x00            0x0F          0x3C           0xF0
MSB is at higher address  

Intel Pentium, Core i5 and Core 2 Duo use little-endian, whereas Sun SPARC uses big-endian.

Protocols specify a byte ordering so that heterogeneous computer systems can exchange data without confusing the byte order.   TCP/IP uses big-endian order.  To make our applications machine independent we should convert our data to network byte order.

Four functions are provided to convert between processor byte order and the network byte order for TCP/IP applications.

uint32_t htonl (uint32_t hostint32);

uint16_t htons (uint16_t hostint16);

uint32_t ntohl (uint32_t netint32);

uint16_t ntohs (uint16_t netint16);

© 2017 – 2020, Eric. All rights reserved.