一、什么是WebSocket API?
WebSocket API是下一代客户端-服务器的异步通信方法。该通信取代了单个的TCP套接字,使用ws或wss协议,可用于任意的客户端和服务器程序。WebSocket目前由W3C进行标准化。WebSocket已经受到Firefox 4、Chrome 4、Opera 10.70以及Safari 5等浏览器的支持。
WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;XHR受到域的限制,而WebSocket允许跨域通信。
Ajax技术很聪明的一点是没有设计要使用的方式。WebSocket为指定目标创建,用于双向推送消息。
二、原理
WebSocket protocol 是HTML5一种新的协议(protocol)。它是实现了浏览器与服务器全双工通信(full-duplex)。
现在,很多网站为了实现即时通讯(real-time),所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(time interval)(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request d的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求(request),然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽。
而最比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求(reuqest)。在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
1. Header
互相沟通的Header是很小的-大概只有 2 Bytes
2. Server Push
服务器可以主动传送数据给客户端
三、握手协议
在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” (handshaking)。
PS1:握手协议在后期的版本中,会标明版本编号,下面的例子属于早期的协定之一,对于新版的 chrome 和 Firefox 皆不适用。
PS2:后期的版本大多属于功能上的扩充,例如使用第7版的握手协议同样也适用于第8版的握手协议。
例子:
浏览器请求
GET /demo HTTP/1.1
Host: 你的网址.com
Connection: Upgrade
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
Sec-WebSocket-Protocol: sample
Upgrade: WebSocket
Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
Origin: http://你的网址.com
^n:ds[4U
服务器回应
HTTP/1.1 101
WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://你的网址.com
Sec-WebSocket-Location: ws://你的网址.com/demo
Sec-WebSocket-Protocol: sample
8jKS’y:G*Co,Wxa-
四、服务器端支持
在服务器端,也出现了一些实现websocket协议的项目:
jetty 7.0.1 包含了一个初步的实现
resin 包含有websocket 实现
pywebsocket, apache http server 扩展
apache tomcat 7.0.27 版本
Nginx 1.3.13 版本
Node.js
Node.js
websocket api在浏览器端的广泛实现似乎只是一个时间问题了, 值得注意的是目前服务器端没有标准的api, 各个实现都有自己的一套api, 并且jcp也没有类似的提案, 所以使用websocket开发服务器端有一定的风险.可能会被锁定在某个平台上或者将来被迫升级.
五、网页客户咨询系统
当客户浏览网页的时候会自动弹出一个对话框询问用户是否需要咨询客户服务人员,如果用户需要咨询打开对话框,通过SOCKET链接进行通讯,在以前该方式是通过网页嵌入FLASH插件与后台服务人员的富客户端系统通讯,现在改为采用HTML5的方式大大的节约了开发时间,前台后台只要通过在服务页面与后台OA页面嵌入JS代码就能实现,服务器端采用jetty来实现非常方便部署与扩展。
客户网页代码
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>chat</title>
<link href="dialog.css" rel="stylesheet" type="text/css" />
</head>
<script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script type="text/javascript">
var ws = null;
var isOpen = false;
var sessionKey = '';
$(document).ready(function() {
$("#dialog").hide();
$("#showDialog").click(function() {
if(!isOpen) {
$("#dialog").show();
//$("#dialog").css("top", width / 2);
//$("#dialog").css("left", height / 2);
initWebSocket();
isOpen = true;
} else {
$("#dialog").hide();
isOpen = false;
}
});
$("#send").click(function() {
var msg = $("#msg").val();
var data = "SESSION:" + sessionKey + ":FORM:" + $("#msg").val();
ws.send(data);
$("#msg").val("");
var texts = $("#viewsMsg").text();
texts += "TO:" + msg + "\n";
$("#viewsMsg").text(texts)
})
});
function initWebSocket() {
ws = new WebSocket("ws://localhost:8080/chat.do")
$("#msg").text("建立连接中")
ws.onopen = function() {
$("#msg").text("");
ws.send("GUEST");
};
ws.onclose = function() {
ws.send("close");
}
ws.onmessage = function(event) {
var values = event.data.split(":");
if(values[0] == 'sessionKey') {
sessionKey = values[1];
}
if(values[0] == 'REVCE') {
var texts = $("#viewsMsg").text();
texts += "FORM:" + values[1] + "\n";
$("#viewsMsg").text(texts)
}
}
}
</script>
<body>
<div>
<button id="showDialog">对话</button>
</div>
<div id="dialog" class="dialog-class">
<div class="msg-class">
<textarea id="viewsMsg" cols="32" rows="6" disabled="disabled"></textarea>
</div>
<div>
<textarea id="msg" cols="25" rows="1"></textarea><button id="send">Send</button>
</div>
</div>
</body>
</html>
客服人员服务端代码
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>chat</title>
<link href="dialog.css" rel="stylesheet" type="text/css" />
</head>
<script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script type="text/javascript">
var ws = null;
var isOpen = false;
var sessionKey = '';
$(document).ready(function() {
$("#dialog").hide();
$("#showDialog").click(function() {
if(!isOpen) {
$("#dialog").show();
//$("#dialog").css("top", width / 2);
//$("#dialog").css("left", height / 2);
initWebSocket();
isOpen = true;
} else {
$("#dialog").hide();
isOpen = false;
}
});
$("#send").click(function() {
var msg = $("#msg").val();
var data = "SESSION:" + sessionKey + ":TO:" + $("#msg").val();
ws.send(data);
$("#msg").val("");
var texts = $("#viewsMsg").text();
texts += "TO:" + msg + "\n";
$("#viewsMsg").text(texts)
})
});
function initWebSocket() {
ws = new WebSocket("ws://localhost:8080/chat.do")
$("#msg").text("建立连接中")
ws.onopen = function() {
$("#msg").text("");
ws.send("SERVICE");
};
ws.onclose = function() {
ws.send("close");
}
ws.onmessage = function(event) {
var values = event.data.split(":");
if(values[0] == 'sessionKey') {
sessionKey = values[1];
}
if(values[0] == 'REVCE') {
var texts = $("#viewsMsg").text();
texts += "FORM:" + values[1] + "\n";
$("#viewsMsg").text(texts)
}
}
}
</script>
<body>
<div>
<button id="showDialog">对话</button>
</div>
<div id="dialog" class="dialog-class">
<div class="msg-class">
<textarea id="viewsMsg" cols="32" rows="6" disabled="disabled"></textarea>
</div>
<div>
<textarea id="msg" cols="25" rows="1"></textarea><button id="send">Send</button>
</div>
</div>
</body>
</html>
CSS样式文件
@CHARSET "UTF-8";
.dialog-class {
position:absolute;
width:320px;
height:220px;
border-style:solid;
border-color:#000;
}
.msg-class {
border-style:solid;
border-color:#000;
}
.input-text-class {
vertical-align: middle;
}
实体代码
package com.chat.entity;
import org.eclipse.jetty.websocket.WebSocket;
public class Client {
private String sessionKey;
private WebSocket clientSocket;
private WebSocket serviceSocket;
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public WebSocket getClientSocket() {
return clientSocket;
}
public void setClientSocket(WebSocket clientSocket) {
this.clientSocket = clientSocket;
}
public WebSocket getServiceSocket() {
return serviceSocket;
}
public void setServiceSocket(WebSocket serviceSocket) {
this.serviceSocket = serviceSocket;
}
}
服务器端代码
package com.chat.servlet;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import com.chat.entity.*;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.eclipse.jetty.websocket.WebSocket.OnTextMessage;
/**
* Servlet implementation class ChatServlet
*/
public class ChatServlet extends WebSocketServlet {
private static final long serialVersionUID = 1L;
private static LinkedList<Client> idelClientList = new LinkedList<Client>();
private static Map<String, Client> sessionMap = new HashMap<String, Client>();
@Override
public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
return new ChatSocket();
}
class ChatSocket implements OnTextMessage {
private Connection connection;
@Override
public void onClose(int arg0, String arg1) {
System.out.println(arg0 + ":" + arg1);
}
@Override
public void onOpen(Connection connection) {
this.connection = connection;
}
public boolean isOpen() {
return this.connection.isOpen();
}
public void sendMessage(String msg) {
try {
this.connection.sendMessage(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onMessage(String data) {
System.out.println(data);
String[] headers = data.split(":");
if(headers[0].equals("SERVICE")) {
Client client = idelClientList.pop();
client.setServiceSocket(this);
sessionMap.put(client.getSessionKey(), client);
try {
this.connection.sendMessage("sessionKey:" + client.getSessionKey());
} catch (IOException e) {
e.printStackTrace();
}
}
if(headers[0].equals("GUEST")) {
Client client = new Client();
client.setSessionKey(UUID.randomUUID().toString());
client.setClientSocket(this);
idelClientList.push(client);
try {
this.connection.sendMessage("sessionKey:" + client.getSessionKey());
} catch (IOException e) {
e.printStackTrace();
}
}
if(headers[0].equals("SESSION")) {
String sessionKey = headers[1];
Client client = sessionMap.get(sessionKey);
ChatSocket cs = null;
if(headers[2].equals("TO")) {
cs = (ChatSocket) client.getClientSocket();
} else {
cs = (ChatSocket) client.getServiceSocket();
}
if(cs.isOpen()) {
cs.sendMessage("REVCE:" + headers[3]);
}
}
}
}
}
部署WEB.XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app id="WebApp_ID">
<display-name>chat</display-name>
<servlet>
<servlet-name>chat</servlet-name>
<servlet-class>com.chat.servlet.ChatServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>chat</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
该服务器端还可以使用Node.js的方式来实现,大家可以尝试,本代码只要经过少许修改就能用于真正的生产环境部署。