1. 程式人生 > 實用技巧 >SpringBoot+WebSocket實現服務端、客戶端

SpringBoot+WebSocket實現服務端、客戶端

一、引言

本人最近一直在使用springboot框架開發專案,畢竟現在很多公司都在採用此框架,之後本人也會陸續寫關於springboot開發常用功能的文章。

什麼場景下會要使用到websocket的呢?

websocket主要功能就是實現網路通訊,比如說最經典的客服聊天視窗、您有新的訊息通知,或者是專案與專案之間的通訊,都可以採用websocket來實現。

二、websocket介紹

在公司實際使用websocket開發,一般來都是這樣的架構,首先websocket服務端是一個單獨的專案,其他需要通訊的專案都是以客戶端來連線,由服務端控制訊息的傳送方式(群發、指定傳送)。 但是也會有服務端、客戶端在同一個專案當中,具體看專案怎麼使用。

本文呢,採用的是服務端與客戶端分離來實現,包括使用springboot搭建websokcet服務端、html5客戶端、springboot後臺客戶端,具體看下面程式碼。

三、服務端實現

步驟一: springboot底層幫我們自動配置了websokcet,引入maven依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

步驟二:如果是你採用springboot內建容器啟動專案的,則需要配置一個Bean。如果是採用外部的容器,則可以不需要配置。

/**
 * @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;
 
/**
 * @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;
 
/**
 * @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;
 
/**
 * @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.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;
 
/**
 * @Description: 測試後臺websocket客戶端
 */
@RestController
@RequestMapping("/websocket")
public class IndexController {
 
    @Autowired
    private ScoketClient webScoketClient;
 
    @GetMapping("/sendMessage")
    public String sendMessage(String message){
        webScoketClient.groupSending(message);
        return message;
    }
}

五、最後

注意:

如果是單例的情況下,這個物件的值都會被修改。

本人就抽了時間Debug了一下,經過下圖也可以反映出,能夠看出,webSokcetSet中存在三個成員,並且vlaue值都是不同的,所以在這裡沒有出現物件改變而把之前物件改變的現象。

服務端這樣寫是沒問題的。

最後總結:在實際WebSocket服務端案例中為什麼沒有出現這種情況,當WebSokcet這個類標識為服務端的時候,每當有新的連線請求,這個類都是不同的物件,並非單例。

import com.alibaba.fastjson.JSON;
 
import java.util.concurrent.ConcurrentHashMap;
 
/**
 * @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":"李四"}}