天天看點

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

本篇内容:

後端 + 前端簡單HTML頁面

功能場景點:

1.  群發,所有人都能收到

2.  局部群發,部分人群都能收到

3.  單點推送, 指定某個人的頁面

慣例,先看看本次實戰示例項目結構:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

可以看到内容不多,也就是說,springboot 整合socket, 跟着我學,輕輕松松。

古有曹植七步成詩,如今,咱們也是 7步學會整合socket!

不多說,開始:

 ① pom引入核心依賴

<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>1.7.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>      

 ② yml加上配置項

server:
  port: 8089

socketio:
   host: localhost
   port: 8503
   maxFramePayloadLength: 1048576
   maxHttpContentLength: 1048576
   bossCount: 1
   workCount: 100
   allowCustomRequests: true
   upgradeTimeout: 10000
   pingTimeout: 60000
   pingInterval: 25000      

③ 建立socket配置加載類 MySocketConfig.java

import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: JCccc
 * @Description:
 * @Date: 2022/06/13 21:50
 */
@Configuration
public class MySocketConfig{

    @Value("${socketio.host}")
    private String host;

    @Value("${socketio.port}")
    private Integer port;

    @Value("${socketio.bossCount}")
    private int bossCount;

    @Value("${socketio.workCount}")
    private int workCount;

    @Value("${socketio.allowCustomRequests}")
    private boolean allowCustomRequests;

    @Value("${socketio.upgradeTimeout}")
    private int upgradeTimeout;

    @Value("${socketio.pingTimeout}")
    private int pingTimeout;

    @Value("${socketio.pingInterval}")
    private int pingInterval;

    @Bean
    public SocketIOServer socketIOServer() {
        SocketConfig socketConfig = new SocketConfig();
        socketConfig.setTcpNoDelay(true);
        socketConfig.setSoLinger(0);
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        buildSocketConfig(socketConfig, config);
        return new SocketIOServer(config);
    }

    /**
     * 掃描netty-socketIo的注解( @OnConnect、@OnEvent等)
     */
    @Bean
    public SpringAnnotationScanner springAnnotationScanner() {
        return new SpringAnnotationScanner(socketIOServer());
    }

    private void buildSocketConfig(SocketConfig socketConfig, com.corundumstudio.socketio.Configuration config) {
        config.setSocketConfig(socketConfig);
        config.setHostname(host);
        config.setPort(port);
        config.setBossThreads(bossCount);
        config.setWorkerThreads(workCount);
        config.setAllowCustomRequests(allowCustomRequests);
        config.setUpgradeTimeout(upgradeTimeout);
        config.setPingTimeout(pingTimeout);
        config.setPingInterval(pingInterval);
    }
}      

④建立消息實體 MyMessage.java

/**
 * @Author: JCccc
 * @Date: 2022-07-23 9:05
 * @Description:
 */
public class MyMessage {

    private String type;

    private String content;

    private String from;

    private String to;

    private String channel;


    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getChannel() {
        return channel;
    }

    public void setChannel(String channel) {
        this.channel = channel;
    }
}      

代碼簡析:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

⑤建立 socket handler 負責記錄用戶端 連接配接、下線

MySocketHandler.java

import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.socket.mysocket.util.SocketUtil;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;


/**
 * @Author: JCccc
 * @Description:
 * @Date: 2022/6/23 21:21
 */
@Component
public class MySocketHandler {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private SocketIOServer socketIoServer;
    @PostConstruct
    private void start(){
        try {
            socketIoServer.start();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    @PreDestroy
    private void destroy(){
        try {
        socketIoServer.stop();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    @OnConnect
    public void connect(SocketIOClient client) {
        String userFlag = client.getHandshakeData().getSingleUrlParam("userFlag");
        SocketUtil.connectMap.put(userFlag, client);
        log.info("用戶端userFlag: "+ userFlag+ "已連接配接");
    }
    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        String userFlag = client.getHandshakeData().getSingleUrlParam("userFlag");
        log.info("用戶端userFlag:" + userFlag + "斷開連接配接");
        SocketUtil.connectMap.remove(userFlag, client);
    }
}      

代碼簡析:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

 ⑥ 封裝的socket 小函數

SocketUtil.java

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

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

/**
 * @Author: JCccc
 * @Description:
 * @Date: 2022/6/23 21:28
 */
@Component
public class SocketUtil {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    //暫且把使用者&用戶端資訊存在緩存
    public static ConcurrentMap<String, SocketIOClient> connectMap = new ConcurrentHashMap<>();

    @OnEvent(value = "CHANNEL_SYSTEM")
    public void systemDataListener(String receiveMsg) {
        if (!StringUtils.hasLength(receiveMsg)){
            return;
        }
        JSONObject msgObject = (JSONObject) JSON.parse(receiveMsg);
        String userFlag = String.valueOf(msgObject.get("from"));
        String content = String.valueOf(msgObject.get("content"));
        log.info("收到使用者 : {} 推送到系統頻道的一條消息 :{}",userFlag,content );
    }

    public void sendToAll(Map<String, Object> msg,String sendChannel) {
        if (connectMap.isEmpty()){
            return;
        }
        //給在這個頻道的每個用戶端發消息
        for (Map.Entry<String, SocketIOClient> entry : connectMap.entrySet()) {
            entry.getValue().sendEvent(sendChannel, msg);
        }
    }

    public void sendToOne(String userFlag, Map<String, Object> msg,String sendChannel) {
        //拿出某個用戶端資訊
        SocketIOClient socketClient = getSocketClient(userFlag);
        if (Objects.nonNull(socketClient) ){
            //單獨給他發消息
            socketClient.sendEvent(sendChannel,msg);
        }
    }


    /**
     * 識别出用戶端
     * @param userFlag
     * @return
     */
    public SocketIOClient getSocketClient(String userFlag){
        SocketIOClient client = null;
        if (StringUtils.hasLength(userFlag) &&  !connectMap.isEmpty()){
            for (String key : connectMap.keySet()) {
                if (userFlag.equals(key)){
                    client = connectMap.get(key);
                }
            }
        }
        return client;
    }



}      

代碼簡析:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

⑦寫1個接口,模拟場景,前端頁面調用後端接口,做消息推送

TestController.java

import com.socket.mysocket.dto.MyMessage;
import com.socket.mysocket.util.SocketUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: JCccc
 * @Description:
 * @Date: 2022/06/13 21:50
 */
@RestController
public class TestController {
    public final static String SEND_TYPE_ALL = "ALL";
    public final static String SEND_TYPE_ALONE = "ALONE";
    @Autowired
    SocketUtil socketUtil;

    @PostMapping("/testSendMsg")
    public String testSendMsg(@RequestBody MyMessage myMessage){
        Map<String, Object> map = new HashMap<>();
        map.put("msg",myMessage.getContent());

        //群發
        if (SEND_TYPE_ALL.equals(myMessage.getType())){
            socketUtil.sendToAll( map,myMessage.getChannel());
            return "success";
        }
        //指定單人
        if (SEND_TYPE_ALONE.equals(myMessage.getType())){
            socketUtil.sendToOne(myMessage.getTo(), map, myMessage.getChannel());
            return "success";
        }

        return "fail";
    }
}      

代碼簡析:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

好了,7步了。一切已經就緒了。

前端簡單頁面

接下來搞點前端HTML頁面, 玩一玩看看效果:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

第一個頁面:

TestClientStudentJC.html

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>我要連SOCKET</title>
    <base>
    <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
    <style>
        body {
            padding: 20px;
        }
        #console {
            height: 450px;
            overflow: auto;
        }
        .msg-color {
            color: green;
        }
    </style>
</head>

<body>
<div id="console" class="well"></div>


<div id="conversationDiv">
    <labal>給系統推消息</labal>
    <input type="text" id="content"/>
    <button id="btnSendToSystem" onclick="sendSys();">發送</button>
</div>


</body>
<script type="text/javascript">
    var socket;
    connect();

    function connect() {
        var userFlag = 'user_JC';
        var opts = {
            query: 'userFlag=' + userFlag
        };
        socket = io.connect('http://localhost:8503', opts);
        socket.on('connect', function () {
            console.log("連接配接成功");
            output('目前使用者是:' + userFlag );
            output('<span class="msg-color">連接配接成功了。</span>');
        });
        socket.on('disconnect', function () {
            output('<span class="msg-color">下線了。 </span>');
        });

        socket.on('CHANNEL_STUDENT', function (data) {
            let msg= JSON.stringify(data)
            output('收到學生頻道消息了:' + msg );
            console.log(data);

        });
        socket.on('CHANNEL_SYSTEM', function (data) {
            let msg= JSON.stringify(data)
            output('收到系統全局消息了:' + msg );
            console.log(data);

        });

    }

    function sendSys() {
        console.log('發送消息給服務端');
        var content = document.getElementById('content').value;

        socket.emit('CHANNEL_SYSTEM',JSON.stringify({
            'content': content,
            'from': 'user_JC'
        }));

    }
    function output(message) {
        var element = $("<div>" + message + "</div>");
        $('#console').prepend(element);
    }
    
</script>
</html>      

代碼簡析:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

第二個頁面,跟第一個基本一樣,改一下使用者唯一辨別:

TestClientStudentPU.html

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>我要連SOCKET</title>
    <base>
    <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
    <style>
        body {
            padding: 20px;
        }
        #console {
            height: 450px;
            overflow: auto;
        }
        .msg-color {
            color: green;
        }
    </style>
</head>

<body>
<div id="console" class="well"></div>


<div id="conversationDiv">
    <labal>給系統推消息</labal>
    <input type="text" id="content"/>
    <button id="btnSendToSystem" onclick="sendSys();">發送</button>
</div>


</body>
<script type="text/javascript">
    var socket;
    connect();

