Netty in action—Netty传输服务
网络传输中数据通常以一种格式:字节。这些字节要怎样传播主要取决于我们指定的网络传输服务,帮助我们抽象底层的数据传输机制。用户不需要关心实现细节,他们只需要确信他们的字节能被可靠地发送和接收。
Netty为它所有的传输服务实现提供了通用的API,使你能很容易的从阻塞传输服务转换到非阻塞传输服务。
案例学习:传输服务迁移
我们以一个简单的应用案例开始我们的传输服务学习。这个应用接收一个连接,然后写入”Hi!”并返回给客户端,最后关闭连接。
使用OIO(阻塞的传输服务)和NIO(异步的传输服务)
我们会仅仅使用JDK的API来实现这个应用的OIO和NIO版本。
下面的代码实现了阻塞的版本。
package com.netty.ch3;import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;public class PlainOioServer {public void serve(int port) throws IOException{final ServerSocket socket = new ServerSocket(port);try{for (;;){final Socket clinetSocket = socket.accept();System.out.println("Accepted connection from " + clinetSocket);new Thread(new Runnable() {//创建一个线程来处理这个链接public void run() {OutputStream out;try{out = clinetSocket.getOutputStream();out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));//把消息写入输出流返回给客户端out.flush();clinetSocket.close();} catch (IOException e) {e.printStackTrace();}finally {try{clinetSocket.close();} catch (IOException e) {e.printStackTrace();}}}}).start();}}catch (IOException e){e.printStackTrace();}}
}
这段代码足够处理中等数量的并发请求。但是如果这个应该大受欢迎,你会发现如果有成千上万的并发请求,你的应用就表现的不太好了,然后你决定转换到非阻塞(异步)的网络编程实现。但是你马上就会发现非阻塞的API和阻塞的API完全不同,因此你需要重写你的应用。
下面给出的非阻塞的版本实现:
package com.netty.ch3;import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class PlainNioServer {public void serve(int port)throws IOException{ServerSocketChannel serverChannel = ServerSocketChannel.open();serverChannel.configureBlocking(false);ServerSocket ssocket = serverChannel.socket();InetSocketAddress address = new InetSocketAddress(port);ssocket.bind(address);Selector selector = Selector.open();serverChannel.register(selector, SelectionKey.OP_ACCEPT);//注册serverChannel到selector,关心ACCEPT事件final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());for (;;){try{selector.select();//阻塞等待下一个事件}catch (IOException ex){ex.printStackTrace();break;}Set<SelectionKey> readyKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = readyKeys.iterator();while(iterator.hasNext()){SelectionKey key = iterator.next();iterator.remove();try{if(key.isAcceptable()){ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel client = server.accept();client.configureBlocking(false);client.register(selector,SelectionKey.OP_WRITE | SelectionKey.OP_READ,msg.duplicate());System.out.println("Accepted connection from " + client);}if(key.isWritable()){SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();while(buffer.hasRemaining()){if(client.write(buffer) == 0){break;}}client.close();}}catch (IOException ex){key.cancel();try{key.channel().close();}catch (IOException cex){}}}}}
}
正如你所看到的,尽管这些代码是处理同样的事情,但是确实完全不同。如果实现一个非阻塞的IO需要完全的重写,考虑下实现这么复杂的代码需要多大的工作量。
下面我们看看如何通过Netty实现OIO和NIO。
通过Netty实现OIO和NIO
首先实现阻塞版本:
package com.netty.ch3;import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.oio.OioServerSocketChannel;import java.net.InetSocketAddress;
import java.nio.charset.Charset;public class NettyOioServer {public void server(int port) throws Exception{final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));EventLoopGroup group = new OioEventLoopGroup();//通过OioEventLoopGroup来实现阻塞IOtry{ServerBootstrap b = new ServerBootstrap();b.group(group).channel(OioServerSocketChannel.class).localAddress(new InetSocketAddress(port)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);}});}});ChannelFuture f = b.bind().sync();f.channel().closeFuture().sync();}finally {group.shutdownGracefully().sync();}}
}
接下来我们会通过Netty实现非阻塞的版本
Netty实现非阻塞的版本
下面的代码几乎上上一个版本的代码相同,除了两行代码(line5,9)。
这就是从OIO转换到NIO所有要做的事情。
public class NettyNioServer {public void server(int port) throws Exception{final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));EventLoopGroup group = new NioEventLoopGroup();//通过NioEventLoopGroup来实现非阻塞IOtry{ServerBootstrap b = new ServerBootstrap();b.group(group).channel(NioServerSocketChannel.class)//对应的Channel也要改.localAddress(new InetSocketAddress(port)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);}});}});ChannelFuture f = b.bind().sync();f.channel().closeFuture().sync();}finally {group.shutdownGracefully().sync();}}
}
因此Netty为所有传输服务的实现都提供了同一套API,无论你选哪一种传输服务,你的代码只要做出很少的修改。
下面更深入的研究一下传输服务API
传输服务API
传输服务API的核心就是Channel
接口,它为所有的IO操作服务。它的结构如下图所示:
如果所示,ChannelPipeline
和ChannelConfig
中都指定了Channel
实例。ChannelConfig
存储了Channel的所有配置同时支持热部署(hot change)。因为一个特定的传输服务可能会有一个唯一的配置,它可能实现了ChannelConfig
的子类。
因为Channel都是独立的,声明Channel
作为java.lang.Comparable
的子接口是为了保证有序性。因此,AbstractChannel
中compareTo()
的实现会抛出异常,如果两个不同的Channel
实例返回同样hash code。
ChannelPipeline
持有所有的ChannelHandler
实例,这些实例会为输入输出的数据和事件服务。
ChannelHandler
的典型使用场景有:
- 转换数据的格式
- 为异常提供通知
- 为Channel的激活(active)和失活(inactive)提供通知
- 为Channel从一个EventLoop中注册或撤销(deregister)提供通知
- 为用户自定义事件提供通知
拦截过滤器
ChannelPipeline
实现了一个通用的设计模式-拦截过滤器。UNIX的管道是另一个常见的例子:命令被链接在一起,一个命令的输出呗链上的下一个命令过滤
如果有需要,你可以通过添加或移除ChannelHandler
实例来修改一个正在运行的ChannelPipeline
。Netty能构造高灵活性(highly flexible)的应用。比如,你能通过简单的增加一个合适的ChannelHandler
(SslHandler)来支持所需的STARTTLS协议。
除了刚刚介绍的ChannelPipeline
和ChannelConfig
,你还能使用其他Channel
接口提供的方法,最重要方法如下表所示:
方法名 | 描述 |
---|---|
eventLoop | 返回这个Channel的EventLoop |
pipeline | 返回这个Channel的ChannelPipeline |
isActive | 返回Channel是否是active的。active的定义可能依赖于底层的传输服务。比如,Socket传输服务认为一旦与远程主机建立连接即active,然后Datagram传输服务认为只要连接打开就算active |
localAddress | 返回本地的SocketAddress |
remoteAddress | 返回远程的SocketAddress |
write | 写数据到远程端,这个数据会被送到ChannelPipeline并进入队列直到被flush才发送出去 |
flush | 将刚才写入的数据刷到底层的传出服务(如:Socket)中 |
writeAndFlush | 一个方便的方法,首先调用write()然后flush() |
后面我们会详细讨论这些特性的使用,但现在只需要知道Netty通过几个接口就可以提供丰富的功能。
考虑写数据并flush到远端这个常见的任务。下面的代码展示了writeAndFlush()
方法的使用:
Channel channel = ...
//创建一个ByteBuf来持有要写的数据
ByteBuf buf = Unpooled.copiedBuffer("your data",CharsetUtil.UTF_8);
ChannelFuture cf = channel.writeAndFlush(buf);
//增加ChannelFutureListener来监听write完成事件
cf.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future){if(future.isSuccess()){System.out.println("Write successful");}else{System.err.println("Write error");future.cause.printStacktrace();}}
});
Netty的Channel
实现是线程安全的,所以你可以在多线程的环境下通过一个Channel
实例的引用来写数据到远端。下面的例子展示了再多线程的环境下将数据有序的写到远端:
final Channel channel = ...//创建一个ByteBuf来持有要写的数据final ByteBuf buf = Unpooled.copiedBuffer("your data", CharsetUtil.UTF_8).retain();Runnable writer = new Runnable() {@Overridepublic void run() {channel.write(buf.duplicate());}};Executor executor = Executors.newCachedThreadPool();executor.execute(writer);//将写数据任务移交到一个线程中executor.execute(writer);
提供的传输服务
Netty提供了几个可用的传输服务。因为并不是所有的传输服务都支持每一个传输协议,你得选择一个适合你应用中传输协议的传输服务。下表列出了Netty提供的传输服务
名称 | 包 | 描述 |
---|---|---|
NIO | io.netty.channel.socket.nio | 基于java.nio.channels(基于selector的途径)包 |
Epoll | io.netty.channel.epoll | 使用JNI的epoll()和非阻塞IO,这个传输服务支持的一些特性在Linux上才有效,如SO_REUSEPORT。且比NIO传输服务和完全非阻塞都要快 |
OIO | io.netty.channel.socket.oio | 基于java.net包,使用阻塞流 |
Local | io.netty.channel.local | 一本地传输服务能用来通过pipe在VM中通信 |
Embedded | io.netty.channel.embedded | 一个嵌入式(embedded)传输服务,允许在没有真正基于网络传输服务的情况下使用ChannelHandler,这对于你测试ChannelHandler的实现很有用 |
NIO—非阻塞IO
NIO为所有的IO操作提供了完全异步的实现。它利用了基于selector的API。
selector作为一个注册器来告知你Channel状态的改变,可能的状态改变有:
- 一个新的Channel被接收且已准备就绪
- 一个Channel的连接已经被建立
- 一个Channel中有准备读取的数据
- 一个Channel可用于写数据
在应用对状态的改变做出反应后,selector被重置然后重复前面的处理,在另一个线程中检查状态的改变并做出相应的相应。
下表显示了java.nio.channels.SelectionKey
中定义的位模式常量,这些位模式常量能组成应用关心的通知集合。
名称 | 描述 |
---|---|
OP_ACCEPT | 请求告知新连接被接收,一个Channel实例被创建 |
OP_CONNECT | 请求告知一个连接被建立 |
OP_READ | 请求告知当数据准备好从Channel中读 |
OP_WRITE | 请求告知当可以向Channel中写数据。这种情况出现在socket的缓存被填满的情况(当数据传输速度高于远端机器处理速度会发送) |
这些NIO的内部细节被用户级的API隐藏,这一点在Netty的传输服务实现中很常见。
下图显示了状态变换的处理流程
Zero-cpoy : Zero-copy是一个特殊属性,当前只能在NIO和Epoll传输服务中可使用,它允许你可以快速高效地移动你的数据从文件系统到网络中而不需要从你的内存空间复制到你的用户空间,这对于你提高协议中的传输性能是一个非常重要的特性,例如FTP和HTTP协议,但是这种特性并不是被所有的操作系统所支持的,具体来说,如果数据被加密或者压缩过就不能正常使用了,只有一些简单的原生的文件内容可以被传输
Epoll—Linux的native非阻塞传输服务
Netty的NIO传输服务是基于Java提供的对非阻塞网络编程的通用抽象来实现的。尽管这足以保证Netty的非阻塞的API可以在任何的平台的可用性,但这依旧会有一些限制,因为JDK为了能在所有的系统上有同样的可用性做了一些折。
因为linux的网络的高性能促使了很多一些特性的产生,包括epoll,一个高扩展性的IO事件通知的新特性。
Netty为Linux提供的NIO的API是使用的epoll的,通过这个方法我们可以与使用的linux保持一致,而不需要浪费一些性能。思考一下如果你的系统是linux,你的应用可以利用这个特性,你会发现在高负载的情况下这比JDK的NIO的实现更加高效。如果用epoll替换NIO,只需要将NioEventLoopGroup
用EpollEventLoopGroup
替换,NioServerSocketChannel
用EpollServerSocketChannel
替换就可以了。
OIO—旧的阻塞IO模型
从Netty的OIO传输服务实现代表了一种折中:它是通过通用传输服务API来实现的,但因为是建立在java.net
包的基础上的,所以不是异步的。这适用于一些特定的场合。
举例来说,你也许需要一些普通的代码来实现阻塞调用例如JDBC,如果你将其转化成非阻塞的也许并不是那么的实用,短期内你可以直接使用Netty的OIO传输服务,如果有需要你可以在未来的时间内将其转化成其他的任意一种异步传输,我们还是先看看阻塞通信是如何工作的吧。
在java.net包下的API,经常有一个线程接收新的连接到serverSocket的请求,一个新的socket即将被创建来与远程服务端进行交互,然后需要一个新的线程分配来处理后继的数据交互,多开一个新的线程是有必要的,因为在一个具体的socket上的任何IO操作都可能随时被阻断,如果用一个线程处理多个sockets很容易导致一个阻塞的操作也会影响其他的操作。
正是因为如此,你可能会有疑问为什么可以使用与非阻塞一样的API来支持OIO呢?因为Netty使用了SO_TIMEOUT这个socket的参数标识,它规定了一个I/O操作完成的最长等待的毫秒数,如果在内部规定的时间内操作并没有完成,那么一个SocketTimeOutException将会被抛出,Netty将会捕获这个异常,然后继续处理循环,在下一个EcentLoop运行的时候,它会再次尝试,这是像Netty这样的异步框架能支持OIO的唯一方式,上图说明了这个逻辑
在JVM中通过本地传输服务通信
Netty为运行在同一个虚拟机上的服务端和客户端提供了本地传输的异步通信。同样,这个传输服务支持的API与其他所Netty的传输服务实现相同。
这种传输方式,与服务端channel相连的SocketAddress并没有绑定物理地址,而是,只要服务器一运行就在注册器上存储注册,只要channel一关闭,就从注册器上解除注册,因为这种传输服务并没有真实的网络传输产生,它不能与其他的传输服务相互操作,因此,在同一个JVM上的客户端想要连接到服务器端的话,必须使用这种传输方式,除了这种限制,它就与其他的传输服务一样了
嵌入式传输服务
Netty还提供了一种额外的传输方式允许你将一些ChannelHandler作为一种辅助工具类嵌入到其他的ChannelHandler中,在这种方式下,我们可以在不修改内部代码的基础上扩展ChannelHandler的功能
传输服务用例
接下来让我们考虑如何为一个特定的用例选择传输协议。正如之前所说的,不是所有的传输服务都支持所有核心传输协议。下表显示了这种支持情况:
传输服务 | TCP | UDP | SCTP | UDT |
---|---|---|---|---|
NIO | X | X | X | X |
Epoll(linux) | X | X | — | — |
OIO | X | X | X | X |
在Linux中启用SCTP
SCTP需要内核支持同时需要用户来安装
比如,Ubuntu系统你使用如下命令:
# sudo apt-get install libsctp1
Fedora系统:
# sudo yum install kernel-modules-extra.x86_64 lksctp-tools.x86_64
尽管只有SCTP协议需要这些特定的配置,但是一些其他的传输服务需要它们自己要考虑的配置选项。而且,如果你想支持更高的并发连接数,服务器的配置可能需要有客户端不同。
下面是你可能会遇到的一些用例:
- 非阻塞代码—如果在你的代码中没有阻塞调用(或者你可以限制),使用NIO或epoll(Linux系统上)是一个好主意。尽管NIO/epoll是用来处理高并发连接,但在低并发连接的情况下也能工作良好,特别是在多个连接中共享线程的情况。
- 阻塞代码—如果你的代码库严重依赖于阻塞的I/O,那么你的应用应该有一个相应的设计,如果你想把你的阻塞操作直接转化成Netty的NIO传输的时候,在且你不是用重写原有代码去完成转化的功能,你可能会遇到一些问题,例如你可以考虑一种迁移场景,你的应用一开始使用OIO,然后迁移到NIO,你需要重新修改你的代码
- 在同一个JVM下通信一同一个JVM上通信且不需要通过网络暴露你的服务的场景下,使用本地传输会是一个很棒的决定,这样可以在使用的你代码的基础上,消除所有的网络传输操作上的开销,如果你在以后的时间里想将你的服务暴露出去,你可以将其转化成NIO或者OIO
- 测试你的ChannelHandler实现一如果你想要为你的channelHandler写一个测试单元的话,你可以考虑使用嵌入式的传输方式,这个方式可以在不需要创造很多mock对象的基础上很轻易地测试你的代码,你可以测试再所有的事件流上的常用的API,且保证你的channelHandler在真实的传输服务上运行正确
Netty in action—Netty传输服务相关推荐
- Netty In Action中文版
第一章:Netty介绍 本章介绍 Netty介绍 为什么要使用non-blocking IO(NIO) 阻塞IO(blocking IO)和非阻塞IO(non-blocking IO)对比 Java ...
- Netty In Action中文版 - 第十二章:SPDY
Netty In Action中文版 - 第十二章:SPDY 本章我将不会直接翻译Netty In Action书中的原文,感觉原书中本章讲的很多废话,我翻译起来也吃力.所以,本章内容我会根据其他资料 ...
- Netty使用kryo序列化传输对象
Netty使用kryo序列化传输对象 横渡 Netty使用kryo序列化传输对象 - 简书参考文章:https://blog.csdn.net/eguid_1/article/details/7931 ...
- Netty in Action (十九) 第九章节 单元测试
本章内容包括: 1)单元测试 2)EmbeddedChannel的说明 3)使用EmbeddedChannel测试ChannelHandler 对于一个Netty应用来说,ChannelHandler ...
- Netty学习笔记(二) 实现服务端和客户端
在Netty学习笔记(一) 实现DISCARD服务中,我们使用Netty和Python实现了简单的丢弃DISCARD服务,这篇,我们使用Netty实现服务端和客户端交互的需求. 前置工作 开发环境 J ...
- Netty 源码解析系列-服务端启动流程解析
netty源码解析系列 Netty 源码解析系列-服务端启动流程解析 Netty 源码解析系列-客户端连接接入及读I/O解析 五分钟就能看懂pipeline模型 -Netty 源码解析 1.服务端启动 ...
- Netty in Action 读书
Netty in Action 作者: Norman Maurer / Marvin Allen Wolfthal 出版社:Manning Publications 出版年:2015-12-31 页数 ...
- Netty简单实现客户端与服务端收发消息
Netty简单实现客户端与服务端收发消息 这个小案例主要是实现netty收发消息,分为客户端,及服务端,以及包含了相关状态处理,主要的代码会放在最后 gitHub 地址上,有需要可以看一下 首先来简单 ...
- netty源码学习之服务端客户端初始化
文章目录 1. AbstractBootstrap类简介 1.1. 核心方法 2. netty服务端创建 2.1. 服务端启动入口 2.2. doBind()方法 2.3. netty服务初始化 2. ...
- 《Netty IN ACTION》中文版《Netty实战》翻译手记——不负好时光
不负好时光--<Netty in Action>中文版<Netty实战>翻译手记 引子 "书中自有黄金屋,书中自有颜如玉",这句话从小我老爸就给我讲,当然那 ...
最新文章
- Vue.js插槽slot和作用域插槽slot-scope学习小结
- Restful、SOAP、RPC、SOA、微服务之间的区别
- 【PHP】月末・月初の出力方法
- 单目视觉里程计 mono vo
- ROS学习笔记8(使用 rqt_console, rqt_graph 和 roslaunch)
- AHS of FCGRC 停课 Day 3
- 移动端适配的理解——REM方案
- 【C++代码整洁之道】遗留系统之殇
- 基于Matlab与Logistic Regression(逻辑回归)的瓶子密封性检测
- cmd批处理文件格式
- 在电路中,耦合是什么?有哪些方式?
- 阿里云DDoS防护产品介绍
- IDEA模块名后面中括号中内容与模块名不一致的问题
- Calendar(日历)
- (24)STM32——待机唤醒(低功耗)笔记
- 【tool】动态注释LOG_NDEBUG宏定义
- ios 内存深度优化_iOS性能优化之内存(memory)优化
- http://nian.so/#网站的拓展工具编写
- daemon函数理解及参数使用——daemon后进程退出的原因
- 多易教育KAFKA实战(4)-原理加强
热门文章
- Laser Reflections solutions
- J2EE Architecture(6)
- 软件工程作业团队作业No.5
- C++中long是什么类型
- 队列的实现(二) 链式队列的实现
- 【P000-004】交易费计算系统,功能类规划
- javascript特效:会随着鼠标而动的眼睛
- STL中queue(队列)介绍
- [导入]【布鲁斯威利斯】【虎胆龙威4最终珍藏版】【1024x432RMVB 1.41GB】【20:50】...
- spring+mybatis通用dao层、service层的实现