【Netty入门01】Netty版本选择以及入门样例

前言

学习Netty之前建议先学习Java BIO、NIO等相关的网络知识,以便后续可以更深刻的理解Netty的底层原理。

一:Netty版本

Netty总共有三个大版本,分别为3.x版本、4.x版本、5.x版本。

3.x版本已经停止维护,但现在还有一些项目在使用。由于在线程模型(输入事件与输出事件使用了不同的线程池,导致频繁的线程上下文切换。在Netty4中通过一个channel绑定一个EventLoop解决,将输入、输出都绑定在同个线程中。)、内存管理(使用直接内存跟堆内存,默认不使用内存池。netty3会创建大量的对象,导致gc压力过高)、API设计等有缺陷,因此Netty4重新设计了,导致Netty4不兼容Netty3。

4.x版本现在还在维护,截止发文时间(2023-07-31)最新版本为Netty-4.1.97.Final。4.x版本解决了3.x版本中出现的一些问题,并且还在维护。因此我们一般选择这个版本

5.x版本由于一些缺陷,例如处理大量长连接时有可能出现内存泄漏、处理高并发HTTP请求时,HTTP解析性能下降、API的变更以及不兼容性等,已经被废弃并停止维护了,不推荐使用。

因此后文我们默认使用Netty代表Netty4。

二:Netty组件

1.Bootstrap(引导器)

分为ServerBootstrap跟Bootstrap。

ServerBootstrap用于配置和启动Netty服务器的辅助类。用于配置服务端参数、添加ChannelPipeline、绑定端口等。

Bootstrap用于配置和启动Netty客户端的辅助类。用于配置链接参数、添加ChannelPipeline、发起链接等。

2.Channel(通道)

主要负责数据的读写。网络传输载体,可以代表一条网络连接,例如Socket。服务端与客户端之间就是通过Channel进行数据传输。而在Netty中的Channel定义了统一的API,其下实现了多种传输类型,例如Nio、Epoll、KQueue等。我们可以根据自己的需求选择适合的传输类型。

3.EventLoop(事件循环)

负责处理所有的IO事件以及执行任务。每个EventLoop都管理一个或者多个Channel并负责他们的读写以及其他IO事件。

4.ChannelHandler(通道处理器)

用于数据的出站、入站以及事件处理。是我们自己创建Netty服务器中自定义程度最高的组件。主要的业务逻辑都是通过ChannelHandler去实现的。同时还包括了基础功能实现,例如数据编解码、限流、状态监控等。都可以通过ChannelHandler去实现。

5.ChannelPipeline(通道管道)

用于管理ChannelHandler以及定义数据的处理过程。它是一条处理器链,数据通过Pipeline顺序地经过各个ChannelHandler处理。

6.Codec(编解码器)

用于对源数据进行编解码。可通过自定义协议解决TCP粘包拆包问题。同时Netty为了简化编解码过程,还提供了一系列的编解码器,例如ByteToMessageDecoder、MessageToMessageCodec等。

7.ByteBuf

Netty提供的高性能的字节容器,用于读写数据。

三:Netty入门使用

1. maven依赖
    <!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.97.Final</version>
    </dependency>
2. 服务端

服务端启动类

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultEventExecutorGroup;

public class NettyServer {
    public static void main(String[] args) {
        //EventLoopGroup相当于线程池, 实际上也实现了线程池
        //bossGroup是用来处理连接的
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //workerGroup是用来处理io的
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                //添加我们自定义的业务handler
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        // 第一个参数为EventExecutorGroup, 实际上是一个线程池,
                        // 作用是当执行handler时, 不使用workerGroup的io线程, 
                        // 而是使用我们传进去的线程池去执行handler里面业务逻辑代码, 防止io线程堵塞造成并发量下降
                        ch.pipeline()
                                .addLast(new DefaultEventExecutorGroup(10), new FirstServerHandler());
                    }
                });
        //绑定本地的8002端口
        serverBootstrap.bind(8002);
    }
}

服务端自定义FirstServerHandler

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.nio.charset.Charset;
import java.util.Date;

public class FirstServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf byteBuf = (ByteBuf) msg;
        String s = byteBuf.toString(Charset.forName("utf-8"));
        System.out.println(new Date() + ": 服务端接收到客户端的数据 -> " + s);
        //接收到客户端的消息后我们再回复客户端
        ByteBuf out = getByteBuf(ctx);
        ctx.channel().writeAndFlush(out);
    }

    private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
        byte[] bytes = "【服务器】:我是服务器,我收到你的消息了!".getBytes(Charset.forName("utf-8"));
        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeBytes(bytes);
        return buffer;
    }
}

ChannelInboundHandlerAdapter为消息入站处理适配器,如果我们想处理对端传来的数据时,就可以继承这个类,并重写其中的channelRead(ChannelHandlerContext ctx, Object msg)方法,这样在客户端传送数据到服务端时,Netty会自动帮我们调用这个ChannelRead方法。我们自己的业务逻辑。

这里我们通过继承ChannelInboundHandlerAdapter来实现自定义的FirstServerHandler,用于接收客户端传送来的数据,将其打印到控制台后,我们通过ChannelHandlerContext获取到与当前服务端连接的客户端channel,并通过这个channel将我们服务端要发送的数据传给客户端。

3. 客户端

客户端启动类

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap
                // 1.指定线程模型
                .group(workerGroup)
                // 2.指定 IO 类型为 NIO
                .channel(NioSocketChannel.class)
                // 3.添加自定义handler,
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new FirstClientHandler());
                    }
                });
        // 4.建立连接
        bootstrap.connect("127.0.0.1", 8002).addListener(future -> {
            if (future.isSuccess()) {
                System.out.println("连接成功!");
            } else {
                System.err.println("连接失败!");
            }
        });
    }
}

客户端自定义FirstClientHandler

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.nio.charset.Charset;
import java.util.Date;

public class FirstClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("客户端发送消息...");
        // 1. 获取数据
        ByteBuf buffer = getByteBuf(ctx);
        // 2. 写数据并将数据刷出
        ctx.channel().writeAndFlush(buffer);
    }

    private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
        // 1. 获取二进制抽象 ByteBuf
        ByteBuf buffer = ctx.alloc().buffer();
        // 2. 准备数据,指定字符串的字符集为 utf-8
        byte[] bytes = ("我是客户端, 我传数据给你啦" + new Date()).getBytes(Charset.forName("utf-8"));
        // 3. 填充数据到 ByteBuf
        buffer.writeBytes(bytes);
        return buffer;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf byteBuf = (ByteBuf) msg;
        //接收服务端的消息并打印
        System.out.println(byteBuf.toString(Charset.forName("utf-8")));
    }
}

客户端的FirstClientHandler也是同样继承ChannelInboundHandlerAdapter类,只是实现的方法不同,我们通过实现void channelActive(ChannelHandlerContext ctx)方法, 这个方法在Channel通道被激活时被Netty调用,我们通过这个方法向服务端传送数据。

这样就成功构建了一个Netty服务端与客户端互相通信的样例了。后面都是围绕这个样例实现新的功能。

【Netty入门02】 Netty空闲检测原理解析以及基于空闲检测的心跳机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值