• 网络编程/tcp协议分析


    一、链接的建立

     TCP Server和Client模型:

    1、bind函数相关

    1 #include <sys/types.h>          /* See NOTES */
    2 #include <sys/socket.h>
    3 
    4 int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

    作用是绑定addr中的<IP:PORT>, 对于tcp server一般port是固定的。当系统有多个IP(多网卡)时,把ip设置为INADDR_ANY, 内核会自动分配ip。如果只有单一的IP,选择

    机器IP和INADDR_ANY效果是一样的。

    客户和服务器通过调用函数bind时可以指定IP地址或端口号,可以都指定,也可以都不指定:


    l 客户端
    1. TCP客户端:

    1) 当TCP客户未绑定IP地址,当它调用connect时内核会根据外出接口给它绑定一个IP地址和一个临时端口号。并且TCP服务器在接到这个连接后会以这个IP地址作为回应数据报的目的IP地址。
    
    2) 当TCP客户绑定了IP地址,它就为发出的数据连接指定了一个源IP地址,并且TCP服务器在接到这个连接后会以这个IP地址作为回应数据报的目的IP地址。
    
    3) TCP客户只能根据四元组(原端口号,原IP地址,目的端口号,目的IP地址)接受数据报。

    2. UDP客户端:

    1) 当UDP客户未绑定IP地址,当它调用sendto时内核会根据外出接口给它绑定一个IP地址和一个临时端口号。(UDP客户可以接收到达它绑定的临时端口的任何UDP数据报)。
    
    2) 当UDP客户绑定了IP地址,它就为发出的数据报指定了一个源IP地址,并且UDP服务器在接到这个数据报后会以这个IP地址作为回应数据报的目的IP地址。(UDP客户只能接收到达它绑定的临时端口并且目的地址为它绑定的IP地址的UDP数据报)。
    
    3) 当UDP客户调用connect,内核记录下对方的IP地址和端口号,它们包含在传递给connect的套接口地址结构中,并为UDP客户绑定了一个临时端口号和IP地址。(UDP客户只能接收目的IP地址为它绑定的IP地址和端口号并且源IP地址为它指定对方的IP地址和端口号的数据报)。
    关于UDP中客户端调用connect的好处,还有一个作用是能够捕获错误
    当UPD客户端像服务器发送一个不存在的端口请求,服务器会像客户端发送ICMP端口不可达消息。不调用connect时,对于返回的ICMP响应,协议栈不知道该传递给上层的哪个应用,所以客户端进程中捕获不到相应的错误。
    如果UDP客户端connect UDP服务器<IP:PORT>,在调用时其实没有向外发包,只是在内核协议栈中记录了该状态。发生错误时本机收到ICMP消息,回来的ICMP不可达的响应能够被协议栈处理,通知客户端进程(因为内核已经知道该PORT的消息对应该服务);当客户端再次对该fd进行操作时,比如读数据时,read等调用会返回一个错误。
    而在两种情况下,write或者sendto操作都是把数据放到协议栈的发送队列之后就返回成功,而相应的ICMP回应则要等数据到达对端后才能返回,所以通常这种情况叫做“异步错误”。


    l 服务器端
    1. TCP服务器:

    1) 当TCP服务器绑定通配IP地址,套接口会接收到达它绑定端口的任何TCP连接。并以接收的目的IP地址作为它的源IP地址(用以确定四源组),以接收的源IP地址作为它的目的IP地址发回应答。
    
    2) 当TCP服务器绑定本地IP地址,这就限制了套接口只接收到达它绑定端口并且目的地址为此IP地址的客户连接。以绑定的目的IP地址作为源IP地址(当然,绑定的IP地址肯定与接收连接的目的IP地址相同,否则它不会接收),并以接收的源IP地址作为它的目的IP地址发回应答。
    一般只有在本机有多个网卡(多IP)才有效。如服务器SERVER有IP地址IPA和IPB,服务bind了IPA,那么发送给IPB的数据SERVER不会接收。如果是bind IPADDR_ANY, 则发到该服务器上的请求都会接受。

    2. UDP服务器:

    1) 当UDP服务器绑定通配IP地址,套接口会接收到达它绑定端口的任何UDP数据报。并以数据报的外出接口的主IP地址为源IP地址,以接收到的源IP地址作为它的目的IP地址发回应答。
    
    2) 当UDP服务器绑定本机IP地址,这就限制了套接口只接收到达它绑定端口并且目的地址为此IP地址的UDP数据报。并以绑定的IP地址作为源IP地址,以接收的源IP地址作为它的目的IP地址发回应答。
    
    3) 当UDP服务器调用connect,内核记录下对方的IP地址和端口号,它们包含在传递给connect的套接口地址结构中,并为UDP服务器绑定了一个临时端口号和IP地址。(UDP服务器只能接收目的IP地址为它绑定的IP地址和端口号并且源IP地址为它指定对方的IP地址和端口号的数据报)。

     2、connect函数

    1  #include <sys/types.h>          /* See NOTES */
    2  #include <sys/socket.h>
    3 
    4  int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。

    通常的情况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。

    关于UDP中客户端调用connect的好处,还有一个作用是能够捕获错误
    当UPD客户端像服务器发送一个不存在的端口请求,服务器会像客户端发送ICMP端口不可达消息。不调用connect时,对于返回的ICMP响应,协议栈不知道该传递给上层的哪个应用,所以客户端进程中捕获不到相应的错误。
    如果UDP客户端connect UDP服务器<IP:PORT>,在调用时其实没有向外发包,只是在内核协议栈中记录了该状态。发生错误时本机收到ICMP消息,回来的ICMP不可达的响应能够被协议栈处理,通知客户端进程(因为内核已经知道该PORT的消息对应该服务);当客户端再次对该fd进行操作时,比如读数据时,read等调用会返回一个错误。
    而在两种情况下,write或者sendto操作都是把数据放到协议栈的发送队列之后就返回成功,而相应的ICMP回应则要等数据到达对端后才能返回,所以通常这种情况叫做“异步错误”。

    3、

    listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接), listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。

    这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。

    所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成

    关于backlog,Linux内核中维护两个队列:SYN_queue(SYN报文接收)和 ACCEPT queue(Established, Server收到ACK,三次握手完成)

    a) When a connection request arrives (i.e., the SYN segment), the system-wide parameter net.ipv4.tcp_max_syn_backlog is checked (default 1000).

    If the number of connections in the SYN_RCVD state would exceed this threshold, the incoming connection is rejected 

    客户端connect发送SYN请求,服务端内核先检查是否超过了SYN,没有超过则加入到SYN queue,服务器的 SYN 响应,其中稍带对客户 SYN 的 ACK(即SYN+ACK)

    b)Each listening endpoint has a fixed-length queue of connections that have been completely accepted by TCP (i.e., the three-way handshake is complete) but not yet accepted by the application 

    Accept queue中的tcp连接是内核建立完三次握手,但是应用程序还没有调用accept取出链接。这个队列的大小就是backlog

    The application specifies a limit to this queue, commonly called the backlog. This backlog must be between 0 and a system-specific maximum called net.core.somaxconn,

    inclusive (default 128)

    backlog has no effect whatsoever on the maximum number of established connections allowed by the system, or on the number of clients that a concurrent server can handle concurrently. 

    accept queue和最大tcp连接数无关

    c)If there is room on this listening endpoint’s queue for this new connection, the TCP module ACKs the SYN and completes the connection. The server application with the listening endpoint does not see this new connection until the third segment of the three-way handshake is received.

    如果SYN队列未满,TCP接收SYN请求并返回SYN的ACK,服务器并不感知这一过程。

    Also, the client may think the server is ready to receive data when the client’s active open completes successfully, before the server application has been notified of the new connection. If this happens, the server’s TCP just queues the incoming data. 

    客户端在Connect返回成功后(三次握手完成,链接已经进入服务端的Accept队列),就认为服务端准备好接收数据。此时服务端可能没有调用Accept函数把链接从Accept队列取出。此时服务端tcp内核只是把数据存入缓冲区。

    d) If there is not enough room on the queue for the new connection, the TCP delays responding to the SYN, to give the application a chance to catch up .it persists in not ignoring incoming connections if it possibly can .

    如果SYN队列满,TCP延迟响应客户端SYN请求。此时客户端会超时重试。

    If the net.ipv4.tcp_abort_on_overflow system control variable is set, new incoming connections arereset with a reset segment. 

    如果服务端开启net.ipv4.tcp_abort_on_overflow,该SYN请求会被拒绝,服务端发送RST报文。

    为什么tcp是三次握手而不是两次握手?

    标准答案:第三次握手时为了防止已失效的连接请求报文段有传送到B,因而产生错误。

    下面做出详细解释:

    所谓“防止已失效的连接请求报文”是这样产生的。考虑一种正常情况。A发出连接请求,但因连接请求报文丢失而未收到确认。于是A再重传一次请求连接。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A共发送了两个连接请求报文段。其中第一个丢失,第二个到达了B。没有“已失效的请求连接报文段”。

    现假定出现一种异常情况,即A发出的第一个请求连接报文段并没有丢失,而是在某些网络结点长时间滞留了,以至到连接释放以后的某个时间才到达B。本来这是一个已失效的报文段。但B收到此失效的连接请求报文段后,就误认为是A又发出一次新的连接请求。于是就向A发出确认报文段,同意建立连接。假定不采用三次握手,那么只要B发出确认,新的连接就建立了。

    由于现在A并没有发出建立请求的连接,因此不会理睬B的确认,也不会向B发送数据,但B却以为新的运输连接已经建立了,并一直等待A发来的数据。B的许多资源就这样白白浪费了。

    采用三次握手的办法可以防止上述现象发生。例如在刚才的情况下,A不会向B的确认发出确认。B由于收不到确认,就知道A并没有要求建立连接,所以就不会分配资源给这个连接。

    二、链接的终止

    如果服务器出了异常,百分之八九十都是下面两种情况:

    1.服务器保持了大量TIME_WAIT状态

    2.服务器保持了大量CLOSE_WAIT状态

    因为linux分配给一个用户的文件句柄是有限的(可以参考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT和CLOSE_WAIT两种状态如果一直被保持,那么意味着对应数目的通道就一直被占着,而且是“占着茅坑不使劲”,一旦达到句柄数上限,新的请求就无法被处理了,接着就是大量Too Many Open Files异常

    2.1 服务器保持了大量CLOSE_WAIT状态

    netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 

    其中$NF表示最后一个字段
    它会显示例如下面的信息:
    TIME_WAIT 814
    CLOSE_WAIT 1
    FIN_WAIT1 1
    ESTABLISHED 634
    SYN_RECV 2

    LAST_ACK 1
    常用的三个状态是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭

     通过原理图,我们知道了CLOSE_WAIT是被动关闭的状态。什么意思呢?比如客户端发了个请求,正常情况下是会收到服务器响应一个状态的,即Response。当客户端读取了这个返回后,会主动告诉服务器收到了,关闭连接。


    由于是客户端发起关闭连接的请求,在TCP协议下双方需要通过四个包的互发完成双向确认工作,才能最终关闭这个连接。
    客户端要求关闭,此时客户端状态为 FIN_WAIT_1,同时向服务器发送了 FIN 包,服务器状态变更为CLOSE_WAIT;
    当然,服务器需要对收到FIN包向客户端确认,于是服务器向客户端发送了 ACK 包,客户端因此变更状态为FIN_WAIT_2;
    服务器处理了这个确认后,再次主动向客户端发送FIN包,同时自己状态变更为LAST_ACK,收到来自服务器FIN包的客户端也将自己状态变更为TIME_WAIT;
    最后一步,客户端会对来自服务器的FIN包回复确认,服务器收到该ACK包后,将自己状态置为CLOSED,如此,整个关闭过程结束。
    简单说就是,客户端 -》 服务器,我要关闭,服务器回复OK,并开始处理后续;服务器后续处理好后,再告诉客户端我可以关闭了,客户端确认,服务端关闭。
    所以,出现CLOSE_WAIT状态的原因是,服务器一端因故没有向客户端发出FIN包,即服务端的LAST_ACK -- FIN -->客户端这步没能执行。

    1 void TcpConnection::HandleRead() {
    2   char buffer[65536] = {0};
    3   size_t nbytes = ::read(socket_->sockfd(), buffer, sizeof buffer);
    4   LOG_TRACE << conn_name_ << ":read buffer size=" << nbytes;
    5   message_cb_(shared_from_this(), buffer, nbytes);
    6 }

    上述代码,当read返回0时,没有调用close,会导致连接半关闭,

    出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接。代码需要判断socket,一旦读到0,断开连接,read返回负,检查一下errno,如果不是AGAIN,就断开连接。

    2.2 服务器保持了大量TIME_WAIT状态

    time-wait开始的时间为tcp四次挥手中主动关闭连接方发送完最后一次挥手,也就是ACK=1的信号结束后,主动关闭连接方所处的状态。

    然后time-wait的的持续时间为2MSL. MSL是Maximum Segment Lifetime,译为“报文最大生存时间”,可为30s,1min或2min。2msl就是2倍的这个时间。工程上为2min,2msl就是4min。但一般根据实际的网络情况进行确定。

    然后,为什么要持续这么长的时间呢?

    原因1:为了保证客户端发送的最后一个ack报文段能够到达服务器。因为这最后一个ack确认包可能会丢失,然后服务器就会超时重传第三次挥手的fin信息报,然后客户端再重传一次第四次挥手的ack报文。如果没有这2msl,客户端发送完最后一个ack数据报后直接关闭连接,那么就接收不到服务器超时重传的fin信息报(此处应该是客户端收到一个非法的报文段,而返回一个RST的数据报,表明拒绝此次通信,然后双方就产生异常,而不是收不到。),那么服务器就不能按正常步骤进入close状态。那么就会耗费服务器的资源。当网络中存在大量的timewait状态,那么服务器的压力可想而知。

    原因2:在第四次挥手后,经过2msl的时间足以让本次连接产生的所有报文段都从网络中消失,这样下一次新的连接中就肯定不会出现旧连接的报文段了。也就是防止我们上一篇文章 为什么tcp是三次握手而不是两次握手? 中说的:已经失效的连接请求报文段出现在本次连接中。如果没有的话就可能这样:这次连接一挥手完马上就结束了,没有timewait。这次连接中有个迷失在网络中的syn包,然后下次连接又马上开始,下个连接发送syn包,迷失的syn包忽然又到达了对面,所以对面可能同时收到或者不同时间收到请求连接的syn包,然后就出现问题了。

    引用网络资源的一段话:

    1. 值得一说的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,假如server一秒钟接收1000个请求,那么就会积压 240*1000=240,000个 TIME_WAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些 TIME_WAIT,所以对于新的 TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。

     

    1. HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个 request/response,一个主要原因就是发现了这个问题。

    也就是说HTTP的交互跟上面画的那个图是不一样的,关闭连接的不是客户端,而是服务器,所以web服务器也是会出现大量的TIME_WAIT的情况的。

    现在来说如何来解决这个问题。

    解决思路很简单,就是让服务器能够快速回收和重用那些TIME_WAIT的资源。

    下面来看一下我对/etc/sysctl.conf文件的修改:

    1.  #对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃,不应该大于255,默认值是5,对应于180秒左右时间   
    2.  net.ipv4.tcp_syn_retries=2  
    3.  #net.ipv4.tcp_synack_retries=2  
    4.  #表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒  
    5.  net.ipv4.tcp_keepalive_time=1200  
    6.  net.ipv4.tcp_orphan_retries=3  
    7.  #表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间  
    8.  net.ipv4.tcp_fin_timeout=30    
    9.  #表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。  
    10.  net.ipv4.tcp_max_syn_backlog = 4096  
    11.  #表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭  
    12.  net.ipv4.tcp_syncookies = 1  
    
    14.  #表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭  
    15.  net.ipv4.tcp_tw_reuse = 1  
    16.  #表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭  
    17.  net.ipv4.tcp_tw_recycle = 1  
    
    19.  ##减少超时前的探测次数   
    20.  net.ipv4.tcp_keepalive_probes=5   
    21.  ##优化网络设备接收队列   
    22.  net.core.netdev_max_backlog=3000   
    

    修改完之后执行/sbin/sysctl -p让参数生效。

    这里头主要注意到的是

    net.ipv4.tcp_tw_reuse
    net.ipv4.tcp_tw_recycle
    net.ipv4.tcp_fin_timeout
    net.ipv4.tcp_keepalive_* 
    

    这几个参数。

    net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的开启都是为了回收处于TIME_WAIT状态的资源。 
    
    net.ipv4.tcp_fin_timeout这个时间可以减少在异常情况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。 
    
    net.ipv4.tcp_keepalive_*一系列参数,是用来设置服务器检测连接存活的相关配置。 

     TIME_WAIT状态被占用的是一个五元组:(协议,本地IP,本地端口,远程IP,远程端口)。对于 Web 服务器,协议是 TCP,本地 IP 通常也只有一个,本地端口默认的 80 或者 443。只剩下远程 IP 和远程端口可以变了。如果远程 IP 是相同的话,就只有远程端口可以变了。这个只有几万个,所以当同一客户端向服务器建立了大量连接之后,会耗尽可用的五元组导致问题。



  • 相关阅读:
    Java中@Override的作用
    微软面试题: LeetCode 152. 乘积最大子数组 出现次数:2
    微软面试题: LeetCode 300. 最长递增子序列 出现次数:2
    微软面试题: LeetCode 76. 最小覆盖子串 出现次数:2
    微软面试题:剑指 Offer 52. 两个链表的第一个公共节点 出现次数:2
    微软面试题: LeetCode 79. 单词搜索 出现次数:2
    微软面试题: LeetCode 39. 组合总和 出现次数:2
    微软面试题: LeetCode 151. 翻转字符串里的单词 出现次数:2
    微软面试题: LeetCode 415. 字符串相加 出现次数:2
    微软面试题: LeetCode 110. 平衡二叉树 出现次数:2
  • 原文地址:https://www.cnblogs.com/ym65536/p/6743007.html
Copyright © 2020-2023  润新知