参考书籍:Netty、Redis、ZooKeeper高并发实战
1. IO读写的基本原理
用户程序进行IO的读写,实际上是缓存区的复制。即read :从内核缓冲区复制到进程缓冲区;write:从进行缓冲区复制到内核缓冲区;上层程序的IO操作,实际上不是物理设备级别的读写,而是缓存的复制。内核缓冲区和物理设备(磁盘、网卡)之间的交换是由操作系统内核完成的。
2. 4种主要的IO模型
- 同步阻塞IO
进程进行读写,需要内核IO操作彻底完成后,才返回到进程空间执行用户的操作。伪代码:String res = read(); 开启read操作等到返回才能执行下一句。当读socket时,没有数据的话就一直等,等到有数据,内核也准备好了,再复制到进程空间返回。
- 同步非阻塞IO
不停轮询去读取,知道内容准备完成。
- IO 多路复用模型
使用一个选择器线程监控多个socket连接,当某个或某些socket网络连接有IO就绪状态,就返回进行相应的操作。一个线程处理成千上万个连接,相比于一个线程维护一个连接的阻塞IO模式相比,大大减少了系统的开销。
- 异步IO模型(AIO)
(1)当用户线程发起了read系统调用,立刻就可以开始去做其他的事,用户线程不阻塞。
(2)内核就开始了IO的第一个阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存)。
(3)内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。
(4)用户线程读取用户缓冲区的数据,完成后续的业务操作。
目前大多数的高并发网络服务使用的仍是IO多路复用模型。
3. Linux文件句柄数
Linux系统默认文件句柄数(即文件描述符,反应IO状态)的限制是1024个,也就是说一个进程最多可以有1024个Socket连接,这远远不够。
临时修改: ulimit -n 1000000
开机执行: etc/rc.local文件, ulimit -SHn 1000000
永久:etc/security/limits.conf soft nofile 1000000 hard nofile 1000000
,-S soft表示软性极限,系统警告的极限值,-H hard表示硬性极限,实际的限制。
4. Java NIO
- 核心组件: Buffer(缓冲区)、Channel(通道)、Selector(选择器)
- Buffer:本质上是一个内存块(数组),Buffer是个抽象类,实际有 ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、 MappedBuffer
属性:
capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时设置并且不能改变 |
limit | 读写的最大上限 |
position | 位置,缓冲区中下一个要被读或写的元素的位置 |
mark | 用来缓存position |
方法
allocate() | 创建缓冲区 |
put() | 写入缓冲区 |
flip() | 切换到读模式 |
get() | 冲缓冲区中读取 |
rewind() | 倒带,位置调整到0,即从头开始 |
mark() | position |
reset() | position= mark |
- Channel(通道)
四种类型
FileChannel | 文件通道 |
|
SocketChannel | 数据传输通道 |
|
ServerSocketChannel | 监听通道 |
|
DatagramChannel | 数据报通道(UDP) |
UDP不是面向连接的协议。
|
- Selector选择器类
可供选择器监控的通道IO事件:
可读: SelectionKey.OP_READ
可写: SelectionKey.OP_WRITE
连接: SelectionKey.OP_CONNECT 完成握手连接,处于“连接就绪状态”
接收: SelectionKey.OP_APPEPT 服务器通道接收到新的连接请求时,处于“接收就绪状态”
(1)通道和选择器之间通过,channel.register(Selector sel, int ops)方法进行注册。 ops为通道IO事件,多个用位或符号 '|' 连接.
(2)并不是所有的通道都可以被监控,FileChannel就不可以。只有继承了SelectableChannel的通道才可以被选择器监控,Java NIO所有的Socket套接字通道都继承了SelectableChannel。
(3)SelectionKey类。一个IO事件发生后,如果之前在选择器中注册过,就会被选择器选中,并放入SelectionKey的集合中。SelectionKey中包含发生IO事件的通道、IO事件类型、选择器对象等很多属性
使用方式:
// 1、获取Selector选择器
Selector selector = Selector.open();
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT));
Logger.info("服务器启动成功");
// 5、将通道注册到选择器上,并注册的IO事件为:“接收新连接”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6、轮询感兴趣的I/O就绪事件(选择键集合)
while (selector.select() > 0) {
// 7、获取选择键集合
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
// 8、获取单个的选择键,并处理
SelectionKey selectedKey = selectedKeys.next();
// 9、判断key是具体的什么事件
if (selectedKey.isAcceptable()) {
// 10、若选择键的IO事件是“连接就绪”事件,就获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 11、切换为非阻塞模式
socketChannel.configureBlocking(false);
// 12、将该通道注册到selector选择器上
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectedKey.isReadable()) {
// 13、若选择键的IO事件是“可读”事件,读取数据
SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
// 14、读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int length = 0;
while ((length = socketChannel.read(byteBuffer)) >0) {
byteBuffer.flip();
Logger.info(new String(byteBuffer.array(), 0, length));
byteBuffer.clear();
}
socketChannel.close();
}
// 15、移除选择键
selectedKeys.remove();
}
}
// 7、关闭连接
serverSocketChannel.close();
标签:Java,NIO,通信,socketChannel,IO,缓冲区,SelectionKey,选择器,通道 From: https://www.cnblogs.com/lostO/p/16758236.html