计算机网络
TCP/IP
协议是什么?协议就是一种约定,就像我们平时说的语言,是一种规定的、需要遵循的模式。
网络协议这方面我主要看了两本书:《TCP/IP详解 卷一》, 《计算机网络》。前一本是经典,后一本是上课的教材。感觉电子专业课程的安排问题很大,没有安排其他的编程课程就直接学计算机网络了,学了也不知所云。学习网络协议要搭配着网络编程来学,推荐《UNP》。
协议模型
模型主要有两种:
- OSI(开放式系统互连) 7层模型
- TCP/IP参考模型 4层(或5层 最底层为物理层)
OSI模型和协议过于复杂,也很少见,最主要的还是TCP/IP参考模型:
分层的目的是为了降低网络设计的复杂性,每一层都建立在其下一层的基础上。每一层使用不同的协议,并为上一层提供服务。数据从高层向下传输会增加消息头部或尾部,而从低层向上会去掉。每层也实现了不同的功能,以TCP/IP模型为例:
数据链路层,有些称为链路层或者网络接口层。主要处理计算机和下面的网络硬件之间的接口。最流行LAN的是以太网(Ethernet),传递的消息称为帧(Frame)。辅助的协议主要有:地址解析协议(ARP)、逆向地址解析协议(RARP),这两个协议是为了IP地址和物理地址(MAC地址)的相互转换。
网络层。主要是IP协议(网际协议),传递的消息称为数据报(Datagram)。提供的最主要的功能是路由(routing),也就是将数据从源端发送到目的端。注意和数据链路层区分:数据链路层是在同一个网络下在主机间进行传输,网络层是将数据通过路由器等在不同网络间传输。辅助的协议主要是Internet控制消息协议(ICMP),用来报告意外事件,比如超时、出错等等。
传输层。传输层为不同主机上的应用程序提供端到端的通信,需要传输的数据主要就封装在这一层。最常见的协议有两种:传输控制协议(TCP)和用户数据报协议(UDP)。TCP提供了面向连接的、可靠的、全双工的字节流通信;而UDP提供的是无连接的、不可靠的、有消息边界的通信。 TCP传递的数据称为分段(Segment)。
应用层。应用层是在传输层的基础上,对数据的格式进行特殊的规定,来实现不同的功能。最常见的有:域名系统(DNS)、超文本传输协议(HTTP)、电子邮件等等。
网际协议(IP)
IP
协议有两种:IPv4
和IPv6
,目前最主要的还是IPv4
。IP
传递的是数据报,提供无连接的尽力而为服务。
IP
协议使用IP
地址来寻址。IP
地址标识的是与物理链路交互的接口(interface),包括主机与物理链路的接口和路由器与链路的接口,IP
要求每台主机和路由器接口都拥有自己的IP
地址,路由器通常
有多个接口,也就有多个IP
地址。
IPv4
使用32比特的IP
地址,通常表示为点分十进制。一个IP
地址划分为两部分:a.b.c.d/x
,x
为子网掩码,指示了子网地址的位数,剩余的32-x
比特用于区分子网内部的设备。
寻址首先找到对应的网络,然后在该网络内部寻址具体的设备,这些独立的网络也就是子网,IP
协议使用分层的网络,子网内部也可能继续划分子网,不同的子网间通过路由器通信。
过去IP
地址是分为5类ABCDE的,每一类有固定的网络ID位数和主机ID位数,容易造成IP
地址的浪费,比如一个C
类(/24)子网能容纳2^8 - 1 = 254台主机,而一个B
类(/16)子网能共容纳65534台主机,
差距太大,分配C
类太小而B
类太大,不易控制。现在使用无类别域间选路(CIDR),子网掩码的位数不再固定,更易选择合适的子网位数。
子网地址一般由互联网服务提供商(ISP)提供。一台设备想要联网需要获取其IP
地址。路由器接口地址,可以由系统管理员手工配置,也可以使用动态主机配置协议(DHCP)从ISP
的DHCP
服务器
获取其地址。主机可以手工配置,不过更常用DHCP
从路由器运行的DHCP
服务器获取IP
地址。DHCP
还可以获取其他地址,如第一跳路由器地址(默认网关)和本地DNS服务器地址。
DHCP
是C/S协议,每个子网一般都拥有一个DHCP
服务器,当一台主机连接时,会通过UDP
与DHCP
服务器交互,自动获取其IP
地址。
还有一种地址分配方法网络地址转换(NAT),可以解决ISP
分配的子网所支持的IP
地址不够用的情况(比如越来越多的设备接入子网)。
在RFC1918
中保留了3部分IP
地址空间,用于专用网络或具有专用地址的地域,即地址只对该网络中的设备有意义,无法与外部交流。这样可以使许多网络采用相同的子网和IP
地址,这些子网
通过NAT
使能路由器与外界交互,NAT
路由器对外界隐藏了专用网络,相当于一个具有单一IP
地址的设备,当有数据报到达时,通过端口号区别子网内部的主机。
互联网控制报文协议(ICMP)
主要用于差错报告,比如目的网络不可达等。ICMP
通常认为是IP
协议的一部分,不过ICMP
报文是承载在IP
有效载荷上的,类似TCP
和UDP
。
IP
数据报到数据链路层传递时,可能会发生分片,原因是IP数据报的大小超过了数据链路层的最大传输单元(MTU)。
还有一个概念叫做路径MTU,是连接的网络间MTU的最小值。以太网的MTU为1500字节。要避免分片的发生,即使只丢失一片数据也要重传整个数据报,不过使用TCP
的话,会重传丢失的数据。
上面提到的都是单播,即点对点,网络层也支持广播和多播:
有一个特殊的全为
1
的IP地址,即
255.255.255.255`,这是广播地址。当一台主机发送一个目的地址为广播地址的数据报时,该报文会被交付给同一个子网中的所有主机。多播采用互联网组管理协议(IGMP)实现,使用
D
类地址关联多播组。
数据链路层
数据链路层提供的服务是将网络层的数据报通过路径上的单段链路节点到节点地传送,节点就是主机和路由器。数据在局域网(LAN)中进行传播,LAN
是一种集中在一个地理区域的计算机网络,
通常联网是从主机经LAN
再经路由器到因特网。最常见的局域网有以太网和802.11无线LAN(WiFi
)。
要注意和网络层区分:
网络层是选路,确定路线,在不同的网络中传输,依靠数据链路层在链路中传输。
子网和
LAN
有些相像,不过子网是三层可达,通过路由器在子网之间路由,而局域网是二层可达,通过交换机在节点中通信。子网内部通过局域网通信,不精确的说子网也对应着局域网。
和IP
协议一样,数据链路层也有自己的编址。每个节点的适配器或者说网卡都有自己的地址,通常称为MAC
地址或物理地址。MAC
地址为6字节,通常由16进制表示:XX-XX-XX-XX-XX-XX
,
MAC
地址标识着节点的硬件设备:适配器,通常是永久的,不会发生变化。而IP
地址一般是动态分配的,会发生变化。
为什么链路层还有自己的地址呢?一是IP
地址标识了目标主机的地址,而在传输的过程中需要在不同的路由器中传输,使用链路层地址可以标识下一跳路由器,从而实现传输。二是为了分层,保持各层次独立。
对于数据链路层来说,不能理解IP
地址,所以需要使用地址解析协议(ARP)在MAC
地址和IP
地址间转换。当需要发送数据报时,首先在ARP
表中查询地址映射,
如果没有就在LAN
中发送一个ARP
查询分组,使用MAC
广播地址(FF-FF-FF-FF-FF-FF)
,如果目标IP
地址在同一个LAN
内,与目标IP
地址匹配的主机就会将自己的MAC
地址响应给源主机,
更新ARP
表。如果需要跨子网传递,路由器就会将自己的MAC
地址相应给主机。
ARP
和DNS
有些像,不过DNS
是将因特网中任何地方的主机名或域名解析为IP
地址,而ARP
只能解析同一个子网上的IP
地址。
UDP
UDP提供的是无连接的、不可靠的、有消息边界的数据报通信。*UDP只是在IP协议上增加了两个东西:校验和和端口*,所以应用程序几乎是直接与IP打交道,不提供可靠的数据传输。
UDP是无连接的,并且没有流量控制,可以更好的控制发送时间,并且在一些情况下速度会比较快,比如如果应用程序只涉及到短的请求和应答,UDP性能就会比TCP好很多,因为没有建立连接和释放连接的开销。
由于UDP
只是简单的封装IP
,所以出错时会返回网络层的ICMP
来通知。使用UDP的例子有:域名系统(DNS)、网络文件系统(NFS)和简单网络管理协议(SNMP)。
TCP
TCP提供的是面向连接的、可靠的、全双工的字节流通信。为了提供可靠性,TCP提供了几个服务:
- [x] 提供了校验位(Checksum),确保到达目的地的数据不会被破坏;
- [x] 每个分段中分配一个序列号,可以使数据按照顺序组合起来,也解决了接收重复分组的问题。
- [x] 提供了超时、重传和确认机制,保证每个段都会被递交到可达目的地。
- [x] 提供了流量和拥塞控制,防止溢出并提高效率。
- [x] 提供了
TCP
传输层的错误通知机制RST
,也可以使用IP
网络层的ICMP
。
TCP
不提供安全性,有一种TCP
的强化版本叫做安全套接字层(SSL),提供了安全性服务,https
就是以SSL
为基础。
端口号
网络层IP只提供了主机到主机的交付,进程间数据交付和差错检测是由传输层提供的最基本的服务。传输层将报文交付到正确的进程(套接字)叫做多路分解,将报文从传输层传递到网络层叫做多路复用。
TCP和UDP协议都使用了端口号来标识套接字。可能的、被正式承认的端口号有 2^16 -1 = 65535 个。
端口被分为三类:著名端口、注册端口和动态端口。
- 著名端口是由因特网赋号管理局(IANA)来分配的,并且通常被用于系统进程,用来指定特定的服务。这些端口的一个显著特征就是限定在0~1023,并且在Linux、UNIX平台均需要Root权限才能监听这些端口。
- 注册端口不由IANA控制,不过由IANA登记并提供它们的适用情况。
- 动态端口通常被用来运行各种用户自己写的服务,服务监听在这些端口下不需要特别的权限。
- 动态端口,通常也称为临时端口。动态端口通常被用来在主动发起连接时随机分配使用,在任何特定的连接外不具有任何意义。有一个特殊的端口号0,当绑定该端口号时,内核就分配一个临时端口号给套接字。
传输层使用端口号来区分套接字,对于不同的协议,端口号的处理情况也不同:
- UDP: UDP是无连接的,一个UDP套接字是由
(IP地址, 端口号)
标识的。只要UDP报文的目的IP和目的端口号与它一致就会发送到该套接字。UDP也可以连接使用,不过是一种虚连接,不是真正的连接,参看
最后。 TCP: TCP是有连接的,连接建立后会返回已连接套接字,要和未连接套接字区别对待:
未连接套接字: 一般是监听套接字才需要绑定
IP
地址和端口号,之后调用listen
变为监听套接字。多个监听套接字通过绑定的IP
地址和端口号区分。已连接套接字: 由服务器
accept
返回得到或客户端connect
得到。已连接套接字通过四元组(源IP地址, 源端口号, 目的IP地址, 目的端口号)
标识,
accept
返回的已连接套接字会继承监听套接字的IP地址和端口号。(绑定0除外, 见下面)
- 在套接字建立时会选定是
TCP
还是UDP
,不同协议间的端口号是独立的,互不影响,所以更准确的说,套接字是由(协议, IP地址, 端口号)
标识。
当绑定通配地址INADDR_ANY
时,当连接建立后(TCP
)或发送、收到消息后(UDP
),会由内核分配IP
地址给套接字;当绑定端口号0
时,会立即分配一个随机的端口号给套接字。
当不绑定时,效果和绑定INADDR_ANY
和端口号0
类似: 当连接建立后(TCP
)或发送消息(UDP
),会分配给套接字IP
地址和端口号;当调用listen
时,会给监听套接字分配端口号。
服务端需要绑定固定的端口号,否则会随机分配端口号,导致无法确定套接字。客户端一般不绑定,由内核分配。
SO_REUSEADDR和SO_REUSEPORT
套接字是由(协议, IP地址, 端口号)
标识的,不能有完全相同的三元组,否则无法区分,所以当相同类型的套接字绑定相同的IP
地址和端口号时,会返回EADDRINUSE: address already in use
。
只要有一个不同就可以绑定。
通配地址INADDR_ANY
比较特殊,当绑定了通配地址后,就相当于所有的本地IP地址都被它同时绑定了,所有后续的套接字无法再绑定相同的端口号,反之也一样,当有套接字绑定了特定的IP
地址后,
也无法再绑定通配地址和相同端口号给套接字。
当有TCP
已连接套接字时,也相当于该IP
地址和端口号被占用了,也无法绑定重复的。TCP
连接套接字是由四元组标识的,而监听套接字是由二元组决定的,不相关,为什么还这样设置呢?
我觉得可能是这样,假如可以绑定相同的IP
地址和端口号,在建立连接时会出现重复的四元组,这当然不允许,就会在客户端connect
返回错误,而客户端的错误一般不易发现和解决,毕竟客户端是由客户使用,
而服务端是由服务提供者维护的。
TIME_WAIT
状态也会导致同样的效果,处于TIME_WAIT
状态的连接和已连接套接字一样会继续占用IP
地址和端口号,直到进入CLOSED
状态。
和TIME_WAIT
有关的还有一个时间叫做Linger Time
用于发送套接字缓冲区中
还未发送的数据,详见下面的close
和SO_LINGER
选项。
SO_REUSEADDR
可以解决上面的问题,设置该选项主要有两种功能:
将通配地址和特定地址区分开: 当通配地址已被绑定时,还可以绑定特定地址,只要不绑定完全相同的地址和端口号,反之亦然。
将未连接套接字和已连接套接字区分开: 这两个使用的
IP
地址和端口号互不影响,所以当处于TIME_WAIT
状态或还有连接时(如连接在子进程中处理,而主进程已退出)可以立即重启服务器。
Linux 3.9
添加了SO_REUSEPORT
选项。设置该选项可以将任意多个套接字绑定到完全相同的IP
地址和端口上,绑定相同地址和端口号的监听套接字由内核实现简单的负载均衡。
对于UDP
来说,SO_REUSEADDR和
SO_REUSEPORT`效果一样。
详细见stackoverfow
要注意标识连接的四元组用完或重复出现的情况,出现的原因可能有绑定了相同地址和端口号的套接字连接到固定地址的服务器,更可能是因为客户端所有地址和端口号都已经用完,比较常出现在
短连接的情况,已关闭连接的TIME_WAIT
状态会占用四元组直到耗尽2MSL
。此时客户端调用connect
会出现EADDRNOTAVAIL: Cannot assign requested address
。
在Linux
上有需要注意的地方:
在
BSD
只需要当前要绑定的套接字设置SO_REUSEADDR
即可;在Linux
上(至少我的电脑上ubuntu 14.04
),先前的套接字也需要设置该选项才可以。当绑定了通配地址的套接字调用
listen
变为监听套接字之后,设置SO_REUSEADDR
也无法再绑定相同的端口号给其余套接字,反之亦然,当有绑定了特定地址的监听套接字时,
也无法再绑定通配地址和相同的端口号给别的套接字。
建立连接
TCP建立连接需要发送三个段,所以也叫做三次握手。连接建立后,内核会为TCP**连接分配TCP缓存和状态变量**。在建立连接的过程中,还有两个重要的选项会被交换:
- 最大段大小(
MSS
)。用于向对端通告每个段所能发送的最大TCP**数据量**,不包括TCP首部。目的是为了避免IP分片,通常大小为1460。(以太网MTU - IP头部 - TCP头部) - 窗口规模选项。用于通告对端当前套接字缓冲区空间大小,可以使用
SO_RCVBUF
选项修改默认大小。通过对端不会发送比收到的窗口规模大的数据量,从而避免接受端发生溢出,实现流量控制。
这两个选项看起来有点重复,其实前一个决定的是每一个TCP分段的大小,后一个决定的是当前能够发送多少数据,而数据是以TCP分段为单位的。
为什么建立连接需要三次握手呢?
为了建立可靠的连接,如果只产生两个数据交互,服务器并不能知道连接是否建立成功并可以发送数据、对端能否接受连接。当收到第三个数据段的ACK时,服务器才能确认对方已收到第二个数据段,已成功建立连接。而三次也是理论上能够建立可靠数据交互的最小值,所以不会发送更高次数。
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误,比如由于网络延迟直到连接已释放才发送到服务端的连接请求。此时会由于服务端收不到第三个数据段导致超时重传,当重传次数过多时就会返回给进程错误。如果不发送第三个数据段,服务器接收到失效的连接请求之后发送ACK,服务器会认为已经建立的连接,等待无效的客户端发送数据,导致资源的浪费和出错。
释放连接
释放连接需要交换4次,也叫做四次挥手。这是因为TCP连接是全双工的,需要每个方向单独进行关闭,当一端结束数据传输时,就发送FIN通知另一端。一个TCP连接在收到一个FIN后仍能发送数据,利用半关闭(shutdown)可以实现。
释放过程中,先发送FIN
的一端执行的是主动关闭,当收到对端的FIN
并发送ACK
时,会进入一个特殊的状态TIME_WAIT
。还有可能发生两边同时发送FIN
,最后都进入TIME_WAIT
状态。这个状态至少需要经过2MSL(最大段生存时间)
后,根据实现不同持续在1分钟到4分钟之间,才会到CLOSED
状态。它主要有两个作用:
- 可靠地实现TCP全双工连接的终止。因为最后发送的
ACK
有可能会丢失掉,对端会再次发送FIN
,如果没有这个状态,会响应一个RST
,导致对方进入错误状态而不是有序终止状态。
当处于这个状态时,每收到一个段都会重启2MSL
的计时器。MSL
是IP数据报存活的最长时间(TTL
跳数限制)也就是TCP段存活的最长时间,
持续时间至少是超时重传的
TIME_WAITtimeout
+ 重传的FIN
的传送时间,
为了保证可靠,采用更加保守的等待时间2MSL
。这个状态不能保证最后的ACK
一定被对端收到,只是在等待足够长的时间后,可以认为连接被关闭了。
- 使老的重复TCP段在网络中消逝。因为数据有可能会重复发送或者经过网络延迟直到连接关闭才到达目的地。如果这时没有相应的连接,TCP仅仅丢弃该数据并响应RST。然而,如果主机间又建立了一个具有相同IP地址和端口号的连接(称为前一个连接的化身),就会认为该段是属于新连接的,导致数据混乱。TIME_WAIT状态不允许发起新的化身,并且持续2MSL,就可以确保每成功建立一个连接,都不会受到之前连接的影响。不过也有例外,有些实现当新的SYN的序列号大于之前连接的结束序列号就会成功发起新的化身。
每个连接是由一个四元组决定的,要求不能重新建立相同的四元组连接,TIME_WAIT
状态会占用之前的IP地址和端口号直到进入到closed
状态。设置SO_REUSEADDR
选项可以使用相同的端口号,但是不同使用相同的五元组。
超时、重传与确认
TCP是有状态的,内核保存着缓冲区和许多状态变量。序号Seq
是段首字节的字节流编号,确认号ACK
是期望收到的下一字节的序号。
TCP采用累积确认,只确认数据流中第一个丢失字节为止的字节,也就是确认收到的ACK
之前的字节。
内核保存着发送过的段,直到接收到对端发送的ACK大于其Seq
才丢弃,见下面的滑动窗口。
当发送一个段时,会启动一个定时器,当定时器超时时还没有收到对端的ACK
就会重传。
重传不意味着数据没有发送到,也有可能是ACK
丢失或者在网络中延时过长导致。
如果一直收不到ACK,TCP会持续重传数据段,直到尝试一定次数才放弃,然后返回给应用进程一个错误,通常是ETIMEOUT
。
如果发生了可以被检测到的错误会直接返回错误,而不是超时重传。
当收到3个冗余ACK时就会重新发送该Seq
的段,而不必等到超时,因为收到3个重复的ACK
意味着在Seq
这个段之后的段被收到了3次,
有理由相信这个段已经丢失,这就是快速重传机制。
使用Seq
和Ack
也可以区分冗余的TCP段,当收到的段的Seq
小于当前的Ack
时,就会知道该段已经接受过了,只返回Ack
不再存储数据。
流量控制和拥塞控制
影响数据传输的主要有两方面:网络和应用程序缓冲区空间。
如果TCP突然有大量的段发送到网络中,由于网络的速率一般都不同,快速的网络的数据会积攒在路由器中,可能会导致路由器的缓冲空间耗尽,路由器丢弃数据,导致重传,进一步使网络拥挤。
为了避免这个问题,TCP使用了慢启动和拥塞避免算法,并维护了一个拥塞窗口(cwnd),这是拥塞控制。
内核为每个套接字维护了两个缓冲区:接收缓冲区和发送缓冲区,每个TCP段都会通告对方当前接受缓冲区的可用大小,对端发送的数据不能超出该大小。
与SYN
一起发送的窗口大小通告了最大缓冲区大小。TCP使用了滑动窗口的技术,配合ACK
来实现高效的数据传输,这是流量控制。
发送窗口由对等方TCP使用,以预防应用程序发送的数据超过接收方TCP缓冲区;拥塞窗口由本方TCP使用,以预防应用程序超过网络所能承受的能力。
任何时候TCP能够发送的最大数据量是发送窗口和拥塞窗口的最小值,以确保两种类型的流量控制都能有效。
慢启动和拥塞避免
当一个连接建立起来时,无法知道合适的拥塞窗口大小。为了从一开始就避免发生拥塞,慢启动算法将拥塞窗口初始化为一个MSS大小,之后每收到一个ACK就增加一个MSS大小。
实际上,每经过RTT
就会使拥塞窗口大小增加一倍,慢启动其实一点也不慢。拥塞窗口一直呈指数级增长,直到发生超时,或者重复确认,或者达到接收方窗口大小。
拥塞避免算法是缓慢增加窗口大小的算法,每个RTT
增加1MSS
的窗口大小,也就是加性增。
慢启动和拥塞避免是如何来实现拥塞控制的呢?TCP使用了一个变量来控制:门限阈值(Threshold),每次发生丢包时,该值被设置为当前拥塞窗口大小的一半。
起始时,阈值设置非常大,并采取慢启动算法。当发生丢包即拥塞时,对待超时和重复确认采取不同的方法:
超时时,拥塞窗口被重置为一个MSS,再次使用慢启动算法,当窗口到达
Threshold
时采用拥塞避免算法。重复确认时,拥塞窗口设置为
Threshold
,并使用拥塞避免算法,取消了慢启动,这就是快速恢复算法。
滑动窗口
在上面也提到了内核中的缓冲区概念。滑动窗口不是一个新的缓冲区,而是一种算法,用来高效的组织、协调TCP连接两端的缓冲区。
TCP连接的每一方都维护着一个接受窗口,窗口的最左边是期望接收的下一个序列号,最后边是能够接受的最大数据的序列号。
当一个TCP段到达时,任何序列号超出接受窗口的数据(左边已收到的或右边未获得缓冲区的)或在窗口范围内但已经接收到的将被丢弃,除了超出窗口范围的,都会重新确认发送ACK
。
TCP段有可能是乱序到达的,就会缓冲在缓冲区中,滑动窗口也不移动,也不会向上层进程返回数据,直到收到期望的数据段,这时会更新ACK,滑动窗口右移,接收后面的数据。
当窗口为0的时候,有两种情况可以继续发送数据段:一是发送紧急数据;二是继续发送只有1个字节的数据段,让接收方通告下一个期望字节和窗口大小。
这是由TCP坚持定时器决定的,当窗口大小为0时,就会启动该定时器,终止时发送一个一个探查段,查询是否有窗口大小更新,
同时也避免了窗口更新消息的丢失导致的死锁,导致一直在等待窗口更新无法发送数据。(因为TCP对于ACK是没有相应的ACK确认的)。
每一端也维护了一个发送窗口。发送窗口分为两部分:已经发送的但没有确认的和可以发送但还没有发送的。为了提供可靠性,TCP发送端直到收到对端的ACK才会将缓冲区的内容踢出去,并向右滑动发送窗口。
Nagle算法、愚笨窗口综合症和ACK延滞算法
Nagle算法和ACK延滞算法是为了减少TCP段以降低拥塞出现的可能性。
Nagle算法的目的是减少小的TCP段的数量,该算法指出在任何时间内至多只能有一个没有确认的“小段”,“小段”也就是大小小于MSS的TCP段。当连接空闲时,如果有数据要发送就会立即发送,如果接下来的数据比较小,就会被收集起来,直到收到前一个的ACK再一起发送出去。数据比较大能充满1个MSS时,一般实现会立即发送。
Nagle算法其实愚笨窗口综合症(SWS)在发送方的表现,还需要在接收方的配合,不要通告小的窗口。假设接收方每次只读取少量数据,导致接受缓冲区只有很小空间,这时对端就会发送一系列小的数据段。所以除非缓冲区空间有了“显著的增长”,否则是不会发送窗口更新消息的。根据实现的不同,空间大小至少为1个MSS或最大窗口的一半大小。
ACK延滞算法是为了减少TCP段的数量,该算法使得TCP在接受到数据后不立即发送ACK,而是等待一小段时间(50~200ms)。如果在这个时间内,自身有数据要发送给对端,就会将这些数据捎带,结合成一个数据段发送给对端。如果没有数据,就会等到计时器超时,发送该ACK。
当Nagle算法和ACK延滞算法在一起时可能会引发问题,会带来很大的延迟:Nagle算法防止数据确认之前发送下一个数据,而延滞ACK要求等到计时器终止才发送ACK。设置TCP_NODELAY
选项可以禁用Nagle算法。
Keepalive
TCP不是轮询,不提供网络连接中断的通知,当出现问题时,除非你调用函数交互,否则就会认为一直在保持连接。这样设计的原因一是因为轮询会带来网络带宽的消耗;二是为了提供网络突然中断时仍能维持通信的能力,只要之后网络恢复。
TCP提供了一个Keepalive机制用于检测连接。当在一定时间间隔后,连接没有进行交互,就会发送一个特殊的段给对方,如果连接正常,对方就会响应一个ACK,如果连接不正常,就会产生错误。一般实现的时间是2个小时。设置SO_KEEPALIVE
选项可以开启该机制。