TCP编程问题总结!
先来复习一下TCP/IP五层模型:从上到下依次是应用层、传输层、网络层、数据链路层、物理层;我们会接触到的是应用层、传输层、网络层。
这三层是干啥的?以下是来自《计算机网络——自顶向下方法》这本书的笔记(是一本好书,深入浅出,把复杂的概念讲的很容易懂,不同于大学时那些味同嚼蜡的课本)。
网络层
网络层提供主机到主机的通信服务,即从一个IP地址到另一个IP地址的数据传输,网络层分为数据平面和控制平面。
数据平面主要作用是从其输入链路向其输出链路转发数据;
控制平面作用是协调这些本地的每个路由器转发动作,使得数据报沿着源和目的地主机之间的路由器路径最终进行端到端传送,简单说,控制平面的作用就是路由选择。计算转发路径的算法称为路由选择算法。
作者举了个例子来说明数据平面和控制平面,一个人驾车从宾夕法尼亚州到佛罗里达州,转发就是经过立交桥的过程,从立交桥的一个入口进入立交桥然后从一个出口离开立交桥走上了另一条路;而控制平面路由选择就是着手行程之前规划路线的过程,查阅地图,从许多可能路线中选择一条。
传输层(又叫运输层)
为运行在不同主机的应用进程提供逻辑通信,网络层为主机之间提供了逻辑通信,报文到达主机后,是传输层协议将报文定向到不同进程的。
作者举了一个很有意思的例子:有两个家庭A和B,每家有12个孩子,他们每个人每星期都要给对方家庭12个小孩写信,那每周有144封信件往来,每星期A家庭由Ann来为大家收集信件并投递到邮车上,信件到时,Ann再把信件一封封分给兄弟姐妹,B家庭由Bill来做这个工作。Ann和Bill做的事情就是传输层做的事。这个例子中:
家庭=主机
兄弟姐妹 =进程
运输层协议 = Ann或者Bill
网络层协议= 邮政服务(包括邮车)
信封上的字符 = 应用层报文
举个例子电脑上跑着好几个网络应用,浏览器、网易云音乐、迅雷下载等,到达电脑的数据怎么知道是给哪个应用进程的?这就是传输层要做的事。
传输层通过什么来区分不同进程?socket端口号。
上面的两个例子都很妙对不对?
传输层的协议分为TCP和UDP,TCP是面向连接的保证数据可靠到达的通信协议(是一个负责任的信件分发员,除了把到达主机的数据分给不同进程还有很多附加服务),UDP是面向无连接的不可靠的数据报文协议(只提供最基本分发服务,只管把到达主机的数据分发一下,收没收到不管)。
那TCP是怎么保证数据可靠到达的?简单说他把要传输的数据,分成一个一个包裹,每个包裹编号,按顺序发,不断传输的数据包形成数据流,每个编号的数据包到达后对方发送ACK,发送方收到对方的ACK才认为这包数据成功发出去了,数据发送的数据包序号窗体往后滑。实际并不是这么简单还有很多内容。
应用层就不说了。
下面总结几个TCP通信遇到过的问题。
1、 数据到达的次序不是预期
遇到过一个这样的问题,有一款洗衣机,一定要先开机,才能再设置洗衣的模式,调试时发现APP明明是先发开机指令,再发设置模式,但家电端wifi板总是先收到设置模式,再收到开机指令。
分析发现,APP是把数据按顺序发给了服务器,但是服务器会按顺序一条一条发给wifi板吗?并不是,服务器是一对多同时与成千上万的设备通信的,有手机APP用户,有家电wifi,所以他通常是并发的,也就是会把消息分给很多个线程同时处理,比如这里APP发来的控制消息应该是分给了两个线程同时处理,应用层2个线程同时向传输层丢数据,这两包数据指向同一个IP同一个端口(即家电wifi板),传输层只有一个,那不同线程的数据谁先传下来谁就排在前面了,所以造成了先发的不一定先到的现象。
那这个问题后来怎么解决的?APP把开关机和设模式一起发下来由wifi板端来处理,首先检索有没有开关机消息,有的话去执行开关机,再检索有没有设模式。
2、 数据分多次连着到达
这是TCP编程要考虑的基本问题,理想的情况是APP发一条完整消息,wifi板收到一条完整消息。但在测试时发现,对着APP点击空调开关机键,不断的开关开关,点个20次左右,APP上最后显示是开空调最后是关,或者APP上最后是关空调最后是开(这个测试是不是很变态?)
分析log发现是最后一条控制消息分两次到达了,tcp传输层是数据流传输,发送方的传输层不会管应用层传下来是什么数据,他只会把数据分装成一个一个小包裹往外发,接收方的传输层也不会管对方发来的数据里面具体是什么,收到多少就传多少给应用层。
我们当时的TCP数据处理程序不完善,无法处理一条消息分两次到达的情况,如果消息分两次到达,第一次收到的片段不符合应用层协议格式就被丢弃掉了,第二次到达的也被丢弃掉了。
一个完善的tcp数据处理程序应该:
(1)最基本能处理一条完整到达的消息;
(2)好几条消息一起收到,能一条一条处理完;
(3)好几条完整消息+一段残缺的消息片段,能把前面完整的一条一条处理完,再把残缺的消息片段存入缓存,等着下次或下几次收到剩下的消息片段组成一条完整消息再处理;
(4)只有一段残缺的消息片段,存入缓存,与下次或者下几次收到的消息组成完整消息再处理。
后来对程序做了完善,消息存在一个循环队列里处理,bug解除了。
3、快速点击APP时发现后面执行变慢
还是上面那个变态的测试,不断的点APP上的开关按钮,测试的人反应说一开始空调反应很快,为什么点了10次以上之后,反应这么慢,也就是APP上已经点完了,空调慢半拍还在那里开关开关好久,好像是在自动开关一样。
检查TCP线程,while主循环里面用了select,当select检测到有数据到达这个socket时,去收数据。而select设置的超时时间是1s,也就是没数据时,最多等待1s才往下走,有数据时马上去收,后来把1s改成了500ms,明显就快多了!
但我还是有疑问,按理说select函数并不会耽误数据的处理和收发啊,因为他是有数据马上返回告诉你有数据,此时马上去收,无数据等待一个timeout的时间返回,那就应该不管timeout时间设多长都没关系才对啊?
4、 数据分两次中间隔了一段时间才到
这是最近在wifi+zigbee网关上出现的一个bug,网关一头是wifi连接服务器,一头是zigbee接着很多子设备,比如开关、水浸、气感等等,bug的现象是概率性出现场景命令无法执行,比如开所有灯或者关所有灯这样的场景命令,用户在APP上点一个场景按钮,消息下发到网关上。
分析log发现,这条消息是分两次到达了,两次到达中间还隔了2秒,奇怪的是第二段数据到达时前面那一段数据被清除掉了,没有存下来,但是大多数时候分两次到达的消息都能正确处理,为什么单单这一次没有处理好?这一次跟其他次有什么区别?
这一次的区别是,看到收到第一段消息后,发ping消息的时间到了,给服务器发了ping消息(这条消息是应用层的心跳消息,30s发一次,为了保持心跳以及侦测掉线,当发给服务器的ping消息5s没收到服务器回复认为掉线了)然后过了2s才收到第二段消息。
搜索所有清除缓存的地方,发现就在发ping消息的地方把缓存清除了!所以造成了消息起那一段丢掉了没有被正确处理,去掉这个清除动作多次测试没有再出现这种情况。
这里发现另外一个问题是:这一次的ping消息没有收到服务器回包,因此网关这边判断掉线了,收到的控制消息也没有再去处理,应该怎么设计掉线的逻辑?
仅仅没有收到心跳消息回包就认为掉线合理不? 此时我们关注的有用的控制消息是能正常收到的啊!
所以应该对判断掉线的逻辑做一些优化:
(1)当没收到ping消息回包但控制消息仍然能收到时不应该判断成掉线,只要还能收到数据就不认为掉线;
(2)消抖处理,当连续几次没有收到ping消息(ping消息30s发一次)回包时才认为掉线。
5、 数据被意外清除
问题4改了后,提测后结果又出现了一次场景消息不执行,前前后后测了两百次出现一次,崩溃!凭直觉我觉得这是一个新bug!
分析log,发现这也是一个分两次到达的消息没有正确处理,第一次到达的数据总共有n条完整的消息+控制消息的前半段,看到最后有去把残缺消息片段拷贝到缓存中的操作,但是当后半段消息收到时,缓存里打印出来却没有前半段消息!
拷贝字符串用的是memcpy这个库函数,要拷贝的字符串长度用的是strlen这个函数。
把这条出问题的消息再次用测试代码运行起来,增加log,看代码怎么跑的,看到确实有去处理前面那些一条一条的完整消息,问题在于,处理的时候直接把主循环用于接收socket数据的缓存指针传进去了,有个地方要计算消息的MD5摘要值与消息中带下来的MD5摘要值去比较,把字符串中某个位置的字符赋值成了0。
file:///C:\Users\Administrator.WIN-STED6B9V5UI\AppData\Local\Temp\ksohtml18176\wps7.png
这个操作是很危险的!直接导致了后面拷贝残缺消息片段时strlen计算出来的长度是0,strlen、strstr、strcpy这类字符串处理函数都是遇到0就停止的。
直接把接收缓存指针传进去,这种操作也不规范。
修改方法是,对赋值0那个函数传进去的指针不再是用于接收socket数据的缓存指针,而是而是另外开辟缓存,把要处理的数据拷贝过去,再把新开辟的缓存指针传进去。
6 、socket端口号问题
写到这好累了,长话短说,与服务器连接过程如下:
(1)调用socket 函数,创建一个tcp类型的socket;
(2)初始化自己的地址my_addr,类型为sockaddr_in,内容包括端口号、类型、IP地址(如下图);
file:///C:\Users\Administrator.WIN-STED6B9V5UI\AppData\Local\Temp\ksohtml18176\wps8.jpg
(3)调用bind,把socket 和my_addr绑定起来;
(4)初始化要连接的服务器地址svr_addr;
file:///C:\Users\Administrator.WIN-STED6B9V5UI\AppData\Local\Temp\ksohtml18176\wps9.jpg
(5)调用connect,连接服务器
第2步有一个特别要注意的是自己地址的端口号必须是一个不重数,也就是说这次用的是2000,那么下次wifi板再次connect时(比如断电上电再次connect)不能再用2000,可以是2001依次递增或者其他。
那么为什么要这样?因为服务器侦测wifi板掉线一般是没有wifi板自身快的,wifi板去重连服务器时如果用的是原来的端口号,而服务器那边还没侦测出wifi板掉线,原来的那个端口号的tcp链接还在,资源没有释放,再用原来那个端口号建立新的链接肯定不会成功;另一种情况,wifi板断电上电重新去连服务器,服务器肯定是不知道wifi板重启了,还用原来的端口号去连也是连不上的,除非断电很久等服务器侦测出wifi掉线再上电。
所以正确的做法是把端口号存入Flash里,每次用时从flash里取,用完更新这个值。
单片机方案都不会自己产生不重数,因此需要自己操心存起来, 有些linux系统方案是底层自己会产生不重数不用自己操心。
7、 没有 keep alive机制引发的问题
好累了,这个有点不记得了,改天完全记起来再补充。今天的分享就到这里 有疑问3250395686
评论
查看更多