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 编写总结的更多相关文章

  1. 关于OPC Client 编写

    昨天又有人问我 OPC Client 编写,实际是他们不了解OPC 客户端的工作原理,要想写客户端程序,必须知道OPC对象, OPC逻辑对象模型包括3类对象:OPC server对象.OPC grou ...

  2. 关于OPC Client 编写2

    最近在搞到一个OPC动态库OPCAutomation.dll,该动态库在http://www.kepware.com/可下载,下面介绍如何用C#进行OPC Client开发. 1.新建C#应用程序,命 ...

  3. Netty In Action中国版 - 第二章:第一Netty程序

    本章介绍 获得Netty4最新的版本号 设置执行环境,以构建和执行netty程序 创建一个基于Netty的server和client 拦截和处理异常 编制和执行Nettyserver和client 本 ...

  4. Netty之多用户的聊天室(三)

    Netty之多用户的聊天室(三) 一.简单说明 笔者有意将Netty做成一个系列的文章,因为笔者并不是一个善于写文章的人,而且笔者学习很多技术一贯的习惯就是敲代码,很多东西敲着敲着就就熟了,然后再进行 ...

  5. Netty入门之HelloWorld

    Netty系列入门之HelloWorld(一) 一. 简介 Netty is a NIO client server framework which enables quick and easy de ...

  6. Netty开发redis客户端,Netty发送redis命令,netty解析redis消息

    关键字:Netty开发redis客户端,Netty发送redis命令,netty解析redis消息, netty redis ,redis RESP协议.redis客户端,netty redis协议 ...

  7. Netty入门——客户端与服务端通信

    Netty简介Netty是一个基于JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞.基于事件驱动.高性能.高可靠性和高可定制性.换句话说,Netty是一个NIO框架,使用它可以简单快速 ...

  8. day 4 Socket 和 NIO Netty

    Scoket通信--------这是一个例子,可以在这个例子的基础上进行相应的拓展,核心也是在多线程任务上进行修改 package cn.itcast.bigdata.socket; import j ...

  9. Netty In Action中文版 - 第三章:Netty核心概念

            在这一章我们将讨论Netty的10个核心类.清楚了解他们的结构对使用Netty非常实用.可能有一些不会再工作中用到.可是也有一些非经常常使用也非常核心,你会遇到. Bootstrap ...

随机推荐

  1. HttpLib - 一个对 Http 协议进行封装的库

    今日,在 Codeplex 上看到一个开源项目,对 Http 协议进行了封装,这下可以方便这些在 .NET 平台下访问 Web 服务器的同学们了,比如,从 Web 服务器抓取一个页面,使用 .NET ...

  2. 设计模式之美:Command(命令)

    索引 别名 意图 结构 参与者 适用性 效果 相关模式 实现 实现方式(一):直接注入 Receiver 对象,Command 决定调用哪个方法. 实现方式(二):注入 Receiver 的指定方法, ...

  3. C# 两行代码实现 延迟加载的单例模式(线程安全)

    关键代码第4,5行. 很简单的原理不解释:readonly + Lazy(.Net 4.0 + 的新特性) public class LazySingleton { //Lazy singleton ...

  4. LINQ-to-SQL那点事~利用反射在LINQ-to-SQL环境中实现Ado.net的CURD操作

    回到目录 对于linq to sql提供的CURD操作,给我们的感觉就是简单,容易使用,更加面向对象,不用拼SQL语句了,这些好处都表示在处理单条实体或者集合长度小的情况下,如果有一个1000条的集合 ...

  5. Leetcode 166. Fraction to Recurring Decimal 弗洛伊德判环

    分数转小数,要求输出循环小数 如2 3 输出0.(6) 弗洛伊德判环的原理是在一个圈里,如果一个人的速度是另一个人的两倍,那个人就能追上另一个人.代码中one就是速度1的人,而two就是速度为2的人. ...

  6. 我所理解的JavaScript闭包

    目录 一.闭包(Closure) 1.1.什么是闭包? 1.2.为什么要用闭包(作用)? 1.2.1.保护函数内的变量安全. 1.2.2.通过访问外部变量,一个闭包可以暂时保存这些变量的上下文环境,当 ...

  7. typeid详解

    在揭开typeid神秘面纱之前,我们先来了解一下RTTI(Run-Time Type Identification,运行时类型识别),它使程序能够获取由基指针或引用所指向的对象的实际派生类型,即允许“ ...

  8. [C/C++] zltabout(带缩进的格式化输出)v1.0。能以相同的代码绑定到 C FILE 或 C++流

    作者:zyl910 一.缘由 在写一些生成文本的程序时,经常需要使用带缩进的格式化输出的功能.以前为此写过不少类似的函数,可惜它们的可重用性很差. 这是因为——1) C语言的FILE*不支持重定向到自 ...

  9. IntelliJ IDEA + Maven创建Java Web项目

    1. Maven简介 相对于传统的项目,Maven 下管理和构建的项目真的非常好用和简单,所以这里也强调下,尽量使用此类工具进行项目构建, 它可以管理项目的整个生命周期. 可以通过其命令做所有相关的工 ...

  10. fastreport totalpage 只有设置doublepassreport为true 才正确否则为0

    fastreport totalpage 只有设置doublepassreport为true 才正确否则为0