美文网首页netty技术干货
第一篇 Java网络编程

第一篇 Java网络编程

作者: Flowaway | 来源:发表于2016-10-30 22:27 被阅读0次

    本篇主要从学习角度整理java的几个网络模型,包括:

    1. BIO通信模型
    2. 伪异步通信模型
    3. NIO通信模型
    4. NIO2.0(AIO)

    BIO通信模型

    BIO同步阻塞I/O通信模型

    BIO通信模型最大的特点是,当服务端程序收到一条网络连接请求时,需要单独为其分配一个处理线程,服务端处理完成之后,将输出流返回给客户端,此时才销毁线程。

    例如上图中的案例,acceptor在编程时一般就是ServerSocket,通过一个无限循环的accept操作获取客户端请求,然后分配一个线程为其进行处理,类似的代码如下:

    //BIO 服务端示例代码
    //其中SomeHandler为具体的网络业务处理器
    ServerSocket server = new ServerSocket(port);
    while(true){
      Socket socket = server.accept();
      new Thread(SomeHandler(socket)).start();
    }
    

    该模型最大的问题是缺乏弹性伸缩能力,客户端和服务端线程个数的比例为1:1,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀,系统性能也将急剧下降,随着并发访问量增大,系统会发生线程堆栈溢出,创建线程失败等问题。

    伪异步I/O通信模型

    伪异步I/O通信模型

    为了改进BIO的一个线程一个连接的模型,引入线程池或者消息队列来实现1个或者多个线程处理N个客户端的模型,但由于底层仍然使用同步阻塞I/O,因此被称为“伪异步”。

    服务端的示例代码如下:

    //伪异步网络通信服务端示例代码
    //其中SomeHandler为具体的网络业务处理器
    ServerSocket server = new ServerSocket(port);
    ExecutorService executor = Executors.newFixedThreadPool(100);
    while(true){
      Socket socket = server.accept();
      executor.submit(SomeHandler(socket));
    }
    

    最大的不同可以看出是在处理网络请求的地方,伪异步使用了线程池。这样可以避免线程的不断销毁和重新创建,但是本质上,一条连接任然独占一个线程,意思是如果一条连接不断开,这个线程将被一直阻塞,不管期间有没有数据传输。

    NIO编程模型

    NIO可以称为非阻塞I/O(Non-block I/O)。它提供了高速的、面向块的I/O。补充一下NIO的一些概念,以便作说明。

    缓冲区Buffer

    BIO编程中,数据的输入输出靠的是流。NIO通过Buffer来缓存操作期间的数据,相比之下,缓冲区提供了对数据的结构化访问。最常用的缓冲区是ByteBuffer,它提供了一组功能来操作byte数组。(事实上,每一种Java基本类型,除了Boolean,都有对应的一种缓冲区,例如CharBuffer、ShortBuffer等)。

    通道Channel

    理解通道就可以认为它像一条水管,网络数据可以在Channel上任意的写入和读取,它是双向的(区别于流的单向)。

    多路复用器

    NIO编程的基础就是多路复用器Selector,它提供选择已经就绪的任务的能力。通常在selector上会注册很多channel(来自于客户端的网络请求),selector通不过不断轮询,侦测哪一个channel上有数据的读写信号,就通过SelectionKey让该channel激活进行相应的读写操作。

    示例

    服务端时序图

    NIO服务端时序图

    上图描述了NIO编程过程中的通信时序图,异步的网络请求代码更不易编写,下面看看服务端的简单示例代码:

    
    private Selector selector;
    private ServerSocketChannel serverChannel;
    public void init(){
      selector = Selector.open();
      serverChannel =  ServerSocketChannel.open();
      serverChannel.configurateBlocking(false);
      serverChannel.socket.bind(port);
      serverChannel.register(selector, SelectionKey.OP_ACCEPT); 
    }
    
    public void run(){
      while(true){
        selector.select();
        Set<SelectionKey> keys = selector.selectedKeys();
        Iterator<SelectionKey> it = keys.iterator();
        SelectionKey key = null;
        while(it.hasNext()){
          key = it.next();
          it.remove();
          handleInput(key); //  网络请求处理器
        }
      }
    }
    

    简单的看这段代码,在初始化时需要执行的操作包括:

    1. open一个selector和ServerSocketChannel
    2. 设置非阻塞模式
    3. 绑定端口号
    4. 将channel注册到selector上,接受accept事件

    接着主循环要做的事情包括:

    1. 轮询select
    2. 从selector中获取触发了信号的SelectionKey
    3. 将SelectionKey交给网络请求处理器进行处理(处理器要完成的事情包括接受连接、接收数据,解码数据,写回数据等)

    客户端时序图

    NIO客户端时序图

    客户端的代码编写逻辑也很类似,基本的原理就是创建一个channel,将其注册到selector上,等待轮询信号。示例代码如下:

    private Selector selector;
    private SocketChannel socketChannel;
    public void init(){
      selector = Selector.open();
      socketChannel =  SocketChannel.open();
      socketChannel.configurateBlocking(false);
    }
    
    public void run(){
      doConnect();//执行连接服务器的操作
      while(true){
        selector.select();
        Set<SelectionKey> keys = selector.selectedKeys();
        Iterator<SelectionKey> it = keys.iterator();
        SelectionKey key = null;
        while(it.hasNext()){
          key = it.next();
          it.remove();
          handleInput(key); //  网络请求处理器
        }
      }
    }
    
    public void doConnect(){
      if(socketChannel.connect(port)){
        socketChannel.register(selector, SelectionKey.OP_READ);
      }else{
        socketChannel.register(selector,  SelectionKey.OP_CONNECT); 
      }
    }
    

    可以对比与服务端的代码,不同的地方包括:服务端会注册OP_ACCEPT事件,用于接受客户端的连接,客户端会注册OP_CONNECT事件,用于连接服务端;此外,他们使用的channel也不一样,服务端使用的是ServerSocketChannel,客户端使用的是SocketChannel。

    除了这两个特殊事件,他们还都能够注册OP_READ和OP_WRITE事件,用于网络数据的读和写。

    我在这里更偏向于网络模型的对比,因此网络数据的实际读写代码不在这里编写,需要注意的是,网络数据的读写需要程序员自己操作buffer对象,同时还要面对“半包读写问题”

    优势

    使用NIO编程的优势主要有:

    1. 客户端发起的连接是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
    2. SocketChannel的读写操作都是异步的,没有可读写的数据时,它不会同步等待,I/O通信线程就可以处理其他的连接。
    3. 线程模型得到优化,一个seletor线程可以同时处理成千上万条连接。

    AIO编程

    在JDK1.7以后升级了NIO类库,被称为NIO2.0。它提供了与UNIX网络编程事件驱动I/O相对应的AIO。AIO不需要通过多路复用器(selector)对注册的通道进行轮询操作即可实现异步读写,简化了NIO的编程模型。事实上,它传递的是一个信号变量。

    AIO编程的示例代码我这里就不再列举,接下来主要看一下,四种方式的对比。

    模型对比

    同步阻塞I/O(BIO) 伪异步I/O 非阻塞I/O(NIO) 异步I/O(AIO)
    客户端个数 1:1 M:N M:1 M:0
    I/O类型 阻塞I/O 阻塞I/O 非阻塞I/O 非阻塞I/O
    I/O类型 同步 同步 异步 异步
    API使用难度 简单 简单 非常复杂 复杂
    调试难度 简单 简单 复杂 复杂
    可靠性 非常差
    吞吐量

    虽然我们本系列主要是学习NIO框架Netty,但是并非意味着所有的Java网络编程都得用NIO和Netty。通过对比我们可以看出,BIO、伪异步I/O也有自己的优势:简单,因此根据业务应用场景,如果客户端并发数不多,服务器负载也低,那就完全可以考虑直接使用较为低级的网络编程模型。

    下一篇开始,我们真正开始学习netty。

    相关文章

      网友评论

        本文标题:第一篇 Java网络编程

        本文链接:https://www.haomeiwen.com/subject/zfmuuttx.html