分隔符和定长解码器的应用
TCP以流的方式进行数据传输。上层的应用协议为了对消息进行区分,往往采用如下4种方式:
- 消息长度固定,累计读取到长度总和为定长LEN的报文后,就认定读取到了一个完成的消息;将计数器置位,重新开始下一个数据报;
- 将回车换行符作为消息的结束符。例如FTP协议,这种方式在文本协议中应用比较广泛;
- 将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符;
- 通过在消息头中定义长度字段来表示消息的总长度;
Netty对上面的4种应用做了统一的抽象,提供了四种解码器来解决对应的问题,使用起来非常方便。有了这些解码器,用户便不需要自己对读取报文进行人工解码,也不需要考虑TCP粘包和拆包。
前面我们学习了LineBasedFrameDecoder解决TCP粘包问题,在这一章我们继续学习另外两个——DelimiterBasedFrameDecoder和FixedLengthFrameDecoder,前者可以完成以分隔符做结束标志的消息的解码,后者可以自动完成对定长消息的解码,它们都能解决TCP粘包/拆包导致的读半包问题。
一、DelimiterBasedFrameDecoder应用开发
通过对DelimiterBasedFrameDecoder的使用,我们可以自动完成以分隔符作为码流结束标识的消息的解码,下面通过经典的Echo服务为例进行演示。EchoServer接收到EchoClient的请求消息后,将其打印出来,然后将原始消息返回个客户端,消息以”$_“作为分隔符。
1.1 服务端开发
EchoServer代码:
public class EchoServer {
public void bind(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoServerHandler());
}
});
//绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
if(args!=null && args.length>0){
try{
port = Integer.parseInt(args[0]);
}catch (NumberFormatException e){
//
}
}
new EchoServer().bind(port);
}
}
EchoServerHandler代码:
public class EchoServerHandler extends ChannelHandlerAdapter {
int count = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("The is " + ++count + "times receive client:[" + body + "]");
body += "$_";
ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(echo);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
1.2客户端开发
EchoClient代码:
public class EchoCilent {
public void connect(int port, String host) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoClientHandler());
}
});
//发起异步连接
ChannelFuture f = b.connect(host, port).sync();
//等待客户端链路关闭
f.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
try{
if(args!=null && args.length>0){
port = Integer.parseInt(args[0]);
}
}catch(NumberFormatException e){
}
new EchoCilent().connect(port,"127.0.0.1");
}
}
EchoClientHandler代码:
public class EchoClientHandler extends ChannelHandlerAdapter {
private int count = 0;
static final String ECHO_REQ = "Hi,Welcome to Netty.$_";
public EchoClientHandler() {
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("This is " + ++count + "times receive server : [" + msg + "]");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
1.3运行代码
二、FixedLengthFrameDecoder应用开发
这是一个固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者吧u需要考虑TCP的粘包/拆包问题。非常实用。
2.1 服务端开发
在服务端的ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再一次增加字符串解码器和EchoServerHandler,代码如下:
EchoServer代码:
public class EchoServer {
public void bind(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,100)
.childHandler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoServerHandler());
}
});
//绑定端口
ChannelFuture f = bootstrap.bind(port).sync();
//等待关闭
f.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
try {
if(args!=null && args.length>0){
port = Integer.parseInt(args[0]);
}
}catch (NumberFormatException e){
}
new EchoServer().bind(port);
}
}
EchoServerHandler代码:
public class EchoServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("Receive client : [" + msg + "]");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
2.2利用telnet 命令模拟客户端连接
win+r打开终端:
输入cmd然后回车,就能打开终端:
在终端输入:telnet localhost 8080
,然后回车:
进入这个页面,说明连接成功了,我们按下·ctrl+l
快捷键打开telnet回显功能,否则你看不到自己输入的东西:
出现Microsoft Telnet
,我们直接回车:
重新回到空白的页面,然后输入信息,此刻面板出现我们输入的信息,无论我们输入多少内容,服务端都会接收,当长度满足定长解码器设置的长度,那么服务端就会将此消息当成一次消息:
服务端打印消息:
后面的信息省略了,因为超出了我们设置的20的长度。