前言
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
相关阅读: 简单易懂理解WebSocket 轮询和长连接的优缺点
1、Web QQ项目结构
2、pom.xml依赖
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.scb</groupId>
<artifactId>websocketdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>websocketdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3、因为使用JSP作为视图,所以在application.yml中进行配置视图解析器
spring:
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
4、创建WebSocketConfig配置类,配置ServerEndpointExporter为bean,放入Spring IOC容器中。ServerEndpointExporter对象用来定义WebSocket服务器的端点,这样客户端就能请求服务器的端点了。
package com.scb.websocketdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
//创建服务器端点
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
有了这个bean,就可以使用@ServerEndpoint 定义一个端点服务类。在这个端点服务类中,还能定义WebSocket的打开、关闭、错误和发送消息的方法,如下所示:
5、定义WebSocket服务端站点
package com.scb.websocketdemo.service;
import org.springframework.stereotype.Service;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/ws")
@Service
public class WebSocketServiceImpl {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount=0;
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServiceImpl对象。
// 若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet<WebSocketServiceImpl> webSocketSet=new CopyOnWriteArraySet<>();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 发送消息
* @param message 客户端消息
* @throws IOException
*/
private void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
}
/**
*
* @return 返回在线人数
*/
private static synchronized int getOnlineCount(){
return onlineCount;
}
/**
* 当连接人数增加时
*/
private static synchronized void addOnlineCount(){
WebSocketServiceImpl.onlineCount++;
}
/**
* 当连接人数减少时
*/
private static synchronized void subOnlineCount(){
WebSocketServiceImpl.onlineCount--;
}
/**
* 连接建立成功调用的方法
* @param session
*/
@OnOpen
public void onOpen(Session session){
this.session=session;
webSocketSet.add(this);
addOnlineCount();
System.out.println("New connection join. Now count is "+getOnlineCount());
try{
sendMessage("has new connection join");
}catch (IOException e){
System.out.println(e);
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove(this);
subOnlineCount();
System.out.println("A connection has closed. Now count is "+getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session
*/
@OnMessage
public void onMessage(String message,Session session){
System.out.println("Message from client:"+message);
// 群发消息(首先,服务器接收到客户端的message,然后在这里遍历所有的客户端WebSocketServiceImpl对象,群发消息给他们。)
for(WebSocketServiceImpl item:webSocketSet){
try{
item.sendMessage(message);
}catch (IOException e){
e.printStackTrace();
}
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session,Throwable error){
System.out.println("Error");
error.printStackTrace();
}
}
@ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址。
onOpen 和 onClose 方法分别被@OnOpen和@OnClose 所注解。这两个注解的作用不言自明:他们定义了当一个新用户连接和断开的时候所调用的方法。
onMessage 方法被@OnMessage所注解。这个注解定义了当服务器接收到客户端发送的消息时所调用的方法。注意:这个方法可能包含一个javax.websocket.Session可选参数(在我们的例子里就是session参数)。如果有这个参数,容器将会把当前发送消息客户端的连接Session注入进去。
@OnError注解是标注客户端请求WebSocket服务端点发生异常时的调用方法。
注意:在onMessage方法中,我们通过轮询对所有的客户端连接都给予发送信息,所以当一个客户端发送消息时,所有的客户端连接都会接收到消息。但是有时候可能只是需要发送给特定的用户,则需要得到用户的信息,可以通过以下方式得到,然后在发送给特定用户。
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session
*/
@OnMessage
public void onMessage(String message,Session session){
System.out.println("Message from client:"+message);
// 群发消息(首先,服务器接收到客户端的message,然后在这里遍历所有的客户端WebSocketServiceImpl对象,群发消息给他们。)
for(WebSocketServiceImpl item:webSocketSet){
try{
/**
* 获取当前用户名称
*/
String userName=item.session.getUserPrincipal().getName();
System.out.println("UserName:"+userName);
item.sendMessage(message);
}catch (IOException e){
e.printStackTrace();
}
}
}
6、WebSocket页面开发
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<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>
test WebSocket endpoint
<br />
<input id="message" type="text" />
<button onclick="sendMessage()">send message</button>
<button onclick="closeWebSocket()">close websocket connection</button>
<div id="context"></div>
</body>
</html>
这段代码中定义了一个文本框和两个按钮和一个div。其中文本框是给予用户输入消息的,两个按钮一个是发送消息,另一个是关闭WebSocket连接。JavaScript则是引入了JQuery和自定义的一个脚本。代码如下:
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连接
// 防止连接还没断开就关闭窗口,srever端会抛出异常
window.onbeforeunload=function (ev) {
websocket.close();
}
// 将消息显示在id为context的区域内
function appendMessage(message){
var context=$("#context").append("<br/>"+message);
}
// 关闭连接
function closeWebSocket(){
websocket.close();
}
// 发送消息
function sendMessage(){
var message=$("#message").val();
websocket.send(message);
}
7、WebSocket控制器
package com.scb.websocketdemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
// 跳转到websocket界面
@GetMapping("/index")
public String websocket(){
return "websocket";
}
}
这样,当我们运行项目,访问http://localhost:8080/websocket/index 时,就可以打开WebSocket连接了。
8、结果截图
这里打开两个页面,访问http://localhost:8080/websocket/index
服务端控制台此时截图如下:
当我们在其中一个客户端发送消息时:
此时服务端控制台输出如下:
后记
目前很多浏览器已经实现了WebSocket协议,但是依然存在很多浏览器没有实现该协议,为了兼容那些没有实现该协议的浏览器,往往还需要通过STOMP协议来完成这些兼容,有关STOMP协议的内容在下一章,我们在继续讨论。
STOMP实践:点对点和广播通信(系统推送公告和用户聊天功能)