上篇博客讲了websocket的使用,只是适用于单台服务器情况下。
需要引入的依赖有
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的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;
}
}
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;
}
}
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 + '\'' +
'}';
}
}
登录有关的类就不做展示了
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();
}
}
这是从网上抄来的,没有用上
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;
}
}
}
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;
}
}
在一个服务器下部署的通信流程图
修改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)
发送消息给商户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文件
websocket8080
org.springframework.boot
spring-boot-maven-plugin
1.4.2.RELEASE
使用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来解决这个问题。
在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做反向代理。
登录消费者
登录商户1
登录商户2
发送消息
没有相应wesocket连接的服务器不会确认消息,将消费放回消息队列。有相应websocket连接的服务器就会消费消息。完成不同服务器之间的消息转发。