TCP小结

简介

TCP是传输层最重要的协议,相比于另外一个也很重要的UDP协议,TCP协议有如下几个特点:

  1. 面向连接,端到端全双工通信
  2. 面向字节流
  3. 可靠传输
  4. 流量与拥塞控制

TCP依赖于底层不可靠的IP协议,网络层的IP数据报路由是不可靠的,一不保证顺序,二没有出错丢失重发等机制,所以要想实现可靠传输,传输层必须要有各种各样的控制。

本文内容大部分来自谢希仁的《计算机网络》以及《图解TCP/IP》两书,图也基本截至两书,除此之外是自己的一些抓包验证。


TCP实现可靠传输的基本思路

在TCP层,发送方发送完一个分段以后,等待接收方的收到确认响应(ACK),收到确认响应后发送方才继续发送下一个分段,如果一定时间内没收到响应,则重新发送该分组。这样一收一发的机制既保证了按序到达,又保证了数据的完整。这种思路也称为:“停止等待”协议。

这里写图片描述

但是,除了上述两种情况,还有可能是接收方返回确认给发送方时丢失了或者还滞留在网络中,这样会导致发送方再次发送分段,这对发送方无可避免,因为它不能确认接收方究竟收到了分段没有,所以只能认定是没收到的。而对于接收方来说,则需要丢弃该重复收到的分组。上述的这些确认应答、重发控制以及重复控制等功能,都是通过序列号来实现的。

TCP是面向字节流的,所以分段数据的每一个字节都被编号,也称为序号(seq)。当接收方收到序列号为1,长度为1000的数据时,会返回一个1001的确认应答号(ack)给接收方,表明:我已经收到1~1000字节的数据了,下一次发送的数据从第1001个字节开始。

这里写图片描述


TCP实现可靠传输的具体实现

上一小节的思路会有一个很明显的问题,传输效率太低了。要提升传输效率,一个比较直接的想法就是发送方可连续发送多个分组,到达一定阈值后才开始等待确认。收到一个确认应答后,发送方的发送窗口则向前滑动,这种思路称为:“滑动窗口协议”,其中可连续发送的最大分段数(实际上是字节数,为了方便理解一般都会说分段数)即为窗口大小。

这里写图片描述

以上图为例,发送方主机A连续发送四个分段,当主机A收到2001确认应答号时,则将发送窗口向前滑动1段,此时发送方便可以继续发送数据的5001~6000字节了。

当发送方把四个分段的窗口数据都发送完后,接收方首先收到第三个分段,前文提到接收方的确认应答号(ack)表明的是“前面的数据都已经收到了,告诉发送方下一个发送分段的序列号(seq),那么这时接收方要不要发送确认(ACK)呢?发吧好像走回失序的老路,不发干等着又甚至有可能引发发送方的超时重传,问题还不知道前面两个分段究竟丢了没有。接收方不能坐以待毙,所以当收到第三个分段时,接收方返回的应答确认号是第一个分段的确认应答号。当发送方连续收到三个重复的确认应答号时就会认为这个分段真的已经丢了,进行重发,这样做能促进前面的分段赶快发完。这种方法称为“快重传”。

这里写图片描述

上图表示窗口为6个分段,发送完1~6个分段后,收到第一个分段确认应答号后,窗口向前移动1段,继续发送第7个分段,第2个分段丢失,后面收到的分段都返回第2个分段的确认应答号,发送方连续收到三个确认后重发第2个分段。当接收方收到第2个分段后,返回的确认应答号便可以一下子跑到了第7个分段,然后发送方一下滑动6个分段的窗口….


流量控制

首先需要明确流量控制是一个端到端的问题,发送方如果发送的数据太多,而接收方没有能力去接收的话,会导致发送方收不到接收方的ACK而进行重传。所以TCP流量控制,就是让发送端根据接收端的实际接收能力控制发送的数据量。它的原理比较简单,就是接收方将接收窗口大小返回给发送方,然后发送方的发送窗口不能大于这个值。这样即可实现接收方对发送方的流量控制。

