netty是如何工作的:实现自己的第一个netty server

兴趣是最好的老师,当你对一件事情感兴趣的时候,你就更容易长久地坚持下去,从而达到一个更高的高度,或许还能够取得意想不到的效果。接下来我会从实现一个 echo server 开始 netty 的探索旅程,如果你也对 netty 感兴趣,那么我们就开始吧。

正如学习一门语言是从 hello world 开始一样,学习一个网络框架通常是从 echo server 开始的。

echo server 启动后会监听一个指定的端口,等待你连接并输入字符,它要做的就是原封不动地将你的输入返回给你。

1
2
hello // 客户端输入「hello」
hello // server 返回的也是「hello」

要通过 netty 实现一个 echo server,它的核心代码非常简单,简单到只有一行代码,是不是很不可思议

1
ctx.writeAndFlush(msg);

这行代码中 msg 是接收到的 client 端的输入信息,writeAndFlush 则是将指定信息发送给 client

当然我们还要写一些其他的代码,netty 是一个框架,那么我们就要按照框架的约定来写。

netty 将 client 与 server 之间的连接抽象为 channel,通过 ChannelInboundHandler 监听 channel 上的输入事件。

对于 echo server 来说,我们需要监听的是 channelRead 事件,那么只需要对该事件的处理行为进行定义。

为了方便,通常不会选择直接实现 ChannelInboundHandler 接口,而是继承 ChannelInboundHandlerAdapter 类,这样就不用写其他输入事件的实现了。

说到这里,你就可以明白下面这段代码是做什么的了

1
2
3
4
5
6
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.writeAndFlush(msg);
}
}

仅仅有这些还是不够,我们还需要监听指定的端口,接收 client 的连接请求,在建立连接之后才会用到我们的 EchoServerHandler

这一切也是相当简单,只需要按照 netty 的固定模式使用就可以了。我们需要一个 main 方法作为程序的启动入口,这很好理解

1
2
3
4
5
public class EchoServer {
public static void main(String[] args) throws InterruptedException {
// todo
}
}

接下来是创建一个 EchoServerHandler 的实例,我们希望用它来处理所有的请求。因为它本身是无状态的,所以多个连接共享同一个实例是没有问题的。

1
final EchoServerHandler serverHandler = new EchoServerHandler();

接下来需要创建一个启动类,它负责 server 的启动管理

1
ServerBootstrap b = new ServerBootstrap();

我们需要告诉它一些必要的信息,包括要采用哪种 IO 模型实现、对应的 EventLoopGroup(这里可以简单地把它当做线程池来理解)、用于自定义业务处理逻辑的 ChannelHandler

我们计划采用 NIO 模型的 NioServerSocketChannel 系列,对应的 EventLoopGroup 实现是 NioEventLoopGroup,总之这两个配套就可以,不理解也没关系

1
2
3
4
// NioServerSocketChannel和NioEventLoopGroup搭配干活
EventLoopGroup group = new NioEventLoopGroup();
b.group(group) // 配置EvnetLoop
.channel(NioServerSocketChannel.class) // 配置ServerSocketChannel

Server 本身的配置就这些了,已经足够用来启动并建立连接了。别忘了我们是要做 Echo server,还要将 EchoServerHandler 用上,这一切就在这几行代码

1
2
3
4
5
6
7
8
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 把它添加到pipeline上
p.addLast(serverHandler);
}
})

你可以将它理解为固定的模式,其实核心也就只有一行代码 p.addLast(serverHandler)。这段代码的作用是:当 client 与 server 建立连接后,将 serverHandler 添加到连接的 pipeline 链上

接下来就是通过 ServerBootStrap 启动 server 了,我们计划将 server 绑定到 localhost 的 8110 端口,它也只有一行代码:

1
ChannelFuture f = b.bind(8110).sync();

好了,我们所有的代码都写完了,执行 main 方法测试一下吧。

稍等,这里有个问题,我们的 echo server 启动后过一会就直接退出,因为 main 方法执行完了。为了让 server 一直执行,我们再加一行代码,只有当程序告诉它要退出时才会退出

1
f.channel().closeFuture().sync();

启动 server 测试一下吧。直接执行 main 方法,server 已启动。

client 要怎么连呢,我们还没写 client 呢。问题不大,可以通过 nc 连接,这个工具很好用

1
nc localhost 8110

看到了吗,它已经连上 server 等待我们的输入了

1
2
➜  netty git:(4.1) ✗ nc localhost 8110

输入 hello 并回车,它会再打印一个 hello 并且换行,等待下次输入

1
2
3
4
➜  netty git:(4.1) ✗ nc localhost 8110
hello
hello

好了,已经完工了,我把 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
public class EchoServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
// 1.创建一个自定义的handler
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 2.把它添加到pipeline上
p.addLast(serverHandler);
}
});

ChannelFuture f = b.bind(8110).sync();

f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
group.shutdownGracefully();
}
}
}

你可能看到多了一行 group.shutdownGracefully(); ,它是用于优雅退出的,具体做了什么,以后再聊。