SocketIO introduction + SpringBoot integrates SocketIO to complete real-time communication

Posted by dgrinberg on Sun, 23 Jan 2022 16:53:10 +0100

Socket.IO notes

Instant messaging is based on TCP long connection. After the connection is established, the client / server can send data to the opposite end at any time for unlimited times, so as to realize the immediacy of server data transmission

HTTP

HTTP is a short link, which is designed to reduce the pressure on the server

HTTP pseudo instant messaging

  • Polling emmet
  • Long polling

Application scenario

  • Chat function
  • Push the following order online and immediately to the merchant

Realize instant messaging

  • Build your own server
    • Select the supported protocol websocket xmpp
    • Use some mature frameworks, socketio xmppframework
    • Self encapsulating socket
  • Use mature third-party solutions
    • Rongyun Huanxin

Websocket

Both websocket and HTTP are based on Tcp. The difference is that HTTP establishes a short link, while websocket establishes a long link

The disadvantage is that websocket can only support some platforms

Socket.IO

Socket.io is a mature solution based on WebSocket protocol, which can realize real-time, two-way and event based communication between browsers and servers. SocketIO encapsulates WebSocket, AJAX and other communication methods into a unified communication interface, that is, we don't have to worry about compatibility when using SocketIO, The bottom layer will automatically select the best communication mode.

advantage

  • Ease of use
  • Cross platform
  • self-adaption

shortcoming

  • The transmitted data does not fully follow the webSocket protocol, which requires that both the client and server must use socket IO solutions

SpringBoot integrates SocketIO to realize real-time push

The directory structure is as follows

1. pom.xml

        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>1.7.11</version>
        </dependency>

2.application.properties

server.port=8080
server.servlet.context-path=/
spring.mvc.view.prefix=/templates
spring.mvc.view.suffix=.html

#============================================================================
# netty socket io setting
#============================================================================
# host can be set to localhost or local IP in local test, and can be changed to server IP in Linux server
socketio.host=localhost
socketio.port=9092
# Set the maximum length of data processed per frame to prevent others from using big data to attack the server
socketio.maxFramePayloadLength=1048576
# Set the maximum content length of http interaction
socketio.maxHttpContentLength=1048576
# Number and size of socket connections (if only one port is monitored, the boss thread group is 1)
socketio.bossCount=1
socketio.workCount=100
socketio.allowCustomRequests=true
# Protocol upgrade timeout (MS), 10 seconds by default. Timeout for upgrading HTTP handshake to ws protocol
socketio.upgradeTimeout=1000000
# The timeout time (MS) of Ping message is 60 seconds by default. If no heartbeat message is received within this time interval, a timeout event will be sent
socketio.pingTimeout=6000000
# Ping message interval (MS), 25 seconds by default. Interval between clients sending a heartbeat message to the server
socketio.pingInterval=25000

3. Create auxiliary class

3.1 create channel connection cache class

package com.erha.socketio.cache;

import com.corundumstudio.socketio.SocketIOClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassNameClientCache
 * @Description TODO Cache user - page sessionId - channel connection
 * @Author DELL
 * @Date 2022/1/2113:55
 * @Version 1.0
 **/
@Component
public class ClientCache {
    /**
     * @Author Old man erha
     * @Description //TODO User information cache
     * @Date 14:00 2022/1/21
     * @Param
     * @return
     **/
    private static Map<String, HashMap<UUID, SocketIOClient>> concurrentHashMap = new ConcurrentHashMap<>();

    /**
     * @Author Old man erha
     * @Description //TODO userId-User ID | sessionId - page sessionid | socketoclient - channel connection corresponding to page
     * @Date 14:03 2022/1/21
     * @Param [userId, sessionId, socketIOClient]
     * @return void
     **/
    public void saveClient(String userId,UUID sessionId,SocketIOClient socketIOClient){
        HashMap<UUID, SocketIOClient> sessionIdClientCache = concurrentHashMap.get(userId);
        if(sessionIdClientCache == null){
            sessionIdClientCache = new HashMap<>();
        }
        sessionIdClientCache.put(sessionId,socketIOClient);
        concurrentHashMap.put(userId,sessionIdClientCache);
    }

    /**
     * @Author Old man erha
     * @Description //TODO Get the user's page channel information
     * @Date 14:12 2022/1/21
     * @Param [userId]
     * @return java.util.HashMap<java.util.UUID,com.corundumstudio.socketio.SocketIOClient>
     **/
    public HashMap<UUID,SocketIOClient> getUserClient(String userId){
        return concurrentHashMap.get(userId);
    }

    /**
     * @Author Old man erha
     * @Description //TODO Delete page channel connection according to user Id and page sessionID
     * @Date 14:14 2022/1/21
     * @Param [userId, sessionId]
     * @return void
     **/
    public void deleteSessionClientByUserId(String userId,UUID sessionId){
        concurrentHashMap.get(userId).remove(sessionId);
    }

