上篇文章我们看了一个简单的网络通信实例,在通信的过程成由于网络等原因,可能存在粘包的问题,对于粘包问题,处理呢。我们来看一个获取服务器时间的实例,这个实例也许不够恰当,我们只是示范处理粘包问题:
服务端:
package netty.main.time; import java.net.InetSocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; 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.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; import netty.handler.time.TimeServerHandler; /** * * @author donald * 2017年6月21日 * 下午12:48:17 */ public class TimeServer { private static final Logger log = LoggerFactory.getLogger(TimeServer.class); static final boolean SSL = System.getProperty("ssl") != null; private static final String ip = "192.168.31.153"; private static final int port = 10010; public static void main(String[] args) throws Exception { run(); } public static void run() throws Exception { // Configure SSL. final SslContext sslCtx; if (SSL) { SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); } else { sslCtx = null; } /* * EventLoopGroup(多线程事件loop),处理IO操作,这里我们用了两个事件loop * 第一个boss用于处理器监听连接请求,第二个worker用于数据的传输; * 具体线程是多少依赖于事件loop的具体实现 * */ EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //ServerBootstrap,用于配置服务端,一般为ServerSocket通道 ServerBootstrap serverBoot = new ServerBootstrap(); serverBoot.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { //添加通道处理器到通道关联的管道,准确的中文翻译为管道线, 此管道线与Mina中过滤链十分相似, //ChannelInitializer用于配置通道的管道线,ChannelPipeline ChannelPipeline pipeline = ch.pipeline(); if (sslCtx != null) { pipeline.addLast(sslCtx.newHandler(ch.alloc())); } // pipeline.addLast(new LoggingHandler(LogLevel.INFO)); pipeline.addLast(new TimeServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128)//socket监听器连接队列大小、 .childOption(ChannelOption.SO_KEEPALIVE, true); //保活,此配置针对ServerSocket通道接收连接产生的Socket通道 InetSocketAddress inetSocketAddress = new InetSocketAddress(ip,port); // 绑定地址,开始监听 ChannelFuture f = serverBoot.bind(inetSocketAddress).sync(); log.info("=========Server is start========="); //等待,直到ServerSocket关闭 f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
服务端处理器:
package netty.handler.time; import java.nio.charset.Charset; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * * @author donald * 2017年6月21日 * 下午12:48:01 */ public class TimeServerHandler extends ChannelInboundHandlerAdapter { private static final Logger log = LoggerFactory.getLogger(TimeServerHandler.class); private static final String TIME_PROTOCL = "?time"; private static final Charset charsetDecoder= Charset.forName("UTF-8"); /** * 读client通道数据,通道处理器上下文ChannelHandlerContext与Mina的会话很像 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf in = (ByteBuf)msg; String message = (String) in.readCharSequence(in.writerIndex(), charsetDecoder); log.info("===Server reciever message:" +message); if(message.equals(TIME_PROTOCL)){ //通过通道处理器上下文的ByteBufAllocator创建容量至少为8个字节的ByteBuf ByteBuf time = ctx.alloc().buffer(8); time.writeLong(System.currentTimeMillis()); /* 在发送数据时,我们并没有调用nio的ByteBuffer#flip类似的方法,这是由于 为了避免nio忘记flip操作的问题,Netty通过readIndex和writeIndex两个index 表示ByteBuf的相对开始和结束位置;当向ByteBuffer中写数据时,writeIndex将会增长, 而readIndex不变。 */ /* ctx#write,writeAndFlush方法返回一个写结果ChannelFuture, ChannelFuture表示一个IO事件操作,如果想要在ctx写操作后,关闭连接,不可以用如下方式: Channel ch = ...; ch.writeAndFlush(message); ch.close(); 因为Netty的写操作时异步的,上面这种关闭连接方式,有可能在消息没发送完前,连接已经关闭,为了 能在消息发送完毕后再关闭会话,可以通过添加通道结果监听器,在消息发送完时,触发监听器operationComplete 事件。*/ final ChannelFuture cfuture = ctx.writeAndFlush(time); final ChannelHandlerContext ctx_refer = ctx; cfuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { assert cfuture == future; ctx_refer.close(); } }); //上面添加监听器,可以直接使用通道结果监听器内部的CLOSE监听器 //cfuture.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { //异常发生时,关闭连接 cause.printStackTrace(); ctx.close(); } }
客户端:
package netty.main.time; import java.net.InetSocketAddress; import javax.net.ssl.SSLException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import netty.handler.time.TimeClientHandler; /** * * @author donald * 2017年6月21日 * 下午12:48:10 */ public final class TimeClient { private static final Logger log = LoggerFactory.getLogger(TimeClient.class); private static final boolean SSL = System.getProperty("ssl") != null; private static final String ip = System.getProperty("host", "192.168.31.153"); private static final int port = Integer.parseInt(System.getProperty("port", "10010")); public static void main(String[] args) throws Exception { run(); } private static void run() throws SSLException, InterruptedException{ //配置安全套接字上下文 final SslContext sslCtx; if (SSL) { sslCtx = SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); } else { sslCtx = null; } EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //Bootstrap与 ServerBootstrap相似,不同的是Bootstrap用于配置客户端, //一般为Socket通道,或无连接通道 Bootstrap bootstrap = new Bootstrap(); //EventLoopGroup有 boss和worker两组,对于客户端只需要用worker bootstrap.group(workerGroup); bootstrap.channel(NioSocketChannel.class); bootstrap.option(ChannelOption.SO_KEEPALIVE, true); bootstrap.handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //添加安全套接字处理器和通道处理器到 ChannelPipeline pipeline = ch.pipeline(); if (sslCtx != null) { pipeline.addLast(sslCtx.newHandler(ch.alloc(), ip, port)); } // pipeline.addLast(new LoggingHandler(LogLevel.INFO)); pipeline.addLast(new TimeClientHandler()); } }); InetSocketAddress inetSocketAddress = new InetSocketAddress(ip,port); //连接socket地址 ChannelFuture f = bootstrap.connect(inetSocketAddress).sync(); log.info("=========Client is start========="); //等待,直到连接关闭 f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } }
客户端处理器:
package netty.handler.time; import java.nio.charset.Charset; import java.util.Date; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * * @author donald * 2017年6月21日 * 下午12:47:53 */ public class TimeClientHandler extends ChannelInboundHandlerAdapter { private static final Logger log = LoggerFactory.getLogger(TimeClientHandler.class); private static final String TIME_PROTOCL = "?time"; private static final Charset charsetEncoder= Charset.forName("UTF-8"); /** * 在通道连接建立时(准备传输数据)触发 */ @Override public void channelActive(ChannelHandlerContext ctx) { ByteBuf timeReq = ctx.alloc().buffer(5); timeReq.writeCharSequence(TIME_PROTOCL, charsetEncoder); ctx.writeAndFlush(timeReq); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf in = (ByteBuf)msg; long nowTime = in.readLong(); Date nowDay = new Date(nowTime); log.info("===Server Time:" +nowDay.toLocaleString()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
启动服务端与客户端,控制台输出:
服务端:
[INFO ] 2017-07-05 22:50:09 netty.main.time.TimeServer =========Server is start=========
[INFO ] 2017-07-05 22:50:14 netty.handler.time.TimeServerHandler ===Server reciever message:?time
客户端:
[INFO ] 2017-07-05 22:50:14 netty.main.time.TimeClient =========Client is start=========
[INFO ] 2017-07-05 22:50:14 netty.handler.time.TimeClientHandler ===Server Time:2017-7-5 22:50:14
针对粘包问题,我们对上面的实例进行改造:
服务端:
package netty.main.time; import java.net.InetSocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; 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.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; import netty.handler.time.TimeServerHandler2; /** * * @author donald * 2017年6月21日 * 下午12:48:17 */ public class TimeServerForDecoder { private static final Logger log = LoggerFactory.getLogger(TimeServerForDecoder.class); static final boolean SSL = System.getProperty("ssl") != null; private static final String ip = "192.168.31.153"; private static final int port = 10010; public static void main(String[] args) throws Exception { run(); } public static void run() throws Exception { // Configure SSL. final SslContext sslCtx; if (SSL) { SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); } else { sslCtx = null; } /* * EventLoopGroup(多线程事件loop),处理IO操作,这里我们用了两个事件loop * 第一个boss用于处理器监听连接请求,第二个worker用于数据的传输; * 具体线程是多少依赖于事件loop的具体实现 * */ EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //ServerBootstrap,用于配置服务端,一般为ServerSocket通道 ServerBootstrap serverBoot = new ServerBootstrap(); serverBoot.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { //添加通道处理器到通道关联的管道,准确的中文翻译为管道线, 此管道线与Mina中过滤链十分相似, //ChannelInitializer用于配置通道的管道线,ChannelPipeline ChannelPipeline pipeline = ch.pipeline(); if (sslCtx != null) { pipeline.addLast(sslCtx.newHandler(ch.alloc())); } // pipeline.addLast(new LoggingHandler(LogLevel.INFO)); pipeline.addLast(new TimeServerHandler2()); } }) .option(ChannelOption.SO_BACKLOG, 128)//socket监听器连接队列大小、 .childOption(ChannelOption.SO_KEEPALIVE, true); //保活,此配置针对ServerSocket通道接收连接产生的Socket通道 InetSocketAddress inetSocketAddress = new InetSocketAddress(ip,port); // 绑定地址,开始监听 ChannelFuture f = serverBoot.bind(inetSocketAddress).sync(); log.info("=========Server is start========="); //等待,直到ServerSocket关闭 f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
服务端处理器:
package netty.handler.time; import java.nio.charset.Charset; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * * @author donald * 2017年6月21日 * 下午12:48:01 */ public class TimeServerHandler2 extends ChannelInboundHandlerAdapter { private static final Logger log = LoggerFactory.getLogger(TimeServerHandler2.class); private static final String TIME_PROTOCL = "?time"; private static final Charset charsetDecoder= Charset.forName("UTF-8"); /** * 读client通道数据,通道处理器上下文ChannelHandlerContext与Mina的会话很像 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf in = (ByteBuf)msg; String message = (String) in.readCharSequence(in.writerIndex(), charsetDecoder); log.info("===Server reciever message:" +message); if(message.equals(TIME_PROTOCL)){ //通过通道处理器上下文的ByteBufAllocator创建容量至少为8个字节的ByteBuf ByteBuf time = ctx.alloc().buffer(8); time.writeLong(System.currentTimeMillis()); /* 在发送数据时,我们并没有调用nio的ByteBuffer#flip类似的方法,这是由于 为了避免nio忘记flip操作的问题,Netty通过readIndex和writeIndex两个index 表示ByteBuf的相对开始和结束位置;当向ByteBuffer中写数据时,writeIndex将会增长, 而readIndex不变。 */ /* ctx#write,writeAndFlush方法返回一个写结果ChannelFuture, ChannelFuture表示一个IO事件操作,如果想要在ctx写操作后,关闭连接,不可以用如下方式: Channel ch = ...; ch.writeAndFlush(message); ch.close(); 因为Netty的写操作时异步的,上面这种关闭连接方式,有可能在消息没发送完前,连接已经关闭,为了 能在消息发送完毕后再关闭会话,可以通过添加通道结果监听器,在消息发送完时,触发监听器operationComplete 事件。*/ ctx.writeAndFlush(time); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { //异常发生时,关闭连接 cause.printStackTrace(); ctx.close(); } }
客户端:
package netty.main.time; import java.net.InetSocketAddress; import javax.net.ssl.SSLException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import netty.codec.time.TimeDecoder; import netty.handler.time.TimeClientHandler; /** * 客户端要与TimeServerForDecoder服务端配合使用 * @author donald * 2017年6月21日 * 下午12:48:10 */ public final class TimeClientWithDecoder { private static final Logger log = LoggerFactory.getLogger(TimeClientWithDecoder.class); private static final boolean SSL = System.getProperty("ssl") != null; private static final String ip = System.getProperty("host", "192.168.31.153"); private static final int port = Integer.parseInt(System.getProperty("port", "10010")); public static void main(String[] args) throws Exception { run(); } private static void run() throws SSLException, InterruptedException{ //配置安全套接字上下文 final SslContext sslCtx; if (SSL) { sslCtx = SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); } else { sslCtx = null; } EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //Bootstrap与 ServerBootstrap相似,不同的是Bootstrap用于配置客户端, //一般为Socket通道,或无连接通道 Bootstrap bootstrap = new Bootstrap(); //EventLoopGroup有 boss和worker两组,对于客户端只需要用worker bootstrap.group(workerGroup); bootstrap.channel(NioSocketChannel.class); bootstrap.option(ChannelOption.SO_KEEPALIVE, true); bootstrap.handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //添加安全套接字处理器和通道处理器到 ChannelPipeline pipeline = ch.pipeline(); if (sslCtx != null) { pipeline.addLast(sslCtx.newHandler(ch.alloc(), ip, port)); } // pipeline.addLast(new LoggingHandler(LogLevel.INFO)); pipeline.addLast(new TimeDecoder(),new TimeClientHandler()); } }); InetSocketAddress inetSocketAddress = new InetSocketAddress(ip,port); //连接socket地址 ChannelFuture f = bootstrap.connect(inetSocketAddress).sync(); log.info("=========Client is start========="); //等待,直到连接关闭 f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } }
客户端解码器:
package netty.codec.time; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; /** * 字节流消息解码器ByteToMessageDecoder,是#ChannelInboundHandler的实现,可以解决粘包问题; * 字节消息解码的内部有一个可累计buffer,当有数据到达时,将会调用#decode方法,解码消息,如果累计buffer中 * 没有足够的数据,则不会添加对象到out,如果有对象添加到out,表示解码器成功解码了一个消息;我们不需要一次解码多个消息, * 解码器将会不断地调用#decode方法,直到没有对象可以添加到out。 * @author donald * 2017年6月22日 * 上午8:55:20 */ public class TimeDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { if (in.readableBytes() < 8) { return; } out.add(in.readBytes(8)); } }
启动服务端与客户端:
服务端:
[INFO ] 2017-07-05 22:57:58 netty.main.time.TimeServerForDecoder =========Server is start=========
[INFO ] 2017-07-05 22:58:08 netty.handler.time.TimeServerHandler2 ===Server reciever message:?time
客户端:
[INFO ] 2017-07-05 22:58:08 netty.main.time.TimeClientWithDecoder =========Client is start=========
[INFO ] 2017-07-05 22:58:08 netty.handler.time.TimeClientHandler ===Server Time:2017-7-5 22:58:08
解码器还有另外一种形式:
package netty.codec.time; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ReplayingDecoder; /** * 回复解码器ReplayingDecoder为字节流消息解码器ByteToMessageDecoder的实现 * @author donald * 2017年6月22日 * 上午8:55:20 */ public class TimeDecoder2 extends ReplayingDecoder<Void> { @Override protected void decode( ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { out.add(in.readBytes(8)); } }
还有另外一种形式,在客户端处理器中处理,当然这种方式我们不建议使用:
package netty.handler.time; import java.nio.charset.Charset; import java.util.Date; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** *TimeClientHandlerX处理粘包情况, *由于网络等原因,有时候我们不能够一次接收一个完成的数据包,我们必须等待完成的数据包, *我们可以等待数据包数据完成时,才解析数据。 *在此示例中,我们接收的是一个8字节的long数据,在网络不佳的情况下,也有可能出现不能一次 *接收的情况。 * @author donald * 2017年6月21日 * 下午12:47:53 */ public class TimeClientHandler2 extends ChannelInboundHandlerAdapter { private static final Logger log = LoggerFactory.getLogger(TimeClientHandler2.class); private static final String TIME_PROTOCL = "?time"; private static final Charset charsetEncoder= Charset.forName("UTF-8").newEncoder().charset(); private ByteBuf buf; /** * 在通道连接建立时(准备传输数据)触发 */ @Override public void channelActive(ChannelHandlerContext ctx) { ByteBuf timeReq = ctx.alloc().buffer(5); timeReq.writeCharSequence(TIME_PROTOCL, charsetEncoder); ctx.writeAndFlush(timeReq); } /** * Gets called after the {@link ChannelHandler} was added to the * actual context and it's ready to handle events. * 在通道处理器添加到实际上下文,准备处理事件时触发,可以用于初始化阻塞时间较短的任务 */ @Override public void handlerAdded(ChannelHandlerContext ctx) { buf = ctx.alloc().buffer(8); } /** * Gets called after the {@link ChannelHandler} was removed from the actual * context and it doesn't handle events anymore. * 在通道处理器从实际上下文移除,不再处理事件时触发,可以用于释放初始化任务申请的资源 */ @Override public void handlerRemoved(ChannelHandlerContext ctx) { buf.release(); buf = null; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf in = (ByteBuf)msg; //将所有的字节序列数据累积到buf中 buf.writeBytes(in); in.release(); /* 待buffer中有足够的数据时,解析数据,否则当更多的数据到达时,netty将会再次调用#channelRead方法 */ if(buf.readableBytes()>=8){ long nowTime = buf.readLong(); Date nowDay = new Date(nowTime); log.info("===Server Time:" +nowDay.toLocaleString()); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }