netty入门之EchoServer

学习一门新的语言,通常是以 HelloWorld 开始的。类似地,学习一个网络框架,通常是以 EchoServer 开始的。接下来我们就来看下如何通过 netty 实现一个 EchoServer。

动手实现一个EchoServer

EchoServer 是一个最简单的 server,它接收请求后将读取到的信息全部原样输出。对于客户端来说,输入一段文字后,服务端返回的还是这段文字,就像一个回音墙一样。

1
2
3
4
5
Client Request: Hello
Server Response: Hello

Client Request: How are you?
Server Response: How are you?

实现一个 EchoServer 的主要步骤如下:

  1. 搭建项目,引入 netty 依赖
  2. 实现 EchoServerHandler 类,用于处理入站事件,对于读取到的请求信息,全部原样输出
  3. 实现 EchoServer 类:配置 ServerBootstrap 的各项参数,指定 childHandler 的初始化方式,然后绑定到指定地址+端口启动服务

引入netty依赖

搭建好项目后,首先要引入 netty 依赖,你可以简单地加入 netty-all 依赖,不用关心具体是依赖 netty 的哪个模块,这里我们选择的是 4.1.51.Final 版本。

1
2
3
4
5
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.51.Final</version>
</dependency>

实现EchoServerHandler类

EchoServerHandler 用于在读到数据时进行相应处理,它关心的事件是 read 和 readComplete,这些事件均为入站(Inbound)事件,因此 EchoServerHandler 需要实现 ChannelInboundHandler 接口。

入站事件不只包括上述两种事件,还包括 channelRegistered、channelActive、channelWritabilityChanged 等,如果我们直接实现 ChannelInboundHandler 接口,那么就意味着要为每个入站事件提供一个对应的实现,这显然不是一个好的方式。

幸运地是,netty 提供了 ChannelInboundHandlerAdapter,它为所有入站事件提供了默认实现,若要改变处理某个事件的行为,我们只需要针对该事件进行处理。因此我们选择继承 ChannelInboundHandlerAdapter 类。

EchoServerHandler 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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) {
// Close the connection when an exception is raised.
cause.printStackTrace();
// 当出现异常情况则关闭channel
ctx.close();
}
}
  1. channelRead

    读取到数据时,将数据写入到输出缓冲区

  2. channelReadComplete

    数据读取完毕,将缓冲区的内存输出到网络

  3. exceptionCaught

    若处理过程中出现异常,则关闭channel

实现EchoServer类

EchoServer 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public final class EchoServer {

static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

public static void main(String[] args) throws Exception {

// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 初始化channelHandler时,将EchoServerHandler添加到pipeline上
p.addLast(serverHandler);
}
});

// Start the server.
ChannelFuture f = b.bind(PORT).sync();

// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
  1. 配置 bossGroup 和 workerGroup

    bossGroup 用于处理连接请求,配置线程数为 1。workerGroup 用于已建立的连接,线程数配置为与 cpu 数相关。针对网络 IO 模型,我们选择的是 NIO,故 bossGroup 和 workerGroup 的实现为 NioEventLoopGroup

  2. 配置 channel

    前面已经选择好了 IO 模型,channel 也要与之配套,故 channel 的实现类是 NioServerSocketChannel.class

  3. 配置 option

    option 有很多配置项,可根据需求配置。对于 EchoServer 来说,可以全部使用默认值,即什么也不配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static final ChannelOption<Boolean> AUTO_CLOSE = valueOf("AUTO_CLOSE");

    public static final ChannelOption<Boolean> SO_BROADCAST = valueOf("SO_BROADCAST");
    public static final ChannelOption<Boolean> SO_KEEPALIVE = valueOf("SO_KEEPALIVE");
    public static final ChannelOption<Integer> SO_SNDBUF = valueOf("SO_SNDBUF");
    public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");
    public static final ChannelOption<Boolean> SO_REUSEADDR = valueOf("SO_REUSEADDR");
    public static final ChannelOption<Integer> SO_LINGER = valueOf("SO_LINGER");
    public static final ChannelOption<Integer> SO_BACKLOG = valueOf("SO_BACKLOG");
    public static final ChannelOption<Integer> SO_TIMEOUT = valueOf("SO_TIMEOUT");

    public static final ChannelOption<Integer> IP_TOS = valueOf("IP_TOS");
    public static final ChannelOption<InetAddress> IP_MULTICAST_ADDR = valueOf("IP_MULTICAST_ADDR");
    public static final ChannelOption<NetworkInterface> IP_MULTICAST_IF = valueOf("IP_MULTICAST_IF");
    public static final ChannelOption<Integer> IP_MULTICAST_TTL = valueOf("IP_MULTICAST_TTL");
    public static final ChannelOption<Boolean> IP_MULTICAST_LOOP_DISABLED = valueOf("IP_MULTICAST_LOOP_DISABLED");

    public static final ChannelOption<Boolean> TCP_NODELAY = valueOf("TCP_NODELAY");
    public static final ChannelOption<Boolean> TCP_FASTOPEN_CONNECT = valueOf("TCP_FASTOPEN_CONNECT");
  4. 配置 handler

    对于每一个连接请求,我们可以将请求的事件打印出来,因此这里添加了 一个 LoggingHandler

  5. 配置 childHandler

    当连接建立后,我们需要处理读写事件了,这里就需要把 EchoServerHandler 添加上去了。netty 支持添加多个 handler 到 pipeline 上,它是通过 ChannelInitializer 实现的,我们只需要按照下面的方式添加即可。

    1
    2
    3
    4
    5
    6
    7
    8
    .childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
    ChannelPipeline p = ch.pipeline();
    // 初始化channelHandler时,将EchoServerHandler添加到pipeline上
    p.addLast(serverHandler);
    }
    });

验证效果

至此,我们已经完成了一个 EchoServer。如何验证呢?我们可以使用 telnet (Telecommunications Network)命令

1
2
- Telnet to a specific port of a host:
telnet ip_address port

执行 EchoServer 的 main 方法启动 server,可以看到如下输出,说明服务已经启动成功

1
2
3
15:12:04.140 [nioEventLoopGroup-2-1] INFO  i.n.handler.logging.LoggingHandler - [id: 0x99a8234b] REGISTERED
15:12:04.146 [nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0x99a8234b] BIND: 0.0.0.0/0.0.0.0:8007
15:12:04.151 [nioEventLoopGroup-2-1] INFO i.n.handler.logging.LoggingHandler - [id: 0x99a8234b, L:/0:0:0:0:0:0:0:0:8007] ACTIVE

输入 telnet localhost 8007 连接到 server

1
2
3
Trying ::1...
Connected to localhost.
Escape character is '^]'.

接下来我们就可以输出文字并查看效果了,server 会返回你所输入的内容

1
2
How are you?
How are you?

若要在 server 端打印读取到的字符串,则可以对 EchoServerHandler 的 channelRead 方法进行修改

1
2
3
4
5
6
7
8
9
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ByteBuf) {
byte[] bytes = ByteBufUtil.getBytes((ByteBuf) msg);
System.out.println("read from client: " + new String(bytes));
}
// // 将读取到的信息写入到缓冲区
ctx.write(msg);
}

当你在 telnet 窗口中输入 How are you? 时,你将会在 server 端的控制台输出中看到如下信息

1
read from client: How are you?

参考

Echo server