天天看點

SpringBoot:Netty-SocketIO + VUE:SocketIO實作前後端實時雙向通信

文章目錄

  • ​​一、WebSocket簡介​​
  • ​​二、Netty-SocketIO 服務端Demo​​
  • ​​1、添加配置類SocketIOServer​​
  • ​​2、添加消息結構類MessageInfo​​
  • ​​3、添加消息處理類MessageEventHandler​​
  • ​​4、添加啟動類ServerRunner​​
  • ​​三、SocketIO 用戶端Demo​​

一、WebSocket簡介

WebSocket是HTML5新增的一種全雙工通信協定,用戶端和服務端基于TCP握手連接配接成功後,兩者之間就可以建立持久性的連接配接,實作雙向資料傳輸。

傳統HTTP和WebSocket的不同點:

HTTP是單向資料流,用戶端向服務端發送請求,服務端響應并傳回資料;

WebSocket連接配接後可以實作用戶端和服務端雙向資料傳遞。

由于是新的協定,HTTP的url使用"http//"或"https//"開頭;WebSocket的url使用"ws//"開頭。

傳統HTTP和WebSocket的相同點:

都需要建立TCP連接配接,都是屬于七層協定中的應用層協定。

傳統通過HTTP請求模拟雙向資料傳遞的方式是http+Polling(輪詢)和http+Long Polling(長輪詢)。輪詢(Polling)就是用戶端定時發送get/post請求向服務端請求資料,這種方式能滿足一定的需求,但是存在一些問題,例如如果服務端沒有新資料,用戶端請求到的資料都是舊資料,這樣不僅浪費了帶寬資源,而且占用CPU記憶體。

LongPolling(長輪詢)就是在Polling上的一些改進,即如果服務端沒有新資料傳回給用戶端,服務端會把目前的這個服務請求保持住(hold),當有新資料時則傳回新資料,如果超過一定時間服務端仍沒有新資料,則服務端傳回逾時請求,用戶端接收到逾時請求,然後在發送服務請求,一直循環執行。

雖然一定程度解決了帶寬資源和CPU記憶體浪費情況,但是當服務端資料更新很快,這和輪詢(Polling)沒有本質上的差別,而且http資料包的頭部資料量往往很大(通常有400多個位元組),但是真正被伺服器需要的資料卻很少(有時隻有10個位元組左右),這樣的資料包在網絡上周期性的傳輸,難免對網絡帶寬是一種浪費。在高并發的情況下,這對伺服器是一個很大的挑戰。綜合上面輪詢的種種問題,WebSocket全雙工通信成為一種很好的解決政策。

二、Netty-SocketIO 服務端Demo

實際應用中,如果需要WebSocket進行雙向資料通信,SocketIO是一個非常好的選擇。它是用JavaScript語言實作的WebSocket架構,簡單易用,穩定可靠。SocketIO不是WebSocket,它隻是将WebSocket和輪詢 (Polling)機制以及其它的實時通信方式封裝成了通用的接口,并且在服務端實作了這些實時機制的相應代碼。也就是說,WebSocket僅僅是SocketIO實作實時通信的一個子集。

常用的方式是前端使用SocketIO,後端使用node.js實作SocketIO的接口。但由于目前服務端使用JAVA,是以我使用的是Netty-SocketIO開源庫,基于Netty網絡庫編寫的WebSocket實作。

下面是從項目工程中提煉出的Netty-SocketIO服務端Demo:

首先在pom.xml中添加相應的依賴庫:

<dependency>
        <groupId>com.corundumstudio.socketio</groupId>
        <artifactId>netty-socketio</artifactId>
        <version>1.7.13</version>
</dependency>
<dependency>
        <groupId>io.socket</groupId>
        <artifactId>socket.io-client</artifactId>
        <version>1.0.0</version>
</dependency>      

1、添加配置類SocketIOServer

添加SocketIO配置類NettySocketConfig.java,用于填寫nettysocket的相關配置資訊, 注冊netty-socketio服務端,相關代碼如下:

package nssc.simulation.DataTransmission.socket;

import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
@Slf4j
public class NettySocketConfig {

    @Bean
    public SocketIOServer socketIOServer() {
        /*
         * 建立Socket,并設定監聽端口
         */
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        // 設定主機名,預設是0.0.0.0
        // config.setHostname("localhost");
        // 設定監聽端口
        config.setPort(9090);
        // 協定更新逾時時間(毫秒), 預設10000, HTTP握手更新為ws協定逾時時間
        config.setUpgradeTimeout(10000);
        // Ping消息間隔(毫秒), 預設25000, 用戶端向伺服器發送一條心跳消息間隔
        config.setPingInterval(60000);
        // Ping消息逾時時間(毫秒), 預設60000, 這個時間間隔内沒有接收到心跳消息就會發送逾時事件
        config.setPingTimeout(180000);

        final SocketIOServer server = new SocketIOServer(config);
        return server;
    }

    @Bean
    public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
        return new SpringAnnotationScanner(socketServer);
    }

}      

