深入理解协议栈的内部结构——收发和断开

1.上期问题的答案

如果客户端connect操作时,服务端对应的端口号不接受连接,在这种情况下不会设置SYN的值,而是会把RST比特设为1

2.本期主题

上一期讲解了在TCP下协议栈的socket操作和connect操作,那么本期我们会讲解TCP协议栈的write操作,read操作和close操作。

3.网络包的大小

3.1 包太小怎么办
3.1.1 MTU和MSS

在控制流程从connect回到应用程序后,应用程序会调用write操作把要发送的数据交给协议栈,并且应用程序还会把要发送数据的长度也一并告诉协议栈。协议栈在收到数据后并不会马上发送出去,而是会把数据放在内部的发送缓冲区中,并等待应用程序的下一段数据。因为如果应用程序发送的是逐行的数据,甚至发送一个个字节,那么如果协议栈一接收到数据就发送出去,那么会发送大量的小包,这会降低网络的效率。因此协议栈要判断是否可以发送数据。

第一个判断依据就是MTU,即一个网络包的最大长度,它包含了头部的总长度。如果要得到网络包中所能容纳的最大数据长度,即MSS,也就是要减去头部的总长度。那么协议栈在发送数据时,如果数据长度一直很接近MSS的值,那么就可以避免发送小包的问题了。

3.1.2 时间

还有一个依据是时间。如果应用程序发送的数据本来就很小,那么如果一直没有达到接近MSS的长度,协议栈就会一直陷入等待,这会造成发送延迟。因此,协议栈内部有一个计时器,在一定的时间里,就算网络包的数据没有达到MSS的大小,它还是会把数据发送出去。

这两个条件是相互矛盾的,因此协议栈的开发者会自己来定一个值来保持平衡,不同种类和版本的操作系统也就会有些差异。

3.2 包太大怎么办

如果HTTP请求的消息太大,长度已经超过了一个网络包可以容纳的最大长度,那么发送缓冲区的数据会被以MSS的长度分成若干个网络包,然后根据套接字中记录的控制信息所对应的IP地址和端口号,然后交给IP模块来发送数据。

4.确认网络包收到

在接收方收到发送方发送的数据后,需要进行确认操作,来告诉发送方已经成功接收到数据。在发送方发送数据时,发送方会把发送数据的长度,和所对应的序号一起告诉接收方。就比如发送方和接收方说:我现在要发送从xxx开始的数据,一共有xxx字节。如果接收方上次接收到第1000字节,如果发送方发送了从1001开始的包,那么说明没有遗漏,那么发送方会把序号加上数据的总长度再加上1,这个值就储存在ACK号中。就相当于对发送方说:xxx前的数据我已经收到了。

然后在收发数据阶段,序号的起始值不是1,而是一个随机的数。这样做的目的是为了防止被有心之人抓到机会发动进攻。而随机出来的序号会在执行连接操作的时候一并发送给通信对象。

上面讲到的操作是单向操作的时候要做的事,那如果是双向操作呢?其实很简单,在连接阶段客户端会先发起连接并把序号初始值告诉服务端,服务端在接收到数据后会返回ACK号,并把自己的序号初始值告诉客户端。客户端在收到服务端发来的数据后也会生成ACK号并返回给服务端。之后收发数据,客户端和服务端各发各的,在收到数据后也会根据对方的序号和长度发送对应的ACK号。

5.TCP通信的补救措施

在收到接收方发回的ACK号之前,发送方发送的数据会保存在发送缓存区中。这是为了防止如果接收方没有收到对应的数据,发送方可以重新发送这种包。这样一来,无论什么错误,如果数据丢失,发送方会重新发送包。如果数据出现错误,接收方会丢弃这些包,并等待发送方重新发送包。但是如果发送方在发送几次数据后仍然无效,那么发送方会强制结束通信,并向应用程序报错。

5.1 ACK号的等待时间

那么发送方等待ACK号的等待时间要设置成多久呢?如果ACK号的等待时间太短,在接收方的ACK号到达之前再一次发送相同的包,那势必会造成网络的拥堵。如果ACK号的等待时间太长,那么如果真的出现了错误,网络包的重传就要经过很久。

因此ACK号的等待时间会被动态调整,如果ACK号的返回很快会缩短等待时间,反之,ACK号的返回时间很慢会延长等待时间。虽然说缩短等待时间,但是这个值是有最小值的,一般在0.5秒到1秒之间。

5.2 滑动窗口

如果每次发送一次数据,就等待对应的ACK号返回,这样的效率其实是很低的。因此协议栈在发送数据的时候会使用滑动窗口来管理发送数据和ACK号。在发送一个网络包后,不会等待ACK号返回,而是继续发送之后的包。

但是这样又引出了别的问题,如果数据一股脑全部发送给接收方,接收方来不及处理,那就只能丢弃新发送过来的包了,这种浪费肯定也是不允许的。

接收方收到包后,会把数据存到接收缓存区中。然后接收方要计算ACK号,然后把数据还原再交给应用程序。因此如果发送方一直发送数据的话,接收方的缓存区是有可能溢出的。因此在连接阶段,接收方其实也会把自己接受缓存区的大小告诉发送方,发送方在每次发送完数据后都会计算接收方还有多少缓存区,如果要溢出了,那么就会更改发送包的速率。如果缓存区越来越多,说明接收方处理数据的速度很快,那么发送方也会提升发送包的速率。

接收方也会一直处理数据,所以在返回ACK值的时候,接收方也会把现在缓存区的大小告诉发送方,好让发送方调整发送数据的策略。这就是TCP调优参数中非常有名的一个。

5.3 ACK与滑动窗口

那么ACK和滑动窗口不可能一直保持同步出现,如果先计算完ACK准备发送,或者先处理好数据并把准备把剩余的缓存区的大小告诉发送方。那么一方一定会等待另一方,比如在发送缓存区还有3000字节的消息的时候,其实最新的消息是缓存区还有5000字节,那不是会影响滑动窗口吗?

TCP做了这样一个优化,如果在计算滑动窗口的时候,ACK号更新了两次,如果ACK号是连续的,那么就可以直接发送那个最新的ACK号。滑动窗口也是一样的,发送最新的缓存区的大小给发送方就可以了。

5.4 接收响应消息

浏览器在发送请求消息之后,会调用read程序来获取响应的消息。协议栈会从接受数据的缓冲区中取出数据,如果里面有数据的话就会直接给浏览器。如果没有数据,协议栈会把这个read操作挂起等到之后再执行。

6.断开连接

在发送方发送完数据后,应用程序会调用close程序,我们假设现在是服务端发送完数据调用了close程序。服务端的协议栈会生成控制位中FIN比特为1的TCP头部,服务端的套接字会记录下断开操作的相关消息。客户端接受到服务端的断开操作消息后,会将自己的套接字标记为断开操作状态,然后客户端会返回ACK号给服务端,之后协议栈就可以等待应用程序来取数据。

应用程序调用read来读取数据,如果有已经被应用程序接受部分的数据,这些数据会被传递给应用程序。否则会告知应用程序数据已经全部结束了。在收到了服务端的所有数据后,客户端也会调用close并发送一个FIN比特为1的包,收到服务端的ACK号后,服务端和客户端的通信就结束了。

7.删除套接字

在和服务端发送FIN比特为1的包后,客户端不会立刻删除套接字。这是因为如果这个FIN比特为1的包如果没有发送到服务端,那么客户端还要进行重发。如果这个时候已经没有套接字了,而这个时候又创建了一个新的套接字,端口号正好就是刚刚删除的那个端口号,那么重发的包就会发送给这个刚刚创建的那个套接字。因此套接字一般会等待几分钟再删除,如果重传了几次依然没有响应,就会停止重传。

8.下期预告

本章就不出思考题了,因为协议栈的内部结构的部分比较多,我分成了上下两部分,因此我在下一期会带大家把创建套接字,连接,收发数据,断开连接这部分内容,从头到尾的串起来讲一遍。

往期内容

用通俗易懂的话理解HTTP

DNS与IP地址的那些事


协议栈发送数据


深入理解协议栈的内部结构——创建和连接
 

  • 29
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用CANopennode协议进行数据收发的过程包括以下几个步骤: 1. 初始化CANopennode协议。可以通过以下代码进行初始化: ```c++ #include "CANopen.h" CO_NMT_t *NMT; CO_CANmodule_t *CANmodule; void initCANopen() { // 初始化CAN模块 CANmodule = CO_CANmodule_init(...); // 初始化NMT NMT = CO_NMT_init(...); // 启动CANopen CO_CANsetNormalMode(CANmodule); CO_NMT_sendCommand(NMT, CO_NMT_ENTER_OPERATIONAL); // 等待CANopen进入运行状态 while (NMT->operatingState != CO_NMT_OPERATIONAL) { CO_CANmodule_process(CANmodule, 0); } } ``` 2. 发送数据。可以通过以下代码进行数据发送: ```c++ void sendData(uint8_t *data, uint32_t len) { CO_CANtx_t *CANtx = CO_CANtxBufferGet(CANmodule, true); if (CANtx != NULL) { CO_CANtxBufferSetStdId(CANtx, 0x123); CO_CANtxBufferSetData(CANtx, data, len); CO_CANtxBufferSetDLC(CANtx, len); CO_CANsend(CANmodule, CANtx); } } ``` 3. 接收数据。可以通过以下代码进行数据接收: ```c++ uint8_t receivedData[8]; void receiveData() { CO_CANrxMsg_t *CANrxMsg; while ((CANrxMsg = CO_CANrxMsg_get(CANrx)) != NULL) { if (CANrxMsg->ident == 0x123) { memcpy(receivedData, CANrxMsg->data, CANrxMsg->DLC); } CO_CANrxMsg_readRelease(CANrxMsg); } } ``` 4. 在主循环中调用`CO_CANmodule_process()`函数来处理CAN消息: ```c++ while (1) { CO_CANmodule_process(CANmodule, 0); // 其他处理代码 } ``` 需要注意的是,以上代码仅供参考,具体的CAN硬件接口和CANopennode协议的初始化参数需要根据具体的应用进行配置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值