Embedded HandbookApps
All sections › IX. The C language: from basics to advanced
STM32F103, STM32F407 · level basic · 18 min read
Open in the interactive handbook

78. Sockets and a web server in C

A socket is a programming interface for exchanging data over a network. Its basis is the same in Linux, in Windows and, through a TCP/IP stack under an RTOS kernel, on the F407 with built-in Ethernet (section 37.1), but the details differ. In Windows you need WSAStartup before the first call, a socket has the type SOCKET, not int, it is closed with closesocket, the error code comes from WSAGetLastError, and MSG_NOSIGNAL and SIGPIPE do not exist there at all. In the lwIP stack on the chip the real names are lwip_socket, lwip_close; the familiar socket and close are provided by macros, and errno is a stack setting. The examples in this section are written for Linux. The F103 has no network controller: there sockets live in an external module.

That is why this section was checked on a computer: the programs were built with gcc under Linux in Docker, and the server and client exchange data over the local address. And everything that does not depend on the operating system (HTTP request parsing, the page, frames with byte stuffing) was also built with armclang for Cortex-M3 and M0: this code moves to STM32 without changes.

Terms used in the documentation

Term Meaning
socket an endpoint of a connection
file descriptor the number of an open file or socket
address family AF_INET the IPv4 address family
stream socket SOCK_STREAM, TCP a stream socket, TCP
host / network byte order byte order of the machine / of the network
bind, listen, accept, connect attach to an address, wait for connections, take one, connect
backlog the queue of connections waiting for accept
blocking call a call that waits until it can complete
errno the code of the last error
TIME_WAIT the state of a port after a connection is closed
SIGPIPE the "write to a closed pipe" signal
request line, status line, header the first line of a request, of a response, a header line
framing, byte stuffing splitting a stream into frames, inserting escape bytes
MMU / MPU memory management unit / memory protection unit
segmentation fault a memory protection violation

78.1 A program on a computer does not see the hardware

On STM32 the address 0x40010800 is the GPIOA port registers of the F103 (section 8), and any code can write there. On a computer you cannot do this.

Under Linux each process has its own virtual address space. The processor's memory management unit (MMU) translates each virtual address into a physical one using a table maintained by the kernel, in pages of 4096 bytes. Addresses that were not given to the process do not exist: the kernel stops an access to them with the SIGSEGV signal, "segmentation fault". In the check, a program on the computer wrote a number to the address 0x40010800 and was killed by signal 11.

That is why a program on a computer works with devices through the kernel: it gets a descriptor, a small integer, and calls functions with it. A file, a socket, a serial port are all descriptors.

Computer F103 F407
address translation MMU, 4 KB pages none none
memory protection each process has its own space none MPU: regions with access rights
access to someone else's address SIGSEGV, the process is terminated executes or BusFault MemManage, if the MPU is configured

The MPU does not translate addresses, it only sets access rights for a few areas of memory: an RTOS uses it, for example, to protect the stack of one task from another. In stm32f407xx.h it is declared (__MPU_PRESENT 1U), in stm32f10x.h only for the F1 XL series: the F103C8 does not have it.


78.2 Socket, address, port

socket(AF_INET, SOCK_STREAM, 0) creates a TCP socket and returns a descriptor, the first free number. In the check it is 3: numbers 0, 1 and 2 are taken by standard input, output and error output. On error the function returns −1, and the reason is in errno; all calls in this section work this way.

An IPv4 address is four bytes (written as four numbers separated by dots), a port is 16 bits, from 0 to 65535. Both are stored in struct sockaddr_in, which is 16 bytes in the check: the family (2), the port (2), the address (4) and 8 bytes of padding, which are zeroed.

Byte order. The network uses "most significant byte first". x86 and Cortex-M do the opposite, least significant byte first (sections 3а and 67). That is why the port and address are converted to network order: htons for 16 bits (the port), htonl for 32 bits (the address). htons(1500) is stored in memory as 05 DC; if you read these bytes as a number on the computer, you get 56325.

static struct sockaddr_in addr_of(uint32_t ip, uint16_t port)
{
    struct sockaddr_in a;
    memset(&a, 0, sizeof a);
    a.sin_family = AF_INET;
    a.sin_addr.s_addr = htonl(ip);
    a.sin_port = htons(port);
    return a;
}

⚠️ htons for the address. The line addr.sin_addr.s_addr = htons(INADDR_ANY); works, because INADDR_ANY is zero, and byte order does not matter for zero. Replace it with INADDR_LOOPBACK (127.0.0.1), and htons cuts the address down to 16 bits: you get 0.1.0.0, and bind fails with EADDRNOTAVAIL. The address always takes htonl.

