运输层TCP协议总结

运输层TCP协议总结


TCP协议的特点

  • TCP是面向连接的运输层协议。就是说,应用程序在使用TCP协议之前,必须先建立TCP连接。在传送完数据之后,必须释放已经建立的TCP连接。
  • 每一条TCP连接只能有2个端点。TCP连接提供点对点的服务,这里的端点指的是socket套接字。根据RFC793的定义:端口号拼接到IP地址之后即构成了套接字。
  • TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复、 并且按序到达。
  • TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能收发数据。
  • 面向字节流。虽然应用进程和TCP交互的方式是每次一个大小各不相等的数据块,但TCP把应用程序交付下来的数据仅仅看成是一连串无结构的字节流。TCP不能保证接收方应用程序所接收的数据块与发送方所发送的数据块具有对应大小的关系。但TCP能保证接收方收到的字节流和发送方发出的字节流完全一样。

TCP报文段的首部格式

TCP报文段的首部格式

  • 源端口和目的端口:各占2个字节,分别写入源端口号和目的端口号。
  • 序号:占4个字节,序号范围为0到2的32次方-1,序号增加到2的32次方-1之后,下一个序号变为0,在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。首部中的序号字段值指的是本报文段所发送的数据的第一个字节的序号。可对4GB的数据进行编号。在一般情况下可保证当序号重复使用时,旧序号的数据早已通过网络到达终点了。
  • 确认号:占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。记住:若确认号是N,则表明:到序号N-1为止的所有数据都已正确收到。
  • 数据偏移:占4位,它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远,这个字段实际上是指出TCP报文段的首部长度。
  • 保留:占6位。保留为今后使用,目前置为0
  • 6个控制位
    • 紧急URG(URGent):当URG=1时,表明紧急字段有效,告诉系统此报文中有紧急数据,应尽快传送。于是发送方TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。这时要与首部中紧急指针字段配合使用。
    • 确认ACK(ACKnowlegment):仅当ACK=1时确认号字段才有效,TCP规定,连接建立后所有传送的报文段都必须把ACK置1。
    • 推送PSH(PuSH):当PSH=1时,表明该报文段需立即发送,并且接受方收到此报文段后会尽快地将其推送给接收应用进程,而不是等整个缓存都填满了再交付。一般不用。
    • 复位RST(ReSeT):当RST=1时,表明TCP连接中出现严重错误,必须释放连接,然后再重新建立运输连接。
    • 同步SYN( SYNchronization):在连接建立时用来同步序号,当SYN=1时,表明这是一个连接请求报文段或连接接受报文。
    • 终止FIN(FINis):用来释放一个连接,当FIN=1时,表示此报文段的发送方的数据已发送完毕,并要求释放运输连接。
  • 窗口,占2个字节,窗口指的是发送本报文段的一方的接收窗口,不是自己的发送窗口,告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。窗口值作为接受方让发送方设置其发送窗口的依据。
  • 校验和,占2字节。校验和字段检验的范围包括首部和数据这两部分。
  • 紧急指针:占2个字节,紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数。当所有紧急数据处理完毕时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为0时也可发送紧急数据。
  • 选项:长度可变,最长可达40字节,当没有选项时,TCP的首部长度是20字节。
    • 最大报文段长度MSS选项,指的是每一个TCP报文段中的数据字段的最大长度。

TCP连接的建立与断开

连接的建立之三次握手

