Operating System 4 | Socket Programming Experiment

Series: Operating System

Operating System 4 | Socket Programming Experiment

0. Clone the Code

Before we start this experiment, we have to download or clone the code server.c and client.c from this repo. You may also need the following Makefile file to compile these codes,

We can use make to compile these codes. After compilation, we are going to have two files named server and client. There may be some warnings when we compile the C programs and you can ignore these warnings.

client.c:32:15: warning: implicit declaration of function 'atoi' is invalid in C99 [-Wimplicit-function-declaration]
server.c:30:15: warning: implicit declaration of function 'atoi' is invalid in C99 [-Wimplicit-function-declaration]

After compiling, we can test the code by (as the server),

$ ./server 12322

Then in another terminal (as the client),

$ ./client localhost 12322
  1. Header Files

Let’s now see how the server and client work. The header files that we are going to use are,

  • stdio.h : standard I/O header, contains declarations used in most input and output and is typically included in all C programs.
  • sys/types.h : data type header, definitions of a number of data types used in system calls. These types are used in the next two include files.
  • sys/socket.h : socket header, includes a number of definitions of structures needed for sockets.
  • netinet/in.h : internet header, contains constants and structures needed for internet domain addresses.
  • unistd.h : UNIX standard functions header, contains functions like read , write , getpid , etc.
  • string.h : string functions header, contains useful functions for strings
  • netdb.h : network database operations header, contains functions for setting or getting the domain, like gethostbyname

There are some other headers that we have to know because they can be useful in the future,

  • errno.h : error number header, used to retrieve error conditions using the symbol errno.
  • stdlib.h: standard library header, provide various types and general functions in the standard library, like the ASCII to integer atoi function
  • getopt.h : for getopt function, see the documentation

2. Socket Establishing Procedures

For a client, a socket can be created by,

  • Step 1. created with the socket() system call
  • Step 2. connected to the address of the server by connect() system call
  • Step 3. send and receive data by the read() and write() system call
  • Step 4. close the socket by the close() system call

For a server, a socket can be created by,

  • Step 1. created with the socket() system call
  • Step 2. bind the socket to an address using the bind() system call
  • Step 3. listen for connections with listen() system call
  • Step 4. accept a connection with accept() system call
  • Step 5. send and receive data by the read() and write() system call
  • Step 6. close the socket by the close() system call

3. socket() Function

The socket function has three parameters, domain , type , and protocol ,

#include <sys/types.h>          
#include <sys/socket.h>

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

The domain argument specifies a communication domain, this selects the protocol family which will be used for communication.

  • AF_INET , for IPv4 Internet protocols
  • AF_INET6 , for IPv6 Internet protocols

The socket has the indicated type, which specifies the communication semantics.

  • SOCK_STREAM : Sequenced, reliable, two-way, connection-based byte streams. Supports TCP.
  • SOCK_DGRAM: Connectionless, unreliable messages of a fixed maximum length. Supports UDP.

The protocol specifies a particular protocol to be used with the socket.

  • IPPROTO_TCP: we can specify this argument to the TCP protocol, or because TCP exists to support a particular socket type within the given protocol family SOCKET_STREAM , in which case protocol can be specified as 0.

On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately. To print an errno, we can use,

#include <errno.h>
if (errno != 0) {
perror("ERROR");
exit(1);
}

And we can also refer to the manual of errno for more information.

Thus, for TCP socket, we can make it by,

tcp_socket = socket(AF_INET, SOCK_STREAM, 0); 

For UDP socket, we can make it by,

udp_socket = socket(AF_INET, SOCK_DGRAM, 0);

However, in our case, we are going to use the TCP protocol.

4. sockaddr_in Structure

The sockaddr_in structure is used to handle the information for the internet addresses. Now we have already made a socket, but we also need to specify the partner we would like to communicate with. The prototype of the sockaddr_in is as follows,

#include <netinet/in.h>

struct sockaddr_in {
short sin_family; // e.g. AF_INET
unsigned short sin_port; // e.g. htons(3490)
struct in_addr sin_addr; // see struct in_addr, below
char sin_zero[8]; // zero this if you want to
};

struct in_addr {
unsigned long s_addr; // load with inet_aton()
};

We can build an instance of this structure by,

struct sockaddr_in Address;
  • The sin_family should be selected from AF_INET or AF_INET6 ,
Address.sin_family = AF_INET;

Or,

Address.sin_family = AF_INET6;
  • The sin_port is the port that the server will listen to. Normally, we can not directly assign the port value to this parameter because the integer values are different from the host and network byte order values.

To convert our integers to the network byte order values, we have to use the htons() function, which is quite common for socket programming. The htons() function converts the unsigned short integer host short from host byte order to network byte order.

Address.sin_port = htons(nHostPort);  // nHostPort is 1025 ~ 65535
  • in_addr is a nested structure with an unsigned long element s_addr. It is used to specify which IP address can be seen as our client.

If we don’t want to specify the inbound address, we can write INADDR_ANY for all the addresses (which is equivalent to 0),

Address.sin_addr.s_addr = INADDR_ANY;

However, if we want to specify some inbound address like 63.161.169.137 , then we have to use the inet_aton function to assign this value.

inet_aton("63.161.169.137", &myaddr.sin_addr.s_addr);

