在构建Spring boot项目时已经提供webSocket依赖的勾选。webSocket是TCP之上的一个非常薄的轻量级层 ,webSocket主要的应用场景离不开即时通讯与消息推送,但只要应用程序需要在浏览器和服务器之间来回发送消息,就可以使用webSocket来降低客户端流量与服务器的负载。
下面将基于Spring boot实现一个非常简单的HelloWorld程序,用来熟悉项目构建逻辑。
主要的webSocket依赖
<dependency> <group>>org.springframework.boot</group>> <artifact>>spring-boot-starter-websocket</artifact>> </dependency>
一些js库依赖,这里也使用maven方式导入,官网 https://www.webjars.org/
<dependency> <group>>org.webjars</group>> <artifact>>webjars-locator-core</artifact>> </dependency> <dependency> <group>>org.webjars</group>> <artifact>>sockjs-client</artifact>> <version>1.0.2</version> </dependency> <dependency> <group>>org.webjars</group>> <artifact>>stomp-websocket</artifact>> <version>2.3.3</version> </dependency> <dependency> <group>>org.webjars</group>> <artifact>>bootstrap</artifact>> <version>3.3.7</version> </dependency> <dependency> <group>>org.webjars</group>> <artifact>>jquery</artifact>> <version>3.1.0</version> </dependency>
Thymeleaf模板引擎,不用多说了吧
<dependency> <group>>org.springframework.boot</group>> <artifact>>spring-boot-starter-thymeleaf</artifact>> </dependency>
接下来开启webSocket并配置一番
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Overr> public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-gu>).withSockJS(); } @Overr> public vo> configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } }
这里从上向下说一下,
1)@EnableWebSocketMessageBroker注解用于开启使用STOMP协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)开始支持@MessageMapping,就像是使用@requestMapping一样。
2)registerStompEndpoints()方法只写了一行代码:
registry.addEndpoint("/gs-gu>).withSockJS();
一是用来注册一个Stomp的节点(endpoint),也就是webSocket的服务端地址,客户端在链接时使用到;
二是withSockJs()方法指定使用SockJS协议。SockJs是一个WebSocket的通信js库,Spring对这个js库进行了后台的自动支持,如果使用它不需要进行过多配置。
3)配置消息代理(MessageBroker),该方法干了两件事,一是启用一个简单的message broker并配置一个或多个前缀来过滤针对代理的目的地(例如以“/topic”为前缀的目的地),该前戳限制了网页客户端设置本地地址时的前戳。 二是设置了一个客户端访问服务端地址的前缀。比如我们设置@MessageMapping("/hello"),那客户端要发送消息到服务器上的地址是 /app/hello。
@Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { System.out.println("收到:" + message.toString() + "消息"); return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); }
1)@MessageMapping和@RequestMapping功能类似,用于设置URL映射地址,浏览器向服务器发起请求,需要通过该地址。
需要注意,这里设置路径为/hello,但是客户端需要访问/app/hello,原因前面已经讲述。
2)@SendTo("/topic/greetings") 设置目的地,这里的目的地是站在服务端的角度对客户端而言。客户端也需要设置相同的地址,而且必须使用/topic前戳,前面也已经讲述。
本示例中如果服务器接受到了消息,就会对订阅了@SendTo括号中的地址传送消息。
备注:HtmlUtils.htmlEscape()方法会将特殊字符转换为HTML字符引用。
HelloMessage.java
public > HelloMessage { private String name; ... }
Greeting.java
public > Greeting { private String content; }
在resources/static目录下:
index.html
<!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <link rel="shortcut icon" href="/favicon.ico" /> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/app.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div >="main-content" >="container"> <div >="row"> <div >="col-md-6"> <form >="form-inline"> <div >="form-group"> <label for="connect">WebSocket connection:</label> <button >="connect" >="btn btn-default" type="submit">Connect</button> <button >="disconnect" >="btn btn-default" type="submit" disabled="disabled">Disconnect </button> </div> </form> </div> <div >="col-md-6"> <form >="form-inline"> <div >="form-group"> <label for="name">What is your name?</label> <input type="text" >="name" >="form-control" placeholder="Your name here..."> </div> <button >="send" >="btn btn-default" type="submit">Send</button> </form> </div> </div> <div >="row"> <div >="col-md-12"> <table >="conversation" >="table table-striped"> <thead> <tr> <th>Greetings</th> </tr> </thead> <tbody >="greetings"> </tbody> </table> </div> </div> </div> </body> </html>
main.css
body { background-color: #f5f5f5; } #main-content { max-w>: 940px; padding: 2em 3em; margin: 0 auto 20px; background-color: #fff; border: 1px sol>#e5e5e5; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; }
app.js
var stompClient = null; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { var socket = new SockJS('/gs-gu>); stompClient = Stomp.over(socket); stompClient.connect({">: "header"}, function (frame) { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).content); }); }); } function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()})); } function showGreeting(message) { $("#greetings").append("<tr><td>" + message + "</td></tr>"); } $(function () { $("form").on('submit', function (e) { e.preventDefault(); }); $( "#connect" ).click(function() { connect(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendName(); }); });
这部分最值得说的就是JS部分了,比较简单主要就是3个点击事件,包括连接、断开、发送消息。
当我们点击了连接后,会新建SockjS对象,并设置服务端的连接点(/gs-gu>
subscribe()方法的第一个参数是注册客户端地址,注意前戳必须是/topic开头,因为在前面服务端已经配置了目的地前戳。与@SendTo中的地址对应。
客户端发送消息只需要调用send()方法,还方法的第一个参数是服务端@MessageMapping地址并且加了指定的/app前戳,第二个参数为header头部信息,第三个是发送的消息内容。
成功实现Hello World程序,下面将通过搭建一个一对一聊天服务器,深入学习更多的socket知识。
在前面我们写了一个自动回复的小例子,用到了@MessageMapping("/hello")和@SendTo("/topic/greetings")两个注解,其实我们还可以使用编程的方式发送消息。
其实很简单,直接引用该消息模板
@Autowired private SimpMessagingTemplate messagingTemplate;
消息模板内置了一系列方法,比如
vo> convertAndSendToUser(String user, String destination, Object payload) vo> convertAndSend(D destination, Object payload)
这俩方法会包装为消息并将其发送到给定的目的地。
监听webSocket服务器的连接只需要实现ApplicationListener<>接口。代码如下:
@Component public > STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> { @Overr> public vo> onApplicationEvent(SessionConnectEvent event) { StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); //判断客户端的连接状态 switch (sha.getCommand()) { case CONNECT: System.out.println("上线"); break; case DISCONNECT: System.out.println("下线"); break; case SUBSCRIBE: System.out.println("订阅"); break; case SEND: System.out.println("发送"); break; case UNSUBSCRIBE: System.out.println("取消订阅"); break; default: break; } } }
监听的所有状态被封装在一个枚举类中,其实还有很多,这里不再一一列举。
有了上面这些知识,我们就可以基于此开发一对一聊天服务器。
webSocket提供的是一个socket框架,并不会帮我们管理session,我们需要自己去编写session管理类,进行session的读写。代码如下:
@Component public > SocketSessionMap { private final static ConcurrentMap<String, String> sessionMap = new ConcurrentHashMap<>(); /** * 注册Session * @param userId * @param sessionId */ public synchronized vo> registerSession(String user> { sessionMap.put(userId,sessionId); } /** * 移除Session * @param userId * @param sessionId */ public synchronized vo> removeSession(String user> { sessionMap.remove(userId); } /** * 获取用户的SessionId * @param userId * @return */ public String getUserSession>(String user>{ return sessionMap.get(userId); } /** * 获取所有Session集合 * @return */ public Map<String, String> queryAllSession(){ return sessionMap; } /** * 获取集合的大小 */ public int onlineCount(){ return sessionMap.size(); } }
接着,对STOMP监听类进行扩展。
@Component public > STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> { @Autowired SocketSessionMap socketSessionMap; @Overr> public vo> onApplicationEvent(SessionConnectEvent event) { StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); String user>">); String sessionId = sha.getSessionId(); switch (sha.getCommand()) { case CONNECT: System.out.println("上线:" + user>" " + sessionId); socketSessionMap.registerSession(userId, sessionId); break; default: break; } } }
服务端通过 sha.getFirstNativeHeader(">读取到客户端的>
当服务端监听到客户端连接时,会将用户Session>
这里我们使用更可靠的请求下线方式,代码如下:
@MessageMapping("/chatOut") public vo> sayHello(String user> { String sessionId = socketSessionMap.getUserSessionId(userId); System.out.println("下线:" + user>" " + sessionId); socketSessionMap.removeSession(userId,sessionId); }
当收到下线请求时,移除Session>
关于Session,也可以设置一个最大值,超时自动移除。
在一对一服务器中,主要处理的就是一对一的消息发送。大致逻辑是接收客户端消息,分析消息结构,通过SessionMap判断对方是否在线,然后发送相应内容。代码如下:
@MessageMapping("/chat") public vo> sayHello(Message user) { System.out.println(user.get>"-->"+user.getP>":"+user.getContent()); String userP>String.valueOf(user.getPid()); String user>String.valueOf(user.getId()); String sendTo = "/topic/chat/"+userPid; String content = user.get>":"+user.getContent(); if (socketSessionMap.getUserSession>null){ messagingTemplate.convertAndSend(sendTo, HtmlUtils.htmlEscape(content)); }else { sendTo = "/topic/chat/"+userId; content = "对方已下线"; messagingTemplate.convertAndSend(sendTo, HtmlUtils.htmlEscape(content)); } }
值得一体的是,关于用户>
Message.java
public > Message { private int >//用户> private String content;//发送内容 private int p>//发送到用户
服务端使用FreeMarker模板引擎返回html网页,代码如下:
@RequestMapping("/chat/{>) public String chat_page(@PathVariable int id, ModelMap model) { model.addAttribute(">, id); int count = socketSessionMap.onlineCount(); model.addAttribute("count", count); return "chat"; }
通过RESTful形式的URl注册>
chat.html
<!DOCTYPE html > <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Hello WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <link rel="shortcut icon" href="/favicon.ico" /> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/chat.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div >="main-content" >="container"> <div >="row"> <div >="col-md-6"> <form >="form-inline"> <div >="form-group"> <label for="connect">WebSocket connection:</label> <button >="connect" >="btn btn-default" type="submit">Connect</button> <button >="disconnect" >="btn btn-default" type="submit" disabled="disabled">Disconnect </button> </div> </form> </div> <div >="col-md-6"> <form >="form-inline"> <div >="form-group"> <label th:text="'Online:'+${count}"> </label> <input type="text" >="> >="form-control" th:value="${>required> <input type="text" >="content" >="form-control" placeholder="Your name here..."required> <input type="text" >="p> >="form-control" placeholder="Your P>required> </div> <button >="send" >="btn btn-default" type="submit">Send</button> </form> </div> </div> <div >="row"> <div >="col-md-12"> <table >="conversation" >="table table-striped"> <thead> <tr> <th>Greetings</th> </tr> </thead> <tbody >="greetings"> </tbody> </table> </div> </div> </div> </body> </html>
chat.js
var stompClient = null; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { var socket = new SockJS('/gs-gu>); stompClient = Stomp.over(socket); stompClient.connect({">: $("#>).val()}, function (frame) { //客户端> setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/chat/' + $("#>).val(), function (greeting) { //表明客户端地址 showGreeting(greeting.body); }, {">: "Host_" + $("#>).val()}); }); } function disconnect() { if (stompClient !== null) { stompClient.send("/app/chatOut", {},$("#>).val()); stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.send("/app/chat", {}, JSON.stringify({ 'content': $("#content").val(), '>: $("#>).val(), 'p>: $("#p>).val() })); showGreeting("我:" + $("#content").val()) } function showGreeting(message) { $("#greetings").append("<tr><td>" + message + "</td></tr>"); } $(function () { $("form").on('submit', function (e) { e.preventDefault(); }); $("#connect").click(function () { connect(); }); $("#disconnect").click(function () { disconnect(); }); $("#send").click(function () { sendName(); }); });
网页客户端的主要逻辑在chat.js中。非常有必要的是在stompClient.connect()方法的第一个参数中传入header头信息,该头部信息必须设置>
其次是在用户断开链接前,会向服务端发送断开通知。
stompClient.send("/app/chatOut", {},$("#>).val());
打开两个网页,URl分别为http://localhost:8080/chat/100和http://localhost:8080/chat/101,点击连接。
查看控制台输出:
接下来演示用户101向用户100发送消息:
查看用户100收到信息:
再次查看控制输出:
消息成功发送!
当用户断开链接时,控制台输出为:
关于Spring boot webSocket就先到这里。
Linux公社的RSS地址 : https://www.linuxidc.com/rssFeed.aspx
本文永久更新链接地址: https://www.linuxidc.com/Linux/2018-11/155549.htm