aoe1231

看得完吗

Netty——3、进阶

1、粘包与半包

TCP以一个段(segment)为单位,每发送一个段就要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差。为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值。

窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用。

1.1、现象分析

粘包现象:发送abc def,接收abcdef。原因:

  • 应用层:接收方ByteBuf设置太大(Netty默认1024);
  • 滑动窗口:假设发送方256Bytes表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这256Bytes字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包。
  • Nagle算法:会造成粘包。

半包现象:发送abcdef,接收abc def。原因:

  • 应用层:接收方ByteBuf小于实际发送数据量;
  • 滑动窗口:假设接收方的窗口只剩了128Bytes,发送方的报文大小是256Bytes,这时放不下了,只能先发送前128Bytes,等待ack后才能发送剩余部分,这就造成了半包。
  • MSS限制:当发送的数据超过MSS限制后,会将数据切分发送,就会造成半包。

本质是因为TCP是流式协议,消息无边界。

1.2、解决方案

1.2.1、短连接

package com.clp.nettyPlus;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.AdaptiveRecvByteBufAllocator;
import io.netty.channel.ChannelFuture;
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.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
class HelloWorldServer {
    public static void main(String[] args) {
        new HelloWorldServer().start();
    }

    void start() {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap()
//                    .option(ChannelOption.SO_RCVBUF, 10) //设置系统的接收缓冲区(接收的滑动窗口)
                    //调整netty的接收缓冲区,即ByteBuf(最小值、初始值、最大值)
                    .childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16,16,16))
                    .group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //调试打印出服务端受到的信息
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
package com.clp.nettyPlus;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorldClient {
    static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            send();
        }
        System.out.println("finish");
    }

    public static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                @Override //在连接 channel 建立成功后触发active事件
                                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, 16,17});
                                    ctx.writeAndFlush(buf); //发送这些数据
                                    ctx.channel().close(); //断开连接
                                    super.channelActive(ctx);
                                }
                            });
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("client error", e);
        } finally {
            worker.shutdownGracefully();
        }
    }
}
结果:
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 16) that reached at the tail of the pipeline. Please check your pipeline configuration.
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x38d017e9, L:/127.0.0.1:8080 - R:/127.0.0.1:57609].
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 - R:/127.0.0.1:57609] READ COMPLETE
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 - R:/127.0.0.1:57609] READ COMPLETE
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57609] INACTIVE
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57609] UNREGISTERED
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] REGISTERED
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] ACTIVE
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] READ: 16B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 16, cap: 16) that reached at the tail of the pipeline. Please check your pipeline configuration.
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626].
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] READ COMPLETE
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 10 11                                           |..              |
+--------+-------------------------------------------------+----------------+
...

1.2.2、定长解码器

1.2.3、基于分割符的解码器

1.2.4、LTC解码器

lengthFieldOffset:长度字段偏移量,表明长度字段部分从哪开始
lengthFieldLength:长度字段本身占用的字节数
lengthAdjustment:长度之后还有几个字节才是内容
initialBytesToStrip:经过解析之后,从头开始要剥离几个字节
package com.clp.decodeEncode;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class TestLengthFieldDecoder {
    public static void main(String[] args) {
        EmbeddedChannel channel = new EmbeddedChannel(
                new LengthFieldBasedFrameDecoder(
                        1024, 0, 4, 1, 5),
                new LoggingHandler(LogLevel.DEBUG)
        );

        //发送 4 字节内容的长度,然后是实际内容
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        write(buffer, "Hello, world");
        write(buffer, "Hi!");
        channel.writeInbound(buffer);
    }

    private static void write(ByteBuf buffer, String content) {
        byte[] bytes = content.getBytes(); //实际内容
        int length = bytes.length; //实际内容长度
        buffer.writeInt(length); //大端表示法写入长度
        buffer.writeByte(1); //加上版本号 1字节
        buffer.writeBytes(bytes); //写入内容
    }
}


结果:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64             |Hello, world    |
+--------+-------------------------------------------------+----------------+
10:32:24.729 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 3B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 69 21                                        |Hi!             |
+--------+-------------------------------------------------+----------------+
10:32:24.729 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE

2、协议设计与解析

2.1、自定义协议要素

  • 魔数:用来在第一时间判断是否是无效数据包。
  • 版本号:可以支持协议的升级。
  • 序列化算法:消息正文到底采用哪种序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk。
  • 指令类型:是登录、注册、单聊、群聊...跟业务相关。
  • 请求序号:为了双工通信,提供异步能力。
  • 正文长度。
  • 消息正文,如json、xml、对象流 ...。

代码演示:

/**
 * 自定义协议构成:
     * 魔数:用来在第一时间判断是否是无效数据包。
     * 版本号:可以支持协议的升级。
     * 序列化算法:消息正文到底采用哪种序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk。
     * 指令类型:是登录、注册、单聊、群聊...跟业务相关。
     * 请求序号:为了双工通信,提供异步能力。
     * 正文长度。
     * 消息正文,如json、xml、对象流 ...。
 * 
 * 需要配合 LengthFieldBasedFrameDecoder 一起使用
 *
 */
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf out) throws Exception {
        // 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4});
        // 1 字节的版本
        out.writeByte(1);
        // 1 字节 使用的序列化方式 jdk=0;json=1
        out.writeByte(0); // 使用jdk
        // 1 字节 指令类型
        out.writeByte(message.getMessageType());
        // 4 字节的序号用于双工通信
        out.writeInt(message.getSequenceId());
        // 添加无意义字节为了对齐填充,保证字节长度为2的整数倍
        out.writeByte(0xFF);
        // 获取内容的字节数组
        ByteArrayOutputStream boStream = new ByteArrayOutputStream();
        ObjectOutputStream ooStream = new ObjectOutputStream(boStream);
        ooStream.writeObject(message);
        byte[] bytes = boStream.toByteArray();
        // 4 字节的长度
        out.writeInt(bytes.length);
        // n 字节的内容
        out.writeBytes(bytes);
    }

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception {
        int magicNumber = 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);
        Message message = null;
        if (serializerType == 0) {
            ObjectInputStream oiStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
            message = (Message) oiStream.readObject();
        }
        log.debug("{}, {}, {}, {}, {}, {}", magicNumber, version, serializerType, messageType, sequenceId, length);
        log.debug("{}", message);

        if (message != null) {
            list.add(message);
        }
    }
}

@Slf4j
public class ChatServer {
    static LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);

    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(new MessageCodec());
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException exception) {
            log.error("server error", exception);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

3、连接假死

连接假死的原因:

  • 网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源;
  • 公网网络不稳定,出现丢包。如果连续出现丢包,这是的现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着;
  • 应用程序线程阻塞,无法进行数据读写。

问题:

  • 假死的连接占用的资源不能自动释放;
  • 向假死的连接发送数据,得到的反馈是发送超时。

解决:发送心跳包。

 

posted on 2022-09-18 15:47  啊噢1231  阅读(103)  评论(0编辑  收藏  举报

导航

回到顶部