02. Socket Programming Concepts

Prerequisites: General programming and basic networking knowledge.

2.1 What to learn: from black boxes to code

Computer networks are often simplified as boxes connected by lines, but the actual coding is left out. However, network programming is not trivial. Given an API with 2 methods: send data & receive data. What else do you need to know?

Data serialization

The “data” you want to send via a network can be high-level objects like strings, structs, lists. However, computer networks only knows 0s and 1s. Thus, it’s necessary to create a mapping between objects and bytes. This is called serialization (object to bytes) and deserialization (bytes to object).

While you can do this trivially via JSON or Protobuf libraries, rolling your own by manipulating bits and bytes is a good start to low-level programming.

The TCP byte stream and protocols

There is more than working with bytes. Many applications are message-based. A message has a length that can be very large. If you download a 10G file via a network, the network cannot possibly store all the data and then forward it to the recipient. Instead, the computer network employs “packets” (IP, UDP) — messages of smaller length (typically a bit more than 1KB).

An application message can be larger than a network packet, so there is a need to assemble them into larger application messages. TCP solved this by combining smaller packets into an unlimited stream of ordered bytes. A byte stream has no length limit, but it also has no boundaries inside. Message boundaries must be introduced by the application via “protocols”, which are contracts between participants on how to interpret the byte stream.

Example 1: DNS uses both UDP and TCP. TCP is used when the DNS message does not fit in a UDP packet. DNS over TCP includes an extra header with the message length, which is used to split the byte stream into DNS messages. The DNS protocol also includes serialization/deserialization of DNS messages.

Example 2: HTTP/1.1 is message-based; you can make multiple requests over a single connection. This is possible because the protocol includes ways to determine the length of each request/response (mostly via “Content-Length” or chunked encoding). The server can tell when a request ends and the next request begins.

Concurrent programming

You can create any client side application with a protocol specification. But the server side is more complicated because it deals with multiple connections. Dealing with many concurrent connections (even mostly idle) is historically difficult (C10K problem). Although modern hardware can handle much more than 10K connections even with naive software, you’ll learn how to create efficient, scalable production software such as Redis, NGINX, and etc.

The modern software solution is event-based concurrency with event loops, which may be a new paradigm for you. Event-based concurrency is really complicated, and you’re likely to mistakes, so it’s best to learn it by doing to overcome potential misunderstandings.

2.2 TCP/IP Quick Review

Layers of protocols

Network protocols are divided into different layers, where the higher layer depends on the lower layer, and each layer provides different capabilities.

 top
  /\    | App |     message or whatever
  ||    | TCP |     byte stream
  ||    | IP  |     packets
  ||    | ... |
bottom

An IP packet is just a message with addresses and extra metadata while TCP does much more work and provides byte streams instead.

TCP: reliable byte streams

Communication with a packet-based scheme is complicated. There are many problems for applications to solve:

  • The packet size limit.
  • Out-of-order packets.
  • Packet loss and reliability.
  • Managing the lifetime of connections.

A byte stream is not just for bypassing the packet size limit, it also implies reliable and ordered delivery. Within the lifetime of a single TCP connection, TCP ensures that all data within the byte stream must appear exactly once and in order, otherwise apps cannot make sense of it due to the lack of boundaries.

Reliability is what most application need, so TCP is the default choice. However, the byte stream without boundaries is what people struggle to understand. A protocol is required to split the byte stream into messages with boundaries. Either use an existing protocol or roll your own.

UDP: packet-based

UDP is on the same layer as TCP, but is still packet-based like the lower layer. UDP just adds port numbers over IP packets. Port numbers are needed to have multiple connections between 2 IPs, multiple servers on the same IP, etc.

2.3 Socket Primitives

Although we’re coding on Linux, these concepts are platform-agnostic.

What is a socket?

A socket is a handle to refer to a connection or something else. The API for networking is called the socket API, which is similar on different operating systems. The name “socket” has nothing to do with sockets on the wall.

A handle is an opague integer used to refer to things that cross an API boundary. In the same way that a Twitter handle refers to a Twitter user. On Linux, a handle is called a file descriptor (fd) and it’s local to the process. The name “file descriptor” is just a name; it has nothing to do with files, nor does it describe anything.

The socket() method allocates and returns a socket fd (handle), which is used later to actually create connections.

A handle must be closed when you’re done to free the associated resources on the OS side. This is the only thing in common between different types of handles.

Listening socket & connection socket

A TCP server listens on a specific address (IP + port) and accepts client connections from that address. The listening address is also represented by a socket fd. When you accept a new client connection, you get the new connection’s fd.

2 types of socket handles:

  1. Listening sockets. Obtained by listening on an address.
  2. Connection sockets. Obtained by accepting a client connection from a listening socket.

The relevant methods:

  • bind(): Configure the listening address of a socket.
  • listen(): Make the socket a listening socket.
  • accept(): Return a client connection socket, when available.

In pseudo code:

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

Connect from a client

The connect() method is for initiating a TCP connection from the client side. Pseudo code:

fd = socket()
connect(fd, address)
do_something_with(fd)
close(fd)

socket() creates a typeless socket. The type of a socket (listening or connection) is determined after the listen() or connect() method.

Read and write

Altough TCP and UDP provide different types of services, they share the same socket API, including the send() and recv() methods, which do what their names say. For message-based sockets (UDP), each send/recv corresponds to a single packet. For byte-stream-based sockets (TCP), each send/recv appends to/consumes from the byte stream.

Sending and receiving is also called reading and writing. The read/write syscalls on Linux are used for both sockets, disk files, pipes, and etc. Different types of handle sharing the same read/write API is merely a coincidence; one cannot simply switch the socket type from TCP to UDP without rewriting, as they are semantically incompatible.

On Linux, you’ll find some variants of read/write syscalls with extra arguments. We’ll only use read/write, and the others are just FYI.

Reading Writing Description
read write Read/write with a single continuous buffer.
readv writev Read/write with multiple buffers.
recv send Has an extra flag.
recvfrom sendto Also get/set the remote address (packet-based).
recvmsg sendmsg readv/writev with more flags and controls.
recvmmsg sendmmsg Multiple recvmsg/sendmmsg in 1 syscall.

Summary: list of socket primitives

  • Listening socket:
    • bind() & listen()
    • accept()
    • close()
  • Using a socket:
    • read()
    • write()
    • close()
  • Create a connection: connect()

The next chapter will help you get started with real code.