文章导航

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测试程序


  1. int main() {
  2. // 1
  3. char *pVoid = malloc(10);
  4. // 2
  5. int serverSocket = anetTcpServer(pVoid, 6380, NULL, 2);
  6. printf("listening...");
  7. while (1) {
  8. int fd;
  9. struct sockaddr_storage sa;
  10. socklen_t salen = sizeof(sa);
  11. // 3
  12. char* err = malloc(20);
  13. // 4
  14. if ((fd = anetGenericAccept(err, serverSocket, (struct sockaddr*)&sa, &salen)) == -1)
  15. return ANET_ERR;
  16. printf("accept...%d",fd);
  17. }
  18. }
  • 1处,我们先分配了一个10字节的内存,这个主要是存放错误信息,在c语言编程中,不能像高级语言一样抛异常,所以,返回值一般用来返回0/1,表示函数调用的成功失败;如果需要在函数内部修改什么东西,一般就会先new一个内存出来,然后把指针传进去,然后在里面就对这片内存空间进行操作,这里也是一样。

  • anetTcpServer 是我们自定义的,内部会实现如下逻辑:在本机的6380端口上进行监听,backlog参数即全连接队列的size,设为2。如果出错的话,就会把错误信息,写入1处的那个内存中。

    这一步调用完成后,端口就起好了。

  • 3处,同样分配了一点内存,供accept连接出错时使用,和1处作用类似

  • 4处,调用accept去从队列取连接

anetTcpServer,监听端口

  1. int anetTcpServer(char *err, int port, char *bindaddr, int backlog) {
  2. return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
  3. }
  4. static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog) {
  5. int s, rv;
  6. char _port[6]; /* strlen("65535") */
  7. struct addrinfo hints, *servinfo, *p;
  8. snprintf(_port, 6, "%d", port);
  9. // 1
  10. memset(&hints, 0, sizeof(hints));
  11. hints.ai_family = af;
  12. hints.ai_socktype = SOCK_STREAM;
  13. hints.ai_flags = AI_PASSIVE; /* No effect if bindaddr != NULL */
  14. // 2
  15. if ((rv = getaddrinfo(bindaddr, _port, &hints, &servinfo)) != 0) {
  16. anetSetError(err, "%s", gai_strerror(rv));
  17. return ANET_ERR;
  18. }
  19. for (p = servinfo; p != NULL; p = p->ai_next) {
  20. // 3
  21. if ((s = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
  22. continue;
  23. // 4
  24. if (anetSetReuseAddr(err, s) == ANET_ERR) goto error;
  25. // 5
  26. if (anetListen(err, s, p->ai_addr, p->ai_addrlen, backlog) == ANET_ERR) goto error;
  27. goto end;
  28. }
  29. error:
  30. s = ANET_ERR;
  31. end:
  32. freeaddrinfo(servinfo);
  33. return s;
  34. }
  • 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是我们自定义的,我们接着看:

  1. /*
  2. * 绑定并创建监听套接字
  3. */
  4. static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len, int backlog) {
  5. // 1
  6. if (bind(s, sa, len) == -1) {
  7. anetSetError(err, "bind: %s", strerror(errno));
  8. close(s);
  9. return ANET_ERR;
  10. }
  11. // 2
  12. if (listen(s, backlog) == -1) {
  13. anetSetError(err, "listen: %s", strerror(errno));
  14. close(s);
  15. return ANET_ERR;
  16. }
  17. return ANET_OK;
  18. }
  • 1处,这里进行绑定
  • 2处,这里调用操作系统的函数,进行监听,其中,第一个参数就是前面的socket file descriptor,第二个,就是backlog。

如何运行

代码地址:

https://gitee.com/ckl111/redis-3.0-annotated-cmake-in-clion/blob/master/our-redis-implementation/my_anet.c

https://gitee.com/ckl111/redis-3.0-annotated-cmake-in-clion/blob/master/our-redis-implementation/my_anet.h

大家把上面这两个文件,自己放到一个linux操作系统的文件夹下,然后执行以下命令,就能把这个demo启动起来:

测试

查看监听端口是否启动

  1. [root@mini2 ~]# netstat -ano|grep 6380
  2. tcp 0 0 0.0.0.0:6380 0.0.0.0:* LISTEN off (0.00/0/0)

开启一个shell,连接到6380端口

我这边开了3个shell,去连接6380端口,然后,我执行:

  1. [root@mini2 ~]# netstat -ano|grep 6380
  2. tcp 0 0 0.0.0.0:6380 0.0.0.0:* LISTEN off (0.00/0/0)
  3. tcp 0 0 127.0.0.1:51386 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
  4. tcp 0 0 127.0.0.1:54442 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
  5. tcp 0 0 127.0.0.1:51930 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
  6. tcp 0 0 127.0.0.1:6380 127.0.0.1:51386 ESTABLISHED off (0.00/0/0)
  7. tcp 0 0 127.0.0.1:6380 127.0.0.1:54442 ESTABLISHED off (0.00/0/0)
  8. 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的升级版,执行以下命令如下:

  1. [root@mini2 ~]# ss -l |grep 6380
  2. tcp LISTEN 0 2 *:6380 *:*

上面我们查询了6380这个监听端口的状态,其中,

  • 第一列,tcp,传输协议的名称

  • 第二列,状态,LISTEN

  • 第三列,查阅man netstat可以看到,

    1. Recv-Q
    2. Established: The count of bytes not copied by the user program connected to this socket.
    3. Listening: Since Kernel 2.6.18 this column contains the current syn backlog.

    当其为Established状态时,应该是缓冲区中没被拷贝到用户程序的字节的数量;

    当其为LISTEN状态时,表示当前backlog这个队列,即前面说的全连接队列的,容量的大小;这里,因为我们的程序一直在accept连接,所以这里为0

  • 第4列,官方文档:

    1. Send-Q
    2. Established: The count of bytes not acknowledged by the remote host.
    3. Listening: Since Kernel 2.6.18 this column contains the maximum size of the syn backlog.

    当其为Established时,表示我方缓冲区中还没有被对方ack的字节数量

    当其为Listen时,表示全连接队列的最大容量,我们是设为2的,所以这里是2。

测试2

当我们程序不去accept的时候,会怎么样呢,修改程序如下:

  1. int main() {
  2. char *pVoid = malloc(10);
  3. int serverSocket = anetTcpServer(pVoid, 6380, NULL, 2);
  4. printf("listening...");
  5. while (1){
  6. sleep(100000);
  7. }
  8. }

然后我们再去开启3个客户端连接,然后,最后看ss命令的情况:

  1. [root@mini2 ~]# ss -l |grep 6380
  2. tcp LISTEN 3 2 *:6380 *:*

再执行netstat看看:

  1. [root@mini2 ~]# netstat -ano|grep 6380
  2. tcp 0 0 127.0.0.1:50238 127.0.0.1:6380 ESTABLISHED off (0.00/0/0)
  3. 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)

