Netty系列-为什么选择Netty?
前言
作为Netty的入门介绍篇,本篇主要通过Java IO网络编程相关概念引出Netty。Netty主要解决了什么问题,为什么网络通讯我们首选Netty呢。
Java网络编程
首先看下Java原生提供的网络编程组件,包括了最早JDK1.0版本就支持的Socket这种BIO(`阻塞`)编程模型;及JDK1.4实现的NIO(`非阻塞`)编程模型。它们有什么优缺点呢?
Java BIO
早起操作系统只支持本地系统套接字库提供的阻塞函数。这种数据传输模型使用的叫BIO编程模型,普通示例如下:
服务端:
/**
* 单线程的服务器
*/
@Slf4j
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(82);
try {
while (true) {
// 阻塞接收连接
Socket socket = serverSocket.accept();
// 接收信息
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
int port = socket.getPort();
int maxLen = 2048;
byte[] context = new byte[maxLen];
// 阻塞获取数据
int readLen = inputStream.read(context, 0, maxLen);
String message = new String(context, 0, readLen);
// 打印
log.info("接收来自端口:{} 的信息:{}", port, message);
// 发送数据
outputStream.write("回应客户端请求".getBytes(StandardCharsets.UTF_8));
//关闭
outputStream.close();
inputStream.close();
socket.close();
}
} catch (Exception e) {
log.error("服务端错误", e);
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
}
}
客户端:
/**
* 客户端socket线程
*/
@Slf4j
public class SocketClient {
public static void main(String[] args) {
Socket socket;
OutputStream out = null;
InputStream in = null;
try {
socket = new Socket("localhost", 82);
out = socket.getOutputStream();
in = socket.getInputStream();
out.write(("客户端请求").getBytes(StandardCharsets.UTF_8));
out.flush();
// 等待服务器返回
log.info("客户端请求发送完成, 等待返回");
int maxLen = 1024;
byte[] context = new byte[maxLen];
int realLen;
String message = "";
// 开始读取
while ((realLen = in.read(context, 0, maxLen)) != -1) {
message += new String(context, 0, realLen);
}
log.info("客户端收到的信息是:" + message);
} catch (Exception e) {
log.error("客户端异常:", e);
} finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (Exception e) {
log.error("客户端异常:", e);
}
}
}
}
运行:先启动服务端;再启动客户端。
该方式在接受连接请求和读写IO时都会导致阻塞。服务器在同一时间只能处理一个连接,如果需要同时处理多个连接,则需要使用多线程。
所以,它只适合并发量少的场景,对于并发量高的场景,线程和内存资源是有限的,该方案性能太差。
Java NIO
Java NIO是在JDK1.4引入的,源于操作系统支持非阻塞函数系统调用和事件通知API(对应着IO多路复用模型),具体大佬的文章参考>。普通示例如下:
服务器:
/**
* nio服务端
*/
@Slf4j
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
// server的socketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 注册selector,对OP_ACCEPT事件感兴趣
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// serverSocket
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress("127.0.0.1", 8888));
while (true) {
// 阻塞获取事件
int select = selector.select();
log.info("select count:{}", select);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 连接事件
if (selectionKey.isAcceptable()) {
ServerSocketChannel ssChannel = (ServerSocketChannel)selectionKey.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据,对OP_READ事件感兴趣
sChannel.register(selector, SelectionKey.OP_READ);
log.info("acceptChannel:{} hashCode:{}", sChannel, sChannel.hashCode());
} else if (selectionKey.isReadable()) {
// 可读
SocketChannel sChannel = (SocketChannel) selectionKey.channel();
log.info("readableChannel:{} hashCode:{}", sChannel, sChannel.hashCode());
String msg = readDataFromSocketChannel(sChannel);
log.info("msg:{}", msg);
// 不能close,否则无法再收到数据
// sChannel.close();
}
// 重要,这里移除的是selector内部的集合中的SelectionKey实例,对应已经处理过的io事件。
// 不移除的话,如果此次的SelectionKey出现,与之前的SelectionKey集合不全相同,会出错;但如果是全部相同的SelectionKey再次出现,会判重相等而不会报错
iterator.remove();
}
}
}
/**
* 读取通道的数据
* 正常需要数据解码,不然客户端发送多次,被一次读取完
*
* @param sChannel:socket通道
*/
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder sb = new StringBuilder();
buffer.clear();
int read = sChannel.read(buffer);
if (read <= 0) {
return "";
}
// 读取buffer
buffer.flip();
// 可读大小
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i= 0; i < limit; i++) {
dst[i] = (char)buffer.get(i);
}
sb.append(dst);
buffer.clear();
return sb.toString();
}
}
客户端:
/**
* 客户端,未使用Nio,直接Socket连接写入数据
*/
@Slf4j
public class NIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream outputStream = socket.getOutputStream();
// 通过consult获取输入
Scanner in = new Scanner(System.in);
while(true){
if(in.hasNextLine()) {
String line = in.nextLine();
outputStream.write(line.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
}
}
}
运行:先启动服务器代码;再启动客户端代码,在客户端输入控制台输入内容回车。
NIO引入了核心的组件包括:
通道 Channel
缓冲区 ByteBuffer
多路复用选择器 Selector
Java NIO解决了BIO的问题,可以使用较少的线程,同时监听多个IO事件;更少的线程也意味着更少的上下文的切换。
但是,在高负载下可靠及高效地处理和调度IO操作仍是一项繁琐且容易出错的任务。
为什么选择Netty
首先Netty被定义为:
是一款异步的事情驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。
Netty所支持的特性,包括如下:
设计方面
对NIO进行了抽象,简化了网络编程的复杂性。
使用方面
提供了简单的API和大量的示例集;也提供了很多超出标准的Java NIO的功能,如多种协议(Http、WebSocket)的支持、常用的编解码器支持。
性能方面
拥有更高的吞吐量及更低的延迟,得益于池化和复用,更少的内存复制;异步和事件驱动模型。
稳定性方面
强大且活跃的社区,很多大型公司在使用Netty,Facebook、Google等。
安全性
完整的SSL/TLS以及StartTLS的支持。
总结
Netty是一个高性能的网络通讯框架,对Java NIO进行抽象,基于异步和事件驱动模型,实现了更高的性能、更方便使用的API等等一系列特性。