WebSocket介紹


WebSocket介紹  

一、為什么需要 WebSocket? 

       初次接觸 WebSocket 的人,都會問同樣的問題:我們已經有了 HTTP 協議,為什么還需要另一個協議?它能帶來什么好處?

       答案很簡單,因為 HTTP 協議有一個缺陷:通信只能由客戶端發起。

       舉例來說,我們想了解今天的天氣,只能是客戶端向服務器發出請求,服務器返回查詢結果。HTTP 協議做不到服務器主動向客戶端推送信息。

      這種單向請求的特點,注定了如果服務器有連續的狀態變化,客戶端要獲知就非常麻煩。我們只能使用"輪詢":每隔一段時候,就發出一個詢問,了解服務器有沒有新的信息。最典型的場景就是聊天室。

      輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。

 

二、簡介                                      

     WebSocket 協議在2008年誕生,2011年成為國際標准。所有瀏覽器都已經支持了。

    它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話,屬於服務器推送技術的一種。

其他特點包括:

(1)建立在 TCP 協議之上,服務器端的實現比較容易。

(2)與 HTTP 協議有着良好的兼容性。默認端口也是80和443,並且握手階段采用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器。

(3)數據格式比較輕量,性能開銷小,通信高效。

(4)可以發送文本,也可以發送二進制數據。

(5)沒有同源限制,客戶端可以與任意服務器通信。

(6)協議標識符是ws(如果加密,則為wss),服務器網址就是 URL。

 

 三.WebSocket 的作用                 

        其實上面已經講了它的優點了,不過最近看知乎看到一段有關WebSocket挺有意義的,所以復制來。

      在講Websocket之前,我就順帶着講下 long poll 和 ajax輪詢 的原理。
     首先是 ajax輪詢 ,ajax輪詢 的原理非常簡單,讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。
場景再現:
客戶端:啦啦啦,有沒有新信息(Request)
服務端:沒有(Response)
客戶端:啦啦啦,有沒有新信息(Request)
服務端:沒有。。(Response)
客戶端:啦啦啦,有沒有新信息(Request)
服務端:你好煩啊,沒有啊。。(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:好啦好啦,有啦給你。(Response)
客戶端:啦啦啦,有沒有新消息(Request)
服務端:。。。。。沒。。。。沒。。。沒有(Response) ---- loop

long poll 
long poll 其實原理跟 ajax輪詢 差不多,都是采用輪詢的方式,不過采取的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連接后,如果沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完之后,客戶端再次建立連接,周而復始。
場景再現
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request)
服務端:額。。 等待到有消息的時候。。來 給你(Response)
客戶端:啦啦啦,有沒有新信息,沒有的話就等有了才返回給我吧(Request) -loop

從上面可以看出其實這兩種方式,都是在不斷地建立HTTP連接,然后等待服務端處理,可以體現HTTP協議的另外一個特點,被動性
何為被動性呢,其實就是,服務端不能主動聯系客戶端,只能有客戶端發起。
簡單地說就是,服務器是一個很懶的冰箱(這是個梗)(不會、不能主動發起連接),但是上司有命令,如果有客戶來,不管多么累都要好好接待。

說完這個,我們再來說一說上面的缺陷(原諒我廢話這么多吧OAQ)
從上面很容易看出來,不管怎么樣,上面這兩種都是非常消耗資源的。
ajax輪詢 需要服務器有很快的處理速度和資源。(速度)
long poll 需要有很高的並發,也就是說同時接待客戶的能力。(場地大小)

 

     通過上面這個例子,我們可以看出,這兩種方式都不是最好的方式,需要很多資源。
一種需要更快的速度,一種需要更多的'電話'。這兩種都會導致'電話'的需求越來越高。
哦對了,忘記說了HTTP還是一個無狀態協議。(感謝評論區的各位指出OAQ)
通俗的說就是,服務器因為每天要接待太多客戶了,是個健忘鬼,你一掛電話,他就把你的東西全忘光了,把你的東西全丟掉了。你第二次還得再告訴服務器一遍。

