吴忠躺衫网络科技有限公司

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

實現一款高可用的TCP數據傳輸服務器(Java版)

Android編程精選 ? 來源:Java知音 ? 作者:Java知音 ? 2022-11-15 11:23 ? 次閱讀

1.netty能做什么

首先netty是一款高性能、封裝性良好且靈活、基于NIO(真·非阻塞IO)的開源框架。可以用來手寫web服務器、TCP服務器等,支持的協議豐富,如:常用的HTTP/HTTPS/WEBSOCKET,并且提供的大量的方法,十分靈活,可以根據自己的需求量身DIV一款服務器。

用netty編寫TCP的服務器/客戶端

1.可以自己設計數據傳輸協議如下面這樣:

8671b226-627e-11ed-8abf-dac502259ad0.png

2.可以自定義編碼規則和解碼規則

3.可以自定義客戶端與服務端的數據交互細節,處理socket流攻擊、TCP的粘包和拆包問題

2.Quick Start

創建一個普通的maven項目,不依賴任何的三方web服務器,用main方法執行即可。

8682f34c-627e-11ed-8abf-dac502259ad0.png

加入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,Listout)throwsException{

if(in.readableBytes()>MIN_DATA_LEN){
logger.debug("開始解碼數據……");
//標記讀操作的指針
in.markReaderIndex();
byteheader=in.readByte();
if(header==PROTOCOL_HEADER){
logger.debug("數據開頭格式正確");
//讀取字節數據的長度
intlen=in.readInt();
//數據可讀長度必須要大于len,因為結尾還有一字節的解釋標志位
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;
}
byte[]data=newbyte[len];
in.readBytes(data);//讀取核心的數據
bytetail=in.readByte();
if(tail==PROTOCOL_TAIL){
logger.debug("數據解碼成功");
out.add(data);
//如果out有值,且in仍然可讀,將繼續調用decode方法再次解碼in中的內容,以此解決粘包問題
}else{
logger.debug(String.format("數據解碼協議結束標志位:%1$d [錯誤!],期待的結束標志位是:%2$d",tail,PROTOCOL_TAIL));
return;
}
}else{
logger.debug("開頭不對,可能不是期待的客服端發送的數,將自動略過這一個字節");
}
}else{
logger.debug("數據長度不符合要求,期待最小長度是:"+MIN_DATA_LEN+"字節");
return;
}

}
}

首先是黏包問題:

如圖,正常的數據傳輸應該是像數據A那樣,一包就是一個完整的數據,但也有不正常的情況,比如一包數據包含多個數據。而在ByteToMessageDecoder會默認把二進制的字節碼放在byteBuf中,因此我們在code的時候要知道會有這樣的場景。

8697411c-627e-11ed-8abf-dac502259ad0.png

而粘包問題實際上不需要我們去解決,下面是ByteToMessageDecoder的源碼,callDecode中回調我們手寫解碼器的decode方法。

protectedvoidcallDecode(ChannelHandlerContextctx,ByteBufin,Listout){
try{
while(in.isReadable()){//buf中是否還有數據
intoutSize=out.size();//標記out的size,解析成功的數據會添加的out中
if(outSize>0){
fireChannelRead(ctx,out,outSize);//這個是回調業務handler的channelRead方法
out.clear();
if(ctx.isRemoved()){
break;
}
outSize=0;//清空了out,將標記size清零
}
intoldInputLength=in.readableBytes();//這里開始準備調用decode方法,標記了解碼前的可讀內容
decode(ctx,in,out);//對應DecoderHandler中的decode方法
if(ctx.isRemoved()){
break;
}

if(outSize==out.size()){//相等說明,并沒有解析出來新的object到out中
if(oldInputLength==in.readableBytes()){//這里很重要,若相等說明decode中沒有讀取任何內容出來,這里一般是發生拆包后,將ByteBuf的指針手動重置。重置后從這個方法break出來。讓ByteToMessageDecoder去處理拆包問題。這里就體現了要按照netty的設計原則來寫代碼
break;
}else{
continue;//這里直接continue,是考慮讓開發者去跳過某些字節,比如收到了socket攻擊時,數據不按照協議體來的時候,就直接跳過這些字節
}
}

if(oldInputLength==in.readableBytes()){//這種情況屬于,沒有按照netty的設計原則來。要么是decode中沒有任何邏輯代碼,要么是在out中添加了內容后,調用了byteBuf的resetReaderIndex重置的讀操作的指針
thrownewDecoderException(
StringUtil.simpleClassName(getClass())+
".decode()didnotreadanythingbutdecodedamessage.");
}

if(isSingleDecode()){//默認為false,用來設置只解析一條數據
break;
}
//這里結束后,繼續wile循環,因為bytebuf仍然有可讀的內容,將會繼續調用decode方法解析bytebuf中的字節碼,以此解決了粘包問題
}
}catch(DecoderExceptione){
throwe;
}catch(Throwablecause){
thrownewDecoderException(cause);
}
}

