网络编程三大模型之BIO模型的实现与原理

网络编程的基本模型是C/S模型,即两个进程间的通信。

服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。

传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

简单的描述一下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。

传统BIO通信模型图:


该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死掉了。

然后我们可以来看下我实现代码(注意看注释):

首先是server端的:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
- BIO服务端源码
- @version 1.0
  */
  public final class ServerNormal {
  //默认的端口号
  private static int DEFAULT_PORT = 12345;
  //单例的ServerSocket
  private static ServerSocket server;
  //根据传入参数设置监听端口,如果没有参数调用以下方法并使用默认值
  public static void start() throws IOException{
    //使用默认值
    start(DEFAULT_PORT);
  }
  //这个方法不会被大量并发访问,不太需要考虑效率,直接进行方法同步就行了
  public synchronized static void start(int port) throws IOException{
    if(server != null) return;
    try{
        //通过构造函数创建ServerSocket
    //如果端口合法且空闲,服务端就监听成功
        server = new ServerSocket(port);
        System.out.println("服务器已启动,端口号:" + port);
        //通过无线循环监听客户端连接
        //如果没有客户端接入,将阻塞在accept操作上。
        while(true){
        Socket socket = server.accept();
            //当有新的客户端接入时,会执行下面的代码
            //然后创建一个新的线程处理这条Socket链路
            new Thread(new ServerHandler(socket)).start();
        }
    }finally{
        //一些必要的清理工作
        if(server != null){
            System.out.println("服务器已关闭。");
            server.close();
            server = null;
        }
    }
  }
  }

客户端消息处理线程ServerHandler源码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import com.anxpp.io.utils.Calculator;
/**

- 客户端线程
- @author yangtao__anxpp.com
- 用于处理一个客户端的Socket链路
  */
  public class ServerHandler implements Runnable{
  private Socket socket;
  public ServerHandler(Socket socket) {
    this.socket = socket;
  }
  @Override
  public void run() {
    BufferedReader in = null;
    PrintWriter out = null;
    try{
    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new PrintWriter(socket.getOutputStream(),true);
        String expression;
        String result;
    while(true){
        //通过BufferedReader读取一行
    //如果已经读到输入流尾部,返回null,退出循环
            //如果得到非空值,就尝试计算结果并返回
            if((expression = in.readLine())==null) break;
            System.out.println("服务器收到消息:" + expression);
            try{
                result = Calculator.cal(expression).toString();
            }catch(Exception e){
            result = "计算错误:" + e.getMessage();
            }
            out.println(result);
        }
    }catch(Exception e){
  
    }finally{
        
        if(in != null){
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            in = null;
        }
        if(out != null){
            out.close();
            out = null;
        }
        if(socket != null){
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            socket = null;
        }
    }
  }
  }

测试代码,为了方便在控制台看输出结果,放到同一个程序(jvm)中运行:

