Andy Niu �����ĵ�

Andy Niu

Andy Niu Help  1.0.0.0

模块

 拥塞控制
 

变量

 测试tcp端口
 
 TCP是一种流协议
 
 TCP的标志位
 
 糊涂窗口综合征以及解决办法
 
 TCP的定时器
 
 TCP知识点
 
 出现RST的几种情况
 
 TCP头部
 
 TCP状态机
 
 长连接和短连接
 

详细描述

变量说明

TCP头部
1、前2个字节是源端口,接下来2字节是目的端口
2、接下来4字节表示发送序号,标识发送数据的第一个字节的序号。第一个字节的序号确定下来,后面的字节序号也都确定下来。
    比如当前序号为7,并发送了4个字节,下一次发送的序号为11。
    注意:这个序号,不同于IP头部中的报文唯一标识。另外,对于TCP连接的三次握手,发送序号往往不是从0开始的。
    但是,知道了第一个syn的发送序号,其它报文的相对序号也都可以计算出来。
3、接下来4字节表示确认序号,告诉对端,已经接收到了第几个字节,期望从下一个字节开始接收。
    确认序号,由对端的发送序号和发送的数据长度确定。
    比如:对端的发送序号为3,并且发送数据长度为4,我的确认序号是7,表示已经收到了第7个字节,期望从下一个字节接收。
4、接下来4个bit,表示TCP头部长度(单位是32bit),这个字节取值一般是80,前面4个bit取值为8,表示TCP长度是8*4=32字节。
    注意TCP头部的最大长度是60个字节,,并且是4字节的倍数,为什么?
    因为只有4个bit表示TCP头部长度,最大取值为15,也就是15*4=60字节
    接下来6个bit是预留位,接下来6个bit是标志字段,分别为UAPRSF(油婆RSF),接下来16个bit表示,我的接收缓冲区大小,
    单位是字节,从对端收到数据,缓冲区变小,应用程序取走数据,缓冲区变大。这个字段,告诉对方,我还能接收多少字节的数据。
    当取值为0,表示缓冲区满了,对端不能再发送数据,否则接收数据溢出。
5、接下来16个bit是TCP校验和,TCP校验和覆盖TCP头部和TCP数据,以及TCP的伪头部,也就是IP头部的前12个字节,
    TCP校验和是必须的。接下来16个bit表示紧急指针,只有当URG标示为1时才有效,紧急指针是一个正的偏移量,和发送序号字段相加
    表示紧急数据最后一个字节的序号。紧急字段的使用场景是,当有紧急数据的时候,TCP把紧急数据放在报文段的最前面,
    接收端就可以优先处理紧急数据,紧急数据后面的数据是普通数据。