綜合上面的源碼分析后,我們發現:decode方法在while循環中,也就是bytebuf只要有內容就會一直調用decode方法進行解碼操作,因此在解決粘包問題時,只需要按照正常流程來就行了,解析協議開頭、數據字節、結束標志后將數據放入到out這個list中即可。后面將會有數據進行粘包測試。

拆包問題

有時候,我們接收到的數據是不完整的,一個包的數據被拆成了很多份被后再發送出去。這種情況有可能是數據太大,被分割成很多份發送出去。比如數據包B被拆成兩份進行發送:

86a9e448-627e-11ed-8abf-dac502259ad0.png

拆包問題,同樣在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
    TCP
    +關注

    關注

    8

    文章

    1378

    瀏覽量

    79302

原文標題:實現一款高可用的 TCP 數據傳輸服務器(Java版)

文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    基于LABVIEW的TCP/IP實現兩臺電腦間的數據傳輸

    本帖最后由 eehome 于 2013-1-5 09:55 編輯 本人初學者,用LABVIEW實現TCP/IP間計算機數據傳輸,但是服務器端始終不能接收,用探針監視結果為未執行,
    發表于 05-08 18:05

    WIFI點對點的數據傳輸定需要服務器嗎?

    我想做氧化碳的監控裝置,濃度過高是會給我的手機報警,我也可以通過手機獲取當前氧化碳數據,可是聽別人說做點對點的數據傳輸還需要
    發表于 11-14 16:56

    請問GPRS如何跟服務器進行雙向的網絡數據傳輸

    請問GPRS如何跟服務器進行雙向的網絡數據傳輸的。我的意思就是GPRS如何發送數據服務器,那么GPRS又是如何接受到服務器傳過來的指令呢?
    發表于 01-16 06:35

    請問怎么通過ESP8266WIFI模塊將32的數據傳輸個云服務器

    想通過ESP8266WIFI模塊將32的數據傳輸個云服務器上,最終實現微信可查看上傳的數據求高人指點
    發表于 06-12 04:35

    如何去實現stm32f107vc lwip tcp客戶端服務器數據傳輸

    怎么去建立LWIP客戶端模式呢?如何去實現stm32f107vc lwip tcp客戶端服務器數據傳輸呢?
    發表于 11-04 06:54

    如何通過ESP8266將STM32串口數據傳輸到MQTT服務器

    如何通過ESP8266將STM32串口數據傳輸到MQTT服務器
    發表于 12-13 06:19

    如何去實現COM口與TCP socket之間的數據傳輸

    數據傳輸的原理是什么?如何去實現COM口與TCP socket之間的數據傳輸呢?
    發表于 02-22 07:44

    tcp ip 數據傳輸

    tcp ip 數據傳輸 現有的許多具有串口管理功能的設備不能進行聯網的管理和數據存取,我們可以利用先進的TCP/IP技術和管理方式對
    發表于 12-25 12:59 ?1086次閱讀

    JAVA教程之TCP服務器

    JAVA教程之TCP服務器端,很好的JAVA的資料,快來學習吧
    發表于 04-11 17:28 ?10次下載

    一款使用GPRS進行無線數據傳輸的嵌入式終端

    模塊簡介 該DTU嵌入式模塊是一款使用GPRS 進行 無線數據傳輸的嵌入式終端 ,體積小、接口簡單,用戶能方便的集成到自己的主板設備上去。支持PPP、TCP、UDP、ICMP等眾多復雜網絡協議和多
    的頭像 發表于 04-08 16:45 ?8367次閱讀

    tcp_ip 協議講座:介紹數據傳輸

    介紹了tcp協議:數據傳輸的問題(交互式數據傳輸,批量數據傳輸,流量控制,擁塞避免)
    的頭像 發表于 07-03 11:05 ?3485次閱讀
    <b class='flag-5'>tcp</b>_ip 協議講座:介紹<b class='flag-5'>數據傳輸</b>

    protues仿真串口數據上傳至Web服務器(COM口 與 TCP socket之間數據傳輸)

    protues仿真串口數據上傳至Web服務器(COM口 與 TCP socket之間數據傳輸)應用場景1.protues仿真
    發表于 12-29 18:56 ?5次下載
    protues仿真<b class='flag-5'>器</b>串口<b class='flag-5'>數據</b>上傳至Web<b class='flag-5'>服務器</b>(COM口 與 <b class='flag-5'>TCP</b> socket之間<b class='flag-5'>數據傳輸</b>)

    工業控制領域基于TCP/IP的數據傳輸方案

    電子發燒友網站提供《工業控制領域基于TCP/IP的數據傳輸方案.pdf》資料免費下載
    發表于 11-16 10:52 ?0次下載
    工業控制領域基于<b class='flag-5'>TCP</b>/IP的<b class='flag-5'>數據傳輸</b>方案

    香港CPU服務器如何處理不同類型的數據傳輸

    香港CPU服務器處理不同類型的數據傳輸通常涉及以下幾個方面: 1、網絡配置:服務器需要有適當的網絡配置,以支持不同類型的數據傳輸協議,如TCP
    的頭像 發表于 05-21 17:23 ?398次閱讀

    ptp對實時數據傳輸的影響

    在現代通信技術中,點對點(P2P)網絡已經成為數據傳輸種重要方式。P2P網絡允許網絡中的每個節點既可以作為客戶端也可以作為服務器,直接進行數據交換。這種去中心化的網絡結構對于實時
    的頭像 發表于 12-29 09:53 ?207次閱讀
    百家乐官网赌博技巧网| 大发888代充| 大发888娱乐场怎样下载| 老k娱乐城注册| 百家乐官网分析资料| 百胜百家乐官网软件| 尊龙百家乐官网娱乐平台| 百家乐五式缆投法| 大上海百家乐娱乐城| 大连娱网棋牌大厅| 白朗县| 百家乐官网的注码技巧| 视频百家乐官网平台| 百家乐五湖四海娱乐场开户注册| 大发888亚付宝充值| 澳门百家乐官网秘诀| 百家乐官网一般的庄闲比例是多少| 百家乐官网网站那个诚信好| 百家乐官网好不好| 百家乐娱乐网备用网址| 大发888娱乐场手机| 在线百家乐官网| 百家乐官网平注法到65| 百家乐千术手法| 大家旺娱乐| 国美百家乐官网的玩法技巧和规则 | 澳门百家乐官网网上赌博| 百家乐视频双扣游戏| 大发888song58| 百家乐官网平台有什么优势| 真人百家乐试玩账号| 中山水果机定位器| 百家乐官网筹码多少钱| 博九百家乐娱乐城| 澳门顶级赌场金沙| 百家乐官网外套| 最新百家乐的玩法技巧和规则| 淮滨县| 做生意挂什么画招财| 大发888娱乐城 17| 百家乐官网大钱赢小钱|