zoukankan      html  css  js  c++  java
  • TCP协议格式

    一. TCP协议协议格式

    0 16 31

    |16位源端口 | 16位目标端口|

    | 32位序号 |

    | 32位确认序号 |

    |4位首部长度|保留(6位)|URG|ACK|PSH|RST|SYN|FIN|16位窗口大小|

    |16位校验和| 16位紧急指针|

    |选项|

    |数据|

    解析:

    16位窗口大小用于流量控制。

    16位校验和:将协议头和数据都计算在内。

    16位紧急指针:紧急数据在数据包中偏移,紧急数据一般放在包尾。

    4位首部长度:以4字节为单位,最长60字节,最短20字节。

    URG:紧急数据,一般置0。

    ACK:1包含应答信息,0不包含应答信息。

    PSH:1包内含数据,0此包为空包。

    RST:1进程结束或无此进程,一般置0.

    SYN:1建立链接,一般置0.

    FIN:1关闭连接,一般置0.

    在数据传输过程中,ACK和确认序号非常重要。应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲器中,发送给对方数据后,只有收到对方应答的ACK段才知道该数据发到了对方,可以从发送缓冲区中释放掉了。如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送的缓冲区中的数据包重发。

    连接建立与关闭

    三次握手:A-> SYN -> B

        A<- SYN|ACK <- B
    
    
        A-> ACK -> B
    
    

    过程:服务器调用socket,bind,listen完成初始化后,调用accept()阻塞等待,处于监听端口的状态。客户端调用socket初始化后,调用connect发送SYN段并且阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

    关闭: A-> FIN -> B

      A<- ACK <- B
    
    
      ...
    
    
      A<- FIN <- B
    
    
      A-> ACK -> B
    
    

    如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭管道一样,服务器的read()返回0(收到FIN段),这样服务器就知道客户端关闭连接了(回复ACK,处理完自身事情后),也调用close()关闭连接。

    注意:任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则链接处于半关闭状态,仍可接收对方发来的数据。

    注:1. 回执包含在发送包中,节省网络传输时间。

    2. 每次连接的首个数据包的起始序号应该比该端口的上次序号(结束连接)大。以此辨认上次错误回复包。

    3. SYN位和FIN位也要占用一个序号。

    4. 主动关闭连接方处于Time-wait状态。

    5. 每发送一个字节,序号加1;发送10字节,序号要增加10。

    RST示例:

    在TCP通讯中,如果一方收到另一方发来的段,读出其中的端口号,发现本机并没有任何进程使用这个端口,就会应答一个RST位的段给另一方。例如,服务器并没有进程使用8080端口,客户端telnet连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端收到RST段后报告错误:Connection refused。

    二. TCP数据传输

    首先,简单总结下TCP协议是怎么实现可靠传输的。

    TCP协议是面向连接的,利用TCP通信的两台主机首先要经历一个拨打电话的过程,等到通信准备结束才开始传输数据,最后结束通话。为了实现可靠传输做了很多复杂的工作,比如在建立连接的时候要经过三次握手;“优雅的关闭”(closeshutdown)时要四次握手等。

    TCP保证可靠性的简单工作原理摘抄如下

    • 应用数据被分割成TCP认为最适合发送的数据块。这和UDP完全不同,应用程序产生的数据报长度将保持不变。由TCP传递给IP的信息单位称为报文段或段( segment
    • TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
    • TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。
    • TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错, T P将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。
    • 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要, TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
    • TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。

    从这段话可以总结出,TCP协议实现可靠的主要方式是对发送的数据需要有确认回复,超时得不到确认回复就会重发,直到得到对方的确认或者判定网络中断。

    为了实现以上机制,TCP协议离不开四种计时器:重传计时器、持久计时器、保活计时器和时间等待计时器。

    1.重传计时器

    大家都知道TCP是保证数据可靠传输的。怎么保证的呢?带确认的重传机制。在滑动窗口协议中,接受窗口会在连续收到的包序列中的最后一个包向接收端发送一个ACK,当网络拥堵的时候,发送端的数据包和接收端的ACK包都可能丢失。TCP为了保证数据可靠传输,就规定在重传计时器时间片到了以后,如果还没有收到对方的ACK,就重发此包,以避免陷入无限等待中。

    TCP发送报文段时,就创建该特定报文段的重传计时器。可能发生两种情况:

    a. 若在计时器截止时间到之前收到了对此特定报文段的确认,则撤销此计时器。

    b. 若在收到了对此特定报文段的确认之前计时器截止期到,则重传此报文段,并将计时器复位。

    最后,重传计时器的重传时间是有一套算法计算的,这里就不再介绍了。

    2、持久计时器

    先来考虑一种情景:发送端“啪啪啪”向接收端发送包直到接受窗口填满了,然后接受窗口告诉发送方接收窗口满了停止发送数据。此时的状态称为“零窗口”状态,发送端和接收端窗口大小均为零。直到接收TCP发送确认并宣布一个非零的窗口大小。但这个确认可能会丢失。我们知道在TCP中,对确认是不需要发送确认的。若确认丢失了,接收TCP并不知道,而是会认为它已经完成任务了,并等待着发送TCP接着会发送更多的报文段。但发送TCP由于没有收到确认,就等待对方发送确认来通知窗口的大小。双方的TCP都在永远地等待着对方。

    要打开这种死锁,TCP为每一个连接使用一个持久计时器。当发送TCP收到一个窗口大小为零的确认时,就启动坚持计时器。当坚持计时器期限到时,发送TCP就发送一个特殊的报文段,叫做探测报文段。这个报文段只有一个字节的数据。它有一个序号,但它的序号永远不需要确认;甚至在计算对其他部分的数据的确认时该序号也被忽略。探测报文段提醒接收TCP:确认已丢失,必须重传。

    坚持计时器的值设置为重传时间的数值。但是,若没有收到从接收端来的响应,则需发送另一个探测报文段,并将坚持计时器的值加倍和复位。发送端继续发送探测报文段,将坚持计时器设定的值加倍和复位,直到这个值增大到门限值(通常是60秒)为止。在这以后,发送端每隔60秒就发送一个探测报文段,直到窗口重新打开。

    3、保活计时器

    保活计时器使用在某些实现中,用来防止在两个TCP之间的连接出现长时期的空闲。假定客户打开了到服务器的连接,传送了一些数据,然后就保持静默了。也许这个客户出故障了。在这种情况下,这个连接将永远地处理打开状态。

    要解决这种问题,在大多数的实现中都是使服务器设置保活计时器。每当服务器收到客户的信息,就将计时器复位。超时通常设置为2小时。若服务器过了2小时还没有收到客户的信息,它就发送探测报文段。若发送了10个探测报文段(每一个相隔75秒)还没有响应,就假定客户出了故障,因而就终止该连接。

    这种连接的断开当然不会使用四次握手,而是直接硬性地中断和客户端的TCP连接。

    4、时间等待计时器

    时间等待计时器是在四次握手的时候使用的。四次握手的简单过程是这样的:假设客户端准备中断连接,首先向服务器端发送一个FIN的请求关闭包(FIN=final),然后由established过渡到FIN-WAIT1状态。服务器收到FIN包以后会发送一个ACK,然后自己由established进入CLOSE-WAIT。此时通信进入半双工状态,即留给服务器一个机会将剩余数据传递给客户端,传递完后服务器发送一个FIN+ACK的包,表示我已经发送完数据可以断开连接了,接着便进入LAST_ACK阶段。客户端收到以后,发送一个ACK表示收到并同意请求,接着由FIN-WAIT2进入TIME-WAIT阶段。服务器端收到ACK,结束连接。此时(即客户端发送完ACK包以后),客户端还要等待2MSL(MSL=maxinum segment lifetime 最长报文生存时间,2MSL就是两倍MSL)才能真正关闭连接。

    那么,为什么要等待2MSL呢?

    因为客服端发送的ACK对方可能没有收到,此时服务端就要重发FIN+ACK包,所以2MSL是从客服端发ACK对方没有到然后服务端重发FIN+ACK的最长时间,等2MSL就是为了保证对方已经收到ACK包了。若不然,客户端提早断开的话服务器端一直重发FIN+ACK,永远无法进入CLOSE状态。时间等待计时器就是用来记2MSL这个时间的,当计时器到了2MSL以后,客服端才能断开连接。

    三. TCP传输可靠吗?

    在我们使用TCP协议的时候,我们认为TCP是一种可靠的传输协议,所以我们把要传输的数据交给TCP之后,就认为数据发送出去了,不再关心这些数据。但是TCP协议的可靠是建立在网络畅通的基础上的,换句话说,当网络稳定时,TCP协议能够保证将数据送达对方,但是当网络断开时,TCP协议将不再可靠,会有数据丢失的风险。也就是说在某些时候我们需要考虑网络异常时TCP协议会造成的损失。

    通过上述说明,我们已经基本了解了TCP是怎样实现可靠传输的----带确认的重传机制那么当网络发生异常,TCP协议会做什么样的处理,能否及时检测到网络情况?另人遗憾的是,TCP协议并不能够及时的检测到网络异常,下面这篇文章有了比较明确的说明( http://www.guigu.org/news/guiguvip/201206117802.html)。那么,当网络断开时,我们怎么才能够发现问题并避免更大的损失?

    结合我们在主程序中的应用,做了一下分析与测试。主程序上报数据时调用write将数据写入对应的socket套接字,若write返回值不小于0则认为发送成功。

    但是write成功并不代表对端tcp(服务器)接收到了数据,只是内核将要发送的数据拷贝到了tcp的发送缓冲区,然后由tcp协议进行数据的发送。Tcp协议对发送的数据要保留,当收到相应的ack应答才丢掉相应的数据,否则会启用超时重发机制以确保数据送达。

    若此时网络中断,将会有如下两种情况:

    一是主程序认为网络是畅通的,继续write数据,直到缓冲区被写满,此时write失败返回-1,主程序会断开连接;

    二是tcp协议由于无法收到已发数据的ack确认报文,则会超时重发,当发送一定次数仍无法收到ack时也会认为网络断开,从而断开连接。

    那么,这两种情况哪个先发生呢?

    可分别对超时重发机制用时测试,发送缓冲区大小测试,TCP协议的keepalive机制测试。

    发送缓冲区

    Tcp协议发送数据时,Writesend成功并不代表对端tcp接收到数据,仅仅是内核将想要发送的数据拷贝到了发送缓冲区中。在网络良好时,tcp协议能够保证缓冲区中的数据发送成功,但是当网络出现异常时,系统无法及时检测到网络异常,应用层仍将数据写入缓冲区,并认为数据发送成功。直到发送缓冲区被写满为止。

    编写测试代码,每十秒发送一次数据,每次数据大小为1K

    拔掉网线后,当Send-Q值累计增大到14056时不再增大。此时Write返回-1state值正常。

    改写测试代码,一次发送16K数据,则断开连接后数据增大到18825write返回值为-1state变为FIN_WAIT1,然后socket关闭:

    结论:MCUtcp缓冲区最大为18824字节,但是慢慢累加是达不到这个值。

    超时重发

    Tcp协议具有超时重发机制,即对每次发送的数据进行计时,计时器溢出时没有收到ack回复则重新发送数据。发送若干次后若均未收到ack回复则断开连接。那么,tcp通过超时重传机制要经过多久才能发现网络中断呢?

    超时重传机制判断网络中断与两个条件有关,一是重传次数,一是重传间隔。

    重传次数可通过指令查看,默认为15次。cat  /proc/sys/net/ipv4/tcp_retries2

    发送间隔采用“指数退避”原则,如第一次间隔为1.5s,则后续间隔为3s6s12s24s48s和若干个64s(最大为64s)。

    默认发送次数15次发送数据后断开网线,此时程序在继续发送数据(写入发送缓冲区),netstat –t 查看tcp连接情况:超时重发时stateESTABLISHED(已建立连接)。

    约经过17分钟后state变为FIN_WAIT1(等待远程TCP连接中断请求,或先前的连接中断请求的确认),然后socket被关闭。(代码每十秒发送十个字节,即相当于,每秒发送一个字节,这样Send-Q发送队列大小跟时间对应。)

    修改重发次数为6网络断线后约1分钟。

    结论:默认情况下,通过超时重发判断出网络中断约需17分钟左右。

    KeepAlive

    Keep alivetcp协议自带的心跳包机制,用于检测无效的连接。若开启该功能,当socket空闲到预设的时间,则tcp发送检测包,如收到回复则重新计时,如检测了预设的次数后没有收到回复则直接断开连接。

    编写测试代码,设置空闲为5s,检测间隔5s,检测次数为三次,发送数据后休眠一分钟。运行程序,测试程序发送数据后拔掉网线,25秒后连接被关闭。

    结论

    经过学习与测试可知,TCP协议不能够通过自身机制来检测到网络的实时情况,如果对网络的实时连接情况依赖较强,最好的解决方法是应用层发送心跳包,或应用层通过其他方式对网络进行检测。

    四. 如何判断TCP数据报发送成功

    问题描述:socket编程,发送少量数据时,send/write等发送函数会立即返回成功,发送的数据会存在TCP发送缓冲区中,依靠TCP协议栈自身的重传机制来保证 该数据 被接收端收到;我们的问题是 发送端应用程序 如何判断 少量数据 已经成功发送到接收端?
    解决思路:发送数据存在缓冲区中,我们判断发送缓冲区大小变化,即可获知发送是否成功;具体方法如下: 发送数据后,获得已使用缓冲区大小buf,如果buf==0,表示成功,否则,表示未发送;
    那么,如何来获得当前已占用发送缓冲区大小?

    1. 第一步我们自然想到是否存在这样的sockopt接口

    getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);

    getsockopt中,有参数SO_SNDBUF,但是是用来获得发送缓冲大小的;但经过试验,我们发现 无论数据是否发送成功,该值一直不变;查看内核代码,发现 该参数的含义是 总共的发送缓冲区,包括 已占用 空闲的。

    2. 第二步,我们发现/proc/net/tcp 中tx_queue大小,即已占用发送缓冲区大小,也就是待发送数据长度

    /proc/net/tcp 中tx_queue 的计算:“tp->write_seq - tp->snd_una”,根据该计算方法,在内核源码中查找,发现内核导出了2个获得已使用发送缓冲的编程接口;

    ioctl接口:ioctl(tcp_socket, SIOCOUTQ, &value);

    netlink接口:socket(AF_NETLINK, SOCK_RAW, NETLINK_TCPDIAG);获得structtcpdiagmsg结构体数据,其中,tcpdiag_wqueue即已占用发送缓冲区大小;

    ps:ioctl接口已经实验验证可用,netlink尚未验证,可参考http://linux.chinaunix.net/techdoc/develop/2006/10/15/942169.shtml

    ioctl(tcp_socket, SIOCOUTQ, &value);对应的内核源码实现:net/ipv4/tcp.c----tcp_ioctl()

  • 相关阅读:
    08-12 NOIP模拟测试18
    08-09 NOIP模拟测试15
    08-11 NOIP模拟测试17
    08-10 NOIP模拟测试16
    08-07 NOIP模拟测试14
    08-03 NOIP模拟测试12
    [SDOI2011]拦截导弹
    08-01 NOIP模拟测试11
    零散知识点
    07-29 NOIP模拟测试10
  • 原文地址:https://www.cnblogs.com/embedded-linux/p/4986982.html
Copyright © 2011-2022 走看看