netty服务器(java netty)
netty系列之:性能为王!创建多路复用http2服务器
简介
在之前的文章中,我们提到了在Netty的客户端通过使用Http2FrameCodec和Http2MultiplexHandler可以支持多路复用,也就是说在一个连接的channel基础上创建多个子channel,通过子channel来处理不同的stream,从而达到多路复用的目的。
既然客户端可以做到多路复用,同样的服务器端也可以,今天给大家介绍一下如何在netty的服务器端打造一个支持http2协议的多路复用服务器。
多路复用的基础
netty中对于http2多路复用的基础类是Http2FrameCodec、Http2MultiplexHandler和Http2MultiplexCodec。
Http2FrameCodec是将底层的HTTP/2 frames消息映射成为netty中的Http2Frame对象。
有了Http2Frame对象就可以通过Http2MultiplexHandler对新创建的stream开启不同的channel。
Http2MultiplexCodec是Http2FrameCodec和Http2MultiplexHandler的结合体,但是已经不再被推荐使用了。
因为Http2FrameCodec继承自Http2ConnectionHandler,而Http2MultiplexHandler继承自Http2ChannelDuplexHandler,所以这两个类可以同时在客户端和服务器端使用。
客户端使用Http2FrameCodecBuilder.forClient().build()来获得Http2FrameCodec,而服务器端通过Http2FrameCodecBuilder.forServer().build()来获得Http2FrameCodec。
多路复用在server端的使用配置TLS处理器
对于服务器端,同样需要处理TLS和普通clear text两种情况。对于TLS来说,我们需要自建ProtocolNegotiationHandler继承自ApplicationProtocolNegotiationHandler,然后实现configurePipeline方法,在其中分别处理http2和http1.1的连接:
protectedvoidconfigurePipeline(ChannelHandlerContextctx,Stringprotocol){if(ApplicationProtocolNames.HTTP_2.equals(protocol)){//添加多路复用支持ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build());ctx.pipeline().addLast(newHttp2MultiplexHandler(newCustMultiplexHttp2Handler()));return;}if(ApplicationProtocolNames.HTTP_1_1.equals(protocol)){ctx.pipeline().addLast(newHttpServerCodec(),newHttpObjectAggregator(MAX_CONTENT_LENGTH),newCustHttp1Handler("ALPNNegotiation"));return;}thrownewIllegalStateException("未知协议:"+protocol);}
首先添加Http2FrameCodec,然后添加Http2MultiplexHandler。因为Http2MultiplexHandler已经封装了多路复用的细节,所以自定义的handler只需要实现正常的消息处理逻辑即可。
因为Http2FrameCodec已经对消息进行了转换成为HTTP2Frame对象,所以只需要处理具体的Frame对象:
publicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{if(msginstanceofHttp2HeadersFrame){onHeadersRead(ctx,(Http2HeadersFrame)msg);}elseif(msginstanceofHttp2DataFrame){onDataRead(ctx,(Http2DataFrame)msg);}else{super.channelRead(ctx,msg);}}配置clear text upgrade
对于h2c的升级来说,需要向pipline中传入sourceCodec和upgradeHandler两个处理器。
sourceCodec可以直接使用HttpServerCodec。
upgradeHandler可以使用HttpServerUpgradeHandler。
HttpServerUpgradeHandler的构造函数需要传入一个sourceCodec和一个upgradeCodecFactory。
sourceCodec我们已经有了,再构造一个upgradeCodecFactory即可:
privatestaticfinalUpgradeCodecFactoryupgradeCodecFactory=protocol->{if(AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME,protocol)){returnnewHttp2ServerUpgradeCodec(Http2FrameCodecBuilder.forServer().build(),newHttp2MultiplexHandler(newCustMultiplexHttp2Handler()));}else{returnnull;}};
从代码中可以看出,upgradeCodecFactory内部又调用了Http2FrameCodec和Http2MultiplexHandler。这和使用TLS的处理器是一致的。
finalChannelPipelinep=ch.pipeline();finalHttpServerCodecsourceCodec=newHttpServerCodec();p.addLast(sourceCodec);p.addLast(newHttpServerUpgradeHandler(sourceCodec,upgradeCodecFactory));总结
通过上述方式,就可以创建出支持多路复用的http2 netty服务器了。
本文的例子可以参考:learn-netty4
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
为什么选择Netty作为基础通信框架
一、什么是Netty
Netty是一个高性能事件驱动、异步非堵塞的IO(NIO)Java开源框架,Jboss提供,用于建立TCP等底层的连接,基于Netty可以建立高性能的Http服务器,快速开发高性能、高可靠性的网络服务器和客户端程序。支持HTTP、 WebSocket、Protobuf、 Binary TCP|和UDP,Netty已经被很多高性能项目作为其Socket底层基础,如HornetQ Infinispan Vert.x Play Framework Finangle和 Cassandra。其竞争对手是:Apache MINA和 Grizzly。
也就是说,Netty是一个基于NIO的客户,服务器端编程框架,使用Netty可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。
“快速”和“简单”并不意味着会让你的最终应用产生维护性或性能上的问题。Netty是一个吸收了多种协议的实现经验,这些协议包括FTP,SMTP,HTTP,各种二进制,文本协议,并经过相当精心设计的项目,最终,Netty成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。
二、不选择Java原生NIO编程的原因
首先开发出高质量的NIO程序并不是一件简单的事情,除去NIO固有的复杂性和BUG不谈,作为一个NIO服务端,还需要能够处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写等情况,如果你没有足够的NIO编程经验积累,一个NIO框架的稳定往往需要半年甚至更长的时间。更为糟糕的是,一旦在生产环境中发生问题,往往会导致跨节点的服务调用中断,严重的可能会导致整个集群环境都不可用,需要重启服务器,这种非正常停机会带来巨大的损失。
从可维护性角度看,由于NIO采用了异步非阻塞编程模型,而且是一个I/O线程处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境中的问题,我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大。
现在我们总结一下为什么不建议开发者直接使用JDK的NIO类库进行开发,具体原因如下。
1)跨平台与兼容性:NIO算是底层的APIs需依赖系统的IO APIs。但Java NIO发现在不同系统平台会出现问题。大量测试也耗不少时间;NIO2只支持JDK1.7+,而且没提供DatagramSocket,故NIO2不支持UDP协议。而Netty提供统一接口,同一语句无论在JDK6.X还是JDK7.X都可运行,无需关心底层架构功能!
2)JAVA NIO的ByteBuffer构造函数私有,无法扩展。Netty提供了自己的ByteBuffer实现,通过简单APIs对其进行构造、使用和操作,一此解决NIO的一些限制。
3)NIO对缓冲区的聚合与分散操作可能会导致内存泄漏。直到JDK1.7才解决此问题。
4)NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
5)使用JAVA NIO需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序。
6)可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题。
7)JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK 1.6版本的update18修复了该问题,但是直到JDK 1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有得到根本性解决。该BUG以及与该BUG相关的问题单可以参见以下链接内容。
异常堆栈如下。
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:210)
at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:65)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:69)
- locked<0x0000000750928190>(a sun.nio.ch.Util$2)
- locked<0x00000007509281a8>(a java.util.Collections$ UnmodifiableSet)
- locked<0x0000000750946098>(a sun.nio.ch.EPollSelectorImpl)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:80)
at net.spy.memcached.MemcachedConnection.handleIO(Memcached Connection.java:217)
at net.spy.memcached.MemcachedConnection.run(MemcachedConnection. java:836)
由于上述原因,在大多数场景下,不建议大家直接使用JDK的NIO类库,除非你精通NIO编程或者有特殊的需求。在绝大多数的业务场景中,我们可以使用NIO框架Netty来进行NIO编程,它既可以作为客户端也可以作为服务端,同时支持UDP和异步文件传输,功能非常强大。
如何构建一个基于netty的后端服务器
直接上干货,这个是前奏,比较山寨的实现,大家可先自行看下
下面将分析手头上一个项目,运用的技术很全,值得学习,先做一个简单介绍,当然业务部分代码就不讲了。
整个工程采用maven来管理,主要的技术是spring+jedis+netty+disruptor.看这个组合,这个服务器端性能应该很不错。
这个工程又引发我对技术无限热爱,哈哈。
这
个工程,目前主要是针对一些基于json/xml/text格式的请求,同时也是支持标准手机请求的,当然,可以自定义一些其他格式或者pc端的请求,而
且针对不同URI,后面挂了不同的handler,这些可能都是一些web处理的基本思想,只是脱离了常规的web容器或者应用服务器。
xml工具采用xstram来处理,两个字,方便。
json工具采用jackson\不知道和业界出名的fastjson\gson\sf.json有何区别,待鉴定。
客
户端的请求,统一继承ClientRequestModel,经过编码统一转化为domainMessage,交由disruptor来处理,其实oop
里什么继承,实现,封装思想,大部分都在围绕一个东西在走,一句话,把看似各有棱角的东西如何转化为共同的东西,求同存异啊(比如,水,石头,空气等,如
果在这一层,我们没法统一用一个特征来表示,我们可以先把它转化为分子,那是不是可以用同一个东西来表示呢?如何高度抽象封装,这真是一门艺术)。
看这个工程对客户端请求,是如何一步步处理的,message->request->event交由disruptor来处理,很美妙的思想。在了解这些之前,我们有必要深入学习一下disruptor,很特别的一个框架,宣言很牛逼,中文文档在这里(),E文好的同学请移步到这里()
了解disruptor之前,先学习下ringbuffer是如何实现的?
1、ringbuffer的特别之处:
只有一个指针,没有尾指针,基于数组,且不会删除元素,元素会覆盖,充分利用缓存行,减少垃圾回收。
2、如何从ringbuffer读取数据:
------------------------------------------2013-9-9补充-----------------------------------------------------
下面主要讲一下请求如何处理这块架构吧,其实架构这个东西,说简单一点,就是一种简单可扩展的实现方式,在某些程度上,不要太在意性能。
底层通信建立在netty之上,基本没做任何改动
Java代码
public class HttpServerPipelineFactory implements ChannelPipelineFactory{
private ChannelUpstreamHandler channelUpstreamHandler;
public ChannelPipeline getPipeline() throws Exception{
// Create a default pipeline implementation.
ChannelPipeline pipeline= pipeline();
// Uncomment the following line if you want HTTPS
//SSLEngine engine= SecureChatSslContextFactory.getServerContext().createSSLEngine();
//engine.setUseClientMode(false);
//pipeline.addLast("ssl", new SslHandler(engine));
pipeline.addLast("decoder", new HttpRequestDecoder());
// Uncomment the following line if you don't want to handle HttpChunks.
pipeline.addLast("aggregator", new HttpChunkAggregator(1048576));
pipeline.addLast("encoder", new HttpResponseEncoder());
// Remove the following line if you don't want automatic content compression.
pipeline.addLast("deflater", new HttpContentCompressor());
//pipeline.addLast("handler", new HttpRequestHandler());
pipeline.addLast("handler", channelUpstreamHandler);
return pipeline;
}
public void setChannelUpstreamHandler(ChannelUpstreamHandler channelUpstreamHandler){
this.channelUpstreamHandler= channelUpstreamHandler;
}
}
相关spring配置
Java代码
<bean id="httpServerPipelineFactory" class="com.yunchao.cm.network.http.HttpServerPipelineFactory">
<property name="channelUpstreamHandler" ref="httpRequestHandler"/>
</bean>
Java代码
<bean id="httpRequestHandler" class="com.yunchao.cm.network.http.HttpRequestHandler">
<property name="urlMaps">
<map>
<entry key="/payorder">
<ref bean="payOrderCodecFactory"/>
</entry>
<entry key="/question">
<ref bean="questionCodecFactory"/>
</entry>
<entry key="/sms">
<ref bean="smsCodecFactory"/>
</entry>
代码太多,不全部贴出来,后面整理一下放到我的github上去。
基如此,我们还是得定义一个handler,继承simpleChannelUpstreamHander,并重写了messageReceied方法,具体在这里。
Java代码
QueryStringDecoder queryStringDecoder= new QueryStringDecoder(request.getUri());
String url= queryStringDecoder.getPath();
CodecFactory codecFactory= urlMaps.get(url);
if(null== codecFactory){
logger.error("unsupported url:{} request.", url);
//sendError(ctx, BAD_REQUEST);
e.getChannel().close();
return;
}
//获取cmwap网络中的手机号码
String phone= PhoneUtils.getPhone(request.getHeader("x-up-calling-line-id"));
if(request.getMethod().equals(HttpMethod.POST)){
ChannelBuffer content= request.getContent();
String postParams= content.toString(CharsetUtil.UTF_8);
logger.debug("request content:{}", postParams);
ClientRequestModel model=(ClientRequestModel) codecFactory.decode(postParams);
model.setProperty(model.MESSAGE_EVENT_KEY, e);
model.setProperty(model.HTTP_REQUEST_KEY, request);
model.setProperty(model.HTTP_PHONE_KEY, phone);
InetSocketAddress remoteAddress=(InetSocketAddress) e.getRemoteAddress();
model.setProperty(model.IP_KEY, remoteAddress.getAddress().getHostAddress());
logger.info("user request model:{}", model);
model.fireSelf();
Java代码
@Override
public DomainMessage fireSelf(){
DomainMessage em= new DomainMessage(this);
EventUtils.fireEvent(em,"alipayNotifyState");
return em;
}
看到这里基本上能够清楚了,是如何把客户端请求包装成ClientRequestModel了,且后面涉及到处理的对象,全部继承它,在整个架构之
中,has a优于 is
a,对于客户端netty的一些对象,也是存储在ClientRequestModel中,codec无非也是采用了xml/json/kv,如斯,实现
了字节与对象之间的转换。
除
此之外,突然想到刚来杭州工作的第一家公司,基于此,采用的架构师servlet充当服务器,因为这是一个公司内部的server,而不是一个平台,采用
的数据格式也比较单一,就是xml,但是采用的外部类库也是xstream来处理的,但是整个系统维持的日调用量也是在百万级别,运用的client则是
采用httpclient,对于不同请求后面挂的handler,是在容器启动时加载到内存中,其余也没有什么亮点了。