1、I/O模型
Linux系统中所有的外部设备都是一个可操作性的文件,一个socket的读写对应相应的文件描述符socketfd。I/O模型在UNIX系统中分为5种:阻塞I/O模型、非阻塞I/O模型、I/O复用模型、型号驱动I/O模型、异步I/O。
由于操作系统中权限不同,并且系统的资源是有限的,如果资源操作过于频繁必然会消耗过多的资源,甚至会造成资源访问的冲突,所以,从宏观上来看分为内核态与用户态。
内核态:控制计算机的硬件资源,CPU资源、存储、I/O等;
用户态:只负责管理自己的应用进程,如果要调用硬件那么需要通过系统的库函数调用;
Unix提供的5中I/O模型
1.1、阻塞IO(所有文件缺省都是阻塞I/O)
所有文件操作都是阻塞的,用户态线程调用recvfrom,recvfrom回去执行内核态调用知道数据包被复制到进程缓冲区才返回,调用recvfrom的整个过程都是阻塞。
1.2、非阻塞IO
用户态线程调用recvfrom时会不断轮询询问是否有数据到来,如果没有则返回EWOULDBLOCK错误,当有数据时,用户态线程也会阻塞知道数据拷贝到进程缓冲区完成再返回。
1.3、I/O复用模型
Linux提供了select/poll,epoll系统调用,select/poll是进程将多个socketfd文件交给select/poll系统调用,select/poll侦测fd是否处于就绪状态,是顺序扫描fd文件;epoll是基于事件驱动方式,相比于顺序扫描性能更高,当有fd就绪时,立刻rollback。
Java NIO的多路复用器就是基于epoll的多路复用技术实现的。
着重说一下I/O多路复用
对比select/poll与epoll
1.3.1、socketFD
select多路复用:由于是顺序循环监听fd,所以单进程打开的fd是有限制的,默认是1024,操作系统在某一时刻只会有少数的socket是活跃的,但是select/poll每次却要现行扫描全部的fd集合,这样效率会线性下降;
epoll多路复用:socketFD不受限制(受限于操作系统的最大文件数,是远远大于1024的),epoll只会对活跃的socket进行操作,但是假如所有socket都处于活跃状态,那么epoll和select的效率差不多;
epoll具体实现?
每个fd上都有callback函数实现,而只有活跃的socket才会去调用callback,其他idle的socket不会,所以epoll实现了一个伪AIO。
1.3.2、mmap内存加速
内核在把FD消息通知给用户态时,为了避免不必要的内存复制,epoll是通过内核与用户态mmap同一块内存实现的,mmap是将文件与进程的虚拟空间进行映射。
1.4、信号驱动I/O模型
进程调用sigaction(一个信号处理函数),用户态线程会立即返回不会阻塞,当数据准备就绪时,为该进程生成一个SIGIO信号,通过这个信号通知进程来调用recvfrom读取数据。
1.5、异步I/O
进程告知内核一个io_read后立即返回,内核态准备完数据后并且将数据赋值到进程缓冲区,通知进程,整个过程用户态不阻塞。
2、BIO
网络编程基本都是client/server模型,client通过连接操作与服务端监听端口建立连接,三次握手建立连接,连接成功后双方通过socket通信。
Acceptor线程负责监听客户端连接,接收到连接之后为每个client创建一个新线程,线程处理完后返回输出流给客户端,销毁线程。
BIO的的client的1个connection对应server的1个线程,众所周知线程的开销是昂贵的,在并发量高的操作系统中,BIO的缺陷也突出的很明显,资源消耗非常高。
代码模拟:client向server请求当前时间,server创建一个线程处理请求输出当前时间,main线程充当Acceptor来接收connection,MyBioHandler线程用来真正的处理数据。
package org.Netty;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
// 模拟BIO服务器端
public class MyBioServer {
public static void main(String[] args) {
Socket socket = null;
try (ServerSocket serverSocket = new ServerSocket(8080);) {
while (true) {
socket = serverSocket.accept();
new Thread(new MyBioHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
package org.Netty;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
// 模拟BIO客户端
public class MyBioClient {
public static void main(String[] args) {
try(
Socket socket = new Socket("127.0.0.1", 8080);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true)
) {
// 发送 "Query time" 请求
out.println("Query time");
String result = in.readLine();
System.out.println("server return : " + result);
}catch (IOException e) {
e.printStackTrace();
}
}
}
package org.Netty;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
public class MyBioHandler implements Runnable{
private Socket socket;
public MyBioHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
PrintWriter out = new PrintWriter(this.socket.getOutputStream(),true);){
while(true) {
String read = in.readLine();
if (read.equals("Query time")) {
// 返回server时间
out.println(new Date(System.currentTimeMillis()).toString());
} else {
out.println("none");
}
}
}catch (IOException e) {
e.printStackTrace();
}finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
BIO模型在少量客户端连接场景下是比较容易实现的,代码实现起来简单容易理解,一旦请求数增加则会消耗大量cpu、内存的系统资源,无法满足高性能需求。
3、伪异步NIO
将socket封装为task任务,交给server的线程池去执行,而不是一个socket对应新建的一个线程
代码模拟:client向server请求当前时间,server创建将socket封装为task交给线程池去处理
package org.Netty;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
// 伪装异步IO,将接收到的connection调用连接池,而不是去new Thread
public class MyPseudoIOServer {
public static MyPseudoIOTheadPool executor = new MyPseudoIOTheadPool(10,100);
public static void main(String[] args) {
Socket socket = null;
try(ServerSocket server = new ServerSocket(8080)) {
while(true) {
socket = server.accept();
executor.execute(new TimeServerHandler(socket));
}
}catch (IOException e) {
e.printStackTrace();
}finally {
if (null != socket) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
package org.Netty;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
// 伪装异步IO,线程池
public class MyPseudoIOTheadPool {
private ExecutorService executor;
public MyPseudoIOTheadPool(int maxSize,int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxSize,120L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueSize));
}
public void execute(Runnable task) {
executor.execute(task);
}
}
伪异步IO弊端:
InputStream:
输入流的read方法会一直阻塞,直到输入数据可用、检测到文件结尾或引发异常为止,当server端的socket在接收数据时,假如发送方需要60s才能将数据发送完毕,那么读取一方的I/O线程也将会被阻塞60s
outputStream:
输出流的writer方法会一直阻塞,知道所有要发送的字节全部写入完毕,或者发生异常,本质与输入流的read一样
不管是输入流还是输出流,假如生产环境网络性能并不是很好,那么这里阻塞时间可能会很长,所以说伪异步IO只是优化了BIO的线程模型,本质上I/O阻塞问题并没有解决。
参考
《Netty权威指南》