vlambda博客
学习文章列表

Netty 笔记-第一个 Netty 程序

重磅干货,第一时间送达


源代码仓库:github.com/zhshuixian/netty-notes
博客:blog.csdn.net/u010974701

这里将编写一个简单的 Netty 程序 Ping-Pong(乒乓球) ,客户端向服务端发送一个信息,服务端将此信息返回给客户端。

这里 demo 项目使用 Maven,使用 Gradle 只需要引入相关依赖即可,如果网络的原因无法下载相关依赖,可以切换为国内的镜像源。

项目环境

  • IDEA 或者 Eclipse (IDE)
  • Maven 或者 Gradle (构建工具)
  • JDK 1.8 或者 11

1、第一个 Netty 程序

新建项目 01-ping-pong,引入如下依赖,以下分别为 Maven 和 Gradle 依赖配置。

<dependencyManagement>
   <dependencies>
     <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-all</artifactId> <!-- Use 'netty-all' for 4.0 or above -->
         <version>${netty.version}</version>
         <!-- 这里使用 4.1.50.Final -->
         <!--Netty 4.1.30.Final及更新版本支持 JDK 11 -->
      </dependency>
   </dependencies>
</dependencyManagement>
// https://mvnrepository.com/artifact/io.netty/netty-all
compile group: 'io.netty'name: 'netty-all'version: '4.1.50.Final'

1.2、编写服务端

编写 Pong 服务器,需要实现如下内容,在项目的 server 包下:

  • ChannelHandler,编写服务器的业务逻辑,即对从客户端读取的数据并进行处理。
  • 将服务器绑定到监听端口上,并指定 Handler 处理具体业务逻辑

ChannelHandler 负责接收并相应事件通知,Pong 服务器需要相应客户端传入的数据,所以需要实现  ChannelInboundHandler 接口用于响应客户端的请求。在这里使用 ChannelInboundHandlerAdapter ,它是 ChannelInboundHandler 默认实现。只需要继承其并覆写一些方法即可。

ChannelHandler 在 Netty 中分为了两类,入站事件处理和出站事件处理:

  • ChannelInboundHandler 是入站处理事件的接口,默认实现是 ChannelInboundHandlerAdapter
  • ChannelOutboundHandler 是出站处理事件的接口,默认实现是 ChannelOutboundHandlerAdapter

代码:PongServerHandler.java

/**
 * 接受和响应事件通知,实现具体的业务逻辑,ChannelHandler,ChannelHandler 是绑定在 ChannelPipeline 中,
 * ChannelPipeline 可以绑定一个或者多个 ChannelHandler ,定义为经过 Channel 的入站和出站的一系列逻辑处理
 * Chanel 建立的时候会绑定一个 ChannelPipeline 中,
 * ChannelHandlerContext 是管理和关联 ChannelPipeline 中 ChannelHandler 之间的交互
 */

@Sharable // 表示可以被多个 Channel 共享
public class PongServerHandler extends ChannelInboundHandlerAdapter {

    /** 当 Channel 中的数据读取成功后调用 */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 从 ByteBuf 读取数据,msg 从 Channel 读取到的数据
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server Received: " + in.toString(CharsetUtil.UTF_8));
        // 写入消息到 ChannelHandlerContext 中
        ctx.write(in);
    }

    /** Channel 上一个读取操作完全完成后调用 */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // 将 ctx 中的数据写入并冲刷到远程节点,这是异步的
        // 返回的是一个 ChannelFuture
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    /** 发生异常时候调用 */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 打印异常和关闭 ChannelHandlerContext
        cause.printStackTrace();
        ctx.close();
    }

    /** 新的客户端连接建立的时候会调用这个方法,可以不覆写 */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        // 从 ChannelHandlerContext 获取 Channel 的信息等
        System.out.println("新建立的链接,客户端地址是 " + ctx.channel().remoteAddress());
    }
}

代码解析:结合代码注释和下图,理解 ChannelHandlerContext、Channel 、ChannelHandler、ChannelPipeline 之间的关系,其中 ChannelHandler 包括了入站处理事件和出站处理事件。

image.png