Created with Raphaël 2.1.0 服务器 服务器 客户端 客户端 LISTEN SYN = 1 , seq = m SYN-SENT ACK = 1 , SYN = 1 , seq = n , ack = m+1 SYN-RCVD ACK = 1 , seq = m+1 , ack = n+1 ESTABLISHED ESTABLISHED
  1. 服务器端进程优先被启动,并创建传输控制块TCB,之后服务器进程进入监听LISTEN状态。
  2. 客户端进程启动,并创建传输控制块TCB。之后客户端主动向服务器发送请求TCP同步报文,其首部SYN位置1,同时选择m做为初始序列号seq的值。该报文段不可携带数据,但仍需消耗一个序列号。随后客户端进程进入SYN-SENT状态。
  3. 服务器端进程收到来自客户端的同步请求报文段,如同意客户端请求,则返回一个同步确认报文,其首部SYN位、ACK位同时置1,取n做为初始序列号,确认号为m+1。以通知客户端其同步请求已被接受,同时服务器也要向客户端申请建立服务器到客户端的TCP链接。同样地此报文段不可携带数据,但仍需消耗一个序列号。随后服务器进程进入SYN-RCVD状态。
  4. 客户端进程收到来自服务器的确认报文,得知其同步请求已被接收,此时客户端到服务器的TCP链接已被建立,接着客户端也要向服务器发送一个确认报文,以示其同意服务器的同步请求。该报文段首部的ACK位置1,序列号为m+1,确认号为n+1。此报文段可以不携带数据,若如此做则不消耗序列号,序列号m+1赋给其下一个报文段。客户端进入ESTABLISHED状态。
  5. 服务器收到客户端发来的确认报文,此时服务器到客户端的TCP链接被创建,服务器也进入ESTABLISHED状态。
那么为什么是3 次握手而不是2次或者4次呢?
  • 若两次握手即可建立连接,那么若客户端向服务器发送了同步请求,而一段时间后仍未收到服务器确认请求,作为客户端会猜测该报文在传送过程中被都丢弃了,客户端会再发送一个同步请求报文。但若是第一个报文并未走失而只是在网络中阻塞住了,并且在一段时仍到达服务器,此时服务器会误认为客户端想与其建立TCP链接,则返回一个确认报文段,并创建链接。而此时客户端并不想和服务器建立链接,这造成了服务器资源的浪费。
  • TCP连接在未建立成功时,规定不能发送携带数据的报文,因此在服务器方收到了客户端的SYN报文,即可将ACK与SYN一起发送回去。而不用像断开连接时专门预留一个阶段去传送数据。

连接的断开之4次挥手

Created with Raphaël 2.1.0 客户端 客户端 服务器 服务器 ESTABLISHED ESTABLISHED FIN = 1 , seq = i FIN-WAIT-1 ACK = 1 , seq = j , ack = i+1 CLOSE-WAIT FIN-WAIT-2 FIN = 1 , seq = k , ack = i+1 LAST-ACK ACK = 1 , seq = i+1 , ack = k+1 TIME-WAIT CLOSED CLOSED
  1. 客户端发送一个FIN报文给服务器,其序列号seq=i,客户端进入FIN-WAIT-1状态。
  2. 服务器收到这个FIN,它发回一个ACK报文,其序列号seq=j,确认号ack=i+1,服务器进入CLOSE-WAIT状态。此时客户端收到服务器的ACK报文,进入FIN-WAIT-2状态。
  3. 服务器将未发送完的消息全发送后(共发送k-1个字节),发一个FIN给客户端,其序列号seq=j+k,确认号ack=i+1。服务器进入LAST-ACK状态。
  4. 客户端发回ACK报文确认,其序列号seq=i+1,确认号ack=j+k+1。客户端进入TIME-WAIT状态,经过2MSL后进入CLOSE状态,连接断开。

TCP的连接的断开需要发送四个包,因此称为四次挥手,客户端或服务器均可主动发起挥手动作,首先挥手的一方将执行主动关闭,而另一方执行被动关闭。并且TCP连接是全双工的,因此每个方向都必须进行一次关闭。这意味着当一方完成它的数据发送任务后即发送一个FIN来终止这个方向的连接,接收方收到FIN意味着其读通道被关闭,但仍能发送数据。

为何是4次挥手而不是3次?

TCP连接在断开时,会在被动断开方收到主动断开方FIN报文时留给被动断开方一段时间去继续处理未传送的数据,此时被动断开方的读通道已被断开,而写通道仍然可发送数据。被动断开方在将所有数据传送完毕后,向对方发送一个FIN报文,示意对方TCP连接的另外一个方向的通道也可以关闭了。

