前言
我们知道HTTP
协议是无状态、无连接的,采用的是请求/响应模式,通信请求只能由客户端发起,服务器响应。这种请求/响应模式在客户端服务器需要持续的交互时候就显得很鸡肋,在HMTL5出来之前,要实现客户端服务器持续交互大多数都是通过AJAX轮询,但是轮询效率低,浪费带宽和服务器资源。因此WebSocket就发明出来了,WebSocket是HTML5提供的一种在单个TCP连接上进行全双工通信的协议。接下来我运用Spring和WebSocket实现一个简单的聊天功能,希望能对大家有帮助。
项目目录
添加依赖包
pom.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118<?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>groupId</groupId>
<artifactId>nChat</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--Sping核心依赖-->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.3.RELEASE</version>
<scope>test</scope>
</dependency>
<!--Mybatis依赖-->
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-messaging -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.1.2.RELEASE</version>
</dependency>
<!--MySQL连接驱动-->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
WebSocket实现
Java实现WebSocket的方式很多,不同厂商实现WebSocket的方式大径相同。
Spring实现WebSocket
Spring实现WebSocket,需要先添加Spring的对WebSocket支持的依赖1
2
3
4
5
6
7
8
9
10
11
12
13<!-- https://mvnrepository.com/artifact/org.springframework/spring-messaging -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.1.2.RELEASE</version>
</dependency>
在Java中导入Spring WebSocket的包import org.springframework.web.socket.*;
,Spring实现WebSocket需要编写以下几项。
- 配置WebSocket
配置WebSocket的方式有2中,一种是编写配置类,另一种是编写配置文件(XML文件),配置WebSocket的作用是将WebSocket处理器、拦截器添加到注册中心,这里我使用的是配置类来配置。
WebSocketConfig.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.nChat.websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
//这个网址是用于websocket连接的建立 通信用的
webSocketHandlerRegistry
.addHandler(new WebSocketHandler(), "/ws/socketServer")
.addInterceptors(new WebSocketInterceptor())
.setAllowedOrigins("*");
}
}
三个注解的作用如下
- @Configuration注解:声明这个类为配置类(相当于web.xml配置文件中的
)配置Spring容器应用上下文,即项目启动的时候会加载这个配置类。 - @EnableWebMvc注解:开启Spring MVC,不加这个的话,在Controller的RequestMapping就失效,我也不知道为啥。
- @EnableWebSocket注解:开启WebSocket服务。
registerWebSocketHandlers
方法配置WebSocket入口、允许访问的域,注册WebSocket处理器、拦截器等,当请求访问/ws/socketServer
的时候,就会建立起WebSocket连接。
- 编写处理器
WebSocketHandler.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77package com.nChat.websocket;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class WebSocketHandler extends TextWebSocketHandler {
public static final Map<Integer,WebSocketSession> USER_SOCKET_SESSION_MAP;
static{
USER_SOCKET_SESSION_MAP = new HashMap<Integer, WebSocketSession>();
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
int uid = Integer.parseInt(session.getAttributes().get("WEBSOCKET_UID").toString());
//如果是新的用户连接 则将session保存在USER_SOCKET_SESSION_MAP中
if (USER_SOCKET_SESSION_MAP.get(uid) == null || !USER_SOCKET_SESSION_MAP.get(uid).isOpen()) {
USER_SOCKET_SESSION_MAP.put(uid, session);
}
super.afterConnectionEstablished(session);
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
super.handleMessage(session, message);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
}
@Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
super.handlePongMessage(session, message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
super.handleTransportError(session, exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
}
@Override
public boolean supportsPartialMessages() {
return super.supportsPartialMessages();
}
/**
* @description: 给指定用户发送信息
* @param: [uid, message]
* @return: void
* @author: Xue 8
* @date: 2019/1/19
*/
public void sendMessageToUser(int uid, TextMessage message){
WebSocketSession session = USER_SOCKET_SESSION_MAP.get(uid);
if (session != null && session.isOpen()) {
try {
session.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
WebSocket处理器继承TextWebSocketHandler
(或BinaryWebSocketHandler
),在这里重写相应的方法和编写自己的业务代码,Spring在收到WebSocket事件时,就会调用相事件相应的方法,这里我自定义了一个发送信息给指定用户的方法sendMessageToUser
。WebSocketSession
是WebSocket的抽象,WebSocketSession就像是连接服务器和客户端之间的一条专属通道,一个WebSocketSession对应一个用户,WebSocket的操作都是基于这个WebSocketSession进行的。
- 编写拦截器
WebSocketInterceptor.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73package com.nChat.websocket;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import java.util.Collection;
import java.util.Map;
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {
public WebSocketInterceptor() {
super();
}
public WebSocketInterceptor(Collection<String> attributeNames) {
super(attributeNames);
}
@Override
public Collection<String> getAttributeNames() {
return super.getAttributeNames();
}
@Override
public void setCopyAllAttributes(boolean copyAllAttributes) {
super.setCopyAllAttributes(copyAllAttributes);
}
@Override
public boolean isCopyAllAttributes() {
return super.isCopyAllAttributes();
}
@Override
public void setCopyHttpSessionId(boolean copyHttpSessionId) {
super.setCopyHttpSessionId(copyHttpSessionId);
}
@Override
public boolean isCopyHttpSessionId() {
return super.isCopyHttpSessionId();
}
@Override
public void setCreateSession(boolean createSession) {
super.setCreateSession(createSession);
}
@Override
public boolean isCreateSession() {
return super.isCreateSession();
}
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
int uid = Integer.parseInt(servletServerHttpRequest.getServletRequest().getParameter("uid"));
System.out.println("coming " + uid);
if (uid != 0) {
//在这里拦截请求 在捂手前将uid保存到WebSocketSession中 让处理器WebSocketHandler根据这个uid进行操作
attributes.put("WEBSOCKET_UID", uid);
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
System.out.println("out");
super.afterHandshake(request, response, wsHandler, ex);
}
}
WebSocket拦截器继承HttpSessionHandshakeInterceptor
,在握手前后对请求进行拦截,在握手前将请求拦截,也就是当请求访问/ws/socketServer
的时候,会对请求拦截,可以获取到请求中的URL参数、请求头、协议等信息,然后将这些信息保存在WebSocketSession中,将用户和WebSocketSession关联起来。
- 编写Spring MVC控制器
IndexController.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45package com.nChat.controller;
import com.nChat.websocket.WebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.socket.TextMessage;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Controller
public class IndexController {
@Autowired
WebSocketHandler webSocketHandler;
@RequestMapping("/send")
public String send(HttpServletRequest request,
HttpServletResponse response){
return "send";
}
@RequestMapping("/doSend")
public String doSend(HttpServletRequest request,
HttpServletResponse response,
@RequestParam(value = "uid") int uid,
@RequestParam(value = "messages") String messages){
HttpSession session = request.getSession(true);
session.setAttribute("SESSION_USERNAME", uid);
webSocketHandler.sendMessageToUser(uid,new TextMessage(messages));
return "send";
}
@RequestMapping("/register")
public String register(HttpServletRequest request,
HttpServletResponse response){
return "register";
}
}
注意这里的@RequestMapping
和WebSocket配置类中的/ws/socketServer
区别,配置类中的/ws/socketServer
是用于客户端和服务器建立WebSocket连接用的,而Controller的@RequestMapping
是用于处理客户端请求用的。
- 编写前端 测试WebSocket的建立和发信息
用于新建WebSocket连接,其中用UID来表示WebSocket连接
register.jsp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57<%@ 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>register</title>
</head>
<body>
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/reconnecting-websocket/1.0.0/reconnecting-websocket.js"></script>
<script type="text/javascript">
var websocket = null;
function createWebSocket() {
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/ws/socketServer?uid=" + $("#uid").val());
console.log($("#uid").val())
}
else if ('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://localhost:8080/ws/socketServer?uid=" + $("#uid").val());
}
else {
websocket = new SockJS("http://localhost:8080/ws/socketServer?uid=" + $("#uid").val());
}
websocket.onopen = onOpen;
websocket.onmessage = onMessage;
websocket.onerror = onError;
websocket.onclose = onClose;
function onOpen(openEvt) {
//alert(openEvt.Data);
}
function onMessage(evt) {
alert(evt.data);
}
function onError() {
}
function onClose() {
}
window.close=function()
{
websocket.onclose();
}
}
</script>
请输入UID:<input rows="5" cols="10" id="uid" name="uid"></input>
<button onclick="createWebSocket();">建立WS连接</button>
</body>
</html>
用于发信息的页面,根据UID进行信息的发送
send.jsp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<%@ 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>
<body>
<h2>send messages</h2>
<body>
<form action="/doSend">
发送给谁:<input type="text" name="uid"/>
发送什么信息:<input type="text" name="messages"/>
<input type="submit" value="发送"/>
</form>
</body>
</body>
</html>
- 运行测试
分别建立UID为1、2的WebSocket连接。
给UID为1的WebSocket发送信息
给UID为2的WebSocket发送信息
Tomcat实现WebSocket
使用Tomcat实现WebSocket没有像Spring实现WebSocket那样繁琐,只需要编写一个处理器即可。
首先添加依赖1
2
3
4
5
6
7<!-- https://mvnrepository.com/artifact/javax.websocket/javax.websocket-api -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
然后编写处理类即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.nChat;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import net.sf.json.JSONObject;
@ServerEndpoint("/websocket/{username}")
public class WebSocket {
private static int onlineCount = 0;
private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
private Session session;
private String username;
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) throws IOException {
this.username = username;
this.session = session;
addOnlineCount();
clients.put(username, this);
System.out.println("已连接");
}
@OnClose
public void onClose() throws IOException {
clients.remove(username);
subOnlineCount();
}
@OnMessage
public void onMessage(String message) throws IOException {
JSONObject jsonTo = JSONObject.fromObject(message);
if (!jsonTo.get("To").equals("All")){
sendMessageTo("给一个人", jsonTo.get("To").toString());
}else{
sendMessageAll("给所有人");
}
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
public void sendMessageTo(String message, String To) throws IOException {
// session.getBasicRemote().sendText(message);
//session.getAsyncRemote().sendText(message);
for (WebSocket item : clients.values()) {
if (item.username.equals(To) )
item.session.getAsyncRemote().sendText(message);
}
}
public void sendMessageAll(String message) throws IOException {
for (WebSocket item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocket.onlineCount--;
}
public static synchronized Map<String, WebSocket> getClients() {
return clients;
}
}
Jetty实现WebSocket
这个好像不常见…这里就不演示如何配置了,有兴趣可以网上搜相关文章。
总结
WebSocket是HTML5提供的一种在单个TCP连接进行的全双工通讯协议,不用的厂商都可以根据WebSocket API去实现自己的WebSocket框架,比如Spring的WebSocket、Tomcat的WebSocket,我觉得WebSocket和Spring的WebSocket、Tomcat的WebSocket的关系就像JPA和hibernate、Mybatis的关系一样,WebSocket和JPA都是定义了标准,而由各个厂商根据这个标准去实现自己的框架。