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_INETthe IPv4 address family stream socket SOCK_STREAM, TCPa 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 acceptblocking call a call that waits until it can complete errnothe code of the last error TIME_WAITthe state of a port after a connection is closed SIGPIPEthe "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;
}
⚠️
htonsfor the address. The lineaddr.sin_addr.s_addr = htons(INADDR_ANY);works, becauseINADDR_ANYis zero, and byte order does not matter for zero. Replace it withINADDR_LOOPBACK(127.0.0.1), andhtonscuts the address down to 16 bits: you get0.1.0.0, andbindfails withEADDRNOTAVAIL. The address always takeshtonl.
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:
acceptreturns a new descriptor for each client, and the listening one stays to wait for the next ones. The client's address, if you need it, goes into a separate structure, not into the one that holds the server's address.- The second argument of
listenis not the number of clients but the length of the queue of connections that have already connected but have not yet been taken byaccept. In the check, withlisten(s, 1), three clients were open at the same time. - All calls are blocking:
acceptwaits for a client,recvfor data. One thread serves one client at a time; for several you need threads, processes or a call that waits on many sockets at once (poll). - Print errors from
errno, not with your own text. A message such as "bind error: socket already set" leads you astray: the real reason is almost always a busy port (78.5).
| 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:
- two
sendcalls of 5 bytes each,helloandworld: onerecvgot 10 bytes,helloworld; - 10 bytes read with a 4-byte buffer arrived in parts of 4, 4 and 2.
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).
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
recvreturned 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)afterstrcpy(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. Sendstrlen(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%1023sorfgets(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.
- A 303 response after a command. A button is a link, that is, a GET request.
If you return the page right away in response to
/on1, the address/on1stays in the browser's address bar, and refreshing the page repeats the command. A 303 response sends the browser back to/. The command itself via GET is a simplification of the example: the browser may request a link in advance, a search robot may crawl the page, and someone else's page may insert<img src="http://плата/on1">, and the lamp switches without a click. In a real device changes are sent with a form using the POST method, and a 303 after it is the same technique. /favicon.ico→ 404. The browser requests the page icon by itself. A server that treats any request as a command gets an extra one.- Where the body ends. Either a
Content-Lengthheader with the exact length of the body, orConnection: closeand closing the connection. We use the second, so the length does not have to be computed in advance. Without either, the browser waits for more and keeps spinning the loading indicator.
⚠️ 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.armclangwarns 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:
- according to
armclang -fstack-usagethe frame of such a function is 1032 bytes, while the startup files of the F103 and F407 give the whole stack as 1024 bytes (section 72); - on every call 1024 bytes are copied from Flash to the stack
(
__aeabi_memcpy4).
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.
#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:
- 1000 random frames with frequent
7Eand7Dwere received byte for byte; - the worst case is when all bytes are special: the frame is twice as long as the data plus two flags, 10 bytes turn into 22. The frame buffer is sized for it;
- garbage before the first flag was discarded; a frame that lost its end was delivered as a short frame of 2 bytes, and the next frame was received correctly.
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
- A socket is the descriptor of a connection. Server:
socket→bind→listen→accept, client:socket→connect.acceptgives a new descriptor for each client;listen(s, 1)does not limit their number. - An error is −1 and
errno:ECONNREFUSED,EADDRINUSE,EADDRNOTAVAIL,EISCONN,EPIPE. - The port takes
htons, the addresshtonl; on Cortex-M swapping bytes is one instruction,rev16orrev. - TCP is a stream of bytes: message boundaries are set by the protocol,
recvmay return a part, and on close it returns 0. SIGPIPEterminates the server on a write to a closed connection;SO_REUSEADDRremovesEADDRINUSEon restart.- HTTP is text: a request line, headers, an empty line. Responses 200,
303, 404, 405, 400; the end of the body is
Content-Lengthor closing the connection. - Request parsing and the page do not depend on the transport and move to STM32 as is; the response buffer there is not on the stack.
- A frame with byte stuffing:
7Eat the edges,7D 5Eand7D 5Dinside, worst case 2n + 2; integrity is checked by a CRC. - A process on a computer lives in virtual memory, someone else's address gives
SIGSEGV. STM32 has no MMU; the F407 has an MPU, the F103C8 does not.
Next: section 79 "How a device is born: the stages of development", the start of part X.