ToB企服应用市场:ToB评测及商务社交产业平台

标题: [ PyQt使用Socket/TCP通信 ] : 智能车上位机体系,pyqt下的socket通信,pyt [打印本页]

作者: 美食家大橙子    时间: 2024-9-21 21:22
标题: [ PyQt使用Socket/TCP通信 ] : 智能车上位机体系,pyqt下的socket通信,pyt
目录
前言:
准备工作:
初级服务器端编写:
中级服务器端编写+客户端收数据函数实现:
数据包格式v1.0
客户端收数据函数V1.0
客户端分析1.0
    本地测试:成功!
     两台主机测试1.0:失败,视频解析失败,直接花屏了!
问题分析:
问题解决:
数据包格式V2.0
客户端接受数据函数V2.0
客户端更新分析2.0:
两主机测试2.0:失败,怎么这么卡?
问题分析:
问题解决:
数据包格式V3.0
客户端接受数据函数V3.0
客户端更新分析3.0:
两主机测试3.0:
两主机测试4.0
成功!     



前言:

        最近看到智能车的上位机,以为很酷,恰恰自己最近也在学习socket,tcp协议,计划也写一款如许的上位机体系来查验一下自己,别的,本文也涉及到了ros相干知识。当然,这个项目在编写过程中也遇到了不少bug,当然也包罗tcp协议中的经典问题:半包和粘包,以及其他小的bug,在这里,我会详细的列出每个bug的缘故原由以及解决办法,以及自己每步代码的用意,目的就是能让小白也能看的懂!
上位机介绍:


这个上位机的ui界面(前端)是用pyqt编写,后端用python,这是一个 客户端.可以通过socket实时订阅图像,小车参数,同时可以 发送文本数据,位置数据,速率数据.此中有关小车数据的收发涉及到 ros的相干知识,本文主讲的 是socket,对pyqt和ros介绍较少,同时,本次的服务端和客户端只给出了焦点函数的实现,没有完备代码.
准备工作:

相信大家能进来这篇博客,想必已经配置好了自己的python,ros,opencv,pyqt环境,(没有ros环境也不要紧,焦点不在这),
初级服务器端编写:

要在python环境下编写socket,socket模块是必不可少的,这里我们使用类的方式来进行编写,代码如下:
  1. import socket
  2. class server(QObject):
  3.    
  4.     def __init__(self):
  5.         self.connect_server()
  6.     def connect_server(self):
  7.         self.ser_soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  8.         self.ser_soc.bind(("127.0.0.1",8899))
  9.         self.ser_soc.listen(5)
  10.         print("服务器监听中")
  11.         self.socketvalue, addr= self.ser_soc.accept()
  12.         print("连接到客户端",addr)
  13.         while True:            
  14.             data = self.socketvalue.recv(1024)
  15.             if data == b"exit":
  16.                 print("客户端断开连接,在时间",time.time())
  17.                 break
  18.         self.ser_soc.close()
  19.         self.socketvalue.close()
  20. if __name__ == "__main__":
  21.     ser = server()
复制代码
代码表明:
服务器端编写+客户端收数据函数实现:


客户端收数据函数V1.0

  1.     #接收缓冲区数据
  2.    def recv(self):
  3.         while self.flag == True:
  4.             data = self.client_socket.recv(88888) #收到数据,字节长度最大888888
  5.             
  6.             length = len(data)#数据长度
  7.             video_length = length - 30 #视频字节长度
  8.             if data == b"exit": #收到服务器断开消息,break
  9.                 self.show_data.emit(time.time(),":服务器断开连接")
  10.                 break
  11.                
  12.             if data[0:2] == 0x55aa.to_bytes(2,"little"):  #判断包头
  13.                 dataval = struct.unpack('fffffff',data[2:30]) #除去包头后的28位字节为小车参数
  14.                 self.show_carinfo.emit(dataval) #发送信号到主线程显示
  15.                 self.vdieo_value = data[30:30+video_length] #小车参数后的字节开始数video_length个字节为图像数据
  16.                 nparry = np.frombuffer(self.vdieo_value,dtype="uint8")
  17.                 fream = cv2.imdecode(nparry,cv2.IMREAD_COLOR) #视频帧解码
  18.                 self.show_vdieo.emit(fream) #将视频帧发送主线程显示         
复制代码

    本地测试:成功!


     两台主机测试1.0:失败,视频解析失败,直接花屏了!


   问题分析:

  这里说句真话我也有点懵,本地调试就行 ,两主机就寄了,因此,就在服务器端send_fream函数加入以下代码
  1. print(len(valuedata))
  2.      break
复制代码
即打印发送端数据包长度,打印一次就break掉。如下显示32671
同样的方法,在客户端收数据的recv函数加入
  1. print(length)
