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. CentOS系统下Hadoop 2.4.1集群安装配置(简易版)

    安装配置 1.软件下载 JDK下载:jdk-7u65-linux-i586.tar.gz http://www.oracle.com/technetwork/java/javase/downloads ...

  2. [汇编] 从键盘输入一个一位数字,然后响铃n声

    ; multi-segment executable file template. data segment ends stack segment dw dup() ends code segment ...

  3. jenkins和docker 在docker里运行jenkins

    在docker里运行jenkins server. 文章来自:http://www.ciandcd.com文中的代码来自可以从github下载: https://github.com/ciandcd ...

  4. Express 4 更新改变文档

    概览 从 Express 3 到Express 4 是一个巨大的变化,这意味着现存的 Express 3 应用在不更新依赖的情况下将不能工作. 这篇文章涵盖一下内容: Express 4 中的变化 一 ...

  5. spring配置entitymangerfactory

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerE ...

  6. JAVA学习中Swing部分JDialog对话框窗体的简单学习

    package com.swing; import java.awt.Color;import java.awt.Container;import java.awt.event.ActionEvent ...

  7. php中的邮件技术

    前言:程序员不谈恋爱就是对物质世界太贪恋 一.通过smtp服务来发送邮件 使用php中的mail()实现邮件的发送; bool mail ( string $to , string $subject ...

  8. pl/sql死锁oracle

    http://jingyan.baidu.com/album/3ea51489eb65b152e61bba8b.html?picindex=2

  9. spring源码 — 一、IoC容器初始化

    IoC容器初始化 注意:本次的spring源码是基于3.1.1.release版本 容器:具有获取Bean功能--这是最基本功能,也是BeanFactory接口定义的主要行为,在添加了对于资源的支持之 ...

  10. SQL Server如何提高数据库备份的速度

    对于一个数据库完整备份来说,备份的速度很大程度上取决于下面两个因素:读磁盘数据.日志文件的吞吐量,写磁盘数据文件的吞吐量. 下图是备份过程中磁盘的变化情况: 读吞吐量 读吞吐量的大小取决于磁盘读取数据 ...