Websocket cluster solution and actual combat (with pictures and text source code)

Recently, I am working on a message push function in the project. For example, after a customer places an order, a system notification is sent to the corresponding customer. This kind of message push requires the use of full-duplex websocket push messages.

The so-called full duplex means that both the client and the server can send messages to each other. The reason why http, which is also full-duplex, is not used is because http can only be actively initiated by the client, and the service returns the message after receiving it. After the websocket connection is established, both the client and the server can actively send messages to each other.

Websocket sends and receives messages in stand-alone mode:
Insert image description here

After user A and user B establish a connection with the web server, user A sends a message to the server, and the server pushes it to user B. On a stand-alone system, all users establish connections with the same server, and all sessions are stored in the same in the server.

A single server cannot support tens of thousands of people connecting to the same server at the same time. Distribution or clustering is needed to load balance request connections to different services. The sender and receiver of the message are on the same server, which is similar to a single server and can successfully receive the message:

Insert image description here
However, load balancing uses a polling algorithm, which cannot guarantee that the message sender and receiver are on the same server. When the sender and receiver are not on the same server, the receiver cannot receive the message:

Insert image description here

Ideas for solving websocket cluster problems.
Every time the client and server establish a connection, a stateful session will be created, and the server will save the session to maintain the connection. The client can only connect to one server in the cluster server at a time, and will also transmit data to that server in the future.

To solve cluster problems, session sharing should be considered. After the client successfully connects to the server, other servers will also know that the client has successfully connected.

Solution 1: Session sharing (not feasible)
How does HTTP similar to websocket solve the cluster problem? One solution is to share the session. After the client logs in to the server, the session information is stored in the Redis database. When connecting to other servers, the session is obtained from Redis. In fact, the session information is stored in Redis to realize redis sharing.

The premise that the session can be shared is that it can be serialized, but the session of websocket cannot be serialized. The session of http records the requested data, and the session of websocket corresponds to the connection, which is connected to different servers. The session is also Different and cannot be serialized.

Option 2: ip hash (not feasible)
If http does not use session sharing, you can use the Nginx load balancing ip hash algorithm. The client requests the same server every time, and the client's session is saved on the server, and subsequent requests are Even if you request the server, you can get the session, so there is no distributed session problem.

Compared with HTTP, websocket can actively push messages to the client by the server. If the server that receives the message and the server that sends the message are not the same server, the server that sends the message cannot find the session corresponding to the message, that is, If the two sessions are not on the same server, messages cannot be pushed. As shown below:

Insert image description here

The way to solve the problem is to put all message senders and receivers under the same server. However, the message senders and receivers are uncertain, which is obviously impossible to achieve.

Option 3: Broadcast mode
requires the sender and receiver of the message to be on the same server before the message can be sent. Then you can change your thinking and notify all servers of the message in the form of message broadcast. You can use message middleware to publish and subscribe. Mode, the message escapes the restrictions of the server and is sent to the middleware and then to the subscribing server. Similar to broadcasting, as long as you subscribe to the message, you can receive the message notification: the publisher publishes the message to the message middleware, and the message
Insert image description here
middleware The email will then be sent to all subscribers:
Insert image description here

Implementation of broadcast mode To
build a stand-alone websocket,
refer to the previously written websocket stand-alone construction article. First, build a stand-alone websocket to push messages.

  1. Add dependencies
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. Create a bean instance of ServerEndpointExporter.
    The bean instance of ServerEndpointExporter automatically registers the websocket endpoint declared by the @ServerEndpoint annotation. This configuration is required for starting tomcat that comes with springboot. This configuration is not required for independent tomcat.
@Configuration
public class WebSocketConfig {
    
    
    //tomcat启动无需该配置
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    
    
        return new ServerEndpointExporter();
    }
}
  1. Create service endpoint ServerEndpoint and client
    service endpoint
@Component
@ServerEndpoint(value = "/message")
@Slf4j
public class WebSocket {
    
    

 private static Map<String, WebSocket> webSocketSet = new ConcurrentHashMap<>();

 private Session session;

 @OnOpen
 public void onOpen(Session session) throws SocketException {
    
    
  this.session = session;
  webSocketSet.put(this.session.getId(),this);

  log.info("【websocket】有新的连接,总数:{}",webSocketSet.size());
 }

 @OnClose
 public void onClose(){
    
    
  String id = this.session.getId();
  if (id != null){
    
    
   webSocketSet.remove(id);
   log.info("【websocket】连接断开:总数:{}",webSocketSet.size());
  }
 }