所以在這種情況下出現了,Websocket出現了。
他解決了HTTP的這幾個難題。
首先,被動性,當服務器完成協議升級后(HTTP->Websocket),服務端就可以主動推送信息給客戶端啦。
所以上面的情景可以做如下修改。
客戶端:啦啦啦,我要建立Websocket協議,需要的服務:chat,Websocket協議版本:17(HTTP Request)
服務端:ok,確認,已升級為Websocket協議(HTTP Protocols Switched)
客戶端:麻煩你有信息的時候推送給我噢。。
服務端:ok,有的時候會告訴你的。
服務端:balabalabalabala
服務端:balabalabalabala
服務端:哈哈哈哈哈啊哈哈哈哈
服務端:笑死我了哈哈哈哈哈哈哈

就變成了這樣,只需要經過一次HTTP請求,就可以做到源源不斷的信息傳送了。(在程序設計中,這種設計叫做回調,即:你有信息了再來通知我,而不是我傻乎乎的每次跑來問你)
這樣的協議解決了上面同步有延遲,而且還非常消耗資源的這種情況。
那么為什么他會解決服務器上消耗資源的問題呢?
其實我們所用的程序是要經過兩層代理的,即HTTP協議在Nginx等服務器的解析下,然后再傳送給相應的Handler(PHP等)來處理。
簡單地說,我們有一個非常快速的接線員(Nginx),他負責把問題轉交給相應的客服(Handler)
本身接線員基本上速度是足夠的,但是每次都卡在客服(Handler)了,老有客服處理速度太慢。,導致客服不夠。
Websocket就解決了這樣一個難題,建立后,可以直接跟接線員建立持久連接,有信息的時候客服想辦法通知接線員,然后接線員在統一轉交給客戶。
這樣就可以解決客服處理速度過慢的問題了。

同時,在傳統的方式上,要不斷的建立,關閉HTTP協議,由於HTTP是非狀態性的,每次都要 重新傳輸identity info(鑒別信息),來告訴服務端你是誰。
雖然接線員很快速,但是每次都要聽這么一堆,效率也會有所下降的,同時還得不斷把這些信息轉交給客服,不但浪費客服的 處理時間,而且還會在網路傳輸中消耗 過多的流量/時間。
但是Websocket只需要 一次HTTP握手,所以說整個通訊過程是建立在一次連接/狀態中,也就避免了HTTP的非狀態性,服務端會一直知道你的信息,直到你關閉請求,這樣就解決了接線員要反復解析HTTP協議,還要查看identity info的信息。
同時由 客戶主動詢問,轉換為 服務器(推送)有信息的時候就發送(當然客戶端還是等主動發送信息過來的。。),沒有信息的時候就交給接線員(Nginx),不需要占用本身速度就慢的 客服(Handler)
 
 

實現游戲公告功能

實現功能:游戲管理里發布游戲公告,其它游戲玩家頁面能夠馬上接受到游戲公告信息。

下面直接上代碼案例,這里主要展示關鍵代碼,底部有源碼。

一、案例

1、pom.xml文件

