Post

Socket Programming in C

State Diagram for Server and Client Model

Sockets:

sockets are the endpoint used for connecting to a node. The signals required to implement the connection between two nodes are sent and received using the sockets on each node respectively. In C, you can create a socket using the socket() function, which takes domain, type, and protocol as arguments. Common domains include AF_INET (IPv4) and AF_INET6 (IPv6), types can be SOCK_STREAM (for TCP) or SOCK_DGRAM (for UDP), and protocols are usually set to 0 for default.

1
2
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
1
2
3
4
5
int socket_fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (socket_fd == -1) {
    perror("socket");
    exit(EXIT_FAILURE);
}

Configuring a Socket

The setsockopt() function in socket programming in C is used to specify some options for the socket to control the behavior of the socket. The syntax is,

1
int setsockopt(int socket_descriptor, int level, int option_name, const void *value_of_option, socklen_t option_length);
  • The socket is the file descriptor returned by the socket() function.
  • The level parameter represents the level at which the option for the socket must be applied. The SOL_SOCKET represents the socket level and IPPROTO_TCP represents the TCP level.
  • The option_name specifies the rules or options that should be modified for the socket. Some useful options are,
    • SO_DEBUG is used to enable the recording of debugging information.
    • SO_REUSEADDR is used to enable the reusing of local addresses in the bind() function.
    • SO_SNDBUF is used to set the maximum buffer size that can be sent using the socket connection.
  • The option_value is used to specify the value for the options set in the option_name parameter.
  • The option_length is the length of the variable used to set the option value.

The function returns a value of 0 of data type int on the successful application of the option and a value of -1 on failure.

Binding a socket:

The bind() function in socket programming in C is used to assign an address to a socket created using the socket() function. The syntax of bind() function is,

1
int bind(int socket_descriptor , const struct sockaddr *address, socklen_t length_of_address);
  • The socket_descriptor is the value of the file descriptor returned by the socket() function.
  • The address is a structure of type sockaddr. We usually use a structure of type sockaddr_in to represent this information, because information such as port and address can only be stored in this structure. The sockaddr_in is cast to the sockaddr data type when calling the bind() function.
  • The length_of_address represents the size of the address passed as the second parameter.

The function returns 0 on binding the address and port successfully or returns -1 on failure.

Listen to a socket:

The listen() function in socket programming is used to make the server node wait and listen for connections from the client node on the port and address specified by the bind() function. The syntax is,

1
 int listen(int socket_descriptor, int back_log);
  • The socket_descriptor represents the value of the file descriptor returned by the socket() function.
  • The back_log marks the maximum number of connection requests that can be made to the server by client nodes at a time. The number of requests made after the number specified by back_log may cause an error or will be ignored by the server if the options for retransmission are set.

The function returns 0 on listening on the address and port specified or returns -1 on failure.

Accept a connection:

The accept() function is used to establish a connection between the server and the client nodes for the transfer of data. The syntax is,

1
int accept(int socket_descriptor, struct sockaddr *restrict address, socklen_t *restrict length_of_address);
  • The socket_descriptor represents the value of the file descriptor returned by the socket() function.
  • The address is the variable of the sockaddr_in structure in which the address of the socket returned from the function will be stored.
  • The length_of_address depicts the size of the address parameter.

The accept() function creates a new socket from the first connection request for the specified socket_descriptor and returns the file descriptor of the new socket. The file descriptor of this new socket is used in the read() and write() functions to send and receive data to and from the client node.

Connect to a socket:

The connect() function is used to send the connection request and connect to the server node. The syntax of the function is,

1
int connect(int socket_descriptor, const struct sockaddr *address, socklen_t length_of_address);
  • The socket_descriptor repre sents the value of the file descriptor returned by the socket() function during the creation of a socket on the client-side.
  • The address represents the structure with the information of the address and port number of the server node to which the connection is to be made.
  • The length_of_address is the size of the address structure used in the second parameter.

The connect() function returns a value of 0 on successfully connecting with the server and returns a value of -1 on error or the connection fails.

Variables:

  • The port number is passed to the htons() function which converts the port number stored in memory to network byte order.

  • The INADDR_ANY represents that any address can be used for binding. It generally indicates the address of 0.0.0.0. Other than INADDR_ANY, some useful addresses are,
    • INADDR_LOOPBACK which refers to the localhost of the node. The address is 127.0.0.1.
    • INADDR_BROADCAST is similar to INADDR_ANY but has the address 255.255.255.255. It is used for broadcast communication.
  • The struct sockaddr_in structure is cast to type struct sockaddr to match the syntax of the function. The sizeof() function is used to find the size of struct sockaddr.

  • The listen() function puts the socket in a listening state and a back_log of 4 states that a maximum of 4 connection requests can be made at a time to the server.

  • The accept() function establishes a connection between server and client and returns a new file descriptor.

  • The read() and write() functions are used to send and receive between client and server. The syntax of read() function is,
1
ssize_t read(int file_descriptor, void *buffer, size_t size);

The data of specified size is read and stored in the buffer. The syntax of the write function is,

1
ssize_t write(int file_descriptor, const void *buffer, size_t size);

The data of specified size is written from the buffer. On successful read or write, the read() and write() functions return the number of bytes of data that has been read or written or return the value of -1 on failure.

  • The close() function is used to terminate the socket connection.

