Redis源码系列的初衷,是帮助咱们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续能够本身阅读源码,或者跟着我这边一块儿阅读。因为我用c也是好几年之前了,些许错误在所不免,但愿读者能不吝指出。html
曹工说Redis源码(1)-- redis debug环境搭建,使用clion,达到和调试java同样的效果java
曹工说Redis源码(2)-- redis server 启动过程解析及简单c语言基础知识补充node
曹工说Redis源码(3)-- redis server 启动过程完整解析(中)linux
早上,技术群里,有个同窗问了个问题:git
这样看来,仍是有部分同窗,对backlog这个参数,不甚了解,因此,干脆本讲就讲讲这个话题。redis
原本能够直接拿java来举例,不过这几天正好在看redis,并且 redis server就是服务端,也是对外提供监听端口的,并且其用 c 语言编写,直接调用操做系统的api,不像java那样封装了一层,咱们直接拿redis server的代码来分析,就能离真相更近一点。shell
我会拿一个例子来说,例子里的代码,是直接从redis的源码中拷贝的,一行没改,经过这个例子,咱们也能更理解redis一些。编程
好比我监听某端口,那么客户端能够来同该端口,创建socket链接;正常状况下,服务端(bio模式)会一直阻塞调用accept。api
你们想过没有,accept是怎么拿到这个新进来的socket的?其实,这中间就有个阻塞队列,当队列没有元素的时候,accept就会阻塞在这个队列的take操做中,因此,我我的感受,accept操做,其实和队列的从队尾或队头取一个元素,是同样的。服务器
当新客户端创建链接时,完成了三次握手后,就会被放到这个队列中,这个队列,咱们通常叫作:全链接队列。
而这个队列的最大容量,或者说size,就是backlog这个整数的大小。
正常状况下,只要服务端程序,accept不要卡壳,这个backlog队列多大多小都无所谓;若是设置大一点,就能在服务端accept速度比较慢的时候,起到削峰的做用,怎么感受和mq有点像,哈哈。
说完了,下面开始测试了,首先测试程序正常accept的状况。
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去从队列取链接
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; }
代码地址:
你们把上面这两个文件,本身放到一个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)
我这边开了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那些呢?有个命令叫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。
当咱们程序不去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)
下面这篇文章,也不错:
使用Netty,咱们到底在开发些什么?