websocket应用
- 基于TCP的一种新的 网络协议
- 浏览器 与 服务器 全双工 full-duplex , 通信
- 允许服务端 主动 发送 信息给客户端
- 为了兼容那些没有实现 该协议的浏览器,还需要 通过 STOMP协议来 完成这写兼容
加入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- security ,因为有时候对于 webSocket而言,需要 点对点的通信,需要用户登录
简易的WebSocket服务
自定义websocket服务端点 配置
- ServerEndpointExporter,定义webSocket服务器的端点(供客户端请求)
@Configuration
public class WebSocketConfig {
// 如果你使用的不是Spring Boot依赖的服务器,才需要自己创建
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
定义WebSocket服务端站点
-
@ServerEndpoint 定义端点服务类
-
定义WebSocket的打开,关闭,错误,发送消息
@ServerEndpoint("/ws") //创建服务端点 地址为/ws @Service public class WebSocketServiceImpl { //每一个客户端打开,都会创建WebSocketServiceImpl对象, 下面是计数 将这个对象保存到 CopyOnWriteArraySet 中 //关闭是 清楚这个对象 ,并且 计数 减一 //消息发送, 通过轮询所有的客户端,都发送消息 //只发送特定的用户,则需要得到用户信息,然后在发送 // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServiceImpl对象。 private static CopyOnWriteArraySet<WebSocketServiceImpl> webSocketSet = new CopyOnWriteArraySet<>(); // 与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; /** * 连接建立成功调用的方法。标注客户端打开websocket服务端点调用方法*/ @OnOpen public void onOpen(Session session) { this.session = session; webSocketSet.add(this); // 加入set中 addOnlineCount(); // 在线数加1 System.out.println("有新连接加入!当前在线人数为" + getOnlineCount()); try { sendMessage("有新的连接加入了!!"); } catch (IOException e) { System.out.println("IO异常"); } } /** * 连接关闭调用的方法。标注客户端关闭websocket服务端点调用方法 */ @OnClose public void onClose() { webSocketSet.remove(this); // 从set中删除 subOnlineCount(); // 在线数减1 System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("来自客户端的消息:" + message); // 群发消息 for (WebSocketServiceImpl item : webSocketSet) { try { /* // 获取当前用户名称 String userName = item.getSession() .getUserPrincipal().getName(); System.out.println(userName); */ item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } /** * 发生错误时调用 客户端 请求服务端 发生异常调用 */ @OnError public void onError(Session session, Throwable error) { System.out.println("发生错误"); error.printStackTrace(); } /** * 发送消息 * @param message 客户端消息 * @throws IOException */ private void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } // 返回在线数 private static synchronized int getOnlineCount() { return onlineCount; } // 当连接人数增加时 private static synchronized void addOnlineCount() { WebSocketServiceImpl.onlineCount++; } // 当连接人数减少时 private static synchronized void subOnlineCount() { WebSocketServiceImpl.onlineCount--; } }
-
this.session.getBasicRemote().sendText(message); 发送消息
-
@ServerEndpoint("/ws") //创建服务端点 地址为/ws
开发websocket 页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>My WebSocket</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="./../js/websocket.js"></script>
</head>
<body>
测试一下WebSocket站点吧
<br />
<input id="message" type="text" />
<button οnclick="sendMessage()">发送消息</button>
<button οnclick="closeWebSocket()">关闭WebSocket连接</button>
<div id="context"></div>
</body>
</html>
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(event) {
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);
}
- new WebSocket(“ws://localhost:8080/ws”);
控制器
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
// 跳转websocket页面
@GetMapping("/index")
public String websocket() {
return "websocket";
}
}
使用STOMP
- 旧的版本浏览器 不能支持 webSocket协议,可以引用 WebSocket协议的子协议 STOMP simple or Streaming Text Orientated Messageing Protocol
- 配置文件要加入 @EnableWebSocket MessageBroker (就会启动websocket下的子协议 stomp)
- 配置stomp 实现 WebSocket MessageBroker Configurer
- 为了更加简单 还提供了抽象类 Abstract WebSocket MessageBroker Configurer
配置STOMP的服务端点 和 请求订阅前缀
@Configuration
@EnableWebSocketMessageBroker //启用STOMP协议
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 如果你使用的不是Spring Boot依赖的服务器,才需要自己创建
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
// 注册服务器端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 增加一个聊天服务端点
registry.addEndpoint("/socket").withSockJS();//也可以支持sockJS
// 增加一个用户服务端点
registry.addEndpoint("/wsuser").withSockJS();
}
// 定义服务器端点请求和订阅前缀
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端订阅路径前缀
registry.enableSimpleBroker("/sub", "/queue");
// 服务端点请求前缀
registry.setApplicationDestinationPrefixes("/request");
}
}
- sockJS 是一个 第三方关于 支持 WebSocket请求的 JavaScript框架
- boot会创建 SimpMessaging Template对象
STOMP下的 控制器
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
@Autowired // 注入Spring Boot自动配置消息模板对象
private SimpMessagingTemplate simpMessagingTemplate;
// 发送页面
@GetMapping("/send")
public String send() {
return "send";
}
// 接收页面
@GetMapping("/receive")
public String receive() {
return "receive";
}
// 对特定用户发送页面
@GetMapping("/sendUser")
public String sendUser() {
return "send-user";
}
// 接收用户消息页面
@GetMapping("/receiveUser")
public String receiveUser() {
return "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,
"/queue/customer", message);
}
}
-
@MessageMapping("/send") 定义消息请求路径
- 与 registry.setApplicationDestinationPrefixes("/request") 连用
-
@SendTo("/sub/chat") 在执行完 这个方法后,将返回结果发送到订阅的这个目的址中
- 这样客户端就可以 得到消息
-
principal 获得当前用户的消息
-
simpMessagingTemplate.convertAndSendToUser(desUser, “/queue/customer”, message);
- 发送给对应的目的地,并且限定特定的用户消息
配置 Security
@SpringBootApplication(scanBasePackages = "com.springboot.chapter13")
@EnableScheduling
public class Chapter13Application extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Chapter13Application.class, args);
}
// 定义3个可以登录的内存用户
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码加密器
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 加入三个内存用户,密码分别为加密后的"p1","p2"和"p3"
// 可以通过 passwordEncoder.encode("p1")这样获得加密后的密码
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder)
.withUser("user1")
.password("$2a$10$7njFQKL2WV862XP6Hlyly.F0lkSHtOOQyQ/rlY7Ok26h.gGZD4IqG").roles("USER").and()
.withUser("user2").password("$2a$10$Q2PwvWNpog5sZX583LuQfet.y1rfPMsqtrb7IjmvRn7Ew/wNUjVwS")
.roles("ADMIN").and().withUser("user3")
.password("$2a$10$GskYZT.34BdhmEdOlAS8Re7D73RprpGN0NjaiqS2Ud8XdcBcJck4u").roles("USER");
}
}
jsp
send.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>My WebSocket</title>
<script type="text/javascript"
src="https://code.jquery.com/jquery-3.2.1.min.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" src="./../js/stomp.min.js"></script>
</head>
<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>
<body>
<div>
<div>
<button id="connect" οnclick="connect();">连接</button>
<button id="disconnect" disabled="disabled"
οnclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<p>
<label>发送的内容</label>
</p>
<p>
<textarea id="message" rows="5"></textarea>
</p>
<button id="sendMsg" οnclick="sendMsg();">Send</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
- 加入了socket.min.js 和 stomp.min.js
receive.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>My WebSocket</title>
<script type="text/javascript"
src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<script type="text/javascript" src="./js/stomp.min.js"></script>
</head>
<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>
<body>
<h1><span id="receive">等待接收消息</span></h1>
</body>
</html>
说明
// 客户端订阅路径前缀
registry.enableSimpleBroker("/sub");
// 服务端点请求前缀
registry.setApplicationDestinationPrefixes("/request");
// 增加一个聊天服务端点
registry.addEndpoint("/socket").withSockJS();
// 定义消息请求路径
@MessageMapping("/send")
// 定义结果发送到特定路径
@SendTo("/sub/chat")
发送消息:
首先是创建了:new SockJS('/socket'); 路径
发送消息的路径:stompClient.send("/request/send", {}, value);
接收消息时:
var s = new SockJS('/socket');
stompClient.subscribe('/sub/chat', function(data) {
$('#receive').html(data.body);
});
send-user.jsp
<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/sendUser’服务端发送消息
function sendMsg() {
var value = $("#message").val();
var user = $("#user").val();
// 用户和消息组成的字符串
var text = user +"," + value;
stompClient.send("/request/sendUser", {}, text);
}
connect();
</script>
<body>
<div>
<div>
<button id="connect" οnclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" οnclick="disconnect();">断开连接</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" οnclick="sendMsg();">发送</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
receive-user.jsp
<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!');
stompClient.subscribe('/user/queue/customer', function(data) {
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
<body>
<h1><span id="receive">等待接收消息</span></h1>
</body>
</html>
说明
var socket = new SockJS('/wsuser');
stompClient.connect({}, function(frame) {
setConnected(true);
});
stompClient.send("/request/sendUser", {}, text);
@MessageMapping("/sendUser")
public void sendToUser(Principal principal, String body) {
// 发送到用户和监听地址
simpMessagingTemplate.convertAndSendToUser(desUser,
"/queue/customer", message);//发送这个地址,供客户端连接
}
var s = new SockJS('/wsuser');
var stompClient = Stomp.over(s);
stompClient.connect({}, function() {
console.log('notice socket connected!');
stompClient.subscribe('/user/queue/customer', function(data) {
$('#receive').html(data.body);
});
});