完全なコード:https://gitee.com/firewolf/java-io/tree/master/java-io/netty-02-tcppacket
問題を開梱、TCPスティックパッケージ/
TCPは、合意の「流れ」で、基本的なTCPのデジタル境界線の束ではない、いわゆるフローは、作品への接続ではない、と何の境界、彼らはサービス層の意味を知らないので、それはTCPバッファの実際の状況に基づいて行われます分割は、ビューのビジネス層で、その結果、完全なTCPパケットが大型パッケージを形成する小さな複数のパケットをパッケージ化することも、複数のパケットに可能に分割されてもよいです。
スティックパッケージおよびアンパックは、以下の発生する可能性があります
バイトの数は、サーバが固定されていない読み取るので、次のような場合が起こり得るように、クライアントがサーバに二つのパケットD1およびD2を送信すると仮定する。
- 2つの完全なパケットを読むために二度、サーバー、それぞれD1およびD2、スティックのパッケージと発生しません開梱。
- サーバーは、2つのデータパケットを読み取るには、D1およびD2がくっついて、スティックのパッケージが発生しました。
- パッケージの一部、別のパケットD2 D2_2の部分の第2の読み出し - サーバ倍の二つは最初の完全なパケットD1およびD2 D2_1を読み、データ・パケットを読み取ります。パッケージをスティックと発生したアンパック。
- サーバ回二つのパケット、最初の読み出しD1_1読み取り - D1パケット部分、パケットの完全なパッケージD1_2 D1及びD2の第二読み取り、およびアンパックスティックパッケージの発生の別の部分を、
- また、それは開梱パッケージを固執繰り返しも発生する可能性があります。
第二に、TCPスティックパッケージの理由は、/アンパック問題が生じます
理由は三つの主要な問題があります。
- 書き込みバイトサイズ書かれたアプリケーションソケットは、送信バッファのサイズよりも大きいです。
- TCPセグメントサイズMSSのための
- MTU実行IPフラグメンテーションより大きいイーサネットペイロード
第三に、ソリューション戦略スティックパッケージTCP /開梱問題
- 不十分な、スペースが満たされる場合、各パケットの長さなどの固定長メッセージは、200です。
- 増加の終わりにキャリッジ・リターンは、TCPプロトコルのように、分割されました。
- 終わりに、特殊文字のセグメンテーションを追加し、キャリッジリターンラインは特殊なケースです。
- メッセージヘッダとメッセージ本体、全長フィールドを示すメッセージを含むメッセージ・ヘッダーにメッセージ。
- より複雑なアプリケーション層プロトコルを使用して、
四、TCPスティックパッケージ/開梱問題がデモ
这里简单的写一个程序,功能是客户端连接服务端之后,向服务端发送20个字符串,服务端收到后进行显示:
(一)服务端
package com.firewolf.java.io.netty.packagee.paste.origin;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.util.HashMap;
import java.util.Map;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloServer {
public HelloServer(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MessageServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("启动服务端监听端口:" + port);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
class MessageServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String message = new String(bytes);
System.out.println("有客户端消息:" + message);
buf.release();
}
}
public static void main(String[] args) {
new HelloServer(9999);
}
}
(二)客户端
package com.firewolf.java.io.netty.packagee.paste.origin;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloClient {
public HelloClient(String host, int port) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MessageClientHandler());
}
});
ChannelFuture f = bootstrap.connect(new InetSocketAddress(host, port)).sync();
System.out.println("连接服务器成功-----");
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
class MessageClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//遍历写出100个字符串
for (int i = 0; i < 20; i++) {
String message = "helllo," + i + System.getProperty("line.separator");
ByteBuf buf = Unpooled.buffer(message.getBytes().length);
buf.writeBytes(message.getBytes());
ctx.writeAndFlush(buf);
}
}
}
public static void main(String[] args) {
new HelloClient("127.0.0.1", 9999);
}
}
启动服务端,再启动客户端,服务端打印信息如下:
有客户端消息:helllo,0
helllo,1
helllo,2
helllo,3
helllo,4
helllo,5
helllo,6
helllo,7
helllo,8
helllo,9
helllo,10
helllo,11
helllo,12
helllo,13
helllo,14
helllo,15
helllo,16
helllo,17
helllo,18
helllo,19
我们会发现,实际上, 服务端一次接受了全部字符串,也就是说,这些包并没有正常的接受到,而是发生了粘包。
五、Netty对粘包/拆包问题解决
为了解决TCP拆包/粘包导致的半读包问题,Netty默认提供了很多编码解码器用于处理半包。
(一)LineBasedFrameDecoder
LineBasedFrameDecoder会对包进行识别,根据包里面的回车(13)换行(10)符进行切割。
1. 服务端代码
package com.firewolf.java.io.netty.packagee.paste.solve;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.util.HashMap;
import java.util.Map;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloServerSolve {
public HelloServerSolve(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
//添加编码器
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder())
.addLast(new MessageServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("启动服务端监听端口:" + port);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
class MessageServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//由于StringDecoder的功劳,接受消息的时候,我们可以直接转成String
String message = (String) msg;
System.out.println("有客户端消息:" + message);
}
}
public static void main(String[] args) {
new HelloServerSolve(9999);
}
}
服务端有两处改动
- 给启动类BootStrap添加了消息解码器;
- 接受消息的时候直接使用String接受,这个是由于添加了StringDecoder具有的功能。
2.客户端代码
package com.firewolf.java.io.netty.packagee.paste.solve;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.net.InetSocketAddress;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloClientSolve {
public HelloClientSolve(String host, int port) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MessageClientHandler());
}
});
ChannelFuture f = bootstrap.connect(new InetSocketAddress(host, port)).sync();
System.out.println("连接服务器成功-----");
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
class MessageClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//遍历写出100个字符串
for (int i = 0; i < 20; i++) {
String message = "helllo," + i + System.getProperty("line.separator");
ByteBuf buf = Unpooled.buffer(message.getBytes().length);
buf.writeBytes(message.getBytes());
ctx.writeAndFlush(buf);
}
}
}
public static void main(String[] args) {
new HelloClientSolve("127.0.0.1", 9999);
}
}
我们可以看到,客户端没有任何变化。
3.查看效果
再次启动后,打印内容如下:
有客户端消息:helllo,0
有客户端消息:helllo,1
有客户端消息:helllo,2
有客户端消息:helllo,3
有客户端消息:helllo,4
有客户端消息:helllo,5
有客户端消息:helllo,6
有客户端消息:helllo,7
有客户端消息:helllo,8
有客户端消息:helllo,9
有客户端消息:helllo,10
有客户端消息:helllo,11
有客户端消息:helllo,12
有客户端消息:helllo,13
有客户端消息:helllo,14
有客户端消息:helllo,15
有客户端消息:helllo,16
有客户端消息:helllo,17
有客户端消息:helllo,18
有客户端消息:helllo,19
可以看到,这些消息是分开了的,没有了粘包和拆包问题。
LineBasedFrameDecoder:以回车换行符作为对包进行切割;
StringDecoder:把ByteBuf转换成字符串
一般情况下,StringDecoder和LineBasedFrameDecoder会配合使用。
(二)DelimiterBasedFrameDecoder
其实DelimiterBasedFrameDecoder的功能和LineBasedFrameDecoder类似,只不过会比LineBasedFrameDecoder更加强大,支持我们自己指定用来切割的字符(串),使用方法和LineBasedFrameDecoder几乎是一模一样,这里只是简单的贴出部分代码:
1.服务端添加解码器
这里使用_$进行分割:
socketChannel.pipeline()
//添加编码器
.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("_$".getBytes())))
2.发送消息的时候,带上_$结尾
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//遍历写出100个字符串
for (int i = 0; i < 20; i++) {
String message = "helllo," + i + "_$";
ByteBuf buf = Unpooled.buffer(message.getBytes().length);
buf.writeBytes(message.getBytes());
ctx.writeAndFlush(buf);
}
}
(三)FixedLengthFrameDecoder
指定された長さに応じてメッセージを復号することができるFixedLengthFrameDecoder固定長復号器。
ここで再び、単に変化(尊敬のDelimiterBasedFrameDecoder付き)コードを置きます。
1.サーバーのデコーダを追加します。
socketChannel.pipeline()
//添加编码器
.addLast(new FixedLengthFrameDecoder(20))
ここで提供される長さ20
2.クライアント
もはや変更しない、掲載さありません
結果は以下の通りであります:
有客户端消息:helllo,0_$helllo,1_$
有客户端消息:helllo,2_$helllo,3_$
有客户端消息:helllo,4_$helllo,5_$
有客户端消息:helllo,6_$helllo,7_$
有客户端消息:helllo,8_$helllo,9_$
有客户端消息:helllo,10_$helllo,11
有客户端消息:_$helllo,12_$helllo,
有客户端消息:13_$helllo,14_$helll
有客户端消息:o,15_$helllo,16_$hel
有客户端消息:llo,17_$helllo,18_$h
私たちは、結果の文字列の長さが20で、見ることができ、かつ20未満の長さに起因する最後のものは、それが表示されません。
VIの概要
いくつかのデコーダのソースコードの上に、自分で読んで、私たちはまた、継承によって、独自のデコーダを実装することができ、すなわち、これらのデコーダは、元の取得ByteBufが処理され、ByteToMessageDecoderを継承していることがわかりますByteToMessageDecoder 。