前言
在上一篇文章中我们介绍了关于UDP是如何实现的,今天我们要介绍的是TCP,关于TCP协议,前面说了是可靠的,今天我们就一起来看看TCP协议为什么是可靠的,关于可靠的实现采用了什么策略。
1.TCP协议
TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制;
当数据拷贝到传输层的时候,继续向下传输数据的时候操作系统有自己的传输策略,所以TCP协议被称为传输控制协议,TCP协议既有发送缓冲区又有接受缓冲区,所以TCP是全双工的
如图所示:
2.TCP协议段格式
a.tcp协议报头是有标准长度的20byte,在读取的时候先读取20byte
b.转化成结构化数据,提取标准报头的四位首部长度,约定tcp报文的总长度=
4位首部长度*4byte [0,60],如果我们报头就是20byte,那么4位首部长度,应该填写5 x * 4byte = 20 x = 5 [0101]
c.得到后续报头剩余的大小:x*4 - 20 = 0; x * 4 - 20 = n;
d.只要把报头处理完毕,剩下的就是有效载荷
3.如何解包如何分用
解包:按照上面的方式进行解包
分用:按照报头里面的目的端口号,就可以找到应用层的进程了,数据就能够向上交付了
4.网络协议栈和文件的关系
如图所示:
5.如何理解TCP报头
报头在语言层面就是一种结构化的数据:
struct tcp_ hdr
{
uint32_t src_port:16;
uint32_t dst_port:16;
uint32_t seq;
uint32_t ack_seq;
uint32_t header_length:4;
....
};
6.TCP的特点
可靠性,传输效率高
a.理解可靠性:
为什么会存在不可靠性:网络传输存在不可靠性的本质原因是因为传输的距离变长了
不可靠性的场景:丢包,乱序,校验错误,重复......
b.tcp如何保证可靠性:
感性理解可靠性
A和B通信:
距离变长了,不存在绝对的可靠性,但是存在相对的可靠性。
如何保证相对的可靠性呢?
一个报文只要收到了应答。就能保证该报文的可靠性!
TCP采用确认应答机制保证可靠性,双方通信一定存在最新消息,如果没有应答,最新消息一般无法保证可靠性
理解TCP的工作模式:
保证可靠性,无论是客户端到服务端还是服务端到客户端都需要有应答,双方在通信的时候,可能除了正常的数据段,还可能包含确认数据段。
TCP的真实工作模式:
tcp协议数据传输的时候达到对面的顺序和发送时的顺序不一定一样。
为了解决上述问题:需要有方式标识数据段本身,标识数据段本身采用序号和确认序号的方式
理解序号和确认序号:
确认序号 = 序号 + 1 并且是连续的
为什么序号有两组:因为TCP是全双工的,客户端向服务端发送消息,服务端向客户端发送消息
真实序号: 因为tcp协议传输数据是面向字节流的,所以将每个字节的数据都进行了编号,即序列号
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
7.TCP字段
7.1 16位窗口大小
tcp在发送数据的时候,快了不行,慢了也不行,如何保证发送合适的数据呢?
解决方式: 16位窗口大小一本质上是一块缓冲区,当一方向对方发送数据的时候,在自己的报头中填写缓冲区剩余空间的大小! 通过16位窗口的方式实现数据传输时的流量控制
7.2标志位
服务器会受到各种各样的报文,接受方需要根据不同的报文,做不同的动作,tcp报文也是有类型的,有的是正常的数据段,有的是ack确认数据段,按照不同的标志位做不同的动作。
tcp报文类型的划分: URG. ACK. PSH, RST, SYN, FIN
SYN:连接建立的时候,将该标志位设置为1
FIN:连接断开的时候,将该标志位设置为1
ACK:报文为确认类型的时候,将该标志位设置为1
PSH:当接收方的缓冲区满了之后,发送方催促接受方尽快将数据拿走,告知这个信息,将该标志设置为1
URG:
数据对于接收方而言,乱序本身就是不可靠的表现,所以要对收到的数据进行排序,保证数据的时序到达! 如何进行排序呢?采用序号的方式进行排序
排序之后:接收方的缓冲区就是一一个队列.
此时如何有数据想要进行插队呢?如何有数据想要进行插队,就将该数据报文中的URG设置为1
被设置URG的数据报文被16位紧急指针标识
16位紧急指针:在有效载荷中的偏移量
该数据有几个字节:在tcp协议中该数据只有一一个字节
此时该数据就能够被应用层找到提前被读取,实现数据插队一般将这种数据称为是带外数据
使用场景:检测服务器的状态
在应用层如何标识带外数据呢?发送方: send . ssize_t send(int sockfd, const void *buf, size_t len,int flags) ; flags: MSG_ OOB 接受方: recv ssize_t recv(int sockfd, void *buf, size_ t len,int flags) ; flags: MSG_ OOB
RST:
三次握手建立连接成功,即使连接成功了,通信的过程中也有可能单方面出现问题
对于一方来说连接还存在,对于出问题的一方来说,连接己经不存在了,但是没有出问题的一方认为连接还存在就会向另一方发送数据,此时数据发送到出问题的一方时, 出问题的一方检测连接己经断开了,但是任然被发送数据,此时就会将RST标志位设置为1,标识恢复连接
8.超时重传
如何理解数据传送时的丢包:
丢包出现的可能性有两种:
1.数据在发送的时候丢失了
2.接受方给发送方发的应答丢失了
针对上面的两种情况tcp协议定制的策略是超时重传
如果是第二种情况,就会出现发送方给接受方发两份同样的数据,此时就需要进行去重,如何进行去重呢?可以采用序号进行去重!
既然存在重传的可能性,就说明发送端发送的数据并没有发送完之后被立即移除掉
如何理解超时:
超时是根据网络情况决定的,是一个浮动的时间间隔!
一般最理想的情况下,找到一个最小的时间,保证”确认应答一定能在这个时间内返回”.
Linux中(BSDUnix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍.
如果重发一次之后,仍然得不到应答,等待2500ms 后再进行重传
如果仍然得不到应答,等待4500ms 进行重传.依次类推,以指数形式递增.
累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接
9.连接管理机制
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接
如何理解三次握手:
三次握手是tcp协议建立连接定制的策略,三次握手不一定保证连接一定会成功!
一次握手和两次握手可以吗?
答案是一定不行的,因为当客户端和服务端建立连接的时候,连接是需要被os管理起来的,管理的方式先描述,再组织。
管理一定会有时间和空间的成本,如果一次握手和两次握手就能够让连接成功,服务端就有可能会受到客户端的攻击,一般将这种攻击称为是SYN洪水
三次握手的特点:
a.用最小的成本验证全双工通信信道是通畅的
b.三次握手可以有效防止单机对服务器进行攻击.
如何理解tcp要建立连接?
tcp建立连接是因为要保证可靠性。
如何保证可靠性?
结构体字段保证了可靠性的数据结构基础,三次握手是创建连接结构体的基础,通过这样的方式间接保证了tcp的可靠性!
如何理解三次握手以上的建立连接:三次握手以上也能建立连接,但是造成了不需要的资源浪费!
如何理解四次挥手:断开连接是双方的事情,需要征得双方的同意
这里所谓的不发数据是指不发用户数据,并不代表底层没有管理报文的交互
tcp是如何知道数据已经发送完了,需要断开连接了呢?
答案是tcp并不知道,但是上层会调用close (sock),关闭文件描述符标识着数据已经传输完毕了,需要断开连接了。
四次挥手时的状态变化:
主动断开连接的一方, 最终状态是TIME_WAIT状态
被动断开连接的一方,最终状态时CLOSE_WAIT状态
上面的两种状态和双方是客户端还是服务端是没有任何关系的,因为TCP是地位对等的协议!
如果服务器大量出现close_wait:
1.服务器有bug,没有做close关闭文件描述符的动作
2.服务器有压力,可能一直在推送消息给client,导致来不及close
四次挥手动作完成,但是主动断开连接的一方要维持一段时间的TIME_WAIT:
为什么?一般需要多长时间,为什么?
一般需要等待两个MSL的时间才能回到CLOSED的状态!
为什么需要等待?
1.是因为可能会存在最后一个ACK响应丢失的情况,一旦丢失就需要进行超时重传,如果没有这个等待时间,就有可能会存在最后一个ACK响应丢失,一端认为并没有断开连接,继续发送信息,此时就是一种bug了
2.双方在断开连接的时候,网络中还有滞留的报文一保证滞留报文进行消散
如何理解MSL:
MSL是TCP报文的最大生存时间
可以通过cat /proc/ sys/net/ ipv4/tcp_ fin_ timeout查看ms1的值:60
如何理解服务器有时候可以重启,有时候不能重启binderror的原因:
当服务器主动断开时,服务器处于TIME_WAIT状态,此时就无法绑定成功了!
如何解决这个问题:
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
10.滑动窗口
如何理解滑动窗口:
首先数据可能存在丢失的情况,因此有超时重传机制,决定了数据由发送端发送数据之后不会被清除掉,而是保存在发送缓冲区中,发送缓冲区又被细化分为:
如图所示
前半部分:已经发送并且受到应答
中间:滑动窗口->已经发送,但是没有收到应答!
后边:数据尚未发送.
最后边:没有数据,只有空间
如何看待滑动窗口:
窗口大小起始如何设定的,未来如何改变?
win_ start = 0; win_ end = win_ start + tcp_ win ->未来无论怎么移动,都要保证对方能够正常接受! 所以起始滑动窗口大小=对方告知我的自己接受能力的大小
窗口会向左滑动吗?一定 会向右滑动吗?
答案是不会向左滑动
可能会向右滑动,也可能保持不变
滑动窗口会一直不变吗?会变大吗?会变小吗?变的依据是什么?
答案是滑动窗口可能会一直不变, 也可能会变大,也可能会变小,变的依据是根据对方可接受缓冲区的大小!
收到应答确认的时候,如果不是最左侧发送报文的确认,而是中间的,结尾的,怎么办?
如果说没有收到应答确认就说明是丢包了:
丢报包含两种情况:
1.数据没丢,只是应答丢了
2.数据真的丢了
针对第一种情况: 因为确认序号的定义是ACK seq X + 1, 表示X + 1之前的数据全部都收到了win_ start+X+1
针对第二种情况:数据真的丢了返回的确认序号依旧是前面的确认序号,规定ack序号连续三个相同的序号,会触发重传机制
**滑动一直向后滑动,空间不够了怎么办? **
发送缓冲区在内核中被设置为环形结构
11.拥塞控制
如何理解拥塞控制:
client向server端发送报文时,可能会出现丢失1~2个报文的问题,此时可以采用重传机制进行重发但是如果一旦出现client像server端发送1000个报文,而丢失了999个报文,此时就可能是网络出现了问题,因为网络出现了问题,所以不能采用超时重传机制向网络中传送大量的报文,如果一旦传送就可能会造成网络问题更加严重,,所以针对网络出现的问题,采用拥塞控制的解决方案!
拥塞控制的机制:
TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
采用指数增长的模式进行传输
在client向server发送数据的时候,client端会有一个滑动窗口,在发送数据的时候,会经过网络,标识网络接受数据能力的大小使用拥塞窗口
拥塞窗口的特点:
1.发送开始的时候,定义拥塞窗口大小为1
2.每次收到一个ACK应答,拥塞窗口+1
3.每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为实际发送窗口的大小->滑动窗口=min(拥寒窗口,窗口大小(server接受的能力))
拥塞窗口的增长速度是指数级别的:
慢启动是指初始是慢,但是增长速度是非常快的,所以为了控制,不能使拥塞窗口单纯的按照指数形式增长
采用的方法:使用一个阈值
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长!
当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
总结:
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
12.延迟应答
延迟应答的目的:在不考虑网络拥塞的情况下,尽量提高传输效率
如何理解延迟应答:
延迟应答的本质是在收到报文的时候,不是立刻进行应答,而是在经过一定的时间隔之后进行答,这种方式的目的是,在这个时间间隔内可能上层会拿掉缓冲区的数据,此时进行应答的时候告知client, sereve端 接受缓冲区变大了,进而client发送更多的数据,提高数据的传输效率!
举例说明:
假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
所有的包都可以延迟应答么?
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;如何理解每个n个包应答一次,能够保证server端收到数据了呢?
原因:是因为确认序号的特性决定的
收到当前确认序号代表在这之前的内容都已经收到了!
13.捎带应答
client向server端发送数据,server端接受到数据之后也会向client发送数据,在发送数据的时候会将上一次c1ient发送的数据的应答一起发送过去,此时,这种应答方式被称为是捎带应答,本质上也是提高数据传输效率!
14.理解TCP的面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用write时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
15.粘包问题
首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
站在应用层的角度, 看到的只是一串连续的字节数据.
那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.
对于定长的包, 保证每次都按固定大小读取即可;
对于变长的包, 可以在包头的位置, 约定一个包头总长度的字段, 从而就知道了包的结束位置;
对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);对于UDP协议来说, 是否也存在 "粘包问题" 呢?
对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况.
16.TCP异常情况
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.
17.TCP小结
可靠性的保证:
校验和,序列号,确认应答,超时重传,连接管理,流量控制,拥塞控制
提高性能:
滑动窗口,快速重传,延迟应答,捎带应答
基于tcp的应用层协议:
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
18.TCP/UDP对比
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定.
19.如何使用UDP实现可靠传输
参考TCP的可靠性机制, 在应用层实现类似的逻辑;
例如:
引入序列号, 保证数据顺序;
引入确认应答, 确保对端收到了数据;
引入超时重传, 如果隔一段时间没有应答, 就重发数据;……
20.理解listen的第二个参数
举一个生活中的例子:
在吃海底捞的时候,如果里面人已经满了,当继续有客户来的时候,一般会在外边有桌子让来的客户坐着排队等候。
排队的本质是:当有人吃完离席的时候,等候的客户可以马上吃海底捞,对于老板来说,提高了桌子的利用率,进而提升收入,但是排队等候的也不利于过长
而listen的第二个参数本质上就是维护一个队列的大小
当client向server发送请求建立连接的时候,如果当前连接数已经满了,会将请求建立连接的client加入到该队列中该队列的大小就是listen的第二个参数
验证:设置listen的第二个参数大小为3
此时启动3个客户端同时连接服务器,用netstat查看服务器状态,一切正常.
但是启动第四个客户端时,发现服务器对于第四个连接的状态存在问题了
如图所示:
客户端状态正常, 但是服务器端出现了 SYN_RECV 状态, 而不是 ESTABLISHED 状态
这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列:
- 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
- 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
而全连接队列的长度会受到 listen 第二个参数的影响.
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.
这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1.
总结
以上就是TCP协议实现的全部内容,相信看完UDP协议的实现和TCP协议的实现对比而言,TCP为了实现可靠性,采取了相当多的策略,感谢大家的阅读,今天的介绍就到这里了,我们下次再见!
版权归原作者 linkindly 所有, 如有侵权,请联系我们删除。