最近时间空闲,稍微研究了一下聊天系统的搭建,深入了解了它的实现原理,那就顺便整理一下成文章好了。我主要是写Android的,所以具体的分析会以移动端的聊天系统搭建为主。
一个聊天系统说复杂也不复杂,但是要实现一个稳健的系统,考虑的事情是非常多的。最基本的就是聊天协议的处理。常见的即时通讯协议有XMPP,Websocket,大公司一般会自己定义协议,如腾讯、网易之类的,他们用的都是自己的协议。我看的源码是Leancloud的即时通讯组件,他们的聊天是基于Websocket的,所以这篇博文的主题是Websocket。Leancloud的Android即时通讯组件里,Websocket的封装用的是Github上的一个开源项目,Nathan Rajlich的Java-Websocket,这是一个“100%Java写的极简Websocket客户端和服务端实现”。
文章的一个大致框架
- Websocket 协议的简单介绍
- 协议的封装与传输
- Websocket 客户端的实现
Websocket协议的简单介绍
Websocket是一种在单个TCP连接上进行全双工通讯的协议,双工(duplex)是指两台通讯设备之间,允许有双向的资料传输。全双工的是指,允许两台设备间同时进行双向资料传输。这是相对于半双工来说的,半双工不能同时进行双向传输,这期间的区别相当于手机和对讲机的区别,手机在讲话的同时也能听到对方说话,对讲机只能一个说完另一个才能说。
长话短说,在Websocket协议中,客户端和服务端只需要做一个握手的动作,就能形成一条通道,两者之间可以进行数据互相传送。
所以WebSocket协议分为两部分:
- 握手
- 数据传输
握手
客户端发送一个请求
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
服务器回应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/
握手时,客户端发送一个随机的Sec-WebSocket-Key,服务端根据这个key做一些处理,返回一个Sec-WebSocket-Accept的值给客户端,具体的原理在后面的文章中再具体说。
数据传输
这是Websocket的数据传输协议,聊天信息一般会按照这个协议的规则来传输,下图中的一整个东西称为一个数据帧,数据帧的成帧和解析是处理这个协议时最麻烦的一部分了。具体这个表怎么看可以参照
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
具体每一bit的意思
FIN 1bit 表示信息的最后一帧
RSV 1-3 1bit each 以后备用的 默认都为 0
Opcode 4bit 帧类型,稍后细说
Mask 1bit 掩码,是否加密数据,默认必须置为1 (这里很蛋疼)
Payload 7bit 数据的长度
Masking-key 1 or 4 bit 掩码
Payload data (x + y) bytes 数据
Extension data x bytes 扩展数据
Application data y bytes 程序数据
协议的封装与传输
1.握手协议的封装与传输
Handshake类是根据请求头
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
来封装的,由于这个请求头的字段顺序是随便的,我们可以用一个map来存储,发送消息时再写入Socket的输出流
下面是Handshakedata类,为了文章易读,简化了代码
public class Handshakedata
{
private byte[] content; //请求体,在握手协议中一般为空
private TreeMap<String, String> map; //用于存储请求头
}
初始化握手请求头,为了代码容易理解,稍微对Java-Websocket中的代码作了修改。
public Handshakedata postProcessHandshakeRequestAsClient(Handshakedata request)
{
request.put("Upgrade", "websocket");
request.put("Connection", "Upgrade");
request.put("Sec-WebSocket-Version", "8");
byte[] random = new byte[16];
this.reuseableRandom.nextBytes(random); //生成一个随机的Sec-WebSocket-Key
request.put("Sec-WebSocket-Key", Base64.encodeBytes(random));
return request;
}
生成数据帧,由于是通过Socket传输消息,最终传输的内容要写入到Socket的OutputStream中,需要一个把握手消息转换成bytebuffer的方法,再通过这个bytebuffer写入流中
public ByteBuffer createHandshake(Handshakedata handshakedata) {
StringBuilder bui = new StringBuilder(100);
bui.append("GET ");
bui.append(handshakedata.getResourceDescriptor());
bui.append(" HTTP/1.1");
bui.append("\r\n");
Iterator it = handshakedata.iterateHttpFields();
while (it.hasNext()) {
String fieldname = (String)it.next();
String fieldvalue = handshakedata.getFieldValue(fieldname);
bui.append(fieldname);
bui.append(": ");
bui.append(fieldvalue);
bui.append("\r\n");
}
bui.append("\r\n");
byte[] httpheader = Charsetfunctions.asciiBytes(bui.toString());
byte[] content = withcontent ? handshakedata.getContent() : null;
ByteBuffer bytebuffer = ByteBuffer.allocate((content == null ? 0 : content.length) + httpheader.length);
bytebuffer.put(httpheader);
bytebuffer.flip();
return bytebuffer;
}
最后写入Socket的流中
ByteBuffer buffer = (ByteBuffer)WebSocketClient.this.engine.outQueue.take(); //从消息队列中取出刚才转换好的bytebuffer
WebSocketClient.this.ostream.write(buffer.array(), 0, buffer.limit()); //this.ostream = this.socket.getOutputStream() Socket的输出流
WebSocketClient.this.ostream.flush(); //刷新,发送消息
以上是客户端发送握手协议的过程。
客户端接收服务端回应
服务端接收到客户端的握手请求后,需要返回响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/
收到这一段响应后,客户端需要比对Sec-WebSocket-Accept值,这个值表示服务器同意握手建立连接,是客户端传输过来的Sec-WebSocket-Key跟“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼接后,用SHA-1加密,并进行BASE-64编码得来的。
客户端收到Sec-WebSocket-Accept后,将本地的Sec-WebSocket-Key进行同样的编码,然后比对。
public Draft.HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response)
throws InvalidHandshakeException
{
if ((!request.hasFieldValue("Sec-WebSocket-Key")) || (!response.hasFieldValue("Sec-WebSocket-Accept"))) {
return Draft.HandshakeState.NOT_MATCHED;
}
//Sec-WebSocket-Key和Sec-WebSocket-Accept进行比对
String seckey_answere = response.getFieldValue("Sec-WebSocket-Accept");
String seckey_challenge = request.getFieldValue("Sec-WebSocket-Key");
seckey_challenge = generateFinalKey(seckey_challenge);
if (seckey_challenge.equals(seckey_answere))
return Draft.HandshakeState.MATCHED;
return Draft.HandshakeState.NOT_MATCHED;
}
//产生Sec-WebSocket-Accept的方法
private String generateFinalKey(String in) {
String seckey = in.trim();
String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
MessageDigest sh1;
try {
sh1 = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return Base64.encodeBytes(sh1.digest(acc.getBytes()));
}
2.数据的封装与传输
....待续