基于Spring 4.0 的 Web Socket 聊天室/游戏服务端简单架构

在现在很多业务场景(比如聊天室),又或者是手机端的一些online游戏,都需要做到实时通信,那怎么来进行双向通信呢,总不见得用曾经很破旧的ajax每隔10秒或者每隔20秒来请求吧,我的天呐(基于Spring 4.0 的 Web Socket 聊天室/游戏服务端简单架构),这尼玛太坑了

跟webservice来相比,Web Socket可以做到保持长连接,或者说强连接,一直握手存在两端可以互相发送消息互相收到消息,而webservice是一次性的,你要我响应就必须要请求我一次(黄盖:“请鞭挞我吧!”)

注:浏览器需要使用高版本的chrome或者Firefox,Tomcat使用8

先来了解一下基本概念

一、WebSocket是HTML5出的,是一种协议,也就是说原版的HTTP协议没有变化的,又或者说这两者压根就是不一样的东西,HTTP本身就不支持强连接
二、Websocket是什么样的协议,具体有什么优点
首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
举个栗子吧,简单来说
1) HTTP的生命周期通过Request来界定,也就是一个Request 对应一个Response,或者多个Request 对应多个Response,
也就是说request对应的response数量是恒定不变的。而且这个response也是被动的,不能主动发起,必须有request才会有response
 
那么Websocket究竟是啥玩意呢
首先Websocket是基于HTTP协议的,或者说引用了HTTP的协议来完成一小部分的握手
简单来说,客服的发起请求到服务端,服务端找到对应的小弟(服务助理),找到好,这个小弟就会一直和老大保持联系,为老大服务

三、Websocket的作用
曾经接触WebSocket之前,我接触过ajax轮询以及long poll ,先来说说这2个概念,因为至今还有一些小项目是这么做的
ajax轮询:
原理非常简单,JS控制让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息,有的话就响应给客户端
以此循环获取后端的数据,同时浏览器又不需要刷新
简单的例子:OA首页显示流程,每个几秒刷新看看有没有需要处理的新流程出现

long poll:
long poll 其实原理跟 ajax轮询 差不多,都是采用循环的方式,不过采取的手段不太友好,是阻塞模型,客户端发起请求后,如果没响应,就一直不返回Response,直到有响应才返回,返回完之后,客户端再次建立连接,如此循环往复不亦乐乎。。。

从上面这两种方式看出他们都是在不断地建立HTTP连接,然后等待服务器处理,这样显得十分被动
 
那么缺点也随之而来:
这两种形式非常消耗资源,性能也不不好

好!接下来说说Websocket
Websocket的出现,使得资源不需要像之前那种方式那么浪费
它非常主动,服务端就可以主动推送信息给客户端
所以,只需建立一次HTTP请求,就可以做到源源不断的信息传送了。(就像你在手机上玩ol游戏,一开始建立连接后,你就一直保持在线,除非你断线再连)


下面贴出我的代码片段以及github地址
功能点:

spring websocket chating room
使用spring websocket实现聊天室基本功能
1.群发消息给所有人
2.悄悄话给某个人

