共计 6719 个字符,预计需要花费 17 分钟才能阅读完成。
大家好,我是 「后端技术进阶」 作者,一个酷爱技术的少年。
@[toc]
感觉不错的话,欢送 star!ღ(´・ᴗ・`)比心
- Netty 从入门到实战系列文章地址:https://github.com/Snailclimb/netty-practical-tutorial。
- RPC 框架源码地址:https://github.com/Snailclimb/guide-rpc-framework
老套路,学习某一门技术或者框架的时候,第一步当然是要理解上面这几样货色。
- 是什么?
- 有哪些特点?
- 有哪些利用场景?
- 有哪些胜利应用的案例?
- …..
为了让你更好地理解 Netty 以及它诞生的起因,先从传统的网络编程说起吧!
还是要从 BIO 说起
传统的阻塞式通信流程
晚期的 Java 网络相干的 API(java.net
包) 应用 Socket(套接字)进行网络通信,不过只反对阻塞函数应用。
要通过互联网进行通信,至多须要一对套接字:
- 运行于服务器端的 Server Socket。
- 运行于客户机端的 Client Socket
Socket 网络通信过程如下图所示:
https://www.javatpoint.com/so…
Socket 网络通信过程简略来说分为上面 4 步:
- 建设服务端并且监听客户端申请
- 客户端申请,服务端和客户端建设连贯
- 两端之间能够传递数据
- 敞开资源
对应到服务端和客户端的话,是上面这样的。
服务器端:
- 创立
ServerSocket
对象并且绑定地址(ip)和端口号(port):server.bind(new InetSocketAddress(host, port))
- 通过
accept()
办法监听客户端申请 - 连贯建设后,通过输出流读取客户端发送的申请信息
- 通过输入流向客户端发送响应信息
- 敞开相干资源
客户端:
- 创立
Socket
对象并且连贯指定的服务器的地址(ip)和端口号(port):socket.connect(inetSocketAddress)
- 连贯建设后,通过输入流向服务器端发送申请信息
- 通过输出流获取服务器响应的信息
- 敞开相干资源
一个简略的 demo
为了便于了解,我写了一个简略的代码帮忙各位小伙伴了解。
服务端:
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);
}
}
ServerSocket
的 accept()
办法是阻塞办法,也就是说 ServerSocket
在调用 accept()
期待客户端的连贯申请时会阻塞,直到收到客户端发送的连贯申请才会持续往下执行代码,因而咱们须要要为每个 Socket 连贯开启一个线程(能够通过线程池来做)。
上述服务端的代码只是为了演示,并没有思考多个客户端连贯并发的状况。
客户端:
/**
* @author shuang.kou
* @createTime 2020 年 05 月 11 日 16:56:00
*/
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());
}
}
发送的音讯实体类:
/**
* @author shuang.kou
* @createTime 2020 年 05 月 11 日 17:02:00
*/
@Data
@AllArgsConstructor
public class Message implements Serializable {private String content;}
首先运行服务端,而后再运行客户端,控制台输入如下:
服务端:
[main] INFO github.javaguide.socket.HelloServer - client connected
[main] INFO github.javaguide.socket.HelloServer - server receive message:content from client
客户端:
client receive message:new content
资源耗费重大的问题
很显著,我下面演示的代码片段有一个很重大的问题:只能同时解决一个客户端的连贯,如果须要治理多个客户端的话,就须要为咱们申请的客户端独自创立一个线程。 如下图所示:
对应的 Java 代码可能是上面这样的:
new Thread(() -> {// 创立 socket 连贯}).start();
然而,这样会导致一个很重大的问题:资源节约。
咱们晓得线程是很贵重的资源,如果咱们为每一次连贯都用一个线程解决的话,就会导致线程越来越好,最好达到了极限之后,就无奈再创立线程解决申请了。解决的不好的话,甚至可能间接就宕机掉了。
很多人就会问了:那有没有改良的办法呢?
线程池虽能够改善,但究竟未从基本解决问题
当然有!比较简单并且理论的改良办法就是应用线程池。线程池还能够让线程的创立和回收老本绝对较低,并且咱们能够指定线程池的可创立线程的最大数量,这样就不会导致线程创立过多,机器资源被不合理耗费。
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 模型。
再看 NIO
Netty 实际上就基于 Java NIO 技术封装欠缺之后失去一个高性能框架,相熟 NIO 的基本概念对于学习和更好地了解 Netty 还是很有必要的!
初识 NIO
NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio
包,提供了 Channel , Selector,Buffer 等形象。
NIO 中的 N 能够了解为 Non-blocking,曾经不在是 New 了(曾经进去很长时间了)。
NIO 反对面向缓冲 (Buffer) 的,基于通道 (Channel) 的 I/O 操作方法。
NIO 提供了与传统 BIO 模型中的 Socket
和 ServerSocket
绝对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现, 两种通道都反对阻塞和非阻塞两种模式:
- 阻塞模式 : 根本不会被应用到。应用起来就像传统的网络编程一样,比较简单,然而性能和可靠性都不好。对于低负载、低并发的应用程序,勉强能够用一下以晋升开发速率和更好的维护性
- 非阻塞模式:与阻塞模式正好相同,非阻塞模式对于高负载、高并发的(网络)利用来说十分敌对,然而编程麻烦,这个是大部分人诟病的中央。所以,也就导致了 Netty 的诞生。
NIO 外围组件解读
NIO 蕴含上面几个外围的组件:
- Channel
- Buffer
- Selector
- Selection Key
这些组件之间的关系是怎么的呢?
- NIO 应用 Channel(通道)和 Buffer(缓冲区)传输数据,数据总是从缓冲区写入通道,并从通道读取到缓冲区。在面向流的 I/O 中,能够将数据间接写入或者将数据间接读到 Stream 对象中。在 NIO 库中,所有数据都是通过 Buffer(缓冲区)解决的。Channel 能够看作是 Netty 的网络操作抽象类,对应于 JDK 底层的 Socket
- NIO 利用 Selector(选择器)来监督多个通道的对象,如数据达到,连贯关上等。因而,单线程能够监督多个通道中的数据。
- 当咱们将 Channel 注册到 Selector 中的时候, 会返回一个 Selection Key 对象, Selection Key 则示意了一个特定的通道对象和一个特定的选择器对象之间的注册关系。通过 Selection Key 咱们能够获取哪些 IO 事件曾经就绪了,并且能够通过其获取 Channel 并对其进行操作。
Selector(选择器,也能够了解为多路复用器)是 NIO(非阻塞 IO)实现的要害。它应用了事件告诉相干的 API 来实现抉择曾经就绪也就是可能进行 I/O 相干的操作的工作的能力。
简略来说,整个过程是这样的:
- 将 Channel 注册到 Selector 中。
- 调用 Selector 的
select()
办法,这个办法会阻塞; - 到注册在 Selector 中的某个 Channel 有新的 TCP 连贯或者可读写事件的话,这个 Channel 就会处于就绪状态,会被 Selector 轮询进去。
- 而后通过 SelectionKey 能够获取就绪 Channel 的汇合,进行后续的 I/O 操作。
NIO 为啥更好?
相比于传统的 BIO 模型来说,NIO 模型的最大改良是:
- 应用比拟少的线程便能够治理多个客户端的连贯,进步了并发量并且缩小的资源耗费(缩小了线程的上下文切换的开销)
- 在没有 I/O 操作相干的事件的时候,线程能够被安顿在其余工作下面,以让线程资源失去充分利用。
应用 NIO 编写代码太难了
一个应用 NIO 编写的 Server 端如下,能够看出还是整体还是比较复杂的,并且代码读起来不是很直观,并且还可能因为 NIO 自身会存在 Bug。
很少应用 NIO,很大状况下也是因为应用 NIO 来创立正确并且平安的应用程序的开发成本和保护老本都比拟大。所以,个别状况下咱们都会应用 Netty 这个比拟成熟的高性能框架来做(Apace Mina 与之相似,然而 Netty 应用的更多一点)。
重要角色 Netty 退场
简略用 3 点概括一下 Netty 吧!
- Netty 是一个基于 NIO 的 client-server(客户端服务器)框架,应用它能够疾速简略地开发网络应用程序。
- 它极大地简化并简化了 TCP 和 UDP 套接字服务器等网络编程, 并且性能以及安全性等很多方面甚至都要更好。
- 反对多种协定如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协定。
用官网的总结就是:Netty 胜利地找到了一种在不斗争可维护性和性能的状况下实现易于开发,性能,稳定性和灵活性的办法。
Netty 特点
依据官网的形容,咱们能够总结出上面一些特点:
- 对立的 API,反对多种传输类型,阻塞和非阻塞的。
- 简略而弱小的线程模型。
- 自带编解码器解决 TCP 粘包 / 拆包问题。
- 自带各种协定栈。
- 真正的无连贯数据包套接字反对。
- 比间接应用 Java 外围 API 有更高的吞吐量、更低的提早、更低的资源耗费和更少的内存复制。
- 安全性不错,有残缺的 SSL/TLS 以及 StartTLS 反对。
- 社区沉闷
- 成熟稳固,经验了大型项目的应用和考验,而且很多开源我的项目都应用到了 Netty 比方咱们常常接触的 Dubbo、RocketMQ 等等。
- ……
应用 Netty 能做什么?
这个应该是老铁们最关怀的一个问题了,凭借本人的理解,简略说一下,实践上 NIO 能够做的事件,应用 Netty 都能够做并且更好。Netty 次要用来做 网络通信 :
- 作为 RPC 框架的网络通信工具:咱们在分布式系统中,不同服务节点之间常常须要互相调用,这个时候就须要 RPC 框架了。不同服务指导的通信是如何做的呢?能够应用 Netty 来做。比方我调用另外一个节点的办法的话,至多是要让对方晓得我调用的是哪个类中的哪个办法以及相干参数吧!
- 实现一个本人的 HTTP 服务器:通过 Netty 咱们能够本人实现一个简略的 HTTP 服务器,这个大家应该不生疏。说到 HTTP 服务器的话,作为 Java 后端开发,咱们个别应用 Tomcat 比拟多。一个最根本的 HTTP 服务器可要以解决常见的 HTTP Method 的申请,比方 POST 申请、GET 申请等等。
- 实现一个即时通讯零碎:应用 Netty 咱们能够实现一个能够聊天相似微信的即时通讯零碎,这方面的开源我的项目还蛮多的,能够自行去 Github 找一找。
- 音讯推送零碎:市面上有很多音讯推送零碎都是基于 Netty 来做的。
- ……
哪些开源我的项目用到了 Netty?
咱们平时常常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
能够说大量的开源我的项目都用到了 Netty,所以把握 Netty 有助于你更好的应用这些开源我的项目并且让你有能力对其进行二次开发。
实际上还有很多很多优良的我的项目用到了 Netty,Netty 官网也做了统计,统计后果在这里:https://netty.io/wiki/related…。
后记
RPC 框架源码曾经开源了, 地址:https://github.com/Snailclimb…