Netty 5用户指南中文版
前言
1、传统IO编程
在学习Java基础的网络编程时,我们写过这样的代码
// 在10086端口启动服务监听
ServerSocket ss = new ServerSocket(10086);
System.out.println("服务器正常启动。。。");
while (true) {
// 阻塞,等待客户端接入
Socket socket = ss.accept();
System.out.println("新的用户接入。。。");
// 启动一个新的线程处理客户连接
new ServerThread(socket).start();
}
上面的服务端代码中我们可以看到,在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护。当客户端比较多时,单机服务端可能需要创建大量的线程处理这些连接,这就带来如下几个问题:
- 线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起。
- 线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
- 除了以上两个问题,IO编程中,我们看到数据读写是以字节流为单位,效率不高。
为了解决这三个问题,JDK在1.4之后提出了NIO。
2、NIO编程
更多NIO详细内容请参考之前的博客:https://blog.csdn.net/qq_31142553/article/details/85925718
直接上一段代码来示范一下
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 读取数据以块为单位批量读取
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
很复杂对吧?总结一下
- NIO模型中通常会有两个线程,每个线程绑定一个轮询器selector,在我们这个例子中
serverSelector
负责轮询是否有新的连接,clientSelector
负责轮询连接是否有数据可读 - 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到
clientSelector
上,这样就不用IO模型中1w个while循环在死等,参见(1) clientSelector
被一个while死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过clientSelector.select(1)
方法可以轮询出来,进而批量处理,参见(2)- 数据的读写以内存块为单位,参见(3)
所以不推荐直接使用Java NIO来进行网络开发,原因如下
- 1、JDK的NIO编程需要了解很多的概念,编程复杂,对NIO入门非常不友好,编程模型不友好,ByteBuffer的api简直反人类
- 2、对NIO编程来说,一个比较合适的线程模型能充分发挥它的优势,而JDK没有给你实现,你需要自己实现,就连简单的自定义协议拆包都要你自己实现
- 3、JDK的NIO底层由epoll实现,该实现饱受诟病的空轮训bug会导致cpu飙升100%
- 4、项目庞大之后,自行实现的NIO很容易出现各类bug,维护成本较高,上面这一坨代码我都不能保证没有bug
JDK的NIO犹如带刺的玫瑰,虽然美好,让人向往,但是使用不当会让你抓耳挠腮,痛不欲生,正因为如此,Netty横空出世!
一、Netty简介
直接摘自官网(https://netty.io/):
Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。
Netty是一个NIO客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。它极大地简化并简化了TCP和UDP套接字服务器等网络编程。
“快速简便”并不意味着最终的应用程序会受到可维护性或性能问题的影响。Netty经过精心设计,具有丰富的协议,如FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议。因此,Netty成功地找到了一种在不妥协的情况下实现易于开发,性能,稳定性和灵活性的方法。
二、Netty特征
1、设计
- 适用于各种传输类型的统一API - 阻塞和非阻塞套接字
- 基于灵活且可扩展的事件模型,可以清晰地分离关注点
- 高度可定制的线程模型 - 单线程,一个或多个线程池,如SEDA
- 真正的无连接数据报套接字支持(自3.1起)
2、便于使用
- 详细记录的Javadoc,用户指南和示例
- 没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了
- 注意:某些组件(如HTTP / 2)可能有更多要求。 有关更多信息,请参阅 “要求”页面。
3、性能
- 吞吐量更高,延迟更低
- 减少资源消耗
- 最小化不必要的内存复制
4、安全
- 完整的SSL / TLS和StartTLS支持
5、社区
- 早发布,经常发布
- 自2003年以来,作者一直在编写类似的框架,他仍然觉得你的反馈很珍贵!
三、选Netty弃NIO的原因
- 使用JDK自带的NIO需要了解太多的概念,编程复杂,一不小心bug横飞
- Netty底层IO模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从NIO模型变身为IO模型
- Netty自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑
- Netty解决了JDK的很多包括空轮询在内的bug
- Netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程模型做到非常高效的并发处理
- 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
- Netty社区活跃,遇到问题随时邮件列表或者issue
- Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大
四、 Netty使用案例
1、引入Maven依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
2、服务端实现部分
public class NettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap
.group(boos, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
.bind(8000);
}
}
这么一小段代码就实现了我们前面NIO编程中的所有的功能,包括服务端启动,接受新连接,打印客户端传来的数据,怎么样,是不是比JDK原生的NIO编程优雅许多?
简单了解下
boos
对应IOServer.java
中的接受新连接线程,主要负责创建新连接worker
对应IOClient.java
中的负责读取数据的线程,主要用于读取数据以及业务逻辑处理
3、客户端实现部分
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
while (true) {
channel.writeAndFlush(new Date() + ": hello world!");
Thread.sleep(2000);
}
}
}
在客户端程序中,group
对应了我们IOClient.java
中main函数起的线程,运行main函数,回到NettyServer.java
的控制台,将会看到效果。