目录
TCP重点:三次握手、四次挥手、timewait状态、为何挥手不是三次(全双工)、拥塞避免(慢启动)、可靠性(超时重传、快速重传)、滑动窗口。
一 TCP报文封装
二 TCP头部
源端口号和目的端口号:再加上Ip首部的源IP地址和目的IP地址可以唯一确定一个TCP连接;
数据序号:表示在这个报文段中的第一个数据字节序号;
确认序号:仅当ACK标志为1时有效。确认号表示期望收到的下一个字节的序号(这个下面再详细分析);
偏移:就是头部长度,有4位,跟IP头部一样,以4字节为单位。最大是60个字节;
保留位:6位,必须为0;
6个标志位:URG-紧急指针有效;ACK-确认序号有效;PSH-接收方应尽快将这个报文交给应用层
RST-连接重置;SYN-同步序号用来发起一个连接;FIN-终止一个连接;
窗口字段:16位,代表的是窗口的字节容量,也就是TCP的标准窗口最大为2^16 - 1 = 65535个字节;指的是发送报文段首部中的确认号算起,接收方目前允许对方发送的数据量。
校验和:源机器基于数据内容计算一个数值,收信息机要与源机器数值 结果完全一样,从而证明数据的有效性。检验和覆盖了整个的TCP报文段:这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证的。
紧急指针:是一个正偏移量,与序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式;
选项与填充(必须为4字节整数倍,不够补0):最常见的可选字段的最长报文大小MSS(Maximum Segment Size),每个连接方通常都在一个报文段中指明这个选项。它指明本端所能接收的最大长度的报文段。该选项如果不设置,默认为536(20+20+536=576字节的IP数据报)。
三 TCP和UDP的区别
- TCP是稳定、可靠、面向连接的传输层协议,它在传递数据前要三次握手建立连接,在数据传递时,有确认机制、重传机制、流量控制、拥塞控制等,可以保证数据的正确性和有序性。UDP是无连接的数据传输协议,端与端之间不需要建立连接,且没有类似TCP的那些机制,会发生丢包、乱序等情况。
- 从头部结构来说,TCP因为有选项部分,所以有首部长度字段;而UDP没有选项部分,所以不需要首部长度字段。
- TCP避免分段,因为有重传机制本来就浪费了一些带宽,一旦出现分段,那么重传会大量增加,将会浪费大量带宽并且会严重降低传输效率;而UDP则不关心分不分段,且因为UDP的头部小(只有8字节,而TCP头部最小也得20字节),故可以携带更多的数据。
- TCP是数据流模式,所以应用程序产生的全体数据与真正发送的单个IP数据报没有什么联系;而UDP是数据报模式,进程的每个操作产生都正好产生一个UDP数据报,并组装成一个IP数据报发送。
- 所谓的“流模式”,是指TCP发送端发送几次数据和接收端接收几次数据是没有必然联系的,比如你通过 TCP 连接给另一端发送数据,你只调用了一次 write,发送了100个字节,但是对方可以分10次收完,每次10个字节;你也可以调用10次 write,每次10个字节,但是对方可以一次就收完。
原因:这是因为TCP是面向连接的,一个 socket 中收到的数据都是由同一台主机发出,且有序地到达,所以每次读取多少数据都可以。
- 所谓的“数据报模式”,是指UDP发送端调用了几次 write,接收端必须用相同次数的 read 读完。UDP 是基于报文的,在接收的时候,每次最多只能读取一个报文,报文和报文是不会合并的,如果缓冲区小于报文长度,则多出的部分会被丢弃。
原因:这是因为UDP是无连接的,只要知道接收端的 IP 和端口,任何主机都可以向接收端发送数据。 这时候, 如果一次能读取超过一个报文的数据, 则会乱套。
四 三次握手
在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。建立连接时syn按一定规则随机产生,并且最后一次ack为1。传输数据时序列号按传输的数据量标记。客户端发起连接接收端缓冲区大小最初为14600,服务端响应接收端为14480,mss大小为1460。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
未连接队列(第二次握手):在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。
五 四次挥手
socket编程中,这一过程由客户端或服务端任一方执行close来触发,流程如下图所示:
(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
六 TCP Api和三次握手对应
(1) connect:发送了一个SYN,收到Server的SYN+ACK后,代表连接完成。发送最后一个ACK是protocol stack,tcp_out完成的。
(2)listen:在server这端,准备了一个未完成的连接队列,保存只收到SYN_C的socket结构;还准备了已完成的连接队列,即保存了收到了最后一个ACK的socket结构。listen监听队列,大多数累计值设置为5,积压值说明的是TCP监听的端点已被TCP接受而等待应用层接受的最大连接数。这个积压值对系统所允许的最大连接数,或者并发服务器所能并发处理的客户数,并无影响。
(3)accept:应用进程调用accept的时候,就是去检查上面说的已完成的连接队列,如果队列里有连接,就返回这个连接;如果没有,即空的,blocking方试调用,就睡眠等待;nonblocking方式调用,就直接返回,一般“EWOULDBLOCK errno”告诉调用者,连接队列是空的。
注意:在上面的socket API和TCP STATE的对应关系中,TCP协议中,客户端收到Server响应时,可能会有会延迟确认。即客户端收到数据后,会阻塞给Server端确认。可以在每次收到数据后:调用setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int)); 快速给Server端确认。
七 TCP滑动窗口
TCP是通过滑动窗口来进行流量控制。我们知道,在TCP头部里有一个字段叫 Advertised-Window(即窗口大小)。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据,于是发送端就可以根据这个剩余空间来发送数据,而不会导致接收端处理不过来。
窗口大小是相对于确认号的。发送方不一定需要发送一个窗口大小的数据。接收方在发送ack时不必等到窗口被填满。
窗口三种移动方式:合拢、张开、收缩。
合拢:收到ack。收到的ack确认号之前的数据都是已经完全发送成功的
张开:窗口右移允许发送更多的数据。当接受方应用程序读取了缓冲区数据,释放TCP接收缓冲区时,会发生这种情况。
收缩:若收到ack在左窗口的左边,则丢弃。当左窗口右移为0时,不能发送数据。
下面是发送端的滑动窗口示意图:
接收端在给发送端回ACK中会汇报自己的 Advertised-Window 剩余缓冲区大小,而发送方会根据这个窗口来控制下一次发送数据的大小。下面是滑动后的示意图(收到36的ack,并发出了46-51的字节):
Zero Window:如果接收端处理缓慢,导致发送方的滑动窗口变为0了,怎么办?—— 这时发送端就不发数据了,但发送端会发ZWP(即Zero Window Probe技术)的包给接收方,让接收方回ack更新Window尺寸,一般这个值会设置成3次,每次大约30-60秒。如果3次过后还是0的话,有的TCP实现就会发RST把连接断了。
Silly Window Syndrome:即“糊涂窗口综合症”,当发送端产生数据很慢、或接收端处理数据很慢,导致每次只发送几个字节,也就是我们常说的小数据包 —— 当大量的小数据包在网络中传输,会大大降低网络容量利用率。比如一个20字节的TCP首部+20字节的IP首部+1个字节的数据组成的TCP数据报,有效传输通道利用率只有将近1/40。
为了避免发送大量的小数据包,TCP提供了Nagle算法,Nagle算法默认是打开的,可以在Socket设置TCP_NODELAY选项来关闭这个算法。
八 拥塞控制
我们知道TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶性循环,最终形成“网络风暴” —— TCP的拥塞控制机制就是用于应对这种情况。
首先需要了解一个概念,为了在发送端调节所要发送的数据量,定义了一个“拥塞窗口”(Congestion Window),在发送数据时,将拥塞窗口的大小与接收端ack的窗口大小做比较,取较小者作为发送数据量的上限。
拥塞控制主要是四个算法:
慢启动:意思是刚刚加入网络的连接,一点一点地提速,不要一上来就把路占满。1、连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。2、每当收到一个ACK,cwnd++; 呈线性上升。3、每当过了一个RTT,cwnd = cwnd*2; 呈指数上升。4、阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。
拥塞避免:当拥塞窗口 cwnd 达到一个阈值时,窗口大小不再呈指数上升,而是以线性上升,避免增长过快导致网络拥塞。1、每当收到一个ACK,cwnd = cwnd + 1/cwnd。2、每当过了一个RTT,cwnd = cwnd + 1。
拥塞发生:当发生丢包进行数据包重传时,表示网络已经拥塞。分两种情况进行处理:1.等到RTO超时,重传数据包:sshthresh = cwnd /2,cwnd 重置为 1,进入慢启动过程;2.在收到3个duplicate ACK时就开启重传,而不用等到RTO超时:sshthresh = cwnd = cwnd /2;进入快速恢复算法——Fast Recovery;
快速恢复:至少收到了3个Duplicated Acks,说明网络也不那么糟糕,可以快速恢复。cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了);重传Duplicated ACKs指定的数据包;如果再收到 duplicated Acks,那么cwnd = cwnd +1;如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。
九 TCP可靠传输的实现
1、应用数据被分割成TCP认为最合适发送的数据块。称为段(Segment)传递给IP层
2、当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送的,通常将推迟几分之一秒
3、当TCP发出一个段后,它会启动一个定时器,等待目的端确认收到这个报文段。若没有及时收到确认,将重新发送这个报文段
4、TCP将保持它首部和数据的校验和,这是一个端到端的校验和,目的是检测数据在传输过程中的任何变化。如果收到段的校验和有差错,TCP将丢弃这个报文也不进行确认(对方就会重复发送了)
5、TCP承载与IP数据报来传输,而IP数据报可能会失序,所以TCP的报文段到达时也可能会失序。但是TCP收到数据后会重新排序到正确的顺序(通过序号)
当队列已满时,TCP将不理会传入的SYN,也不发回RST作为应答,因为这是一个软错误,而不是一个硬错误。
十 TCP定时器
BSD Reno版本(reno算法,目前广泛使用的,很多都是基于此实现的)
Tcp提供可靠的传输层,是通过确认机制保证的,当然确认包与数据包都有可能丢失,解决的办法是设置定时器,当定时器溢出时,就重传数据包或者ack;实现的关键是如何确定超时时间以及重传的频率。
对于每个TCP连接,维持4个定时器:1、重传定时器;2、坚定定时器,保证窗口的流动;3、保活定时器;4、2msl的维持TIME_WAIT状态的定时器。
1 保活定时器
保活定时器:当一端主动关闭时,处于半连接状态,另一端设置保活定时器,在交互数据流中,Nagle算法保证的是小块数据流的发送,避免拥塞的产生。它是自适应的,确认到达的越快,发送速率越快。规则是:a长度大于mss的数据直接发送;b小于mss的数据只能存在一个为被确认的发送包;c超时发送,一般为200ms;d设置了TCP_NODEAY,取消nagle算法;e设置了TCP_CORK,开启算法;f有fin,直接发送。
保活定时器时间长为2小时,并坚持发送10个探测段,间隔时间75秒;对端四种情况:1、正常;2、重启,发送rst复位;3、对端崩溃等情况,发送10次探测;4、路由问题,探测不可达,收到icmp不可达报文;
Connect超时时间为75秒;msl一般为2分钟,2msk30秒到2分钟。
2 重传定时器
指数退避:即倍乘关系,从开始的比如1.5秒,到发生重传后的3,6,12,24,48,64,直到最多的64秒将会放弃发送,从而发送一个复位报文段,并通知主机断开信息。
往返时间的测量:(由于路由器与网上流量的动态变化,需要动态的进行测定,通常使用RTT进行测量)
分组重传机制:
当发生分组丢失时,接收端发送一个丢失数据包起始序列号的ACK,当接收端收到三次(为什么是三呢,可能发生丢包或者接收端顺序调整,若三次还是收到重复的ACK,就表示可能发生了包丢失)的丢失数据包的序列号的ACK时就会重传该序列号对应的数据包,当重传报文发送后,不会等待重传报文的确认(当再次丢失呢?重复收到该序列号的ACK,再次等待3次后重传。。。)
拥塞避免算法和慢启动算法是两个不同的算法,但是当发生拥塞的时候,就需要减少降低分组在网络中的传输速率,此时需要用到慢启动就行控制。
二者都需要为每个链接维持两个变量,一个是拥塞窗口(cwnd)、另个时慢启动的门阀值(ssthresh)。
拥塞避免(拥塞窗口是发送方的流量控制,通告窗口是接收端的流量控制)
当拥塞发生时(超时或者是收到重复的确认),就会将ssthresh设置为当前窗口(拥塞窗口以及通告窗口的最小值,但是最小为2个报文段长度)的一半大小,此外,如果是超时引起的,那就会将cwnd设置为一个报文段长度(慢启动)
当新的数据被确认时,就会增加cwnd,但增加的方法依赖于当前是在进行拥塞避免或者慢启动(指数增长)。当拥塞窗口cwnd的值小于ssthresh的值时则证明在进行慢启动(小于阀值),否则在进行拥塞避免。慢启动一直持续到我们回到发生拥塞窗口的一般的时候才停止,此时将进行拥塞避免。
拥塞避免算法要求每次收到一个ACK,将会增加1/cwnd大小,希望在一个往返的时间内,最多增加一个cwnd(无论收到了多少个ACK);
快速重传与快速恢复算法:
当发生收到重复的ACK时有两种可能,一种是顺序性错乱(当接收端通过调整后可以不用重传),一般会收到一两个重复的ACK属于正常,另一种是发生丢包,当重复ACK的数量达到三或者三个以上时,就非常有可能发生丢包,此时不用考虑定时器是否溢出,采用的算法就是快速重传,此外不需要等待重传数据报的ACK。当发生快速重传后采用的是拥塞避免而不是慢启动。这就是快速恢复算法。
当出现分组丢失时我们的速率将减半(执行的是拥塞避免)。
总结:当超时引发的阻塞,不仅会修改当前ssthresh为当前窗口的一半(前边有述),还会进行慢启动,当是由于收到重复的ack(顺序调整或者丢包),就会进行拥塞避免,而不是慢启动。
然后就是快速重传(不用等待定时器是否溢出)以及快速恢复算法(不进行慢启动,原因是,能收到重复的ACK,表明数据包已经离开网络,而不需要采用慢启动从一开始,只需要采用拥塞避免进行控制快速恢复即可)。
按每条路由进行初始化:
当一个tcp链接断开,同时这条链接已经发送了足够多的信息用与提取有用信息时,我们就可以将:被平滑RTT,被平滑的均值偏差以及慢启动阀值记录在路由表项中,这样当建立一个新的链接时,如果路有表项中存在信息,将会以这写信息进行初始化。
ICMP的差错
多种ICMP差错对tcp链接的影响,在旧的实现版本上,当出现ICMP 显示主机不存在时,直接会关闭tcp链接,但是,这个信息是不完整的,又是可能只是延时(路由替换)。
重新分组
当T C P超时并重传时,它不一定要重传同样的报文段。相反, T C P允许进行重新分组而发送一个较大的报文段,这将有助于提高性能(当然,这个较大的报文段不能够超过接收方声明的M S S)。
十一 关闭链接过程中的TCP状态和SOCKET处理,及可能出现的问题
1 TIME_WAIT
TIME_WAIT
是主动关闭 TCP 连接的那一方出现的状态,系统会在TIME_WAIT状态下等待2MSL(maximum segment lifetime)后才能释放连接(端口)。通常约合4分钟以内。
TIME_WAIT状态等待2MSL的意义:1、确保连接可靠地关闭,即防止最后一个ACK丢失。2、避免产生套接字混淆(同一个端口对应多个套接字)。
为什么说可以用来避免套接字混淆呢?
一方close发送了关闭链接请求,对方的应答迟迟到不了(例如网络原因),导致TIME_WAIT超时,此时这个端口又可用了,我们在这个端口上又建立了另外一个socket链接。 如果此时对方的应答到了,怎么处理呢?其实这个在TCP层已经处理了,由于有TCP序列号,所以内核TCP层,就会将包丢掉,并给对方发包,让对方将sockfd关闭。所以应用层是没有关系的。即我们用socket API编写程序,就不用处理。
通常调用close(),或者closesock()的时候,会立即返回,不考虑缓冲区的数据有没有被完全发送完,所以应用程序也不知到这些数据是否被发送成功。
为了避免这种情况的发生,设置了套接字选项,SO_LINGER,填写linger结构
Struct longer
{
Int l_onoff;
Int l_linger;
};
如果成员l_onoff为0,标示被关闭;非零标示开启,其行为由l_linger控制,当该值非零时,就将其作为内核等待挂起的数据被发送出去并被确认时逗留的时间间隔。也就说当close等调用在缓冲区数据全部发送完毕或者时间间隔超时之前是不会返回的。当时间到期后,返回EWOULDBLOCK,成功返回0;如果成员l_linker为0,就直接丢弃链接,返回一个 rst的复位报文段,不经过TIME_WAIT状态就直接关闭链接(TIEM_WAIT暗杀)。
如果要立即重用链接,可以使用SO_REUSEADDR选项,保证使用一个处于TIME_WAIT状态的端口。
将写操作合并起来:禁止Nagle算法一部分原因是一系列小规模的写操作来发送逻辑上相互关联的数据造成性能问题(比如与ack延时交互后,造成大量的延时,其原理为:当客户端需要发送逻辑上相互关联的数据,比如两份数据构成一个完整的请求,当使用nagle算法时,由于发送出前半部分请求后,后半部分作为小分组将不会被发送直到收到ack,对于服务端程序中的ack延时(也用来减少网络上的分组),等待服务端的响应信息,使得ack与响应信息同时发送,但是服务端没有收到完整的请求信息,所以只有在ack延时定时器到期后才会发送ack,这样nagle与ack延时相互交互造成了大量的延时)使用writev与readv可以避免Nagle与ack延时算法的交互。
即将数据合并起来再一起发送出去。
总结:尽量使用大规模的写操作,因为小规模的写操作可能会引起相当严重的性能问题。
注意:TIME_WAIT是指操作系统的定时器会等2MSL,而主动关闭sockfd的一方,并不会阻塞。(即应用程序在close时,并不会阻塞)。
当主动方关闭sockfd后,对方可能不知道这个事件。那么当对方(被动方)写数据,即send时,将会产生错误,即errno为: ECONNRESET。
服务器产生大量 TIME_WAIT 的原因:(一般我们不这样开发Server,但是web服务器等这种多客户端的Server,是需要在完成一次请求后,主动关闭连接的,否则可能因为句柄不够用,而造成无法提供服务。)
服务器存在大量的主动关闭操作,需关注程序何时会执行主动关闭(如批量清理长期空闲的套接字等操作)。
一般我们自己写的服务器进行主动断开连接的不多,除非做了空闲超时之类的管理。(TCP短链接是指,客户端发送请求给服务器,客户端收到服务器端的响应后,关闭链接)。
2 CLOSE_WAIT
CLOSE_WAIT
是被动关闭 TCP 连接时产生的,如果收到另一端关闭连接的请求后,本地(Server端)不关闭相应套接字就会导致本地套接字进入这一状态。(如果对方关闭了,没有收到关闭链接请求,就是下面的不正常情况)
按TCP状态机,我方收到FIN,则由TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果我方不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。
如果存在大量的 CLOSE_WAIT,则说明客户端并发量大,且服务器未能正常感知客户端的退出,也并未及时 close 这些套接字。(如果不及时处理,将会出现没有可用的socket描述符的问题,原因是sockfd耗尽)。
正常情况下:一方关闭sockfd,另外一方将会有读事件产生, 当recv数据时,如果返回值为0,表示对端已经关闭。此时我们应该调用close,将对应的sockfd也关闭掉。
不正常情况下:一方关闭sockfd,另外一方并不知道,(比如在close时,自己断网了,对方就收不到发送的数据包)。此时,如果另外一方在对应的sockfd上写send或读recv数据。
recv时,将会返回0,表示链接已经断开。
send时, 将会产生错误,errno为ECONNRESET。
十二 关于三次握手和四次挥手的问题
1、为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
2、TIME_WAIT状态的产生、危害、如何避免?
TCP协议在关闭连接的四次挥手中,为了应对最后一个ACK丢失的情况,Client(即主动关闭连接的一方)需要维持time_wait状态并停留 2个MSL(个数据报文在网络中能够生存的最长时间通常为2分钟)的时间。
3、为什么TIME_WAIT需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
①、为了保证A发送的最后一个ACK报文段能够到达B。即最后这个确认报文段很有可能丢失,那么B会超时重传,然后A再一次确认,同时启动2MSL计时器,如此下去。如果没有等待时间,发送完确认报文段就立即释放连接的话,B就无法重传了(连接已被释放,任何数据都不能出传了),因而也就收不到确认,就无法按照步骤进入CLOSE状态,即必须收到确认才能close。
②、防止“已失效的连接请求报文段”出现在连接中。经过2MSL,那些在这个连接持续的时间内,产生的所有报文段就可以都从网络中消失。即在这个连接释放的过程中会有一些无效的报文段滞留在楼阁结点,但是呢,经过2MSL这些无效报文段就肯定可以发送到目的地,不会滞留在网络中。这样的话,在下一个连接中就不会出现上一个连接遗留下来的请求报文段了。
4、为什么要采用三次握手,两次不行吗?
若A发送的第一个请求报文滞留,延误到连接已经释放的某个时间点到达B,而B以为是新的连接,进行连接,但是A不会建立连接,则B一直处于等待转态。
5、我们如何判断有一个建立链接请求或一个关闭链接请求:
建立链接请求:connect将完成三次握手,accept所监听的fd上,产生读事件,表示有新的链接请求。
关闭链接请求:close将完成四次挥手,如果有一方关闭sockfd,对方将感知到有读事件,如果read读取数据时,返回0,即读取到0个数据,表示有断开链接请求。(在操作系统中已经这么定义)
十三 串包
有时候,我们以API的方式为客户提供服务,如果此时你提供的API采用TCP长连接,而且还使用了TCP接收超时机制(API一般都会提供设置超时的接口,例如通过setsockopt设置SO_RCVTIMEO或这select),那你可能需要小心下面这种情况(这里姑且称之为“窜包”,应用程序没有将应答包与请求包正确对应起来):
如果某一笔以TCP接收的请求超时(例如设置为3秒)返回客户,此时客户继续使用该链接发送第二个请求,此时后者就有可能收到前一笔请求的应答(前一笔的应答在3秒后才到达),倘若错误的将此应答当做后者的应答处理,那就可能会导致严重的问题。如果网络不稳定,或者后台处理较慢,超时严重,其中一笔请求应答窜包了,很可能导致后续多个请求应答窜包。例如网上常见的抽奖活动,第一个用户中了一个iPad,而第二个用户在后台中仅为一个虚拟物品,若此时出现窜包,那第二个用户也会被提示中了iPad。
这个问题,初看起来最简单的解决办法就是:一旦发现有请求超时,就断开并重新建立连接,但这种方案理论上是不严谨的,考虑下面这种情况:
1、应答超时的原因是因为应答包在网络中游荡(例如某个路由器崩溃等原因,这类在网络中游荡的包,俗称迷途的分组);
2、API在检测到超时后,断开并重新建立的连接的IP和Port与原有连接相同(新连接为被断开连接的化身);
3、在新连接建立后,立即发送了一个新的请求,但随后那个迷途的应答包又找到了回家的路,重新到达,此时新连接很有可能将这个不属于自己的包,当做第二个请求的应答(该包的TCP Sequence恰好是新连接期望的TCP Sequence,这种情况是可能的,但是基本不可能发生)。
注:正常情况下,TCP通过维持TIME_WAIT状态2MSL时间,以避免因化身可能带来的问题。但是在实际应用中,我们可以通过调整系统参数,或者利用SO_LINGER选项使得close一个连接时,直接到CLOSE状态,跳过TIME_WAIT状态,又或者利用了端口重用,这样就可能会出现化身。在实际应用中,上面这种情况基本不会发生,但是从理论上来说,是可能的。
再仔细分析,就会发现这个问题表面上看是因为“窜包”导致,但本质原因是程序在应用层没有对协议包效验。例如另外一种情况:A、B两个客户端与Server端同时建立了两个连接,如果此时Server端有BUG,错将A的应答,发到B连接上,此时如果没有效验,那同样会出现A请求收到B应答的情况。所以这个问题解决之道就是:在应用层使用类似序列号这类验证机制,确保请求与应答的一一对应。
十四 问题
tcpdump中发现icmp host unreachable admin prohibited
,分析导致这个问题的原因是防火墙中阻止了ICMP消息,在iptables中开放icmp就没有问题了。
打开/etc/sysconfig/iptables,注释以下行:-A RH-Firewall-1-INPUT -j REJECT --reject-with icmp-host-prohibited
,然后重新启iptables,再刷新postfix队列。
iptables-save -c > /etc/iptables-rules
:将规则保存到iptables-rules文件,重启一个服务:service iptables restart
。
iptables
:静态防火墙,过滤数据包,可以设置规则。
RTT:给定连接的往返时间。
重传多义性:分组在rtt时间内没有到达,增减rtt的值,下次到达的ack是上次的还是第二次的,不能更行rtt的值。