"让新离开地表,才能找到盘旋爬升的动力。"
一、认识Tcp报头
(1) 协议报头格式
我们先来认识认识tcp协议报头字段。
跟tcp协议字段报头比起来,udp可真是太轻松了。
协议字段 | 作用 |
---|---|
源/目的端口号 | 从哪里来,到哪里去 |
32位序号/32位确认号 | 就是一种序号(之后会细讲) |
4位首部字段 | 表示TCP头部多少字段,最大长度为:15 * 4 = 60 |
6位标志位 | 区别不同的tcp报头类型 |
16位窗口大小 | 对端接受缓冲区大小 |
16位校验和 | 接收端校验不通过, 则认为数据有问题 |
16位紧急指针 | 保存的是紧急数据的偏移量 |
40字节头部选项 | 暂时忽略 |
数据 | 有效载荷 |
(2) 相关字段的理解
如何理解4位首部字段?
根据以上关于TCP协议字段,我们可以得出以下结论:
TCP报头是有长度的:20字节。要进行正常的网络通信的一个条件是,要让协议报头与有效载荷完成分离,也就是 " 解包与分用 "。然而,TCP的协议报头不是说是20字节嘛?那么要解包分用直接拿掉头上的20字节不就得了?
但其实,这是不对的。
如何理解特定的port 与 bind? 网络协议栈与文件有何关系?
我们在实际生活中,或多或少都知道,一些特别的网络协议栈,都会bind特定的服务端口。比如说: http\https 80:443 FTP:21 ssh:22 telnet:23 ……
同样,在LInux环境下,编写自己的第一份tcp套接字编程时,使用read\write系统函数write\read,向socket句柄读取或者写入数据时,这压根同LInux文件操作没有任何区别!
二、TCP可靠性
只要谈到TCP,尤其是说TCP和UDP有什么区别时,那TCP保证可靠性的特性那就不得不骄傲地提上几嘴。那么TCP是如何保证可靠性的呢?要理解可靠性,那么不得不先理解到 造成不可靠通信的原因。
如何理解不可靠?
我们可以举一个生活中,通信不可靠的例子。说白了,所谓的通信,本质上是为了让通信双方知晓对端发来的消息。
比如说你的舍友张三,此时正在你身旁的椅子上,饶有风趣地刷着抖音,看着视频,你一看现在时间已经晚上八点整了,把自律作为座右铭的你,此时想也不想对着手机屏幕说了句,
"几点了?!走吃饭去。"
此时,你可以确信无疑,你这句话张三是接收到了的。因为此时,你从他的眼里读出了无比坚定且期盼的眼神。
同样的,现如今你正在楼层为5F的宿舍里,啪啪敲击着键盘,碍于敲写代码的激情,但却难以忍耐解渴的本性,索性望向窗外,此时你的目光正抓住了开开心心走回宿舍大门的张三,于是乎,你赶忙跑向阳台,对着楼下的张三喊着,
"张三,你上来捎带帮我瓶水!"。
你确定的是(或许存疑),张三是没有听到这话的,因为他毫无表情、没有丝毫迟疑地走进了宿舍大门,消失在建筑物的遮挡之中。
对于第一种场景而言,当我发起与张三同学的对话,因为距离近,我可以从张三的表情或动作中,也或许他直接接上了我的回话,“走,吃饭去”,这样的直接了当中知晓,我"发送"的消息,他确乎是接收到了的。然而,到了第二种场景,我与张三的距离变长了,不是面对面了,这时,我向张三发送"消息",我又该怎么知道这个"消息",他是否知晓呢?
反过来,两进程跨网络通信,它们通信数据的传输出现不可靠性的根本在于,"距离边长了"。
不可靠性有哪些表现呢?
丢包、乱序、校验和错误、重复……
(1) 确认应答(ACK)机制 && 效率提升
再次回到上述场景,我知晓张三确乎收到了我发送的 "消息"的原因是什么? 即张三给我进行了"回应"——换个词语,也就是应答!
而在实际的网络通信过程中,Client端不止Server端发送一条消息。
我们认为:
① 只有收到了应答,“历史消息" 就可以100%确认对方已经收到了 ---> 这是可靠的
② 但,双方通信一定存在 "最新消息",即没有收到应答的部分 ---> 是不可靠的。
因此,我们常听说啊什么, TCP是可靠的,其实本质上可靠性指的是 "相对的可靠" ,而不是"绝对的可靠"。
TCP可靠性保证的一定是:
一个报文只要是收到应答,该报文一定可靠。
同样,也许你会说,啊难道我发一条消息,收到一条应答后,才能再发其他消息吗?这样未免太慢了。 答案是当然不是,真实的TCP工作模式应该是这样的:
序号与确认序号:
实际中,也会遇到报文 "乱序"的情况。比如说你先发报文1,过一会儿又发了报文2,但由于报文2路由很快,对端就先接收到了报文2,而后接收到了报文1。可是对于对端而言,它可"不知道"正确的报文顺序,因为"没有明显的标识"。
在TCP协议字段的报头里,分别有32位的序号和确认序号。
TCP正因为有序号、确认序号这种数据段标识自身,因此,如何Sever端能够清晰地知道自己接收的报文是否存在 "乱序" 的情况。
当客户端接收到了 "确认序号"也就意味着:
该 "确认序号"之前的内容已经全部被接收了,下次发送数据的序号请从 "确认序号" 开始进行编写。
为什么需要两组序号 ?
因为tcp是全双工的,不管是客户端还是服务端,都有自己的接收、发送缓冲区,即我可以想你发送消息,你也可以向我发送消息。
(2) 超时重传机制
根据图示,我们可以得出下面两个结论:
主机A发送数据给B之后, 数据无法到达主机B
如果主机A在一个 "特定时间间隔内" 没有收到B发来的确认应答, 就会进行重发;
对于主机A而言,它只知道结果,即我没有收到任何的应答包。
可是,也有可能主机B收到消息,并做出应答,但却发生了丢包。
因此,针对上述两种情况,一旦TCP检测到 在这个 "特定的时间间隔内" 都没有收到ACK应答,就会出发 "超时重传机制"。
由此,如果出现 "网络拥塞"等原因,导致主机B可能收到多份 因为超时触发的"重传机制",也就意味着主机B收到的报文一定出现 "重复"的现象。当然,对于TCP而言,因为有每一个发送的报文都有自己相应的序号,对于主机B而言,只需要将重复序号的报文进行丢弃即可。
如何确定超时时间?
● 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
● 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
● 如果超时时间设的太长, 会影响整体的重传效率;
● 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较"高性能"的通信, 因此会动态计算这个最大超时时间。
● Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时
● 时间都是500ms的整数倍.
● 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
● 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
● 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
小结:
根据以上的两个小节,我们似乎回答了一些网络通信中可能会出现的不可靠性问题:
(3) 连接管理机制
当一看到这个 "连接管理"机制的宏观图时,我想学过网络的不禁大呼一生: "三次握手,四次挥手"。
三次握手与四次挥手:
①Client端发起连接请求,发送携带"SYN"信息的报头给Server端,并进入SYN_SENT。
②Server端收到该消息后。进入SYN_RCVD状态,并且向客户端响应 "SYN+ACK"应答。
③Client端收到ACK应答后,进入ESTABLISHED,并发送最后ACK,Server收到后也进入ESTABLISHED。
至此,双方三次握手完毕,成功建立连接。
①Client端发送断开连接请求,发送携带"FIN"信息的报头给Server,并进入FIN_WAIT1状态。
②Server端收到消息后,进入ClOSE_WAIT状态,收到ACK的客户端进入FIN_WAIT2。
③Server端发送携带"FIN"信息的报头给Client,进入LAST_ACK。
④Client端收到ACK并发送最后ACK给Server后,进入TIME_WAIT状态,收到ACK后的Server直接进入CLOSED状态。
至此,双方通信信道关闭,不再维护TCP连接。
注: 首先发起断开请求的 会进入TIME_WAIT状态
其实三次握手、四次挥手看图示的过程都能够看懂,但更重要的不是过程,而是原因。
为什么要三次握手?不能是四次?五次?
预备:
① tcp是保证可靠性的,但是是相对的可靠。也就意味着握手是可能失败的。
② 链接是需要被管理的!是需要被OS管理的!先组织,再描述(结构化数据),是有维护成本的(时空)。
我们要谈论三次握手为什么行,就得谈论其他次数握手是不行的局面!
握手一次:
绝对不行!因为一旦连接建立就需要维护,这个成本维护是Server来负担的,一旦遭遇"SYN"泛红攻击,客户端只给发SYN而不建立连接,消耗的是你Server端的链接资源,以至于无法对外为其他正常请求提供服务。
握手二次:
同上原因。
握手三次:
a. 最小的成本检验双方接收、发送数据(全双工信道)是通畅的。
b. 有效防止单击对服务器的SYN攻击。因为是你客户端需要ESTABLISHED,客户端此时承受链接维护的成本。
握手四次五次?
当然这也就不用谈原因了,因为三次就够了,为什么还需要这么多次??
你说,"啊,这不行,如果对方攻击该怎么办呢?"
记住, "TCP不是为了帮你处理网络安全的问题的,它是网路通信的传输控制协议"。
为什么需要四次挥手?
断开连接同建立连接一样,在TCP中Clinet端与Server端地位都是对等的。
"这个是双方的事情,都需要经得双方的同意"。
理解CLOSE_WAIT状态:
CLOSE_WAIT状态是在接收到主动断开连接的一方发送的"FIN"消息后出现的。当被动断开的一方进行close() 后,那么状态就会发生改变。但,如果一个服务器出现了大量的CLOSE_WAIT状态,导致这个的原因可能是:
① 服务器有bug,没有写close关闭文件描述符的动作。
② 服务器有压力,可能一直推送消息给Client,来不及close
理解TIME_WAIT状态:
TIME_WAIT状态出现在主动断开连接的一方。如果断开的一方是服务器,那么还会出现服务器bind error的问题(因为陷入到了TIME_WAIT状态,原端口还被占用着!)。
当进入到TIME_WAIT状态后,说明四次挥手已经完成,为什么主动断开连接的一方还需要维持一段时间的 “time_wait”?
a. 保证最后一条ACK能被收到.
因为,如果客户端收不到ACK,超过一定的时间,就会出发"超时重传机制",但是此时你的Client端已经关闭了,不会再对FIN报文做出任何响应了,可是服务器仍然会对这个并不存在的连接付出维护成本。
b.保证滞留报文的消散(教材理由).
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL的时间后才能回到CLOSED状态.MSL是TCP报文的最大生存时间。就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失。(否则,如果是服务器的话,就会立刻收到上一个进程未被处理完的、迟到的数据,而这种数据其实是错误的)。
同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
四次挥手能不能变成三次?
这是一个常见的问题,一般而言指的是将 "ACK+FIN" 一起发送给Client端。这也和TCP效率优化相关的机制有关。
我们可以在Linux系统中查看内核设置的FIN_WAIT_TIME:
(4) 滑动窗口
我们知道tcp拥有一定的机制保证数据传输的可靠性,如确认应答、超时重传、以及连接管理。可是我们也知道,tcp通信的双方都有各自接收、发送缓冲区的,也就意味着,是啊我tcp有能力让你的数据能够可靠地到对面,但是由于对方接收数据能力实在太差,上层应用数据处理慢,导致对端接收缓冲区已被打满,此时不能再接收到新的数据,而新的数据因此会被丢弃!
显然,这也是我们不能容忍的!所以,发送数据的需要考虑的条件又得增加一条,
你得考虑 "对端的接收能力"。你说,那我们怎么知道对端的缓冲区大小呢?答案其实是我们当然知道!
在双方进行通信前,“三次握手” 就可以确定双方各自的 "窗口大小"。这里又提到"窗口大小",前面又提到tcp控制层的缓冲区,到底这是啥呀?
建模一:
这里就会提出几个滑动窗口的问题:
● 滑动窗口是怎样设定的?
● 滑动窗口未来是如何变化的? 会变大嘛?会变小嘛?
这里仅仅是讨论了滑动窗口两端边界的情况,有没有一种可能,发送端收到了中间的应答,没有收到win_start的应答呢?
丢包情况:
● 数据收到了,但是ACK应答丢了。● 数据真的丢了。
如果遇到只收到了中间的应答,但是没有收到左侧发送报文的应答这种情况,其实完完全全不用慌张,因为确认应答机制的定义是: 确认序号之前的内容,全部被对端收到了。
因此,即便你没有收到ACK_start,但是因为收到了ack_mid,你也就可以断定你左侧的报文数据是让对端收到了的。
此时只需将滑动窗口的win_start 滑动到确认序号处即可。
● 滑动窗口必须滑动嘛?会不会不动?或者变为0?
建模二:
(5) 拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题。你可得记住,网络中可不止有你一台主机可以发起请求,如果当前的网络状态就已经比较拥堵,如果贸然发送大量的数据,是很有可能引起雪上加霜的。
你不是有超时重传嘛? 到时候传不就得了?可是,你是tcp有超时重传,他也是tcp有超时重传,大家普遍使用的都是tcp传输控制协议,发现丢包后都进行超时重传,这样不管重传多少次,已经出现问题的网络,又会增加更多的报文,只会加重网络的故障!
因此,面对这样的情况,如果你有很大的文件传输没有收到应答,还应该重传嘛?答案是 不应该,不应该大量地传!
拥塞窗口:
此时引入一个拥塞窗口的概念,发送开始的时候, 定义拥塞窗口大小为1,每次收到一个ACK应答, 拥塞窗口加1。
有了上面的场景,我们就不得不再一次更正我们认知的发送数据方不仅仅要考虑对端是否呢个接收到,还得考虑网络的情况。
窗口大小 = 对端接收窗口大小
窗口大小 = Min(对端反馈的窗口大小,拥塞窗口大小)
妈耶,你这样看那拥塞窗口增长的怕是有点慢诶,反而因为多发小报文导致tcp通信效率的降低。
慢启动:
真实的拥塞窗口增值是指数级别的,所谓的 "慢启动",仅仅是刚开始的时候发送的报文少,一旦检测到网络状况良好后,增长速度是很快的。
考虑到当增长速度快时,有可能导致网络出现拥塞的状况,通常会设置一个 “慢启动阈值”,一旦发送的报文大小超过了阈值,此时的增长就会 按线性递增,减缓增长速度。
如果发生少量的丢包,那么重传的成本似乎并不大,也不会对加重网络拥塞的问题。
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
三、 TCP效率优化
(1) 延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。
其实说白了就是,你可以等待一定的时间给客户端进行ack应答,在这个时间段里,你的上层逻辑可能可以取走更多的数据,那么此时你能响应的ack窗口就会增加。
窗口越大,网络的吞吐量也就越大,传输的效率也就会越高。当然,这个大前提是 保证网络不拥塞。
(2) 捎带应答
我们可以发现,在tcp建立连接时,第二次握手是 ACK+SYN,而在四次挥手时,反而是将ACK与FIN分开进行发放。因为有时候也会提到 四次挥手可否改为三次?答案是可以的,因为这一次ACK+FIN可以通过捎带应答一起发送给主动断开连接的一方。
(3) 滑动窗口
没想到吧,这儿还有我。
我们都知道TCP是面向字节流的,而UDP是面向数据报的。一个很明显的特征是,对于UDP而言是会携带有效载荷的大小的,而UDP读取报文也是一个一个读,要么读成功要么读不成功,不会出现把报文读了一半的情况。
创建一个TCP的socket, 同时操作系统会在内核中创建一个发送缓冲区和一个接收缓冲区。当上层数据要发送数据时,需要使用系统调用接口write将数据拷贝到内核缓冲区当中,在TCP看来,这些所谓的上层数据,就是一个一个的字节!至于什么时候法就与上层业务逻辑无关了,因为这是由TCP自己决定!
● 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
● 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
而最终控制这个收发数据的多少的,就是滑动窗口!发多更好还是发少更好?都不如发合适最好,控制的含义不仅仅关于 限制,也可能是放开。滑动窗口可以在网络通畅,对方接收数据能力强的时候多发数据,也可以在网路出现问题,对方处理数据能力弱的情况下少发数据,从而保证数据传输的可靠,另一层面也提高了TCP通信的效率。
所以,udp协议通信效率一定高于tcp嘛?那可不一定。
四、TCP异常
下面有几种状态:
● Client进程或者Server进程挂掉
● 电脑关机
● 被拔网线或者断电
面对这些情况,tcp连接是如何进行处理的呢?
学过系统的都知道,一旦一个进程退出或者异常崩溃,那么操作系统会将其开辟但未来得及释放的堆空间资源进行管理释放,同样如果上述Client或者Server端进程挂掉,该进程的系统资源都会经由操作系统进行管理释放掉。对于 ”电脑关机",很多时候如果你的电脑运行了很多程序,此时你选择关机,系统其实会提醒你 存在下面的一些进程还在运行,其实本质上是在等操作系统将这些进程杀掉,清理其申请的资源,所以它的情况和第一种无差别。
通过tcp的连接管理机制我们知道,断开连接是需要双方都同意的!但此时如果你网线已经被拔了,你还怎么想服务端发送FIN字段报文?肯定不行。那么此时,服务端一定还是认为客户端存在的。维护一条压根不存在的连接,显然这个负担服务器是不能容忍的。
TCP自己也内置了保活定时器,会定期询问对方是否还在.如果对方不在,会把连接释放。
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接。
TCP总结:
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
● 校验和
● 序列号(按序到达)
● 确认应答
● 超时重发
● 连接管理
● 流量控制
● 拥塞控制
提高性能:
● 滑动窗口
● 延迟应答
● 捎带应答
基于TCP应用层协议HTTP、HTTP、SSH、Telnet、FTP、SMTP……
本篇到此结束,感谢你的阅读
祝你好运,向阳而生~