引言
《跟闪电侠学 Netty》 并不是集体接触的第一本 Netty 书籍,但集体更举荐读者把它作为作为第一本 Netty 入门的书籍。
和 《Netty In Action》 不同,这本书间接从 Netty 入门程序代码开始引入 Netty 框架,前半部分教你如何用 Netty 搭建繁难的通信零碎,整体难度比拟低,后半局部间接从服务端源码、客户端源码、ChannelPipeline 开始介绍,和前半部分割裂较为重大。
相较于入门的程序,源码剖析毫无疑问是比拟有干货的局部,然而和后面入门程序相比有点学完了 99 乘法表就让你去做微积分的卷子一样,如果 Netty 应用陌生源码局部解说必定是非常难懂的,所以更倡议只看前半截。
集体比拟举荐这本书吃透 Netty 编写的简略通信“我的项目”之后,间接去看《Netty In Action》做一个更为零碎的深刻和根底坚固。等《Netty In Action》看明确之后,再回过头来看《跟闪电侠学 Netty》的源码剖析局部。
抛开源码剖析局部,这本书是“我奶奶都能学会”的优良入门书籍,用代码实战加解说形式学起来轻松印象粗浅。
开篇入门局部先不引入我的项目,这里先对于过来 JDK 的网络 IO 模型作为引子介绍为什么咱们须要用 Netty,学习 Netty 带来的益处等。
思维导图
Netty 依赖版本(4.1.6.Final)
本书应用的 Netty 版本为 4.1.6,为了防止前面浏览源码的时候产生误解,倡议以此版本为基准。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
JDK 原生编程模型
到目前为止,JDK 一共实现了三种网络 IO 编程模型:BIO、NIO 和 AIO。遗憾的是这三种模型不仅产生的距离时间跨度大,并且由三组齐全不同编程格调的开发人员设计 API,不同编程模型和设计思路之间的切换十分复杂,开发者的学习老本也比拟大。
针对这些问题,咱们间接理解 Netty 如何对立这些模型以及如何升高并发编程的开发难度,咱们先对过来的 JDK 网络 IO 编程模型做一个理解。
洗衣机洗衣服例子理清了解阻塞非阻塞,同步异步概念
在理解 JDK 的网络 IO 模型之前,比方得先理解绕不过的 阻塞非阻塞 , 同步异步 的概念。
同步和异步指的是工作之间是否须要期待其它工作实现或者期待某个事件的产生。如果一个工作必须期待另一个工作实现能力继续执行,那么这两个工作就是 同步的;如果一个工作能够间接继续执行而无需期待另一个工作的实现,那么这两个工作就是异步的。
阻塞和非阻塞指的是工作在期待后果时是否会始终占用 CPU 资源。如果一个工作在期待后果时会始终占用 CPU 资源,那么这个工作就是阻塞的;如果一个工作在期待后果时不会占用 CPU 资源,那么这个工作就是非阻塞的。
这里给一个生存中洗衣服的例子帮忙齐全没有理解过这些概念的读者加深印象,这个例子来源于某个网课,集体感觉非常贴切和易懂就拿过去用了。
同步阻塞
了解:
洗衣服丢到洗衣机,全程看着洗衣机洗完,洗好之后晾衣服。
类比:
- 申请接口
- 期待接口返回后果,两头不能做其余事件。
- 拿到后果解决数据
剖析:
同步:全程看着洗衣机洗完。
阻塞:期待洗衣机洗好衣服之后跑过来晾衣服。
同步非阻塞
了解:
把衣服丢到洗衣机洗,而后回客厅做其余事件,定时看看洗衣机是不是洗完了,洗好后再去晾衣服。(期待期间你能够做其余事件,比方用电脑刷剧看视频)。
这种模式相似日常生活洗衣机洗衣服。
类比:
- 申请接口。
- 期待期间切换到其余工作,然而须要定期察看接口是否有回送数据。
- 拿到后果解决数据。
剖析:
和阻塞形式的最大区别是不须要始终盯着洗衣机,期间能够抽空干其余的事件。
同步:期待洗衣机洗完这个事件没有实质变动,洗好衣服之后还是要跑过来晾衣服。
非阻塞:拿到衣服之前能够干别的事件,只不过须要每次隔一段时间查看能不能拿到洗好的衣服。
异步阻塞
了解:
把衣服丢到洗衣机洗,而后看着洗衣机洗完,洗好后再去晾衣服(没这个状况,简直没这个说法,能够疏忽)。
类比:
- 申请接口,不须要关怀后果。
- 客户端能够抽空干其余事件,然而非得期待接口返回后果
- 拿到服务端的处理结果
剖析:
难以描述,简直不存在这种说法。
异步非阻塞
了解:
把衣服丢到洗衣机洗,而后回客厅做其余事件,洗衣机洗好后会主动去晾衣服,晾实现后放个音乐通知你 洗好衣服并晾好了。
类比:
- 申请接口,此时客户端能够继续执行代码。
- 服务端筹备并且解决数据,在 解决实现 之后在适合的工夫告诉客户端
- 客户端收到服务端解决实现的后果。
剖析:
异步:洗衣机本人不仅把衣服洗好了还帮咱们把衣服晾好了。
非阻塞:拿到“衣服”后果之前能够干别的事件。
留神异步非阻塞状况下,“咱们”看待洗衣服这件事件的“态度”齐全变了。
BIO 编程模型
BIO 叫做阻塞 IO 模型,在阻塞 IO 模型中两个工作之间须要期待响应后果,利用过程须要期待内核把整个数据筹备好之后能力开始进行解决。
BIO 是入门网络编程的第一个程序,从 JDK1.0 开始便存在了,存在于
java.net
包当中。上面的程序也是入门 Tomcat 源码的根底程序。
Java 实现代码
在 BIO 的实现代码中,服务端通过 accept
始终阻塞期待直到有客户端连贯。首先是服务端代码。
public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket(8000);
// 承受连贯
new Thread(() -> {while (true) {
// 1. 阻塞获取连贯
try {Socket socket = serverSocket.accept();
// 2. 为每一个新连贯应用一个新线程
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 字节流读取数据
while ((-1 != (len = inputStream.read()))) {System.err.println(new String(data, 0, len));
}
} catch (IOException ioException) {ioException.printStackTrace();
}
}).start();} catch (IOException e) {e.printStackTrace();
}
}
}).start();}
较为外围的局部是 serverSocket.accept()
这一串代码,会导致服务端阻塞期待客户端的连贯申请,即便没有连贯也会始终阻塞。
服务端启动之后会监听 8000 端口,期待客户端连贯,此时须要始终占用 CPU 资源,获取到客户端连贯之将会开拓一个新的线程独自为客户端提供服务。
而后是客户端代码。
public static void main(String[] args) {new Thread(()->{
try {Socket socket = new Socket("127.0.0.1", 8000);
while (true){socket.getOutputStream().write((new Date() + ":"+ "hellow world").getBytes(StandardCharsets.ISO_8859_1));
try {Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();
}
}
} catch (IOException ioException) {ioException.printStackTrace();
}
}).start();}
客户端的外围代码如下,通过建设 Socket 和服务端建设连贯。
Socket socket = new Socket("127.0.0.1", 8000);
Connected to the target VM, address: '127.0.0.1:5540', transport: 'socket'
客户端启动之后会距离两秒发送数据给服务端,服务端收到申请之后打印客户端传递的内容。
Connected to the target VM, address: '127.0.0.1:5548', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:5548', transport: 'socket'
Process finished with exit code 130
优缺点剖析
传统的 IO 模型有如下优缺点:
-
长处
- 实现简略。
- 客户端较少状况下运行良好。
-
毛病
- 每次连贯都须要一个独自的线程。
- 单机单核心线程上下文切换代价微小。
- 数据读写只能以字节流为单位。
- while(true)死循环十分节约 CPU 资源。
- API 艰涩难懂,对于编程人员须要思考十分多的内容。
论断:
在传统的 IO 模型中,每个连贯创立胜利之后都须要一个线程来保护,每个线程蕴含一个 while 死循环,那么 1w 个连贯对应 1w 个线程,继而 1w 个 while 死循环。
单机是不可能实现同时撑持 1W 个线程的,然而在客户端连贯数量较少的时候,这种形式效率很高并且实现非常简单。
NIO 编程模型
NIO 编程模型是 JDK1.4 呈现的全新 API,它实现的是 同步非阻塞 IO编程模型。以上面的模型为例,第二阶段仍然须要期待后果之后被动解决数据,次要的区别在第一阶段(红线局部)轮询的时候能够干别的事件,只需屡次调用查看是否有数据能够开始读取。
Java 实现代码
NIO 编程模型中,新来一个连贯不再创立一个新的线程,而是能够把这条连贯间接绑定到某个 指定线程。
概念上了解 NIO 并不难,然而要写出 JDK 的 NIO 编程模板代码却不容易。
public static void main(String[] args) throws IOException {Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 对应 IO 编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 监测是否有新的连贯,这里的 1 指的是阻塞的工夫为 1ms
if (serverSelector.select(1) > 0) {Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {// (1) 每来一个新连贯,不须要创立一个线程,而是间接注册到 clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {}}).start();
new Thread(() -> {
try {while (true) {// (2) 批量轮询是否有哪些连贯有数据可读,这里的 1 指的是阻塞的工夫为 1ms
if (clientSelector.select(1) > 0) {Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 读取数据以块为单位批量读取
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {}}).start();}
下面的代码不须要过多纠结,NIO 的代码模板的确非常复杂,咱们能够把下面的两个线程看作是两个传送带,第一条传送带只负责接管内部的连贯申请,收到申请数据之后间接丢给第二条传送带解决。第二条传送带收到工作之后进行解析和解决,最初把后果返回即可。
书中并没有给 NIO 的客户端案例,然而有意思的是 Netty 的客户端启动连贯代码 能够完满连接 JDK 的 NIO Server 服务端,从这一点上能够发现 Netty 的 NIO 编程模型实际上就是对于 JDK NIO 模型的改进和优化。
PS:后续篇章的源码浏览能够看到 Netty 和 JDK 的 API 的关系密不可分。
public static void main(String[] args) throws InterruptedException {Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
// 疏导器疏导启动
bootstrap.group(eventExecutors)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {channel.pipeline().addLast(new StringEncoder());
}
});
// 建设通道
Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
while (true){channel.writeAndFlush(new Date() + "Hello world");
Thread.sleep(2000);
}
}
Netty 无论是客户端启动还是服务端启动都会打印一堆日志,上面是客户端启动日志。
14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimIntervalMillis: 0
14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.useCacheForAllThreads: false
14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedByteBuffersPerChunk: 1023
14:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.allocator.type: pooled
14:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.threadLocalDirectBufferSize: 0
14:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 16384
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 4096
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.chunkSize: 32
14:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.blocking: false
14:42:24.060 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkAccessible: true
14:42:24.060 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkBounds: true
14:42:24.060 [nioEventLoopGroup-2-1] DEBUG i.n.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@310af49
Disconnected from the target VM, address: '127.0.0.1:13875', transport: 'socket'
Process finished with exit code 130
客户端连贯之后会距离 2S 向服务端推送以后工夫。
Connected to the target VM, address: '127.0.0.1:13714', transport: 'socket'
Tue Apr 11 14:42:24 CST 2023 Hello world
Tue Apr 11 14:42:26 CST 2023 Hello world
Tue Apr 11 14:42:28 CST 2023 Hello world
JDK 的 NIO 针对 BIO 的改进点
NIO 模型工作上有了“分工”的细节,即两个 Selector,一个负责承受新连贯,另一个负责解决连贯传递的数据。
比照 BIO 模型一个连贯就调配一个线程的策略,NIO 模型的策略是让所有的连贯注册过程变为由 一个 Selector实现,Selector 会定期轮询查看哪个客户端连贯能够接入,如果能够接入就注册到以后的 Selector,后续遇到数据读取只须要轮询一个 Selector 就行了。
线程资源受限问题通过 Selector 将每个客户端的while(true) 转为只有一个 while(true) 死循环得以解决,它的“副作线程用”是线程的缩小间接带来了切换效率的晋升。不仅如此 NIO 还提供了面向 Buffer 的缓存 ByteBuffer,进步读写效率,挪动指针任意读写。
JDK 的 NIO 编程模型毛病
看起来无非就是代码简单了一点,其实 NIO 模型看起来也“还不错”?
NO!NO!NO!JDK 的 NIO 实际上还有很多其余问题:
-
- API 简单难用,须要了解十分多的底层概念。(尤其是臭名远扬的 ByteBuffer)
-
- JDK 没有线程模型,用户须要本人设计底层 NIO 模型。
-
- 自定义协定也要拆包。
-
- JDK 的 NIO 是因为 Epoll 实现的,底层存在空轮询的 BUG。
-
- 自行实现 NIO 模型会存在很多问题。
-
- 编程人员的编程程度档次不齐,集体定制的 NIO 模型难以通用,替换性也很差。
基于以上种种问题,Netty 通通都有解决方案。
简略介绍 AIO
JDK 的 AIO 不是很成熟,AIO 底层仍然因为 Epoll 的遗留问题存在臭名远扬的空轮询 BUG,这里并不举荐读者应用 JDK 的 AIO 进行编程。
Java AIO 的外围在于两个要害类:AsynchronousSocketChannel 和 AsynchronousServerSocketChannel。
AsynchronousSocketChannel 实现异步套接字通信,能够让咱们在不同的客户端连贯之间切换,而无需创立新的线程或线程池。
AsynchronousServerSocketChannel 则用于异步地监听客户端的连贯申请。
Java 实现代码
这里用 ChatGPT 生成了一段 JDK 的 AIO 代码,为了更好了解顺带让它把正文一块生成了。
public class AIOServer {public static void main(String[] args) throws IOException {
// 创立一个 ExecutorService,用于解决异步操作的线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 创立一个 AsynchronousChannelGroup,将线程池与该 Channel 组关联
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(executor);
// 创立 AsynchronousServerSocketChannel,并绑定到指定地址和端口
final AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);
InetSocketAddress address = new InetSocketAddress("localhost", 12345);
serverSocketChannel.bind(address);
System.out.println("Server started on port" + address.getPort());
// 调用 accept 办法接管客户端连贯,同时传入一个 CompletionHandler 解决连贯后果
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
// 当连贯胜利时会调用 completed 办法,传入客户端的 SocketChannel 实例作为参数
@Override
public void completed(AsynchronousSocketChannel clientSocketChannel, Object attachment) {
// 持续承受下一个客户端连贯,并解决以后客户端的申请
serverSocketChannel.accept(null, this);
handleClient(clientSocketChannel);
}
// 当连贯失败时会调用 failed 办法,传入异样信息作为参数
@Override
public void failed(Throwable exc, Object attachment) {System.out.println("Error accepting connection:" + exc.getMessage());
}
});
// 在主线程中期待,避免程序退出
while (true) {
try {Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {break;}
}
}
private static void handleClient(AsynchronousSocketChannel clientSocketChannel) {ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取客户端发送的数据,同时传入一个 CompletionHandler 解决读取后果
clientSocketChannel.read(buffer, null, new CompletionHandler<Integer, Object>() {
// 当读取胜利时会调用 completed 办法,传入读取到的字节数和附件对象(此处不须要)@Override
public void completed(Integer bytesRead, Object attachment) {if (bytesRead > 0) {
// 将 Buffer 翻转,以便进行读取操作
buffer.flip();
byte[] data = new byte[bytesRead];
buffer.get(data, 0, bytesRead);
String message = new String(data);
System.out.println("Received message:" + message);
// 向客户端发送数据
clientSocketChannel.write(ByteBuffer.wrap(("Hello," + message).getBytes()));
buffer.clear();
// 持续读取下一批数据,并传入以后的 CompletionHandler 以解决读取后果
clientSocketChannel.read(buffer, null, this);
} else {
try {// 当客户端敞开连贯时,敞开该 SocketChannel clientSocketChannel.close();
} catch (IOException e) {System.out.println("Error closing client socket channel:" + e.getMessage());
}
}
}
// 当读取失败时会调用 failed 办法,传入异样信息和附件对象(此处不须要)@Override
public void failed(Throwable exc, Object attachment) {System.out.println("Error reading from client socket channel:" + exc.getMessage());
}
});
}
}
AIO 编程模型优缺点
长处:
并发性高、CPU 利用率高、线程利用率高。
毛病:
不适宜轻量级数据传输,因为过程之间频繁的通信在追错、治理,资源耗费上不是很可观。
实用场景:
对并发有需要的 重量级 数据传输。
从下面的代码也能够看出,AIO 的 API 和 NIO 又是截然不同的写法,为了不持续减少学习老本,这里点到为止,不再深刻 AIO 编程模型的局部了,让咱们持续回到 Netty,理解 Netty 的编程模型。
应用 Netty 带来的益处
- Netty 不须要理解过多概念
- 底层 IO 模型随便切换
- 自带粘包拆包的问题解决
- 解决了空轮询问题
- 自带协定栈,反对通用协定切换
- 社区沉闷,各种问题都有解决方案
- RPC、消息中间件实际,健壮性极强
网络 IO 通信框架过程
一个网络 IO 通信框架从客户端发出请求到承受到后果,根本蕴含了上面这 8 个操作:
-
- 解析指令
-
- 构建指令对象
-
- 编码
-
- 期待响应
-
- 解码
-
- 翻译指令对象
-
- 解析指令
-
- 执行
上面来看看 Netty 的编程模型。
Netty 启动模板代码(重要)
通过下面一长串的铺垫,当初来到整体 Netty 的代码局部:
服务端
首先是服务端代码:
public static void main(String[] args) {ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap
.group(boos, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {protected void initChannel(NioSocketChannel ch) {ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println(msg);
}
});
}
})
.bind(8000);
}
初学 Netty 的时候可能没有 NIO 的教训,所以咱们简略做个类比:
NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
能够间接看作
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
其中 boss 负责解决连贯,worker 负责读取申请和解决数据。两者的工作模式也是相似的,boss 就像是老板负责“接单”,worker 打工仔负责接管单子的内容而后开始打工干活。
客户端
客户端的启动代码如下。
public static void main(String[] args) throws InterruptedException {Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
// 疏导器疏导启动
bootstrap.group(eventExecutors)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {channel.pipeline().addLast(new StringEncoder());
}
});
// 建设通道
Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
while (true){channel.writeAndFlush(new Date() + "Hello world");
Thread.sleep(2000);
}
}
客户端的代码中的 NioEventLoopGroup
理论对应了 main 函数独自开启的线程。下面的代码能够完满的代替调 JDK 的 NIO、AIO、BIO 的 API,学习老本大大降低,Netty 为使用者做了大量的“筹备”工作,提供了很多 ” 开箱即用 ” 的性能,十分不便。
Netty 的服务端和客户端的入门程序代码是剖析源码的开始,这部分代码须要有较深的印象。
问题
摘录局部 Netty 入门级别的八股。
Linux 网络编程中的五种 I / O 模型
关键点:
不同的角度了解 IO 模型的概念会有变动。留神本局部站在 用户程序和内核的网络 IO 交互 的角度了解的。
权威:
- RFC 规范
- 书籍 《UNIX Network Programming》(中文名《UNIX 网络编程 - 卷一》)第六章。
上面局部总结自:《UNIX Network Programming》(中文名《UNIX 网络编程 - 卷一》)
1)阻塞式 I /O
留神原书中 阻塞式 I /O给出的例子是 UDP 而不是 TCP 的例子。recvfrom 函数能够看作是零碎调用,在阻塞 I / O 模型中,recvfrom 的零碎调用要期待内核把数据从内核态拷贝到用户的缓冲池或者产生谬误的时候(比方信号中断)才进行返回。recvfrom 收到数据之后再执行数据处理。
2)非阻塞式 I /O
recvfrom 的零碎调用会在设置非阻塞的时候,会要求内核在无数据的时候返回谬误,所以后面三次都是谬误调用,在第四次调用之后此时 recvfrom 轮询到数据,于是开始失常的期待内核把数据复制到用户过程缓存。
此处轮询的定义为:对于描述符进行 recvfrom 循环调用,会减少 CPU 的开销。留神非阻塞的轮询不肯定要比阻塞期待要强,有时候甚至会有无意义的开销反而不如阻塞。
3)I/ O 复用(select,poll,epoll…)
I/ O 多路复用是阻塞在 select,epoll 这样的零碎调用,没有阻塞在真正的 I / O 零碎调用如 recvfrom。过程受阻于 select,期待可能多个套接口中的任一个 变为可读。
IO 多路复用最大的区别是应用 两个零碎调用(select 和 recvfrom)。Blocking IO(BIO)只调用了一个零碎调用(recvfrom)。
select/epoll 外围是能够同时解决多个 connection,然而这并不一定晋升效率,连接数不高的话性能不肯定比多线程 + 阻塞 IO 好。然而连接数比拟宏大之后会有显著的差距。
多路复用模型中,每一个 socket 都须要设置为 non-blocking,否则是无奈进行 elect 的。
listenerChannel.configureBlocking(false);
这个设置的意义就在于此。
4)信号驱动式 I /O(SIGIO)
信号驱动的劣势是期待数据报到之前过程不被阻塞,主循环能够继续执行,期待信号到来即可,留神这里有可能是数据曾经筹备好被解决,或者数据复制实现能够筹备读取。
信号驱动 IO 也是同步模型,尽管能够通过信号的形式缩小交互,然而零碎调用过程当中仍然须要进行期待,内核也仍然是告诉何时开启一个 IO 操作,和后面介绍的 IO 模型比照发现劣势并不显著。
5)异步 I /O(POSIX 的 aio_系列函数)
外围:Future-Listener 机制
-
IO 操作分为两步
- 发动 IO 申请,期待数据筹备(Waiting for the data to be ready)
- 理论的 IO 操作,将数据从内核拷贝到过程中(Copying the data from the kernel to the process)
前四种 IO 模型都是同步 IO 操作,次要的区别在于第一阶段解决形式,而他们的第二阶段是一样的:在数据从内核复制到利用缓冲区期间(用户空间),过程阻塞于 recvfrom 调用 或者select() 函数。异步 I / O 模型外在这两个阶段都要(自行)解决。
阻塞 IO 和非阻塞 IO的 区别 在于第一步,发动 IO 申请是否会被阻塞,如果阻塞直到实现那么就是传统的阻塞 IO,如果不阻塞,那么就是非阻塞 IO。
同步 IO 和异步 IO 的区别就在于第二个步骤是否阻塞,如果理论的 IO 读写阻塞申请过程,那么就是同步 IO,因而阻塞 IO、非阻塞 IO、IO 复用、信号驱动 IO 都是同步 IO,如果不阻塞,而是 操作系统帮你做完 IO 操作再将后果返回给你 ,那么就是 异步 IO。
异步 IO 模型十分像是咱们日常点外卖,咱们时不时看看配送进度就是在“轮询”,当外卖员把外卖送到指定地位打电话告诉咱们去拿即可。
交互几个外围点
再次强调是针对用户程序和内核的网络 IO 交互角度了解的。
- 阻塞非阻塞说的是线程的状态(重要)
- 同步和异步说的是音讯的告诉机制(重要)
– 同步须要被动读写数据, 异步是不须要被动读写数据
– 同步 IO 和异步 IO 是针对用户应用程序和内核的交互
为什么 Netty 应用 NIO 而不是 AIO?
- Netty 不看重 Windows 上的应用,在 Linux 零碎上,AIO 的底层实现仍应用 EPOLL,没有很好实现 AIO,因而在性能上没有显著的劣势,而且 被 JDK 封装了一层不容易深度优化。
- Netty整体架构是reactor 模型, 而AIO 是 proactor 模型, 混合在一起会十分凌乱, 把 AIO 也革新成 reactor 模型看起来是把 epoll 绕个弯又绕回来
- AIO 还有个毛病是接收数据须要事后调配缓存, 而不是 NIO 那种须要接管时才须要调配缓存, 所以对连贯数量十分大但流量小的状况, 内存节约很多
- Linux 上 AIO 不够成熟,解决回调后果速度跟不到解决需要,比方外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)。
论断
Netty 整体架构是 reactor 模型,采纳 epoll 机制,所以往深的说,还是 IO 多路复用模式,所以也可说 netty 是 同步非阻塞模型(看的档次不一样),只不过理论是异步 IO。
Netty 利用场景理解么?
- 作为 RPC 框架的网络通信工具。分布式系统之间的服务器通信能够应用 Netty 实现,尽管是 Java 编写的框架,然而性能十分靠近 C 和 C ++ 执行效率。
- 作为 RPC 框架的网络通信工具,Netty 自身就是
- 音讯队列 :比方赫赫有名的RocketMq 底层齐全依赖 Netty,编程人员不须要很强的并发编程功底也能够疾速上手和保护代码。
- 实现一个即时通讯零碎:正好和本书利用场景重合了。
介绍 Netty
简短介绍
Netty 是一个 高性能 、 异步 、NIO 编程模型 的网络编程框架。它提供了简略易用的 API,能够疾速地开发各种网络应用程序,如客户端、服务器和协定实现等。同时,Netty 还具备良好的可扩展性和灵活性,反对多种传输协定和编解码器。
略微简单一点
Netty 是由 JBOSS 提供的一个 java 开源框架, 是业界最风行的 NIO 框架,整合了多种协定(包含 FTP、SMTP、HTTP 等各种二进制文本协定)的实现教训,精心设计的框架,在多个大型商业我的项目中失去充沛验证。1)API 应用简略 2)成熟、稳固 3)社区沉闷 有很多种 NIO 框架 如 mina 4)通过大规模的验证(互联网、大数据、网络游戏、电信通信行业)。
总结
- 开篇简略介绍了 JDK 的 BIO、NIO 和 AIO,三者不仅呈现时间跨度大,三个团队编写,和 JDK 的 IO 编程一样艰涩难懂和不好用,开发人员须要花大量事件学习底层细节。
- 用洗衣机的例子,了解网络编程模型的重要概念:同步、非同步、阻塞、非阻塞。从入门的角度来看,同步和异步能够认为是否是由客户端被动获取数据,而阻塞和非阻塞则是客户端是否须要拿到后果进行解决,两者是相辅相成的。
- Netty 编程模型对立了 JDK 的编程模型,升高了学习老本,同时效率比原生 JDK 更高,并且解决了 NIO 中的空轮询问题。
- Netty 底层实际上和 JDK 的网络编程模型密切相关,从案例代码能够看到 Netty 的客户端 API 代码能够间接往 NIO 的 Server 发送数据。
- 补充书中没有介绍的 AIO 编程模型,用 ChatGPT 生成的代码简略易懂。
- 最初补充无关 Netty 的问题。
写在最初
开篇局部补充了书中没介绍的一些网络编程模型的基本概念,以及在最初关联了些相干书籍的知识点和,最初顺带演绎了一些八股问题,当然最为重要的局部是相熟 Netty 的入门程序代码。
开篇入门篇到此就完结了,如果内容形容有误,欢送评论或者私信留言。
参考
- 《跟闪电侠学 Netty》开篇:Netty 是什么?– 简书 (jianshu.com)
- 网课专栏:《高并发系列之百万连贯 Netty 实战课程》
Netty 书籍举荐
- 《Netty 权威指南》
- 《Netty 进阶之路》