主要是添加springBoot和webSocket相關jar包,和一些輔助工具jar包(注意我采用的是springBoot2.1.0版本

  pom.xml

 

2、WebSocketConfig

 這個是websocket配置中心,配置一些核心配置。

復制代碼
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
//注解用於開啟使用STOMP協議來傳輸基於代理(MessageBroker)的消息,這時候控制器(controller)開始支持@MessageMapping,就像是使用@requestMapping一樣。
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {


    /**
     * 注冊端點,發布或者訂閱消息的時候需要連接此端點
     * setAllowedOrigins 非必須,*表示允許其他域進行連接
     * withSockJS  表示開始sockejs支持
     */
    public void registerStompEndpoints(StompEndpointRegistry registry) {

        registry.addEndpoint("/endpoint-websocket").setAllowedOrigins("*").withSockJS();
    }

    /**
     * 配置消息代理(中介)
     * enableSimpleBroker 服務端推送給客戶端的路徑前綴
     * setApplicationDestinationPrefixes  客戶端發送數據給服務器端的一個前綴
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");

    }
}
復制代碼

 

3、GameInfoController

  管理員發布公告消息對應的接口

復制代碼
/*
 *模擬游戲公告
 */
@Controller
public class GameInfoController {

  //@MessageMapping和@RequestMapping功能類似,用於設置URL映射地址,瀏覽器向服務器發起請求,需要通過該地址。
  //如果服務器接受到了消息,就會對訂閱了@SendTo括號中的地址傳送消息。
    @MessageMapping("/gonggao/chat")
    @SendTo("/topic/game_chat")
    public OutMessage gameInfo(InMessage message){

        return new OutMessage(message.getContent());
    }
}
復制代碼

 

4、管理員頁面和用戶頁面

 admin頁面和user頁面唯一的區別就是管理員多一個發送公告的權限,其它都一樣,user1和user2完全一樣。

(1)admin.html

  admin.html

(2)user1.html

  user1.html

 (3)user2.html

  user2.html

 

5.app.js

  這個是客戶端連接websocket的核心,通過html的點擊事件來完成。

復制代碼
var stompClient = null;

//這個方法僅僅是用來改變樣式,不是核心
function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#notice").html("");
}

//1、建立連接(先連接服務端配置文件中的基站,建立連接,然后訂閱服務器目錄消息
function connect() {
    //1、連接SockJS的endpoint是“endpoint-websocket”,與后台代碼中注冊的endpoint要一樣。
    var socket = new SockJS('/endpoint-websocket');

    //2、用stom進行包裝,規范協議
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {

    //3、建立通訊
        setConnected(true);
        console.log('Connected: ' + frame);

    //4、通過stompClient.subscribe()訂閱服務器的目標是'/topic/game_chat'發送過來的地址,與@SendTo中的地址對應。
        stompClient.subscribe('/topic/game_chat', function (result) {
            console.info(result)
            showContent(JSON.parse(result.body));
        });
    });
}

//2、關閉連接
function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

//3、游戲管理員發送公告信息(這個也是游戲用戶所沒有功能,其它都一樣)
function sendName() {
    //1、通過stompClient.send 向/app/gonggao/chat 目標 發送消息,這個是在控制器的@messageMapping 中定義的。(/app為前綴,配置里配置)
    stompClient.send("/app/gonggao/chat", {}, JSON.stringify({'content': $("#content").val()}));
}

//4、訂閱的消息顯示在客戶端指定位置
function showContent(body) {
    $("#notice").append("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleString()+"</td></tr>");
}


$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $( "#connect" ).click(function() { connect(); });
    $( "#disconnect" ).click(function() { disconnect(); });
    $( "#send" ).click(function() { sendName(); });
});
復制代碼

 

6、查看運行結果

 

7、小總結

  首先很明顯看的出,websocket最大的優點,就是可以服務端主動向客戶端發送消息,而此前http只能是客戶端向服務端發送請求。

   gitHub源碼:https://github.com/yudiandemingzi/spring-boot-websocket-study

 

實現一對一聊天功能

功能介紹:實現A和B單獨聊天功能,即A發消息給B只能B接收,同樣B向A發消息只能A接收。

本篇博客是在上一遍基礎上搭建,上一篇博客地址:【WebSocket】---實現游戲公告功能。底部有源碼。

先看演示效果:

一、案例解析

1、PTPContoller

復制代碼
/**
 * 功能描述:簡單版單人聊天
 * 這里沒有用到@SendTo("/topic/game_chat")來指定訂閱地址,而是通過SimpMessagingTemplate來指定
 */
@Controller
public class PTPContoller {
    @Autowired
    private WebSocketService ws;

    @MessageMapping("/ptp/single/chat")
    public void singleChat(InMessage message) {
        ws.sendChatMessage(message);
    }
}
復制代碼

這里和前面的公告消息,最大的區別就是接口上沒有通過@SendTo("/topic/game_chat")來發送消息。

