从本章开始,后面一段时间都学习网络和IO方面的知识。网络和IO都是知识盲区,慢慢整理,争取把这部分梳理清楚,首先IO是Input和Output的简写。代表输入和输出。而从位置来区分,又分为本地IO和网络IO。本地IO代表本机系统内的设备进行通信,网络代表本机系统和网络上的系统进行通信。而网络通讯模型又分为BIO(同步阻塞IO),NIO(同步非阻塞IO),AIO(异步非阻塞IO)。本章主要记录BIO的简单使用。
本地IO介绍
本地IO代表机器内部设备通信,比如,硬盘到内存,内存到网卡,内存到CPU等都是本地IO,我们以本地读写文件为例,其中包含了应用程序,CPU,内存,硬盘等设备。
读文件
Java代码指定文件位置,并通过流的方式读取文件。
我将Java读文件涉及的对象图像形式展示,如下:

- 应用程序通知操作系统需要访问某个位置的文件。
- 操作系统通知DMA进行操作
- DMA将硬盘上的资源加载到内核空间的缓冲区。
- 然后再将内核缓冲区的文件拷贝到用户空间的用户缓冲区。
- 实现对文件的访问功能
DMA 全称Direct Memory Access,即直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。当CPU初始化这个传输动作,传输动作本身是由DMA控制器来实现和完成的。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场过程,通过硬件为RAM和IO设备开辟一条直接传输数据的通道,使得CPU的效率大大提高。
写文件
- 执行读文件过程
- 修改用户空间内存缓冲区内容。
- 将用户缓冲区内容复制到内核缓冲区
- 调用sync刷新内存缓冲区的内容到硬盘
网络IO介绍
网络IO和本地IO不同的地方在于会使用到网卡,Internet,TCP协议等,下面将对TCP的发送和接收的网络模型进行整理.
网络发送

处理流程如下:
- 应用在用户空间将数据处理完成并写入应用缓冲区。
- 触发系统调用send命令将用户空间数据拷贝到内核空间的TCP发送缓冲区
- 最后通过DMA将内核缓冲区的数据复制到网卡缓冲区。
网络接收
网络接收正好相反。
- 首先网卡缓冲区接收到数据。
- 通过DMA将网卡缓冲区数据复制到TCP接收缓冲区
- 系统调用recv将TCP缓冲区数据拷贝到应用缓冲区
BIO
BIO介绍
上面我们提到了BIO(同步阻塞IO),那么什么是同步?什么是阻塞?
阻塞IO 和非阻塞IO 这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行(但是使用线程一直轮询,直到有IO资源准备好了)。
同步IO 和 异步IO,这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题:前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。
这个解答也是我在网络上看到的,暂时没有理清楚这个关系,等理解清楚再更新。
BIO编码实践
下面的代码实现了一个比较简单的长链接和短连接,没有心跳,连接池,只作为测试使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| public class BioSocket { private static final int PORT = 8080; private static final String HOST = "127.0.0.1"; private static final ExecutorService executor = Executors.newCachedThreadPool();
public static void main(String[] args) { executor.submit(BioSocket::start); executor.submit((Runnable) () -> { while (true){ BIOClient.sendMsg("msg: 1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); executor.submit((Runnable) () -> { while (true){ BIOClient.sendMsg("msg: 2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); executor.submit((Runnable) () -> { while (true){ BIOClient.sendMsg("msg: 3"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } });
}
private static synchronized void start(){
try(ServerSocket serverSocket = new ServerSocket(PORT)) { while (true){ System.out.println("服务端启动,可以处理连接了..."); Socket socket = serverSocket.accept(); executor.submit(new SocketHandler(socket)); } } catch (IOException e) { e.printStackTrace(); } }
static class SocketHandler implements Runnable{ private Socket socket;
public SocketHandler(Socket socket) { this.socket = socket; }
@Override public void run() { try(BufferedReader r = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter w = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) { while (true){ String line = r.readLine(); if(!TextUtils.isEmpty(line)) { System.out.println("服务器readline: " + line); String result = "线程名称: " + Thread.currentThread().getName() + "消息内容: " + line; w.write(result); w.newLine(); w.flush(); } } } catch (IOException e) { e.printStackTrace(); }
} }
static class BIOClient{ public static void sendMsg(String msg){ try (Socket socket = new Socket(HOST,PORT); BufferedReader r = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter w = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))){ w.write(msg); w.newLine(); w.flush(); String readMsg = r.readLine(); if (!TextUtils.isEmpty(readMsg)){ System.out.println("客户端: "+readMsg); } } catch (IOException e) { e.printStackTrace(); } } } }
|
说明:
- 使用缓存池复用线程,减少线程创建和CPU上下文切换问题。
- 通过读取数据为空检查是否释放连接(注释即可实现长连接)
总结
BIO模型及缺陷
我们运行上面的代码

代码中我们模拟长链接的实现,看到打印的日志中线程池的线程数不断的创建,大量的连接会创建大量的线程,但是连接是阻塞的,就算没有做任何事情,线程仍然不会被销毁。创建线程是需要占用内存资源的,维护线程是需要CPU资源的。每个系统可运行非常线程有限,所以不适合高并发的场景。
如果我们模拟短连接,并根据条件主动销毁无用连接,看看日志

使用短连接的方式保证了线程的复用,减小了CPU上下文切换,根据场景可以自适用线程数,这是一种伪异步IO的模式。但是底层还是使用同步阻塞模型。
在网上找了一份整理的Java IO流的表格整理。可以参考使用