6、接下来是TCP的可选项。这部分最长40字节,因为TCP头部最长60字节(TCP头部固定长度20字节),为什么最长60字节?
    因为TCP头部中只有4个bit表示TCP头部长度(单位是32bit,也就是4字节),这个值最大为15,因此15*4=60字节
    常用的可选项如下:
    kind=0
    kind=1
    kind=2  length=4        MSS取值(2字节)
    kind=3  length=3        移位数(1字节)
    kind=4  length=2
    kind=5  length=8*N+2    第一块左边沿和右边沿  第二块左边沿和右边沿
    kind=8  length=10       时间戳(4字节)    时间戳回显(4字节)
    
    kind=0是选项表结束
    kind=1是空操作(nop)选项,没有特殊意义,只是为了填充,使头部长度是4字节的整数倍
    kind=2是MSS选项,只在syn包设置,三次握手双方协商MSS。
            发送syn报的时候告诉对方,我期望接收的最大段长度是多少,
            你别发太长,短一些没有问题。MSS一般是1460,因为以太网MTU最大为1500(不包括以太网帧头的14字节),
            而TCP最小的头部长度为20字节,因此1500-20(IP头)-20=1460字节
            UDP有效数据的最大长度是1500-20-8=1472字节
            TCP报文段在传输过程中,如果经过网络(路由器)的MTU较小,就需要在IP网络层进行分段,IP头部有字段表示。
    kind=3是窗口缩放因子,只在syn包设置。解决什么问题?
            作为接收端,我告诉对方我的接收缓冲区还有多大,如果满了,你就不能发送了,这样来控制对方的发送。
            这就是TCP头部中的接收窗口。但是这个字段是16bit,最大为65535,这会导致什么问题?
            这个值很快就递减为0,导致对方不能发送数据,出现卡顿。
            实际上我的接收缓冲区很大,甚至1个G,可以不停地接收数据。但是TCP头部的窗口大小只能表示65535,怎么办?
            发送syn报的时候告诉对方,这个缩放因子(取值0~14,表示左移几位),比如取值为2。
            在发数据交互的时候,我发送的接收通告窗口是64,那么你要认为我的接收缓冲区是64*2的2次方,
            也就是256,也就是实际的接收窗口要扩大一下。
            作为发送端,我通告的时候,要缩小一下,实际上我的接收窗口是256,但是我要告诉对方是64,因为对方会扩大的。
    kind=4是否支持选择性确认选项,只在syn包设置。解决什么问题?
            比如我发了A、B、C、D四个报文段,你收了A、C、D三个,B报文段丢失了。
            按道理,你必须按顺序确认A、B、C、D,因此只会确认A,即使收到了C、D,因为B丢失了,不会确认C、D。
            超时,我没有收到B的确认,认为B、C、D都丢失了,BCD全部重发,降低性能。怎么解决?
            设置这个字段,告诉对方你可以选择性的确认。这样的话,即使没有收B,你也可以选择性确认CD,
            那么我就只需要发送B就好了。
    kind=5是选择性确认,和kind=4结合使用。在交互的过程中,前面的报文段丢失了,对后面已经收到的报文段选择性确认。
            可以一次性的选择确认多个,确认一个报文段需要8字节,左边沿4字节和右边沿4字节,这个选项长度为8*N+2
            因此一次最多确认4个
    kind=8是时间戳选项,时间戳选项的作用有:
            序号回绕,计算RTT。按道理数据报的序号是单调递增的,当超过最大值的时候,通过时间戳可以判断出是后续的包。
            我发送的时候带上当前时间,你Ack的时候,回显一下,当我收到Ack,我就能计算出一来一回的时间,也就是RTT
            注意:这不是没有个报文段的RTT,而是每个Ack的RTT,因为一次可以ACk多个报文段。
            另外,时间戳的时间单位可以是1毫秒到1秒,回显的一端不关心。
TCP是一种流协议
1、TCP是一种流协议,发送者以字节流的方式发给接收者,发送者的写操作与接收者的读操作没有一一对应的关系。
    接收者无法预先得知在一次读操作中返回多少字节,接收的数据没有 "报文边界" 的概念。
2、那么接收者如何识别收到一个完整的报文?
    两种解决办法:
    a、报文结尾增加结束标记,用于分割记录,需要注意的是:要表示结束标记本身,需要使用转义字符。
    b、报文头部增加字段,表示报文长度,读取到一定长度就认为是一个完成的包。
TCP状态机

使用网络助手观察tcp状态机

场景一:客户端主动断开连接
1、服务端使用12345端口监听,客户端去连接。
2、服务端的网络连接
    C:\Users\niu>netstat -ano|findstr 12345
    TCP    10.65.200.168:12345    0.0.0.0:0              LISTENING       7064
    TCP    10.65.200.168:12345    10.36.65.80:34814      ESTABLISHED     7064
3、客户端的网络连接
    C:\Users\25697>netstat -ano|findstr 12345
    TCP    10.36.65.80:34814      10.65.200.168:12345    ESTABLISHED     6208
4、注意:netstat的输出最后一个字段是进程Id,前面一个端口是当前进程的端口,后面一个端口是对端进程的端口。
    对于在一台机器上的输出,很有帮助,如下在Linux下:
    [root@localhost IBP]# netstat -anp|grep 9932
    tcp        0      0 0.0.0.0:9932                0.0.0.0:*                   LISTEN      22851/./vmu_main    
    tcp        0      0 172.16.2.102:9932           172.16.2.102:43061          ESTABLISHED 22851/./vmu_main    
    tcp        0      0 172.16.2.102:9932           172.16.2.102:43008          ESTABLISHED 22851/./vmu_main    
    tcp        0      0 172.16.2.102:43061          172.16.2.102:9932           ESTABLISHED 22734/./vms_main    
    tcp        0      0 172.16.2.102:43008          172.16.2.102:9932           ESTABLISHED 22792/./mgw_main
