文章导航
Redis 源码系列的初衷, 是帮助我们更好地理解 Redis, 更懂 Redis, 而怎么才能懂, 光看是不够的, 建议跟着下面的这一篇, 把环境搭建起来, 后续可以自己阅读源码, 或者跟着我这边一起阅读. 由于我用 c 也是好几年以前了, 些许错误在所难免, 希望读者能不吝指出.
曹工说 Redis 源码(1)-- Redis debug 环境搭建, 使用 clion, 达到和调试 java 一样的效果
曹工说 Redis 源码(2)-- Redis server 启动过程解析及简单 c 语言基础知识补充
曹工说 Redis 源码(3)-- Redis server 启动过程完整解析(中)
本讲主题
早上, 技术群里, 有个同学问了个问题:
这样看来, 还是有部分同学, 对 backlog 这个参数, 不甚了解, 所以, 干脆本讲就讲讲这个话题.
本来可以直接拿 java 来举例, 不过这几天正好在看 Redis, 而且 Redis server 就是服务端, 也是对外提供监听端口的, 而且其用 c 语言编写, 直接调用操作系统的 API, 不像 java 那样封装了一层, 我们直接拿 Redis server 的代码来分析, 就能离真相更近一点.
我会拿一个例子来讲, 例子里的代码, 是直接从 Redis 的源码中拷贝的, 一行没改, 通过这个例子, 我们也能更理解 Redis 一些.
demo 讲解
backlog 参数简单讲解
比如我监听某端口, 那么客户端可以来同该端口, 建立 socket 连接; 正常情况下, 服务端 (bio 模式) 会一直阻塞调用 accept.
大家想过没有, accept 是怎么拿到这个新进来的 socket 的? 其实, 这中间就有个阻塞队列, 当队列没有元素的时候, accept 就会阻塞在这个队列的 take 操作中, 所以, 我个人感觉, accept 操作, 其实和队列的从队尾或队头取一个元素, 是一样的.
当新客户端建立连接时, 完成了三次握手后, 就会被放到这个队列中, 这个队列, 我们一般叫做: 全连接队列.
而这个队列的最大容量, 或者说 size, 就是 backlog 这个整数的大小.
正常情况下, 只要服务端程序, accept 不要卡壳, 这个 backlog 队列多大多小都无所谓; 如果设置大一点, 就能在服务端 accept 速度比较慢的时候, 起到削峰的作用, 怎么感觉和 mq 有点像, 哈哈.
说完了, 下面开始测试了, 首先测试程序正常 accept 的情况.
main 测试程序
- int main() {
- // 1
- char *pVoid = malloc(10);
- // 2
- int serverSocket = anetTcpServer(pVoid, 6380, NULL, 2);
- printf("listening...");
- while (1) {
- int fd;
- struct sockaddr_storage sa;
- socklen_t salen = sizeof(sa);
- // 3
- char* err = malloc(20);
- // 4
- if ((fd = anetGenericAccept(err, serverSocket, (struct sockaddr*)&sa, &salen)) == -1)
- return ANET_ERR;
- printf("accept...%d",fd);
- }
- }
1 处, 我们先分配了一个 10 字节的内存, 这个主要是存放错误信息, 在 c 语言编程中, 不能像高级语言一样抛异常, 所以, 返回值一般用来返回 0/1, 表示函数调用的成功失败; 如果需要在函数内部修改什么东西, 一般就会先 new 一个内存出来, 然后把指针传进去, 然后在里面就对这片内存空间进行操作, 这里也是一样.
anetTcpServer 是我们自定义的, 内部会实现如下逻辑: 在本机的 6380 端口上进行监听, backlog 参数即全连接队列的 size, 设为 2. 如果出错的话, 就会把错误信息, 写入 1 处的那个内存中.
这一步调用完成后, 端口就起好了.
3 处, 同样分配了一点内存, 供 accept 连接出错时使用, 和 1 处作用类似
4 处, 调用 accept 去从队列取连接
anetTcpServer, 监听端口
- int anetTcpServer(char *err, int port, char *bindaddr, int backlog) {
- return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
- }
- static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog) {
- int s, rv;
- char _port[6]; /* strlen("65535") */
- struct addrinfo hints, *servinfo, *p;
- snprintf(_port, 6, "%d", port);
- // 1
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = af;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_flags = AI_PASSIVE; /* No effect if bindaddr != NULL */
- // 2
- if ((rv = getaddrinfo(bindaddr, _port, &hints, &servinfo)) != 0) {
- anetSetError(err, "%s", gai_strerror(rv));
- return ANET_ERR;
- }
- for (p = servinfo; p != NULL; p = p->ai_next) {
- // 3
- if ((s = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
- continue;
- // 4
- if (anetSetReuseAddr(err, s) == ANET_ERR) goto error;
- // 5
- if (anetListen(err, s, p->ai_addr, p->ai_addrlen, backlog) == ANET_ERR) goto error;
- goto end;
- }
- error:
- s = ANET_ERR;
- end:
- freeaddrinfo(servinfo);
- return s;
- }
1 处, new 一个结构体, c 语言中, new 一个对象比较麻烦, 要先定义一个结构体类型的变量, 如 struct addrinfo hints,, 然后调用 memset 来初始化内存, 然后设置各个属性. 总体来说, 这里就是 new 了一个 ipv4 的地址
2 处, 因为一般服务器都有多网卡, 多个 ip 地址, 还有环回网卡之类的, 这里的 getaddrinfo, 是利用我们第一步的 hints, 去帮助我们筛选出一个最终的网卡地址出来, 然后赋值给 servinfo 变量.
这里可能有不准确的地方, 大家可以直接看官方文档:
- int getaddrinfo(const char *node, const char *service,
- const struct addrinfo *hints,
- struct addrinfo **res);
- Given node and service, which identify an Internet host and a service, getaddrinfo() returns one or more addrinfo structures, each of which contains an Internet address that can be specified in a call to bind(2) or connect(2).
3 处, 使用第二步拿到的地址, new 一个 socket
4 处, anetSetReuseAddr, 设置 SO_REUSEADDR 选项, 我简单查了下, 可参考:
[socket 常见选项之 SO_REUSEADDR,SO_REUSEPORT]
SO_REUSEADDR
一般来说, 一个端口释放后会等待两分钟之后才能再被使用, SO_REUSEADDR 是让端口释放后立即就可以被再次使用
5 处, 调用 listen 进行监听, 这里用到了我们传入的 backlog 参数.
其中, backlog 参数的官方说明, 如下, 意思也就是说, 是队列的 size:
其中, anetListen 是我们自定义的, 我们接着看:
- /*
- * 绑定并创建监听套接字
- */
- static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len, int backlog) {
- // 1
- if (bind(s, sa, len) == -1) {
- anetSetError(err, "bind: %s", strerror(errno));
- close(s);
- return ANET_ERR;
- }
- // 2
- if (listen(s, backlog) == -1) {
- anetSetError(err, "listen: %s", strerror(errno));
- close(s);
- return ANET_ERR;
- }
- return ANET_OK;
- }
1 处, 这里进行绑定
2 处, 这里调用操作系统的函数, 进行监听, 其中, 第一个参数就是前面的 socket file descriptor, 第二个, 就是 backlog.
如何运行
代码地址:
大家把上面这两个文件, 自己放到一个 Linux 操作系统的文件夹下, 然后执行以下命令, 就能把这个 demo 启动起来:
测试
查看监听端口是否启动
- [root@mini2 ~]# netstat -ano|grep 6380
- tcp 0 0 0.0.0.0:6380 0.0.0.0:* LISTEN off (0.00/0/0)
开启一个 shell, 连接到 6380 端口
我这边开了 3 个 shell, 去连接 6380 端口, 然后, 我执行:
- [root@mini2 ~]# netstat -ano|grep 6380
- tcp 0 0 0.0.0.0:6380 0.0.0.0:* LISTEN off (0.00/0/0)
- tcp 0 0 127.0.0.1:51386 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
- tcp 0 0 127.0.0.1:54442 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
- tcp 0 0 127.0.0.1:51930 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
- tcp 0 0 127.0.0.1:6380 127.0.0.1:51386 ESTABLISHED off (0.00/0/0)
- tcp 0 0 127.0.0.1:6380 127.0.0.1:54442 ESTABLISHED off (0.00/0/0)
- tcp 0 0 127.0.0.1:6380 127.0.0.1:51930 ESTABLISHED off (0.00/0/0)
可以看到, 已经有 3 个 socket, 连接到 6380 端口了.
查看端口对应的 backlog 队列的相关东西
怎么看 backlog 那些呢? 有个命令叫 ss, 其是 netstat 的升级版, 执行以下命令如下:
- [root@mini2 ~]# ss -l |grep 6380
- tcp LISTEN 0 2 *:6380 *:*
上面我们查询了 6380 这个监听端口的状态, 其中,
第一列, tcp, 传输协议的名称
第二列, 状态, LISTEN
第三列, 查阅 man netstat 可以看到,
- Recv-Q
- Established: The count of bytes not copied by the user program connected to this socket.
- Listening: Since Kernel 2.6.18 this column contains the current syn backlog.
当其为 Established 状态时, 应该是缓冲区中没被拷贝到用户程序的字节的数量;
当其为 LISTEN 状态时, 表示当前 backlog 这个队列, 即前面说的全连接队列的, 容量的大小; 这里, 因为我们的程序一直在 accept 连接, 所以这里为 0
第 4 列, 官方文档:
- Send-Q
- Established: The count of bytes not acknowledged by the remote host.
- Listening: Since Kernel 2.6.18 this column contains the maximum size of the syn backlog.
当其为 Established 时, 表示我方缓冲区中还没有被对方 ack 的字节数量
当其为 Listen 时, 表示全连接队列的最大容量, 我们是设为 2 的, 所以这里是 2.
测试 2
当我们程序不去 accept 的时候, 会怎么样呢, 修改程序如下:
- int main() {
- char *pVoid = malloc(10);
- int serverSocket = anetTcpServer(pVoid, 6380, NULL, 2);
- printf("listening...");
- while (1){
- sleep(100000);
- }
- }
然后我们再去开启 3 个客户端连接, 然后, 最后看 ss 命令的情况:
- [root@mini2 ~]# ss -l |grep 6380
- tcp LISTEN 3 2 *:6380 *:*
再执行 netstat 看看:
- [root@mini2 ~]# netstat -ano|grep 6380
- tcp 0 0 127.0.0.1:50238 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
- tcp 0 0 127.0.0.1:50362 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
发现了吗, 只有 2 个连接是 ok 的. 因为我们的全连接队列, 最大为 2, 现在已经 full 了啊, 所以新连接进不来了.
总结
大家可以跟着我的 demo 试一下, 相信理解会更深刻一点.
以前我也写了一篇, 大家可以参考下.
Linux 中, Tomcat 怎么承载高并发(深入 Tcp 参数 backlog)
来源: https://www.cnblogs.com/grey-wolf/p/12694767.html