视频盒子项目遇到的问题

本文主要记录了作者在项目中使用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 1111
   int data=255;   
    //c语言接受的是111 1111 然后用无符号接受,就可以拿到255了
   byte bdata = (byte) data;   
  

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

    //假设接受数据是255 二进制 1111 1111 java 输出的是-1
	byte data = (byte) 0xff;	
	//255 的二进制 1111 1111 和 0xff(1111 1111) 进行与运算结果还是1111 1111,但是因为是int接受,是4个字节,
	//所以高位补0,最终就是  0000 0000  0000 0000 0000 0000 1111 1111也就是十进制255
	int 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)强转成byte
 byte[] 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的特性

如下:

 @Override
    protected 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个字节)>= 可读的数据,这样才是一个完整的数据包,可以看到实现的功能很简单,但是代码量却很多

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

 @Override
    protected 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让我们使用

猜你喜欢

转载自blog.csdn.net/weixin_43803688/article/details/125066971