利用Spring_Boot WebSocKet實現(xiàn)一個推送的小Demo
簡介:
文章共分為兩個模塊旦部,包括全局推送功能的實現(xiàn),和點對點的推送功能實現(xiàn)忌堂,源代碼在文末給出連接
模塊一:全局推送
1. 新建項目喘漏,利用引導創(chuàng)建
新建時采用引導的方式,下面給出的是幾張重點截圖白嘁,其他的過程截圖省略,
以上是根據(jù)引導來創(chuàng)建Spring_Boot WebSocket項目過程中的幾個主要界面,其余界面較為普遍膘流,不加以介紹絮缅,詳細的pom.xml文件內(nèi)容此處不提供鲁沥,在點對點模塊有整體的pom.xml文件。
2. 配置WebSocketConfig
具體內(nèi)容看代碼注釋盟蚣,
注:測試全局推送的時候可將代碼中的“/socket”節(jié)點相關(guān)信息注釋掉黍析,也可去一對一模塊尋找對應(yīng)的攔截器代碼
//WebSocketConfig配置文件
@Configuration
@EnableWebSocketMessageBroker //開啟使用STOMP協(xié)議來傳輸基于代理的消息
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
//注冊STOMP協(xié)議的節(jié)點,并指定映射的URL
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
/**
* 注冊STOMP協(xié)議節(jié)點屎开,同時指定使用SockJS協(xié)議阐枣,在前端界面中會使用該處的節(jié)點進行SocKet連接
* 根據(jù)需要注冊即可,這里因為只是測試奄抽,注冊了3個節(jié)點
* 另外蔼两,在第三個節(jié)點中,可以看到比前兩個節(jié)點多了一些內(nèi)容逞度,制定了一個攔截器及其他信息
* 這個根據(jù)需要來擴展额划,攔截器是定義的一個class文件,該攔截器在demo中主要用在點對點模塊档泽,因本人也是初次接觸這部分內(nèi)容俊戳,不太了解。
*/
stompEndpointRegistry.addEndpoint("/endpointSang").withSockJS();
stompEndpointRegistry.addEndpoint("/endpointChat").withSockJS();
stompEndpointRegistry.addEndpoint("/socket").withSockJS()
.setInterceptors( new ChatHandlerShareInterceptor())
.setStreamBytesLimit(512 * 1024)
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000);
}
//配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/**
* 配置消息代理馆匿,前端通過這個代理來進行消息訂閱,
* 消息代理可以有一個抑胎,也可以有多個,用 “渐北,” 號分隔
* 這里配置了兩個阿逃,"/topic"用作全局推送,"/queue"用做點對點使用
*/
registry.enableSimpleBroker("/queue","/topic");
/**
* 配置接收前端信息的消息代理赃蛛,前端通過這個代理來向后臺傳遞消息
* 簡單來說恃锉,前端通過這個消息代理訪問對應(yīng)controller中的MessageMapping(value)
*/
registry.setApplicationDestinationPrefixes("/app");
//registry.setPathMatcher(new AntPathMatcher("."));
}
@Override
public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
@Override
public WebSocketHandler decorate(final WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
// 客戶端與服務(wù)器端建立連接后,此處記錄誰上線了
String username = session.getPrincipal().getName();
//log.info("online: " + username);
System.out.println("online: " + username);
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
// 客戶端與服務(wù)器端斷開連接后呕臂,此處記錄誰下線了
String username = session.getPrincipal().getName();
// log.info("offline: " + username);
System.out.println("offline: " + username);
super.afterConnectionClosed(session, closeStatus);
}
};
}
});
super.configureWebSocketTransport(registration);
}
}
3. 建立實體類
分別建立兩個實體類破托,一個用于封裝向瀏覽器發(fā)送的消息,一個用于封裝接受來自瀏覽器的消息
/**
* 接收來自瀏覽器的消息
*/
public class RequestMessage {
private String name;
public String getName() {
return name;
}
}
/**
* 保存服務(wù)器發(fā)給瀏覽器的信息
*/
public class ResponseMessage {
private String responseMessage;
public ResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
public String getResponseMessage() {
return responseMessage;
}
}
4. Controller代碼
控制器歧蒋,用來控制頁面的跳轉(zhuǎn)和推送服務(wù)的使用
/**
* 控制器
*/
@Controller
public class WsController {
@Autowired
private SimpMessagingTemplate messagingTemplate; //實現(xiàn)向瀏覽器發(fā)送信息的功能
//轉(zhuǎn)到ws.html界面,ws是一個測試界面土砂,既可以發(fā)布消息也可以接受推送
@RequestMapping(value = "/ws")
public String tows(){
return "/ws";
}
/**
* 進行公告推送,此處只用注解疏尿,不使用注解請參考點對點中的@MessageMapping("/chat1")對應(yīng)具體方法
* @MessageMapping("/welcome") 對應(yīng)ws.html中的stompClient.send("/app/welcome")
* 多出來的“/app"是WebSocKetConfig中定義的,如不定義瘟芝,則HTML中對應(yīng)改為stompClient.send("/welcome")
* @SendTo("/topic/getResponse") 指定訂閱路徑易桃,對應(yīng)HTML中的stompClient.subscribe('/topic/getResponse', ……)
* 意味將信息推送給所有訂閱了"/topic/getResponse"的用戶
*/
@MessageMapping("/welcome")
@SendTo("/topic/getResponse") //指明發(fā)布路徑
public ResponseMessage say(RequestMessage message) {
System.out.println(message.getName());
return new ResponseMessage("welcome," + message.getName() + " !");
}
}
5. 前端ws.html
前提:添加腳本褥琐,將stomp.js、sockjs.min.js 以及jqury.js 腳本放在src/main/resources/static下晤郑〉谐剩可在這里下載贸宏。
代碼:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>廣播式WebSocket</title>
<script th:src="@{js/sockjs.min.js}"></script>
<script th:src="@{js/stomp.js}"></script>
<script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">Sorry,瀏覽器不支持WebSocket</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">連接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">斷開連接</button>
</div>
<div id="conversationDiv">
<label>輸入你的名字</label><input type="text" id="name"/>
<button id="sendName" onclick="sendName();">發(fā)送</button>
<p id="response"></p>
</div>
</div>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
document.getElementById("conversationDiv").style.visibility = connected ? 'visible' : 'hidden';
// $("#connect").disabled = connected;
// $("#disconnect").disabled = !connected;
$("#response").html();
}
//點擊連接按鈕后執(zhí)行
function connect() {
//指明連接的節(jié)點磕洪,節(jié)點在WebSocKetConfig中配置
var socket = new SockJS('/endpointSang');
//利用Stomp協(xié)議創(chuàng)建socket客戶端
stompClient = Stomp.over(socket);
/**
* 調(diào)用stompClient中的connect方法來連接服務(wù)端吭练,
* 連接成功之后調(diào)用setConnected方法,該隱藏的隱藏析显,該顯示的顯示
*/
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected:' + frame);
//調(diào)用stompClient中的subscribe方法來訂閱/topic/getResponse發(fā)送來的消息
stompClient.subscribe('/topic/getResponse', function (response) {
showResponse(JSON.parse(response.body).responseMessage);
})
});
}
function disconnect() {
if (stompClient != null) {
//斷開連接
stompClient.disconnect();
}
setConnected(false);
console.log('Disconnected');
}
function sendName() {
var name = $('#name').val();
console.log('name:' + name);
/**
* 發(fā)送一條消息到服務(wù)端鲫咽,"/app/welcome"由WebSocketConfig中配置的“/app"
* 和控制器中對應(yīng)的MessageMapping("welcome")
*/
stompClient.send("/app/welcome", {}, JSON.stringify({'name': name}));
}
//顯示消息
function showResponse(message) {
$("#response").html(message);
}
</script>
</body>
</html>
6. 啟動入口DemoApplication.Java
自動生成的入口代碼。不做更改
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
7. 結(jié)果測試
啟動該工程谷异,在瀏覽器中訪問 http://localhost:8080/ws
打開兩個或多個分尸,連接后在其中一個界面輸入內(nèi)容,點擊“發(fā)送”后可在所有的界面看到相關(guān)信息歹嘹,證明全局推送成功箩绍。
效果圖如下:
模塊二:點對點推送
在進行點對點推送的開發(fā)時,考慮到點對點里面的每個“點”都需要有一個唯一的標識符尺上,所以在項目中引入了pring-boot –security材蛛,用來對每個“點”進行唯一的標識。
1. pom.xml
本項目采用maven的構(gòu)建方式怎抛,下面是項目的pom.xml文件內(nèi)容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.tourage</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</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-test</artifactId>
<scope>test</scope>
</dependency>
<!--增加的security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.創(chuàng)建WebSecurityConfig
配置WebSecurityConfig.java卑吭,設(shè)置攔截規(guī)則,初始化用戶及其角色
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//設(shè)置攔截規(guī)則
.antMatchers("/","/login") ////1根路徑和/login路徑不攔截
.permitAll()
.anyRequest()
.authenticated()
.and()
//開啟默認登錄頁面
.formLogin()
//默認登錄頁面
.loginPage("/login")
//默認登錄成功跳轉(zhuǎn)頁面
.defaultSuccessUrl("/chat")
.permitAll()
.and()
//設(shè)置注銷
.logout()
.permitAll();
}
//設(shè)置用戶
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("111").password("111").roles("USER")
.and()
.withUser("222").password("222").roles("USER")
.and()
.withUser("333").password("333").roles("USER")
.and()
.withUser("444").password("444").roles("USER");
}
@Override
public void configure(WebSecurity web) throws Exception {
//設(shè)置不攔截規(guī)則
web.ignoring().antMatchers("/resources/static/**");
}
}
3. 創(chuàng)建WebSocketConfig
該類與上一模塊相同抽诉,其中陨簇,在設(shè)置STOMP節(jié)點時引入了一個攔截器功能,具體見“/socket”節(jié)點及相關(guān)注釋信息
@Configuration
@EnableWebSocketMessageBroker //開啟使用STOMP協(xié)議來傳輸基于代理的消息
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
//注冊STOMP協(xié)議的節(jié)點迹淌,并指定映射的URL
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
/**
* 注冊STOMP協(xié)議節(jié)點河绽,同時指定使用SockJS協(xié)議,在前端界面中會使用該處的節(jié)點進行SocKet連接
* 根據(jù)需要注冊即可唉窃,這里因為只是測試耙饰,注冊了3個節(jié)點
* 另外,在第三個節(jié)點中纹份,可以看到比前兩個節(jié)點多了一些內(nèi)容苟跪,制定了一個攔截器及其他信息,該攔截器在demo中主要用在點對點模塊
* 這個根據(jù)需要來擴展蔓涧,攔截器是定義的一個class文件件已,因本人也是初次接觸這部分內(nèi)容,不太了解元暴。
*/
stompEndpointRegistry.addEndpoint("/endpointSang").withSockJS();
stompEndpointRegistry.addEndpoint("/endpointChat").withSockJS();
stompEndpointRegistry.addEndpoint("/socket").withSockJS()
.setInterceptors( new ChatHandlerShareInterceptor())
.setStreamBytesLimit(512 * 1024)
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000);
}
//配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/**
* 配置消息代理篷扩,前端通過這個代理來進行消息訂閱,
* 消息代理可以有一個,也可以有多個茉盏,用 “鉴未,” 號分隔
* 這里配置了兩個枢冤,"/topic"用作全局推送,"/queue"用做點對點使用
*/
registry.enableSimpleBroker("/queue","/topic");
/**
* 配置接收前端信息的消息代理铜秆,前端通過這個代理來向后臺傳遞消息
* 簡單來說淹真,前端通過這個消息代理訪問對應(yīng)controller中的MessageMapping(value)
*/
registry.setApplicationDestinationPrefixes("/app");
//registry.setPathMatcher(new AntPathMatcher("."));
}
@Override
public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
@Override
public WebSocketHandler decorate(final WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
// 客戶端與服務(wù)器端建立連接后,此處記錄誰上線了
String username = session.getPrincipal().getName();
//log.info("online: " + username);
System.out.println("online: " + username);
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
// 客戶端與服務(wù)器端斷開連接后连茧,此處記錄誰下線了
String username = session.getPrincipal().getName();
// log.info("offline: " + username);
System.out.println("offline: " + username);
super.afterConnectionClosed(session, closeStatus);
}
};
}
});
super.configureWebSocketTransport(registration);
}
}
4.配置攔截器
攔截器功能可通過自己定義來實現(xiàn)具體的功能核蘸,此處的攔截器并未在測試項目中有太多價值的用法,增加該功能只是為了在具體使用“點對點推送”功能時進行擴展啸驯。
/**
* websocket攔截器配置值纱,讀取session.
*/
public class ChatHandlerShareInterceptor extends HttpSessionHandshakeInterceptor {
private static Logger logger = LoggerFactory.getLogger(ChatHandlerShareInterceptor.class);
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
// TODO Auto-generated method stub
super.afterHandshake(request, response, wsHandler, ex);
}
@Override
public boolean beforeHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2,
Map<String, Object> arg3) throws Exception {
if(arg0 instanceof ServletServerHttpRequest){
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) arg0;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
//使用userName區(qū)分WebSocketHandler,以便定向發(fā)送消息
String httpSessionId = session.getId();
logger.info(httpSessionId);
arg3.put("HTTP_SESSION_ID",httpSessionId);
}else{
}
}
return true;
}
}
5. Controller控制器
/**
* 控制器
*/
@Controller
public class WsController {
@Autowired
private SimpMessagingTemplate messagingTemplate; //實現(xiàn)向瀏覽器發(fā)送信息的功能
// 轉(zhuǎn)到相關(guān)的HTML界面
@RequestMapping(value = "/login")
public String toLogin() {
return "/login";
}
@RequestMapping(value = "/chat")
public String toChat() {
return "/chat";
}
/**
* 轉(zhuǎn)到消息推送坯汤,對應(yīng)前端chat.html中的stomp.send("/chat1",{},text)
* 此處未使用注解虐唠,而是使用代碼編程來完成推送,目的是為了方便對批量(可理解為分組)推送進行擴展
* 編程實現(xiàn)消息推送,點對點使用convertAndSendToUser惰聂,廣播使用convertAndSendTo
* 使用注解可參考全局推送里面的具體實現(xiàn)疆偿,注:點對點用SendToUser()來代替SendTo()
*/
@MessageMapping("/chat1")
// 可以直接在參數(shù)中獲取Principal,Principal中包含有當前用戶的用戶名搓幌。
public void handleChat(Principal principal, String msg) {
List<String> list = new ArrayList<String>();
list.add("222");
list.add("333");
list.add("444");
if (principal.getName().equals("111")) {
for (String s : list)
/**
* 第一個參數(shù):用戶名杆故;第二個參數(shù):訂閱路徑;第三個參數(shù):要推送的消息文本
* 注:全局推送對應(yīng)方法有兩個參數(shù)溉愁,第一個參數(shù):訂閱路徑处铛;第二個參數(shù):要推送的消息文本
*/
messagingTemplate.convertAndSendToUser(s, "/queue/notifications", principal.getName() + "給您發(fā)來了消息:" + msg);
} else {
messagingTemplate.convertAndSendToUser("111", "/queue/notifications", principal.getName() + "給您發(fā)來了消息:" + msg);
}
}
}
6. 前端界面
這部分使用兩個前端界面,一個簡單的登錄界面login.html拐揭,還有一個聊天界面chat.html撤蟆,在chat.html中同時可以使用發(fā)布信息和接受推送的功能,可按照實際需求進行自我定制
注:在前端編寫的時候一定要記得添加相關(guān)的js文件堂污,下載地址:(http://pan.baidu.com/s/1gfKJCAJ)
Login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta charset="UTF-8" />
<head>
<title>登陸頁面</title>
</head>
<body>
<div th:if="${param.error}">
無效的賬號和密碼
</div>
<div th:if="${param.logout}">
你已注銷
</div>
<form th:action="@{/login}" method="post">
<div><label> 賬號 : <input type="text" name="username"/> </label></div>
<div><label> 密碼: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="登陸"/></div>
</form>
</body>
</html>
Chat.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>聊天室</title>
<script th:src="@{js/sockjs.min.js}"></script>
<script th:src="@{js/stomp.js}"></script>
<script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body>
<p>聊天室</p>
<form id="sangForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit" value="發(fā)送"/>
</form>
<script th:inline="javascript">
$("#sangForm").submit(function (e) {
e.preventDefault();
var textArea = $("#sangForm").find('textarea[name="text"]');
var text = textArea.val();
sendSpittle(text);
textArea.val('');
});
var sock = new SockJS("/endpointChat");
var stomp = Stomp.over(sock);
/**
* 連接服務(wù)端家肯,成功后注冊監(jiān)聽,注冊地址為/user/queue/notifications
* 監(jiān)聽地址比WebSocket配置文件中的多了一個/user盟猖,這個/user是必不可少的讨衣,使用了它消息才會點對點傳送。
*/
stomp.connect('guest','guest',function (frame) {
stomp.subscribe("/user/queue/notifications", handleNotification);
});
function handleNotification(message) {
$("#output").append("<b>Received: "+message.body+"</b><br/>")
}
/**
* 消息轉(zhuǎn)發(fā)式镐,對應(yīng)相關(guān)controller中的MessageMapping("/chat1")
* 增加的“/app”源于WebSocketConfig中配置的結(jié)果
* @param text
*/
function sendSpittle(text) {
stomp.send("/app/chat1", {}, text);
}
// 關(guān)閉連接
$("#stop").click(function () {
sock.close();
});
</script>
<div id="output"></div>
</body>
</html>
7. 啟動入口:
略反镇,項目默認的啟動界面
8. 結(jié)果測試
采用兩個瀏覽器分別登入兩個用戶,輸入測試可看到如下結(jié)果
注意:一定要使用兩個瀏覽器登錄娘汞,不然會默認為同一個Session
項目地址:
https://github.com/AnRic/WebSocket_demo
GitHub新人歹茶,求關(guān)注
寫在最后
本文是我在參考網(wǎng)上資源并親身實踐后編寫的一篇小文章,做一個簡單的學習記錄,也希望能幫助到大家辆亏。在其中可能也會有一些的不足之處,請包容鳖目。如有疑問扮叨,請發(fā)送私信聯(lián)系我,謝謝领迈。