什么是WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
例如ws://localhost:5000/
(加密协议为wss://localhost:5000
)
WebSocket协议与Http协议
- Http是基于TCP协议的应用层协议,需通过“三次握手”建立连接。分为长连接与短连接,短连接即每次请求都需重新经历“三次握手”,而后才可以通信,也就是说每一个request对应一个response。长连接在一定期限内可以保持TCP连接不中断,但是必须每次由客户端给服务器发送请求来获取请求结果。无论是长连接还是短连接,皆需由客户端主动请求服务器。
- WebSocket协议是为了解决Http在轮询请求服务器获取资源时的性能浪费问题。由于WebSocket协议实现了多路复用,所以在建立连接后,服务器端也可以主动给客户端发消息。
建立了WenSocket之后服务器不必在浏览器发送request请求之后才能发送信息到浏览器。这时的服务器已有主动权想什么时候发就可以发送信息到服务器。而且信息当中不必在带有head的部分信息了与http的长链接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。
Http协议的长连接、短连接、长轮询与WebSocket的持久连接
- 长连接:Http1.1的连接默认使用长连接,即在一定期限内保持连接。可以用在短时间内请求大量资源的情况,在这里一直是客户端主动请求服务器。虽然在一个TCP连接上传输多个请求(Request)和响应(Response)对比短连接显得优雅了很多,但是依然存在一些资源的浪费,并且实时性不强。
- 短连接:短连接在请求每个资源时都要建立一个TCP连接,也就是经历“三次握手”阶段,所以在请求大量资源时,是性能最差、最浪费资源的。
- 长轮询:需要客户端发送一个特别长的超时时间,然后服务器保持这个连接,在有新数据到达时返回响应。
- WebSocket的持久连接:只需建立一次Request/Response消息对,之后都是TCP连接,避免了需要多次建立Request/Response消息对而产生的冗余头部信息。(在“三次握手”阶段,依然是使用Http协议,建立连接后切换为WebSocket协议)
WebSocket和Socket的区别
在网络中的两个应用程序(进程)需要全双工相互通信(全双工即双方可同时向对方发送消息),需要用到的就是socket,它能够提供端对端通信,对于程序员来讲,他只需要在某个应用程序的一端(暂且称之为客户端)创建一个socket实例并且提供它所要连接一端(暂且称之为服务端)的IP地址和端口,而另外一端(服务端)创建另一个socket并绑定本地端口进行监听,然后客户端进行连接服务端,服务端接受连接之后双方建立了一个端对端的TCP连接,在该连接上就可以双向通讯了,而且一旦建立这个连接之后,通信双方就没有客户端服务端之分了,提供的就是端对端通信了。我们可以采取这种方式构建一个桌面版的im程序,让不同主机上的用户发送消息。从本质上来说,socket并不是一个新的协议,它只是为了便于程序员进行网络编程而对tcp/ip协议族通信机制的一种封装。
websocket是html5规范中的一个部分,它借鉴了socket这种思想,为web应用程序客户端和服务端之间(注意是客户端服务端)提供了一种全双工通信机制。同时,它又是一种新的应用层协议,websocket协议是为了提供web应用程序和服务端全双工通信而专门制定的一种应用层协议。
其他即时通讯的方法
除上面提到的定期轮询与WebSocket之外,还有SSE技术与Comet技术
SSE
SSE(Server-Sent Event,服务端推送事件)是一种允许服务端向客户端推送新数据的HTML5技术。与由客户端每隔几秒从服务端轮询拉取新数据相比,这是一种更优的解决方案。
相较于WebSocket,它也能从服务端向客户端推送数据。WebSocket能做的,SSE也能做,反之亦然,但在完成某些任务方面,它们各有千秋。
在使用SSE时,服务器响应的MIME类型必须是text/event-stream(响应的格式是UTF-8 编码的纯文本),而且是浏览器中的JavaScript API 能解析格式输出。SSE 支持短轮询、长轮询和HTTP 流,而且能在断开连接时自动确定何时重新连接。
示例代码
客户端代码
//判断是否支持SSE
if('EventSource' in window){
//初始化SSE
var url="http:localhost:8080/test/push";
var source=new EventSource(url);
//开启时调用
source.onopen=(event)=>{
console.log("开启SSE");
}
//监听message事件
source.onmessage=(event)=>{
var data=event.data;
$("body").append($("<p>").text(data));
}
//监听like事件
source.addEventListener('like',function(event){
var data=event.data;
$("body").append($("<p>").text(data));
},false);
//发生异常时调用
source.onerror=(event)=>{
console.log(event);
}
服务端代码
@Controller
@RequestMapping(value="/test")
public class TestSSEController {
@ResponseBody
@RequestMapping(value="/push",produces="text/event-stream;charset=UTF-8")
public String push(HttpServletResponse res){
res.setHeader("Access-Control-Allow-Origin","*");
Date date=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
String nowDate=sdf.format(date);
return "data: 我是一个data 现在时间是"+nowDate+" \nevent:like\n retry:5000\n\n";
}
}
消息的格式
每一次发送的消息都由若干message组成,每两个message之间用\n\n
分隔,每个message内部可以有若干行,使用\n
换行,每行的消息有字段+冒号+取值组成。
字段可以为
- data:数据
- event:事件
- id:id
- retry:轮询频率
如
data:value\n event:event_value\n retry:3000\n\n
即3秒轮询一次,数据为value,事件为event_value。
Comet技术
Comet并不是一种新的通信技术,它是在客户端请求服务端这个模式上的一种hack技术,大体可以分为以下两种:
- 基于长轮询的服务端推送技术
类似于上面说到的长轮询,客户端首先给服务端发送一个请求,服务端收到该请求之后如果数据没有更新则并不立即返回,服务端阻塞请求的返回,直到数据发生了更新或者发生了连接超时,服务端返回数据之后客户端再次发送同样的请求。 - 基于流式数据传输的长连接
做法是在页面中嵌入一个隐藏的iframe,然后让这个iframe的src属性指向我们请求的一个服务端地址,并且为了数据更新,我们将页面上数据更新操作封装为一个js函数,将函数名当做参数传递到这个地址当中。
服务端收到请求后解析地址取出参数(客户端js函数调用名),每当有数据更新的时候,返回对客户端函数的调用,并且将要跟新的数据以js函数的参数填入到返回内容当中,例如返回“”这样一个字符串,意味着以data为参数调用客户端update函数进行客户端view更新。
从这两种实现能够知道,Commet技术其实就是针对客户端请求服务器响应模型而模拟出的一个服务器实时推送数据的技术。并且各种浏览器的兼容性也不是很好,所以无法得到广泛应用。
WebSocket代码示例
客户端
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/WebSocketTest/websocket");
}
else {
alert('当前浏览器 Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
服务器端
@ServerEndpoint("/websocket")
public class WebsocketTest {
private static int onlineCount=0;//记录当前在线人数
//确保线程安全
private static CopyOnWriteArraySet<WebsocketTest> webSocketSet=new CopyOnWriteArraySet<WebsocketTest>();
private Session session;
@OnOpen
public void onOpen(Session session){
this.session=session;
webSocketSet.add(this);
addOnlineCount();
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
@OnClose
public void OnClose(){
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
@OnMessage
public void OnMessage(String message, Session session){
System.out.println("来自客户端的消息:" + message);
//群发消息
for(WebsocketTest item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
@OnError
public void OnError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebsocketTest.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebsocketTest.onlineCount--;
}
}