复制代码
输出为:

  很显着,我只发送了一次数据,长度为32671,精确的话客户端也应打印32671,但是他却打印了好几次,服务器发送的数据被客户端分成几次接收,很显着,这是半包了。
  咱们来验证一下:
  将客户端收到的字节数相加,这里帮大家算了,结果正好和服务器短发的数据长度32671相等,这就更加的证实数据 被半包了!
  

  问题解决:

  根据数据被半包,我们引导tcp是流式传输协议,没有边界,所以我们应该在数据包中添加边界,使得客户端的在读取数据的时候有边界参考,那么我就在包中加入数据总长。根据这个思绪,我更新了数据报格式。
  数据包格式V2.0

  1. valuedata = (0x55aa).to_bytes(2,"little")+(len(car_info+self.video_data)+2+4).to_bytes(4,"little")+self.car.encode()+self.video_data
复制代码
相比v1.0多了(len(car_info+self.video_data)+2+2+4).to_bytes(4,"little")将小车参数和视频参数相加的总长,再加上2+4,指的是包头(2字节)+总长(4字节)
  客户端接受数据函数V2.0

  1.     def recv(self):
  2.         while self.flag == True:
  3.             data = self.client_socket.recv(88888) #收到数据,字节长度最大888888
  4.             #收到服务器断开消息,break
  5.             length = len(data)
  6.             print(length)
  7.             video_length = length - 34
  8.             if data == b"exit":
  9.                 self.show_data.emit(time.time(),":服务器断开连接")
  10.                 break
  11.                
  12.             if data[0:2] == 0x55aa.to_bytes(2,"little"): #判断包头
  13.                     print("pppppppppppppppppp")
  14.                     if data[2:6] == len(data).to_bytes(4,"little"): #判断数据总长是否正确
  15.                         print("-------")
  16.                         dataval = struct.unpack('fffffff',data[6:34]) #除去包头后的28位字节为小车参数
  17.                         self.show_carinfo.emit(dataval) #发送信号到主线程显示
  18.                         self.vdieo_value = data[34:34+video_length] #小车参数后的字节开始数video_length个字节为图像数据
  19.                         nparry = np.frombuffer(self.vdieo_value,dtype="uint8")
  20.                         fream = cv2.imdecode(nparry,cv2.IMREAD_COLOR) #视频帧解码
  21.                         if fream is not None:
  22.                             self.show_vdieo.emit(fream) #将视频帧发送主线程显示
复制代码
客户端更新分析2.0:

  if data[2:6] == len(data).to_bytes(4,"little"):判断是否为数据总长,是的话说明数据精确。因为从索引2再数4个字节为数据包总长。
  
   两主机测试2.0:失败,怎么这么卡?

  问题分析:

  同过之前的查验方法,在一些判断语句下打印一些显着字符,判断有没有 进入判断比如我的是“ppppp”,“------”等。发现打印“-----”很少,通过 print(length)发现,每次卡出画面的时候,就打印一次“----”,而且此时的length与服务器端的length相等,说明我只截取到了在服务器端发送整条数据,我客户端也刚刚好一次就全部收到了数据,那么此时画面是正常的,但是没有一次受到的话,画面就卡在了上一次正常一次收到的数据的画面。这种一次就收到整条数据(不出现半包)我认为是随机的。(卡的过程中客户端由于半包收到的是被分割的数据)
  
  问题解决:

  我需要添加一个包尾,在没有到包尾是就将数据累加,如许就可以了。
  数据包格式V3.0

  1. valuedata = (0x55aa).to_bytes(2,"little")+(len(car_info+self.video_data)+2+2+4).to_bytes(4,"little")+self.car.encode()+self.video_data+(0x55cc).to_bytes(2,"little")