下面这篇文章,也不错:

使用Netty,我们到底在开发些什么?

曹工说Redis源码(4)-- 通过redis server源码来理解 listen 函数中的 backlog 参数的更多相关文章

  1. 曹工说mini-dubbo(2)--分析eureka client源码,想办法把我们的服务提供者注册到eureka server(上)

    前言 eureka是spring cloud Netflix技术体系中的重要组件,主要完成服务注册和发现的功能:那现在有个问题,我们自己写的rpc服务,如果为了保证足够的开放性和功能完善性,那肯定要支 ...

  2. 曹工杂谈:为什么很少需要改Spring源码,因为扩展点太多了,说说Spring的后置处理器

    前言 最近发了好几篇,都是覆盖框架源码,但是spring的代码,我是从没覆盖过,毕竟,如果方便扩展,没谁想去改源码,而spring就是不需要改源码的那个,真的是"对扩展开放,对修改关闭&qu ...

  3. 曹工说Redis源码(5)-- redis server 启动过程解析,以及EventLoop每次处理事件前的前置工作解析(下)

    曹工说Redis源码(5)-- redis server 启动过程解析,eventLoop处理事件前的准备工作(下) 文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis ...

  4. 曹工说Redis源码(6)-- redis server 主循环大体流程解析

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

  5. 曹工说Redis源码(7)-- redis server 的周期执行任务,到底要做些啥

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

  6. 曹工说Redis源码(8)--面试时,redis 内存淘汰总被问,但是总答不好

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

  7. 曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 深入理解 Node.js 中 EventEmitter源码分析(3.0.0版本)

    events模块对外提供了一个 EventEmitter 对象,即:events.EventEmitter. EventEmitter 是NodeJS的核心模块events中的类,用于对NodeJS中 ...

  9. 曹工说Redis源码(2)-- redis server 启动过程解析及简单c语言基础知识补充

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

随机推荐

  1. qt creator源码全方面分析(3-5)

    目录 qtcreatorlibrary.pri 使用实例 上半部 下半部 结果 qtcreatorlibrary.pri 上一章节,我们介绍了src.pro,这里乘此机会,把src目录下的所有项目文件 ...

  2. Mol Cell Proteomics. | 用于鉴定新型融合转录本及其在癌细胞中的潜在翻译产物的多功能蛋白质组基因组学工具FusionPro

    期刊:Molecular & Cellular Proteomics 发表时间:June 17, 2019 DOI:10.1074/mcp.RA119.001456 分享人:任哲 内容与观点: ...

  3. Magento2-2.3.4 win10安装完magento无法加载静态资源导致无法进入后台登录页面

    后台面无法进入,截图如下

  4. The instance of entity type 'manager' cannot be tracked because another instance with the same key value for {'id'} is already being tracked. When attaching existing entities, ensure that only one ent

    最近在用ASP.NET CORE时遇到一些问题,现记录下: 出现上述错误,即在更新实体数据时出现的错误 services.AddDbContext<StoreContext>(c => ...

  5. 图解Java设计模式之模板模式

    图解Java设计模式之模板模式 豆浆制作问题 模板方法模式基本介绍 模板方法模式原理类图 模板方法模式解决豆浆制作问题 模板方法模式的钩子方法 模板方法模式在Spring框架中的源码分析 模板方法模式 ...

  6. JAVA开发中如何优化类的设计

    具体类依赖于抽象类,而非抽象类依赖于具体类.这样做有利于一个抽象类扩展多个具体类. 开放封闭原则:对扩展开放,对修改封闭. 1.永远保持数据私有 保持数据的私有是设计类时,必须重点考虑的问题.保持私有 ...

  7. python基本数据类型及其使用方法

    前言 ​ python中的数据类型主要为int.float.string.list.tuple.dict.set.bool.bytes.接下来int和float统称为数字类型. 1.数据类型总结 按存 ...

  8. 设计模式 - 观察者模式 (C++实现)

    #include <iostream> #include <list> #include <string> using namespace std; class I ...

  9. centos 安装activeMq

    Apache ActiveMQ是一个免费的开源消息代理和集成模式服务器.它支持来自JAVA.c++.C.Python.Perl.PHP等多种语言的客户端和协议.它提供了许多功能,如消息组.虚拟目的地. ...

  10. 从 ASP.NET Core 3.1 迁移到 5.0

    3月中旬,微软官方已经发布了dotnet 5的第一个预览版:5.0.0-preview.1. dotnet core经过前几个版本的发展和沉淀,到3.1已经基本趋于稳定. 所以从.net core 3 ...