Netty系列之——初探Netty

1. 什么是Netty

1.1 定义

Netty实质上是一个Java网络编程框架。其利用Java高级网络编程的能力,隐藏背后复杂性提供易于使用API的服务器(包括客户端)框架。

那它有什么特点呢?Netty是一个基于异步与事件驱动的网络应用程序框架。它可以用来快速并且简单地开发出高性能易维护的服务器和客户端。

它可以用来做什么呢?使用Netty,你可以实现自己的HTTP服务器、UDP服务器、RPC服务器、WebSocket服务器,在后边的系列内容中我会讲解如何自己实现这些服务器。

如果你进行过Java Web开发,对Tomcat一定不陌生吧!

你可能会想?Netty和Tomcat有什么区别?既然Java已经有Tomcat可以让我们进行Web开发,那Netty有什么其他作用呢?其实,Tomcat是基于HTTP协议的,实际上是一个基于HTTP协议的Web容器;而Netty则可以通过编程自定义各种协议,通过codec(编解码器)自己来编码/解码字节流,从而实现各类的服务器。

1.2 特性

对于传统底层的socket网络编程,由于处理客户端socketI/O操作是阻塞,因此我们需要创建新的子线程来处理客户端的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author Pushy
* @since 2018/11/8 20:16
*/
public class SocketServer {

public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
// 创建新的线程处理客户端连接
new ServerSockThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

这样做的确达到了同时接受多客户端请求的目的,但是过多的创建和销毁子线程开销以及频繁的上下文切换的操作,在更大量的并发条件下,将会严重影响到服务器的性能。

如果你对Java的concurrent模块有所了解,你可能会使用线程池可以缓存和重用Thread,来解决线程开销的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author Pushy
* @since 2018/11/8 20:21
*/
public class ThreadPoolSocket {

// 创建一个可无限扩大的线程池
private static final ExecutorService threadPool = Executors.newCachedThreadPool();

public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
final Socket socket = serverSocket.accept();
// 让线程池来处理客户端的socket连接逻辑
threadPool.execute(() -> {
ServerHandler.handlerRequest(socket);
});
}
}
}

虽然池化和重用线程相对于无限创建线程是一种进步,但是并不能消除由上下文切换所带来的开销。而在Netty中这一切都被迎刃而解。

1.2.1 异步非阻塞

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。

传统的BIO处理流程是:单个子线程处理一个客户端的所有I/O操作:

而在Netty的NIO多路复用的模型中,一个I/O线程可以并发处理N个客户端连接和读写操作:

可以看到,NIO相对于BIO多了一个组件——Selector(多路复用选择器)。Selector处理的流程为:当一个Socket建立好之后,Thread会将这个情况交给Selector,Selector会不断地去遍历所有的Socket,一旦有Socket建立完成将会通知Thread处理,然后Thread处理完数据再返回给客户端。伪代码如下:

1
2
3
4
5
6
7
8
while (true) {
events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠
for (event in events) {
if (event.isAcceptable) {
doAccept() // 新链接来了
}
}
}

1.2.2 简单API

让我们来看一个使用Netty创建的最简单echo服务器,它所做的任务非常简单,就是将客户端发送的消息打印出来。但是通过Netty提供的简单API,短短几十行代码就能实现高性能的异步服务器:

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
/**
* @author Pushy
* @since 2018/11/8 20:24
*/
public class EchoServer {

public static void main(String[] args) throws InterruptedException {
// 1. 创建一个事件循环组,用于存放所有的事件循环
NioEventLoopGroup group = new NioEventLoopGroup();
// 2. 创建服务端引导
ServerBootstrap b = new ServerBootstrap();
b.group(group) // 添加事件循环组
.channel(NioServerSocketChannel.class) // 指定要使用的Channel实现
// 3. 设置用于处理已被接受的Channel I/O及数据的ChannelHandler
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// 简单地打印出客户端发送的消息
System.out.println("Received =>" + msg.toString(CharsetUtil.UTF_8));
}
});

// 4. 通过配置好的ServerBootstrap同步绑定到本地的端口
ChannelFuture f = b.bind("localhost", 8080).sync();
// 5.同步等待服务器的socket关闭
f.channel().closeFuture().sync();

