目录
1. Single Thread Socket Server
2. Multiple Thread Socket Server
一.Linux IO模型
在《Unix网络编程》这本书中将IO模型划分为以下五种:
- 阻塞式IO模型Blocking IO
- 非阻塞式IO模型Non-Blocking IO/New IO
- IO复用
- 信号驱动式IO模型
- 异步IO模型Asynchronous IO
其中前四种都是同步模式。
二. 了解Linux IO流程
在Linux中运行的应用程序如果需要进行IO操作,需要涉及到两个空间的概念,用户空间和内核空间。
上图表示的是一个数据读取的过程:
- DMA先从将磁盘数据拷贝到内核空间,该过程也称为数据准备过程;
- 应用程序拷贝内核空间数据到用户空间。
- 写数据的过程与之相反。
从应用程序角度来看,分为两步:
- 等待内核将数据准备好(Waiting for the data to be ready)
- 从内核向进程复制数据(Copying the data from the kernel to the process)
三. 各IO模型执行过程
(一)阻塞式IO模型
应用程序发起一次系统调用,即IO请求,询问内核数据是否准备好,发现没有,则进程一直等待数据准备好为止;在接收到系统调用后内核开始准备数据报,准备好后进行内核态到用户态的数据拷贝,拷贝完成后,返回准备就绪信息给调用进程。
(二)非阻塞式IO模型
应用进程轮询询问内核数据是否准备好。当有数据报准备好时就进行数据报的拷贝操作,当没有准备好时,内核直接返回未准备就绪的信号,不让进程阻塞,并且开始准备数据,等待进程下一次询问。
(三)IO多路复用模型
IO多路复用模型会通过一个select函数对多个文件描述符(集合)进行循环监听,当某个文件描述符就绪时,就对这个文件描述符的数据进行处理。
(四)信号驱动式IO模型
应用程序通知内核,当数据准备就绪时给它发送一个SIGIO信号,应用程序会对这个信号进行捕捉,并且调用相应的信号处理函数执行数据拷贝的过程。
(五)异步IO模型
当应用程序调用aio_read时,内核一边开始准备数据,另一边将程序控制权返回给应用进程,让应用进程处理其他事情;当内核中有数据报准备就绪时,由内核将数据报拷贝到用户空间,这也是与其他四种模型的最大区别,拷贝完成后返回aio_read中定义好的函数处理程序。
(六)各IO模型对比
从下图中可以看出阻塞程度是:阻塞式IO >非阻塞式IO > IO复用>信号驱动式IO >异步IO。
四. BIO下的Socket处理方式
下图是Socket和ServerSocket通信模型:
1. Single Thread Socket Server
public void startServer() throws IOException {
final ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Listening for connection on port 8080...");
while (!Thread.interrupted()) {
final Socket socket = serverSocket.accept();
// 1. Read request from the socket of client.
// 2. Prepare a response.
// 3. Send response to the client.
// 4. Close the socket.
}
}
2. Multiple Thread Socket Server
public void startServer() throws IOException {
final ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Listening for connection on port 8080...");
while (!Thread.interrupted()) {
final Socket socket = serverSocket.accept();
new Thread(() -> {
// 1. Read request from the socket of client.
// 2. Prepare a response.
// 3. Send response to the client.
// 4. Close the socket.
}).start();
}
}
3. Thread Pool Socket Server
public void startServer() throws IOException {
final ServerSocket serverSocket = new ServerSocket(8080);
// 为了代码简洁,这里直接通过工具类创建一个线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
System.out.println("Listening for connection on port 8080...");
while (!Thread.interrupted()) {
final Socket socket = serverSocket.accept();
executor.execute(() -> {
// 1. Read request from the socket of client.
// 2. Prepare a response.
// 3. Send response to the client.
// 4. Close the socket.
});
}
}
五. Java NIO
Java IO和Java NIO的区别
Java NIO三个关键对象
- Buffer
- Channel
- Selector
(一)Java NIO Buffer
1. 一个Buffer本质上是内存的一个内存块,允许对这块内存进行数据读写操作,在Java NIO中定义了以下几种Buffer实现:
2. Java NIO的Buffer主要有三个核心属性:
(1)Capacity:
缓冲区容量,一旦设定就不可更改,比如capacity为1024的IntBuffer,代表其最大可以存放1024个int类型的数据。
(2)Position:
记录下一个可操作(可读/可写)地址。
(3)Limit:
Java Buffer是通过一个指针来维护读写操作的,从写操作模式切换到读操作模式,position都会归零,这些可以保证从头开始读写,读写模式切换必须使用flip方法。
在初始化后默认是写操作模式,此时limit代表的是最大能写入数据,即limit = capacity;
最大能写入的数据,初始状态下limit = capacity。
写操作结束后,flip切换到读模式下,此时limit等于Buffer中实际写入数据的大小,比如在写模式下写入了10个int类型的数据,那么此时limit=10。
(4)Mark:
标记当前position位置,在执行reset方法后将pisition恢复到标记位置,mark的位置必须小于等于position,它们之间的关系:0<=mark<=position<=limit<=capacity
3. ByteBuffer实现
Java NIO中ByteBuffer有两种具体实现Direct ByteBuffer和Heap ByteBuffer,其中HeapByteBuffer也被认为是Non-Direct ByteBuffer。
| DirectByteBuffer | HeapByteBuffer |
创建开销 | 大 | 小 |
存储位置 | Native heap | JVM heap 维护一个字节数组byte[] |
数据拷贝 | 无需临时缓冲区做拷贝 | 先拷贝到DirectByteBuffer类型的临时缓冲区,并且这个缓冲区具有缓存功能。 |
GC影响 | 每次创建或者释放时都调用一次System.gc() | 无 |
4. Buffer操作方法
方法名称 | 作用 |
allocate | 创建一个Heap ByteBuffer类型的缓冲区 |
allocateDirect | 创建一个Direct ByteBuffer类型的缓冲区 |
wrap | 通过外部传入一个数组创建Heap ByteBuffer类型的缓冲区 |
flip | 切换读写操作模式 |
put | 将单个字节写入缓冲区position位置 |
get | 从缓冲区中读取单个字节 |
mark | 标记当前position |
reset | 恢复position到mark标记的位置 |
rewind | 将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变。 |
clear | 清空缓存区,这里的清空并不是清除缓冲区数据,而是position被重置为0,limit被重置为capacity。 |
compact | Compact与clear不同,compact是将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读数据下一位。limit属性依然像clear()方法一样设置成capacity。 |
remaining | 计算并返回Buffer缓冲区中剩余数据大小 |
(二)Channel
所有的NIO操作都是基于通道Channel的,通道是数据来源和数据写入的目标,Java NIO中主要实现了以下几类Channel:
- FileChannel:文件通道,用于文件读写
- SocketChannel:可以将它理解为TCP协议方式的连接通道,也可以简单理解为TCP客户端
- ServerSocketChannel:TCP协议方式的服务端,用于监听具体端口的数据接受请求
- DatagramChannel:用于基于UDP协议连接的数据接收和发送
(三)Selector
Selector是Java NIO规范中十分重要的组件,它的作用是用来检查一个或多个NIO Channel通道的状态是否处于可读或可写,实现了单线程可以管理多个Channel的目标。
六. 代码实例
package com.zjhuang.socket.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 实现Java NIO处理socket请求
*
* @author
* @create 2018/11/20 19:21
**/
public class NioSocketServer implements Runnable {
/**
* 选择器
*/
private Selector selector;
/**
* Server端Socket通道
*/
private ServerSocketChannel channel;
public NioSocketServer(int port) throws IOException {
// 创建选择器
selector = Selector.open();
// 打开一个server端socket通道
channel = ServerSocketChannel.open();
// 设置为non-blocking模式
channel.configureBlocking(false);
// 通道绑定到指定端口
channel.socket().bind(new InetSocketAddress(port), 1024);
// 将通道注册到selector上,并且对Accept事件感兴趣
channel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("正在监听" + port + "端口請求...");
}
@Override
public void run() {
while (!Thread.interrupted()) {
try {
// 检查就绪的通道
if (selector.select(1000) == 0) {
continue;
}
// 返回就绪通道集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
handle(key);
// 移除已处理通道
iterator.remove();
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (null != selector) {
try {
// 关闭Selector,并使注册到该Selector上的所有SelectionKey实例无效
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理通道的读写事件
*
* @param key SelectionKey对象
* @throws IOException
*/
private void handle(SelectionKey key) throws IOException {
// 判断新接入的通道是否有效
if (key.isValid()) {
if (key.isAcceptable()) {
// 当通道就绪时注册一个新的socketChannel,并对读取事件感兴趣
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer);
if (readBytes == -1) {
socketChannel.close();
key.cancel();
} else {
// 切换到读模式
readBuffer.flip();
// 创建一个readBuffer钟剩余数据大小的字节数组
byte[] bytes = new byte[readBuffer.remaining()];
// 从readBuffer中读取指定大小的数据到字节数组中
readBuffer.get(bytes);
// 打印接收到的数据
System.out.println("服务端接收到数据:" + new String(bytes, "UTF-8"));
// 回显数据
socketChannel.write(ByteBuffer.wrap("success\r\n".getBytes()));
}
}
}
}
public static void main(String[] args) throws Exception {
new Thread(new NioSocketServer(8080)).start();
}
}