On Cortex-M swapping bytes costs one instruction. armclang turns __builtin_bswap16 and __builtin_bswap32, and even the same thing written with shifts, into rev16 and rev, both on M3 and on M0. For a network stack on STM32 this costs nothing.


78.3 Server and client

Step Server Client
1 socket: create socket: create
2 bind: attach to an address and port — (the system picks the port)
3 listen: accept connections —
4 accept: take one, a new descriptor connect: connect to the server
5 recv / send through the new descriptor send / recv
6 close close
/* слушающий сокет на 127.0.0.1:port; reuse — SO_REUSEADDR до bind */
static int listener(uint16_t port, int reuse, int *err)
{
    int s = socket(AF_INET, SOCK_STREAM, 0);
    if (reuse)
        setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof reuse);
    struct sockaddr_in a = addr_of(INADDR_LOOPBACK, port);
    if (bind(s, (struct sockaddr *)&a, sizeof a) < 0) {
        if (err)
            *err = errno;
        close(s);
        return -1;
    }
    listen(s, 1);
    return s;
}

static int dial(uint16_t port, int *err)
{
    int c = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in a = addr_of(INADDR_LOOPBACK, port);
    if (connect(c, (struct sockaddr *)&a, sizeof a) < 0) {
        if (err)
            *err = errno;
        close(c);
        return -1;
    }
    return c;
}

What matters here:

Server and client over time: accept waits for a connection and returns the new descriptor 4, recv returns 5 bytes of hello, and after the client's close it returns 0

Situation errno
connect, but the server is not running ECONNREFUSED
bind to a busy port EADDRINUSE
bind to an address the machine does not have EADDRNOTAVAIL
a second connect of a connected socket EISCONN
writing to a connection closed by the peer EPIPE

78.4 TCP is a stream of bytes, not of messages

The main thing to understand about TCP: it guarantees the order and delivery of bytes, but does not keep message boundaries. In the check:

The rule "one send, one recv" is wrong. In a local network on your desk it often holds, and it breaks in a real one. Boundaries are set by the protocol: a delimiter (HTTP ends the headers with an empty line), a length before the data, or frames with flags (78.7).

Two sends merged into one recv, and ten bytes arrived in parts of 4, 4 and 2

recv returned Meaning
> 0 this many bytes arrived, possibly fewer than requested
0 the peer closed the connection
−1 an error, the reason is in errno
/* читать, пока не встретится end (NULL — до закрытия соединения) */
static size_t recv_until(int fd, char *buf, size_t cap, const char *end)
{
    size_t n = 0;
    buf[0] = '\0';
    while (n + 1 < cap) {
        ssize_t r = recv(fd, buf + n, cap - 1 - n, 0);
        if (r <= 0)               /* 0 — собеседник закрыл, −1 — ошибка */
            break;
        n += (size_t)r;
        buf[n] = '\0';
        if (end && strstr(buf, end))
            break;
    }
    return n;
}

The function is fine for one request per connection. If messages follow one another over the connection, everything read after the delimiter is the beginning of the next message, and it must be kept, not thrown away.

⚠️ A closed connection gives 0, every time. In the check recv returned 0 three times in a row after the close. A loop "read until the message ends" that does not look at the result spins forever on the old contents of the buffer.

⚠️ Send the length of the data, not of the buffer. send(c, buf, sizeof buf, 0) after strcpy(buf, "OK") sent 1024 bytes, together with everything that was in the buffer before. In the check, a password written there earlier went out to the network this way. Send strlen(buf), plus one if the receiver expects the terminating zero.

⚠️ Input into a buffer needs a limit. scanf("%s", buf) does not know the size of the buffer: a long string writes past its end. Use %1023s or fgets (section 68).


78.5 A closed connection, SIGPIPE and restart

SIGPIPE. The peer has closed the connection, and the program writes to it. The first write goes through: the data goes into the buffer, and the peer replies with a connection reset. On the second one the kernel sends the process the SIGPIPE signal, which by default terminates the process. The server dies because the client closed a browser tab. In the check the process was killed by signal 13.

There are two cures, both checked:

send(c, buf, n, MSG_NOSIGNAL);   /* Linux: вместо сигнала −1 и EPIPE */
signal(SIGPIPE, SIG_IGN);        /* один раз в начале программы */

