IO模型之BIO代码详解及其优化演进
Posted jing99
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IO模型之BIO代码详解及其优化演进相关的知识,希望对你有一定的参考价值。
一、BIO简介
BIO是java1.4之前唯一的IO逻辑,在客户端通过socket向服务端传输数据,服务端监听端口。由于传统IO读数据的时候如果数据没有传达,IO会一直等待输入传入,所以当有请求过来的时候,新起一条线程对数据进行等待、处理,导致每一个链接都对应着服务器的一个线程。
BIO是同步阻塞的,如图所示:
二、原理简述
BIO对应linux io模型的阻塞IO,服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
重要组件
ServerSocket:负责绑定IP地址,启动监听端口
Socket:负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
三、手写服务端
首先我们来用ServerSocket和Socket写个服务端,并进行测试,代码以及注释如下:
public class BioserverSingle { //blocking public static void main(String[] args) { // 服务端开启一个端口进行监听 int port = 8080; ServerSocket serverSocket = null; //服务端 Socket socket; //客户端 InputStream in = null; OutputStream out = null; try { serverSocket = new ServerSocket(port); //通过构造函数创建ServerSocket,指定监听端口,如果端口合法且空闲,服务器就会监听成功 // 通过无限循环监听客户端连接,如果没有客户端接入,则会阻塞在accept操作 while (true) { System.out.println("Waiting for a new Socket to establish" + " ," + new Date().toString()); socket = serverSocket.accept();//阻塞 三次握手 in = socket.getInputStream(); byte[] buffer = new byte[1024]; int length = 0; while ((length = in.read(buffer)) > 0) {//阻塞 System.out.println("input is:" + new String(buffer, 0, length) + " ," + new Date().toString()); out = socket.getOutputStream(); out.write("success".getBytes()); System.out.println("Server end" + " ," + new Date().toString()); } } } catch (Exception e) { e.printStackTrace(); } finally { // 必要的清理活动 if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
执行代码main方法启动服务端,并查看堆栈信息,可以看到服务端阻塞在ServerSocket的accept方法,如截图:
此处即是ServerSocket的第一个阻塞点,接下来在控制台执行命令:
telnet localhost 8080
打印此时堆栈信息可以看到ServerSocket阻塞在read方法,如截图所示:
如果此时在控制台输入一些内容,那么服务端将接收到控制台输入,并会成功将服务端想要输出的的内容返回给控制台,如图所示:
如图所示,这样就完成了一次socket的连接与数据传输,但是这样的Socket并未关闭,你仍旧可以继续在控制台输入内容,然后服务端接收输入,因为这一次的Socket并未关闭,从哪里可以看出来呢?
如图所示:执行的代码并未重新等待一次新的Socket客户端请求,而且原先的Socket客户端(即控制台)仍旧可以继续同服务端进行输入输出交互,同时也可以打印堆栈信息进行确认,此时依旧阻塞在read方法而不是accept方法,此处不再截图。如果此时关闭客户端Socket(即控制台),那么此次Socket通信将结束,然后重新阻塞在Accept继续监听端口,等待新的客户端连接的到来。
因此可以确认ServerSocket两个阻塞点,分别是accept、read,accept阻塞等待新的socket通信建立请求,read阻塞等待客户端输入。
四、手写客户端
public class BioClient { public static void main(String[] args) { Socket clientSocket = null; OutputStream outputStream = null; InputStream inputStream = null; try{ // 新建一个Socket请求 clientSocket = new Socket("localhost",8080); System.out.println("Build the connection successfully!"+" ,"+new Date().toString()); outputStream = clientSocket.getOutputStream(); inputStream = clientSocket.getInputStream(); byte[] buffer = new byte[1024]; Scanner scanner = new Scanner(System.in); while(true){ System.out.println("Please input a String !"+" ,"+new Date().toString()); String string = scanner.nextLine(); if("end".equals(string)){ System.out.println("Client end"+" ,"+new Date().toString()); break; } outputStream.write(("This is a new request: "+string).getBytes()); int length = 0; if ((length = inputStream.read(buffer)) > 0) {//阻塞 } System.out.println("The response is:" + new String(buffer, 0, length)+" ,"+new Date().toString()); } }catch (Exception e){ } finally { if (clientSocket != null) { try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
启动main方法,如下截图(客户端和服务端):
客户端可以一直接受请求,直到输入end结束此次客户端输入,那么服务端能一直接受请求,然后给出答复,当客户端断开请求后,服务端重新恢复监听,等待新的socket请求的到来。
因此,在BIO通信模型中,Socket通信的发起以及结束都是客户端进行的。
五、多Socket客户端请求处理
上面进行的是1对1的Socket通信建立,如果已经有一个Socket请求在处理的时候,此时再来一个socket请求,那么会怎样呢,我们通过两个控制台简历请求进行测试。
如截图所示,客户端1和客户端建立Socket请求的通信并未报错,客户端①可以正常通信,但是客户端②的请求并未得到响应。
我们因此可以知道BIO是一对一应答模型,一个Socekt只能支持一个通信,如果一个客户端没有断开,当前线程会阻塞在读操作。要想实现同时处理多个通信,那么就必须建立多个Socket,因此此时服务端可以通过多线程,在socket请求到来时,单独为通信建立一个Socket进行通信,代码如下:
public class BioServerThread { public static void main(String[] args) { int port=8080; ServerSocket serverSocket=null; try{ serverSocket=new ServerSocket(port); Socket socket=null; while (true){ socket=serverSocket.accept();//拿到socket //连接量大的时候 会拖垮cpu new Thread(new SocketHandler(socket)).start(); } }catch (Exception e){ e.printStackTrace(); }finally { if (serverSocket!=null){ try { serverSocket.close(); serverSocket=null; }catch (IOException e){ e.printStackTrace(); } } } } }
public class SocketHandler implements Runnable { private Socket socket; public SocketHandler(Socket socket) { this.socket=socket; } public void run() { InputStream in = null; OutputStream out = null; try { in = socket.getInputStream(); byte[] buffer = new byte[1024]; int length = 0; while ((length = in.read(buffer)) > 0) {//阻塞 System.out.println("input is:" + new String(buffer, 0, length)); out = socket.getOutputStream(); out.write("success".getBytes()); System.out.println("end"); } } catch (Exception e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); }catch (Exception e){ e.printStackTrace(); } } if (socket!=null){ try { socket.close(); }catch (Exception e){ e.printStackTrace(); } } } } }
此时用两个控制台建立两个socket请求进行测试:
如图所示,客户端①和客户端②同时得到了服务端的返回,服务端成功完成了多请求的处理。
六、线程池管理Socket
虽然为了解决该问题可以引入多线程,实现伪异步IO,但是因为处理线程和客户端是1:1的关系,随着客户端请求增大,线程数随着上升,会极大的消耗cpu资源,引起服务器异常。为了保障服务器资源可以实现线程池,如果发生读取数据较慢时,大量并发的情况下,其他接入的客户都只能一直等待。
代码如下:
public class ServerHandlerExcutePool { private ExecutorService executor; public ServerHandlerExcutePool(int maxPoolSize, int queueSize){ executor=new ThreadPoolExecutor(2, maxPoolSize, 120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize)); } public void execute(Runnable task){ executor.execute(task); } }
public class BioServerThreadPool { public static void main(String[] args) { int port=8080; ServerSocket serverSocket=null; try{ //1000000 serverSocket=new ServerSocket(port); Socket socket=null; //100000 100000 ServerHandlerExcutePool excutePool=new ServerHandlerExcutePool(2,100); while (true){ socket=serverSocket.accept(); excutePool.execute(new SocketHandler(socket)); } }catch (Exception e){ e.printStackTrace(); }finally { if (serverSocket!=null){ try { serverSocket.close(); serverSocket=null; }catch (IOException e){ e.printStackTrace(); } } } } }
如截图所示,我设置线程池最多处理两个请求,那么此时第三个客户端的连接依旧无法处理,这样避免了创建大量的线程创建新的Socket,多余线程池大容量的请求只能阻塞。
综上所述,BIO的特点是缺乏弹性伸缩能力,在高并发这种大量请求来临时,并不具备强大的处理能力。
以上是关于IO模型之BIO代码详解及其优化演进的主要内容,如果未能解决你的问题,请参考以下文章
java网络编程系列之JavaIO的“前世”:BIO阻塞模型