// 6. 当服务器关闭之后,释放所有的资源,并且关闭正在使用中的channel
group.shutdownGracefully();
}

}
  1. 首先我们创建NioEventLoopGroup,用于存放EventLoopEventLoop被分配给一个或多个Chanenl,被给定的Channel的I/O操作都是在相同EventLoop所绑定Thread上执行的。
  2. 创建ServerBootstrap配置它,ServerBootstrap引导在网络编程的作用是:将进程绑定到一个本地的端口。
  3. 设置用于处理已被接受的Channel I/O及数据的ChannelHandler,在这里我们实现自己的业务逻辑代码。
  4. 通过引导将该服务器进程同步绑定(由于bind是一个异步过程,调用sync等待执行完成)到本地的8080端口监听客户端的请求,执行方法后会返回一个ChannelFuture对象,简单来说,它提供了另一种在操作完成时通知应用程序的方式。
  5. 获得Channel的closeFuture阻塞等待关闭,在服务器Channel关闭时closeFuture会完成。
  6. 通过优雅地方来在程序关闭后释放所有的资源。

这样,一个简单Echo服务器就实现了。我们使用客户端(代码见Echoclient)连接,并在成功连接服务器后发送一条“Hello World”。服务器在会接受到并打印出客户端发送的消息:

TIM截图20181109134534.png

怎么样?使用Netty提供的简单API开发出自己的服务器是不是很简单?除此之外,Netty还提供了很多编解码器,让你开发出HTTP服务器、WebSocket服务器都能够快速简单地开发!了解了Netty的基本含义和特性,我们再来了解下Netty中的基本组件。

2. 基本组件

2.1 Channel

Channel即数据传输流,表示一个连接。可以理解为每一个请求,就是一个Channel。类似Java网络编程中的迎接套接字Socket对象。

Channel提供了Java底层的Socket编程所提供的API,例如bind()connect()read()write()等操作。大大降低了直接只用Socket类的复杂性。Channel拥有许多预定义的、专门化实现的广泛类层次结构的根,如NioSocketChannelNioDatagramChannel等。

1
2
3
4
5
Channel channel = ...
// write
channel.writeAndFlush(Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));
// reade
channel.read();

2.2 ChannelHandler

ChannelHandler充当了所有处理入站和出站数据的应用程序逻辑的容器。用于处理业务请求,核心的业务都聚集在这。

ChannelHandler实现类中定义了生命周期中许多方法,该方法都是由网络事件来触发。例如:

  • ChannelRegistered:当Channel被注册到EventLoop中触发;
  • ChannelActive:当Channel成功连接到远程节点时触发;
  • ChannelRead:当从Channel读取数据时触发;

Netty中定义了常用的几个子接口。

2.2.1 ChannelHandlerAdapter

ChannelHandlerAdapter实现了ChannelHandler,提供了部分方法的实现。因此我们只需要继承ChannelHandlerAdapter,重写需要处理的生命周期方法即可:

1
2
3
4
5
6
7
8
9
10
11
/**
* @author Pushy
* @since 2018/11/9 19:30
*/
public class EchoChannelHandler extends ChannelHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("received => " + msg);
}
}

在过去的Netty版本中,由ChannelInboundHandlerAdapterChannelOutboundHandlerAdapter组成。前者用于处理入站数据,一般用来读取客户端数据、进行业务处理等;后者用来处理出站的数据,一般用来发送报文到客户端。现在这两个类都被标记为@Deprecated,官方说明通过ChannelHandlerAdapter来代替。

2.2.2 SimpleChannelInboundHandler

SimpleChannelInboundHandler继承自SimpleChannelInboundHandler,可以通过泛型指定入站的消息类型,处理入站数据我们只需要重新实现messageReceived方法即可。我们通常用它来接受解码消息,例如接受通过HttpServerCodec编解码器解码之后的消息:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 用于处理HTTP请求的ChannelHandler
* @author Pushy
* @since 2018/11/9 19:39
*/
public class HttpChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

@Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
System.out.println("Request =>" + msg.uri() + msg.method());
}
}

另外,SimpleChannelInboundHandler在入站被读取之后会自动释放资源。所以注意不要存储该数据的引用,因为这些引用都将会无效,并且如果你想传递给下一个ChannelHandler也是没有作用的。

2.3 ChannelPipeline

ChannelPipeline是一个拦截流经Channel的入站和出站事件的ChannelHandler 实例链ChannelPipeline类似一个U型管道,从一端(称为入站)流向终点,到达终点后从另一端(称为出站)流出:

TIM截图20181022202320.png

每一个新创建的Channel都将会被永久性地分配一个新的ChannelPipeline。如果一个入站事件被触发,它将被从ChannelPipeline的头部开始一直传播到ChannelPipeline的尾端。

ChannelPipeline提供方法,调用这些方法ChannelHandler可以添加、删除或者替换其他的ChannelHandler,从而改变ChannelPipeline的管道布局:

