怎么用netty做netty 发送数据的接口响

请问Netty适合做HTTP服务容器吗?
[问题点数:20分,结帖人luo112cn]
本版专家分:0
结帖率 90.91%
CSDN今日推荐
本版专家分:26577
本版专家分:0
结帖率 90.91%
本版专家分:3
本版专家分:0
结帖率 90.91%
本版专家分:26577
本版专家分:306
本版专家分:0
匿名用户不能发表回复!|
CSDN今日推荐自己用 Netty 实现一个简单的 RPC - 简书
自己用 Netty 实现一个简单的 RPC
创建 maven 项目,导入 Netty 4.1.16。
项目目录结构
提供者相关实现
消费者相关实现
源码地址:
众所周知,dubbo 底层使用了 Netty 作为网络通讯框架,而 Netty 的高性能我们之前也分析过源码,对他也算还是比较了解了。今天我们就自己用 Netty 实现一个简单的 RPC 框架。
模仿 dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty 4.1.16。
创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据。
1. 创建 maven 项目,导入 Netty 4.1.16。
&groupId&cn.thinkinjava&/groupId&
&artifactId&rpc-demo&/artifactId&
&version&1.0-SNAPSHOT&/version&
&dependencies&
&dependency&
&groupId&io.netty&/groupId&
&artifactId&netty-all&/artifactId&
&version&4.1.16.Final&/version&
&/dependency&
2. 项目目录结构如下:
3. 设计接口
一个简单的 hello world:
public interface HelloService {
String hello(String msg);
4. 提供者相关实现
4.1. 首先实现约定接口,用于返回客户端数据:
public class HelloServiceImpl implements HelloService {
public String hello(String msg) {
return msg != null ? msg + " -----& I am fine." : "I am fine.";
4.2. 实现 Netty 服务端和自定义 handler
启动 Netty Server 代码:
private static void startServer0(String hostName, int port) {
ServerBootstrap bootstrap = new ServerBootstrap();
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
bootstrap.group(eventLoopGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer&SocketChannel&() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new HelloServerHandler());
bootstrap.bind(hostName, port).sync();
} catch (InterruptedException e) {
e.printStackTrace();
上面的代码中添加了 String类型的编解码 handler,添加了一个自定义 handler。
自定义 handler 逻辑如下:
* 用于处理请求数据
public class HelloServerHandler extends ChannelInboundHandlerAdapter {
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 如何符合约定,则调用本地方法,返回数据
if (msg.toString().startsWith(ClientBootstrap.providerName)) {
String result = new HelloServiceImpl()
.hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
ctx.writeAndFlush(result);
这里显示判断了是否符合约定(并没有使用复杂的协议,只是一个字符串判断),然后创建一个具体实现类,并调用方法写回客户端。
还需要一个启动类:
public class ServerBootstrap {
public static void main(String[] args) {
NettyServer.startServer("localhost", 8088);
好,关于提供者的代码就写完了,主要就是创建一个 netty 服务端,实现一个自定义的 handler,自定义 handler 判断是否符合之间的约定(算是协议吧),如果符合,就创建一个接口的实现类,并调用他的方法返回字符串。
5. 消费者相关实现
消费者有一个需要注意的地方,就是调用需要透明,也就是说,框架使用者不用关心底层的网络实现。这里我们可以使用 JDK 的动态代理来实现这个目的。
思路:客户端调用代理方法,返回一个实现了 HelloService 接口的代理对象,调用代理对象的方法,返回结果。
我们需要在代理中做手脚,当调用代理方法的时候,我们需要初始化 Netty 客户端,还需要向服务端请求数据,并返回数据。
5.1. 首先创建代理相关的类
public class RpcConsumer {
private static ExecutorService executor = Executors
.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static HelloClientH
* 创建一个代理对象
public Object createProxy(final Class&?& serviceClass,
final String providerName) {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class&?&[]{serviceClass}, (proxy, method, args) -& {
if (client == null) {
initClient();
// 设置参数
client.setPara(providerName + args[0]);
return executor.submit(client).get();
* 初始化客户端
private static void initClient() {
client = new HelloClientHandler();
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer&SocketChannel&() {
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(client);
b.connect("localhost", 8088).sync();
} catch (InterruptedException e) {
e.printStackTrace();
该类有 2 个方法,创建代理和初始化客户端。
初始化客户端逻辑: 创建一个 Netty 的客户端,并连接提供者,并设置一个自定义 handler,和一些 String 类型的编解码器。
创建代理逻辑:使用 JDK 的动态代理技术,代理对象中的 invoke 方法实现如下:
如果 client 没有初始化,则初始化 client,这个 client 既是 handler ,也是一个 Callback。将参数设置进 client ,使用线程池调用 client 的 call 方法并阻塞等待数据返回。
看看 HelloClientHandler 的实现:
public class HelloClientHandler extends ChannelInboundHandlerAdapter implements Callable {
private ChannelHandlerC
public void channelActive(ChannelHandlerContext ctx) {
* 收到服务端数据,唤醒等待线程
public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) {
result = msg.toString();
* 写出数据,开始等待唤醒
public synchronized Object call() throws InterruptedException {
context.writeAndFlush(para);
void setPara(String para) {
this.para =
该类缓存了 ChannelHandlerContext,用于下次使用,有两个属性:返回结果和请求参数。
当成功连接后,缓存 ChannelHandlerContext,当调用 call 方法的时候,将请求参数发送到服务端,等待。当服务端收到并返回数据后,调用 channelRead 方法,将返回值赋值个 result,并唤醒等待在 call 方法上的线程。此时,代理对象返回数据。
再看看设计的测试类:
public class ClientBootstrap {
public static final String providerName = "HelloService#hello#";
public static void main(String[] args) throws InterruptedException {
RpcConsumer consumer = new RpcConsumer();
// 创建一个代理对象
HelloService service = (HelloService) consumer
.createProxy(HelloService.class, providerName);
for (; ; ) {
Thread.sleep(1000);
System.out.println(service.hello("are you ok ?"));
测试类首先创建了一个代理对象,然后每隔一秒钟调用代理的 hello 方法,并打印服务端返回的结果。
成功打印。
看了这么久的 Netty 源码,我们终于实现了一个自己的 Netty 应用,虽然这个应用很简单,甚至代码写的有些粗糙,但功能还是实现了,RPC 的目的就是允许像调用本地服务一样调用远程服务,需要对使用者透明,于是我们使用了动态代理。并使用 Netty 的 handler 发送数据和响应数据,完成了一次简单的 RPC 调用。
当然,还是那句话,代码比较简单,主要是思路,以及了解 RPC 底层的实现。
好吧。good luck!!!!
Java 程序员
个人网站:http://thinkinjava.cn.
GitHub:https://github.com/stateIs0
Stay hungry. Stay foolish.
pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 1. 面向对象都有哪些特性以及你对这些特性的理解 21 2. 访问权限修饰符public、private、protected, 以及不写(默认)时的区别(201...
Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线)。分布式系统的协调导致了样板模式, 使用Spring Cloud开发人员可以快速地支持实现这些模式的服务和应用程序。他们将在任何分布式...
该文章为转载,原文章请点击 1. 背景 1.1. Netty 3.X系列版本现状 根据对Netty社区部分用户的调查,结合Netty在其它开源项目中的使用情况,我们可以看出目前Netty商用的主流版本集中在3.X和4.X上,其中以Netty 3.X系列版本使用最为广泛。 N...
用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金Cover 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? ... Android 获取 View 宽高的常用正确方式,避免为零 - 掘金相信有很多朋友...
用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? ... Android 获取 View 宽高的常用正确方式,避免为零 - 掘金 相信有很多...
《zoo》是本短篇小说集,推理、悬疑、恐怖集于一身。 每个故事不长,但正如作者乙一所言,“每个短篇我都想得很久。” 11个故事,11种奇思妙想,耐人寻味。 《小饰与阳子》,双胞胎姐妹,生活待遇完全相反,如果非黑即白,那么姐姐过着生活的阴暗面,妹妹过着生活的阳光面,妈妈把爱都...
范范: 已经给蓁蓁和廖廖都写了信,接下来就轮到你了。 一开始,真的分不清你和谢雨蓁,那发型,真心鬼畜...... 我相信,那一定是你一生的痛。:) 军训的时候,对你完全没有印象。。。也不是完全吧,只记得在军训的最后一天,你讲起你奶奶的事,居然把我们整个宿舍感动的稀里哗啦的。...
ImageLoader加载图片的方法有两种:1.loadImage2.displayImage经过查看源码,发现其实两种加载方法最终是一样的,因为loadImage方法最终也是调用的displayImage方法。代码如下: 所以分析ImageLoader加载图片流程时,只需...
这个月业绩不达标,被扣了1000块工资,迟到被扣了500元。我的保底工资一个月2000,其他的全靠提成,这个月靠提成赚了2000块。算上被扣的1500,我到手只剩下2500元。付了房租2000元,我还剩500元。看着银行卡余额,我在床边抱着自己的膝盖痛哭。 我以为只要我努力...
超跑文化发展至今也离不开大家的支持,为了给用户提供足够的便利,我们的租车手续省之又省,不过好在,得到的用户反馈还不错。今天我们就分享下租赁超跑一般租车手续流程: 1、提供证件资料 为了确保豪车租赁的客户能够合法使用车辆,首先要向租赁公司提供本人的身份证、驾驶证等证件资料。如...| 作者 张松然张松然 关注3 他的粉丝 发布于 日. 估计阅读时间: 17 分钟 |QCon北京站9折倒计时,人工智能、区块链、大数据、架构等领域海内外先驱实践! 亲爱的读者:我们最近添加了一些个人消息定制功能,您只需选择感兴趣的技术主题,即可获取重要资讯的邮件和网页通知。京麦从 2014 年构建网关,从 HTTP 网关发展到 TCP 网关。在 2016 年重构完成基于 Netty4.x+Protobuf3.x 实现对接 PC 和 App 上下行通信的高可用、高性能、高稳定的 TCP 长连接网关。本文重点介绍京麦 TCP 网关的背景、架构及 Netty 的应用实践。早期京麦搭建 HTTP 和 TCP 长连接功能主要用于消息通知的推送,并未应用于 API 网关。随着逐步对 NIO 的深入学习和对 Netty 框架的了解,以及对系统通信稳定能力越来越高的要求,开始有了采用 NIO 技术应用网关实现 API 请求调用的想法,最终在 2016 年实现,并完全支撑业务化运行。由于诸多的改进,包括 TCP 长连接容器、Protobuf 的序列化、服务泛化调用框架等等,性能比 HTTP 网关提升 10 倍以上,稳定性也远远高于 HTTP 网关。基于 Netty 构建京麦 TCP 网关的长连接容器,作为网关接入层提供服务 API 请求调用。客户端通过域名 + 端口访问 TCP 网关,域名不同的运营商对应不同的 VIP,VIP 发布在 LVS 上,LVS 将请求转发给后端的 HAProxy,再由 HAProxy 把请求转发给后端的 Netty 的 IP+Port。LVS 转发给后端的 HAProxy,请求经过 LVS,但是响应是 HAProxy 直接反馈给客户端的,这也就是 LVS 的 DR 模式。TCP 网关的核心组件是 Netty,而 Netty 的 NIO 模型是 Reactor 反应堆模型(Reactor 相当于有分发功能的多路复用器 Selector)。每一个连接对应一个 Channel(多路指多个 Channel,复用指多个连接复用了一个线程或少量线程,在 Netty 指 EventLoop),一个 Channel 对应唯一的 ChannelPipeline,多个 Handler 串行的加入到 Pipeline 中,每个 Handler 关联唯一的 ChannelHandlerContext。TCP 网关长连接容器的 Handler 就是放在 Pipeline 的中。我们知道 TCP 属于 OSI 的传输层,所以建立 Session 管理机制构建会话层来提供应用层服务,可以极大的降低系统复杂度。所以,每一个 Channel 对应一个 Connection,一个 Connection 又对应一个 Session,Session 由 Session Manager 管理,Session 与 Connection 是一一对应的,Connection 保存着 ChannelHandlerContext(ChannelHanderContext 可以找到 Channel),Session 通过心跳机制来保持 Channel 的 Active 状态。每一次 Session 的会话请求(ChannelRead)都是通过 Proxy 代理机制调用 Service 层,数据请求完毕后通过写入 ChannelHandlerConext 再传送到 Channel 中。数据下行主动推送也是如此,通过 Session Manager 找到 Active 的 Session,轮询写入 Session 中的 ChannelHandlerContext,就可以实现广播或点对点的数据推送逻辑。京麦 TCP 网关使用 Netty Channel 进行数据通信,使用 Protobuf 进行序列化和反序列化,每个请求都将被封装成 Byte 二进制字节流,在整个生命周期中,Channel 保持长连接,而不是每次调用都重新创建 Channel,达到链接的复用。创建 ServerBootstrap,设定 BossGroup 与 WorkerGroup 线程池。bind 指定的 port,开始侦听和接受客户端链接。(如果系统只有一个服务端 port 需要监听,则 BossGroup 线程组线程数设置为 1。)在 ChannelPipeline 注册 childHandler,用来处理客户端链接中的请求帧。TCP 网关使用 Netty 的线程池,共三组线程池,分别为 BossGroup、WorkerGroup 和 ExecutorGroup。其中,BossGroup 用于接收客户端的 TCP 连接,WorkerGroup 用于处理 I/O、执行系统 Task 和定时任务,ExecutorGroup 用于处理网关业务加解密、限流、路由,及将请求转发给后端的抓取服务等业务操作。NioEventLoop 是 Netty 的 Reactor 线程,其角色:Boss Group:作为服务端 Acceptor 线程,用于 accept 客户端链接,并转发给 WorkerGroup 中的线程。Worker Group:作为 IO 线程,负责 IO 的读写,从 SocketChannel 中读取报文或向 SocketChannel 写入报文。Task Queue/Delay Task Queue:作为定时任务线程,执行定时任务,例如链路空闲检测和发送心跳消息等。其中步骤一至步骤九是 Netty 服务端的创建时序,步骤十至步骤十三是 TCP 网关容器创建的时序。步骤一:创建 ServerBootstrap 实例,ServerBootstrap 是 Netty 服务端的启动辅助类。步骤二:设置并绑定 Reactor 线程池,EventLoopGroup 是 Netty 的 Reactor 线程池,EventLoop 负责所有注册到本线程的 Channel。步骤三:设置并绑定服务器 Channel,Netty Server 需要创建 NioServerSocketChannel 对象。步骤四:TCP 链接建立时创建 ChannelPipeline,ChannelPipeline 本质上是一个负责和执行 ChannelHandler 的职责链。步骤五:添加并设置 ChannelHandler,ChannelHandler 串行的加入 ChannelPipeline 中。步骤六:绑定监听端口并启动服务端,将 NioServerSocketChannel 注册到 Selector 上。步骤七:Selector 轮训,由 EventLoop 负责调度和执行 Selector 轮询操作。步骤八:执行网络请求事件通知,轮询准备就绪的 Channel,由 EventLoop 执行 ChannelPipeline。步骤九:执行 Netty 系统和业务 ChannelHandler,依次调度并执行 ChannelPipeline 的 ChannelHandler。步骤十:通过 Proxy 代理调用后端服务,ChannelRead 事件后,通过发射调度后端 Service。步骤十一:创建 Session,Session 与 Connection 是相互依赖关系。步骤十二:创建 Connection,Connection 保存 ChannelHandlerContext。步骤十三:添加 SessionListener,SessionListener 监听 SessionCreate 和 SessionDestory 等事件。Session 是客户端与服务端建立的一次会话链接,会话信息中保存着 SessionId、连接创建时间、上次访问事件,以及 Connection 和 SessionListener,在 Connection 中保存了 Netty 的 ChannelHandlerContext 上下文信息。Session 会话信息会保存在 SessionManager 内存管理器中。通过源码分析,如果 Session 已经存在销毁 Session,但是这个需要特别注意,创建 Session 一定不要创建那些断线重连的 Channel,否则会出现 Channel 被误销毁的问题。因为如果在已经建立 Connection(1) 的 Channel 上,再建立 Connection(2),进入 session.close 方法会将 cxt 关闭,Connection(1) 和 Connection(2) 的 Channel 都将会被关闭。在断线之后再建立连接 Connection(3),由于 Session 是有一定延迟,Connection(3) 和 Connection(1/2) 不是同一个,但 Channel 可能是同一个。所以,如何处理是否是断线重练的 Channel,具体的方法是在 Channel 中存入 SessionId,每次事件请求判断 Channel 中是否存在 SessionId,如果 Channel 中存在 SessionId 则判断为断线重连的 Channel。2. 心跳心跳是用来检测保持连接的客户端是否还存活着,客户端每间隔一段时间就会发送一次心跳包上传到服务端,服务端收到心跳之后更新 Session 的最后访问时间。在服务端长连接会话检测通过轮询 Session 集合判断最后访问时间是否过期,如果过期则关闭 Session 和 Connection,包括将其从内存中删除,同时注销 Channel 等。通过源码分析,在每个 Session 创建成功之后,都会在 Session 中添加 TcpHeartbeatListener 这个心跳检测的监听,TcpHeartbeatListener 是一个实现了 SessionListener 接口的守护线程,通过定时休眠轮询 Sessions 检查是否存在过期的 Session,如果轮训出过期的 Session,则关闭 Session。同时,注意到 session.connect 方法,在 connect 方法中会对 Session 添加的 Listeners 进行添加时间,它会循环调用所有 Listner 的 sessionCreated 事件,其中 TcpHeartbeatListener 也是在这个过程中被唤起。3. 数据上行数据上行特指从客户端发送数据到服务端,数据从 ChannelHander 的 channelRead 方法获取数据。数据包括创建会话、发送心跳、数据请求等。这里注意的是,channelRead 的数据包括客户端主动请求服务端的数据,以及服务端下行通知客户端的返回数据,所以在处理 object 数据时,通过数据标识区分是请求 - 应答,还是通知 - 回复。4. 数据下行数据下行通过 MQ 广播机制到所有服务器,所有服务器收到消息后,获取当前服务器所持有的所有 Session 会话,进行数据广播下行通知。如果是点对点的数据推送下行,数据也是先广播到所有服务器,每天服务器判断推送的端是否是当前服务器持有的会话,如果判断消息数据中的信息是在当前服务,则进行推送,否则抛弃。通过源码分析,数据下行则通过 NotifyProxy 的方式发送数据,需要注意的是 Netty 是 NIO,如果下行通知需要获取返回值,则要将异步转同步,所以 NotifyFuture 是实现 java.util.concurrent.Future 的方法,通过设置超时时间,在 channelRead 获取到上行数据之后,通过 seq 来关联 NotifyFuture 的方法。下行的数据通过 TcpConnector 的 send 方法发送,send 方式则是通过 ChannelHandlerContext 的 writeAndFlush 方法写入 Channel,并实现数据下行,这里需要注意的是,之前有另一种写法就是 cf.await,通过阻塞的方式来判断写入是否成功,这种写法偶发出现 BlockingOperationException 的异常。关于 BlockingOperationException 的问题我在 StackOverflow 进行提问,非常幸运的得到了 Norman Maurer(Netty 的核心贡献者之一)的解答。最终结论大致分析出,在执行 write 方法时,Netty 会判断 current thread 是否就是分给该 Channe 的 EventLoop,如果是则行线程执行 IO 操作,否则提交 executor 等待分配。当执行 await 方法时,会从 executor 里 fetch 出执行线程,这里就需要 checkDeadLock,判断执行线程和 current threads 是否时同一个线程,如果是就检测为死锁抛出异常 BlockingOperationException。本篇文章粗浅地向大家介绍了京麦 TCP 网关中使用 Netty 实现长连接容器的架构,对涉及 TCP 长连接容器搭建的关键点一一进行了阐述,以及对源码进行简单地分析。在京麦发展过程里 Netty 还有很多的实践应用,例如 Netty4.11+HTTP2 实现 APNs 的消息推送等等。张松然,京东商城商家研发部架构师。丰富的构建高性能高可用大规模分布式系统的研发、架构经验。2013 年加入京东,目前负责京麦服务网关的系统研发工作。感谢雨多田光对本文的审校。 此内容所在的主题为架构 & 设计相关主题:语言 & 开发语言 & 开发 关注282 他的粉丝架构 & 设计架构 & 设计 关注681 他的粉丝NettyNetty 关注0 他的粉丝架构 架构 关注42 他的粉丝RTCRTC 关注0 他的粉丝京东京东 关注1 他的粉丝
周一至周日 9:00-18:00(全国免费咨询)
您正在使用移动设备访问,是否切换到手机版?
欢迎访问APISTORE
免费试用,在线咨询
工作时间:& 9:00-24:00这部分是我想唠叨的一些话,想直接了解 Netty 的同学可以跳过这一节
上文我们说到了介绍 Netty 的原因和重要性以及 Netty 的基础 —— Java NIO 的一些知识。接下来本文将对 Netty 的重要的类做一个介绍。
在正式开始之前先多唠叨几句。我写 Netty 这一系列(目前只有两篇,斗胆叫系列吧)文章的主要目的还是为了自我学习,为了更深入地了解 Netty。
学一个技术,看一篇文章,读一本书,并不是看完了,自认为读懂了就结束了。看完读懂只是第一步,对于需要熟练掌握的技术,深入理解的知识,一个善于学习的人接下来要做的是尝试用自己的语言去表达自己所学到的东西。通过表达可以检验自己是否真的掌握技术、理解知识。如果有条件,最好能去通过演讲的形式去讲给别人。通过与他人交流的方式进一步检验自己的掌握程度。同时还能锻炼口才和交流能力,这是很多技术人员,包括一些顶尖技术人员所缺乏的。
我对于 Netty 来说只是一个初学者。因为产品的特点,我们需要为遗留系统提供更多协议的支持、负载均衡组件需要支持一些老的协议,安全方面也要有全面的掌控,这使得我们要在 TCP 层面开发很多东西,最终 Netty 便成为了我们的选择。
在使用 Netty 的过程中发现,想要用好它并不是很容易,遇到一些奇怪的问题是家常便饭。再加上 Netty 不像 Spring 那样文档详细到可以当书看,所以便开始考虑去看 Netty 的源代码。
另一方面,作为一个在 Java 服务器端开发领域工作了8年的,收入平平的,但尚有理想的一个”老“程序员,我是不甘把完成一些简单的业务功能代码作为一生的工作内容。我想去能够去实现技术复杂的系统是多数有理想的程序员的目标。所以,去理解一个复杂的框架的源代码便成为我的学习计划中的一项。
好了,唠叨了这么多该开始正式内容了。
2. Netty 简介
Netty 是一个 Java NIO 库,基于 Reactor 模式。Reactor 直译为反应器、反应堆。这个翻译容易误导人。其实将 Reactor 意译为分发器更为恰当。Reactor 模式本质上是一个事件机制,通过一个或一组线程检查事件,发现事件之后交由另一组事件处理线程执行该事件所对应的事件处理器(回调),从而实现高响应的程序。
Reactor 模式和 Java NIO 的结合,变为 Java 世界带来了一批高性能 IO 实现,Netty 便是其中一个。当使用 Netty 实现服务器端,Netty 会使用两个线程池,一个线程池用于不断轮询 Selector 上的事件;另一个线程池用于处理 IO 事件。
接下来的内容是基于 Netty 4.0.30。
3. 对学习 Netty 源代码重要性的补充
在 Netty 中,异步调用被大量使用。虽然 Java(包括其它很多语言)写成的程序大部分还是同步调用,但现在为了提高系统响应能力,越来越多的系统开始引入了 Reactor 设计思想和异步风格的代码调用。在语言层面,从 Java 5 开始引入线程池,Java 7 引入 ForkJoin,Java 8 引入了 CompletableFuture 和在 Stream 中加入了异步并行的特性。专注并发的 Scala 和 Go 语言出现并流行。在框架领域,Spring 5 计划引入
() 以支持响应式的编程。Akka 的出现。这些都说明了异步和 Reactive 编程越来越重要。通过了解 Netty 的源代码,了解 Netty 的实现原理,我们也可以通过一个被广泛使用的真实技术去了解在 Java 编程领域,异步编程如何被实现和使用。进一步,在我们自己的日常工作中,在服务器端开发上,要经常思考是同步调用还是异步。
3.1 异步编程
可能有人会问异步编程有什么好处?我们先来看同步编程。在同步编程中,一个个方法的执行必须按照先来后到的顺序,后面的方法必须等前面的方法执行完之后才能执行(这里我们就不考虑指令重排)。不管后面的方法是否真的需要等前面方法执行完毕后再执行。同时这些方法也都是在一个线程里执行的,如果前一个方法出问题,抛出异常,后面的方法便无法执行。或者前面的方法耗时过程或者因为某些原因挂起线程,后面的方法也不会执行。
另外,各个方法之间也有轻重缓急之分,有些方法需要被优先执行,有些方法的优先级就比较低。这种情况,不同优先级的方法最好被不同调度逻辑的线程执行。
异步编程虽然会写起来复杂一些,但是可以解决上述这些问题。这对构建一个高可用的服务器端应用是很重要的。
上述原因也导致了各种 Reactive 框架和 Go 等新语言的出现。
总结一下,同步编程对方法的调度产生限制,而异步编程可以打破这些限制,帮助构建可用性和响应度更好的系统。
4. Netty 源码中重要的接口和类
Netty 主要由这么几个主要的组件组成:Channel、ChannelPipeline、ChannelHandler、EventLoopGroup、EventLoop、Bootstrap
4.1 Channel
Channel 是 Netty 中最主要的概念之一。在 Netty 中,有一系列的定义和实现 Channel 概念的接口和类。
如同 JDK 中对 Socket Channel 的定义一样,Netty 中的 Socket Channel 也被分为两大类:SocketChannel 和 ServerSocketChannel。其中,对于服务器端开发最为常用的 Socket Channel 实现类是 NioSocketChannel 和 NioServerSocketChannel。下面我们来看这两个类的类图:
NioSocketChannel 类图
NioServerSocketChannel 类图
NioSocketChannel 和 NioServerSocketChannel 的底层实现基于 JDK 的 SocketChannel 和 ServerSocketChannel 。这两个类(其实主要是其父类),通过其 Unsafe 内部类,实现了大量的底层实际操作。例如,AbstractNioChannel 通过其内部类 AbstractNioUnsafe 实现了很多底层的操作。在后面的文章中,我们会经常看到 Unsafe 内部类的源码。
Channel 接口
在 Netty 的 Channel 接口定义了很多的方法。除了大量和 IO 操作相关的方法外,这里重点介绍两个方法:
EventLoop eventLoop();
ChannelPipeline pipeline();
EventLoop eventLoop()
在 Netty 中,大多数 Channel 都有一个 EventLoop 处理它(少数没有 EventLoop 是尚未完成注册工作)。在 NIO 场景中,Channel 通过注册到 Selector 而与一个 NioEventLoop 关联上。这时,当调用到这个 Channel 的 EventLoop eventLoop() 方法时,这个 NioEventLoop 便会被返回。
ChannelPipeline pipeline()
一个 Channel 包含一个 ChannelPipeline 的引用,进而引用了一组 ChannelHandler。
4.2 ChannelPipeline
ChannelPipeline 类图
在 Netty 中,Channel 是通讯的载体,而 ChannelPipeline 则是数据处理器的载体。虽然 Netty 是基于事件机制实现的,但在 4.0 中并没有名字包含 Event 的接口(在 3.0 版本中存在一个 ChannelEvent)。ChannelPipeline 上各种事件的传递是直接通过传递数据进行的,或者不包含数据(例如 Channel Active 事件)。
从上图中可以看到,ChannelPipeline 是一个接口,其有一个默认的实现类 DefaultChannelPipeline 。在 DefaultChannelPipeline 中有两个属性:head 和 tail。这两者都扩展了 AbstractChannelHandlerContext,所以他们都是 ChannelHandlerContext。同时,它们两个还是 ChannelHandler。这两者就是 ChannelPipeline 中 ChannelHandler 链的头和尾。后续文章会对此详细介绍。
ChannelPipeline 的 一个重要作用就是承载 ChannelHandler。向 ChannelPipeline 中添加 ChannelHandler 非常简单,可以使用下列方法:
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(aChannelHandler);
pipeline.addFirst(aChannelHandler);
pipeline.addLast(anEventExecutorGroup, aChannelHandler);
pipeline.addLast(&theNameOftheChannelHandler&, aChannelHandler);
ChannelPipeline 所提供的添加 ChannelHandler 的方法还有很多,我就不一一列举了,它们的形式和功能大体类似。
ChannelPipeline 可以在运行时动态进行动态修改其中的 ChannelHandler。
4.3 ChannelHandler
ChannelHandler 是承载业务处理逻辑的地方,是开发人员使用最多的 Netty 接口。ChannelHandler 可分为两大类:ChannelInboundHandler 和 ChannelOutboundHandler。故名思维,这两接口分别对应入站和出站消息的处理。(Netty 5 不再区分 ChannelInboundHandler 和 ChannelOutboundHandler)
开发人员通常不直接实现上面提到的三个接口,而通常是继承 ChannelInboundHandlerAdapter 或 ChannelOutboundHandlerAdapter。
需要注意的是,不建议在 ChannelHandler 中直接实现耗时或阻塞的操作,因为这可能会阻塞 Netty 工作线程,导致 Netty 无法及时响应 IO 处理。
4.4 ChannelHandlerContext
ChannelHandlerContext 是一个接口,被定义用于在 ChannelHandler 的 Inbound 和 Outbound 业务处理方法中传递上下文信息和传播事件。所谓传播事件指的是调用某个方法会导致相应的事件在 ChannelPipeline 的 ChannelHandler 链中按照规定的顺序传播。规定顺序指的是 inbound 事件是从 head -& tail 的顺序传播,outbound 事件是按照 tail -& head 的顺序传播,这在后面会有详细介绍。(Netty 5 还是会区分 inbound 和 outbound 事件)
上下文信息包括 Channel、EventExecutor 等属性。可触发的事件包括 Channel 的注册、激活、读、写等,这些事件被分为了 inbound 和 outbound 两大类。因为事件比较多,所以不一一列举相关的事件触发方法。简单来说,fire 开头的事件触发方法都是用来触发 inbound 事件,例如 ChannelHandlerContext fireChannelRegistered()、ChannelHandlerContext fireChannelActive() 和 ChannelHandlerContext fireChannelRead() 等。其它的事件触发方法都是触发 outbound 事件的,例如 ChannelFuture bind(SocketAddress, ChannelPromise)、ChannelFuture connect(SocketAddress, SocketAddress, ChannelPromise)、ChannelFuture write(Object, ChannelPromise)。关于这些事件传播方法的完整介绍可以在 ChannelHandlerContext 和 ChannelPipeline 的 Javadoc 中找到。
需要注意的一点是在 inbound 和 outbound 事件中都有 read,这是容易让初学者困惑的地方(至少我困惑了)。Inbound 事件中的 read 事件所对应的方法为 fireChannelRead(Object),用于读到的消息继续在 ChannelHandler 链中传播。Outbound 事件中的 read 所对应的方法为 read(),用于将当前 Channel 设置为可读消息的状态。如果这个 Channel 为 AbstractNioChannel 时,即调用 selectionKey.interestOps(interestOps | readInterestOp)。
ChannelHandlerContext 是 Netty 中一个很重要的概念,正确理解这个接口所定义的方法的使用,深入理解该接口实现类的原理对于正确使用 Netty 至关重要。
4.5 ChannelFuture & Promise
Netty 源码中大量使用了异步编程,从代码实现角度看就是大量使用了线程池和 Future。对于 Netty 的线程池部分我们稍后介绍,这里先介绍 Future。熟悉 Java 5 的同学一定对 Future 不陌生。简单来说就是其代表了一个异步任务,任务将在未来某个时刻完成,而 Future 这个接口就是用来提供例如获取接口、查看任务状态等功能。
Netty 扩展了 Java 5 引入的 Future 机制。从下面的类图我们可以看到相关类的关系:
DefaultChannelPromise 类图
4.5.1 Netty 的 Future 接口
需要注意的是,上面类图中有两个 Future,最上面的是 java.util.concurrent.Future,而其下面的则是 io.netty.util.concurrent.Future。后面如果提到 Future,不加说明,指的就是 Netty 中的 Future。Netty 的 Future 对 JDK Future 的一个主要改变是增加了用于表示任务是否成功的方法 —— boolean isSuccess()。因为 JDK Future 的 boolean isDone() 方法不区分任务是正常结束,还是因为异常或被取消。ChannelFuture 的 Javadoc 详细说明了任务未完成、正常完成、异常完成和被取消四种状态下相关方法的返回值,这里就不重复介绍了。
除了 boolean isSuccess() 方法外,Future 还定义了下列方法:
Future&V& addListener(GenericFutureListener&? extends Future&? super V&& listener)
Future&V& addListeners(GenericFutureListener&? extends Future&? super V&&... listeners)
Future&V& removeListener(GenericFutureListener&? extends Future&? super V&& listener)
Future&V& removeListeners(GenericFutureListener&? extends Future&? super V&&... listeners)
Future&V& await() throws InterruptedException
Future&V& awaitUninterruptibly()
Future&V& sync() throws InterruptedException
Future&V& syncUninterruptibly()
前四个方法是顾名思义,是用来添加删除 GenericFutureListener,这里就不过多做介绍了。本系列后续文章会提到某些实现了对这几个方法的实现。
这里稍微说明一下 await() 和 sync() 这两个用于线程同步的方法。简单来说 sync() 就是会抛出异常的 await()。
4.5.2 Promise
什么是 Promise?
用 Javascript 的同学可能对 Promise 这个词不陌生,但是 Java 程序员刚看到这个词可能会丈二和尚摸不着头脑。所以这里我先简单介绍一下我对 Promise 的理解。
从尚在制定中的 ECMAScript 6 引入 Promise 机制,Promise 是一个用来定义异步调用链的东东。举个栗子:
Promise.all([
fetchPromised(&http://backend/foo.txt&, 500),
fetchPromised(&http://backend/bar.txt&, 500),
fetchPromised(&http://backend/baz.txt&, 500)
]).then((data) =& {
let [ foo, bar, baz ] = data
console.log(`success: foo=${foo} bar=${bar} baz=${baz}`)
}, (err) =& {
console.log(`error: ${err}`)
在 Java 8 中被引入的 CompletableFuture 也有类似的 API,这在我博客里的一篇中有提到。在这篇阅读总结里面,我所读到的关于 CompletableFuture 文章里有一句对 Promise 的总结我觉得很恰当 —— “future without underlying task or thread pool”。但这句话说的还不是太明白。我的理解是 Promise 不想 Future 那样代表一个已经提交的任务。Promise 反映的是对未来的“允诺”,即它反映了将来会被提交的异步任务的行为。虽然现在这个任务尚未被提交,刚未开始执行。从代码的形式上看,Promise 机制提供了一些 API 用来编排组织异步任务。
但是 Netty 的 Promise 和上面提到的 Javascript 和 Java 8 的 Promise 不太一样。Netty 中的 Promise,如同它的 Javadoc 里的解释一样,是一个可写的 Future。这个还不是太好理解,但是看完我对 Netty 回调机制便会明白。
在介绍 Future 接口的时候我们提到了其定义了添加和删除 GenericFutureListener 的方法,那这些监听器在添加之后如何被调用呢。这就涉及到了 Promise 所定义的方法了:
* Marks this future as a success and notifies all
* listeners.
* If it is success or failed already it will throw an {@link IllegalStateException}.
Promise&V& setSuccess(V result);
* Marks this future as a success and notifies all
* listeners.
* @return {@code true} if and only if successfully marked this future as
a success. Otherwise {@code false} because this future is
already marked as either a success or a failure.
boolean trySuccess(V result);
从其 Javadoc 就可以看出,这两个方法不仅会标记任务成功完成,同时还会通知所有的监听器。在本系列后续文章会介绍其实现类是具体如何实现通知功能的。
4.5.3 ChannelFuture
除了 Promise,Netty 中另一个继承自 Future 的接口是 ChannelFuture。这个接口增加的内容不多,和 Future 相比只是增加了 Channel channel() 方法,用来返回与之对应的 Channel。
4.6 EventLoop & EventLoopGroup
NioEventLoop 和 NioEventLoopGroup 是常用的 EventLoop 和 EventLoopGroup 的实现类。当我们使用 ServerBootstrap 去构建 Netty 服务端组件的时候,我们通常会这么写:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
NioEventLoop 没有在上面的代码中体现出来,但实际也是被使用了的。所以我们从这两个实现类来简单了解一下相关的接口和类。
4.6.1 NioEventLoopGroup
我们先从 NioEventLoopGroup 开始,先来看看类图:
NioEventLoopGroup 类图
从上图中我们可以看出,Netty 的 EventLoopGroup 是通过继承 EventExecutorGroup 接口继承了 JDK 的 Iterable 和 ScheduledExecutorService 接口,即它是一个可以被迭代的可调度线程池服务。EventExecutorGroup 所增加的方法主要用于任务调度,这里不做介绍。EventLoopGroup 中定义了三个新方法:
EventLoop next();
ChannelFuture register(Channel channel);
ChannelFuture register(Channel channel, ChannelPromise promise);
接下来分别对它们做一下解释
EventLoop next()
其实这个方法是覆写了其父接口中的方法,但是我们在这里介绍一下。
EventLoopGroup 顾名思义是 EventLoop 的 group,即包含了一组 EventGroup 。在实际的业务处理中,EventLoopGroup 会通过 EventLoop next() 方法选择一个 EventLoop,然后将实际的业务处理交给这个被选出的 EventLoop 去做。
对于 NioEventLoopGroup 来说,其真实功能都会交给 EventLoopGroup 去实现。
ChannelFuture register(Channel channel)
此方法的功能是将一个 Channel 注册到这个 EventLoopGroup 上。这样,当和这个 Channel 相关的事件发生时,EventLoopGroup 就能进行处理。从这个方法的返回值可以看出,这是一个异步方法。
ChannelFuture register(Channel channel, ChannelPromise promise)
此方法同上面的方法类似,只是增加了对回调功能的支持。
4.6.2 NioEventLoop
我们依旧先来看类图:
NioEventLoop 类图
从上图可以看出,EventLoop 继承自 EventLoopGroup,即 EventLoop 也是一种 EventLoopGroup。这样可以形成多层级的 EventLoopGroup。同样,EventLoop 和 EventLoopGroup 彼此的父接口 EventExecutor 和 EventExecutorGroup 也是这样的关系。
因为同 EventExecutor 相比,EventLoop 几乎没有新加方法(只是覆写了一个方法),所以我们这里就只看 EventExecutor 里新定义的方法
EventExecutor
EventExecutorGroup parent();
boolean inEventLoop();
boolean inEventLoop(Thread thread);
&V& Promise&V& newPromise();
&V& ProgressivePromise&V& newProgressivePromise();
&V& Future&V& newSucceededFuture(V result);
&V& Future&V& newFailedFuture(Throwable cause);
这里说一下 boolean inEventLoop()。其它的方法 EventExecutorGroup parent() 顾名思义就好了,不用怎么解释。几个 newXXX() 方法通常的实现就是 new 一个相应的对象,所以这里也不做介绍了。
boolean inEventLoop() 和 boolean inEventLoop(Thread thread) 方法在 Netty 源码中的被经常使用。当 Netty 准备执行一个异步任务,即准备向 EventLoop 提交一个新任务的时候,会先调用 boolean inEventLoop() 方法判断当前线程(提交任务的线程,而非执行任务的线程)是否属于 EventLoop 的执行线程。如果是,通常的做法就是直接调用,否则才是调用 EventLoop 的 execute 之类的方法提交任务。
NioEventLoop
这个实现类实现了很多具体的工作。我们前面提到 EventLoopGroup 的 ChannelFuture register(Channel channel) 方法,在 NioEventLoop 的实现中便是将 Channel 注册到 NioEventLoop 中的 Selector
上(实际工程不是这么简单直接,在后续文章会详细介绍)
4.6.3 EventLoopGroup 的其它一些事儿
以 NioEventLoopGroup 为例,其中的每个线程都有自己的任务队列。和 JDK 的 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 线程池实现不同,Netty 的 NioEventLoopGroup 每一个执行线程都有自己的任务队列,而不是像 JDK 的线程池那样,执行线程共享一个任务队列(ForkJoinPool 除外,其每个执行线程都有自己的任务队列,并可以从其它执行线程的任务队列获取任务,即工作盗取 Work stealing 算法)。这种方式避免了线程之间为获取任务而产生的竞争和线程同步带来的开销。
在 Netty 中,一个 ChannelHandler 实例从始至终都是被一个线程调用。所以一个 ChannelHandler 实例肯定也不会被多个线程同时调用。这避免了内存可见性等的问题,简化了线程安全方面的设计。(Netty 5 目前不保证 ChannelHandler 实例会始终被一个线程调用,但是保证不会被多个线程同时调用)
在 Netty 4 和 5 中,Inbound 和 outbound 事件处理都是在工作线程中执行的。(Netty 3 中 outbound 会在用户发起的线程中执行)
5. 调用关系
在了解完 Netty 中主要接口和类的功能和作用之后,我们来看一下它们直接是如何协调配合的。下面是一个简化过的,当有 IO 事件发生时的时序图,反映了 Netty 接口和类之间大致的调用关系。
Netty 简化调用关系图
简单来说,当有 IO 事件发生时,NioEventLoop 会通过不断轮询 Selector 获得相应的 SelectionKey。进而通过,SelectionKey 的 Object attachment() 方法获得底层连接对应的对象,通常是一个 AbstractNioChannel。进而调用 Channel 上相应的方法。然后是调用 ChannelPipeline 上的事件触发方法,最终调用到用户自定义的 ChannelHandler 上。
写到这里,啰啰嗦嗦地把 Netty 简单介绍了一下。介绍这么多代码层面的东西是为了后续正是开始介绍 Netty 的源码做铺垫。
其实 OSChina、ITeye 等国内的技术社区已经有很多篇介绍 Netty 源码的文章。我在这里继续造车轮的原因主要是为了自己更好的学习 Netty,同时也为了能和大家交流。同时,我希望我的文章能够补全一些别的文章没涉及的内容,包含一些我的理解和想法。
软件和其它技术一样,最核心、最有价值的部分是思想。通过阅读源码,在帮助更好地使用软件、设计软件的同时,理解、吸收好的思想,并产生自己的有价值的思想,这才是最重要的。
& 著作权归作者所有
人打赏支持
码字总数 107958
写在2017年末尾,翻看文集的第一篇文章已经是三个月前的事了,也没想过这文集会写那么久,这么慢。。。 Netty文集中的文章主要都是我学习过程的笔记,写博客的主要目的是为了通过输出来倒逼输...
tomas家的小拨浪鼓 ?
本文是Netty文集中“Netty 源码解析”系列的文章。主要对Netty的重要流程以及类进行源码解析,以使得我们更好的去使用Netty。Netty是一个非常优秀的网络框架,对其源码解读的过程也是不断学习...
tomas家的小拨浪鼓 ?
Channel是理解和使用Netty的核心。Channel的涉及内容较多,这里我使用由浅入深的介绍方法。在这篇文章中,我们主要介绍Channel部分中Pipeline实现机制。为了避免枯燥,借用一下《盗梦空间》的...
Dubbo Github地址 https://github.com/alibaba/dubbo.git Dubbo Remoting 模块是dubbo底层通信模块的实现。实现对请求/应答的各种逻辑处理,包括同步,异步,心跳等逻辑,最底层的通信借助n...
robin-yao ?
Netty是JBoss出品的高效的Java NIO开发框架,关于其使用,可参考我的另一篇文章 netty使用初步。本文将主要分析Netty实现方面的东西,由于精力有限,本人并没有对其源码做了极细致的研 究。如...
JVM JVM系列:类装载器的体系结构 JVM系列:Class文件检验器 JVM系列:安全管理器 JVM系列:策略文件 Java垃圾回收机制 深入剖析Classloader(一)--类的主动使用与被动使用 深入剖析Classloader(二...
background netty 是一个异步事件驱动的网络通信层框架,其官方文档的解释为 Netty is a NIO client server framework which enables quick and easy development of network applications ......
闪电侠 ? 05/29 ?
引言 Channel是理解和使用Netty的核心。Channel的涉及内容较多,这里我使用由浅入深的介绍方法。在这篇文章中,我们主要介绍Channel部分中Pipeline实现机制。为了避免枯燥,借用一下《盗梦空...
目录 源码之下无秘密 ── 做最好的 Netty 源码分析教程 Netty 源码分析之 番外篇 Java NIO 的前生今世 Java NIO 的前生今世 之一 简介 Java NIO 的前生今世 之二 NIO Channel 小结 Java NIO...
文章摘要:借用小厮的一句话“消息队列的本质在于消息的发送、存储和接收”。那么,对于一款消息队列来说,如何做到消息的高效发送与接收是重点和关键 一、RocketMQ中Remoting通信模块概览 ...
癫狂侠 ? 06/30 ?
没有更多内容
加载失败,请刷新页面
《jQuery零基础入门》系列博文是在廖雪峰老师的博文基础上,补充了个人的理解和日常遇到的点,用我的理解表述出来,主干出处来自廖雪峰老师的技术分享。 除了基本的选择器外,jQuery的层级选...
JandenMa ? 5分钟前 ?
0、算法概述 0.1 算法分类 十种常见排序算法可以分为两大类: 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。 ...
o0无忧亦无怖 ? 今天 ?
go 基础之并发笔记 协程 与传统的系统级线程和进程相比,协程的大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常多也不能超过1万个。这也是协程也叫轻...
D_Kim ? 今天 ?
sed是流式编辑器,是针对文档的行来操作的。grep只能实现查找功能,不能把查找的内容替换。vim可以实现替换,它只能在文本的内部操作,不能输出的屏幕上。而sed工具就可以替换文本并把它输出...
黄昏残影 ? 今天 ?
高并发系统的保护策略:限流、降级、缓存 限流算法: 漏桶算法(Leaky Bucket)概念:请求从漏桶的入口处流入到漏桶中,漏桶以一定的速率将请求从出口处流出,当漏桶已满时(请求的流入速度大...
A__17 ? 今天 ?
之前一直因为:反复讨论系统方案最终定下来的策略在实施时具体情况跟一开始想的并不一样,而导致对系统设计并不是很在意。但经过实践发现,系统设计是非常必需的,因为分析得很清楚会带来很多...
Mediv ? 昨天 ?
linux的命令操作 1、日常操作命令 **查看当前所在的工作目录 pwd **查看当前系统的时间 date **查看有谁在线(哪些人登陆到了服务器) who 查看当前在线 last 查看最近的登陆历史记录 **清理...
em_aaron ? 昨天 ?
Servlet容器在收到客户机请求的时候,首先会解析请求url,根据web.xml的配置,去找到对应的servlet。如果没有找到对应的servlet,服务器有一个默认Servlet的配置项,位于$CATALINA_HOME/conf...
Wakeeee_ ? 昨天 ?
最近公司的微服务maven分模块一个父工程多个子工程 因为子工程依赖父工程,直接对其某个子工程进行maven clean后maven install,,,install失败 提示在本地仓库中,找不到父工程的.jar这个包......
故久呵呵 ? 昨天 ?
参考网页 http://tramp.cincout.cn//spring-boot--spring-boot-multi-cache-manager/ 为什么要混用缓存(本地缓存和分布式缓存)? 这个要看项目实际需要。一种场景就是...
karma123 ? 昨天 ?
没有更多内容
加载失败,请刷新页面
文章删除后无法恢复,确定取消删除此文章吗?
亲,自荐的博客将通过私信方式通知管理员,优秀的博客文章审核通过后将在博客推荐列表中显示
确定推荐此文章吗?
确定推荐此博主吗?
聚合全网技术文章,根据你的阅读喜好进行个性推荐
指定官方社区
深圳市奥思网络科技有限公司版权所有

我要回帖

更多关于 netty 客户端等待响应 的文章

 

随机推荐