首先,我们需要知道TCP在网络OSI的七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在第二层——Data Link层,在第二层上的数据,我们叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。
首先,我们需要知道,我们程序的数据首先会打到TCP的Segment中,然后TCP的Segment会打到IP的Packet中,然后再打到以太网Ethernet的Frame中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。
《一》TCP协议头的结构:
----16位端口号:告知主机该报文段来自哪里(源端口号),又将传给哪个应用程序或上层协议(目的端口号)的
----32位序号:用来解决网络包乱序的问题,序号值被系统初始化为某个随机值ISN(初始化序号值),随后的序号值将被系统设置为ISN加上该报文段所携带的第一个字节在整个字节流中的偏移
----32位确认号:用来解决丢包问题,(ACK) ,它的值=序号值+1.
----4位头部长度:标识该TCP头部有多少个32位(4字节),因为4位最大能标识15,所以TCP头部最大有60个字节(15*4)。
----6位标志位:URG:标识紧急指针是否有效 ACK:表示确认号是否有效 SYN:表示请求建立连接
PSH:提示接收端应从TCP接收缓冲区读走数据,为接收后面的数据腾出空间
FIN:通知对方本端要结束连接 RST:(复位报文段)要求对方重新建起连接
----16位窗口大小:这里说的窗口就是滑动窗口,是TCP流量控制的手段,它告诉了本端TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据。
----16位检验和:由发送端填充,接收端对TCP报文段进行CRC算法,以检验报文段在传输的过程中是否有损坏。
----16位紧急指针:是一个正的偏移值,他和序号字段的值相加表示最后一个紧急数据的下一字节的序号。
注意:1:TCP的包是没有IP地址的,那是IP层上的事,但是有源端口号和目的端口号,
2:一个TCP连接需要四个元祖表示是同一连接(src_ip src_port dst_ip dst_port)准确的来说是五元组,还有一个是协议
《二》TCP 的状态机
其实,网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。
下面两个图真的是非常 非常的重要
图一:“TCP建链接”、“TCP断链接”、“传数据”
图二:TCP协议的状态机
我们要弄清楚TCP为什么要建立连接要3次握手?为什么断开连接要4次挥手?
对于建链接的3次握手:主要是要初始化Seq的初始值和分配资源。通信的双方要互相通知对方自己的初始化的Seq(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就图一中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。
对于4次挥手:因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。
大部分情况下建立连接需要三次握手,也不一定都是三次,有可能出现四次握手来建立连接的(对于有些TCP/IP的实现,可能不支持这种同时打开的情况)。如图三:
四次挥手如果两边同时断开连接那么两端可能出现完全一样的状态转移 FIN_WAIT1-->CLOSEING-->TIME_WAIT,也就会Client和Server最后同时进入TIME_WAIT状态。如图四:
在三次握手过程中,可能会产生以下疑问:
1:关于建连接时SYN超时:试想一下,如果服务器收到了客户端发送的SYN后给客户端回了SYN-ACK然后客户端掉线了,服务器没有收到客户端发过来的ACK,此时的连接处于一种“既没成功也没失败”的中间状态,这就需要一个超时时间让Server将这个连接断开,否则这个连接就会一直占用Server的SYN连接队列中的一个位置,大量这样的连接就会将Server的SYN连接队列耗尽,让正常的连接无法得到处理。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
2 : TCP 连接的初始化序列号能否固定:假如ISN固定为1,客户端和服务器建立好连接以后,客户端连续给服务器发送了10个包,很不幸这10个包在去往服务器的路上迷路了,碰巧此时客户端掉线了(可以说是很倒霉了)。然后客户端又以同样的端口号连接上服务器,并且给服务器发送了5个包,接着之前的那10个包又到达了服务器,服务器就给客户端恢复确认号11,此时客户端可能会表示整个人都不好了,什么情况?我的序号才到5,你怎么给我回的确认号是11?
RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL – Wikipedia语条),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。
3:关于SYN Flood攻击:服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这个时候攻击者就可以恶意地把SYN队列占满,正常的连接请求就不会被处理,Linux下给了一个叫tcp_syncookies的参数来应对这个事——当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。
4:为什么是3次握手呢?为什么不能是两次呢?试想一下,如果是两次握手的话,服务器给客户端发送了SYN-ACK后并没有收到客户端发来的ACK,此时服务器会一直给客户端发SYN,会把SYN队列占满,这样正常的请求连接就不会被处理了,
5:四次挥手能不能变成三次挥手呢?TCP是全双工通信,cli在自己已经不会在有新的数据要发送给Ser后,可以发送FIN信号告知Ser,这边已经终止Cli到对端Ser那边的数据传输。但是,这个时候对端Ser可以继续往Cli这边发送数据包。于是,两端数据传输的终止在时序上是独立并且可能会相隔比较长的时间,这个时候就必须最少需要2+2 = 4 次挥手来完全终止这个连接。但是,如果Ser在收到Cli的FIN包后,在也没数据需要发送给Cli了,那么对Cli的ACK包和Server自己的FIN包就可以合并成为一个包发送过去,这样四次挥手就可以变成三次了。
6:TIME_WAIT状态是用来解决或避免什么问题呢?为什么不直接给转成CLOSED状态呢?<1>可靠的终止TCP的连接:如果没有TIME_WAIT状态主动方收到被动方发送过来的FIN-ACK,然后给被动方回了一个ACK就进入CLOSED状态了,此时如果这个ACK在半路丢失了,被动方没有收到自己FIN的ACK,就会重传FIN,此时这个FIN包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个FIN包,协议栈会认为对方疯了,都还没建立连接你给我来个FIN包?,于是回复一个RST包给被动关闭方,被动关闭方就会收到一个错误,原本应该正常关闭的连接,给我来个错误,很难让人接受。<二>保证迟来的数据能被识别并丢弃:(你要知道,有些自做主张的路由器会缓存IP数据包)在这段期间,TCP发送端可能会因为确认超时而重新发送这个数据包,重新发送的数据包到达了目的地,如果没有TIME_WAIT状态就是直接进入CLOSED状态,那么这个连接成功的关闭了。(那个被路由器“扣留”的包到达时就会引起问题)。这时在关闭前一个连接之后,马上又建立起一个新相同端口的连接,前一个连接那个被路由器“扣留”的包在前一个连接终止后到达了,而被新连接收到了,这个时候就全乱了。所以为了避免这个情况,TCP协议不容许处于TIME_WAIT状态的连接启动一个新的连接,因为TIME_WAIT状态持续2MSL,就可以保证成功的建起一个新的连接时,来自旧连接的数据报已经在网络中被丢弃。(MSL:TCP Segment在网络上的存活时间,TIME_WAIT超时设置是2*MSL:触发被动端重发Fin,一来一去正好2个MSL)
7:TIME_WAIT会带来哪些问题呢?TIME_WAIT带来的问题主要是源于:一个连接进入TIME_WAIT状态后需要等待2*MSL(一般是1到4分钟)那么长的时间才能断开连接释放连接占用的资源,会造成以下问题:
1) 作为服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple,严重消耗着服务器的资源;
2) 作为客户端,短时间内大量的短连接,会大量消耗的Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法在发起新的连接