03. Hello Server/Client
This chapter continues the introduction of socket programming. We’ll write 2 simple (incomplete and broken) programs to demonstrate the syscalls from the last chapter. The first program is a server, it accepts connections from clients, reads a single message, and writes a single reply. The second program is a client, it connects to the server, writes a single message, and reads a single reply. Let’s start with the server first.
First, we need to obtain a socket fd:
int fd = socket(AF_INET, SOCK_STREAM, 0);
The AF_INET
is for IPv4, use AF_INET6
for
IPv6 or dual-stack socket. For simplicity, we’ll just use
AF_INET
throughout this book.
The SOCK_STREAM
is for TCP. We won’t use anything other
than TCP in this book. All the 3 parameters of the socket()
call are fixed in this book.
Next, we’ll introduce a new syscall:
int val = 1;
(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); setsockopt
The setsockopt()
call is used to configure various
aspects of a socket. This particular call enables the
SO_REUSEADDR
option. Without this option, the server won’t
able to bind to the same address if restarted. Exercise to reader: find
out what exactly is SO_REUSEADDR
and why it is needed.
The next step is the bind()
and listen()
,
we’ll bind on the wildcard address 0.0.0.0:1234
:
// bind, this is the syntax that deals with IPv4 addresses
struct sockaddr_in addr = {};
.sin_family = AF_INET;
addr.sin_port = ntohs(1234);
addr.sin_addr.s_addr = ntohl(0); // wildcard address 0.0.0.0
addrint rv = bind(fd, (const sockaddr *)&addr, sizeof(addr));
if (rv) {
("bind()");
die}
// listen
= listen(fd, SOMAXCONN);
rv if (rv) {
("listen()");
die}
Loop for each connection and do something with them.
while (true) {
// accept
struct sockaddr_in client_addr = {};
socklen_t socklen = sizeof(client_addr);
int connfd = accept(fd, (struct sockaddr *)&client_addr, &socklen);
if (connfd < 0) {
continue; // error
}
(connfd);
do_something(connfd);
close}
The do_something()
function is simply read and
write.
static void do_something(int connfd) {
char rbuf[64] = {};
ssize_t n = read(connfd, rbuf, sizeof(rbuf) - 1);
if (n < 0) {
("read() error");
msgreturn;
}
("client says: %s\n", rbuf);
printf
char wbuf[] = "world";
(connfd, wbuf, strlen(wbuf));
write}
Note that the read()
and write()
call
returns the number of read or written bytes. A real programmer must deal
with the return value of functions, but in this chapter, I have omitted
lots of things for brevity. And the code in this chapter is not the
correct way to do networking anyway.
The client program is much simpler:
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
("socket()");
die}
struct sockaddr_in addr = {};
.sin_family = AF_INET;
addr.sin_port = ntohs(1234);
addr.sin_addr.s_addr = ntohl(INADDR_LOOPBACK); // 127.0.0.1
addrint rv = connect(fd, (const struct sockaddr *)&addr, sizeof(addr));
if (rv) {
("connect");
die}
char msg[] = "hello";
(fd, msg, strlen(msg));
write
char rbuf[64] = {};
ssize_t n = read(fd, rbuf, sizeof(rbuf) - 1);
if (n < 0) {
("read");
die}
("server says: %s\n", rbuf);
printf(fd); close
Compile our programs with the following command line:
g++ -Wall -Wextra -O2 -g 03_server.cpp -o server g++ -Wall -Wextra -O2 -g 03_client.cpp -o client
Run ./server
in a window and then run
./client
in another window. You should see the following
results:
$ ./server client says: hello
$ ./client server says: world
Exercise for readers: read manpages of APIs used in this chapter, or find online tutorials for them. Make sure you know how to find helps on API usage since this book won’t cover the details of API usage.
Source code:
codecrafters.io offers “Build Your Own X” courses in many programming languages.
Including Redis, Git, SQLite, Docker, and more.