构建第一个Netty应用:Echo服务器与客户端

1. 构建第一个Netty应用:Echo服务器与客户端

1.1. 简单的 Echo 服务器

这里,我们直接使用Netty作为独立的进程启动

1.1.1. Netty 依赖

maven依赖如下:

<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.2.7.Final</version>
</dependency>

1.1.2. Netty Server

这里我们直接通过Netty监听8080端口,作为服务端启动:

  • EchoServer.java
import com.huawei.athena.netty.handler.EchoServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
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;

import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
public class EchoServer {
    private final int port = 8080;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;
    private ChannelFuture serverChannelFuture;
    private final AtomicBoolean running = new AtomicBoolean(false);

    public void start() {
        if (!running.compareAndSet(false, true)) {
            log.info("the server is running");
            return;
        }
        // 创建事件循环组
        bossGroup = new NioEventLoopGroup();  // 用于接受连接
        workerGroup = new NioEventLoopGroup(); // 用于处理连接

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // 使用NIO传输通道
                    .option(ChannelOption.SO_BACKLOG, 100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)          // 服务器套接字选项
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // 客户端套接字选项

            // 绑定端口并开始接收连接
            serverChannelFuture = b.bind(port).sync();
            log.info("Echo服务器启动,监听端口: {}", port);

            // 等待服务器套接字关闭
        } catch (InterruptedException e) {
            log.error("can't start the netty server, ", e);
            stop();
        }
    }

    public void stop() {
        if (!running.compareAndSet(true, false)) {
            log.info("the server is running");
            return;
        }
        log.info("Shutting down Echo server...");
        // 先关闭服务器通道
        if (serverChannelFuture != null) {
            serverChannelFuture.channel().close().awaitUninterruptibly();
        }

        // 优雅关闭EventLoopGroup
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
        }
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
        }

        log.info("Echo server shutdown complete");
    }
}

1.1.3. 服务端回显处理

这里,我们实现了一个简单的业务逻辑,即收到客户端的数据后,不做任何处理,直接原样发送给客户端:

import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

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

1.1.4. 主程序

public class NettyApp {
    public static void main(String [] args) {
        new EchoServer().start();
    }
}

1.1.5. 验证

服务端启动进程NettyApp后,我们可以通过telnet命令与服务器交互:

telnet localhost 8008

如果一切正常,当你启动服务器后,在控制台上输入字符后,你应该能看到它原样的输出,类似如下打印:

12233445566778899

1.2. 简单的 Echo 客户端

1.2.1. Echo 客户端

这里我们为了介绍Netty的客户端的编程方式,我们使用Netty来构建一个Echo客户端应用程序,其代码如下:

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;
import io.netty.util.CharsetUtil;

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new EchoClientHandler());
                        }
                    })
                    .option(ChannelOption.TCP_NODELAY, true);

            // 连接服务器
            ChannelFuture future = bootstrap.connect(host, port).sync();
            System.out.println("连接到Echo服务器 " + host + ":" + port);
            
            // 从控制台读取输入
            Channel channel = future.channel();
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            
            while (true) {
                String input = reader.readLine();
                if (input == null || "quit".equalsIgnoreCase(input)) {
                    break;
                }
                channel.writeAndFlush(input);
            }
            
            // 等待连接关闭
            channel.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    // 客户端处理器
    private static class EchoClientHandler extends ChannelInboundHandlerAdapter {
        
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String message = (String) msg;
            System.out.println("服务器响应: " + message);
        }

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

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("已连接到服务器");
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("与服务器断开连接");
        }
    }

    public static void main(String[] args) throws Exception {
        String host = "localhost";
        int port = 8080;

        if (args.length > 0) {
            host = args[0];
        }
        if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

        new EchoClient(host, port).start();
    }
}

1.2.2. 验证方式

为了测试服务器和客户端,测试步骤如下:

  • 先启动Echo服务器;

  • 再启动Echo客户端;

  • 等待客户端启动后,在客户端控制台上输入任意字符串;

    客户端会接收控制台输入,并将客户端控制台的输入字符串发送到服务器,服务器接收到后,再原样发送回客户端。

程序运行的示例如下:

已连接到服务器
连接到Echo服务器 localhost:8080
123
服务器响应: 123
321
服务器响应: 321

2. 程序架构

2.1. 架构原理

整体的架构原理如下:

deepseek_mermaid_20251109_7a173e

图中的虚线代表数据流的走向。

这里我们简单介绍一下服务器和客户端的核心组件:

  • 服务器端组件

    • ServerBootstrap: 服务器启动引导类

    • Boss Group: 接受客户端连接(1个线程)

    • Worker Group: 处理网络I/O(多个线程)

    • ChannelPipeline: 处理器链

    • ChannelHandler: 业务逻辑处理器

  • 客户端组件

    • Bootstrap: 客户端启动引导类

    • EventLoopGroup: 处理所有I/O操作

    • ChannelPipeline: 与服务器相同的处理器链结构

这里我们就不详细展开了,后面的章节,会深入地介绍这些核心组件。

2.2. 通信时序

这个简单的Echo示例的交互时序如下:

deepseek_mermaid_20251109_ee925a

这里,我们只需要知道,服务器启动后,当客户端运行后,就会立即与服务器建立一个TCP的长连接,后续的消息发送都在这条长连接上进行,服务端由EchoServerHandler处理客户端的网络数据(原样返回客户端的消息),这样,我们就在客户端能接收到服务器的回显内容了。

这里,需要注意的是,每一个新的TCP连接建立后,Netty都会重新创建一个新的 Channel Pipeline 用于处理网络数据。

3. 总结

这个示例简单展示了 Netty 基于事件驱动和 Reactor 模式的高性能网络编程原理,通过清晰的组件分离和流水线处理机制,实现了高效可靠的网络通信。

posted @ 2025-11-08 14:14  LARRY1024  阅读(10)  评论(0)    收藏  举报