TCP协议

TCP(transport control protocal)传输控制协议,是一种面向字节流的有连接的可靠传输协议。

报文格式

源端口号是为了告诉接收方消息是谁发来的,目的端口号是为了标识报文要向上层的哪个端口交付有谁来处理。

首部长度表示报文首部的长度,首部长度最大值为15,但是计算时要×4,所以报文首部的最大长度是60。

序号是发送数据的标号,因为TCP是面向字节流的,发送数据是按顺序发的但是收到数据的顺序不一定是按顺序的,数据接收方要对数据作排序以获得正确的数据。

确认序号是接收方通知发送方的标识,例如发送方连续发送了多个报文,接收方收到后通过32位序号确认哪些序号前的报文数据已经收到了,如果前1000个字节的数据收到后会将确认序号设置为1001。发送方收到报文后就知道前1000个字节的数据已经安全可靠的送达,现在从1001个字节开始发。

6个标志位:区分报文的类型
SYN
:请求建立连接; 携带SYN标识的称为同步报文段

ACK: 确认号是否有效,只有这个标识位是1的时候确认序号才是有效的

FIN: 通知对方, 这个端要关闭了,携带SYN标识的称为结束报文段

PSH:提醒接收方要尽快把接收缓冲区的数据读走,当接收方的接收缓冲区快满的时候会出现这种情况

RST:对方要求重新建立连接; 我们把携带RST标识的称为复位报文段。例如:第三次握手的报文丢包,客户端以为已经和服务器建立连接,当其发送数据后,服务器由于未建立连接收到数据后就会向客户端发送RST报文。

URG:紧急指针是否有效,只有这个标识位是1的时候紧急指针才是有效的。

窗口大小是接收方告诉发送方自己还有多少缓冲区可以接收,发送端会根据这个值来发送数据,避免接收端处理不过来。

窗口大小由接收方的窗口大小决定,通常情况下发送方发送的数据大小不能超过接收方的窗口大小。

校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也 包含TCP数据部分

紧急指针:标识需要紧急处理的数据在报文数据部分的位置,紧急数据的大小一般是1字节。例如:服务器由于某种原因,资源紧缺不能及时响应客户端的请求,客户端会发送一个紧急指针有效的报文询问服务器状态,服务器收到后会优先处理紧急数据(毕竟紧急数据很小只有1字节),根据标识的数据位置在报文的数据中寻找并处理,发送报文给告知客户端服务器资源紧缺。

TCP机制

为什么TCP是可靠的传输?因为TCP有比UDP更复杂的处理机制。

  • 可靠性:
    • 校验和
    • 序列号(按序到达)
    • 确认应答
    • 超时重发
    • 连接管理
    • 流量控制
    • 拥塞控制
  • 提高性能:
    • 滑动窗口
    • 快速重传
    • 延迟应答
    • 捎带应答

确认应答机制

TCP通信双方每次收到消息都会发送一个确认应答的报文,用户来告诉发送者这个消息我收到了

捎带应答机制

TCP是全双工的(每个端都可以发送和接收数据),如果一个端要发送数据的同时又要对刚收到的数据进行应答,这个机制就可以提高通信的效率。

超时重传机制

主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B; 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发

主机A未收到B发来的确认应答, 也可能是因为ACK丢失了,主机B会收到很多重复数据. TCP协议利用序号字段就能够识别出哪些包是重复的包, 并且把重复的丢弃掉.

那么超时时间如何计算呢?TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.

Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时 时间都是500ms的整数倍. 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传. 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接 

管理连接机制

三次握手

三次握手的作用

  • 与通信的主机建立可靠连接
  • 与通信主机协商窗口大小(流量控制)
  • 与通信主机协商发送数据报的序号

序号是一个随机值,例如:双方确认的序号是n,发送方发送1-1000的数据时,这个数据包的序号就是n+1~n+1000;如果接收方收到的数据序号是i~j,那么接收方认为收到的数据序号是i-n~j-n;确认号-n就是下次发送方要发送数据的位置

三次握手是由操作系统自动完成的(与是否调用accept接口无关),操作系统会维护两个队列,全连接队列和半连接队列,它们是什么有什么用呢?

