spring websocket實現前後端通訊
專案要用websocket實現一個前後端實時通訊的功能,做完之後感觸頗多,寫個部落格回顧下整個歷程,也希望能給後面的同志有點幫助。
百度網盤示例原始碼:連結:https://pan.baidu.com/s/1Gi3qRyLO-lTnkVn4MqGIJA 密碼:4ovr
我使用springmvc的websocket元件,官網地址:點選開啟連結
示例內容:使用者登陸之後往設定session設定登陸名,之後跳轉到傳送訊息頁面,載入頁面時建立websocket連線,這時,springmvc攔截器攔截到websocket請求,把session中登陸名儲存到attributes中,這個值會對映到WebSocketSession裡,從而在SpringWebSocketHandler類中使用, 這部分看不懂沒關聯,結合下面的程式碼來看就懂了。
步驟一:新增maven依賴,注意兩點問題
1、spring的websocket依賴容器支援,我選用的是tomcat7.077,tomcat7以下是不支援websocket的
2、javax-servlet-api和java-websocket-api兩個包都限定了<scope>provided</scope>因為這兩個包tomcat容器已經帶了,provided表示編譯時使用,打包不會包含在war包裡,如不知道重啟啟動會報錯。
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.1.5.RELEASE</version> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <!-- spring websocket --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>4.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>4.1.7.RELEASE</version> </dependency> <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.0</version> <scope>provided</scope> </dependency>
步驟二:編輯SpringWebSocketConfig,根據spring文件,編寫websocketConfig,這裡可參看文件,xml配置和使用註解兩種方式,我選擇註解方式
registerWebSocketHandlers:這個方法是向spring容器註冊一個handler地址,我把他理解成requestMapping
addInterceptors:攔截器,當建立websocket連線的時候,我們可以通過繼承spring的HttpSessionHandshakeInterceptor來搞事情。
setAllowedOrigins:跨域設定,*表示所有域名都可以,不限制, 域包括ip:port, 指定*可以是任意的域名,不加的話預設localhost+本服務埠
withSockJS: 這個是應對瀏覽器不支援websocket協議的時候降級為輪詢的處理。
@Configuration
@EnableWebSocket
public class SpringWebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler(),"/websocket/socketServer")
.addInterceptors(new SpringWebSocketHandlerInterceptor()).setAllowedOrigins("*");
registry.addHandler(webSocketHandler(), "/sockjs/socketServer").setAllowedOrigins("http://localhost:28180")
.addInterceptors(new SpringWebSocketHandlerInterceptor()).withSockJS();
}
@Bean
public TextWebSocketHandler webSocketHandler(){
return new SpringWebSocketHandler();
}
}
步驟三:編寫SpringWebSocketHandlerInterceptor
這個是建立websocket連線是的攔截器,記錄建立連線的使用者的session以便根據不同session來通訊
public class SpringWebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
System.out.println("Before Handshake");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
//使用userName區分WebSocketHandler,以便定向傳送訊息
String userName = (String) session.getAttribute("SESSION_USERNAME"); //一般直接儲存user實體
if (userName!=null) {
attributes.put("WEBSOCKET_USERID",userName);
}
}
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
}
步驟四:編寫SpringWebSocketHandler
public class SpringWebSocketHandler extends TextWebSocketHandler {
private static final Map<String, WebSocketSession> users; //Map來儲存WebSocketSession,key用USER_ID 即線上使用者列表
//使用者標識
private static final String USER_ID = "WEBSOCKET_USERID"; //對應監聽器從的key
static {
users = new HashMap<String, WebSocketSession>();
}
public SpringWebSocketHandler() {}
/**
* 連線成功時候,會觸發頁面上onopen方法
*/
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("成功建立websocket連線!");
String userId = (String) session.getAttributes().get(USER_ID);
users.put(userId,session);
System.out.println("當前線上使用者數量:"+users.size());
//這塊會實現自己業務,比如,當用戶登入後,會把離線訊息推送給使用者
//TextMessage returnMessage = new TextMessage("成功建立socket連線,你將收到的離線");
//session.sendMessage(returnMessage);
}
/**
* 關閉連線時觸發
*/
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
String userId= (String) session.getAttributes().get(USER_ID);
System.out.println("使用者"+userId+"已退出!");
users.remove(userId);
System.out.println("剩餘線上使用者"+users.size());
}
/**
* js呼叫websocket.send時候,會呼叫該方法
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
/**
* 收到訊息,自定義處理機制,實現業務
*/
System.out.println("伺服器收到訊息:"+message);
if(message.getPayload().startsWith("#anyone#")){ //單發某人
sendMessageToUser((String)session.getAttributes().get(USER_ID), new TextMessage("伺服器單發:" +message.getPayload())) ;
}else if(message.getPayload().startsWith("#everyone#")){
sendMessageToUsers(new TextMessage("伺服器群發:" +message.getPayload()));
}else{
}
}
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if(session.isOpen()){
session.close();
}
System.out.println("傳輸出現異常,關閉websocket連線... ");
String userId= (String) session.getAttributes().get(USER_ID);
users.remove(userId);
}
public boolean supportsPartialMessages() {
return false;
}
/**
* 給某個使用者傳送訊息
*
* @param userId
* @param message
*/
public void sendMessageToUser(String userId, TextMessage message) {
for (String id : users.keySet()) {
if (id.equals(userId)) {
try {
if (users.get(id).isOpen()) {
users.get(id).sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
/**
* 給所有線上使用者傳送訊息
*
* @param message
*/
public void sendMessageToUsers(TextMessage message) {
for (String userId : users.keySet()) {
try {
if (users.get(userId).isOpen()) {
users.get(userId).sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
步驟五:配置檔案掃描config類 我的SpringWebSocketConfig配置在包com.thunisoft.config下
<context:component-scan base-package="com.thunisoft.ssm.controller,com.thunisoft.config"></context:component-scan>
步驟六:編寫springmvc controller
@Controller
public class WebSocketController {
@Bean//這個註解會從Spring容器拿出Bean
public SpringWebSocketHandler infoHandler() {
return new SpringWebSocketHandler();
}
@RequestMapping("/websocket/loginPage")
public String loginPage(HttpServletRequest request, HttpServletResponse response) throws Exception {
return "/order/login";
}
@RequestMapping("/websocket/login")
public String login(HttpServletRequest request, HttpServletResponse response) throws Exception {
String username = request.getParameter("username");
System.out.println(username+"登入");
HttpSession session = request.getSession(false);
session.setAttribute("SESSION_USERNAME", username); //一般直接儲存user實體
return "/order/send";
}
@RequestMapping("/websocket/send")
@ResponseBody
public String send(HttpServletRequest request) {
String username = request.getParameter("username");
infoHandler().sendMessageToUser(username, new TextMessage("你好,測試!!!!"));
return null;
}
@RequestMapping("/websocket/broad")
@ResponseBody
public String broad() {
infoHandler().sendMessageToUsers(new TextMessage("傳送一條小Broad"));
System.out.println("群發成功");
return "broad";
}
}
步驟七:編輯登陸jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>測試spring websocket</title>
</head>
<body>
<form action="${ctx}/websocket/login">
登入名:<input type="text" name="username"/>
<input type="submit" value="登入聊天室"/>
</form>
</body>
步驟八:編寫通訊頁面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title></title>
<!--
<link rel="stylesheet" href="/css/style.css"/>
-->
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.js"></script>
<script type="text/javascript">
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/springfirst/websocket/socketServer");
}
else if ('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://localhost:8080/springfirst/websocket/socketServer");
}
else {
websocket = new SockJS("http://localhost:8080/springfirst/sockjs/socketServer");
}
websocket.onopen = onOpen;
websocket.onmessage = onMessage;
websocket.onerror = onError;
websocket.onclose = onClose;
function onOpen(openEvt) {
alert(openEvt.Data);
}
function onMessage(evt) {
alert("super is:" + evt.data);
}
function onOpen() {
}
function onError() {}
function onClose() {}
function doSendUser() {
alert(websocket.readyState + ":" + websocket.OPEN);
if (websocket.readyState == websocket.OPEN) {
var msg = document.getElementById("inputMsg").value;
websocket.send("#anyone#"+msg);//呼叫後臺handleTextMessage方法
alert("傳送成功!");
} else {
alert("連線失敗!");
}
}
function doSendUsers() {
if (websocket.readyState == websocket.OPEN) {
var msg = document.getElementById("inputMsg").value;
websocket.send("#everyone#"+msg);//呼叫後臺handleTextMessage方法
alert("傳送成功!");
} else {
alert("連線失敗!");
}
}
window.close=function()
{
websocket.onclose();
}
function websocketClose() {
websocket.close();
}
</script>
</head>
<body>
請輸入:<textarea rows="5" cols="10" id="inputMsg" name="inputMsg"></textarea>
<button onclick="doSendUser();">傳送</button>
<button onclick="doSendUsers();">群發</button>
<button onclick="websocketClose();">關閉連線</button>
</body>
</html>
演示效果圖
登陸:
單發:
群發:
後續配合nginx釋出,在nginx代理的情況下需要配置,這個示例專案,我的跟目錄是/springfirst ,springfirst是我的專案名,實際專案釋出的時候是隱藏專案名的,所以配置/就可以。
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
upstream websocket {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name max.eqshow.cnn;
#charset koi8-r;
#access_log logs/host.access.log main;
location /springfirst {
proxy_pass http://websocket;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
總結: 做完之後感覺整個過程並不複雜,單其實經歷了好幾天,從最初的不知道什麼是websocket,到後來不知道nginx配置出問題,從不知道到知道,其實遇到了很多問題。比如,不知道websocket需要容器和瀏覽器的支援,不知道跨域需要設定setAllowedOrigins("*"),demo寫好怎麼融入到專案中也遇到了很多問題。但這一切在有結果的時候都豁然開朗。同時也瞭解了器用分析法,先了解基本用法,寫個demo,然後研究其原理。到目前為止也是淺薄的是瞭解而已,後面遇到問題在繼續更新吧。
springmvc websocket:點選開啟連結
http://www.runoob.com:點選開啟連結