Netty 进阶学习(十一)-- 扩展与源码

3、聊天室案例

视频地址:https://www.bilibili.com/video/BV1py4y1E7oA?p=108&vd_source=3db50b368637ed14b6374cc45e14b74f

空闲监测

连接假死

原因

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

问题

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

服务器端解决

  • 怎么判断客户端连接是否假死呢?如果能收到客户端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
    
    
    // 用来触发特殊事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
    
    
        IdleStateEvent event = (IdleStateEvent) evt;
        // 触发了读空闲事件
        if (event.state() == IdleState.READER_IDLE) {
    
    
            log.debug("已经 5s 没有读到数据了");
            ctx.channel().close();
        }
    }
});

客户端定时心跳

  • 客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
    
    
    // 用来触发特殊事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
    
    
        IdleStateEvent event = (IdleStateEvent) evt;
        // 触发了写空闲事件
        if (event.state() == IdleState.WRITER_IDLE) {
    
    
            //                                log.debug("3s 没有写数据了,发送一个心跳包");
            ctx.writeAndFlush(new PingMessage());
        }
    }
});

4、扩展与源码

4.1、扩展

4.1.1、扩展序列化算法

/**
 * 用于扩展序列化、反序列化算法
 */
public interface Serializer {
    
    

    /** 反序列化方法 */
    <T> T deserialize(Class<T> clazz, byte[] bytes);

    /** 序列化方法 */
    <T> byte[] serialize(T object);

    /** 序列化枚举类实现 */
    enum Algorithm implements Serializer {
    
    
        /** Java 序列化 */
        Java {
    
    
            @Override
            public <T> T deserialize(Class<T> clazz, byte[] bytes) {
    
    
                try {
    
    
                    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
                    return (T) ois.readObject();
                } catch (IOException | ClassNotFoundException e) {
    
    
                    throw new RuntimeException("反序列化失败", e);
                }
            }
            @Override
            public <T> byte[] serialize(T object) {
    
    
                try {
    
    
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    ObjectOutputStream oos = new ObjectOutputStream(bos);
                    oos.writeObject(object);
                    return bos.toByteArray();
                } catch (IOException e) {
    
    
                    throw new RuntimeException("序列化失败", e);
                }
            }
        },
        /** Gson 序列化 */
        Json {
    
    
            @Override
            public <T> T deserialize(Class<T> clazz, byte[] bytes) {
    
    
                Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();
                String json = new String(bytes, StandardCharsets.UTF_8);
                return gson.fromJson(json, clazz);
            }
            @Override
            public <T> byte[] serialize(T object) {
    
    
                Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();
                String json = gson.toJson(object);
                return json.getBytes(StandardCharsets.UTF_8);
            }
        }
    }
    
    // 处理 Gson().toJson(String.class) 序列化失败问题
    class ClassCodec implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {
    
    
        @Override
        public Class<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
    
    
            try {
    
    
                String str = json.getAsString();
                return Class.forName(str);
            } catch (ClassNotFoundException e) {
    
    
                throw new JsonParseException(e);
            }
        }
        @Override             //   String.class
        public JsonElement serialize(Class<?> src, Type typeOfSrc, JsonSerializationContext context) {
    
    
            // class -> json
            return new JsonPrimitive(src.getName());
        }
    }
}

Gson 需要的依赖

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
</dependency>

4.2、参数调优

1)CONNECT_TIMEOUT_MILLIS

  • 基于 SocketChannel 参数
  • 用在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 TimeOut 异常
  • SO_TIMEOUT 主要用在阻塞 IO,阻塞 IO 中 accept、read 等都是无限等待的,如果不希望永远阻塞,使用它调整超时时间
@Slf4j
public class TimeOutClient {
    
    
    public static void main(String[] args) {
    
    
        // 1、客户端
        // new Bootstrap().option();        给 SocketChannel 配置参数

        // 2、服务端
        // new ServerBootstrap().option();         给 ServerSocketChannel 配置参数
        // new ServerBootstrap().childOption();    给 SocketChannel 配置参数

        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
    
    
            Bootstrap bootstrap = new Bootstrap()
                    .group(worker)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500)
                    .channel(NioSocketChannel.class)
                    .handler(new LoggingHandler());
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888);
            channelFuture.sync().channel().closeFuture().sync();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            log.error("time out");
        } finally {
    
    
            worker.shutdownGracefully();
        }
    }
}

抛出的异常:

io.netty.channel.ConnectTimeoutException: connection timed out: /127.0.0.1:8888
	at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe$1.run(AbstractNioChannel.java:265)
	at io.netty.util.concurrent.PromiseTask$RunnableAdapter.call(PromiseTask.java:38)
	at io.netty.util.concurrent.ScheduledFutureTask.run(ScheduledFutureTask.java:120)
	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:400)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:401)
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:805)
	at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:145)
	at java.lang.Thread.run(Thread.java:745)

