百科问答小站 logo
百科问答小站 font logo



QQ 等即时通讯软件的消息传输的技术原理是什么? 第1页

  

user avatar   eechen-php 网友的相关建议: 
      

题主感兴趣的应该是即时通讯软件的开发.

最简单的即时通讯软件(IM)就是聊天室(Chat Room).

聊天室可以理解为一个包含所有在线用户的QQ群.

服务器会把用户发布的消息广播推送(Push)给所有在线的用户.

这里涉及到即时通讯中的关键操作就是:服务器推(Server Push).

这里以PHP Swoole为例,实现基于现代浏览器WebSocket全双工连接的服务器推:

       浏览器: var ws = new WebSocket("ws://localhost:8080"); ws.onmessage = function(e) {  var msg = JSON.parse(e.data);  console.log(msg.fd);  console.log(msg.data);  console.log(msg.time); }   服务器: $ws = new SwooleWebsocketServer('localhost', 8080); $ws->on('message', function($ws, $frame) {  // 消息格式可以用JSON  $msg = json_encode(array(   'fd' => $frame->fd,   'data' => $frame->data,   'time' => date('Y年m月d日 H:i:s')  ));  // 【服务器推】推送消息给所有客户端  foreach($ws->connections as $fd) { $ws->push($fd, $msg); } });     

上面服务器是向所有连接fd推送消息.

向某个用户对应的连接fd推送,那就是私聊.

向某几个用户对应的连接fd推送,那就是群聊.

道理都类似,关键是建立用户编号uid和连接编号fd的关系.


比如用户1向用户2发送信息"你好":

       浏览器: ws.send(JSON.stringify({"uid": 2, "msg": "你好"})); 服务器: $ws->push($fd, $msg);   服务器推送消息时,需要先判断对方是否在线,是否是其好友. 显然,这些关系判断需要访问数据库如MySQL.     

服务器可以用Redis存储用户2对应的若干个连接(电脑和手机),从而根据用户编号uid找到用户对应的连接编号fd.

比如用户2对应的连接编号为4和6:

       key(uid:2:fd) => value(4, 6)     

Swoole推送消息给用户2时就是这样:

       foreach(array(4, 6) as $fd) { $ws->push($fd, $msg); }     

向群组(多个用户多个连接)推送消息也是同样道理.


用户编号uid和连接编号fd的绑定和唯一约束:

数据库用户表添加一个fd字段(唯一约束),用于记录用户对应的连接编号fd.

唯一约束的fd字段没有内容时,可以使用用户编号的负数作为默认值,做到不违反唯一约束.

用户登录时: onopen 填充fd字段,缓存新的fd到Redis中,旧的fd通过$ws->close($fd)触发onclose事件使其下线.

用户退出时: onclose 清空fd字段,删除缓存Redis中用户对应的该fd.

心跳检测关闭连接时: 也会触发onclose事件.

对于其他异常的连接:

异常关闭的连接的fd如果还在用户表中,下次该fd插入用户表时,因为唯一约束将会导致插入失败.这时需要先删除该fd对应的用户在缓存Redis(或MySQL内存表)中的fd,然后删除该用户在MySQL用户表中的异常fd,最后插入fd到新用户的MySQL记录中.


QQ支持1个账号两个客户端(手机+电脑)同时登录,即允许手机电脑同步在线.

这样用户表fd字段就需要存储2个fd数字,这时就需要自己配合FULLTEXT KEY实现唯一约束.

假设用户1分配得到一个值为100的fd,全文检索用户表的fd字段,查看是否存在包含100的记录:

SELECT * FROM user WHERE MATCH(fd) AGAINST('100' IN BOOLEAN MODE) FOR UPDATE;

假设查询发现用户2的fd字段为'50,100',这时应该删除掉100:

UPDATE user SET fd = '50' WHERE id = 2;

$redis->set('uid:2:fd', '50');

然后往用户1的fd字段加入100(假设用户1本来已经存在一个值为20的fd):

SELECT * FROM user WHERE id = 1 FOR UPDATE;

UPDATE user SET fd = '20,100' WHERE id = 1;

$redis->set('uid:1:fd', '20,100');


心跳检测机制用于回收异常断开的连接.
服务器端WebSocket连接的心跳检测,Swoole已经帮开发者实现,Swoole提供了2个参数:
heartbeat_check_interval: 默认30(心跳检测间隔,单位秒.Swoole会轮询所有TCP连接,将超过heartbeat_idle_time的连接关掉)
heartbeat_idle_time: 默认60(TCP连接的最大闲置时间,单位秒.如果某fd最后一次发包距离现在的时间超过这个值,Swoole会把这个连接关闭)
至于客户端浏览器上的WebSockets心跳检测,则需要开发者自行实现,比如:

       https://github.com/zimv/WebSocketHeartBeat/blob/master/heartBeat.js     


QQ是支持离线消息存储的,也就是如果对方不在线,消息将被持久化到数据库中.
等到对方上线拉取到消息后,数据库里的消息,将会在某个时间后删除.
不过QQ还为用户提供了一个付费的消息漫游服务.
也就是说,消息的存储和处理,仍然离不开传统的数据库CRUD操作.
QQ用户的注册/登录,好友的查找/添加/删除/分组,这些也离不开传统数据库操作.
作为区别,好友上线/下线通知,私聊/群聊(组聊),这些则依赖服务器推.

QQ还支持文件传输,在浏览器里,如果你不用WebRTC提供的点对点P2P的RTCDataChannel技术,可以这样做:用户通过HTTP把文件上传到服务器,然后服务器把文件的URL(字符串)推送给接收文件的用户.
这样做的话,对服务器的带宽和存储压力比较大,传一些小文件(比如图片和文档)还可以,传大文件就太消耗服务器了.

QQ还支持语音和视频聊天,显然,这种场景用点对点的P2P技术最合适,在浏览器里,可以用WebRTC实现.WebRTC里实现点对点的P2P通信,需要一个防火墙打洞服务器ICE(STUN+TURN)来实现NAT穿透:

       https://github.com/coturn/coturn     

WebRTC信令(signaling)的传输可以用WebSocket.
WebRTC已经很大程度简化了P2P实时通信的编程,但对普通Web开发者还是有一定的难度,普通Web开发者还必须熟悉JS会话建立协议JSEP的建立过程,才能跑通WebRTC服务.


最后有一个问题就是,腾讯QQ具有这么大规模的在线用户,不可能所有用户的连接fd都在同一台服务器上,这时就需要实现跨服务器消息推送.也就是Redis里不仅要记录用户对应的fd,而且还要记录这些fd所在的服务器.

比如服务器A上的用户张三向服务器B上的用户李四发送信息. 利用Redis的订阅发布功能,B服务器异步订阅,一旦A服务器发布消息到Redis,B服务器就会即时拿到,然后推送消息给B服务器上的用户李四即可.


还有个问题就是,一些老旧的浏览器比如IE8和IE9都不支持WebSocket协议,这时只能退化到HTTP,比如可以用AJAX长轮询,用PHP+Swoole实现大概可以这样:

       浏览器(接收消息) => AJAX长轮询 => PHP7-Swoole => 异步订阅 => Redis <= 同步发布 <= PHP7-FPM(消息入库) <= 浏览器(发送消息)     

也就是PHP-FPM跟PHP-Swoole通过Redis实现通信.
好处是能利用PHP-Swoole内置的事件驱动异步无阻塞的HTTP服务器和Redis客户端等.

2019/07/01 补充:

用一个字典保存用户(user)和连接(fd)的映射关系,然后根据用户找到对应的连接,从而向该用户推送消息.思路是可行的,但建议引入一个PubSub订阅发布中间件比如Redis,使用订阅发布的模式实现私聊和群聊.
私聊:
用户subscribe订阅自己的用户频道,比如(webim:user:1000:channel).
其他用户给该用户发消息时,向该用户的频道publish发布消息即可.
该用户的订阅事件message会自动触发,在该事件中可以推送消息给用户.
群聊:
用户subscribe订阅自己的群频道,比如(webim:group:2000:channel).
其他用户向该群publish发布消息时,
该用户的订阅事件message会自动触发,在该事件中可以推送群消息给用户.
说明:订阅事件message就是subscribe操作的回调函数.




  

相关话题

  链表和数组的插入删除时间复杂度都是o(n),为什么教材网络上说链表效率高? 
  参加ICPC国际编程竞赛的大学生到底有多厉害,在校生该如何准备? 
  你见过哪些奇趣的代码注释? 
  你对C#最绝望的时刻是什么? 
  计算机执行一条指令要多长时间? 
  有了 IP 地址,为什么还要用 MAC 地址? 
  对C++的匿名函数应具有怎样的态度? 
  相对论刚提出时,号称全球能完全理解的人不超过十人,现在却成为理工科必修课程,是我们智商提高了吗? 
  为什么大学不教javaScript? 
  大龄门外汉如何进入软件开发行业? 

前一个讨论
有什么好看的银行卡?
下一个讨论
美国留学生用什么信用卡好呢?





© 2024-05-19 - tinynew.org. All Rights Reserved.
© 2024-05-19 - tinynew.org. 保留所有权利