Netty http client 编写总结
Apache http client 有两个问题,第一个是 apache http client 是阻塞式的读取 Http request, 异步读写网络数据性能更好些。第二个是当 client 到 server 的连接中断时,http client 无法感知到这件事的发生,需要开发者主动的轮训校验,发 keep alive 或者 heart beat 消息,而 netty 可以设置回调函数,确保网络连接中断时有逻辑来 handle
使用 Netty 编写 Http client,也有一些问题。首先是 netty 是事件驱动的,逻辑主要基于回调函数。数据包到来了也好,网络连接中断也好,都要通过写回调函数确定这些事件来临后的后续操作。没有人喜欢回调函数,Future 是 scala 里讨人喜欢的特性,它能把常规于语言里通过回调才能解决的问题通过主动调用的方式来解决,配合 map, flatmap, for 甚至 async,scala 里可以做到完全看不到回调函数。所以用 netty 做 client 第一个问题是如何把 回调函数搞成主动调用的函数。第二点是 长连接,一个 channel 不能发了一个消息就关闭了,每次发消息都要经过 http 三次握手四次挥手效率太低了,最好能重用 channel。第三个是 thread-safe,这个一开始并没有考虑到,后来发现这个是最难解决的问题。当然 netty 作为一个比较底层的包,用它来实现一些高层的接口是比较费时费力的,有很多事情都要手动去做。我花了四五天的时间,没有解决这几个问题,只留下一些经验,供以后参考(见后面的 update)。
回调函数变主动调用函数
netty 的操作都是基于回调函数的
消息到达时的逻辑
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
if (content instanceof HttpContent) {
sendFullResponse(ctx, content);
} else {
log.error("content is not http content");
}
}
}
到 server 的连接建立后创建 channel 的逻辑
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpClientCodec());
p.addLast(new HttpContentDecompressor());
p.addLast(new HttpObjectAggregator(512 * 1024));
p.addLast(new ResponseHandler());
}
});
这是我就希望有一个像 scala Future/Promise 一样的东西,帮我把回调函数转成主动调用函数,这是 scala 的一个例子
Promise promise = Promise[HttpContent]
def channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
HttpContent content = (HttpContent) msg
promise.success(content)
}
//somewhere else
promise.future.map(content => println("content has been recieved in client"))
可以说有了 promise,我们接收到 httpContent 以后的事情就都能用主动调用的方式来写了,虽然不完全像普通的 java 代码那样简单,需要加一些组合子,但是已经够好了。
Java 里没有 promise,需要自己实现,参考了别人的代码,发现 CountDownLatch 是实现 promise 的关键。setComplete 和 await 是最重要的两个函数,一个设置 CountDownLatch,一个等待 CountDownLatch。
private boolean setComplete(ResultHolder holder) {
log.info("set complete");
if (isDone()) {
return false;
}
synchronized (this) {
if (isDone()) {
return false;
}
this.result = holder;
if (this.complteLatch != null) {
log.info("set complete time: " + System.currentTimeMillis());
this.complteLatch.countDown();
} else {
log.info("completeLatch is null at the time: " + System.currentTimeMillis());
}
}
return true;
}
public TaskFuture await() throws InterruptedException {
if (isDone()) {
return this;
}
synchronized (this) {
if (isDone()) {
return this;
}
if (this.complteLatch == null) {
log.info("await time: " + System.currentTimeMillis());
this.complteLatch = new CountDownLatch(1);
}
}
this.complteLatch.await();
return this;
}
有了 Promise 以后就能把回调函数转为主动调用的函数了。虽然没有组合子,但是已经够好了,起码 await 函数能够保证开发者拿到 HttpContent 后能够像正常的 java 代码一样操纵这个值。
public TaskPromise executeInternal(HttpRequest httpRequest)
重用 channel
根据上面那一节,得到了这个函数
public TaskPromise executeInternal(HttpRequest httpRequest) {
final TaskPromise promise = new DefaultTaskPromise();
log.info("new created promise hashcode is " + promise.hashCode());
Channel channel = channelFuture.channel();
channel.pipeline().get(ResponseHandler.class).setResponseHandler(promise);
channel.writeAndFlush(httpRequest).addListener((ChannelFutureListener) future -> {
if(future.isSuccess()) {
log.info("write success");
}
});
public class ResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
Logger log = LoggerFactory.getLogger(getClass());
private TaskPromise promise;
public void setResponseHandler(TaskPromise promise) {
this.promise = promise;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
log.info("channel read0 returned");
promise.setSuccess(new NettyHttpResponse(ctx, msg));
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) throws Exception {
log.info("exception caught in response handler");
this.promise.setFailure(cause);
}
}
每次调用 executeInternal 都创建一个 promise 将此 promise 放到 ResponseHandler 注册一下,然后将 promise 句柄当做返回值。channel.pipeline().get(xxx).set(yyy) 是在 SO 找到的,看起来像个黑科技。这个函数看起来可以满足需求了。
实际上不然,它不是线程安全的。当两个线程同时调用 executeInternal 时,可能会同时 setResponseHandler,导致第一个 promise 被冲掉,然后两个线程持有同一个 promise,一个 promise 只能被 setComplete 一次,第二次时会 exception。假如把 executeInernal 写成同步的,线程安全问题仍在,因为只要是在一个请求返回来之前设置了 promise,第一个 promise 总是会被冲掉的。看起来这是一个解决不了的问题。
在 github 看了很多别人的代码,发现大家都没认真研究线程安全的问题,或者一个 channel 只发一个消息。查阅了一些资料,了解到InboundHandler 的执行是原子的,不用担心线程安全问题,但这对我也没什么帮助。找到 AsyncRestTemplate 的底层实现, Netty4ClientHttpRequest,我觉得它想做的事情跟我很像,但不过它好像是每个 channel 只发一个消息。因为每次发新的消息,Bootstrap 都会调用 connect 函数。
@Override
protected ListenableFuture<ClientHttpResponse> executeInternal(final HttpHeaders headers) throws IOException {
final SettableListenableFuture<ClientHttpResponse> responseFuture =
new SettableListenableFuture<ClientHttpResponse>();
ChannelFutureListener connectionListener = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
Channel channel = future.channel();
channel.pipeline().addLast(new RequestExecuteHandler(responseFuture));
FullHttpRequest nettyRequest = createFullHttpRequest(headers);
channel.writeAndFlush(nettyRequest);
}
else {
responseFuture.setException(future.cause());
}
}
};
this.bootstrap.connect(this.uri.getHost(), getPort(this.uri)).addListener(connectionListener);
return responseFuture;
}
如果 bootstrap 能够缓存住以前的连接,那么他就是我想要的东西了,但是我循环了 executeInternal 十次,发现建立了十个到 Server 的连接,也就说它并没有重用 channel
update:
上一次写总结时还卡在一个解决不了的并发问题上,当初的并发问题实际上可以写成 how concurrent response mapping to request. 在 Stackoverflow 和中文论坛上有人讨论过这个问题,从他们的讨论中看的结论是:
在 Netty 里,channel 是 multiplex 的,但是返回的 Response 不会自动映射到发出的 Request 上,Netty 本身没有这种能力,为了达到这个效果,需要在应用层做一些功夫。一般有两种做法
- 如果 Client, Server 都由开发者掌控,那么 client 和 server 可以在交互协议上添加 requestId field, request 和 response 都有 requestId 标识。client 端每发送一个 request 后,就在本地记录 (requestId, Future[Response]) 这么一个 pair, 当 response 返回后,根据 requestId 找到对应的 future, 填充 future
- 当 server 端不由开发者掌控时,channel 只能被动接受没有状态的 response,没有其他信息可供 client 分辨它对应的是那个 request, 此时就只能使用 sync 模式发送消息了,这样能够保证 response 对应着的就是正在等待它的那个 request. 使用这种方法就失掉了并发的特性,但是可以创建一个 channel pool, 提供一定的并发性
对于有些不需要 response, request 对应关系的服务,channel 的写法可以保持原始的回调函数,比如 heartbeat 服务就可以可以这么写。
源码链接https://github.com/sangszhou/NettyHttpClient
做了个简单的 benchmark, 发现比 apache http client 慢了 2~3 倍,目前还不确定性能瓶颈的位置。
Netty http client 编写总结的更多相关文章
- 关于OPC Client 编写
昨天又有人问我 OPC Client 编写,实际是他们不了解OPC 客户端的工作原理,要想写客户端程序,必须知道OPC对象, OPC逻辑对象模型包括3类对象:OPC server对象.OPC grou ...
- 关于OPC Client 编写2
最近在搞到一个OPC动态库OPCAutomation.dll,该动态库在http://www.kepware.com/可下载,下面介绍如何用C#进行OPC Client开发. 1.新建C#应用程序,命 ...
- Netty In Action中国版 - 第二章:第一Netty程序
本章介绍 获得Netty4最新的版本号 设置执行环境,以构建和执行netty程序 创建一个基于Netty的server和client 拦截和处理异常 编制和执行Nettyserver和client 本 ...
- Netty之多用户的聊天室(三)
Netty之多用户的聊天室(三) 一.简单说明 笔者有意将Netty做成一个系列的文章,因为笔者并不是一个善于写文章的人,而且笔者学习很多技术一贯的习惯就是敲代码,很多东西敲着敲着就就熟了,然后再进行 ...
- Netty入门之HelloWorld
Netty系列入门之HelloWorld(一) 一. 简介 Netty is a NIO client server framework which enables quick and easy de ...
- Netty开发redis客户端,Netty发送redis命令,netty解析redis消息
关键字:Netty开发redis客户端,Netty发送redis命令,netty解析redis消息, netty redis ,redis RESP协议.redis客户端,netty redis协议 ...
- Netty入门——客户端与服务端通信
Netty简介Netty是一个基于JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞.基于事件驱动.高性能.高可靠性和高可定制性.换句话说,Netty是一个NIO框架,使用它可以简单快速 ...
- day 4 Socket 和 NIO Netty
Scoket通信--------这是一个例子,可以在这个例子的基础上进行相应的拓展,核心也是在多线程任务上进行修改 package cn.itcast.bigdata.socket; import j ...
- Netty In Action中文版 - 第三章:Netty核心概念
在这一章我们将讨论Netty的10个核心类.清楚了解他们的结构对使用Netty非常实用.可能有一些不会再工作中用到.可是也有一些非经常常使用也非常核心,你会遇到. Bootstrap ...
随机推荐
- Dynamic CRM 2013学习笔记(四十二)流程5 - 实时/同步工作流(Workflow)用法图解
实时工作流跟插件一样,也是用事件执行管道来执行,能在pre,post或核心操作中执行.跟插件一样,不能在创建之前和删除之后执行.如果执行过程中有异常发生,会取消并回滚整个操作.实时工作流里所有的活动和 ...
- C#笔试题(一)
一.下面是一个由*号组成的4行倒三角形图案. 要求: 1.输入倒三角形的行数,行数的取值3-21之间,对于非法的行数,要求抛出提示"非法行数!": 2.在屏幕上打印这个指定了行数的 ...
- [ACM_其他] Square Ice (poj1099 规律)
Description Square Ice is a two-dimensional arrangement of water molecules H2O, with oxygen at the v ...
- 投票系统开发总结struts2,jfreechart,cookie应用,以及前端技术
struts2配置web.xml+struts.xml: <?xml version="1.0" encoding="UTF-8"?> <we ...
- javascript 日常总结
1. 将彻底屏蔽鼠标右键 oncontextmenu=”window.event.returnValue=false” < table border oncontextmenu=return(f ...
- nosql/nodejs基础
nosql定义:nosql--no only sql 目前流行的非关系型数据库:mongodb,redis,cassandra 非关系型数据库和内存存储hashmap数据结构有什么区别?hashmap ...
- docker学习笔记一:基本安装和设置容器静态ip
docker是一个lxc升级版的容器类虚拟环境,具有快速部署,灵活,易迁移的虚拟机模式,现在各大公司已经开始广泛使用为了自己方便学习linux,需要多台虚拟机环境,但是vmware开启多台虚拟机时需要 ...
- atitit.提升备份文件复制速度(3) ----建立同步删除脚本
atitit.提升备份文件复制速度(3) ----建立同步删除脚本 1. 建立同步删除脚本两个方法.. 1 2. 1从回收站info2文件... 1 3. 清理结束在后snap比较 1 4. Npp ...
- iOS开发----调用地图导航
注意:本文章下的代码有个别变量未知,所以是不能直接跑通的,我也是转别人的 在IOS6.0系统后,兼容iOS5.0与iOS6.0地图导航,需要分两个步骤 #define SYSTEM_VERSION_L ...
- Template Method模式和Strategy模式[继承与委托]
继承 program by difference. 通过继承,可以建立完整的软件结构分层.其中每一层都可以重用该层次以上的Code. 过度使用继承的代价是巨大的.应使用组合或者委托来替代继承. Tem ...