Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 51

TCP Server Socket Call Sequence

Recommended resources

Introduction

Hello! In our last lesson, we established the core concept of a socket as a communication endpoint, represented in C as a file descriptor. We ended on a crucial point: a TCP server uses a special listening socket to wait for incoming connections, and then creates a new connection socket for each client.

Today, we'll dive into the practical C code that makes this happen. We will explore the precise sequence of system calls a server program must execute to set itself up to receive connections. This lesson directly addresses the learning outcome: to describe the sequence of socket API calls for a typical TCP server (socket, bind, listen, accept). By the end, you'll understand the role of each function in bringing a server to life.


The Server's Lifecycle: A High-Level View

Before we examine each function in detail, let's get a bird's-eye view of the process. Setting up a server is like opening a new business with a dedicated phone line:

  1. socket(): You order a phone line to be installed. You have a physical phone (the socket endpoint), but it has no number and isn't active.
  2. bind(): You assign a specific phone number (IP address and port) to your phone. Now, people know how to reach you.
  3. listen(): You turn the ringer on and tell the phone company you're open for business. The phone company will start queuing incoming calls for you.
  4. accept(): You answer a call from a customer. This call is on a new line, so your main business line is immediately free to receive other incoming calls.

This flowchart provides a great visual summary of the sequence we'll be studying today. The server-side steps are on the right.

TCP Socket API Call Flowchart
This flowchart shows the distinct steps for a TCP server (right side) and client (left side). Today, we focus on the server's path: socket, bind, listen, and accept.

1. socket(): Creating the Endpoint

The first step is to ask the operating system for a socket. This is done with the socket() system call, which creates the endpoint for communication but doesn't connect it to anything yet.

int socket(int domain, int type, int protocol);
  • domain: Specifies the communication domain. For internet communication using IPv4, we use AF_INET (Address Family - Internet).
  • type: Specifies the communication semantics. For TCP, we use SOCK_STREAM, which provides a reliable, connection-oriented, two-way byte stream.
  • protocol: Specifies the protocol. We typically use 0, which tells the OS to choose the most appropriate protocol for the given type (which is TCP for SOCK_STREAM).

If successful, socket() returns a small, non-negative integer—the socket descriptor. This is the file descriptor we'll use to reference this socket in all future calls. A return value of -1 indicates an error.

The following video provides an excellent analogy for what this first step accomplishes.

The Linux socket API explained

This clip from 'The Linux socket API explained' likens creating a socket to building a door frame that isn't yet part of a house. It's an endpoint that exists but isn't yet configured or connected.

Watch from 03:27 to 04:42. Notice how the call creates a file descriptor but doesn't associate it with any address or connection yet.


2. bind(): Assigning an Address

Our newly created socket is just a generic endpoint. To be useful, a server's socket needs to be associated with a specific port on the machine so that clients know where to connect. This is the job of the bind() system call.

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

To use bind(), we first need to populate a struct sockaddr_in with the address information:

struct sockaddr_in server_addr;

server_addr.sin_family = AF_INET; // Address family is IPv4
server_addr.sin_port = htons(8080); // Port number (e.g., 8080), converted to network byte order
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to any available local IP address

Key points here:

  • htons() and htonl(): As we saw in the last lesson, these functions are crucial for converting numbers from host byte order to network byte order, ensuring our server works correctly regardless of the underlying system architecture.
  • INADDR_ANY: This is a special constant that tells the socket to accept connections on any of the machine's network interfaces (e.g., your Wi-Fi IP, Ethernet IP, or localhost). This is standard practice for servers.

Once the struct is ready, we call bind(), passing it the socket descriptor and a pointer to our address struct.

To learn more, let's turn to an article that provides clear C examples for each step.

Implementing a TCP server in C - ops.tips

This article, 'Implementing a TCP server in C', gives a detailed, code-centric explanation of each system call. We'll use it as our primary text reference.

