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

目录结构

一、页面

1.登录界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <form action="/login">
        <input type="text" name="username" placeholder="用户名"/>
        <input type="text" name="password" placeholder="密码"/>
        <input type="submit" />
    </form>
</body>
</html>

2.商户界面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商户页面</title>
</head>
<body>
<input type="hidden" th:value="${user.id}" id="userId">
</body>
<script>
    var websocket = null;
    var userId = document.getElementById('userId').value;
    if('websocket' in window){
        websocket = new WebSocket('ws://www.syyrjx.syyrjx/websocket');
    }else{
        alert('当前浏览器不支持websocket!');
    }

    websocket.onopen = function () {
        //连接建立时通过websocket发送商户id给服务器
        websocket.send("{\"type\":\"open\",\"data\":" + userId + "}");
    }

    //接收到信息打印在控制台
    websocket.onmessage = function (event) {
        console.log(event.data);
    }

    //关闭窗口时关闭websocket连接
    window.onbeforeunload = function (){
        websocket.close();
    }



</script>
</html>

3.消费者页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>消费者页面</title>
</head>
<body>
    <input id="targetId" type="text" placeholder="输入目标id"/>
    <input id="sendBtn" type="button" value="发送订单"/>
</body>
<script>
    //通过ajax方式发送创建订单请求,指定目标商户id
    var sendBtn = document.getElementById('sendBtn');
    var targetId = document.getElementById('targetId');
    sendBtn.onclick = function () {
        var xmlHttp = new XMLHttpRequest();
        xmlHttp.open("get","create?targetId=" + targetId.value,true);
        xmlHttp.send();
    }
</script>
</html>

二、实体类

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

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

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

    }

    /*
     *  encode()方法里的参数和Text<T>里的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<Integer,Session> 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<Session> 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;
    }
}

在一个服务器下部署

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

修改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中启动。

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

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

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

 控制台打印两个连接对象

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

 

 

只有商户2接到消息 。

发送消息给商户3。

 

部署多台服务器

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

 如上图:商户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文件

<build>
		<finalName>websocket8080</finalName>

		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<version>1.4.2.RELEASE</version>
			</plugin>
		</plugins>
	</build>

使用maven打包插件打包。

再打包一份8081端口的。

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

 修改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

 

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

 

登录商户3连接到8081 

 登录消费者

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

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

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

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

 在pom文件中引入rabbitmq的依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.amqp</groupId>
	<artifactId>spring-rabbit-test</artifactId>
	<scope>test</scope>
</dependency>

 在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做反向代理。

登录消费者

登录商户1

 

登录商户2

 发送消息

 

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

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值