免责声明:本人最近在研读《Netty实战》书籍,对于里面内容颇感兴趣,本文旨在于技术学习交流,不存在盈利性目的。
Java 网络编程
BIO:block input output
早期的网络编程开发人员,需要花费大量的时间去学习复杂的 C 语言套接字库,去处理它们在不同的操作系统上出现的古怪问题。早期的 Java API(java.net)只支持由本地系统套接字库提供的所谓的阻塞函数。Java创建一个复杂的客户端/服务器协议需要大量的样板代码(以及相当多的底层研究才能使它整个流畅地运行起来)。早期Java阻塞 I/O 示例代码:
ServerSocket serverSocket = new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
String request, response;
while ((request = in.readLine()) != null) {
if ("Done".equals(request)) {
break;
}
response = processRequest(request);
out.println(response);
}
这段代码片段将只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端Socket 创建一个新的 Thread,对于这种模式,主要存在以下几个问题:
第一,在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。
第二,需要为每个线程的调用栈都分配内存,其默认值大小区间为 64 KB 到 1 MB,具体取决于操作系统。
第三,即使 Java 虚拟机(JVM)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的开销就会带来麻烦,例如,在达到 10 000 个连接的时候。虽然这种并发方案对于支撑中小数量的客户端来说还算可以接受,但是为了支撑 100 000 或者更多的并发连接所需要的资源使得它很不理想。
这就是JAVA中典型的BIO模型,模型结构图如下所示:

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
NIO:Non-blocking input output
NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
新的还是非阻塞的
NIO 最开始是新的输入/输出(New Input/Output)的英文缩写,但是,该Java API 已经出现足够长的时间了,不再是“新的”了,因此,如今大多数的用户认为NIO 代表非阻塞 I/O(Non-blocking I/O),而阻塞I/O(blocking I/O)是旧的输入/输出(old input/output,OIO)。你也可能遇到它被称为普通I/O(plain I/O)的时候。

Java NIO
的核心组件 包括:
- 通道(
Channel
) - 缓冲区(
Buffer
) - 选择器(
Selectors
)

NIO具体实现代码示例:
基于通道 & 缓冲数据
// 1. 获取数据源 和 目标传输地的输入输出流(此处以数据源 = 文件为例)
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
// 2. 获取数据源的输入输出通道
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
// 3. 创建 缓冲区 对象:Buffer(共有2种方法)
// 方法1:使用allocate()静态方法
ByteBuffer buff = ByteBuffer.allocate(256);
// 上述方法创建1个容量为256字节的ByteBuffer
// 注:若发现创建的缓冲区容量太小,则重新创建一个大小合适的缓冲区
// 方法2:通过包装一个已有的数组来创建
// 注:通过包装的方法创建的缓冲区保留了被包装数组内保存的数据
ByteBuffer buff = ByteBuffer.wrap(byteArray);
// 额外:若需将1个字符串存入ByteBuffer,则如下
String sendString="你好,服务器. ";
ByteBuffer sendBuff = ByteBuffer.wrap(sendString.getBytes("UTF-16"));
// 4. 从通道读取数据 & 写入到缓冲区
// 注:若 以读取到该通道数据的末尾,则返回-1
fcin.read(buff);
// 5. 传出数据准备:将缓存区的写模式 转换->> 读模式
buff.flip();
// 6. 从 Buffer 中读取数据 & 传出数据到通道
fcout.write(buff);
// 7. 重置缓冲区
// 目的:重用现在的缓冲区,即 不必为了每次读写都创建新的缓冲区,在再次读取之前要重置缓冲区
// 注:不会改变缓冲区的数据,只是重置缓冲区的主要索引值
buff.clear();
基于选择器(Selecter)
// 1. 创建Selector对象
Selector sel = Selector.open();
// 2. 向Selector对象绑定通道
// a. 创建可选择通道,并配置为非阻塞模式
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
// b. 绑定通道到指定端口
ServerSocket socket = server.socket();
InetSocketAddress address = new InetSocketAddress(port);
socket.bind(address);
// c. 向Selector中注册感兴趣的事件
server.register(sel, SelectionKey.OP_ACCEPT);
return sel;
// 3. 处理事件
try {
while(true) {
// 该调用会阻塞,直到至少有一个事件就绪、准备发生
selector.select();
// 一旦上述方法返回,线程就可以处理这些事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey) iter.next();
iter.remove();
process(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
class java.nio.channels.Selector 是Java 的非阻塞 I/O 实现的关键。它使用了事件通知 API以确定在一组非阻塞套接字中有哪些已经就绪能够进行 I/O 相关的操作。因为可以在任何的时间检查任意的读操作或者写操作的完成状态,个单一的线程便可以处理多个并发的接。
总体来看,与阻塞 I/O 模型相比,这种模型提供了更好的资源管理:
- 使用较少的线程便可以处理许多连接,因此也减少了内存管理和上下文切换所带来开销;
- 当没有 I/O 操作需要处理的时候,线程也可以被用于其他任务。

尽管已经有许多直接使用 Java NIO API 的应用程序被构建了,但是要做到如此正确和安全并不容易。特别是,在高负载下可靠和高效地处理和调度 I/O 操作是一项繁琐而且容易出错的任务,最好留给高性能的网络编程专家——Netty。
Netty 简介
Netty 是一款用于创建高性能网络应用程序的高级框架。是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。在网络编程领域,Netty是Java的卓越框架。它驾驭了Java高级API的能力,并将其隐藏在一个易于使用的API之后。Netty使你可以专注于自己真正感兴趣的——你的应用程序的独一无二的价值。

异步和事件驱动
异步也就是非同步,本质上,一个既是异步的又是事件驱动的系统会表现出一种特殊的、对我们来说极具价值的行为:它可以以任意的顺序响应在任意的时间点产生的事件。这种能力对于实现最高级别的可伸缩性至关重要,定义为:“一种系统、网络或者进程在需要处理的工作不断增长时,可以通过某种可行的方式或者扩大它的处理能力来适应这种增长的能力。”
同步交互:指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程;
异步交互:指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待。 区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
netty 是事件驱动的,这里面有两个含义,一是 netty 接收到 socket 数据后,会产生事件,事件在 pipeline 上传播,二是事件由特定的线程池处理。
异步和可伸缩性之间的联系又是什么呢?
- 非阻塞网络调用使得我们可以不必等待一个操作的完成。完全异步的 I/O 正是基于这个特性构建的,并且更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户。
- 选择器使得我们能够通过较少的线程便可监视许多连接上的事件。
将这些元素结合在一起,与使用阻塞 I/O 来处理大量事件相比,使用非阻塞 I/O 来处理更快速、更经济。从网络编程的角度来看,这是构建我们理想系统的关键,这也是Netty 的设计底蕴的关键。