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 ...
随机推荐
- iis php5.3.8 默认文档无效 404 - 找不到文件或目录
环境:WIN2008 R2 IIS7.5 / .NET4.X 新开1站点,使用php(5.3.8),默认首页文档已设置为index.php,网站所在目录的网站运行时用户权限正确,应用程序池是asp.n ...
- 使用cocos2d-x 3.0 beta开发的小游戏
主要是参考了http://philon.cn/post/cocos2d-x-3.0-zhi-zuo-heng-ban-ge-dou-you-xi 这篇文章,只是移植到了3.0 beta版. 代码地址: ...
- CentOS系统下Hadoop 2.4.1集群安装配置(简易版)
安装配置 1.软件下载 JDK下载:jdk-7u65-linux-i586.tar.gz http://www.oracle.com/technetwork/java/javase/downloads ...
- 学习WPF——了解路由事件
入门 我们先来看一个例子 前台代码: 后台代码: 点击按钮的运行效果第一个弹出窗口 第二个弹出窗口: 第三个弹出窗口: 说明 当点击按钮之后,先触发按钮的click事件,再上查找,发现stackpan ...
- Nginx学习笔记(七) 创建子进程
Nginx创建子进程 ngx_start_worker_processes位于Nginx_process_cycle.c中,主要的工作是创建子进程. 在Nginx中,master进程和worker进程 ...
- iOS——Swift开发中的单例设计模式(摘译,非原创)
最近在开发一个小的应用,遇到了一些Objective-c上面常用的单例模式,但是swift上面还是有一定区别的,反复倒来倒去发现不能按常理(正常的oc to swift的方式)出牌,因此搜索了一些帖子 ...
- mobilebone.js使用笔记
mobilebone.js主要用来是网页呈现单页效果,添加类似native app的页面切换效果.原理是:当打开a链接里的页面时,不再以传统的新页面打开,而是以ajax-html的方式,将新页面的内容 ...
- Java程序员的日常 —— static的用法讲解实践
之前文章说过Java中static的作用,有朋友想看个例子.于是便抽空写了个小栗子 代码 package xing.test.thinking.chap5; class A{ public A() { ...
- Java程序员的日常 —— 工作一天的收获
看题目可能是扯皮,其实还是有很多专业知识的.从最开始没有注意到设计原则,到后面的jquery实战技巧,都是今天一天碰到的问题. 每天整理一点点,每天收获一点点. 关于软件设计 在设计系统结构的时候,一 ...
- RAR和ZIP:压缩大战真相
转:http://fqd2eh4y.blog.163.com/blog/static/69195855200801035015857 前言--王者归来? 等待足足两年之久,压缩霸主WinZip终于在万 ...