基于BIO的Java Socket通信详解
BIO,即阻塞IO,在基于Socket的消息通信过程中,Socket服务端向外部提供服务,而Socket客户端可以建立到Socket服务端的连接,进而发送请求数据,然后等待Socket服务端处理,并返回处理结果(响应)。
基于BIO的通信,Socket服务端会发生阻塞,即在监听过程中每次accept到一个客户端的Socket连接,就要处理这个请求,而此时其他连接过来的客户端只能阻塞等待。可见,这种模式下Socket服务端的处理能力是非常有限的,客户端也只能等待,直到服务端空闲时进行请求的处理。
BIO通信实现
下面基于BIO模式,来实现一个简单的Socket服务端与Socket客户端进行通信的逻辑,对这种通信方式有一个感性的认识。具体逻辑描述如下:
1、Socket客户端连接到Socket服务端,并发送数据“I am the client N.”;
2、Socket服务端,监听服务端口,并接收客户端请求数据,如果请求数据以“I am the client”开头,则响应客户端“I am the server, and you are the Nth client.”;
Socket服务端实现,代码如下所示:
package org.shirdrn.java.communications.bio; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * 基于BIO的Socket服务器端 * * @author shirdrn */ public class SimpleBioTcpServer extends Thread { /** 服务端口号 */ private int port = 8888; /** 为客户端分配编号 */ private static int sequence = 0; public SimpleBioTcpServer(int port) { this.port = port; } @Override public void run() { Socket socket = null; try { ServerSocket serverSocket = new ServerSocket(this.port); while(true) { socket = serverSocket.accept(); // 监听 this.handleMessage(socket); // 处理一个连接过来的客户端请求 } } catch (IOException e) { e.printStackTrace(); } } /** * 处理一个客户端socket连接 * @param socket 客户端socket * @throws IOException */ private void handleMessage(Socket socket) throws IOException { InputStream in = socket.getInputStream(); // 流:客户端->服务端(读) OutputStream out = socket.getOutputStream(); // 流:服务端->客户端(写) int receiveBytes; byte[] receiveBuffer = new byte[128]; String clientMessage = ""; if((receiveBytes=in.read(receiveBuffer))!=-1) { clientMessage = new String(receiveBuffer, 0, receiveBytes); if(clientMessage.startsWith("I am the client")) { String serverResponseWords = "I am the server, and you are the " + (++sequence) + "th client."; out.write(serverResponseWords.getBytes()); } } out.flush(); System.out.println("Server: receives clientMessage->" + clientMessage); } public static void main(String[] args) { SimpleBioTcpServer server = new SimpleBioTcpServer(1983); server.start(); } }
上述实现,没有进行复杂的异常处理。
Socket客户端实现,代码如下所示:
package org.shirdrn.java.communications.bio; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException; import java.util.Date; /** * 基于BIO的Socket客户端 * * @author shirdrn */ public class SimpleBioTcpClient { private String ipAddress; private int port; private static int pos = 0; public SimpleBioTcpClient() {} public SimpleBioTcpClient(String ipAddress, int port) { this.ipAddress = ipAddress; this.port = port; } /** * 连接Socket服务端,并模拟发送请求数据 * @param data 请求数据 */ public void send(byte[] data) { Socket socket = null; OutputStream out = null; InputStream in = null; try { socket = new Socket(this.ipAddress, this.port); // 连接 // 发送请求 out = socket.getOutputStream(); out.write(data); out.flush(); // 接收响应 in = socket.getInputStream(); int totalBytes = 0; int receiveBytes = 0; byte[] receiveBuffer = new byte[128]; if((receiveBytes=in.read(receiveBuffer))!=-1) { totalBytes += receiveBytes; } String serverMessage = new String(receiveBuffer, 0, receiveBytes); System.out.println("Client: receives serverMessage->" + serverMessage); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { try { // 发送请求并接收到响应,通信完成,关闭连接 out.close(); in.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { int n = 1; StringBuffer data = new StringBuffer(); Date start = new Date(); for(int i=0; i<n; i++) { data.delete(0, data.length()); data.append("I am the client ").append(++pos).append("."); SimpleBioTcpClient client = new SimpleBioTcpClient("localhost", 1983); client.send(data.toString().getBytes()); } Date end = new Date(); long cost = end.getTime() - start.getTime(); System.out.println(n + " requests cost " + cost + " ms."); } }
首先启动Socket服务端进程SimpleBioTcpServer,然后再运行Socket客户端SimpleBioTcpClient。可以看到,服务端接收到请求数据,然后响应客户端,客户端接收到了服务端的响应数据。
上述实现中,对于Socket客户端和服务端都是一次写入,并一次读出,而在实际中如果每次通信过程中数据量特别大的话,服务器端是不可能接受的,可以在确定客户端请求数据字节数的情况,循环来读取并进行处理。
另外,对于上述实现中流没有进行装饰(Wrapped)处理,在实际中会有性能的损失,如不能缓冲等。
对于Socket服务端接收数据,如果可以使多次循环读取到的字节数据通过一个可变长的字节缓冲区来存储,就能方便多了,可是使用ByteArrayOutputStream,例如:
ByteArrayOutputStream data = new ByteArrayOutputStream(); data.write(receiveBuffer, totalBytes , totalBytes + receiveBytes);
BIO通信测试
下面测试一下大量请求的场景下,Socket服务端处理的效率。
第一种方式:通过for循环来启动5000个Socket客户端,发送请求,代码如下所示:
public static void main(String[] args) { int n = 5000; StringBuffer data = new StringBuffer(); Date start = new Date(); for(int i=0; i<n; i++) { data.delete(0, data.length()); data.append("I am the client ").append(++pos).append("."); SimpleBioTcpClient client = new SimpleBioTcpClient("localhost", 1983); client.send(data.toString().getBytes()); } Date end = new Date(); long cost = end.getTime() - start.getTime(); System.out.println(n + " requests cost " + cost + " ms."); }
经过测试,大约需要9864ms,大概接近10s。
第二种方式:通过启动5000个独立的客户端线程,同时请求,服务端进行计数:
package org.shirdrn.java.communications.bio; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.util.Date; /** * 基于BIO的Socket通信测试 * * @author shirdrn */ public class SimpleBioTcpTest { static int threadCount = 5000; /** * 基于BIO的Socket服务端进程 * * @author shirdrn */ static class SocketServer extends Thread { /** 服务端口号 */ private int port = 8888; /** 为客户端分配编号 */ private static int sequence = 0; public SocketServer(int port) { this.port = port; } @Override public void run() { Socket socket = null; int counter = 0; try { ServerSocket serverSocket = new ServerSocket(this.port); boolean flag = false; Date start = null; while(true) { socket = serverSocket.accept(); // 监听 // 有请求到来才开始计时 if(!flag) { start = new Date(); flag = true; } this.handleMessage(socket); // 处理一个连接过来的客户端请求 if(++counter==threadCount) { Date end = new Date(); long last = end.getTime() - start.getTime(); System.out.println(threadCount + " requests cost " + last + " ms."); } } } catch (IOException e) { e.printStackTrace(); } } /** * 处理一个客户端socket连接 * @param socket 客户端socket * @throws IOException */ private void handleMessage(Socket socket) throws IOException { InputStream in = socket.getInputStream(); // 流:客户端->服务端(读) OutputStream out = socket.getOutputStream(); // 流:服务端->客户端(写) int receiveBytes; byte[] receiveBuffer = new byte[128]; String clientMessage = ""; if((receiveBytes=in.read(receiveBuffer))!=-1) { clientMessage = new String(receiveBuffer, 0, receiveBytes); if(clientMessage.startsWith("I am the client")) { String serverResponseWords = "I am the server, and you are the " + (++sequence) + "th client."; out.write(serverResponseWords.getBytes()); } } out.flush(); System.out.println("Server: receives clientMessage->" + clientMessage); } } /** * 基于BIO的Socket客户端线程 * * @author shirdrn */ static class SocketClient implements Runnable { private String ipAddress; private int port; /** 待发送的请求数据 */ private String data; public SocketClient(String ipAddress, int port) { this.ipAddress = ipAddress; this.port = port; } @Override public void run() { this.send(); } /** * 连接Socket服务端,并模拟发送请求数据 */ public void send() { Socket socket = null; OutputStream out = null; InputStream in = null; try { socket = new Socket(this.ipAddress, this.port); // 连接 // 发送请求 out = socket.getOutputStream(); out.write(data.getBytes()); out.flush(); // 接收响应 in = socket.getInputStream(); int totalBytes = 0; int receiveBytes = 0; byte[] receiveBuffer = new byte[128]; if((receiveBytes=in.read(receiveBuffer))!=-1) { totalBytes += receiveBytes; } String serverMessage = new String(receiveBuffer, 0, receiveBytes); System.out.println("Client: receives serverMessage->" + serverMessage); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { try { // 发送请求并接收到响应,通信完成,关闭连接 out.close(); in.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public void setData(String data) { this.data = data; } } public static void main(String[] args) throws Exception { SocketServer server = new SocketServer(1983); server.start(); Thread.sleep(3000); for(int i=0; i<threadCount; i++) { SocketClient client = new SocketClient("localhost", 1983); client.setData("I am the client " + (i+1) + "."); new Thread(client).start(); Thread.sleep(0, 1); } } }
经过测试,大约需要7110ms,大概接近7s,没有太大提高。
BIO通信改进
通过上面的测试我们可以发现,在Socket服务端对来自客户端的请求进行处理时,会发生阻塞,严重地影响了能够并发处理请求的效率。实际上,在Socket服务端接收来自客户端连接能力的范围内,可以将接收请求独立出来,从而在将处理请求独立粗话来,通过一个请求一个线程处理的方式来解决上述问题。这样,服务端是多处理线程对应客户端多请求,处理效率有一定程度的提高。
下面,通过单线程接收请求,然后委派线程池进行多线程并发处理请求:
/** * 基于BIO的Socket服务端进程 * * @author shirdrn */ static class SocketServer extends Thread { /** 服务端口号 */ private int port = 8888; /** 为客户端分配编号 */ private static int sequence = 0; /** 处理客户端请求的线程池 */ private ExecutorService pool; public SocketServer(int port, int poolSize) { this.port = port; this.pool = Executors.newFixedThreadPool(poolSize); } @Override public void run() { Socket socket = null; int counter = 0; try { ServerSocket serverSocket = new ServerSocket(this.port); boolean flag = false; Date start = null; while(true) { socket = serverSocket.accept(); // 监听 // 有请求到来才开始计时 if(!flag) { start = new Date(); flag = true; } // 将客户端请求放入线程池处理 pool.execute(new RequestHandler(socket)); if(++counter==threadCount) { Date end = new Date(); long last = end.getTime() - start.getTime(); System.out.println(threadCount + " requests cost " + last + " ms."); } } } catch (IOException e) { e.printStackTrace(); } } /** * 客户端请求处理线程类 * * @author shirdrn */ class RequestHandler implements Runnable { private Socket socket; public RequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { InputStream in = socket.getInputStream(); // 流:客户端->服务端(读) OutputStream out = socket.getOutputStream(); // 流:服务端->客户端(写) int receiveBytes; byte[] receiveBuffer = new byte[128]; String clientMessage = ""; if((receiveBytes=in.read(receiveBuffer))!=-1) { clientMessage = new String(receiveBuffer, 0, receiveBytes); if(clientMessage.startsWith("I am the client")) { String serverResponseWords = "I am the server, and you are the " + (++sequence) + "th client."; out.write(serverResponseWords.getBytes()); } } out.flush(); System.out.println("Server: receives clientMessage->" + clientMessage); } catch (IOException e) { e.printStackTrace(); } } } }
可见,这种改进方式增强服务端处理请求的并发度,但是每一个请求都要由一个线程去处理,大量请求造成服务端启动大量进程进行处理,也是比较占用服务端资源的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
相关文章
java IO流 之 输入流 InputString()的使用
这篇文章主要介绍了java IO流 之 输入流 InputString()的使用,以及读取数据的三种方式详解,非常不错,需要的朋友可以参考下2016-12-12一文搞懂接口参数签名与验签(附含java python php版)
这篇文章主要为大家介绍了java python php不同版的接口参数签名与验签示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-06-06java使用Process调用exe程序及Process.waitFor()死锁问题解决
在编写Java程序时,有时候我们需要调用其他的诸如exe,shell这样的程序或脚本,下面这篇文章主要给大家介绍了关于java使用Process调用exe程序及Process.waitFor()死锁问题解决的相关资料,需要的朋友可以参考下2022-12-12当面试官问我ArrayList和LinkedList哪个更占空间时,我是这么答的(面试官必问)
今天介绍一下Java的两个集合类,ArrayList和LinkedList,这两个集合的知识点几乎可以说面试必问的。感兴趣的朋友跟随小编一起看看吧2020-08-08
最新评论