共计 9488 个字符,预计需要花费 24 分钟才能阅读完成。
丸趣 TV 小编今天带大家了解如何分析高性能服务器 Server 中的 Reactor 模型,文中知识点介绍的非常详细。觉得有帮助的朋友可以跟着丸趣 TV 小编一起浏览文章的内容,希望能够帮助更多想解决这个问题的朋友找到问题的答案,下面跟着丸趣 TV 小编一起深入学习“如何分析高性能服务器 Server 中的 Reactor 模型”的知识吧。
在这个充斥着云的时代, 我们使用的软件可以说 99% 都是 C / S 架构的!
你发邮件用的 Outlook,Foxmail 等
你看视频用的优酷,土豆等
你写文档用的 Office365,googleDoc,Evernote 等
你浏览网页用的 IE,Chrome 等(B/ S 是特殊的 C /S)
C/ S 架构的软件带来的一个明显的好处就是:只要有网络,你可以在任何地方干同一件事。
例如:你在家里使用 Office365 编写了文档。到了公司,只要打开编辑地址就可以看到在家里编写的文档,进行展示或者继续编辑。甚至在手机上进行阅读与编辑。不再需要 U 盘拷来拷去了。
C/ S 架构可以抽象为如下模型:
C 就是 Client(客户端), 上面的 B 是 Browser(浏览器)
S 就是 Server(服务器):服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务
C/ S 架构之所以能够流行的一个主要原因就是网速的提高以及费用的降低,特别是无线网络速度的提高。试想在 2G 时代,大家最多就是看看文字网页,小说什么的。看图片,那简直就是奢侈! 更别说看视频了!
网速的提高,使得越来越多的人使用网络,例如:优酷,微信都是上亿用户量,更别说天猫双 11 的瞬间访问量了! 这就对服务器有很高的要求! 能够快速处理海量的用户请求! 那服务器如何能快速的处理用户的请求呢?
高性能服务器
高性能服务器至少要满足如下几个需求:
效率高:既然是高性能,那处理客户端请求的效率当然要很高了
高可用:不能随便就挂掉了
编程简单:基于此服务器进行业务开发需要足够简单
可扩展:可方便的扩展功能
可伸缩:可简单的通过部署的方式进行容量的伸缩,也就是服务需要无状态
而满足如上需求的一个基础就是高性能的 IO!
Socket
无论你是发邮件,浏览网页,还是看视频~实际底层都是使用的 TCP/IP,而 TCP/IP 的编程抽象就是 Socket!
我一直对 Socket 的中文翻译很困惑,个人觉得是我所接触的技术名词翻译里最莫名其妙的,没有之一!
Socket 中文翻译为”套接字”! 什么鬼? 在很长的时间里我都无法将其和网络编程关联上! 后来专门找了一些资料,*** 在知乎上找到了一个还算满意的答案(具体链接,请见文末的参考资料链接)!
Socket 的原意是插口,想表达的意思是插口与插槽的关系!”send socket”插到”receive socket”里,建立了链接,然后就可以通信了!
套接字的翻译,应该是参考了套接管(如下图)! 从这个层面上来看,是有那么点意思!
套接字这个翻译已经是标准了,不纠结这个了!
我们看一下 Socket 之间建立链接及通信的过程! 实际上就是对 TCP/IP 连接与通信过程的抽象:
服务端 Socket 会 bind 到指定的端口上,Listen 客户端的”插入”
客户端 Socket 会 Connect 到服务端
当服务端 Accept 到客户端连接后
就可以进行发送与接收消息了
通信完成后即可 Close
对于 IO 来说,我们听得比较多的是:
BIO: 阻塞 IO
NIO: 非阻塞 IO
同步 IO
异步 IO
以及其组合:
同步阻塞 IO
同步非阻塞 IO
异步阻塞 IO
异步非阻塞 IO
那么什么是阻塞 IO、非阻塞 IO、同步 IO、异步 IO 呢?
一个 IO 操作其实分成了两个步骤:发起 IO 请求和实际的 IO 操作
阻塞 IO 和非阻塞 IO 的区别在于 *** 步:发起 IO 请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞 IO; 如果不阻塞,那么就是非阻塞 IO
同步 IO 和异步 IO 的区别就在于第二个步骤是否阻塞,如果实际的 IO 读写阻塞请求进程,那么就是同步 IO,因此阻塞 IO、非阻塞 IO、IO 复用、信号驱动 IO 都是同步 IO; 如果不阻塞,而是操作系统帮你做完 IO 操作再将结果返回给你,那么就是异步 IO
举个不太恰当的例子:比如你家网络断了,你打电话去中国电信报修!
你拨号 mdash;- 客户端连接服务器
电话通了 mdash;- 连接建立
你说:“我家网断了, 帮我修下”mdash;- 发送消息
说完你就在那里等,那么就是阻塞 IO
如果正好你有事,你放下带电话,然后处理其他事情了,过一会你来问下,修好了没 mdash;- 那就是非阻塞 IO
如果客服说:“马上帮你处理,你稍等”mdash;- 同步 IO
如果客服说:“马上帮你处理,好了通知你”,然后挂了电话 mdash;- 异步 IO
本文只讨论 BIO 和 NIO,AIO 使用度没有前两者普及,暂不讨论!
下面从代码层面看看 BIO 与 NIO 的流程!
BIO
客户端代码
//Bind,Connect Socket client = new Socket(127.0.0.1 ,7777); // 读写 PrintWriter pw = new PrintWriter(client.getOutputStream()); BufferedReader br= new BufferedReader(new InputStreamReader(System.in)); pw.write(br.readLine()); //Close pw.close(); br.close();
服务端代码
Socket socket; //Bind,Listen ServerSocket ss = new ServerSocket(7777); while (true) { //Accept socket = ss.accept(); // 一般新建一个线程执行读写 BufferedReader br = new BufferedReader( new InputStreamReader(socket .getInputStream())); System.out.println(you input is : + br.readLine()); }
上面的代码可以说是学习 Java 的 Socket 的入门级代码了
代码流程和前面的图可以一一对上
模型图如下所示:
BIO 优缺点
优点
模型简单
编码简单
缺点
性能瓶颈低
优缺点很明显。这里主要说下缺点:主要瓶颈在线程上。每个连接都会建立一个线程。虽然线程消耗比进程小,但是一台机器实际上能建立的有效线程有限,以 Java 来说,1.5 以后,一个线程大致消耗 1M 内存! 且随着线程数量的增加,CPU 切换线程上下文的消耗也随之增加,在高过某个阀值后,继续增加线程,性能不增反降! 而同样因为一个连接就新建一个线程,所以编码模型很简单!
就性能瓶颈这一点,就确定了 BIO 并不适合进行高性能服务器的开发! 像 Tomcat 这样的 Web 服务器,从 7 开始就从 BIO 改成了 NIO,来提高服务器性能!
NIO
NIO 客户端代码(连接)
// 获取 socket 通道 SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); // 获得通道管理器 selector=Selector.open(); channel.connect(new InetSocketAddress(serverIp, port)); // 为该通道注册 SelectionKey.OP_CONNECT 事件 channel.register(selector, SelectionKey.OP_CONNECT);
NIO 客户端代码(监听)
while(true){ // 选择注册过的 io 操作的事件(*** 次为 SelectionKey.OP_CONNECT) selector.select(); while(SelectionKey key : selector.selectedKeys()){ if(key.isConnectable()){ SocketChannel channel=(SocketChannel)key.channel(); if(channel.isConnectionPending()){ channel.finishConnect();// 如果正在连接,则完成连接 } channel.register(selector, SelectionKey.OP_READ); }else if(key.isReadable()){ // 有可读数据事件。 SocketChannel channel = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(10); channel.read(buffer); byte[] data = buffer.array(); String message = new String(data); System.out.println(recevie message from server:, size: + buffer.position() + msg: + message); } } }
NIO 服务端代码(连接)
// 获取一个 ServerSocket 通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); serverChannel.socket().bind(new InetSocketAddress(port)); // 获取通道管理器 selector = Selector.open(); // 将通道管理器与通道绑定,并为该通道注册 SelectionKey.OP_ACCEPT 事件, serverChannel.register(selector, SelectionKey.OP_ACCEPT);
NIO 服务端代码(监听)
while(true){ // 当有注册的事件到达时,方法返回,否则阻塞。 selector.select(); for(SelectionKey key : selector.selectedKeys()){ if(key.isAcceptable()){ ServerSocketChannel server = (ServerSocketChannel)key.channel(); SocketChannel channel = server.accept(); channel.write(ByteBuffer.wrap( new String( send message to client).getBytes())); // 在与客户端连接成功后,为客户端通道注册 SelectionKey.OP_READ 事件。 channel.register(selector, SelectionKey.OP_READ); }else if(key.isReadable()){// 有可读数据事件 SocketChannel channel = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(10); int read = channel.read(buffer); byte[] data = buffer.array(); String message = new String(data); System.out.println(receive message from client, size: + buffer.position() + msg: + message); } } }
NIO 模型示例如下:
Acceptor 注册 Selector,监听 accept 事件
当客户端连接后,触发 accept 事件
服务器构建对应的 Channel,并在其上注册 Selector,监听读写事件
当发生读写事件后,进行相应的读写处理
NIO 优缺点
优点
性能瓶颈高
缺点
模型复杂
编码复杂
需处理半包问题
NIO 的优缺点和 BIO 就完全相反了! 性能高,不用一个连接就建一个线程,可以一个线程处理所有的连接! 相应的,编码就复杂很多,从上面的代码就可以明显体会到了。还有一个问题,由于是非阻塞的,应用无法知道什么时候消息读完了,就存在了半包问题!
半包问题
简单看一下下面的图就能理解半包问题了!
我们知道 TCP/IP 在发送消息的时候,可能会拆包(如上图 1)! 这就导致接收端无法知道什么时候收到的数据是一个完整的数据。例如: 发送端分别发送了 ABC,DEF,GHI 三条信息,发送时被拆成了 AB,CDRFG,H,I 这四个包进行发送,接受端如何将其进行还原呢? 在 BIO 模型中,当读不到数据后会阻塞,而 NIO 中不会! 所以需要自行进行处理! 例如,以换行符作为判断依据,或者定长消息发生,或者自定义协议!
NIO 虽然性能高,但是编码复杂,且需要处理半包问题! 为了方便的进行 NIO 开发,就有了 Reactor 模型!
Reactor 模型
AWT Events
Reactor 模型和 AWT 事件模型很像,就是将消息放到了一个队列中,通过异步线程池对其进行消费!
Reactor 中的组件
Reactor:Reactor 是 IO 事件的派发者。
Acceptor:Acceptor 接受 client 连接,建立对应 client 的 Handler,并向 Reactor 注册此 Handler。
Handler: 和一个 client 通讯的实体,按这样的过程实现业务的处理。一般在基本的 Handler 基础上还会有更进一步的层次划分, 用来抽象诸如 decode,process 和 encoder 这些过程。比如对 Web Server 而言,decode 通常是 HTTP 请求的解析, process 的过程会进一步涉及到 Listener 和 Servlet 的调用。业务逻辑的处理在 Reactor 模式里被分散的 IO 事件所打破, 所以 Handler 需要有适当的机制在所需的信息还不全 (读到一半) 的时候保存上下文,并在下一次 IO 事件到来的时候 (另一半可读了) 能继续中断的处理。为了简化设计,Handler 通常被设计成状态机,按 GoF 的 state pattern 来实现。
对应上面的 NIO 代码来看:
Reactor:相当于有分发功能的 Selector
Acceptor:NIO 中建立连接的那个判断分支
Handler:消息读写处理等操作类
Reactor 从线程池和 Reactor 的选择上可以细分为如下几种:
Reactor 单线程模型
这个模型和上面的 NIO 流程很类似,只是将消息相关处理独立到了 Handler 中去了!
虽然上面说到 NIO 一个线程就可以支持所有的 IO 处理。但是瓶颈也是显而易见的! 我们看一个客户端的情况,如果这个客户端多次进行请求,如果在 Handler 中的处理速度较慢,那么后续的客户端请求都会被积压,导致响应变慢! 所以引入了 Reactor 多线程模型!
Reactor 多线程模型
Reactor 多线程模型就是将 Handler 中的 IO 操作和非 IO 操作分开,操作 IO 的线程称为 IO 线程,非 IO 操作的线程称为工作线程! 这样的话,客户端的请求会直接被丢到线程池中,客户端发送请求就不会堵塞!
但是当用户进一步增加的时候,Reactor 会出现瓶颈! 因为 Reactor 既要处理 IO 操作请求,又要响应连接请求! 为了分担 Reactor 的负担,所以引入了主从 Reactor 模型!
主从 Reactor 模型
主 Reactor 用于响应连接请求,从 Reactor 用于处理 IO 操作请求!
Netty
Netty 是一个高性能 NIO 框架,其是对 Reactor 模型的一个实现!
Netty 客户端代码
EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.SO_KEEPALIVE, true); b.handler(new ChannelInitializer SocketChannel () { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); } }); ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); }
Netty Client Handler
public class TimeClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf m = (ByteBuf) msg; try { long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L; System.out.println(new Date(currentTimeMillis)); ctx.close(); } finally { m.release(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
Netty 服务端代码
EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer SocketChannel () { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); // Bind and start to accept incoming connections. ChannelFuture f = b.bind(port).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); }
Netty Server Handler
public class TimeServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(final ChannelHandlerContext ctx) { final ByteBuf time = ctx.alloc().buffer(4); time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L)); final ChannelFuture f = ctx.writeAndFlush(time); f.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { assert f == future; ctx.close(); } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
我们从 Netty 服务器代码来看,与 Reactor 模型进行对应!
EventLoopGroup 就相当于是 Reactor,bossGroup 对应主 Reactor,workerGroup 对应从 Reactor
TimeServerHandler 就是 Handler
child 开头的方法配置的是客户端 channel,非 child 开头的方法配置的是服务端 channel
具体 Netty 内容,请访问 Netty 官网!
Netty 的问题
Netty 开发中一个很明显的问题就是回调,一是打破了线性编码习惯,
二就是 Callback Hell!
看下面这个例子:
a.doing1(); //1 a.doing2(); //2 a.doing3(); //3
1,2,3 处代码如果是同步的,那么将按顺序执行! 但是如果不是同步的呢? 我还是希望 2 在 1 之后执行,3 在 2 之后执行! 怎么办呢? 想想 AJAX! 我们需要写类似如下这样的代码!
a.doing1(new Callback(){ public void callback(){ a.doing2(new Callback(){ public void callback(){ a.doing3(); } }) } });
那有没有办法解决这个问题呢? 其实不难,实现一个类似 Future 的功能! 当 Client 获取结果时,进行阻塞,当得到结果后再继续往下走! 实现方案,一个就是使用锁了,还有一个就是使用 RingBuffer。经测试,使用 RingBuffer 比使用锁 TPS 有 2000 左右的提高!
感谢大家的阅读,以上就是“如何分析高性能服务器 Server 中的 Reactor 模型”的全部内容了,学会的朋友赶紧操作起来吧。相信丸趣 TV 丸趣 TV 小编一定会给大家带来更优质的文章。谢谢大家对丸趣 TV 网站的支持!