本文主要记录了作者在项目中使用netty遇到的问题,以及一些问题,避免以后踩坑

项目背景:公司有一个h5管理平台查看电梯设备的基本情况,然后现在要在平台上添加查看故障回放和实时预览功能,2021年基于海康做了一版,有兴趣的人可以看下我之前的文章,但是海康摄像头因为某些原因,和我们自研的开发板冲突,所以现在这套底层架构,都是公司自研的

关系如下:
浏览器->websocket服务器 -> socket服务器 ->开发板运行的c程序

其中服务器都是java使用netty框架开发的,为什么要有一个websocket和socket
是因为服务器需要把数据推给浏览器这个数据还不知道什么时候才可以返回,http实现不了这个,http是一次请求对应一次响应,socket 服务器主要和设备打交道,用于接受和发送指令到设备

一.项目疑难杂症

1.使用netty搭建websocket 服务器

服务器写给客户端的数据是byte数组(视频二进制流),客户端一直报解码失败,因为服务器是new TextWebSocketFrame然后使用它的构造,是传byte数组,但是实际在websocket协议中,有一个opcode属性的值还是字符串,所以浏览器还是按照字符串解码,导致报错
正确的是:new BinaryWebSocketFrame(ByteBuf对象)
所以关键是对TextWebSocketFrame和BinaryWebSocketFrame的理解
TextWebSocketFrame的构造有ByteBuf,导致我误以为可以传

2 服务器使用java写的,客户端是公司的linux工程师,是c语言开发的,然后对接期间遇到了很多问题,这里简单记录下,
(1). c语言是小端序,所以发送的时候要转大端,因为java,tcp是大端
(2). c语言结构体有个字节对齐的问题,简单说就是cpu为了优化取指令的时间,将内存的地址进行对齐,然后客户端发送的时候使用的是 结构体,导致结构体里有一个字节的char,发送到服务器是4个字节,因为内存对齐为4,解决办法就是设置结构体字节对齐为1

3.spring boot 项目 需要启动 websocket 和socket,都是netty实现的,然后spring boot入口类 implements CommandLineRunner这个接口的run方法,注意必须要new 两个Thread,要不然,会阻塞

4.当前做的视频盒子项目 需要 同时 有三个服务(port),http 服务器(和浏览器,app打交道)
socket服务器 和设备交互
websocket 和浏览器 保持长链接,传输 视频和直播流
这时候有个问题,就是 spring boot的controller接受app的指令请求后,需要下发到 socket 服务器 所关联的设备,这个是怎么实现的,socket 模块写一个 全局 map,保存 imei和channel(客户端)的关系,这样别的模块想用,直接用这个map就可以,注意:要保证这三个服务是同一个进程下的,因为同进程下的线程共享数据.

另外一个问题,由于有了全局 map,导致 http,websocket ,socket 三个模块都可以对设备发送指令,这个时候注意线程安全问题,否则会有问题,解决办法
使用 channel.eventLoop().execute(),把任务放到阻塞队列,netty底层会依次执行这个队列的任务,阻塞队列保证了线程安全

底层原理:
NioEventLoop的run方法,处理完processSelectedKeys读写事件后,会执行runAllTasks(),这个里面的taskQueue保存了我们通过 channel.eventloop.execute保存的runable任务,这个时候会按队列的顺序依次执行队列的任务

5…java 和c 使用 socket通信单字节 有符合无符号问题
java都是有符号数, 单字节最大表示127,而c 可以是有符号,也可以是无符号,怎么办呢?
1.调整通信协议,本来是1个字节,改成2个字节
2. java 往c发送:

 //这个时候 bdata其实输出的是-1,对应的二进制为 1111 1111int data=255;   //c语言接受的是111 1111 然后用无符号接受,就可以拿到255了byte bdata = (byte) data;   

c语言往 java发送,java接受:

    //假设接受数据是255 二进制 1111 1111 java 输出的是-1byte data = (byte) 0xff;  //255 的二进制 1111 1111 和 0xff(1111 1111) 进行与运算结果还是1111 1111,但是因为是int接受,是4个字节,//所以高位补0,最终就是  0000 0000  0000 0000 0000 0000 1111 1111也就是十进制255int expected = data & 0xff;

其实就是用更高位数来表示低位数

6.java 和c socket通信发送 MD5进行文件校验

c生成的md5是无符号16进制数,然后发送到java服务器,java 拿到这16个字节的16进制数,无法转换成正确的md5,
因为java没有无符号数,只好让c客户端将16进制数转成 字符串,比如 c转成的md5是 :
b7 da 0c 91 0a 79 35 7e 02 b2 50 0f 93 3d 4e 28 共16个字节,2个16进制数是1个字节,
最快的办法就是 让c 把md5前4个字节和后4个字节截掉,然后 发给我 最后就是 0a 79 35 7e 02 b2 50 0f 然后转成字符串(ascll)
也就是48, 97, 55, 57, 51, 53, 55, 101, 48, 50, 98, 50, 53, 48, 48, 102

第二种解决办法 其实就是用上面的问题5解决方法,不用客户端转成字符串了:服务器拿到那16个字节之后,挨个和255进行与运算,然后用short类型接受,就是正确的10进制数,然后转成16进制,在变成字符串存储也可以

//假设这是客户端传输的md5,为什么有的值前面加(byte),因为java 赋值默认是int类型,然后使用(byte)强转成bytebyte[] bytes = {(byte) 0xb7, (byte) 0xda, 0x0c, (byte) 0x91, 0x0a, 0x79, 0x35, 0x7e, 0x02, (byte) 0xb2, 0x50, 0x0f, (byte) 0x93, 0x3d, 0x4e, 0x28};ByteBuf byteBuf = Unpooled.buffer(16);byteBuf.writeBytes(bytes);StringBuilder builder = new StringBuilder();for (int i = 0; i < byteBuf.capacity(); i++) {//底层其实就是拿一个byte和255进行&运算,然后用short接受,这样就可以表示正确值了short byte2 = byteBuf.readUnsignedByte();System.out.println(byte2);//拼在一起builder.append(Integer.toHexString(byte2));}//最后输出System.out.println(builder.toString());

7. 1ffmpeg转码
板子上的摄像头使用ffmpeg录制的视频是mp4封装格式,但是视频编码格式是MPEG-4,h5的video标签只能播放h264视频编码格式的视频,只能在服务器通过ffmpeg 转成h264之后发送给浏览器了,ffmpeg安装这块我之前有文章写了,这里不在阐述了.
转码命令:

ffmpeg -i old.mp4 -c:v libx264 new.mp4

old.mp4是原文件,new.mp4是转码后的文件
7.2 ffmpeg 转码报错 Output file #0 does not contain any stream
刚开始以为是机器资源不够用,才报错,最后发现原来是原视频有问题,只有几百个字节,所以才转码失败

二.使用netty时需要注意的点

1.自定义的Handler最好继承SimpleChannelInboundHandler,避免内存泄漏

2.在写项目时,因为是tcp,所以不可避免的要解决的就是粘包分包问题,如果有人对粘包分包问题有疑问,看我之前写的那篇Socket粘包分包吧粘包因为我们的协议,所以可以避免,分包这里继承了netty的ByteToMessageDecoder,然后自己写解码逻辑
,netty自己实现了这么一套逻辑:如果你不read,那原来的数据它会给你存着,
然后直到协议的长度等于实际获取的数据长度时候,完成一个整包,就可以继续执行下面逻辑了

刚开始我用了一种原生scoket实现的,根本没有用到netty提供的ByteBuf的特性

