- Client-Server communication
- Sockets Procedures
- TCP Socket Program
- UDP Socket Program
- Blocking calls
- Solution to blocking calls
- Non-blocking socket
- By setting flag
- Timeout
- Tricky Interview Questions
Client-Server communication
- Server
- Passively waits for and responds to the clients
- Passive Socket
- Client
- Initiates the communication
- Must know the address and the port number of the server
- Active socket
Sockets – Procedure
Primitives | Meaning |
socket | Creates a new communication endpoint |
bind | Attach a local address and port to a socket |
listen | Announce willingness to accept connection |
accept | Block caller until a connection request arrives |
connect | Actively attempt to establish connection |
send/sendto | Send some data over the connection |
recv/recvfrom | Receive some data over the connection |
Close | Release the connection |
Create a socket:
int sock_fd = socket (int domain, int type, int protocol)
- The socket can be created by using an API called socket().
- The first parameter is nothing but the address family which can be AF_INET (IPv4) or AF_INET6 (IPv6) or AF_UNIX (Unix file sockets).
- The second parameter is the type of socket which can be SOCK_DGRAM (UDP Datagram socket) or SOCK_STREAM ( TCP Socket) or SOCK_RAW (Raw Socket).
- The third parameter is the protocol that is used for communication which is usually determined by domain and type . Hence it is passed 0.
- On success it return socket Descriptor which is used for other socket API(bind, connect,read, write,close) and 0 on failure.
- One reason for failure can be exceeding the maximum number of socket descriptor allowed in the system.
Sample code:
int sock_fd = socket (AF_INET, SOCK_STREAM, 0); if (sock_fd ==0){ printf("\n Socket creation failed %d %s \n",errno,strerror(errno)); exit(EXIT_FAILURE); } printf("\n Socket creation success sock id : %d\n", sock_fd);
Naming a Socket:
int bind (int sock_fd, const struct sockaddr *addr, socklen_t addrlen);
- Naming a socket is nothing but binding the above created socket with IP address and port number.
- The other process can use the socket once it is named.
- The first parameter is the socket descriptor returned from socket() API.
- The second parameter is the pointer to structure which holds port number, family and IP address.
- The third parameter is the size of the structure.
- On successful completion, it returns 0 and -1 on failure and sets errno.
Sample Code
char *ip = "127.0.0.1"; struct sockaddr_in addr; bzero(&addr,sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(1234); inet_aton(ip, &addr.sin_addr); int ret = bind (sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)); if (ret == -1) { printf("\n Socket bind failed %d %s \n",errno,strerror(errno)); exit(EXIT_FAILURE); } printf("\n Socket bind successful\n");
Note:
- Generally, bind() function is not used on the client-side as a temporary port number is automatically assigned.
- It becomes necessary when an application needs to connect(compulsory) with a specific port because of any firewall issues or security issues or connects to a specific interface on specific port, on machine having two interfaces
Request for a connections
int connect (int sock_fd,const struct sockaddr *dest_addr, socklen_t addrlen);
- The connect() API is used for initiating a connection with the server.
- It connects the socket referred to by the file descriptor sock_fd to the server address specified by dest_addr.
- The addrlen argument specifies the size of dest_addr.
- It returns 0 on success and -1 on errors.
Note:
- Generally, connect() API is used with SOCK_STREAM(TCP) to initiate a connection on a socket specified by addr.
- But can also be used with the socket of type SOCK_DGRAM then addr is the address to which datagram is sent by default, and the only address from which datagram is received that is exactly one peer.
- There are few applications where the UDP server communicates with a single client for a long time(e.g, TFTP); in this case, both client and server call connect() API.
- Generally, connection-oriented protocol sockets may successfully connect() only once while connectionless protocol sockets may use connect() multiple times.
- connect() API is a blocking call that is if a connection cannot be set up immediately, connect will block for an unspecified timeout period. Once this timeout has expired, the connection will be aborted, and connect will fail.
- However, connect() API can be made non-blocking by setting the O_NONBLOCK flag on the file descriptor using fcntl() API.
Sample Code:
/* Making the socket Non-blocking(if required) */ int flags = fcntl (sock_fd, F_GETFL, 0); fcntl (sock_fd, O_NONBLOCK|flags); ret = connect(sock_fd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr_in)); if (ret == -1) { printf("\n Socket connect failed %d %s \n",errno,strerror(errno)); exit(EXIT_FAILURE); } printf("\n Socket connect successful\n");
Sending Data:
The data can be transferred to a socket by using either send() or sendto() API.
send() API:
int send (int sock_fd, const void *buf, size_t len, int flags)
- The send() API can be used for sending data across sockets.
- The first parameter is the socket descriptor returned from socket() API.
- The second parameter is the pointer to buffer having a message to send and the third is the size of the message.
- The flag is used for controlling the operation that is how the data is sent. The most common flag is MSG_DONTWAIT and MSG_DONTROUTE.
- MSG_DONTWAIT makes it a non-blocking call that is to return immediately if there is no data to send or outbound traffic is clogged.
- MSG_DONTROUTE makes sure that data is not over a router, just keep it local.
- On success, it returns a number of bytes send and -1 in case of failure.
sendto() API:
int sendto (int sock_fd, const void *buf, size_t len, int flags, const const struct sockaddr *dest_addr, socklen_t dest_len);
- Generally, send() API is used with the socket of type SOCK_STREAM(TCP) and sendto() API is used with the socket of type SOCK_DGRAM(UDP).
- But even sendto() API can be used to transfer data across sockets for TCP socket.
- dest_addr points to sockaddr structure containing the destination address and dest_len specify the length of the sockaddr structure.
- If sendto() to be used with socket of type SOCK_STREAM, the argument addr and its len are ignored.
- Even sendto() can be made non-blocking by using flag MSG_DONTWAIT and MSG_DONTROUTE can be used..
- On success, it returns a number of bytes send and -1 in case of failure.
Sample Code:
/* Build Destination */ char *msg ="Hello World"; struct sockaddr_in dest_addr; dest_addr.sin_family = AF_INET; dest_addr.sin_port = htons(1239); inet_aton("127.0.0.1", &dest_addr.sin_addr); /* Socket of type SOCK_STREAM */ connect (sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr)); send (sock_fd , msg, strlen(msg) , 0 ); sendto (sock_fd , msg, strlen(msg) , 0, 0, 0 );
Receiving Data:
The message can be received from a socket either by using recv() and revfrom() API.
recv() API:
int recv (int sock_fd, void * buf, size_t len, int flags);
- The message can be received from a socket by calling recv() API.
- The first parameter is the socket descriptor returned from the socket() API.
- The second parameter is the pointer to buffer to keep read message and the third is the size of the message.
- The flag is used for controlling how the data is received. The most common flag is MSG_DONTWAIT which makes recv() API non-blocking that is to return immediately if there is no data to read.
- On success, it returns a number of bytes recv and -1 in case of failure.
recvfrom():
int recvfrom (int sock_fd, void *buf, size_t len, int flags, const struct sockaddr *from_addr, socklen_t from_len);
- The recvfrom() system call receives a message from a socket and captures the address from which the data was sent.
- Generally recv() API is used with socket of type SOCK_STREAM(TCP) while recvfrom() API is used for a socket of type SOCK_DGRAM().
- But even recvfrom() API can be used to transfer data across sockets for TCP type.
- If recvfrom() to be used with socket of type SOCK_STREAM, the argument from_addr and its len are ignored.
- If from_addr is non zero and its a UDP socket, the structure from_addr is filled in with client information like its family, port, IP address.
- The value returned from_len is initialized to the size of this structure and is modified on return to indicate the actual size of the peer from_addr
- Even recvfrom() can be made non blocking by using flag MSG_DONTWAIT.
- On success it returns number of bytes received and -1 in case of failure.
Sample Code:
/* Socket of type SOCK_STREAM(TCP) */ valread = recv (sock_fd , buffer, 1024, 0); valread = recvfrom (sock_fd , buffer, 1024, 0, 0, 0);
Closing the Connection:
int close(int sock_fd);
- Once the data is send to and receive from a socket, it need to be closed.
- This is done by calling close() API on socket descriptor.
- sock_fd is file descriptor( socket being closed).
- It closes a connection(for stream socket), frees up the port used by socket.
- It returns 0 on success and -1 on failure.
- Close() API should be used on both client and server to release the file descriptor to be reused.
Listening for the new connection:
int listen (int sock_fd, int backlog);
- To accept incoming connections on a socket, a server program must create a queue to store pending requests from clients. This is done by using listen() API.
- The first parameter is the socket descriptor.
- A Linux system may limit the maximum length of the queue which is the maximum number of pending connections placed in a queue. This is done with the help of a second parameter backlog.
- Incoming connections up to this queue length are held pending on the socket; further connections will be refused and client connections will fail.
- This mechanism is provided by listen() API to allow incoming connections to be held pending while the server is busy dealing with the previous client.
- It is a non-blocking call: returns immediately.
- A value of 5 for the backlog is very common.
- It returns 0 on success and -1 on error.
Accepting the Connection:
int sock_fd_server = accept (int sock_fd, struct sockaddr *cli_addr, addrlen_t cli_addrlen);
- The server gets a socket for an incoming client connection by calling accept()
- The accept() API will pick the first connection request from the queue.
- The address of the calling client must be placed in second parameter that is sockaddr structure pointed by cli_addr upon return.
- The third parameter is the sizeof(cli_addr) which must be set appropriately before the call and are updated to actual value on return.
- The accept() system call creates a new socket to communicate with the client and returns its socket descriptor.
- This is a blocking call that is if there is no connection request in the queue, accept will block until the client does make a connection.
- This can be made non-blocking by using the O_NONBLOCK flag on the socket file descriptor using fcntl() API.
- It returns a new socket descriptor if there is a client connection pending or -1 on error.
- A new socket descriptor will be used for sending and receiving data from clients.
Sample Code
struct sockaddr_in addr; struct sockaddr_in cli_addr; bzero(&addr,sizeof(addr)); bzero(&cli_addr,sizeof(cli_addr)); char buffer[1024] = {0}; int cli_addr_len = sizeof(cli_addr); int server_fd = socket (AF_INET, SOCK_STREAM, 0); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(4567); bind(server_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)); listen(server_fd, 3); int new_socket = accept(server_fd, (struct sockaddr *)&cli_addr, (socklen_t*)&cli_addr_len);
Summation:
The following set of APIs are used un TCP socket programming
TCP Client : socket() => connect() => send() => recv() => close()
TCP Server : socket() => bind() => listen() =>accept() => recv() => send() => close()
UDP Socket Program:
The following set of APIs are used in UDP socket programming
UDP Client : socket() => bind() => sendto() => recvfrom() => close()
UDP Server : socket() => bind() => recvfrom() => sendto () => close()
TCP Socket Client Program:
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
int sock_fd = 0, valread;
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
char *msg = "Hello from client";
char buffer[1024] = {0};
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("\n Socket creation error %d %s \n", errno, strerror(errno));
return -1;
}
serv_addr.sin_family = AF_INET;
unsigned short int port = atoi(argv[1]);
serv_addr.sin_port = htons(port);
if (inet_aton ("127.0.0.1", &serv_addr.sin_addr) == 0)
{
printf("\n Invalid IP address\n");
return -1;
}
if (connect (sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("\n Socket Connection Failed %d %s \n",errno,strerror(errno));
return -1;
}
send (sock_fd, msg, strlen(msg), 0 );
printf("\n Message send to server is: %s\n", msg);
valread = recv (sock_fd, buffer, 1024, 0);
printf("\n Server Message is: %s\n",buffer );
/* sendto() and recvfrom() can also be used
sendto(sock_fd , msg , strlen(msg) , 0 , 0, 0);
valread = recvfrom (sock_fd, buffer, 1024, 0, 0, 0);
sendto(sock_fd , msg , strlen(msg) , 0 , (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in));
valread = recvfrom( sock_fd, buffer, 1024, 0, (struct sockaddr *)&serv_addr, (socklen_t *)sizeof(struct sockaddr_in));
printf("\n Message send to server is %s\n", msg);
printf("\n Server Message is %s\n", buffer);
*/
close(sock_fd);
return 0;
}
TCP socket Server Program:
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
int server_fd, new_socket, valread;
struct sockaddr_in serv_addr;
bzero (&serv_addr, sizeof(serv_addr));
char buffer[1024] = {0};
char *hello = "Hello from server";
struct sockaddr_in cli_addr;
bzero (&cli_addr, sizeof(cli_addr));
int cli_addr_len = sizeof(cli_addr);
/* Creating socket file descriptor */
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
{
printf("\n Socket creation error %d %s \n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
unsigned short int port = atoi(argv[1]);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons( port );
if (bind(server_fd, (struct sockaddr *)&serv_addr,
sizeof(serv_addr))<0)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0)
{
printf("\n Socket creation error %d %s \n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
/* Getting a new socket for an incoming client and filling in cli_add structure on return */
if ((new_socket = accept(server_fd, (struct sockaddr *)&cli_addr,
(socklen_t*)&cli_addr_len))<0)
{
perror("accept");
exit(EXIT_FAILURE);
}
valread = recv (new_socket, buffer, 1024, 0);
printf("\n Message received from client is: %s\n",buffer );
send(new_socket , hello , strlen(hello) , 0 );
printf("\n Message send to client is: %s\n", hello);
/* cli_addr is filled upon return of accept() with client IP and length, port */
printf ("\n Client IP address is: %s\n",inet_ntoa(cli_addr.sin_addr));
printf ("\n Client cli_addr_len is: %d\n", ntohs(cli_addr.sin_port));
printf ("\n Client Socket Family is: %d\n", cli_addr.sin_family);
/* recvfrom() and sendto() API can also be used
valread = recvfrom (new_socket , buffer, 1024, 0, 0, 0);
sendto(new_socket , hello , strlen(hello) , 0, 0 ,0 );
valread = recvfrom (new_socket, buffer, 1024, 0, (struct sockaddr *)&cli_addr,
(socklen_t *)&cli_addr_len);
sendto(new_socket , hello , strlen(hello) , 0, 0 ,0 );
printf("Message received from client is: %s\n",buffer );
printf("\n Message send to client is %s\n", hello);
*/
close (new_socket);
close (server_fd);
return 0;
}
Output:
Server:
[aprakash@wtl-lview-6 socket]$ gcc tcp_ser.c -o ser
[aprakash@wtl-lview-6 socket]$ gcc tcp_cli.c -o cli
[aprakash@wtl-lview-6 socket]$ ./ser 1234
Message received from client is: Hello from client
Message send to client is: Hello from server
Client IP address is: 127.0.0.1
Client cli_addr_len is: 60104
Client Socket Family is: 2
Client:
[aprakash@wtl-lview-6 socket]$ gcc tcp_cli.c -o cli
[aprakash@wtl-lview-6 socket]$ ./cli 1234
Message send to server is: Hello from client
Server Message is: Hello from server
UDP Socket Client Program
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
int main(int argc, char const *argv[])
{
int sock = 0, valread;
struct sockaddr_in serv_addr;
char *msg = "Hello from client";
char buffer[1024] = {0};
memset(&serv_addr, 0, sizeof(serv_addr));
/* Equivallent */
//bzero(&serv_addr, sizeof(serv_addr));
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
printf("\n Socket creation error %d %s \n", errno, strerror(errno));
return -1;
}
serv_addr.sin_family = AF_INET;
unsigned short int port = atoi(argv[1]);
serv_addr.sin_port = htons(port);
serv_addr.sin_addr.s_addr = INADDR_ANY;
/*
if (inet_aton("127.0.0.1", &serv_addr.sin_addr) == 0)
{
printf("\n Invalid IP address\n");
return -1;
}
int len;
sendto(sock, msg, strlen(msg), 0, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in));
printf("\n Message send to server is: %s\n", msg);
valread = recvfrom(sock , buffer, 1024, 0, (struct sockaddr *)&serv_addr,(socklen_t *)&len);
buffer[valread] = '\0';
printf("\n Server responce is: %s\n",buffer);
close(sock);
/* send() and recv() can be used only when connect() is used
* but on the server side, send() and recv() cannot be used as there is
* no client address to send data to and recv data from */
/*
connect(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
send(sock, msg, strlen(msg), MSG_CONFIRM);
valread = recv( sock , buffer, 1024, MSG_WAITALL);
*/
return 0;
}
UDP socket Server Program:
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
int server_fd, valread;
struct sockaddr_in address, cli;
bzero(&address,sizeof(address));
bzero(&cli,sizeof(cli));
char buffer[1024] = {0};
char *hello = "Hello from server";
if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) == 0)
{
printf("\n Socket creation error %d %s \n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
unsigned short int port = atoi(argv[1]);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons( port );
/*
if (inet_aton("127.0.0.1", &address.sin_addr) == 0)
{
printf("\n Invalid IP address\n");
return -1;
}
*/
if (bind(server_fd, (struct sockaddr *)&address,
sizeof(address))<0)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
int len;
valread = recvfrom( server_fd, buffer, 1024, 0, (struct sockaddr *)&cli, (socklen_t *)&len);
buffer[valread] = '\0';
printf("\n Message received from client is:%s\n",buffer );
sendto(server_fd, hello, strlen(hello), 0, (struct sockaddr *)&cli, len );
printf("\n Message sent to client is: %s\n",hello);
return 0;
}
Output:
Server:
[aprakash@wtl-lview-6 socket]$ gcc tcp_ser.c -o ser
[aprakash@wtl-lview-6 socket]$ ./ser 1234
Message received from client is: Hello from client
Message send to client is: Hello from server
Client:
[aprakash@wtl-lview-6 socket]$gcc tcp_cli.c -o cli
[aprakash@wtl-lview-6 socket]$ ./cli 1234
Message send to server is: Hello from client
Server Message is: Hello from server
Blocking Calls
Many of the socket APIs block( by default) until a certain event:
- connect: until the connection is established.
- accept: until a connection comes in.
- recv, recvfrom: until a packet(data) is received, packet may get lost in UDP socket.
- send: until data are pushed into socket’s buffer.
- sendto: until the data are given to network system.
The solution to blocking calls:
There are three solutions to blocking calls:
- Non-blocking socket
- By setting flag
- Timeout
Non-blocking socket:
Make the socket non-blocking by use of O_NONBLOCK using fcntl() API.
int flags = fcntl (sock_fd, F_GETFL, 0); fcntl (sock_fd, O_NONBLOCK|flags);
By setting flags:
Flag argument of send(), sendto(), recv() and recvfrom() can be set to MSG_DONTWAIT.
Timeout:
select() API can be used with timeout option to return once the time elapses.
Tricky Interview Questions:
1. Can connect() API be used with a UDP socket?
Answer: Yes, for details please refer to connect() API above.
2. Can listen() and accept() be used with a UDP socket?
Answer: No, Operation not permitted.
3. Can sendto() and recvfrom() API be used with TCP socket?
Answer: Yes, for details please refer sendto() and recvfrom() API above.
4. Can send() and recv() API be used with the UDP socket?
Answer: Yes, can be used with connect() but only on the client side.
5. Is bind() API mandatory on the client-side?
Answer: No, for details refer to bind() API above.
6. Is it possible that a mix of send() and recvfrom() be used or another similar combination?
Answer: Yes
Relevant Posts:
- Sockets Introduction
- Socket Options
- Multi-client server support- select() API
- Host and Network Byte ordering
- Little-endian and big-endian
References:
- https://www.csd.uoc.gr/~hy556/material/tutorials/cs556-3rd-tutorial.pdf
- https://notes.shichao.io/unp/ch1/
Categories: Operating system (OS)
Leave a Reply