websoket原理和實戰
websoket原理和實戰
概述
專案包結構
兩個包:
com.javasea.web.websocket.springb.websocket
使用實現WebSocketConfigurer
介面的方式實現
com.javasea.web.websocket.springb.websocket2
通過註解@ServerEndpoint
方式實現
場景引用
場景:頁面需要實時顯示被分配的任務,頁面需要實時顯示線上人數。
思考:像這樣的訊息功能怎麼實現? 如果網頁不重新整理,服務端有新訊息如何推送到瀏覽器?
解決方案,採用輪詢的方式。即:通過js不斷的請求伺服器,檢視是否有新資料,如果有,就獲取到新資料。
這種解決方法是否存在問題呢?
當然是有的,如果服務端一直沒有新的資料,那麼js也是需要一直的輪詢查詢資料,這就是一種資源的浪費。
那麼,有沒有更好的解決方案? 有!那就是採用WebSocket技術來解決。
什麼是WebSocket?
WebSocket 是HTML5一種新的協議。它實現了瀏覽器與伺服器全雙工通訊(full-duplex)。一開始的握手需要藉助HTTP請求完成。 WebSocket是真正實現了全雙工通訊的伺服器向客戶端推的網際網路技術。 它是一種在單個TCP連線上進行全雙工通訊協議。Websocket通訊協議與2011年倍IETF定為標準RFC 6455,Websocket API被W3C定為標準。
什麼叫做全雙工和半雙工?
比如對講機,說話的時候就聽不到對方說話,那麼就是半雙工。
我們打電話的時候說話的同時也能聽到對方說話,就是全雙工。
http與websocket的區別
http協議是短連線,因為請求之後,都會關閉連線,下次重新請求資料,需要再次開啟連結。
WebSocket協議是一種長連結,只需要通過一次請求來初始化連結,然後所有的請求和響應都是通過這個TCP連結進行通訊。
瀏覽器支援情況
伺服器支援情況:Tomcat 7.0.47+以上才支援。
快速入門
建立專案
配置pom.xml
-
整合javaee
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version
-
配置tomcat外掛
<plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8082</port> <path>/</path> </configuration> </plugin> 複製程式碼
之後
啟動服務
只需要在maven中直接執行即可。
pom的詳細配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.iee</groupId>
<artifactId>javasea-web-websoecket-quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax/javaee-api -->
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<!--<scope>provided</scope>-->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--maven編譯外掛,指定jdk為1.8 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!-- 使用jdk進行編譯 -->
<fork>true</fork>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- 配置Tomcat外掛 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8082</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
複製程式碼
websocket的相關註解說明
-
@ServerEndpoint("/websocket/{uid}")
申明這是一個websocket服務 需要指定訪問該服務的地址,在地址中可以指定引數,需要通過{}進行佔位 -
@OnOpen
用法:public void onOpen(Session session,@PathParam("uid") String uid) throws IOException{} 該方法將在建立連線後執行,會傳入session物件,就是客戶端與服務端建立的長連線通道 通過@PathParam獲取url申明中的引數 -
@OnClose
用法:public void onClose() {} 該方法是在連線關閉後執行 -
@OnMessage
用法:public void onMessage(String message,Session session) throws IOException {}客戶端訊息到來時呼叫,包含會話Session,根據訊息的形式,如果是文字訊息,傳入String型別引數或者Reader,如果是二進位制訊息,傳入byte[]型別引數或者InputStream。
message:發來的訊息資料 session:會話物件(也是通道) 傳送訊息到客戶端 用法:session.getBasicRemote().sendText("你好"); 通過session進行傳送。
實現websocket服務
@ServerEndpoint("/websocket/{uid}")
public class MyWebSocket {
@OnOpen
public void onOpen(Session session,@PathParam("uid") String uid) throws
IOException {
// 連線成功
session.getBasicRemote().sendText(uid + ",你好,歡迎連線WebSocket!");
}
@OnClose
public void onClose() {
System.out.println(this + "關閉連線");
}
@OnMessage
public void onMessage(String message,Session session) throws IOException {
System.out.println("接收到訊息:" + message);
session.getBasicRemote().sendText("訊息已收到.");
}
@OnError
public void onError(Session session,Throwable error) {
System.out.println("發生錯誤");
error.printStackTrace();
}
}
複製程式碼
maven中啟動tomcat:
mv tomcat7:run
複製程式碼
也可以用上文中在IDE中直接啟動。
測試
一共有三種測試方式,直接js指令碼方式、chrome外掛方式或者通過線上工具進行測試:
-
直接js指令碼方式,直接用如下程式碼進行測試:
var socket; if(typeof(WebSocket) == "undefined") { console.log("您的瀏覽器不支援WebSocket"); }else{ console.log("您的瀏覽器支援WebSocket"); //實現化WebSocket物件,指定要連線的伺服器地址與埠 建立連線 socket = new WebSocket("ws://localhost:8080/websocket2/22"); //開啟事件 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(); //}); } 複製程式碼
瀏覽器隨便開啟一個網頁,然後貼上到console下,回車即可
-
chrome外掛方式,需要安裝chrome外掛,Simple WebSocket Client: chrome.google.com/webstore/de…
-
線上工具進行測試(推薦):www.websocket-test.com/
我一直測試失敗,還沒找到原因。下文整合springboot的測試成功。
編寫js客戶端
在webapp下編寫兩個html檔案
-
websocket.html
內容如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
const socket = new WebSocket("ws://localhost:8082/websocket/1");
// 連線建立時觸發
socket.onopen = (ws) => {
console.log("建立連線!",ws);
};
// 客戶端接收服務端資料時觸發
socket.onmessage = (ws) => {
console.log("接收到訊息 >> ",ws.data);
};
// 連線關閉時觸發
socket.onclose = (ws) => {
console.log("連線已斷開!",ws);
};
// 通訊發生錯誤時觸發
socket.onerror = (ws) => {
console.log("傳送錯誤!",ws);
};
// 2秒後向服務端傳送訊息
setTimeout(() => {
// 使用連線傳送資料
socket.send("傳送一條訊息試試");
},2000);
// 5秒後斷開連線
setTimeout(() => {
// 關閉連線
socket.close();
},5000);
</script>
</body>
</html>
複製程式碼
-
websocket2.html
內容如下
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>菜鳥教程(runoob.com)</title>
<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的瀏覽器支援 WebSocket!");
// 開啟一個 web socket
var ws = new WebSocket("ws://localhost:8082/websocket/1");
ws.onopen = function()
{
// Web Socket 已連線上,使用 send() 方法傳送資料
ws.send("傳送資料");
alert("資料傳送中...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
alert("資料已接收...");
};
ws.onclose = function()
{
// 關閉 websocket
alert("連線已關閉...");
};
}
else
{
// 瀏覽器不支援 WebSocket
alert("您的瀏覽器不支援 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">執行 WebSocket</a>
</div>
</body>
</html>
複製程式碼
在瀏覽器請求http://localhost:8082/websocket2.html
emmm,失敗的,還沒找到原因,下文整合springboot,測試是成功的。
整合springboot
使用springboot內建tomcat時,就不需要引入javaee-api了,spring-boot已經包含了。
springboot的高階元件會自動引用基礎的元件,像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter,所以不要重複引入
springboot已經做了深度的整合和優化,注意是否添加了不需要的依賴、配置或宣告。由於很多講解元件使用的文章是和spring整合的,會有一些配置。在使用springboot時,由於springboot已經有了自己的配置,再這些配置有可能導致各種各樣的異常。
pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>javasea-web-websocket</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.5.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>javasea-web-websocket-springb</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax/javaee-api -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins> <!-- java編譯外掛 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!-- 使用jdk進行編譯 -->
<fork>true</fork>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
複製程式碼
springboot有兩種方式實現websocket
- 通過註解
@ServerEndpoint
方式實現
webSocket核心是@ServerEndpoint
這個註解。這個註解是Javaee標準裡的註解,tomcat7以上已經對其進行了實現,如果是用傳統方法使用tomcat釋出的專案,只要在pom檔案中引入javaee標準即可使用。
快速入門中的例子就是通過@ServerEndpoint來實現的WebSocket服務,在整合springboot的時候需要額外配置Config類,建立一個ServerEndpointExporter();
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
複製程式碼
-
使用實現
WebSocketConfigurer
介面的方式實現下文就是這種方式的實現
編寫WebSocketHandler
在Spring中,處理訊息的具體業務邏輯需要實現WebSocketHandler介面。
package com.javasea.web.websocket.springb.websocket;
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;
import java.io.IOException;
/**
* @Description 在Spring中,處理訊息的具體業務邏輯需要實現WebSocketHandler介面。
* @Author [email protected]
* @Date 16:50 2019/10/27 0027
**/
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session,TextMessage message) throws IOException {
System.out.println("獲取到訊息 >> " + message.getPayload());
session.sendMessage(new TextMessage("訊息已收到"));
if (message.getPayload().equals("10")) {
for (int i = 0; i < 10; i++) {
//回寫訊息到client
session.sendMessage(new TextMessage("訊息 -> " + i));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
session.sendMessage(new TextMessage("歡迎連線到ws服務"));
}
@Override
public void afterConnectionClosed(WebSocketSession session,CloseStatus status) throws Exception {
System.out.println("斷開連線!");
}
}
複製程式碼
編寫配置類來實現WebSocket服務
package com.javasea.web.websocket.springb.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(),"/ws").setAllowedOrigins("*");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
複製程式碼
編寫啟動類
package com.javasea.web.websocket.springb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class APPlication {
public static void main(String[] args) {
SpringApplication.run(APPlication.class,args);
}
}
複製程式碼
測試
線上進行測試,url:ws://localhost:8080/ws
用上文的html頁面也可以測試的,修改地址為
ws://localhost:8080/ws
然後在資料夾下直接用瀏覽器開啟即可。
新增攔截器
package com.javasea.web.websocket.springb.websocket;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
@Component
public class MyHandshakeInterceptor implements HandshakeInterceptor {
/*** 握手之前,若返回false,則不建立連結 */
@Override
public boolean beforeHandshake(ServerHttpRequest request,ServerHttpResponse response,WebSocketHandler wsHandler,Map<String,Object> attributes) throws Exception { //將使用者id放入socket處理器的會話(WebSocketSession)中
attributes.put("uid",1001);
System.out.println("開始握手。。。。。。。");
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request,Exception exception) {
System.out.println("握手成功啦。。。。。。");
}
}
複製程式碼
將攔截器新增到websocket服務中:
就是在上文的config中新增
addInterceptors(this.myHandshakeInterceptor);
。
package com.javasea.web.websocket.springb.websocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private MyHandshakeInterceptor myHandshakeInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(),"/ws").setAllowedOrigins("*").addInterceptors(this.myHandshakeInterceptor);
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
複製程式碼
測試攔截器
在MyHandler
類afterConnectionEstablished
方法下輸出獲取到的uid
。
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("uid =>" + session.getAttributes().get("uid"));
session.sendMessage(new TextMessage("歡迎連線到ws服務"));
}
複製程式碼
連線websocket服務 ws://localhost:8080/ws
,console輸出:
握手成功啦。。。。。。
uid =>1001
複製程式碼
說明測試成功。
專案地址
github地址:github.com/longxiaonan…