Spring+Tomcat+WebSocket教程 附源码

前言

我们知道HTTP协议是无状态、无连接的,采用的是请求/响应模式,通信请求只能由客户端发起,服务器响应。这种请求/响应模式在客户端服务器需要持续的交互时候就显得很鸡肋,在HMTL5出来之前,要实现客户端服务器持续交互大多数都是通过AJAX轮询,但是轮询效率低,浪费带宽和服务器资源。因此WebSocket就发明出来了,WebSocket是HTML5提供的一种在单个TCP连接上进行全双工通信的协议。接下来我运用Spring和WebSocket实现一个简单的聊天功能,希望能对大家有帮助。

项目目录

image

添加依赖包

pom.xml

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
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需要编写以下几项。

  1. 配置WebSocket
    配置WebSocket的方式有2中,一种是编写配置类,另一种是编写配置文件(XML文件),配置WebSocket的作用是将WebSocket处理器、拦截器添加到注册中心,这里我使用的是配置类来配置。
    WebSocketConfig.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package 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连接。
  1. 编写处理器
    WebSocketHandler.java
    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
    package 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事件时,就会调用相事件相应的方法,这里我自定义了一个发送信息给指定用户的方法sendMessageToUserWebSocketSession是WebSocket的抽象,WebSocketSession就像是连接服务器和客户端之间的一条专属通道,一个WebSocketSession对应一个用户,WebSocket的操作都是基于这个WebSocketSession进行的。

  1. 编写拦截器
    WebSocketInterceptor.java
    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
    package 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关联起来。

  1. 编写Spring MVC控制器
    IndexController.java
    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
    package 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是用于处理客户端请求用的。

  1. 编写前端 测试WebSocket的建立和发信息
    用于新建WebSocket连接,其中用UID来表示WebSocket连接
    register.jsp
    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
    <%@ 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.jsp

1
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>

  1. 运行测试
    分别建立UID为1、2的WebSocket连接。
    image
    image

给UID为1的WebSocket发送信息
image
image

给UID为2的WebSocket发送信息
image
image

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都是定义了标准,而由各个厂商根据这个标准去实现自己的框架。

完整源代码:https://github.com/xue8/Java-Demo/tree/master/nChat