全连接队列

先回忆一下TCP套接字的使用,客户端socket获得文件描述符然后bind主机IP和端口号,最后listen监听,而listen的第二个参数就和全连接队列有关,这个参数的大小+1就是全连接队列的大小。操作系统为了管理主机建立的多个连接,创建一个全连接队列,当第三次握手服务端收到ACK确认后服务端连接会进入ESTABLISHED状态,此时双方互相建立了连接即所谓的全连接,服务端将这个连接放到全连接队列。全连接队列的大小不会太大,因为服务器资源不是无穷无尽的,队列太大服务器可能无法及时处理队列中的请求。

半连接队列

当全连接队列满后,服务器有新的连接请求时,在第三次握手客户端发来ACK后,服务端接收报文但是不进入ESTABLISHED状态一直保持SYN_RCVD状态,并将这个连接放到半连接队列中,当全连接队列中有连接结束时,才会将半连接队列中的连接的状态变为ESTABLISHED,然后将这个连接放到全连接队列进行通信。注意,操作系统不会维持半连接状态太长的时间,半连接队列也是有大小的。

四次挥手

挥手的过程就不赘述了,值得一提的是先提出断开连接的一方在断开连接后会进入TIME_WAIT状态。这个状态会持续一段时间才会结束,这是在干什么呢?

以服务端为例,如果服务端先提出断开连接,在断开后立马重启服务器,就会发现不能重启。

这是因为挂掉的服务的IP和端口还在被使用。那么为什么断开了还在被使用?有两个原因:
1.等待双方通信的历史数据消散。双方即使断开连接,也可能有数据包正在网络中传输,如果立马进行下一个连接,通信时可能会导致数据的冲突(虽然报文有序号字段,冲突的可能很小,为了保证可靠传输必须避免冲突)。

2.让四次挥手更可靠的完成。如果第四次挥手ACK发生了丢包,发出第三次挥手FIN的一端会超时未应答,重新发送一个请求断开的报文。

服务器在挂掉后,往往需要立即重启,而且端口号是固定的不能修改,怎么办?

在服务端使用setsockopt接口,实现端口复用:

函数原型

int setsockopt(int sockFd, int level, int optname, const void *optval, socklen_t optlen);

参数说明
1、sockfd:将要被设置或者获取选项的套接字。
2、level:选项定义的层次;一般设成SOL_SOCKET以存取socket层。

3、optname: 欲设置的选项。此处使用SO_REUSEADDR参数(端口复用),它是optname选项之一,允许套接口和一个已在使用中的地址捆绑。

4、optval: 对于setsockopt(),指针,指向存放选项待设置的新值的缓冲区。获得或者是设置套接字选项.根据选项名称的数据类型进行转换。

5、optlen:optval缓冲区长度。

int sockfd = socket(...)
//先socket创建套接字

int opt = 1;
setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR, (const void *)&opt, sizeof(opt)

//在设置端口复用后bind IP和端口号

bind(...)

为什么要三次握手?
其实三次握手可以分为四次,一般情况下把第二次应答和第三次请求一起发送(捎带应答)。

原因有两个方面:

一方面:三次握手可以保证双方都分别进行了一次可靠的数据传输和数据接收,即可靠验证全双工。为什么不能是两次呢?因为第三次确认确保了第二次的请求成功发送了,即主机B进行了一次可靠传输。

另一方面:如果只进行一次握手,会造成SYN洪水。当恶意主机一直请求连接,服务器会建立很多连接,由于服务器维护连接是需要开销的,就会导致服务器资源不足甚至终止。如果只进行两次握手,服务器的连接请求不会收到应答,就导致服务器会在第一次握手时就建立连接,而客户端在第二次握手也就是服务器之后建立连接。在服务器先于客户端建立连接的情况下,如果第二次握手丢包或者恶意客户端不建立连接,都会导致服务器的连接失效,产生无用的开销。只有三次握手才会避免前两者的问题。

为什么要四次挥手?

在没有数据要发的情况下,一端会请求断开。而两端都会发送数据,两端都需要断开,总共要断开2次,即四次挥手。

为什么不是三次挥手?