5、这个时候抓包可以看到连接的三次握手。
6、然后客户端主动断开连接。
7、服务端的网络连接
    C:\Users\niu>netstat -ano|findstr 12345
    TCP    10.65.200.168:12345    0.0.0.0:0              LISTENING       7064
    TCP    10.65.200.168:12345    10.36.65.80:34814      CLOSE_WAIT      7064
8、客户端的网络连接
    C:\Users\25697>netstat -ano|findstr 12345
    TCP    10.36.65.80:34814      10.65.200.168:12345    FIN_WAIT_2      6208
9、分析如下:
    客户端主动断开连接,先发送fin包,进入FIN_WAIT_1状态,服务端回复ack,客户端进入FIN_WAIT_2状态
    同时服务端进入CLOSE_WAIT状态
    按道理,这种情况下,服务端应该接着发送fin包,进入LAST_ACK状态
    客户端收到fin包,回复ack(服务端收到ack进入CLOSED状态),进入TIME_WAIT状态,经过2MSL(2 Max Segment Lifetime)进入CLOSED状态
10、但是服务端并没有发送第二个fin包,这时候状态一直保持。
    过了两分钟之后,处于半连接的客户端意识到这个问题,于是发送rst包进行复位,两端的连接close掉。
    注意:还有一种情况发送rst包,就是去连接没有监听的端口,对端发送rst包
11、为什么服务端没有发送第二个fin包?
    推测:发送fin包意味着我没有数据发给对方了,作为被动关闭的服务端可能不知道是否还有数据发给对方。
场景二:服务端主动断开连接
1、服务端主动断开连接,可以看到断开连接的四次握手。
2、服务端发送fin包,客户端回复ack,然后发送fin包,服务端回复ack,进入TIME_WAIT状态,客户端收到ack立即进入CLOSED状态
TCP的定时器
1、tcp有四种定时器,分别解决不同的问题。
    超时重传定时器
    坚持定时器
    保活定时器
    2MSL定时器
2、超时重传定时器,也就是RTO(Retransmission Time Out)定时器,解决的问题是:
    我发送的数据,如果一段时间内没有收到对方的确认,我就重传。
    注意:这个RTO是根据RTT(Round Trip Time),往返时间计算出来的,RTT就是从发送数据,到收到对方的确认所经历的时间。
3、坚持定时器,解决什么问题?
    解决0窗口的问题,考虑接收端缓冲区满了,发送一个0窗口,发送端收到0窗口,意识到我不能再发送数据了。
    现在应用程序从接收端的缓冲区读取了一部分数据,接收端就发送确认,并告诉对方一个非0的窗口大小,也就是说,我现在可以接收数据了。
    特别注意:这个确认是不包含数据的(窗口大小在tcp首部中),也就是载荷为0,对于这样的ack,对方不需要确认。
    那么问题来了,假如这个ack丢失了,接收端认为我已经告诉对方我可以接收数据了,就等着收数据。而发送端认为,对方缓冲区满了,
    我不能发送数据,就等着对方告诉新的窗口大小。由于这个ack不需要确认,接收端也不会重新发送这个ack,导致了死锁。
    怎么解决这个问题?
    理论上,发送方认为对方的缓冲区不可能是一直满的。当收到一个0窗口的通知时,开启一个定时器,
    如果一段时间没有告诉我新的窗口大小,我就主动向你去查询。
    这个报文就是 探测报文段。
4、保活定时器,解决什么问题?
    考虑tcp连接建好之后,处于长时间的空闲,假设客户端崩溃了,服务端还不知道,一直处于打开状态。
    (注意:tcp不是轮询的,无法将连接的丢失立即通知应用程序)
    解决办法就是:
    定时发送心跳包,一段时间内没有收到确认,就认为连接出问题了,主动断开连接。
    注意:tcp保活机制的心跳包间隔比较大,检测到连接丢失延迟比较大。可以在应用层进行管理,发送时间间隔较小的心跳包。
5、2MSL定时器,解决什么问题?
    主动断开连接的一方,发送ack之后,如果立即进入closed状态。假设这个ack丢失了,对方由于没有收到这个ack,
    会重新发送fin包,但是我已经closed了,无法再对这个fin进行确认,导致半关闭。而对方会定时重传fin包。
    怎么解决这个问题?
    站在我的角度考虑,发送ack之后,如果对方没有收到的ack,肯定还会重传fin包。如果一段时间没有再次受到fin包,
    则证明对方肯定收到了我的ack。
    因此,发送ack之后,开启一个定时器,时间为2MSL(从我发送ack,到再发一个fin所经历的时间),这段时间没有再次收到fin,
    说明对方肯定收到了我的ack,我就可以放心地进入closed状态了。
TCP的标志位
1、使用wireshark抓包可以看到三次握手:
    syn
    syn ack
    ack
2、使用tcpdump直接抓包查看,如下:
    [root@localhost ~]# tcpdump -i any -s 0 tcp port 12345 -A
    tcpdump: WARNING: Promiscuous mode not supported on the "any" device
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
    16:34:47.340784 IP 10.36.65.60.40343 > 10.36.65.80.italk: S 837002707:837002707(0) win 5840 <mss 1460,sackOK,timestamp 227133078 0,nop,wscale 6>
    E..<cE@.@.@.
    $A<
    $AP..091........... ..........
    ...........
    16:34:47.341712 IP 10.36.65.80.italk > 10.36.65.60.40343: S 1352403151:1352403151(0) ack 837002708 win 8192 <mss 1460,nop,wscale 8,sackOK,timestamp 82366271 227133078>
    E..<sY@[email protected].
    $AP
    $A<09..P...1..... ................
    ...?
    16:34:47.341721 IP 10.36.65.60.40343 > 10.36.65.80.italk: . ack 1 win 92 <nop,nop,timestamp 227133079 82366271>
    E..4cF@.@.@.
    $A<
    $AP..091...P......\8P.....
    ......?
    可以看到第三次的标志有个点. 这个点表示syn fin rst psh 这些标志比特位都是0,与是否ack urg无关
TCP知识点
1、syn、fin占用一个序列号,ack不占用序列号
2、mss和窗口大小,是告诉对端,我能接收到的最大报文长度和还能接收的数据大小(也就是我的接收缓冲区)
    对端发送的报文长度超过mss,我不接收。对端发送的数据大小超过窗口大小,会导致我的接收缓冲区溢出。
3、发送fin,意味着我不再向你发送数据,但是我还可以接收你发过来的数据。
4、一个进程可以重新使用仍然处于2MSL等待的接口(设置套接字SO_REUSEADDR),
    但是tcp不允许一个新的连接建立在相同的套接字对上。
    比如服务端主动断开连接,处于2MSL的TIME_WAIT状态,客户端还可以来进行连接,
    但是新建立的连接,套接字对不同于上一次的套接字对。
5、ack的传输并不可靠,tcp只对那些包含有数据的ack报文段进行确认,对那些没有包含数据的ack报文段不进行确认。
    思考为什么?
    如果对于没有数据的ack报文段也进行确认,那就变成了死循环,永远结束不了。建立连接的时候,一直握手下去,拆除连接的时候,永远结束不了。
    在建立连接的时候,三次握手中的最后一次ack,不需要对方确认。在拆除连接的时候,最后一次ack,也不需要对方确认。
6、现在思考另一个问题,为什么ack不占用序列号?
    假如ack也占用一个序列号,根据tcp的超时重传特性,我的ack需要对方确认,而对方的ack我也要确认,对方还要确认我的ack,变成了死循环。
    注意:ack可以单独发送,但是为了效率,往往是发送数据的时候,捎带发送ack
出现RST的几种情况
1、端口未打开,C向S发送SYN,去连接S的端口9820,但是S没有打开9820端口,这个时候S发送RST
2、请求超时,C向S发送SYN,S回复ACK+SYN,如果C从发送SYN到收到S的ACK+SYN,时间过长,认为超时,
    C发送RST,表示拒绝进一步发送数据。
