进程|tcp 服务端接收数据处理思路梳理,以及select: Invalid argument报错 笔记

每次在实现tcp服务器端时,总会思考:处理接收到的客户端的消息细节时,总会陷入一点点的误区,
加上分析公司前人各式各样的业务代码,总是会被略微带偏,这里做简单的tcp接收后相关流的处理思路。
进程|tcp 服务端接收数据处理思路梳理,以及select: Invalid argument报错 笔记
文章图片

0:总结 在处理tcp的接收时:
1:tcp是可靠的,内核为每个tcp 客户端连接都分配了一个发送缓冲区和接收缓冲区。
2:基于第一点:
====》针对每个连接,可以可靠,按顺序得接收到数据(即放入对应得接收缓冲区中)。
====》每个连接的缓冲区是独立的,不会有串包现象。
====》缓冲区中存放,是流的形式,无法识别多个包的边界,需要业务层适配。
3:基于第二点:
====》我们需要在业务层适配,识别到一个完整的包,一般有两种方案:(Length+Data), (特定的头部或者尾部标识),我的理解其他相关特定协议思路大概相同
====》作为服务器端,我们需要同时处理多个接收缓冲区(epoll/select管理),同时要考虑如何读取到一个完整的包(每次读多长的数据,肯定能读到一个完整的包吗?)
4:基于第三点:怎么保证一次的事件触发,能正确的读取到缓冲区的内容。
===》比如如果是epoll ET模式,应该循环进行读读完所有的数据(可能有粘包现象,需要处理)
===》如果是epoll LT或者select模式,代码比较简单,但是,如果tcp内核底层拆过包,分多次发过来,可能有半包现象,以及recv时这个长度如何定义?
===》针对以上:(我能思考到的最优的就是: 读固定的长度,while读放入应用层缓冲区(我们应该每个连接维持一个应用层缓冲区)中,然后缓冲区做拆包处理)
5:基于第四点,如果有的代码没有用应用层缓冲区暂存呢?(仅仅考虑的是Length+data的模式)
看到有的代码,是特定的业务,就是每次io事件触发,先接收特定的长度,在读取实际数据,这能确定一个完整的包,去做业务处理。
我就会思考这中处理会不会有缺陷,最容易思考的就是
? 1:一次事件触发处理一个包,会不会有数据没有处理完,内核的接收缓冲区中仍然有数据,比如epoll的ET模式场景,但是貌似select和epoll的LT影响不大
? 2: 如果业务层没有做相关的处理,有可能的场景是tcp底层拆包,这样先读取特定的自己取长度,再读取实际长度数据时,可能是有问题的?下个包一直没到。
===》但是,最终思考,如果业务层做过处理,保证tcp底层不会拆包,我们如果不用应用层缓冲区应该也是可行的。
===》即,业务层已经保证每次接收是一个特定格式(length+data)的完整的包,每次先接收特定字节头部,解析后接收实际长度数据 后做完整包的处理。
===》考虑事件触发特性,保证处理完善,我们epoll ET处理时,应该循环一次性读完所有的包,而epoll LT以及select的模式下,貌似影响不大,都能正确取完。
1:tcp进行相关recv的处理时 在实际的业务中,我们通常配合select或者epoll对服务器端相关连接进行管理,在接收客户端的消息时,有一些关注点:
1.1:tcp是可靠的流式传输,并且实际上服务器端为每个客户端都维持了一个接收缓冲区和一个发送缓冲区。
第一:tcp底层的流的接收可以保证。(有自己的缓冲区,并且可靠,顺序) 对于每个客户端的连接,可以保证可靠按顺序接收到,放进自己对应的缓冲区中。
===》我一直陷入一个误区,如果依赖tcp底层的拆包逻辑,可能在收到多个包的中间会收到其他的包,这其实是一个思想误区,不可能串包。
===》服务端对tcp每个连接都设有一个发送缓冲区和接收缓冲区,针对一个连接加上tcp的可靠传输,tcp底层的接收是可以得到保障的。
第二:需要定制协议,对缓冲区流正确处理。 ===》tcp虽然可靠,但是却是流的形式进行接收,无法知道多个包的边界。
===》为了能正确识别到每个完整的包,去正确处理(识别到一个完整的包(tcp recv时是从缓冲区中拿数据,第一:缓冲区可能多个包(粘包)第二:可能recv读了半个包,或者tcp底层拆包,第二个包还没来))
===》所以我们需要在业务层对流做特定的限制,保证能识别到一个包 : 比如length+data,比如 加特定标识的协议头部或者尾部
第三:在特定协议的基础上,如何保证正确读取多个缓冲区。 ===》如果是epoll的ET边缘触发模式,就得用while循环进行多次读取
===》如果是epoll的LT水平触发模式,或者select,是能下次触发的,不过也可以循环读取完做处理提升效率。
第四:如何保证包的完整性,一个完整的包去做对应业务处理。 ===》根据应用层协议,可以先进行数据读取,放入缓存中,然后根据协议解析缓存中的相关数据进行完整包的处理。 (相对应的,应用层缓冲区也应该是一个连接对应一个缓冲区)
??采用缓冲区的方式是没有问题的,但是针对特定的协议,比如length+data的方式,先读取特定字节,解析实际长度,再接收特定的实际数据可以吗? ===》个人理解是在一定的业务层保证上是可以的,
===》tcp如果发送一个过大的包,会进行拆包的,这种场景事件触发后,根据特定的自己字节读实际的长度,下个包迟迟不到就有问题。
===》但是如果tcp我们业务层保证了不回tcp拆包,我们控制了发包大小,我觉得其实也是可行的。
2:关注细节:网络字节序,结构体的对齐方式 如果代码中实现tcp发送与接收相关协议设计时,需要关注一些细节:
1:如果协议用的结构体的方式,要注意结构体字节对齐大小,会影响接收端的解析。
2:一般大于2byte的字节,最后按照特定的函数进行相关的主机字节序和网络字节序的转换

#include uint32_t htonl(uint32_t hostlong); //32位主机字节序转为网络字节序Host to Network Long4字节 uint16_t htons(uint16_t hostlong); //十六位主机字节序转为网络字节序Host to Network Short2字节uint32_t ntohl(uint32_t hostlong)//32位网络字节序转为主机字节序Network to Host Long uint16_t ntohs(uint16_t hostlong)//16位网络字节序转为主机字节序Network to Host Short

3:代码测试时 select: Invalid argument
栈内存定义结构变量最好清零。
在简单demo进行测试时,环境运行启动不起来,报错select: Invalid argument
百度结合测试后,发现时select最后参数struct timeval tv;设置的问题
===》1:参考百度,有类似的问题是因为设置了 tv.tv_usec 值过大。
===》2:然而我代码中并没有对这个值设置过大,但是没有对初始化时清零。
struct timeval tv; memset(&tv, 0, sizeof(struct timeval)); //注意这里清理初始化后的内存 fd_set rset; int maxfd = m_listenfd + 1; while(m_running) { tv.tv_sec = 30; //tv.tv_usec = 0; //是因为用的栈内存如果这里的内存比较大的话就会导致selectInvalid argumentrset = m_allset; ret = select(maxfd, &rset, (fd_set *)0,(fd_set *)0, (struct timeval *)&tv); ... }

【进程|tcp 服务端接收数据处理思路梳理,以及select: Invalid argument报错 笔记】===》另外,我听同事说tcp缓冲区溢出问题,个人理解是 tcp接收缓冲区不会有所谓的溢出问题,发送缓冲区因为发送频率应该会出现类似问题。

    推荐阅读