开始
最近在学习Netty,其核心便是基于Java的NIO编程封装而来,这篇文章对NIO编程的原理进行介绍,并提供一个NIO编程的完整实践案例。
什么是Netty?
Netty的特点总结如下:
- Netty是由JBOSS提供的一个Java开源框架,现在为Github上的独立开发项目。
- Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。
- Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。
- Netty的本质是一个NIO框架,用于服务器通信。
Java网络编程三兄弟:BIO、NIO、NIO2(AIO)
显然NIO是Netty的灵魂,由于NIO属于Java三大I/O模型之一,要学习它不如把这三者合在一起先做个了解。
I/O模型 | 类型 | 描述 |
---|
Java BIO | 同步阻塞(传统阻塞型) | 服务器实现模式为一个连接绑定一个线程即客户端有一个连接,当有客户端请求时,就需要启动一个线程进行处理。如果这个连接不做任何事情则造不必要的线程开销。 |
Java NIO | 同步非阻塞 | 服务器实现模式为一个线程处理多个请求,即客户端发送的请求会注册到多路复用器上,多路复用器可以轮询存在的I/O请求进行处理。 |
Java AIO | 异步非阻塞 | AIO引入异步通道的概念,采用Proactor模式,简化了程序的编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 |
NIO核心原理
NIO
由三大核心部分组成:
Channel
:通道。Buffer
:缓冲区。Selector
:选择器。
原理说明:
- 每个
Channel
对应一个Buffer
。 Selector
对应一个线程,一个线程对应多个Channel
。- 多个
Channel
可以注册到一个Selector。
- 程序切换Channel由事件决定。
Selector
会根据不同事件,在各个Channel
切换。- 数据的读取或者写入通过
Buffer
,是双向的,但需要flip()
切换读/写模式。 Channel
是双向的,可以返回底层操作系统的情况(比如Linux
底层是双向的)。
NIO模式示意图如下:
关于Channel
基本介绍:
BIO
中的stream
是单向的,例如FileInputStream
对象只能进行读取数据的操作,而NIO中的通道(Channel
)是双向的,可以读操作,也可以写操作。Channel
在NIO
中是一 个接口:public interface Channel extends Closeable{}。- 常用的
Channel
类有: FileChannel
、DatagramChannel
、ServerSocketChannel
和SocketChannel
。ServerSocketChannel
类似ServerSocket
,SocketChannel
类似Socket
FileChannel
用于文件的数据读写,DatagramChannel
用于UDP
的数据读写,ServerSocketChannel
和SocketChannel
用于TCP
的数据读写。
关于Buffer
Java的基本数据类型(除了boolean)均有一个Buffer类与之对应,最常用的自然是ByteBuffer
类,该类的主要方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public abstract class ByteBuffer{ public static ByteBuffer llocateDirect(int capacty) public static ByteBuffer llocate(int capacty) public static ByteBuffer wrap(byte[] array) public static ByteBuffer wrap(byte[] array,int offset, int length) public abstract byte get( ) public abstract byte get (int index); public abstract ByteBuffer put (byte b): public abstract ByteBuffer put (int index, byte b); }
|
关于Selector
Selector类是一个抽象类,常用的方法如下:
1 2 3 4 5
| public abstract class Selector implements Closeable { public static Selector open(); public int select(long timeout); public Set<SelectionKey> selectedKeys(); }
|
NIO编程流程
- 当客户端需要连接时,通过
ServerSocketChannel
得到SocketChannel
对象。 - 将该
SocketChannel
对象注册到Selector
上,使用register(Selector sel)
方法,在一个selector
上可以注册多个SocketChannel
对象。(将监听服务通道和监听到的对象通道均注册到Selector) - 注册后返回一个
SelectionKey
,会和该Selector
关联(集合)。 Selector
进行监听select
方法,返回有事件发生的通道个数。- 进一步得到各个
SelectionKey
。 - 通过使用
SelectionKey
的channel()
方法反向获取SocketChannel
。 - 通过得到的
channel
完成业务处理。
NIO代码实践—简单的多用户聊天服务场景
服务端:NIOServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| package com.tosang.nio;
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set;
public class NIOServer { public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){ if(selector.select(1000) == 0){ System.out.println("服务器等待了1秒,无连接"); continue; } Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); if(key.isAcceptable()){ SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端连接成功,生成了一个通道 "+ socketChannel.hashCode()); socketChannel.configureBlocking(false); socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } if(key.isReadable()){ SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer)key.attachment(); channel.read(buffer); System.out.println("From 客户端:"+new String(buffer.array())); } keyIterator.remove(); } } } }
|
客户端:NIOClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package com.tosang.nio;
import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel;
public class NIOClient { public static void main(String[] args) throws Exception{ SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666); if(!socketChannel.connect(inetSocketAddress)){ while (!socketChannel.finishConnect()){ System.out.println("因为连接需要时间,客户端不会阻塞,可以继续工作");
} } String str = "hello ,小yi"; ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(buffer); System.in.read();
} }
|
启动NIOServer
和多个NIOClient
,可以看到打印的hashcode
值不相等,说明一个Selector
同时管理着多个连接。