TCP/IP学习笔记

TCP/IP采用分层设计,从下到上分别为物理层、数据链路层、网络层、传输层、应用层。发送数据时,应用层把数据交给传输层,传输层加上自己的包头,再交给网层每层加上自己的包头后,把数据发送到网络中,当数据到达目的后,再一层层的拆包,最终交给接收缓冲区,并通知应用层读取。下面简要介绍TCP从建立连接到传输数据的流程。

一、名词解释

  • MTU(Maximum Transmission Unit,最大传输单元):在RFC894中规定MTU的最大值为1500字节。MTU = IP包头 + TCP包头 + MSS,一般建议MSS<1400字节(1500-IP包头20-TCP包头20-可选项),如果超过1400字节,就可能产生ip分片。
  • MSS(TCP Maximum Segment Size,TCP最大报文段长度):表示TCP一次性可往另一端发送数据的最大数据长度(不包含包头)。如果单次发送数据超过MSS,且没有设置DF(Dont’t Frament)标志,就会产生IP分片。
  • wscale(扩大窗口选项):使得窗口最大值可以从16bit增加到32bit。在图4 第1行中,客户端的win:64240, wscale:8,扩大后的窗口大小为:64240*x2^8 = 16445440 原文
  • MSL(Maximum Segment Lifetime,报文最大生存时间):任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,linux上默认为30s。在IP层有个叫TTL的计数,每经过一个路由器就会减1,为0时,包会被路由器丢弃。

二、建立连接

1.TCP数据报格式
  • 源端口号:数据发送方的端口号
  • 目的端口号:数据接收方的端口号
  • 序号(seq):TCP是可靠的传输协议,会对每个包编号,便于确认和重传。
  • 确认序号(ack):收到的序号+1(ack=seq+1),只有ACK标志位为1时,确认序号字段才有效。
  • 标志位:SYN:发起一个新连接; ACK:确认序号; FIN:用于四次挥手过程释放一个连接; RST:直接关闭连接,不走四次握手(比如:进程挂了);
  • PSH:发送方会立即发出数据,接收方收到这个报会立即文交给应用层。
  • 16位窗口大小:用于动态反映接收端读缓冲大小。
  • 数据:应用层数据
2.IP数据报格式
  • IP数据=TCP层包头+数据
3. 三次握手
1.1 握手流程:

1.客户端服务器都处于关闭(CLOSE)状态
2.服务器开始监听(LISTEN)
3.客户端发送连接请求(SYN)进入(SYN-SENT)状态
4.服务器收到SYN并回复SYN+ACK进入(SYN-RCVD)状态
5.客户端收到ACK+SYN并发送ACK进入(ESTABLISHED)状态
6.服务器收到ACK进入(ESTABLISHED)状态

1.2 握手抓包分析
  • 第1行:客户端向服务器发送SYN,除seq和win以外,options中包含了2个重要内容:mss和wscale
  • 第2行:服务器向客户端发送ACK,SYN, win, mss, wscale
  • 第3行:客户端向服务器发送ACK,win。
  • 注意:序号,win是时时变化的,每个包都要发送,而mss, wscale只会在建立连接时告知对方,建立连接后不会变(当然mss不是绝对不变,这跟每个包经过的路由器的mtu相关)。

三、传输数据

连接建立好后,双方就可以正式传输数据了,在传输数据过程中会涉及到数据包的分片、重传、流量控制等。

1.1 缓冲区

每条TCP连接的双端都会有一个发送缓冲区和接收缓冲区,大小可调整(Linux下可以通过 cat /proc/sys/net/ipv4/tcp_wmem 和 cat /proc/sys/net/ipv4/tcp_rmem查看读写缓冲区的大小, setsockopt 函数设置SO_RCVBUF和SO_SNDBUF选项来分别调整SOCKET读缓冲区和写缓冲区的大小)。

  • send:当应用层调用send()函数后,会把待发送的数据拷贝到发送缓冲区中,但数据何时发,每次发送的大小对应用层来说是透明的。所以send()函数的返回值并不是向网络中发送了多少数据,而是向发送缓冲区拷贝的字节数。
  • recv:从缓冲区中收数据,当接收方的传输层收到数据后,会把数据放到接收缓冲区中,内核通知进程可读,进程层调用recv()函数从接收缓冲区中拷贝数据到应用层,所以recv并不是从网络中读取数据。
  • 当发送缓冲区满:调用send函数就会阻塞,直到缓冲区中有足够的空间能容纳本次send数据
  • 当接收缓冲区满:接收方的win大小会变为0,发送方传输层停止向接收方发数据,若发送方应用层还在继续发数据,就会把自己的发送缓冲区填满后阻塞。
1.2 IP分片