TIME_WAIT. Whoever closed the connection first keeps the port in the TIME_WAIT state for some time, so that late packets of the old connection do not get into a new one. A web server closes the connection first after every response. Restart it, and bind fails with EADDRINUSE. In the check, without SO_REUSEADDR this is exactly what happened, and with it the port was free. The option is set with setsockopt before bind, on every start, as in listener above.


78.6 Web server

HTTP is a text protocol on top of TCP. The browser connects and sends a request line, headers and an empty line; each line ends with the pair \r\n:

GET /on1 HTTP/1.1
Host: stm32
                          ← пустая строка: заголовки кончились

The server replies with a status line, headers, an empty line and a body:

HTTP/1.1 303 See Other
Location: /
Content-Type: text/html; charset=utf-8
Connection: close
                          ← пустая строка, тела у этого ответа нет

Parsing the request and building the response do not depend on where the bytes came from. This code was built both for the computer and with armclang for Cortex-M:

int led[2];                       /* на плате здесь были бы выводы GPIO */

/* По строке запроса «GET /on1 HTTP/1.1» выполняет команду; вернёт код ответа */
int http_route(const char *req)
{
    char method[8], path[32];
    if (sscanf(req, "%7s %31s", method, path) != 2)
        return 400;
    if (strcmp(method, "GET") != 0)
        return 405;
    if (strcmp(path, "/") == 0)
        return 200;
    int on = strncmp(path, "/on", 3) == 0;
    int off = strncmp(path, "/off", 4) == 0;
    const char *num = path + (on ? 3 : 4);
    if ((on || off) && (num[0] == '1' || num[0] == '2') && num[1] == '\0') {
        led[num[0] - '1'] = on;
        return 303;               /* выполнили — и назад на страницу */
    }
    return 404;
}

/* Ответ целиком в out; 0 — не поместился. Конец тела браузер узнаёт
 * по закрытию соединения: заголовок Connection: close. */
size_t http_reply(int code, char *out, size_t cap)
{
    const char *status = code == 200 ? "200 OK"
                       : code == 303 ? "303 See Other"
                       : code == 404 ? "404 Not Found"
                       : code == 405 ? "405 Method Not Allowed"
                       : "400 Bad Request";
    int n = snprintf(out, cap, "HTTP/1.1 %s\r\n%s"
                     "Content-Type: text/html; charset=utf-8\r\n"
                     "Connection: close\r\n"
                     "\r\n",
                     status, code == 303 ? "Location: /\r\n" : "");
    if (code == 200 && n > 0 && (size_t)n < cap)
        n += snprintf(out + n, cap - (size_t)n,
                      "<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
                      "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
                      "<title>STM32</title></head><body><h1>Светодиоды</h1>"
                      "<p>LED 1: %s <a href=\"/on1\"><button>ON</button></a>"
                      " <a href=\"/off1\"><button>OFF</button></a></p>"
                      "<p>LED 2: %s <a href=\"/on2\"><button>ON</button></a>"
                      " <a href=\"/off2\"><button>OFF</button></a></p>"
                      "</body></html>",
                      led[0] ? "ON" : "OFF", led[1] ? "ON" : "OFF");
    return n > 0 && (size_t)n < cap ? (size_t)n : 0;
}

And this is the server on the computer: one connection, one request.

static void serve_one(int ls)
{
    static char req[512], resp[1024];
    int c = accept(ls, NULL, NULL);
    recv_until(c, req, sizeof req, "\r\n\r\n");
    size_t n = http_reply(http_route(req), resp, sizeof resp);
    send(c, resp, n, MSG_NOSIGNAL);
    close(c);
}

In the check, real requests were sent to this server:

Request Response What happened
GET /on1 303, Location: / LED 1 is on, the browser returns to the page
GET /favicon.ico 404 nothing was switched
GET / 200 the page: LED 1: ON, LED 2: OFF, the whole response is 464 bytes
POST /on2 405 the command was not executed
empty request 400 —
a path of 100 characters 404 %31s truncated the path, no overflow

Three decisions in this code need explaining.

⚠️ An extra semicolon in the page. A long page is glued together from strings: the compiler merges adjacent literals "…" "…" into one. One extra ; in the middle, and the rest turns into separate expressions that do nothing:

c static const char page[] = "<p>LED 2</p>" "</p>"; "</html>";

This builds, but </html> does not get into the page. armclang warns even with no options: expression result unused; read the warnings (section 77).

The response buffer on STM32 is not on the stack

On a computer it is common to write in a handler char response[1024] = "HTTP/1.1 200 OK…";. On STM32 this is a disaster:

