WebSocket协议概述
-
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。 WebSocket是真正实现了全双工通信的服务器向客户端推的互联网技术。 它是一种在单个TCP连接上进行全双工通讯协议。Websocket通信协议与2011年倍IETF定为标准RFC 6455,Websocket API被W3C定为标准。
-
全双工和单工的区别(了解): 全双工(Full Duplex)是通讯传输的一个术语。通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时(瞬时)进行信号的双向传输(A→B且B→A)。指A→B的同时B→A,是瞬时同步的。 单工、半双工(Half Duplex),所谓半双工就是指一个时间段内只有一个动作发生,举个简单例子,一条窄窄的马路,同时只能有一辆车通过,当目前有两辆车对开,这种情况下就只能一辆先过,等到头儿后另一辆再开,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是基于半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台。
-
推的技术和拉的技术(了解): 推送(PUSH)技术是一种建立在客户服务器上的机制,就是由服务器主动将信息发往客户端的技术。就像是广播电台播音。 同传统的拉(PULL)技术相比,最主要的区别在于推送(PUSH)技术是由服务器主动向客户机发送信息,而拉(PULL)技术则是由客户机主动请求信息。PUSH技术的优势在于信息的主动性和及时性。
-
简单的说,相对于服务端:拉的技术是被动向客户端提供数据,推的技术是主动向客户端提供数据。
-
互联网技术(了解): 互联网技术定义:互联网技术指在计算机技术的基础上开发建立的一种信息技术(直译:Internet Technology;简称:IT)。 该技术把互联网上分散的资源融为有机整体,实现资源的全面共享和有机协作,使人们能够透明地使用资源的整体能力并按需获取信息。
详细的通讯过程
- 1)客户端发起http请求,附加头信息为:“Upgrade Websocket”
- 2)服务端解析,并返回握手信息,从而建立连接
- 3)传输数据(双向) 4)客户端或服务端主动断开连接。客户端主动断开:客户端发起http请求,请求断开连接,服务端收到消息后断开WebSocket连接;服务端主动断开:直接断开WebSocket连接,客户端的API会立刻得知。
客户端-浏览器的支持
WebSocket通信的客户端使用的是浏览器,客户端操作的API是HTML5中新增的API,使用这些API可以让客户端(浏览器)和服务端(服务器)进行全双工的通讯。 支持的浏览器如下: 浏览器类型 浏览器版本 Chrome Supported in version 4+ Firefox Supported in version 4+ Internet Explorer Supported in version 10+ Opera Supported in version 10+ Safari Supported in version 5+ 问题出现了,Html5 websocket兼容性还不是很好,不是所有的浏览器都支持这些新的API,特别是在IE10以下。 但幸运的是现在绝大多数主流的浏览器都支持这些API,即使不支持的哪些旧的浏览器,也有解决方案。如: 为了处理不同浏览器和浏览器版本的兼容性,spring webscoket基于SockJS protocol提供了一种解决兼容性的方法,在底层屏蔽兼容性的问题,提供统一的,透明的,可理解性的webscoket解决方案。 SockJS 是一个浏览器上运行的 JavaScript 库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持,实现浏览器和 Web 服务器之间低延迟、全双工、跨域的通讯通道。
客户端的API
// 创建一个Socket实例(需要浏览器支持)ws:WebSocket协议地址开头var socket = new WebSocket('ws://localhost:8080'); //下面有几个回调函数,自动调用(什么时候调用?)// 打开Socket socket.onopen = function(event) { //握手成功后,会自动调用该函数 } // 监听消息:用来获取服务端的消息 socket.onmessage = function(event) { console.log('Client received a message',event); }; // 监听Socket的关闭 socket.onclose = function(event) { console.log('Client notified socket has closed',event); }; // 关闭Socket.... //socket.close()
服务端的API
/** * * 说明:WebScoket配置处理器 * 把处理器和拦截器注册到spring websocket中 * @author * @version 1.0 * @date 2016年10月27日 */@Component("webSocketConfig")//配置开启WebSocket服务用来接收ws请求@EnableWebSocketpublic class WebSocketConfig implements WebSocketConfigurer { //注入处理器 @Autowired private ChatWebSocketHandler webSocketHandler; @Autowired private ChatHandshakeInterceptor chatHandshakeInterceptor; public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { //添加一个处理器还有定义处理器的处理路径 registry.addHandler(webSocketHandler, "/ws").addInterceptors(chatHandshakeInterceptor); /* * 在这里我们用到.withSockJS(),SockJS是spring用来处理浏览器对websocket的兼容性, * 目前浏览器支持websocket还不是很好,特别是IE11以下. * SockJS能根据浏览器能否支持websocket来提供三种方式用于websocket请求, * 三种方式分别是 WebSocket, HTTP Streaming以及 HTTP Long Polling */ registry.addHandler(webSocketHandler, "/ws/sockjs").addInterceptors(chatHandshakeInterceptor).withSockJS(); } }/** * websocket的链接建立是基于http握手协议,我们可以添加一个拦截器处理握手之前和握手之后过程 * @author BoBo * */@Componentpublic class ChatHandshakeInterceptor implements HandshakeInterceptor{ /** * 握手之前,若返回false,则不建立链接 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Mapattributes) throws Exception { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession session = servletRequest.getServletRequest().getSession(false); //如果用户已经登录,允许聊天 if(session.getAttribute("loginUser")!=null){ //获取登录的用户 User loginUser=(User)session.getAttribute("loginUser") ; //将用户放入socket处理器的会话(WebSocketSession)中 attributes.put("loginUser", loginUser); System.out.println("Websocket:用户[ID:" + (loginUser.getId() + ",Name:"+loginUser.getNickname()+"]要建立连接")); }else{ //用户没有登录,拒绝聊天 //握手失败! System.out.println("--------------握手已失败..."); return false; } } System.out.println("--------------握手开始..."); return true; } /** * 握手之后 */ @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("--------------握手成功啦..."); }}/** * * 说明:WebSocket处理器 * @author * @version 1.0 * @date 2016年10月27日 */@Component("chatWebSocketHandler")public class ChatWebSocketHandler implements WebSocketHandler { //在线用户的SOCKETsession(存储了所有的通信通道) public static final Map USER_SOCKETSESSION_MAP; //存储所有的在线用户 static { USER_SOCKETSESSION_MAP = new HashMap (); } /** * webscoket建立好链接之后的处理函数--连接建立后的准备工作 */ @Override public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception { //将当前的连接的用户会话放入MAP,key是用户编号 User loginUser=(User) webSocketSession.getAttributes().get("loginUser"); USER_SOCKETSESSION_MAP.put(loginUser.getId(), webSocketSession); //群发消息告知大家 Message msg = new Message(); msg.setText("风骚的【"+loginUser.getNickname()+"】踩着轻盈的步伐来啦。。。大家欢迎!"); msg.setDate(new Date()); //获取所有在线的WebSocketSession对象集合 Set > entrySet = USER_SOCKETSESSION_MAP.entrySet(); //将最新的所有的在线人列表放入消息对象的list集合中,用于页面显示 for (Entry entry : entrySet) { msg.getUserList().add((User)entry.getValue().getAttributes().get("loginUser")); } //将消息转换为json TextMessage message = new TextMessage(GsonUtils.toJson(msg)); //群发消息 sendMessageToAll(message); } @Override /** * 客户端发送服务器的消息时的处理函数,在这里收到消息之后可以分发消息 */ //处理消息:当一个新的WebSocket到达的时候,会被调用(在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理) public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage message) throws Exception { //如果消息没有任何内容,则直接返回 if(message.getPayloadLength()==0)return; //反序列化服务端收到的json消息 Message msg = GsonUtils.fromJson(message.getPayload().toString(), Message.class); msg.setDate(new Date()); //处理html的字符,转义: String text = msg.getText(); //转换为HTML转义字符表示 String htmlEscapeText = HtmlUtils.htmlEscape(text); msg.setText(htmlEscapeText); System.out.println("消息(可存数据库作为历史记录):"+message.getPayload().toString()); //判断是群发还是单发 if(msg.getTo()==null||msg.getTo().equals("-1")){ //群发 sendMessageToAll(new TextMessage(GsonUtils.toJson(msg))); }else{ //单发 sendMessageToUser(msg.getTo(), new TextMessage(GsonUtils.toJson(msg))); } } @Override /** * 消息传输过程中出现的异常处理函数 * 处理传输错误:处理由底层WebSocket消息传输过程中发生的异常 */ public void handleTransportError(WebSocketSession webSocketSession, Throwable exception) throws Exception { // 记录日志,准备关闭连接 System.out.println("Websocket异常断开:" + webSocketSession.getId() + "已经关闭"); //一旦发生异常,强制用户下线,关闭session if (webSocketSession.isOpen()) { webSocketSession.close(); } //群发消息告知大家 Message msg = new Message(); msg.setDate(new Date()); //获取异常的用户的会话中的用户编号 User loginUser=(User)webSocketSession.getAttributes().get("loginUser"); //获取所有的用户的会话 Set > entrySet = USER_SOCKETSESSION_MAP.entrySet(); //并查找出在线用户的WebSocketSession(会话),将其移除(不再对其发消息了。。) for (Entry entry : entrySet) { if(entry.getKey().equals(loginUser.getId())){ msg.setText("万众瞩目的【"+loginUser.getNickname()+"】已经退出。。。!"); //清除在线会话 USER_SOCKETSESSION_MAP.remove(entry.getKey()); //记录日志: System.out.println("Socket会话已经移除:用户ID" + entry.getKey()); break; } } //并查找出在线用户的WebSocketSession(会话),将其移除(不再对其发消息了。。) for (Entry entry : entrySet) { msg.getUserList().add((User)entry.getValue().getAttributes().get("loginUser")); } TextMessage message = new TextMessage(GsonUtils.toJson(msg)); sendMessageToAll(message); } @Override /** * websocket链接关闭的回调 * 连接关闭后:一般是回收资源等 */ public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception { // 记录日志,准备关闭连接 System.out.println("Websocket正常断开:" + webSocketSession.getId() + "已经关闭"); //群发消息告知大家 Message msg = new Message(); msg.setDate(new Date()); //获取异常的用户的会话中的用户编号 User loginUser=(User)webSocketSession.getAttributes().get("loginUser"); Set > entrySet = USER_SOCKETSESSION_MAP.entrySet(); //并查找出在线用户的WebSocketSession(会话),将其移除(不再对其发消息了。。) for (Entry entry : entrySet) { if(entry.getKey().equals(loginUser.getId())){ //群发消息告知大家 msg.setText("万众瞩目的【"+loginUser.getNickname()+"】已经有事先走了,大家继续聊..."); //清除在线会话 USER_SOCKETSESSION_MAP.remove(entry.getKey()); //记录日志: System.out.println("Socket会话已经移除:用户ID" + entry.getKey()); break; } } //并查找出在线用户的WebSocketSession(会话),将其移除(不再对其发消息了。。) for (Entry entry : entrySet) { msg.getUserList().add((User)entry.getValue().getAttributes().get("loginUser")); } TextMessage message = new TextMessage(GsonUtils.toJson(msg)); sendMessageToAll(message); } @Override /** * 是否支持处理拆分消息,返回true返回拆分消息 */ //是否支持部分消息:如果设置为true,那么一个大的或未知尺寸的消息将会被分割,并会收到多次消息(会通过多次调用方法handleMessage(WebSocketSession, WebSocketMessage). ) //如果分为多条消息,那么可以通过一个api:org.springframework.web.socket.WebSocketMessage.isLast() 是否是某条消息的最后一部分。 //默认一般为false,消息不分割 public boolean supportsPartialMessages() { return false; } /** * * 说明:给某个人发信息 * @param id * @param message * @author 传智.BoBo老师 * @throws IOException * @time:2016年10月27日 下午10:40:52 */ private void sendMessageToUser(String id, TextMessage message) throws IOException{ //获取到要接收消息的用户的session WebSocketSession webSocketSession = USER_SOCKETSESSION_MAP.get(id); if (webSocketSession != null && webSocketSession.isOpen()) { //发送消息 webSocketSession.sendMessage(message); } } /** * * 说明:群发信息:给所有在线用户发送消息 * @author 传智.BoBo老师 * @time:2016年10月27日 下午10:40:07 */ private void sendMessageToAll(final TextMessage message){ //对用户发送的消息内容进行转义 //获取到所有在线用户的SocketSession对象 Set > entrySet = USER_SOCKETSESSION_MAP.entrySet(); for (Entry entry : entrySet) { //某用户的WebSocketSession final WebSocketSession webSocketSession = entry.getValue(); //判断连接是否仍然打开的 if(webSocketSession.isOpen()){ //开启多线程发送消息(效率高) new Thread(new Runnable() { public void run() { try { if (webSocketSession.isOpen()) { webSocketSession.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } } } }
一个简单的demo
var path = '<%=basePath%>'; var uid='${sessionScope.loginUser.id}'; //发送人编号 var from='${sessionScope.loginUser.id}'; var fromName='${sessionScope.loginUser.nickname}'; //接收人编号 var to="-1"; // 创建一个Socket实例 //参数为URL,ws表示WebSocket协议。onopen、onclose和onmessage方法把事件连接到Socket实例上。每个方法都提供了一个事件,以表示Socket的状态。 var websocket; //不同浏览器的WebSocket对象类型不同 //alert("ws://" + path + "/ws?uid="+uid); if ('WebSocket' in window) { websocket = new WebSocket("ws://" + path + "ws"); console.log("=============WebSocket"); //火狐 } else if ('MozWebSocket' in window) { websocket = new MozWebSocket("ws://" + path + "ws"); console.log("=============MozWebSocket"); } else { websocket = new SockJS("http://" + path + "ws/sockjs"); console.log("=============SockJS"); } console.log("ws://" + path + "ws"); //打开Socket, websocket.onopen = function(event) { console.log("WebSocket:已连接"); } // 监听消息 //onmessage事件提供了一个data属性,它可以包含消息的Body部分。消息的Body部分必须是一个字符串,可以进行序列化/反序列化操作,以便传递更多的数据。 websocket.onmessage = function(event) { console.log('Client received a message',event); //var data=JSON.parse(event.data); var data=$.parseJSON(event.data); console.log("WebSocket:收到一条消息",data); //2种推送的消息 //1.用户聊天信息:发送消息触发 //2.系统消息:登录和退出触发 //判断是否是欢迎消息(没用户编号的就是欢迎消息) if(data.from==undefined||data.from==null||data.from==""){ //===系统消息 $("#contentUl").append("
聊天室系统
当前登录用户:${sessionScope.loginUser!=null?sessionScope.loginUser.nickname:"请登录" }
demo2
var loginUserID = 1; var webSocketProtocol = ('https:' == document.location.protocol) ? "wss" : "ws"; var url = webSocketProtocol + "://" + window.location.host + ":8888/csm/v2.0/message/" + loginUserID + "/"; // var url = webSocketProtocol + "://" + window.location.host + "/csm/v2.0/message/" + loginUserID + "/"; console.log(url); var socket = new WebSocket(url); console.log(socket); // handle the message socket.onmessage = function (e) { console.log(e.data); var item = JSON.parse(e.data); $("#dNotification").append(renderAlertItem(item)); }