(1)@SendTo和SimpMessagingTemplate區別

       spring websocket基於注解的@SendTo和@SendToUser雖然方便,但是有局限性,例如我這樣子的需求,我想手動的把消息推送給某個人,或者特定一組人,怎么辦,

  @SendTo只能推送給所有人(它是一個具體地址,一點都不靈活),@SendToUser只能推送給請求消息的那個人,這時,我們可以利用SimpMessagingTemplate這個類

  SimpMessagingTemplate有倆個推送的方法

convertAndSend(destination, payload);            //將消息廣播到特定訂閱路徑中,類似@SendTo 
convertAndSendToUser(user, destination, payload);//將消息推送到固定的用戶訂閱路徑中,類似@SendToUser

 

2、WebSocketService

復制代碼
import com.jincou.websocket.model.InMessage;
import com.jincou.websocket.model.OutMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

/**
 * 功能描述:簡單消息模板,用來推送消息
 */
@Service
public class WebSocketService {

    @Autowired
    private SimpMessagingTemplate template;

    /**
     * 簡單點對點聊天室
     */
    public void sendChatMessage(InMessage message) {
        //可以看出template最大的靈活就是我們可以獲取前端傳來的參數來指定訂閱地址
        //前面參數是訂閱地址,后面參數是消息信息
        template.convertAndSend("/chat/single/"+message.getTo(),
                new OutMessage(message.getFrom()+" 發送:"+ message.getContent()));
    }
復制代碼

 

3、app.js

其它地方和公告的app.js一樣,只有下面兩點做了一點修改

復制代碼
function connect() {
    var from = $("#from").val();
    var socket = new SockJS('/endpoint-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        //1、通過+from就可以靈活的用當前用戶的某一信息來指定該用戶訂閱地址。
        stompClient.subscribe('/chat/single/'+from, function (result) {
            showContent(JSON.parse(result.body));
        });
    });
}

function sendName() {
     //2、這里出了發送content信息外,還發送了發送者用戶信息,和接受者的信息
    stompClient.send("/app/ptp/single/chat", {}, JSON.stringify({'content': $("#content").val(), 'to':$("#to").val(), 'from':$("#from").val()}));
}
復制代碼

 

4、user.html

其它地方也和之前公告的一樣,下面是修改的地方

復制代碼
  <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <input type="text" id="from" class="form-control" placeholder="我是">
                    <input type="text" id="to" class="form-control" placeholder="發送給誰">
                    
                    <input type="text" id="content" class="form-control" placeholder="請輸入...">
          
                    
                </div>
                <button id="send" class="btn btn-default" type="submit">發送</button>
            </form>
        </div>
復制代碼

 

5、再把整個思路縷一縷

以 A 向 B 發送消息為例

(1)form輸入框輸入:“A”,to輸入框輸入 “B” 點擊“Connect”建立websocket連接

(2)那么的 A 用戶的訂閱地址就是'/chat/single/A'

(3)前端在“content”按鈕中輸入“你今天吃雞了嗎?”,再點擊“發送”按鈕

(4)后台通過接受處理就成了:

template.convertAndSend("/chat/single/B",new OutMessage(" A 發送:你今天吃雞了嗎?"));

那么 B 向 A 發送性質一模一樣。就可以實現一對一聊天。

 

實現定時推送比特幣交易信息

 

實現功能:跟虛擬幣交易所一樣,時時更新當前比特幣的價格,最高價,最低價,買一價等等......

提示:(1)本篇博客是在上一遍基礎上搭建,上一篇博客地址:【WebSocket】---實現游戲公告功能

         (2)底部有相關源碼

先看效果演示

當前的信息就是虛擬幣交易所最新BTC的數據信息。

我們看到每隔1秒都會更新一次最新的比特幣當前信息。(截止到我發這篇博客時,比特幣當前價格:6473美元左右)

一、案例解析

 

1、如何調用虛擬幣的接口

你想獲得BTC最新的價格信息,你首先的有它的相關接口,不然如何獲取數據,我是在阿里雲上購買的。

