前面可能廢話太多了,下面進入正題,講解spring具體的實現方式。javascript
先來講一下幾個核心的步驟吧: 1.構建websocket服務端業務處理程序 2.構建websocket服務端自身配置程序 3.註冊websocket服務端、配置、攔截過濾器等 3.客戶端發送握手請求並創建鏈接``` 這裏輸入代碼html
4.客戶端發送消息給服務端 5.服務端處理客戶端發送來的消息 6.服務端發送消息給客戶端(指定或者羣發) 7.客戶端展示。 所需jar包如圖: https://static.oschina.net/uploads/img/201609/12105945_t83F.png 第一步:構建websocket spring實現方式中,有一個核心的接口WebSocketHandler,此接口中定義了websokcet服務端核心的幾個方法,如用戶成功鏈接服務端方法afterConnectionEstablished、處理客戶端發送消息的方法handleTextMessage等等。查看源碼發現其有一個重要的實現類:.AbstractWebSocketHandler該類是一個抽象類,其有兩個實現類分別爲:BinaryWebSocketHandler, TextWebSocketHandler。BinaryWebSocketHandler此類中定義了特殊的方法handleBinaryMessage(WebSocketSession session, BinaryMessage message),看其源碼中,BinaryMessage 構造函數裏面,能夠傳入一個字節形的數組,經過集成該類可用次方法用於附件上傳等功能。 通常信息傳遞,可經過繼承TextWebSocketHandler類來實現。下面先貼出代碼: websockethandler處理程序。
package cn.com.mt.websocket;前端
import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map;java
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler;jquery
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject;web
import cn.com.mt.model.Common_Constant; import cn.com.mt.model.UserInfo; /**redis
*/ public class ContextWebSocketHandler extends TextWebSocketHandler{spring
/* * 用於存放在線用戶集合,key用用戶的id,真實生產環境中,未避免大數據出現的性能、內存、速度等一系列問題,採起內存數據庫如redis存儲 */ private static final Map<String,WebSocketSession> users; static { users = new HashMap<String,WebSocketSession>(); } /** * 當客戶端有消息發送過來時,會進入此方法進行處理 * session:消息來源者 * message:封裝了發送過來的消息信息 * 說明:再此處說明一下,消息格式最好以下:{to:消息接收者ID/Username惟一標識/服務端標識...,content:消息內容},方便業務操做 */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { super.handleTextMessage(session, message); //若是想取得原始session中封存的用戶身份等信息,可經過:session.getAttributes().get(名稱)方法取得 UserInfo user = (UserInfo)session.getAttributes().get(Common_Constant.USER_INFO); //經過message.getPayload()方法獲取客戶端發送過來的有效信息 String content = message.getPayload(); JSONObject obj = JSON.parseObject(content); //取得標識令牌 String token = obj.getString("to"); if("server".equals(token)){//發送過來的信息爲服務器接收的系統信息,則處理相關係統級業務邏輯,此處只作原樣信息返回 TextMessage returnMessage = new TextMessage("{'token':'socket_info','fromUser':'server','content':'信息已經收到,內容爲:"+obj.getString("content")+"'}"); session.sendMessage(returnMessage); }else if("all".equals(token)){//發送過來的信息爲廣播信息,則調用所有在線人員接收信息方法 TextMessage returnMessage = new TextMessage("{'token':'broadcast','fromUser':'"+user.getUsername()+"','content':'"+obj.getString("content")+"'}"); sendMessageToUsers(returnMessage); }else{//發給我的接收 TextMessage returnMessage = new TextMessage("{'token':'info','fromUser':'"+user.getUsername()+"','toUser':'"+obj.getString("toUser")+"','content':'"+obj.getString("content")+"'}"); sendMessageToUser(obj.getString("to"),returnMessage); } } /** * 用戶成功鏈接成功後會調用該方法 * 說明:次方法中,咱們在實際生產環境中可能用到的場景如: * 1.用戶上線以後,接收自身的離線消息。 * 2.刷新全局在線用戶列表 */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { UserInfo user = (UserInfo)session.getAttributes().get(Common_Constant.USER_INFO); if(null != user){ users.put(user.getUsername(), session);//放入在線用戶集合中 } //發送給當前登陸用戶在線用戶列表信息 sendMessageToUsers(new TextMessage("{'token':'refreshlines','list':"+getlineUsers()+"}")); } /** * 給某個用戶發送消息 * * @param userName * @param message */ public void sendMessageToUser(String username, TextMessage message) { WebSocketSession user = users.get(username); /* * 斷定接收信息的一方是否存在而且在線狀態,若是在線就發送 * 說明:在這個地方,真實的應用裏面,處理邏輯應該是先在當前在線用戶列表裏面,先取指定的用戶是否在線,若是不在線,則再去數據庫中查找該用戶是否存在,若是存在,則該消息 * 將加入離線消息存儲邏輯處理,待該用戶上線時,服務器推送離線消息給該用戶。 */ if(null != user && user.isOpen()){ try { user.sendMessage(message); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * 給全部在線用戶發送消息 * * @param message */ public void sendMessageToUsers(TextMessage message) { for (Map.Entry<String,WebSocketSession> entry : users.entrySet()) { try { if (entry.getValue().isOpen()) { entry.getValue().sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } } } /** * 返回在線用戶列表,json字符串格式 * @return */ public String getlineUsers(){ return JSON.toJSONString(users.keySet()); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { if(session.isOpen()){ session.close(); } System.out.println("傳輸處理錯誤......"); users.remove(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { System.out.println("websocket connection closed......"); users.remove(session); } @Override public boolean supportsPartialMessages() { return false; }
}數據庫
握手攔截器經過繼承HttpSessionHandshakeInterceptor實現,以下代碼:
package cn.com.mt.websocket;json
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import cn.com.mt.model.Common_Constant; import cn.com.mt.model.UserInfo; /** * HandShakeInterceptor是websocket握手攔截器,用於攔截websocket初始化鏈接的請求 * @author 寒冰 * QQ羣號: 67746867 * */ public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor{
/** * 握手以前調用該方法 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("握手開始"); if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession session = servletRequest.getServletRequest().getSession(false); if (session != null) { //握手之初,封裝一下當前用戶的信息,爲的是在後面的消息處理邏輯中,取出session中封裝的用戶信息 UserInfo user = (UserInfo) session.getAttribute(Common_Constant.USER_INFO); if (user==null) { attributes.put(Common_Constant.USER_INFO,null); }else{ attributes.put(Common_Constant.USER_INFO,user); } } } return super.beforeHandshake(request, response, wsHandler, attributes); }
@Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { System.out.println("握手完成"); super.afterHandshake(request, response, wsHandler, ex); }
}
經過xml註冊websocket相關信息以下: spring-websocket.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 聲明服務端處理程序 --> <bean id="contextwebsocketHandler" class="cn.com.mt.websocket.ContextWebSocketHandler"/> <!-- 註冊服務端處理程序路徑及相關握手攔截器 --> websocket:handlers <websocket:mapping path="/chandler" handler="contextwebsocketHandler"/> websocket:handshake-interceptors <bean class="cn.com.mt.websocket.HandshakeInterceptor"></bean> </websocket:handshake-interceptors> </websocket:handlers> <!-- 註冊服務端處理程序路徑及相關握手攔截器 開啓sockjs方式,開啓以後,前端必須用sockjs方式發起請求鏈接--> websocket:handlers <websocket:mapping path="/jhandler" handler="contextwebsocketHandler"/> websocket:handshake-interceptors <bean class="cn.com.mt.websocket.HandshakeInterceptor"></bean> </websocket:handshake-interceptors> websocket:sockjs/ </websocket:handlers>
<!-- 配置websocket消息的最大緩衝區長度 --> <bean class="org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean"> <property name="maxTextMessageBufferSize" value="8192"/> <property name="maxBinaryMessageBufferSize" value="8192"/> </bean>
</beans>
上述的代碼中,用到了兩個本身寫的類,一個是實體類UserInfo,一個是用於定義常量的類,以下: UserInfo:
package cn.com.mt.model; /** * 用戶信息實體 *@author 寒冰 *QQ羣號: 67746867 * */ public class UserInfo {
private int id; private String username;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
}
Common_Constant:
package cn.com.mt.model; /** * 系通通一常量定義實體 * @author 寒冰 * QQ羣號: 67746867 * */ public class Common_Constant {
public static final String USER_INFO = "USER_INFO";//用戶身份信息 }
同時爲了記錄登錄用戶信息,寫了一個action,LoginAction,代碼以下:
package cn.com.mt.action;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;
import cn.com.mt.model.Common_Constant; import cn.com.mt.model.UserInfo;
/** * 登錄action *@author 寒冰 *QQ羣號: 67746867 * */ @Controller public class LoginAction {
/** * 登錄驗證 * 說明:此處僅爲闡明websocket在線用戶業務,不作用戶身份信息認證聲明。 * 特別說明:如作集羣部署,如需解決session同步問題,需作session共享。 * @param userinfo * @param request * @return */ @RequestMapping("/login.action") public String login(UserInfo userinfo,String index,HttpServletRequest request){ request.getSession().setAttribute(Common_Constant.USER_INFO, userinfo); return "0".equals(index)?"index0":"index1"; } }
爲了可以websocketSession與httpsession能掛上關聯,需額外添加請求攔截器RequestListener,讓每一次的請求都帶着session信息,以下實現:
package cn.com.mt.listener;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.http.HttpServletRequest; /** * 用戶請求/響應監聽器 * * 此監聽器的主要做用是讓用戶每次向websocket發出的請求,都帶上session信息 * @author 寒冰 * QQ羣號: 67746867 * */ public class RequestListener implements ServletRequestListener {
@Override public void requestInitialized(ServletRequestEvent request) { // TODO Auto-generated method stub ((HttpServletRequest) request.getServletRequest()).getSession(); } @Override public void requestDestroyed(ServletRequestEvent arg0) { // TODO Auto-generated method stub }
}
最後貼出web.xml 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>yao_yan</display-name> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:spring-*.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet>
<!-- websocket URL標識 --> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/socket/</url-pattern> </servlet-mapping> <!-- action URL標識 --> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>.action</url-pattern> </servlet-mapping> <!-- 註冊請求監聽器 --> <listener> <listener-class>cn.com.mt.listener.RequestListener</listener-class> </listener> </web-app>
前端登錄login.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script> <script> $(function(){ $("#login").click(function(){ var username = $.trim($("#username").val()); if(""==username){ alert("enter username please....."); return ; }else{ f1.submit(); } }); }); </script> </head> <body> <form action="login.action" method="post" name="f1"> username:<input type="text" name="username" id="username"> <input type="radio" name="index" value="0" checked="checked">普通方式 <input type="radio" name="index" value="1">sockjs方式 <input type="button" id="login" value="login"> </form>
</body> </html> ```
普通方式jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script> <script> var socket_url = "ws://192.168.1.107:8888/yao_yan/socket/chandler"; var websocket ; $(function(){ //鏈接websocket服務器 $("#connect").click(function(){ websocket = new WebSocket(socket_url); websocket.onopen = function (evt) { onOpen(evt) }; websocket.onclose = function (evt) { onClose(evt) }; websocket.onmessage = function (evt) { onMessage(evt) }; websocket.onerror = function (evt) { onError(evt) }; }); //註冊發送消息按鈕事件 $("#send").click(function(){ var who = $("input[name='who']:checked").val();//獲取消息接收方類別 var content = $.trim($("#content").val());//獲取消息內容 var msg = "{"; if(""==content){ alert("輸入消息內容!"); return; } if("server"==who){ msg+="'to':'server',"; $("#msg").after("<p>你對服務器說:"+content+"</p>"); }else if("all"==who){ msg+="'to':'all',"; $("#msg").after("<p>你對你們說:"+content+"</p>"); }else if("one"==who){ var to_user = $.trim($("#userlist").val()); if(""==to_user){ alert("選擇接收消息方!"); return ; } msg+="'to':'"+to_user+"',"; $("#msg").after("<p>你對"+to_user+"說:"+content+"</p>"); } msg += "'content':'"+content+"'}"; sendd(msg); }); }); //鏈接成功websocket服務器時執行 function onOpen(evt) { $("#msg").text("<p>成功鏈接到websocket服務器</p>"); $("#send").removeAttr("disabled");//發送信息按鈕可用 $("#connect").attr("disabled","disabled");//鏈接服務器按鈕不可用 } //鏈接關閉時執行 function onClose(evt) { console.log("鏈接關閉。。。。"); } //服務器有推送消息過來時執行 function onMessage(evt) { var obj = eval('(' + evt.data + ')'); if(obj.token=="refreshlines"){//斷定token爲刷新在線用戶列表 refresh_line_users(obj.list); }else if(obj.token=="broadcast"){//廣播消息 $("#msg").after("<p>"+obj.fromUser+" 對你們說:"+obj.content+"</p>"); }else if(obj.token=="info"){//私信 $("#msg").after("<p>"+obj.fromUser+" 對你說:"+obj.content+"</p>"); }else if(obj.token=="socket_info"){//系統提醒消息 $("#msg").after("<p>"+obj.fromUser+" 響應消息:"+obj.content+"</p>"); } } //有錯誤信息時執行 function onError(evt) { console.log('Error occured: ' + evt.data); } //向服務器發送消息,此方式實際生產中考慮消息加密傳遞 function sendd(msg){ websocket.send(msg); } //刷新在線用戶列表 function refresh_line_users(data){ $("#userlist").empty(); for(var i=0;i<data.length;i++){ $("#userlist").append("<option value='"+$.trim(data[i])+"'>"+$.trim(data[i])+"</option>"); } $("#userlist").append("<option value='all'>所有</option>"); } </script> </head> <body> <h1>普通請求方式</h1> 響應提示區域:<div id="msg" style="color:red"></div> <hr> <br> 消息內容: <input type="text" id="content" name="content"> <br><br> 在線列表: <select id="userlist"> </select> <br><br> 我要發消息給:<input type="radio" id="server" name="who" value="server" checked="checked">服務器識別消息 <input type="radio" id="one" name="who" value="one">我的 <input type="radio" id="all" name="who" value="all">廣播 <br><br> <input type="button" id="connect" value="鏈接websocket服務器"> <input type="button" id="send" value="發送" disabled="disabled"> </body> </html>
sockjs方式:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <script type="text/javascript" src="js/jquery-1.10.2.min.js"></script> <script src="js/sockjs-1.1.1.min.js"></script> <script> var socket_url = "http://192.168.1.107:8888/yao_yan/socket/jhandler"; var sock ; $(function(){ //鏈接websocket服務器 $("#connect").click(function(){ sock = new SockJS(socket_url); sock.onopen = function (evt) { onOpen(evt) }; sock.onclose = function (evt) { onClose(evt) }; sock.onmessage = function (evt) { onMessage(evt) }; sock.onerror = function (evt) { onError(evt) }; }); //註冊發送消息按鈕事件 $("#send").click(function(){ var who = $("input[name='who']:checked").val();//獲取消息接收方類別 var content = $.trim($("#content").val());//獲取消息內容 var msg = "{"; if(""==content){ alert("輸入消息內容!"); return; } if("server"==who){ msg+="'to':'server',"; $("#msg").after("<p>你對服務器說:"+content+"</p>"); }else if("all"==who){ msg+="'to':'all',"; $("#msg").after("<p>你對你們說:"+content+"</p>"); }else if("one"==who){ var to_user = $.trim($("#userlist").val()); if(""==to_user){ alert("選擇接收消息方!"); return ; } msg+="'to':'"+to_user+"',"; $("#msg").after("<p>你對"+to_user+"說:"+content+"</p>"); } msg += "'content':'"+content+"'}"; sendd(msg); }); }); //鏈接成功websocket服務器時執行 function onOpen(evt) { $("#msg").after("<font color='red'>系統提示:</font><p>你已經成功鏈接到websocket服務器!!!如今能夠聊天了</p>"); $("#send").removeAttr("disabled");//發送信息按鈕可用 $("#connect").attr("disabled","disabled");//鏈接服務器按鈕不可用 } //鏈接關閉時執行 function onClose(evt) { console.log("鏈接關閉。。。。"); } //服務器有推送消息過來時執行 function onMessage(evt) { var obj = eval('(' + evt.data + ')'); if(obj.token=="refreshlines"){//斷定token爲刷新在線用戶列表 refresh_line_users(obj.list); }else if(obj.token=="broadcast"){//廣播消息 $("#msg").after("<p>"+obj.fromUser+" 對你們說:"+obj.content+"</p>"); }else if(obj.token=="info"){//私信 $("#msg").after("<p>"+obj.fromUser+" 對你說:"+obj.content+"</p>"); }else if(obj.token=="socket_info"){//系統提醒消息 $("#msg").after("<p>"+obj.fromUser+" 響應消息:"+obj.content+"</p>"); } } //有錯誤信息時執行 function onError(evt) { console.log('Error occured: ' + evt.data); } //向服務器發送消息,此方式實際生產中考慮消息加密傳遞 function sendd(msg){ sock.send(msg); } //刷新在線用戶列表 function refresh_line_users(data){ $("#userlist").empty(); for(var i=0;i<data.length;i++){ $("#userlist").append("<option value='"+$.trim(data[i])+"'>"+$.trim(data[i])+"</option>"); } $("#userlist").append("<option value='all'>所有</option>"); } </script> </head> <body> <h1>sockjs請求方式</h1> 響應提示區域:<div id="msg" style="color:red"></div> <hr> <br> 消息內容: <input type="text" id="content" name="content"> <br><br> 在線列表: <select id="userlist"> </select> <br><br> 我要發消息給:<input type="radio" id="server" name="who" value="server" checked="checked">服務器識別消息 <input type="radio" id="one" name="who" value="one">我的 <input type="radio" id="all" name="who" value="all">廣播 <br><br> <input type="button" id="connect" value="鏈接websocket服務器"> <input type="button" id="send" value="發送" disabled="disabled"> </body> </html>
下面的章節,會詳細介紹上述代碼。