效果:
基于Spring 4.0 的 Web Socket 聊天室/游戏服务端简单架构
主要代码:
pom.xml引入必要的库
  1 <?xml version="1.0" encoding="UTF-8"?>
  2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  4     <modelVersion>4.0.0</modelVersion>
  5     <groupId>com.lee</groupId>
  6     <artifactId>websocket</artifactId>
  7     <name>maven-spring-websocket-01</name>
  8     <packaging>war</packaging>
  9     <version>1.0.0-BUILD-SNAPSHOT</version>
 10 
 11     <properties>
 12         
 13         <java.version>1.7</java.version>
 14         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 15         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 16         
 17         <spring.version>4.0.0.RELEASE</spring.version>
 18         
 19         <junit.version>4.11</junit.version>
 20         
 21         <!-- Logging -->
 22         <logback.version>1.0.13</logback.version>
 23         <slf4j.version>1.7.7</slf4j.version>
 24     </properties>
 25 
 26     <dependencies>
 27         <!--spring MVC -->
 28         <dependency>
 29             <groupId>org.springframework</groupId>
 30             <artifactId>spring-core</artifactId>
 31             <version>${spring.version}</version>
 32         </dependency>
 33 
 34         <dependency>
 35             <groupId>org.springframework</groupId>
 36             <artifactId>spring-web</artifactId>
 37             <version>${spring.version}</version>
 38         </dependency>
 39 
 40         <dependency>
 41             <groupId>org.springframework</groupId>
 42             <artifactId>spring-webmvc</artifactId>
 43             <version>${spring.version}</version>
 44         </dependency>
 45 
 46         <!-- jstl -->
 47         <dependency>
 48             <groupId>jstl</groupId>
 49             <artifactId>jstl</artifactId>
 50             <version>1.2</version>
 51         </dependency>
 52 
 53         <!--spring测试框架 -->
 54         <dependency>
 55             <groupId>org.springframework</groupId>
 56             <artifactId>spring-test</artifactId>
 57             <version>${spring.version}</version>
 58             <scope>test</scope>
 59         </dependency>
 60 
 61         <!--spring数据库操作库 -->
 62         <dependency>
 63             <groupId>org.springframework</groupId>
 64             <artifactId>spring-jdbc</artifactId>
 65             <version>${spring.version}</version>
 66         </dependency>
 67 
 68         <dependency>
 69             <groupId>junit</groupId>
 70             <artifactId>junit</artifactId>
 71             <version>4.8.2</version>
 72             <scope>test</scope>
 73         </dependency>
 74 
 75         <!--spring websocket库 -->
 76         <dependency>
 77             <groupId>org.springframework</groupId>
 78             <artifactId>spring-websocket</artifactId>
 79             <version>${spring.version}</version>
 80         </dependency>
 81         <dependency>
 82             <groupId>org.springframework</groupId>
 83             <artifactId>spring-messaging</artifactId>
 84             <version>${spring.version}</version>
 85         </dependency>
 86 
 87         <!--jackson用于json操作 -->
 88         <dependency>
 89             <groupId>com.fasterxml.jackson.core</groupId>
 90             <artifactId>jackson-databind</artifactId>
 91             <version>2.3.0</version>
 92         </dependency>
 93 
 94         <dependency>
 95             <groupId>commons-fileupload</groupId>
 96             <artifactId>commons-fileupload</artifactId>
 97             <version>1.2.2</version>
 98         </dependency>
 99         <dependency>
100             <groupId>commons-io</groupId>
101             <artifactId>commons-io</artifactId>
102             <version>2.2</version>
103         </dependency>
104         
105         <!-- Logging with SLF4J & LogBack -->
106         <dependency>
107             <groupId>org.slf4j</groupId>
108             <artifactId>slf4j-api</artifactId>
109             <version>${slf4j.version}</version>
110             <scope>compile</scope>
111         </dependency>
112         <dependency>
113             <groupId>ch.qos.logback</groupId>
114             <artifactId>logback-classic</artifactId>
115             <version>${logback.version}</version>
116             <scope>runtime</scope>
117         </dependency>
118         
119         <!--使用阿里的连接池 -->
120         <dependency>
121             <groupId>com.alibaba</groupId>
122             <artifactId>druid</artifactId>
123             <version>1.0.4</version>
124         </dependency>
125         
126         <!--mysql connector -->
127         <dependency>
128             <groupId>mysql</groupId>
129             <artifactId>mysql-connector-java</artifactId>
130             <version>5.1.29</version>
131         </dependency>
132 
133     </dependencies>
134     
135     <build>
136         <plugins>
137             <plugin>
138                 <groupId>org.apache.maven.plugins</groupId>
139                 <artifactId>maven-compiler-plugin</artifactId>
140                 <configuration>
141                     <source>1.7</source>
142                     <target>1.7</target>
143                 </configuration>
144             </plugin>
145         </plugins>
146     </build>
147     
148 </project>

