Operating System 4 | Socket Programming Experiment
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
- 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 likeread
,write
,getpid
, etc.string.h
: string functions header, contains useful functions for stringsnetdb.h
: network database operations header, contains functions for setting or getting the domain, likegethostbyname
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 symbolerrno
.stdlib.h
: standard library header, provide various types and general functions in the standard library, like the ASCII to integeratoi
functiongetopt.h
: forgetopt
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()
andwrite()
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()
andwrite()
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 protocolsAF_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 familySOCKET_STREAM
, in which case protocol can be specified as0
.
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 fromAF_INET
orAF_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 elements_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 thesocket
functionaddr
is the socket address structure that we have built above. Because this is a pointer argument, we have to get the address of theAddress
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 thesocket
functionbacklog
is a little bit tricky. In simple words, thebacklog
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 thebacklog
is actually the queue size of the pending clients. In our case, it will be assigned to5
.
# 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 thesocket
functionaddr
is the socket address structure that we have built above. Because this is a pointer argument, we have to get the address of theAddress
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 thesocket
functionaddr
is the socket address structure that we have built above. Because this is a pointer argument, we have to get the address of theAddress
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.