小編最近一直在使用springboot框架開發項目,畢竟現在很多公司都在採用此框架,之後小編也會陸續寫關於springboot開發常用功能的文章。
什麼場景下會要使用到websocket的呢?
websocket主要功能就是實現網絡通訊,比如說最經典的客服聊天窗口、您有新的消息通知,或者是項目與項目之間的通訊,都可以採用websocket來實現。
二、websocket介紹百度百科介紹:WebSokcet
在公司實際使用websocket開發,一般來都是這樣的架構,首先websocket服務端是一個單獨的項目,其他需要通訊的項目都是以客戶端來連接,由服務端控制消息的發送方式(群發、指定發送)。但是也會有服務端、客戶端在同一個項目當中,具體看項目怎麼使用。
本文呢,採用的是服務端與客戶端分離來實現,包括使用springboot搭建websokcet服務端、html5客戶端、springboot後臺客戶端, 具體看下面代碼。
三、服務端實現*步驟一*:springboot底層幫我們自動配置了websokcet,引入maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>*步驟二*:如果是你採用springboot內置容器啟動項目的,則需要配置一個Bean。如果是採用外部的容器,則可以不需要配置。
/**
* @Auther: liaoshiyao
* @Date: 2019/1/11 11:49
* @Description: 配置類
*/
@Component
public class WebSocketConfig {
/**
* ServerEndpointExporter 作用
*
* 這個Bean會自動註冊使用@ServerEndpoint註解聲明的websocket endpoint
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}*步驟三*:最後一步當然是編寫服務端核心代碼了,其實小編不是特別想貼代碼出來,貼很多代碼影響文章可讀性。
package com.example.socket.code;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Auther: liaoshiyao
* @Date: 2019/1/11 11:48
* @Description: websocket 服務類
*/
/**
*
* @ServerEndpoint 這個註解有什麼作用?
*
* 這個註解用於標識作用在類上,它的主要功能是把當前類標識成一個WebSocket的服務端
* 註解的值用戶客戶端連接訪問的URL地址
*
*/
@Slf4j
@Component
@ServerEndpoint("/websocket/{name}")
public class WebSocket {
/**
* 與某個客戶端的連接對話,需要通過它來給客戶端發送消息
*/
private Session session;
/**
* 標識當前連接客戶端的用戶名
*/
private String name;
/**
* 用於存所有的連接服務的客戶端,這個對象存儲是安全的
*/
private static ConcurrentHashMap<String,WebSocket> webSocketSet = new ConcurrentHashMap<>();
@OnOpen
public void OnOpen(Session session, @PathParam(value = "name") String name){
this.session = session;
this.name = name;
// name是用來表示唯一客戶端,如果需要指定發送,需要指定發送通過name來區分
webSocketSet.put(name,this);
log.info("[WebSocket] 連接成功,當前連接人數為:={}",webSocketSet.size());
}
@OnClose
public void OnClose(){
webSocketSet.remove(this.name);
log.info("[WebSocket] 退出成功,當前連接人數為:={}",webSocketSet.size());
}
@OnMessage
public void OnMessage(String message){
log.info("[WebSocket] 收到消息:{}",message);
//判斷是否需要指定發送,具體規則自定義
if(message.indexOf("TOUSER") == 0){
String name = message.substring(message.indexOf("TOUSER")+6,message.indexOf(";"));
AppointSending(name,message.substring(message.indexOf(";")+1,message.length()));
}else{
GroupSending(message);
}
}
/**
* 群發
* @param message
*/
public void GroupSending(String message){
for (String name : webSocketSet.keySet()){
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 指定發送
* @param name
* @param message
*/
public void AppointSending(String name,String message){
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
}
四、客戶端實現*HTML5實現*:以下就是核心代碼了,其實其他博客有很多,小編就不多說了。
var websocket = null;
if('WebSocket' in window){
websocket = new WebSocket("ws://192.168.2.107:8085/websocket/testname");
}
websocket.onopen = function(){
console.log("連接成功");
}
websocket.onclose = function(){
console.log("退出連接");
}
websocket.onmessage = function (event){
console.log("收到消息"+event.data);
}
websocket.onerror = function(){
console.log("連接出錯");
}
window.onbeforeunload = function () {
websocket.close(num);
}*SpringBoot後臺實現*:小編發現多數博客都是採用js來實現客戶端,很少有用後臺來實現,所以小編也就寫了寫,大神請勿噴?。很多時候,項目與項目之間通訊也需要後臺作為客戶端來連接。
*步驟一*:首先我們要導入後臺連接websocket的客戶端依賴
<!--websocket作為客戶端-->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.5</version>
</dependency>*步驟二*:把客戶端需要配置到springboot容器裡面去,以便程序調用。
package com.example.socket.config;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.net.URI;
/**
* @Auther: liaoshiyao
* @Date: 2019/1/11 17:38
* @Description: 配置websocket後臺客戶端
*/
@Slf4j
@Component
public class WebSocketConfig {
@Bean
public WebSocketClient webSocketClient() {
try {
WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://localhost:8085/websocket/test"),new Draft_6455()) {
@Override
public void onOpen(ServerHandshake handshakedata) {
log.info("[websocket] 連接成功");
}
@Override
public void onMessage(String message) {
log.info("[websocket] 收到消息={}",message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
log.info("[websocket] 退出連接");
}
@Override
public void onError(Exception ex) {
log.info("[websocket] 連接錯誤={}",ex.getMessage());
}
};
webSocketClient.connect();
return webSocketClient;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}*步驟三*:使用後臺客戶端發送消息
1、首先小編寫了一個接口,裡面有指定發送和群發消息兩個方法。
2、實現發送的接口,區分指定發送和群發由服務端來決定(小編在服務端寫了,如果帶有TOUSER標識的,則代表需要指定發送給某個websocket客戶端)
3、最後採用get方式用瀏覽器請求,也能正常發送消息
package com.example.socket.code;
/**
* @Auther: liaoshiyao
* @Date: 2019/1/12 10:57
* @Description: websocket 接口
*/
public interface WebSocketService {
/**
* 群發
* @param message
*/
void groupSending(String message);
/**
* 指定發送
* @param name
* @param message
*/
void appointSending(String name,String message);
}
package com.example.socket.code;
import org.java_websocket.client.WebSocketClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @Auther: liaoshiyao
* @Date: 2019/1/12 10:56
* @Description: websocket接口實現類
*/
@Component
public class ScoketClient implements WebSocketService{
@Autowired
private WebSocketClient webSocketClient;
@Override
public void groupSending(String message) {
// 這裡我加了6666-- 是因為我在index.html頁面中,要拆分用戶編號和消息的標識,只是一個例子而已
// 在index.html會隨機生成用戶編號,這裡相當於模擬頁面發送消息
// 實際這樣寫就行了 webSocketClient.send(message)
webSocketClient.send(message+"---6666");
}
@Override
public void appointSending(String name, String message) {
// 這裡指定發送的規則由服務端決定參數格式
webSocketClient.send("TOUSER"+name+";"+message);
}
}
package com.example.socket.chat;
import com.example.socket.code.ScoketClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Auther: liaoshiyao
* @Date: 2019/1/11 16:47
* @Description: 測試後臺websocket客戶端
*/
@RestController
@RequestMapping("/websocket")
public class IndexController {
@Autowired
private ScoketClient webScoketClient;
@GetMapping("/sendMessage")
public String sendMessage(String message){
webScoketClient.groupSending(message);
return message;
}
}
五、最後其實,貼了這麼多大一片的代碼,感覺上影響了博客的美觀,也不便於瀏覽,如果沒看懂小夥伴,可以下載源碼看下。
裡面一共兩個項目,服務端、客戶端(html5客戶端、後臺客戶端),是一個網頁群聊的小案例。
https://download.csdn.net/download/weixin_38111957/10912384
*祝大家學習愉快~~~*
六、針對評論區的小夥伴提出的疑點進行解答看了小夥伴提出的疑問,小編也是非常認可的,如果是單例的情況下,這個對象的值都會被修改。
小編就抽了時間Debug了一下,經過下圖也可以反映出,能夠看出,webSokcetSet中存在三個成員,並且vlaue值都是不同的,所以在這裡沒有出現對象改變而把之前對象改變的現象。
服務端這樣寫是沒問題的。
緊接著,小編寫了一個測試類,代碼如下,經過測試輸出的結果和小夥伴提出的疑點是一致的。
最後總結:這位小夥伴提出的觀點確實是正確的,但是在實際WebSocket服務端案例中為什麼沒有出現這種情況,當WebSokcet這個類標識為服務端的時候,每當有新的連接請求,這個類都是不同的對象,並非單例。
這裡也感謝「煙花蘇柳」所提出的問題。
import com.alibaba.fastjson.JSON;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Auther: IT賤男
* @Date: 2018/11/1 16:15
* @Description:
*/
public class TestMain {
/**
* 用於存所有的連接服務的客戶端,這個對象存儲是安全的
*/
private static ConcurrentHashMap<String, Student> webSocketSet = new ConcurrentHashMap<>();
public static void main(String[] args) {
Student student = Student.getStudent();
student.name = "張三";
webSocketSet.put("1", student);
Student students = Student.getStudent();
students.name = "李四";
webSocketSet.put("2", students);
System.out.println(JSON.toJSON(webSocketSet));
}
}
/**
* 提供一個單例類
*/
class Student {
public String name;
private Student() {
}
private static final Student student = new Student();
public static Student getStudent() {
return student;
}
}{"1":{"name":"李四"},"2":{"name":"李四"}}
點擊閱讀原文,前往學習SpringCloud實戰項目