主要结构

基于Spring 4.0 的 Web Socket 聊天室/游戏服务端简单架构

HandshakeInterceptor.java

 1 package com.lee.websocket;
 2 
 3 import java.util.Map;
 4 
 5 import javax.servlet.http.HttpSession;
 6 
 7 import org.springframework.http.server.ServerHttpRequest;
 8 import org.springframework.http.server.ServerHttpResponse;
 9 import org.springframework.http.server.ServletServerHttpRequest;
10 import org.springframework.web.socket.WebSocketHandler;
11 
12 public class HandshakeInterceptor implements org.springframework.web.socket.server.HandshakeInterceptor {
13 
14     //进入hander之前的拦截
15     @Override
16     public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
17         if (request instanceof ServletServerHttpRequest) {
18             ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
19             
20             String clientName = (String)servletRequest.getServletRequest().getParameter("name");
21             System.out.println(clientName);
22             
23             HttpSession session = servletRequest.getServletRequest().getSession(true);
24 //            String userName = "lee";
25             if (session != null) {
26                 //使用userName区分WebSocketHandler,以便定向发送消息
27 //                String clientName = (String) session.getAttribute("WEBSOCKET_USERNAME");
28                 map.put("WEBSOCKET_USERNAME", clientName);
29             }
30         }
31         return true;
32     }
33 
34     @Override
35     public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
36 
37     }
38     
39 }

 

HomeController.java

 1 package com.lee.websocket;
 2 
 3 import java.text.DateFormat;
 4 import java.util.Date;
 5 import java.util.Locale;
 6 
 7 import org.slf4j.Logger;
 8 import org.slf4j.LoggerFactory;
 9 import org.springframework.stereotype.Controller;
10 import org.springframework.ui.Model;
11 import org.springframework.web.bind.annotation.RequestMapping;
12 import org.springframework.web.bind.annotation.RequestMethod;
13 
14 /**
15  * Handles requests for the application home page.
16  */
17 @Controller
18 public class HomeController {
19     
20     private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
21     
22     /**
23      * Simply selects the home view to render by returning its name.
24      */
25     @RequestMapping(value = "/", method = RequestMethod.GET)
26     public String home(Locale locale, Model model) {
27         logger.info("Welcome home! The client locale is {}.", locale);
28         
29         Date date = new Date();
30         DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
31         
32         String formattedDate = dateFormat.format(date);
33         
34         model.addAttribute("serverTime", formattedDate );
35         
36         return "home";
37     }
38     
39     @RequestMapping(value = "/chat", method = RequestMethod.GET)
40     public String chat(Locale locale, Model model) {
41         return "chat";
42     }
43     
44 }

 

WebSocketConfig.java

 1 package com.lee.websocket;
 2 
 3 import org.springframework.context.annotation.Configuration;
 4 import org.springframework.web.socket.config.annotation.EnableWebSocket;
 5 import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
 6 import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
 7 
 8 @Configuration
 9 @EnableWebSocket//开启websocket
10 public class WebSocketConfig implements WebSocketConfigurer {
11     @Override
12     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
13         registry.addHandler(new WebSocketHander(),"/echo").addInterceptors(new HandshakeInterceptor()); //支持websocket 的访问链接
14         registry.addHandler(new WebSocketHander(),"/sockjs/echo").addInterceptors(new HandshakeInterceptor()).withSockJS(); //不支持websocket的访问链接
15     }
16 }


