#include <sys/types.h>
#include <sys/socket.h>
int listen(int fd, int backlog);
listen在服务器端的作用就是将服务器端的套接字文件描述符转换成被动监听的描述符,何为被动监听?
在服务端,我们的端口和ip肯定是众所周知的,我们需要调用listen来将服务端socket套接字描述符设置为被动监听外界的连接才可以实现服务端的业务.相当于让服务端处于”守株待兔”的状态,客户端是即将撞树的”兔子”.
在讨论backlog之前,我们再来重述一遍TCP三次握手的过程及状态转换,因为他真的很重要!!!
-
服务器运行开启被动监听,客户端要与服务端建立新连接的时,先发送一个syn包,进入到
syn_sent状态
(表示syn已经发了,sent为send的过去式嘛~~); -
然后服务器收到syn包后,进入到
syn_rcvd状态
,并回复对端一个syn包和一个ack包,; -
客户端收到请求数据包后,进入到
established状态
,三次握手完成;
对于客户端这只主动”撞到树上”的”兔子”,服务器是怎样有序处理的呢?
在UNP上有讲在以前tcp怎样处理新连接请求的,我在文章中只描述现在tcp服务端处理连接请求的方式!
现在,服务器在listen时会维护两个队列,一个是处于established状态的队列即
完成三次握手的队列
,另一个是服务端收到客户端syn包后的队列,也就是以上讲的服务端进入
syn_rcvd状态
的队列,这两个队列中存的是什么?以及两个队列又有什么联系呢?
这两个队列中存的就是每个客户端的IP和端口相关信息!
说到两个队列之间联系,那么
backlog的故事
根据UNP上所述,backlog
在之前定义的是处于listen的syn_rcvd状态和established状态下的两个用户连接队列长度之和的最大值(
这句话是多余的可以选择忽略
)
,现在表示的是完成三次握手
即处于established状态
的用户连接队列的长度,就通过listen第二个参数指定.
以下是示意图:
ubuntu18.10中上SYN_RCVD状态的队列的最大长度查看
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
ESTABLISHED状态的最大长度查看 cat /proc/sys/net/core/somaxconn
当backlog值设置的比系统中定义的值大时,就使用系统定义的值.当然我们也可以修改系统中定义的值.
当我将backlog值将以下服务器backlog值设置为5的时候:
//相关头文件省略
//创建服务器端socket的类
#pragma once
class sockfd{
private :
static sockfd* sock ;
public :
struct sockaddr_in serv ;
int fd ;
sockfd() ;
~sockfd() ;
static sockfd* getsockfd( char* ip, char* port ) ;
//设置非阻塞
void s_bind() ;
void s_listen() ;
};
sockfd* sockfd:: sock = NULL ;
sockfd :: sockfd()
{
sock = NULL ;
}
sockfd* sockfd :: getsockfd( char* ip, char* port )
{
if(sock == NULL )
{
sock = new sockfd() ;
sock->fd = socket( AF_INET, SOCK_STREAM, 0) ;
int flag = 1 ;
int ret = setsockopt( sock->fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) ;
if( ret < 0 )
{
free(sock) ;
cout << "build socketopt error!" << endl ;
exit( 1 ) ;
}
cout << sock->fd <<endl ;
if( sock->fd < 0 )
{
free(sock) ;
cout << "build socket error!" << endl ;
exit( 1 ) ;
}
( sock->serv ).sin_family = AF_INET ;
( sock->serv ).sin_port = htons( atoi(port) ) ;
( sock->serv ).sin_addr.s_addr = inet_addr( ip );
memset(sock->serv.sin_zero,'\0',8);
}
return sock ;
}
void sockfd :: s_bind()
{
int ret = bind( sock->fd, ( struct sockaddr* )&serv, sizeof( serv ) ) ;
if( ret < 0 )
{
free( sock ) ;
cout << "bind socket error!" << endl ;
exit( 1 ) ;
}
cout << "binded......" << endl ;
}
void sockfd :: s_listen(){
//设置backlog为5
int ret = listen( sock->fd , 5) ;
if( ret < 0 )
{
free(sock) ;
cout << "listen socket error!" << endl ;
exit( 1 ) ;
}
cout << "listen......" << endl ;
}
sockfd :: ~sockfd()
{
close( fd ) ;
free( sock ) ;
sock = NULL ;
}
//
//服务器程序
typedef void (*sighandler_t)(int) ;
sockfd* sock = NULL ;
void sig_handle( int signo )
{
if( signo == SIGINT )
{
delete sock ;
exit(0) ;
}
}
int main(int argc,char** argv)
{
if( argc!=3 )
{
cout <<"NAME:" << argv[0] << "IP:"<<argv[1]<<"PORT:"<< argv[2] <<endl ;
exit(1);
}
sock = sockfd::getsockfd(argv[1], argv[2]) ;
sock->s_bind() ;
sock->s_listen() ;
int connfd ;
if((connfd = accept(sock->fd, NULL, NULL)) < 0)
{
cout << connfd << "--------------" << endl ;
cout << "accept error!" << endl ;
exit(1) ;
}
char buf[20] = "hello client!" ;
send(connfd, buf, 20, 0) ;
//接收信号,创建子进程重启服务器
while(1)
{
signal( SIGINT, sig_handle ) ;
}
}
//执行服务器程序
./server 127.0.0.1 2345
开终端并执行9次
telnet 127.0.0.1 2345
//检查端口2345的tcp网络连接情况
netstat -t|grep 2345
效果如下:
可见当backlog为5时,处于established状态的已完成连接队列长度为7,
参考UNP,源自Berkerly的实现给backlog值设置一个模糊因子,把它乘1.5后为未被处理队列的最大长度
,所以当backlog为5时,未被处理队列的长度最大不超过8.下面是我将backlog值设置为10时,开启15个连接,有12个处于已完成队列中,三个处于未完成队列中,我的ubuntu18.10已连接对列长度为12,如图:
所以可以说该队列的长度总是大于backlog真实值,小于backlog乘上1.5的值.
至于其他多余的客户端连接在服务端还处于syn_rcvd状态的未完成连接队列中,改队列在系统中的长度为256,可以使用前面所述的方法查看自己系统的默认未完成队列最大长度.(
在图中显示的是客户端连接处于syn_sent状态,服务端处于syn_rcvd状态
).
那么思考一个问题…
要是syn_rcvd和established状态下listen维护的两个队列满了的话,服务器该如何处理过来的请求呢?
参考UNP上所述,TCP选择忽略该分节,客户端在syn_rcvd队列没满之前一直会发送syn请求尝试连接服务器.UNP上描述说服务器只是简单忽略,也不会发送RST分节的原因是,要是服务器发送RST分节回复该响应的话,会导致客户端不能确定所连接端口是没有服务还syn_rcvd队列已满!想了想这样挺合理!!!
参考博客: