Network Protocols

Published May 28, 2025

In the previous blog, we were introduced to internet sockets and their importance in sharing messages between devices, whether they’re in the same room or on opposite sides of the world. We learned about the layers of network protocols and how each layer encapsulates its underlying layer in a hierarchical fashion. We also explored how the actual message transfer (handshake) occurs between a client and server using IPv4 + TCP protocol through a simple Python API call.
We looked at protocols from a programmer’s perspective. It is important to understand how messages are sent and received through a shared interface between two systems — these are called network or transport protocols (TCP/IP/UDP). But it is equally important to understand how those systems interact with each other. This will be more clear when we learn about communication patterns.

Internet Protocols (IP) attitude to transporting data packets can be characterised as “send and forget”. It means that IP does not guarantee to actually deliver the data to the destination, nor does it guarantee that the data will be delivered undamaged, nor does it guarantee that data packets will be delivered to the destination in the order in which they were sent by the source, nor does it guarantee that only one copy of the data will be delivered to the destination.
The no-promises service offered by IP is not directly useful to many applications. Applications typically rely on TCP or UDP to ensure data integrity and, in the case of TCP, ordered and complete data delivery. This allows IP to move packets around the network on its behalf.

Transmission Control Protocol (TCP) provides a reliable byte-stream transfer service between two endpoints on an internet. But before diving deep, let’s take a step back and get ourselves familiar with socket API’s and how to create them. The code you will see in the following sections is incomplete and incorrect (
uhh-oh) because network programming is more than calling APIs, which you’ll learn as you go.

Prerequisites

Get familiar with Linux

Although the principles of network programming are the same, there are many platform differences on Windows & MacOS. For beginners, it’s most convenient to just use Linux, even if you have no Linux experience. You don’t need to know much about Linux to program in it.

  1. Get a Linux environment either via VirtualBox, WSL, or cloud providers (VPS).
  2. Learn how to edit, move, copy, and delete files. You don’t have to write code in Linux. Learn how to copy files into Linux, or share files with a VM.
  3. Compile code with g++. You don’t have to mess with build systems like makefiles.

Learn how to get documentation

Hey, but why not just chatgpt? — probably yes, but it is only recommended when starting with networks — to just get away with a few things.
man socket.2This command shows the man page for the socket()syscall (probably the first time i am using the term, also i like it). On Linux, all socket API methods are syscalls. It’s a good habit to read manpages for looking up things you already know, but not for learning new things.


Creating a TCP server

Client-Server interaction

Let’s see a simple pseudocode: read data from a client, write a response, that’s it.

fd = socket()
bind(fd, address)
listen(fd)
while True:
conn_fd = accept(fd)
do_something_with(conn_fd)
close(conn_fd)

The socket()syscall takes 3 integer arguments.
int fd = socket(AF_INET, SOCK_STREAM, 0);
The combination of the 3 arguments determines the socket protocol.

We’ll only be using TCP, so you can forget about those arguments for now.

Bind to an address

We’ll bind to the wildcard address 0.0.0.0:1234(can be left blank; the OS picks one for you). This is just a parameter for listen()

struct sockaddr_in addr = {};      
addr.sin_family = AF_INET;        // IPv4  
addr.sin_port = htons(1234);      // expose port 1234   
addr.sin_addr.s_addr = htonl(0);  // wildcard IP 0.0.0.0   
int rv = bind(fd, (const struct sockaddr *)&addr,sizeof(addr));  
if (rv)   
{
  die("bind()")
}

struct sockaddris not used anywhere, so just type cast struct sockaddr_in to this * pointer type to match the function prototype.

Listen

Before sending data across the network, TCP establishes a connection with the destination via an exchange of management packets. All the previous steps are just passing parameters. The socket is actually created after listen()command.

rv = listen(fd, backlog);  
if (rv)   
{
   die("listen()")
}

backlog is the number of connections allowed on the incoming queue. What does that mean? Well, incoming connections are going to wait in this queue until you accept()them, and this is the limit on how many can queue up.

Accept connections

You call accept() which tells the system to get the pending connection. It returns a brand new socket fd to use for this single connection. So, you now have two file descriptors. The original one (fd) is still listening for more new connections and the latter is ready to send and receive data. The server now enters a loop that accepts and processes each client connection.

while (true) {
 
 // accept  
 struct sockaddr_in client_addr = {};          
 socklen_t addrlen =sizeof(client_addr);          
 int connfd = accept(fd, (struct sockaddr *)&client_addr, &addrlen);  
 if (connfd < 0)   
 {
  continue; // error   
 }
         
 do_something(connfd);          
 close(connfd);      
}

This should look familiar by now. We have established the connection and connfd()is the listen()ing socket descriptor.

Read & Write — Talk to me!

Let’s test our connection by doing something — by reading from and writing to the call.

static void do_something(int connfd) {
      
 char rbuf[64] = {};      
 ssize_t n = read(connfd, rbuf, sizeof(rbuf) - 1);  
 if (n < 0) {
  msg("read() error");  
  return;     
 }
     
 printf("client says: %s\n", rbuf);
     
 char wbuf[] = "world";      
 write(connfd, wbuf, strlen(wbuf));  
}

You can now pass data back and forth on stream sockets.
Side note: get address of each side
If you are using wildcard address or letting the OS pick the port, you don’t know the exact address. getsockname()gets you the local address of a TCP connection and getpeername()retrieves the remote address (the same address returned from accept).

int getsockname(int fd,struct sockaddr *addr, socklen_t *addrlen); // local
int getpeername(int fd,struct sockaddr *addr, socklen_t *addrlen); // remote