WebSocketHander.java

  1 package com.lee.websocket;
  2 
  3 import java.io.IOException;
  4 import java.util.ArrayList;
  5 
  6 import org.slf4j.Logger;
  7 import org.slf4j.LoggerFactory;
  8 import org.springframework.web.socket.CloseStatus;
  9 import org.springframework.web.socket.TextMessage;
 10 import org.springframework.web.socket.WebSocketHandler;
 11 import org.springframework.web.socket.WebSocketMessage;
 12 import org.springframework.web.socket.WebSocketSession;
 13 
 14 public class WebSocketHander implements WebSocketHandler {
 15     private static final Logger logger = LoggerFactory.getLogger(WebSocketHander.class);
 16 
 17     private static final ArrayList<WebSocketSession> users = new ArrayList<>();
 18 
 19     //初次链接成功执行
 20     @Override
 21     public void afterConnectionEstablished(WebSocketSession session) throws Exception {
 22         logger.debug("链接成功......");
 23         users.add(session);
 24         String userName = (String) session.getHandshakeAttributes().get("WEBSOCKET_USERNAME");
 25         if(userName!= null){
 26             session.sendMessage(new TextMessage("欢迎来到Nathan的聊天室,我们开始聊天吧!~"));
 27         }
 28     }
 29 
 30     //接受消息处理消息
 31     @Override
 32     public void handleMessage(WebSocketSession session, WebSocketMessage<?> webSocketMessage) throws Exception {
 33         String clientName = (String) session.getHandshakeAttributes().get("WEBSOCKET_USERNAME");
 34         
 35         clientName = "<a onclick='changeChater(this)'>" + clientName + "</a>";
 36             
 37         String msg = webSocketMessage.getPayload().toString();
 38         String charter = "";
 39         
 40         String msgs[] = msg.split("\\|");
 41         if (msgs.length > 1) {
 42             msg = msgs[1];
 43             charter = msgs[0];
 44             sendMessageToUser(charter, new TextMessage(clientName + "  悄悄地对你说 :" + msg));
 45         } else {
 46             sendMessageToUsers(new TextMessage(clientName + "  说:" + msg));
 47         }
 48         
 49     }
 50 
 51     @Override
 52     public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
 53         if(webSocketSession.isOpen()){
 54             webSocketSession.close();
 55         }
 56         logger.debug("链接出错,关闭链接......");
 57         users.remove(webSocketSession);
 58     }
 59 
 60     @Override
 61     public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
 62         logger.debug("链接关闭......" + closeStatus.toString());
 63         users.remove(webSocketSession);
 64     }
 65 
 66     @Override
 67     public boolean supportsPartialMessages() {
 68         return false;
 69     }
 70     
 71     /**
 72      * 给所有在线用户发送消息
 73      *
 74      * @param message
 75      */
 76     public void sendMessageToUsers(TextMessage message) {
 77         for (WebSocketSession user : users) {
 78             try {
 79                 if (user.isOpen()) {
 80                     user.sendMessage(message);
 81                 }
 82             } catch (IOException e) {
 83                 e.printStackTrace();
 84             }
 85         }
 86     }
 87 
 88     /**
 89      * 给某个用户发送消息
 90      *
 91      * @param userName
 92      * @param message
 93      */
 94     public void sendMessageToUser(String userName, TextMessage message) {
 95         for (WebSocketSession user : users) {
 96             if (user.getHandshakeAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
 97                 try {
 98                     if (user.isOpen()) {
 99                         user.sendMessage(message);
100                     }
101                 } catch (IOException e) {
102                     e.printStackTrace();
103                 }
104                 break;
105             }
106         }
107     }
108 }

 

Person.java
 1 package com.lee.websocket.entity;
 2 
 3 public class Person {
 4 
 5     private int age;
 6     private String name;
 7     private String sex;
 8     
 9     public int getAge() {
10         return age;
11     }
12     public void setAge(int age) {
13         this.age = age;
14     }
15     public String getName() {
16         return name;
17     }
18     public void setName(String name) {
19         this.name = name;
20     }
21     public String getSex() {
22         return sex;
23     }
24     public void setSex(String sex) {
25         this.sex = sex;
26     }
27     
28 }

