网站开发与推广,新网站开发工作总结,网站建设网络推广平台,个人主页图标我们经常听到各种各样的概念——阻塞、非阻塞、同步、异步#xff0c;这些概念都与我们采用的网络编程模式有关。
例如#xff0c;如果采用BIO网络编程模式#xff0c;那么程序就具有阻塞、同步等特质。
诸如此类#xff0c;不同的网络编程模式具有不同的特点#xff0c…我们经常听到各种各样的概念——阻塞、非阻塞、同步、异步这些概念都与我们采用的网络编程模式有关。
例如如果采用BIO网络编程模式那么程序就具有阻塞、同步等特质。
诸如此类不同的网络编程模式具有不同的特点这些网络编程模式就相当于我们的网络编程套路。
因此了解并掌握网络编程模式是学习Netty和使用Netty进行网络编程的必经之路。
下面我们学习网络编程模式以及Netty如何对它们提供支持。
网络编程的3种模式
当我们去饭店吃饭时会经常遇到以下三种模式。
排队打饭模式。这种模式主要出现在食堂等场所。人们在窗口前排队饭菜打好后才走若饭菜没有打好人们一般是不会主动离开的。点餐等待被叫模式。在这种模式下我们会收到点餐号饭店备好饭菜后就呼叫点餐号我们需要自行去取。包厢模式。这是我们最喜欢的模式点餐后什么都不用管坐在那里等着饭菜被服务员端上桌即可。
为了方便解释我们可以将就餐模式与服务器应用做类比。
例如把饭店比作服务器而把饭菜比作数据。
这样的话饭菜好了就相当于数据就绪而端菜行为则可以当作读取数据。
通过进行类比我们发现就餐的3种模式其实正好对应经典的3种网络编程I/O模式。 阻塞与非阻塞之间的区别在于要不要一直等直到饭菜做好。
换言之对于阻塞而言在数据没有传输过来之前会阻塞等待直到数据到来写的过程也类似当缓冲区满时写操作也会被阻塞直到缓冲区可写”。
但是对于非阻塞而言遇到这些情况则不做任何停留 直接返回。
同步和异步之间的区别在于数据就绪后谁来读类似于“饭菜好了谁来端”的问题。
数据就绪后如果需要应用程序自行读取就是同步过程数据就绪后如果由系统直接读取并回调给程序就是异步过程。
在区分完以上两组概念后我们可以做下对应BIO是阻塞同步方式NIO是非阻塞同步方式AIO是非阻塞异步方式。
网络编程模式的选择要点
从表面上看我们一般倾向于使用新的模式。
例如我们青睐的顺序可能是AIO-NTO-BIO但是实际上看似明显的优先顺序并不是什么黄金准则”。
我们需要结合更多的因素来决定如何做出选择。
服务的连接数
假设应用程序的连接数有限比如只有一两个连接我们就无法预见NIO模式的性能肯定比传统程序的BIO模式好。
不过可以预见的是NIO模式的代码实现复杂度肯定高于BIO模式。
因此在选择I/O模式时我们需要了解应用程序到底能够支持多少个连接。
服务将要部署到的平台
对于Windows平台我们一般都会优先选择AIO模式。 但是对于Linux平台情况将可能有所不同毕竟Linux平台对AIO模式的支持还不够成熟。
另外有关NIO和AIO模式的一些测试表明其实在Linux平台上NIO和AIO模式的代码实现并无太大性能差别。
因此对于大多数使用Linux作为服务器的应用平台而言NIO模式或许才是更好的选择。
当前已有的架构和可用的实现
如果当前项目一直使用某种模式那么我们一般很少有勇气去直接变革和采用新的模式而是修修补补并沿用旧的模式。
此外即使对于全新的项目也不一定会选择我们想用的模式。例如假设要选择AIO模式但我们的技术栈是基于Netty的那么AIO模式用不了。
综上所述不同网络编程模式有自己适用的场景我们不能仅仅依据推出时间的前后就直接做出决策。
换言之具体问题具体分析、具体场景具体选择才是永恒之道。
Netty对网络编程模式的支持 Netty目前只推崇NIO模式。为什么不推荐另外两种模式呢
Netty为什么不推荐BIO/OIO模式
在连接数较多的情况下BIO/OIO模式的阻塞特性就意味着耗资源、效率低。具体而言 阻塞就意味着等待等待就会占用线程。 考虑一下在连接数比较多的情况下如果每个连接上的请求又都在等待占用的线程将会非常多资源耗费就太大了。 不仅如此一个进程对所能创建的线程数量也是有约束的这一点无法克服。
其次Netty为什么废弃AIO模式原因主要有三点。
Windows平台上的实现虽然已经非常成熟但是Windows平台本身很少用作服务器。Linux平台经常用作服务器但是Linux平台上AIO模式的实现还不够成熟。Linux平台上AIO模式的实现相比NIO模式而言稍复杂一些但性能提升并不明显。
由上可知对于Netty而言NIO是Netty推崇的核心I/O模式。但是有必要补充说明的是对于NIO模式的实现方式Netty支持的不止一种。 Netty的通用NIO模式的实现方式在Linux平台上使用的也是EpolL那么为什么此处还需要一套专有的Epoll相关实现呢 “重新造轮子”无非有两个原因自己的轮子更好、更强这个道理同样适用于此。
具体原因包括以下两个方面。 Epoll相关实现能够暴露更多可控的参数JDK的很多参数都不可调。例如JDK的NIO模式在Linux平台上默认是水平触发且不可修改的但对于Netty而言水平触发和边缘触发都支持并且可以切换(默认是边缘触发)。 垃圾回收更少性能更好这是Netty开发者自己给出的理由。不过我们有理由相信Netty确实做到了否则没有必要重新造轮子。
Netty对网络编程模式的实现 // 创建一个ServerBootstrap实例ServerBootstrap serverBootstrap new ServerBootstrap();// 设置服务器通道类型为OioServerSocketChannelserverBootstrap.channel(OioServerSocketChannel.class);// 创建一个OioEventLoopGroup实例OioEventLoopGroup eventLoopGroup new OioEventLoopGroup();// 设置服务器线程组为创建的OioEventLoopGroup实例serverBootstrap.group(eventLoopGroup);
OIO模式的使用主要涉及OioServerSocketChannel和OioEventLoopGroup这两个关键类。
ServerSocketChannel 的创建
当执行 serverBootstrap.channel (OioServerSocketChannel.class)时实际上执行的是如下方法:
//AbstractBootstrap.java/*** 设置通道的类型为指定的通道类* * param channelClass 通道类* return 通道配置对象*/public B channel(Class? extends C channelClass) {return channelFactory(new ReflectiveChannelFactoryC(ObjectUtil.checkNotNull(channelClass, channelClass)));}
上述代码创建了 ReflectiveChannelFactory 来负责创建 OioServerSocketChannelo 顾名思义ReflectiveChannelFactory使用“反射方式来完成上述工作。
//ReflectiveChannelFactory.javaprivate final Constructor? extends T constructor; // 构造器用于创建指定类型的对象public ReflectiveChannelFactory(Class? extends T clazz) { // 构造函数用于创建ReflectiveChannelFactory对象ObjectUtil.checkNotNull(clazz, clazz); // 检查clazz是否为nulltry {this.constructor clazz.getConstructor(); // 获取指定类的无参构造函数} catch (NoSuchMethodException e) {throw new IllegalArgumentException(Class StringUtil.simpleClassName(clazz) does not have a public non-arg constructor, e); // 如果指定类没有无参公共构造函数则抛出异常}}Overridepublic T newChannel() { // 创建一个指定类型的对象try {return constructor.newInstance(); // 使用构造器创建对象} catch (Throwable t) {throw new ChannelException(Unable to create Channel from class constructor.getDeclaringClass(), t); // 如果创建对象过程中发生异常则抛出ChannelException异常}}
EventLoopGroup 的功能
EventLoopGroup 负责给每个通道分配 EventLoop0 例如OioEventLoopGroup 负责给 BIOChannel分配ThreadPerChannelEventLoop 注意这里的命名方式和其他的EventLoop不同。
//ThreadPerChannelEventLoop.javaOverrideprotected void run() {for (;;) {// 从任务队列中取出一个任务Runnable task takeTask();if (task ! null) {// 执行任务task.run();// 更新最后一次执行时间updateLastExecutionTime();}// 获取当前的通道Channel ch this.ch;if (isShuttingDown()) {// 如果正在关闭if (ch ! null) {// 关闭通道ch.unsafe().close(ch.unsafe().voidPromise());}// 如果确认关闭if (confirmShutdown()) {break;}} else {// 如果没有正在关闭if (ch ! null) {// 处理注销if (!ch.isRegistered()) {// 执行所有任务runAllTasks();// 注销deregister();}}}}}
ThreadPerChannelEventLoop在本质上相当于任务的执行体而任务本身就是执行通道上的读写操作。例如当写数据时,写数据这一操作会被当作任务提交给SingleThreadEventExecutor (ThreadPerChannelEventLoop 的父类)的 execute 方法来执行。 //SingleThreadEventExecutor.javaprivate void execute(Runnable task, boolean immediate) {// 判断是否在事件循环中boolean inEventLoop inEventLoop();// 添加任务到任务队列addTask(task);if (!inEventLoop) {// 启动线程startThread();// 如果已经关闭if (isShutdown()) {// 是否从任务队列中移除任务boolean reject false;try {// 如果移除了任务则标记为拒绝if (removeTask(task)) {reject true;}} catch (UnsupportedOperationException e) {// 任务队列不支持移除任务只能继续希望在任务完全终止之前能够取而代之。// 最坏情况下在任务终止时进行日志记录。}// 如果被拒绝则进行拒绝处理if (reject) {reject();}}}// 如果不希望通过添加任务唤醒线程以及立即执行if (!addTaskWakesUp immediate) {// 唤醒线程wakeup(inEventLoop);}}
最终执行的是OioByteStreamChannel#doWriteBytes()方法。
//OioByteStreamChannel.javaOverrideprotected void doWriteBytes(ByteBuf buf) throws Exception {// 获取输出流OutputStream os this.os;// 如果输出流为空则抛出未连接异常if (os null) {throw new NotYetConnectedException();}// 将ByteBuf中的字节写入输出流中buf.readBytes(os, buf.readableBytes());}
要完成对一种I/O模式的支持我们至少需要两个组件 SocketChannel负责完成具体的读写务NioEventLoop负责任务的执行。当需要切换I/O模式时直接替换掉这些实现即可。
常见疑问
水平触发和边缘触发的定义
Netty既支持水平触发也支持边缘触发。Netty确实提供了这两种触发方式的定义。
public enum EpollMode {EDGE_TRIGGERED,LEVEL_TRIGGERED;private EpollMode() {}
}那么什么是水平触发和边缘触发在理论层次上进行解析。
水平触发
当被监控的文件描述符上有可读写事件时通知用户去读写。如果用户一次没有读写完数据就一直通知用户。
在用户确实不怎么关心这个文件描述符的情况下频繁通知用户会导致用户真正关心的那些文件描述符的处理效率降低。
比如点餐后饭菜做好了(数据就绪)服务员端上来问你吃不吃(读写数据)。 不管你吃溢还是吃不完服务员总是过来反复提醒你吃饭。
边缘触发
当被监控的文件描述符上有可读写事件时通知用户去读写但只通知一次这就需要用户一次性把数据读写完。
如果用户没有一次性读写完数据那就需要等待下一次新的数据到来时才能读写上次未读写完的数据。
比如服务员端来饭菜后你没有一次性吃完等你想吃剩下的饭菜时,就必须再次点餐才行。
Epoll既支持水平触发也支持边缘触发那么该如何选择呢如果选择水平触发就要注意效率和资源利用率而如果选择边缘触发就要注意自身是否能一次性完成数据的读写。