最近写了一个小demo,用到了消息推送,想了想还是记录一下。这篇文章其实和第2篇文章 还是差不多的、
本系列文章:
1、springboot+websocket构建在线聊天室(群聊+单聊)
2、SpringBoot+STOMP 实现聊天室(单聊+多聊)及群发消息详解
3、websocket stomp+rabbitmq实现消息推送
目录
1、技术栈
2、依赖
3、修改配置文件
4、RabbitConfig
5、消息包装类
6、利用STOMP实现前后端长连接
7、编写前端页面:
8、编写消息生产者和消费者
9、测试
后端:springboot2.0.6
前端:html js
org.springframework.boot
spring-boot-starter-parent
2.0.6.RELEASE
com.zj
online-test
0.0.1-SNAPSHOT
online-test
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-data-jpa
commons-lang
commons-lang
2.6
org.springframework.boot
spring-boot-starter-websocket
org.webjars
webjars-locator-core
org.webjars
sockjs-client
1.0.2
org.webjars
stomp-websocket
2.3.3
org.springframework.boot
spring-boot-starter-amqp
#rabbitmq
spring.rabbitmq.host=192.168.XXX
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
本文省略rabbitmq的安装,这边将你安装的rabbitmq的相关信息填入即可
先创一个rabbitmq 的配置类,由于我们这边业务逻辑比较简单,就简单使用rabbitmq一下
这边创一个hello的消息队列。
@Configuration
public class RabbitConfig {
@Bean
public Queue helloQueue() {
return new Queue("hello");
}
}
先讲一下消息包装类,前后端的消息都以这种格式来传递,大家可以根据自己的需求自定义。
public class RequestMessage {
private String room;//频道号
private String type;//消息类型('1':客户端到服务端 '2':客户端到服务端)
private String content;//消息内容(即答案)
private String userId;//用户id
private String questionId;//题目id
private String createTime;//时间
public RequestMessage() {
}
public RequestMessage(String room, String type, String content, String userId, String questionId, String createTime) {
this.room = room;
this.type = type;
this.content = content;
this.userId = userId;
this.questionId = questionId;
this.createTime = createTime;
}
public String getRoom() {
return room;
}
public String getType() {
return type;
}
public String getContent() {
return content;
}
public void setRoom(String room) {
this.room = room;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getQuestionId() {
return questionId;
}
public void setQuestionId(String questionId) {
this.questionId = questionId;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
public void setType(String type) {
this.type = type;
}
public void setContent(String content) {
this.content = content;
}
}
这部分与我之前的文章类似:https://blog.csdn.net/qq_41603102/article/details/88351729
本文这边 省略了消息群发
首先编写websocket配置类:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/**
* 订阅来自"/topic"和"/user"的消息,
* /topic 单聊
* /all 群聊
*/
config.enableSimpleBroker("/topic","/all");
/**
* 客户端发送过来的消息,需要以"/app"为前缀,再经过Broker转发给响应的Controller
*/
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* 路径"/websocket"被注册为STOMP端点,对外暴露,客户端通过该路径接入WebSocket服务
*/
registry.addEndpoint("/websocket").setAllowedOrigins("*").withSockJS();
}
}
然后编写消息转发器(前端的消息通过这边,进行转发)
@RestController
public class WebSocketTestController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
Sender senderMQ;
/**聊天室(单聊+多聊)&&消息转发
* @param requestMessage
* @throws Exception
*/
@CrossOrigin
@MessageMapping("/chat")
public void messageHandling(RequestMessage requestMessage) throws Exception {
String destination = "/topic/" + HtmlUtils.htmlEscape(requestMessage.getRoom());
String room = HtmlUtils.htmlEscape(requestMessage.getRoom());//htmlEscape 转换为HTML转义字符表示
String type = HtmlUtils.htmlEscape(requestMessage.getType());
String content = HtmlUtils.htmlEscape(requestMessage.getContent());
String userId = HtmlUtils.htmlEscape(requestMessage.getUserId());
String questionId = HtmlUtils.htmlEscape(requestMessage.getQuestionId());
String createTime = HtmlUtils.htmlEscape(requestMessage.getCreateTime());
System.out.println( requestMessage.getRoom() );
System.out.println( content );
messagingTemplate.convertAndSend(destination, requestMessage);
}
}
注意:
1、使用@MessageMapping注解来标识所有发送到“/chat”这个destination的消息,都会被路由到这个方法进行处理
2、使用@SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic”
3、传入的参数RequestMessage requestMessage为客户端发送过来的消息,是自动绑定的。
将前端传过来的消息解析,然后再通过messagingTemplate.convertAndSend(“目的地”,“消息内容”)转发出去。
这边的前端页面仅仅用了html+js
My WebSocket
频道号:
做题区:
页面都很简单,就不细讲了,但要引入sockjs.min.js jquery.min.js stomp.min.js这3个js
大家可以从我github上拉取:https://github.com/zj827622690/online-test/tree/master/src/main/resources/static/js
这样就前后端长连接就完成了,赶紧来试试~
可以看出长连接成功
下面我们要来讲讲 消息推送如何实现,当我们前端发一个http请求,在这个请求结束前,后端会新开一个线程。当原本的请求结束时,后端新开的那个线程还在运行,当它结束时,后端应该要返回消息给前端,但之前的请求已经结束,http握手已经早结束了,消息怎么传递。所以我们需要用到rabbitmq(用来消息的解耦) websocket(用来保证前后端通信通道不关闭),来让后端主动推送的消息,能到前端页面显示。 可以这么笼统地理解:}。
现在我们讲讲 rabbitmq 消息2个重要的组成部分:消息生产者和消息消费者
sender:
@Component
public class Sender {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send(String context) { //注意因为是AmqpTemplate,所有这里只接受String,byte[],Seriz..
System.out.println("Sender : " + context);
this.rabbitTemplate.convertAndSend("hello", context);
}
}
这里注意一下,发送消息采用了AmqpTemplate 模板 。AmqpTemplate接口已经定义了发送和接收消息的基本操作。我们直接使用即可,但要注意的是 必须符合它的类型,这里只支持 String,byte[],Seriz..类型的消息
convertAndSend("hello", context); hello就是我们前面创的消息队列,context是消息的内容
但我们有时候消息特别复杂,一般用对象来储存消息。这个怎么办呢?,这边我们先不讲,到我们下面再讲。我们继续来讲消息消费者
receiver:
@Component
@RabbitListener(queues = "hello")
public class Receiver {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@RabbitHandler
public void process(String context) throws IOException {
System.out.println("Receiver : " + context);
RequestMessage mqTask = new RequestMessage( );
BeanUtils.copyProperties( JsonUtils.jsonToObject( context,RequestMessage.class ),mqTask );
if (Objects.equals( mqTask.getType(), "2" )) {
String destination = "/topic/" +mqTask.getRoom();
messagingTemplate.convertAndSend( destination, mqTask);
}
}
}
这里注意:我们前面说过,由于消息一般比较复杂,所以发送过来的消息一般是对象类型,但AmqpTemplate不支持,所以我们需要在消息生产者这边把它先转化成String型,传到消息消费者这边,再转化成对象。然后对消息进行处理。
最后通过 messagingTemplate.convertAndSend( destination, mqTask); 将消息发送到前端页面上。
我们这边用到的工具类:
public class JsonUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 对象-->Json字符串
* @version 创建时间:2018年4月17日 下午3:39:35
*/
public static String objectToJson(Object data) {
try {
return MAPPER.writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* Json字符串-->对象
* @version 创建时间:2018年4月17日 下午3:39:45
*/
public static T jsonToObject(String jsonData, Class beanType) {
try {
return MAPPER.readValue(jsonData, beanType);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Json字符串--> List<对象>
* @version 创建时间:2018年4月17日 下午3:40:09
*/
public static List jsonToList(String jsonData, Class beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
return MAPPER.readValue(jsonData, javaType);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static Map jsonToMap(String jsonData) {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readValue(jsonData, Map.class);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static String mapToJson(Map map) {
try {
return MAPPER.writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
public static Set jsonToSet(String jsonData) {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readValue(jsonData, Set.class);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static String setToJson(Set set) {
try {
return MAPPER.writeValueAsString(set);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
我们先定义一个sevice方法,来实现异步。
public interface CommonService {
void testAsync();
}
@Service
public class CommonServiceImpl implements CommonService {
@Autowired
Sender sender;
@Async
@Override
public void testAsync() {
RequestMessage mqTask = new RequestMessage( );
for(int i=0;i<6;i++) {
mqTask.setRoom( "123");
mqTask.setUserId("000");
mqTask.setType( "2" );
mqTask.setQuestionId( "0000");
mqTask.setCreateTime( "0000");
mqTask.setContent("this:"+i);
sender.send( JsonUtils.objectToJson( mqTask ) );
try {
Thread.sleep( 1000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里实现异步,为了简单就只用了@Async,关于线程池有兴趣可以看看我之前的一篇文章:P
https://blog.csdn.net/qq_41603102/article/details/89486109
再写一个测试controller
@RestController
public class TestController {
@Autowired
CommonService commonService;
@GetMapping("/test/testAsync")
public String testAsync() {
commonService.testAsync();
return "http请求已结束";
}
}
打开浏览器,先连接websocket,然后再开一个窗口,输入请求localhost:8080/test/testAsync
发现如下:
表明成功,本文暂且就到这了 = - =
:P 五一 在图书馆写论文+博客 :}