IO概念
阻塞(Block)和非阻塞(Non-Block)
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。
- 阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。
- 非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。
同步(Synchronization)和异步(Asynchronous)
同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。
比如
同步:是应用程序要直接参与 IO 读写的操作。
异步:所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。
同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO 事件的方式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完 成真正的 IO 操作,当操作完成 IO 后,会给我们的应用程序一个通知。
同步 : 阻塞到 IO 事件,阻塞到 read 或则 write。这个时候我们就完全不能做自己的事情。让读写方法加入到线程里面,然后阻塞线程来实现,对线程的性能开销比较大。
BIO 与 NIO 对比
IO模型 | BIO | NIO |
---|---|---|
通信 | 面向流 | 面向缓冲 |
处理 | 阻塞IO(多线程) | 非阻塞(反应对Reactor) |
触发 | 无 | 选择器(轮询机制) |
面向流与面向缓冲
Java NIO 和 BIO 之间第一个最大的区别是,BIO 是面向流的,NIO 是面向缓冲区的。 Java BIO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。 如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是 否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞
Java BIO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被 读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO 的非阻塞模式,使一个线程从某通道发送请 求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞, 所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到 某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它 通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
选择器
Java NIO 的选择器(Selector)允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制, 使得一个单独的线程很容易来管理多个通道
Java NIO 三件套
缓冲区 Buffer
Buffer 操作基本 API
缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。在读 取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都 是将它放到缓冲区中。而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。
在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer,对于 Java 中的基本类型,基本都有 一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示:
package com.paic.io.nio.buffer;
import java.nio.IntBuffer;
public class IntBufferDemo {
public static void main(String[] args) {
// 分配新的int缓冲区,参数为缓冲区容量
// 新缓冲区的当前位置将为零,其界限(限制位置)将为其容量。它将具有一个底层实现数组,其数组偏移量将为零。
IntBuffer buffer = IntBuffer.allocate(8);
for (int i = 0; i < buffer.capacity(); ++i) {
int j = 2 * (i + 1);
// 将给定整数写入此缓冲区的当前位置,当前位置递增
buffer.put(j);
}
// 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
buffer.flip();
// 查看在当前位置和限制位置之间是否有元素
while (buffer.hasRemaining()) {
// 读取此缓冲区当前位置的整数,然后当前位置递增
int j = buffer.get();
System.out.print(j + " ");
}
}
}
Buffer 的基本的原理
在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制, 能够跟踪和记录缓冲区的状态变化情况,如果我们使用 get()方法从缓冲区获取数据或者使用 put()方法把数据写入缓冲 区,都会引起缓冲区状态的变化。
在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:
- position:指定下一个将要被写入或者读取的元素索引,它的值由 get()/put()方法自动更新,在新创建一个 Buffer 对象 时,position 被初始化为 0。
- limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
- capacity:指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我 们使用的底层数组的容量。
以上三个属性值之间有一些相对大小的关系:0 <= position <= limit <= capacity。如果我们创建一个新的容量大小为 10 的 ByteBuffer 对象,在初始化的时候,position 设置为 0,limit 和 capacity 被设置为 10,在以后使用 ByteBuffer 对象过程中,capacity 的值不会再发生变化,而其它两个个将会随着使用而变化。
D://ws//test.txt
this is test.txt
package com.paic.io.nio.buffer;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class BufferDemo {
//put/get
public static void main(String args[]) throws Exception {
//这用用的是文件IO处理
FileInputStream fin = new FileInputStream("D://ws//test.txt");
//创建文件的操作管道
FileChannel fc = fin.getChannel();
//分配一个10个大小缓冲区,说白了就是分配一个10个大小的byte数组
ByteBuffer buffer = ByteBuffer.allocate(10);
output("初始化", buffer);
//先读一下
fc.read(buffer);
output("调用read()", buffer);
//准备操作之前,先锁定操作范围
buffer.flip();
output("调用flip()", buffer);
//判断有没有可读数据
while (buffer.remaining() > 0) {
byte b = buffer.get();
System.out.print(((char)b));
}
System.out.println();
output("调用get()", buffer);
//可以理解为解锁
buffer.clear();
output("调用clear()", buffer);
//最后把管道关闭
fin.close();
}
//把这个缓冲里面实时状态给答应出来
public static void output(String step, ByteBuffer buffer) {
System.out.println(step + " : ");
//容量,数组大小
System.out.print("capacity: " + buffer.capacity() + ", ");
//当前操作数据所在的位置,也可以叫做游标
System.out.print("position: " + buffer.position() + ", ");
//锁定值,flip,数据操作范围索引只能在position - limit 之间
System.out.println("limit: " + buffer.limit());
System.out.println();
}
}
从缓冲区中读取数据,在此之前,必须调用 flip()方法,该方法将会完 成两件事情:
- 把 limit 设置为当前的 position 值
- 把 position 设置为 0
缓冲区的分配
在创建一个缓冲区对象时,会调用静态方法 allocate()来指定缓冲区的容量,其实调用 allocate()相当于创建了一个指定大小的数组,并把它包装为缓冲区对象。或者我们也可以直接将一个现有的数组,包装为缓冲区对 象
/**
* 手动分配缓冲区
*/
public class BufferWrap {
public void myMethod() {
// 分配指定大小的缓冲区
ByteBuffer buffer1 = ByteBuffer.allocate(10);
// 包装一个现有的数组
byte array[] = new byte[10];
ByteBuffer buffer2 = ByteBuffer.wrap( array );
}
}
缓冲区分片
在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切 出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是 现有缓冲区的一个视图窗口。调用 slice()方法可以创建一个子缓冲区
import java.nio.ByteBuffer;
/**
* 缓冲区分片
*/
public class BufferSlice {
static public void main( String args[] ) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate( 10 );
// 缓冲区中的数据0-9
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
// 创建子缓冲区
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
// 改变子缓冲区的内容
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 10;
slice.put( i, b );
}
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}
}
}
只读缓冲区
只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的 asReadOnlyBuffer()方法,将任何常规缓 冲区转 换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原 缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化:
import java.nio.ByteBuffer;
/**
* 只读缓冲区
*/
public class ReadOnlyBuffer {
static public void main( String args[] ) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate( 10 );
// 缓冲区中的数据0-9
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
// 创建只读缓冲区
ByteBuffer readonly = buffer.asReadOnlyBuffer();
// 改变原缓冲区的内容
for (int i=0; i<buffer.capacity(); ++i) {
byte b = buffer.get( i );
b *= 10;
buffer.put( i, b );
}
readonly.position(0);
readonly.limit(buffer.capacity());
// 只读缓冲区的内容也随之改变
while (readonly.remaining()>0) {
System.out.println( readonly.get());
}
}
}
直接缓冲区
直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区
JDK 文档中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中或者从一个中间缓冲区中拷贝数据。
要分配直接缓冲区,需要调用 allocateDirect() 方法,而不是 allocate()方法,使用方式与普通缓冲区并无区别
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 直接缓冲区
* Zero Copy 减少了一个拷贝的过程
*/
public class DirectBuffer {
static public void main( String args[] ) throws Exception {
//在Java里面存的只是缓冲区的引用地址
//管理效率
//首先我们从磁盘上读取刚才我们写出的文件内容
String infile = "D://ws/test.txt";
FileInputStream fin = new FileInputStream( infile );
FileChannel fcin = fin.getChannel();
//把刚刚读取的内容写入到一个新的文件中
String outfile = String.format("D://ws/testcopy.txt");
FileOutputStream fout = new FileOutputStream(outfile);
FileChannel fcout = fout.getChannel();
// 使用allocateDirect,而不是allocate
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
buffer.clear();
int r = fcin.read(buffer);
if (r==-1) {
break;
}
buffer.flip();
fcout.write(buffer);
}
}
}
内存映射
内存映射是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快的多。内存映射文件 I/O 是通过使文件中的 数据出现为 内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说, 只有文件中实际读取或者写入的部分才会映射到内存中。
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* IO映射缓冲区
*/
public class MappedBuffer {
static private final int start = 0;
static private final int size = 26;
static public void main( String args[] ) throws Exception {
RandomAccessFile raf = new RandomAccessFile( "D://ws/test.txt", "rw" );
FileChannel fc = raf.getChannel();
//把缓冲区跟文件系统进行一个映射关联
//只要操作缓冲区里面的内容,文件内容也会跟着改变
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,start, size );
mbb.put( 0, (byte)97 ); //a
mbb.put( 25, (byte)122 ); //z
raf.close();
}
}
选择器 Selector
传统的 Server/Client 模式会基于 TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理 一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题, 都采用了线程池模型,并设置线程池线程的最大数量,这又带来了新的问题,如果线程池中有 200 个线程,而有 200 个用户都在 进行大文件下载,会导致第201个用户的请求无法及时处理,即便第201个用户只想请求一个几KB大小的页面。传统的 Server/Client 模式如下图所示:
NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到 达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是 注册各种 I/O 事件地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:
从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可 以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。
使用 NIO 中非阻塞 I/O 编写服务器处理程序,大体上可以分为下面三个步骤:
- 1、 向 Selector 对象注册感兴趣的事件。
- 2、从 Selector 中获取感兴趣的事件。
- 3、根据不同的事件进行相应的处理。
在 Java1.4 之前的 I/O 系统中,提供的都是面向流的 I/O 系统,系统一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,面向流的 I/O 速度非常慢,而在 Java 1.4 中推出了 NIO,这是一个面向块的 I/O 系统,系统以块的方式处理处理,每一个操作在 一步中产生或者消费一个数据库,按块处理要比按字节处理数据快的多。
通道 Channel
通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过 Buffer 对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
在 NIO 中,提供了多种通道对象,而所有的通道对象都实现了 Channel 接口。它们之间的继承关系如下图所示:
使用 NIO 读取数据
任何时候读取数据,都不是直接从通道读取,而是从通道读取到缓冲区。所以使用 NIO 读取数据可
以分为下面三个步骤:
- 从 FileInputStream 获取 Channel
- 创建 Buffer
- 将数据从 Channel 读取到 Buffer 中 下面是一个简单的使用 NIO 从文件中读取数据的例子:
public class FileInputDemo {
static public void main( String args[] ) throws Exception {
FileInputStream fin = new FileInputStream("D://ws//test.txt");
// 获取通道
FileChannel fc = fin.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到缓冲区
fc.read(buffer);
buffer.flip();
while (buffer.remaining() > 0) {
byte b = buffer.get();
System.out.print(((char)b));
}
fin.close();
}
}
使用 NIO 写入数据
使用 NIO 写入数据与读取数据的过程类似,同样数据不是直接写入通道,而是写入缓冲区:
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileOutputDemo {
static private final byte message[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 };
static public void main( String args[] ) throws Exception {
FileOutputStream fout = new FileOutputStream( "D://ws//test2.txt" );
// 1. 从 FileOutputStream 获取 Channel。
FileChannel fc = fout.getChannel();
// 2. 创建 Buffer。
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
// 3. 将数据从 Channel 写入到 Buffer 中。
fc.write( buffer );
fout.close();
}
}
IO 多路复用
目前流行的多路复用 IO 实现主要包括四种:select、poll、epoll、kqueue。下表是他们的一些重要特性的比较
IO模型 | 相对性能 | 关键思路 | 操作系统 | JAVA支持 |
---|---|---|---|---|
select | 较高 | Reactor | windows/Linux | 支持,Reactor 模式(反应器设计模式)。Linux 操作 系统的 kernels 2.4 内核版本之前,默认使用 select;而目前 windows 下对同步 IO 的支持,都 是 select 模型。 |
poll | 较高 | Reactor | Linux | Linux 下的 JAVA NIO 框架,Linux kernels 2.6 内 核版本之前使用 poll 进行支持。也是使用的 Reactor 模式。 |
epoll | 高 | Reactor/Proactor | Linux | Linux kernels 2.6 内核版本及以后使用 epoll 进行 支持; Linux kernels 2.6 内核版本之前使用 poll 进行支持; 另外一定注意,由于 Linux 下没有 Windows 下的 IOCP 技术提供真正的 异步 IO 支 持,所以 Linux 下使用 epoll 模拟异步 IO。 |
kqueue | 高 | Proactor | Linux | 目前 JAVA 的版本不支持。 |
多路复用 IO 技术最适用的是“高并发”场景,所谓高并发是指 1 毫秒内至少同时有上千个连接请求准备好。其他情 况下多路复用 IO 技术发挥不出来它的优势。另一方面,使用 JAVA NIO 进行功能实现,相对于传统的 Socket 套接字 实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择
阻塞IO
就是当客户端的数据从网卡缓冲区复制到内核缓冲区之前,服务端会一直阻塞。以socket接口为例,进程空间中调用 recvfrom,进程从调用 recvfrom 开始到它返回的整段时间内都是被阻塞的,
因此被成为阻塞 IO 模型
非阻塞 IO
非阻塞 IO 模型的原理很简单,就是进程空间调用 recvfrom,如果这个时候内核缓冲区没有数据的话,就直接返回一EWOULDBLOCK 错误,然后应用程序通过不断轮询来检查这个状态状态,看内核是不是有数据过来。
I/O 复用模型
I/O 多路复用的本质是通过一种机制(系统内核缓冲 I/O 数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
小知识点:在 linux 中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个 fd(文件描述符)。而对于一个 socket 的读写也会有相应的文件描述符,成为 socketfd
select模型
select:进程可以通过把一个或者多个 fd 传递给 select 系统调用,进程会阻塞在 select 操作上,这样 select 可以帮我们检测多个 fd 是否处于就绪状态。
这个模式有二个缺点
- 由于他能够同时监听多个文件描述符,假如说有 1000 个,这个时候如果其中一个 fd 处于就绪状态了,那么当前进程需要线性轮询所有的 fd,也就是监听的 fd 越多,性能开销越大。
- 同时,select 在单个进程中能打开的 fd 是有限制的,默认是 1024,对于那些需要支持单机上万的 TCP 连接来说确实有点少
epoll模型
epoll:linux 还提供了 epoll 的系统调用,epoll 是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的 fd 中,有 fd 就绪时,会告知当前进程具体哪一个 fd 就绪,那么当前进程只需要去从指定的 fd 上读取数据即可另外,epoll 所能支持的 fd 上线是操作系统的最大文件句柄,这个数字要远远大于1024
由于 epoll 能够通过事件告知应用进程哪个 fd 是可读的,所以我们也称这种 IO 为异步非阻塞 IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行
反应堆 Reactor
现在我们已经对阻塞 I/O 已有了一定了解,我们知道阻塞 I/O 在调用 InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在调用 ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会 返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。阻塞 I/O 的通信模型示意图如下:
根据阻塞 I/O 通信模型,我总结了它的两点缺点:
- 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些 CPU 时间
- 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。在这种情况下非阻塞式 I/O 就有了它的应 用前景。
Java NIO 是在 jdk1.4 开始使用的,它既可以说成“新 I/O”,也可以说成非阻塞式 I/O。下面是 Java NIO 的工作原理:
- 由一个专门的线程来处理所有的 IO 事件,并负责分发。
- 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
- 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
编程实践
属性 | 同步阻塞 IO(BIO) | 伪异步 IO 非阻塞 IO(NIO) | 异步 IO(AIO) |
---|---|---|---|
客户端数:IO 线程数 | 1:1 | M:N(M>=N) | M:1 |
阻塞类型 | 阻塞 | 阻塞 | 非阻塞 |
同步 | 同步 | 同步 | 同步(多路复用) |
API 使用难度 | 简单 | 简单 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 |
BIO
BIOServer
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
// 阻塞等待客户端连接
Socket socket = server.accept();
String line;
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter os = new PrintWriter(socket.getOutputStream());
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Client: " + is.readLine());
line = sin.readLine();
while (!line.equals("bye")) {
os.println(line);
os.flush();
System.out.println("Server:" + line);
System.out.println("Client:" + is.readLine());
line = sin.readLine();
}
os.close();
is.close();
socket.close();
server.close();
}
BIOClient
public static void main(String[] args) throws IOException {
Socket client = new Socket("localhost", 8080);
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
PrintWriter os = new PrintWriter(client.getOutputStream());
BufferedReader is = new BufferedReader(new InputStreamReader(client.getInputStream()));
String readLine;
// 从系统标准输入读取一个字符串
readLine = sin.readLine();
while (!readLine.equals("bye")) {
os.println(readLine);
os.flush();
System.out.println("Client:" + readLine);
System.out.println("Server:" + is.readLine());
readLine = sin.readLine();
}
os.close();
is.close();
client.close();
}
NIO
/**
* 编解码工具类
*/
public class CodecUtil {
public static ByteBuffer read(SocketChannel channel) {
// 注意,不考虑拆包的处理
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int count = channel.read(buffer);
if (count == -1) {
return null;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return buffer;
}
public static void write(SocketChannel channel, String content) {
// 写入 Butter
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
buffer.put(content.getBytes("UTF-8"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 写入 Channel
buffer.flip();
try {
// 注意,没有考虑写入超过 Channel缓冲区上限
channel.write(buffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static String newString(ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
System.arraycopy(buffer.array(), buffer.position(), bytes, 0, buffer.remaining());
try {
return new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
NIOServer
public class NIOServer {
/**
* 轮询器
*/
private Selector selector;
@SuppressWarnings("Duplicates")
public void handleKeys() throws IOException {
while (true) {
// 通过 Selector 选择 Channel
int selectNums = selector.select(30 * 1000L);
if (selectNums == 0) {
continue;
}
System.out.println("选择 Channel 数量: " + selectNums);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
// 不断迭代,轮询
// 同步体现在这里,因为每次只能哪一个key,每次只能处理一种状态
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 移除下面要处理的 SelectionKey
iterator.remove();
// 忽略无效的SelectionKey
if (!key.isValid()) {
continue;
}
handleKey(key);
}
}
}
private void handleKey(SelectionKey key) throws IOException {
// 接受连接就绪
if (key.isAcceptable()) {
handleAcceptableKey(key);
}
// 读就绪
if (key.isReadable()) {
handleReadableKey(key);
}
// 写就绪
if (key.isWritable()) {
handleWritableKey(key);
}
}
private void handleAcceptableKey(SelectionKey key) throws IOException {
// 接受 Client Socket Channel
SocketChannel clientSocketChannel = ((ServerSocketChannel) key.channel()).accept();
// 配置为非阻塞
clientSocketChannel.configureBlocking(false);
// log
System.out.println("接受新的 Channel");
// 注册 Client Socket Channel 到 Selector
clientSocketChannel.register(selector, SelectionKey.OP_READ, new ArrayList<String>());
}
@SuppressWarnings({"unchecked","Duplicates"})
private void handleReadableKey(SelectionKey key) throws IOException {
// Client Socket Channel
SocketChannel clientSocketChannel = (SocketChannel) key.channel();
// 读取数据
ByteBuffer readBuffer = CodecUtil.read(clientSocketChannel);
// 处理连接已经断开的情况
if (readBuffer == null) {
System.out.println("断开 Channel");
clientSocketChannel.register(selector, 0);
return;
}
// 打印数据
if (readBuffer.position() > 0) { // 写入模式下,
String content = CodecUtil.newString(readBuffer);
System.out.println("读取数据:" + content);
// 添加到响应队列
List<String> responseQueue = (ArrayList<String>) key.attachment();
responseQueue.add("响应:" + content);
// 注册 Client Socket Channel 到 Selector
clientSocketChannel.register(selector, SelectionKey.OP_WRITE, key.attachment());
}
}
@SuppressWarnings({"unchecked","Duplicates"})
private void handleWritableKey(SelectionKey key) throws ClosedChannelException {
// Client Socket Channel
SocketChannel clientSocketChannel = (SocketChannel) key.channel();
// 遍历响应队列
List<String> responseQueue = (ArrayList<String>) key.attachment();
for (String content : responseQueue) {
// 打印数据
System.out.println("写入数据:" + content);
// 返回
CodecUtil.write(clientSocketChannel, content);
}
responseQueue.clear();
// 注册 Client Socket Channel 到 Selector
clientSocketChannel.register(selector, SelectionKey.OP_READ, responseQueue);
}
public NIOServer(int port) {
try {
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(port));
// BIO升级NIO,为了兼容BIO,NIO模型默认采用阻塞模式
server.configureBlocking(false);
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server 启动完成,listen port: " + port);
handleKeys();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new NIOServer(8080);
}
}
NIOClient
public class NIOClient {
private SocketChannel clientSocketChannel;
private Selector selector;
private CountDownLatch connected = new CountDownLatch(1);
private final List<String> responseQueue = new ArrayList<String>();
public NIOClient() throws IOException, InterruptedException {
// 打开 Client Socket Channel
clientSocketChannel = SocketChannel.open();
// 配置为非阻塞
clientSocketChannel.configureBlocking(false);
// 创建 Selector
selector = Selector.open();
// 注册 Server Socket Channel 到 Selector
clientSocketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接服务器
clientSocketChannel.connect(new InetSocketAddress(8080));
new Thread(() -> {
try {
handleKeys();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
if (connected.getCount() != 0) {
connected.await();
}
System.out.println("Client 启动完成");
}
@SuppressWarnings("Duplicates")
private void handleKeys() throws IOException {
while (true) {
// 通过 Selector 选择 Channel
int selectNums = selector.select(30 * 1000L);
if (selectNums == 0) {
continue;
}
// 遍历可选择的 Channel 的 SelectionKey 集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 移除下面要处理的 SelectionKey
if (!key.isValid()) { // 忽略无效的 SelectionKey
continue;
}
handleKey(key);
}
}
}
private synchronized void handleKey(SelectionKey key) throws IOException {
// 接受连接就绪
if (key.isConnectable()) {
handleConnectableKey(key);
}
// 读就绪
if (key.isReadable()) {
handleReadableKey(key);
}
// 写就绪
if (key.isWritable()) {
handleWritableKey(key);
}
}
private void handleConnectableKey(SelectionKey key) throws IOException {
// 完成连接
if (!clientSocketChannel.isConnectionPending()) {
return;
}
clientSocketChannel.finishConnect();
// log
System.out.println("接受新的 Channel");
// 注册 Client Socket Channel 到 Selector
clientSocketChannel.register(selector, SelectionKey.OP_READ, responseQueue);
// 标记为已连接
connected.countDown();
}
@SuppressWarnings("Duplicates")
private void handleReadableKey(SelectionKey key) throws ClosedChannelException {
// Client Socket Channel
SocketChannel clientSocketChannel = (SocketChannel) key.channel();
// 读取数据
ByteBuffer readBuffer = CodecUtil.read(clientSocketChannel);
// 打印数据
if (readBuffer.position() > 0) { // 写入模式下,
String content = CodecUtil.newString(readBuffer);
System.out.println("读取数据:" + content);
}
}
@SuppressWarnings({"unchecked", "Duplicates"})
private void handleWritableKey(SelectionKey key) throws ClosedChannelException {
// Client Socket Channel
SocketChannel clientSocketChannel = (SocketChannel) key.channel();
// 遍历响应队列
List<String> responseQueue = (ArrayList<String>) key.attachment();
for (String content : responseQueue) {
// 打印数据
System.out.println("写入数据:" + content);
// 返回
CodecUtil.write(clientSocketChannel, content);
}
responseQueue.clear();
// 注册 Client Socket Channel 到 Selector
clientSocketChannel.register(selector, SelectionKey.OP_READ, responseQueue);
}
public synchronized void send(String content) throws ClosedChannelException {
// 添加到响应队列
responseQueue.add(content);
// 打印数据
System.out.println("写入数据:" + content);
// 注册 Client Socket Channel 到 Selector
clientSocketChannel.register(selector, SelectionKey.OP_WRITE, responseQueue);
selector.wakeup();
}
public static void main(String[] args) throws IOException, InterruptedException {
NIOClient client = new NIOClient();
for (int i = 0; i < 30; i++) {
client.send("你好: " + i);
Thread.sleep(1000L);
}
}
}
AIO
服务端:AsynchronousServerSocketChannel
客服端:AsynchronousSocketChannel
用户处理器:CompletionHandler
接口,这个接口实现应用程序向操作系统发起 IO 请求,当完成后处理具体逻辑,否则做自己该做的事情
“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程, 由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放 在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。异步IO模型使用了Proactor
设计模式实 现了这一机制
AIOServer
public class AIOServer {
private final int port;
public AIOServer(int port) {
this.port = port;
listen();
}
private void listen() {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
AsynchronousChannelGroup threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
final AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(threadGroup);
server.bind(new InetSocketAddress(port));
System.out.println("server is start,handleKeys port: " + port);
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
@Override
public void completed(AsynchronousSocketChannel result, Object attachment) {
System.out.println("IO操作成功,开始获取数据");
byteBuffer.clear();
try {
result.read(byteBuffer).get();
byteBuffer.flip();
result.write(byteBuffer);
byteBuffer.flip();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
result.close();
server.accept(null, this);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("操作完成");
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("IO操作失败:" + exc);
}
});
} catch (IOException e) {
e.printStackTrace();
}
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new AIOServer(8080);
}
}
AIOClient
public class AIOClient {
private final AsynchronousSocketChannel client;
public AIOClient() throws IOException {
this.client = AsynchronousSocketChannel.open();
}
public void connect(String host, int port) {
client.connect(new InetSocketAddress(host, port), null, new CompletionHandler<Void, Void>() {
@Override
public void completed(Void result, Void attachment) {
try {
client.write(ByteBuffer.wrap("这是一条测试数据".getBytes())).get();
System.out.println("已经发送至服务器");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
client.read(byteBuffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
System.out.println("IO操作完成"+result);
System.out.println("获取反馈结果"+new String(byteBuffer.array()));
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
new AIOClient().connect("localhost",8080);
}
}