SpringBoot2+WebSocket chat application practice

Posted by ikelove on Fri, 03 Dec 2021 16:38:30 +0100

What is WebSocket?

 

 

WebSocket protocol is a new network protocol based on TCP. It implements full duplex communication between the browser and the server -- allowing the server to actively send information to the client

Why do I need WebSocket?

People who come into contact with WebSocket for the first time will ask the same question: we already have the HTTP protocol. Why do we need another protocol? What benefits can it bring?

The answer is very simple, because the HTTP protocol has a defect: communication can only be initiated by the client, and the HTTP protocol cannot push information to the client actively.

Don't say much. Enter the dry goods moment immediately.

maven dependency

Springboot 2.0's support for WebSocket is great. There are packages that can be introduced directly

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

WebSocketConfig

Enabling WebSocket support is also very simple, with a few words of code

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
* Enable WebSocket support
*/
@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocketServer

That's the point. The core is here.

1. Because WebSocket is similar to client and server (ws protocol is adopted), the WebSocket server here is actually equivalent to a Controller of ws protocol

    2. Directly enable @ ServerEndpoint("/imserver/{userId}") and @ Component, and then implement @ OnOpen to open the connection, @ onClose to close the connection, and @ onMessage to receive messages.

    3. Create a new ConcurrentHashMap webSocketMap to receive the WebSocket of the current userId, so as to facilitate the push message of userId between IM S. The stand-alone version can be implemented here.

    4. The cluster Version (multiple ws nodes) also needs to be processed with the help of mysql or redis, and the corresponding sendMessage method can be modified.

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;

@ServerEndpoint("/imserver/{userId}")
@Component
public class WebSocketServer {

    static Log log=LogFactory.get(WebSocketServer.class);
    /**Static variable, used to record the current number of online connections. It should be designed to be thread safe.*/
    private static int onlineCount = 0;
    /**concurrent The thread safe Set of the package is used to store the MyWebSocket object corresponding to each client.*/
    private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    /**The connection session with a client needs to send data to the client through it*/
    private Session session;
    /**Receive userId*/
    private String userId="";

    /**
     * Method successfully called for connection establishment*/
    @OnOpen
    public void onOpen(Session session,@PathParam("userId") String userId) {
        this.session = session;
        this.userId=userId;
        if(webSocketMap.containsKey(userId)){
            webSocketMap.remove(userId);
            webSocketMap.put(userId,this);
            //Add to set
        }else{
            webSocketMap.put(userId,this);
            //Add to set
            addOnlineCount();
            //Online number plus 1
        }

        log.info("User connection:"+userId+",The number of people currently online is:" + getOnlineCount());

        try {
            sendMessage("Connection succeeded");
        } catch (IOException e) {
            log.error("user:"+userId+",Network exception!!!!!!");
        }
    }

    /**
     * Method called for connection closure
     */
    @OnClose
    public void onClose() {
        if(webSocketMap.containsKey(userId)){
            webSocketMap.remove(userId);
            //Delete from set
            subOnlineCount();
        }
        log.info("User exit:"+userId+",The number of people currently online is:" + getOnlineCount());
    }

    /**
     * Method of calling after receiving client message
     *
     * @param message Messages sent by the client*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("User message:"+userId+",message:"+message);
        //You can send messages in groups
        //Save messages to database and redis
        if(StringUtils.isNotBlank(message)){
            try {
                //Analyze the sent message
                JSONObject jsonObject = JSON.parseObject(message);
                //Add sender (prevent serial modification)
                jsonObject.put("fromUserId",this.userId);
                String toUserId=jsonObject.getString("toUserId");
                //websocket transmitted to the corresponding toUserId user
                if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){
                    webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());
                }else{
                    log.error("Requested userId:"+toUserId+"Not on this server");
                    //Otherwise, it will not be sent to mysql or redis on this server
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("user error:"+this.userId+",reason:"+error.getMessage());
        error.printStackTrace();
    }
    /**
     * Realize server active push
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


    /**
     * Send custom message
     * */
    public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {
        log.info("Send message to:"+userId+",message:"+message);
        if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){
            webSocketMap.get(userId).sendMessage(message);
        }else{
            log.error("user"+userId+",Not online!");
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

Message push

As for pushing new information, you can write a method in your Controller to call WebSocketServer.sendInfo(); that will do

import com.softdev.system.demo.config.WebSocketServer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;

/**
 * WebSocketController
 * @author zhengkai.blog.csdn.net
 */
@RestController
public class DemoController {

    @GetMapping("index")
    public ResponseEntity<String> index(){
        return ResponseEntity.ok("Request succeeded");
    }

    @GetMapping("page")
    public ModelAndView page(){
        return new ModelAndView("websocket");
    }

    @RequestMapping("/push/{toUserId}")
    public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws IOException {
        WebSocketServer.sendInfo(message,toUserId);
        return ResponseEntity.ok("MSG SEND SUCCESS");
    }
}

Page initiation

The page calls websocket with js code. Of course, too old browsers can't do it. Generally, there is no problem with new browsers or Google browsers. Also, remember that the protocol is ws. If some path classes are used, replace("http", "ws") can be used to replace the protocol.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket communication</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("Your browser does not support WebSocket");
        }else{
            console.log("Your browser supports WebSocket");
            //Implement the WebSocket object, specify the server address to be connected and establish a connection with the port
            //Equivalent to socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
            //var socketUrl="${request.contextPath}/im/"+$("#userId").val();
            var socketUrl="http://localhost:9999/demo/imserver/"+$("#userId").val();
            socketUrl=socketUrl.replace("https","ws").replace("http","ws");
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //Open event
            socket.onopen = function() {
                console.log("websocket Opened");
                //socket.send("this is a message from the client" + location.href + new Date());
            };
            //Get message event
            socket.onmessage = function(msg) {
                console.log(msg.data);
                //The discovery message enters the start processing front-end trigger logic
            };
            //Close event
            socket.onclose = function() {
                console.log("websocket Closed");
            };
            //An error event occurred
            socket.onerror = function() {
                console.log("websocket An error has occurred");
            }
        }
    }
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("Your browser does not support WebSocket");
        }else {
            console.log("Your browser supports WebSocket");
            console.log('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
            socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
        }
    }
