本章节主要介绍粘包半包的解决方法、协议的设计、序列化知识;同时通过实现聊天室案例将这些知识点串联起来。
3.1 粘包半包
-
粘包半包现象
- 粘包:多条数据粘连,一次发送给服务器
- 半包:一条完整消息从某个点断开,发送给服务器的消息不完整
演示代码
//服务器演示代码 public void start(){ //声明工作线程及主线程 NioEventLoopGroup boss = new NioEventLoopGroup(1); NioEventLoopGroup worker = new NioEventLoopGroup(); try { //启动配置 ServerBootstrap server = new ServerBootstrap(); server.channel(NioServerSocketChannel.class); //非阻塞 //调整系统的接收缓冲区 server.option(ChannelOption.SO_RCVBUF,10); //设置接收缓冲区 server.group(boss,worker); server.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); } }); ChannelFuture future = server.bind(8080).sync(); future.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("服务器启动错误:{}",e.getMessage()); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } //客户端演示代码 static void send(){ NioEventLoopGroup worker = new NioEventLoopGroup(); try { Bootstrap client = new Bootstrap(); client.channel(NioSocketChannel.class); client.group(worker); client.handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { sc.pipeline().addLast(new ChannelInboundHandlerAdapter(){ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for (int i = 0; i < 10; i++) { ByteBuf buf = ctx.alloc().buffer(16); buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}); ctx.writeAndFlush(buf); } } }); } }); ChannelFuture future = client.connect("127.0.0.1", 8080).sync(); future.channel().closeFuture().sync(); }catch (Exception e){ log.error("客户端错误:{}",e.getMessage()); }finally { worker.shutdownGracefully(); } }
日志说明
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
±-------±------------------------------------------------±---------------+
|00000000| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |…|
|00000010| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |…|
|00000020| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |…|
|00000030| 06 07 |… |
±-------±------------------------------------------------±---------------+
通过日志可以看出,服务器前三次接收的属于粘包现象,最后一次属于半包现象。
-
粘包半包的解决方案
-
短链接
//客户端 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ByteBuf buf = ctx.alloc().buffer(16); buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}); ctx.writeAndFlush(buf); ctx.channel().close(); //发完消息接断开连接,可以解决粘包,不能处理半包 } //服务器,模拟半包现象 //调整系统的接收缓冲区(滑动窗口) //server.option(ChannelOption.SO_RCVBUF,10); //设置接收缓冲区 //调整netty的接收缓冲区(ByteBuf) server.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
-
定长解码器
//服务器端 protected void initChannel(SocketChannel sc) throws Exception { //添加定长解吗器,位于最上端 sc.pipeline().addLast(new FixedLengthFrameDecoder(10)); sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); } //客户端,fill10Bytes不足的补全10个字节 public void channelActive(ChannelHandlerContext ctx) throws Exception { ByteBuf buf = ctx.alloc().buffer(); char c = '0'; Random r = new Random(); for (int i = 0; i < 10; i++) { byte[] bytes = fill10Bytes(c,r.nextInt(10) + 1); c++; buf.writeBytes(bytes); } ctx.writeAndFlush(buf); } //fill10Bytes()方法代码 public static byte[] fill10Bytes(char c ,int len){ byte[] bytes = new byte[10]; Arrays.fill(bytes,(byte) '_'); for (int i = 0; i < len; i++) { bytes[i] = (byte)c; } System.out.println("内容:" + new String(bytes)); return bytes; }
-
行解码器
//服务器代码 protected void initChannel(SocketChannel sc) throws Exception { sc.pipeline().addLast(new LineBasedFrameDecoder(1024)); //添加行解吗器 sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); } //客户端代码,makeString给消息添加分隔符‘\n’ public void channelActive(ChannelHandlerContext ctx) throws Exception { ByteBuf buf = ctx.alloc().buffer(); char c = '0'; Random r = new Random(); for (int i = 0; i < 10; i++) { byte[] bytes = makeString(c,r.nextInt(256) + 1); c++; buf.writeBytes(bytes); } ctx.writeAndFlush(buf); } //makeString()方法代码 public static byte[] makeString(char c ,int len){ StringBuilder sb = new StringBuilder(len + 2); for (int i = 0; i < len; i++) { sb.append(c); } sb.append("\n"); return sb.toString().getBytes(); }
-
LTC解码器
public static void main(String[] args) { EmbeddedChannel channel = new EmbeddedChannel( new LengthFieldBasedFrameDecoder(1024,0,4,1,4), new LoggingHandler(LogLevel.DEBUG) ); //4个字节的内容长度,实际长度 ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(); send(buf,"hello, world"); send(buf,"Hi"); channel.writeInbound(buf); } private static void send(ByteBuf buf,String content){ byte[] bytes = content.getBytes(); //实际内容 int len = bytes.length; //实际内容长度 buf.writeInt(len); //写入内容长度 buf.writeByte(1); //写入任意数据 buf.writeBytes(bytes); //写入实际内容 }
注解:
- maxFrameLength:发送的数据帧最大长度
- lengthFieldOffset:定义长度域位于发送的字节数组中的下标
- lengthFieldLength:用于描述定义的长度域的长度
- lengthAdjustment:自长度域以后几个字节为内容域
- initialBytesToStrip:接收到的发送数据包,去除前initialBytesToStrip位
-
3.2 协议的制定及解析
-
Redis
final byte[] LINE = {13,10}; NioEventLoopGroup worker = new NioEventLoopGroup(); try { Bootstrap client = new Bootstrap(); client.channel(NioSocketChannel.class); client.group(worker); client.handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); sc.pipeline().addLast(new ChannelInboundHandlerAdapter(){ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //set name zhangsan ByteBuf buf = ctx.alloc().buffer(); buf.writeBytes("*3".getBytes()); //命令数组长度 buf.writeBytes(LINE); //回车换行 buf.writeBytes("$3".getBytes()); //命令字段长度 set buf.writeBytes(LINE); buf.writeBytes("set".getBytes()); //指令 => set buf.writeBytes(LINE); buf.writeBytes("$4".getBytes()); //key字段长度 name buf.writeBytes(LINE); buf.writeBytes("name".getBytes()); //key => name buf.writeBytes(LINE); buf.writeBytes("$8".getBytes()); //value字段长度 zhangsan buf.writeBytes(LINE); buf.writeBytes("zhangsan".getBytes()); //value => zhangsan buf.writeBytes(LINE); ctx.writeAndFlush(buf); //写入并发送 } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println(buf.toString(Charset.defaultCharset())); } }); } }); ChannelFuture future = client.connect("127.0.0.1", 6379).sync(); future.channel().closeFuture().sync(); }catch (Exception e){ log.error("客户端错误:{}",e.getMessage()); }finally { worker.shutdownGracefully(); }
-
Http
NioEventLoopGroup boss = new NioEventLoopGroup(1); NioEventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap client = new ServerBootstrap(); client.channel(NioServerSocketChannel.class); client.group(boss,worker); client.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { sc.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); sc.pipeline().addLast(new HttpServerCodec()); sc.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>(){ @Override protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception { //获取请求 log.info(msg.uri()); //返回响应 DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK); byte[] bytes = "<h1>Hello, World!</h1>".getBytes(); response.headers().setInt(CONTENT_LENGTH,bytes.length); response.content().writeBytes(bytes); //写回响应 ctx.writeAndFlush(response); } }); } }); ChannelFuture future = client.bind("127.0.0.1", 8080).sync(); future.channel().closeFuture().sync(); }catch (Exception e){ log.error("客户端错误:{}",e.getMessage()); }finally { boss.shutdownGracefully(); worker.shutdownGracefully(); }
-
自定义协议
-
要素
- 魔数:用来在第一时间判定是否是无效的数据包
- 版本号:可以支持协议的升级
- 序列化算法:消息正文到底采用哪种序列化方式,可以由此扩展,例如JSON\protobuf\jdk
- 指令类型:是登录、注册…与业务相关
- 请求序号:为了双工通信,提供异步能力
- 正文长度:传递数据的长度
- 消息正文:传递的数据
-
编码
@Override protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { //4字节的魔数 out.writeBytes(new byte[]{1,2,3,4}); //1字节的版本 out.writeByte(1); //1字节的序列化方式0:jdk;1:json out.writeByte(0); //1字节的指令类型 out.writeByte(msg.getMessageType()); //4字节序列号 out.writeByte(msg.getSequenceId()); //对齐填充 out.writeByte(0xff); //获取字节的内容数组 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(msg); byte[] bytes = bos.toByteArray(); //内容长度 out.writeInt(bytes.length); //内容写入 out.writeBytes(bytes); }
-
解码
@Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { int magicNum = in.readInt(); byte version = in.readByte(); byte serializerType = in.readByte(); byte messageType = in.readByte(); int sequenceId = in.readInt(); in.readByte(); int length = in.readInt(); byte[] bytes = new byte[length]; in.readBytes(bytes,0,length); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); Message message = (Message) ois.readObject(); log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length); log.debug("{}", message); out.add(message); }
-
测试
EmbeddedChannel channel = new EmbeddedChannel( //解决粘包半包问题 new LengthFieldBasedFrameDecoder(1024,12,4,0,0), new LoggingHandler(LogLevel.DEBUG), new MessageCodec() ); LoginRequestMessage request = new LoginRequestMessage("zhangsan","123","张三"); //出站 channel.writeOutbound(request); ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(); new MessageCodec().encode(null,request,buf); //模拟半包现象 ByteBuf s1 = buf.slice(0, 100); ByteBuf s2 = buf.slice(100,buf.readableBytes() - 100); //入站 s1.retain(); //防止内存释放 channel.writeInbound(s1); channel.writeInbound(s2);
-