innercoder.com

A blog for Linux programming enthusiasts

The Basics of a TCP Server With C Sockets

| Comments

This post serves as a explanation of the basics of a TCP socket server, which serves as the base to a future post explaining the basics of a web server.

Note: the complete code of an older version is fully posted in the end. Check my github repo for an updated version. Link.

There are good tutorials out there that explain each of the functions of the sockets POSIX API in detail so this post will serve mostly to understand the overall basics.

As we all know, a TCP connection is also know a connection oriented protocol which handles acknoledgment of bytes when they are received and sent. So it makes sure that all packets are ordered and complete for each transaction.

How do we start a TCP socket connection? Below is a graph of the transition between sockets system calls to have a TCP server up and running.

1
2
3
4
5
6
7
Connection Establishment         --------
TCP (Stream Sockets)             |      |
Server (Passive Socket)          v
socket() --> bind() --> listen() --> accept() --> read()/write() --> close()
                                        |            |   ^
Client (Active Socket)            ^     v            v   |
socket() ----> connect() ---------|-------------> read()/write() --> close()

A server starts by creating a socket descriptor, this particular socket is only used by the server to listen for incoming connections. It then binds a particular socket descriptor with an IP address and then sets to listen on that socket and IP address. Then the server sits on the accept function call. When the server gets a connection request with the accept function call, it creates another socket descriptor which is used for I/O with the client. When the I/O with the client is done, the server closes the socket descriptor used for I/O and uses the listening descriptor once again to listen for a new client.

On the client side is much easier, you create socket descriptor for the server you are going to connect to and fill int some properties for this particular host you are after and then call connect(). If the call was successful, your program is ready for I/O with the server.

Here’s a quick summary of the function calls:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-socket(): creates a new socket. Opens a socket descriptor.
-bind(): usually used in servers, binds a socket to an existing address.
-listen(): allows a stream socket to accept incoming connections.
-accept(): consumes a connection from a peer application on a listening stream socket.
-connect(): establish a connection with another socket if server is available.
If not it waits for up to a number of pending connections.
-calling socket() is similar to calling open. A file descriptor will be 
 obtained which can be used for I/O. When the application is done, the descriptor needs
 to be closed.
-socket I/O can be performed using the convetional read() and write() system 
 calls, or using a range of socket-specific system calls (e.g., send(), recv(),
 sendto(), and recvfrom()). By default, these system calls can block. But they
 can be run atomically by using the fcntl() F_SETFL operation to enable the 
 O_NONBLOCK open file status flag.

So now that we get an idea of what the system calls do, I’m going to show you some actual code. This code belongs to a TCP server implementation in my github account. I am going to analyze it in pieces.

1
2
3
4
5
/* server socket variables */
int sockfd, new_sockfd;
struct sockaddr_in host_addr, client_addr;
socklen_t sin_size = sizeof(struct sockaddr_in);
int recv_length = 1, yes = 1;

This section declares and defines our basic variables for both of server and clients. sockfd is used to establish socket connections and new_sockfd is used for I/O. struct sockaddr_in is the structure holding everything regarding IP addressing for both the server and client in particular for IPv4. The rest of the variables are mostly to enable options for other socket system calls.

1
2
3
4
5
6
/* defining listening socket descriptor */
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
      perror("in socket");
  if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int))
          == -1)
      perror("setting socket option SO_REUSEADDR");

Here we finally create our socket descriptor to accept connections from clients. And use setsockoptions to enable some beneficial options for our sockets. Explaining this options will take us out of scope, but to get an idea, they help in re-using a previously use address when binding and restarting the tcp server. You can see that for socket we specify that we are using TCP and IPv4.

1
2
3
4
/* initializing server address socket structure */
  host_addr.sin_family = AF_INET;       /* host byte order */
  host_addr.sin_port = htons(S_PORT); /* port, network byte order */
  host_addr.sin_addr.s_addr = 0;     /* use host address */

This piece fills some values for our server address structure. We are specifying the use of IPv4 and port number to use. See that we are not specifying an IP address. We are telling the kernel to automatically fill it with the current address of our active interface.

