要知道,在Linux中,一切都是文件,对socket的操作也可以看做对文件的操作,文件操作有一个文件描述符(File descriptor,简称为fd)。对文件的读写操作会调用内核提供的命令,返回fd给调用方,通过它来操作。
同步堵塞IO模型
最常见的,进程调用recvFrom,当前线程会一直堵塞,一直等到数据复制到进程的缓冲区或者发生异常的时候才返回(有用户态和系统态,一般的数据流向是,数据一开始是放在磁盘的,当进程调用读取命令的时候,内核会开始读取数据,将磁盘上的数据读取到内核的缓存区,此时还是系统态,接着会将数据从内核的缓冲区里面读取到用户态的缓存区里面)
非阻塞IO模型
这种模式采用的是轮训的模式。调用revcFrom的时候,没有数据直接返回错误。应用程序就可以一直轮训,看是否有数据。
IO复用模型
提供了select/poll/epoll操作。
简单来说就是多个socket堵塞到同一上面,有数据就会将对应的socket返回来操作。
linux提供了上面的几个操作,支持进程将多个socket(其实就是多个fd)传递给上面的几个操作,堵塞在上面,这样它们就可以来侦测这些socket(多个fd)上面哪些是活动的。如果有活动的就直接返回。进程就可以调用receFrom来读取数据。将数据从系统态读取到用户态。
信号驱动IO模型
简单来说,先给内核注册,之后有数据了,内核通过发信号给进程,进程自己来复制数据。
首先,得先开启信号驱动I/O功能,并且通过系统调用sigacation执行一个信号处理函数(调用之后立即返回,进程继续工作,他是非堵塞的)。放数据准备有了之后,就会为改进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据。
注意,这里的读取(将数据从系统态读取到用户态的操作)还是应用程序来主动做的
异步IO模型
告诉内核启动某个操作,这个时候应用程序不会堵塞,当数据准备好之后(注意,这里是内核将数据从系统态拷贝到了用户态)才会通知应用程序。
上面异步和同步最大的区别在于,数据的准备过程,同步是应用程序自己将数据拷贝,异步是内核将数据拷贝到用户进程的缓存区里面。
他们都是内核在I/O复用模型中的产物。epoll是在select之后出来的。说明select有问题。
select打开的socket(fd)是有限制的。
这是他的最大的缺陷,通过FD_SETSIZE来设置,默认是1024,但是修改它需要重新编译内核。对于支持上万连接的服务器明显不够
epoll解决了这个问题,它没有这个限制,他所支持的是操作系统的最大的文件句柄数。
select随着socket变多,性能会下降
它每次都会线性扫描全部的socket的集合。随着socket的数量变多,性能直线下降。一般来说,任意时刻,只有小部分的socket是活跃的。但是epoll不会有这个问题,它只会针对活跃的socket来操作(它是因为在在每个fd上面都有回调函数,只有活跃的才会去调用这些函数,其他的不会)
select的api复杂
epoll会利用mmap来读取数据,加快速度。
Java的BIO就是一开始我们接触的ServerSocket,这种模式简单,但是他的性能在连接数多的情况下,不容乐观,如果连接小,采用BIO编码简单。
每个socket都会有一个线程对应。在数据没有准备好的时候,线程也只能等待,不能干别的事情。当连接数多的时候,一般会将Socket创建一个新的线程。或者利用线程池(起码可以控制线程的数量,不至于线程直接开满)。处理不了的就走线程池的触发策略(一般是拒绝连接)。
IO的Read方法会一直一直堵塞,一直到有数据可读、发送异常、可用数据读取完毕,这三种其中之一的情况下才会返回。write方法写的时候,也会一直堵塞,直到所有要发送的字节全部写入完毕,或者发生异常。
public class IoMain {
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(9000));
while (true) {
Socket accept = serverSocket.accept();
EXECUTOR.execute(new HandleSocketRunnable(accept));
}
}
static class HandleSocketRunnable implements Runnable{
Socket socket;
public HandleSocketRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try (
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
) {
System.out.println(bufferedReader.readLine());
out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Java的NIO称为(New IO,或者NOBlockIO),NIO的出现使得Java也支持了非堵塞IO。
有三个部件
数据的交换(读写)都是通过Buffer来做的,每个Channel都关联着一个Buffer。Buffer实际上是一个数组,在此基础上增加了一些标志位来实现别的功能。具体的可以看java.nio.Buffer
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HUWMFM7l-1649681832662)(/Users/liuchen/Library/Application Support/typora-user-images/image-20220411002216784.png)]
多路复用器是Java Nio的基础,可以将多个Channel注册到它,它监听这些Channel,如果有哪些Channel活动了,就可以通过SelectKey来获取到。这样一个Select就可以监听多个Channel。JDK使用了epoll()代替了select实现,所以,没有最大句柄的限制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-StBob09T-1649681832663)(/Users/liuchen/Library/Application Support/typora-user-images/image-20220411203929237.png)]
在NIO编程的时候,需要将Channel注册到Selector上,在Selector上监听不同类型的事件。其中注意到在Accept类型的事件的时候,会拿到ServerSocketChannel,并且配置上去,重新注册到Selector中去,关注的事件类型是OP_READ。(这个操作是因为,每一个Channel都要注册到Selector中去)
public class IoMain {
public static void main(String[] args) throws IOException {
final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 打开serverSocket
serverSocketChannel.bind(new InetSocketAddress(9000));// 绑定端口号
serverSocketChannel.configureBlocking(false); // 配置为非堵塞
final Selector selector = Selector.open(); // 开始select
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// 注册到Select中,关注的事件为Accept
while (true) {
selector.select(1 * 1000);//
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 得从集合中移除。
try {
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
int readBytes = channel.read(allocate);
if (readBytes > 0) {
allocate.flip();
byte[] bytes = new byte[allocate.remaining()];
allocate.get(bytes);
String s = new String(bytes, "utf-8");
System.out.println(s);
// 写数据
doWriter(channel, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
} else if (readBytes < 0) {
key.cancel();
channel.close();
}
}
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}
private static void doWriter(SocketChannel socketChannel, String msg) throws IOException {
ByteBuffer allocate = ByteBuffer.allocate(1024);
allocate.put(msg.getBytes(StandardCharsets.UTF_8));
allocate.flip();
socketChannel.write(allocate);
}
}
在AIO中,是不需要Selector来监听多个事件的,回想上面说过的Linux的异步IO模型,这里的原理和上面说的是一致的。
有下面的两种方式来获取结果:
还需要传入一个回调函数。
具体的可以看Java AIO使用详解
文中内容参考自《Netty 权威指南第2版》
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。