TCP在传输数据时,对包的大小有限制,超过这个限制就会把数据拆分成多个包。MTU在建立连接时确定,这个值是由建立连接的数据包经过所有路由器的MTU最小值。(有兴趣可以登陆路由器控制台查看)

  • IP分片的问题:如果单次发送数据大于MSS,链路层会把数据包拆分成多个<=MSS分片再发送,发送过程中,只要有其中一个分片丢失,整个数据就会重传,这里没有断点续传,即使最后一个分片丢失,这个包的所有分片都会重传。所以单次发送数据尽量<=MSS。 当然也不能分得太小,太小了效率会降低,毕竟包头是有开销。
1.3 拥塞控制与滑动窗口

TCP是面向连接的、可靠的传输协议,下面说说它是如何保证高效和可靠的。

  • 拥塞控制:拥塞控制目标是在拥塞发生时能及时发现并通过减少数据报文进入网络的速率和数量,达到防止网络拥塞的目的,这种机制可以确保网络大部分时间是可用的。拥塞控制的前提在于能发现有网络拥塞的迹象,TCP/IP协议栈的算法是通过分组发送超时和收到对端对某个分组的重复ACK。

  • 慢启动:当新链接上的数据进入一个拥塞状况不可预知的网络时,贸然过快的数据发送可能会加重网络负担。TCP通过参数拥塞窗口(cwnd,Congestion Window)和慢启动门限(ssthresh,Slow Start Threashold)来决定向网络中发送数据的速度。算法如下:
    1.TCP链接建立好以后,cwnd初始化1MSS(一次能发送数据的大小为1MSS)。ssthresh初始化为65535字节;
    2.每当收到一个ACK,cwnd++,cwnd程线性上升;
    3.每当经过一个RTT(Round Trip Time,网络往返时间),cwnd = cwnd * 2,cwnd呈指数让升;
    4.当cwnd >= ssthresh时,就会进入”拥塞避免算法”;

  • 拥塞避免:只要判断网络出现拥塞,就要把慢启动开始门限(ssthresh)设置为发送窗口的一半(win>2时),cwnd(拥塞窗口)设置为1。
    注意:步骤2,3发送数据量不能超过cwnd和接收方通告的TCP窗口大小(win),现在大部分linux中,TCP拥塞窗口(cwnd)默认值为10,即第一个RTT最多可发送10k数据,如果初始值为1,发送10k数据大概需要3个RTT,如下图所示。

  • 滑动窗口:在建立TCP连接三次握手时,连接的双方会根据自己接收缓冲区的大小初始化接收窗口,并发给对方。双方根据对方的接收窗口大小初始化自己的发窗口。滑动窗口可确保发送方的每个数据都正确到达接收方。

  • 窗口滑动相关术语:
    1.TCP窗口左边沿向右边沿靠近称为窗口”合拢”,发生在数据被发送和确认时。如果左右边沿重合,则形成一个零窗口,此时发送方不能再发送任何数据;
    2.TCP窗口右边沿向右移动称为窗口”张开”,发生在接收方应用层已经读取了已确认过的数据并释放了TCP接收缓冲区时;
    3.TCP窗口右边沿向左移动称为窗口”收缩”,RFC强烈建议避免使用这种方式;

  • 发送缓冲区和发送窗口分为四个部分:
    1.已发送并收到ACK确认的数据(即已成功到达客户端),0-3部分;
    2.已发送还未收到ACK确认的数据(即已发送但尚未能确认已被客户端成功收到)4-5部分;
    3.处于发送窗口中还未发出的数据(即对端接收窗口通告还可容纳的部分),6-11部分;
    4.处于发送窗口以外还未发出的数据(即对端接收窗口通告无法容纳的部分),12-15部分;

四、断开连接

1.1 挥手流程

1.双方已经建立连接(ESTABLISHED)
2.客户端发送FIN后进入(FIN-WAIT-1)
3.服务器收到FIN后回复ACK进入(CLOSE-WAIT)
4.客户端收到ACK后进入(FIN-WAIT-2)
5.服务端发送ACK+FIN进入(LAST-ACK)
6.客户端收到ACK+FIN进入(TIME-WAIT(等待2MSL)后进入CLOSE状态)
7.服务端收到ACK进入(CLOSE)

之所以等待2MSL主要有2个原因:
1. 2MSL可以确保在建立新连接前,老的报文在网络中消失
2. 主动关闭方需要确保自己最后发送响应对端FIN的ACK能被对端收到,如果对端出现超时重传了FIN,则意味着自己上次发的ACK丢失了,那么自己还有机会再次发送ACK确认,乘以2就是为了给重传的ACK充裕的到达时间。

1.2 断开连接抓包
1.3 TCP状态机

加黑加粗的部分,时序时序图的主要部分,阿拉伯数字的序号,是连接过程中的顺序,而大写中文数字的序号,是连接断开过程中的顺序。加粗的实线是客户端 A 的状态变迁,加粗的虚线是服务端 B 的状态变迁