BIO(Blocking I/O),同步阻塞。是传统SocketIO的方式,它有两个阻塞点
serverSocket.accept()socket.getInputStream().read(bytes);
BIO有两种情况:
- 单线程BIO:一次只能有一个客户端,一个客户处理完后,才能有下一个客户
-
单线程BIO
这是一份单线程BIO的示例代码
客户端代码
public class Client{public static void main(String[] args) throws IOException{//创建连接Socket socket = new Socket("127.0.0.1", 9090);//从控制台中获取一行Scanner scanner = new Scanner(System.in);String s = scanner.nextLine();//把信息发送到服务器中socket.getOutputStream().write(s.getBytes());//关闭连接socket.close();}}
服务端代码
public class Server{public static void main(String[] args) throws IOException{ServerSocket serverSocket = new ServerSocket(9090);while(true) {//等待客户端连接,注意这里是阻塞的//只有一个客户端连接时,才会继续下去Socket socket = serverSocket.accept(); //返回一个与客户连接的Socket对象socket//有一个客户端连接了System.out.println("有人连进来了");byte[] bytes = new byte[1024];//等待客户发消息,注意这里也是阻塞的socket.getInputStream().read(bytes);System.out.println("收到客户端的消息" + new String(bytes));}}}
服务器有两个阻塞的方法serverSocket.accept()、socket.getInputStream().read(bytes);。
这样肯定是不行的
- 当没有客户来连接时,服务器将一直卡在
serverSocket.accept(),等待客户的连接,而不能做别的事情 - 如果客户A发送了Socket,和服务器进行通信,但它始终都不发送消息,程序就一直卡在
socket.getInputStream().read(bytes);。其他人再想来通信,都会失败(一次只能处理一个客户,只有当这个客户处理完成后,才能处理下一个客户)
总结:这种方法的最大问题是无法并发,效率太低。如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。
多线程BIO
可以用多线程的方式对单线程BIO进行改良:一个连接一个线程(connection per thread)
即当有客户端连接时,服务器端需为其单独分配一个线程
流程
- 服务器端启动一个SeverSocket
- 客户端启动Socket对服务器端发起通信,默认情况下服务器端需为每个客户端创建一个线程与之通讯
- 客户端发起请求后,先咨询服务器端是否有线程响应,如果没有则会等待或被拒绝
- 如果有线程响应,客户端线程会等待请求结束后,再继续执行

代码
客户端代码
//BIO-客户端public class BIOClient {public static void main(String[] args) throws IOException {//创建SocketSocket socket = new Socket("localhost", 6666); //服务器地址与端口OutputStream outputStream = socket.getOutputStream(); //获得输出流//从控制台获取输入Scanner scanner = new Scanner(System.in);while (scanner.hasNextLine()){String message = scanner.nextLine();if ("exit".equals(message)) {break;}outputStream.write(message.getBytes()); //将输入的内容发送到服务端}outputStream.close();//关闭socketsocket.close();}}
服务端代码
//BIO-服务器端public class BIOSever {public static void main(String[] args) throws IOException {//线程池ExecutorService cachedThreadPool = Executors.newCachedThreadPool();//服务器SocketServerSocket serverSocket = new ServerSocket(6666);System.out.println("服务器已启动");//不断轮询while (true){//等待客户端连接,注意这里是阻塞的//只有一个客户端连接时,才会继续下去System.out.println("等待客户端连接.....(阻塞中)");Socket socket = serverSocket.accept(); //返回一个与客户连接的Socket对象socket//有一个客户端连接了,新增一个线程,处理这个连接System.out.println("客户端连接");cachedThreadPool.execute(new Runnable() {public void run() {handler(socket);}});}}//从客服端socket读取数据public static void handler(Socket socket){try{InputStream inputStream = socket.getInputStream();byte[] b = new byte[1024];while (true){//等到客户输入信息,注意这里是阻塞的//只有当客户输入了一个信息之后,才会继续下去System.out.println("等待客户端输入.....(阻塞中)");int read = inputStream.read(b); //获得用户输入的内容if (read != -1){System.out.println(new String(b, 0, read));}else {break;}}inputStream.close();}catch (Exception e){e.printStackTrace();}finally {try {socket.close();} catch (IOException e) {e.printStackTrace();}}}}
评价
这个BIO的改良方法,支持了多并发
- 只适用于连接数目较小切固定的架构
- 适合短连接,一问一答的形式(收到问题,回答一下,立刻关闭)
对服务器资源的要求较高,不适合长连接
- 服务器端在监听客户端连接时(
serverSocket.accept()),服务器端处于阻塞状态,不能处理其他事务 - 服务器端需要为每个客户端建立一个线程,虽然可以用线程池来优化,但在并发较大时,线程开销依旧很大
- 当连接的客户端没有发送数据时,服务器端会阻塞在read操作上,等待客户端输入,造成线程资源浪费
比喻:餐厅

- 整个服务端,就是一个餐厅
- 大门就是服务器的地址与端口
- ServerSocket是一个门卫,他负责看是否有客人进来。如果有客人进来,要为这个客人分配一个服务员
- 多线程BIO的做法就是:一个服务员只能服务一个客人,只有等这个客人走了,才能服务另外一个
如此,你会发现
- 如果你的客人吃了很久,你这个服务员一直都呆在那,不能服务其他人(线程堵塞,造成资源浪费)
- 如果客人多了起来,你就要有很多服务员,你的餐厅就很难盈利(数量一大,服务器撑不住)
- 如果客人只是过来拿个外卖,立刻就走(适合短连接)
回过头来,想想单线程BIO
- 只有一个员工,这个员工既当服务员,又当门卫
- 如果有一个客户进来,这个员工只能去服务它。外面的客户都进不来了,被堵在门口
-
参考文章
- 传统Socket分析
- 高性能IO之Reactor模式