 @OnMessage
 public void onMessage(String message){
    
    
  if (!message.equals("ping")){
    
    
   log.info("【wesocket】收到客户端发送的消息,message={}",message);
   sendMessage(message);
  }
 }

 /**
  * 发送消息
  * @param message
  * @return
  */
 public void sendMessage(String message){
    
    
  for (WebSocket webSocket : webSocketSet.values()) {
    
    
   webSocket.session.getAsyncRemote().sendText(message);
  }
  log.info("【wesocket】发送消息,message={}", message);

 }

}

client endpoint

<div>
    <input type="text" name="message" id="message">
    <button id="sendBtn">发送</button>
</div>
<div style="width:100px;height: 500px;" id="content">
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script type="text/javascript">
    var ws = new WebSocket("ws://127.0.0.1:8080/message");
    ws.onopen = function(evt) {
    
    
        console.log("Connection open ...");
    };

    ws.onmessage = function(evt) {
    
    
        console.log( "Received Message: " + evt.data);
        var p = $("<p>"+evt.data+"</p>")
        $("#content").prepend(p);
        $("#message").val("");
    };

    ws.onclose = function(evt) {
    
    
        console.log("Connection closed.");
    };

    $("#sendBtn").click(function(){
    
    
        var aa = $("#message").val();
        ws.send(aa);
    })

</script>

OnOpen, onclose, and onmessage in the server and client are all in one-to-one correspondence.

After the service is started, the client ws.onopen calls the server's @OnOpen annotation method, stores the client's session information, and shakes hands to establish the connection.
The client calls ws.send to send a message, and the method corresponding to the @OnMessage annotation on the server receives the message.
The server calls session.getAsyncRemote().sendText to send the message, and the corresponding client ws.onmessage receives the message.
Add controller

@GetMapping({
    
    "","index.html"})
public ModelAndView index() {
    
    
 ModelAndView view = new ModelAndView("index");
 return view;
}

Effect display
Open two clients, one client sends a message, and the other client can also receive the message.

Insert image description here

Add RabbitMQ middleware.
The more commonly used RabbitMQ is used as the message middleware, and RabbitMQ supports the publish and subscribe mode:

Add message subscription
The switch uses a sector switch, and messages are distributed to each queue bound to the switch. Use the IP + port of the server as the unique identifier to name the queue, start a service, use the queue to bind the switch, and implement message subscription:

@Configuration
public class RabbitConfig {
    
    

    @Bean
    public FanoutExchange fanoutExchange() {
    
    
        return new FanoutExchange("PUBLISH_SUBSCRIBE_EXCHANGE");
    }

    @Bean
    public Queue psQueue() throws SocketException {
    
    
        // ip + 端口 为队列名 
        String ip = IpUtils.getServerIp() + "_" + IpUtils.getPort();
        return new Queue("ps_" + ip);
    }

    @Bean
    public Binding routingFirstBinding() throws SocketException {
    
    
        return BindingBuilder.bind(psQueue()).to(fanoutExchange());
    }
}

Modify the service endpoint ServerEndpoint
to add a message receiving method in WebSocket. @RabbitListener receives messages. The queue name is named with a constant. The dynamic queue name is #{name}, where name is the bean name of the Queue:

@RabbitListener(queues= "#{psQueue.name}")
public void pubsubQueueFirst(String message) {
    
    
  System.out.println(message);
  sendMessage(message);
}

Then call the sendMessage method to send it to the connected client.

Modify message sending.
In the onMessage method of the WebSocket class, change the message sending to RabbitMQ mode:

@OnMessage
public void onMessage(String message){
    
    
  if (!message.equals("ping")){
    
    
    log.info("【wesocket】收到客户端发送的消息,message={}",message);
    //sendMessage(message);
    if (rabbitTemplate == null) {
    
    
      rabbitTemplate = (RabbitTemplate) SpringContextUtil.getBean("rabbitTemplate");
    }
    rabbitTemplate.convertAndSend("PUBLISH_SUBSCRIBE_EXCHANGE", null, message);
  }
}

The message notification process is as follows:

Insert image description here
Start two instances to simulate the cluster environment.
Open the Edit Configurations of the idea:
Insert image description here
click COPY in the upper left corner, and then add the port server.port=8081:
Insert image description here
start two services, the ports are 8080 and 8081. After starting the service on port 8081, change the front-end connection port to 8081:

var ws = new WebSocket("ws://127.0.0.1:8081/message");

Show results

Insert image description here

Guess you like

Origin blog.csdn.net/u014374009/article/details/133156579