目录
- 一、BIO特点介绍
- 二、BIO代码实现
- 2.1、客户端代码准备
- 2.2、服务端单线程处理
- 2.2.1、服务端代码
- 2.2.2、阻塞代码分析
- 2.2.3、存在问题
- 2.3、服务端多线程处理
- 2.3.1、服务端代码
- 2.3.2、存在问题
一、BIO特点介绍
- BIO(blocking I/O):同步阻塞IO,在每个I/O操作(如读取或写入)都会导致线程被阻塞,直到操作完成。
- BIO方式适用于连接数目比较小且固定的架构,这种模型适合于较低的并发需求,每个连接通常都需要一个独立的线程,也可以使用线程池管理,JDK1.4以前的唯一选择,程序简单易理解。
- BIO是基于字节流和字符流进行操作的。
- 使用java.io包中的类,如InputStream和OutputStream,它们提供了阻塞式的I/O操作。
二、BIO代码实现
2.1、客户端代码准备
这里准备一个通用的客户端,会给服务端发送两条消息,发送时间间隔2秒,用于做后面的测试。
public class BioClient{public static void main(String[] args) {for (int i=0;i<1;i++) {// 这里使用多线程处理,为了适配多线程服务端new Thread(()->{BioClient bioClient = new BioClient();try {bioClient.run();} catch (Exception e) {e.printStackTrace();}}).start();}}public void run() throws Exception{Socket socket = null;InputStream inputStream = null;OutputStream outputStream = null;BufferedReader br = null;try {socket = new Socket("127.0.0.1", 9998);System.out.println(getTime() + "和服务端建立连接");Thread.sleep(2000);outputStream = socket.getOutputStream();PrintStream ps = new PrintStream(outputStream);System.out.println(getTime() + "给服务端发送消息:hello");ps.println("hello");Thread.sleep(2000);System.out.println(getTime() + "间隔2秒在给服务端发送消息:&end");ps.println("&end");ps.flush();inputStream = socket.getInputStream();br = new BufferedReader(new InputStreamReader(inputStream));String s = null;System.out.println(getTime() + "阻塞等待服务端发送给客户端的数据");while ((s = br.readLine()) != null) {System.out.println(getTime() + Thread.currentThread().getName()+" 接收到服务端的数据:" + s);}} finally {br.close();inputStream.close();outputStream.close();socket.close();}}public static String getTime(){return "time=" + System.currentTimeMillis()/1000 + "\t";}
}
2.2、服务端单线程处理
2.2.1、服务端代码
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(9998);while (true) {// 阻塞等待socket连接Socket accept = serverSocket.accept();System.out.println(getTime() + "建立连接 port=" + accept.getPort());InputStream inputStream = accept.getInputStream();BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));//连接后阻塞等待连接数据输入String s = null;while ((s = br.readLine()) != null) {System.out.println(getTime() + Thread.currentThread().getName() + " 接收到的数据:" + s);// 当收到&end消息时给客户端发送一个消息,并且关闭流跳出当前循环if ("&end".equals(s)) {PrintStream ps = new PrintStream(accept.getOutputStream());ps.println("早点睡");ps.flush();System.out.println(getTime() + "收到&end消息时给客户端发送一个消息完毕");br.close();inputStream.close();accept.close();break;}}System.out.println(getTime() + "一次连接处理完成,等待下一个连接");System.out.println("-----------------------------------------------------------");}}public static String getTime() {return "time=" + System.currentTimeMillis() / 1000 + "\t";}
}
2.2.2、阻塞代码分析
BIO有两个地方会阻塞线程,第一个就是在serverSocket.accept();
时,当没有连接建立时会一直等待,当有连接进入时开始执行后续流程,我这个例子中建立连接后会获取输入流读取客户端发送的消息,在调用br.readLine()
方法真正读取数据时,如果客户端还没有发送消息只是建立了连接,这一步也是会阻塞的,我在客户端建立socket连接后会暂停2秒才给服务端发送消息,所以这里会阻塞2s,当客户端暂停结束后会给服务端发送一条消息,当消息发送到服务端,服务端br.readLine()
方法读取到数据开始执行后续流程,因为这里是循环读取的所以第一条消息读取完之后又会执行br.readLine()
方法,这时又会阻塞等待客户端消息,客户端发送第一条消息后间隔2s会再次给服务端发送一条消息,消息内容为&end
,服务端br.readLine()
方法读取到第二条数据后判断消息内容是否为&end
,如果为&end
则给客户端发送一条早点睡
,发送完毕后会将流和连接全部关闭并且跳出循环,等待下一个连接。
2.2.3、存在问题
- BIO单线程同时只能处理一个连接,当调用
br.readLine()
方法读取数据时会阻塞,只有当连接断开或者读取到一次数据后才会执行后续逻辑,也就是说如果连接没有断开,客户端一直不给服务端发送消息那么服务端就会一直阻塞,我们这里的代码读取一次数据后还会继续循环读取直到读到&end
才会自己跳出循环,如果不跳出循环连接不断开则其它连接处理。 - BIO一般情况下无法使用一个线程处理多个连接,其实也是可以的,设想我们将获取到的socket存储在数组里,每收到两个socket再去处理,这样就能一个线程处理两个socket连接了,但是肯定不会这样做,因为
serverSocket.accept();
会阻塞,如果只来了一个连接,那么这个连接就一直不会处理了。 - 针对这些问题,竟然单线程无法处理那么下面使用多线程处理,继续分析。
2.3、服务端多线程处理
2.3.1、服务端代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class BioConcurrentServer {public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket(9998);ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));while (true) {// 阻塞等待socket连接Socket accept = serverSocket.accept();System.out.println(getTime() + "建立连接 port=" + accept.getPort());// 将socket连接交由线程池处理threadPoolExecutor.execute(()->{try {System.out.println(getTime() + "开始处理socket消息");run(accept);} catch (Exception e) {throw new RuntimeException(e);}});}}public static void run(Socket accept) throws Exception {InputStream inputStream = accept.getInputStream();BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));//连接后阻塞等待连接数据输入String s = null;while ((s = br.readLine()) != null) {System.out.println(getTime() + Thread.currentThread().getName() + " 接收到的数据:" + s);// 当收到&end消息时给客户端发送一个消息,并且关闭流跳出当前循环if ("&end".equals(s)) {PrintStream ps = new PrintStream(accept.getOutputStream());ps.println("早点睡");ps.flush();System.out.println(getTime() + "收到&end消息时给客户端发送一个消息完毕");br.close();inputStream.close();accept.close();break;}}System.out.println(getTime() + "一次连接处理完成,等待下一个连接");System.out.println("-----------------------------------------------------------");}public static String getTime() {return "time=" + System.currentTimeMillis() / 1000 + "\t";}
}
2.3.2、存在问题
- 在接收到客户端连接后使用线程池处理 Read / Write ,这样就能同时处理多个连接,看着好像没有什么问题,但是仔细想想如果客户端只是建立了连接没有给服务端发送消息,那么服务端在子线程调用
br.readLine()
时是不是会一直阻塞,那么就会一直占用线程,这也就说明了为什么BIO中通常一个连接就需要一个线程。