Source Port和Destination Port(源端口和目的端口):分别占用16位,用于区别主机中的不同进程;而IP地址是用来区分不同的主机的,源端口号和目的端口号配合上IP首部中的源IP地址和目的IP地址就能唯一 的确定一个TCP连接。
Sequence Number(序号):用来标识从TCP发送端向TCP接收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节在数据流中的序号;主要用来解决网络报乱序的问题。
Acknowledgment Number(确认序号):32位确认序列号包含发送确认的一端所期望收到的下一个序号,因此,确认序号应当是上次已成功收到数据字节序号加1。不过,只有当标志位中的ACK标志为1时该确认序列号的字段才有效。主要用来解决不丢包的问题。
Offset(头部字段):给出首部中32 bit(4字节)的数目,需要这个值是因为任选字段的长度是可变的。这个字段占4bit(最多能 表示15个32bit的字,即4*15=60个字节的首部长度),因此TCP最多有60字节的首部。然而,没有任选字段, 正常的长度是20字节。
标志位字段(U、A、P、R、S、F):占6比特。各比特的含义如下:
它们中的多个可同时被设置为1,主要是用于操控TCP的状态机的,依次 为URG,ACK,PSH,RST,SYN,FIN。
URG(紧急数据标志位):如果URG为1,表示本数据包中包含紧急数据。此时紧急数据指针表示的值有效,它表示在紧急数据之后的第一个字节的偏移值(即紧急数据的总长度)。若URG为0,则紧急指针没有意义。
PSH(推位):当设置为1时,要求把数据尽快的交给应用层,不做处理。当两个应用进程进行交互式的通信时,有时在一端的应用进程希望再键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送操作。这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段,就尽快交付接收应用进程,而不再等到整个缓存都填满了再向上交付。
虽然应用进程可以选择推送操作,但推送操作还是很少使用。
两者都可理解为处理紧急数据的标志位,只是处理方法不同。URG的紧急数据仅在报文内,而PSH的紧急数据还在接受缓冲区内。
服务端的TCP进程先创建传输控制块TCB,准备接受客户端进程的连接请求,然后服务端进程处于LISTEN状态,等待客户端的连接请求,如有,则作出响应。
第一次握手:客户端的TCP进程也首先创建传输控制模块TCB,然后向服务端发出连接请求报文段,该报文段首部中的SYN=1,ACK=0,同时选择一个初始序号 seq=x。TCP规定,SYN=1的报文段不能携带数据,但要消耗掉一个序号。这时,TCP客户进程进入SYN—SENT(同步已发送)状态。
第二次握手:服务端收到客户端发来的请求报文后,如果同意建立连接,则向客户端发送确认。确认报文中的SYN=1,ACK=1,确认号ack=x+1,同时为自己选择一个初始序号seq=y。同样该报文段也是SYN=1的报文段,不能携带数据,但同样要消耗掉一个序号。这时,TCP服务端进入SYN—RCVD(同步收到)状态。
第三次握手:TCP客户端进程收到服务端进程的确认后,还要向服务端给出确认。确认报文段的ACK=1,确认号ack=y+1,而自己的序号为seq=x+1。 TCP的标准规定,ACK报文段可以携带数据,但如果不携带数据则不消耗序号,因此,如果不携带数据,则下一个报文段的序号仍为seq=i+1。这时,TCP连接已经建立,客户端进入ESTABLISHED(已建立连接)状态,可以看出第三次握手客户端已经可以发送携带数据的报文段了。
当服务端收到确认后,也进入ESTABLISHED(已建立连接)状态。
第三次握手看似多余其实不然,这主要是为了防止已失效的请求报文段突然又传送到了服务端而产生连接的误判。
比如:客户端发送了一个连接请求报文段A到服务端,但是在某些网络节点上长时间滞留了,而后客户端又超时重发了一个连接请求报文段B该服务端,而后 正常建立连接,数据传输完毕,并释放了连接。但是请求报文段A延迟了一段时间后,又到了服务端,这本是一个早已失效的报文段,但是服务端收到后会误以为客户端又发出了一次连接请求,于是向客户端发出确认报文段,并同意建立连接。那么问题来了,假如这里没有三次握手,这时服务端只要发送了确认,新的 连接就建立了,但由于客户端没有发出建立连接的请求,因此不会理会服务端的确认,也不会向服务端发送数据,而服务端却认为新的连接已经建立了,并在 一直等待客户端发送数据,这样服务端就会一直等待下去,直到超出保活计数器的设定值,而将客户端判定为出了问题,才会关闭这个连接。这样就浪费了很多服务器的资源。而如果采用三次握手,客户端就不会向服务端发出确认,服务端由于收不到确认,就知道客户端没有要求建立连接,从而不建立该连接。
那四次分手又是为何呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2, 它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文 段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN 报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此 就会愉快的中断这次TCP连接。如果要正确的理解四次分手的原理,就需要了解四次分手过程中的状态变化。
第一次挥手: 客户端发送FIN(表示要结束连接)给服务器,客户端状态由 ESTABLISHED 变为 FIN_WAIT_1。
第二次挥手:
服务器收到ACK、,服务器状态由 ESTABLISHED 变为CLOSE_WAIT。
服务器将缓存中没发送的数据完继续发送给客户端,客户端收到ACK后状态由FIN_WAIT_1变为FIN_WAIT_2。
第三次挥手:服务器发送FIN给客户端,这时服务器的状态由CLOSE_WAIT变为 LAST_ACK。
第四次挥手:
客户端收到FIN后返回ACK给服务器,然后客户端的状态由FIN_WAIT_2变为TIME_WAIT。
服务器收到ACK后,状态由LAST_ACK变为CLOSED。
而客户端再经过TIME_WAIT时间后变为CLOSED状态。
PS:TIME_WAIT = 2MSL (maximum segement lifetime 分节在网络中最长生存时间,30秒到2分钟,根据系统实现不同而不同) 2MSL 范围是 1分钟到4分钟。
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
TCP设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
面向连接的:使用TCP协议通信的双方必须先建立连接,然后才能开始数据的读写,TCP连接是全双工的,即双方的数据读写可以通过一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放资源。TCP协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用TCP服。而无连接协议UDP则非常适合于广播和多播。
流式服务:TCP的字节流服务的表现形式就体现在,发送端执行的写操作数和接收端执行的读操作次数之间没有任何数量关系,当发送端应用程序连续执行多次写操作时,TCP模块先将这些数据放入TCP发送缓冲区中。当TCP模块真正开始发送数据的时候,发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出。
UPD的数据报服务:发送端应用程序每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操作(通过recvfrom系统调用),否则就会丢包(这经常发生在较慢的服务器上)。并且,如果没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。
实现TCP可靠传输的具体机制见下文
TCP通过下列方式来提供可靠性:
TCP没有报文长度字段,只有序号字段,应用数据通过序号被分割成TCP认为最适合发送的数据块,应用程序产生的数据报长度将保持不变。
超时重发——当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
对于收到的请求,给出确认响应——当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒 。 (之所以推迟,是因为延时确认可以更有效利用接收端的缓冲区)
TCP将保持它首部和数据的检验和——这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。 (校验出包有错,丢弃报文段,不给出响应,TCP发送数据端,超时时会重发数据)
失序重排——既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
丢弃重复数据——既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
TCP还能提供流量控制——TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳 的数据。这将防止较快主机致使较慢主机的缓冲区溢出。(TCP可以进行流量控制,防止较快主机致使较慢主机的缓冲区溢出)TCP使用的流量控制协议是可变大小的滑动窗口协议。
TCP将每个字节的数据都进行了编号, 即为序列号;每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你要从哪里开始发。
比如, 客户端向服务器发送了1005字节的数据, 服务器返回给客户端的确认序号是1003, 那么说明服务器只收到了1~1002的数据,1003, 1004, 1005都没收到,此时客户端就会从1003开始重发。
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B,如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发,但是主机A没收到确认应答也可能是ACK丢失了,这种情况下, 主机B会收到很多重复数据,这时候利用前面提到的序列号, 就可以很容易做到去重。
最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”。但是这个时间的长短, 随着网络环境的不同, 是有差异的。
如果超时时间设的太长, 会影响整体的重传效率; 如果超时时间设的太短, 有可能会频繁发送重复的包。
TCP为了保证任何环境下都能保持较高性能的通信, 因此会动态计算这个最大超时时间。
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。
如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传. 如果仍然得不到应答, 等待 4500ms 进行重传。依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络异常或者对端主机出现异常, 强制关闭连接。
刚才我们讨论了确认应答机制, 对每一个发送的数据段, 都要给一个ACK确认应答.,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返时间较长的时候。
那么我们可不可以一次发送多个数据段呢?
窗口:窗口大小指的是无需等待确认应答就可以继续发送数据的最大值,上图的窗口大小就是4000个字节 (四个段)。
发送前四个段的时候, 不需要等待任何ACK, 直接发送。收到第一个ACK确认应答后, 窗口向后移动, 继续发送第五六七八段的数据。
因为这个窗口不断向后滑动, 所以叫做滑动窗口。
操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有ACK确认应答过的数据, 才能从缓冲区删掉。
在滑动窗口下,如果出现丢包,进行重传的方法如下:
这种情况下, 部分ACK丢失并无大碍, 因为还可以通过后续的ACK来确认对方已经收到了哪些数据包。
个人理解,窗口大小是协商好的,当第一个窗口段的ACK丢包,主机收到第二个窗口段的ACK时,发现确认序号是2001,那表明1001~2000的字节的数据已经收到了,可是主机并没有收到确认序号为1001的ACK,那么就要对1—1000字节的数据启用重传机制。
当某一段报文丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001”。
如果发送端主机连续三次(是固定三次还是和窗口大小有关我不太清楚)收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送。
这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了,因为2001 - 7000接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中。
这种机制被称为 “高速重发控制” ( 也叫 “快重传” )
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被填满, 这个时候如果发送端继续发送, 就会造成丢包, 进而引起丢包重传等一系列连锁反应。
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)
我们的TCP首部中, 有一个16位窗口大小字段, 就存放了窗口大小的信息,16位数字最大表示65536,通过ACK通知发送端,窗口大小越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端。
发送端接受到这个窗口大小的通知之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0,这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 让接收端把窗口大小再告诉发送端。
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位(左移一位相当于乘以2)。
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠地发送大量数据。但是如果在刚开始就发送大量的数据,仍然可能引发一些问题。因为网络上有很多计算机, 可能当前的网络状态已经比较拥堵,在不清楚当前网络状态的情况下, 贸然发送大量数据, 很有可能雪上加霜。
因此, TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态以后, 再决定按照多大的速度传输数据。
在此引入一个概念“拥塞窗口(cwnd)”
PS:在考虑拥塞的时候我们一般不考虑rwnd的值
(可以尝试回顾一下,和上述的滑动窗口机制、流量控制机制联系起来,叙述一下TCP数据传输的过程。)
像上面这样的拥塞窗口增长速度,是指数级别的,“慢启动” 只是指初始时慢, 但是增长速度非常快。
为了不增长得那么快, 此处引入一个名词叫做慢启动的阈值(ssthresh), 当拥塞窗口的大小超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。
当TCP开始启动的时候, 慢启动阈值有一个初始值
无论是慢启动算法还是拥塞避免算法,只要判断网络出现拥塞,就要把慢启动开始门限(ssthresh)设置为设置为发送窗口的一半(>=2),拥塞窗口(cwnd)设置为1。
快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。(不能使用捎带应答,下文提到)
其过程有以下两个要点:
当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。
由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
下图给出了快重传和快恢复的示意图,并标明了“TCP Reno版本”。
区别:新的 TCP Reno 版本在快重传之后采用快恢复算法而不是采用慢开始算法。
也有的快重传实现是把开始时的拥塞窗口cwnd值再增大一点,即等于 ssthresh + 3 X MSS 。这样做的理由是:既然发送方收到三个重复的确认,就表明有三个分组已经离开了网络。这三个分组不再消耗网络 的资源而是停留在接收方的缓存中。可见现在网络中并不是堆积了分组而是减少了三个分组。因此可以适当把拥塞窗口扩大了些。
在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。
采用这样的拥塞控制方法使得TCP的性能有明显的改进。
少量的丢包, 我们仅仅是触发超时重传;大量的丢包, 我们就认为是网络拥塞。
当TCP通信开始后, 网络吞吐量会逐渐上升;随着网络发生拥堵, 吞吐量会立刻下降。
前言:
窗口越大, 网络吞吐量就越大, 传输效率就越高。
TCP的目标是在保证网络不拥堵的情况下尽量提高传输效率。
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
假设接收端缓冲区为1M. 一次收到了500K的数据,如果立刻应答, 返回的窗口大小就是500K。
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区处理掉了; 在这种情况下, 接收端其实可以再次接受1M的数据,可他返回的窗口大小只有500K,处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来。
如果接收端稍微等一会儿再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M。
######那么所有的数据包都可以延迟应答么?
有两个限制:
具体的数量N和最大延迟时间, 依操作系统不同也有差异
一般 N 取2, 最大延迟时间取200ms
在延迟应答的基础上, 我们发现, 很多情况下,客户端和服务器在应用层也是 “一发一收” 的。
意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”,那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起发送给客户端 。
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区。调用write时, 数据会先写入发送缓冲区中:
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用read从接收缓冲区拿数据。
TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区,那么对于这一个连接, 既可以读数据, 也可以写数据, 这个概念叫做全双工。
由于缓冲区的存在, 所以TCP程序的读和写不需要一一匹配
例如:
首先要明确, 粘包问题中的 “包”, 是指应用层的数据包。
在TCP的协议头中, 没有如同UDP一样的 “报文长度” 字段,但是有一个序号字段。站在传输层的角度, TCP是一个一个报文传过来的. 按照序号排好序放在缓冲区中,站在应用层的角度, 看到的只是一串连续的字节数据。
那么应用程序看到了这一连串的字节数据, 就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包?此时数据之间就没有了边界, 就产生了粘包问题。
那么如何避免粘包问题呢?
明确两个包之间的边界
对于定长的包
保证每次都按固定大小读取即可,例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可。
对于变长的包
可以在数据包的头部, 约定一个数据包总长度的字段, 从而就知道了包的结束位置。
还可以在包和包之间使用明确的分隔符来作为边界(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)。
不会。对于UDP, 如果还没有向上层交付数据, UDP的报文长度仍然存在。同时, UDP是一个一个把数据交付给应用层的, 就有很明确的数据边界。
站在应用层的角度, 使用UDP的时候,要么收到完整的UDP报文, 要么不收,不会出现收到 “半个” 的情况。
另外, 应用层的某些协议, 也有一些这样的检测机制:
例如HTTP长连接中, 也会定期检测对方的状态
例如QQ, 在QQ断线之后, 也会定期尝试重新连接
因为既要保证可靠性, 同时又要尽可能提高性能。
HTTP、HTTPS、SSH、Telnet、FTP、SMTP等
TCP和UDP之间的优点和缺点, 不能简单绝对地进行比较
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景
UDP用于对高速传输和实时性要求较高的通信领域
例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播
部分内容参考博客:rugu_xxx