WebSocket应用
WebSocket应用
WebSocket
协议是基于TCP
的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通讯——允许服务器主动发送消息给客户端,这样就可以实现从客户端发送消息到服务器,而服务器又可以转发消息到客户端,这样就能够实现客户端之间的交互。
目前很多浏览器已经实现了WebSocket
协议,但是依旧存在着很多浏览器没有实现该协议,为了兼容哪些没有实现该协议的浏览器,往往还需要通过STOMP
协议来完成这些兼容。
本篇基于Spring Boot2.x
进行开发WebSocket应用
Maven依赖
<!-- websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这里加入了spring security
的依赖,因为有时候对于WebSocket
而言,需要的是点对点通信,这一点需要用户进行登陆。
服务端点配置
ServerEndpointExporter
对象可以定义WebSocket
服务器的端点,这样客户端就能够请求服务器的端点。
package com.lay.websocket.config;
/**
* @Description:
* @Author: lay
* @Date: Created in 11:39 2018/11/22
* @Modified By:IntelliJ IDEA
*/
@Configuration
public class WebSocketConfig {
//创建服务端点
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
有了这个bean,就可以使用@ServerEndPoint
定义一个端点服务类。
在这个端点服务类种还可以定义WebSocket
的打开、关闭、错误和发送消息的方法。
服务端点开发
接口
package com.lay.websocket.service;
import javax.websocket.Session;
/**
* @Description:
* @Author: lay
* @Date: Created in 11:43 2018/11/22
* @Modified By:IntelliJ IDEA
*/
public interface WebSocketService {
//连接成功调用的方法
public void onOpen(Session session);
//连接关闭调用的方法
public void onClose();
//收到客户端消息后调用的方法
public void onMessage(String message,Session session);
//发生异常时调用的方法
public void onError(Session session,Throwable error);
}
实现类
package com.lay.websocket.service.impl;
import com.lay.websocket.service.WebSocketService;
import org.springframework.stereotype.Service;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @Description:
* @Author: lay
* @Date: Created in 11:51 2018/11/22
* @Modified By:IntelliJ IDEA
*/
@Service
@ServerEndpoint("/ws")
public class WebSocketServiceImpl implements WebSocketService {
//静态变量,用来记录当前在线连接数,应该把它涉及成线程安全的
private static int onlineCount = 0;
// concurrent包含的线程安全Set,用来存放每个客户端对应的WebSocketServiceImpl对象
private static CopyOnWriteArraySet<WebSocketServiceImpl> webSocketSet = new CopyOnWriteArraySet<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//连接加你成功调用的方法
@Override
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this);//加入set中
addOnllineCount();
System.out.println("有新连接加入!当前在线人数为:" + getOnlineCount());
try {
sendMessage("有新的连接加入了!!");
} catch (IOException e) {
System.out.println("IO异常");
}
}
//连接关闭调用方法
@Override
@OnClose
public void onClose() {
webSocketSet.remove(this);
subOnlineCount();
System.out.println("有一连接关闭!当前在线人数为:" + getOnlineCount());
}
//收到客户端消息后调用方法
@Override
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
webSocketSet.forEach(p -> {
//获取当前用户名称
String userName = p.getSession().getUserPrincipal().getName();
System.out.println(userName);
try {
p.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
});
}
//当发生错误时调用方法
@Override
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
//发送消息
private void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
//返回在线人数
private static synchronized int getOnlineCount() {
return onlineCount;
}
//当连接人数增加时
private static synchronized void addOnllineCount() {
WebSocketServiceImpl.onlineCount++;
}
//当连接人数减少时
private static synchronized void subOnlineCount() {
WebSocketServiceImpl.onlineCount--;
}
public Session getSession() {
return session;
}
}
这里说明下重要的几点
- @ServerEndpoint("/ws"):表示让Spring创建WebSocket的服务端点,其中请求地址是
/ws
。 - @OnOpen:标注客户端打开WebSocket服务端点调用方法。
- @OnClose:标注客户端关闭WebSocket服务端点调用方法。
- @OnMessage:标注客户端发送消息,WebSocket服务端点调用方法。
- @OnError:标注客户端请求WebSocket服务端点发生异常调用方法。
因为每个客户端打开时,都会为其创建一个WebSocketServiceImpl对象,素以这里的打开方法终都会去计数并且将这个对象保存到CopyOnWriteArraySet中,这样你就可以知道有多少连接。对于关闭方法则是清除这个对象,并且计数减一。对于消息发送方法,则是通过轮询对所有客户端的连接都给予发送消息,所以所有的连接都可以接收这个消息,但是有时候可能只是需要发送给特定的用户,则需要得到用户信息,然后再发送给特定用户。
页面开发
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<title>websocket</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript" th:src="@{/websocket.js}"></script>
</head>
<body>
测试WebSocket站点
<br />
<input id="message" type="text"/>
<button onclick="sendMessage()">发送消息</button>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<div id="context"></div>
</body>
</html>
引入相关JS
alert("coming");
var websocket=null;
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//创建WebSocket对象,连接服务器端点
websocket=new WebSocket("ws://localhost:8080/ws");
}else {
alert('Not Support WebSocket');
}
//连接发生错误的回调方法
websocket.onerror=function () {
appendMessage("error");
}
//连接成功建立的回调方法
websocket.onopen=function () {
appendMessage("open");
}
//接收到消息的回调方法
websocket.onmessage=function (event) {
appendMessage(event.data);
}
//连接关闭的回调方法
websocket.onclose=function () {
appendMessage("close");
}
//监听窗口关闭事件,当窗口关闭时,主动关闭webSocket连接
//防止连接还没断开就关闭窗口,server端会抛出异常
window.onbeforeunload=function () {
websocket.close();
}
//将消息显示在网页上
function appendMessage(message) {
var context=$('#context').html()+"<br/>"+message;
$("#context").html(context);
}
// 关闭连接
function closeWebSocket() {
websocket.close();
}
// 发送消息
function sendMessage(){
var message=$('#message').val();
websocket.send(message);
}
控制器
package com.lay.websocket.controller;
/**
* @Description:
* @Author: lay
* @Date: Created in 15:11 2018/11/22
* @Modified By:IntelliJ IDEA
*/
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
//跳转websocket页面
@GetMapping("/index")
public String websocket(){
return "websocket";
}
}
使用STOMP
并不是所有的浏览器都支持WebSocket协议,一些旧版本的浏览器并不能支持WebSocket协议,因此还需要去兼容这些浏览器。为此可以引入WebSocket
的子协议STOMP
(Simple or Streaming Text Orientated Messaging Protocol),通过它即可兼容那些不支持WebSocket协议的浏览器。
首先需要在配置文件中加入注解@EnableWebSocketMessageBroker
,这个注解将会启动WebSocket
下的子协议STOMP
。为了配置这个协议,可以实现Spring 提供的接口WebSocketMessageBrokerConfigurer
。为了更为简单,spring还提供了这个接口的空实现的抽象类AbstractWebSocketMessageBrokerConfigurer
。通过覆盖它所定义的方法即可。下面给我具体的实例。
配置STOMP服务端点和请求订阅前缀
package com.lay.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @Description:
* @Author: lay
* @Date: Created in 17:46 2018/11/22
* @Modified By:IntelliJ IDEA
*/
//启用STOMP协议
@EnableWebSocketMessageBroker
@Configuration
public class StomopWebSocktconfig implements WebSocketMessageBrokerConfigurer {
//注册服务器端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//增加一个聊天服务端点
registry.addEndpoint("/socket").withSockJS();
//增加一个用户服务端点
registry.addEndpoint("/wsuser").withSockJS();
}
//定义服务器端点请求和订阅前缀
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//客户端订阅路径前缀
registry.enableSimpleBroker("/sub","/queue");
//服务端点请求前缀
registry.setApplicationDestinationPrefixes("/request");
}
}
代码中使用了@EnableWebSocketMessageBroker
驱动Spring 启用STOMP协议。然后这个配置类继承了AbstractWebSocketMessageBrokerConfigurer
,这样覆盖其定义的方法就可以配置STOMP相关服务。其中registerStompEndpoints
方法是用于注册端点的方法。这里定义了"/socket"
和"/wsuser"
两个服务端点,而在定义端点时还加入了withSockJS
方法,这个方法的声明代表着可以支持SockJS
,SockJS它是一个第三方关于支持WebSocket
请求的JavaScript
框架。configureMessageBroker
方法注册请求的前缀和客户端订阅的前缀。
配置之后,spring boot就会自动篡改就SimpMessagingTemplate
对象,它是一个可以进行转发消息的模板,通过这个模板可以发送消息到特定的地址,甚至是限制给特定的用户发送消息。
控制器
package com.lay.websocket.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.security.Principal;
/**
* @Description:
* @Author: lay
* @Date: Created in 17:59 2018/11/22
* @Modified By:IntelliJ IDEA
*/
@Controller
@RequestMapping("stompws")
public class StompWebSocketController {
//注入spring boot 自动配置消息模板对象
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
//发送页面
@GetMapping("/send")
public String send(){
return "/stomp/send";
}
//接收页面
@GetMapping("/receive")
public String receive(){
return "/stomp/receive";
}
//对特定用户发送页面
@GetMapping("/sendUser")
public String sendUser(){
return "/stomp/send-user";
}
//接收用户消息页面
@GetMapping("/receiveUser")
public String receiveUser(){
return "/stomp/receive-user";
}
//定义消息请求路径
@MessageMapping("/send")
//定义结果发送到特定路径
@SendTo("/sub/chat")
public String sendMsg(String value){
return value;
}
//将消息发送给特定用户
@MessageMapping("/sendUser")
public void sendToUser(Principal principal,String body){
String srcUser=principal.getName();
//解析用户和消息
String[] args=body.split(",");
String desUser=args[0];
String message="【"+srcUser+"】给你发来消息:"+args[1];
//发送到用户和监听地址
simpMessagingTemplate.convertAndSendToUser(desUser,"/query/customer",message);
}
}
@MessageMapping注解和@RequestMapping类似,它是定义WebSocket
请求的路径,当然需要与WebSocketConfig
所定义的前缀"/request"
连用。
@SendTo注解配置为"/sub/chat"
,说明在执行玩这个方法后,会将返回结果发送到订阅的这个目的地中,这样客户端就可以得到消息。
sendToUser
方法存在Principal
参数,如果使用了spring security,这个参数可以获取当前用户的消息,然后通过simpMessagingTemplate
的convertAndSendToUser
方法,就可以设置发送给对应的目的地并且限定特定的用户消息。
因为这里涉及到用户,所以需要使用spring security,我们进行配置。
配置Spring Security
package com.lay.websocket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootApplication
@EnableScheduling
public class WebsocketApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(WebsocketApplication.class, args);
}
//定义3个可以登陆的内存用户
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//密码加密器
PasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
//加入3个内存用户,密码分别为加密后的 p1,p2,p3
//可以通过passwordEncoder.encode("p1")这样获得加密后的密码
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder)
.withUser("user1").password(passwordEncoder.encode("p1"))
.roles("USER").and()
.withUser("user2").password(passwordEncoder.encode("p2"))
.roles("USER").and()
.withUser("user3").password(passwordEncoder.encode("p3"))
.roles("USER");
}
}
这样就限定了3个可以登陆的用户和密码。wile测试还需要客户端,这里涉及了WebSocketController
所定义的4个映射地址,下面一个一个给出。
页面
首先是WebSocketController
的send
方法映射的/stomp/send.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<title>websocket</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
<script type="text/javascript" th:src="@{/stomp.min.js}"></script>
<script type="text/javascript">
var stompClient=null;
//设置连接
function setConnected(connected) {
$("#connect").attr({"disabled":connected});
$('#disconnect').attr({"disabled":!connected});
if(connected){
$('#conversationDiv').show();
}else {
$('#conversationDiv').hide();
}
$('#response').html("");
}
//开启socket连接
function connect() {
//定义请求服务器的端点
var socket=new SockJS('/socket');
// stomp客户端
stompClient=Stomp.over(socket);
//连接服务器端点
stompClient.connect({},function (frame) {
//建立连接后的回调
setConnected(true);
});
}
//断开socket连接
function disconnect() {
if(stompClient!=null){
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
//向/request/send服务端发送消息
function sendMsg() {
var value=$('#message').val();
//发送消息到"/request/send",其中/request是服务器定义的前缀
//而send则是@MessageMapping所配置的路径
stompClient.send("/request/send",{},value);
}
//启用连接
connect();
</script>
</head>
<body>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" onclick="disconnect();" disabled="disabled">断开连接</button>
</div>
<div id="conversationDiv">
<p>
<label>发送的内容</label>
</p>
<p>
<textarea id="message" rows="5"></textarea>
</p>
<button id="sendMsg" onclick="sendMsg();">Send</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
这里加入了socket.min.js
和stomp.min.js
两个javaSript脚本,这样就可以通过对应JavaScript API进行请求服务器端点。
- 首先是建立连接,可以看到
connect
函数 - 其次是发送消息可以看到
sendMsg
函数 - 最后是关闭连接可以看到
disconnect
函数
这里的sendMsg
函数请求额是StompWebSockertController
的sendMsg
方法,这样这个方法就将消息发送到"/sub/chat"
中,所以需要一个客户端去订阅这个地址,者就是WebSocketControlelr
的receive
方法所映射的/stomp/receive.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<title>websocket</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
<script type="text/javascript" th:src="@{/stomp.min.js}"></script>
<script type="text/javascript">
var noticeSocket=function () {
//连接服务器端点
var s=new SockJS('/socket');
//客户端
var stompClient=Stomp.over(s);
stompClient.connect({},function () {
console.log('notice socket connected');
//订阅消息地址
stompClient.subscribe('/sub/chat',function (data) {
$('#receive').html(data.body);
})
});
};
noticeSocket();
</script>
</head>
<body>
<h1>
<span id="receive">等待接收消息</span>
</h1>
</body>
</html>
此处,客户端stompClient在服务器端点之后,加入了订阅消息的地址,这样就能够获取StompWebSocketController
的sendMsg
方法发送到"/sub/chat"
的消息。
这里只是可以实现群发的功能,有时候可能仅仅希望只是对一个用户发送,这样就会用到StompWebSocketController
的sendToUser
方法。这里先定义发送的页面,它对应的StompWebSocketController
的sendUser
方法
send-user.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<title>websocket</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
<script type="text/javascript" th:src="@{/stomp.min.js}"></script>
<script type="text/javascript">
var stompClient=null;
//设置连接
function setConnected(connected) {
$("#connect").attr({"disabled":connected});
$('#disconnect').attr({"disabled":!connected});
if(connected){
$('#conversationDiv').show();
}else {
$('#conversationDiv').hide();
}
$('#response').html("");
}
//开启socket连接
function connect() {
//定义请求服务器的端点/wsuser
var socket=new SockJS('/wsuser');
// stomp客户端
stompClient=Stomp.over(socket);
//连接服务器端点
stompClient.connect({},function (frame) {
//建立连接后的回调
setConnected(true);
});
}
//断开socket连接
function disconnect() {
if(stompClient!=null){
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
//向/request/send服务端发送消息
function sendMsg() {
var value=$('#message').val();
var user=$('#user').val();
//用户和消息组成的字符串
var text=user+","+value;
//发送消息到"/request/sendUser",其中/request是服务器定义的前缀
//而send则是@MessageMapping所配置的路径
stompClient.send("/request/sendUser",{},text);
}
//启用连接
connect();
</script>
</head>
<body>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" onclick="disconnect();" disabled="disabled">断开连接</button>
</div>
<div id="conversationDiv">
<p><label>发送给用户</label></p>
<p><input type="text" id="user" /></p>
<p><label>发送的内容</label></p>
<p><textarea id="message" rows="5"></textarea>
</p><button id="sendMsg" onclick="sendMsg();">发送</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
这里连接的是服务器端点"/wsuser"
,而请求的是"/request/sendUser"
,这样就对应了StompWebSocketController
的sendToUser
方法。这里值得注意的是,把用户名和消息都发送给了服务器端点,所以在sendToUser
方法里,分离了用户名和小米,然后就通过消息模板(simpMessagingTemplate)的convertAndSendToUser
方法,指定了用户的参数,然后发送到地址/queue/customer
,这样对应的用户登陆后,通过订阅合格地址就能够得到服务器发送的消息。
下面是订货这个地址的页面代码
receive-user.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<title>websocket</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
<script type="text/javascript" th:src="@{/stomp.min.js}"></script>
<script type="text/javascript">
var noticeSocket=function () {
//连接服务器端点
var s=new SockJS('/wsuser');
//客户端
var stompClient=Stomp.over(s);
stompClient.connect({},function () {
console.log('notice socket connected');
//订阅消息地址
//这里/user前缀是不能缺少的,它代表着订阅指定用户的消息
stompClient.subscribe('/user/queue/customer',function (data) {
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
</head>
<body>
<h1><span id="receive">等待接收消息</span></h1>
</body>
</html>
代码中的/user/queue/customer
的/user
前缀是不能缺少的,它代表着订阅指定用户的消息。至此,所有的页面和代码都开发完成。我们可以登陆不同的用户去进行测试,这里就不讲了。
上一篇: 深入理解 Epoll
下一篇: Python : 术语对照表