如果接收方在接收过程中接收能力溢出,会发送一个0大小的窗口值给发送方。后续可以接收数据时,会发送窗口更新通知会发送方,如果这个通知不幸丢了,发送方也能通过定期发出的窗口探测从接收方得到新的窗口大小。

这里写图片描述


拥塞控制

相比于流量控制是端到端的控制,拥塞控制这是整个网络全局性的控制,试想一下,如果所有发送方主机一开始都可以无节制的发送数据,则容易造成网络的拥堵,如果网络拥堵了还继续发送大量数据,只会造成网络更加瘫痪。所以拥塞控制,是网络全局性的控制。

拥塞控制同样是通过限制发送方的发送窗口来实现的,这个窗口称为拥塞窗口。由此可知,发送方的实际窗口大小是由接收方的接受窗口和拥塞窗口共同决定的,取其中较小值。

拥塞控制主要由四个算法构成:

a. 慢开始(slow start)
  当发送方开始发送数据时,如果立即把大量数据直接注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚当前网络的负荷情况。所以较好的方法是,由小到大逐渐增大发送的拥塞窗口值。通常一开始发送分段时,先把拥塞窗口设置为一个MSS(最大传输分段)值,收到ACK后拥塞窗口加一个分段大小,此时可以发出两个分段数据,收到两个ACK后拥塞窗口又加二,这样一轮一轮下去拥塞窗口呈指数增长。
  
b. 拥塞避免(congestion avoidance)
  当慢开始算法不断指数增长时,显然到后面会指数爆炸,所以需要一个阈值去限制,当到了这个阈值后,每次收到ACK后不再暴力拥塞窗口直接加一个MSS,而是线性增长,使得每一轮不再增加一倍,而是增加一个MSS。
  
  比如现在MSS是1000个字节,拥塞窗口是2个MSS,慢开始每次收到ACK都增加一个MSS,一轮ACK下来拥塞窗口变成了4,这是指数增长的;而如果拥塞避免的阈值是4,收到一个ACK拥塞窗口增加的大小是1000 / 4 = 250个字节,收到4个ACK后,拥塞窗口才增加一个MSS的值,这是线性增长的。

c. 快重传(fast retransmit)
  快重传算法,在”实现”小节已经提到,即接收方收到失序的报文段时发出的确认应答号不是当前收到的报文段的,而是已经收到的连续报文段的确认应答号,发送方连续收到三个重复确认应答号后会重发应答号的报文段。
  
d. 快恢复(fast recovery)
  快恢复是配合快重传算法的。如果没有使用快重传,会比较容易出现超时的状况,当超时未收到ACK时,拥塞避免阈值会变为当前窗口的一半,然后把当前拥塞窗口设为1个MSS,进行慢开始算法,即指数增长,当增到阈值后才进行拥塞避免算法。
  而如果使用了快重传,超时的状况相对比较不容易出现,当发送方连续收到三个重复应答确认后,拥塞避免阈值同样会变为当前窗口的一半(也可能是一半+3个MSS),然后进行的是拥塞避免算法,即线性增长。而如果超时了仍然是拥塞窗口设1,进行慢开始算法。
  
虽然不同操作系统可能实现的具体方式有点不同,但是其基本思路都是一致的:

  1. 慢开始:从较小的窗口值比如1个MSS,指数增长拥塞窗口;
  2. 拥塞避免:到达阈值后,线性增长拥塞窗口;
  3. 快重传:接收方收到失序报文段会发出已收到的连续报文段的重复确认号;
  4. 快恢复:发送方收到三个重复确认号,拥塞窗口阈值变为当前窗口一半,进行拥塞避免算法。如果超时,拥塞窗口变为当前窗口一半,从1个MSS开始,进行慢开始算法。

这里写图片描述

上图的TCP Tahoe版本可以视为超时的状况


