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

第一篇 Java网络编程

作者: Souloose | 来源:发表于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网络编程(第四版) PDF 超清版

    《Java网络编程(第四版)》是一本关于Java编程相关的电子书资源,介绍了关于Java、网络编程、Java编程方...

  • Android应用开发:网络编程2

    网络编程 Java基础:网络编程 Uri、URL、UriMatcher、ContentUris详解 Android...

  • Http协议

    网络编程 Java基础:网络编程 Uri、URL、UriMatcher、ContentUris详解 Android...

  • Chapter 12 . 网络编程

    阅读原文 Chapter 12 . 网络编程 12.1网络编程概述 • Java是 Internet ...

  • 《Netty实战》读书笔记01——第一章

    第 1 章 笔记 Java 网络编程 早期Java的网络编程,需要学习很多C语言套接字(Socket)的知识,但...

  • Java NIO

    书本 Netty权威指南netty实战O’Reilly的《Java nio》Unix网络编程 《unix网络编程》...

  • IO编程

    一、Java网络编程 https://www.runoob.com/java/java-networking.ht...

  • 第一篇 Java网络编程

    本篇主要从学习角度整理java的几个网络模型,包括: BIO通信模型 伪异步通信模型 NIO通信模型 NIO2.0...

  • Java网络编程

    Java 网络编程 网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。 java.net...

  • Java 网络爬虫,就是这么的简单

    这是 Java 网络爬虫系列文章的第一篇,如果你还不知道 Java 网络爬虫系列文章,请参看学 Java 网络爬虫...

网友评论

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

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