全连接队列(accept队列)与半连接队列(syn队列)

在实际编程中我们在服务器端用listen(int fd, int backlog)函数去监听是否有新的客户连接请求。那么,listen函数的作用是什么,它的2个参数各有什么意义,它又对应3次挥手中的哪一步呢?
在Linux操作系统内核中,服务器端通过调用listen函数来创建两个监听队列,全连接队列与半连接队列。fd代表着一个指向服务器端socket端口的文件描述符,而backlog参数在内核2.4以后用来指定全连接队列的长度,半连接队列的长度则由系统默认值tcp_max_syn_backlog给出。在tcp3握手中当第一次握手后即服务器收到客户端的SYN同步请求报文后,系统内核将该客户端端口信息存入半连接队列中,并返还SYN、ACK报文,服务器进入SYN-RECD状态。当服务器收到客户端再次发来的ACK确认报文后,系统将半连接队列中对应的端口信息取出,存入到全连接队列中。之后再由程序调用accept()函数从全连接队列中获得该客户端的套接字信息。
下面我们针对tcp连接创建时几种具体情况进行分析:

  1. 当客户端发送SYN请求后,由于某些特殊情况,如突然掉线,而使其不能返还ACK确认,或者是客户端返还了ACK确认,但是此报文在网络中被丢失了,导致无法到达服务器。针对这种情况该如何处理?

    当服务器接收到客户端发来的SYN请求后,服务器会将其端口信息存入半连接队列中,然后返还一个ACK、SYN报文,并等待客户端的ACK确认。此时若客户端突然掉线而不能返还ACK确认,或客户端返还的ACK报文在网络中被丢失了,导致服务器无法接收。那么,该客户端的端口信息会一直存在于半连接队列中,若网络中有大量的连接处于这种状态,那么半连接队列的大量资源被无端浪费,甚至会消耗殆尽,使得正常连接无法得到处理。
    因此我们需要一个超时时间让服务器对这些连接进行处理。目前Linux下默认会进行5次重发SYN_ACK报文,当客户端收到服务器重发的SYN、ACK报文时,就知道其发送的ACK确认未被服务器收到,客户端会重新发送一个确认报文。当服务器重发了5个SYN_ACK报文依然没有收到客户端的回应,那么即可确认该客户端已经终止创建TCP连接,此时服务器将其半连接队列中对应的客户端端口信息取出并丢弃,以防其资源被无端消耗。

    2.SYN flood 攻击

    服务器5次重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s,因此,即使这样服务器也需 1s + 2s + 4s+ 8s+ 16s + 32s = 63s才会将该无用连接舍弃。这给了网络攻击者可乘之机,攻击者在短时间向服务器发送大量SYN请求,但却对服务器返回的SYN-ACK报文不加理会,那么即使服务器会在一定时间内舍弃无用连接,但也跟不上攻击者潮水般的攻击速度,因而造成服务器瘫痪。
    syn-cookie
    syn-cookie是一种有效的防范syn-flood攻击的机制,它的原理是,在服务器接收到客户端SYN请求并返回SYN-ACK报文后,不分配一个专门的数据区,而是根据这个SYN包计算出一个cookie值。这个cookie作为将要返回的SYN-ACK报文的初始序列号。当客户端返回一个ACK包时,根据包头信息计算cookie,与返回的确认序列号(初始序列号 + 1)进行对比,如果相同,则是一个正常连接,然后,分配资源,建立连接。
    当syn-cookie启用后服务器端相当于拥有一个理论上无穷大的半连接队列。

    3.半连接队列饱和

    当syn-cookie未启用时,因各种情况导致半连接队列已满,新来的SYN请求无法记录,此时服务器会丢弃新来的SYN请求,而客户端若在多次重发SYN请求后仍得不到响应则返回(connection time out)错误。
    若syn-cookie启用时,半连接队列没有逻辑上的最大值,理论上永远不会饱和。

    4.全连接队列饱和
    服务器虽然收到客户端发来的第三次握手ACK确认报文,但因为其全连接队列已满无法接收从半连接队列提取而来的信息,导致服务器无法建立TCP连接,此时服务器通过tcp_abort_on_overflow参数来决定如何处理,0表示直接丢弃该ACK报文(该连接信息依然存在于半连接队列中,系统依然会进行5次重发SYN-ACK报文工作),1表示发送RST报文通知客户端释放此连接,然后再重新建立;相应的,client则会分别返回read timeout 或者 connection reset by peer。