2、添加消息結構類MessageInfo

添加消息結構類MessageInfo.java,用于接收前台使用者資訊,相關代碼如下:

消息類型按需自己設計,這裡我定義的是byte[]位元組數組。

package nssc.simulation.DataTransmission.socket;

import lombok.ToString;
import org.springframework.stereotype.Component;

@Component
@ToString
public class MessageInfoStructure {
    //消息類型
    private String msgType;
    //消息内容
    private byte[] msgContent;

    public String getMsgType() {
        return msgType;
    }
    public void setMsgType(String msgType) {
        this.msgType = msgType;
    }
    public byte[] getMsgContent() {
        return msgContent;
    }
    public void setMsgContent(byte[] msgContent) {
        this.msgContent = msgContent;
    }
}      

3、添加消息處理類MessageEventHandler

添加消息處理類MessageEventHandler.Java,用于前後端消息事件的互動,相關代碼如下:

package nssc.simulation.DataTransmission.socket;

import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.HandshakeData;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import lombok.extern.slf4j.Slf4j;
import nssc.simulation.EmulationDataTransmission.udp.UdpCommonSendSave;
import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

@Component
@Slf4j
public class NettySocketEventHandler {
    public static ConcurrentMap<String, SocketIOClient> socketIOClientMap =
            new ConcurrentHashMap<>();

    //socket事件消息接收入口
    @OnEvent(value = "message_event") //value值與前端自行商定
    public void onEvent(SocketIOClient client, AckRequest ackRequest, MessageInfoStructure data) throws Exception {
        //根據msgType類别進行資料類型判斷,
        if (data.getMsgType().equals("XXXXXData")){ //資料類型辨別
            client.sendEvent("message_event", "已成功接收資料"); //向前端發送接收資料成功辨別
            
            //data.getMsgContent()擷取前端推送資料
            //......這裡可填寫接收資料後的相關業務邏輯代碼
        }
    }

    //socket添加@OnDisconnect事件,用戶端斷開連接配接時調用,重新整理用戶端資訊
    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        log.info("--------------------用戶端已斷開連接配接--------------------");
        client.disconnect();
    }

    //socket添加connect事件,當用戶端發起連接配接時調用
    @OnConnect
    public void onConnect(SocketIOClient client) {
        if (client != null)
        {
            HandshakeData client_mac = client.getHandshakeData();
            String mac = client_mac.getUrl();
            //存儲SocketIOClient,用于向不同用戶端發送消息
            socketIOClientMap.put(mac, client);

            log.info("--------------------用戶端連接配接成功---------------------");
        } else {
            log.error("用戶端為空");
        }
    }

    /**
     * 廣播消息 函數可在其他類中調用
     */
    public static void sendBroadcast(byte[] data) {
        for (SocketIOClient client : socketIOClientMap.values()) { //向已連接配接的所有用戶端發送資料,map實作用戶端的存儲
            if (client.isChannelOpen()) {
                client.sendEvent("message_event", data);
            }
        }
    }
}      

4、添加啟動類ServerRunner

在項目服務啟動的時候啟動socket.io服務, 新增啟動類ServerRunner.java,相關代碼如下所示:

package nssc.simulation.DataTransmission.socket;

import com.corundumstudio.socketio.SocketIOServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(value=1)
@Slf4j
public class NettySocketRunnable implements CommandLineRunner {
    private final SocketIOServer server;

    @Autowired
    public NettySocketRunnable(SocketIOServer server) {
        this.server = server;
    }

    @Override
    public void run(String... args) throws Exception {
        log.info("--------------------前端socket.io通信啟動成功!---------------------");
        server.start();
    }
}      

三、SocketIO 用戶端Demo

用戶端Demo這裡參考了Github上的官方開源庫SocketIO,按照項目所需功能可以後續進行修改

<script src="js/socket.io/socket.io.js"></script>
    <script src="js/moment.min.js"></script>
    <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>

<script>
/**
* 前端js的 socket.emit("事件名","參數資料")方法,是觸發後端自定義消息事件的時候使用的,
* 前端js的 socket.on("事件名",匿名函數(伺服器向用戶端發送的資料))為監聽伺服器端的事件
**/

var socket =  io.connect('http://localhost:9092');

//監聽伺服器連接配接事件
socket.on('connect', function() {
  output('<span class="connect-msg">Client has connected to the server!</span>');
});
//監聽伺服器端發送消息事件
socket.on('message_event', function(data) { 
  message(data)
  //console.log("伺服器發送的消息是:"+data); 
}); 
//監聽伺服器關閉服務事件
socket.on('disconnect', function() {
  output('<span class="disconnect-msg">The client has disconnected!</span>');
});

              function sendDisconnect() {
                      socket.disconnect();
              }
//點選發送消息觸發 
function sendMessage() {
                      var message = $('#msg').val();
                      $('#msg').val('');

                      var jsonObject = {userName: userName,
                                        message: message};
                      socket.emit('chatevent', jsonObject);
}
</script>      

繼續閱讀