最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

白话tcp协议

热点资讯 loading 250浏览 0评论

一.tcp

1.为什么TCP客户端最后还要发送一次确认呢

一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。

image

image

如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。 如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

2.为什么四次挥手

一句话,只有服务器才知道数据是否发送完成,发送完成是有服务器告知客户端可以关闭了。 image

客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2 ∗ * ∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些

3.如果已经建立了连接,但是客户端突然出现故障了怎么办

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接

4.滑动窗口大小

w = 带宽 * 时延

  • w 滑动窗口大小
  • 带宽B 10M/s
  • 时延数据传输来回耗时tr tcprtt
  • 在W<B*Tr时,影响TCP发送数据速率的最直接的因素是滑动窗口的大小
  • 当W>B*Tr时,则影响速率的因素就是带宽
  • TCP的滑动窗口大小实际上就是socket的接收缓冲区大小的字节数
/* 
 * 设置发送缓冲区大小 
 */ 
snd_size = 10*1024;    /* 发送缓冲区大小为8K */ 
optlen = sizeof(10*1024); 
err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen); 


/* 
 * 设置接收缓冲区大小 
 */ 
rcv_size = 10*1024;    /* 接收缓冲区大小为8K */ 
optlen = sizeof(rcv_size); 
err = setsockopt(s,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen); 

5.滑动窗口原理

发送窗口在收到回复ack后,像后移动可用窗口个位置,并loading数据到发送缓冲区

6.队首阻塞问题

队头阻塞(head-of-line-blocking)发生在一个TCP分节丢失,导致其后续分节不按序到达接收端的时候。该后续分节将被接收端一直保持直到丢失的第一个分节被发送端重传并到达接收端为止。该后续分节的延迟递送确保接收应用进程能够按照发送端的发送顺序接收数据。

7.快速失败重传与超时重传

超时重传

  • 超时重传时间RTO略大于RTT,RTT是平均观测RTT
  • 当出现超时重传时,会将RTO翻倍,再次发送包

快速重传

  • 发送数据不等待ack,来判定超时
  • 而是根据如果出现三次以上ack等待同一个包序则判定失败
image

image

如图,所出现ack = 2 连续三次,则触发seq=2的包的重传。

8.拆包

  • 当发送包大小大于mss大小时,需要将包拆分为多个包发送
  • 如果发送端在较短的间隔内发送了多个mss包,则接收端收到的多个包需要在应用层来组合,比如http协议通过content-length来表示包大小。或者通过trun — 包 — 包 — 0 来区分。
  • 解决方式:通过自定义消息在包体中加入包内容大小的字段

9.quic对比

image

image

10.ack

ack的值为当前seq的值+包大小,当出现丢包时,ack的值不会发生变化直到收到该包。每个ack包含期望收到的包的id以及当前窗口大小。为了便于理解,这里就把它称为1号包。假定这个包的负载长度是100字节,那么可以推算出下一个包的编号应该是101 image

11.seq

  • tcp为了保证包的有序性和完整性,为每个包定义了一个seq值。
  • 第一个包的编号是一个随机数。每个数据包都可以得到两个编号:自身的编号,以及下一个包的编号。接收方由此知道,应该按照什么顺序将它们还原成原始文件。

12.tcp包大小

  • 以太网数据包(packet)的大小是固定的,最初是1518字节,后来增加到1522字节。其中,1500字节是负载(payload),22字节是头信息(head)。
  • tcp包需要在packet上减少ip包头20字节和tcp包头20字节。整体最大负载约为1460字节

13.一次发送tcp包大小

  • tcp一次发送包大小不是一个tcp包,而是一组tcp包。这组tcp包的大小由滑动窗口控制,与接受窗口和发送窗口大小直接相关。

14.应用层如何读取数据

  • linux中应用一般用socket来进行io通信。可以采用epoll机制将建立socket的句柄,添加到监听实现非阻塞式数据读取。或者用无限循环一直读取数据。
# epoll方式
connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &addrSize);
epoll_ctl(epollFd,EPOLL_CTL_ADD,connfd,&ev);

# 无限循环方式
while((data=bReader.readLine())!=null){
    //遍历集合client
    for(Socket s : client){
        //輸出發送的數據
        s.getOutputStream().write((data+"\n").getBytes());
    }
}
  • 应用层根据协议读取数据,如http协议根据content-length来读取响应的数据大小。

转载请注明:Android开发中文站 » 白话tcp协议

您必须 登录 才能发表评论!