BIO(Blocking I/O),同步阻塞。是传统SocketIO的方式,它有两个阻塞点

  1. serverSocket.accept()
  2. socket.getInputStream().read(bytes);

BIO有两种情况:

  1. 单线程BIO:一次只能有一个客户端,一个客户处理完后,才能有下一个客户
  2. 多线程BIO:可以有多个客户端连接,但是非常消耗性能

    单线程BIO

    这是一份单线程BIO的示例代码

  3. 客户端代码

    1. public class Client{
    2. public static void main(String[] args) throws IOException{
    3. //创建连接
    4. Socket socket = new Socket("127.0.0.1", 9090);
    5. //从控制台中获取一行
    6. Scanner scanner = new Scanner(System.in);
    7. String s = scanner.nextLine();
    8. //把信息发送到服务器中
    9. socket.getOutputStream().write(s.getBytes());
    10. //关闭连接
    11. socket.close();
    12. }
    13. }
  4. 服务端代码

    1. public class Server{
    2. public static void main(String[] args) throws IOException{
    3. ServerSocket serverSocket = new ServerSocket(9090);
    4. while(true) {
    5. //等待客户端连接,注意这里是阻塞的
    6. //只有一个客户端连接时,才会继续下去
    7. Socket socket = serverSocket.accept(); //返回一个与客户连接的Socket对象socket
    8. //有一个客户端连接了
    9. System.out.println("有人连进来了");
    10. byte[] bytes = new byte[1024];
    11. //等待客户发消息,注意这里也是阻塞的
    12. socket.getInputStream().read(bytes);
    13. System.out.println("收到客户端的消息" + new String(bytes));
    14. }
    15. }
    16. }

服务器有两个阻塞的方法serverSocket.accept()socket.getInputStream().read(bytes);
这样肯定是不行的

  1. 当没有客户来连接时,服务器将一直卡在serverSocket.accept(),等待客户的连接,而不能做别的事情
  2. 如果客户A发送了Socket,和服务器进行通信,但它始终都不发送消息,程序就一直卡在socket.getInputStream().read(bytes);。其他人再想来通信,都会失败(一次只能处理一个客户,只有当这个客户处理完成后,才能处理下一个客户)

总结:这种方法的最大问题是无法并发,效率太低。如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。

多线程BIO

可以用多线程的方式对单线程BIO进行改良:一个连接一个线程(connection per thread)
即当有客户端连接时,服务器端需为其单独分配一个线程

流程

  1. 服务器端启动一个SeverSocket
  2. 客户端启动Socket对服务器端发起通信,默认情况下服务器端需为每个客户端创建一个线程与之通讯
  3. 客户端发起请求后,先咨询服务器端是否有线程响应,如果没有则会等待或被拒绝
  4. 如果有线程响应,客户端线程会等待请求结束后,再继续执行

BIO同步阻塞 - 图1

代码

  1. 客户端代码

    1. //BIO-客户端
    2. public class BIOClient {
    3. public static void main(String[] args) throws IOException {
    4. //创建Socket
    5. Socket socket = new Socket("localhost", 6666); //服务器地址与端口
    6. OutputStream outputStream = socket.getOutputStream(); //获得输出流
    7. //从控制台获取输入
    8. Scanner scanner = new Scanner(System.in);
    9. while (scanner.hasNextLine()){
    10. String message = scanner.nextLine();
    11. if ("exit".equals(message)) {
    12. break;
    13. }
    14. outputStream.write(message.getBytes()); //将输入的内容发送到服务端
    15. }
    16. outputStream.close();
    17. //关闭socket
    18. socket.close();
    19. }
    20. }
  2. 服务端代码

    1. //BIO-服务器端
    2. public class BIOSever {
    3. public static void main(String[] args) throws IOException {
    4. //线程池
    5. ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    6. //服务器Socket
    7. ServerSocket serverSocket = new ServerSocket(6666);
    8. System.out.println("服务器已启动");
    9. //不断轮询
    10. while (true){
    11. //等待客户端连接,注意这里是阻塞的
    12. //只有一个客户端连接时,才会继续下去
    13. System.out.println("等待客户端连接.....(阻塞中)");
    14. Socket socket = serverSocket.accept(); //返回一个与客户连接的Socket对象socket
    15. //有一个客户端连接了,新增一个线程,处理这个连接
    16. System.out.println("客户端连接");
    17. cachedThreadPool.execute(new Runnable() {
    18. public void run() {
    19. handler(socket);
    20. }
    21. });
    22. }
    23. }
    24. //从客服端socket读取数据
    25. public static void handler(Socket socket){
    26. try{
    27. InputStream inputStream = socket.getInputStream();
    28. byte[] b = new byte[1024];
    29. while (true){
    30. //等到客户输入信息,注意这里是阻塞的
    31. //只有当客户输入了一个信息之后,才会继续下去
    32. System.out.println("等待客户端输入.....(阻塞中)");
    33. int read = inputStream.read(b); //获得用户输入的内容
    34. if (read != -1){
    35. System.out.println(new String(b, 0, read));
    36. }else {
    37. break;
    38. }
    39. }
    40. inputStream.close();
    41. }catch (Exception e){
    42. e.printStackTrace();
    43. }finally {
    44. try {
    45. socket.close();
    46. } catch (IOException e) {
    47. e.printStackTrace();
    48. }
    49. }
    50. }
    51. }

评价

这个BIO的改良方法,支持了多并发

  1. 只适用于连接数目较小切固定的架构
  2. 适合短连接,一问一答的形式(收到问题,回答一下,立刻关闭)

对服务器资源的要求较高,不适合长连接

  1. 服务器端在监听客户端连接时(serverSocket.accept()),服务器端处于阻塞状态,不能处理其他事务
  2. 服务器端需要为每个客户端建立一个线程,虽然可以用线程池来优化,但在并发较大时,线程开销依旧很大
  3. 当连接的客户端没有发送数据时,服务器端会阻塞在read操作上,等待客户端输入,造成线程资源浪费

比喻:餐厅

image.png

  1. 整个服务端,就是一个餐厅
  2. 大门就是服务器的地址与端口
  3. ServerSocket是一个门卫,他负责看是否有客人进来。如果有客人进来,要为这个客人分配一个服务员
  4. 多线程BIO的做法就是:一个服务员只能服务一个客人,只有等这个客人走了,才能服务另外一个

如此,你会发现

  1. 如果你的客人吃了很久,你这个服务员一直都呆在那,不能服务其他人(线程堵塞,造成资源浪费)
  2. 如果客人多了起来,你就要有很多服务员,你的餐厅就很难盈利(数量一大,服务器撑不住)
  3. 如果客人只是过来拿个外卖,立刻就走(适合短连接)

回过头来,想想单线程BIO

  1. 只有一个员工,这个员工既当服务员,又当门卫
  2. 如果有一个客户进来,这个员工只能去服务它。外面的客户都进不来了,被堵在门口
  3. 只有当这个客户离开了,它才能接待下一个人

    参考文章

  4. 网络编程NIO:BIO和NIO

  5. 传统Socket分析
  6. 高性能IO之Reactor模式