JAVA阻塞IO(BIO)简介
一、BIO编程
传统的BIO编程
网络编程的基本模型是C/S模型,即两个进程间的通信。
服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
简单的描述一下BIO的服务端通信模型:
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答模型。
传统BIO通信模型图
二、代码示例
模拟20个客户端并发请求,服务器端使用单线程:
服务器端(SocketServer1)单个线程
package BIO; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer1 { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(83); try { while (true) { Socket socket = serverSocket.accept(); //下面我们收取信息 InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); Integer sourcePort = socket.getPort(); int maxLen = 2048; byte[] contextBytes = new byte[maxLen]; //这里也会被阻塞,直到有数据准备好 int realLen = in.read(contextBytes, 0, maxLen); //读取信息 String message = new String(contextBytes, 0, realLen); //下面打印信息 System.out.println("服务器收到来自于端口: " + sourcePort + "的信息: " + message); //下面开始发送信息 out.write("回发响应信息!".getBytes()); //关闭 out.close(); in.close(); socket.close(); } } catch (Exception e) { System.out.println(e.getMessage() + e); } finally { serverSocket.close(); } } }
客户端代码(SocketClientRequestThread模拟请求)
package BIO; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.CountDownLatch; public class SocketClientRequestThread implements Runnable { private CountDownLatch countDownLatch; /** * 这个线层的编号 * * @param countDownLatch */ private Integer clientIndex; /** * countDownLatch是java提供的同步计数器。 * 当计数器数值减为0时,所有受其影响而等待的线程将会被激活。这样保证模拟并发请求的真实性 * * @param countDownLatch */ public SocketClientRequestThread(CountDownLatch countDownLatch, Integer clientIndex) { this.countDownLatch = countDownLatch; this.clientIndex = clientIndex; } @Override public void run() { Socket socket = null; OutputStream clientRequest = null; InputStream clientResponse = null; try { socket = new Socket("localhost", 83); clientRequest = socket.getOutputStream(); clientResponse = socket.getInputStream(); //等待,直到SocketClientDaemon完成所有线程的启动,然后所有线程一起发送请求 this.countDownLatch.await(); //发送请求信息 clientRequest.write(("这是第" + this.clientIndex + " 个客户端的请求。").getBytes()); clientRequest.flush(); //在这里等待,直到服务器返回信息 System.out.println("第" + this.clientIndex + "个客户端的请求发送完成,等待服务器返回信息"); int maxLen = 1024; byte[] contextBytes = new byte[maxLen]; int realLen; String message = ""; //程序执行到这里,会一直等待服务器返回信息(注意,前提是in和out都不能close,如果close了就收不到服务器的反馈了) while ((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) { message += new String(contextBytes, 0, realLen); } System.out.println("接收到来自服务器的信息:" + message); } catch (Exception e) { System.out.println(e.getMessage() + e); } finally { try { if (clientRequest != null) { clientRequest.close(); } if (clientResponse != null) { clientResponse.close(); } } catch (IOException e) { System.out.println(e.getMessage() + e); } } } }
客户端代码(SocketClientDaemon)
package BIO; import java.util.concurrent.CountDownLatch; public class SocketClientDaemon { public static void main(String[] args) throws Exception { Integer clientNumber = 20; CountDownLatch countDownLatch = new CountDownLatch(clientNumber); //分别开始启动这20个客户端 for(int index = 0 ; index < clientNumber ; index++ , countDownLatch.countDown()) { SocketClientRequestThread client = new SocketClientRequestThread(countDownLatch, index); new Thread(client).start(); } //这个wait不涉及到具体的实验逻辑,只是为了保证守护线程在启动所有线程后,进入等待状态 synchronized (SocketClientDaemon.class) { SocketClientDaemon.class.wait(); } } }
多线程来优化服务器端
客户端代码和上文一样,最主要是更改服务器端的代码:
服务器端多线程(SocketServer2)
package BIO; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer2 { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(83); try { while (true) { Socket socket = serverSocket.accept(); //当然业务处理过程可以交给一个线程(这里可以使用线程池),并且线程的创建是很耗资源的。 //最终改变不了.accept()只能一个一个接受socket的情况,并且被阻塞的情况 SocketServerThread socketServerThread = new SocketServerThread(socket); new Thread(socketServerThread).start(); } } catch (Exception e) { System.out.println(e.getMessage() + e); } finally { if (serverSocket != null) { serverSocket.close(); } } } } /** * 当然,接收到客户端的socket后,业务的处理过程可以交给一个线程来做。 * 但还是改变不了socket被一个一个的做accept()的情况。 * * @author yinwenjie */ class SocketServerThread implements Runnable { private Socket socket; public SocketServerThread(Socket socket) { this.socket = socket; } @Override public void run() { InputStream in = null; OutputStream out = null; try { //下面我们收取信息 in = socket.getInputStream(); out = socket.getOutputStream(); Integer sourcePort = socket.getPort(); int maxLen = 1024; byte[] contextBytes = new byte[maxLen]; //使用线程,同样无法解决read方法的阻塞问题, //也就是说read方法处同样会被阻塞,直到操作系统有数据准备好 int realLen = in.read(contextBytes, 0, maxLen); //读取信息 String message = new String(contextBytes, 0, realLen); //下面打印信息 System.out.println("服务器收到来自于端口: " + sourcePort + "的信息: " + message); //下面开始发送信息 out.write("回发响应信息!".getBytes()); } catch (Exception e) { System.out.println(e.getMessage() + e); } finally { //试图关闭 try { if (in != null) { in.close(); } if (out != null) { out.close(); } if (this.socket != null) { this.socket.close(); } } catch (IOException e) { System.out.println(e.getMessage() + e); } } } }
三、结论
模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终宕机或者假死。
参考: https://blog.csdn.net/yinwenjie/article/details/48274255
本文作者:MuXinu
本文链接:https://www.cnblogs.com/MuXinu/p/18091120
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!