计算机网络之TCP

面试复习

TCP

  • 字节流协议

  • 三次握手、四次挥手

  • 重点在于能够清楚讲出TCP的11种状态变迁(CLOSING状态比较罕见,忽略)

TCP过程

提供一个思路过程(不够细致严谨,仅适用于让你更容易理解):

三次握手

首先客户端和服务端都是关闭的
           ↓           ↓
    主动打开  被动打开
此时客户端状态为 SYN_SENT 、服务端状态从 LISTEN 状态 到 SYN_RCVD
            ↓①                                 ↓②
发送一个SYN J                      发送SYN K 和 ACK J+1
此时客户端和服务端状态都为 ESTABLISHED ,他们进行数据传输
           ↓③
发送一个ACK K+1

四次挥手:(这里注明一下因为TCP是全双工的,所以都可以请求关闭,这里我们说的是客户端主动请求关闭的情况)

首先客户端和服务端都是连接的
           ↓          ↓
    主动关闭  被动关闭
此时客户端状态为 FIN_WAIT1 、服务端状态为 CLOSED_WAIT
         ↓①                                   ↓②
发送一个FIN M                      发送一个ACK M+1
此时服务端状态为 LAST_ACK 、客户端状态为 FIN_WAIT2
         ↓③                                   ↓④
发送一个FIN N                     发送一个ACK N+1
此时客户端状态为 TIME_WAIT 、服务端状态为 CLOSED

TIME_WAIT

这时,你就会被问到TIME_WAIT的相关知识 如果没被问到,你就自己说了吧

主要说说设置TIME_WAIT的原因吧

  • 设置TIME_WAIT的原因

从两个方面来说吧,首先可靠地实现TCP全双工连接(full duplex)的终止,因为客户端需要维持这个TIME_WAIT这个将关未关的状态来发送最后一个ACK,(这让我想到了中学生物书那个奄奄一息在生命的最后关头抖擞精神完成交配保留了自己染色体为遗传学研究做出了巨大贡献的白眼果蝇hhh),如果它直接CLOSED的话,客户端会响应一个RST(进阶),服务器收到后会响应一个错误

第二个方面是TIME_WAIT允许了旧的重复分节在网络中消逝,因为在这个状态下的客户端不允许启动新的连接,而这个状态持续2MSL(进阶),于是就保证了当建立新的TCP连接的时候,旧的重复分节在网络中消逝。

  • 大量的TIME_WAIT

在HTTP协议中,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态。

解决办法:
①. 开启socket重用,允许将TIME_WAIT的socket重新用于TCP连接
②. 开启快速回收。

然后你就被问到 TCP为什么需要3次握手与4次挥手

三次握手:为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误

四次挥手:因为tcp是全双工模式(这里可以用英文说 full duplex),接收到FIN时意味将没有数据再发来,但是还是可以继续发送数据。

TCP进阶

每个状态一句话简洁解释,可以把这个嵌入到前面的部分:

三次握手状态:

  • LISTEN:
    表示服务器端的某个SOCKET处于监听状态。

  • SYN_SENT: 当客户端SOCKET执行CONNECT连接时,发送SYN,随即进入到SYN_SENT,并等待服务端发送的SYN, ACK

  • SYN_RCVD:
    服务端接受到了SYN报文。

在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个 ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。(服务器端)

  • ESTABLISHED:表示连接已经建立了。

四次挥手状态:

  • FIN_WAIT_1:
    表示等待对方的FIN报文。

其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)

  • FIN_WAIT_2:
    对方回应ACK报文后,则进入到FIN_WAIT_2状态。

上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)

  • TIME_WAIT:
    收到了对方的FIN报文,并发送出了ACK报文,即将CLOSED。

表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)

  • CLOSING(比较少见):
    表示双方都正在关闭SOCKET连接

这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

  • CLOSE_WAIT:
    这种状态的含义其实是表示在等待关闭。

当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)

  • LAST_ACK:
    等待最后一个ACK,即将CLOSED。

这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)

  • CLOSED:
    表示连接中断。

下面这两个术语也是之前有提到的,如果不熟悉可以不提:

RST

RST:TCP首部中的6个标志比特之一,表示重置连接、复位连接。

在TCP协议中RST表示复位,用来异常的关闭连接,在TCP的设计中它是不可或缺的。发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。

出现RST包的原因:

  • 服务器端口未打开而客户端来连接时

  • 在一个已关闭的SOCKET上收到数据

  • 请求超时

  • 提前关闭

MSL

MSL(Maximum Segment Lifetime)报文最长存活时间

TCP高端

tcp窗口滑动以及拥塞控制

TCP协议作为一个可靠的面向流的传输协议,其可靠性和流量控制由滑动窗口协议保证,而拥塞控制则由控制窗口结合一系列的控制算法实现

滑动窗口协议

该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输,提高网络吞吐量。

为了减少发送方等待ACK的时间

个人总结的要点:

窗口:

