一文带你快速上手Netty!附带用Netty实现Http静态资源服务器案例。
一、简介
Netty 是一个 基于 Java NIO(New I/O)和事件驱动的网络应用框架 ,用于快速开发高性能、可扩展的网络应用程序。
Netty 提供了一种简单而强大的方式来实现网络客户端和服务器,支持多种协议(如 HTTP、WebSocket、FTP 等)和自定义协议的开发。它的设计理念是高性能、高可靠性以及易于使用。
主要特点包括:
异步和事件驱动:Netty 基于事件驱动模型,通过异步的方式处理网络操作,提高了应用程序的性能和吞吐量。
高度可定制:Netty 提供了丰富的 API 和组件,可以根据具体需求定制和扩展功能,满足不同场景的需求。
易于使用:Netty 封装了复杂的网络编程细节,提供了简洁的 API 和清晰的文档,使开发人员能够快速上手并构建稳定的网络应用。
支持多种协议:Netty 支持多种主流协议的开发,包括 HTTP、WebSocket、TCP、UDP 等,同时也支持自定义协议的实现。
高性能:Netty 在网络编程性能方面表现出色,采用了零拷贝技术、内存池管理等优化手段,提高了系统的性能和效率。
由于其优秀的性能和灵活的设计,Netty 在众多大型互联网公司和开源项目中被广泛应用,是构建高性能网络应用的首选框架之一。
当然,简单来说Netty是基于Java Nio多路复用的高性能开发框架,封装了java nio中一系列复杂的api,如果你还不知道java的nio的话,推荐先去看看上一篇文章: 打牢Netty基础,一文带你彻底搞懂Java的Nio多路复用!
二、环境准备
Netty的版本有3.x,4.x,5.x,但是因为某些原因,5.x已被弃用,所以我们选择使用4.x的版本即可,先导入jar包。
io.netty netty-all 4.1.95.Final
三、Netty工作模型简介
首先netty会去去获取连接交由BossGroup下的线程去分发处理,这些连接请求将会被发送给多个WorkGroup,WorkGroup会使用EventLoop进行处理,简单来说,BossGroup主要用于处理连接,WorkGroup主要用于处理各类IO事件。EventLoop会去调用pipline,pipline中有一系列的handler,它们会以流水线的方式挨个去处理请求。
四、Netty核心组件介绍
要使用netty进行开发,就必须要了解Netty中的几大核心组件
1.Channel
2.EventLoopGroup
3.ByteBuf
4.PipLine
5.ChannelHandler
1.Channel
channel是数据传输的通道,所有数据的流通都会通过channel ,如果熟悉Java nio的同学可能知道,channel是受到Selector调度的,而这也是多路复用的核心。
2.EventLoopGroup
EventLoopGroup可以理解为一个线程池,里面维护着多个线程循环处理任务,在使用Netty时,需要指明使用的EventLoopGroup的实例。
EventLoopGroup是Netty中的一个重要概念,它代表一个处理 I/O 操作的线程池。每个 EventLoopGroup包含一个或多个EventLoop,而每个EventLoop则负责处理一个或多个 Channel 的事件。EventLoopGroup 负责管理线程的生命周期,以及将事件分发给适当的 EventLoop。
在Netty中,通常会创建两种类型的 EventLoopGroup:一个用于处理客户端的连接,即BoosGroup,另一个用于处理已经建立的连接的 I/O 操作,即WorkGroup。通过EventLoopGroup,Netty 能够实现高效的事件处理和并发控制,从而提供高性能的网络应用程序开发能力。
3.ByteBuf
ByteBuf 起到数据缓冲的作用,它对 Java NIO 中 ByteBuffer 的改进和增强。比其以前直接用byte数组作为缓冲区,使用ByteBuf有以下优点:
灵活性: ByteBuf 提供了两种不同的缓冲区实现:Heap ByteBuf 和 Direct ByteBuf。Heap ByteBuf 使用 JVM 堆内存,而 Direct ByteBuf 使用直接内存,可以提高数据传输效率。
1.可扩展性: ByteBuf 是可扩展的,可以根据需要动态调整其容量。通过自动扩容或者手动调整容量,可以更好地适应不同的数据处理需求。
2.读写操作: ByteBuf 提供了方便的读写操作方法,支持读取和写入各种基本数据类型(如 int、long、float 等),同时也支持复制、切片、合并等操作。
3.引用计数: ByteBuf 使用引用计数来管理其生命周期,确保在适当的时候释放内存,避免内存泄漏。通过 retain() 和 release() 方法来增加和减少引用计数。
4.池化: Netty 提供了 ByteBuf 池化功能,通过池化机制可以重复利用 ByteBuf 对象,减少对象的创建和销毁,提高性能。
4.Pipline和channelHandler
Pipeline(管道)是一种用于管理ChannelHandler的机制。每个Channel都有自己的 Pipeline,用于处理传入或传出的事件流。Pipeline 是一个由一系列 ChannelHandler 组成的处理链,每个 ChannelHandler 负责处理特定类型的事件或数据。pipline本质是一个双向链表,采用的是责任链模式,内部channelHandler的数据可以双向传递。
当数据通过Channel时,它会依次经过Pipeline中的每个 ChannelHandler 进行处理。每个ChannelHandler可以对数据进行处理、转换或者传递给下一个ChannelHandler。通过配置和定制Pipeline,使用pipline可以实现自定义的数据处理逻辑,例如数据解码、编码、加密、解密等操作。
五、代码演示
了解完理论,接下来就该上代码了,我们先用Netty实现一个简单的服务器和客户端,客户端向服务器发送消息,服务器可以接受客户端的数据并对其进行多道处理。代码如下:
客户端
public class Client { public static void main(String ... args) throws InterruptedException { //创建EventLoopGroup实例,NioEventLoopGroup是非阻塞的,更常用 EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); //Bootstrap是配置和启动客户端的引导类 //顺带获取以下channel的关闭futrue,用于同步关闭事件 ChannelFuture channelFuture = new Bootstrap() //放入EventLoopGroup实例 .group(eventLoopGroup) //传入需要使用的Channel的实现类型, .channel(NioSocketChannel.class) //加入handler,写入处理逻辑 .handler(new ChannelInitializer() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { //获取pipline流水线,向其中注册handler nioSocketChannel.pipeline() //StringDecoder是netty提供的入站处理器,能对信息进行解码 .addLast(new StringDecoder()) //StringEncoder是netty提供的出站处理器,能对信息进行编码 .addLast(new StringEncoder()) //自定义入站处理器 .addLast(new ChannelInboundHandlerAdapter() { //重写channelRead方法,读取到数据时执行 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //读取服务器传来的数据,打印 System.out.println("message from server: " + msg); //接受到数据后关闭channel ctx.channel().close(); } }); } //连接服务器 }).connect(new InetSocketAddress("localhost", 8080)) //同步阻塞,只有当连接建立后才会向下执行 .sync() //获取channel .channel() //向服务器发送数据并立刻刷新 .writeAndFlush("hello, server!!") .channel() //获取channel关闭同步对象 .closeFuture(); //channel关闭同步,只有channel关闭后才会向下执行 channelFuture.sync(); System.out.println("close the client..."); //关闭eventLoopGroup eventLoopGroup.shutdownGracefully(); } }
在上述代码中,提到了两个关于handler的非常重要的概念,出站处理器和入站处理器。这两个处理器都是交由pipline管理的处理器,区别在于**当数据流入channel时,只会调用入站处理器,而当数据流出channel时才会调用出站处理器,而且入站处理器是以从前往后的顺序执行,而出站处理器是从数据写出的位置开始从后往前执行**。
因此在上述代码中,StringDecoder是一个入站处理器,自定以处理器也是一个入站处理器,所以当数据传入时执行顺序为 StringDecoder -> 自定义处理器。
当写出数据时,执行出站处理器,所以执行StringEncoder。
服务器
public class Server { public static void main(String ... args) throws InterruptedException { //创建EventLoopGroup,同样采用非阻塞类型,并可以指定线程数 //boos用于处理连接,worker用于处理io,和上文提到的一样 EventLoopGroup boos = new NioEventLoopGroup(1); EventLoopGroup worker = new NioEventLoopGroup(4); //创建服务器引导类,注意服务器的引导类前面有要加”Server“ new ServerBootstrap() //传入EventLoopGroup实例 .group(boos, worker) //确定要使用的channel类 .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { //获取pipline流水线,向其中注入handler nioSocketChannel.pipeline() //注册入站处理器StringDecoder,用于解码信息 .addLast(new StringDecoder()) //注册出站处理器StringEncoder,用于编码信息 .addLast(new StringEncoder()) //添加自定义入站处理器,对数据做出处理 .addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //加工传入的数据 msg += "\nThe first inbound handler handles \n"; //调用次父类方法将数据传入下一个入站处理器处理 super.channelRead(ctx, msg); } }) //定义第二个自定义入站处理器进行第二次信息加工 .addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //对传入的信息进行第二次加工 msg += "The second inbound handler handles \n"; //再次调用该方法,把信息传入下一个入站处理器 super.channelRead(ctx, msg); } }) //定义第二个出站处理器(之所以是第二个是因为出站处理器的执行顺序和入站处理器相反为从右向左) .addLast(new ChannelOutboundHandlerAdapter() { //重写write方法,当数据写出时触发 @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { //对要传出的数据进行第二次处理 msg += "\n the second outbound handler handles"; //传递给下一个出站处理器(StringEncoder) super.write(ctx, msg, promise); } }) //定义第一个出站处理器 .addLast(new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { //对传出的数据进行第一次加工 msg += "\n the first outbound handler handles"; //传入下一个出站处理器 super.write(ctx, msg, promise); } }) //定义最后一个入站处理器 .addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //打印从服务器传来,经过多次加工后的信息 System.out.println("message from client: " + msg); //返回给客户端的消息 String msgToServer = "hello, client!!"; //将消息发送给客户端,并调用在此之前的出站处理器 //也就是说如果该入站处理器之后还有出站处理器的话,是不会执行后面的出站处理器的 ctx.writeAndFlush(msgToServer); } }); } }) //绑定端口8080 .bind(new InetSocketAddress(8080)); } }
这里需要注意的点是,当执行写出操作(例如writeAndFlush)时,只会执行当前写出了数据的处理器之前的出站处理器,在此之后的处理器是不会执行的。
代码写完后,先启动客户端,再启动服务器。
最终结果如下:
客户端
服务端
六、Netty实现Http静态资源服务器案例
接下来,让我们用netty来实现一个http静态资源服务器来练练手。
该服务器能代理任意路径下的图片、html、css、js等常用的静态网页文件,并将其发送给浏览器。
1.服务器主体
public class NettyHttpStaticServer { public static void main(String ... args) throws Exception { //同样准备boss线程池和worker线程池 EventLoopGroup boss = new NioEventLoopGroup(1); EventLoopGroup worker = new NioEventLoopGroup(4); //创建服务器引导类 new ServerBootstrap() //写入需要使用的channel实际类型 .channel(NioServerSocketChannel.class) //注册eventLoopGroup .group(boss, worker) .childHandler(new ChannelInitializer() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { //获取pipline,向其中注册handler nioSocketChannel.pipeline() //注册HttpServerCodec处理器 //这个处理器既是入站处理器又是出站处理器,也就是说数据写入写出都会调用 //用于对http请求和响应进行编码解码 .addLast(new HttpServerCodec()) //传入自定义http请求处理器 //由于处理http请求且返回响应的逻辑稍微有些复杂,所以这里就不采用之前直接写匿名内部类的方式了 //待会我们对HandleHttpRequestInboun这个类做出实现 .addLast(new HandleHttpRequestInbound()); } }) //绑定端口 .bind(new InetSocketAddress(8080)) //同步,只有绑定完成才会向下执行 .sync() .channel() .closeFuture() //同步,channel关闭后才会向下执行 .sync(); //关闭两个eventLoopGroup boss.shutdownGracefully(); worker.shutdownGracefully(); } }
2.HandleHttpRequestInbound(处理http请求)
//创建HandleHttpRequestInbound类并继承SimpleChannelInboundHandler作为入站处理器 //SimpleChannelInboundHandler是一个入站处理器,该处理器只处理泛型参数指定的类型数据,这里我们只用处理HttpRequest的数据 //因为HttpServerCodec这个处理器会分别向后传递请求头和请求体,但我们不需要请求体,所以用该处理器做一下限制。 public class HandleHttpRequestInbound extends SimpleChannelInboundHandler { //定义静态资源所在路径 public static final String FILE_PATH = "F:\\java\\java_work\\Netty\\src\\main\\resources"; //读取到内容时执行该方法时 @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) throws Exception { //将获取到的HttpRequest对象传递给我们自定义的handleRequest方法处理,并获取响应对象 HttpResponse httpResponse = this.handleRequest(httpRequest); //将响应发送给浏览器 channelHandlerContext.writeAndFlush(httpResponse); } //处理HttpRequest的方法,返回一个HttpResponse对象 private HttpResponse handleRequest(HttpRequest httpRequest) { //将HttpRequest对象强转为DefaultHttpRequest DefaultHttpRequest defaultHttpRequest = (DefaultHttpRequest) httpRequest; //创建响应对象,并指定http协议版本,和响应状态码,这里我们先默认响应失败 DefaultFullHttpResponse defaultFullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); //设置响应头的Content-Length(响应体长度为0) defaultFullHttpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 0); //获取请求的方法 HttpMethod method = defaultHttpRequest.method(); //因为是静态资源服务器,只需要Get请求,其余请求都视为是失败,直接返回默认的响应对象 if (method.compareTo(HttpMethod.GET) != 0) { return defaultFullHttpResponse; } //获取http请求的请求资源路径 String uri = defaultHttpRequest.uri(); //将请求资源的路径和静态资源所在的根路径进行拼接,并获取文件的传输channel try (FileChannel fileChannel = new FileInputStream(FILE_PATH + uri).getChannel()) { //文件打开成功,设置响应状态码为成功(200) defaultFullHttpResponse.setStatus(HttpResponseStatus.OK); //获取请求资源文件的大小 int len = (int) fileChannel.size(); //将资源文件通过channel写入响应对象的响应体 defaultFullHttpResponse.content().writeBytes(fileChannel, 0, len); //设置Content-Length,即文件的大小 defaultFullHttpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, len); //通过自定义的setContentType方法来获取响应类型 this.setContentType(defaultFullHttpResponse, uri); //返回响应对象 return defaultFullHttpResponse; } catch (Exception e) { //文件获取失败直接返回错误的响应对象 return defaultFullHttpResponse; } } //该方法用于判断和设置Content-Type(响应类型) private void setContentType(HttpResponse response, String uri) { //默认响应类型为text/html String contentType = "text/html"; //通过请求资源的后缀名来确认该返回什么类型的响应 if (uri.endsWith(".jpg") || uri.endsWith(".png") || uri.endsWith(".jpeg") || uri.endsWith(".ico")) { contentType = "image/jpeg"; } else if (uri.endsWith(".css")) { contentType = "text/css"; } else if (uri.endsWith(".js")) { contentType = "text/javascript"; } //设置响应类型 response.headers().set("Content-Type", contentType); } }
这里测试所用的资源路径下的目录如下
F:\java\java_work\Netty\src\main\resources\
启动服务器,并在浏览其中输入地址和对应的文件名,就能成功访问到静态资源。
访问成功
七、总结
Netty是一个基于Java Nio的高性能、异步事件驱动的网络应用程序框架,在使用Netty时,需要定义ChannelHandler来处理各种事件和消息,配置ChannelPipeline来管理Handler的执行顺序,以及配置Bootstrap来启动服务器或客户端。Netty还提供了许多工具类和组件,简化了网络编程过程。