Netty

本文深入解析Netty框架,探讨其作为异步事件驱动网络应用框架的优势,包括简化网络编程、提高性能和稳定性。文章对比了BIO与NIO模型,详细介绍了Netty的零拷贝技术和基于拦截链模式的事件模型,最后通过一个简单的客户端-服务器实例演示了Netty的实际应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 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);
	}

}

 服务器输出:

客户端输出: 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值