例如 springboot 整合 websocket
要实现两种情况*********
1、实现本地一对一聊天功能 测试同一网段 实时双向通信(同一网段 例如内网 用户登录网站例如jobcenter 统一访问一个服务器上的网站)
2、实现一个内网一个外网进行通信由服务端外网 主动向内网发送消息测试是否能收到消息****(类似情景:像腾讯qq这种、或是你用手机随便关注一个公众号
发就会给你的手机推送消息 这个是外网将网页或是数据流发送到你的手机或是内网电脑上 主要测试 外网真的能通过websocket 直接访问内网问题)
3、实现服务端像客户端主动推送消息(同一网段 例如内网 用户登录网站例如jobcenter 统一访问一个服务器上的网站)
springboot 集成websocket 常见有三种方式 1、原生jdk注解,2、spring封装,3、spring封装STOMP。
第一种太原生了,使用内嵌容器和外部容器还要改代码
第三种封装的太多了,对新手不友好
第二种挺好的只是做了简单封装
了解WEBSOCKET是什么
https://www.cnblogs.com/wuyiran/p/6398480.html
Websocket是什么样的协议,具体有什么优点
首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。
我会顺便讲解下作用。
Upgrade: websocket
Connection: Upgrade
这个就是Websocket的核心了,告诉Apache、Nginx等服务器:注意啦,窝发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦大家都使用的一个东西 脱水:服务员,我要的是13岁的噢→_→
然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~
Upgrade: websocket
Connection: Upgrade
依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket。
然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。
后面的,Sec-WebSocket-Protocol 则是表示最终使用的协议。
为什么WebSocket至今没有流行起来?
最重要的原因就是兼容性,要是IE6就兼容早就流行起来了!问题是从IE10才开始兼容,虽然对低版本IE的兼容我们可以变相实现,但很多人就是不愿意使用变相兼容。他们觉得这种实现方式稳定性太差不可靠了。除了兼容性问题就是构架困难的问题,WebSocket的握手协议虽然是形似HTTP但它不是,所以很多HTTP服务器上难以实现WebSocket。几乎所有CGI方式运行的服务器程序都对WebSocket不友好,所以在服务器方面它就吃了个闭门羹。另外,目前也没有什么比较成熟的WebSocket框架可以用,所以一般的开发者都不会考虑它。
什么时候不该用WebSocket?
最大的问题依然是兼容性,虽然我们可以变相兼容到低版本IE上,但是那样就失去了原有的优势。所以如果一个项目面向的用户群使用的浏览器大部分都为低版本IE时就不该使用WebSocket。另外,WebSocket是长连接,如果客户端的程序没有数据实时同步的需求就没必要使用它。因为长连接会带来一定的服务器内存开销。如果Ajax就能轻松搞定的话就完全没必要兴师动众的搞WebSocket。
二、
https://www.cnblogs.com/merray/p/7918977.html
在这里,我们只需要知道,HTTP、WebSocket 等协议都是处于 OSI 模型的最高层: 应用层 。而 IP 协议工作在网络层(第3层),TCP 协议工作在传输层(第4层)。
3. WebSocket、HTTP 与 TCP
从上面的图中可以看出,HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的。我们可以把这些高级协议理解成对 TCP 的封装。
既然大家都使用 TCP 协议,那么大家的连接和断开,都要遵循 TCP 协议中的三次握手和四次挥手 ,只是在连接之后发送的内容不同,或者是断开的时间不同。
更详细内容可阅读:wireshark抓包图解 TCP三次握手/四次挥手详解
对于 WebSocket 来说,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层 TCP/IP 协议来建立 TCP 连接。建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。
而 WebSocket 则不同,它是一个完整的 应用层协议,包含一套标准的 API 。
所以,从使用上来说,WebSocket 更易用,而 Socket 更灵活。
5. HTML5 与 WebSocket
WebSocket API 是 HTML5 标准的一部分, 但这并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于浏览器的应用程序中使用。
实际上,许多语言、框架和服务器都提供了 WebSocket 支持,例如:
基于 C 的 libwebsocket.org
Nginx 对 WebSockets 的支持: NGINX as a WebSockets Proxy 、 NGINX Announces Support for WebSocket Protocol 、WebSocket proxying
三、要实现两种情况*********
1、实现本地一对一聊天功能 测试同一网段 实时双向通信
2、实现一个内网一个外网进行通信由服务端外网 主动向内网发送消息测试是否能收到消息
四、 既然 websocket 已经是长连接 为什么还用 netty 长连接呢???? tcp 这个长连接是如何使用的呢???下面就是例子
websocket是协议 netty这类的只是实现的载体的一种 也可以不用netty自己写
https://blog.csdn.net/zl1zl2zl3/article/details/84660271
京东到家基于netty与websocket的实践
在京东到家商家中心系统中,商家提出在 Web 端实现自动打印的需求,不需要人工盯守点击打印,直接打印小票,以节约人工成本。
解决思路
关于问题的思考逻辑:
第一种:想到的是可以用ajax来轮询服务端获取最新订单,也就是pull。
第二种:我们是否可以用类似推送的设计来实现,也就是push。
两种思路我们评估其优缺点:
ajax方式实现简单,只需要定时从服务端pull数据即可,但也增加了很多次无效的轮询, 无形中增加服务端无效查询。
push方式实现稍复杂,需要服务端与PC端保持连接,这就需要建立长连接,最终通过长连接的方式来实现push效果。
经过讨论,我们选择了第二种,订单中心生产出的新订单,通过MQ的方式推送给web端,最终获得一个比较好的用户体验。
方案介绍
关于长连接方案的选择,我们参考了不少帖子,最终选择使用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端的自动打印,经过长时间的内部测试,服务端与客户端通信稳定,我们将灰度商家做用户体验。
在特定的场景下,选择适当的技术会提高我们的效率,否则会适得其反。选择长连接,大家可以把握三个大原则:
服务端是否需要主动推送数据到客户端以实现控制的效果。
对于实时性的要求是否苛刻。
对于客户端是否需要关注其在线状态的实时变化。
更多关于netty技术持续研究中。
https://github.com/yudiandemingzi/spring-boot-websocket-study.
https://www.cnblogs.com/qdhxhz/p/8467715.html.
https://www.cnblogs.com/qdhxhz/p/8467715.html
https://www.cnblogs.com/qdhxhz/p/9438954.html
https://www.cnblogs.com/qdhxhz/p/9452237.html
https://www.cnblogs.com/qdhxhz/p/9452404.html
https://www.cnblogs.com/qdhxhz/p/9471659.html