具體步驟:

 (1)登陸阿里雲-->雲市場-->股票行情於匯率

 (2)有很多企業都有相關接口有股票也有虛擬幣

 (3)我選的一家名字叫:實時加密貨幣行情+推送

  網址:https://market.aliyun.com/products/57000002/cmapi029361.html?spm=5176.730005.productlist.d_cmapi029361.xtd4I4

 (4)對於接口都有相關說明,按照它的說明就可以獲取json數據。同時也可以在線調試。

 

2、通過定時任務時時向客戶端發送消息 

因為需要服務端隔一定時間向客戶端發送消息,所有服務端用定時任務再好不過了。

復制代碼
/**
 * //要啟動定時任務記得在啟動類上添加下面兩個注解
 * @ComponentScan(basePackages="com.jincou.websocket")
 * @EnableScheduling
 * 功能描述:股票推送,這里只需通過定時任務向客服端發送消息
 */
@Component
public class CoinSchedule {
    @Autowired
    private WebSocketService ws;
    
    //代表每一秒執行一次任務
    @Scheduled(fixedRate=1000)
    public void coinInfo(){
        ws.sendCoinInfo();
    }
}
復制代碼

  

3、WebSocketService類

消息模版工具類,用來推送消息用的。

復制代碼
/**
 * 功能描述:簡單消息模板,用來推送消息
 */
@Service
public class WebSocketService {

    @Autowired
    private SimpMessagingTemplate template;
 
    /**
     * 功能描述:Coin版本,虛擬幣信息推送
     */
    public void sendCoinInfo() {

        //CoinService.getStockInfo()已經把json數據轉為實體對象
        CoinResult coinResult = CoinService.getStockInfo();
        
  String msgTpl = "虛擬幣名稱: %s ;代碼: %s; 現價格: %s元 ;買一價: %s ; 買一量: %s ; 買二價: %s ; 賣二量: %s;";
        CoinResult.Obj  obj=coinResult.getObj();
        if (null != obj) {
            //將 %s 替換成實際值
            String msg = String.format(msgTpl, obj.getName(), obj.getSecurityCode(), obj.getNow(),
                    obj.getBid1(), obj.getBid1Volume(), obj.getAsk1(), obj.getAsk1Volume());

            //前面參數是訂閱地址,后面參數是消息信息(也就是比特幣時時消息)
            template.convertAndSend("/topic/coin_info",new OutMessage(msg));
        }
    }
}
復制代碼

 

4、CoinService調用接口,並把json格式數據賦值給對象

這個是最關鍵的一步,主要做的事:去調遠程接口獲取數據后,將數據封裝到自己所寫的bean實體中。

復制代碼
import java.util.HashMap;
import java.util.Map;
import com.jincou.websocket.model.CoinResult;
import com.jincou.websocket.utils.HttpUtils;
import com.jincou.websocket.utils.JsonUtils;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;

/**
 * 功能描述:接口服務,調用虛擬幣行情接口
 */
public class CoinService {

