一、延迟应答

如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.

(1)假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;

(2)但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;

(3)在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;

(4)如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;

      根据冯诺依曼体系结构,网卡也算是外设,所以按道理来说发送方发送更多的数据,发送的效率也就更高!!窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率

 问题1:所有包都可以延迟应答吗???

——>肯定不是!!(1)数量限制: 每隔N个包就应答一次; (2)时间限制: 超过最大延迟时间就应答一次;具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;

问题2:我们用户层可以怎么做?

——>虽然延迟发送时TCP去做的,但是我们能做的就是尽快将数据从缓冲区拿上去,比较推荐的做法就是每次都尽快通过read、recv把数据从内核中拿上去,这样tcp就能在底层给对方更新更大的窗口,让发送方的传送报文效率变高,这样通信效率也就变高了! 

所以延迟应答其实是一个博概率的方法,赌在这段时间内上层可以把数据都刷走 

二、拥塞控制

       虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.

     因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.

    两端的机器虽然可以对网络做评估,但是很难去对网络做什么,所以TCP也考虑了网络方面的策略,因为就算你得知对方的接收能力很强,也并不代表现在网络非常好!! 

     而几乎所有的方案都是在两端机器上进行的,所以TCP替我们考虑了网络的情况!!

     所以 TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;

此处引入一个概念程为拥塞窗口

发送开始的时候, 定义拥塞窗口大小为1;

每次收到一个ACK应答, 拥塞窗口加1;

每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;

 像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快.

为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.

此处引入一个叫做慢启动的阈值

当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长

当TCP开始启动的时候, 慢启动阈值等于窗口最大值;

在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;

问题1:难道网络拥塞是因为我发了数据吗??我少发了网络就能好了吗??

——>其实我们的网络资源属于共享资源,网络影响大家的通信,并且大家用的是tcp ip协议,所以大家都少发了网络就能变好

问题2:机器怎么判断当前网络属于拥塞呢??所有的机器都会延迟阻塞的决策么?

——>要注意不是网络一旦拥塞了,大家都能立即做出拥塞控制的决策,可能其中一部分只有大量丢包,那么他才会判定为网络阻塞,而其他少量丢包的机器是感知不来的(只会进行超时重传),网络拥塞越严重就会让越多的主机加入到拥塞控制的过程中

        你们去控制了 不代表我就得控制,如果因为你们的控制使得网络状况变好了,那么我也会正常通信。所以我们会发现其实网络拥塞是博概率的,是自适应的!!

      TCP协议让多主机面对网络时做出拥塞控制的决策是“共识”!!

问题3:会一直指数增长吗??

——>不会一直指数增长,该图的前提是拥塞起效果了才会建立起来!如果没有拥塞的话是以对方的接受能力为主的 

       拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.  

问题4:这个阈值要减半吗?减三分之二可以吗?这个数据是由数学或者科学家深入计算出来的吗?

--——>其实网络里很多数据都是通过实验和测试得来的!

         就像代码里有局部性原理(访问一行代码时访问周边代码的概率是很大的) 在计算机发明之前大家是没有这个概念的,只不过后来大家在使用的时候可不可以提前加载进来,但是如果没有局部性原理这样是没有任何意义的,因为不过就是白白IO,但是后来通过实验发现预先加载却还是可以提高代码运行的效率,所以根据实验结果的验证再有了局部性原理的概念 

         还有STL中的vector 为什么有时候扩容一倍,有时候扩容一点五倍呢??其实他们未必是最好的,但是一定是大量实验得出的一个较为理想的结果,可以更好地适应各种情况!! 

问题5: TCP的拥塞控制很像热恋的感觉

--——>刚开始很羞涩,但是后来慢慢聊天之后就开始迅速升温,在某个临界点结婚了,因为平时的一些日常生活的琐事再加上双方经常一起走,慢慢也就趋于平淡了,后来吵架了会瞬间跌倒冰点,然后其中一方又去道歉,这个时候复合所需要的临界点会比之前小一点

三、面向字节流

创建一个TCP的socket, 同时在内核中创建一个发送缓冲区 和一个接收缓冲区;

调用write时, 数据会先写入发送缓冲区中;

如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;

如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;

接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;

