以下来自湖科大计算机网络公开课笔记及个人所搜集资料
TCP的连接建立要解决三个问题:
6个控制位:紧急、确认、推送、复位、同步、终止
连接过程涉及的控制位:ACK、SYN
TCP首部格式:
SYN就是同步位,设为1就表明这是一个TCP连接请求报文段。
TCP规定,SYN被设置为1的报文段不能携带数据, 但要消耗掉1个序号。
消耗掉1个序号指的是 序号seq加1了。
seq:TCP报文段首部中的序号,在流量控制时,取值100表示TCP报文段数据载荷的第一个字节的序号是100,那个是发送数据的时候,这里不是。这里才在建立连接,不发送数据。seq被设置为一个初始值x,作为TCP客户进程所选择的初始序号(意思是随便选个数 字)。
ACK就是确认位,取值1,表示这是一个TCP确认报文段;
小写ack: 首部中的确认号字段(注意是第三行那个);(是对序号seq的确认,取值为seq+1)ACK = 1,确认号ack才生效,ack = N,则说明序号seq = N - 1为止的所有数据都已经正确收到。
窗口:16位的字段,由接收方填充,用来告知发送方当前本端还能接收的数据长度。零窗口通知:
因为接收方一直在接收并缓存数据,但是又没有处理,装不下来,就给对方发送一个0窗口的报文段。TCP的连接一方会启动持续计时器,并在计时器到期发送一个零窗口探测报文段,对方会发来此时的窗口值,要是依然是0,那就继续重启计时器,直到不是0.
最初两端的TCP进程都处于关闭状态,一开始 TCP服务器进程首先创建传输控制块
,用来存储TCP连接中的一些重要信息,例如 TCP连接表,指向发 送和接收缓存的指针,指向重传队列的指针,当前发送和接收序号等。
然后,准备接受TCP客户进程的连接请求。此时TCP服务器进程就要进入监听状态
,等待TCP客户进程的连接请求。TCP服务器进程是被动等待来自TCP客户进程的连接请求(调用listen()函数),而不是主动发起,因此称为被动打开连接
。——这里说的是连接被动,但是listen()还是主动调用的。
TCP客户进程也是首先创建传输控制块
,(创建socket()时)然后在打算建立TCP连接时,向TCP服务器进程发送TCP连接请求报文段,并进入同步已发送状态
1—同步位SYN
,设为1就表明这是一个TCP连接请求报文段。客户端第一个seq = x,是客户进程选择的初始序号;
2—服务器给客户进程发送TCP连接请求确认报文段并进入同步已接收状态(同步位SYN
和确认位ACK
都设为1,表明这是一个连接请求确认报文段) ,序号seq设置为初值y,作为服务器进程所选择的初始序号;确认位ack设置
为x + 1表明这是对客户端进程所选初始序号的确认
上面两个都是SYN为1,不能携带数据
3—发送连接请求的确认报文段的确认(见上图绿字),ACK = 1
说明是确认报文段,还需要seq + 1,即seq = x + 1,还需要对服务器所选初始序号进行确认,因此确认号ack = y + 1
第三次握手没有SYN,可以发送数据(http请求报文
就是在TCP的第三次握手
中携带在TCP的数据载荷部分)
TCP规定,普通的TCP确认报文段可以携带数据,但如果不携带数据则不消耗序号。
那么客户端在三次握手之后,发送给服务器端的数据报文seq仍然是 x + 1
假设改为2次握手:
之前三次握手是,TCP服务器发完对TCP连接请求的确认报文后就进入同步已接收状态,那么两次握手情况下,就是直接进入连接已建立状态了。TCP客户端也是进入连接已建立状态(这个之前就是),只是不再发确认报文了。
这节课介绍这里内容的角度是这样的:
假如一开始TCP客户端发的TCP连接请求滞留在了网络,从而引发了==超时重传==,于是重新发送了下TCP连接请求,这个时候TCP服务器收到了,那么按照2次握手,然后建立连接后就发送数据,最后释放连接,两者处于关闭状态。
然后之前滞留在网络中的TCP连接请求到达了TCP服务器,它会以为是TCP客户端又发来了新的连接请求,给客户端发送针对TCP连接请求的确认,然后进入==连接已建立状态==(2次握手嘛),注意,这个时候服务器的操作系统是需要消耗进程的资源的,维持TCP通信是需要进程资源,也就消耗内存,因为TCP通信是进程间通信。
这个时候TCP客户端不会理会服务器,因为它没有发起连接请求,于是TCP服务器一直处于连接请求状态,这个时候消耗服务器的资源!
如果是三次握手就不会呀,TCP服务器没收到第三次握手的那个报文,即针对TCP连接请求的确认的确认报文,是不会进入已建立连接状态,而是同步已接收状态。——这个等久了可能就会自动关闭,还需要深入了解下。
三次握手不多余,这是为了防止失效的连接请求报文段突然又传到了TCP服务器,因而导致错误。
小林coding说的三次握手最主要是防止历史连接初始化了连接,其实这里的红色线,就相当于历史连接。当然两次握手还有其它弊端,这门课只讲了这个,在小林coding中还讲了其它问题,但也强调了两次握手最主要的还是历史连接问题。
回顾下上一节中的TCP报文首部,尤其是6个标记位。
首先TCP客户进程上层的应用进程,要求TCP客户进程关闭TCP连接(应用程序调用close(socketFd);),于是TCP客户进程发送TCP连接释放报文。FIN=1,ACK=1,表明这是一个TCP连接释放报文段,同时也对之前收到的报文段进行确认即也是一个确认报文。(注意没有什么专门的TCP客户进程,其实就是创建了套接字并采用TCP通信的那个应用进程)
TCP规定终止位FIN等于1的报文段,即使不携带数据也要消耗掉一个序号。(所以TCP客户端第二次发的seq=u+1)
序号seq字段的值设置为u,等于TCP客户进程之前已传送过的数据的最后一个字节的序号 +1;确认号ack字段的值设置为v,它等于TCP客户进程之前已收到的数据的最后一个字节的序号 +1.
TCP服务器进程收到TCP连接释放报文段后,会发送一个普通的TCP确认报文段,并进入关闭等待状态(还会通知服务器的应用进程对方要断开连接)。该报文段首部中的确认为ACK的值,被设置为1,表明这是一个普通的TCP确认报文段。
1—FIN = 1表示这是一个连接释放报文段,ACK = 1是对之前收到报文的确认,这是个确认报文段,seq和ack的值就是跟根据之前传的相应报文字段+1(FIN和SYN一样,不携带数据依然消耗序号)
2—服务器收到后发送一个普通的TCP确认报文,ACK = 1;TCP服务器进程通知上层应用层,说对方要断开连接。此时,从TCP客户进程到TCP服务器进程这个方向的连接就释放了,这时的TCP连接属于半关闭状态,客户进程进入终止等待2状态。
这里服务器还可以给客户端发送数据,如图,在2,3次挥手之间。所以第三次的seq不一定是v + 1
3—TCP客户进程没有数据要发送了,但TCP服务进程如果还有数据要发送,客户进程还是要接收,即服务器到客户进程的连接并未关闭。如果服务器没有数据要发送了,那么上层就通知服务进程释放连接,然后就发送FIN = 1的连接释放报文段,并进入最后确认状态。还要设置ACK为1表明是确认报文段,因此ack = u+1,2和3次挥手,第三次是对客户端的重复确认
4—客户端收到后发送普通的确认报文段,ACK设置为1,并进入时间等待状态。经过2MSL之后再进入关闭状态
MSL一般2分钟,那么客户端4分钟后进入关闭状态。可以取更小的值,现在的网络更快。
如果TCP客户进程直接关闭了,那如果第四次挥手,即TCP客户端给TCP服务器发的确认报文段丢失了,会出现TCP服务器无法关闭。因为TCP客户端发完就直接关闭了,那TCP服务器那边因为没收到TCP客户进程的确认报文,就会发起超时重传(即重传第三次挥手的报文FIN),结果TCP客户端那边关了,没接收,然后服务器就会一直发,从而无法进入CLOSED状态。
因此客户端别急着关闭,等待2MSL时长,一方面,可以确保服务器收得到最后一个确认报文段而进入关闭状态。
另一方面,在这2MSL时间,可以使本次双方的TCP连接持续时间内产生的所有报文段都从网络中消失,这样下一个连接中也不会出现旧连接中的报文段。(在网络上延迟到的包,叫lost duplicate)。否则之前的连接请求又来了。因为TCP有超时重传,那些延时的包已经被重传了,服务器收到了,等超时的包(要么是在路由器上消耗掉了跳数TTL,要么还是到达服务器了),到达服务器后,因为它的重传包先到过,于是它会被TCP协议栈抛弃。
MSL是报文在网络中能存活的最长时间,如果没有time-wait,那么服务器的第三次回收,FIN包会影响下一次连接,下一次客户端和服务器连接后,会突然收到服务器的FIN报文,此时服务器成为了主动关闭一方,使得连接断开。
——所以TCP第一次握手之前也就不会出现之前延迟到达的报文了。
那为什么该状态设计在主动关闭这方?
因为四次挥手最后一次的ACK是主动关闭这一方发的。——对应2MSL的第一个作用
只要有一方保持TIME_WAIT状态,就能让这次连接的报文都消失,不需要双方都等 。——对应第二个作用
(主动关闭方有时候是服务器,比如异常下线)
试想这种场景:正在连接的双方,客户端突然出现故障,但是又没给服务器发送连接释放报文。
当然这个时间是可以设置的。
少哪次?
如果是最后那次,那道理和少TIME_WAIT一样,主要是怕FIN报文干扰下一次的连接,双方在连接中,结果突然来了一个客户端延时到达的FIN,这个时候服务器就以为要关闭了。
如果是少第二次,那客户端会以为对方没收到,所以服务器一定要回应
如果是少第三次,因为服务器还有数据需要发送,这个时候处于半关闭,客户端不能发数据但可以接收数据
网络编程书里称之为:优雅的断开连接。即不是简单粗暴的关闭双方的收发,而是控制只关闭接收或发送的一个,比如只让当前服务器接收数据而不发送数据。
四次挥手过程中的客户端,在第二次挥手收到服务器的ACK后就处于半关闭状态,即不再发送数据,但能接收数据。当然这个是协议层面的,而网络编程里是用户层面,可以由程序员控制的。
使用shutdown()函数,第一个参数是sockfd,第二个参数是关闭方式,这里就可以填半关闭:
SHUT_RD 断开输入流
SHUT_WR: 断开输出流
SHUT_RDWR: 同时断开输入输出流
如果服务器这边发送完数据后,使用SHUT_WR方式关闭,那么服务器会给客户端发送EOF,表示不会再发送数据了,然后服务器的输出流关闭了,不过还能接收客户端的消息,因为输入流没关闭。