CLOSE-WAIT与TIME-WAIT

假设A为主动断开连接一方,B为被动断开连接一方。

CLOSE-WAIT状态出现与被动断开一方,当B获得A的FIN请求,并返还ACK确认后,即第二次挥手与第三次挥手之间。当B处于CLOSE-WAIT状态时B的读端口被关闭,但其写窗口依然存在,以便B将其未发送完的信息继续向A传递,而不是在收到A的断开连接请求后就粗暴的将其读窗口与写窗口一并关闭,导致剩余数据无法传输。一般的B在传输完所有剩余数据后就该主动返回一个FIN终止请求给A,但实际情况中会因多种情况导致B没有返还以至于B一直处在CLOSE-WAIT状态无法释放连接。

  • 在应用层若A进程想主动断开与B进程的连接那么A进程会发送一个空的消息给B,而此时B正忙着处理其他事件未能即时对该事件做出响应,则B会滞留在CLOSE-WAIT状态。针对这种情况,当B每次调用read函数时或者每间隔一段时间即判断一下socket是否可用,一旦探知己方的读端口已被关闭,则此时程序应做出相应的处理,例如调用close函数释放tcp连接。
  • 有时被动方在对应的逻辑中也调用了close函数但依然未释放连接。其原因是close函数的作用仅仅是将调用该socket的进程计数器的值-1,只有当其值为0时,被动方才会真正的释放TCP连接。而在多进程并发的服务器模型中,由于父进程的一切资源都被子进程继承,那么仅在单个进程中调用close函数并不能断开连接。针对这种情况我们可以使用shutdown函数代替close,shutdown函数会直接关闭指定的socket,而不管其是否仍被其它进程调用。

对于服务器断来说若有大量的客户端处于CLOSE-WAIT状态,那么服务器内核的性能会受到严重的影响,甚至崩溃。上面2条是针对客户端程序做出的改善。在TCP连接中还设有一个保活计时器,对于服务器来说大量处于CLOSE-WAIT状态的客户端不管是因为其自身逻辑产生了问题,还是因为意外事件造成掉线无法返回FIN终止请求。一旦客户端在规定的时间内依然不返还任何数据,那么服务器就强制关闭这个连接。在各个系统中保活计时器默认的时间限制不一样,但基本上都是一个很大的值。我们也可以通过缩小值,来处理大量处于CLOSE-WAIT状态的客户端所带来的问题。

TIME-WAIT状态出现在主动关闭连接一方。TCP协议规定主动断开连接一方即使在受到被动断开方的FIN报文,并返回ACK报文后,仍不能直接释放TCP连接,需要持续等待2个MSL(最长报文寿命)才能释放连接。MSL默认值为2分钟。
设置TIME-WAIT状态的意义有两点:

  • 保证A发送的最后一个ACK报文段能够到达B。若此报文在传输中丢失B会重发FIN报文,A收到该报文后得知其ACK报文未能顺利到达,A会重发ACK报文,并重置2MSL计时。
  • 保证A发送的报文都已在网络中消失,以防新建立的连接受到旧连接中的报文影响。

对于一个处理大量短连接的服务器,如果是由服务器主动关闭与客户端的连接,则会导致服务器端存在大量的处于TIME_WAIT状态的socket, 严重影响服务器的处理能力。针对此类问题处理的方法一般有两种:

  1. 减小MSL默认值
  2. 将连接的关闭交于客户端发起
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值