一,理论概述:
在传统的web设计中,都是基于“请求——响应”的模式。在整个通信的过程中,都是web客户端首先发起一个请求,服务器再立即根据请求作出响应,一般服务端响应完毕(比如在60s之内)就自动断开这一次连接(见图1)。
这其中有一个最大的局限性就是服务端不能主动发送信息给客户端。现在我们的需求很简单,基本和必要的事情就是如何实现双工通信。如果要让服务端主动发信息给web的客户端,我们以前采取的一种策略就是轮询。
轮询就是不断地发送携带了没有实际意义的数据来维持与服务端“保持沟通”以达到不间断监听的目的。虽然轮询可以比较及时地发现服务器产生的消息,但也并没有真正意义上实现即时通讯,发生在轮询之间的消息仍有延迟,况且最为致命的缺陷就是为了维持连接客户端和后台服务端都要产生极大的开销。然而,Websocket技术的出现让即时通讯功能在实现的基础上发生的很大的改变(见图3)。
实际来说,WebSocket技术并不是使用了HTTP协议,HTTP协议是一种无状态的,理论上是无法实现全双工通讯的。Websocket技术采用了websocket协议,使其能够达到类似Socket技术的通讯效果。Websocket协议也是基于TCP协议的,这种在协议底层技术层面上的支持,使得websocket技术达到了全双工通讯的效果。然而WebSocket技术是为web技术提供服务的,在实际操作中,我们可以使用前端的JavaScript语言加上WEB后端(比如PHP、Java等语言)建立通讯,这看上去和传统的web编程并没有任何区别。只要服务端能够及时地主动发信息,我们就可以及时地把消息转发给另一个客户端,实现客户端与客户端之间的即时通讯。
那么在WebSocket的技术支持下我们理论上可以在WEB技术中实现类似于市面上的任何一款即时通讯软件的功能(比如QQ和微信)下面是我尝试着要实现的需求(下表1)。
信息载体 | 消息类型 | 接收方 |
字符流 | 文字消息(text) | 个人或群体 |
字节流 | 图片消息(image) | 个人或群体 |
语音消息(audio) | 个人或群体 | |
视频消息(video) | 个人或群体 | |
文件传送(application/octet-stream) | 个人或群体 |
表1可实现的目标
下面可以采用javascript语言(前端)+ java语言(后端)并通过WebSocket技术实现即时通讯模块。但在此之前我们还得认识一下没有在java API掩盖之下的websocket工作示意图(图4):
图4 底层WebSocket通讯技术示意图
如图4所示,如果你打算使用PHP做后台,就可能要使用到一个死循环来模拟这个流程。
在JAVA这种高级语言对底层WebSocket进行封装后,有了相当大的改变。下图5展示了WebSocket在Tomcat服务器上的使用Tomcat提供的java API来操作通讯的示意图:
有了这些基础。我们就可以轻松达到一对一发送消息(见图6)
二,概要设计:
通讯模块应当具备登录和注册页面。与以往的web开发不同的是,每登录成功一个用户还应当连接上WebSocket服务。并记录下本用户的相关会话信息。
这是一个必须考虑的问题,前端必须实现可以查询在线人数、某个人的在线转态,更新某个人的在线状态等方法。
首先,为了能够让服务器知道消息要传送给谁,或者反过来,让消息回复给谁,我们的消息除了正确的正文信息以外,消息应当携带一定的描述信息,我们可以把这类消息称作普通消息;其次,客户端和服务器之间要实现基本信息的记录、一些文件描述信息的发送和某些复杂的操作时仅仅需要客户端和服务器之间协调,这些信息并不需要服务器转发,我们可以称作为命令消息。
在web编程中,文字是很好展示的,比如采用按列表展示。首先把接收到的消息描述和正文分离后,提取发送人信息,和正文消息内容,然后按照一定的格式放入li标签中来列表展示,必要的时候可采用浮动(float)属性实现左右排列的效果。当然整个聊天窗口应当是每来一条新的消息,就多累积一条消息并且能够自动向下滑动。基于这一点。我们的都应该通过一定的js来实现动态的展示。
在web前端发送文件是一个难点。主要面临这么几个问题:
1,不能使用传统的表单提交文件,因为WebSocket发送消息没有使用HTTP协议,不能发送POST请求;
2,文件的传送的完整性判断,避免服务器因为网络的不稳定接收到残缺的文件;
3,客户端加载文件和传输文件的速度匹配问题,避免传输过慢,加载速度过快导致客户端卡机。
以上三点就是在具体实现中要考虑和解决的问题。
对于文件,标准的做法应该是使用MIME去区分他们,对此我们在展示文件消息的时候必须实现一个消息类型解析器,判断文件类型并实现不同的展示效果。既然要展示文件,我们应该展示一些比较通用并且容易展示的文件,比如图片,音频、视频。图片在网站上的展示是极为常见的,音频和视频文件或许在以前还是个相当大的问题,现在我们可以基于HTML5的audio和video标签也能够很方便地展示。
网络不稳定可以说是个非常常见的问题,比如信号不好,或者是切换网络的连接方式。对此WebSocket已经连接好的TCP通道必然会异常断开。为此我们的前端应该加入某种重连机制,比如在回调方法OnError中执行重新连接的机制。
使用传统的Servlet技术,或常见的框架来实现普通的登录注册功能。
在Tomcat实现的WebSocket技术体系中WebSocket会话(session对象)在整个即时通讯系统中有着极为关键的作用。每个登录用户都应具备其对应的一个session,依靠session这个用户才能即时地发送消息。所以一要解决的问题就是按照用户和session的对应关系来保存session。一种比较好的方式就是在考虑并发操作的前提下使用HashTable来保存这个信息。
既然前端的消息分了两个部分,每个部分的格式又非常迥异,如果我们要处理这种复杂的关系,就要在后台建立一定复杂的配套的消息解析机制。这个机制的稳定性决定了消息的传输效率和消息传达的正确性。
每个人的聊天信息都应该缓存一部分在服务器上,以备客户端接收离线消息。在实现的时候主要的问题有 5点:
a,以什么形式保存
b,什么时候保存
c,怎么个形式保存
d,如何能够方便地获取消息记录
e,什么时候清理
基于上面的考虑。首先肯定的是不能在每发一段短的文字消息就把它保存在数据库中,而应该首先缓存在内存中。所以在内存中的消息积累一定的量后以一定格式文件的形式保存,并把文件路径记录到数据库中是个不错的选择。
在客户端处理文件上传的时候,可以采取两种策略:一是,A客户端每上传一份数据(即使文件没有发送完)在服务器上就立即转发一份数据,最终的数据在B客户端完成合并;二是,服务器把A客户端上传的每一份数据先缓存在服务器本地,直到A客户端发送文件完毕后,整个文件在服务端合成完整文件后,再由服务器立即发送文件URL给B客户端,再由B客户端决定随时获取。我们可以把第一种方式称作在线发送。可以把第二种方式称作离线发送。很显然在线发送的即时性比较好,但对技术的要求显然更高(特别是对于JavaScript这种技术在读写本地磁盘方面有着很大的局限性),同时也要求两个客户端必须同时在线。所以可以在WEB上考虑第二种方式是目前阶段比较理智的行为。
三,编码的大致框架:
//定义保存WebSocket对象的变量
var ws;
//定义请求路径
var wsUrl =‘ws://127.0.0.1:8080/tomcatwebsocket/websocket‘;
// 定义创建和服务器的WebSocket连接的函数
function createSocket(){
//new 一个WebSocket对象
ws = new WebSocket(wsUrl);
//连接成功时回调
ws.onopen = function () {
console.log(‘连接成功‘);
… …
}
//关闭连接时回调
ws.onclose = function (e) {
console.log(‘连接断开‘);
… …
}
//出现异常时回调
ws.onerror = function (e) {
console.log(‘传输中发生异常‘);
… …
}
//接收到消息时回调
ws.onmessage=function(data){
console.log(“服务端发来的消息是:”+data);
… …
}
}
//执行函数,建立WebSocket连接
createSocket()
//发送消息
ws.send(“你好”);
//为WebSocket服务注解能接收URL请求的路径,相当于一个控制器
@ServerEndpoint("/websocket")
public classWebSocket {
/** 使用Hashtable来保存所有连接信息 */
private final static Hashtable<String,WebSocket> clients = new Hashtable<String, WebSocket>();
//连接建立成功调用的方法(服务器自动调用)
// session为某个客户端的连接会话,需要通过它来给客户端发送
@OnOpen
public void onOpen(Session session) {
log.info("新来一个会话" + "连接会话的id是:" + this.session.getId());
…. ….
}
//连接关闭调用的方法(服务器自动调用)
@OnClose
public void onClose() {
……
}
//接收文本消息(服务器自动调用)
@OnMessage
public void onMessage(String message,Session session) {
log.info("onMessage messageis "+message);
… …
}
//发生错误时调用
@OnError
public void onError(Session session,Throwable error) {
… …
}
/**
* 发送文本消息
*@param message要发送的消息内容
*@param sendToSession接收方
*/
public void sendMessage(String message,Session sendToSession) {
Basic basic =sendToSession.getBasicRemote();
try {
basic.sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
最后我们可以实现的结果可以达到如下:
启动客户端会见到如图7所示内容。在图中,点击“没账号,去注册”就可以前往注册页面。
图7 登录界面
一对一测试如下图8:
图8 一对一发送消息效果图
群聊如下图9:
图9 群发效果图
如图9所示群发时,最右边的用户发了张图片,左1和左2展示的用户均可以死接收到图片。
本文仅代表个人看法,如有不足和错误,欢迎指正。
小沄邮箱:
mengshengneng@163.com
本文出自 “msn_technology” 博客,请务必保留此出处http://bewweb.blog.51cto.com/12604306/1900201
原文:http://bewweb.blog.51cto.com/12604306/1900201