IO简介
IO即为Input/Output,IO可分为两种磁盘IO和网络IO。顾名思义,磁盘IO指的是从磁盘中进行输入和输出,也就是磁盘数据的读取和存储;而网络IO便指的是网络数据的读取和写入,具体的数据读取方式则是前文提到的JAVA网络编程之Socket。本系列主要内容为java网络编程IO系列,涵盖java中的BIO、NIO和AIO三种模型。对于IO模型不太了解的建议先看一下Linux下的五种网络IO模型
BIO概念
起初,java中的IO操作,都是基于BIO的,BIO原意为Base IO ,因为是最早使用的,也是IO调用的基础用法,但因为BIO会阻塞当前线程B又被理解为Block,即阻塞IO,也是现在通用的说法。传统BIO执行时,执行方式为同步阻塞的,当前线程会等待IO调用执行完成,返回结果,这个会阻塞当前线程,此时cpu会保存当前线程上下文,进行线程切换,执行其他就绪线程。线程切换是需要耗费cpu资源的,频繁的切换线程会导致cpu资源的极大浪费。
BIO处理流程
为了支持并发,服务器通常会采用多线程并发处理请求。
即有新连接进入时,创建新线程,在新线程中进行IO和计算处理。过程如下所示:
通常为了避免频繁创建和销毁线程,服务器会使用线程池进行线程调度和管理。
BIO实现
/**
* BIO服务端
**/
public class BIOServer {
static ExecutorService serverService = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws IOException {
//定义服务端socket,监听9090端口
ServerSocket serverSocket = new ServerSocket(9090);
//死循环接收客户端请求
while (true) {
//祖舍获取连接
Socket socket = serverSocket.accept();
//获取新连接,提交线程池处理
serverService.submit(new ServerRequestHandler(socket));
}
}
}
/**
* 处理请求
**/
public class ServerRequestHandler implements Runnable {
private Socket socket;
public ServerRequestHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
DataInputStream dis = new DataInputStream(socket.getInputStream());
System.out.println("收到消息:" +dis.readUTF());
DataOutputStream dos=new DataOutputStream(socket.getOutputStream());
dos.writeUTF("消息已收到");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 简易客户端
**/
public class Client {
public static void main(String[] args) throws IOException {
for (int i=1;i<5;i++) {
// 构造客户端socket,连接本地9090端口
Socket socket = new Socket("localhost", 9090);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF("这是来自客户端" + i + "的请求");
DataInputStream dis=new DataInputStream(socket.getInputStream());
System.out.println("服务端响应:"+dis.readUTF());
socket.close();
}
}
}
总结
从处理流程图及简易代码实现可以看出,一个连接会分配一个线程处理,而我们的线程资源是异常宝贵的,不可能无限多,这样一来线程数就限制了服务器的并发。当线程内的请求处理的越慢,服务端可处理的并发请求就越低。而在线程内进行数据的读取和写入IO操作又是阻塞的,进而使得单个线程处理请求的时间拉长,最终导致服务器的整体并发能力不足。当需要满足更高并发的要求时,就要使用其他的IO模式了。