AbstractNioChannel.java:265 源码:

// Schedule connect timeout.	了解线程之间的通信方式
int connectTimeoutMillis = config().getConnectTimeoutMillis();			// 设置的超时时间参数
if (connectTimeoutMillis > 0) {
    
    
    connectTimeoutFuture = eventLoop().schedule(new Runnable() {
    
    		// 定时任务
        @Override
        public void run() {
    
    
            ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;		// Promise 和主线程是同一个对象
            ConnectTimeoutException cause =
                new ConnectTimeoutException("connection timed out: " + remoteAddress);
            if (connectPromise != null && connectPromise.tryFailure(cause)) {
    
    			// 抛出异常
                close(voidPromise());
            }
        }
    }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}

2)SO_BACKLOG

  • 属于 ServerSocketChannel 参数
client server syns queue 半连接队列 (未完成三次握手) accept queue 全连接队列 (完成三次握手) bind() listen() contect() SYN_SEND(状态) 1. SYN put SYN_RCVD(状态) 2. SYN + ACK ESTABLISHED(状态) 3. ACK put ESTABLISHED(状态) accept() accept() 发生在三次握手后 client server syns queue 半连接队列 (未完成三次握手) accept queue 全连接队列 (完成三次握手)

1、第一次握手,client 发送 SYN 到 server ,状态修改为 SYN_SEND ,server 收到,状态改变为 SYN_REVD,并将该请求放入 sync queue 队列

2、第二次握手,server 回复 SYN + ACK 给 client,client 收到,状态改变为 ESTABLISHED ,并且发送 ACK 给 server

3、第三次握手,server 收到 ACK,状态改变为 ESTABLISHED ,该请求从 sync queue 队列 放入 accept queue 队列

其中

  • 在 Linux 2.2 之前,backlog 大小包括了两个队列的大小,在 Linux 2.2 之后分别用以下两个参数控制
  • sync queue - 半连接队列
    • 大小通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在 syncookies 启用的情况下,逻辑上没有最大限制,这个设置便被忽略
  • accept queue - 全连接队列
    • 其大小通过 /proc/sys/net/core/somaxconn 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取两者的较小者
    • 如果 accept queue 队列满了,server 将发送一个拒绝连接的错误信息带 client

Netty 中 可以通过 option(ChannelOption.SO_BACKLOG, 值) 来设置大小(NIO 通过 bind(端口,backlog)

3)ulimit -n

  • 属于操作系统参数 (文件句柄数限制)

4)TCP_NODELAY

  • 属于 SocketChannel 参数 (默认是开启 false 开启了 nagle 算法,数据包攒到一定量再发送。建议设置为 true)

5)SO_SNDBUF & SO_RCVBUF

  • SO_SNDBUF 属于 SocketChannel 参数
  • SO_RCVBUF 即可用于 SocketChannel 参数,也可以用于 ServerSocketChannel 参数 (建议设置到 ServerSocketChannel 上)

6)ALLOCATOR

  • 属于 SocketChannel 参数

  • 用来分配 ByteBuf,ctx.alloc()

7)RCVBUF_ALLOCATOR

  • 属于 SocketChannel 参数
  • 控制 Netty 接收缓冲区的大小
  • 负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定

4.3、Netty 实现 RPC 框架

远程方法调用

地址:
https://www.bilibili.com/video/BV1py4y1E7oA?p=130&vd_source=3db50b368637ed14b6374cc45e14b74f

5、 源码分析

地址:
https://www.bilibili.com/video/BV1py4y1E7oA?p=139&vd_source=3db50b368637ed14b6374cc45e14b74f

2.1 启动剖析

我们就来看看 netty 中对下面的代码是怎样进行处理的

// 1 netty 中使用 NioEventLoopGroup (简称 nio boss 线程)来封装线程和 selector
Selector selector = Selector.open(); 

// 2 创建 NioServerSocketChannel,同时会初始化它关联的 handler,以及为原生 ServerSocketChannel 存储 config
NioServerSocketChannel attachment = new NioServerSocketChannel();

// 3 创建 NioServerSocketChannel 时,创建了 java 原生的 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 
serverSocketChannel.configureBlocking(false);

// 4 启动 nio boss 线程执行接下来的操作

// 5 注册(仅关联 selector 和 NioServerSocketChannel),未关注事件
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);

// 6 head -> 初始化器 -> ServerBootstrapAcceptor -> tail,初始化器是一次性的,只为添加 acceptor

// 7 绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));

// 8 触发 channel active 事件,在 head 中关注 op_accept 事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);

主线:

Selector selector = Selector.open(); 
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);
serverSocketChannel.bind(new InetSocketAddress(8080));
selectionKey.interestOps(SelectionKey.OP_ACCEPT);

猜你喜欢

转载自blog.csdn.net/weixin_43989102/article/details/126756423
今日推荐