视频盒子项目遇到的问题
本文主要記錄了作者在項(xiàng)目中使用netty遇到的問(wèn)題,以及一些問(wèn)題,避免以后踩坑
項(xiàng)目背景:公司有一個(gè)h5管理平臺(tái)查看電梯設(shè)備的基本情況,然后現(xiàn)在要在平臺(tái)上添加查看故障回放和實(shí)時(shí)預(yù)覽功能,2021年基于海康做了一版,有興趣的人可以看下我之前的文章,但是海康攝像頭因?yàn)槟承┰?#xff0c;和我們自研的開發(fā)板沖突,所以現(xiàn)在這套底層架構(gòu),都是公司自研的
關(guān)系如下:
瀏覽器->websocket服務(wù)器 -> socket服務(wù)器 ->開發(fā)板運(yùn)行的c程序
其中服務(wù)器都是java使用netty框架開發(fā)的,為什么要有一個(gè)websocket和socket
是因?yàn)榉?wù)器需要把數(shù)據(jù)推給瀏覽器這個(gè)數(shù)據(jù)還不知道什么時(shí)候才可以返回,http實(shí)現(xiàn)不了這個(gè),http是一次請(qǐng)求對(duì)應(yīng)一次響應(yīng),socket 服務(wù)器主要和設(shè)備打交道,用于接受和發(fā)送指令到設(shè)備
一.項(xiàng)目疑難雜癥
1.使用netty搭建websocket 服務(wù)器
服務(wù)器寫給客戶端的數(shù)據(jù)是byte數(shù)組(視頻二進(jìn)制流),客戶端一直報(bào)解碼失敗,因?yàn)榉?wù)器是new TextWebSocketFrame然后使用它的構(gòu)造,是傳byte數(shù)組,但是實(shí)際在websocket協(xié)議中,有一個(gè)opcode屬性的值還是字符串,所以瀏覽器還是按照字符串解碼,導(dǎo)致報(bào)錯(cuò)
正確的是:new BinaryWebSocketFrame(ByteBuf對(duì)象)
所以關(guān)鍵是對(duì)TextWebSocketFrame和BinaryWebSocketFrame的理解
TextWebSocketFrame的構(gòu)造有ByteBuf,導(dǎo)致我誤以為可以傳
2 服務(wù)器使用java寫的,客戶端是公司的linux工程師,是c語(yǔ)言開發(fā)的,然后對(duì)接期間遇到了很多問(wèn)題,這里簡(jiǎn)單記錄下,
(1). c語(yǔ)言是小端序,所以發(fā)送的時(shí)候要轉(zhuǎn)大端,因?yàn)閖ava,tcp是大端
(2). c語(yǔ)言結(jié)構(gòu)體有個(gè)字節(jié)對(duì)齊的問(wèn)題,簡(jiǎn)單說(shuō)就是cpu為了優(yōu)化取指令的時(shí)間,將內(nèi)存的地址進(jìn)行對(duì)齊,然后客戶端發(fā)送的時(shí)候使用的是 結(jié)構(gòu)體,導(dǎo)致結(jié)構(gòu)體里有一個(gè)字節(jié)的char,發(fā)送到服務(wù)器是4個(gè)字節(jié),因?yàn)閮?nèi)存對(duì)齊為4,解決辦法就是設(shè)置結(jié)構(gòu)體字節(jié)對(duì)齊為1
3.spring boot 項(xiàng)目 需要啟動(dòng) websocket 和socket,都是netty實(shí)現(xiàn)的,然后spring boot入口類 implements CommandLineRunner這個(gè)接口的run方法,注意必須要new 兩個(gè)Thread,要不然,會(huì)阻塞
4.當(dāng)前做的視頻盒子項(xiàng)目 需要 同時(shí) 有三個(gè)服務(wù)(port),http 服務(wù)器(和瀏覽器,app打交道)
socket服務(wù)器 和設(shè)備交互
websocket 和瀏覽器 保持長(zhǎng)鏈接,傳輸 視頻和直播流
這時(shí)候有個(gè)問(wèn)題,就是 spring boot的controller接受app的指令請(qǐng)求后,需要下發(fā)到 socket 服務(wù)器 所關(guān)聯(lián)的設(shè)備,這個(gè)是怎么實(shí)現(xiàn)的,socket 模塊寫一個(gè) 全局 map,保存 imei和channel(客戶端)的關(guān)系,這樣別的模塊想用,直接用這個(gè)map就可以,注意:要保證這三個(gè)服務(wù)是同一個(gè)進(jìn)程下的,因?yàn)橥M(jìn)程下的線程共享數(shù)據(jù).
另外一個(gè)問(wèn)題,由于有了全局 map,導(dǎo)致 http,websocket ,socket 三個(gè)模塊都可以對(duì)設(shè)備發(fā)送指令,這個(gè)時(shí)候注意線程安全問(wèn)題,否則會(huì)有問(wèn)題,解決辦法
使用 channel.eventLoop().execute(),把任務(wù)放到阻塞隊(duì)列,netty底層會(huì)依次執(zhí)行這個(gè)隊(duì)列的任務(wù),阻塞隊(duì)列保證了線程安全
底層原理:
NioEventLoop的run方法,處理完processSelectedKeys讀寫事件后,會(huì)執(zhí)行runAllTasks(),這個(gè)里面的taskQueue保存了我們通過(guò) channel.eventloop.execute保存的runable任務(wù),這個(gè)時(shí)候會(huì)按隊(duì)列的順序依次執(zhí)行隊(duì)列的任務(wù)
5…java 和c 使用 socket通信單字節(jié) 有符合無(wú)符號(hào)問(wèn)題
java都是有符號(hào)數(shù), 單字節(jié)最大表示127,而c 可以是有符號(hào),也可以是無(wú)符號(hào),怎么辦呢?
1.調(diào)整通信協(xié)議,本來(lái)是1個(gè)字節(jié),改成2個(gè)字節(jié)
2. java 往c發(fā)送:
c語(yǔ)言往 java發(fā)送,java接受:
//假設(shè)接受數(shù)據(jù)是255 二進(jìn)制 1111 1111 java 輸出的是-1byte data = (byte) 0xff; //255 的二進(jìn)制 1111 1111 和 0xff(1111 1111) 進(jìn)行與運(yùn)算結(jié)果還是1111 1111,但是因?yàn)槭莍nt接受,是4個(gè)字節(jié),//所以高位補(bǔ)0,最終就是 0000 0000 0000 0000 0000 0000 1111 1111也就是十進(jìn)制255int expected = data & 0xff;其實(shí)就是用更高位數(shù)來(lái)表示低位數(shù)
6.java 和c socket通信發(fā)送 MD5進(jìn)行文件校驗(yàn)
c生成的md5是無(wú)符號(hào)16進(jìn)制數(shù),然后發(fā)送到j(luò)ava服務(wù)器,java 拿到這16個(gè)字節(jié)的16進(jìn)制數(shù),無(wú)法轉(zhuǎn)換成正確的md5,
因?yàn)閖ava沒有無(wú)符號(hào)數(shù),只好讓c客戶端將16進(jìn)制數(shù)轉(zhuǎn)成 字符串,比如 c轉(zhuǎn)成的md5是 :
b7 da 0c 91 0a 79 35 7e 02 b2 50 0f 93 3d 4e 28 共16個(gè)字節(jié),2個(gè)16進(jìn)制數(shù)是1個(gè)字節(jié),
最快的辦法就是 讓c 把md5前4個(gè)字節(jié)和后4個(gè)字節(jié)截掉,然后 發(fā)給我 最后就是 0a 79 35 7e 02 b2 50 0f 然后轉(zhuǎn)成字符串(ascll)
也就是48, 97, 55, 57, 51, 53, 55, 101, 48, 50, 98, 50, 53, 48, 48, 102
第二種解決辦法 其實(shí)就是用上面的問(wèn)題5解決方法,不用客戶端轉(zhuǎn)成字符串了:服務(wù)器拿到那16個(gè)字節(jié)之后,挨個(gè)和255進(jìn)行與運(yùn)算,然后用short類型接受,就是正確的10進(jìn)制數(shù),然后轉(zhuǎn)成16進(jìn)制,在變成字符串存儲(chǔ)也可以
//假設(shè)這是客戶端傳輸?shù)膍d5,為什么有的值前面加(byte),因?yàn)閖ava 賦值默認(rèn)是int類型,然后使用(byte)強(qiáng)轉(zhuǎn)成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++) {//底層其實(shí)就是拿一個(gè)byte和255進(jìn)行&運(yùn)算,然后用short接受,這樣就可以表示正確值了short byte2 = byteBuf.readUnsignedByte();System.out.println(byte2);//拼在一起builder.append(Integer.toHexString(byte2));}//最后輸出System.out.println(builder.toString());7. 1ffmpeg轉(zhuǎn)碼
板子上的攝像頭使用ffmpeg錄制的視頻是mp4封裝格式,但是視頻編碼格式是MPEG-4,h5的video標(biāo)簽只能播放h264視頻編碼格式的視頻,只能在服務(wù)器通過(guò)ffmpeg 轉(zhuǎn)成h264之后發(fā)送給瀏覽器了,ffmpeg安裝這塊我之前有文章寫了,這里不在闡述了.
轉(zhuǎn)碼命令:
old.mp4是原文件,new.mp4是轉(zhuǎn)碼后的文件
7.2 ffmpeg 轉(zhuǎn)碼報(bào)錯(cuò) Output file #0 does not contain any stream
剛開始以為是機(jī)器資源不夠用,才報(bào)錯(cuò),最后發(fā)現(xiàn)原來(lái)是原視頻有問(wèn)題,只有幾百個(gè)字節(jié),所以才轉(zhuǎn)碼失敗
二.使用netty時(shí)需要注意的點(diǎn)
1.自定義的Handler最好繼承SimpleChannelInboundHandler,避免內(nèi)存泄漏
2.在寫項(xiàng)目時(shí),因?yàn)槭莟cp,所以不可避免的要解決的就是粘包分包問(wèn)題,如果有人對(duì)粘包分包問(wèn)題有疑問(wèn),看我之前寫的那篇Socket粘包分包吧粘包因?yàn)槲覀兊膮f(xié)議,所以可以避免,分包這里繼承了netty的ByteToMessageDecoder,然后自己寫解碼邏輯
,netty自己實(shí)現(xiàn)了這么一套邏輯:如果你不read,那原來(lái)的數(shù)據(jù)它會(huì)給你存著,
然后直到協(xié)議的長(zhǎng)度等于實(shí)際獲取的數(shù)據(jù)長(zhǎng)度時(shí)候,完成一個(gè)整包,就可以繼續(xù)執(zhí)行下面邏輯了
剛開始我用了一種原生scoket實(shí)現(xiàn)的,根本沒有用到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個(gè)字節(jié)byte[] packageHead = new byte[6];//從哪里讀取,讀多少,但是readindex不變,否則使用readBytes如果包的長(zhǎng)度不夠,會(huì)導(dǎo)致丟包byteBuf.getBytes(byteBuf.readerIndex(), packageHead);DataPackage dataPackage = new DataPackage();dataPackage.setMagicCode(packageHead[0]);//獲取數(shù)據(jù)包長(zhǎng)度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());//可讀數(shù)據(jù)是否滿足 包的數(shù)據(jù)長(zhǎng)度if ((byteBuf.readableBytes() - packageHead.length) >= dataPackage.getPkgSize()) {byte[] data = new byte[dataPackage.getPkgSize()];byteBuf.readBytes(6); //移動(dòng)指針到數(shù)據(jù)包開始的位置//讀取數(shù)據(jù)包byteBuf.readBytes(data);dataPackage.setData(data);list.add(dataPackage);} else {log.info("數(shù)據(jù)包長(zhǎng)度不夠");}整個(gè)decode的思路就是先獲取我們定義的協(xié)議包頭6個(gè)字節(jié),然后第1個(gè)字節(jié)是魔法號(hào),后4個(gè)字節(jié)是數(shù)據(jù)包長(zhǎng)度(不是整包長(zhǎng)度,整包長(zhǎng)度=包頭6+包體),然后如果不夠6個(gè)字節(jié),說(shuō)明包有問(wèn)題,就不處理,直到Bytebuf.readableBytes(-包頭6個(gè)字節(jié))>= 可讀的數(shù)據(jù),這樣才是一個(gè)完整的數(shù)據(jù)包,可以看到實(shí)現(xiàn)的功能很簡(jiǎn)單,但是代碼量卻很多
后面我就換了種寫法代碼如下:
@Overrideprotected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {log.info("客戶端可讀:{}", byteBuf.readableBytes());while (byteBuf.readableBytes() > 6) {//標(biāo)記數(shù)據(jù)包起始位置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("客戶端發(fā)送心跳包");return;}//可讀數(shù)據(jù)是否滿足 包的數(shù)據(jù)長(zhǎng)度if (byteBuf.readableBytes() >= dataPackage.getPkgSize()) {byte[] data = new byte[dataPackage.getPkgSize()];byteBuf.readBytes(data);dataPackage.setData(data);list.add(dataPackage);} else {log.info("數(shù)據(jù)包長(zhǎng)度不夠");byteBuf.resetReaderIndex();return;}}}可以看到簡(jiǎn)潔了很多,所以以后我也會(huì)這么實(shí)現(xiàn),減少代碼量和復(fù)雜度,這里要注意的是我用到了while,是因?yàn)榭蛻舳擞锌赡芏鄠€(gè)包一起給我,如果我不用while,那么我只能處理一個(gè)包,然后就走下面的handler了,剩下的包要等到下次read事件發(fā)生的時(shí)候處理了,這樣會(huì)有問(wèn)題
3.發(fā)送數(shù)據(jù): 如果你沒加encode的話,netty默認(rèn)使用channel.writeAndFlush只支持ByteBuf和fileRegin類型的數(shù)據(jù),所以很多時(shí)候你發(fā)了數(shù)據(jù)包,客戶端卻收不到,你可以拿到writeAndFlush返回的listen對(duì)象看下結(jié)果,然后我項(xiàng)目剛開始發(fā)送的是byte數(shù)組,然后自己往里面System.arraycopy放數(shù)據(jù),最后Unpooled.wrappedBuffer包裝下byte數(shù)組發(fā)送,操作很簡(jiǎn)單,但是也很繁瑣,后面再新增協(xié)議的時(shí)候就直接使用ButeBuf了,然后根據(jù)協(xié)議往里面write數(shù)據(jù),也很方便,底層netty都實(shí)現(xiàn)好了
4.由于要將設(shè)備的imei和chanel綁定,我這里使用了map,切記是concurrentHashMap線程安全的map,所以其實(shí)是雙向綁定channel斷開連接后,這個(gè)map要移除掉對(duì)應(yīng)的imei
那我怎么從channel拿到imei呢,我總不能在搞個(gè)channel和imei的map吧,后面看書學(xué)到了一種方式
channelHandlerContext.channel().attr(AttributeKey.valueOf(“imei”)).set(imei);
其實(shí)就是netty底層幫我們封裝好了一個(gè)map讓我們使用.
看到文章末尾的,可以看下的我的程序人生這篇文章,主要是講我在編程這條路上的經(jīng)歷,祝愿對(duì)你有用,感謝!
總結(jié)
以上是生活随笔為你收集整理的视频盒子项目遇到的问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 81.(cesium之家)cesium修
- 下一篇: 窥尽大数据背后被遮掩起来的财富