①发送方和接收方都有一个序列号和一个窗口(窗口可以变化但是一般简单实现是固定的,只有它大于0才能操作)。

②发送方发送的时候检查序列号是否超出窗口。

③接收方接收数据是检查是否在窗口内,不在就丢弃。

滑动:

④只有接收窗口向前滑动并且ACK了,发送窗口才有可能向前滑动。

注意事项

(1)发送方不必发送一个全窗口大小的数据。

(2)来自接收方的一个报文段确认数据并把窗口向右边滑动,这是因为窗口的大小是相对于确认序号的。

(3)窗口的大小可以减小,但是窗口的右边沿却不能够向左移动。

(4)接收方在发送一个ACK前不必等待窗口被填满。

流量控制一一数据不要太快

TCP利用滑动窗口实现流量控制的机制

所谓流量控制,主要是接收方传递信息给发送方,使其不要发送数据太快,是一种端到端的控制。主要的方式就是返回的ACK中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。

就比如说,接收端会告诉发送端我的rwnd也就是缓冲区还能接收多少数据,并且引入了持续计时器(Persistence timer),当发送端收到对方没有窗口的通知时启动计时器,时间到则发送一个一字节的探测报文。目的就是为了解决一方等待通知,一方等待数据的死锁状态。

如何考虑流量控制中的传输效率

主要从两个方面提高

①一个是尽可能一次多发送几个字节

Nagle算法

该算法为了减少广域网的小分组数目,从而减小网络拥塞的出现

简单点的说就是它只允许TCP上最多只有一个未被确认的小分组,然后趁机收集所有的小分组,在ACK到来之时一次性以一个分组发出去。其中小分组的定义是小于MSS(Maximum Segment Size最大报文段长度)的任何分组。

这里给出它的伪代码,有兴趣的可以去看看Linux内核里面的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if there is new data to send #有数据要发送
# 发送窗口缓冲区和队列数据 >=mss,队列数据(available #data)为原有的队列数据加上新到来的数据
# 也就是说缓冲区数据超过mss大小,nagle算法尽可能发送足够大的数据包
if the window size >= MSS and available data is >= MSS
send complete MSS segment now # 立即发送
else
if there is unconfirmed data still in the pipe #前一次发送的包没有收到ack
# 将该包数据放入队列中,直到收到一个ack再发送缓冲区数据
enqueue data in the buffer until an acknowledge is received
else
send data immediately # 立即发送
end if
end if
end if

TCP_CORK 、TCP_NODELAY

这两个选项是互斥的,打开或者关闭TCP的nagle算法,实际上就是用来设置TCP连接的两个buffer(发送缓冲区和接收缓冲区)尺寸的

  • TCP_CORK:

尽量向发送缓冲区中攒数据,攒到多了再发送,这样网络的有效负载会升高。简单粗暴地解释一下这个有效负载的问题。假如每个包中只有一个字节的数据,为了发送这一个字节的数据,再给这一个字节外面包装一层厚厚的TCP包头,那网络上跑的几乎全是包头了,有效的数据只占其中很小的部分,很多访问量大的服务器,带宽可以很轻松的被这么耗尽。那么,为了让有效负载升高,我们可以通过这个选项指示TCP层,在发送的时候尽量多攒一些数据,把他们填充到一个TCP包中再发送出去。这个和提升发送效率是相互矛盾的,空间和时间总是一堆冤家!!

适用于:

webserver/下载服务器(ftp的发送文件服务器/需要带宽量比较大的服务器

  • TCP_NODELAY:

尽量不要等待,只要发送缓冲区中有数据,并且发送窗口是打开的,就尽量把数据发送到网络上去。

适用于:

涉及到交互的服务器,比如ftp的接收命令的服务器/HTTP服务器


②另一个是在窗口空余比较多的时候通知发送方一次多发送多个字节

ACK延迟确认

说到Nagle算法就不得不提和它配合使用的ACK延迟确认了

简单的说就是在发送数据的时候顺便把上一个的ACK也一起打包了

拥塞控制一一这样传数据会慢一点

这部分我们需要了解的就是四个点:

慢开始

简单来说就是刚开始发送数据的时候从1开始依次递增2的指数级,最后到达最大限制的话就使用下面的算法。

拥塞控制

主要就是拥塞避免算法:每经过一个往返时间RTT(Round-Trip Time 往返时延)就把发送方的拥塞窗口+1,即让拥塞窗口缓慢地增大,按照线性规律增长。

以上两个好基友经常作为一个整体使用,目的就是为了减少拥塞发生时发送的数据,使得发生拥塞的路由器有足够的时间来处理那些积压的分组。

快重传

字面意思,当包出现错误,立马重传。

快恢复

其实就是回到原点,在快重传之后回到最开始的慢开始。

以上两个好基友解决的则是减少拥塞发生时重传时间。
一个减少数据,一个减少时间,这样就达到了拥塞控制的目的。

总结

参考

TCP为什么需要3次握手与4次挥手
tcp窗口滑动以及拥塞控制