import java.io.IOException;
import java.util.Random;
/**

- 测试方法
- @author yangtao__anxpp.com
- @version 1.0
*/
public class Test {
//测试主方法
public static void main(String[] args) throws InterruptedException {
  //运行服务器
  new Thread(new Runnable() {
      @Override
      public void run() {
          try {
              ServerBetter.start();
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
  }).start();
  //避免客户端先于服务器启动前执行代码
  Thread.sleep(100);
  //运行客户端 
  char operators[] = {'+','-','*','/'};
  Random random = new Random(System.currentTimeMillis());
  new Thread(new Runnable() {
      @SuppressWarnings("static-access")
      @Override
      public void run() {
          while(true){
              //随机产生算术表达式
              String expression = random.nextInt(10)+""+operators[random.nextInt(4)]+(random.nextInt(10)+1);
              Client.send(expression);
              try {
                  Thread.currentThread().sleep(random.nextInt(1000));
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }).start();
}
}

运行结果:

服务器已启动,端口号:12345
算术表达式为:4-2
服务器收到消息:4-2
___结果为:2
算术表达式为:5-10
服务器收到消息:5-10
___结果为:-5
算术表达式为:0-9
服务器收到消息:0-9
___结果为:-9
算术表达式为:0+6
服务器收到消息:0+6
___结果为:6
算术表达式为:1/6
服务器收到消息:1/6
___结果为:0.16666666666666666

从以上代码,很容易看出,BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程来处理这条链路,在需要满足高性能、高并发的场景是没法应用的(大量创建新的线程会严重影响服务器性能,甚至罢工)。

伪异步I/O编程

为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程(需要了解更多请参考前面提供的文章),实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。
伪异步I/O模型图:


image.png

实现很简单,我们只需要将新建线程的地方,交给线程池管理即可,只需要改动刚刚的Server代码即可:

/**

- BIO服务端源码__伪异步I/O
- @author yangtao__anxpp.com
- @version 1.0
  */
  public final class ServerBetter {
  //默认的端口号
  private static int DEFAULT_PORT = 12345;
  //单例的ServerSocket
  private static ServerSocket server;
  //线程池 懒汉式的单例
  private static ExecutorService executorService = Executors.newFixedThreadPool(60);
  //根据传入参数设置监听端口,如果没有参数调用以下方法并使用默认值
  public static void start() throws IOException{
    //使用默认值
    start(DEFAULT_PORT);
  }
  //这个方法不会被大量并发访问,不太需要考虑效率,直接进行方法同步就行了
  public synchronized static void start(int port) throws IOException{
    if(server != null) return;
    try{
        //通过构造函数创建ServerSocket
        //如果端口合法且空闲,服务端就监听成功
        server = new ServerSocket(port);
        System.out.println("服务器已启动,端口号:" + port);
        //通过无线循环监听客户端连接
        //如果没有客户端接入,将阻塞在accept操作上。
        while(true){
        Socket socket = server.accept();
            //当有新的客户端接入时,会执行下面的代码
            //然后创建一个新的线程处理这条Socket链路
            executorService.execute(new ServerHandler(socket));
        }
    }finally{
        //一些必要的清理工作
        if(server != null){
            System.out.println("服务器已关闭。");
            server.close();
            server = null;
        }
    }
  }
  }

测试运行结果是一样的。

我们知道,如果使用CachedThreadPool线程池(不限制线程数量,如果不清楚请参考文首提供的文章),其实除了能自动帮我们管理线程(复用),看起来也就像是1:1的客户端:线程数模型,而使用FixedThreadPool我们就有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N:M的伪异步I/O模型。

但是,正因为限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。而对Socket的输入流就行读取时,会一直阻塞,直到发生:

  1. 有数据可读
    2.可用数据以及读取完毕
    3.发生空指针或I/O异常
    所以在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。

总结一下:

BIO模型是最早的jdk提供的一种处理网络连接请求的模型,是同步阻塞结构,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。

应用:

适用于请求量较小,相应比较快的场景。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容

  • 在学习NIO之前需要先整理清楚一些概念,以下都是我之前收集的一些笔记: 1.堵塞和非堵塞:堵塞和非堵塞是进程在访问...
    先生zeng阅读 1,211评论 0 1
  • 熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也...
    小王学java阅读 2,074评论 0 0
  • BIO 线程模型 在 JDK 1.4 推出 Java NIO 之前,基于 Java 的所有 Socket 通信都采...
    tracy_668阅读 7,283评论 0 7
  • 看到简友分享的水滴教程, 忍不住自己也试试。。 但好像失败了吖。。。 同学来句你这是画的细胞分裂? …… …… 下...
    乔伊乔一阅读 214评论 0 3
  • 翰墨雨静这个名字陪伴了我些许时光,今日鼓足勇气将其移除,去除的不仅仅是一个名称,而是一段回忆,关于我和她的回忆。翰...
    翰墨雨静阅读 186评论 0 1