Read the sections titled 'Creating a Socket' and 'Binding the socket to an address'. This will reinforce the socket() call and provide a clear example of populating the sockaddr_in struct and calling bind().


3. listen(): Opening for Connections

At this point, we have a socket that is bound to an address, but the OS isn't yet accepting connections on it. The listen() call flips the switch, marking the socket as passive and telling the OS to start queuing incoming connection requests.

int listen(int sockfd, int backlog);
  • sockfd: Our bound socket descriptor.
  • backlog: The maximum number of pending connections the kernel should queue for this socket. When a client tries to connect, the OS performs the three-way handshake. If successful, the connection is placed in a queue, waiting for the application to call accept(). The backlog value defines the size of this queue. A common value is 5 or 10.

After a successful listen() call, our server is officially "listening" and ready to receive connections.

Implementing a TCP server in C - ops.tips

Let's return to the 'ops.tips' article to see the implementation of listen().

Read the section 'Making the socket listen for connections'. Pay attention to how this call changes the socket's state to LISTEN.


4. accept(): Welcoming a Client

The server is listening, and clients can now connect. The final step is to handle an incoming connection using accept().

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

This is arguably the most important call in the sequence:

  • Blocking Call: accept() is a blocking function. If the queue of completed connections is empty, your program will pause execution and wait here until a client connects.
  • New Socket Descriptor: When a connection is taken from the queue, accept() creates a brand new socket and returns its file descriptor. This is the connection socket we discussed previously.
  • Communication: All subsequent communication with this specific client (using read() and write(), or recv() and send()) happens on this new descriptor.
  • Original Socket: The original listening socket (sockfd) is unaffected. It remains in the LISTEN state, ready for you to call accept() on it again to handle the next client.

This design is what allows a single server process to manage connections from many clients.

The following video uses a great analogy to explain the listen/accept distinction and the creation of the new file descriptor.

The Linux socket API explained

This clip uses analogies of opening a shop and having a doorman to distinguish between listen and accept. It clearly explains why accept returns a new file descriptor.

Watch from 08:12 to 12:14. Focus on the difference between the 'server fd' (the listening socket) and the new file descriptor returned by accept (the connection socket).


Putting It All Together: A Code Walkthrough

Now that you understand the purpose of each individual function, let's watch them being put together in a complete C program.

C Programming: TCP Server

This video walks through the process of writing a simple TCP server in C from scratch. Seeing the code written in sequence will help solidify the order and purpose of each API call.

Watch the following segments to see each part of our sequence in action: socket(): Watch from 07:06 to 10:39. bind(): Watch from 12:16 to 15:13 (this includes setting up the sockaddr_in struct). listen(): Watch from 15:13 to 16:09. accept(): Watch from 16:09 to 17:27. Don't worry about the setsockopt() call for now; it's a configuration step that isn't essential to the main sequence.

For a complete text-based example, you can also refer to the server.c file described in the "Sockets Tutorial" resource from linuxhowtos.org (resource ID LINK), which breaks down a simple server line-by-line.


Conclusion

You have now traced the complete setup sequence for a TCP server. By orchestrating these four system calls, a C program can create an endpoint, assign it a public address, listen for clients, and accept connections.

Key Takeaways:

  • A server's setup follows a strict order: socket(), bind(), listen(), accept().
  • socket() creates the communication endpoint and returns a file descriptor.
  • bind() associates that descriptor with a specific IP address and port number, giving the server a "name".
  • listen() marks the socket as passive, telling the OS to queue incoming connection requests.
  • accept() is a blocking call that waits for a client. When one connects, accept() returns a new socket descriptor for communicating with that client, leaving the original socket free to listen for others.

Preview of the Next Lesson:

We've covered the server's side of the story. But how does a client initiate the connection that the server accepts? In the next lesson, we will examine the client's API calls: socket() and connect(). After that, you will have all the pieces needed to write your first complete client-server application in C.