5. bind() Function [Server]

The bind is used to bind a name to a socket. We have to bind the Address structure that we have built to this socket. You can find more about why we do this bind function from the link here.

When a socket is created with socket, it exists in a namespace (address family) but has no address assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd. The synopsis of this bind function is,

#include <sys/types.h>       
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
  • sockfd is the socket file descriptor (return value) that we have generated through the socket function
  • addr is the socket address structure that we have built above. Because this is a pointer argument, we have to get the address of the Address structure that we have constructed and then convert it to a pointer,
(struct sockaddr*) &Address
  • addrlen is the size of the socket address structure, we can directly retrieve this value by,
sizeof(Address)

On success, 0 is returned. On error, -1 is returned, and errno is set appropriately. If we get an -1 as our returned value, this means that we can not connect to the host.

We can also test our connection with the getsockname function. For example,

getsockname(hServerSocket, (struct sockaddr *) &Address,(socklen_t *)&nAddressSize);

See more information about this function, you can refer to this link.

6. listen() Function [Server]

After binding, we have to listen for connections on a socket. The synopsis of the listen is quite easy with only two arguments,

#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • sockfd is the socket file descriptor (return value) that we have generated through the socket function
  • backlog is a little bit tricky. In simple words, the backlog parameter specifies the number of pending connections the queue will hold. When multiple clients connect to the server, the server then holds the incoming requests in a queue. The clients are arranged in the queue, and the server processes their requests one by one as and when queue-member proceeds. The value of the backlog is actually the queue size of the pending clients. In our case, it will be assigned to 5.
# define QUEUE_SIZE = 5
listen(hServerSocket, QUEUE_SIZE)

Note that on success, 0 is returned. On error, -1 is returned, and errno is set appropriately.

7. Resolve the Hostname [Client]

For our client, we have to resolve the hostname before connection. The first argument provides the hostname in our case, and we can store it in the strHostName variable. The strcpy function is used to copy a string to another variable. The pHostInfo is a pointer pointing to the hostent structure which can be used to store host information,

struct hostent* pHostInfo;
char strHostName[HOST_NAME_SIZE];
strcpy(strHostName,argv[1]);
pHostInfo=gethostbyname(strHostName);

After this, we can copy the address of this host to a long integer by memcpy ,

long nHostAddress;
memcpy(&nHostAddress,pHostInfo->h_addr,pHostInfo->h_length);

8. accept() Function [Server]

The server should use the accept function to wait for the connection request from a client. The accept is a system call for accepting a connection on a socket.

#include <sys/types.h>   
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd is the socket file descriptor (return value) that we have generated through the socket function
  • addr is the socket address structure that we have built above. Because this is a pointer argument, we have to get the address of the Address structure that we have constructed and then convert it to a pointer,
(struct sockaddr*) &Address
  • addrlen is the size of the socket address structure, we can directly retrieve this value by,
sizeof(Address)

On success, these system calls return a file descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately. Thus,

int hSocket;
hSocket=accept(hServerSocket,(struct sockaddr*)&Address,(socklen_t *)&nAddressSize);

9. connect() Function [Client]

The connect function is quite similar to the bind function with all the same arguments. However, this is not used to bind the addresses but to be used for connecting to the server. The synopsis for connect is,

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  • sockfd is the socket file descriptor (return value) that we have generated through the socket function
  • addr is the socket address structure that we have built above. Because this is a pointer argument, we have to get the address of the Address structure that we have constructed and then convert it to a pointer,
(struct sockaddr*) &Address
  • addrlen is the size of the socket address structure, we can directly retrieve this value by,
sizeof(Address)

On success, 0 is returned. On error, -1 is returned, and errno is set appropriately. If we get an -1 as our returned value, this means that we can not connect to the server.

10. write() and read() Functions

The write and read functions are used to receive or send data between the server and the client. write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.

#include <unistd.h> 
ssize_t write(int fd, const void *buf, size_t count);

read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.

#include <unistd.h> 
ssize_t read(int fd, void *buf, size_t count);

On success, the number of bytes read is returned (0 indicates the end of the file), and the file position is advanced by this number. On error, -1 is returned, and errno is set appropriately.

Before we begin our communication, we have to set up a character buffer pBuffer for both the server and the client, by

#define BUFFER_SIZE 100
char pBuffer[BUFFER_SIZE];

We can write to the buffer pBuffer by strcpy ,

#define MESSAGE "This is the message I'm sending back and forth"
strcpy(pBuffer,MESSAGE);

Then we can send this message in the buffer to the client by write function,

write(hSocket,pBuffer,strlen(pBuffer)+1);

Note that we should add 1 to the count parameter instead of using strlen(pBuffer) in a direct way. Or if we want to send back the information that we have read, we can directly use the returned value of the read function. For example,

nReadAmount = read(hSocket,pBuffer,BUFFER_SIZE);

Then,

write(hSocket,pBuffer,nReadAmount);

11. close() Function

In the end, after we finish our communication, we have to close the socket. We can use the close() function to close a file descriptor. The synopsis is,

#include <unistd.h>
int close(int fildes);

If close() is interrupted by a signal that is to be caught, it shall return -1 with errno set to [EINTR], and the state of fildes is unspecified.