一首《感谢你曾经来过》,谢谢支持。。。。。
本次所涉及代码已上传至GitHub: https://github.com/lgli/lgli-netty-tomcat/tree/master 前面已经对于IO的发展历程和基本原理,及其一些小例子已经说得差不多了,今天需要进入主题了,在这之前,再次回忆下各种版本的聊天小程序小例子,因为今天的Netty入门级也是从聊天室开始的。 传送门: 传统IO方式 NIO方式 AIO方式 从最开始的同步阻塞,同步非阻塞,到最后的异步非阻塞 这里简单总结下几种方式: 传统IO《BIO》: 当客户端发起一个TCP连接,客户端通过java.net.ServerSocket#accept获取到一个java.net.Socket对象,于是在服务端启动一个独立的线程来处理这个java.net.Socket的所有内容,包括读写数据;当启动多个客户端连接的时候,服务端的线程数量等同客户端连接数量,这种Thread Per Request模式带来的问题就是大量的线程将给服务器带来巨大的线程开销,为了避免这个问题,可以采用线程池的方式来解决,但是线程池也会带来一个问题,假定线程池线程数量是500,此时刚好有500个客户端连接好服务端并在进行数据交互,此时第501个客户端的请求也是无法处理的,即这种方式不能较好的解决高并发的问题,模式的方式如下图所示:NIO
NIO启用了一个多路复用的概念,即将所有连接通道都注册在一个
java.nio.channels.Selector上,然后通过轮询
java.nio.channels.Selector中的状态来进行对应的操作
如下图所示:
java.nio.channels.Selector的底层模型是一个基于Reactor反应堆的一种实现方式,在这里先提醒这么一个概念,后面会详述这个模型
AIO
AIO则是主要针对于在同步阻塞的等待数据读写方式上的一种改变,即不在同步阻塞的等待底层操作系统对数据的操作结果。而是通过传入回调函数来执行成功或者失败之后应该做的事情,其底层主要基于Proactor模型。
基于相对较复杂的NIO操作,Netty诞生了,除了对NIO进行优化和封装之外,Netty还可以随意切换多种不同的网络协议,提供很多标准的协议、安全、编码解码的支持,解决了很多 NIO 不易用的问题,同时应用于多种大型框架内,比如Dubbo、RocketMQ等。
先来一个基于Netty的聊天室改造
服务端
package com.lgli.netty.chart;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.group.ChannelGroup;import io.netty.channel.group.DefaultChannelGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;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;import io.netty.util.concurrent.GlobalEventExecutor;import java.net.InetSocketAddress;/** * ChartNettyServer * @author lgli */public class ChartNettyServer { public static void main(String[] args){ //配置服务端的NIO线程组 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); try{ bootstrap.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) //针对主线程的配置,分配最大线程数 .option(ChannelOption.SO_BACKLOG,128) //针对子线程的配置 保持长连接 .option(ChannelOption.SO_KEEPALIVE,true) .childHandler(new MyChannelInitial()); //绑定端口,同步等待成功 ChannelFuture future = bootstrap.bind(new InetSocketAddress("localhost", 8080)).sync(); //等待服务端监听端口关闭 future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { //出现异常,则释放资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } static class MyChannelInitial extends ChannelInitializer{ protected void initChannel(SocketChannel socketChannel) throws Exception { System.out.println("client connection :" + socketChannel.remoteAddress()); socketChannel.pipeline() .addLast("frame",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())) .addLast("decode",new StringDecoder()) .addLast("encode",new StringEncoder()) .addLast("handler",new MyHandler()); } } static class MyHandler extends SimpleChannelInboundHandler{ /** * 保存所有的连接 */ private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); /** * 接收到数据,发送给其他服务端 * @param channelHandlerContext 发送数据的通道 * @param s 发送的数据 * @throws Exception */ protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception { Channel inputChannel = channelHandlerContext.channel(); for(Channel channel : channels){ if(channel == inputChannel){ continue; } channel.writeAndFlush(inputChannel.remoteAddress()+":"+s+"\n"); } } /** * 有新的连接连接进来 * @param ctx * @throws Exception */ @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { Channel inChannel = ctx.channel(); //通知其他服务器有新的上线了 for(Channel channel : channels){ channel.writeAndFlush("欢迎"+inChannel.remoteAddress()+"进入聊天室! \n"); } inChannel.writeAndFlush("欢迎您进入聊天室!\n"); channels.add(inChannel); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { //通知其他服务器,下线了 Channel outChannel = ctx.channel(); for(Channel channel : channels){ if(outChannel == channel){ continue; } channel.writeAndFlush(outChannel.remoteAddress()+"下线了 \n"); } channels.remove(outChannel); } }}
这里对代码做一点解释:
Main方法入口
27-28行:配置服务端主线程和工作线程组
29行:创建启动类
31-37行:方法需要传入配置的2个线程组,设置通道类型为
io.netty.channel.socket.nio.NioServerSocketChannel
根据主线程配置,设置最大线程数
针对子线程配置,保持长连接
传入事件分发Handler
39-40行:绑定服务,端口,等待服务端监听端口关闭
46-47行:出现异常,关闭线程组,释放资源
下面看下事件分发Handler:
需要是io.netty.channel.ChannelHandler类型的对象,
这里选择自定一个对象,com.lgli.netty.chart.ChartNettyServer.MyChannelInitial
继承于io.netty.channel.ChannelInitializer
上图显示了自定义对象和两者之间的关系
56-60行:
TCP数据处理方式
这个处理TCP粘包粘包的类,在后续Netty正式篇中会详细说到
加解密方式
自定义Handler
自定义Handler主要做了3件事:接收数据时候的处理,有新的连接以及客户端下线的处理
com.lgli.netty.chart.ChartNettyServer.MyHandler#channelRead0
78-86行:接收数据的时候,将接收到的数据,发送给除数据来源的所有其他客户端
com.lgli.netty.chart.ChartNettyServer.MyHandler#handlerAdded
95-103行:有新的客户端进入,则通知除新的客户端之外的其他客户端,同时,添加新的客户端保存待用
com.lgli.netty.chart.ChartNettyServer.MyHandler#handlerRemoved
106-116行:有客户端下线,则通知其他还在线的所有客户端
看客户端代码:
package com.lgli.netty.chart;import io.netty.bootstrap.Bootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;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;import java.net.InetSocketAddress;import java.util.Scanner;/** * ChartNettyClient * @author lgli */public class ChartNettyClient { public static void main(String[] args) { EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); try{ bootstrap.group(eventLoopGroup) .channel(NioSocketChannel.class) .handler(new MyChannelInitial()); Channel channel = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync().channel(); Scanner scanner = new Scanner(System.in); while(scanner.hasNextLine()){ String s = scanner.nextLine(); if("".equals(s)){ continue; } channel.writeAndFlush(s+"\n"); } }catch (Exception e){ e.printStackTrace(); }finally { eventLoopGroup.shutdownGracefully(); } } static class MyChannelInitial extends ChannelInitializer{ protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline() .addLast("frame",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())) .addLast("decode",new StringDecoder()) .addLast("encode",new StringEncoder()) .addLast("handler",new MyChannelHandler()); } } static class MyChannelHandler extends SimpleChannelInboundHandler{ protected void channelRead0(ChannelHandlerContext channelHandlerContext, String o) throws Exception { System.out.println(o); } }}
同样的,Main方法开始
23-24行:配置客户端线程组和客户端启动类
26-28行:配置相关参数,具体见服务端说明
31-38行:监听客户端键盘数据,向服务端发送数据
44行:出现异常则退出线程组
自定义事件分发Handler和服务端类似
TCP数据处理方式
加解密方式
自定义Handler
自定义Handler,这里选择直接打印输出:
运行服务端和客户端程序,可以有以下效果:
基于Netty的简易Tomcat实现
这里实现了一个很简单的Tomcat功能,即监听端口,然后接受请求,根据请求分发请求对应的处理器
基本思路:
启动类:
package com.lgli.netty.tomcat;import com.lgli.netty.tomcat.http.NettyRequest;import com.lgli.netty.tomcat.http.NettyResponse;import com.lgli.netty.tomcat.servlet.NettyServlet;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.http.HttpRequest;import io.netty.handler.codec.http.HttpRequestDecoder;import io.netty.handler.codec.http.HttpResponseEncoder;import java.io.InputStream;import java.net.InetSocketAddress;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;import java.util.Properties;/** * NettyTomcat * @author lgli */public class NettyTomcat { private static Map maps = new HashMap<>(16); public NettyTomcat() { try{ //初始化资源信息 Properties properties = new Properties(); InputStream in = NettyTomcat.class.getClassLoader().getResourceAsStream("netty-tomcat.properties"); properties.load(in); Enumeration> enumeration = properties.propertyNames(); while(enumeration.hasMoreElements()){ Object o = enumeration.nextElement(); if(!(o instanceof String)){ continue; } String url = (String)o; if(!url.endsWith("-url")){ continue; } Object urlClass = Class.forName(properties.getProperty(url.replace("url", "class"))).newInstance(); if(!(urlClass instanceof NettyServlet)){ continue; } maps.put(properties.getProperty(url),(NettyServlet) urlClass); } }catch (Exception e){ e.printStackTrace(); } } private void monitor() { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); try{ serverBootstrap.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG,128) .childOption(ChannelOption.SO_KEEPALIVE,true) .childHandler(new NettyTomcatChannelInitial()); Channel channel = serverBootstrap.bind(new InetSocketAddress("localhost", 8080)).sync().channel(); channel.closeFuture().sync(); }catch (Exception e){ e.printStackTrace(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } static class NettyTomcatChannelInitial extends ChannelInitializer{ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast("encode",new HttpResponseEncoder()) .addLast("decode",new HttpRequestDecoder()) .addLast("handler",new NettyTomcatHandler()); } } static class NettyTomcatHandler extends SimpleChannelInboundHandler{ @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { if(!(o instanceof HttpRequest)){ return; } HttpRequest httpRequest = (HttpRequest) o; NettyRequest request = new NettyRequest(channelHandlerContext,httpRequest); NettyResponse response = new NettyResponse(channelHandlerContext,httpRequest); String url = request.getUrl(); if(maps.containsKey(url)){ maps.get(url).service(request,response); }else{ response.write("404 Not Found,找不到资源"); } } } public static void main(String[] args) { new NettyTomcat().monitor(); }}
com.lgli.netty.tomcat.NettyTomcat构造方法中,主要是初始化资源信息:
即读取配置资源文件,同时将请求和对应的类的实例保存在一个Map中
初始化线程组和初始化启动对象,前面已经提到,这里也不再过多解释:
需要对这个Handler做一点说明:
当有客户端发来请求的时候,
90-93行:这里直接根据Netty中的
io.netty.handler.codec.http.HttpRequest
接收
94-95行:用两个类似平常用的比较多的
HttpServletRequest和HttpServletResponse
实现,即
NettyRequest和NettyResponse
96-101行:根据初始化的请求路径和处理对象类,对比这里接收到的请求路径,获取到处理对象类的实例,执行其service方法
最后主类方法则执行完毕
下面看下自定义的NettyRequest和NettyResponse
NettyRequest:
package com.lgli.netty.tomcat.http;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.http.HttpRequest;/** * NettyRequest * @author lgli */public class NettyRequest { private ChannelHandlerContext ctx; private HttpRequest request; public NettyRequest(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) { this.ctx = channelHandlerContext; this.request = httpRequest; } public String getRequestMethod() { return request.method().name(); } public String getUrl() { return request.uri(); }}
NettyRequest主要调用
io.netty.handler.codec.http.HttpRequest
的API操作获取请求名和请求uri
NettyResponse
package com.lgli.netty.tomcat.http;import io.netty.buffer.Unpooled;import io.netty.buffer.UnpooledDirectByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.http.*;import java.io.UnsupportedEncodingException;import java.nio.charset.StandardCharsets;/** * NettyResponse * @author lgli */public class NettyResponse { private ChannelHandlerContext ctx; public NettyResponse(ChannelHandlerContext channelHandlerContext) { this.ctx = channelHandlerContext; } public void write(String result) { if(null == result || "".equalsIgnoreCase(result) || "".equalsIgnoreCase(result.trim())){ return; } try{ FullHttpResponse response = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(result.getBytes("GBK")) ); response.headers().set("Content-Type","text/html"); ctx.write(response); }catch (Exception e){ e.printStackTrace(); }finally { ctx.flush(); ctx.close(); } }}
看write方法
32-37行:NettyResponse调用了
io.netty.handler.codec.http.FullHttpResponse
封装返回结果
38行:
io.netty.channel.ChannelHandlerContext
输出数据到客户端《即浏览器》
剩下用到的3个Servlet:
总的父类NettyServlet
第一个OneServlet
第二个TwoServlet
执行NettyTomcat主类,开启基于Netty简易版的Tomcat:
后续,将深入了解Netty底层及其应用,还有比较关注的Netty性能调优!!