TCP协议

TCP报头

TCP报头
源端口号/目的端口号: 表示数据从哪个进程来, 到哪个进程去.
32位序号:
4位首部长度: 表示该tcp报头有多少个4字节(32个bit)
6位保留: 顾名思义, 先保留着, 以防万一
6位标志位
URG: 标识紧急指针是否有效
ACK: 标识确认序号是否有效
PSH: 用来提示接收端应用程序立刻将数据从tcp缓冲区读走
RST: 要求重新建立连接. 我们把含有RST标识的报文称为复位报文段
SYN: 请求建立连接. 我们把含有SYN标识的报文称为同步报文段
FIN: 通知对端, 本端即将关闭. 我们把含有FIN标识的报文称为结束报文段
16位窗口大小
16位检验和: 由发送端填充, 检验形式有CRC校验等. 如果接收端校验不通过, 则认为数据有问题. 此处的校验和不光包含TCP首部, 也包含TCP数据部分.
16位紧急指针: 用来标识哪部分数据是紧急数据.
选项和数据暂时忽略

TCP连接三次握手

三次握手

TCP断开四次挥手

四次挥手

关于TIME_WAIT状态

TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待2*MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.
TIME_WAIT状态存在的作用:首先,可靠安全地关闭TCP连接。比如网络拥塞,如果主动关闭方最后一个ACK没有被被动关闭方接收到,这时被动关闭方会对FIN进行超时重传,在这时尚未关闭的TIME_WAIT就会把这些尾巴问题处理掉,不至于对新连接及其他服务产生影响。其次,防止由于没有持续TIME_WAIT时间导致的新的TCP连接建立起来,延迟的FIN重传包会干扰新的连接。
无TIME_WAIT的危害:首先,当网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包到达后会直接影响新的TCP连接;其次,当网络情况不好时,同时没有TIME_WAIT等待时,关闭连接后无新连接,那么当接收到被动方重传或延迟的FIN包后,会给被动方回送一个RST包,可能会影响被动方其他的服务连接。

我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;

解决TIME_WAIT引起的bind失败问题
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的.
服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有大量的客户端来请求).
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.
由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 导致服务器的端口不够用, 无法处理新的连接.
解决方法:

  • 使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符.
    用法:
    在server代码的socket()和bind()调用之间插入如下代码
 int opt = 1;
 setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

确认应答机制(ACK机制)

确认应答机制
TCP将每个字节的数据都进行了编号, 即为序列号.
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你要从哪里开始发.比如, 客户端向服务器发送了1005字节的数据, 服务器返回给客户端的确认序号是1003, 那么说明服务器只收到了1-1002的数据.1003, 1004, 1005都没收到.此时客户端就会从1003开始重发

超时重传机制

超时重传
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B , 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发 ,但是主机A没收到确认应答也可能是ACK丢失了.
这种情况下, 主机B会收到很多重复数据. 那么TCP协议需要识别出哪些包是重复的, 并且把重复的丢弃.
这时候利用前面提到的序列号, 就可以很容易做到去重.

滑动窗口

滑动窗口
窗口大小指的是无需等待确认应答就可以继续发送数据的最大值.上图的窗口大小就是4000个字节 (四个段).

发送前四个段的时候, 不需要等待任何ACK, 直接发送
收到第一个ACK确认应答后, 窗口向后移动, 继续发送第五六七八段的数据…

因为这个窗口不断向后滑动, 所以叫做滑动窗口.
操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答
只有ACK确认应答过的数据, 才能从缓冲区删掉.

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被填满, 这个时候如果发送端继续发送, 就会造成丢包, 进而引起丢包重传等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度.
这个机制就叫做 流量控制(Flow Control)

接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,
通过ACK通知发送端;
窗口大小越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口大小的通知之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0;
这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 让接收端把窗口大小再告诉发送端.
流量控制

粘包问题

首先要明确, 粘包问题中的 “包”, 是指应用层的数据包.
在TCP的协议头中, 没有如同UDP一样的 “报文长度” 字段
但是有一个序号字段.
站在传输层的角度, TCP是一个一个报文传过来的. 按照序号排好序放在缓冲区中.
站在应用层的角度, 看到的只是一串连续的字节数据.
那么应用程序看到了这一连串的字节数据, 就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包.
此时数据之间就没有了边界, 就产生了粘包问题
那么如何避免粘包问题呢?
归根结底就是一句话, 明确两个包之间的边界
对于定长的包

  • 保证每次都按固定大小读取即可
    例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可

对于变长的包

  • 可以在数据包的头部, 约定一个数据包总长度的字段, 从而就知道了包的结束位置
    还可以在包和包之间使用明确的分隔符来作为边界(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)

TCP 异常情况

进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制.
例如HTTP长连接中, 也会定期检测对方的状态.
例如QQ, 在QQ断线之后, 也会定期尝试重新连接.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值