Java BIO 就是传统的 java io 编程, 其相关的类和接口在 java.io
中.
BIO 编程简单流程
- 服务器端启动一个 ServerSocket.
- 客户端启动 Socket 对服务器进行通讯, 默认情况下服务器需要对每个客户建立一个县线程与之通讯.
- 客户端发出请求后, 先咨询服务器是否有线程响应, 如果没有则会等待, 或者被拒绝.
- 如果有响应, 客户端线程会等待请求结束后才继续执行.
Java BIO 应用实例
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class BIOServer {public static void main(String[] args) throws Exception {ExecutorService executorService = Executors.newCachedThreadPool();ServerSocket serverSocket = new ServerSocket(6666);System.out.println("服务器启动");while (true) {// 监听, 等待客户端连接, 每一个 Socket 对象就是一个 客户端.final Socket socket = serverSocket.accept();System.out.println("一个客户端连接");// 创建线程与客户端通讯executorService.execute(() -> {try {InputStream inputStream = socket.getInputStream();while (true) {byte[] bytes = new byte[1024];int read = inputStream.read(bytes);if (read != -1) {System.out.println(new String(bytes, 0, read));} else {break;}}} catch (IOException e) {e.printStackTrace();} finally {System.out.println("关闭客户端连接");try {socket.close();} catch (IOException e) {e.printStackTrace();}}});}}
}
SocketInputStream
在上面的示例代码中, 要读取客户端发送的数据时, 需要先调用 socket.getInputStream()
方法获得输入流, 也就是在该方法内部调用 SocketImpl#getInputStream()
, SocketImpl
是个抽象类, socket 通用超类用于创建客户端和服务器套接字.
该输入流对象就是 SocketInputStream
, 它是 FileInputStream
的子类.
当调用 read(byte[])
方法读取数据时, 最终会调用 socketRead0
这个本地方法.
该本地方法会根据是否设置了超时时间, 做不同的系统调用:
- 有超时时间: 最终会在
for(;;)
中调用select
方法(阻塞), 当超时, 失败或读到数据时候, 该方法会返回 0, -1或大于0的整数, 这个整数表示就绪描述符的数目. 当有数据可读的时候在调用recv
函数非阻塞读取数据. 该函数的最后一个参数值为MSG_DONTWAIT
, 即非阻塞操作. - 没有超时时间: 这种情况比较简单, 直接调用以阻塞方式
recv
函数, 也就是最后一个参数值为0
.
上面是读, 下面说说写.
需要获得输出流, socket.getOutputStream()
.
在 OutputStream
中的 write(byte b[], int off, int len)
方法中也有限制, 每次只能写一个字节.
最终调用本地方法 socketWrite0
, 在该方法中会以阻塞方式调用 send
方法写数据.
FileInputStream
- 读数据: 调用
read
方法读数据时, 最终会调用该类中的readBytes(byte b[], int off, int len)
本地方法, 该方法中会调用系统函数read
, 以阻塞形式读取指定长度的数据. - 写数据: 调用
write
方法写数据时, 最终会调用该类中的writeBytes(byte b[], int off, int len, boolean append)
本地方法, 该方法中会调用系统函数write
直接写指定长度数据.
需要注意我现在使用的 jdk 为:
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.252-b09, mixed mode)
Java BIO 问题分析
- 每个请求都需要创建独立的线程, 与对应的客户端进行数据连接 Read, 业务处理. 数据 Write.
- 当并发数较大时, 需要创建大量线程来处理连接, 系统资源占用较大.
- 连接创建后, 如果当前线程暂时没有数据可读, 则线程就阻塞在 Read 操作上, 造成线程资源浪费, 而且线程的阻塞和唤醒需要占用系统资源.
讨论
recv 方法可以读取任意长度的字节, 为什么在 Java 中要规定每次只读一个字节?
首先限制每次只读取一个字节是在 InputStream
抽象类中的 read(byte b[], int off, int len)
方法限制的.
int c = read();if (c == -1) {return -1;}b[off] = (byte)c;int i = 1;try {for (; i < len ; i++) {c = read();if (c == -1) {break;}b[off + i] = (byte)c;}} catch (IOException ee) {}
如果返回 -1
就说明已经读完数据了, 如果不是 -1
就将数据添加到传进来的 byte[]
数组中.
至于为啥要这样, 我个人认为和 & 0xff
有关.
参考文章
JavaIO原理剖析之 网络IO