本文主要是重新梳理了Java的IO模型,基于之前NIO的文章进行补充,为学习Netty做准备。
1、什么是IO模型: 简单地说,就是用什么样的通道进行数据的发送和接收。比如通道是阻塞的还是非阻塞的,是同步还是异步的。
2、Java支持的IO模型: java支持的IO模型有:
1、BIO编程流程:
2、BIO的应用实例:
public class BioServer {
public static void main(String[] args) throws IOException {
// 1. 创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 2. 创建serverSocket并监听端口
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务端已启动");
// 3. 等待客户端连接
System.out.println("等待连接……");
while (true) {
final Socket socket = serverSocket.accept();
System.out.println("客户端连接进来了");
// 4. 创建一个线程与之通信
executorService.execute(new Runnable() {
@Override
public void run() {
handler(socket);
}
});
}
}
public static void handler(Socket socket) {
try {
byte[] bys = new byte[1024];
// 通过socket获取输入流
InputStream inputstream = socket.getInputStream();
// 循环读取客户端发送的数据
while (true) {
System.out.println("线程id:" + Thread.currentThread().getId() + ",线程名:" + Thread.currentThread().getName());
System.out.println("reading……");
int read = inputstream.read(bys);
// 不等于负一表示还没读取完
if (read != -1) {
System.out.println(new String(bys, 0, read));
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭socket连接
try { socket.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
}
这段代码就是按照需求编写了一个服务端。启动这个类,然后在CMD窗口输入telnet 127.0.0.1 6666
回车,然后按ctrl
+ ]
,就进入了telnet,输入send hello
,服务端控制台就会打印出hello
以及线程信息。可以发现,当我们启动服务端后,控制台会打印出等待连接……
,然后就卡在这里不动了,当我们通过telnet连接后,会打印出reading……
,并且卡在那里,说明这是阻塞的。我们启动两个telnet去连接,通过控制台打印的线程id可以发现,处理这两个客户端连接的是两个线程,这与之前的模型分析一致。
1、NIO三大核心部分:
selector、buffer、channel之间的关系:
2、buffer: buffer有四个重要的属性:
读取数据的时候可以设置position和limit,表示从哪儿开始读,读到哪儿结束。
3、channel: channel类似BIO的流,但是有些区别,如下:
channel是一个接口,用得比较多的实现有如下几个:
看几个实操案例:
public class NioFileChannel01 {
public static void main(String[] args) throws IOException {
String str = "带你去爬山啊";
FileOutputStream fos = new FileOutputStream("C:\\Users\\14751\\Desktop\\test01.txt");
// 1. 通过FileOutputStream获取对应的FileChannel
FileChannel fc = fos.getChannel();
// 2. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 将str放入buffer中
buffer.put(str.getBytes());
// 4. 切换写数据模式
buffer.flip();
// 5. 将buffer数据写入到通道
fc.write(buffer);
// 6. 关闭资源
fos.close();
fc.close();
}
}
public class NioFileChannel02 {
public static void main(String[] args) throws IOException {
// 1. 读取test01.txt文件
File file = new File("C:\\Users\\14751\\Desktop\\test01.txt");
// 2. 将file转成FileInputStream
FileInputStream fis = new FileInputStream(file);
// 3. 获取通道
FileChannel channel = fis.getChannel();
// 4. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int)file.length());
// 5. 将通道数据读到buffer中
channel.read(buffer);
System.out.println(new String(buffer.array()));
// 6. 关闭资源
fis.close();
channel.close();
}
}
public class NioFileChannel03 {
public static void main(String[] args) throws IOException {
// 1. 读取源文件
FileInputStream fis = new FileInputStream("C:\\Users\\14751\\Desktop\\test01.txt");
// 2. 获取通道
FileChannel sourceChannel = fis.getChannel();
// 3. 加载目标文件
FileOutputStream fos = new FileOutputStream("C:\\Users\\14751\\Desktop\\test02.txt");
// 4. 获取通道
FileChannel targetChannel = fos.getChannel();
// 5. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// 6. 标志位复位,一定不能漏了这步,否则死循环
buffer.clear();
// 7. 读取数据
int read = sourceChannel.read(buffer);
if (read == -1) {
break;
}
// 8. 切换到写数据模式,并将buffer中的数据写入到targetChannel
buffer.flip();
targetChannel.write(buffer);
}
// 9. 关闭资源
fis.close();
sourceChannel.close();
fos.close();
targetChannel.close();
}
}
public class NioFileChannel04 {
public static void main(String[] args) throws IOException {
// 1. 读取源文件
FileInputStream fis = new FileInputStream("C:\\Users\\14751\\Desktop\\test01.txt");
// 2. 获取通道
FileChannel sourceChannel = fis.getChannel();
// 3. 加载目标文件
FileOutputStream fos = new FileOutputStream("C:\\Users\\14751\\Desktop\\test03.txt");
// 4. 获取通道
FileChannel targetChannel = fos.getChannel();
// 5. 使用transferFrom完成拷贝
targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
// 6. 关闭资源
fis.close();
sourceChannel.close();
fos.close();
targetChannel.close();
}
}
public class NioFileChannel05 {
public static void main(String[] args) throws IOException {
// 1. 加载文件
RandomAccessFile file = new RandomAccessFile("C:\\Users\\14751\\Desktop\\test01.txt", "rw"); // rw表示读写
// 2. 获取文件通道
FileChannel channel = file.getChannel();
// 3. 获取MappedByteBuffer,这三个参数,第一个表示读写模式,第二个表示直接修改的起始位置,第三个表示映射到内存中的大小
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
// 4. 对test01.txt进行修改
buffer.put(0, (byte)'A'); // 第一个字符改成A
buffer.put(1, (byte)'B'); // 第二个字符改成B
// 5. 关闭资源
file.close();
channel.close();
}
}
/**
* scattering:将数据写入到buffer时,可以采用buffer数组,依次写入
* gathering:从buffer读数据的时候,可以采用buffer数组,依次读取
* @author zhu
*
*/
public class NioFileChannel06 {
public static void main(String[] args) throws IOException {
// 1. 创建channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 2. 绑定端口并启动
InetSocketAddress address = new InetSocketAddress(6666);
serverChannel.socket().bind(address);
// 3. 创建buffer数组
ByteBuffer[] buffers = new ByteBuffer[2];
buffers[0] = ByteBuffer.allocate(5);
buffers[1] = ByteBuffer.allocate(4);
// 4. 等待客户端连接
SocketChannel channel = serverChannel.accept();
// 5. 循环读取
// 假设客户端会发送8个字节
int len = 8;
while (true) {
int read = 0;
while (read < len) {
long byteNum = channel.read(buffers);
read += byteNum;
System.out.println("读取到的字节数:" + read);
}
// 6. 切换模式
Arrays.asList(buffers).forEach(buffer -> buffer.flip());
// 7. 将读取到的数据显示到客户端
long writeLen = 0;
while (writeLen < len) {
long byteNum = channel.write(buffers);
writeLen += byteNum;
}
// 8. 将所有buffer进行clear
Arrays.asList(buffers).forEach(buffer -> buffer.clear());
}
}
}
4、selector: selector能够检测多个通道是否有事件要发生,多个channel以事件的方式可以注册到同一个selector中。主要工作流程如下:
看一个实操案例:用NIO实现服务端和客户端的通讯:
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 创建NIOServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2. 得到Selector对象
Selector selector = Selector.open();
// 3. 绑定端口,进行监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 4. 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 5. 把serverSocketChannel注册到selector中,设置关心事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6. 循环等待客户端连接
while (true) {
if (selector.select(1000) == 0) { // 没有事件
System.out.println("服务器等待了1秒钟,没有事件发生");
continue;
} else { // 有事件
// 7. 有事件发生,就拿到selectionKey的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 8. 通过selectionKeys得到channel
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 9. 根据key的不同事件,做对应的处理
if (key.isAcceptable()) { // 如果是OP_ACCEPT连接事件
// 10. 为该客户端生成一个SocketChannel并设置成非阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 11. 将当前socketChannel也注册到selector中,关注事件为OP_READ,并且关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) { // 如果是OP_READ读取事件
// 12. 通过key得到channel
SocketChannel channel = (SocketChannel) key.channel();
// 13. 获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 14. 将channel中的数据读到buffer中去
channel.read(buffer);
System.out.println("客户端发送的数据:" + new String(buffer.array()));
}
// 15. 移除当前的selectionKey,防止重复操作
keyIterator.remove();
}
}
}
}
}
public class NIOClient {
public static void main(String[] args) throws IOException {
// 1. 设置ip和端口
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
// 2. 创建SocketChannel并设置成非阻塞
SocketChannel socketChannel = SocketChannel.open(address);
socketChannel.configureBlocking(false);
// 3. 连接服务器
String str = "hello world";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
// 4. 将数据写入channel
socketChannel.write(buffer);
System.in.read();
}
}