目录
1. I/O
input/output 的缩写,简单的理解为数据的输入输出,我应用从网络中,或者从硬盘中读取数据写入数据,都会用到IO,IO的类型分为同步/异步IO,阻塞/非阻塞IO,然后他们组合就会形成具体的IO模型。IO和java没有太大的关系,IO基本上属于操作系统层面的知识。操作系统层面的IO模型如下
- 同步阻塞IO
- 同步非阻塞IO
- 异步非阻塞IO
- IO多路复用
- 信号IO
1.1 Java 读写数据
说明一下我们写的java 程序是怎么从硬件里面拿数据,写数据的。
发送数据:如果我们要发送数据,我们首先要把数据发送给内核,然后内核把数据给网卡,网卡发送到互联网中
接收数据:我们先要让内核去看看网卡或者硬盘里拿数据,有的话就把数据先放入内核,然后内核再放入jvm,jvm给我们具体的应用程序。
当然我们jvm和内核交互的逻辑都是内核暴露的方法、api。
1.1.1 阻塞IO/非阻塞IO
上面说到,如果我要接收数据,我是不是告诉内核你去网卡里面去拿数据,万一数据没有,阻塞IO就会一直等,等到你要的数据来,你的应用程序也是处于一个等待状态。
非阻塞IO就是我告诉你内核有数据来了,如果没有我应用程序就不等内核拿到数据,就去干别的事,我时不时过来问一下数据有了吗?有了你就给我,没有你就继续等着。
1.1.2 同步IO/异步IO
同步IO的意思就是内核是首我应用程序调用的,应用程序要数据我就给你,你不要,就算数据到了,内核也不会主动告诉你数据到了。应用程序需要有个轮询机制一直问内核数据到没到,到了给我。
异步IO就是应用程序问内核要数据,内核如果没有的话,就会注册一个回调函数,内核数据到了就主动告诉应用程序,你的数据到了,并且把数据给到相应的应用程序。
1.2 Java BIO 模型
BIO用的就是上面说的同步阻塞模式
1.2.1 BIO编写服务端
为了方便理解直接用代码说话,其实上面的话都是总结代码的实现,代码来说更直观一些
这边为了可以 循环接受就用了一个while保持运行。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
while(true){
//这串代码就是向内核快去拿数据,内核拿不到就会一直阻塞在这边
Socket socket = serverSocket.accept();//获取数据
//起个线程处理拿回来的数据,如果不用线程的话,就算拿到了数据,处理的话也会阻塞
//为了拿到数据不阻塞因此我们开个线程
new Thread(new ServerRunable(socket)).start();
}
}
private static class ServerRunable implements Runnable{
private final Socket socket;
public ServerRunable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
OutputStream outputStream =null;
// 这就是拿socket做处理
try {
//这个是从socket里面拿数据
inputStream = socket.getInputStream();
//这个是向客户端发数据
outputStream = socket.getOutputStream();
//reader解析
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
//写数据
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
while (true){
//按行读数据,用换行符区分
String line = reader.readLine();
System.out.println("接收到了数据"+line);
writer.write("我是老6\n");
//刷出去,刷到网络中去
writer.flush();
}
} catch (IOException e) {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
}
}
1.2.1 BIO 编写客户端
其实和客户端差不多,都是建立连接接受数据发送数据
package com.itheima.bio;
import java.io.*;
import java.net.Socket;
public class BioClient {
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
BufferedWriter out = null;
try {
socket = new Socket("127.0.0.1",8888);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
System.out.println("准备向服务端写数据!");
//向服务端写数据
out.write("你好,我是老八 \n");
out.flush();
//接收来自服务端的数据
String line = in.readLine();
System.out.println("成功接收到来自服务端的数据:"+line);
} catch (IOException e) {
if (in != null) {
try {
in.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
}
1.2.3 BIO 的缺点
上面代码看过之后,会发现,这种编码模式,资源开销会非常大。所以一般不用这种BIO模型
1.3 Java NIO 模型
NIO 有三大组件,这个是一定要知道的分别是Buffer 数据载体、channel 通道,服务器与服务器通信的通道、selector 调用多路复用器。
1.3.1 Buffer 缓冲区
专业术语,其实没什么用,可以理解为就是个数据存储器,当然按照类型分可以分为好多。
我这边使用的都是ByteBuffer
1.3.2 Channel 通信通道
channel就是一个通信通道,具体干什么要看具体的实现类,比如我们上面BIO的读写数据就可以在SocketChannel中完成,而不用写input output流某种意义上简化了我们的代码开发。
最核心的就是ServerSocketChannel和SocketChannel俩个类
OP_开头的是事件类型分别是接受建立、写数据、读数据、建立。
这两个Channel分别对应以下功能,具体的功能是可以配置的。
类型 | OP_ACCEPT | OP_WRITE | OP_READ | OP_CONNECT |
ServerSocketChannel | Y | |||
SocketChannel | Y | Y | Y |
他们之间的关系如下图
1.3 Selector 多路复用器
通道建立了都要注册到多路复用器上,是因为selector 有个轮询机制,可以监听各个通道有没有相应的事件需要处理,为什么要这么做呢,主要解决的就是线程开销问题,如果没有这个Selector那么每一个Sockert Channel就会对应一个线程,监听每个通道的事件。现在有了selector 那么只需要一个线程去轮询这些channel 看看有没有事件就行。
这边非阻塞是什么意思呢,就是我通道里没有数据,我业务层面去拿数据,他是不会阻塞的。
1.3.1 NIO 服务端代码实现
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
public class NioServer {
public static void main(String[] args) throws IOException {
//建立一个接受建立的通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//把通道设置成非阻塞
serverSocketChannel.configureBlocking(false);
//绑定端口8888
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
//建立多路复用器
Selector selector = Selector.open();
//建立通道注册一个接受事件到复用器上面
SelectionKey register = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//然后多路复用器放入一个自己的线程去查找注册并且有事件产生的通道
new Thread(new nioRunable(selector)).start();
}
public static class nioRunable implements Runnable{
private final Selector selector;
public nioRunable(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
while(true){
try {
//这边就是查询这个多路复用器有没有通道有信息过来
//如果没有什么东西,那么会阻塞在这边,一直等到有事件过来
//当然这个阻塞是可以设置时间的
//selector.select(1000);
selector.select();
//这边就是查询到了有事件过来了,就会返回一个SelectionKey
//更具这个key 我就可以知道哪个通道上产生了哪些信息
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
resoleKey(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
void resoleKey(SelectionKey key) throws IOException{
//是否有效
if (key.isValid()){
//判断是否是接受建立连接事件
if(key.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//这边的channel 实际上就是main方法里的那个serverSocketChannel
SocketChannel accept = serverSocketChannel.accept();
//设置阻塞非阻塞
accept.configureBlocking(false);
//注册到selector中并且绑定读事件
accept.register(selector,SelectionKey.OP_READ);
}
//判断是否是Read事件
if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
//读数据
//buffer就是数据容器
ByteBuffer buffer = ByteBuffer.allocate(1024);
//数据从通道读取到buffer中
socketChannel.read(buffer);
//切换读写模式
buffer.flip();
//获取buff的大小
byte[] result = new byte[buffer.remaining()];
//buff往result数组里面写数据
buffer.get(result);
String msg = new String(result, Charset.defaultCharset());
System.out.println("数据"+msg);
buffer.clear();
buffer.put("hello".getBytes(StandardCharsets.UTF_8));
//读写模式转换
buffer.flip();
socketChannel.write(buffer);
}
}
}
}
}
这边的读写转换用下图解释一下,结合代码看一下
1.3.2 客户端代码实现
逻辑和服务端差不多
package com.itheima.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @description
* @author: ts
* @create:2021-04-02 15:24
*/
public class NioClient {
public static void main(String[] args) {
try {
//这边还是建立一个通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞模式,
socketChannel.configureBlocking(false);
//建立Selector
Selector selector = Selector.open();
//创建线程
new Thread(new SingleReactorClient(socketChannel,selector)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class SingleReactorClient implements Runnable{
private final SocketChannel socketChannel;
private final Selector selector;
public SingleReactorClient(SocketChannel socketChannel, Selector selector) {
this.socketChannel = socketChannel;
this.selector = selector;
}
public void run() {
try {
//连接服务端
doConnect(socketChannel,selector);
} catch (IOException e) {
e.printStackTrace();
//程序直接终止
System.exit(1);
}
//多路复用器执行多路复用程序
while (true) {
try {
//设置堵塞1s
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//处理监听到的事件
processKey(selectionKey);
iterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void doConnect(SocketChannel sc, Selector selector) throws IOException {
System.out.println("客户端成功启动,开始连接服务端");
boolean connect = sc.connect(new InetSocketAddress("127.0.0.1", 8888));
System.out.println("connect="+connect);
//如果连接成功了
if (connect) {
//注册读事件
sc.register(selector, SelectionKey.OP_READ);
System.out.println("客户端成功连上服务端,准备发送数据");
doService(sc);
}else {
sc.register(selector,SelectionKey.OP_CONNECT);
}
}
private void processKey(SelectionKey key) throws IOException {
//判断是否有效
if (key.isValid()) {
//如果是连接事件
if (key.isConnectable()) {
//从selector中拿出通道
SocketChannel sc = (SocketChannel) key.channel();
//看看是否连接上
if (sc.finishConnect()) {
//注册读事件
sc.register(selector,SelectionKey.OP_READ);
//读取数据
doService(sc);
}else {
System.exit(1);
}
}
//如果是读数据
if (key.isReadable()) {
//拿到通道
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBufer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBufer);
if (readBytes > 0) {
readBufer.flip();
byte[] bytes = new byte[readBufer.remaining()];
readBufer.get(bytes);
String msg = new String(bytes,"utf-8");
doService(msg);
}else if (readBytes < 0) {
key.cancel();
sc.close();
}else {
}
}
}
}
private static void doService(SocketChannel socketChannel) throws IOException {
System.out.println("客户端开始向服务端发送数据:");
byte[] bytes = "我是你爹!".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
socketChannel.write(writeBuffer);
}
private String doService(String msg) {
System.out.println("成功接收来自服务端响应的数据:"+msg);
return "";
}
}
2. Reactor 线程模型
这个说起来就很抽象,他是一种思想,多线程的思想。在这种思想中定义了三个角色。Reactor负责监听和分配事件、Acceptor处理客户端新连接、Hander 处理业务逻辑并且返回数据给channel
2.1 单Reactor单线程模型
其实就是三个角色都是一个线程来承担,我一个Reactor线程里面处理连接的建立,也要处理读写、也要处理业务数据,很容易阻塞。
2.2 单Reactor多线程模型
其实就是剥离一部分处理业务的逻辑进入线程池去做
2.3 主从Reactor 多线程(1+M+N)
Reactor里面监听读写之后读写操作其实是非常消耗时间的,那么这部分就要剥离出来
主线程只建立连接并且负责注册到子Selector中,子selector 自己监听注册到自己的通道,通道读取数据之后交给线程池处理。
这边实际上还是可以优化,在一个子的subReactor中某个channel 读写非常耗时了,那么会影响该subReactor 轮询监听事件,解决方案就是把轮询这个方法剥离出去,读写IO再搞多个线程进行处理