    function connect() {
        var userFlag = 'user_PU';
        var opts = {
            query: 'userFlag=' + userFlag
        };
        socket = io.connect('http://localhost:8503', opts);
        socket.on('connect', function () {
            console.log("連接配接成功");
            output('目前使用者是:' + userFlag );
            output('<span class="msg-color">連接配接成功了。</span>');
        });
        socket.on('disconnect', function () {
            output('<span class="msg-color">下線了。 </span>');
        });

        socket.on('CHANNEL_STUDENT', function (data) {
            let msg= JSON.stringify(data)
            output('收到學生頻道消息了:' + msg );
            console.log(data);

        });
        socket.on('CHANNEL_SYSTEM', function (data) {
            let msg= JSON.stringify(data)
            output('收到系統全局消息了:' + msg );
            console.log(data);

        });

    }

    function sendSys() {
        console.log('發送消息給服務端');
        var content = document.getElementById('content').value;

        socket.emit('CHANNEL_SYSTEM',JSON.stringify({
            'content': content,
            'from': 'user_PU'
        }));

    }
    function output(message) {
        var element = $("<div>" + message + "</div>");
        $('#console').prepend(element);
    }

</script>
</html>      

OK,把項目跑起來,開始玩。

直接通路用戶端頁面 模拟學生 JC連接配接socket:

​​​http://127.0.0.1:8089/TestClientStudentJC.html​​

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

 可以看到服務端有監測到:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

 這裡監測的:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

先試試用戶端給系統推消息先:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

 可以看到服務端成功收到消息:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

 這種方式,其實是因為服務監聽了相關的頻道:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

 前端使用JS推到這個系統頻道:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

ps: 其實前端給服務端推消息,其實調用接口就可以。

OK,進入核心應用場景1:

 群發,所有人都能收到

 系統給連上的用戶端都推送消息

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多
{
"type": "ALL",
"content":"你們好,這是一條廣播消息,全部人都能收到",
"channel":"CHANNEL_SYSTEM"
}      

看看效果:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

 然後是場景2,局部群發,部分人群都能收到

其實也就是通過HTML 用戶端監聽主題做區分就好:

直接拉人口,升3 :

 模拟2個學生,1個老師都連接配接上了socket

當然,老師監聽的是 老師頻道:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

然後我們模拟推送一下消息到指定的老師頻道: 

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多
{
"type": "ALL",
"content":"給老師們推一條消息!!!",
"channel":"CHANNEL_TEACHER"
}      
Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

最後一個場景,也就是單點推送,指定某個人收到

模拟 學生 PU 給 學生JC 推消息:

Springboot 整合 Socket 實戰案例 ,實作 單點發送、廣播群發,1對1,1對多

可以看到在學生頻道的JC正常收到了PU的消息:

繼續閱讀