chat.jsp

  1 <%@ page contentType="text/html; charset=utf-8" language="java" %>
  2 <html>
  3 <head lang="en">
  4     <meta charset="UTF-8">
  5     <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
  6     <!-- 新 Bootstrap 核心 CSS 文件 -->
  7     <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
  8     <!-- 可选的Bootstrap主题文件(一般不用引入) -->
  9     <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
 10     <!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
 11     <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
 12     <!--<script type="text/javascript" src="js/jquery-1.7.2.js"></script>-->
 13     <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
 14     <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
 15     <title>webSocket测试</title>
 16     <script type="text/javascript">
 17         var chater;
 18         
 19         $(function(){
 20             
 21             var websocket;
 22             function connectServer() {
 23                 var clientName = $("#client_name").val();
 24                 if ("WebSocket" in window) {
 25                     websocket = new WebSocket("ws://127.0.0.1:8080/websocket/echo?name=" + clientName);
 26                 } else if ("MozWebSocket" in window) {
 27                     alert("MozWebSocket");
 28                     websocket = new MozWebSocket("ws://echo");
 29                 } else {
 30                     alert("SockJS");
 31                     websocket = new SockJS("http://127.0.0.1:8080/websocket/sockjs/echo");
 32                 }
 33             }
 34             
 35 //             websocket.onopen = function (evnt) {
 36 //                 $("#tou").html("链接服务器成功!")
 37 //             };
 38 //             websocket.onmessage = function (evnt) {
 39 //                 $("#msg").html($("#msg").html() + "<br/>" + evnt.data);
 40 //             };
 41 //             websocket.onerror = function (evnt) {
 42 //             };
 43 //             websocket.onclose = function (evnt) {
 44 //                 $("#tou").html("与服务器断开了链接!")
 45 //             }
 46             
 47             $("#conncet_server").bind("click", function() {
 48                 connectServer();
 49                 
 50                 websocket.onopen = function (evnt) {
 51                     $("#tou").html("链接服务器成功!")
 52                 };
 53                 websocket.onmessage = function (evnt) {
 54                     $("#msg").html($("#msg").html() + "<br/>" + evnt.data);
 55                 };
 56                 websocket.onerror = function (evnt) {
 57                 };
 58                 websocket.onclose = function (evnt) {
 59                     $("#tou").html("与服务器断开了链接!")
 60                 }
 61             });
 62             
 63             $("#send").bind("click", function() {
 64                 send();
 65             });
 66             
 67             function send(){
 68                 if (websocket != null) {
 69                     var message = document.getElementById("message").value;
 70                     
 71                     if ($.trim(chater) != "") {
 72                         message = chater + "|" + message;
 73                     }
 74                     
 75                     websocket.send(message);
 76                 } else {
 77                     alert("未与服务器链接.");
 78                 }
 79             }
 80         });
 81         
 82         function changeChater(e) {
 83             chater = $(e).html();
 84             alert("您将和" + chater + "进行聊天...");
 85         }
 86     </script>
 87 
 88 </head>
 89 <body>
 90 
 91     <div class="page-header" id="tou">webSocket及时聊天Demo程序</div>
 92     <div class="well" id="msg"></div>
 93     <div class="col-lg">
 94         <div class="input-group">
 95             <input type="text" class="form-control" placeholder="请输入用户名..." id="client_name"> 
 96             <span class="input-group-btn">
 97                 <button class="btn btn-default" type="button" id="conncet_server">连接服务器</button>
 98             </span>
 99         </div>
100     </div>
101     
102     <br/>
103     
104     <div class="col-lg">
105         <div class="input-group">
106             <input type="text" class="form-control" placeholder="发送信息..." id="message"> 
107             <span class="input-group-btn">
108                 <button class="btn btn-default" type="button" id="send">发送</button>
109             </span>
110         </div>
111     </div>
112 </body>
113 
114 </html>

 

 
有兴趣的朋友可以关注github地址:https://github.com/leechenxiang/maven-spring-websocket-01
上一篇:《C++必知必会》读书笔记3


下一篇:《Oracle PL/SQL必知必会》——2.2 起步