1.netty能做什么
首先netty是一款高性能、封裝性良好且靈活、基于NIO(真·非阻塞IO)的開源框架。可以用來手寫web服務器、TCP服務器等,支持的協議豐富,如:常用的HTTP/HTTPS/WEBSOCKET,并且提供的大量的方法,十分靈活,可以根據自己的需求量身DIV一款服務器。
用netty編寫TCP的服務器/客戶端
1.可以自己設計數據傳輸協議如下面這樣:
2.可以自定義編碼規則和解碼規則
3.可以自定義客戶端與服務端的數據交互細節,處理socket流攻擊、TCP的粘包和拆包問題
2.Quick Start
創建一個普通的maven項目,不依賴任何的三方web服務器,用main方法執行即可。
加入POM依賴
io.netty netty-all 4.1.6.Final com.fasterxml.jackson.core jackson-databind 2.9.7 log4j log4j 1.2.17
設計一套基于TCP的數據傳輸協議
publicclassTcpProtocol{ privatebyteheader=0x58; privateintlen; privatebyte[]data; privatebytetail=0x63; publicbytegetTail(){ returntail; } publicvoidsetTail(bytetail){ this.tail=tail; } publicTcpProtocol(intlen,byte[]data){ this.len=len; this.data=data; } publicTcpProtocol(){ } publicbytegetHeader(){ returnheader; } publicvoidsetHeader(byteheader){ this.header=header; } publicintgetLen(){ returnlen; } publicvoidsetLen(intlen){ this.len=len; } publicbyte[]getData(){ returndata; } publicvoidsetData(byte[]data){ this.data=data; } }
這里使用16進制表示協議的開始位和結束位,其中0x58代表開始,0x63代表結束,均用一個字節來進行表示。
TCP服務器的啟動類
publicclassTcpServer{ privateintport; privateLoggerlogger=Logger.getLogger(this.getClass()); publicvoidinit(){ logger.info("正在啟動tcp服務器……"); NioEventLoopGroupboss=newNioEventLoopGroup();//主線程組 NioEventLoopGroupwork=newNioEventLoopGroup();//工作線程組 try{ ServerBootstrapbootstrap=newServerBootstrap();//引導對象 bootstrap.group(boss,work);//配置工作線程組 bootstrap.channel(NioServerSocketChannel.class);//配置為NIO的socket通道 bootstrap.childHandler(newChannelInitializer(){ protectedvoidinitChannel(SocketChannelch)throwsException{//綁定通道參數 ch.pipeline().addLast("logging",newLoggingHandler("DEBUG"));//設置log監聽器,并且日志級別為debug,方便觀察運行流程 ch.pipeline().addLast("encode",newEncoderHandler());//編碼器。發送消息時候用過 ch.pipeline().addLast("decode",newDecoderHandler());//解碼器,接收消息時候用 ch.pipeline().addLast("handler",newBusinessHandler());//業務處理類,最終的消息會在這個handler中進行業務處理 } }); bootstrap.option(ChannelOption.SO_BACKLOG,1024);//緩沖區 bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);//ChannelOption對象設置TCP套接字的參數,非必須步驟 ChannelFuturefuture=bootstrap.bind(port).sync();//使用了Future來啟動線程,并綁定了端口 logger.info("啟動tcp服務器啟動成功,正在監聽端口:"+port); future.channel().closeFuture().sync();//以異步的方式關閉端口 }catch(InterruptedExceptione){ logger.info("啟動出現異常:"+e); }finally{ work.shutdownGracefully(); boss.shutdownGracefully();//出現異常后,關閉線程組 logger.info("tcp服務器已經關閉"); } } publicstaticvoidmain(String[]args){ newTcpServer(8777).init(); } publicTcpServer(intport){ this.port=port; } }
只要是基于netty的服務器,都會用到bootstrap 并用這個對象綁定工作線程組,channel的Class,以及用戶DIV的各種pipeline的handler類,注意在添加自定義handler的時候,數據的流動順序和pipeline中添加hanlder的順序是一致的。也就是說,從上往下應該為:底層字節流的解碼/編碼handler、業務處理handler。
編碼器
編碼器是服務器按照協議格式返回數據給客戶端時候調用的,繼承MessageToByteEncoder代碼:
publicclassEncoderHandlerextendsMessageToByteEncoder{ privateLoggerlogger=Logger.getLogger(this.getClass()); protectedvoidencode(ChannelHandlerContextctx,Objectmsg,ByteBufout)throwsException{ if(msginstanceofTcpProtocol){ TcpProtocolprotocol=(TcpProtocol)msg; out.writeByte(protocol.getHeader()); out.writeInt(protocol.getLen()); out.writeBytes(protocol.getData()); out.writeByte(protocol.getTail()); logger.debug("數據編碼成功:"+out); }else{ logger.info("不支持的數據協議:"+msg.getClass()+" 期待的數據協議類是:"+TcpProtocol.class); } } }
解碼器
解碼器屬于比較核心的部分,自定義解碼協議、粘包、拆包等都在里面實現,繼承自ByteToMessageDecoder,其實ByteToMessageDecoder的內部已經幫我們處理好了拆包/粘包的問題,只需要按照它的設計原則去實現decode方法即可:
publicclassDecoderHandlerextendsByteToMessageDecoder{ //最小的數據長度:開頭標準位1字節 privatestaticintMIN_DATA_LEN=6; //數據解碼協議的開始標志 privatestaticbytePROTOCOL_HEADER=0x58; //數據解碼協議的結束標志 privatestaticbytePROTOCOL_TAIL=0x63; privateLoggerlogger=Logger.getLogger(this.getClass()); protectedvoiddecode(ChannelHandlerContextctx,ByteBufin,List
首先是黏包問題:
如圖,正常的數據傳輸應該是像數據A那樣,一包就是一個完整的數據,但也有不正常的情況,比如一包數據包含多個數據。而在ByteToMessageDecoder會默認把二進制的字節碼放在byteBuf中,因此我們在code的時候要知道會有這樣的場景。
而粘包問題實際上不需要我們去解決,下面是ByteToMessageDecoder的源碼,callDecode中回調我們手寫解碼器的decode方法。
protectedvoidcallDecode(ChannelHandlerContextctx,ByteBufin,List
綜合上面的源碼分析后,我們發現:decode方法在while循環中,也就是bytebuf只要有內容就會一直調用decode方法進行解碼操作,因此在解決粘包問題時,只需要按照正常流程來就行了,解析協議開頭、數據字節、結束標志后將數據放入到out這個list中即可。后面將會有數據進行粘包測試。
拆包問題
有時候,我們接收到的數據是不完整的,一個包的數據被拆成了很多份被后再發送出去。這種情況有可能是數據太大,被分割成很多份發送出去。比如數據包B被拆成兩份進行發送:
拆包問題,同樣在ByteToMessageDecoder 給我們解決了,我們只需要按照netty的設計原則去寫decode代碼即可。
首先,假設需要我們自己去解決拆包問題應該怎么實現?
先從問題開始分析,需要的是數據B,但是卻只收到了數據B_1,這個時候應該等待剩余的數據B_2的到來,收到的數據B_1應該用一個累加器存起來,等到B_2到來的時候將兩包數據合并起來再進行解碼。
那么問題是,如何讓ByteToMessageDecoder這個知道數據不完整呢,在DecoderHandler.decode中有這樣一段代碼:
if(len>=in.readableBytes()){ logger.debug(String.format("數據長度不夠,數據協議len長度為:%1$d,數據包實際可讀內容為:%2$d正在等待處理拆包……",len,in.readableBytes())); in.resetReaderIndex(); /* **結束解碼,這種情況說明數據沒有到齊,在父類ByteToMessageDecoder的callDecode中會對out和in進行判斷 *如果in里面還有可讀內容即in.isReadable為true,cumulation中的內容會進行保留,,直到下一次數據到來,將兩幀的數據合并起來,再解碼。 *以此解決拆包問題 */ return; }
當讀到協議中的len大于bytebuf的可讀內容時候說明數據不完整,發生了拆包,調用resetReaderIndex將讀操作指針復位,并結束方法。再看看父類中的CallDecode方法的一段代碼:
if(outSize==out.size()){//相等說明,并沒有解析出來新的object到out中 if(oldInputLength==in.readableBytes()){//這里很重要,若相等說明decode中沒有讀取任何內容出來,這里一般是發生拆包后,將ByteBuf的指針手動重置。重置后從這個方法break出來。讓ByteToMessageDecoder去處理拆包問題。這里就體現了要按照netty的設計原則來寫代碼 break;//退出該方法 }else{ continue;//這里直接continue,是考慮讓開發者去跳過某些字節,比如收到了socket攻擊時,數據不按照協議體來的時候,就直接跳過這些字節 } }
退出callDecode后,返回到channelRead中:
publicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{ if(msginstanceofByteBuf){ CodecOutputListout=CodecOutputList.newInstance(); try{ ByteBufdata=(ByteBuf)msg; first=cumulation==null; if(first){ cumulation=data; }else{ cumulation=cumulator.cumulate(ctx.alloc(),cumulation,data); } callDecode(ctx,cumulation,out);//注意這里傳入的不是data,而是cumulator,這個對象相當于一個累加器,也就是說每次調用callDecode的時候傳入的byteBuf實際上是經過累加后的cumulation }catch(DecoderExceptione){ throwe; }catch(Throwablet){ thrownewDecoderException(t); }finally{ if(cumulation!=null&&!cumulation.isReadable()){//這里若是數據被讀取完,會清空累加器cumulation numReads=0; cumulation.release(); cumulation=null; }elseif(++numReads>=discardAfterReads){ //WedidenoughreadsalreadytrytodiscardsomebytessowenotrisktoseeaOOME. //Seehttps://github.com/netty/netty/issues/4275 numReads=0; discardSomeReadBytes(); } intsize=out.size(); decodeWasNull=!out.insertSinceRecycled(); fireChannelRead(ctx,out,size); out.recycle(); } }else{ ctx.fireChannelRead(msg); } }
而channelRead方法是,收到一包數據后就會調用一次。至此,netty幫我們完美解決了拆包問題。我們只需要按著他的設計原則:len>byteBuf.readableBytes時候,重置讀指針,結束decode即可。
業務處理handler類
這一層中數據已經被完整的解析出來了,可以直接使用了:
publicclassBusinessHandlerextendsChannelInboundHandlerAdapter{ privateObjectMapperobjectMapper=ByteUtils.InstanceObjectMapper(); privateLoggerlogger=Logger.getLogger(this.getClass()); @Override publicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{ if(msginstanceofbyte[]){ logger.debug("解碼后的字節碼:"+newString((byte[])msg,"UTF-8")); try{ ObjectobjectContainer=objectMapper.readValue((byte[])msg,DTObject.class); if(objectContainerinstanceofDTObject){ DTObjectdata=(DTObject)objectContainer; if(data.getClassName()!=null&&data.getObject().length>0){ Objectobject=objectMapper.readValue(data.getObject(),Class.forName(data.getClassName())); logger.info("收到實體對象:"+object); } } }catch(Exceptione){ logger.info("對象反序列化出現問題:"+e); } } } }
由于在decode中并沒有將字節碼反序列成對象,因此需要進一步反序列化。在傳輸數據的時候,可能傳遞的對象不只是一種,因此在反序列化也要考慮到這一問題。解決辦法是將傳輸的對象進行二次包裝,將全名類信息包含進去:
publicclassDTObject{ privateStringclassName; privatebyte[]object; }
這樣在反序列化的時候使用Class.forName()獲取Class,避免了要寫很多if循環判斷反序列化的對象的Class。前提是要類名和包路徑要完全匹配!
接下來編寫一個TCP客戶端進行測試
啟動類的init方法:
publicvoidinit()throwsInterruptedException{ NioEventLoopGroupgroup=newNioEventLoopGroup(); try{ Bootstrapbootstrap=newBootstrap(); bootstrap.group(group); bootstrap.channel(NioSocketChannel.class); bootstrap.option(ChannelOption.SO_KEEPALIVE,true); bootstrap.handler(newChannelInitializer(){ @Override protectedvoidinitChannel(Channelch)throwsException{ ch.pipeline().addLast("logging",newLoggingHandler("DEBUG")); ch.pipeline().addLast(newEncoderHandler()); ch.pipeline().addLast(newEchoHandler()); } }); bootstrap.remoteAddress(ip,port); ChannelFuturefuture=bootstrap.connect().sync(); future.channel().closeFuture().sync(); }catch(InterruptedExceptione){ e.printStackTrace(); }finally{ group.shutdownGracefully().sync(); } }
客戶端的handler:
publicclassEchoHandlerextendsChannelInboundHandlerAdapter{ //連接成功后發送消息測試 @Override publicvoidchannelActive(ChannelHandlerContextctx)throwsException{ Useruser=newUser(); user.setBirthday(newDate()); user.setUID(UUID.randomUUID().toString()); user.setName("冉鵬峰"); user.setAge(22); DTObjectdtObject=newDTObject(); dtObject.setClassName(user.getClass().getName()); dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user)); TcpProtocoltcpProtocol=newTcpProtocol(); byte[]objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject); tcpProtocol.setLen(objectBytes.length); tcpProtocol.setData(objectBytes); ctx.write(tcpProtocol); ctx.flush(); } }
這個handler是為了模擬在TCP連接建立好之后發送一包的數據到服務端經行測試,通過channel的write去發送數據,只要在啟動類TcpClient配置了編碼器的EncoderHandler,就可以直接將對象tcpProtocol傳進去,它將在EncoderHandler的encode方法中被自動轉換成字節碼放入bytebuf中。
正常數據傳輸測試:
結果:
2019-01-1416:30:34DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-1416:30:34DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-1416:30:34DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-1416:30:34DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjMwOjE0IiwidWlkIjoiOGY0OTM0OGEtMWNmMy00ZTEyLWEzZTAtY2M1ZTJjZTkzMDdlIn0="} 2019-01-1416:30:34INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='8f49348a-1cf3-4e12-a3e0-cc5e2ce9307e',birthday=MonJan1404:30:00CST2019}
可以看到最終的實體對象User被成功的解析出來。
在debug模式下還會看到這樣的一個表格在控制臺輸出:
+-------------------------------------------------+ |0123456789abcdef| +--------+-------------------------------------------------+----------------+ |00000000|58000000b57b22636c6173734e616d65|X....{"className| |00000010|223a22706f6a6f2e55736572222c226f|":"pojo.User","o| |00000020|626a656374223a2265794a755957316c|bject":"eyJuYW1l| |00000030|496a6f693559614a36626d5035624f77|Ijoi5YaJ6bmP5bOw| |00000040|496977695957646c496a6f794e437769|IiwiYWdlIjoyNCwi| |00000050|596d6c796447686b59586b694f694979|YmlydGhkYXkiOiIy| |00000060|4d4445354c7a41784c7a453049444130|MDE5LzAxLzE0IDA0| |00000070|4f6a4d774f6a45304969776964576c6b|OjMwOjE0IiwidWlk| |00000080|496a6f694f4759304f544d304f474574|IjoiOGY0OTM0OGEt| |00000090|4d574e6d4d7930305a5445794c57457a|MWNmMy00ZTEyLWEz| |000000a0|5a54417459324d315a544a6a5a546b7a|ZTAtY2M1ZTJjZTkz| |000000b0|4d44646c496e303d227d63|MDdlIn0="}c| +--------+-------------------------------------------------+----------------+
這個是相當于真實的數據抓包展示,數據被轉換成字節碼后是以二進制的形式在TCP緩存區沖傳輸過來。但是二進制太長了,所以一般都是轉換成16進制顯示的,一個表格顯示一個字節的數據,數據由地位到高位由左到右,由上到下進行排列。
其中0x58為TcpProtocol中設置的開始標志,00 00 00 b5為數據的長度,因為是int類型所以占用了四個字節從7b--7d內容為要傳輸的數據內容,結尾的0x63為TcpProtocol設置的結束標志位。
粘包測試
為了模擬粘包,首先將啟動類TcpClient中配置的編碼器的EncoderHandler注釋掉:
bootstrap.handler(newChannelInitializer(){ @Override protectedvoidinitChannel(Channelch)throwsException{ ch.pipeline().addLast("logging",newLoggingHandler("DEBUG")); //ch.pipeline().addLast(newEncoderHandler());因為需要在byteBuf中手動模擬粘包的場景 ch.pipeline().addLast(newEchoHandler()); } });
然后在發送的時候故意將三幀的數據,放在一個包中就行發送,在EchoHanlder做如下修改:
publicvoidchannelActive(ChannelHandlerContextctx)throwsException{ Useruser=newUser(); user.setBirthday(newDate()); user.setUID(UUID.randomUUID().toString()); user.setName("冉鵬峰"); user.setAge(24); DTObjectdtObject=newDTObject(); dtObject.setClassName(user.getClass().getName()); dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user)); TcpProtocoltcpProtocol=newTcpProtocol(); byte[]objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject); tcpProtocol.setLen(objectBytes.length); tcpProtocol.setData(objectBytes); ByteBufbuffer=ctx.alloc().buffer(); buffer.writeByte(tcpProtocol.getHeader()); buffer.writeInt(tcpProtocol.getLen()); buffer.writeBytes(tcpProtocol.getData()); buffer.writeByte(tcpProtocol.getTail()); //模擬粘包的第二幀數據 buffer.writeByte(tcpProtocol.getHeader()); buffer.writeInt(tcpProtocol.getLen()); buffer.writeBytes(tcpProtocol.getData()); buffer.writeByte(tcpProtocol.getTail()); //模擬粘包的第三幀數據 buffer.writeByte(tcpProtocol.getHeader()); buffer.writeInt(tcpProtocol.getLen()); buffer.writeBytes(tcpProtocol.getData()); buffer.writeByte(tcpProtocol.getTail()); ctx.write(buffer); ctx.flush(); }
運行結果:
2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-14 1651 DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="} 2019-01-14 1651 INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='81de59ae-3482-4d1a-bc43-7cc12f925e51',birthday=MonJan140400CST2019} 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-14 1651 DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="} 2019-01-14 1651 INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='81de59ae-3482-4d1a-bc43-7cc12f925e51',birthday=MonJan140400CST2019} 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-141651DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-14 1651 DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="} 2019-01-14 1651 INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='81de59ae-3482-4d1a-bc43-7cc12f925e51',birthday=MonJan140400CST2019}
服務器成功解析出來了三幀的數據,BusinessHandler的channelRead方法被調用了三次。
而抓到的數據包也確實是模擬的三幀數據黏在一個包中:
+-------------------------------------------------+ |0123456789abcdef| +--------+-------------------------------------------------+----------------+ |00000000|58000000b57b22636c6173734e616d65|X....{"className| |00000010|223a22706f6a6f2e55736572222c226f|":"pojo.User","o| |00000020|626a656374223a2265794a755957316c|bject":"eyJuYW1l| |00000030|496a6f693559614a36626d5035624f77|Ijoi5YaJ6bmP5bOw| |00000040|496977695957646c496a6f794e437769|IiwiYWdlIjoyNCwi| |00000050|596d6c796447686b59586b694f694979|YmlydGhkYXkiOiIy| |00000060|4d4445354c7a41784c7a453049444130|MDE5LzAxLzE0IDA0| |00000070|4f6a51304f6a45304969776964576c6b|OjQ0OjE0IiwidWlk| |00000080|496a6f694f44466b5a54553559575574|IjoiODFkZTU5YWUt| |00000090|4d7a51344d6930305a4446684c574a6a|MzQ4Mi00ZDFhLWJj| |000000a0|4e444d744e324e6a4d544a6d4f544931|NDMtN2NjMTJmOTI1| |000000b0|5a545578496e303d227d【63】58000000b5|ZTUxIn0="}cX....| |000000c0|7b22636c6173734e616d65223a22706f|{"className":"po| |000000d0|6a6f2e55736572222c226f626a656374|jo.User","object| |000000e0|223a2265794a755957316c496a6f6935|":"eyJuYW1lIjoi5| |000000f0|59614a36626d5035624f774969776959|YaJ6bmP5bOwIiwiY| |00000100|57646c496a6f794e437769596d6c7964|WdlIjoyNCwiYmlyd| |00000110|47686b59586b694f6949794d4445354c|GhkYXkiOiIyMDE5L| |00000120|7a41784c7a4530494441304f6a51304f|zAxLzE0IDA0OjQ0O| |00000130|6a45304969776964576c6b496a6f694f|jE0IiwidWlkIjoiO| |00000140|44466b5a545535595755744d7a51344d|DFkZTU5YWUtMzQ4M| |00000150|6930305a4446684c574a6a4e444d744e|i00ZDFhLWJjNDMtN| |00000160|324e6a4d544a6d4f5449315a54557849|2NjMTJmOTI1ZTUxI| |00000170|6e303d227d【63】58000000b57b22636c61|n0="}cX....{"cla| |00000180|73734e616d65223a22706f6a6f2e5573|ssName":"pojo.Us| |00000190|6572222c226f626a656374223a226579|er","object":"ey| |000001a0|4a755957316c496a6f693559614a3662|JuYW1lIjoi5YaJ6b| |000001b0|6d5035624f77496977695957646c496a|mP5bOwIiwiYWdlIj| |000001c0|6f794e437769596d6c796447686b5958|oyNCwiYmlydGhkYX| |000001d0|6b694f6949794d4445354c7a41784c7a|kiOiIyMDE5LzAxLz| |000001e0|4530494441304f6a51304f6a45304969|E0IDA0OjQ0OjE0Ii| |000001f0|776964576c6b496a6f694f44466b5a54|widWlkIjoiODFkZT| |00000200|5535595755744d7a51344d6930305a44|U5YWUtMzQ4Mi00ZD| |00000210|46684c574a6a4e444d744e324e6a4d54|FhLWJjNDMtN2NjMT| |00000220|4a6d4f5449315a545578496e303d227d|JmOTI1ZTUxIn0="}| |00000230|【63】|c| +--------+-------------------------------------------------+----------------+
可以看到確實存在三個尾巴【63】
在netty4.x版本中,粘包問題確實被netty的ByteToMessageDecoder中的CallDecode方法中給處理掉了。
拆包問題
這次還是將TcpClient中的編碼器EncoderHandler注釋掉,然后在EchoHandler的channelActive中模擬數據的拆包問題:
publicvoidchannelActive(ChannelHandlerContextctx)throwsException{ Useruser=newUser(); user.setBirthday(newDate()); user.setUID(UUID.randomUUID().toString()); user.setName("冉鵬峰"); user.setAge(24); DTObjectdtObject=newDTObject(); dtObject.setClassName(user.getClass().getName()); dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user)); TcpProtocoltcpProtocol=newTcpProtocol(); byte[]objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject); tcpProtocol.setLen(objectBytes.length); tcpProtocol.setData(objectBytes); ByteBufbuffer=ctx.alloc().buffer(); buffer.writeByte(tcpProtocol.getHeader()); buffer.writeInt(tcpProtocol.getLen()); buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//只發送二分之一的數據包 //模擬拆包 ctx.write(buffer); ctx.flush(); Thread.sleep(3000);//模擬網絡延時 buffer=ctx.alloc().buffer(); buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen()));//將剩下的二分之和尾巴發送過去 buffer.writeByte(tcpProtocol.getTail()); ctx.write(buffer); ctx.flush(); }
運行結果:
首先是客戶端這邊:
2019-01-141733DEBUG[DEBUG][id:0x3b8cbbbb,L:/127.0.0.1:51138-R:/127.0.0.1:8777]WRITE:95B +-------------------------------------------------+ |0123456789abcdef| +--------+-------------------------------------------------+----------------+ |00000000|58000000b57b22636c6173734e616d65|X....{"className| |00000010|223a22706f6a6f2e55736572222c226f|":"pojo.User","o| |00000020|626a656374223a2265794a755957316c|bject":"eyJuYW1l| |00000030|496a6f693559614a36626d5035624f77|Ijoi5YaJ6bmP5bOw| |00000040|496977695957646c496a6f794e437769|IiwiYWdlIjoyNCwi| |00000050|596d6c796447686b59586b694f6949|YmlydGhkYXkiOiI| +--------+-------------------------------------------------+----------------+ 2019-01-141733DEBUG[DEBUG][id:0x3b8cbbbb,L:/127.0.0.1:51138-R:/127.0.0.1:8777]FLUSH 2019-01-141736DEBUG[DEBUG][id:0x3b8cbbbb,L:/127.0.0.1:51138-R:/127.0.0.1:8777]WRITE:92B +-------------------------------------------------+ |0123456789abcdef| +--------+-------------------------------------------------+----------------+ |00000000|794d4445354c7a41784c7a4530494441|yMDE5LzAxLzE0IDA| |00000010|314f6a41344f6a45304969776964576c|1OjA4OjE0IiwidWl| |00000020|6b496a6f694f5745795a6a49354d6d4d|kIjoiOWEyZjI5MmM| |00000030|744d6a4d354f4330305a6a6b774c5746|tMjM5OC00ZjkwLWF| |00000040|6b5a5759745a6d466c4e44457a5a6a55|kZWYtZmFlNDEzZjU| |00000050|354e324533496e303d227d63|5N2E3In0="}c| +--------+-------------------------------------------------+----------------+ 2019-01-141736DEBUG[DEBUG][id:0x3b8cbbbb,L:/127.0.0.1:51138-R:/127.0.0.1:8777]FLUSH
確實是將數據分成兩包發送出去了
再看看服務端的輸出日志:
2019-01-141733DEBUG[DEBUG][id:0x8e5811b3,L:/127.0.0.1:8777-R:/127.0.0.1:51138]RECEIVED:95B +-------------------------------------------------+ |0123456789abcdef| +--------+-------------------------------------------------+----------------+ |00000000|58000000b57b22636c6173734e616d65|X....{"className| |00000010|223a22706f6a6f2e55736572222c226f|":"pojo.User","o| |00000020|626a656374223a2265794a755957316c|bject":"eyJuYW1l| |00000030|496a6f693559614a36626d5035624f77|Ijoi5YaJ6bmP5bOw| |00000040|496977695957646c496a6f794e437769|IiwiYWdlIjoyNCwi| |00000050|596d6c796447686b59586b694f6949|YmlydGhkYXkiOiI| +--------+-------------------------------------------------+----------------+ 2019-01-141733DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141733DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-14 1733 DEBUG [org.wisdom.server.decoder.DecoderHandler]數據長度不夠,數據協議len長度為:181,數據包實際可讀內容為:90正在等待處理拆包…… 2019-01-141736DEBUG[DEBUG][id:0x8e5811b3,L:/127.0.0.1:8777-R:/127.0.0.1:51138]RECEIVED:92B +-------------------------------------------------+ |0123456789abcdef| +--------+-------------------------------------------------+----------------+ |00000000|794d4445354c7a41784c7a4530494441|yMDE5LzAxLzE0IDA| |00000010|314f6a41344f6a45304969776964576c|1OjA4OjE0IiwidWl| |00000020|6b496a6f694f5745795a6a49354d6d4d|kIjoiOWEyZjI5MmM| |00000030|744d6a4d354f4330305a6a6b774c5746|tMjM5OC00ZjkwLWF| |00000040|6b5a5759745a6d466c4e44457a5a6a55|kZWYtZmFlNDEzZjU| |00000050|354e324533496e303d227d63|5N2E3In0="}c| +--------+-------------------------------------------------+----------------+ 2019-01-141736DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141736DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-141736DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-14 1736 DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjA4OjE0IiwidWlkIjoiOWEyZjI5MmMtMjM5OC00ZjkwLWFkZWYtZmFlNDEzZjU5N2E3In0="} 2019-01-14 1736 INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='9a2f292c-2398-4f90-adef-fae413f597a7',birthday=MonJan140500CST2019}
在第一包數據,判斷到bytebuf中的可讀內容不夠的時候,終止解碼,并且從父類的callDecode中的while循環break出去,在父類的channelRead中等待下一包數據到來的時候將兩包數據合并起來再次decode解碼。
最后測試下同時出現拆包、粘包的場景
還是將TcpClient中的編碼器EncoderHandler注釋掉,然后在EchoHandler的ChannelActive方法:
publicvoidchannelActive(ChannelHandlerContextctx)throwsException{ Useruser=newUser(); user.setBirthday(newDate()); user.setUID(UUID.randomUUID().toString()); user.setName("冉鵬峰"); user.setAge(24); DTObjectdtObject=newDTObject(); dtObject.setClassName(user.getClass().getName()); dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user)); TcpProtocoltcpProtocol=newTcpProtocol(); byte[]objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject); tcpProtocol.setLen(objectBytes.length); tcpProtocol.setData(objectBytes); ByteBufbuffer=ctx.alloc().buffer(); buffer.writeByte(tcpProtocol.getHeader()); buffer.writeInt(tcpProtocol.getLen()); buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//拆包,只發送一半的數據 ctx.write(buffer); ctx.flush(); Thread.sleep(3000); buffer=ctx.alloc().buffer(); buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen()));//拆包發送剩余的一半數據 buffer.writeByte(tcpProtocol.getTail()); //模擬粘包的第二幀數據 buffer.writeByte(tcpProtocol.getHeader()); buffer.writeInt(tcpProtocol.getLen()); buffer.writeBytes(tcpProtocol.getData()); buffer.writeByte(tcpProtocol.getTail()); //模擬粘包的第三幀數據 buffer.writeByte(tcpProtocol.getHeader()); buffer.writeInt(tcpProtocol.getLen()); buffer.writeBytes(tcpProtocol.getData()); buffer.writeByte(tcpProtocol.getTail()); ctx.write(buffer); ctx.flush(); }
最后直接查看服務端的輸出結果:
2019-01-141725DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141725DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-14 1725 DEBUG [org.wisdom.server.decoder.DecoderHandler]數據長度不夠,數據協議len長度為:181,數據包實際可讀內容為:90正在等待處理拆包…… 2019-01-141728DEBUG[DEBUG][id:0xc46234aa,L:/127.0.0.1:8777-R:/127.0.0.1:51466]RECEIVED:466B 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-14 1728 DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="} 2019-01-14 1728 INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='816f86d2-40a1-404d-801b-ff578112a61f',birthday=MonJan140500CST2019} 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-14 1728 DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="} 2019-01-14 1728 INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='816f86d2-40a1-404d-801b-ff578112a61f',birthday=MonJan140500CST2019} 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]開始解碼數據…… 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]數據開頭格式正確 2019-01-141728DEBUG[org.wisdom.server.decoder.DecoderHandler]數據解碼成功 2019-01-14 1728 DEBUG [org.wisdom.server.business.BusinessHandler]解碼后的字節碼:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="} 2019-01-14 1728 INFO [org.wisdom.server.business.BusinessHandler]收到實體對象:User{name='冉鵬峰',age=24,UID='816f86d2-40a1-404d-801b-ff578112a61f',birthday=MonJan140500CST2019}
總結
對于拆包、粘包只要配合netty的設計原則去實現代碼,就能愉快且輕松的解決了。本例雖然通過DTObject包裝了數據,避免解碼時每增加一種對象類型,就要新增一個if判斷的尷尬。但是仍然無法處理傳輸List、Map時候的場景。
審核編輯:湯梓紅
-
服務器
+關注
關注
12文章
9304瀏覽量
86066 -
JAVA
+關注
關注
19文章
2975瀏覽量
105149 -
TCP
+關注
關注
8文章
1378瀏覽量
79302
原文標題:實現一款高可用的 TCP 數據傳輸服務器(Java版)
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論