1
2
3
4
5
6
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec()); // 用于编解码HTTP报文
pipeline.addLast(new HttpObjectAggregator(512 * 1024)); // 聚合HTTP消息

pipeline.addLast(new EchoChannelHandler()); // 打印出客户端消息
pipeline.addLast(new HttpServerHandler()); // 处理客户端具体业务代码

在上面的管道布局中,入站数据首先会被HttpServerCodec编解码器解码,并通过HttpObjectAggregator聚合成HTTP消息对象。然后流经到EchoChannelHandler打印出聚合后的消息,并传递给HttpServerHandler处理客户端的数据的业务代码。

2.4 ChannelHandlerContext

如果你有去观察,会发现ChannelHandler中的每个生命周期方法都会一个参数ChannelHandlerContext。它代表了ChannelHandlerChannelPipeline之前的关联,使得ChannelHandler能够和它的ChannelPipeline以及其他的ChannelHandler进行交互,

TIM截图20181022204303.png

ChannelHandlerContext提供了丰富的用于处理事件和执行I/O操作的API(更多的API可以看Interface ChannelHandlerContext),例如:

  • write():将消息写入并经过ChannelPipeline
  • writeFlush():写入并冲刷消息并经过ChannelPipeline
  • fireChannelRead():触发下一个ChannelHandlerChannelRead()方法。
1
2
3
4
5
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.writeAndFlush(msg); // 冲刷
ctx.fireChannelRead(msg); // 转发
}

2.5 Eventloop

Eventloop即事件循环,用于处理连接的生命周期中所发生的事件。下图展示出了ChannelEventLoopThread 以及 EventLoopGroup下关系:

TIM截图20181109210708.png

从图中可以发现:

  • 一个 EventLoopGroup 包含一个或者多个 EventLoop
  • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop
  • 一个 EventLoop 可能会被分配给一个或多个 Channel

在这种模型下,一个给定的Channel的I/O操作都是由相同的Thread执行的。

2.6 Bootstarp

2.6.1 引导

引导(Bootstrap)一个应用程序是指对它进行配置,并使它运行起来的过程。Netty的引导类为应用程序的网络层配置提供了容器。有将一个进程绑定到某个指定的
端口的ServerBootstrap和将一个进程连接到另一个运行在某个指定主机的指定端口上的进程的Bootstrap:

类别 Bootstrap ServerBootstrap
网络编程中的作用 连接到远程主机和端口 将进程绑定到一个本地的端口
EventLoopGroup的数目 1 2(或1个)
使用端点 客户端/UDP 服务端

这两个类都继承自AbstractBootstrap,继承了许多方法可以进行配置:

  • group():设置用于处理Channel的I/O事件的EventLoopGroup
  • channel():设置Channel的实现类;
  • handler():设置被添加到ChannelPipelineChannelHandler
  • option():设置ChannelOption,配置一些信息。
1
2
3
4
5
6
7
8
9
10
EventLoopGroup bossGroup = new NioEventLoopGroup();   // boss 接受传入的连接
EventLoopGroup workerGroup = new NioEventLoopGroup(); // worker 处理已经被接收的连接

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
// 设置Channel的实现类为NioServerSocketChannel
// 如果设置为OioServerSocketChannel则该服务器为阻塞的
.channel(NioServerSocketChannel.class)
.handler(new EchoChannelHandler())
.option(ChannelOption.SO_BACKLOG, 128); // 设置发送和接受缓冲区大小

2.6.2 引导多个ChannelHandler

在前面的代码中都是通过handler方法设置一个ChannelHandler。如果我们想通过在ChannelPipeline中将它们链接起来,部署更多的ChannelHandler。那么则可以使用Netty提供的一个特殊的ChannelHandlerAdapter抽象子类ChannelInitializer

它只定义了一个抽象方法initChannel(C ch),我们只需要简单地向 BootstrapServerBootstrap 的实例提供你的 ChannelInitializer实现即可,并且一旦 Channel 被注册到了它的 EventLoop 之后,就会调用你的initChannel()版本。在该方法返回之后,ChannelInitializer 的实例将会从 ChannelPipeline
中移除它自己。

1
2
3
4
5
6
7
8
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
.option(ChannelOption.SO_BACKLOG, 128);

Netty的基本组件就介绍到这了,这篇博文的大部分图文来自《Netty实战》一书,对于入门来说确实是一本不错的书籍,推荐大家可以看下,书本大概200多页,没多久就看完了。在后面的文章中,我将会带着大家实践,写出自己的服务器。拜拜!

诶!诶!是不是忘了什么?

对了!老规矩,Github链接奉上——netty-start

Pushy wechat
欢迎订阅我的微信公众号