然后应用程序可以调用read从接收缓冲区拿数据;

另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可 以写数据. 这个概念叫做 全双工 

       udp很像快递,并且他有长度,所以他必须整体发整体收,这是面向数据报的,所以他不需要去解析,因为他是整体发整体收的,每个报文都是单独的 

      而用过tcp协议可以将数据从发送缓冲区原封不动的发送到对方的接收缓冲区,但是tcp发送的步调和上层把数据拿上去的步调不一样!! 可能数据刚到对方那的时候对方并没有及时去处理,后来缓冲区放了很多数据了,然后我们后来基于一次尽可能把缓冲区都拿走的原则(一次取多少事由用户何时调用read决定的)把全部的数据读上去

你以为的4个请求,具体内容是什么我内核并不关心,因为在我TCP协议眼里只有字节的概念! 

所以这个过程有字节流动起来的概念,这就是面向字节流(特别像自来水,你不需要关心这个水是怎么提炼出来的,你只需要关心我该用杯子接还是用盆接还是用碗接) 

     所以TCP并不需要像UDP一样有长度,反正因为有序号的存在至少可以保证你发给我什么我就能按顺序接受到什么,但是具体用户read之后怎么解析(要边读边解析,如果读不到完整报文就下一次再读),那是你们上层协议要做的,跟我无关

四、粘包问题

首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.

在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.

站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.

站在应用层的角度, 看到的只是一串连续的字节数据.

那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.

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

      对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;

      对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);

问题:UDP是否存在粘包问题吗??

——> 

       对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.

       站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况.

五、TCP异常情况

进程终止:连接正常断开

机器重启:杀死所有的进程

机器掉电/网线断开:接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行RST

总结:

如果是软件方面的问题,正常四次挥手就可以了

如果是硬件问题,服务端认为链接存在,客户端认为链接没有了,后面服务端把链接关掉了,而大当你客户端重新启动时发现过去的连接不存在的时候,双方发生了链接认知不一致的情况,此时服务端会给客户端发送RST让他重新建立连接    如果客户端永远不发消息,那么服务端会有相应的保活机制,就是类似定时器的东西,然后到了一定时间会询问客户端怎么不给我发消息,如果一直得不到回应,那么他就会断定连接已经断开了,就会把之前维护的连接给释放掉!!

        另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.

六、TCP小结

6.1 TCP的设计总结

可靠性:

校验和(确保数据没有任何错误,不会出现比特位反转、数据报文校验失败等情况)

序列号(保证数据按时到达、去重)

确认应答(不存在百分百可靠是因为网络历史上最新一条报文一定是没有应答的)

超时重发

连接管理

流量控制

拥塞控制

提高性能:

滑动窗口

快速重传

延迟应答

捎带应答

其他:

定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

6.2 基于TCP应用层协议

HTTP

HTTPS

SSH

Telnet

FTP

SMTP

当然, 也包括你自己写TCP程序时自定义的应用层协议;

6.3 TCPvsUDP

      我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢?

——>TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较 

TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;

UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;

归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定.

6.4 用UDP实现可靠传输

     你要先问面试官场景是什么(因为UDP要实现的可靠性非常多,所以如果我们可以知道具体的场景的话就可以知道需要借鉴TCP的哪部分方案,如果可靠性非常非常高,那么就只能使用TCP了)根据这个场景来引入TCP的可靠性机制!!在应用层实现类似的逻辑!

例如: 

引入序列号, 保证数据顺序;

引入确认应答 , 确保对端收到了数据 ;

引 入超时重传 ,如果隔一段时间没有应答, 就重发数据 ;

……

七、打通一下文件和socket 

文件和socket究竟有什么关系呢??我们来尝试从代码去打通他们的联系!

一个进程,有一个文件描述符表,表里面存储了struct file类型 

里面有函数指针的操作方法f_op    有文件缓冲区f_mapping 但是在网络中 还有一个  他指向了socket 然后socket里面也有一个指针可以回指向file 

 所以我们上层只要通过文件描述符就可以找到里面的socket,屏蔽差异化 (OS里面的数据结构很复杂,可能你指向我我指向你)

socket里面有成员 

他是当网络条件不就绪的时候,将进程挂接进去的阻塞队列!!