    public static CoinResult getStockInfo(){
         String host = "http://alirm-gbdc.konpn.com";
            String path = "/query/gbdc";
            String method = "GET";
            String appcode = "你的AppCode";
            Map<String, String> headers = new HashMap<String, String>();
            //最后在header中的格式(中間是英文空格)為Authorization:APPCODE 83359fd73fe94948385f570e3c139105
            headers.put("Authorization", "APPCODE " + appcode);
            Map<String, String> querys = new HashMap<String, String>();
            //BTC代表返回比特幣相關信息,如果這里傳入ETH那就代表返回以太坊信息
            querys.put("symbol", "BTC");

        try {
            //返回連接信息,如果里面帶有200,說明連接接口成功
            HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);

            //將response的body信息轉為字符串
            String responseText=EntityUtils.toString(response.getEntity());

            //上面部分只要根據你購買的api接口說明操作就可以,下面才是你需要處理的
            
            //將json格式的字符串(根據一定規則)賦值給實體對象(JsonUtils是自己的一個工具類)
            CoinResult coinResult = JsonUtils.objectFromJson(responseText, CoinResult.class);

            System.out.println("控制台打印虛擬幣當前信息=======================================");
            System.out.println(coinResult.toString());
            return coinResult;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
復制代碼

 

5、json格式如何封裝到實體

這步主要講,將json格式字符串通過工具類封裝到實體對象需要滿足的規則:

CoinResult coinResult = JsonUtils.objectFromJson(responseText, CoinResult.class); //看這步所需要滿足的規則

(1)先看接口的json格式

復制代碼
{"Code":0,"Msg":"",
    "Obj":{
    "B1":271.100,     --買一
    "B1V":129,        --買一量
    "B2":0,           --買二
    "B2V":0,
    "B3":0,           --買三
    "B3V":0,
    "B4":0,           --買四
    "B4V":0,        
    "B5":0,           --買五
    "B5V":0,
    "S1":271.150,    --賣一
    "S1V":20,        --賣一量
    "S2":0,          --賣二
    "S2V":0,
    "S3":0,          --賣三
    "S3V":0,
    "S4":0,          --賣四
    "S4V":0,
    "S5":0,          --賣五
    "S5V":0,
    "ZT":280.85,       --漲停價
    "DT":259.19,       --跌停價
    "O":270.39,        --今開
    "H":271.69,        --最高
    "L":270.14,        --最低
    "YC":270.55,       --昨收
    "A":35513202100.0, --交易額
    "V":130972,        --交易量
    "P":271.14,        --當前價
    "Tick":1529911046, --標准時間戳
    "N":"比特幣",       --品種名
    "M":"",            --市場
    "S":"BTC",         --品種代碼
    "C":""             --編號
    }
}
復制代碼

(2)在看我的實體對象屬性

復制代碼
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class CoinResult {

    //狀態碼,0代表成功
    @JsonProperty("Code")
    private int Code;
    //具體數據(注意這里json用{表示,所有代表對象
    @JsonProperty("Obj")
    private Obj obj;

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)

    public static class Obj {

        //虛擬幣代碼
        @JsonProperty("S")
        private String securityCode;

        //虛擬幣名稱
        @JsonProperty("N")
        private String name;

        //現在價格
        @JsonProperty("P")
        private double now;

        //最高價格
        @JsonProperty("H")
        private double high;

        //最低價格
        @JsonProperty("L")
        private double low;

        //買一價
        @JsonProperty("B1")
        private double bid1;

        //買一量
        @JsonProperty("B1V")
        private int bid1Volume;

        //賣一價
        @JsonProperty("S1")
        private double ask1;

        //賣一量
        @JsonProperty("S1V")
        private double ask1Volume;
        
        //已成交價,這個接口沒有提供,只要記住{}代表是對象,【】代表是結合那就需要集合接受:如下
        //private List<Transaction> transactions;
    }
}
復制代碼

總結規則:

(1)json中的名字和實體中屬性名一定要一致才能賦值。

(2)如果只要有一個你名字一致而數據類型不一樣,那么就會整體賦值失敗返回null。比如這里B1價,它明明是double,如你你用int接收,那么就會返回null。

(3)json格式中的數據如果是{},那么可以用對象來接收,好比這的"Obj":{...},如果是{[],[]},那就需要List<對象>來接收

 

 6、看前端

 前端沒啥好說的只需要訂閱:/topic/coin_info 這個地址就可以接收服務端時時發來的消息了。

  gitHub源碼https://github.com/yudiandemingzi/spring-boot-websocket-study

 

多人聊天系統

功能說明:多人聊天系統,主要功能點:

    1、當你登陸成功后,可以看到所有在線用戶(實際開發可以通過redis實現,我這邊僅僅用map集合)

    2、實現群聊功能,我發送消息,大家都可以看到。

先看案例效果:

      這里面有關在線人數有個bug,就是在線用戶會被覆蓋,lisi登陸的話,zhangsan在線信息就丟來,xiaoxiao登陸,lisi就丟來,這主要原因是因為我放的是普通集合,所以在線用戶數據是無法共享

所以只能顯示最后顯示的用戶,如果放到redis就不會有這個問題。

一、案例說明

1、UserChatController

復制代碼
@Controller
public class UserChatController {

    @Autowired
    private WebSocketService ws;

