websocket應用
- 基於TCP的一種新的 網絡協議
- 瀏覽器 與 服務器 全雙工 full-duplex , 通信
- 允許服務端 主動 發送 信息給客戶端
- 爲了兼容那些沒有實現 該協議的瀏覽器,還需要 通過 STOMP協議來 完成這寫兼容
加入pom依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- security ,因爲有時候對於 webSocket而言,需要 點對點的通信,需要用戶登錄
簡易的WebSocket服務
自定義websocket服務端點 配置
- ServerEndpointExporter,定義webSocket服務器的端點(供客戶端請求)
@Configuration
public class WebSocketConfig {
// 如果你使用的不是Spring Boot依賴的服務器,才需要自己創建
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
定義WebSocket服務端站點
-
@ServerEndpoint 定義端點服務類
-
定義WebSocket的打開,關閉,錯誤,發送消息
@ServerEndpoint("/ws") //創建服務端點 地址爲/ws @Service public class WebSocketServiceImpl { //每一個客戶端打開,都會創建WebSocketServiceImpl對象, 下面是計數 將這個對象保存到 CopyOnWriteArraySet 中 //關閉是 清楚這個對象 ,並且 計數 減一 //消息發送, 通過輪詢所有的客戶端,都發送消息 //只發送特定的用戶,則需要得到用戶信息,然後在發送 // 靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 private static int onlineCount = 0; // concurrent包的線程安全Set,用來存放每個客戶端對應的WebSocketServiceImpl對象。 private static CopyOnWriteArraySet<WebSocketServiceImpl> webSocketSet = new CopyOnWriteArraySet<>(); // 與某個客戶端的連接會話,需要通過它來給客戶端發送數據 private Session session; /** * 連接建立成功調用的方法。標註客戶端打開websocket服務端點調用方法*/ @OnOpen public void onOpen(Session session) { this.session = session; webSocketSet.add(this); // 加入set中 addOnlineCount(); // 在線數加1 System.out.println("有新連接加入!當前在線人數爲" + getOnlineCount()); try { sendMessage("有新的連接加入了!!"); } catch (IOException e) { System.out.println("IO異常"); } } /** * 連接關閉調用的方法。標註客戶端關閉websocket服務端點調用方法 */ @OnClose public void onClose() { webSocketSet.remove(this); // 從set中刪除 subOnlineCount(); // 在線數減1 System.out.println("有一連接關閉!當前在線人數爲" + getOnlineCount()); } /** * 收到客戶端消息後調用的方法 * @param message 客戶端發送過來的消息 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("來自客戶端的消息:" + message); // 羣發消息 for (WebSocketServiceImpl item : webSocketSet) { try { /* // 獲取當前用戶名稱 String userName = item.getSession() .getUserPrincipal().getName(); System.out.println(userName); */ item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } /** * 發生錯誤時調用 客戶端 請求服務端 發生異常調用 */ @OnError public void onError(Session session, Throwable error) { System.out.println("發生錯誤"); error.printStackTrace(); } /** * 發送消息 * @param message 客戶端消息 * @throws IOException */ private void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } // 返回在線數 private static synchronized int getOnlineCount() { return onlineCount; } // 當連接人數增加時 private static synchronized void addOnlineCount() { WebSocketServiceImpl.onlineCount++; } // 當連接人數減少時 private static synchronized void subOnlineCount() { WebSocketServiceImpl.onlineCount--; } }
-
this.session.getBasicRemote().sendText(message); 發送消息
-
@ServerEndpoint("/ws") //創建服務端點 地址爲/ws
開發websocket 頁面
<%@ 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>My WebSocket</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="./../js/websocket.js"></script>
</head>
<body>
測試一下WebSocket站點吧
<br />
<input id="message" type="text" />
<button οnclick="sendMessage()">發送消息</button>
<button οnclick="closeWebSocket()">關閉WebSocket連接</button>
<div id="context"></div>
</body>
</html>
var websocket = null;
// 判斷當前瀏覽器是否支持WebSocket
if ('WebSocket' in window) {
// 創建WebSocket對象,連接服務器端點
websocket = new WebSocket("ws://localhost:8080/ws");
} else {
alert('Not support websocket')
}
// 連接發生錯誤的回調方法
websocket.onerror = function() {
appendMessage("error");
};
// 連接成功建立的回調方法
websocket.onopen = function(event) {
appendMessage("open");
}
// 接收到消息的回調方法
websocket.onmessage = function(event) {
appendMessage(event.data);
}
// 連接關閉的回調方法
websocket.onclose = function() {
appendMessage("close");
}
// 監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,
// 防止連接還沒斷開就關閉窗口,server端會拋異常。
window.onbeforeunload = function() {
websocket.close();
}
// 將消息顯示在網頁上
function appendMessage(message) {
var context = $("#context").html() +"<br/>" + message;
$("#context").html(context);
}
// 關閉連接
function closeWebSocket() {
websocket.close();
}
// 發送消息
function sendMessage() {
var message = $("#message").val();
websocket.send(message);
}
- new WebSocket(“ws://localhost:8080/ws”);
控制器
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
// 跳轉websocket頁面
@GetMapping("/index")
public String websocket() {
return "websocket";
}
}
使用STOMP
- 舊的版本瀏覽器 不能支持 webSocket協議,可以引用 WebSocket協議的子協議 STOMP simple or Streaming Text Orientated Messageing Protocol
- 配置文件要加入 @EnableWebSocket MessageBroker (就會啓動websocket下的子協議 stomp)
- 配置stomp 實現 WebSocket MessageBroker Configurer
- 爲了更加簡單 還提供了抽象類 Abstract WebSocket MessageBroker Configurer
配置STOMP的服務端點 和 請求訂閱前綴
@Configuration
@EnableWebSocketMessageBroker //啓用STOMP協議
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 如果你使用的不是Spring Boot依賴的服務器,才需要自己創建
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
// 註冊服務器端點
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 增加一個聊天服務端點
registry.addEndpoint("/socket").withSockJS();//也可以支持sockJS
// 增加一個用戶服務端點
registry.addEndpoint("/wsuser").withSockJS();
}
// 定義服務器端點請求和訂閱前綴
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客戶端訂閱路徑前綴
registry.enableSimpleBroker("/sub", "/queue");
// 服務端點請求前綴
registry.setApplicationDestinationPrefixes("/request");
}
}
- sockJS 是一個 第三方關於 支持 WebSocket請求的 JavaScript框架
- boot會創建 SimpMessaging Template對象
STOMP下的 控制器
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
@Autowired // 注入Spring Boot自動配置消息模板對象
private SimpMessagingTemplate simpMessagingTemplate;
// 發送頁面
@GetMapping("/send")
public String send() {
return "send";
}
// 接收頁面
@GetMapping("/receive")
public String receive() {
return "receive";
}
// 對特定用戶發送頁面
@GetMapping("/sendUser")
public String sendUser() {
return "send-user";
}
// 接收用戶消息頁面
@GetMapping("/receiveUser")
public String receiveUser() {
return "receive-user";
}
// 定義消息請求路徑
@MessageMapping("/send")
// 定義結果發送到特定路徑
@SendTo("/sub/chat")
public String sendMsg(String value) {
return value;
}
// 將消息發送給特定用戶
@MessageMapping("/sendUser")
public void sendToUser(Principal principal, String body) {
String srcUser = principal.getName();
// 解析用戶和消息
String []args = body.split(",");
String desUser = args[0];
String message = "【" + srcUser + "】給你發來消息:" + args[1];
// 發送到用戶和監聽地址
simpMessagingTemplate.convertAndSendToUser(desUser,
"/queue/customer", message);
}
}
-
@MessageMapping("/send") 定義消息請求路徑
- 與 registry.setApplicationDestinationPrefixes("/request") 連用
-
@SendTo("/sub/chat") 在執行完 這個方法後,將返回結果發送到訂閱的這個目的址中
- 這樣客戶端就可以 得到消息
-
principal 獲得當前用戶的消息
-
simpMessagingTemplate.convertAndSendToUser(desUser, “/queue/customer”, message);
- 發送給對應的目的地,並且限定特定的用戶消息
配置 Security
@SpringBootApplication(scanBasePackages = "com.springboot.chapter13")
@EnableScheduling
public class Chapter13Application extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Chapter13Application.class, args);
}
// 定義3個可以登錄的內存用戶
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密碼加密器
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 加入三個內存用戶,密碼分別爲加密後的"p1","p2"和"p3"
// 可以通過 passwordEncoder.encode("p1")這樣獲得加密後的密碼
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder)
.withUser("user1")
.password("$2a$10$7njFQKL2WV862XP6Hlyly.F0lkSHtOOQyQ/rlY7Ok26h.gGZD4IqG").roles("USER").and()
.withUser("user2").password("$2a$10$Q2PwvWNpog5sZX583LuQfet.y1rfPMsqtrb7IjmvRn7Ew/wNUjVwS")
.roles("ADMIN").and().withUser("user3")
.password("$2a$10$GskYZT.34BdhmEdOlAS8Re7D73RprpGN0NjaiqS2Ud8XdcBcJck4u").roles("USER");
}
}
jsp
send.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>
<title>My WebSocket</title>
<script type="text/javascript"
src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<!--
stomp.min.js的下載地址:
https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js
該地址設定爲文本,所以不能直接載入,需要自行先下載,再使用
-->
<script type="text/javascript" src="./../js/stomp.min.js"></script>
</head>
<script type="text/javascript">
var stompClient = null;
// 設置連接
function setConnected(connected) {
$("#connect").attr({"disabled": connected});
$("#disconnect").attr({"disabled": !connected});
if (connected) {
$("#conversationDiv").show();
} else {
$("#conversationDiv").hide();
}
$("#response").html("");
}
// 開啓socket連接
function connect() {
// 定義請求服務器的端點
var socket = new SockJS('/socket');
// stomp客戶端
stompClient = Stomp.over(socket);
// 連接服務器端點
stompClient.connect({}, function(frame) {
// 建立連接後的回調
setConnected(true);
});
}
// 斷開socket連接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 向‘/request/send’服務端發送消息
function sendMsg() {
var value = $("#message").val();
// 發送消息到"/request/send",其中/request是服務器定義的前綴,
// 而/send則是@MessageMapping所配置的路徑
stompClient.send("/request/send", {}, value);
}
connect();
</script>
<body>
<div>
<div>
<button id="connect" οnclick="connect();">連接</button>
<button id="disconnect" disabled="disabled"
οnclick="disconnect();">斷開連接</button>
</div>
<div id="conversationDiv">
<p>
<label>發送的內容</label>
</p>
<p>
<textarea id="message" rows="5"></textarea>
</p>
<button id="sendMsg" οnclick="sendMsg();">Send</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
- 加入了socket.min.js 和 stomp.min.js
receive.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>
<title>My WebSocket</title>
<script type="text/javascript"
src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<script type="text/javascript" src="./js/stomp.min.js"></script>
</head>
<script type="text/javascript">
var noticeSocket = function() {
// 連接服務器端點
var s = new SockJS('/socket');
// 客戶端
var stompClient = Stomp.over(s);
stompClient.connect({}, function() {
console.log('notice socket connected!');
// 訂閱消息地址
stompClient.subscribe('/sub/chat', function(data) {
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
<body>
<h1><span id="receive">等待接收消息</span></h1>
</body>
</html>
說明
// 客戶端訂閱路徑前綴
registry.enableSimpleBroker("/sub");
// 服務端點請求前綴
registry.setApplicationDestinationPrefixes("/request");
// 增加一個聊天服務端點
registry.addEndpoint("/socket").withSockJS();
// 定義消息請求路徑
@MessageMapping("/send")
// 定義結果發送到特定路徑
@SendTo("/sub/chat")
發送消息:
首先是創建了:new SockJS('/socket'); 路徑
發送消息的路徑:stompClient.send("/request/send", {}, value);
接收消息時:
var s = new SockJS('/socket');
stompClient.subscribe('/sub/chat', function(data) {
$('#receive').html(data.body);
});
send-user.jsp
<script type="text/javascript">
var stompClient = null;
// 重置連接狀態頁面
function setConnected(connected) {
$("#connect").attr({"disabled": connected});
$("#disconnect").attr({"disabled": !connected});
if (connected) {
$("#conversationDiv").show();
} else {
$("#conversationDiv").hide();
}
$("#response").html("");
}
// 開啓socket連接
function connect() {
// 連接/wsuser服務端點
var socket = new SockJS('/wsuser');
// stomp客戶端
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
});
}
// 斷開socket連接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 向‘/request/sendUser’服務端發送消息
function sendMsg() {
var value = $("#message").val();
var user = $("#user").val();
// 用戶和消息組成的字符串
var text = user +"," + value;
stompClient.send("/request/sendUser", {}, text);
}
connect();
</script>
<body>
<div>
<div>
<button id="connect" οnclick="connect();">連接</button>
<button id="disconnect" disabled="disabled" οnclick="disconnect();">斷開連接</button>
</div>
<div id="conversationDiv">
<p><label>發送給用戶</label></p>
<p><input type="text" id="user"/></p>
<p><label>發送的內容</label></p>
<p><textarea id="message" rows="5"></textarea></p>
<button id="sendMsg" οnclick="sendMsg();">發送</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
receive-user.jsp
<script type="text/javascript">
var noticeSocket = function() {
var s = new SockJS('/wsuser');
var stompClient = Stomp.over(s);
stompClient.connect({}, function() {
console.log('notice socket connected!');
stompClient.subscribe('/user/queue/customer', function(data) {
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
<body>
<h1><span id="receive">等待接收消息</span></h1>
</body>
</html>
說明
var socket = new SockJS('/wsuser');
stompClient.connect({}, function(frame) {
setConnected(true);
});
stompClient.send("/request/sendUser", {}, text);
@MessageMapping("/sendUser")
public void sendToUser(Principal principal, String body) {
// 發送到用戶和監聽地址
simpMessagingTemplate.convertAndSendToUser(desUser,
"/queue/customer", message);//發送這個地址,供客戶端連接
}
var s = new SockJS('/wsuser');
var stompClient = Stomp.over(s);
stompClient.connect({}, function() {
console.log('notice socket connected!');
stompClient.subscribe('/user/queue/customer', function(data) {
$('#receive').html(data.body);
});
});