Netty

Netty (网络编程框架)

Netty 是一个 基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。

应用场景

  1. 作为 RPC 框架的网络通信工具 : 我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC 框架了。不同服务指点的通信是如何做的呢?可以使用 Netty 来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!
  2. 实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
  3. 实现一个即时通讯系统 : 使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
    实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。

优点

  1. 统一的 API,支持多种传输类型,阻塞和非阻塞的。
  2. 简单而强大的线程模型。
  3. 自带编解码器解决 TCP 粘包/拆包问题。
  4. 自带各种协议栈。
  5. 真正的无连接数据包套接字支持。
  6. 比直接使用Java核心API有更高吞吐量、更低延迟、更低资源消耗和更少内存复制。
  7. 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
  8. 社区活跃成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等。

BIO

BIO

传统的阻塞式通信流程

早期的 Java 网络相关的API(java.net包)使用Socket(套接字)进行网络通信,不过只支持阻塞函数使用。

要通过互联网进行通信,至少需要一对套接字:

  1. 运行于服务器端的 Server Socket。
  2. 运行于客户机端的 Client Socket

Socket 网络通信过程简单来说分为下面 4 步:

  1. 建立服务端并且监听客户端请求
  2. 客户端请求,服务端和客户端建立连接
  3. 两端之间可以传递数据
  4. 关闭资源

服务器端:

  1. 创建 ServerSocket 对象并且绑定地址(ip)和端口号(port): server.bind(new InetSocketAddress(host, port))
  2. 通过 accept()方法监听客户端请求
  3. 连接建立后,通过输入流读取客户端发送的请求信息
  4. 通过输出流向客户端发送响应信息
  5. 关闭相关资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class HelloServer {
private static final Logger logger = LoggerFactory.getLogger(HelloServer.class);

public void start(int port) {
//1.创建 ServerSocket 对象并且绑定一个端口
try (ServerSocket server = new ServerSocket(port);) {
Socket socket;
//2.通过 accept()方法监听客户端请求, 这个方法会一直阻塞到有一个连接建立
while ((socket = server.accept()) != null) {
logger.info("client connected");
try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
//3.通过输入流读取客户端发送的请求信息
Message message = (Message) objectInputStream.readObject();
logger.info("server receive message:" + message.getContent());
message.setContent("new content");
//4.通过输出流向客户端发送响应信息
objectOutputStream.writeObject(message);
objectOutputStream.flush();
} catch (IOException | ClassNotFoundException e) {
logger.error("occur exception:", e);
}
}
} catch (IOException e) {
logger.error("occur IOException:", e);
}
}

public static void main(String[] args) {
HelloServer helloServer = new HelloServer();
helloServer.start(6666);
}
}

客户端:

  1. 创建Socket 对象并且连接指定的服务器的地址(ip)和端口号(port):socket.connect(inetSocketAddress)
  2. 连接建立后,通过输出流向服务器端发送请求信息
  3. 通过输入流获取服务器响应的信息
  4. 关闭相关资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HelloClient {

private static final Logger logger = LoggerFactory.getLogger(HelloClient.class);

public Object send(Message message, String host, int port) {
//1. 创建Socket对象并且指定服务器的地址和端口号
try (Socket socket = new Socket(host, port)) {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//2.通过输出流向服务器端发送请求信息
objectOutputStream.writeObject(message);
//3.通过输入流获取服务器响应的信息
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
return objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
logger.error("occur exception:", e);
}
return null;
}

public static void main(String[] args) {
HelloClient helloClient = new HelloClient();
helloClient.send(new Message("content from client"), "127.0.0.1", 6666);
System.out.println("client receive message:" + message.getContent());
}
}

发送的消息实体类:

1
2
3
4
5
6
@Data
@AllArgsConstructor
public class Message implements Serializable {

private String content;
}

很明显,我上面演示的代码片段有一个很严重的问题:只能同时处理一个客户端的连接,如果需要管理多个客户端的话,就需要为我们请求的客户端单独创建一个线程。

对应的 Java 代码可能是下面这样的:

1
2
3
new Thread(() -> {
// 创建 socket 连接
}).start();

但是,这样会导致一个很严重的问题:==资源浪费==。

我们知道线程是很宝贵的资源,如果我们为每一次连接都用一个线程处理的话,就会导致线程越来越好,最好达到了极限之后,就无法再创建线程处理请求了。处理的不好的话,甚至可能直接就宕机掉了。

很多人就会问了:那有没有改进的方法呢?

线程池虽可以改善,但终究未从根本解决问题
当然有! 比较简单并且实际的改进方法就是使用线程池。线程池还可以让线程的创建和回收成本相对较低,并且我们可以指定线程池的可创建线程的最大数量,这样就不会导致线程创建过多,机器资源被不合理消耗。

1
2
3
4
5
ThreadFactory threadFactory = Executors.defaultThreadFactory();
ExecutorService threadPool = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(100), threadFactory);
threadPool.execute(() -> {
// 创建 socket 连接
});

但是,即使你再怎么优化和改变。也改变不了它的底层仍然是同步阻塞的 BIO 模型的事实,因此无法从根本上解决问题。

为了解决上述的问题,Java 1.4 中引入了 NIO ,一种同步非阻塞的 I/O 模型。

IO比较

BIO,NIO和AIO 3者的区别

image

  • BIO (Blocking I/O):

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

  • NIO (Non-blocking/New I/O):

NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发

  • AIO (Asynchronous I/O):

在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。