在客户端发送中断后,如果服务端还有发送的数据,就先发数据报文直到发送完毕后才断开。服务端并不是确认客户端断开后就要立即发出中断请求的。

流量控制

因为数据接收方对数据的处理能力是有限的,如果发送方无休止的发送大量数据,可能会将接收方的接收缓冲区塞满,造成丢包。所以每次数据接收方收到数据后,都会通过ACK应答将窗口大小发送给数据发送方。数据发送方根据对方的窗口大小控制数据的发送速度,这就是流量控制。

拥塞控制

造成丢包的原因不只有通信双方的问题,还可能是通信网络出现问题。如果网络状况不好,继续发送大量数据,造成丢包,再不断的重传数据,就会让网络状况更差。所以需要拥塞控制通过实时对网络状况评估,调整通信双方数据发送的速度。

拥塞控制是一种慢启动机制。引入一个概念叫拥塞窗口,即当前网络状况下允许发送的数据量。慢是因为每次拥塞控制的拥塞窗口大小都从1开始,目的是发送少量数据包“探路”,如果网络状况好会将拥塞窗口大小翻倍,以此类推。

拥塞窗口的大小是指数增长的,所以慢启动并不慢。但是增长不是无止境的,当拥塞窗口大小达到慢启动的阈值,会开始线性增长,即每次拥塞窗口的大小+1。

当拥塞窗口达到网络的承载能力,发现发送的数据开始大量丢包(数据少量丢包不会认为网络拥塞,只会进行超时重传),拥塞窗口的大小会直接变为1,慢启动阈值减半,重新开始慢启动过程。

快重传

发送方发送一批报文中,如果某个报文丢失。例如图中的发送方发送了1-7000的数据,1001-2000丢失。接收方收到数据后会一直发送确认序号1001的报文,发送方连续三次收到重复的应答后,会立即重发1001-2000的数据。

滑动窗口

滑动窗口是在发送缓冲区中的一个概念。

滑动窗口可以由两个指针维护,缓冲区被滑动窗口划分为3个部分,第一部分的数据可以被直接覆盖可以理解为在发送并确认后被删除了,第二部分的数据正在发送和确认的路上,第三部分为待发送的数据。

滑动窗口的大小=min{接收方窗口大小,阻塞窗口大小,剩余数据大小},根据已确认送达数据的位置确认滑动窗口左指针的位置,左指针位置+滑动窗口大小就是右指针位置。

延迟应答

网络的吞吐量越大,数据传输的速率越高。接收窗口的大小是影响网络吞吐量的要素之一,当接收方收到数据后立马发送ACK,此时应用层还没有将接收缓冲区的数据拿走,那么返回的窗口大小就会减小。可能在很短时间内,应用层就会将数据拿走,如果等待一点时间接收方再发送ACK,窗口大小就可能变大,网络的吞吐量就会提高。

在上图中,主机B收到1 ~ 1000的数据后并没有立即确认,而是等到1001 ~ 2000到达后直接发送确认号为2001的ACK,这样做也减少了应答次数,减少了报文发送数量。

粘包问题

接收缓冲区往往有多个数据包,在应用层角度这些数据包就是一个很长的字符串,那么应用层就要有能够分离数据包的能力,怎么分离数据包呢?

这需要程序员自定义协议,例如在应用层规定数据包之间必须以回车换行符结尾,那么在读缓冲区时只需要寻找回车换行符来依次获取数据。自定义协议可以这样设计:

  • 定长报文,每次读取固定大小的字节数据就是一个报文
  • 特殊字符,用特殊字符分割,对读取的数据处理,特殊字符间的数据就是数据包
  • 自描述字段+定长报头,类似于UDP报头,报头中规定有效载荷的大小。在获取报头后继续读取有效载荷字段标识大小的字节数据。

TCP异常情况

在进程通信时发生如下情况:

进程终止:进程终止四次挥手是由操作系统自动完成的,仍然可以发送FIN请求

设备重启:设备重启时会先结束进程,所以和进程终止一样

设备断电/断网: TCP内置了一个保活定时器,会定期检测连接状态,如果发现对方未连接就会尝试重新连接或者断开连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值