细说WebSocket – Node篇 – Barret Lee – 博客园
Home
Github
个人博客
Weibo
订阅
小胡子哥 (Barret Lee) console.log( ” Hi, I’m Barret, a Web Developer, try to be Excellent~ ” );
细说WebSocket – Node篇
2013-12-20 13:42 by Barret Lee, 22526 阅读, 20 评论, 收藏, 编辑
在上一篇提高到了 web 通信的各种方式,包括 轮询、长连接 以及各种 HTML5 中提到的手段。本文将详细描述 WebSocket协议 在 web通讯 中的实现。
一、WebSocket 协议
1. 概述
websocket协议允许不受信用的客户端代码在可控的网络环境中控制远程主机。该协议包含一个握手和一个基本消息分帧、分层通过TCP。简单点说,通过握手应答之后,建立安全的信息管道,这种方式明显优于前文所说的基于 XMLHttpRequest 的 iframe 数据流和长轮询。该协议包括两个方面,握手链接(handshake)和数据传输(data transfer)。
2. 握手连接
这部分比较简单,就像路上遇到熟人问好。
Client:嘿,大哥,有火没?(烟递了过去)
Server:哈,有啊,来~ (点上)
Client:火柴啊,也行!(烟点上,验证完毕)
握手连接中,client 先主动伸手:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
客户端发了一串 Base64 加密的密钥过去,也就是上面你看到的 Sec-WebSocket-Key。 Server 看到 Client 打招呼之后,悄悄地告诉 Client 他已经知道了,顺便也打个招呼。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Server 返回了 Sec-WebSocket-Accept 这个应答,这个应答内容是通过一定的方式生成的。生成算法是:
mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 这是算法中要用到的固定字符串
accept = base64( sha1( key + mask ) );
key 和 mask 串接之后经过 SHA-1 处理,处理后的数据再经过一次 Base64 加密。分解动作:
1. t = "GhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
-> "GhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
2. s = sha1(t)
-> 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6
0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea
3. base64(s)
-> "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
上面 Server 端返回的 HTTP 状态码是 101,如果不是 101 ,那就说明握手一开始就失败了~
下面就来个 demo,跟服务器握个手:
var crypto = require('crypto');
require('net').createServer(function(o){
var key;
o.on('data',function(e){
if(!key){
// 握手
// 应答部分,代码先省略
console.log(e.toString());
}else{
};
});
}).listen(8000);
客户端代码:
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onerror=function(e){
console.log(e);
};
运行代码
上面当然是一串不完整的代码,目的是演示握手过程中,客户端给服务端打招呼。在控制台我们可以看到:
看起来很熟悉吧,其实就是发送了一个 HTTP 请求,这个我们在浏览器的 Network 中也可以看到:
但是 WebSocket协议 并不是 HTTP 协议,刚开始验证的时候借用了 HTTP 的头,连接成功之后的通信就不是 HTTP 了,不信你用 fiddler2 抓包试试,肯定是拿不到的,后面的通信部分是基于 TCP 的连接。
服务器要成功的进行通信,必须有应答,往下看:
//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
var key;
o.on('data',function(e){
if(!key){
//握手
key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
key = crypto.createHash('sha1').update(key + WS).digest('base64');
o.write('HTTP/1.1 101 Switching Protocols\r\n');
o.write('Upgrade: websocket\r\n');
o.write('Connection: Upgrade\r\n');
o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
o.write('\r\n');
}else{
console.log(e);
};
});
}).listen(8000);
关于crypto模块,可以看看官方文档,上面的代码应该是很好理解的,服务器应答之后,Client 拿到 Sec-WebSocket-Accept ,然后本地做一次验证,如果验证通过了,就会触发 onopen 函数。
//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000/");
ws.onopen=function(e){
console.log("握手成功");
};
运行代码
可以看到
3. 数据帧格式
官方文档提供了一个结构图
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 ... |
+---------------------------------------------------------------+
第一眼瞟到这张图恐怕是要吐血,如果大学修改计算机网络这门课应该不会对这东西陌生,数据传输协议嘛,是需要定义字节长度及相关含义的。
FIN 1bit 表示信息的最后一帧,flag,也就是标记符
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 程序数据
每一帧的传输都是遵从这个协议规则的,知道了这个协议,那么解析就不会太难了,下面我就直接拿了次碳酸钴同学的代码。
4. 数据帧的解析和编码
数据帧的解析代码:
function decodeDataFrame(e){
var i=0,j,s,frame={
//解析前两个字节的基本数据
FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7,
PayloadLength:e[i++]&0x7F
};
//处理特殊长度126和127
if(frame.PayloadLength==126)
frame.length=(e[i++]<<8)+e[i++];
if(frame.PayloadLength==127)
i+=4, //长度一般用四字节的整型,前四个字节通常为长整形留空的
frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++];
//判断是否使用掩码
if(frame.Mask){
//获取掩码实体
frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
//对数据和掩码做异或运算
for(j=0,s=[];j<frame.PayloadLength;j++)
s.push(e[i+j]^frame.MaskingKey[j%4]);
}else s=e.slice(i,frame.PayloadLength); //否则直接使用数据
//数组转换成缓冲区来使用
s=new Buffer(s);
//如果有必要则把缓冲区转换成字符串来使用
if(frame.Opcode==1)s=s.toString();
//设置上数据部分
frame.PayloadData=s;
//返回数据帧
return frame;
}
decodeDataFrame Function
数据帧的编码:
//NodeJS
function encodeDataFrame(e){
var s=[],o=new Buffer(e.PayloadData),l=o.length;
//输入第一个字节
s.push((e.FIN<<7)+e.Opcode);
//输入第二个字节,判断它的长度并放入相应的后续长度消息
//永远不使用掩码
if(l<126)s.push(l);
else if(l<0x10000)s.push(126,(l&0xFF00)>>2,l&0xFF);
else s.push(
127, 0,0,0,0, //8字节数据,前4字节一般没用留空
(l&0xFF000000)>>6,(l&0xFF0000)>>4,(l&0xFF00)>>2,l&0xFF
);
//返回头部分和数据部分的合并缓冲区
return Buffer.concat([new Buffer(s),o]);
}
encodeDataFrame Function
有些童鞋可能没有明白,应该解析哪些数据。这的解析任务主要是服务端处理,客户端送过去的数据是二进制流形式的,比如:
var ws = new WebSocket("ws://127.0.0.1:8000/");
ws.onopen = function(){
ws.send("握手成功");
};
运行代码
Server 收到的信息是这样的:
一个放在Buffer格式的二进制流。而当我们输出的时候解析这个二进制流:
//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
var key;
o.on('data',function(e){
if(!key){
//握手
key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
key = crypto.createHash('sha1').update(key + WS).digest('base64');
o.write('HTTP/1.1 101 Switching Protocols\r\n');
o.write('Upgrade: websocket\r\n');
o.write('Connection: Upgrade\r\n');
o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
o.write('\r\n');
}else{
// 输出之前解析帧
console.log(decodeDataFrame(e));
};
});
}).listen(8000);
那输出的就是一个帧信息十分清晰的对象了:
5. 连接的控制
上面我买了个关子,提到的Opcode,没有详细说明,官方文档也给了一张表:
|Opcode | Meaning | Reference |
-+--------+-------------------------------------+-----------|
| 0 | Continuation Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 1 | Text Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 2 | Binary Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 8 | Connection Close Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 9 | Ping Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 10 | Pong Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
decodeDataFrame 解析数据,得到的数据格式是:
{
FIN: 1,
Opcode: 1,
Mask: 1,
PayloadLength: 4,
MaskingKey: [ 159, 18, 207, 93 ],
PayLoadData: '握手成功'
}
那么可以对应上面查看,此帧的作用就是发送文本,为文本帧。因为连接是基于 TCP 的,直接关闭 TCP 连接,这个通道就关闭了,不过 WebSocket 设计的还比较人性化,关闭之前还跟你打一声招呼,在服务器端,可以判断frame的Opcode:
var frame=decodeDataFrame(e);
console.log(frame);
if(frame.Opcode==8){
o.end(); //断开连接
}
客户端和服务端交互的数据(帧)格式都是一样的,只要客户端发送 ws.close()
, 服务器就会执行上面的操作。相反,如果服务器给客户端也发送同样的关闭帧(close frame):
o.write(encodeDataFrame({
FIN:1,
Opcode:8,
PayloadData:buf
}));
客户端就会相应 onclose 函数,这样的交互还算是有规有矩,不容易出错。
二、注意事项
1. WebSocket URIs
很多人可能只知道 ws://text.com:8888
,但事实上 websocket 协议地址是可以加 path 和 query 的。
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
如果使用的是 wss 协议,那么 URI 将会以安全方式连接。 这里的 wss 大小写不敏感。
2. 协议中”多余”的部分(吐槽)
握手请求中包含Sec-WebSocket-Key字段,明眼人一下就能看出来是websocket连接,而且这个字段的加密方式在服务器也是固定的,如果别人想黑你,不会太难。
再就是那个 MaskingKey 掩码,既然强制加密了(Mask为1表示加密,加密方式就是 MaskingKey 与 PayLoadData 进行异或处理),还有必要让开发者处理这个东西么?直接封装到内部不就行了?
3. 与 TCP 和 HTTP 之间的关系
WebSocket协议是一个基于TCP的协议,就是握手链接的时候跟HTTP相关(发了一个HTTP请求),这个请求被Server切换到(Upgrade)websocket协议了。websocket把 80 端口作为默认websocket连接端口,而websocket的运行使用的是443端口。
三、参考资料
- tools.ietf.org/html/rfc645… web standard – The WebSocket Protocol
- www.w3.org/TR/websocke… W3.ORG – WebSockets
四、特别感谢
再次感谢 次碳酸钴 跟我交流了几个小时 : ),本文部分 node 代码参考自他的博客。
下次将以php作为后台,讲解websocket的相关知识。
版权声明: 署名-非商业性使用-禁止演绎 3.0 国际(CC BY-NC-ND 3.0)
#1楼 simonleung 2013-12-20 14:31
websocket 是用http 的onupgrade事件開始進行握手的
不是用net的ondata.
nodejs.org/api/http.ht… 支持(0) 反对(0)#2楼[楼主] Barret Lee 2013-12-20 14:37
@ simonleung
net 的 data 比 http 的 onupgrade 更底层。
从websocket协议的说明到测试的结果展示,也没有任何问题。 支持(0)反对(0) http://pic.cnblogs.com/face/387325/20150805014702.png#3楼 尼玛范爷 2013-12-20 21:21
写的真好,佩服 支持(0)反对(0) http://pic.cnblogs.com/face/395638/20140818132358.png
#4楼 布尔 2013-12-20 22:04
用node-webkit做的时候不知道为什么,连接数一多(100个以上)就接入很慢 支持(0)反对(0) http://pic.cnblogs.com/face/u17705.jpg
#5楼 Tony二师弟 2013-12-21 11:56
正在找这类文章 支持(0)反对(0) http://pic.cnblogs.com/face/348990/20160120104927.png
#6楼 Ace8793 2013-12-22 22:00
mark 支持(0)反对(0) http://pic.cnblogs.com/face/377458/20161107194642.png
#7楼 flyher 2013-12-23 08:26
@BarretLee
好文章,周一才看到,真是相见恨晚,收藏备用。 支持(0)反对(0) http://pic.cnblogs.com/face/u218282.jpg#8楼 dotNetDR_ 2013-12-24 15:26
#9楼 微积分g 2013-12-26 21:07
#10楼 it蓝精灵 2014-03-26 20:08
#11楼 题叶 2014-04-02 10:54
#12楼[楼主] Barret Lee 2014-04-02 16:38
@ 题叶
我这里好的耶,你刷新下是不是就好了? 支持(0)反对(0) http://pic.cnblogs.com/face/387325/20150805014702.png#13楼 mcarzx 2014-04-21 17:46
1、t = “GhlIHNhbXBsZSBub25jZQ==”,串最前面少了一个d
2、console.log(crypto.createHash(‘sha1’).update(‘dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11’).digest(‘sha1’)); //输出:<SlowBuffer b3 7a 4f 2c c0 62 4f 16 90 f6 46 06 cf 38 59 45 b2 be c4 ea>,不像你的,前面带0x,你是怎么弄的?3、解码部分:if(frame.PayloadLength==126) frame.length=(e[2]<<8) + e[3], i=4; //其中的frame.length,好像应该是frame.Payloadlength。下一行中也是。看了一下,次碳酸钴,那边已经改了。
4、编码部分:s.push(126,(l&0xFF00)>>2,l&0xFF); 其中的>>2,好像应该是>>8。下一行类似。 支持(0)反对(0)
#14楼 马斯塔 2014-11-12 21:03
#15楼[楼主] Barret Lee 2014-11-12 21:51
@ 马斯塔
页面 A 是和该页面一个异域的 iframe B 通信。
postMessage 和 onmessage 都是在同一个 window 对象触发的。在 A 获取 B 的 window 句柄,使用 B.contentWindow.postMessage 发送信息到 B,B 中有 onmessage 监听。
B 到 A 发送消息的原理是,B 获取 A 的 window 句柄,使用 A.parent.postMessage 发送信息到 A,A 中有 onmessage 监听。
搞清楚一个原理,一定是同一个 window 向自身发送信息,只是我们在另一个 window 上拿到这个 window 句柄,如 iframe.contentWindow,或者 iframe 中获取其服窗口 window.parent。
看不懂不是你理解能力有问题,是你没用心看懂这些概念。还是不懂请自己阅读 MSDN/MDN 文档。 支持(0)反对(0) http://pic.cnblogs.com/face/387325/20150805014702.png
#16楼 riskers 2015-03-04 14:24
引用key 和 mask 串接之后经过 SHA-1 处理,处理后的数据再经过一次 Base64 加密。
#17楼 CoderZ 2015-06-29 12:05
请问小胡子哥,刚刚实现了你的方法都好用。又看了一下socket.io,却发现socket.io的请求头中没有丝毫的websocket信息,而它却是基于websocket,不知这是什么原因呢? 支持(0)反对(0) http://pic.cnblogs.com/face/579305/20131227132708.png
#18楼 挨踢男.Orz 2015-07-20 15:55
mark 支持(0)反对(0) http://pic.cnblogs.com/face/520225/20131008114345.png
#19楼 gongmaolan123 2017-01-13 12:59
GoEasy web三步轻松实现web实时推送
1. 引入goeasy.js2. 客户端订阅,
Var goeasy = new GoEasy({appkey:’your appkey’});
goeasy.subscribe(channel:”your channel”, onMessage:function(message)
{alert(‘received message’+ message.content)}
)3. 三种推送方式
Javascript: goeasy.publish({channel:’your channel’, message:’your publish msg’});Java SDK:
GoEasy goeasy = new GoEasy(“appkey”);
goeasy.publish(“your channel”,”your msg”);RestAPI: goeasy.io/goeasy/publ…
三步轻松实现web推送及接收。官网: goeasy.io,文档齐全 支持(0)反对(0)#20楼38077282017/10/11 18:57:09 Cinux丶 2017-10-11 18:57
刷新评论
刷新页面
返回顶部 注册用户登录后才能发表评论,请
登录 或
注册,
访问网站首页。
【推荐】50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
·
技术教育的兴起
»
更多新闻…
最新知识库文章:
»
更多知识库文章…
博客迁址:http://barretlee.com
围脖:
@Barret,李靖,小胡子哥 微信公众号:
小胡子哥 😃
Github:
barretlee
现状:淘宝UED-F2E & 慢慢沉淀
声明:无特殊说明文章皆为原创,因常修改或删除陈旧、错误内容,转载请注明源地址。文章为个人观点,不代表公司。 您是第
位访客,欢迎您~ 昵称:
Barret Lee
园龄:
5年7个月
荣誉:
推荐博客
粉丝:
2731
最新随笔
- Nginx 配置简述
- 谈一谈我在阿里的成长
- 揭秘 0.1 + 0.2 != 0.3
- 如何做好一名实习生
- 谈谈我这三年在技术上的成长
- 工作五年,后面四年重复着第一年的活儿?
- ECMAScript 6 扫盲
- 当前端也拥有 Server 的能力
- 简述 OAuth 2.0 的运作流程
- 近几年前端技术盘点以及 2016 年技术发展方向
- NodeJS的代码调试和性能调优
- 新应用上线 Snippet
- 这两天说到的苹果软件中毒是个什么情况?
- 网站的SEO以及它和站长工具的之间秘密
- 博客已经迁移至 http://barretlee.com/entry/,时而同步分享到这里
最新评论
- Re:细说websocket – php篇
学习了,感谢 — stormpass - Re:谈一谈我在阿里的成长
小胡子哥,我们会见面的~ — wbxjiayou - Re:细说WebSocket – Node篇
@simonleung http模块是基于net写的,net没有这个事件啊 — Cinux丶 - Re:前端代码异常日志收集与监控
连浏览器源码都搬出来了,不得不赞 — -云- - Re:javascript链式调用的实现方式
您真厉害!自己很不足,还需努力。 — 注定要被代码虐
随笔档案
- 2016年11月(1)
- 2016年10月(3)
- 2016年7月(3)
- 2016年2月(1)
- 2016年1月(2)
- 2015年10月(1)
- 2015年9月(4)
- 2015年8月(6)
- 2015年7月(1)
- 2015年5月(2)
- 2015年4月(4)
- 2014年12月(1)
- 2014年11月(1)
- 2014年10月(1)
- 2014年9月(1)
- 2014年8月(4)
- 2014年7月(1)
- 2014年5月(6)
- 2014年4月(4)
- 2014年3月(8)
- 2014年2月(7)
- 2014年1月(4)
- 2013年12月(6)
- 2013年11月(5)
- 2013年10月(2)
- 2013年9月(3)
- 2013年8月(3)
- 2013年7月(3)
- 2013年6月(6)
- 2013年5月(5)
- 2013年4月(12)
- 2013年3月(6)
- 2012年9月(2)
javascript
- ajaxian
- alistapart
- developer.mozilla.org
- diveintohtml5
- ecmascript
- Eric Meyer
- es5
- nanto
- perfectionkills
- ppk
- sitepoint
- webfx
- webkit系的文档
- 高性能网站建设
日历
| ||||||
日 | 一 | 二 | 三 | 四 | 五 | 六 |
---|---|---|---|---|---|---|
29 | 30 | 31 | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
随笔分类
- AJAX(5)
- Cross Domain(3)
- Design patterns(4)
- DOM(2)
- ECMAScript规范(8)
- HTML[5]+CSS[3](17)
- JavaScript(62)
- Javascript plugins(7)
- jQuery and plugins(1)
- Linux(2)
- Node(5)
- Others(52)
- PHP(4)
- translation(4)
推荐排行榜
- 1. 从登录框看前端(136)
- 2. 近几年前端技术盘点以及 2016 年技术发展方向(85)
- 3. 前端代码异常日志收集与监控(63)
- 4. 我在阿里这仨月(62)
- 5. JavaScript异步编程原理(43)
- 6. JavaScript模板引擎原理,几行代码的事儿(36)
- 7. 修改Hosts为何不生效,是DNS缓存?(29)
- 8. 如何让你的JavaScript代码更加语义化(25)
- 9. 细说WebSocket – Node篇(23)
- 10. JavaScript多文件下载(23)
阅读排行榜
- 1. 细说websocket – php篇(40514)
- 2. 从登录框看前端(29573)
- 3. PJAX的实现与应用(27324)
- 4. 让浏览器不再显示 https 页面中的 http 请求警报(25059)
- 5. JavaScript模板引擎原理,几行代码的事儿(23234)
- 6. 细说WebSocket – Node篇(22523)
- 7. 前端代码异常日志收集与监控(21883)
- 8. JavaScript多文件下载(21859)
- 9. JavaScript异步编程原理(17802)
- 10. 修改Hosts为何不生效,是DNS缓存?(16597)
css
friends
Tools
- cleancss
- css-validator
- proxyie.cn/
- jsbeautifier
- JSLint
- Packer-Decoder
- regexper
- RunJS
- stackoverflow
- User Agent
- 在线格式化HTML
准备做的~
- 前端架构思想学习
- ECMAScript6研究
- ECMA-262细读
最近在…
- 微分享
- 动画库
- →_→上班←_←
广告
- 淘宝UED前端实习生,你的简历呢?发到这里barret.china@gmail.com吧~
- 最近搞的微分享
- 当前 201 人群聊 0
注意:聊天日志不会保存,请注意备份. 提示:点击头像可进入私聊
上面是条广告,接着聊。 发送 取个名字: 确认