</script>
<body>
<p>[userId]: <div><input id="userId" name="userId" type="text" value="10"></div>
<p>[toUserId]: <div><input id="toUserId" name="toUserId" type="text" value="20"></div>
<p>[toUserId]: <div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>[[operation]:<div><a onclick="openSocket()">open socket</a></div>
<p>[[operation]:<div><a onclick="sendMessage()">send message</a></div>
</body>

</html>

Operation effect

First open two pages and press F12 to call up the control console to view the test effect:

Then open the socket respectively and send the message

 

 

  Figure 2:

2. Push data to the front end:

http://localhost:9999/demo/push/10?message=123123

 

 

  By calling the push api, you can push information to the specified userId. Of course, the message is scribbled here. It is recommended to specify the format.

ServerEndpointExporter error

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverEndpointExporter' defined in class path resource [com/xxx/WebSocketConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available

If the tomcat deployment keeps reporting this error, please remove the injection of @ Bean ServerEndpointExporter in WebSocketConfig.

ServerEndpointExporter is a standard implementation officially provided by Spring. It is used to scan ServerEndpointConfig configuration classes and @ ServerEndpoint annotation instances. The rules are also simple:

If you use a default embedded container such as Tomcat, you must manually provide the server endpoint exporter in the context.

If you use an external container to deploy the war package, you do not need to provide the ServerEndpointExporter, because at this time, SpringBoot defaults to the external container to handle the scanning behavior of the server, so you should note out the code injected into the bean in WebSocketConfig during online deployment.

websocket connection for Vue version

<script>
export default {
    data() {
        return {
            socket:null,
            userId:localStorage.getItem("ms_uuid"),
            toUserId:'2',
            content:'3'
        }
    },
  methods: {
    openSocket() {
      if (typeof WebSocket == "undefined") {
        console.log("Your browser does not support WebSocket");
      } else {
        console.log("Your browser supports WebSocket");
        //Implement the WebSocket object, specify the server address to be connected and establish a connection with the port
        //Equivalent to socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
        //var socketUrl="${request.contextPath}/im/"+$("#userId").val();
        var socketUrl =
          "http://localhost:8081/imserver/" + this.userId;
        socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
        console.log(socketUrl);
        if (this.socket != null) {
          this.socket.close();
          this.socket = null;
        }
        this.socket = new WebSocket(socketUrl);
        //Open event
        this.socket = new WebSocket(socketUrl);
        //Open event
        this.socket.onopen = function() {
          console.log("websocket Opened");
          //socket.send("this is a message from the client" + location.href + new Date());
        };
        //Get message event
        this.socket.onmessage = function(msg) {
          console.log(msg.data);
          //The discovery message enters the start processing front-end trigger logic
        };
        //Close event
        this.socket.onclose = function() {
          console.log("websocket Closed");
        };
        //An error event occurred
        this.socket.onerror = function() {
          console.log("websocket An error has occurred");
        };
      }
    },
    sendMessage() {
      if (typeof WebSocket == "undefined") {
        console.log("Your browser does not support WebSocket");
      } else {
        console.log("Your browser supports WebSocket");
        console.log(
          '{"toUserId":"' +
             this.toUserId +
            '","contentText":"' +
             this.content +
            '"}'
        );
        this.socket.send(
          '{"toUserId":"' +
             this.toUserId +
            '","contentText":"' +
             this.content +
            '"}'
         );
    
    }
}

 

Topics: websocket