京东到家基于netty与websocket的实践

背景

在京东到家商家中心系统中,商家提出在 Web 端实现自动打印的需求,不须要人工盯守点击打印,直接打印小票,以节约人工成本。java

解决思路web

关于问题的思考逻辑:面试

第一种:想到的是能够用ajax来轮询服务端获取最新订单,也就是pullajax

第二种:咱们是否能够用相似推送的设计来实现,也就是pushspring

两种思路咱们评估其优缺点:tomcat

ajax方式实现简单,只须要定时从服务端pull数据便可,但也增长了不少次无效的轮询, 无形中增长服务端无效查询。websocket

push方式实现稍复杂,须要服务端与PC端保持链接,这就须要创建长链接,最终经过长链接的方式来实现push效果。网络

通过讨论,咱们选择了第二种,订单中心生产出的新订单,经过MQ的方式推送给web端,最终得到一个比较好的用户体验。session

方案介绍架构

关于长链接方案的选择,咱们参考了很多帖子,最终选择使用websocket协议来实现长链接,相似场景如IM,服务端即时推送等都使用了这个协议。

接下来咱们比较一下websocket的框架,比较主流的有netty、tomcat、socketIO 三个框架。

基于支持websocket的容器,开发简单,例如tomcat,但在高并发的支持不是很好,链接的时候容易链接断开,还有就是依赖容器。

netty-socketIO是在netty4基础之上作了一层封装,效率如同netty同样,是一个全平台方案,友好的API,京东的logbook也是用了socketIO来传递日志,也是咱们的一个备选方案。

netty是业内主流的NIO框架,netty对javaNIO作了封装,让开发者更多关注业务,下降开发成本,不少著名的RPC框架都采用了netty做为传输层,友好的API,功能强大,内置了不少编解码协议,实现websocket协议也是十分方便。

那咱们横向比较一下这些框架。

因此在选型方面咱们仍是定位在socketIO 与 netty 上面,在兼顾扩展性与灵活性的同时,咱们也考虑到netty能够提供http的功能,最终咱们选择了使用netty,固然socketIO封装了不少功能,也是十分强大,相比较来讲netty更适合咱们,比较轻量。

netty的特性

netty具备异步非阻塞的特性,传统IO是面向流的,NIO是面向缓冲区的,这也是它的非阻塞缘由所在。

netty的线程模型如图所示:

这种模型就是咱们常说的Reactor模型,boss线程实际上是一个独立的NIO线程池,用于接收client请求,默认线程池大小为1,worker线程池用于处理具体的读写操做,默认线程池大小为2*cpu个

在上述模型中要特别注意ExecutionHandler,ExecutionHandler是运行在worker线程中的,因此耗时的操做最好在线程池中运行, 好比IO或者计算,否则会影响整个netty的吞吐。

了解了这些,咱们根据本身的业务设计出流程以下图所示:

步骤(1) web端请求服务端进行注册,注册成功保持长链接。

步骤(2)服务端发送MQ。

步骤(3)netty将收到的消息推送给web端。

步骤(4)web端调用打印控件进行打印,打印控件需提早安装好(打印控件是pc上安装的一个驱动程序,用过JS方式来调用)。

若是调用JS成功,控件将把打印信息放入打印队列,若是不成功,重复步骤(4)

固然如今的结构只是单机版,不知足生产条件,那未来的结构可能会演变成以下图所示:

咱们会在服务端与netty之间创建路由层,路由层的主要职责:

第一:收集集群存活信息。

第二:记录落点,落在哪一台机器上面

第三:接收消息与分发消息

有了这三种能力,咱们就能够轻松的指定信息分发策略。这里咱们但愿使用http协议来路由,因此就须要netty有http短链接接收的能力 ,因此netty总体上须要长短链接两种能力。

讲了这么多,仍是来点干货,下面是部分代码。

netty启动类,咱们经过spring来启动netty,由于netty启动会阻塞主线程,因此须要在子线程中来启动netty,下面是启动参数。

接着来写咱们的ChannelInitializer,HttpServerCodec为编解码器,WSServerProtocolHandler为websocket协议握手,其中咱们更关注业务层面自定义的两个hander,httpRequestHandler,authorizeHandler。