如下:

 @Overrideprotected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {log.info("客户端可读:{}", byteBuf.readableBytes());if (byteBuf.readableBytes() < 6) {return;}//        while (byteBuf.readableBytes() > 6) {//先读包头的6个字节byte[] packageHead = new byte[6];//从哪里读取,读多少,但是readindex不变,否则使用readBytes如果包的长度不够,会导致丢包byteBuf.getBytes(byteBuf.readerIndex(), packageHead);DataPackage dataPackage = new DataPackage();dataPackage.setMagicCode(packageHead[0]);//获取数据包长度byte[] dataLengthBytes = new byte[4];System.arraycopy(packageHead, 1, dataLengthBytes, 0, 4);dataPackage.setPkgSize(BinaryUtil.my_bb_to_int_be(dataLengthBytes));dataPackage.setCmdId(packageHead[5]);log.info(dataPackage.toString());//可读数据是否满足 包的数据长度if ((byteBuf.readableBytes() - packageHead.length) >= dataPackage.getPkgSize()) {byte[] data = new byte[dataPackage.getPkgSize()];byteBuf.readBytes(6);  //移动指针到数据包开始的位置//读取数据包byteBuf.readBytes(data);dataPackage.setData(data);list.add(dataPackage);} else {log.info("数据包长度不够");}

整个decode的思路就是先获取我们定义的协议包头6个字节,然后第1个字节是魔法号,后4个字节是数据包长度(不是整包长度,整包长度=包头6+包体),然后如果不够6个字节,说明包有问题,就不处理,直到Bytebuf.readableBytes(-包头6个字节)>= 可读的数据,这样才是一个完整的数据包,可以看到实现的功能很简单,但是代码量却很多

后面我就换了种写法代码如下:

 @Overrideprotected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {log.info("客户端可读:{}", byteBuf.readableBytes());while (byteBuf.readableBytes() > 6) {//标记数据包起始位置byteBuf.markReaderIndex();DataPackage dataPackage = new DataPackage();dataPackage.setMagicCode(byteBuf.readByte());dataPackage.setPkgSize(byteBuf.readInt());dataPackage.setCmdId(byteBuf.readByte());log.info(dataPackage.toString());//心跳包if (dataPackage.getMagicCode() == 3 && dataPackage.getCmdId() == 0) {log.info("客户端发送心跳包");return;}//可读数据是否满足 包的数据长度if (byteBuf.readableBytes() >= dataPackage.getPkgSize()) {byte[] data = new byte[dataPackage.getPkgSize()];byteBuf.readBytes(data);dataPackage.setData(data);list.add(dataPackage);} else {log.info("数据包长度不够");byteBuf.resetReaderIndex();return;}}}

可以看到简洁了很多,所以以后我也会这么实现,减少代码量和复杂度,这里要注意的是我用到了while,是因为客户端有可能多个包一起给我,如果我不用while,那么我只能处理一个包,然后就走下面的handler了,剩下的包要等到下次read事件发生的时候处理了,这样会有问题

3.发送数据: 如果你没加encode的话,netty默认使用channel.writeAndFlush只支持ByteBuf和fileRegin类型的数据,所以很多时候你发了数据包,客户端却收不到,你可以拿到writeAndFlush返回的listen对象看下结果,然后我项目刚开始发送的是byte数组,然后自己往里面System.arraycopy放数据,最后Unpooled.wrappedBuffer包装下byte数组发送,操作很简单,但是也很繁琐,后面再新增协议的时候就直接使用ButeBuf了,然后根据协议往里面write数据,也很方便,底层netty都实现好了

4.由于要将设备的imei和chanel绑定,我这里使用了map,切记是concurrentHashMap线程安全的map,所以其实是双向绑定channel断开连接后,这个map要移除掉对应的imei
那我怎么从channel拿到imei呢,我总不能在搞个channel和imei的map吧,后面看书学到了一种方式
channelHandlerContext.channel().attr(AttributeKey.valueOf(“imei”)).set(imei);
其实就是netty底层帮我们封装好了一个map让我们使用.

看到文章末尾的,可以看下的我的程序人生这篇文章,主要是讲我在编程这条路上的经历,祝愿对你有用,感谢!

视频盒子项目遇到的问题相关推荐

  1. hadoop中HBase子项目入门讲解

    HBase 是Hadoop的一个子项目,HBase采用了Google BigTable的稀疏的,面向列的数据库实现方式的理论,建立在hadoop的hdfs上,一方面里用了hdfs的高可靠性和可伸缩行, ...

  2. Django项目中的子项目中自动生成自己想要的文件内容

    当我们用命令生成子项目时,会自动生成一些文件,如下图所示: 那为什么会自动生成这些文件呢? 原因如下: 那么如何自动生成我们自己想要的文件呢? 可以直接在app_template文件夹中新建文件,然后 ...

  3. springcloud 子项目怎么导入_使用eclipse一步一步创建SpringCloud项目(二)—— 使用feign和ribbon调用微服务...

    一.创建服务提供者service-hi 1.创建子项目方法类似eureka,略 2.修改pom文件,主要是添加jar包 4.0.0 com.landcode land 1.0.0-SNAPSHOT l ...

  4. 如何为网站项目添加子项目

    最近要给公司的电子商务网站添加个圈子的功能.网站功能本来就包含有新闻发布,会员管理,商品购物,后台管理等,现在又要再加上圈子的功能,网站项目越来越复 杂,每次编译生成的dll超过100k.这样每次修改 ...

  5. springcloud 子项目怎么导入_Spring Cloud基础面试题大集合

    整理自:ThinkWon  链接:blog.csdn.net/ThinkWon/article/details/104397367 本文知识点目录 为什么需要学习Spring Cloud 什么是Spr ...

  6. maven子项目传git_Maven Git发布

    maven子项目传git 我需要在开始这篇文章时指出我在去年才开始认真地与Git合作. 不幸的是,我从事的许多项目仍在使用SVN或CVS,但是现在我终于开始使用Git了. 在过去的几年中,我使用Mav ...

  7. 谈大数据里各子项目搭建时的环境变量配置(深入)

    无论是hadoop.spark还是storm.搭建各子项目需要配置环境变量,这里列举我所尝试过的各种版本. vim /etc/profilesuorce /etc/profile vim /etc/p ...

  8. 国内首个 Kubernetes SIG-Cloud-Provider 子项目揭秘 | 云原生生态周报 Vol. 37

    作者 | 高相林.陈俊.陈有坤.敖小剑 业界要闻 国内首个 Kubernetes SIG-Cloud-Provider 子项目揭秘 阿里云作为坚定的云原生计算推动者,贡献了阿里云上运行 Kuberne ...

  9. 在springboot项目中如何创建子项目

    第一步: 点击项目,鼠标右击,点击new,然后点击module 第二步: 选择左边的Spring Initilizr 选择自己的SDK版本,然后点击next下一步 第三步: 依次填写下面所需的内容,然 ...

最新文章

  1. document 获得元素节点,属性节点,文本节点
  2. boost::serialization模块测试 auto_ptr 序列化的测试程序
  3. LintCode 1692. 组队打怪(田忌赛马,二分查找)
  4. 运算器(串行加法器和并行加法器,ALU)
  5. python中一切都是对象_python中一切皆对象
  6. docker 日志_解决docker容器日志导致主机磁盘空间满了的情况
  7. DICOM C-GET vs C-MOVE
  8. mysql中的select into 与insert into语句用于备份数据
  9. Win7 系统解压安装
  10. Python「剪藏」网页为 PDF
  11. 要想文章好,图片少不了,22个技能助您获得美图(带字幕视频)
  12. Redis数据莫名其妙全部丢失
  13. java gmail 发送邮件_Java通过Gmail发送电子邮件
  14. mips的旁路_【参赛手记】MIPS流水线之阵列乘除法的实现
  15. 空间点到平面的垂足坐标的计算方法以及matlab实现
  16. 不动产登记 科学用好大数据
  17. 009仓储物流中心里的人机工程学设计
  18. matlab中1代表什么颜色,利用matlab如何在一个图中表示不同颜色得点
  19. 北京二中通州分校2021高考成绩查询,速来查看!通州3所、昌平1所、门头沟1所学校2021中招计划...
  20. 夺冠了,8年之约......

热门文章

  1. Linux 安装rabbitmq
  2. 【精】JAVA各大厂问题汇总-HELLO XF
  3. 【C++】多态之组合与聚合
  4. 禁止div被拖动 css,【Web前端问题】div在拖动时出现禁止图标
  5. markdown编辑器——文字颜色、大小、字体、背景色、图片大小与居中对齐的设置方法
  6. 想考阿里云ACP认证,网上买题库靠谱吗?
  7. C++ 读取wav文件中的PCM数据
  8. 《密码法》之商用密码应用安全性评估----六问
  9. 五分钟搞懂什么是红黑树(全程图解)
  10. 上榜豆瓣2020年度经管榜,让《重来3》带你逃离内卷、摆脱焦虑