深入2淺出boot2.0 第13章4 WebSocket

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);
			});
            
		});
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章