上面的代码只是定义了一个 ChannelHandler,还需要 Bootstrap (引导)将 ChannelHandler 和 ChannelPipeline  组合起来,以及监听服务器端端口号。

代码:PongServer.java

public class PongServer {
    private final int port;

    public PongServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws InterruptedException {
        int port = 8080;
        new PongServer(port).start();
    }

    public void start() throws InterruptedException {
        // Handler 处理的类实例化
        final PongServerHandler serverHandler = new PongServerHandler();
        // Netty 会为每个 Channel 分配一个 EventLoop,一个 EventLoop 可以分配给多个 Channel
        // 这里使用 NIO 的 EventLoop
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 引导,功能是将 ChannelHandler,ChannelPipeline、EventLoop 组织起来,
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(group)
                    // 使用 NIO 的 Channel
                    .channel(NioServerSocketChannel.class)
                    // 绑定的端口号
                    .localAddress(new InetSocketAddress(this.port))
                    // 绑定 Handler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // 将 serverHandler 绑定到 ChannelPipeline
                            ch.pipeline().addLast(serverHandler);
                        }
                    });
            // 每个 Netty 出站 I/O 操作返回 ChannelFuture
            // 核心的bind()方法,用于监听端口和新建一个 ServerSocketChannel
            ChannelFuture future = bootstrap.bind();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

代码解析:Netty 将 Selector 从应用程序中抽取出来,即为 EventLoop,使用 Netty 的时候无需自己手动实现 Selector 来处理 Channel。一个 EventLoop 对应的一个线程,EventLoop 主要的功能是:

  • 将事件派发给 ChannelHandler
  • 注册 Channel,以及注册监听事件
  • 负责 I/O  操作

https://www.cnblogs.com/leesf456/p/6902636.html EventLoop 的参考和扩展

Bootstrap(客户端) 和 ServerBootstrap(服务端) 是 Netty 的引导类,

  • 服务端还需要指定监听端口、使用 bind() 方法开始监听
  • 客户端需要指定服务端 IP 地址和服务端口号,使用 connect() 方法连接到服务端
  • 两者相同的是绑定 ChannelPipeline  和 ChannelHandler 等一些组件

1.2、编写客户端

使用 Netty 编写 Ping 客户端跟服务端差不多,

  • ChannelHandler,编写客户端的业务逻辑,即向服务端发送和接受服务端返回的数据。
  • 连接到服务器,并指定 Handler 处理具体业务逻辑

代码:PingClientHandler.java,代码跟 PongServerHandler.java  差不多

@Sharable
public class PingClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 使用 UTF-8 编码,ByteBuf Netty 的数据容器
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty!", CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        String string = in.toString(CharsetUtil.UTF_8);
        System.out.println("Client Received: " + string);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.close();
    }
}

代码:PingClient.java,代码解析见注释和 PongServer.java

public class PingClient {


    public static void main(String[] args) throws InterruptedException {
        new PingClient().start();
    }

    public void start() throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 客户端使用 Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            int port = 8080;
            String host = "127.0.0.1";
            // 通道使用 NioSocketChannel
            bootstrap.group(group).channel(NioSocketChannel.class)
                    // 远程服务器路径和端口号
                    .remoteAddress(new InetSocketAddress(host, port))
                    // Channel 通道的绑定 ChannelPipeline
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 绑定 PingClientHandler
                            ch.pipeline().addLast(new PingClientHandler());
                        }
                    });
            // 链接到服务端和使用 ChannelFuture 接收返回的数据
            ChannelFuture future = bootstrap.connect().sync();
            future.channel().closeFuture().sync();

        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

1.3、运行项目

运行 PongServer 的 mian 方法后,运行 PingClient 的 mian 方法,在输出的窗口查看结果即可。服务端输出:

新建立的链接,客户端地址是 /127.0.0.1:3532
Server Received: Hello Netty!

客户端输出:

Client Received: Hello Netty!

通过以上的简单的 Ping-Pong (客户端-服务端)的通信,可以看出 Netty 比起原生 Java NIO 的 API 简洁的多,而且为了保证 Java NIO 的应用程序的正确性也不容易。

下一节,将使用 Netty 构建一个简单的 HTTP Web 服务器。