目录
websocket%20%E8%83%8C%E6%99%AF%E5%92%8C%E7%89%B9%E6%80%A7-toc" name="tableOfContents" style="margin-left:0px">一、websocket 背景和特性
websocket%20%E5%92%8C%20ajax%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%20%EF%BC%9F-toc" name="tableOfContents" style="margin-left:0px">二、websocket 和 ajax区别是什么 ?
websocket%20%E6%96%B9%E6%A1%88%E5%AF%B9%E6%AF%94%C2%A0-toc" name="tableOfContents" style="margin-left:0px">三、传统方案和 websocket 方案对比
服务端推送web方案
1.短轮询
2.长轮询
3.Websocket长连接
四、websocket 代码实现方案
1.tomcat实现websocket
2.netty实现websocket
为啥选netty不用tomcat?
websocket的连接过程
netty实现websocket
获取用户ip和连接token
心跳包
请求处理
消息协议
前端请求
后端返回
抓包查看
五、其他相关文章
websocket%20%E8%83%8C%E6%99%AF%E5%92%8C%E7%89%B9%E6%80%A7">一、websocket 背景和特性
WebSocket是HTML5中的协议,支持持久连续,本质上是为了解决HTTP协议本身的单向性问题:
因为请求必须由客户端向服务端发起,然后服务端进行响应。这个关系,对于一般的网页浏览和访问当然没问题,旦需要服务端主动向客户端发送消息时就麻烦了,因为此前TCP连接已经释放,根本找不到客户端在哪。
由于浏览器只支持HTTP,为能及时从服务器获取数据。要么定时去轮询,要么就靠长连接——客户端发起请求,服务端把这个连接攥在手里不回复,等有消息了再回,如果超时了客户端就再请求一次——这只是个减少了请求次数、实时性更好的轮询,本质没变。
WebSocket 则从技术根本上解决这个问题:它借用了Web端口和消息头来创建连接,后续数据传输又和基于TCP的Socket几乎完全一样,但封装了好多原本在Socket开发时需要我们手动去做功能。比如原生支持wss安全访问(跟https共用端口和证书)、创建连接时的校验、从数据帧中自动拆分消息包等等。
换句话说,原本在浏览器里只能使用HTTP协议,现在有了Socket,还是个更好用的Socket。
websocket%20%E5%92%8C%20ajax%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%20%EF%BC%9F" name="%E4%BA%8C%E3%80%81websocket%20%E5%92%8C%20ajax%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%20%EF%BC%9F">二、websocket 和 ajax区别是什么 ?
了解WebSocket背景和特性之后,就可以回答它能不能取代AJAX,它们之间的区别是什么这个问题了:
对于服务器与客户端双向通信,WebSocket简直是不二之选。如果不是还有少数旧版浏览器尚在服役的话,所有的轮询、长连接等方式早该废弃掉。那些整合多种双向推送消息方式的库(如http://Socket.IO、SignalR)当初最大卖点就是兼容所有浏览器版本,自动识别旧版浏览器并采取不同连接方式,现在也渐渐失去了优势——所有新版浏览器都兼容WebSocket,直接用原生就行了。
这点很像jQuery,在原生js难用时迅速崛起,当其他库和原生js都吸收了它的很多优势时,慢慢就不那么重要了。 但是,很大一部分AJAX使用场景仍然是传统的请求-响应形式,比如获取json数据、post表单之类。这些功能虽然靠WebSocket也能实现,但就像在原本传输数据流的TCP之上定义了基于请求的HTTP协议一样,也要在WebSocket之上重新定义一种新的协议,最少也要加个request id用来区分每次响应数据对应的请求吧。 ……何苦一层叠一层地造个新轮子呢?直接使用AJAX不是更简单、更成熟吗?
另外还有一种情况,也就是传输大文件、图片、媒体流的时候,最好还是老老实实用HTTP来传。如果一定要用WebSocket的话,至少也专门为这些数据专门开辟个新通道,而别去占用那条用于推送消息、对实时性要求很强的连接。否则会把串行的WebSocket彻底堵死的。
所以说,WebSocket在用于双向传输、推送消息方面能够做到灵活、简便、高效,但在普通的Request-Response过程中并没有太大用武之地,比起普通的HTTP请求来反倒麻烦了许多,甚至更为低效。
每项技术都有自身的优缺点,在适合它的地方能发挥出最大长处,而看到它的几个优点就不分场合地全方位推广的话,可能会适得其反。
websocket%20%E6%96%B9%E6%A1%88%E5%AF%B9%E6%AF%94%C2%A0" name="%E4%B8%89%E3%80%81%E4%BC%A0%E7%BB%9F%E6%96%B9%E6%A1%88%E5%92%8C%20websocket%20%E6%96%B9%E6%A1%88%E5%AF%B9%E6%AF%94%C2%A0">三、传统方案和 websocket 方案对比
不仅仅是IM通讯系统,在很多业务中都会有服务端需要主动推送web的场景。比如小红点提醒,新消息提醒,审批流提醒等。那么常见的推送方案有哪些?
服务端推送web方案
1.短轮询
短轮询,就是web端不停地间隔一段时间向服务端发一个 HTTP 请求,如果有新消息,就会在某次请求返回。
比如某OA系统,用户需要收到小红点,审批流提醒等信息,为了方便,就直接采用每秒1次的请求,等待后端返回数据。
适用场景:
- 扫码登录:短时间内频繁查询二维码状态
- 小OA系统:客户端使用量不大的情况下可以使用
缺点:
- 大量无效请求:大量的无效请求,浪费服务器资源
- 服务端请求压力大:万人群聊频繁访问,上万并发服务扛不住。
2.长轮询
长轮询和短轮询相比,一个最大的改进之处在于:
- 短轮询模式下,服务端不管本轮有没有新消息产生,都会马上响应并返回。而长轮询模式当本次请求没有获取到新消息时,并不会马上结束返回,而是会在服务端“悬挂(hang)”,等待一段时间;
- 如果在等待的这段时间内有新消息产生,就能马上响应返回。
这也意味着web端的请求超时时长得设置长一些。
优点:相比短轮询模式
- 大幅降低短轮询模式中客户端高频无用的轮询导致的网络开销和功耗开销
- 降低了服务端处理请求的 QPS
缺点:
- 无效请求:长轮询在超时时间内没有获取到消息时,会结束返回,因此仍然没有完全解决客户端“无效”请求的问题。
- 服务端压力大:服务端悬挂(hang)住请求,只是降低了入口请求的 QPS,并没有减少对后端资源轮询的压力。假如有 1000 个请求在等待消息,可能意味着有 1000 个线程在不断轮询消息存储资源。(轮询转移到了后端)
3.Websocket长连接
长轮询
和短轮询
都算作是服务端没法主动向客户端推送的一种曲线救国
的方式,那最好的方案,就是能不能解决这个问题,因此诞生了websocket。
实现原理:客户端和服务器之间维持一个 TCP/IP 长连接,全双工通道。
基本弥补了上面的缺点,唯一的缺点就是实现起来可能会有些复杂,我们需要去管理链接。
四、websocket 代码实现方案
支持websocket的容器很多,我们实现一般用两种常见方案。
1.tomcat实现websocket
原理和使用细节可以查看如何使用Tomcat实现WebSocket即时通讯服务服务端_tomcat websocket-CSDN博客
2.netty实现websocket
用netty实现websocket可以看项目代码,或者参考文章:netty对websocket协议的实现_netty实现websocket-CSDN博客
为啥选netty不用tomcat?
- netty是nio基于事件驱动的多路复用框架,使用单线程或少量线程处理大量的并发连接。相比之下,Tomcat 是基于多线程的架构,每个连接都会分配一个线程,适用于处理相对较少的并发连接。最近的 Tomcat 版本(如 Tomcat 8、9)引入了 NIO(New I/O)模型。所以这个点并不是重点。
- Netty 提供了丰富的功能和组件,可以灵活地构建自定义的网络应用。它具有强大的编解码器和处理器,可以轻松处理复杂的协议和数据格式。Netty 的扩展性也非常好,可以根据需要添加自定义的组件。比如我们可以用netty的pipeline方便的进行前置后置的处理,可以用netty的心跳处理器来检查连接的状态。这些都是netty的优势。
websocket的连接过程
客户端依靠发起HTTP握手,告诉服务端进行WebSocket协议通讯,并告知WebSocket协议版本。服务端确认协议版本,升级为WebSocket协议。之后如果有数据需要推送,会主动推送给客户端。
连接开始时,客户端使用HTTP协议和服务端升级协议,升级完成后,后续数据交换遵循WebSocket协议。我们看看Request Headers
其中关键的字段就是Upgrade
,Connection
,告诉 Apache 、 Nginx 等服务器:注意啦,我发起的是Websocket协议,不再 使用原先的HTTP。其中,Sec-WebSocket-Key
当成是请求id就好了。
Sec-WebSocket-Accept
: 用来告知服务器愿意发起一个websocket连接, 值根据客户端请求头的Sec-WebSocket-Key计算出来。
netty实现websocket
public void run() throws InterruptedException {// 服务器启动引导对象ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 128).option(ChannelOption.SO_KEEPALIVE, true).handler(new LoggingHandler(LogLevel.INFO)) // 为 bossGroup 添加 日志处理器.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {ChannelPipeline pipeline = socketChannel.pipeline();//30秒客户端没有向服务器发送心跳则关闭连接pipeline.addLast(new IdleStateHandler(30, 0, 0));// 因为使用http协议,所以需要使用http的编码器,解码器pipeline.addLast(new HttpServerCodec());// 以块方式写,添加 chunkedWriter 处理器pipeline.addLast(new ChunkedWriteHandler());/*** 说明:* 1. http数据在传输过程中是分段的,HttpObjectAggregator可以把多个段聚合起来;* 2. 这就是为什么当浏览器发送大量数据时,就会发出多次 http请求的原因*/pipeline.addLast(new HttpObjectAggregator(8192));//保存用户ippipeline.addLast(new HttpHeadersHandler());/*** 说明:* 1. 对于 WebSocket,它的数据是以帧frame 的形式传递的;* 2. 可以看到 WebSocketFrame 下面有6个子类* 3. 浏览器发送请求时: ws://localhost:7000/hello 表示请求的uri* 4. WebSocketServerProtocolHandler 核心功能是把 http协议升级为 ws 协议,保持长连接;* 是通过一个状态码 101 来切换的*/pipeline.addLast(new WebSocketServerProtocolHandler("/"));// 自定义handler ,处理业务逻辑pipeline.addLast(new NettyWebSocketServerHandler());}});// 启动服务器,监听端口,阻塞直到启动成功serverBootstrap.bind(WEB_SOCKET_PORT).sync();System.out.println("启动成功");
}
明白了websocket的升级过程,对netty的处理的就比较简单了。websocket初期是通过http请求,进行升级,建立双方的连接。
1.所以编解码器需要用到
HttpServerCodec
。2.
WebSocketServerProtocolHandler
是netty进行websocket升级的处理器。在这期间会抹除http相关的信息,比如请求头啥的。如果想获取相关信息,需要在这之前获取。3.
HttpHeadersHandler
是我们自己的处理器。赶在websocket升级之前,获取用户的ip地址,然后保存到channel的附件里。4.
NettyWebSocketServerHandler
是我们的业务处理器,里面处理客户端的事件。5.
IdleStateHandler
实现心跳检测。
获取用户ip和连接token
我们获取用户ip的点有两处,注册和连接认证。两处都是需要从socket建立连接的时候,赶在协议升级前,保存用户的ip地址。
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if (msg instanceof FullHttpRequest) {FullHttpRequest request = (FullHttpRequest) msg;UrlBuilder urlBuilder = UrlBuilder.ofHttp(request.uri());// 获取token参数String token = Optional.ofNullable(urlBuilder.getQuery()).map(k->k.get("token")).map(CharSequence::toString).orElse("");NettyUtil.setAttr(ctx.channel(), NettyUtil.TOKEN, token);// 获取请求路径request.setUri(urlBuilder.getPath().toString());HttpHeaders headers = request.headers();String ip = headers.get("X-Real-IP");if (StringUtils.isEmpty(ip)) {//如果没经过nginx,就直接获取远端地址InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();ip = address.getAddress().getHostAddress();}NettyUtil.setAttr(ctx.channel(), NettyUtil.IP, ip);ctx.pipeline().remove(this);ctx.fireChannelRead(request);}else{ctx.fireChannelRead(msg);}}
}
在协议升级前,消息体还是FullHttpRequest
类型的,这个if逻辑只会走一次,以后都不会了。所以我们要趁着在升级前,获取请求头里的ip,保存到channel
的附件中,以后需要的时候都能提取。
token的原理我们会在后面的握手认证
介绍。
心跳包
如果用户突然关闭网页,是不会有断开通知给服务端的。那么服务端永远感知不到用户下线。因此需要客户端维持一个心跳,当指定时间没有心跳,服务端主动断开,进行用户下线操作。
直接接入netty的现有组件new IdleStateHandler(30, 0, 0)
可以实现30秒链接没有读请求,就主动关闭链接。我们的web前端需要保持每10s发送一个心跳包。
请求处理
自己实现的处理器NettyWebSocketServerHandler
接受websocket信息。根据消息类型进行路由处理。
目前请求对websocket依赖很低,只做这一件事
@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {WSBaseReq wsBaseReq = JSONUtil.toBean(msg.text(), WSBaseReq.class);WSReqTypeEnum wsReqTypeEnum = WSReqTypeEnum.of(wsBaseReq.getType());switch (wsReqTypeEnum) {case LOGIN:this.webSocketService.handleLoginReq(ctx.channel());log.info("请求二维码 = " + msg.text());break;case HEARTBEAT:break;default:log.info("未知类型");}}
消息协议
用websocket的目的,主要是用于后端推送前端,前端能用http的就尽量用http。这样的好处是,http丰富的拦截器,注解,请求头等功能,可以更好地实现或者是收口我们想要的功能。尽量对websocket的依赖降到最低。
前后端的交互用的是json串,里面通过type标识次此次的事件类型。
前端请求
{
type:1//1.请求登录二维码,2心跳检测
}
1.请求登录二维码
发送type=1从后端请求一个登录二维码
2.心跳包
后端返回
后端的返回场景就多了。
{type:1//1.登录返回二维码 2.用户扫描成功等待授权 3.用户登录成功返回用户信息 4.收到消息 5.上下线推送6.前端token失效 7.拉黑用户(隐藏它的所有消息)data:jsondata//根据不同的类型有不同的返回对象
}
抓包查看
也可以通过f12抓包查看前后端的消息交互,用于问题排查。很多人用模拟websocket客户端的方式,连接上B站APP,进行弹幕的捕捉。
五、其他相关文章
Java网络编程Socket(使用字节流)