    /**
     * @Author Old man erha
     * @Description //TODO Delete the user channel connection cache according to the user Id, and the cache is not used
     * @Date 14:19 2022/1/21
     * @Param [userId]
     * @return void
     **/
    public void deleteUserCacheByUserId(String userId){
        concurrentHashMap.remove(userId);
    }
}

3.2 create message class

package com.erha.socketio.pojo;

import org.springframework.stereotype.Component;

/**
 * @ClassNameMessageInfo
 * @Description TODO
 * @Author DELL
 * @Date 2022/1/2114:36
 * @Version 1.0
 **/
@Component
public class MessageInfo {
    private String userID;
    private String userName;
    private String message;

    public MessageInfo() {
    }

    public MessageInfo(String userID, String userName, String message) {
        this.userID = userID;
        this.userName = userName;
        this.message = message;
    }

    public MessageInfo(String userName, String message) {
        this.userName = userName;
        this.message = message;
    }

    @Override
    public String toString() {
        return "MessageInfo{" +
                "userID='" + userID + '\'' +
                ", userName='" + userName + '\'' +
                ", message='" + message + '\'' +
                '}';
    }

    public String getUserID() {
        return userID;
    }

    public void setUserID(String userID) {
        this.userID = userID;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

4. Create SocketIO configuration class

package com.erha.socketio.config;

import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.erha.socketio.handler.SocketIOHandler;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * @ClassNameSocketIOConfig
 * @Description TODO
 * @Author DELL
 * @Date 2022/1/2114:19
 * @Version 1.0
 **/
@Configuration
public class SocketIOConfig implements InitializingBean {

    @Resource
    private SocketIOHandler socketIOHandler;

    @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;


    @Override
    public void afterPropertiesSet() throws Exception {
        SocketConfig socketConfig = new SocketConfig();
        socketConfig.setReuseAddress(true);
        socketConfig.setTcpNoDelay(true);
        socketConfig.setSoLinger(0);

        com.corundumstudio.socketio.Configuration configuration = new com.corundumstudio.socketio.Configuration();
        configuration.setSocketConfig(socketConfig);
        // host can be set to localhost or local IP in local test, and can be changed to server IP in Linux server
        configuration.setHostname(host);
        configuration.setPort(port);
        // Number and size of socket connections (if only one port is monitored, the boss thread group is 1)
        configuration.setBossThreads(bossCount);
        configuration.setWorkerThreads(workCount);
        configuration.setAllowCustomRequests(allowCustomRequests);
        // Protocol upgrade timeout (MS), 10 seconds by default. Timeout for upgrading HTTP handshake to ws protocol
        configuration.setUpgradeTimeout(upgradeTimeout);
        // The timeout time (MS) of Ping message is 60 seconds by default. If no heartbeat message is received within this time interval, a timeout event will be sent
        configuration.setPingTimeout(pingTimeout);
        // Ping message interval (MS), 25 seconds by default. Interval between clients sending a heartbeat message to the server
        configuration.setPingInterval(pingInterval);

        SocketIOServer socketIOServer = new SocketIOServer(configuration);
        //Add event listener
        socketIOServer.addListeners(socketIOHandler);
        //Start SocketIOServer
        socketIOServer.start();
        System.out.println("SocketIO Start up complete");
    }
}

5.SocketIO event response class

package com.erha.socketio.handler;

import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import com.erha.socketio.cache.ClientCache;
import com.erha.socketio.pojo.MessageInfo;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;

/**
 * @ClassNameSocketIOHandler
 * @Description TODO
 * @Author DELL
 * @Date 2022/1/2114:29
 * @Version 1.0
 **/
@Component
public class SocketIOHandler {
    @Resource
    private ClientCache clientCache;

    /**
     * @Author Old man erha
     * @Description //TODO Triggered when the client connects, the front-end js triggers: socket = io connect(" http://localhost:9092 ");
     * @Date 12:12 2022/1/20
     * @Param [client]
     * @return void
     **/
    @OnConnect
    public void onConnect(SocketIOClient client){
        String userId = client.getHandshakeData().getSingleUrlParam("userId");
        UUID sessionId = client.getSessionId();
        clientCache.saveClient(userId,sessionId, client);
        System.out.println("userId: "+userId+"Connection established successfully - "+sessionId);
    }

    /**
     * @Author Old man erha
     * @Description //TODO Trigger when the client closes the connection: front end js trigger: socket disconnect();
     * @Date 12:14 2022/1/20
     * @Param [client]
     * @return void
     **/
    @OnDisconnect
    public void onDisconnect(SocketIOClient client){
        String userId = client.getHandshakeData().getSingleUrlParam("userId");
        UUID sessionId = client.getSessionId();
        clientCache.deleteSessionClientByUserId(userId,sessionId);
        System.out.println("userId: "+userId+"Connection closed successfully - "+sessionId);
    }

    /**
     * @Author Old man erha
     * @Description
     *  //TODO Custom message event, triggered by client js: socket emit('messageevent', {msgContent: msg}); This method is triggered when
     *  //TODO Front end js socket The emit ("event name", "parameter data") method is used when triggering back-end custom message events
     *  //TODO Front end js socket On ("event name", anonymous function (data sent by the server to the client)) is to listen to events on the server
     * @Date 13:51 2022/1/20
     * @Param [client, request, data]
     * @return void
     **/
    @OnEvent("chatevent")
    public void chatEvent(SocketIOClient client, AckRequest ackRequest, MessageInfo message){
        HashMap<UUID, SocketIOClient> userClient = clientCache.getUserClient("79");
        Iterator<Map.Entry<UUID, SocketIOClient>> iterator = userClient.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<UUID, SocketIOClient> next = iterator.next();
            next.getValue().sendEvent("chatevent", message);
        }
        System.out.println(message);
    }
}

6. Control layer Controller

6.1 page Jump to basic class

@Controller
public class baseController {
    @RequestMapping(value = "/")
    public String index(){
        return "index";
    }
}

6.2 the page sends information according to the channel connection test class

@RestController
@RequestMapping("/push")
public class PushController {
    @Resource
    private ClientCache clientCache;

