websocket结合消息队列完成多台服务器下的订单服务主动通知

上篇博客讲了websocket的使用,只是适用于单台服务器情况下。

编写springboot程序

需要引入的依赖有

1.spring-boot-starter-web

2.spring-boot-starter-thymeleaf

3.mysql-connector-java

4.druid

5.mybatis-spring-boot-starter

6.spring-boot-starter-websocket

7.fastjson

8.jackson-core

9.jackson-databind

目录结构

websocket结合消息队列完成多台服务器下的订单服务主动通知_第1张图片

一、页面

1.登录界面




    
    登录


    

2.商户界面




    
    商户页面





3.消费者页面




    
    消费者页面


    
    


二、实体类

1.接收消息对象MyMessage

用于接收前端传来的消息,主要目的是将websocket的session和商户id做绑定

import java.io.Serializable;

public class MyMessage implements Serializable {
    private String type;
    private Object data;

    public MyMessage() {
    }

    public MyMessage(String type, Object data) {
        this.type = type;
        this.data = data;
    }

    public String getType() {
        return type;
    }

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

    @Override
    public String toString() {
        return "MyMessage{" +
                "type='" + type + '\'' +
                ", data=" + data +
                '}';
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

2.发送消息对象MySendMessage

import java.io.Serializable;

public class MySendMessage implements Serializable {
    private Integer targetId;
    private String data;

    public MySendMessage() {
    }

    public MySendMessage(Integer targetId, String data) {
        this.targetId = targetId;
        this.data = data;
    }

    @Override
    public String toString() {
        return "MySendMessage{" +
                "targetId=" + targetId +
                ", data=" + data +
                '}';
    }

    public Integer getTargetId() {
        return targetId;
    }

    public void setTargetId(Integer targetId) {
        this.targetId = targetId;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

3.用户对象User

public class User {
    private Integer id;
    private String username;
    private String password;

    public User() {
    }

    public User(Integer id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

登录有关的类就不做展示了

三、websocket配置类

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

@Configuration
public class WebSocketConfiguration {
    @Bean
    public ServerEndpointExporter getServerEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

四、websocket消息解析类

这是从网上抄来的,没有用上

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import xyz.syyrjx.websocketmq.entity.MyMessage;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

/*
 * Text里的ResponseMessage是我自己写的一个消息类
 * 如果你写了一个名叫Student的类,需要通过sendObject()方法发送,那么这里就是Text
 */
public class ServerEncoder implements Encoder.Text {

    @Override
    public void destroy() {
        // TODO Auto-generated method stub
        // 这里不重要
    }

    @Override
    public void init(EndpointConfig arg0) {
        // TODO Auto-generated method stub
        // 这里也不重要

    }

    /*
     *  encode()方法里的参数和Text里的T一致,如果你是Student,这里就是encode(Student student)
     */
    @Override
    public String encode(MyMessage responseMessage) throws EncodeException {
        try {
            /*
             * 这里是重点,只需要返回Object序列化后的json字符串就行
             * 你也可以使用gosn,fastJson来序列化。
             */
            JsonMapper jsonMapper = new JsonMapper();
            return jsonMapper.writeValueAsString(responseMessage);

        } catch ( JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

五、WebSocket服务类

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.springframework.stereotype.Component;
import xyz.syyrjx.websocketmq.entity.MyMessage;
import xyz.syyrjx.websocketmq.entity.MySendMessage;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint(value = "/websocket",encoders = {ServerEncoder.class})
public class WebSocket {

    private static ConcurrentHashMap sessionMap = new ConcurrentHashMap<>();


    /**
     * 绑定接收消息事件
     * @param msg 接收到的消息
     * @param session webscoekt的session
     */
    @OnMessage
    public void getMessage(String msg,Session session){
        //解析接收到的消息
        MyMessage message = discodingMessage(msg);
        String messageType = message.getType();
        Object messageData = message.getData();
        //查看消息的类型
        switch (messageType){
            //如果是open就加入sessionMap
            case "open":
                sessionMap.put((Integer) messageData,session);
                System.out.println(messageData + "=" + session);
                System.out.println("有新的连接" + session + "进入,当前连接数" + sessionMap.size());
                break;

        }
    }


    /**
     * 发送信息方法
     * @param message 发送信息对象
     */
    public void sendMessage(MySendMessage message){
        Integer targetId = message.getTargetId();
        String data = message.getData();
        if (targetId != null){
            System.out.println("发送给商户" + targetId);
            try {
                System.out.println(sessionMap.get(targetId));
                sessionMap.get(targetId).getBasicRemote().sendText(data);
            } catch (IOException e) {
                System.err.println(e.getClass() + ":" + e.getMessage());
            }

        }else {
            System.out.println("广播发送给所有商户");
            Collection sessions = sessionMap.values();
            for (Session session : sessions){
                try {
                    session.getBasicRemote().sendText(data);
                } catch (IOException e) {
                    System.err.println(e.getClass() + ":" + e.getMessage());
                }

            }
        }
    }

    /**
     * 解析信息信息为信息对象
     * @param message 信息字符串
     * @return 返回信息对象,解析失败返回null
     */
    private MyMessage discodingMessage(String message){
        JsonMapper jsonMapper = new JsonMapper();
        MyMessage res = null;
        try {
            res = jsonMapper.readValue(message, MyMessage.class);
        } catch (JsonProcessingException e) {
            return null;
        }
        return res;
    }
}

六、订单控制器

接收到创建订单请求并发送消息给商户

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.syyrjx.websocketmq.entity.MySendMessage;
import xyz.syyrjx.websocketmq.util.WebSocket;

import javax.annotation.Resource;

@RestController
public class OrderController {
    @Resource
    WebSocket webSocket;

    @RequestMapping("/create")
    public Object create(Integer targetId){
        webSocket.sendMessage(new MySendMessage(targetId,"收到一个新的订单"));
        return null;
    }
}

在一个服务器下部署

在一个服务器下部署的通信流程图

websocket结合消息队列完成多台服务器下的订单服务主动通知_第2张图片

修改application.properties

server.port=80
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456

mybatis.mapper-locations=classpath:/dao/*.xml

直接在idea中启动。

websocket结合消息队列完成多台服务器下的订单服务主动通知_第3张图片

 修改C:\Windows\System32\drivers\etc路径下的HOST文件,添加一条

 这个域名会在前端创建websocket时用到。访问这个页面时也可以使用这个域名。

访问这个域名,登录消费者和商户

websocket结合消息队列完成多台服务器下的订单服务主动通知_第4张图片

websocket结合消息队列完成多台服务器下的订单服务主动通知_第5张图片 控制台打印两个连接对象

消费者发送创建订单消息给商户2(第一个登录的商户在数据库中id为2) 

websocket结合消息队列完成多台服务器下的订单服务主动通知_第6张图片

 websocket结合消息队列完成多台服务器下的订单服务主动通知_第7张图片

 websocket结合消息队列完成多台服务器下的订单服务主动通知_第8张图片

websocket结合消息队列完成多台服务器下的订单服务主动通知_第9张图片只有商户2接到消息 。

发送消息给商户3。

websocket结合消息队列完成多台服务器下的订单服务主动通知_第10张图片

websocket结合消息队列完成多台服务器下的订单服务主动通知_第11张图片

websocket结合消息队列完成多台服务器下的订单服务主动通知_第12张图片 websocket结合消息队列完成多台服务器下的订单服务主动通知_第13张图片

部署多台服务器

当部署多台服务器,通过nginx网关转发时,就会出现与消费者不在同一台服务器上的商户无法接收到请求。

websocket结合消息队列完成多台服务器下的订单服务主动通知_第14张图片

 如上图:商户2无法接收到消费者发起的订单消息。

修改程序配置

修改端口号为8080

server.port=8080
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

spring.datasource.url=jdbc:mysql://192.168.188.130:3306/test
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456

mybatis.mapper-locations=classpath:/dao/*.xml

修改pom文件


		websocket8080

		
			
				org.springframework.boot
				spring-boot-maven-plugin
				1.4.2.RELEASE
			
		
	

使用maven打包插件打包。

再打包一份8081端口的。

将两个打好的jar包丢到虚拟机的/opt目录下

websocket结合消息队列完成多台服务器下的订单服务主动通知_第15张图片

 修改nginx的配置文件nginx.conf

upstream www.syyrjx.syyrjx{
		server 192.168.188.130:8080;
		server 192.168.188.130:8081;
	}
	server{
		listen 80;
		location / {
			proxy_pass http://www.syyrjx.syyrjx;
		}
		#这个是websocket的ws协议转发配置
		location /websocket {
            proxy_pass http://www.syyrjx.syyrjx;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
        }
	}

记得映射www.syyrjx.syyrjx这个域名到虚拟机ip

启动8080和8081

websocket结合消息队列完成多台服务器下的订单服务主动通知_第16张图片 websocket结合消息队列完成多台服务器下的订单服务主动通知_第17张图片

 登录商户2,连接到服务8080

websocket结合消息队列完成多台服务器下的订单服务主动通知_第18张图片

 

登录商户3连接到8081 

websocket结合消息队列完成多台服务器下的订单服务主动通知_第19张图片

 登录消费者

websocket结合消息队列完成多台服务器下的订单服务主动通知_第20张图片

 发送一个请求给商户2,第一次被转发给了8081,消息转发失败

websocket结合消息队列完成多台服务器下的订单服务主动通知_第21张图片

  发送一个请求给商户2,第二次被转发给了8080,消息转发才能成功(后面的报错好像是websocket连接断开了,不影响)

websocket结合消息队列完成多台服务器下的订单服务主动通知_第22张图片

 在测试中两个订单消息发给了商户2却只被收到了一个,这显然是不行的。我们可以通过结合消息队列mq来解决这个问题。

多台服务器通过nginx转发,加入rabbitmq

websocket结合消息队列完成多台服务器下的订单服务主动通知_第23张图片

 在pom文件中引入rabbitmq的依赖


	org.springframework.boot
	spring-boot-starter-amqp


	org.springframework.amqp
	spring-rabbit-test
	test

 在application.propreties中添加rabbitmq的配置

spring.rabbitmq.host=192.168.188.130
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
#启用手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

 声明交换机绑定队列

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("directExchange");
    }

    @Bean
    public Queue directQueue(){
        return new Queue("directQueue");
    }

    @Bean
    public Binding directBinding(Queue directQueue,DirectExchange directExchange){
        return BindingBuilder.bind(directQueue).to(directExchange).with("key");
    }
}

 添加一个MQ发送消息服务类

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service("MQSendService")
public class SendMessageToMQ {
    @Resource
    private AmqpTemplate template;

    public void sendMessage(String message){
        template.convertAndSend("directExchange","key",message);
    }
}

添加一个MQ接收消息服务类(使用监听器方式)

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import xyz.syyrjx.websocketmq.entity.MySendMessage;

import javax.annotation.Resource;
import java.io.IOException;

@Service("MQReceiveService")
public class ReceiveMessageFromMQ {
    @Resource
    WebSocket webSocket;

    @RabbitListener(queues = {"directQueue"})
    private void directListener(Message msg,MySendMessage message , Channel channel) throws IOException {
        Integer id = message.getTargetId();
        //System.out.println("监听到信息");
        long deliveryTag = msg.getMessageProperties().getDeliveryTag();
        if (!WebSocket.mapContainsKey(id)){
            //消息id,是否批量,是否回队列
            channel.basicNack(deliveryTag,false,true);
            //System.out.println("不确认");
        }else{
            webSocket.sendMessage(message);
            //消息id,是否批量
            channel.basicAck(deliveryTag,true);
            //System.out.println("确认");
        }
    }
}

修改OrderController为接收到订单创建请求就将消息发送到rabbitmq

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.syyrjx.websocketmq.entity.MySendMessage;
import xyz.syyrjx.websocketmq.util.SendMessageToMQ;


import javax.annotation.Resource;

@RestController
public class OrderController {
    @Resource
    SendMessageToMQ MQSendService;

    @RequestMapping("/create")
    public Object create(Integer targetId){
        MQSendService.sendMessage(new MySendMessage(targetId,"收到一个新的订单"));
        return null;
    }
}

打包8080和8081两个jar包,在centos中启动,由nginx做反向代理。

登录消费者

websocket结合消息队列完成多台服务器下的订单服务主动通知_第24张图片

登录商户1

websocket结合消息队列完成多台服务器下的订单服务主动通知_第25张图片 

登录商户2

websocket结合消息队列完成多台服务器下的订单服务主动通知_第26张图片

 发送消息

websocket结合消息队列完成多台服务器下的订单服务主动通知_第27张图片

 websocket结合消息队列完成多台服务器下的订单服务主动通知_第28张图片

 没有相应wesocket连接的服务器不会确认消息,将消费放回消息队列。有相应websocket连接的服务器就会消费消息。完成不同服务器之间的消息转发。

你可能感兴趣的:(java,开发语言,websocket,rabbitmq)