httpRequestHandler的做用是处理url是否合法,接收参数,httpRequestHandler此方法中也能够根据URI来过滤,自定义本身的短链接请求。

authorizeHandler的做用是校验数据是否正确,若是正确会将channel保存到map中,经过map创建起业务ID与通道之间的关系。

校验的过程咱们在authorizeHandler中的channelRead展开,若是未经过,直接关闭当前channel,若是经过校验,则经过ctx.fireChannelRead(msg);方法将信息传入下一个handler去处理。

在项目里主要是以传递参数来进行数据校验的,也就是经过URL传参来实现。在httpRequestHandler中咱们将URL参数set到channel的attr中,并传递给了下一个handler,也就是authorizeHandler,因此在authorize方法中咱们能够利用get()方法获得参数值,u是通过加密的数据,咱们须要在这里进行解密,解密失败,可认为校验失败。

固然若是有跨应用的服务,也能够经过Cookie的方式来进行加密串的读写,经过request.getHeader 是能够获取Cookie中的信息,这就看具体业务了,示例代码以下:

这个map 能够理解为servlet中的session,当有信息须要传送给某个客户端时,咱们调用map.get(key)方式的到当前该客户端的channel,调用writeAndFlush方法将信息发送出去,下面举例经过接收MQ消息后的处理逻辑。

接下来有人可能想到,那若是通道关闭了怎么办?map中的channel是否是就失效了呢?那其实咱们还须要有一个相似心跳的机制去维护channel,间接的去维护这个map,若是是通道正常关闭,能够经过channelInactive方法来监听,若是是长时间空闲:在项目中咱们使用了增长的IdleStateHandler来处理,经过覆盖userEventTriggered方法来监听空闲channel,当某个channel到达咱们设置的超时时间时,netty会回调此方法。

至此,核心部分已经处理完成,剩下的就是经过保存的channel来发送信息给客户端了。

最后在web端,咱们采用了 reconnecting-websocket,它是一个小型的 JavaScript 库,封装了 WebSocket API, 提供了在链接断开时自动重连的机制,很可以帮助咱们完成断开重连的操做。

遇到的问题

通过测试,在ws的uri后面不能传递参数,否则在netty实现websocket协议握手的时候会出现断开链接的状况,针对这种状况在websocketHandler以前作了一层httpHander过滤,将传递参数放入channel的attr中,而后重写request的uri,并传入下一个管道中,基本上解决这个问题。

在读写空闲的时候尽可能以发心跳包的方式维护链接,但在客户端因为网络不稳定或者是服务端重启,链接会断开,瞬间有可能接收不到订单消息,为此在客户端须要实现断开重连机制,此问题咱们采用 reconnecting-websocket的js框架,此框架扩展了原生websocket的实现,作了断开重连机制,有效的防止断开后不能及时链接。

在测试过程当中因为控件与小票机的问题,可能会出现打印异常或者小票机没纸的状况,Lodop控件实际上是将打印信息放入电脑的打印队列,若是没纸了,小票机会报警,再次放入小票纸,打印机会自动打印队列中的数据。

出现调用控件异常偶尔发生,如今处理办法是在js中进行了的try catch 若是失败 进行重试,重试次数自定义,超太重试次数暂不作处理,此处还不太严谨,须要在进行优化。

总结

经过上面的实践,咱们基本已经实现了web端的自动打印,通过长时间的内部测试,服务端与客户端通讯稳定,咱们将灰度商家作用户体验。

在特定的场景下,选择适当的技术会提升咱们的效率,不然会拔苗助长。选择长链接,你们能够把握三个大原则:

服务端是否须要主动推送数据到客户端以实现控制的效果。

对于实时性的要求是否苛刻。

对于客户端是否须要关注其在线状态的实时变化。

以为不错请点赞支持,欢迎留言或进个人我的群855801563领取【架构资料专题目合集90期】、【BATJTMD大厂JAVA面试真题1000+】,本群专用于学习交流技术、分享面试机会,拒绝广告,我也会在群内不按期答题、探讨。