A network byte order is an order in which the most significant bits are stored first.

  • The read() and write() can help with communication between sockets but they’re limited the more appropriate way is to use send() and recv(). They give much more control over data transmittion.

addrinfo

  • The addrinfo structure describes address information for use with TCP/IP. To get this information, call getaddrinfo(); to free a linked list of these structures, call freeaddrinfo().
  • It’s also used in host name lookups, and service name lookups.
1
2
3
4
5
6
7
8
9
10
11
12
#include <netdb.h>

struct addrinfo {
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen;
    char * ai_canonname_;
    struct sockaddr * ai_addr;
    struct addrinfo * ai_next;
};

The addrinfo structure includes these members:

ai_flags: Flags. Includes AI_PASSIVE, AI_CANONNAME, and AI_NUMERICHOST. For a complete list, see . _ai_family_: Protocol family. Includes PF_UNSPEC and PF_INET. For a complete list, see <sys/socket.h>. _ai_socktype_: Socket type. Includes SOCK_STREAM and SOCK_DGRAM. For a complete list, see <sys/socket.h>. _ai_protocol_: Protocol. Includes IPPROTO_TCP and IPPROTO_UDP. For a complete list, see <netinet/in.h>. _ai_addrlen_: The length of the _ai_addr_ member. _ai_canonname_: The canonical name for _nodename_. _ai_addr_: Binary socket address. _ai_next_: A pointer to the next addrinfo structure in the linked list.

sockaddr

  • The struct sockaddr holds socket address information for many types of sockets.
    1
    2
    3
    4
    
    struct sockaddr {
      unsigned short sa_family; // address family, AF_xxx
      char sa_data[14]; // 14 bytes of protocol address
    };
    

sa_family can be a variety of things, but it’ll be AF_INET (IPv4) or AF_INET6 (IPv6). Usually sa_data contains a destination address and port number for the socket. This is rather unwieldy since you don’t want to tediously pack the address in the sa_data by hand. To deal with struct sockaddr, programmers created a parallel structure: struct sockaddr_in (“in” for “Internet”) to be used with IPv4.

sockaddr_in

  • The sockaddr_in structure has four data elements to represent the address family, port, internet address, and an additional space that is used to match the size of the sockaddr structure when performing casting operation. The structure of sockaddr_in is given below,
1
2
3
4
5
6
struct sockaddr_in { 
	short sin_family; // address family or domain
	unsigned short sin_port; // port
	struct in_addr sin_addr; // Structure for Address
	char sin_zero[8]; //  padding for casting operations
};

a pointer to a struct sockaddr_in can be cast to a pointer to a struct sockaddr and vice-versa. So even though connect() wants a struct sockaddr*, you can still use a struct sockaddr_in and cast it at the last minute!

in_addr

The structure in_addr present inside the sockaddr_in structure is,

1
2
3
struct in_addr {
    unsigned long s_addr; // internet address value
};

A data element inside the structure can be accessed by the . operator and values are assigned to the elements.

struct sockaddr_storage

  • is designed to be large enough to hold both IPv4 and IPv6 structures. See, for some calls, sometimes you don’t know in advance if it’s going to fill out your struct sockaddr with an IPv4 or IPv6 address. So you pass in this parallel. structure, very similar to struct sockaddr except larger, and then cast it to the type you need:
1
2
3
4
5
6
struct sockaddr_storage {
	sa_family_t ss_family; // address family 
	
	// all this is padding, implementation specific, ignore it: 
	char __ss_pad1[_SS_PAD1SIZE];
	int64_t __ss_align; c

Helper Functions

inet_pton()

converts an IP address in numbers-and-dots notation into either a struct in_addr or a struct in6_addr depending on whether you specify AF_INET or AF_INET6. (“pton” stands for “presentation to network”— you can call it “printable to network” if that’s easier to remember.) The conversion can be made as follows:

1
2
3
4
struct sockaddr_in sa; // IPv4 
struct sockaddr_in6 sa6; // IPv6
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6

(Quick note: the old way of doing things used a function called inet_addr() or another function called inet_aton(); these are now obsolete and don’t work with IPv6.)

inet_ntop()

1
2
3
4
5
6
7
8
9
10
11
// IPv4:
char ip4[INET_ADDRSTRLEN]; // space to hold the IPv4 string
struct sockaddr_in sa; // pretend this is loaded with something
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4); 9 10 11 

// IPv6: 
char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string
struct sockaddr_in6 sa6; // pretend this is loaded with something
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);

getaddrinfo

  • It helps set up the structs you need later on.
  • A tiny bit of history: it used to be that you would use a function called gethostbyname() to do DNS lookups. Then you’d load that information by hand into a struct sockaddr_in, and use that in your calls.
1
2
3
4
5
6
7
8
9
10
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, // e.g. "www.example.com" or IP 
				const char *service, // e.g. "http" or port number (in IANA Or /etc/services)
				const struct addrinfo *hints, // addrinfo filled with relevant information
				struct addrinfo **res);
				
// You give this function three input parameters, and it gives you a pointer to a linked-list, res, of results.

Refrences:

Refrences:
https://www.scaler.com/topics/socket-programming-in-c/
https://www.geeksforgeeks.org/socket-programming-cc/
https://www.qnx.com/developers/docs/6.3.2/neutrino/lib_ref/lib-s.html
https://beej.us/guide/bgnet/

This post is licensed under CC BY 4.0 by the author.