您的位置:首页 > 编程语言 > Java开发

【Java】WebSocket协议与 SpringMVC整合WebSocket demo

2018-01-15 14:22 405 查看
WebSocket协议WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket协议支持(在受控环境中运行不受信任的代码的)客户端与(选择加入该代码的通信的)远程主机之间进行全双工通信。用于此的安全模型是Web浏览器常用的基于原始的安全模式。 协议包括一个开放的握手以及随后的TCP层上的消息帧。 该技术的目标是为基于浏览器的、需要和服务器进行双向通信的(服务器不能依赖于打开多个HTTP连接(例如,使用XMLHttpRequest或<iframe>和长轮询))应用程序提供一种通信机制。长久以来, 创建实现客户端和用户端之间双工通讯的web app都会造成HTTP轮询的滥用: 客户端向主机不断发送不同的HTTP呼叫来进行询问。 这会导致一系列的问题: 1.服务器被迫为每个客户端使用许多不同的底层TCP连接:一个用于向客户端发送信息,其它用于接收每个传入消息。 2.有些协议有很高的开销,每一个客户端和服务器之间都有HTTP头。 3.客户端脚本被迫维护从传出连接到传入连接的映射来追踪回复。 一个更简单的解决方案是使用单个TCP连接双向通信。 这就是WebSocket协议所提供的功能。 结合WebSocket API ,WebSocket协议提供了一个用来替代HTTP轮询实现网页到远程主机的双向通信的方法。 WebSocket协议被设计来取代用HTTP作为传输层的双向通讯技术,这些技术只能牺牲效率和可依赖性其中一方来提高另一方,因为HTTP最初的目的不是为了双向通讯。(获得更多关于此的讨论可查阅RFC6202)在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:Header 互相沟通的Header是很小的-大概只有 2 Bytes
Server Push 服务器的推送,服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
(上面资料来自百度)Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。简单的举个例子吧,用目前应用比较广泛的PHP生命周期来解释。HTTP的生命周期通过 Request 来界定,也就是一个 Request 一个 Response ,那么在 HTTP1.0 中,这次HTTP请求就结束了。在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。特点:事件驱动异步使用ws或者wss协议的客户端socket能够实现真正意义上的推送功能缺点:少部分浏览器不支持,浏览器支持的程度与方式有区别。Websocket的使用场景ajax轮询ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。long polllong poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。 何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。没有其他技术能够像WebSocket一样提供真正的双向通信,许多web开发者仍然是依赖于ajax的长轮询来实现。 决定手头的工作是否需要使用WebSocket技术的方法很简单:
你的应用提供多个用户相互交流吗?
你的应用是展示服务器端经常变动的数据吗?
如果你的回答是肯定的,那么请考虑使用WebSocket。如果你仍然不确定,并想要更多的灵感,这有一些杀手锏的案例。1.社交订阅对社交类的应用的一个裨益之处就是能够即时的知道你的朋友正在做什么。虽然听起来有点可怕,但是我们都喜欢这样做。你不会想要在数分钟之后才能知道一个家庭成员在馅饼制作大赛获胜或者一个朋友订婚的消息。你是在线的,所以你的订阅的更新应该是实时的。 2.多玩家游戏 网络正在迅速转变为游戏平台。在不使用插件(我指的是Flash)的情况下,网络开发者现在可以在浏览器中实现和体验高性能的游戏。无论你是在处理DOM元素、CSS动画,HTML5的canvas或者尝试使用WebGL,玩家之间的互动效率是至关重要的。我不想在我扣动扳机之后,我的对手却已经移动位置。3.协同编辑/编程我们生活在分布式开发团队的时代。平时使用一个文档的副本就满足工作需求了,但是你最终需要有一个方式来合并所有的编辑副本。版本控制系统,比如Git能够帮助处理某些文件,但是当Git发现一个它不能解决的冲突时,你仍然需要去跟踪人们的修改历史。通过一个协同解决方案,比如WebSocket,我们能够工作在同一个文档,从而省去所有的合并版本。这样会很容易看出谁在编辑什么或者你在和谁同时在修改文档的同一部分。4.点击流数据分析用户与你网站的互动是提升你的网站的关键。HTTP的开销让我们只能优先考虑和收集最重要的数据部分。然后,经过六个月的线下分析,我们意识到我们应该收集一个不同的判断标准——一个看起来不是那么重要但是现在却影响了一个关键的决定。与HTTP请求的开销方式相比,使用Websocket,你可以由客户端发送不受限制的数据。想要在除页面加载之外跟踪鼠标的移动?只需要通过WebSocket连接发送这些数据到服务器,并存储在你喜欢的NoSQL数据库中就可以了(MongoDB是适合记录这样的事件的)。现在你可以通过回放用户在页面的动作来清楚的知道发生了什么。5.股票基金报价金融界瞬息万变——几乎是每毫秒都在变化。我们人类的大脑不能持续以那样的速度处理那么多的数据,所以我们写了一些算法来帮我们处理这些事情。虽然你不一定是在处理高频的交易,但是,过时的信息也只能导致损失。当你有一个显示盘来跟踪你感兴趣的公司时,你肯定想要随时知道他们的价值,而不是10秒前的数据。使用WebSocket可以流式更新这些数据变化而不需要等待。6.体育实况更新现在我们开始讨论一个让人们激情澎湃的愚蠢的东西——体育。我不是运动爱好者,但是我知道运动迷们想要什么。当爱国者在打比赛的时候,我的妹夫将会沉浸于这场比赛中而不能自拔。那是一种疯狂痴迷的状态,完全发自内心的。我虽然不理解这个,但是我敬佩他们与运动之间的这种强烈的联系,所以,最后我能做的就是给他的体验中降低延迟。如果你在你的网站应用中包含了体育新闻,WebSocket能够助力你的用户获得实时的更新。7.多媒体聊天视频会议并不能代替和真人相见,但当你不能在同一个屋子里见到你谈话的对象时,视频会议是个不错的选择。尽管视频会议私有化做的“不错”,但其使用还是很繁琐。我可是开放式网络的粉丝,所以用WebSockets getUserMedia API和HTML5音视频元素明显是个不错的选择。WebRTC的出现顺理成章的成为我刚才概括的组合体,它看起来很有希望,但其缺乏目前浏览器的支持,所以就取消了它成为候选人的资格。8.基于位置的应用越来越多的开发者借用移动设备的GPS功能来实现他们基于位置的网络应用。如果你一直记录用户的位置(比如运行应用来记录运动轨迹),你可以收集到更加细致化的数据。如果你想实时的更新网络数据仪表盘(可以说是一个监视运动员的教练),HTTP协议显得有些笨拙。借用WebSocket TCP链接可以让数据飞起来。9.在线教育上学花费越来越贵了,但互联网变得更快和更便宜。在线教育是学习的不错方式,尤其是你可以和老师以及其他同学一起交流。很自然,WebSockets是个不错的选择,可以多媒体聊天、文字聊天以及其它优势如与别人合作一起在公共数字黑板上画画...WebSocket方法介绍:
websocket允许通过JavaScript建立与远程服务器的连接,从而实现客户端与服务器间双向的通信。在websocket中有两个方法:  
    1、send() 向远程服务器发送数据
    2、close() 关闭该websocket链接
  websocket同时还定义了几个监听函数    
    1、onopen 当网络连接建立时触发该事件
    2、onerror 当网络发生错误时触发该事件
    3、onclose 当websocket被关闭时触发该事件
    4、onmessage 当websocket接收到服务器发来的消息的时触发的事件,也是通信中最重要的一个监听事件。msg.data
  websocket还定义了一个readyState属性,这个属性可以返回websocket所处的状态:
    1、CONNECTING(0) websocket正尝试与服务器建立连接
    2、OPEN(1) websocket与服务器已经建立连接
    3、CLOSING(2) websocket正在关闭与服务器的连接
    4、CLOSED(3) websocket已经关闭了与服务器的连接

  websocket的url开头是ws,如果需要ssl加密可以使用wss,当我们调用websocket的构造方法构建一个websocket对象(new WebSocket(url))的之后,就可以进行即时通信了。
SpringMVC项目整合WebSocket案例:1.pom.xml配置
<!-- websocket协议 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- websocket协议  end-->
2.代码编写chat.jsp页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>

<head lang="en">
<meta charset="UTF-8">
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<title>webSocket-测试用户</title>
<script type="text/javascript">
$(function() {
var websocket;
if('WebSocket' in window) {
console.log("此浏览器支持websocket");
websocket = new WebSocket("ws://127.0.0.1:8080/chat/12345");
} else if('MozWebSocket' in window) {
alert("此浏览器只支持MozWebSocket");
} else {
alert("此浏览器只支持SockJS");
}
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').click(function(){
send();
});

function send() {
if(websocket != null) {
var message = document.getElementById('message').value;
websocket.send(message);
} else {
alert('未与服务器链接.');
}
}
});
</script>
</head>

<body>
<div class="page-header" id="tou">
webSocket多终端聊天测试
</div>
<div class="well" id="msg"></div>
<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>
后台代码
package com.naton.controller;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.apache.log4j.Logger;
import org.springframework.web.socket.server.standard.SpringConfigurator;

//websocket连接URL地址和可被调用配置
@ServerEndpoint(value="/chat/{userId}",configurator = SpringConfigurator.class)
public class WebSocketChat {

private static Logger logger = Logger.getRootLogger();
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;

//记录每个用户下多个终端的连接
private static Map<String, Set<WebSocketChat>> userSocket = new HashMap<>();

//需要session来对用户发送数据, 获取连接特征userId
private Session session;
private String userId;

/**
* @Title: onOpen
* @Description: websocekt连接建立时的操作
* @param @param userId 用户id
* @param @param session websocket连接的session属性
* @param @throws IOException
*/
@OnOpen
public void onOpen(@PathParam("userId") String userId,Session session) throws IOException{
this.session = session;
this.userId = userId;
onlineCount++;
//根据该用户当前是否已经在别的终端登录进行添加操作
if (userSocket.containsKey(this.userId)) {
logger.info("当前用户id:{"+this.userId+"}已有其他终端登录");
userSocket.get(this.userId).add(this); //增加该用户set中的连接实例
}else {
logger.info("当前用户id:{"+this.userId+"}第一个终端登录");
Set<WebSocketChat> addUserSet = new HashSet<>();
addUserSet.add(this);
userSocket.put(this.userId, addUserSet);
}
logger.info("用户{"+userId+"}登录的终端个数是为{"+userSocket.get(this.userId).size()+"}");
logger.info("当前在线用户数为:{"+userSocket.size()+"},所有终端个数为:{"+onlineCount+"}");
}

/**
* @Title: onClose
* @Description: 连接关闭的操作
*/
@OnClose
public void onClose(){
//移除当前用户终端登录的websocket信息,如果该用户的所有终端都下线了,则删除该用户的记录
if (userSocket.get(this.userId).size() == 0) {
userSocket.remove(this.userId);
}else{
userSocket.get(this.userId).remove(this);
}
logger.info("用户{"+this.userId+"}登录的终端个数是为{"+userSocket.get(this.userId).size()+"}");
logger.info("当前在线用户数为:{"+userSocket.size()+"},所有终端个数为:{"+onlineCount+"}");
}

/**
* @Title: onMessage
* @Description: 收到消息后的操作
* @param @param message 收到的消息
* @param @param session 该连接的session属性
*/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("收到来自用户id为:{"+this.userId+"}的消息:{"+message+"}");
if(session ==null)  logger.info("session null");
//测试向客户端发送消息发送
sendMessageToUser(this.userId,"服务器收到你的消息:"+ message);
}

/**
* @Title: onError
* @Description: 连接发生错误时候的操作
* @param @param session 该连接的session
* @param @param error 发生的错误
*/
@OnError
public void onError(Session session, Throwable error){
logger.info("用户id为:{"+this.userId+"}的连接发送错误");
error.printStackTrace();
}

/**
* @Title: sendMessageToUser
* @Description: 发送消息给用户下的所有终端
* @param @param userId 用户id
* @param @param message 发送的消息
* @param @return 发送成功返回true,反则返回false
*/
public Boolean sendMessageToUser(String userId,String message){
if (userSocket.containsKey(userId)) {
logger.info(" 给用户id为:{"+userId+"}的所有终端发送消息:{"+message+"}");
for (WebSocketChat WS : userSocket.get(userId)) {
logger.info("sessionId为:{"+WS.session.getId()+"}");
try {
WS.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
logger.info(" 给用户id为:{"+userId+"}发送消息失败");
return false;
}
}
return true;
}
logger.info("发送错误:当前连接不包含id为:{"+userId+"}的用户");
return false;
}

}
WSMessageService类
package com.naton.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.naton.controller.WebSocketChat;

@Service("webSocketMessageService")
public class WSMessageService {
private Logger logger = LoggerFactory.getLogger(WSMessageService.class);
//声明websocket连接类
private WebSocketChat webSocketChat = new WebSocketChat();

/**
* @Title: sendToAllTerminal
* @Description: 调用websocket类给用户下的所有终端发送消息
* @param @param userId 用户id
* @param @param message 消息
* @param @return 发送成功返回true,否则返回false
*/
public Boolean sendToAllTerminal(String userId,String message){
logger.info("向用户{}的消息:{}",userId,message);
if(webSocketChat.sendMessageToUser(userId,message)){
return true;
}else{
return false;
}
}
}
MessageController类
package com.naton.controller;

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.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.naton.service.WSMessageService;

@Controller
@RequestMapping("/message")
public class MessageController {

//websocket服务层调用类
@Autowired
private WSMessageService wsMessageService;

//请求入口
@RequestMapping(value="/TestWS",method=RequestMethod.GET)
@ResponseBody
public String TestWS(@RequestParam(value="userId",required=true) String userId,
@RequestParam(value="message",required=true) String message){
System.out.println("收到发送请求,向用户{"+userId+"}的消息:{"+message+"}");

if(wsMessageService.sendToAllTerminal(userId, message)){
return "发送成功";
}else{
return "发送失败";
}
}
}
接下来就可以运行项目进行实验了。界面效果如图:


参考资料:http://blog.csdn.net/frank_good/article/details/50856585https://www.jianshu.com/p/d79bf8174196https://www.jianshu.com/p/d79bf8174196http://blog.csdn.net/yebanfengdi/article/details/52042626
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: