Spring之WebSocket网页聊天以及服务器推送
https://blog.csdn.net/tanga842428/article/details/77140501
原文地址:http://www.xdemo.org/spring-websocket-comet/
Websocket简介 摘要自百度百科
1. WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。
2. 轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的有用数据可能只是一个很小的值,这样会占用很多的带宽。
3. 比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求
4. 在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送
5. 在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
5.1. Header
互相沟通的Header是很小的-大概只有 2 Bytes
5.2. Server Push
浏览器支持情况
Chrome | 4+ |
Firefox | 4+ |
Internet Explorer | 10+ |
Opera | 10+ |
Safari | 5+ |
服务器支持
jetty | 7.0.1+ |
tomcat | 7.0.27+ |
Nginx | 1.3.13+ |
resin | 4+ |
API
-
var ws = new WebSocket(“ws://echo.websocket.org”);
-
-
ws.onopen = function(){ws.send(“Test!”); };
-
//当有消息时,会自动调用此方法
-
ws.onmessage = function(evt){console.log(evt.data);ws.close();};
-
-
ws.onclose = function(evt){console.log(“WebSocketClosed!”);};
-
-
ws.onerror = function(evt){console.log(“WebSocketError!”);};
Demo简介
模拟了两个用户的对话,张三和李四,然后还有发送一个广播,即张三和李四都是可以接收到的,登录的时候分别选择张三和李四即可
Demo效果
Maven依赖
-
<dependency>
-
<groupId>com.fasterxml.jackson.core</groupId>
-
<artifactId>jackson-annotations</artifactId>
-
<version>2.3.0</version>
-
</dependency>
-
<dependency>
-
<groupId>com.fasterxml.jackson.core</groupId>
-
<artifactId>jackson-core</artifactId>
-
<version>2.3.1</version>
-
</dependency>
-
<dependency>
-
<groupId>com.fasterxml.jackson.core</groupId>
-
<artifactId>jackson-databind</artifactId>
-
<version>2.3.3</version>
-
</dependency>
-
<dependency>
-
<groupId>org.springframework</groupId>
-
<artifactId>spring-messaging</artifactId>
-
<version>4.0.5.RELEASE</version>
-
</dependency>
-
<dependency>
-
<groupId>org.springframework</groupId>
-
<artifactId>spring-websocket</artifactId>
-
<version>4.0.5.RELEASE</version>
-
</dependency>
-
<dependency>
-
<groupId>org.springframework</groupId>
-
<artifactId>spring-webmvc</artifactId>
-
<version>4.0.5.RELEASE</version>
-
</dependency>
-
<dependency>
-
<groupId>com.google.code.gson</groupId>
-
<artifactId>gson</artifactId>
-
<version>2.3.1</version>
-
</dependency>
-
<dependency>
-
<groupId>javax.servlet</groupId>
-
<artifactId>javax.servlet-api</artifactId>
-
<version>3.1.0</version>
-
<scope>provided</scope>
-
</dependency>
-
<dependency>
-
<groupId>junit</groupId>
-
<artifactId>junit</artifactId>
-
<version>3.8.1</version>
-
<scope>test</scope>
-
</dependency>
Web.xml,spring-mvc.xml,User.java请查看附件
WebSocket相关的类
WebSocketConfig,配置WebSocket的处理器(MyWebSocketHandler)和拦截器(HandShake)
-
package org.xdemo.example.websocket.websocket;
-
-
import javax.annotation.Resource;
-
-
import org.springframework.stereotype.Component;
-
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
-
import org.springframework.web.socket.config.annotation.EnableWebSocket;
-
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
-
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
-
-
/**
-
* WebScoket配置处理器
-
* @author Goofy
-
* @Date 2015年6月11日 下午1:15:09
-
*/
-
@Component
-
@EnableWebSocket
-
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
-
-
@Resource
-
MyWebSocketHandler handler;
-
-
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
-
registry.addHandler(handler, "/ws").addInterceptors(new HandShake());
-
-
registry.addHandler(handler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();
-
}
-
-
}
MyWebSocketHandler
-
package org.xdemo.example.websocket.websocket;
-
-
import java.io.IOException;
-
import java.text.SimpleDateFormat;
-
import java.util.Date;
-
import java.util.HashMap;
-
import java.util.Iterator;
-
import java.util.Map;
-
import java.util.Map.Entry;
-
-
import org.springframework.stereotype.Component;
-
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;
-
import org.xdemo.example.websocket.entity.Message;
-
-
import com.google.gson.Gson;
-
import com.google.gson.GsonBuilder;
-
-
/**
-
* Socket处理器
-
*
-
* @author Goofy
-
* @Date 2015年6月11日 下午1:19:50
-
*/
-
@Component
-
public class MyWebSocketHandler implements WebSocketHandler {
-
public static final Map<Long, WebSocketSession> userSocketSessionMap;
-
-
static {
-
userSocketSessionMap = new HashMap<Long, WebSocketSession>();
-
}
-
-
/**
-
* 建立连接后
-
*/
-
public void afterConnectionEstablished(WebSocketSession session)
-
throws Exception {
-
Long uid = (Long) session.getAttributes().get("uid");
-
if (userSocketSessionMap.get(uid) == null) {
-
userSocketSessionMap.put(uid, session);
-
}
-
}
-
-
/**
-
* 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理
-
*/
-
public void handleMessage(WebSocketSession session, WebSocketMessage
-
if(message.getPayloadLength()==0)return;
-
Message msg=new Gson().fromJson(message.getPayload().toString(),Message.class);
-
msg.setDate(new Date());
-
sendMessageToUser(msg.getTo(), new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
-
}
-
-
/**
-
* 消息传输错误处理
-
*/
-
public void handleTransportError(WebSocketSession session,
-
Throwable exception) throws Exception {
-
if (session.isOpen()) {
-
session.close();
-
}
-
Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap
-
.entrySet().iterator();
-
// 移除Socket会话
-
while (it.hasNext()) {
-
Entry<Long, WebSocketSession> entry = it.next();
-
if (entry.getValue().getId().equals(session.getId())) {
-
userSocketSessionMap.remove(entry.getKey());
-
System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
-
break;
-
}
-
}
-
}
-
-
/**
-
* 关闭连接后
-
*/
-
public void afterConnectionClosed(WebSocketSession session,
-
CloseStatus closeStatus) throws Exception {
-
System.out.println("Websocket:" + session.getId() + "已经关闭");
-
Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap
-
.entrySet().iterator();
-
// 移除Socket会话
-
while (it.hasNext()) {
-
Entry<Long, WebSocketSession> entry = it.next();
-
if (entry.getValue().getId().equals(session.getId())) {
-
userSocketSessionMap.remove(entry.getKey());
-
System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
-
break;
-
}
-
}
-
}
-
-
public boolean supportsPartialMessages() {
-
return false;
-
}
-
-
/**
-
* 给所有在线用户发送消息
-
*
-
* @param message
-
* @throws IOException
-
*/
-
public void broadcast(final TextMessage message) throws IOException {
-
Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap
-
.entrySet().iterator();
-
-
// 多线程群发
-
while (it.hasNext()) {
-
-
final Entry<Long, WebSocketSession> entry = it.next();
-
-
if (entry.getValue().isOpen()) {
-
// entry.getValue().sendMessage(message);
-
new Thread(new Runnable() {
-
-
public void run() {
-
try {
-
if (entry.getValue().isOpen()) {
-
entry.getValue().sendMessage(message);
-
}
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
}
-
-
}).start();
-
}
-
-
}
-
}
-
-
/**
-
* 给某个用户发送消息
-
*
-
* @param userName
-
* @param message
-
* @throws IOException
-
*/
-
public void sendMessageToUser(Long uid, TextMessage message)
-
throws IOException {
-
WebSocketSession session = userSocketSessionMap.get(uid);
-
if (session != null && session.isOpen()) {
-
session.sendMessage(message);
-
}
-
}
-
-
}
HandShake(每次建立连接都会进行握手)
-
package org.xdemo.example.websocket.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;
-
import org.springframework.web.socket.server.HandshakeInterceptor;
-
-
-
/**
-
* Socket建立连接(握手)和断开
-
*
-
* @author Goofy
-
* @Date 2015年6月11日 下午2:23:09
-
*/
-
public class HandShake implements HandshakeInterceptor {
-
-
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
-
System.out.println("Websocket:用户[ID:" + ((ServletServerHttpRequest) request).getServletRequest().getSession(false).getAttribute("uid") + "]已经建立连接");
-
if (request instanceof ServletServerHttpRequest) {
-
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
-
HttpSession session = servletRequest.getServletRequest().getSession(false);
-
// 标记用户
-
Long uid = (Long) session.getAttribute("uid");
-
if(uid!=null){
-
attributes.put("uid", uid);
-
}else{
-
return false;
-
}
-
}
-
return true;
-
}
-
-
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
-
}
-
-
}
一个Controller
-
package org.xdemo.example.websocket.controller;
-
-
import java.io.IOException;
-
import java.util.Date;
-
import java.util.HashMap;
-
import java.util.Map;
-
-
import javax.annotation.Resource;
-
import javax.servlet.http.HttpServletRequest;
-
-
import org.springframework.stereotype.Controller;
-
import org.springframework.web.bind.annotation.ModelAttribute;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
import org.springframework.web.bind.annotation.RequestMethod;
-
import org.springframework.web.bind.annotation.ResponseBody;
-
import org.springframework.web.servlet.ModelAndView;
-
import org.springframework.web.socket.TextMessage;
-
import org.xdemo.example.websocket.entity.Message;
-
import org.xdemo.example.websocket.entity.User;
-
import org.xdemo.example.websocket.websocket.MyWebSocketHandler;
-
-
import com.google.gson.GsonBuilder;
-
-
@Controller
-
@RequestMapping("/msg")
-
public class MsgController {
-
-
@Resource
-
MyWebSocketHandler handler;
-
-
Map<Long, User> users = new HashMap<Long, User>();
-
-
//模拟一些数据
-
@ModelAttribute
-
public void setReqAndRes() {
-
User u1 = new User();
-
u1.setId(1L);
-
u1.setName("张三");
-
users.put(u1.getId(), u1);
-
-
User u2 = new User();
-
u2.setId(2L);
-
u2.setName("李四");
-
users.put(u2.getId(), u2);
-
-
}
-
-
//用户登录
-
@RequestMapping(value="login",method=RequestMethod.POST)
-
public ModelAndView doLogin(User user,HttpServletRequest request){
-
request.getSession().setAttribute("uid", user.getId());
-
request.getSession().setAttribute("name", users.get(user.getId()).getName());
-
return new ModelAndView("redirect:talk");
-
}
-
-
//跳转到交谈聊天页面
-
@RequestMapping(value="talk",method=RequestMethod.GET)
-
public ModelAndView talk(){
-
return new ModelAndView("talk");
-
}
-
//跳转到发布广播页面
-
@RequestMapping(value="broadcast",method=RequestMethod.GET)
-
public ModelAndView broadcast(){
-
return new ModelAndView("broadcast");
-
}
-
-
//发布系统广播(群发)
-
@ResponseBody
-
@RequestMapping(value="broadcast",method=RequestMethod.POST)
-
public void broadcast(String text) throws IOException{
-
Message msg=new Message();
-
msg.setDate(new Date());
-
msg.setFrom(-1L);
-
msg.setFromName("系统广播");
-
msg.setTo(0L);
-
msg.setText(text);
-
handler.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
-
}
-
-
}
一个消息的封装的类
-
package org.xdemo.example.websocket.entity;
-
-
import java.util.Date;
-
/**
-
* 消息类
-
* @author Goofy
-
* @Date 2015年6月12日 下午7:32:39
-
*/
-
public class Message {
-
-
//发送者
-
public Long from;
-
//发送者名称
-
public String fromName;
-
//接收者
-
public Long to;
-
//发送的文本
-
public String text;
-
//发送日期
-
public Date date;
-
-
public Long getFrom() {
-
return from;
-
}
-
-
public void setFrom(Long from) {
-
this.from = from;
-
}
-
-
public Long getTo() {
-
return to;
-
}
-
-
public void setTo(Long to) {
-
this.to = to;
-
}
-
-
public String getText() {
-
return text;
-
}
-
-
public void setText(String text) {
-
this.text = text;
-
}
-
-
public String getFromName() {
-
return fromName;
-
}
-
-
public void setFromName(String fromName) {
-
this.fromName = fromName;
-
}
-
-
public Date getDate() {
-
return date;
-
}
-
-
public void setDate(Date date) {
-
this.date = date;
-
}
-
-
}
聊天页面
-
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
-
<%
-
String path = request.getContextPath();
-
String basePath = request.getServerName() + ":"
-
+ request.getServerPort() + path + "/";
-
String basePath2 = request.getScheme() + "://"
-
+ request.getServerName() + ":" + request.getServerPort()
-
+ path + "/";
-
%>
-
-
-
-
<html xmlns="http://www.w3.org/1999/xhtml">
-
<head>
-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-
<title></title>
-
<script type="text/javascript" src="<%=basePath2%>resources/jquery.js"></script>
-
<style>
-
textarea {
-
height: 300px;
-
100%;
-
resize: none;
-
outline: none;
-
}
-
-
input[type=button] {
-
float: right;
-
margin: 5px;
-
50px;
-
height: 35px;
-
border: none;
-
color: white;
-
font-weight: bold;
-
outline: none;
-
}
-
-
.clear {
-
background: red;
-
}
-
-
.send {
-
background: green;
-
}
-
-
.clear:active {
-
background: yellow;
-
}
-
-
.send:active {
-
background: yellow;
-
}
-
-
.msg {
-
100%;
-
height: 25px;
-
outline: none;
-
}
-
-
#content {
-
border: 1px solid gray;
-
100%;
-
height: 400px;
-
overflow-y: scroll;
-
}
-
-
.from {
-
background-color: green;
-
80%;
-
border-radius: 10px;
-
height: 30px;
-
line-height: 30px;
-
margin: 5px;
-
float: left;
-
color: white;
-
padding: 5px;
-
font-size: 22px;
-
}
-
-
.to {
-
background-color: gray;
-
80%;
-
border-radius: 10px;
-
height: 30px;
-
line-height: 30px;
-
margin: 5px;
-
float: right;
-
color: white;
-
padding: 5px;
-
font-size: 22px;
-
}
-
-
.name {
-
color: gray;
-
font-size: 12px;
-
}
-
-
.tmsg_text {
-
color: white;
-
background-color: rgb(47, 47, 47);
-
font-size: 18px;
-
border-radius: 5px;
-
padding: 2px;
-
}
-
-
.fmsg_text {
-
color: white;
-
background-color: rgb(66, 138, 140);
-
font-size: 18px;
-
border-radius: 5px;
-
padding: 2px;
-
}
-
-
.sfmsg_text {
-
color: white;
-
background-color: rgb(148, 16, 16);
-
font-size: 18px;
-
border-radius: 5px;
-
padding: 2px;
-
}
-
-
.tmsg {
-
clear: both;
-
float: right;
-
80%;
-
text-align: right;
-
}
-
-
.fmsg {
-
clear: both;
-
float: left;
-
80%;
-
}
-
</style>
-
<script>
-
var path = '<%=basePath%>';
-
var uid=${uid eq null?-1:uid};
-
if(uid==-1){
-
location.href="<%=basePath2%>";
-
}
-
var from=uid;
-
var fromName='${name}';
-
var to=uid==1?2:1;
-
-
var websocket;
-
if ('WebSocket' in window) {
-
websocket = new WebSocket("ws://" + path + "/ws?uid="+uid);
-
} else if ('MozWebSocket' in window) {
-
websocket = new MozWebSocket("ws://" + path + "/ws"+uid);
-
} else {
-
websocket = new SockJS("http://" + path + "/ws/sockjs"+uid);
-
}
-
websocket.onopen = function(event) {
-
console.log("WebSocket:已连接");
-
console.log(event);
-
};
-
websocket.onmessage = function(event) {
-
var data=JSON.parse(event.data);
-
console.log("WebSocket:收到一条消息",data);
-
var textCss=data.from==-1?"sfmsg_text":"fmsg_text";
-
$("#content").append("<div><label>"+data.fromName+" "+data.date+"</label><div class='"+textCss+"'>"+data.text+"</div></div>");
-
scrollToBottom();
-
};
-
websocket.onerror = function(event) {
-
console.log("WebSocket:发生错误 ");
-
console.log(event);
-
};
-
websocket.onclose = function(event) {
-
console.log("WebSocket:已关闭");
-
console.log(event);
-
}
-
function sendMsg(){
-
var v=$("#msg").val();
-
if(v==""){
-
return;
-
}else{
-
var data={};
-
data["from"]=from;
-
data["fromName"]=fromName;
-
data["to"]=to;
-
data["text"]=v;
-
websocket.send(JSON.stringify(data));
-
$("#content").append("<div><label>我 "+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</label><div>"+data.text+"</div></div>");
-
scrollToBottom();
-
$("#msg").val("");
-
}
-
}
-
-
function scrollToBottom(){
-
var div = document.getElementById('content');
-
div.scrollTop = div.scrollHeight;
-
}
-
-
Date.prototype.Format = function (fmt) { //author: meizz
-
var o = {
-
"M+": this.getMonth() + 1, //月份
-
"d+": this.getDate(), //日
-
"h+": this.getHours(), //小时
-
"m+": this.getMinutes(), //分
-
"s+": this.getSeconds(), //秒
-
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
-
"S": this.getMilliseconds() //毫秒
-
};
-
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
-
for (var k in o)
-
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
-
return fmt;
-
}
-
-
function send(event){
-
var code;
-
if(window.event){
-
code = window.event.keyCode; // IE
-
}else{
-
code = e.which; // Firefox
-
}
-
if(code==13){
-
sendMsg();
-
}
-
}
-
-
function clearAll(){
-
$("#content").empty();
-
}
-
</script>
-
</head>
-
<body>
-
欢迎:${sessionScope.name }
-
<div id="content"></div>
-
<input type="text" placeholder="请输入要发送的信息" id="msg" onkeydown="send(event)">
-
<input type="button" value="发送" onclick="sendMsg()" >
-
<input type="button" value="清空" onclick="clearAll()">
-
</body>
-
</html>
发布广播的页面
-
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
-
<%
-
String path = request.getContextPath();
-
String basePath= request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
-
%>
-
-
-
-
<html xmlns="http://www.w3.org/1999/xhtml">
-
<head>
-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-
<title></title>
-
<script type="text/javascript" src="<%=basePath%>resources/jquery.js"></script>
-
<script type="text/javascript">
-
var path='<%=basePath%>';
-
function broadcast(){
-
$.ajax({
-
url:path+'msg/broadcast',
-
type:"post",
-
data:{text:$("#msg").val()},
-
dataType:"json",
-
success:function(data){
-
alert("发送成功");
-
}
-
});
-
}
-
</script>
-
</head>
-
<body>
-
发送广播
-
<textarea style="100%;height:300px;" id="msg" ></textarea>
-
<input type="button" value="发送" onclick="broadcast()">
-
</body>
-
</html>
Chrome的控制台网络信息
Type:websocket
Time:Pending
表示这是一个websocket请求,请求一直没有结束,可以通过此通道进行双向通信,即双工,实现了服务器推送的效果,也减少了网络流量。
Chrome控制台信息
Demo下载