连接管理与Java相关抓包

TCP是面向连接的,所以在数据通信开始之前需要先做好两端的准备工作,相较于UDP非面向连接,不知道对方究竟能否连通就发出数据,TCP在发出数据之前已经确认对方是可连通的,这样就不会浪费带宽。TCP一个连接的建立和断开,正常情况下需要来回发送7个包才能完成,也即常说的三次握手,四次挥手。有了前面可靠传输的铺垫,握手和挥手的过程应该是比较好理解的。
这里写图片描述
三次握手服务端的回包既是客户端的连接确认应答,也是服务端的连接请求。

这里写图片描述
四次挥手是需要两个方向各自断开的,因为客户端(也可以是服务端先)断开后,客户端不能发送给服务端,但仍能接收从服务端发过来的数据,这时称为半关闭状态。

最后我们通过Java Socket和HttpUrlConnection两个API来抓包验证一下TCP的连接管理,另外也会验证相关事宜。

Socket

// Code
Socket socket = new Socket();
SocketAddress address = new InetSocketAddress("www.sina.com.cn", 80); // DNS Block
long start = System.currentTimeMillis();
socket.connect(address); // TCP Block
long cost = System.currentTimeMillis() - start;
System.out.println("connect cost: " + cost);

Thread.sleep(5000);

long closeStart = System.currentTimeMillis();
socket.close();
long closeCost = System.currentTimeMillis() - closeStart;
System.out.println("disconnect cost: " + closeCost);

这里写图片描述

打印内容

connect cost: 37   // 比ACK - SYN即三次握手的时间会稍大一点,程序耗时
disconnect cost: 0 // 感觉断开是异步调用,马上返回的...

由抓包内容可以看出,三次握手基本和上面的内容吻合,但是连接断开就未必是四次挥手了,如果客户端断开后服务端也马上断开,也可以是“三次”挥手的,其过程和三次握手几乎一样。

另外,上述这种写法Socket#connect是不包含DNS的,DNS发生在SocketAddress address = new InetSocketAddress("www.sina.com.cn", 80);而如果Socket s = new Socket(host,port)这种写法,则包含DNS+三次握手建立连接。

Socket#connect过程包不包含ssl握手呢?
这里写图片描述
从抓包结果看也是不包含SSL握手的,其实应该是不言而喻的,只是三次握手而没有传送数据,怎么能完成SSL握手呢…..

小结:Socket#connect就是用于TCP三次握手建立连接的,但是Socket socket = new Socket(host,port);这个API会包含DNS过程。

HttpUrlConnection

// Code
URL url = new URL("http://www.qq.com");
// URL url = new URL("https://www.zhihu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
long start = System.currentTimeMillis();
connection.connect();
long cost = System.currentTimeMillis() - start;
System.out.println("connect cost: " + cost);

// connection.getInputStream();
Thread.sleep(5000);

long closeStart = System.currentTimeMillis();
connection.disconnect();
long closeCost = System.currentTimeMillis() - closeStart;
System.out.println("disconnect cost: " + closeCost);

Http协议是基于TCP协议的,所以connect过程和Socket#connect过程一致
这里写图片描述

当把connection.getInputStream();这句注释去掉时,会有如下现象:
这里写图片描述

HttpUrlConnection如果单纯调用connect只是进行三次握手建立连接,TCP层是不会发生数据传输的,而一旦调用了getInputStream,则底层便会马上在缓冲区发生数据传输而不管应用层是否真的有去读这些数据。

最后,HttpUrlConnection#connect是包含DNS以及SSL握手的。
这里写图片描述
这里写图片描述

connect cost: 412 // dns200ms, syn180ms

去掉URL url = new URL("https://www.zhihu.com");注释
这里写图片描述

connect cost: 456

本人的Java环境如下,不同的Java环境或者网络API可能会有所不同,比如HttpClient和OkHttp的实现是不是一样的就不知道了,不同环境可能都需要具体抓包信息。
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值