一 Netty 是什么?
Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架,是一个用以快速开发高性能、可扩展协议的服务器和客户端。Netty 是一个 NIO 客户端服务器框架,使用它可以快速简单的开发网络应用程序,比如服务器和客户端的协议,Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。
二 为什么要使用 Netty?
我们常用的 Java 的通信模型有同步阻塞 I/O 模型 (BIO)、非阻塞 I/O (NIO)
- 基于 IO 的经典同步阻塞模型 BIO
在 JDK 1.4 之前,基于 Java 的所有 Socket 通信都采用了同步阻塞模型 (BIO),当服务器端的 accept() 得到一个新的连接时,就会开启一个线程来处理这个连接任务。之所以使用多线程,主要原因在于 socket.accept() 、socket.read()、socket.write() 三个函数都是同步阻塞的,当一个连接在处理 I/O 的时候,系统是阻塞的,所以需要开启多线程去处理更多的任务。这个模型的本质问题在于,严重的依赖线程,很消耗资源。
1. 线程的创建和销毁成本很高,创建和销毁都是重量级的系统函数。
2. 线程的切换成本很高。操作系统在发生线程切换时,需要保留线程的上下文,然后执行系统调用,如果线程数过高,可能执行的线程切换时间甚至会大于线程执行的时间,导致 CPU 利用率不高。
3. 容易造成锯齿状的系统负载,因为系统负载是用活动线程数或 CPU 核心数,一旦线程数量高但外部网络环境不是很稳定,就容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
- 基于 NIO 的异步模型
NIO 采用 Reactor 线程模型,一个 Reactor 聚合一个多路复用器 Selector ,它可以同时注册、监听和轮询成白上千个 Channel ,一个 IO 线程可以同时并发处理 N 个客户端连接,线程模型优化为 1:N 。
由于 IO 线程总数有限,不会存在频繁的 IO 线程之间上下文切换和竞争,CPU 利用率高。
所有 IO 操作都是异步的,即使业务线程直接进行 IO 操作,也不会被同步阻塞(如果一个线程不能进行读写,会在 Selector 上注册标记,然后切换到其它就绪的连接来继续进行读写),系统不再依赖外部的网络环境和外部的应用程序的处理性能。
- Netty
NIO 的不足之处:
1. NIO 的类库和 API 相当复杂,使用麻烦,需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
2. 需要具备使用其它额外技能做铺垫,例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
3. 可靠性能力不全,工作量和开发难度都非常大。例如客户端面临断线重连、网络闪断、半包读写、失败缓存、网路拥塞和异常码流的处理等等,NIO 编程的特点是功能开发相对容易,但是可靠性能力不全工作量和难度非常大。
4 NIO存有 BUG,存在 epoll bug,会导致 Selector 空轮询,导致 CPU 利用率低。
Netty 的优势
1. API 使用简单,开发门槛低
2. 功能强大,预置了多种编解码功能,支持主流协议。
3. 定制能力强,可以通过 ChannelHandler 对通信框架进行灵活的扩展。
4. 性能高,通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优。
5. 成熟,稳定,Netty 修复了已经发现的所有 NIO BUG,业务开发人员不需再为 NIO 的 BUG 而烦恼。
6. 有大规模的商业应用考验,质量可靠性有很好的验证。
三 Netty 框架图
- 零拷贝:
在发送数据时,传统的实现方式需要四次数据拷贝和四次上下文切换:a. 数据从磁盘读取到内核的read buffer b. 数据从内核缓冲区拷贝到用户缓冲区 c.数据从用户缓冲区拷贝到内核的 socket buffer d. 数据从内核的 socket buffer 拷贝到网卡接口的缓冲区
所谓的 Zero-copy ,就是指在操作数据时,不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域,因此少了一次内存的拷贝,CPU 的效率就得到了提高。这是一种在OS层面上的 Zero-copy ,目的是避免在用户态和内核态之间来回拷贝数据。Java 类库通过 java.nio.channels.FileChannel 中的transferTo() 方法在 Linux 与 UNIX 系统上支持零拷贝。
Netty 中的 Zero-copy 与上述不同,Netty 中的零拷贝完全是在用户态的,它的零拷贝更多的是偏于优化数据操作。Netty 的接收和发送优先采用 Direct Buffers ,使用堆外内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。
Netty 使用 CompositeByteBuf 类将多个 ByteBuf 合并为一个逻辑上的 ByteBuf ,CompositeByteBuf 是由多个ByteBuf 组合而成,不过在 CompositeByteBuf 内部,这些 ByteBuf 都是单独存在的,CompositeByteBuf 只是一个逻辑上的整体,这样就避免了数据的拷贝,实现了零拷贝。
- 基于拦截链模式的事件模型:
在一个 ChannelPipeline 内部有一个 ChannelEvent 被一组 ChannelHandler 处理,这个管道是 InterceptingFilter (拦截过滤器) 模式的一种高级形式实现,因此对于一个事件如何被处理以及管道内部处理器间的交互过程,我们都拥有绝对的控制能力。
四 Netty 开发实例
接下来开发一个简单的客户端发送消息,服务器应答消息的小程序。点击以下可以下载资源哦,里面包括开发所需的Netty 开发包 https://download.youkuaiyun.com/download/weixin_39453325/10714999 。
总结构:
服务器端:
package Server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class SimpleServer {
public static void main(String[] args) {
try {
new SimpleServer(8000).run();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
};
}
private final int port;
public SimpleServer(int port) {
this.port=port;
}
public void run() throws Exception{
/**
* NioEventLoopGroup 是用来处理 I/O 操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现来处理不同的
* 传输。本例子中我们实现了一个服务端的应用,因此会有2个NioEventGroup 会被使用,一个叫 boss ,用来接受进来的连接,一个叫 worker,
* 用来处理已经被接受的连接,一旦boss接受到连接,就会把连接信息注册到worker上,如何知道多少个线程已经被使用,如何映射到已经创建的
* Channel上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。
*/
EventLoopGroup bossGroup=new NioEventLoopGroup();
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
/**
* ServerBootstrap 是一个启动 NIO 服务的辅助启动类。
*/
ServerBootstrap b=new ServerBootstrap()
.group(bossGroup,workerGroup)
/*这里我们指定使用 NioServerSocketChannel 类来举例说明一个新的 Channel 如何接受进来的连接*/
.channel(NioServerSocketChannel.class)
/*这里的事件处理类经常会被用来处理一个最近的已经接收的Channel。ChannelInitializer 是一个特殊的处理类
* 他的目的是帮助使用者配置一个新的 Channel。也许你想通过增加一些处理类来配置一个新的 Channle 或者其对应的
* ChannelPipeline来实现你的网络程序。当你的程序变得复杂时,可能你会增加更多的处理类到pipeline上,然后
* 提取这些匿名类到最顶层*/
.childHandler(new SimpleServerInitializer())
/*设置指定的 Channel 实现的配置参数,提供给NioServerSocketChannel 用来接收进来的连接*/
.option(ChannelOption.SO_BACKLOG,128)
/*提供给父管道ServerChannel接收到的连接,这个例子中也是NioServerSocketChannel*/
.childOption(ChannelOption.SO_KEEPALIVE,true);
System.out.println("SimpleServer 启动了");
/*绑定端口启动服务*/
ChannelFuture f=b.bind(port).sync();
System.out.println("Server start listen at " + port);
f.channel().closeFuture().sync();
}finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
System.out.println("SimpleServer 关闭了");
}
}
}
package Server;
import java.nio.charset.Charset;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class SimpleServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline=arg0.pipeline();
pipeline.addLast("framer",new DelimiterBasedFrameDecoder(8192,Delimiters.lineDelimiter()));
pipeline.addLast("decoder",new StringDecoder(Charset.forName("UTF-8")));
pipeline.addLast("encoder",new StringEncoder(Charset.forName("UTF-8")));
pipeline.addLast("handler",new SimpleServerHandler());
}
}
package Server;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class SimpleServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext arg0, String arg1) throws Exception {
// TODO Auto-generated method stub
Channel incomming=arg0.channel();
System.out.println("server reciver:"+arg1);
/*writeAndFlush 会把缓冲在内部的消息强行输出*/
incomming.writeAndFlush("Server return "+arg1+"\n");
}
}
客户端:
package Client;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class SimpleClient {
public static void main(String[] args) {
new SimpleClient("127.0.0.1",8000).run();
}
private final String host;
private final int port;
public SimpleClient(String host,int port) {
this.host=host;
this.port=port;
}
public void run() {
EventLoopGroup group=new NioEventLoopGroup();
try {
Bootstrap bootstrap=new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new SimpleClientInitializer());
Channel channel=bootstrap.connect(host,port).sync().channel();
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
while(true) {
channel.writeAndFlush(in.readLine()+"\r\n");
}
}catch(Exception e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
package Client;
import java.nio.charset.Charset;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class SimpleClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline=arg0.pipeline();
pipeline.addLast("framer",new DelimiterBasedFrameDecoder(8192,Delimiters.lineDelimiter()));
pipeline.addLast("decoder",new StringDecoder(Charset.forName("UTF-8")));
pipeline.addLast("encoder",new StringEncoder(Charset.forName("UTF-8")));
pipeline.addLast("handler",new SimpleChatClientHandler());
}
}
package Client;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class SimpleChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext arg0, String arg1) throws Exception {
// TODO Auto-generated method stub
System.out.println("client reciver:"+arg1);
}
}
服务器输出:
客户端输出: