基于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引入必要的库
 <?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lee</groupId>
<artifactId>websocket</artifactId>
<name>maven-spring-websocket-01</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version> <properties> <java.version>1.7</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring.version>4.0.0.RELEASE</spring.version> <junit.version>4.11</junit.version> <!-- Logging -->
<logback.version>1.0.13</logback.version>
<slf4j.version>1.7.7</slf4j.version>
</properties> <dependencies>
<!--spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency> <!-- jstl -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency> <!--spring测试框架 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency> <!--spring数据库操作库 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency> <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency> <!--spring websocket库 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring.version}</version>
</dependency> <!--jackson用于json操作 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.3.0</version>
</dependency> <dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.2</version>
</dependency> <!-- Logging with SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency> <!--使用阿里的连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.4</version>
</dependency> <!--mysql connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency> </dependencies> <build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build> </project>

主要结构

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

HandshakeInterceptor.java

 package com.lee.websocket;

 import java.util.Map;

 import javax.servlet.http.HttpSession;

 import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler; public class HandshakeInterceptor implements org.springframework.web.socket.server.HandshakeInterceptor { //进入hander之前的拦截
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; String clientName = (String)servletRequest.getServletRequest().getParameter("name");
System.out.println(clientName); HttpSession session = servletRequest.getServletRequest().getSession(true);
// String userName = "lee";
if (session != null) {
//使用userName区分WebSocketHandler,以便定向发送消息
// String clientName = (String) session.getAttribute("WEBSOCKET_USERNAME");
map.put("WEBSOCKET_USERNAME", clientName);
}
}
return true;
} @Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) { } }

HomeController.java

 package com.lee.websocket;

 import java.text.DateFormat;
import java.util.Date;
import java.util.Locale; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; /**
* Handles requests for the application home page.
*/
@Controller
public class HomeController { private static final Logger logger = LoggerFactory.getLogger(HomeController.class); /**
* Simply selects the home view to render by returning its name.
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Locale locale, Model model) {
logger.info("Welcome home! The client locale is {}.", locale); Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale); String formattedDate = dateFormat.format(date); model.addAttribute("serverTime", formattedDate ); return "home";
} @RequestMapping(value = "/chat", method = RequestMethod.GET)
public String chat(Locale locale, Model model) {
return "chat";
} }

WebSocketConfig.java

 package com.lee.websocket;

 import org.springframework.context.annotation.Configuration;
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
@EnableWebSocket//开启websocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new WebSocketHander(),"/echo").addInterceptors(new HandshakeInterceptor()); //支持websocket 的访问链接
registry.addHandler(new WebSocketHander(),"/sockjs/echo").addInterceptors(new HandshakeInterceptor()).withSockJS(); //不支持websocket的访问链接
}
}

WebSocketHander.java

 package com.lee.websocket;

 import java.io.IOException;
import java.util.ArrayList; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession; public class WebSocketHander implements WebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(WebSocketHander.class); private static final ArrayList<WebSocketSession> users = new ArrayList<>(); //初次链接成功执行
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.debug("链接成功......");
users.add(session);
String userName = (String) session.getHandshakeAttributes().get("WEBSOCKET_USERNAME");
if(userName!= null){
session.sendMessage(new TextMessage("欢迎来到Nathan的聊天室,我们开始聊天吧!~"));
}
} //接受消息处理消息
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> webSocketMessage) throws Exception {
String clientName = (String) session.getHandshakeAttributes().get("WEBSOCKET_USERNAME"); clientName = "<a onclick='changeChater(this)'>" + clientName + "</a>"; String msg = webSocketMessage.getPayload().toString();
String charter = ""; String msgs[] = msg.split("\\|");
if (msgs.length > 1) {
msg = msgs[1];
charter = msgs[0];
sendMessageToUser(charter, new TextMessage(clientName + " 悄悄地对你说 :" + msg));
} else {
sendMessageToUsers(new TextMessage(clientName + " 说:" + msg));
} } @Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
if(webSocketSession.isOpen()){
webSocketSession.close();
}
logger.debug("链接出错,关闭链接......");
users.remove(webSocketSession);
} @Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
logger.debug("链接关闭......" + closeStatus.toString());
users.remove(webSocketSession);
} @Override
public boolean supportsPartialMessages() {
return false;
} /**
* 给所有在线用户发送消息
*
* @param message
*/
public void sendMessageToUsers(TextMessage message) {
for (WebSocketSession user : users) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} /**
* 给某个用户发送消息
*
* @param userName
* @param message
*/
public void sendMessageToUser(String userName, TextMessage message) {
for (WebSocketSession user : users) {
if (user.getHandshakeAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
}
Person.java
 package com.lee.websocket.entity;

 public class Person {

     private int age;
private String name;
private String sex; public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
} }

chat.jsp

 <%@ page contentType="text/html; charset=utf-8" language="java" %>
<html>
<head lang="en">
<meta charset="UTF-8">
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<!-- 新 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- 可选的Bootstrap主题文件(一般不用引入) -->
<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<!--<script type="text/javascript" src="js/jquery-1.7.2.js"></script>-->
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<title>webSocket测试</title>
<script type="text/javascript">
var chater; $(function(){ var websocket;
function connectServer() {
var clientName = $("#client_name").val();
if ("WebSocket" in window) {
websocket = new WebSocket("ws://127.0.0.1:8080/websocket/echo?name=" + clientName);
} else if ("MozWebSocket" in window) {
alert("MozWebSocket");
websocket = new MozWebSocket("ws://echo");
} else {
alert("SockJS");
websocket = new SockJS("http://127.0.0.1:8080/websocket/sockjs/echo");
}
} // websocket.onopen = function (evnt) {
// $("#tou").html("链接服务器成功!")
// };
// websocket.onmessage = function (evnt) {
// $("#msg").html($("#msg").html() + "<br/>" + evnt.data);
// };
// websocket.onerror = function (evnt) {
// };
// websocket.onclose = function (evnt) {
// $("#tou").html("与服务器断开了链接!")
// } $("#conncet_server").bind("click", function() {
connectServer(); websocket.onopen = function (evnt) {
$("#tou").html("链接服务器成功!")
};
websocket.onmessage = function (evnt) {
$("#msg").html($("#msg").html() + "<br/>" + evnt.data);
};
websocket.onerror = function (evnt) {
};
websocket.onclose = function (evnt) {
$("#tou").html("与服务器断开了链接!")
}
}); $("#send").bind("click", function() {
send();
}); function send(){
if (websocket != null) {
var message = document.getElementById("message").value; if ($.trim(chater) != "") {
message = chater + "|" + message;
} websocket.send(message);
} else {
alert("未与服务器链接.");
}
}
}); function changeChater(e) {
chater = $(e).html();
alert("您将和" + chater + "进行聊天...");
}
</script> </head>
<body> <div class="page-header" id="tou">webSocket及时聊天Demo程序</div>
<div class="well" id="msg"></div>
<div class="col-lg">
<div class="input-group">
<input type="text" class="form-control" placeholder="请输入用户名..." id="client_name">
<span class="input-group-btn">
<button class="btn btn-default" type="button" id="conncet_server">连接服务器</button>
</span>
</div>
</div> <br/> <div class="col-lg">
<div class="input-group">
<input type="text" class="form-control" placeholder="发送信息..." id="message">
<span class="input-group-btn">
<button class="btn btn-default" type="button" id="send">发送</button>
</span>
</div>
</div>
</body> </html>
 
有兴趣的朋友可以关注github地址:https://github.com/leechenxiang/maven-spring-websocket-01
上一篇:Entity Framework在不同数据库下的配置


下一篇:advertisingIdentifier