    /**
     * 1、登陸時,模擬數據庫的用戶信息
     */
    //模擬數據庫用戶的數據
    public static Map<String, String> userMap = new HashMap<String, String>();
    static{
        userMap.put("zhangsan", "123");
        userMap.put("lisi", "456");
        userMap.put("wangwu", "789");
        userMap.put("zhaoliu", "000");
        userMap.put("xiaoxiao", "666");
    }

    /**
     *2、 模擬用戶在線進行頁面跳轉的時候,判斷是否在線
     * (這個實際開發中肯定存在redis或者session中,這樣數據才能共享)
     * 這里只是簡單的做個模擬,所以暫且用普通map吧
     */
    public static Map<String, User> onlineUser = new HashMap<>();
    static{
        //key值一般是每個用戶的sessionID(這里表示admin用戶一開始就在線)
        onlineUser.put("123",new User("admin","888"));
    }
    
    
    /**
     *3、 功能描述:用戶登錄接口
     */
    @RequestMapping(value="login", method=RequestMethod.POST)
    public String userLogin( @RequestParam(value="username", required=true)String username, 
            @RequestParam(value="pwd",required=true) String pwd, HttpSession session) {

        //判斷是否正確
        String password = userMap.get(username);
        if (pwd.equals(password)) {
            User user = new User(username, pwd);
            String sessionId = session.getId();

            //用戶登陸成功就把該用戶放到在線用戶中...
            onlineUser.put(sessionId, user);
            //跳到群聊頁面
            return "redirect:/group/chat.html";
        } else {
            return "redirect:/group/error.html";
        }
        
    }
    
    /**
     *4、 功能描述:用於定時給客戶端推送在線用戶
     */
    @Scheduled(fixedRate = 2000)
    public void onlineUser() {
        ws.sendOnlineUser(onlineUser);
    }
    
    /**
     *5、 功能描述 群聊天接口
     * message 消息體
     * headerAccessor 消息頭訪問器,通過這個獲取sessionId
     */
    @MessageMapping("/group/chat")
    public void topicChat(InMessage message, SimpMessageHeaderAccessor headerAccessor){
        //這個sessionId是在HttpHandShakeIntecepter攔截器中放入的
        String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString();
        //通過sessionID獲得在線用戶信息
        User user = onlineUser.get(sessionId);
        message.setFrom(user.getUsername());
        ws.sendTopicChat(message);
        
    }    
}
復制代碼

 

2、握手請求的攔截器

復制代碼
/**
 * WebSocket握手請求的攔截器. 檢查握手請求和響應, 對WebSocketHandler傳遞屬性
 * 可以通過這個類的方法獲取resuest,和response
 */
public class HttpHandShakeIntecepter implements HandshakeInterceptor{


    //在握手之前執行該方法, 繼續握手返回true, 中斷握手返回false. 通過attributes參數設置WebSocketSession的屬性
    //這個項目只在WebSocketSession這里存入sessionID
    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
            ServerHttpResponse response, WebSocketHandler wsHandler,
            Map<String, Object> attributes) throws Exception {

        System.out.println("【握手攔截器】beforeHandshake");

        if(request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
            HttpSession session =  servletRequest.getServletRequest().getSession();
            String sessionId = session.getId();
            System.out.println("【握手攔截器】beforeHandshake sessionId="+sessionId);
            //這里將sessionId放入SessionAttributes中,
            attributes.put("sessionId", sessionId);
        }
        
        return true;
    }
 
    //在握手之后執行該方法. 無論是否握手成功都指明了響應狀態碼和相應頭(這個項目沒有用到該方法)
    @Override
    public void afterHandshake(ServerHttpRequest request,
            ServerHttpResponse response, WebSocketHandler wsHandler,
            Exception exception) {
        System.out.println("【握手攔截器】afterHandshake");
        
        if(request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
            HttpSession session =  servletRequest.getServletRequest().getSession();
            String sessionId = session.getId();
            System.out.println("【握手攔截器】afterHandshake sessionId="+sessionId);
        }
    }
}
復制代碼

 

3、頻道攔截器