还有成员ops  他里面协议的操作方法集

 问题:这个ops和前面的f_op有什么关系呢???

——> 在网络里,前者不再指向磁盘的方法而是指向的是网络的方法,后者是指向协议栈的方法集。前者解决的是对上的问题(比如说怎么将数据都应用层形成一个节点拷贝到sock中的接收和发送队列中),然后后者解决的是对下的问题(比如怎么把数据从队列拿下来交给下层)  具体后面怎么处理不同协议栈还有自己的方法集 (TCP并没有把数据发出去,他只是交给下层!!)

还有成员sk

 

其中有sk_receive_queue和sk_write_queue(发送缓冲区和接受缓冲区) 

其实我们在创建套接字的时候,底层并不是直接创建sock,而是通过你传的确认你要创建的是TCP还是UDP,然后他们结构体里的第一个成员都是sock类型的 

 sock中的部分字段可以标识他是TCP还是UDP(socket类型)然后只要做强转就可以访问到全部的内容了   (本质就是用C语言实现了多态   继承是结构体套结构体做到的   多态是用函数指针指向不同的方法

 所以协议栈的本质如下:特定的数据结构表示的协议+特定协议匹配的方法集(每一层都有)

OS里可能会同时存在大量报文,所以他必须要想办法把报文管理起来!!先描述再组织

为什么有锁??因为接收和发送的过程本质在内核里是一个生产者消费者模型!

这四个指针会互相配合  指向一个内存空间!

所以sk_buff在层和层之间会进行流动!!所以封装和解包本质上就是指针的移动(根据报头相关的协议字段来移动)!!向上交付的本质就是不断将sk_buff组织到缓冲区,将报头分发给各层,然后最后上层再从缓冲区里拿报文

所以传输层、网络层的解包操作都是在接收缓冲区中操作的!!不需要频繁的拷贝,只是指针移动

总体图: 

 所以文件系统和网络是可以打通的!!Linux一切皆文件

并且链接建立是有很大成本的!!需要创建很多数据结构!! 

八、 使用 wireshark 分析 TCP 通信流程

      wireshark是 windows 下的一个网络抓包工具. 虽然 Linux 命令行中有 tcpdump 工具同样能完成抓包, 但是tcpdump 是纯命令行界面, 使用起来不如 wireshark 方便.

下载 wireshark 

https://1.na.dl.wireshark.org/win64/Wireshark-win64-2.6.3.exe

或者

链接:https://pan.baidu.com/s/159UUIoZ8b7guWDeuAHoF9A 提取码:k79r

安装 wireshark

直接双击安装, 没啥太多注意的.

启用 telnet 客户端

参考 https://jingyan.baidu.com/article/95c9d20d96ba4aec4f756154.html

启动 wireshark 并设置过滤器

由于机器上的网络数据报可能较多, 我们只需要关注我们需要的. 因此需要设置过滤器 

在过滤器栏中写入 

 ip.addr == [服务器 ip]

则只抓取指定ip的数据包. 

 或者在过滤器中写入

tcp.port == 9090 

 则只关注 9090 端口的数据

更多过滤器的设置, 参考wireshark 实用过滤表达式(针对ip、协议、端口、长度和内容)_wireshark过滤端口-CSDN博客

观察三次握手过程

启动好服务器.

使用 telnet 作为客户端连接上服务器

 telnet [ip] [port]

 抓包结果如下:

观察三个报文各自的序列号和确认序号的规律.

在中间部分可以看到 TCP 报文详细信息

 观察确认应答

 在 telnet 中输入一个字符

 可以看到客户端发送一个长度为 1 字节的数据, 此时服务器返回了一个 ACK 以及一个 9 个字节的响应(捎带应答), 然 后客户端再反馈一个 ACK(注意观察 序列号和确认序号)

 观察四次挥手

 在 telnet 中输入 ctrl + ], 回到 telnet 控制界面, 输入 quit 退出.

 实际上是 "三次挥手", 由于捎带应答, 导致其中的两次重合在了一起.

注意事项

如果使用虚拟机部署服务器, 建议使用 "桥接网卡" 的方式连接网络. NAT 方式下由于进行了 ip 和 port 的替换.

使用云服务器测试, 更加直观方便.

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