1、BIO
关于BIO有同学可能会说不知道,但是知道IO。其实你之前学过那些java.io包下面的输入流和输出流就是BIO中的文件IO部分。例如InputStream、OutputStream。还有一部分就是网络IO,在java.net包下面的提供了部分网络API,例如Socket, ServerSocket。
1.1 概念
Java BIO就是传统的java io编程,在java.io包下有相关的类和接口。
BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情就会造成不必要的线程开销(当然可以通过线程池机制改善)。
同步和异步是针对应用程序和内核的交互而言的。
同步:指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。
生活案例:自己拿上银行卡去银行取钱(使用同步IO时,Java自己处理IO读写)。
异步:指的是用户进程触发IO操作以后便开始做其他的事情,而当IO操作已经完成的时候会得到IO完成的通知。
生活案例:托朋友拿着自己的银行卡去银行取钱,告诉朋友自己的银行卡密码,自己去办理其他事情。同时,你还要告诉朋友取完钱给你送到哪里。(
使用异步I/O时, Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS )。
阻塞和非阻塞是针对进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式。
阻塞:指的是准备对文件进行读写时,如果当时没有东西可读,或暂时不可写,程序就进入等待状态,直到有东西可读或可写为止。
生活案例:你在ATM取款,发现前面有人,你只能等待,等其他人办理完你才能取钱(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)。
非阻塞:指的是如果没有东西可读,或不可写,读写函数马上返回,而不会等待。
生活案例:在银行办业务时,人多要先取个号,然后我们可以跟朋友开黑,等轮到我们,银行就会通知,这时候我们就可以去办业务了(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。
1.2 工作模型
1、启动一个服务器(serverSocket),等待客户端的连接
2、启动一个客户端(Socket),然后与服务器进行通信(默认情况下服务器端需要给每个客户端建立一个线程与其通信)。
3、客户端发出请求,询问服务器是否有线程响应,如果没有就会等待或被拒绝。
4、如果有响应,客户端线程会等待请求结束后再继续执行。
1.3 案例
使用BIO模型编写一个服务器(服务器端口为10086),客户端可以发消息给服务器。
服务器端:
package testIO;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO模式下的服务器
*/
public class BIOServer {
public static void main(String[] args) {
try {
//服务器
ServerSocket serverSocket=new ServerSocket(10086);
System.out.println("服务器启动--端口号是10086");
while(true){
//连接到服务器的客户端--堵塞方法
Socket client = serverSocket.accept();
System.out.println("有客户端连接成功!");
//为每个客户端连接都创建一个新的线程与之通信
new Thread(()->{
System.out.print("线程id:"+Thread.currentThread().getId());
System.out.println("线程名 称:"+Thread.currentThread().getName());
try {
int len=0;
byte[] byteArr=new byte[1024];
//读取来自客户端的数据
InputStream inputStream = client.getInputStream();
while((len=inputStream.read(byteArr))!=-1){
System.out.println(len);
String msg=new String(byteArr,0,len);
System.out.println("来自客户端的消息:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
package testIO;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
/**
* BIO模式的客户端
*/
public class BIOClient {
public static void main(String[] args) {
try {
//创建客户端
Socket client=new Socket("127.0.0.1", 10086);
String msg="hi,xiaoli";
OutputStream outputStream = client.getOutputStream();
System.out.println(msg.length());
outputStream.write(msg.getBytes(), 0, msg.length());
outputStream.close();
System.in.read();//目的是让客户端保持与服务器的连接
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.4 服务器优化+Telnet测试
1.4.1 通过线程池
服务端代码改进如下:
package testIO;
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;
/**
* BIO模式下的服务器
*/
public class BIOServer {
public static void main(String[] args) {
//创建线程池
ExecutorService executorService= Executors.newCachedThreadPool();
try {
//服务器
ServerSocket serverSocket=new ServerSocket(10086);
System.out.println("服务器启动--端口号是10086");
while(true){
//连接到服务器的客户端--堵塞方法
Socket client = serverSocket.accept();
System.out.println("有客户端连接成功!");
//为每个客户端连接都创建一个新的线程与之通信
executorService.execute(()->{
System.out.print("线程id:"+Thread.currentThread().getId());
System.out.println("线程名 称:"+Thread.currentThread().getName());
try {
int len=0;
byte[] byteArrs=new byte[1024];
//读取来自客户端的数据
InputStream inputStream = client.getInputStream();
while((len=inputStream.read(byteArrs))!=-1){
String msg=new String(byteArrs,0,len);
System.out.println("来自客户端的消息:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
可以看到一直用的是线程池的一个线程
客户端代码还是如上;
1.4.2 Telnet 方式测试
这个方法就不需要用到client代码了
解决方案:
windows7 在这里找
Windows11在这里找;
如果是第⼀次打开这个功能,加载⽐较慢。加载完成后,找到
Telnet客户端选项,勾选这个选项,然后点击确定保存。
OK了!!
这回可以进来了
按Ctrl + ]
可以看到已经发送成功了。
1.5 总结
1.5.1 BIO缺点:
客户端越来越多,服务器就要开启越来越多的线程,对服务器的压力就会越大;而且客户端发起一个连接之后不一定都在做事情,这个时候服务器也要维护,造成不必要的压力。
1.5.2 使用场景:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,该方式是JDK1.4以前的唯一选择,但程序直观简单易理解。