3、提前关闭连接,S端tcp协议收到的数据,应用程序没有接收,S发送RST,提前关闭连接。
4、S端socket调用close不愿再接收数据,但是还是从C收到数据,发送RST
5、在现场环境中,遇到一种情况发送rst包,特别注意一下,测试一下。
    a、M70解码器和主控板在一个网络内,M70从业务板卡vtdu获取码流,通过OVER_RTSP方式,当连接vtdu的内网时,可以连接成功。
        但是,一旦有数据交互,就发现M70发送rst包。
    b、原因是:从M70抓包发现,TCP的三次握手都是直接和vtdu的内网交互。
        一旦有数据交互,vtdu回复ack,IP地址就成了主控板的外网地址,因为vtdu板卡的默认路由器是主控板的内网IP,
        经过主控板设置了SNAT,源IP转换为主控板的外网IP,导致M70认为异常,发送了rst包。
    c、怎么修改?
        强制M70经过主控板,去连接内网的vtdu。
测试tcp端口
1、测试网络通不通,使用ping测试,使用traceroute(windows下是tracert)测试网络路由
2、测试tcp端口是否打开,使用telnet ip port,ctrl+]退出,进入telnet,再quit或者q退出telnet
3、在windows下,输入telnet,报错telnet不是内部或外部命令,怎么解决?
    需要开启telnet客户端,操作:控制面板-->程序-->程序和功能-->打开或关闭Windows功能-->Telnet客户端,打钩,确定
    相同位置还有Telnet服务器,在命令输入 services.msc进入服务,可以查看Telnet服务是否开启
    注意: services.msc    进入服务管理
            mstsc           远程桌面
            eventvwr        进入事件查看器
4、另外windows下如何开启telnet服务。win7默认不开启telnet服务
    a、控制面板-->程序-->程序和功能-->打开或关闭Windows功能-->Telnet服务器,打钩,确定
    b、重新进入 services.msc找到telnet,启动。
    c、启动失败,通过属性,找到依存关系,把依赖的组件启动,然后再启动telnet
糊涂窗口综合征以及解决办法
1、糊涂窗口综合症是指当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,
    就会使应用进程间传送的报文段很小,特别是有效载荷很小。
    极端情况下,有效载荷可能只有1个字节,传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象。
2、怎么解决糊涂窗口综合征?
    从接收端来考虑,当前缓冲区已满,应用程序取走一个字节,接收端通知发送端,缓冲区多了一个字节,
    这样发送端就会发送一个字节,导致糊涂窗口综合征。解决办法很简单,就是当应用程序取走一个字节,接收端通知发送端,
    缓冲区还是偏移0个字节。当应用程序取走比较多的字节,积攒到一定程度,再通知发送端,缓冲区偏移了多个字节,
    这样发送端可以一次性发送多个字节。
    但是,这样会导致发送端窗口收缩的问题。正常情况下,发送端可以发送但是还没有发送的字节序号是增加的,不会减小。
    考虑下面的情况,接收端期望接收第10000个字节,缓冲区满了,右边界也是10000,通知发送端是[10000,10000],
    然后应用程序取走1025个字节,通知发送端[10000,11025],发送端发送1024个字节,接收端收到,上层应用程序还没有取走,
    按道理应该通知发送端[11024,11025],但是为了防止发送端发送1个字节过来,通知发送端为[11024,11024]
    站在发送端的角度来看,上一次为[10000,11025],这一次为[11024,11024],感觉很奇怪,允许发送的字节序号变小了。
    这就是发送端的发送窗口收缩。
3、注意:在实际情况下,为了解决糊涂窗口综合征,发送端也会进行相应的处理。比如,现在允许我发送一个字节,我不发,
    等到积累到一定程度我再发送。
长连接和短连接
1、长连接和短连接的使用场景不一样:
    长连接建立连接后,开始频繁地交互。
    短连接建立连接后,交互一下子,下一次交互不知道什么时候。
2、tcp连接一旦长时间不交互,连接就会断掉。那么,对于长连接怎么保持连接不断呢?
    使用tcp的保活机制,keep-alive,
    当然也可以在应用层处理,定时发送心跳包。
Copyright (c) 2015~2016, Andy Niu @All rights reserved. By Andy Niu Edit.