WebSocket实现群发和单聊--Springboot实现
一:WebSocket原理
1、要谈WebSocket就不得不提起HTTP连接
WebSocket是HTML5出的东西(协议,就是大家一起约定好的东西),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充。有交集,但是并不是全部。当然他们都属于网络的7层协议中的应用层。
另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系。
通俗来说,你可以用HTTP协议传输非Html数据,就是这样。再简单来说,层级不一样。
此外,ws协议是全双工协议,意味着不光服务器向客户端推送,客户端也可以像服务器发送请求。
2、Websocket是什么样的协议,具体有什么优点
首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
1) HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。
2)首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。在握手阶段是一样的。
在三次握手进行通信的过程中,Websocket协议要比HTTP协议的握手请求中,多了几个东西。
这个就是Websocket的核心了,告诉Apache、Nginx等后端服务器:发起的是Websocket协议,即协议是ws:// 而不是http://。
之后,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:我要验证你是不是真的是Websocket。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。不过现在还好,已经定下来啦~大家都使用的一个东西。
3、Websocket的作用
在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。
首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。相当于java代码的while循环,不段的发起请求,如果客户端连接数量过多的话,对服务器压力无疑是个重大的考验。
其次是 long poll,其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(就是请求到服务器后,一直不返回相应,也不超时),直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。
一种需要更快的速度,一种需要更多的'电话'。这两种都会导致'电话'的需求越来越高。
HTTP还是一个无状态协议。通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。
所以在这种情况下出现了,Websocket出现了。他解决了HTTP的这几个难题。
首先,被动性,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。所以我们其实一直建立者连接,一旦有消息了,服务器就会推送给客户端。
只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你)
这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。
那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(JAVA等)来处理。
简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)。
本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。
Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。
同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了。
至于怎么在不支持Websocket的客户端上使用Websocket。。答案是:不能。
但是可以通过上面说的 long poll 和 ajax 轮询来模拟出类似的效果。
二:代码实现
首先是jar包引入,springboot可以很容易的实现websocket协议的配置(springboot版本2.0以上)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
接下来就是配置启动文件了
package cn.chinotan.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * @program: test * @description: WebSocketConfig启动配置 * @author: xingcheng * @create: 2019-05-30 19:33 **/ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
紧接着就是业务逻辑处理
package cn.chinotan.websocket; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; /** * @program: test * @description: WebSocketServer 服务器 * @author: xingcheng * @create: 2019-05-30 19:34 **/ @ServerEndpoint("/websocket/{sid}") @Component @Slf4j public class WebSocketServer { /** * 静态变量,用来记录当前在线连接数。线程安全。 */ private static AtomicInteger onlineCount = new AtomicInteger(0); /** * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 */ private static CopyOnWriteArraySet& ff8 lt;WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); /** * 与某个客户端的连接会话,需要通过它来给客户端发送数据 */ private Session session; /** * 接收sid */ private String sid = ""; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { this.session = session; // 加入set中 webSocketSet.add(this); // 在线数加1 addOnlineCount(); log.info("有新窗口开始监听:" + sid + ",当前在线人数为" + getOnlineCount()); this.sid = sid; sendMessage("连接成功: " + sid); } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { //从set中删除 webSocketSet.remove(this); //在线数减1 subOnlineCount(); log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info("收到来自窗口" + sid + "的信息:" + message); //群发消息 for (WebSocketServer item : webSocketSet) { item.sendMessage(message); } } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } /** * 实现服务器主动推送 */ public void sendMessage(String message) { try { this.session.getBasicRemote().sendText(message); } catch (IOException e) { log.error("消息推送失败"); e.printStackTrace(); } } /** * 群发自定义消息 */ public static void sendInfo(String message, String sid) { log.info("推送消息到窗口" + sid + ",推送内容:" + message); for (WebSocketServer item : webSocketSet) { //这里可以设定只推送给这个sid的,为null则全部推送 if (sid == null) { item.sendMessage(message); } else if (item.sid.equals(sid)) { item.sendMessage(message); } } } public static int getOnlineCount() { return onlineCount.get(); } public static void addOnlineCount() { WebSocketServer.onlineCount.addAndGet(1); } public static void subOnlineCount() { WebSocketServer.onlineCount.decrementAndGet(); } }
可以看到,通过springboot的注解,可以很容易的实现ws协议的编写,类似于controller,并且还有对应的生命周期监听注解
最后服务器端,我们配置一个推送请求,通过平常的http协议编写,模拟服务器想客户端推送过程
package cn.chinotan.controller; import cn.chinotan.websocket.WebSocketServer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.util.List; /** * @program: test * @description: websocket控制器 * @author: xingcheng * @create: 2019-06-01 15:13 **/ @RestController @RequestMapping("/webSocket") public class WebSocketController { @Autowired WebSocketServer webSocketServer; @GetMapping("/server/push/{sid}") public void push(@PathVariable("sid") Integer sid) { if (0 == sid) { WebSocketServer.sendInfo(sid + " 1ff8 :全员注意啦!", null); } else { WebSocketServer.sendInfo(sid + ":注意啦! -> " + (sid + 1), String.valueOf(sid + 1)); } } }
最后,我们编写客户端的代码,采用html
<html> <head> <title>WebSocket测试页面</title> </head> <body> <script> var socket; if(typeof(WebSocket) == "undefined") { console.log("您的浏览器不支持WebSocket"); }else{ console.log("您的浏览器支持WebSocket"); let sid = getUrlParam("sid"); //实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接 if (!sid) { alert("sid不存在") }; socket = new WebSocket("ws://localhost:9000/websocket/" + sid); //打开事件 socket.onopen = function() { console.log("Socket 已打开"); socket.send("这是来自客户端的消息" + location.href + new Date()); }; //获得消息事件 socket.onmessage = function(msg) { console.log(msg.data); //发现消息进入 开始处理前端触发逻辑 }; //关闭事件 socket.onclose = function() { console.log("Socket已关闭"); }; //发生了错误事件 socket.onerror = function() { alert("Socket发生了错误"); //此时可以尝试刷新页面 } //离开页面时,关闭socket //jquery1.8中已经被废弃,3.0中已经移除 // $(window).unload(function(){ // socket.close(); //}); } function getUrlParam(paraName) { var url = document.location.toString(); var arrObj = url.split("?"); if (arrObj.length > 1) { var arrPara = arrObj[1].split("&"); var arr; for (var i = 0; i < arrPara.length; i++) { arr = arrPara[i].split("="); if (arr != null && arr[0] == paraName) { return arr[1]; } } return ""; } else { return ""; } } </script> </body> </html>
接下来就可以进行测试了:
首先,我们启动在谷歌浏览器上启动三个窗口,连接地址分别是file:///Users/xingcheng/Downloads/websocket.html?sid=21,file:///Users/xingcheng/Downloads/websocket.html?sid=22,file:///Users/xingcheng/Downloads/websocket.html?sid=23,代表3个不同的客户端
来看服务器和客户端的交互过程
服务器:
客户端:
可以看到,连接的过程全部监听到了
接下来模拟服务器的推送过程:群聊,访问连接:http://localhost:9000/webSocket/server/push/0
服务器:
客户端:
最后,验证单聊:22客户端通过服务器给23发送消息,推送地址:http://localhost:9000/webSocket/server/push/22
服务器:
客户端:
- Spring Boot整合WebSocket 实现消息群发(群聊)功能
- SpringBoot整合WebSocket实现实时消息群发功能
- Spring Boot之WebSocket实现点对点通讯
- 在Spring Boot框架下使用WebSocket实现聊天功能
- spring boot 注解方式实现websocket
- Spring Boot整合websocket实现群聊,点对点聊天
- SpringBoot WebSocket 实现简单的聊天功能
- springboot整合websocket实现一对一消息推送和广播消息推送
- spring boot 集成webSocket 实现前后端数据实时交互
- spring boot2.0.1实现websocket
- 在Spring Boot框架下使用WebSocket实现消息推送
- VUE+SpringBoot+Websocket实现前后端通信案例分享
- spring-boot websocket 配置和实现(包括html)
- springboot整合websocket实现一对一消息推送和广播消息推送
- spring配置websocket并实现群发/单独发送消息
- spring配置websocket并实现群发/单独发送消息
- spring boot中使用webSocket组件实现实时通讯
- 在Spring Boot框架下使用WebSocket实现消息推送
- [ Spring Boot ] 整合 Websocket 实现消息推送框架的设计笔记
- SpringBoot中WebSocket不能实现注入的问题