The constant parts are static const: they stay in Flash, and the stack frame of the function that sends them is zero bytes. The buffer for the changing part is static, like resp in serve_one. The -fstack-usage option is useful in general: a .su file with the frame of each function appears next to the object file. For http_route it is 48 bytes, for the frame receiver from 78.7 it is zero.

Where this works on STM32

The code of http_route and http_reply is the same for any connection method (section 37.1); only where the bytes come from and go to changes:

Connection Sockets Chip
built-in Ethernet + TCP/IP stack under an RTOS the same socket, bind, listen, accept, recv, send F407
a chip with a hardwired stack over SPI in the chip, controlled through registers any
a Wi-Fi module over UART in the module, controlled with AT commands any

78.7 Frames: message boundaries in a byte stream

Both TCP and UART (section 16) give a stream of bytes. To send messages, the stream is split into frames. A reliable scheme is byte stuffing: the byte 7E is the flag at the edges of a frame, and inside the data 7E and 7D are replaced with the pair "7D, the byte XOR 20":

In the data In the frame
7E 7D 5E
7D 7D 5D
others as is

The data 01 7E 02 7D 03 turns into the frame 7E 01 7D 5E 02 7D 5D 03 7E. There is never a flag inside a frame, so a flag is always a boundary: after garbage or a lost byte the receiver finds the start of the next frame by itself.

Byte stuffing: the data, the frame with replacements and receiving a stream with garbage before the first flag

#define FLAG 0x7E
#define ESC  0x7D

/* FLAG, данные с заменами, FLAG; out — не меньше 2 * n + 2 байт */
size_t frame_encode(const uint8_t *in, size_t n, uint8_t *out)
{
    size_t k = 0;
    out[k++] = FLAG;
    for (size_t i = 0; i < n; i++) {
        if (in[i] == FLAG || in[i] == ESC) {
            out[k++] = ESC;
            out[k++] = in[i] ^ 0x20;
        } else {
            out[k++] = in[i];
        }
    }
    out[k++] = FLAG;
    return k;
}

typedef struct {
    uint8_t buf[64];
    size_t len;
    uint8_t inside, esc, overflow;
} deframer_t;

/* Приём по одному байту — как из прерывания UART. Вернёт длину кадра,
 * когда он закончился (данные в d->buf до следующего вызова), иначе 0. */
size_t frame_feed(deframer_t *d, uint8_t b)
{
    if (b == FLAG) {
        size_t n = (d->inside && !d->esc && !d->overflow) ? d->len : 0;
        d->inside = 1;            /* флаг закрывает кадр и открывает следующий */
        d->len = 0;
        d->esc = d->overflow = 0;
        return n;
    }
    if (!d->inside)
        return 0;                 /* мусор до первого флага */
    if (b == ESC) {
        d->esc = 1;
        return 0;
    }
    if (d->esc) {
        b ^= 0x20;
        d->esc = 0;
    }
    if (d->len < sizeof d->buf)
        d->buf[d->len++] = b;
    else
        d->overflow = 1;
    return 0;
}

What was checked:

The last point is an important conclusion: stuffing finds the boundaries but does not check the contents. A checksum, a CRC, is added to the frame (section 43.1, the hardware unit is in section 12.5), and a frame with a wrong checksum is thrown away.


78.8 Pitfalls

The server works on the zero address and breaks on 127.0.0.1. htons instead of htonl for the address.

Messages stick together or arrive in halves. TCP is a stream; boundaries are set by the protocol.

The server spins in a loop after the client leaves. recv returned 0, and the loop does not check for it.

Extra data went out to the network, even passwords. send with the buffer size instead of the data length.

The server silently exits when the client closes a tab. SIGPIPE: MSG_NOSIGNAL or signal(SIGPIPE, SIG_IGN).

After a restart: "Address already in use". TIME_WAIT; SO_REUSEADDR before bind.

The lamp switches "by itself". The page was refreshed with the command address, or the server took the /favicon.ico request for a command. After a command, respond with 303 to the page.

The browser loads the page forever. There is neither Content-Length nor a closed connection.

The end of the page is missing. An extra semicolon among the strings being glued together.

A web server on STM32 behaves unpredictably from the first request. The response buffer on the stack is bigger than the whole stack.

Reconnecting gives an error. A second connect of the same socket gives EISCONN; a new connection needs a new socket.

A program on a computer crashes when it accesses a register. Physical addresses are not available to a process: virtual memory.


78.9 Summary

Next: section 79 "How a device is born: the stages of development", the start of part X.

Open in the interactive handbook