04. 协议解析
# 04. 协议解析
咱们的服务器得能处理来自客户端的多个请求,要实现这一点,就得搞个“协议”出来,至少得把TCP字节流里的各个请求给区分开。把请求区分开最简单的办法,就是在请求开头声明请求的长度。咱们就用下面这种方案。
+-----+------+-----+------+--------
| len | msg1 | len | msg2 | more...
+-----+------+-----+------+--------
2
3
这个协议由两部分组成:一个4字节的小端序(little - endian)整数,用来表示后面请求的长度,还有一个长度可变的请求内容。
从咱们上一章的代码开始,服务器的循环部分要修改一下,来处理多个请求:
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; // 出错了,跳过这次循环
}
// 一次只处理一个客户端连接
while (true) {
int32_t err = one_request(connfd);
if (err) {
break;
}
}
close(connfd);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
one_request
函数只负责解析一个请求并回复,直到出问题或者客户端连接断开。在后面章节引入事件循环(event loop)之前,咱们的服务器一次只能处理一个连接。
在列出one_request
函数之前,先添加两个辅助函数:
static int32_t read_full(int fd, char *buf, size_t n) {
while (n > 0) {
ssize_t rv = read(fd, buf, n);
if ( rv <= 0) {
return -1; // 出错了,或者遇到意外的文件结束符(EOF)
}
assert((size_t)rv <= n);
n -= (size_t)rv;
buf += rv;
}
return 0;
}
static int32_t write_all(int fd, const char *buf, size_t n) {
while (n > 0) {
ssize_t rv = write(fd, buf, n);
if ( rv <= 0) {
return -1; // 出错了
}
assert((size_t)rv <= n);
n -= (size_t)rv;
buf += rv;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这里有两点要注意:
read()
系统调用只会返回内核里可用的数据,如果没有数据就会阻塞。处理数据不足的情况得靠应用程序。read_full()
函数会一直从内核读取数据,直到读到恰好n
字节。- 同样,
write()
系统调用如果遇到内核缓冲区满了,可能只写入部分数据就返回成功。当write()
返回的字节数比我们期望的少时,就得继续尝试写入。
one_request
函数才是真正干活的:
const size_t k_max_msg = 4096;
static int32_t one_request(int connfd) {
// 4字节的头部
char rbuf[4 + k_max_msg + 1];
errno = 0;
int32_t err = read_full(connfd, rbuf, 4);
if (err) {
if (errno == 0) {
msg("EOF");
} else {
msg("read() error");
}
return err;
}
uint32_t len = 0;
memcpy(&len, rbuf, 4); // 假设是小端序
if (len > k_max_msg) {
msg("too long");
return -1;
}
// 请求体
err = read_full(connfd, &rbuf[4], len);
if (err) {
msg("read() error");
return err;
}
// 做点什么
rbuf[4 + len] = '\0';
printf("client says: %s\n", &rbuf[4]);
// 用同样的协议回复
const char reply[] = "world";
char wbuf[4 + sizeof(reply)];
len = (uint32_t)strlen(reply);
memcpy(wbuf, &len, 4);
memcpy(&wbuf[4], reply, len);
return write_all(connfd, wbuf, 4 + len);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
为了方便,我们给最大请求大小设了个限制,还弄了个足够大的缓冲区来存放请求。以前解析协议的时候,字节序(Endianness)是个需要考虑的问题,不过现在没那么重要啦,所以我们直接用memcpy
来处理整数。
客户端发送请求和接收回复的代码:
static int32_t query(int fd, const char *text) {
uint32_t len = (uint32_t)strlen(text);
if (len > k_max_msg) {
return -1;
}
char wbuf[4 + k_max_msg];
memcpy(wbuf, &len, 4); // 假设是小端序
memcpy(&wbuf[4], text, len);
if (int32_t err = write_all(fd, wbuf, 4 + len)) {
return err;
}
// 4字节的头部
char rbuf[4 + k_max_msg + 1];
errno = 0;
int32_t err = read_full(fd, rbuf, 4);
if (err) {
if (errno == 0) {
msg("EOF");
} else {
msg("read() error");
}
return err;
}
memcpy(&len, rbuf, 4); // 假设是小端序
if (len > k_max_msg) {
msg("too long");
return -1;
}
// 回复体
err = read_full(fd, &rbuf[4], len);
if (err) {
msg("read() error");
return err;
}
// 做点什么
rbuf[4 + len] = '\0';
printf("server says: %s\n", &rbuf[4]);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
用多个命令来测试咱们的服务器:
int main() {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
die("socket()");
}
// 省略部分代码...
// 多个请求
int32_t err = query(fd, "hello1");
if (err) {
goto L_DONE;
}
err = query(fd, "hello2");
if (err) {
goto L_DONE;
}
err = query(fd, "hello3");
if (err) {
goto L_DONE;
}
L_DONE:
close(fd);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
运行服务器和客户端:
$ ./server
client says: hello1
client says: hello2
client says: hello3
EOF
2
3
4
5
$ ./client
server says: world
server says: world
server says: world
2
3
4
解析协议的代码每个请求至少得用两次read()
系统调用。要是用“缓冲I/O(Buffered IO)”的话,系统调用的次数能减少。也就是说:一次性把尽可能多的数据读到缓冲区里,然后试着从这个缓冲区里解析出多个请求。强烈建议大家把这个当作练习题试试,因为这对理解后面的章节可能会有帮助。
关于协议的一些说明:本章用的协议是最简单实用的那种。现实里大多数协议可比这复杂多了。有些协议用文本而不是二进制数据。虽然文本协议有可读性强的优点,但比起二进制协议,文本协议需要更多的解析操作,写代码的时候更麻烦,还容易出错。让协议解析更复杂的是,有些协议没办法直接把消息区分开,这些协议可能会用分隔符,或者需要进一步解析才能拆分消息。要是协议里携带任意数据,使用分隔符就又多了个麻烦事儿,因为数据里的分隔符得进行“转义”处理。后面的章节咱们还是用这个简单的二进制协议。
04_client.cpp
04_server.cpp