    @GetMapping("/user/{userId}")
    public String pushTuUser(@PathVariable("userId") String userId){
        HashMap<UUID, SocketIOClient> userClient = clientCache.getUserClient(userId);
        userClient.forEach((uuid, socketIOClient) -> {
            //Push message to client
            socketIOClient.sendEvent("chatevent",new MessageInfo("administrators","Messages sent to customer segments"));
        });
        return "success";
    }
}

7. Front page

The front-end page needs to introduce socket io. js package of js

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Demo Chat</title>
    <link href="css/bootstrap.css" rel="stylesheet">
    <style>
        body {
            padding:20px;
        }
        #console {
            height: 400px;
            overflow: auto;
        }
        .username-msg {color:orange;}
        .connect-msg {color:green;}
        .disconnect-msg {color:red;}
        .send-msg {color:#888}
    </style>
    <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>
        var userName = 'user' + Math.floor((Math.random()*1000)+1);
        
        //Create channel connection
        var socket =  io.connect('http://localhost:9092?userId=79');

        socket.on('connect', function() {
            output('<span class="connect-msg">Client has connected to the server!</span>');
        });
        
        socket.on('chatevent', function(message) {
            output('<span class="username-msg">' + message.userName + ':</span> ' + message.message);
        });

        socket.on('disconnect', function() {
            output('<span class="disconnect-msg">The client has disconnected!</span>');
        });

        //Closing a channel connection can be monitored by the @ OnDisconnect annotated method
        function sendDisconnect() {
            socket.disconnect();
        }

        function sendMessage() {
            var message = $('#msg').val();
            $('#msg').val('');
            var jsonObject = {
                userName: userName,
                message: message
            };
            //Method to send back-end @ OnEvent("chatevent") annotation
            socket.emit('chatevent', jsonObject);
        }

        function output(message) {
            var currentTime = "<span class='time'>" +  moment().format('HH:mm:ss.SSS') + "</span>";
            var element = $("<div>" + currentTime + " " + message + "</div>");
            $('#console').prepend(element);
        }

        $(document).keydown(function(e){
            if(e.keyCode == 13) {
                $('#send').click();
            }
        });
    </script>
</head>

<body>

<h1>Netty-socketio Demo Chat</h1>
<br/>
<div id="console" class="well">
</div>
<form class="well form-inline" onsubmit="return false;">
    <input id="msg" class="input-xlarge" type="text" placeholder="Type something..."/>
    <button type="button" onClick="sendMessage()" class="btn" id="send">Send</button>
    <button type="button" onClick="sendDisconnect()" class="btn">Disconnect</button>
</form>

</body>
</html>

Core code

Create a connection. The execution of this statement will call the method annotated by @ OnConnect in the corresponding class of SocketIO event on the server

//Create channel connection 
var socket =  io.connect('http://localhost:9092? userId=79');  // --@ Method of onconnect annotation

Listening code

//The @ OnConnect annotation method of event listening server-side listener
socket.on('connect', function() {
    output('<span class="connect-msg">Client has connected to the server!</span>');
});
//Method of @ OnDisconnect annotation of event listening server-side listener
socket.on('disconnect', function() {
    output('<span class="disconnect-msg">The client has disconnected!</span>');
});

//The user-defined page listening event listens to the information sent by the back-end. The corresponding code of the back-end page is socketoclient sendEvent("chatevent",message);
socket.on('chatevent', function(message) {
    output('<span class="username-msg">' + message.userName + ':</span> ' + message.message);
});

//Custom event format
socket.on("Custom event name",function(message){.....})

Function code

//Closing a channel connection can be monitored by the @ OnDisconnect annotated method
socket.disconnect();

//The method of sending @ OnEvent("chatevent") annotation to the server
socket.emit('chatevent', message);

Effect display

Enter integration link http://localhost:8080/ Enter the chat page

Background prompt:

Input http://localhost:8080/push/user/79 Send a message from the server first

Page information sending

Close link


Topics: socket Spring Boot server websocket