七的博客

Netty快速入门系列(二)-HelloWorld

网络通信

Netty快速入门系列(二)-HelloWorld

在了解一些核心概念前,先直接动手实践一个 HelloWorld 例子。HelloWorld 例子非常的简单,就是一个服务端跟客户端之间互相发送一句 【HelloWorld 】。

Netty HelloWorld 流程

1. 环境准备

  • JDK8
  • Netty 4
  • Maven

2. 编写服务端程序

服务端的大概逻辑如下:

  • 创建 bossGroup 来负责接受新的连接。
  • 创建 workerGroup 来处理已建立好的连接后续的 I/O 操作。
  • 创建 ServerBootstrap 实例,指定 channel 类型。
  • 创建 ChannelPipeline ,然后添加协议报文的编码、解码、自定义的业务逻辑处理器。
  • 调用 bind 方法绑定服务到计算机的指定端口,启动服务。
package com.suny.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;


public class NettyServer {

    // 服务端暴露的端口号
    private static final int PORT = 8080;

    public static void main(String[] args) throws Exception {
        // bossGroup 主要是负责接受客户端的连接
        final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // workerGroup 主要是负责处理连接的后续IO操作
        final EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            final ServerBootstrap b = new ServerBootstrap();
            
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            
                            // StringDecoder 将接收到的字节流为字符串
                            p.addLast(new StringDecoder());
                            
                            // StringEncoder 将字符串为字节流以发送
                            p.addLast(new StringEncoder());
                            
                            // 添加自定义的 ServerHandler 来处理后续的业务逻辑
                            p.addLast(new ServerHandler());
                        }
                    });
            
            // 绑定服务到指定的端口并启动
            final ChannelFuture f = b.bind(PORT).sync();
            System.out.println("Netty服务暴露在端口 " + PORT);

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    private static class ServerHandler extends SimpleChannelInboundHandler<String> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            // 处理从客户端发送的消息,打印。 然后将其发送回客户端
            System.out.println("服务端收到客户端的消息: " + msg);
            ctx.writeAndFlush(msg);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            // 处理异常情况
            cause.printStackTrace();
            ctx.close();
        }
    }
}

3. 编写客户端程序

客户端的逻辑跟服务端的基本差不多,只是客户端只有一个 NioEventLoopGroup 去处理 I/O ,不像服务端使用了两个 NioEventLoopGroup 去分别处理连接跟 I/O。

大致逻辑如下:

  • 创建 EventLoopGroup 负责处理所有的I/O操作,包括创建连接和数据传输。
  • 创建 Bootstrap 实例,这是 Netty 客户端的启动类实例。同时指定 channel 类型。
  • 创建 ChannelPipeline ,然后添加协议报文的编码、解码、自定义的业务逻辑处理器。
  • 调用 Bootstrap 的 connect 方法连接到服务端的 IP 和端口。
package com.suny.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;


public class NettyClient {

    // 定义了服务端的 IP 和端口
    private static final String HOST = "localhost";
    private static final int PORT = 8080;

    public static void main(String[] args) throws Exception {
        // 用于处理I/O操作
        final EventLoopGroup group = new NioEventLoopGroup();

        try {
            // Bootstrap 是 Netty客户端的启动类
            final Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            
                            ChannelPipeline p = ch.pipeline();
                            
                            // StringDecoder 将接收到的字节流为字符串
                            p.addLast(new StringDecoder());
                            
                            // StringEncoder用于编码字符串为字节流以发送
                            p.addLast(new StringEncoder());
                            
                             // 添加自定义的 ClientHandler 来处理后续的业务逻辑
                            p.addLast(new ClientHandler());
                        }
                    });

            // 连接到服务器 localhost:8080
            final ChannelFuture f = b.connect(HOST, PORT).sync();

            // 获取 channel 并向服务端发送消息 HelloWorld, Netty!
            final Channel channel = f.channel();
            channel.writeAndFlush("HelloWorld, Netty!");

            channel.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
        System.out.println("Netty 客户端生命周期结束.");
    }

    private static class ClientHandler extends SimpleChannelInboundHandler<String> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            // 处理从服务端接收到的消息,并打印出来
            System.out.println("客户端收到服务端的消息: " + msg);
        }

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

4. 运行示例程序

先运行服务器端的程序 , 再运行客户端的程序。

  • 运行服务端的 NettyServer ,可以看到控制台输出:
  Netty服务暴露在端口 8080
  • 运行客户端的 NettyClient ,可以看到控制台输出:
  客户端收到服务端的消息: HelloWorld, Netty!
  • 同时可以观察到服务端输出:
  服务端收到客户端的消息: HelloWorld, Netty!

这样就完成了一个简单的 Netty 服务端跟客户端的 HelloWorld ,可以看到的是代码量并不多,都是一些模版代码。再复杂的程序基本也就是在这个模版代码的基础上进行完善。