1
2
3
4
/* bind socket to IP address */
  if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr))
          == -1)
      perror("binding to socket");

After we specify our address structure and socket descriptor we bind the socket descriptor to the current IP on the given port.

1
2
3
/* listen on socket */
  if (listen(sockfd, 5) == -1)
      perror("listening on socket");

The call to listen() opens the socket to accept incoming connections up to a maximum of 5 pending connections. All connections are sent into a queue until a call to accept() consumes a connection.

1
2
new_sockfd = accept(sockfd, (struct sockaddr *) &client_addr,
               &sin_size);

This last piece accepts an incoming connection returning a new socket descriptor. This is the descriptor that needs to be use for the read()/write() system calls. These are the ones that handle back and forth communication between client and server.

All the piece of code presented so far is the basic structure of implementing an TCP server. After this, pretty much anything can be done. A web server is a good idea. It teaches how to go from RFC details to an actual working program. It is just amazing to see when you get your own web page and server handling requests from a web browser. Other examples are a proxy server, network scanners, and sniffers.

Check the code, edit, and improve on it. Let me know if you have any question in the comments section.

Here is the complete code. The dump() function was taken from Jon Erickson’s book

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/*
 * A simple server to demonstrate TCP sockets implementation. The server
 * takes input from server and shows dump of the data.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define DATA 50
#define S_PORT 7890

void dump(char *, const unsigned int);

int main(void)
{
  /* server socket variables */
  int sockfd, new_sockfd;
  struct sockaddr_in host_addr, client_addr;
  socklen_t sin_size = sizeof(struct sockaddr_in);
  int recv_length = 1, yes = 1;
  
  /* data holding */
  char buffer[1024];

  /* defining listening socket descriptor */
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
      perror("in socket");
  if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int))
          == -1)
      perror("setting socket option SO_REUSEADDR");
  
  /* initializing server address socket structure */
  host_addr.sin_family = AF_INET;       /* host byte order */
  host_addr.sin_port = htons(S_PORT); /* port, network byte order */
  host_addr.sin_addr.s_addr = 0;     /* use host address */

  /* zero the rest of struct */
  memset(&(host_addr.sin_zero), '\0', sizeof(host_addr.sin_zero));

  /* bind socket to IP address */
  if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr))
          == -1)
      perror("binding to socket");
  
  /* listen on socket */
  if (listen(sockfd, 5) == -1)
      perror("listening on socket");
  
  /* main procedure */
  while (1) {
      new_sockfd = accept(sockfd, (struct sockaddr *) &client_addr,
               &sin_size);
      if (new_sockfd == -1)
          perror("accepting connection");
      printf("server: got connection from %s port %d\n",
          inet_ntoa(client_addr.sin_addr),
          ntohs(client_addr.sin_port));
      recv_length = recv(new_sockfd, &buffer, DATA, 0);

      while (recv_length > 0) {
          printf("RECV: %d bytes\n", recv_length);
          dump(buffer, recv_length);
          recv_length = recv(new_sockfd, &buffer, DATA, 0);
      }
  
      close(new_sockfd);
  }
  return 0;
}

/* dumps raw memory in hex byte and printable split format */
void dump(char *data_buffer, const unsigned int length)
{
  unsigned char byte;
  unsigned int i, j;
  
  for (i = 0; i < length; i++) {
      byte = data_buffer[i];
      printf("%02x ", data_buffer[i]);  /* displays byte in hex */
      
      if (((i % 16) == 15) || (i == length - 1)) {
          for (j = 0; j < 15 - (i % 16); j++)
              printf("   ");
          printf("| ");
          /* display printable bytes from line */
          for (j = (i - (i % 16)); j <= i; j++) {
              byte = data_buffer[j];
              /* outside printable char range */
              if ((byte > 31) && (byte < 127))
                  printf("%c", byte);
              else
                  printf(".");
          }
          /* end of the dump line (each line 16 bytes) */
          printf("\n");
      }
  }
}

Comments