复制代码
相比v2.0多了包尾0x55cc。设不定长的视频数据长度为n,那么计算以下就得到数据包总长为:2+4+28+n+2=36+n
  数据格式变了,客户端的读数据函数理应改变。
  客户端接受数据函数V3.0

  1.     def recv(self):
  2.         while self.flag == True:
  3.             data = self.client_socket.recv(88888) #收到数据,字节长度最大888888
  4.             #收到服务器断开消息,break
  5.             length = len(data)
  6.             video_length = length - 36
  7.             if data == b"exit":
  8.                 self.show_data.emit(time.time(),":服务器断开连接")
  9.                 break
  10.                
  11.             if data[0:2] == 0x55aa.to_bytes(2,"little") or self.tcp_flag: #判断包头
  12.                 self.tcp_flag = True #第一次发送完整数据标志位
  13.                 if data[-2:] == 0x55cc.to_bytes(2,"little"): #判断包尾
  14.                     self.aimdata = data + self.tempdata  #将最后一次接收到的数据加上
  15.                     longth = len(self.aimdata) #打印数据总长
  16.                     video_length = longth- 36 #计算视频字节大小
  17.                     # print(len(self.aimdata))
  18.                     self.tcp_flag = False # 第一次发送完了完整数据后,标志位置0
  19.                     if self.aimdata[2:6] == len(self.aimdata).to_bytes(4,"little"): #判断数据总长是否正确
  20.                         dataval = struct.unpack('fffffff',self.aimdata[6:34]) #除去包头后的28位字节为小车参数
  21.                         self.show_carinfo.emit(dataval) #发送信号到主线程显示
  22.                         self.vdieo_value = self.aimdata[34:34+video_length] #小车参数后的字节开始数video_length个字节为图像数据
  23.                         nparry = np.frombuffer(self.vdieo_value,dtype="uint8")
  24.                         fream = cv2.imdecode(nparry,cv2.IMREAD_COLOR) #视频帧解码
  25.                         if fream is not None:
  26.                             self.show_vdieo.emit(fream) #将视频帧发送主线程显示
  27.                     #第一次数据接收完成后字节段清空
  28.                     self.tempdata = b''
  29.                     self.aimdata = b''
  30.                 else:
  31.                     self.tempdata += data #半包现象解决核心,没有到包尾就将字节拼接  
复制代码
客户端更新分析3.0:

  这里要说的就比较多了,不过不要紧,一步一步来。
  根据之前的分析,一条数据被分成多份,在这里做个假设,加入数据被分成6份,那么第一份数据肯定是包含包头0x55aa,同理,末了一条数据肯定是包含包尾0x55cc。
  1:tcp_flag:因此我引入了一个bool变量tcp_flag,初始值为假,如许我不管你把数据分成几份,第一份肯定包含包头,此时tcp_flag被置为真,因为第二份数据不再含有包头,所以if中的第一个条件失效,但此时我 or 了一下tcp_flag 有用了,可以接受第一次发数据中的不含包头的那几份数据了,这就是tcp_flag的意义地点。
  2:data[-2:] == 0x55cc.to_bytes(2,"little") 这里的data[-2:]指的倒着数2个字节,即判断包尾,如果在这里if条件符合了,说明我整个数据接受完成了,因为数据格式V2.0末了就是包尾巴0x55cc嘛。
  3.else:
     self.tempdata += data:这是焦点了,当没有到包尾的时候,说明整条数据我还没有接受完,我就将每次的数据拼接到tempdata中不就行了。
  4:self.aimdata = data + self.tempdata,识别到包尾别忘了还要加上含有包尾的那一份数据。
  5:self.tcp_flag = False ,置为假,体现整条数据接受完毕,你这个条件没用了,该是否为包头这个条件闪亮登场了,即用于下一条数据的到来。
  6:  if self.aimdata[2:6] == len(self.aimdata).to_bytes(4,"little")末了再次判断包长是否想等,满足这几个条件之后,数据格式应该对了。
  7: self.tempdata = b'' self.aimdata = b'',每次接受完备条数据,一定要清空这两个,不然数据长会越来越大。
  
   两主机测试3.0:

  本以为要成功了,但是又失败了,我太难了,呜呜呜呜.....
  画面直接为空,啥都没有。
  

  经过数据比对后发现第一次的数据好大,但是今后的数据就相同了,这就是出现粘包了,由于有数据长度的判断,所以没有显示画面。但是经过我的测试,只有在前几次数据发生粘包,堆到一块,但是后面的就没有再粘,数据都是正常的,就没有再管它.

  

  经过打印二进制数据发现,浏览这些字节发现(看的我眼花缭乱),包头包尾居然连在一起,
  原来是 self.aimdata = data + self.tempdata ,这两个加反了,好低级的错误,换一下位置 self.aimdata = self.tempdata +data 就好了。
  

  两主机测试4.0

成功!  
这里没用两个主机测试,使用本地
   


下面这是通过网络发送目的点,ros端仿真小车移动目的点。(里程计和速率都会随之更新,偏航角没有发布,所以没有更新)

末了

相信能看到这里的同砚对socket有了一定的相识,本文只给了服务端和客户端的焦点代码,没有给全部,里面还有pyqt和 ros的相干代码,如果有同砚想要相识这方面的代码,可在评论中讨论,如果需要的多的话,我可以在下一片博客中写到。别的,我本人也是初学者,可能有些地方明白不到位,如果有大佬发现上述的内容有误,接待 在评论或者私信指正。然后,如果大家以为我写的还行,对你有帮助的话,可以留下你宝贵的点赞和关注吗,你们的支持对我真的很重要,求求了!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4