復制代碼
/** 
 * 功能描述:頻道攔截器 ,類似管道,可以獲取消息的一些meta數據
 */
public class SocketChannelIntecepter extends ChannelInterceptorAdapter{

    /**
     * 在完成發送之后進行調用,不管是否有異常發生,一般用於資源清理
     */
    @Override
    public void afterSendCompletion(Message<?> message, MessageChannel channel,
            boolean sent, Exception ex) {
        System.out.println("SocketChannelIntecepter->afterSendCompletion");
        super.afterSendCompletion(message, channel, sent, ex);
    }

    
    /**
     * 在消息被實際發送到頻道之前調用
     */
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        System.out.println("SocketChannelIntecepter->preSend");
        
        return super.preSend(message, channel);
    }

    /**
     * 發送消息調用后立即調用
     */
    @Override
    public void postSend(Message<?> message, MessageChannel channel,
            boolean sent) {
        System.out.println("SocketChannelIntecepter->postSend");
        
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);//消息頭訪問器
        
        if (headerAccessor.getCommand() == null ) return ;// 避免非stomp消息類型,例如心跳檢測
        
        String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString();
        System.out.println("SocketChannelIntecepter -> sessionId = "+sessionId);
        
        switch (headerAccessor.getCommand()) {
        case CONNECT:
            connect(sessionId);
            break;
        case DISCONNECT:
            disconnect(sessionId);
            break;
        case SUBSCRIBE:    
            break;
        
        case UNSUBSCRIBE:
            break;
        default:
            break;
        }
    }

    /**
     * 連接成功
     */
    private void connect(String sessionId){
        System.out.println("connect sessionId="+sessionId);
    }

    /**
     * 斷開連接
     */
    private void disconnect(String sessionId){
        System.out.println("disconnect sessionId="+sessionId);
        //用戶下線操作
        UserChatController.onlineUser.remove(sessionId);
    }
    
}
復制代碼

 

4、修改webSocket配置類

    既然寫了兩個攔截器,那么肯定需要在配置信息里去配置它們。

復制代碼
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {


    /**
     *配置基站
     */
    public void registerStompEndpoints(StompEndpointRegistry registry) {

        registry.addEndpoint("/endpoint-websocket").addInterceptors(new HttpHandShakeIntecepter()).setAllowedOrigins("*").withSockJS();
    }

    /**
     * 配置消息代理(中介)
     * enableSimpleBroker 服務端推送給客戶端的路徑前綴
     * setApplicationDestinationPrefixes  客戶端發送數據給服務器端的一個前綴
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/topic","/chat");
        registry.setApplicationDestinationPrefixes("/app");

    }

  
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors( new SocketChannelIntecepter());
    }

    @Override
    public void configureClientOutboundChannel(ChannelRegistration registration) {
        registration.interceptors( new SocketChannelIntecepter());
    }

}
復制代碼

 

5、app.js

   登陸頁面和群聊頁面就不細聊,貼上代碼就好。

   index.html

  index.html

   chat.html

  chat.html

  app.js

復制代碼
var stompClient = null;

//一加載就會調用該方法 function connect() { var socket = new SockJS('/endpoint-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); //訂閱群聊消息 stompClient.subscribe('/topic/chat', function (result) { showContent(JSON.parse(result.body)); }); //訂閱在線用戶消息 stompClient.subscribe('/topic/onlineuser', function (result) { showOnlieUser(JSON.parse(result.body)); }); }); } //斷開連接 function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } //發送聊天記錄 function sendContent() { stompClient.send("/app/group/chat", {}, JSON.stringify({'content': $("#content").val()})); } //顯示聊天記錄 function showContent(body) { $("#record").append("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleTimeString()+"</td></tr>"); } //顯示實時在線用戶 function showOnlieUser(body) { $("#online").html("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleTimeString()+"</td></tr>"); } $(function () { connect();//自動上線 $("form").on('submit', function (e) { e.preventDefault(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendContent(); }); });
復制代碼

   gitHub源碼https://github.com/yudiandemingzi/spring-boot-websocket-study


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM