发布时间:2023-09-02 13:00
每次在实现tcp服务器端时,总会思考:处理接收到的客户端的消息细节时,总会陷入一点点的误区,
加上分析公司前人各式各样的业务代码,总是会被略微带偏,这里做简单的tcp接收后相关流的处理思路。
在处理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的模式下,貌似影响不大,都能正确取完。
在实际的业务中,我们通常配合select或者epoll对服务器端相关连接进行管理,在接收客户端的消息时,有一些关注点:
对于每个客户端的连接,可以保证可靠按顺序接收到,放进自己对应的缓冲区中。
===》我一直陷入一个误区,如果依赖tcp底层的拆包逻辑,可能在收到多个包的中间会收到其他的包,这其实是一个思想误区,不可能串包。
===》服务端对tcp每个连接都设有一个发送缓冲区和接收缓冲区,针对一个连接加上tcp的可靠传输,tcp底层的接收是可以得到保障的。
===》tcp虽然可靠,但是却是流的形式进行接收,无法知道多个包的边界。
===》为了能正确识别到每个完整的包,去正确处理(识别到一个完整的包(tcp recv时是从缓冲区中拿数据,第一:缓冲区可能多个包(粘包)第二:可能recv读了半个包,或者tcp底层拆包,第二个包还没来))
===》所以我们需要在业务层对流做特定的限制,保证能识别到一个包 : 比如length+data,比如 加特定标识的协议头部或者尾部
===》如果是epoll的ET边缘触发模式,就得用while循环进行多次读取
===》如果是epoll的LT水平触发模式,或者select,是能下次触发的,不过也可以循环读取完做处理提升效率。
===》根据应用层协议,可以先进行数据读取,放入缓存中,然后根据协议解析缓存中的相关数据进行完整包的处理。 (相对应的,应用层缓冲区也应该是一个连接对应一个缓冲区)
===》个人理解是在一定的业务层保证上是可以的,
===》tcp如果发送一个过大的包,会进行拆包的,这种场景事件触发后,根据特定的自己字节读实际的长度,下个包迟迟不到就有问题。
===》但是如果tcp我们业务层保证了不回tcp拆包,我们控制了发包大小,我觉得其实也是可行的。
如果代码中实现tcp发送与接收相关协议设计时,需要关注一些细节:
1:如果协议用的结构体的方式,要注意结构体字节对齐大小,会影响接收端的解析。
2:一般大于2byte的字节,最后按照特定的函数进行相关的主机字节序和网络字节序的转换
#include
uint32_t htonl(uint32_t hostlong); //32位主机字节序转为网络字节序 Host to Network Long 4字节
uint16_t htons(uint16_t hostlong); //十六位主机字节序转为网络字节序 Host to Network Short 2字节
uint32_t ntohl(uint32_t hostlong) //32位网络字节序转为主机字节序 Network to Host Long
uint16_t ntohs(uint16_t hostlong) //16位网络字节序转为主机字节序 Network to Host Short
栈内存定义结构变量最好清零。
在简单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; //是因为用的栈内存 如果这里的内存比较大的话就会导致select Invalid argument
rset = m_allset;
ret = select(maxfd, &rset, (fd_set *)0,(fd_set *)0, (struct timeval *)&tv);
...
}
===》另外,我听同事说tcp缓冲区溢出问题,个人理解是 tcp接收缓冲区不会有所谓的溢出问题,发送缓冲区因为发送频率应该会出现类似问题。