开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用

1、前言 RTP/RTCP协议是流媒体通信的基石。RTP协议定义流媒体数据在互联网上传输的数据包格式,而RTCP协议则负责可靠传输、流量控制和拥塞控制等服务质量保证。在WebRTC项目中,RTP/RTCP模块作为传输模块的一部分,负责对发送端采集到的媒体数据进行进行封包,然后交给上层网络模块发送;在接收端RTP/RTCP模块收到上层模块的数据包后,进行解包操作,最后把负载发送到解码模块。因此,RTP/RTCP 模块在WebRTC通信中发挥非常重要的作用。 本文在深入研究WebRTC源代码的基础上,以Video数据的发送和接收为例,力求用简洁语言描述RTP/RTCP模块的实现细节,为进一步深入掌握WebRTC打下良好基础。 2、RTP/RTCP协议概述 RTP协议是Internet上针对流媒体传输的基础协议,该协议详细说明在互联网上传输音视频的标准数据包格式。RTP协议本身只保证实时数据的传输,RTCP协议则负责流媒体的传输质量保证,提供流量控制和拥塞控制等服务。在RTP会话期间,各参与者周期性彼此发送RTCP报文。报文中包含各参与者数据发送和接收等统计信息,参与者可以据此动态控制流媒体传输质量。 RFC3550 定义RTP/RTCP协议的基本内容,包括报文格式、传输规则等。除此之外,IETF还定义一系列扩展协议,包括RTP协议基于档次的扩展,和RTCP协议基于报文类型的扩展,等等。详细内容可参考:《学习RFC3550:RTP/RTCP实时传输协议基础知识》。 3、WebRTC的数据处理和传输过程 WebRTC对外提供两个线程:Signal和Worker,前者负责信令数据的处理和传输,后者负责媒体数据的处理和传输。在WebRTC内部,有一系列线程各司其职,相互协作完成数据流管线。下面以Video数据的处理流程为例,说明WebRTC内部的线程合作关系。 WebRTC线程关系和数据Pipline:   如上图所示,Capture线程从摄像头采集原始数据,得到VideoFrame;Capture线程是系统相关的,在Linux系统上可能是调用V4L2接口的线程,而在Mac系统上可能是调用AVFoundation框架的接口。接下来原始数据VideoFrame从Capture线程到达Worker线程,Worker线程起搬运工的作用,没有对数据做特别处理,而是转发到Encoder线程。Encoder线程调用具体的编码器(如VP8, H264)对原始数据VideoFrame进行编码,编码后的输出进一步进行RTP封包形成RTP数据包。然后RTP数据包发送到Pacer线程进行平滑发送,Pacer线程会把RTP数据包推送到Network线程。最终Network线程调用传输层系统函数把数据发送到网络。 在接收端,Network线程从网络接收字节流,接着Worker线程反序列化为RTP数据包,并在VCM模块进行组帧操作。Decoder线程对组帧完成的数据帧进行解码操作,解码后的原始数据VideoFrame会推送到IncomingVideoStream线程,该线程把VideoStream投放到render进行渲染显示。至此,一帧视频数据完成从采集到显示的完整过程。 在上述过程中,RTP数据包产生在发送端编码完成后,其编码输出被封装为RTP报文,然后经序列化发送到网络。在接收端由网络线程收到网络数据包后,经过反序列化还原成RTP报文,然后经过解包得到媒体数据负载,供解码器进行解码。RTP报文在发送和接收过程中,会执行一系列统计操作,统计结果作为数据源供构造RTCP报文之用。RTP报文构造、发送/接收统计和RTCP报文构造、解析反馈,是接下来分析的重点。 4、RTP报文发送和接收 RTP报文的构造和发送发生在编码器编码之后、网络层发送数据包之前,而接收和解包发生在网络层接收数据之后、解码器编码之前。本节详细分析这两部分的内容。 1RTP报文构造和发送 下图描述发送端编码之后RTP报文的构造和发送过程,涉及三个线程:Encoder、Pacer和Network,分别负责编码和构造RTP报文,平滑发送和传输层发送。下面详细描述这三个线程的协同工作过程。 RTP报文构造和发送:   Encode线程调用编码器(比如VP8)对采集到的Raw VideoFrame进行编码,编码完成以后,其输出EncodedImage通过回调到达VideoSendStream::Encoded()函数,进而通过PayloadRouter路由到ModuleRtpRtcpImpl::SendOutgoingData()。接下来,该函数向下调用RtpSender::SendOutgoingData(),进而调用RtpSenderVideo::SendVideo()。该函数对EncodedImage进行打包,然后填充RTP头部构造RTP报文;如果配置了FEC,则进一步封装为FEC报文。最后返回RtpSender::SendToNetwork()进行下一步发送。 RtpSender::SendToNetwork()函数把报文存储到RTPPacketHistory结构中进行缓存。接下来如果开启PacedSending,则构造Packe发送到PacedSender进行排队,否则直接发送到网络层。 Pacer线程周期性从队列中获取Packet,然后调用PacedSender::SendPacket()进行发送,接下来经过ModuleRtpRtcpImpl到达RtpSender::TimeToSendPacket()。该函数首先从RtpPacketHistory缓存中拿到Packet的负载,然后调用PrepareAndSendPacket()函数:更新RtpHeader的相关域,统计延迟和数据包,调用SendPacketToNetwork()把报文发送到传输模块。 Network线程则调用传输层套接字执行数据发送操作。至此,发送端的RTP构造和发送流程完成。需要注意的是,在RtpSender中进行Rtp发送后,会统计RTP报文相关信息。这些信息作为RTCP构造SR/RR报文的数据来源,因此非常重要。 2RTP报文接收和解析 在接收端,RTP报文的接收和解包操作主要在Worker线程中执行,RTP报文从Network线程拿到后,进入Worker线程,经过解包操作,进入VCM模块,由Decode线程进行解码,最终由Render线程进行渲染。下图描述RTP报文在Worker线程中的处理流程。 RTP报文接收和解析:   RTP数据包经网络层到达Call对象,根据其SSRC找到对应的VideoReceiveStream,通过调用其DeliverRtp()函数到RtpStreamReceiver:eliverRtp()。该函数首先解析数据包得到RTP头部信息,接下来执行三个操作:1.码率估计;2.继续发送数据包;3.接收统计。码率估计模块使用GCC算法估计码率,构造REMB报文,交给RtpRtcp模块发送回发送端。而接收统计则统计RTP接收信息,这些信息作为RTCP RR报文的数据来源。下面重点分析接下来的数据包发送流程。 RtpStreamReceiver::ReceivePacket()首先判断数据包是否是FEC报文,如果是则调用FecReceiver进行解包,否则直接调用RtpReceiver::IncomingRtpPacket()。该函数分析RTP报文得到通用的RTP头部描述结构,然后调用RtpReceiverVideo:arseRtpPacket()进一步得到Video相关信息和负载,接着经过回调返回RtpStreamReceiver对象。该对象把Rtp描述信息和负载发送到VCM模块,继续接下来的JitterBuffer缓存和解码渲染操作。 RTP报文解包过程是封包的逆过程,重要的输出信息是RTP头部描述和媒体负载,这些信息是下一步JitterBuffer缓存和解码的基础。另外对RTP报文进行统计得到的信息则是RTCP RR报文的数据来源。 5、RTCP报文发送和接收 RTCP协议是RTP协议的控制下可以,负责流媒体的服务质量保证。比较常用的RTCP报文由发送端报告SR和接收端报告RR,分别包含数据发送统计信息和数据接收信息。这些信息对于流媒体质量保证非常重要,比如码率控制、负载反馈,等等。其他RTCP报文还有诸如SDES、BYE、SDES等,RFC3550对此有详细定义。 本节重点分析WebRTC内部RTCP报文的构造、发送、接收、解析、反馈等流程。需要再次强调的是,RTCP报文的数据源来自RTP报文发送和接收时的统计信息。在WebRTC内部,RTCP报文的发送采取周期性发送和及时发送相结合的策略:ModuleProcess线程周期性发送RTCP报文;而RtpSender则在每次发送RTP报文之前都判断是否需要发送RTCP报文;另外在接收端码率估计模块构造出REMB报文后,通过设置超时让ModuleProcess模块立即发送RTCP报文。 1RTCP报文构造和发送 在发送端,RTCP以周期性发送为基准,辅以RTP报文发送时的及时发送和REMB报文的立即发送。发送过程主要包括Feedback信息获取、RTCP报文构造、序列化和发送。下图描述了RTCP报文的构造和发送过程。 RTCP报文构造和发送:   ModuleProcess线程周期性调用ModuleRtpRtcpImpl:rocess()函数,该函数通过RTCPSender::TimeToSendRtcpReport()函数确定当前是否需要立即发送RTCP报文。若是,则首先从RTPSender::GetDataCounters()获取RTP发送统计信息,然后调用RTCPSender::SendRTCP(),接着是SendCompoundRTCP()发送RTCP组合报文。关于RTCP组合报文的定义,请参考文献[1]。 在SendCompoundRTCP()函数中,首先通过PrepareReport()确定将要发送何种类型的RTCP报文。然后针对每一种报文,调用其构造函数(如构造SR报文为BuildSR()函数),构造好的报文存储在PacketContainer容器中。最后调用SendPackets()进行发送。 接下来每种RTCP报文都会调用各自的序列化函数,把报文序列化为网络字节流。最后通过回调到达PacketContainer::OnPacketReady(),最终把字节流发送到传输层模块:即通过TransportAdapter到达BaseChannel,Network线程调用传输层套接字API发送数据到网络。 RTCP报文的构造和发送过程总体不是很复杂,最核心的操作就是获取数据源、构造报文、序列化和发送。相对来说构造报文和序列化比较繁琐,基于RFC定义的细节进行。 2RTCP报文接收和解析 接收端的RTCP报文接收和解析过程如下图所示:   在接收端,RTCP报文的接收流程和RTP一样,经过网络接收之后到达Call对象,进而通过SSRC找到VideoReceiveStream,继而到达RtpStreamReceiver。接下来RTCP报文的解析和反馈操作都在ModuleRtpRtcpImpl::IncomingRtcpPacket()函数中完成。该函数首先调用RTCPReceiver::IncomingRtcpPacket()解析RTCP报文,得到RTCPPacketInformation对象,然后调用 TriggerCallbacksFromRTCPPacket(),触发注册在此处的各路观察者执行回调操作。 RTCPReceiver::IncomingRtcpPacket()使用RTCPParser解析组合报文,针对每一种报文类型,调用对应的处理函数(如处理SDES的HandleSDES函数),反序列化后拿到报文的描述结构。最后所有报文综合在一起形成RTCPPacketInformation对象。该对象接下来作为参数调用TriggerCallbacksFromRTCPPacket()函数触发回调操作,如处理NACK的回调,处理SLI的回调,处理REMB的回调,等等。这些回调在各自模块控制流媒体数据的编码、发送、码率等服务质量保证,这也是RTCP报文最终起作用的地方。 至此,我们分析了RTCP报文发送和接收的整个流程。 6、小结 本文在深入分析WebRTC源代码的基础上,结合流程图描述出RTP/RTCP模块的实现流程,在关键问题上(如RTCP报文的数据来源)进行深入细致的研究。为进一步深入掌握WebRTC的实现原理和细节打下良好基础。 (原文链接:http://www.cnblogs.com/lingyunhu/p/rtc86.html) 附录:更多实时音视频技术文章 [1] 开源实时音视频技术WebRTC的文章: 《开源实时音视频技术WebRTC的现状》 《简述开源实时音视频技术WebRTC的优缺点》 《访谈WebRTC标准之父:WebRTC的过去、现在和未来》 《良心分享:WebRTC 零基础开发者教程(中文)[附件下载]》 《WebRTC实时音视频技术的整体架构介绍》 《新手入门:到底什么是WebRTC服务器,以及它是如何联接通话的?》 《WebRTC实时音视频技术基础:基本架构和协议栈》 《浅谈开发实时视频直播平台的技术要点》 《[观点] WebRTC应该选择H.264视频编码的四大理由》 《基于开源WebRTC开发实时音视频靠谱吗?第3方SDK有哪些?》 《开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《实时通信RTC技术栈之:视频编解码》 《开源实时音视频技术WebRTC在Windows下的简明编译教程》 《网页端实时音视频技术WebRTC:看起来很美,但离生产应用还有多少坑要填?》 [2] 实时音视频开发的其它精华资料: 《专访微信视频技术负责人:微信实时视频聊天技术的演进》 《即时通讯音视频开发(一):视频编解码之理论概述》 《即时通讯音视频开发(二):视频编解码之数字视频介绍》 《即时通讯音视频开发(三):视频编解码之编码基础》 《即时通讯音视频开发(四):视频编解码之预测技术介绍》 《即时通讯音视频开发(五):认识主流视频编码技术H.264》 《即时通讯音视频开发(六):如何开始音频编解码技术的学习》 《即时通讯音视频开发(七):音频基础及编码原理入门》 《即时通讯音视频开发(八):常见的实时语音通讯编码标准》 《即时通讯音视频开发(九):实时语音通讯的回音及回音消除概述》 《即时通讯音视频开发(十):实时语音通讯的回音消除技术详解》 《即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解》 《即时通讯音视频开发(十二):多人实时音视频聊天架构探讨》 《即时通讯音视频开发(十三):实时视频编码H.264的特点与优势》 《即时通讯音视频开发(十四):实时音视频数据传输协议介绍》 《即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况》 《即时通讯音视频开发(十六):移动端实时音视频开发的几个建议》 《即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生》 《实时语音聊天中的音频处理与编码压缩技术简述》 《网易视频云技术分享:音频处理与压缩技术快速入门》 《学习RFC3550:RTP/RTCP实时传输协议基础知识》 《基于RTMP数据传输协议的实时流媒体技术研究(论文全文)》 《声网架构师谈实时音视频云的实现难点(视频采访)》 《浅谈开发实时视频直播平台的技术要点》 《还在靠“喂喂喂”测试实时语音通话质量?本文教你科学的评测方法!》 《实现延迟低于500毫秒的1080P实时音视频直播的实践分享》 《移动端实时视频直播技术实践:如何做到实时秒开、流畅不卡》 《如何用最简单的方法测试你的实时音视频方案》 《技术揭秘:支持百万级粉丝互动的Facebook实时视频直播》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端实时音视频直播技术详解(一):开篇》 《移动端实时音视频直播技术详解(二):采集》 《移动端实时音视频直播技术详解(三):处理》 《移动端实时音视频直播技术详解(四):编码和封装》 《移动端实时音视频直播技术详解(五):推流和传输》 《移动端实时音视频直播技术详解(六):延迟优化》 《理论联系实际:实现一个简单地基于HTML5的实时视频直播》 《IM实时音视频聊天时的回声消除技术详解》 《浅谈实时音视频直播中直接影响用户体验的几项关键技术指标》 《如何优化传输机制来实现实时音视频的超低延迟?》 《首次披露:快手是如何做到百万观众同场看直播仍能秒开且不卡顿的?》 《Android直播入门实践:动手搭建一套简单的直播系统》 《网易云信实时视频直播在TCP数据传输层的一些优化思路》 《实时音视频聊天技术分享:面向不可靠网络的抗丢包编解码器》  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

实时通信RTC技术栈之:视频编解码

1、前言 RTC(Real-time Communications),实时通信,是一个正在兴起的风口行业,经过短短一年的时间,已经有很多玩家进入了这个行业,最典型的应用就是直播连麦和实时音视频通信。但是,很多开发者对一些概念还是有混淆的,比如 RTC 与 WebRTC,RTC 与直播,RTC 与 IM。 那么 RTC 技术栈究竟包含哪些技术,我们会提供一系列文章,来解读 RTC 技术栈。本文是系列文章的第一篇:讲述视频编解码的一些基本知识。 先来纠正几个概念。 2、RTC 和 WebRTC 有什么区别? 实时通信(RTC)最容易和 WebRTC 混淆,实际上,二者不能划等号。   RTC 从功能流程上来说,包含采集、编码、前后处理、传输、解码、缓冲、渲染等很多环节,上图展现了一次 RTC 通信的简要流程。每一个细分环节,还有更细分的技术模块。比如,前后处理环节 有美颜、滤镜、回声消除、噪声抑制等,采集有麦克风阵列等,编解码有 VP8、VP9、H.264、H.265 等等。   上图展现了 RTC 与 WebRTC 的关系,WebRTC 是 RTC 的一部分。WebRTC,是 Google 的一个专门针对网页实时通信的标准及开源项目,只提供了基础的前端功能实现,包括编码解码和抖动缓冲等,开发者若要基于 WebRTC 开发商用项目,那么需要自行做服务端实现和部署,信令前后端选型实现部署,以及手机适配等一系列具体工作;在此之外还要在可用性和高质量方面,进行大量的改进和打磨,对自身开发能力的门槛要求非常高。一个专业的 RTC 技术服务系统,需要除了涵盖上述的通信环节外,实际上还需要有解决互联网不稳定性的专用通信网络,以及针对互联网信道的高容忍度的音视频信号处理算法。当然常规云服务的高可用、服务质量的保障和监控维护工具等都只能算是一个专业服务商的基本模块。 所以,WebRTC 仅是 RTC 技术栈中的几个小细分的技术组合,并不是一个全栈解决方案。 3、RTC 和直播有什么区别?   上图展现的就是 RTC 与直播的关系,RTC 的一个具体应用是直播场景中的直播连麦,也就是低延时直播。普通直播,一般采用 TCP 协议,使用 CDN 进行内容分发,会有几秒甚至十几秒的延时,主播和观众的互动只能通过文字短消息或送礼来进行。而直播连麦,使用 UDP 协议,内容实时传输,主播和观众可以进行音视频连麦互动,实时沟通,延时一般低至几百毫秒。 4、视频编解码的现状 视频编解码的作用,就是在设备的摄像头采集画面和前处理后,将图像进行压缩,进行数字编码,用于传输。编解码器的优劣基本在于:压缩效率的高低,速度和功耗。 VP8,是视频压缩解决方案厂商 On2 Technologies 的第八代视频编解码标准,Google 收购 On2 后,就将 VP8 开源了,并且将其应用到 WebRTC 中。目前,Google 也在主推新一代的编解码标准——VP9。 H.264,是由 ITU-T 视频编码专家组(VCEG)和 ISO/IEC 动态图像专家组(MPEG)联合组成的联合视频组(JVT,Joint Video Team)提出的高度压缩数字视频编解码器国际标准。 WebRTC 也同时支持 H.264。 VP8 和 H.264 是十几年前发明的标准,属于同一代技术。这两个标准处于发展成熟的阶段,编码效率、运算复杂度和功耗上都达到了比较好的均衡。技术和应用程度上,二者也略有区别,比如,硬件厂商对 H.264 的支持较广泛,而对 VP8 的支持就比较有限。 VP9,开发始于 2011 年。VP9 的目标之一是在保证相同质量的情况下相对于 VP8 可以减少 50% 左右的码率,换句话说,相同的码率,VP9 能比 VP8 画质有非常明显的提高。VP9 的一大的优势是专利费用,Google 声明可以免费进行使用。这和 H.264 和 H.265 不同有较大的差异(虽然,2013 年 cisco 已将 open264 开源,并声称在不修改 open264 代码的情况下,能保证由 cisco 覆盖相关的专利费用)。 H.265 旨在在有限带宽下传输更高质量的网络视频,仅需原先的一半带宽即可播放相同质量的视频。它与 H.264 有着相类似的算法架构,并同时对一些相关技术加以改进而大幅提高视频质量。举例来说,H.264 编码器可以以 1Mbps 码率实现标清数字视频压缩;而 H.265 编码器则可以利用相同的码率编码 720P 甚至更高的分辨率的高清视频。这也意味着,在现有的家庭网络情况下,我们的智能手机、平板机等移动设备将能够直接在线播放 1080p 的全高清视频。同时,H.265 标准也同时支持 4K 和 8K 超高清视频。 VP9 和 H.265,是最近 5 年制定的标准,是当前已经完成标准中压缩效率最高的。同样的,H.265 是国际标准,VP9 是 Google 目前主推的标准。H.265 在硬件支持上比较广泛,Apple、高通、intel 等的芯片都支持 H.265 的硬件编解码器。VP9 的硬件支持依然十分有限。总体来说,新一代编码器,编码效率能比上一代提高了 30-50%,但是复杂度和功耗会比上一代大很多,所以纯软件编码实现的话有一定瓶颈,现有的技术下,还是需要依靠硬件编解码为主。 AVS 是我国具备自主知识产权的第二代信源编码标准。目前,AVS1.0 在第三世界国家中已有广泛应用。AVS2.0,属于与 H.265 和 VP9 同级的新一代标准。 编码器只是标准和语法,并没有限定应用场景。因此,在实际应用中,还要结合场景特点,来进行改进和深度优化。声网的视频编码器,针对实时音视频通信做了深度改进,更适应公共互联网的特点,实时性和质量上有很大提升。尤其是与网络的深度结合,同时兼顾对抗丢包和网络带宽的波动。 5、视频编解码的探索方向 1VR 视频标准 VR 视频标准是当前不论是学术界,还是商业应用的热门探索方向之一。VR 视频的编码目前继续解决的技术问题有:图像的显示质量、合成质量和传输带宽。 VR 视频编码先前的做法是,将已有的视频压缩标准,应用到 VR 场景中。但是,由于 VR 视频内容的特殊性和网络带宽的限制,目前的标准无法满足 VR 视频的压缩需求。业界对 VR 视频压缩标准呼声极高。将来高级的 VR 视频形态应该是自由沉浸立体视频:在一定空间范围内提供 Anywhere + Anytime + Anyview +Stereo 的沉浸体验。 2高分辨率的需求 在 H.264 时代,编码器主要应用于低于 HD 的中小分辨率,稍微兼顾 1080P 高分辨率。但 H.265 时代,随着硬件设备更好、带宽更高,用户开始对视频分辨率的要求更高,人们开始发现,用户对视频质量要求是没有止境。因此,新一代编码器,更倾向于支持高分辨率,比如 4K 高清分辨率。新一代编码器对高分辨率的压缩效率可提高 50% 以上。 附录:更多实时音视频技术文章 [1] 开源实时音视频技术WebRTC的文章: 《开源实时音视频技术WebRTC的现状》 《简述开源实时音视频技术WebRTC的优缺点》 《访谈WebRTC标准之父:WebRTC的过去、现在和未来》 《良心分享:WebRTC 零基础开发者教程(中文)[附件下载]》 《WebRTC实时音视频技术的整体架构介绍》 《新手入门:到底什么是WebRTC服务器,以及它是如何联接通话的?》 《WebRTC实时音视频技术基础:基本架构和协议栈》 《浅谈开发实时视频直播平台的技术要点》 《[观点] WebRTC应该选择H.264视频编码的四大理由》 《基于开源WebRTC开发实时音视频靠谱吗?第3方SDK有哪些?》 《开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《实时通信RTC技术栈之:视频编解码》 《开源实时音视频技术WebRTC在Windows下的简明编译教程》 《网页端实时音视频技术WebRTC:看起来很美,但离生产应用还有多少坑要填?》 [2] 实时音视频开发的其它精华资料: 《专访微信视频技术负责人:微信实时视频聊天技术的演进》 《即时通讯音视频开发(一):视频编解码之理论概述》 《即时通讯音视频开发(二):视频编解码之数字视频介绍》 《即时通讯音视频开发(三):视频编解码之编码基础》 《即时通讯音视频开发(四):视频编解码之预测技术介绍》 《即时通讯音视频开发(五):认识主流视频编码技术H.264》 《即时通讯音视频开发(六):如何开始音频编解码技术的学习》 《即时通讯音视频开发(七):音频基础及编码原理入门》 《即时通讯音视频开发(八):常见的实时语音通讯编码标准》 《即时通讯音视频开发(九):实时语音通讯的回音及回音消除概述》 《即时通讯音视频开发(十):实时语音通讯的回音消除技术详解》 《即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解》 《即时通讯音视频开发(十二):多人实时音视频聊天架构探讨》 《即时通讯音视频开发(十三):实时视频编码H.264的特点与优势》 《即时通讯音视频开发(十四):实时音视频数据传输协议介绍》 《即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况》 《即时通讯音视频开发(十六):移动端实时音视频开发的几个建议》 《即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生》 《实时语音聊天中的音频处理与编码压缩技术简述》 《网易视频云技术分享:音频处理与压缩技术快速入门》 《学习RFC3550:RTP/RTCP实时传输协议基础知识》 《基于RTMP数据传输协议的实时流媒体技术研究(论文全文)》 《声网架构师谈实时音视频云的实现难点(视频采访)》 《浅谈开发实时视频直播平台的技术要点》 《还在靠“喂喂喂”测试实时语音通话质量?本文教你科学的评测方法!》 《实现延迟低于500毫秒的1080P实时音视频直播的实践分享》 《移动端实时视频直播技术实践:如何做到实时秒开、流畅不卡》 《如何用最简单的方法测试你的实时音视频方案》 《技术揭秘:支持百万级粉丝互动的Facebook实时视频直播》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端实时音视频直播技术详解(一):开篇》 《移动端实时音视频直播技术详解(二):采集》 《移动端实时音视频直播技术详解(三):处理》 《移动端实时音视频直播技术详解(四):编码和封装》 《移动端实时音视频直播技术详解(五):推流和传输》 《移动端实时音视频直播技术详解(六):延迟优化》 《理论联系实际:实现一个简单地基于HTML5的实时视频直播》 《IM实时音视频聊天时的回声消除技术详解》 《浅谈实时音视频直播中直接影响用户体验的几项关键技术指标》 《如何优化传输机制来实现实时音视频的超低延迟?》 《首次披露:快手是如何做到百万观众同场看直播仍能秒开且不卡顿的?》 《Android直播入门实践:动手搭建一套简单的直播系统》 《网易云信实时视频直播在TCP数据传输层的一些优化思路》 《实时音视频聊天技术分享:面向不可靠网络的抗丢包编解码器》  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

WebRTC实时音视频技术基础:基本架构和协议栈

概述 本文主要介绍WebRTC的架构和协议栈。 最基本的三角形WebRTC架构 为了便于理解,我们来看一个最基本的三角形WebRTC架构(见下图):   在这个架构中,移动电话用“浏览器M”表示,笔记本电脑用“浏览器L”表示,通过Web服务器将它们连接起来。要建立一个实时媒体通讯,两台设备需要了解彼此的媒体功能,通过交换呼叫信令控制协议实现。 诸如这样的信令协议在WebRTC标准中并非事先规定,而是由开发者自行制定。在浏览器RTC会话的步骤如下: 首先,两个浏览器都从Web服务器下载了WebRTC程序(HTML5/JavaScript); 其次,两个浏览器通过Web服务器交换控制信令信息(使用嵌入式信令服务器),建立媒体功能功能互通。 最后,两个浏览器直接建立RTC媒体的音频、视频和数据通道。 真正实用的基于P2P的WebRTC架构 WebRTC使用P2P媒体流,音频、视频和数据的连接直接通过浏览器实现。但是,浏览器却隐藏在NAT(网络地址翻译)和防火墙的后面,这增加了建立P2P媒体会话的难度。这些流程和协议,如ICE或Trickle ICE,STUN和TURN,在建立P2P媒体流都是必不可少的。   如何使用STUN协议建立一个P2P RTC媒体(如图5所示),简化版的ICE流程如下: 1.两个浏览器通过自己的公网IP地址,使用STUN协议信息和STUN服务器建立联系; 2.两个浏览器通过SDP提供/应答机制,使用呼叫控制信令消息交换它们已发现的公共IP地址(ICE候选); 3.两个浏览器执行连接检查(ICE冲孔),确保P2P可以连接; 4.建立连接后,RTC媒体会话和媒体交换就可以实现了。 5.但是,假如在一个高度限制的NAT或防火墙,这种直接的路径将无法建立,只能到达TURN服务器。结果是媒体通过TURN服务器分程传递(如下图所示)。   WebRTC的协议栈 由互联网工程任务组(IETF)基于标准的可互操作的通信模型和协议栈详细地定义了WebRTC技术(参见图7),如下:   如前所述的信令栈,并非由WebRTC实现规定,而是由开发者自行决定。在这个例子中,我们将使用SIP-over-WebSocket(SIPoWS)作为信令栈。HTTP协议用于浏览器下载HTML5/JavaScript程序内容;NAT栈解决P2P连接问题;媒体栈用于发送和接收RTC的音频和视频。 LETF标准规定G.711和Opus作为音频/视频解码器。视频解码器尚未授权,但是H.248和VP8已经获得授权。媒体栈也用于交换RTC数据。本例中,实时信息采用消息会话中继协议(MSRP),实时会议采用二层控制协议(BFCP),实时文本服务采用T.140。 附录:更多实时音视频技术文章 [1] 开源实时音视频技术WebRTC的文章: 《开源实时音视频技术WebRTC的现状》 《简述开源实时音视频技术WebRTC的优缺点》 《访谈WebRTC标准之父:WebRTC的过去、现在和未来》 《良心分享:WebRTC 零基础开发者教程(中文)[附件下载]》 《WebRTC实时音视频技术的整体架构介绍》 《新手入门:到底什么是WebRTC服务器,以及它是如何联接通话的?》 《WebRTC实时音视频技术基础:基本架构和协议栈》 《浅谈开发实时视频直播平台的技术要点》 《[观点] WebRTC应该选择H.264视频编码的四大理由》 《基于开源WebRTC开发实时音视频靠谱吗?第3方SDK有哪些?》 《开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《实时通信RTC技术栈之:视频编解码》 《开源实时音视频技术WebRTC在Windows下的简明编译教程》 《网页端实时音视频技术WebRTC:看起来很美,但离生产应用还有多少坑要填?》 [2] 实时音视频开发的其它精华资料: 《专访微信视频技术负责人:微信实时视频聊天技术的演进》 《即时通讯音视频开发(一):视频编解码之理论概述》 《即时通讯音视频开发(二):视频编解码之数字视频介绍》 《即时通讯音视频开发(三):视频编解码之编码基础》 《即时通讯音视频开发(四):视频编解码之预测技术介绍》 《即时通讯音视频开发(五):认识主流视频编码技术H.264》 《即时通讯音视频开发(六):如何开始音频编解码技术的学习》 《即时通讯音视频开发(七):音频基础及编码原理入门》 《即时通讯音视频开发(八):常见的实时语音通讯编码标准》 《即时通讯音视频开发(九):实时语音通讯的回音及回音消除概述》 《即时通讯音视频开发(十):实时语音通讯的回音消除技术详解》 《即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解》 《即时通讯音视频开发(十二):多人实时音视频聊天架构探讨》 《即时通讯音视频开发(十三):实时视频编码H.264的特点与优势》 《即时通讯音视频开发(十四):实时音视频数据传输协议介绍》 《即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况》 《即时通讯音视频开发(十六):移动端实时音视频开发的几个建议》 《即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生》 《实时语音聊天中的音频处理与编码压缩技术简述》 《网易视频云技术分享:音频处理与压缩技术快速入门》 《学习RFC3550:RTP/RTCP实时传输协议基础知识》 《基于RTMP数据传输协议的实时流媒体技术研究(论文全文)》 《声网架构师谈实时音视频云的实现难点(视频采访)》 《浅谈开发实时视频直播平台的技术要点》 《还在靠“喂喂喂”测试实时语音通话质量?本文教你科学的评测方法!》 《实现延迟低于500毫秒的1080P实时音视频直播的实践分享》 《移动端实时视频直播技术实践:如何做到实时秒开、流畅不卡》 《如何用最简单的方法测试你的实时音视频方案》 《技术揭秘:支持百万级粉丝互动的Facebook实时视频直播》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端实时音视频直播技术详解(一):开篇》 《移动端实时音视频直播技术详解(二):采集》 《移动端实时音视频直播技术详解(三):处理》 《移动端实时音视频直播技术详解(四):编码和封装》 《移动端实时音视频直播技术详解(五):推流和传输》 《移动端实时音视频直播技术详解(六):延迟优化》 《理论联系实际:实现一个简单地基于HTML5的实时视频直播》 《IM实时音视频聊天时的回声消除技术详解》 《浅谈实时音视频直播中直接影响用户体验的几项关键技术指标》 《如何优化传输机制来实现实时音视频的超低延迟?》 《首次披露:快手是如何做到百万观众同场看直播仍能秒开且不卡顿的?》 《Android直播入门实践:动手搭建一套简单的直播系统》 《网易云信实时视频直播在TCP数据传输层的一些优化思路》 《实时音视频聊天技术分享:面向不可靠网络的抗丢包编解码器》  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

IM群聊消息如此复杂,如何保证不丢不重?

1、前言 群聊已经成为主流IM软件的基本功能,不管是QQ群、还是微信群,一个群友在群内发了一条消息,那么对于IM服务器来说需要保证: 在线的群友能第一时间收到消息; 离线的群友能在登陆后收到消息。 由于“消息风暴扩散系数”的存在(概念详见《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》),群消息的复杂度要远高于一对一的单聊消息。群消息的实时性、可达性、离线消息是今天将要讨论的核心话题。 2、IM开发干货系列文章 本文是系列文章中的第5篇,总目录如下: 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》(本文) 《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》 《移动端IM登录时拉取数据如何作到省流量?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《浅谈移动端IM的多点登陆和消息漫游原理》 《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 《IM群聊消息的已读回执功能该怎么实现?》 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》 更多群聊技术文章: 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM群聊消息的已读回执功能该怎么实现?》 《关于IM即时通讯群聊消息的乱序问题讨论》 《现代IM系统中聊天消息的同步和存储方案探讨》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《微信后台团队:微信后台异步消息队列的优化升级实践分享》 《IM群聊消息如此复杂,如何保证不丢不重?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《如何保证IM实时消息的“时序性”与“一致性”?》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《一套高可用、易伸缩、高并发的IM群聊架构方案设计实践》 另外,如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。 3、常见的群消息流程 开始讲群消息投递流程之前,先介绍两个群业务的核心数据结构: 1 2 3 4 群成员表:用来描述一个群里有多少成员 t_group_users(group_id, user_id) 群离线消息表:用来描述一个群成员的离线消息 t_offine_msgs(user_id, group_id, sender_id,time, msg_id, msg_detail) 业务场景举例: 1)一个群中有x,A,B,C,D共5个成员,成员x发了一个消息; 2)成员A与B在线,期望实时收到消息; 3)成员C与D离线,期望未来拉取到离线消息。 系统架构简介: 1)客户端:x,A,B,C,D共5个客户端用户; 2)服务端:   2.1)所有模块与服务抽象为server; 2.2)所有用户在线状态抽象存储在高可用cache里; 2.3)所有数据信息,例如群成员、群离线消息抽象存储在db里。   典型群消息投递流程,如上图步骤1-4所述: 步骤1:群消息发送者x向server发出群消息; 步骤2:server去db中查询群中有多少用户(x,A,B,C,D); 步骤3:server去cache中查询这些用户的在线状态; 步骤4:对于群中在线的用户A与B,群消息server进行实时推送; 步骤5:对于群中离线的用户C与D,群消息server进行离线存储。   典型的群离线消息拉取流程,如上图步骤1-3所述: 步骤1:离线消息拉取者C向server拉取群离线消息; 步骤2:server从db中拉取离线消息并返回群用户C; 步骤3:server从db中删除群用户C的群离线消息。 存在的问题: 上述流程是最容易想,也最容易理解的,存在的问题也最显而易见:对于同一份群消息的内容,多个离线用户存储了很多份。假设群中有200个用户离线,离线消息则冗余了200份,这极大的增加了数据库的存储压力。 4、群消息优化1:减少存储量 为了减少离线消息的冗余度,增加一个群消息表,用来存储所有群消息的内容,离线消息表只存储用户的群离线消息msg_id,就能大大的降低数据库的冗余存储量,思路如下。 1 2 3 4 群消息表:用来存储一个群中所有的消息内容 t_group_msgs(group_id, sender_id, time,msg_id, msg_detail) 群离线消息表:优化后只存储msg_id t_offine_msgs(user_id, group_id, msg_id)   这样优化后,群在线消息发送就做了一些修改: 步骤3:每次发送在线群消息之前,要先存储群消息的内容; 步骤6:每次存储离线消息时,只存储msg_id,而不用为每个用户存储msg_detail。 拉取离线消息时也做了响应的修改: 步骤1:先拉取所有的离线消息msg_id; 步骤3:再根据msg_id拉取msg_detail; 步骤5:删除离线msg_id。 存在的问题(如同单对单消息的发送一样): 1)在线消息的投递可能出现消息丢失,例如服务器重启,路由器丢包,客户端crash; 2)离线消息的拉取也可能出现消息丢失,原因同上。 需要和单对单消息的可靠投递一样,加入应用层的ACK,才能保证群消息一定到达。 5、群消息优化2:应用层ACK   应用层ACK优化后,群在线消息发送又发生了一些变化: 步骤3:在消息msg_detail存储到群消息表后,不管用户是否在线,都先将msg_id存储到离线消息表里; 步骤6:在线的用户A和B收到群消息后,需要增加一个应用层ACK,来标识消息到达; 步骤7:在线的用户A和B在应用层ACK后,将他们的离线消息msg_id删除掉。   对应到群离线消息的拉取也一样: 步骤1:先拉取msg_id; 步骤3:再拉取msg_detail; 步骤5:最后应用层ACK; 步骤6:server收到应用层ACK才能删除离线消息表里的msg_id。 存在的问题: 1)如果拉取了消息,却没来得及应用层ACK,会收到重复的消息么? 答案是肯定的,不过可以在客户端去重,对于重复的msg_id,对用户不展现,从而不影响用户体验 2)对于离线的每一条消息,虽然只存储了msg_id,但是每个用户的每一条离线消息都将在数据库中保存一条记录,有没有办法减少离线消息的记录数呢? 6、群消息优化3:离线消息表 其实,对于一个群用户,在ta登出后的离线期间内,肯定是所有的群消息都没有收到的,完全不用对所有的每一条离线消息存储一个离线msg_id,而只需要存储最近一条拉取到的离线消息的time(或者msg_id),下次登录时拉取在那之后的所有群消息即可,而完全没有必要存储每个人未拉取到的离线消息msg_id。 1 2 3 4 5 群成员表:用来描述一个群里有多少成员,以及每个成员最后一条ack的群消息的msg_id(或者time) t_group_users(group_id, user_id, last_ack_msg_id(last_ack_msg_time)) 群消息表:用来存储一个群中所有的消息内容,不变 t_group_msgs(group_id, sender_id, time,msg_id, msg_detail) 群离线消息表:不再需要了   离线消息表优化后,群在线消息的投递流程: 步骤3:在消息msg_detail存储到群消息表后,不再需要操作离线消息表(优化前需要将msg_id插入离线消息表); 步骤7:在线的用户A和B在应用层ACK后,将last_ack_msg_id更新即可(优化前需要将msg_id从离线消息表删除)。   群离线消息的拉取流程也类似: 步骤1:拉取离线消息; 步骤3:ACK离线消息; 步骤4:更新last_ack_msg_id。 存在的问题: 由于“消息风暴扩散系数”的存在,假设1个群有500个用户,“每条”群消息都会变为500个应用层ACK,将对服务器造成巨大的冲击,有没有办法减少ACK请求量呢? 7、群消息优化4:批量ACK 由于“消息风暴扩散系数”的存在,如果每条群消息都ACK,会给服务器造成巨大的冲击,为了减少ACK请求量,很容易想到的方法是批量ACK。 批量ACK的方式又有两种: 1)每收到N条群消息ACK一次,这样请求量就降低为原来的1/N了; 2)每隔时间间隔T进行一次群消息ACK,也能达到类似的效果。 新的问题:批量ACK有可能导致:还没有来得及ACK群消息,用户就退出了,这样下次登录会拉取到重复的离线消息。 解决方案:msg_id去重,不对用户展现,保证良好的用户体验。 还可能存在的问题:群离线消息过多:拉取过慢。 解决方案:分页拉取(按需拉取),分页拉取的细节在《IM消息送达保证机制实现(下篇):保证离线消息的可靠投递》一章中有详细叙述,此处不再展开。 8、本文小结 群消息还是非常有意思的,可达性、实时性、离线消息、消息风暴扩散等等等等,做个总结: 1)不管是群在线消息,还是群离线消息,应用层的ACK是可达性的保障; 2)群消息只存一份,不用为每个用户存储离线群msg_id,只需存储一个最近ack的群消息id/time; 3)为了减少消息风暴,可以批量ACK; 4)如果收到重复消息,需要msg_id去重,让用户无感知; 5)离线消息过多,可以分页拉取(按需拉取)优化。 (原文链接:http://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959643&idx=1&sn=844afa6a31770fa587474ecd73c3b3b3&chksm=bd2d04878a5a8d91c5c93ad8e85254185c63eb419457efbedcba54a9b8a9053da1e8980a694a&scene=21#wechat_redirect)  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

一套高可用、易伸缩、高并发的IM群聊架构方案设计实践

本文原题为“一套高可用群聊消息系统实现”,由作者“于雨氏”授权即时通讯网整理和发布,内容有些许改动,作者博客地址:alexstocks.github.io。应作者要求,如需转载,请联系作者获得授权。 一、引言 要实现一整套能用于大用户量、高并发场景下的IM群聊,技术难度远超IM系统中的其它功能,原因在于:IM群聊消息的实时写扩散特性带来了一系列技术难题。 举个例子:如一个2000人群里,一条普通消息的发出问题,将瞬间写扩散为2000条消息的接收问题,如何保证这些消息的及时、有序、高效地送达,涉及到的技术问题点实在太多,更别说个别场景下万人大群里的炸群消息难题了更别说个别场景下万人大群里的炸群消息难题了。 这也是为什么一般中大型IM系统中,都会将群聊单独拎出来考虑架构的设计,单独有针对性地进行架构优化,从而降低整个系统的设计难度。 本文将分享的是一套生产环境下的IM群聊消息系统的高可用、易伸缩、高并发架构设计实践,属于原创第一手资料,内容较专业,适合有一定IM架构经验的后端程序员阅读。 推荐:如有兴趣,本文作者的另一篇《一套原创分布式即时通讯(IM)系统理论架构方案》,也适合正在进行IM系统架构设计研究的同学阅读。 二、群聊技术文章 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM群聊消息的已读回执功能该怎么实现?》 《关于IM即时通讯群聊消息的乱序问题讨论》 《现代IM系统中聊天消息的同步和存储方案探讨》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《微信后台团队:微信后台异步消息队列的优化升级实践分享》 《IM群聊消息如此复杂,如何保证不丢不重?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《如何保证IM实时消息的“时序性”与“一致性”?》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 三、万事开头难:初始的极简实现 所谓的群聊消息系统,就是一种多对多群体聊天方式,譬如直播房间内的聊天室对应的服务器端就是一个群聊消息系统。 2017年9月初,我们初步实现了一套极简的群聊消息系统,其大致架构如下:   系统名词解释: 1)Client : 消息发布者【或者叫做服务端群聊消息系统调用者】,publisher; 2)Proxy : 系统代理,对外统一接口,收集Client发来的消息转发给Broker; 3)Broker :系统消息转发Server,Broker 会根据 Gateway Message 组织一个 RoomGatewayList【key为RoomID,value为 Gateway IP:Port 地址列表】,然后把 Proxy 发来的消息转发到 Room 中所有成员登录的所有 Gateway; 4)Router :用户登录消息转发者,把Gateway转发来的用户登入登出消息转发给所有的Broker; 5)Gateway :所有服务端的入口,接收合法客户端的连接,并把客户端的登录登出消息通过Router转发给所有的Broker; 6)Room Message : Room聊天消息; 7)Gateway Message : Room内某成员 登录 或者 登出 某Gateway消息,包含用户UIN/RoomID/Gateway地址{IP:Port}等消息。 当一个 Room 中多个 Client 连接一个 Gateway 的时候,Broker只会根据 RoomID 把房间内的消息转发一次给这个Gateway,由Gateway再把消息复制多份分别发送给连接这个 Gateway 的 Room 中的所有用户的客户端。 这套系统有如下特点: 1)系统只转发房间内的聊天消息,每个节点收到后立即转发出去,不存储任何房间内的聊天消息,不考虑消息丢失以及消息重复的问题; 2)系统固定地由一个Proxy、三个Broker和一个Router构成; 3)Proxy接收后端发送来的房间消息,然后按照一定的负载均衡算法把消息发往某个Broker,Broker则把消息发送到所有与Room有关系的接口机Gateway; 4)Router接收Gateway转发来的某个Room内某成员在这个Gateway的登出或者登录消息,然后把消息发送到所有Broker; 5)Broker收到Router转发来的Gateway消息后,更新(添加或者删除)与某Room相关的Gateway集合记录; 6)整个系统的通信链路采用UDP通信方式。 从以上特点,整个消息系统足够简单,没有考虑扩缩容问题,当系统负载到达极限的时候,就重新再部署一套系统以应对后端client的消息压力。 这种处理方式本质是把系统的扩容能力甩锅给了后端Client以及前端Gateway:每次扩容一个系统,所有Client需要在本地配置文件中添加一个Proxy地址然后全部重启,所有Gateway则需要再本地配置文件添加一个Router地址然后全部重启。 这种“幸福我一人,辛苦千万家”的扩容应对方式,必然导致公司内部这套系统的使用者怨声载道,下一阶段的升级就是必然的了。 四、进一步重点设计:“可扩展性” 4.1、基本思路 大道之行也,天下为公,不同的系统有不同的构架,相同的系统总有类似的实现。类似于数据库的分库分表【关于分库分表,目前看到的最好的文章是《一种支持自由规划无须数据迁移和修改路由代码的Replicaing扩容方案》】,其扩展实现核心思想是分Partition分Replica,但各Replica之间还区分leader(leader-follower,只有leader可接受写请求)和non-leader(所有replica均可接收写请求)两种机制。 从数据角度来看,这套系统接收两种消息:Room Message(房间聊天消息)和Gateway Message(用户登录消息)。两种消息的交汇之地就是Broker,所以应对扩展的紧要地方就是Broker,Broker的每个Partition采用non-leader机制,各replica均可接收Gateway Message消息写请求和Room Message转发请求。 首先,当Room Message量加大时可以对Proxy进行水平扩展,多部署Proxy即可因应Room Message的流量。 其次,当Gateway Message量加大时可以对Router进行水平扩展,多部署Router即可因应Gateway Message的流量。 最后,两种消息的交汇之地Broker如何扩展呢?可以把若干Broker Replica组成一个Partition,因为Gateway Message是在一个Partition内广播的,所有Broker Replica都会有相同的RoomGatewayList 数据,因此当Gateway Message增加时扩容Partition即可。当Room Message量增加时,水平扩容Partition内的Broker Replica即可,因为Room Message只会发送到Partition内某个Replica上。 从个人经验来看,Room ID的增长以及Room内成员的增加量在一段时间内可以认为是直线增加,而Room Message可能会以指数级增长,所以若设计得当则Partition扩容的概率很小,而Partition内Replica水平增长的概率几乎是100%。 不管是Partition级别的水平扩容还是Partition Replica级别的水平扩容,不可能像系统极简版本那样每次扩容后都需要Client或者Gateway去更新配置文件然后重启,因应之道就是可用zookeeper充当角色的Registriy。通过这个zookeeper注册中心,相关角色扩容的时候在Registry注册后,与之相关的其他模块得到通知即可获取其地址等信息。采用zookeeper作为Registry的时候,所以程序实现的时候采用实时watch和定时轮询的策略保证数据可靠性,因为一旦网络有任何的抖动,zk就会认为客户端已经宕机把链接关闭。 分析完毕,与之相对的架构图如下:   以下各分章节将描述各个模块详细流程。 4.2、Client Client详细流程如下: 1)从配置文件加载Registry地址; 2)从Registy上Proxy注册路径/pubsub/proxy下获取所有的Proxy,依据各个Proxy ID大小顺序递增组成一个ProxyArray; 3)启动一个线程实时关注Registry路径/pubsub/proxy,以获取Proxy的动态变化,及时更新ProxyArray; 4)启动一个线程定时轮询获取Registry路径/pubsub/proxy下各个Proxy实例,作为关注策略的补充,以期本地ProxyArray内各个Proxy成员与Registry上的各个Proxy保持一致;定时给各个Proxy发送心跳,异步获取心跳回包;定时清除ProxyArray中心跳超时的Proxy成员; 5)发送消息的时候采用snowflake算法给每个消息分配一个MessageID,然后采用相关负载均衡算法把消息转发给某个Proxy。 4.3、Proxy Proxy详细流程如下: 1)读取配置文件,获取Registry地址; 2)把自身信息注册到Registry路径/pubsub/proxy下,把Registry返回的ReplicaID作为自身ID; 3)从Registry路径/pubsub/broker/partition(x)下获取每个Broker Partition的各个replica; 4)从Registry路径/pubsub/broker/partition_num获取当前有效的Broker Partition Number; 5)启动一个线程关注Registry上的Broker路径/pubsub/broker,以实时获取以下信息:     {Broker Partition Number}      - 新的Broker Partition(此时发生了扩容);      - Broker Partition内新的broker replica(Partition内发生了replica扩容);      - Broker Parition内某replica挂掉的信息; 6)定时向各个Broker Partition replica发送心跳,异步等待Broker返回的心跳响应包,以探测其活性,以保证不向超时的replica转发Room Message; 7)启动一个线程定时读取Registry上的Broker路径/pubsub/broker下各个子节点的值,以定时轮询的策略观察Broker Partition Number变动,以及各Partition的变动情况,作为实时策略的补充;同时定时检查心跳包超时的Broker,从有效的BrokerList中删除; 8)依据规则【BrokerPartitionID = RoomID % BrokerPartitionNum, BrokerReplicaID = RoomID % BrokerPartitionReplicaNum】向某个Partition的replica转发Room Message,收到Client的Heatbeat包时要及时给予响应。 之所以把Room Message和Heartbeat Message放在一个线程处理,是为了防止进程假死这种情况。 当/pubsub/broker/partition_num的值发生改变的时候(譬如值改为4),意味着Router Partition进行了扩展,Proxy要及时获取新Partition路径(如/pubsub/broker/Partition2和/pubsub/broker/Partition3)下的实例,并关注这些路径,获取新Partition下的实例。 之所以Proxy在获取Registry下所有当前的Broker实例信息后再注册自身信息,是因为此时它才具有转发消息的资格。 Proxy转发某个Room消息时候,只发送给处于Running状态的Broker。为Broker Partition内所有replica依据Registry给其分配的replicaID进行递增排序,组成一个Broker Partition Replica Array,规则中BrokerPartitionReplicaNum为Array的size,而BrokerReplicaID为replica在Array中的下标。 4.4、Pipeline 收到的 Room Message 需要做三部工作:收取 Room Message、消息协议转换和向 Broker 发送消息。 初始系统这三步流程如果均放在一个线程内处理,proxy 的整体吞吐率只有 50 000 Msg/s。 最后的实现方式是按照消息处理的三个步骤以 pipeline 方式做如下流程处理: 1)启动 1 个消息接收线程和 N【N == Broker Parition 数目】个多写一读形式的无锁队列【称之为消息协议转换队列】,消息接收线程分别启动一个 epoll 循环流程收取消息,然后把消息以相应的 hash 算法【队列ID = UIN % N】写入对应的消息协议转换队列; 2)启动 N 个线程 和 N * 3 个一写一读的无锁队列【称之为消息发送队列】,每个消息协议专家线程从消息协议转换队列接收到消息并进行协议转换后,根据相应的 hash 算法【队列ID = UIN % 3N】写入消息发送队列; 3)启动 3N 个消息发送线程,分别创建与之对应的 Broker 的连接,每个线程单独从对应的某个消息发送队列接收消息然后发送出去。 经过以上流水线改造后,Proxy 的整体吞吐率可达 200 000 Msg/s。 关于 pipeline 自身的解释,本文不做详述,可以参考下图:   4.5、大房间消息处理 每个 Room 的人数不均,最简便的解决方法就是给不同人数量级的 Room 各搭建一套消息系统,不用修改任何代码。 然所谓需求推动架构改进,在系统迭代升级过程中遇到了这样一个需求:业务方有一个全国 Room,用于给所有在线用户进行消息推送。针对这个需求,不可能为了一个这样的 Room 单独搭建一套系统,况且这个 Room 的消息量很少。 如果把这个 Room 的消息直接发送给现有系统,它有可能影响其他 Room 的消息发送:消息系统是一个写放大的系统,全国 Room 内有系统所有的在线用户,每次发送都会卡顿其他 Room 的消息发送。 最终的解决方案是:使用类似于分区的方法,把这样的大 Room 映射为 64 个虚拟 Room【称之为 VRoom】。在 Room 号段分配业务线的配合下,给消息系统专门保留了一个号段,用于这种大 Room 的切分,在 Proxy 层依据一个 hash 方法 【 VRoomID = UserID % 64】 把每个 User 分配到相应的 VRoom,其他模块代码不用修改即完成了大 Room 消息的路由。 4.6、Broker Broker详细流程如下: 1)Broker加载配置,获取自身所在Partition的ID(假设为3); 2)向Registry路径/pubsub/broker/partition3注册,设置其状态为Init,注册中心返回的ID作为自身的ID(replicaID); 3)接收Router转发来的Gateway Message,放入GatewayMessageQueue; 4)从Database加载数据,把自身所在的Broker Partition所应该负责的 RoomGatewayList 数据加载进来; 5)异步处理GatewayMessageQueue内的Gateway Message,只处理满足规则【PartitionID == RoomID % PartitionNum】的消息,把数据存入本地路由信息缓存; 6)修改Registry路径/pubsub/broker/partition3下自身节点的状态为Running; 7)启动线程实时关注Registry路径/pubsub/broker/partition_num的值; 8)启动线程定时查询Registry路径/pubsub/broker/partition_num的值; 9)当Registry路径/pubsub/broker/partition_num的值发生改变的时候,依据规则【PartitionID == RoomID % PartitionNum】清洗本地路由信息缓存中每条数据; 10)接收Proxy发来的Room Message,依据RoomID从路由信息缓存中查找Room有成员登陆的所有Gateway,把消息转发给这些Gateway。 注意Broker之所以先注册然后再加载Database中的数据,是为了在加载数据的时候同时接收Router转发来的Gateway Message,但是在数据加载完前这些受到的数据先被缓存起来,待所有 RoomGatewayList 数据加载完后就把这些数据重放一遍; Broker之所以区分状态,是为了在加载完毕 RoomGatewayList 数据前不对Proxy提供转发消息的服务,同时也方便Broker Partition应对的消息量增大时进行水平扩展。 当Broker发生Partition扩展的时候,新的Partition个数必须是2的幂,只有新Partition内所有Broker Replica都加载实例完毕,再更改/pubsub/broker/partition_num的值。 老的Broker也要watch路径/pubsub/broker/partition_num的值,当这个值增加的时候,它也需要清洗本地的路由信息缓存。 Broker的扩容过程犹如细胞分裂,形成中的两个细胞有着完全相同的数据,分裂完成后【Registry路径/pubsub/broker/partition_num的值翻倍】则需要清洗垃圾信息。这种方法称为翻倍法。 4.7、Router Router详细流程如下: 1)Router加载配置,Registry地址; 2)把自身信息注册到Registry路径/pubsub/router下,把Registry返回的ReplicaID作为自身ID; 3)从Registry路径/pubsub/broker/partition(x)下获取每个Broker Partition的各个replica; 4)从Registry路径/pubsub/broker/partition_num获取当前有效的Broker Partition Number; 5)启动一个线程关注Registry上的Broker路径/pubsub/broker,以实时获取以下信息:     {Broker Partition Number}     - 新的Broker Partition(此时发生了扩容);     - Broker Partition内新的broker replica(Partition内发生了replica扩容);     - Broker Parition内某replica挂掉的信息; 6)定时向各个Broker Partition replica发送心跳,异步等待Broker返回的心跳响应包,以探测其活性,以保证不向超时的replica转发Gateway Message; 7)启动一个线程定时读取Registry上的Broker路径/pubsub/broker下各个子节点的值,以定时轮询的策略观察Broker Partition Number变动,以及各Partition的变动情况,作为实时策略的补充;同时定时检查心跳包超时的Broker,从有效的BrokerList中删除; 8)从Database全量加载路由 RoomGatewayList 数据放入本地缓存; 9)收取Gateway发来的心跳消息,及时返回ack包; 10)收取Gateway转发来的Gateway Message,按照一定规则【BrokerPartitionID % BrokerPartitionNum = RoomID % BrokerPartitionNum】转发给某个Broker Partition下所有Broker Replica,保证Partition下所有replica拥有同样的路由 RoomGatewayList 数据,再把Message内数据存入本地缓存,当检测到数据不重复的时候把数据异步写入Database。 4.8、Gateway Gateway详细流程如下: 1)读取配置文件,加载Registry地址; 2)从Registry路径/pubsub/router/下获取所有router replica,依据各Replica的ID递增排序组成replica数组RouterArray; 3)启动一个线程实时关注Registry路径/pubsub/router,以获取Router的动态变化,及时更新RouterArray; 4)启动一个线程定时轮询获取Registry路径/pubsub/router下各个Router实例,作为关注策略的补充,以期本地RouterArray及时更新;定时给各个Router发送心跳,异步获取心跳回包;定时清除RouterArray中心跳超时的Router成员; 5)当有Room内某成员客户端连接上来或者Room内所有成员都不连接当前Gateway节点时,依据规则【RouterArrayIndex = RoomID % RouterNum】向某个Router发送Gateway Message; 6)收到Broker转发来的Room Message时,根据MessageID进行去重,如果不重复则把消息发送到连接到当前Gateway的Room内所有客户端,同时把MessageID缓存起来以用于去重判断。 Gateway本地有一个基于共享内存的LRU Cache,存储最近一段时间发送的消息的MessageID。 五、接下来迫切要解决的:系统稳定性 系统具有了可扩展性仅仅是系统可用的初步,整个系统要保证最低粒度的SLA(0.99),就必须在两个维度对系统的可靠性就行感知:消息延迟和系统内部组件的高可用。 5.1、消息延迟 准确的消息延迟的统计,通用的做法可以基于日志系统对系统所有消息或者以一定概率抽样后进行统计,但限于人力目前没有这样做。 目前使用了一个方法:通过一种构造一组伪用户ID,定时地把消息发送给proxy,每条消息经过一层就把在这层的进入时间和发出时间以及组件自身的一些信息填入消息,这组伪用户的消息最终会被发送到一个伪Gateway端,伪Gateway对这些消息的信息进行归并统计后,即可计算出当前系统的平均消息延迟时间。 通过所有消息的平均延迟可以评估系统的整体性能。同时,因为系统消息路由的哈希方式已知,当固定时间内伪Gateway没有收到消息时,就把消息当做发送失败,当某条链路失败一定次数后就可以产生告警了。 5.2、高可用 上面的方法同时能够检测某个链路是否出问题,但是链路具体出问题的点无法判断,且实时性无法保证。 为了保证各个组件的高可用,系统引入了另一种评估方法:每个层次都给后端组件发送心跳包,通过心跳包的延迟和成功率判断其下一级组件的当前的可用状态。 譬如proxy定时给每个Partition内每个broker发送心跳,可以依据心跳的成功率来快速判断broker是否处于“假死”状态(最近业务就遇到过broker进程还活着,但是对任何收到的消息都不处理的情况)。 同时依靠心跳包的延迟还可以判断broker的处理能力,基于此延迟值可在同一Partition内多broker端进行负载均衡。 六、进一步优化:消息可靠性 公司内部内部原有一个走tcp通道的群聊消息系统,但是经过元旦一次大事故(几乎全线崩溃)后,相关业务的一些重要消息改走这套基于UDP的群聊消息系统了。这些消息如服务端下达给客户端的游戏动作指令,是不允许丢失的,但其特点是相对于聊天消息来说量非常小(单人1秒最多一个),所以需要在目前UDP链路传递消息的基础之上再构建一个可靠消息链路。 国内某IM大厂的消息系统也是以UDP链路为基础的(见《为什么QQ用的是UDP协议而不是TCP协议?》),他们的做法是消息重试加ack构建了可靠消息稳定传输链路。但是这种做法会降低系统的吞吐率,所以需要独辟蹊径。 UDP通信的本质就是伪装的IP通信,TCP自身的稳定性无非是重传、去重和ack,所以不考虑消息顺序性的情况下可以通过重传与去重来保证消息的可靠性。 基于目前系统的可靠消息传输流程如下: 1)Client给每个命令消息依据snowflake算法配置一个ID,复制三份,立即发送给不同的Proxy; 2)Proxy收到命令消息以后随机发送给一个Broker; 3)Broker收到后传输给Gateway; 4)Gateway接收到命令消息后根据消息ID进行重复判断,如果重复则丢弃,否则就发送给APP,并缓存之。 正常的消息在群聊消息系统中传输时,Proxy会根据消息的Room ID传递给固定的Broker,以保证消息的有序性。 七、Router需要进一步强化 7.1、简述 当线上需要部署多套群聊消息系统的时候,Gateway需要把同样的Room Message复制多份转发给多套群聊消息系统,会增大Gateway压力,可以把Router单独独立部署,然后把Room Message向所有的群聊消息系统转发。 Router系统原有流程是:Gateway按照Room ID把消息转发给某个Router,然后Router把消息转发给下游Broker实例。新部署一套群聊消息系统的时候,新系统Broker的schema需要通过一套约定机制通知Router,使得Router自身逻辑过于复杂。   重构后的Router架构参照上图,也采用分Partition分Replica设计,Partition内部各Replica之间采用non-leader机制;各Router Replica不会主动把Gateway Message内容push给各Broker,而是各Broker主动通过心跳包形式向Router Partition内某个Replica注册,而后此Replica才会把消息转发到这个Broker上。 类似于Broker,Router Partition也以2倍扩容方式进行Partition水平扩展,并通过一定机制保证扩容或者Partition内部各个实例停止运行或者新启动时,尽力保证数据的一致性。 Router Replica收到Gateway Message后,replica先把Gateway Message转发给Partition内各个peer replica,然后再转发给各个订阅者。Router转发消息的同时异步把消息数据写入Database。 独立Router架构下,下面小节将分别详述Gateway、Router和Broker三个相关模块的详细流程。 7.2、Gateway Gateway详细流程如下: 1)从Registry路径/pubsub/router/partition(x)下获取每个Partition的各个replica; 2)从Registry路径/pubsub/router/partition_num获取当前有效的Router Partition Number; 3)启动一个线程关注Registry上的Router路径/pubsub/router,以实时获取以下信息:{Router Partition Number} -> 新的Router Partition(此时发生了扩容);  Partition内新的replica(Partition内发生了replica扩容);  Parition内某replica挂掉的信息; 4)定时向各个Partition replica发送心跳,异步等待Router返回的心跳响应包,以探测其活性,以保证不向超时的replica转发Gateway Message; 5)启动一个线程定时读取Registry上的Router路径/pubsub/router下各个子节点的值,以定时轮询的策略观察Router Partition Number变动,以及各Partition的变动情况,作为实时策略的补充;同时定时检查心跳包超时的Router,从有效的BrokerList中删除; 6 依据规则向某个Partition的replica转发Gateway Message。 第六步的规则决定了Gateway Message的目的Partition和replica,规则内容有: 如果某Router Partition ID满足condition(RoomID % RouterPartitionNumber == RouterPartitionID % RouterPartitionNumber),则把消息转发到此Partition; 这里之所以不采用直接hash方式(RouterPartitionID = RoomID % RouterPartitionNumber)获取Router Partition,是考虑到当Router进行2倍扩容的时候当所有新的Partition的所有Replica都启动完毕且数据一致时才会修改Registry路径/pubsub/router/partitionnum的值,按照规则的计算公式才能保证新Partition的各个Replica在启动过程中就可以得到Gateway Message,也即此时每个Gateway Message会被发送到两个Router Partition。 当Router扩容完毕,修改Registry路径/pubsub/router/partitionnum的值后,此时新集群进入稳定期,每个Gateway Message只会被发送固定的一个Partition,condition(RoomID % RouterPartitionNumber == RouterPartitionID % RouterPartitionNumber)等效于condition(RouterPartitionID = RoomID % RouterPartitionNumber)。 如果Router Partition内某replia满足condition(replicaPartitionID = RoomID % RouterPartitionReplicaNumber),则把消息转发到此replica。 replica向Registry注册的时候得到的ID称之为replicaID,Router Parition内所有replica按照replicaID递增排序组成replica数组RouterPartitionReplicaArray,replicaPartitionID即为replica在数组中的下标。 Gateway Message数据一致性: Gateway向Router发送的Router Message内容有两种:某user在当前Gateway上进入某Room 和 某user在当前Gateway上退出某Room,数据项分别是UIN(用户ID)、Room ID、Gateway Addr和User Action(Login or Logout。 由于所有消息都是走UDP链路进行转发,则这些消息的顺序就有可能乱序。Gateway可以统一给其发出的所有消息分配一个全局递增的ID【下文称为GatewayMsgID,Gateway Message ID】以保证消息的唯一性和全局有序性。 Gateway向Registry注册临时有序节点时,Registry会给Gateway分配一个ID,Gateway可以用这个ID作为自身的Instance ID【假设这个ID上限是65535】。 GatewayMsgID字长是64bit,其格式如下: 1 2 // 63 -------------------------- 48 47 -------------- 38 37 ------------ 0 // |  16bit Gateway Instance ID    |   10bit Reserve    |    38bit自增码  | 7.3、Router Router系统部署之前,先设置Registry路径/pubsub/router/partition_num的值为1。 Router详细流程如下: 1)Router加载配置,获取自身所在Partition的ID(假设为3); 2)向Registry路径/pubsub/router/partition3注册,设置其状态为Init,注册中心返回的ID作为自身的ID(replicaID); 3)注册完毕会收到Gateway发来的Gateway Message以及Broker发来的心跳消息(HeartBeat Message),先缓存到消息队列MessageQueue; 4)从Registry路径/pubsub/router/partition3下获取自身所在的Partition内的各个replica; 5)从Registry路径/pubsub/router/partition_num获取当前有效的Router Partition Number; 6)启动一个线程关注Registry路径/pubsub/router,以实时获取以下信息:{Router Partition Number}  -> Partition内新的replica(Partition内发生了replica扩容);  Parition内某replica挂掉的信息; 7)从Database加载数据; 8)启动一个线程异步处理MessageQueue内的Gateway Message,把Gateway Message转发给同Partition内其他peer replica,然后依据规则【RoomID % BrokerPartitionNumber == BrokerReplicaPartitionID % BrokerPartitionNumber】转发给BrokerList内每个Broker;处理Broker发来的心跳包,把Broker的信息存入本地BrokerList,然后给Broker发送回包; 9)修改Registry路径/pubsub/router/partition3下节点的状态为Running; 10)启动一个线程定时读取Registry路径/pubsub/router下各个子路径的值,以定时轮询的策略观察Router各Partition的变动情况,作为实时策略的补充;检查超时的Broker,把其从BrokerList中剔除; 11)当RouterPartitionNum倍增时,Router依据规则【RoomID % BrokerPartitionNumber == BrokerReplicaPartitionID % BrokerPartitionNumber】清洗自身路由信息缓存中数据; 12)Router本地存储每个Gateway的最大GatewayMsgID,收到小于GatewayMsgID的Gateway Message可以丢弃不处理,否则就更新GatewayMsgID并根据上面逻辑进行处理。 之所以把Gateway Message和Heartbeat Message放在一个线程处理,是为了防止进程假死这种情况。 Broker也采用了分Partition分Replica机制,所以向Broker转发Gateway Message时候路由规则,与Gateway向Router转发消息的路由规则相同。 另外启动一个工具,当水平扩展后新启动的Partition内所有Replica的状态都是Running的时候,修改Registry路径/pubsub/router/partition_num的值为所有Partition的数目。 7.4、Broker Broker详细流程如下: 1)Broker加载配置,获取自身所在Partition的ID(假设为3); 2)向Registry路径/pubsub/broker/partition3注册,设置其状态为Init,注册中心返回的ID作为自身的ID(replicaID); 3)从Registry路径/pubsub/router/partition_num获取当前有效的Router Partition Number; 4)从Registry路径/pubsub/router/partition(x)下获取每个Router Partition的各个replica; 5)启动一个线程关注Registry路径/pubsub/router,以实时获取以下信息:{Router Partition Number} -> 新的Router Partition(此时发生了扩容);  Partition内新的replica(Partition内发生了replica扩容);  Parition内某replica挂掉的信息; 6)依据规则【RouterPartitionID % BrokerPartitionNum == BrokerPartitionID % BrokerPartitionNum,RouterReplicaID = BrokerReplicaID % BrokerPartitionNum】选定目标Router Partition下某个Router replica,向其发送心跳消息,包含BrokerPartitionNum、BrokerPartitionID、BrokerHostAddr和精确到秒级的Timestamp,并异步等待所有Router replica的回复,所有Router转发来的Gateway Message放入GatewayMessageQueue; 7)依据规则【BrokerPartitionID == RoomID % BrokerParitionNum】从Database加载数据; 8)依据规则【BrokerPartitionID % BrokerParitionNum == RoomID % BrokerParitionNum】异步处理GatewayMessageQueue内的Gateway Message,只留下合乎规则的消息的数据; 9)修改Registry路径/pubsub/broker/partition3下自身节点的状态为Running; 10)启动一个线程定时读取Registry路径/pubsub/router下各个子路径的值,以定时轮询的策略观察Router各Partition的变动情况,作为实时策略的补充;定时检查超时的Router,某Router超时后更换其所在的Partition内其他Router替换之,定时发送心跳包; 11)当Registry路径/pubsub/broker/partition_num的值BrokerPartitionNum发生改变的时候,依据规则【PartitionID == RoomID % PartitionNum】清洗本地路由信息缓存中每条数据; 12)接收Proxy发来的Room Message,依据RoomID从路由信息缓存中查找Room有成员登陆的所有Gateway,把消息转发给这些Gateway; 13)Broker本地存储每个Gateway的最大GatewayMsgID,收到小于GatewayMsgID的Gateway Message可以丢弃不处理,否则更新GatewayMsgID并根据上面逻辑进行处理。 BrokerPartitionNumber可以小于或者等于或者大于RouterPartitionNumber,两个数应该均是2的幂,两个集群可以分别进行扩展,互不影响。譬如BrokerPartitionNumber=4而RouterPartitionNumber=2,则Broker Partition 3只需要向Router Partition 1的某个follower发送心跳消息即可;若BrokerPartitionNumber=4而RouterPartitionNumber=8,则Broker Partition 3需要向Router Partition 3的某个follower发送心跳消息的同时,还需要向Router Partition 7的某个follower发送心跳,以获取全量的Gateway Message。 Broker需要关注/pubsub/router/partitionnum和/pubsub/broker/partitionnum的值的变化,当router或者broker进行parition水平扩展的时候,Broker需要及时重新构建与Router之间的对应关系,及时变动发送心跳的Router Replica对象【RouterPartitionID = BrokerReplicaID % RouterPartitionNum,RouterPartitionID为Router Replica在PartitionRouterReplicaArray数组的下标】。 当Router Partition内replica死掉或者发送心跳包的replica对象死掉(无论是注册中心通知还是心跳包超时),broker要及时变动发送心跳的Router replica对象。 另外,Gateway使用UDP通信方式向Router发送Gateway Message,如若这个Message丢失则此Gateway上该Room内所有成员一段时间内(当有新的成员在当前Gateway上加入Room 时会产生新的Gateway Message)都无法再接收消息,为了保证消息的可靠性,可以使用这样一个约束解决问题:在此Gateway上登录的某Room内的人数少于3时,Gateway会把Gateway Message复制两份非连续(如以10ms为时间间隔)重复发送给某个Partition leader。因Gateway Message消息处理的幂等性,重复Gateway Message并不会导致Room Message发送错误,只在极少概率的情况下会导致Gateway收到消息的时候Room内已经没有成员在此Gateway登录,此时Gateway会把消息丢弃不作处理。 传递实时消息群聊消息系统的Broker向特定Gateway转发Room Message的时候,会带上Room内在此Gateway上登录的用户列表,Gateway根据这个用户列表下发消息时如果检测到此用户已经下线,在放弃向此用户转发消息的同时,还应该把此用户已经下线的消息发送给Router,当Router把这个消息转发给Broker后,Broker把此用户从用户列表中剔除。通过这种负反馈机制保证用户状态更新的及时性。 八、离线消息的处理 8.1、简述 前期的系统只考虑了用户在线情况下实时消息的传递,当用户离线时其消息便无法获取。 若系统考虑用户离线消息传递,需要考虑如下因素: 1)消息固化:保证用户上线时收到其离线期间的消息; 2)消息有序:离线消息和在线消息都在一个消息系统传递,给每个消息分配一个ID以区分消息先后顺序,消息顺序越靠后则ID愈大。 离线消息的存储和传输,需要考虑用户的状态以及每条消息的发送状态,整个消息核心链路流程会有大的重构。 新消息架构如下图:   系统名词解释: 1)Pi : 消息ID存储模块,存储每个人未发送的消息ID有序递增集合; 2)Xiu : 消息存储KV模块,存储每个人的消息,给每个消息分配ID,以ID为key,以消息内为value; 3)Gateway Message(HB) : 用户登录登出消息,包括APP保活定时心跳(Hearbeat)消息。 系统内部代号貔貅(貔貅者,雄貔雌貅),源自上面两个新模块。 这个版本架构流程的核心思想为“消息ID与消息内容分离,消息与用户状态分离”。消息发送流程涉及到模块 Client/Proxy/Pi/Xiu,消息推送流程则涉及到模块 Pi/Xiu/Broker/Router/Gateway。 下面小节先细述Pi和Xiu的接口,然后再详述发送和推送流程。 8.2、Xiu Xiu模块功能名称是Message Storage,用户缓存和固化消息,并给消息分配ID。Xiu 集群采用分 Partition 分 Replica 机制,Partition 初始数目须是2的倍数,集群扩容时采用翻倍法。 8.2.1存储消息 存储消息请求的参数列表为{SnowflakeID,UIN, Message},其流程如下: 1)接收客户端发来的消息,获取消息接收人ID(UIN)和客户端给消息分配的 SnowflakeID; 2)检查 UIN % Xiu_Partition_Num == Xiu_Partition_ID % Xiu_Partition_Num 添加是否成立【即接收人的消息是否应当由当前Xiu负责】,不成立则返回错误并退出; 3)检查 SnowflakeID 对应的消息是否已经被存储过,若已经存储过则返回其对应的消息ID然后退出; 4)给消息分配一个 MsgID: 每个Xiu有自己唯一的 Xiu_Partition_ID,以及一个初始值为 0 的 Partition_Msg_ID。MsgID = 1B[ Xiu_Partition_ID ] + 1B[ Message Type ] + 6B[ ++ Partition_Msg_ID ]。每次分配的时候 Partition_Msg_ID 都自增加一。 5)以 MsgID 为 key 把消息存入基于共享内存的 Hashtable,并存入消息的 CRC32 hash值和插入时间,把 MsgID 存入一个 LRU list 中: LRU List 自身并不存入共享内存中,当进程重启时,可以根据Hashtable中的数据重构出这个List。把消息存入 Hashtable 中时,如果 Hashtable full,则依据 LRU List 对Hashtable 中的消息进行淘汰。 6)把MsgID返回给客户端; 7)把MsgID异步通知给消息固化线程,消息固化线程根据MsgID从Hashtable中读取消息并根据CRC32 hash值判断消息内容是否完整,完整则把消息存入本地RocksDB中。 8.2.2读取消息 读取消息请求的参数列表为{UIN, MsgIDList},其流程为: 1)获取请求的 MsgIDList,判断每个MsgID MsgID{Xiu_Partition_ID} == Xiu_Partition_ID 条件是否成立,不成立则返回错误并退出; 2)从 Hashtable 中获取每个 MsgID 对应的消息; 3)如果 Hashtable 中不存在,则从 RocksDB 中读取 MsgID 对应的消息; 4)读取完毕则把所有获取的消息返回给客户端。 8.2.3主从数据同步 目前从简,暂定Xiu的副本只有一个。 Xiu节点启动的时候根据自身配置文件中分配的 Xiu_Partition_ID 到Registry路径 /pubsub/xiu/partition_id 下进行注册一个临时有序节点,注册成功则Registry会返回Xiu的节点 ID。 Xiu节点获取 /pubsub/xiu/partition_id 下的所有节点的ID和地址信息,依据 节点ID最小者为leader 的原则,即可判定自己的角色。只有leader可接受读写数据请求。 数据同步流程如下: 1)follower定时向leader发送心跳信息,心跳信息包含本地最新消息的ID; 2)leader启动一个数据同步线程处理follower的心跳信息,leader的数据同步线程从LRU list中查找 follower_latest_msg_id 之后的N条消息的ID,若获取到则读取消息并同步给follower,获取不到则回复其与leader之间消息差距太大; 3)follower从leader获取到最新一批消息,则存储之; 4)follower若获取leader的消息差距太大响应,则请求leader的agent把RocksDB的固化数据全量同步过来,整理完毕后再次启动与leader之间的数据同步流程。 follower会关注Registry路径 /pubsub/xiu/partition_id 下所有所有节点的变化情况,如果leader挂掉则及时转换身份并接受客户端请求。如果follower 与 leader 之间的心跳超时,则follower删掉 leader 的 Registry 路径节点,及时进行身份转换处理客户端请求。 当leader重启或者follower转换为leader的时候,需要把 Partition_Msg_ID 进行一个大数值增值(譬如增加1000)以防止可能的消息ID乱序情况。 8.2.4集群扩容 Xiu 集群扩容采用翻倍法,扩容时新 Partition 的节点启动后工作流程如下: 1)向Registry的路径 /pubsub/xiu/partition_id 下自己的 node 的 state 为 running,同时注册自己的对外服务地址信息; 2)另外启动一个工具,当水平扩展后所有新启动的 Partition 内所有 Replica 的状态都是 Running 的时候,修改 Registry 路径 /pubsub/xiu/partition_num 的值为扩容后 Partition 的数目。按照开头的例子,即由2升级为4。 之所以 Xiu 不用像 Broker 和 Router 那样启动的时候向老的 Partition 同步数据,是因为每个 Xiu 分配的 MsgID 中已经带有 Xiu 的 PartitionID 信息,即使集群扩容这个 ID 也不变,根据这个ID也可以定位到其所在的Partition,而不是借助 hash 方法。 8.3、Pi Pi 模块功能名称是 Message ID Storage,存储每个用户的 MsgID List。Xiu 集群也采用分 Partition 分 Replica 机制,Partition 初始数目须是2的倍数,集群扩容时采用翻倍法。 8.3.1存储消息ID MsgID 存储的请求参数列表为{UIN,MsgID},Pi 工作流程如下: 1)判断条件 UIN % Pi_Partition_Num == Pi_Partition_ID % Pi_Partition_Num 是否成立,若不成立则返回error退出; 2)把 MsgID 插入UIN的 MsgIDList 中,保持 MsgIDList 中所有 MsgID 不重复有序递增,把请求内容写入本地log,给请求者返回成功响应。 Pi有专门的日志记录线程,给每个日志操作分配一个 LogID,每个 Log 文件记录一定量的写操作,当文件 size 超过配置的上限后删除之。 8.3.2读取消息ID列表 读取请求参数列表为{UIN, StartMsgID, MsgIDNum, ExpireFlag},其意义为获取用户 UIN 自起始ID为 StartMsgID 起(不包括 StartMsgID )的数目为 MsgIDNum 的消息ID列表,ExpireFlag意思是 所有小于等于 StartMsgID 的消息ID是否删除。 流程如下: 1)判断条件 UIN % Pi_Partition_Num == Pi_Partition_ID % Pi_Partition_Num 是否成立,若不成立则返回error退出; 2)获取 (StartID, StartMsgID + MsgIDNum] 范围内的所有 MsgID,把结果返回给客户端; 3)如果 ExpireFlag 有效,则删除MsgIDList内所有在 [0, StartMsgID] 范围内的MsgID,把请求内容写入本地log。 8.3.3主从数据同步 同 Xiu 模块,暂定 Pi 的同 Parition 副本只有一个。 Pi 节点启动的时候根据自身配置文件中分配的 Pi_Partition_ID 到Registry路径 /pubsub/pi/partition_id 下进行注册一个临时有序节点,注册成功则 Registry 会返回 Pi 的节点 ID。 Pi 节点获取 /pubsub/pi/partition_id 下的所有节点的ID和地址信息,依据 节点ID最小者为leader 的原则,即可判定自己的角色。只有 leader 可接受读写数据请求。 数据同步流程如下: 1)follower 定时向 leader 发送心跳信息,心跳信息包含本地最新 LogID; 2)leader 启动一个数据同步线程处理 follower 的心跳信息,根据 follower 汇报的 logID 把此 LogID; 3)follower 从 leader 获取到最新一批 Log,先存储然后重放。 follower 会关注Registry路径 /pubsub/pi/partition_id 下所有节点的变化情况,如果 leader 挂掉则及时转换身份并接受客户端请求。如果follower 与 leader 之间的心跳超时,则follower删掉 leader 的 Registry 路径节点,及时进行身份转换处理客户端请求。 8.3.4集群扩容 Pi 集群扩容采用翻倍法。则节点启动后工作流程如下: 1)向 Registry 注册,获取 Registry 路径 /pubsub/xiu/partition_num 的值 PartitionNumber; 2)如果发现自己 PartitionID 满足条件 PartitionID >= PartitionNumber 时,则意味着当前 Partition 是扩容后的新集群,更新 Registry 中自己状态为start; 3)读取 Registry 路径 /pubsub/xiu 下所有 Parition 的 leader,根据条件 自身PartitionID % PartitionNumber == PartitionID % PartitionNumber 寻找对应的老 Partition 的 leader,称之为 parent_leader; 4)缓存收到 Proxy 转发来的用户请求; 5)向 parent_leader 获取log; 6)向 parent_leader 同步内存数据; 7)重放 parent_leader 的log; 8)更新 Registry 中自己的状态为 Running; 9)重放用户请求; 10)当 Registry 路径 /pubsub/xiu/partition_num 的值 PartitionNumber 满足条件 PartitionID >= PartitionNumber 时,意味着扩容完成,处理用户请求时要给用户返回响应。 Proxy 会把读写请求参照条件 UIN % Pi\_Partition\_Num == Pi\_Partition\_ID % Pi\_Partition\_Num 向相关 partition 的 leader 转发用户请求。假设原来 PartitionNumber 值为2,扩容后值为4,则原来转发给 partition0 的写请求现在需同时转发给 partition0 和 partition2,原来转发给 partition1 的写请求现在需同时转发给 partition1 和 partition3。 另外启动一个工具,当水平扩展后所有新启动的 Partition 内所有 Replica 的状态都是 Running 的时候,修改Registry路径/pubsub/xiu/partition_num的值为扩容后 Partition 的数目。 8.4、数据发送流程 消息自 PiXiu 的外部客户端(Client,服务端所有使用 PiXiu 提供的服务者统称为客户端)按照一定负载均衡规则发送到 Proxy,然后存入 Xiu 中,把 MsgID 存入 Pi 中。 其详细流程如下: 1)Client 依据 snowflake 算法给消息分配 SnowflakeID,依据 ProxyID = UIN % ProxyNum 规则把消息发往某个 Proxy; 2)Proxy 收到消息后转发到 Xiu; 3)Proxy 收到 Xiu 返回的响应后,把响应转发给 Client; 4)如果 Proxy 收到 Xiu 返回的响应带有 MsgID,则发起 Pi 写流程,把 MsgID 同步到 Pi 中; 5)如果 Proxy 收到 Xiu 返回的响应带有 MsgID,则给 Broker 发送一个 Notify,告知其某 UIN 的最新 MsgID。 8.5、数据转发流程 转发消息的主体是Broker,原来的在线消息转发流程是它收到 Proxy 转发来的 Message,然后根据用户是否在线然后转发给 Gateway。 PiXiu架构下 Broker 会收到以下类型消息: 1)用户登录消息; 2)用户心跳消息; 3)用户登出消息; 4)Notify 消息; 5)Ack 消息。 Broker流程受这五种消息驱动,下面分别详述其收到这五种消息时的处理流程。 用户登录消息流程如下: 1)检查用户的当前状态,若为 OffLine 则把其状态值为在线 OnLine; 2)检查用户的待发送消息队列是否为空,不为空则退出; 3)向 Pi 模块发送获取 N 条消息 ID 的请求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false},设置用户状态为 GettingMsgIDList 并等待回应; 4)根据 Pi 返回的消息 ID 队列,向 Xiu 发起获取消息请求 {UIN: uin, MsgIDList: msg ID List},设置用户状态为 GettingMsgList 并等待回应; 5)Xiu 返回消息列表后,设置状态为 SendingMsg,并向 Gateway 转发消息。 可以把用户心跳消息当做用户登录消息处理。 Gateway的用户登出消息产生有三种情况: 1)用户主动退出; 2)用户心跳超时; 3)给用户转发消息时发生网络错误。 用户登出消息处理流程如下: 1)检查用户状态,如果为 OffLine,则退出; 2)用户状态不为 OffLine 且检查用户已经发送出去的消息列表的最后一条消息的 ID(LastMsgID),向 Pi 发送获取 MsgID 请求{UIN: uin, StartMsgID: LastMsgID, MsgIDNum: 0, ExpireFlag: True},待 Pi 返回响应后退出。 处理 Proxy 发来的 Notify 消息处理流程如下: 1)如果用户状态为 OffLine,则退出; 2)更新用户的最新消息 ID(LatestMsgID),如果用户发送消息队列不为空则退出; 3)向 Pi 模块发送获取 N 条消息 ID 的请求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false},设置用户状态为 GettingMsgIDList 并等待回应; 4)根据 Pi 返回的消息 ID 队列,向 Xiu 发起获取消息请求 {UIN: uin, MsgIDList: msg ID List},设置用户状态为 GettingMsgList 并等待回应; 5)Xiu 返回消息列表后,设置状态为 SendingMsg,并向 Gateway 转发消息。 所谓 Ack 消息,就是 Broker 经 Gateway 把消息转发给 App 后,App 给Broker的消息回复,告知Broker其最近成功收到消息的 MsgID。 Ack 消息处理流程如下: 1)如果用户状态为 OffLine,则退出; 2)更新 LatestAckMsgID 的值; 3)如果用户发送消息队列不为空,则发送下一个消息后退出; 4)如果 LatestAckMsgID >= LatestMsgID,则退出; 5)向 Pi 模块发送获取 N 条消息 ID 的请求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false},设置用户状态为 GettingMsgIDList 并等待回应; 6)根据 Pi 返回的消息 ID 队列,向 Xiu 发起获取消息请求 {UIN: uin, MsgIDList: msg ID List},设置用户状态为 GettingMsgList 并等待回应; 7)Xiu 返回消息列表后,设置状态为 SendingMsg,并向 Gateway 转发消息。 总体上,PiXiu 转发消息流程采用拉取(pull)转发模型,以上面五种消息为驱动进行状态转换,并作出相应的动作行为。 九、本文总结 这套群聊消息系统尚有以下task list需完善: 1)消息以UDP链路传递,不可靠【2018/01/29解决之】; 2)目前的负载均衡算法采用了极简的RoundRobin算法,可以根据成功率和延迟添加基于权重的负载均衡算法实现; 3)只考虑传递,没有考虑消息的去重,可以根据消息ID实现这个功能【2018/01/29解决之】; 4)各个模块之间没有考虑心跳方案,整个系统的稳定性依赖于Registry【2018/01/17解决之】; 5)离线消息处理【2018/03/03解决之】; 6)区分消息优先级。 此记。 参考文档:《一种支持自由规划无须数据迁移和修改路由代码的Replicaing扩容方案》 十、本文成文历程 于雨氏,2017/12/31,初作此文于丰台金箱堂。 于雨氏,2018/01/16,于海淀添加“系统稳定性”一节。 于雨氏,2018/01/29,于海淀添加“消息可靠性”一节。 于雨氏,2018/02/11,于海淀添加“Router”一节,并重新格式化全文。 于雨氏,2018/03/05,于海淀添加“PiXiu”一节。 于雨氏,2018/03/14,于海淀添加负反馈机制、根据Gateway Message ID保证Gateway Message数据一致性 和 Gateway用户退出消息产生机制 等三个细节。 于雨氏,2018/08/05,于海淀添加 “pipeline” 一节。 于雨氏,2018/08/28,于海淀添加 “大房间消息处理” 一节。 附录:更多IM架构设计文章 《浅谈IM系统的架构设计》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一套原创分布式即时通讯(IM)系统理论架构方案》 《从零到卓越:京东客服即时通讯系统的技术架构演进历程》 《蘑菇街即时通讯/IM服务器开发之架构选择》 《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《17年的实践:腾讯海量产品的技术方法论》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《现代IM系统中聊天消息的同步和存储方案探讨》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 《WhatsApp技术实践分享:32人工程团队创造的技术神话》 《微信朋友圈千亿访问量背后的技术挑战和实践总结》 《王者荣耀2亿用户量的背后:产品定位、技术架构、网络方案等》 《IM系统的MQ消息中间件选型:Kafka还是RabbitMQ?》 《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》 《以微博类应用场景为例,总结海量社交系统的架构设计步骤》 《快速理解高性能HTTP服务端的负载均衡技术原理》 《子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践》 《知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》 《微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)》 《微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)》 《新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践》 《一套高可用、易伸缩、高并发的IM群聊架构方案设计实践》  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列

1、引言 消息是互联网信息的一种表现形式,是人利用计算机进行信息传递的有效载体,比如即时通讯网坛友最熟悉的即时通讯消息就是其具体的表现形式之一。 消息从发送者到接收者的典型传递方式有两种: 1)一种我们可以称为即时消息:即消息从一端发出后(消息发送者)立即就可以达到另一端(消息接收者),这种方式的具体实现就是平时最常见的IM聊天消息; 2)另一种称为延迟消息:即消息从某端发出后,首先进入一个容器进行临时存储,当达到某种条件后,再由这个容器发送给另一端。 在上述“消息传递方式2)”中所指的这个容器的一种具体实现就是MQ消息队列服务。 MQ消息队列中间件是中大型分布式系统中重要的组件,它主要用来解决:应用解耦、异步消息、流量削锋等问题,用以实现高性能、高可用、可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等。MQ消息队列中间件已被广泛用于电商、即时通讯、社交等各种中大型分布式应用系统。 ▲ 各种MQ消息队列,玲琅满目 在一个典型的IM即时通讯应用中,MQ消息队列可以用于: 1)用户的聊天消息离线存储环节:因为IM消息的发送属于高吞吐场景,直接操纵DB很容易就把DB搞挂了,所以离线消息在落地入库前,可以先扔到MQ消息队列中,再由单独部署的消费者来有节奏地存储到DB中; 2)用户的行为数据收集环节:因为用户的聊天消息和指令等,可以用于大数据分析,而且基于国家监管要求也是必须要存储一段时间的,所以此类数据的收集同样可以用于MQ消息队列,再由单独部署的消费者存储到DB中; 3)用户的操作日志收集环节:log这种数据价值不高,但关键时刻又非常有用,而且数据量又很大,要想存储起来难度很高,这时就轮到Linkedin公司开源的Kafka出场了; .... 因此,对于即时通讯开发者来说,正确地理解MQ消息队列,对于IM或消息推送系统的架构设计、方案选型等都大有裨益。 ▲ 一个典型的消息队列原理图(生产者将消息通过队列传递给消费者) 2、系列文章 ▼ IM开发干货系列文章(本文是其第16篇): 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》 《移动端IM登录时拉取数据如何作到省流量?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《浅谈移动端IM的多点登陆和消息漫游原理》 《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 《IM群聊消息的已读回执功能该怎么实现?》 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》(本文) 如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。 3、MQ消息队列的典型应用场景 MQ消息队列目前在中大型分布式系统实际应用中常用的使用场景主要有:异步处理、应用解耦、流量削锋和消息通讯四个场景。 3.1应用场景1:异步处理 场景说明:一个典型的IM即时通讯系统中,用户注册成功后可能需要发送注册邮件和注册通知短信。 传统的做法有两种: 1)串行的方式:即将注册信息写入数据库成功后、发送注册邮件、再发送注册短信。以上三个任务全部完成后,返回给客户端; 2)并行方式:即将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。 假设三个业务节点每个使用50毫秒,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。 因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)。 小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。 如何解决这个问题呢?答案是:引入消息队列,将不是必须的业务逻辑,异步处理。 改造后的架构如下: 按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。 3.2应用场景2:应用解耦 场景说明:一个典型的电商购物系统中,用户下订单后,订单系统需要通知库存系统。 传统的做法是:订单系统调用库存系统的接口。如下图所示: 传统模式的缺点:假如库存系统无法访问,则订单减库存将失败,从而导致订单失败,订单系统与库存系统耦合。 如何解决以上问题呢?答案是:引入应用消息队列后的方案。如下图: 如上图所示,大致的原理是: 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功; 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。 好处就是:假如在下单时库存系统不能正常使用,也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。 3.3应用场景3:流量削锋 流量削锋也是消息队列中的常用场景,一般在电商秒杀等大型活动(比如双11)、团购抢单活动中使用广泛。 应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。 在这种场景下加入消息队列服务的好处: 1)可以控制活动的人数; 2)可以缓解短时间内高流量压垮应用。 ▲ 原理图如上图所示 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。秒杀业务根据消息队列中的请求信息,再做后续处理。 3.4应用场景4:日志处理 日志处理是指将消息队列用在日志处理中,比如Linkedin这种大型职业社交应用架构中Kafka的应用(Kafka就是Linkedin开发并开源的),解决大量日志传输的问题。 使用Kafka后的架构简化如下: 上图所示的架构原理就是: 日志采集客户端:负责日志数据采集,定时写入Kafka队列; Kafka消息队列:负责日志数据的接收,存储和转发; 日志处理应用:订阅并消费kafka队列中的日志数据。 3.5应用场景5:即时消息通讯 即时消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的即时消息通讯场景。比如实现点对点消息队列或者IM聊天室等(但Jack Jiang认为,在中大型IM系统中,MQ并不适合这么用,具体的讨论请见:《请教可以使用MQ消息队列中间件做即时通讯系统吗?》)。 点对点通讯:客户端A和客户端B使用同一队列,进行消息通讯; 聊天室通讯:客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。 以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。 4、MQ消息队列的常见消息模式 常见的MQ消息队列消息模式有: 1)P2P模式; 2)Pub/sub模式(也就是常说的“发布/订阅”模式); 3)推(Push)模式和拉(Pull)模式。 下面将逐个介绍这几种常消息模式。 4.1P2P模式 ▲ 典型的P2P消息模式原理图 P2P模式包含三个角色: 1)消息队列(Queue); 2)发送者(Sender); 3)接收者(Receiver)。 每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。 P2P消息模式的特点: 每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列接收者在成功接收消息之后需向队列应答成功 如果希望发送的每个消息都会被成功处理的话,那么需要P2P模式。 4.2Pub/sub模式 ▲ 典型的Pub/sub消息模式原理图 如上图所示,此消息模式包含三个角色: 1)主题(Topic); 2)发布者(Publisher); 3)订阅者(Subscriber)。 多个发布者将消息发送到Topic,系统将这些消息传递给多个订阅者。 Pub/Sub的特点: 每个消息可以有多个消费者发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。为了消费消息,订阅者必须保持运行的状态。为了缓和这样严格的时间相关性,有些MQ消息队列(比如RabbitMQ)允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型。 4.3推模式和拉模式 ▲ 一个典型的推模式和拉模式原理图 推(push)模式是一种基于C/S机制、由服务器主动将信息送到客户器的技术。 在Push模式应用中,服务器把信息送给客户器之前,并没有明显的客户请求。push事务由服务器发起。push模式可以让信息主动、快速地寻找用户/客户器,信息的主动性和实时性比较好。但精确性较差,可能推送的信息并不一定满足客户的需求。 Push模式不能保证能把信息送到客户器,因为推模式采用了广播机制,如果客户器正好联网并且和服务器在同一个频道上,推送模式才是有效的。 Push模式无法跟踪状态,采用了开环控制模式,没有用户反馈信息。在实际应用中,由客户器向服务器发送一个申请,并把自己的地址(如IP、port)告知服务器,然后服务器就源源不断地把信息推送到指定地址。在多媒体信息广播中也采用了推模式。 拉(Pull)模式与推(Push)模式相反,是由客户器主动发起的事务。服务器把自己所拥有的信息放在指定地址(如IP、port),客户器向指定地址发送请求,把自己需要的资源“拉”回来。不仅可以准确获取自己需要的资源,还可以及时把客户端的状态反馈给服务器。 5、主流的MQ消息队列技术选型对比 一份主流MQ技术对比清单: 另外,即时通讯网整理的另一篇《IM系统的MQ消息中间件选型:Kafka还是RabbitMQ?》,可以详细了解一下Kafka和RabbitMQ的对比。 5.1Kafka Kafka是Linkedin开源的MQ系统(现已是Apache下的一个子项目),它是一个高性能跨语言分布式发布/订阅消息队列系统,主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,0.8开始支持复制,不支持事务,适合产生大量数据的互联网服务的数据收集业务。 Kafka还具有以下特性: 1)快速持久化,可以在O(1)的系统开销下进行消息持久化; 2)高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率; 3)完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡; 4)支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制统一了在线和离线的消息处理。 Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。 5.2RabbitMQ 3 RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。 RabbitMQ本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。 5.3RocketMQ RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。 5.4ZeroMQ ZeroMQ只是一个网络编程的Pattern库,将常见的网络请求形式(分组管理,链接管理,发布订阅等)模式化、组件化,简而言之socket之上、MQ之下。对于MQ来说,网络传输只是它的一部分,更多需要处理的是消息存储、路由、Broker服务发现和查找、事务、消费模式(ack、重投等)、集群服务等。 ZeroMQ是号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但是ZeroMQ仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter的Storm 0.9.0以前的版本中默认使用ZeroMQ作为数据流的传输(Storm从0.9版本开始同时支持ZeroMQ和Netty作为传输模块)。 5.5小结 RabbitMQ/Kafka/ZeroMQ 都能提供消息队列服务,但有很大的区别。 在面向服务架构中通过消息代理(比如 RabbitMQ / Kafka等),使用生产者-消费者模式在服务间进行异步通信是一种比较好的思想。 因为服务间依赖由强耦合变成了松耦合。消息代理都会提供持久化机制,在消费者负载高或者掉线的情况下会把消息保存起来,不会丢失。就是说生产者和消费者不需要同时在线,这是传统的请求-应答模式比较难做到的,需要一个中间件来专门做这件事。其次消息代理可以根据消息本身做简单的路由策略,消费者可以根据这个来做负载均衡,业务分离等。 缺点也有,就是需要额外搭建消息代理集群(但优点是大于缺点的 ) 。 ZeroMQ 和 RabbitMQ/Kafka 不同,它只是一个异步消息库,在套接字的基础上提供了类似于消息代理的机制。使用 ZeroMQ 的话,需要对自己的业务代码进行改造,不利于服务解耦。 RabbitMQ 支持 AMQP(二进制),STOMP(文本),MQTT(二进制),HTTP(里面包装其他协议)等协议。Kafka 使用自己的协议。 Kafka 自身服务和消费者都需要依赖 Zookeeper。 RabbitMQ 在有大量消息堆积的情况下性能会下降,Kafka不会。毕竟AMQP设计的初衷不是用来持久化海量消息的,而Kafka一开始是用来处理海量日志的。 总的来说,RabbitMQ 和 Kafka 都是十分优秀的分布式的消息代理服务,只要合理部署,基本上可以满足生产条件下的任何需求。 关于这两种MQ的比较,网上的资料并不多,最权威的的是kafka的提交者写一篇文章:http://www.quora.com/What-are-the-differences-between-Apache-Kafka-and-RabbitMQ 这篇文间里面提到的要点: 1) RabbitMq比kafka成熟,在可用性上,稳定性上,可靠性上,RabbitMq超过kafka; 2) Kafka设计的初衷就是处理日志的,可以看做是一个日志系统,针对性很强,所以它并没有具备一个成熟MQ应该具备的特性; 3) Kafka的性能(吞吐量、tps)比RabbitMq要强,这篇文章的作者认为,两者在这方面没有可比性; 4)总的来说,目前RocketMq、Kafka、RabbitMq在各家公司都有使用,具体看技术团队的熟悉程度及使用场景了。 附录1:有关IM即时通讯架构设计的文章 《浅谈IM系统的架构设计》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一套原创分布式即时通讯(IM)系统理论架构方案》 《从零到卓越:京东客服即时通讯系统的技术架构演进历程》 《蘑菇街即时通讯/IM服务器开发之架构选择》 《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《17年的实践:腾讯海量产品的技术方法论》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《现代IM系统中聊天消息的同步和存储方案探讨》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 《WhatsApp技术实践分享:32人工程团队创造的技术神话》 《微信朋友圈千亿访问量背后的技术挑战和实践总结》 《王者荣耀2亿用户量的背后:产品定位、技术架构、网络方案等》 《IM系统的MQ消息中间件选型:Kafka还是RabbitMQ?》 《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》 《以微博类应用场景为例,总结海量社交系统的架构设计步骤》 《快速理解高性能HTTP服务端的负载均衡技术原理》 《子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践》 《知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》 >> 附录2:有关IM即时通讯的更多热点问题的文章 《移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”》 《移动端IM开发者必读(二):史上最全移动弱网络优化方法总结》 《从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》 《现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障》 《腾讯技术分享:社交网络图片的带宽压缩技术演进之路》 《小白必读:闲话HTTP短连接中的Session和Token》 《IM开发基础知识补课:正确理解前置HTTP SSO单点登陆接口的原理》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《移动端IM开发需要面对的技术问题》 《开发IM是自己设计协议用字节流好还是字符流好?》 《请问有人知道语音留言聊天的主流实现方式吗?》 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《一个低成本确保IM消息时序的方法探讨》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《谈谈移动端 IM 开发中登录请求的优化》 《移动端IM登录时拉取数据如何作到省流量?》 《浅谈移动端IM的多点登陆和消息漫游原理》 《完全自已开发的IM该如何设计“失败重试”机制?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《微信对网络影响的技术试验及分析(论文全文)》 《即时通讯系统的原理、技术和应用(技术论文)》 《开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀》 《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》 《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》 《腾讯原创分享(三):如何大幅压缩移动网络下APP的流量消耗(下篇)》 《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》 《基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?》 《腾讯技术分享:腾讯是如何大幅降低带宽和网络流量的(图片压缩篇)》 《腾讯技术分享:腾讯是如何大幅降低带宽和网络流量的(音视频技术篇)》 《为什么说即时通讯社交APP创业就是一个坑?》 《字符编码那点事:快速理解ASCII、Unicode、GBK和UTF-8》 《全面掌握移动端主流图片格式的特点、性能、调优等》 《最火移动端跨平台方案盘点:React Native、weex、Flutter》 《子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token

1、前言 众所周之,IM是个典型的快速数据流交换系统,当今主流IM系统(尤其移动端IM)的数据流交换方式都是Http短连接+TCP或UDP长连接来实现。Http短连接主要用于从服务器读取各种持久化信息:比如用户信息、聊天历史记录、好友列表等等,长连接则是用于实时的聊天消息或指令的接收和发送。 作为IM系统中不可或缺的技术,Http短连的重要性无可替代,但Http作为传统互联网信息交换技术,一些典型的概念比如:Cookie、Session、Token,对于IM新手程序员来说并不容易理解。鉴于Http短连接在IM系统中的重要性,如何正确地理解Cookie、Session、Token这样的东西,决定了您的技术方案能否找到最佳实践。本文将从基础上讲解这3者的原理、用途以及正确地应用场景。 题外话:本文讨论的使用Http短连接的话题可能并不适用于微信这样的IM,因为微信的短连接并非使用Http标准协议实现,而是基于自研的Mars网络层框架再造了一套短连接机制,从而更适用于IM这种场景(更低延迟、更省流量、更好的弱网适应算法等),详情请见《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》。当然,Mars虽好,但不一定适合您的团队,因为定制的方案相较于标准通用方案来说,没有强大的技术实力,还是不太容易掌控的了的。 文章:《移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”》、《移动端IM开发者必读(二):史上最全移动弱网络优化方法总结》、《现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障》详述了现今移动网络下http短连接的网络层技术问题,有助于更好地理解本文,有兴趣的话也推荐读一读。 小白必读:如果本文对你来说有点枯燥,那么读这篇吧:《小白必读:闲话HTTP短连接中的Session和Token》。 2、系列文章 ▼ IM开发干货系列文章(本文是其第13篇): 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》 《移动端IM登录时拉取数据如何作到省流量?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《浅谈移动端IM的多点登陆和消息漫游原理》 《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》(本文) 《IM群聊消息的已读回执功能该怎么实现?》 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》 如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。 3、什么是Cookie? Cookie 技术产生源于 HTTP 协议在互联网上的急速发展。随着互联网时代的策马奔腾,带宽等限制不存在了,人们需要更复杂的互联网交互活动,就必须同服务器保持活动状态(简称:保活)。于是,在浏览器发展初期,为了适应用户的需求技术上推出了各种保持 Web 浏览状态的手段,其中就包括了 Cookie 技术。Cookie 在计算机中是个存储在浏览器目录中的文本文件,当浏览器运行时,存储在 RAM 中发挥作用 (此种 Cookies 称作 Session Cookies),一旦用户从该网站或服务器退出,Cookie 可存储在用户本地的硬盘上 (此种 Cookies 称作 Persistent Cookies)。 Cookie 起源:1993 年,网景公司雇员 Lou Montulli 为了让用户在访问某网站时,进一步提高访问速度,同时也为了进一步实现个人化网络,发明了今天广泛使用的 Cookie。(所以,适当的偷懒也会促进人类计算机发展史的一小步~) Cookie时效性:目前有些 Cookie 是临时的,有些则是持续的。临时的 Cookie 只在浏览器上保存一段规定的时间,一旦超过规定的时间,该 Cookie 就会被系统清除。 Cookie使用限制:Cookie 必须在 HTML 文件的内容输出之前设置;不同的浏览器 (Netscape Navigator、Internet Explorer) 对 Cookie 的处理不一致,使用时一定要考虑;客户端用户如果设置禁止 Cookie,则 Cookie 不能建立。 并且在客户端,一个浏览器能创建的 Cookie 数量最多为 300 个,并且每个不能超过 4KB,每个 Web 站点能设置的 Cookie 总数不能超过 20 个。 执行流程: A:首先,客户端会发送一个http请求到服务器端; B: 服务器端接受客户端请求后,发送一个http响应到客户端,这个响应头,其中就包含Set-Cookie头部; C:在客户端发起的第二次请求(注意:如果服务器需要我们带上Cookie,我们就需要在B步骤上面拿到这个Cookie然后作为请求头一起发起第二次请求),提供给了服务器端可以用来唯一标识客户端身份的信息。这时,服务器端也就可以判断客户端是否启用了cookies。尽管,用户可能在和应用程序交互的过程中突然禁用cookies的使用,但是,这个情况基本是不太可能发生的,所以可以不加以考虑,这在实践中也被证明是对的。 为了方便理解,可以先看下这张流程执行图加深概念:   那么,在浏览器上面的请求头和Cookie在那?下图给大家截取了其中一种: 4、Cookie 和 Session 众所周知,HTTP 是一个无状态协议,所以客户端每次发出请求时,下一次请求无法得知上一次请求所包含的状态数据,如何能把一个用户的状态数据关联起来呢? 比如在淘宝的某个页面中,你进行了登陆操作。当你跳转到商品页时,服务端如何知道你是已经登陆的状态? 5、关于Session Cookie 虽然很方便,但是使用 Cookie 有一个很大的弊端,Cookie 中的所有数据在客户端就可以被修改,数据非常容易被伪造,那么一些重要的数据就不能存放在 Cookie 中了,而且如果 Cookie 中数据字段太多会影响传输效率。为了解决这些问题,就产生了 Session,Session 中的数据是保留在服务器端的。 总之:Session是对于服务端来说的,客户端是没有Session一说的。Session是服务器在和客户端建立连接时添加客户端连接标志,最终会在服务器软件(Apache、Tomcat、JBoss)转化为一个临时Cookie发送给给客户端,当客户端第一请求时服务器会检查是否携带了这个Session(临时Cookie),如果没有则会添加Session,如果有就拿出这个Session来做相关操作。 Session 的运作通过一个session_id来进行。session_id通常是存放在客户端的 Cookie 中,比如在 express 中(说的是Nodejs),默认是connect.sid这个字段,当请求到来时,服务端检查 Cookie 中保存的 session_id 并通过这个 session_id 与服务器端的 Session data 关联起来,进行数据的保存和修改。 这意思就是说,当你浏览一个网页时,服务端随机产生一个 1024 比特长的字符串,然后存在你 Cookie 中的connect.sid字段中。当你下次访问时,Cookie 会带有这个字符串,然后浏览器就知道你是上次访问过的某某某,然后从服务器的存储中取出上次记录在你身上的数据。由于字符串是随机产生的,而且位数足够多,所以也不担心有人能够伪造。伪造成功的概率比坐在家里编程时被邻居家的狗突然闯入并咬死的几率还低。 一个完整的Cookie+Session应用过程如下图所示: Session 可以存放在: 1)内存; 2)Cookie本身; 3)redis 或 memcached 等缓存中; 4)数据库中。 线上来说,缓存的方案比较常见,存数据库的话,查询效率相比前三者都太低,不推荐;Cookie Session 有安全性问题,下面会提到。 传统的身份验证方法从最早的Cookie到Session以及给Session Cookie做个加密,接下来我们来看看Token认证。 6、什么是Token? 6.1Token的起源 诸如Ember,Angular,Backbone之类的Web前端框架类库正随着更加精细的Web应用而日益壮大。正因如此,服务器端的组建也正正在从传统的任务中解脱,转而变的更像API。API使得传统的前端和后端的概念解耦。开发者可以脱离前端,独立的开发后端,在测试上获得更大的便利。这种途径也使得一个移动应用和网页应用可以使用相同的后端。 当使用一个API时,其中一个挑战就是认证(authentication)。在传统的web应用中,服务端成功的返回一个响应(response)依赖于两件事。一是,他通过一种存储机制保存了会话信息(Session)。每一个会话都有它独特的信息(id),常常是一个长的,随机化的字符串,它被用来让未来的请求(Request)检索信息。其次,包含在响应头(Header)里面的信息使客户端保存了一个Cookie。服务器自动的在每个子请求里面加上了会话ID,这使得服务器可以通过检索Session中的信息来辨别用户。这就是传统的web应用逃避HTTP面向无连接的方法(This is how traditional web applications get around the fact that HTTP is stateless)。 API应该被设计成无状态的(Stateless)。这意味着没有登陆,注销的方法,也没有sessions,API的设计者同样也不能依赖Cookie,因为不能保证这些request是由浏览器所发出的。自然,我们需要一个新的机制。Token这种东西就应运而生了。 6.2Token是什么 token是用户身份的验证方式,我们通常叫它:令牌。最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。还可以把不变的参数也放进token,避免多次查库。 我们可以把Token想象成一个安全的护照。你在一个安全的前台验证你的身份(通过你的用户名和密码),如果你成功验证了自己,你就可以取得这个。当你走进大楼的时候(试图从调用API获取资源),你会被要求验证你的护照,而不是在前台重新验证。 简单来说,就像下图这样: 6.3Token的应用场景 Token的使用流程: A:当用户首次登录成功(注册也是一种可以适用的场景)之后, 服务器端就会生成一个 token 值,这个值,会在服务器保存token值(保存在数据库中),再将这个token值返回给客户端; B:客户端拿到 token 值之后,进行本地保存。(SP存储是大家能够比较支持和易于理解操作的存储); C:当客户端再次发送网络请求(一般不是登录请求)的时候,就会将这个 token 值附带到参数中发送给服务器; D:服务器接收到客户端的请求之后,会取出token值与保存在本地(数据库)中的token值做对比。 Token的身份认证逻辑: 对比一:如果两个 token 值相同, 说明用户登录成功过!当前用户处于登录状态! 对比二:如果没有这个 token 值, 则说明没有登录成功; 对比三:如果 token 值不同: 说明原来的登录信息已经失效,让用户重新登录。 6.4Token的安全性 我们可以保存认证过的Token记录在服务器上,来添加一个附加的安全层,然后在每一步验证Token的时候验证这个记录(比如每次客户端请求API时检查这个Token的合法性)。这将会阻止第三方伪装一个Token,也将会使得服务器可以失效一个Token。 7、Cookie和Session的区别小结 1)cookie数据存放在客户的浏览器上,session数据放在服务器上; 2)cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session; 3)session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie; 4)单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。 所以个人建议: 将登陆信息等重要信息存放为session; 其他信息如果需要保留,可以放在cookie中。 8、Token 和 Session 的区别小结 Session和 token并不矛盾,作为身份认证token安全性比Session好,因为每个请求都有签名还能防止监听以及重放攻击,而Session就必须靠链路层来保障通讯安全了。如上所说,如果你需要实现有状态的会话,仍然可以增加session来在服务器端保存一些状态 App通常用restful api跟server打交道。Rest是stateless的,也就是app不需要像browser那样用cookie来保存Session,因此用Session token来标示自己就够了,session/state由api server的逻辑处理。如果你的后端不是stateless的rest api,那么你可能需要在app里保存Session.可以在app里嵌入webkit,用一个隐藏的browser来管理cookie Session. Session是一种HTTP存储机制,目的是为无状态的HTTP提供的持久机制。所谓Session认证只是简单的把User信息存储到Session里,因为SID的不可预测性,暂且认为是安全的。这是一种认证手段。而Token,如果指的是OAuth Token或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对App。其目的是让 某App有权利访问 某用户 的信息。这里的Token是唯一的。不可以转移到其它App上,也不可以转到其它 用户 上。转过来说Session。Session只提供一种简单的认证,即有此SID,即认为有此User的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方App。所以简单来说,如果你的用户数据可能需要和第三方共享,或者允许第三方调用API接口,用Token。如果永远只是自己的网站,自己的App,用什么就无所谓了。 Token就是令牌,比如你授权(登录)一个程序时,他就是个依据,判断你是否已经授权该软件;cookie就是写在客户端的一个txt文件,里面包括你登录信息之类的,这样你下次在登录某个网站,就会自动调用cookie自动登录用户名;session和cookie差不多,只是Session是写在服务器端的文件,也需要在客户端写入cookie文件,但是文件里是你的浏览器编号。Session的状态是存储在服务器端,客户端只有Session id;而Token的状态是存储在客户端。 附录:更多IM技术文章 [1] 有关IM安全的文章: 《即时通讯安全篇(一):正确地理解和使用Android端加密算法》 《即时通讯安全篇(二):探讨组合加密算法在IM中的应用》 《即时通讯安全篇(三):常用加解密算法与通讯安全讲解》 《即时通讯安全篇(四):实例分析Android中密钥硬编码的风险》 《即时通讯安全篇(五):对称加密技术在Android平台上的应用实践》 《即时通讯安全篇(六):非对称加密技术的原理与应用实践》 《传输层安全协议SSL/TLS的Java平台实现简介和Demo演示》 《理论联系实际:一套典型的IM通信协议设计详解(含安全层设计)》 《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》 《来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端安全通信的利器——端到端加密(E2EE)技术详解》 《Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)》 《通俗易懂:一篇掌握即时通讯的消息传输安全原理》 [2] 有关IM架构设计的文章: 《浅谈IM系统的架构设计》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一套原创分布式即时通讯(IM)系统理论架构方案》 《从零到卓越:京东客服即时通讯系统的技术架构演进历程》 《蘑菇街即时通讯/IM服务器开发之架构选择》 《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《17年的实践:腾讯海量产品的技术方法论》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《现代IM系统中聊天消息的同步和存储方案探讨》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 [3] IM开发综合文章: 《从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》 《现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障》 《腾讯技术分享:社交网络图片的带宽压缩技术演进之路》 《IM开发基础知识补课:正确理解前置HTTP SSO单点登陆接口的原理》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《移动端IM开发需要面对的技术问题》 《开发IM是自己设计协议用字节流好还是字符流好?》 《请问有人知道语音留言聊天的主流实现方式吗?》 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《一个低成本确保IM消息时序的方法探讨》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《谈谈移动端 IM 开发中登录请求的优化》 《移动端IM登录时拉取数据如何作到省流量?》 《浅谈移动端IM的多点登陆和消息漫游原理》 《完全自已开发的IM该如何设计“失败重试”机制?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《微信对网络影响的技术试验及分析(论文全文)》 《即时通讯系统的原理、技术和应用(技术论文)》 《开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀》 《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》 《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》 《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》 《基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?》 >  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议

1、前言 IM应用从服务端数据的角度来看,它是一种很特殊的应用场景,抛开基础数据、增值业务和附属功能不谈,单从IM聊天工具的立身之本——聊天数据来说,理论上是不需要在服务端存储的(或者说只需要短暂存储——比如离线消息,上线即拉走),这也是为什么微信在前段时间号称绝不存储用户聊天数据的原因(从技术上说这不是没有道理的,但到底有没有存储,这已经超越技术范畴了,不在此文讨论之列 ^_^)。 那么为什么说IM系统的服务端从技术上说,是不需要存储聊天数据的呢? 原因很简单,我们知道IM的聊天数据分两种: 一种是实时消息(就是你在线,对方也在线情况下的聊天数据交互); 一种是离线消息(就是你在线,对方不在线时,你发过去的消息,对于对方而言就是离线消息了)。 实时消息的收发:服务端只作为中转角色(关于中转的技术问题,很多人可能还在结纠老思维为何不用P2P,我已经论坛说烂了,说白了跟技术无关,其实一个很重要的原因就是为了运营的可控性:比如用户P2P去了,违法的锅你运营方来背好不好?),聊天消息在此时就相当于左手倒右手——即聊天数据的本质就是从A用户经过服务端到达B用户就完了,服务端完全没必要存储(当然,我们讨论的是技术理想情况,实际上抛开技术因素来说,这么多丰富的用户行为数据你是运营方你会放过吗?但,这跟技术无关对吧)。 离线消息的收发:当接收方不在线时,发送方的聊天数据在服务端只需要作短因果报应存储,因为接收方一旦上线就拉走了,服务器删除即可(注意:从技术上来说就是这样的哦)。对用户而言聊天消息的社会学的本质来说就像两个人在对话,我已经听见你说的就好了,干吗老像复读机一样一遍一遍一说给我听? 正如上述所言,IM系统中最重要的聊天数据从技术上不说其实是没有存储的必要的。不过话虽如此,但一个大型的IM系统的方方面面数据量也是很可观的,所以开发IM系统时讨论服务端数据库的读写分离、水平分表等,是很有必要的。因而通过本文快速理解服务端数据库的读写分离原理你不应错过,本文也同时建议您在正确理解它的前提下再慎重决定您的服务端架构方案是否需要数据库读写分离,因为很多时候增加缓存策略就能解决的问题,就没有必在大炮打蚊子了。 好了,费话多说了几句,我们开始阅读正文。 2、相关文章 ▼ 跟IM数据存储架构有关的文章,有如下几篇,或许对你有用: 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《现代IM系统中聊天消息的同步和存储方案探讨》 ▼ IM开发干货系列文章适合作为IM开发热点问题参考资料(本文是其第12篇): 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》 《移动端IM登录时拉取数据如何作到省流量?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《浅谈移动端IM的多点登陆和消息漫游原理》 《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》(本文) 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 《IM群聊消息的已读回执功能该怎么实现?》 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》 如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。 3、什么是数据库读写分离?   如上图所示,一主多从、读写分离、主动同步,是一种常见的数据库架构。 一般来说: 主库——提供数据库写服务; 从库——提供数据库读服务。 主从库之间,通过某种机制同步数据,例如mysql的binlog。 像上述图中这样,一个主从同步集群通常被称为一个“分组”。 那么,数据库“分组”架构究竟解决什么问题? 大部分互联网业务读多写少,数据库的读往往最先成为性能瓶颈,如果希望: 线性提升数据库读性能; 通过消除读写锁冲突提升数据库写性能; 此时可以使用分组架构。 一句话总结:“分组”主要解决“数据库读性能瓶颈”问题,在数据库扛不住读的时候,用“分组”架构实现读写分离,通过增加从库线性提升系统读性能。 4、什么是数据库水平切分?   如上图所示,跟数据库“分组”架构实现读写分离一样,水平切分(也称大表拆分、分表),也是一种常见的数据库架构手段。 一般来说: 每个数据库之间没有数据重合,没有类似binlog同步的关联; 所有数据并集,组成全部数据; 会用算法,来完成数据分割,例如“取模”; 一个水平切分集群中的每一个数据库,通常称为一个“分片”。 水平切分架构究竟解决什么问题? 大部分互联网业务数据量很大,单库容量容易成为瓶颈,如果希望: 线性降低单库数据容量; 线性提升数据库写性能; 此时可以使用水平切分架构。 一句话总结:数据库水平切分架构主要解决“数据库数据量大”(或者更细一点说是单表数据量太大)问题,在数据库容量扛不住的时候,通常水平切分。 5、数据库读写分离虽好,但不应滥用 对于互联网大数据量、高并发量、高可用要求高、一致性要求高、前端面向用户的业务场景,如果数据库读写分离: 数据库连接池需要区分:读连接池,写连接池; 如果要保证读高可用,读连接池要实现故障自动转移; 有潜在的主库从库一致性问题。   实际上,如果您的系统面临的是“读性能瓶颈”问题,增加缓存可能来得更直接,更容易一点。 另外,从成本上说,从库的成本比缓存高不少。而且对于云上的架构,以阿里云为例,主库提供高可用服务,从库不提供高可用服务,实现方案上更主流。 所以,上述业务场景下,建议使用缓存架构来加强系统读性能,替代数据库主从分离架构。 当然,使用缓存架构的潜在问题:如果缓存挂了,流量全部压到数据库上,数据库会雪崩。不过幸好,云上的缓存一般都提供高可用的服务。 6、简单小结 典型的大型互联应用架构中,服务端数据库架构主要使用以下两种: 使用“分组”架构实现数据库读写分离:解决“数据库读性能瓶颈”问题; 使用“分片”架构实现数据库水平切分:解决“数据库数据量大”问题。 但对于互联网大数据量、高并发量、高可用要求高、一致性要求高、前端面向用户的业务场景,使用微服务缓存架构,很多时候可能比数据库读写分离架构更合适。 附录:更多IM开发文章 [1] 有关IM架构设计: 《浅谈IM系统的架构设计》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一套原创分布式即时通讯(IM)系统理论架构方案》 《从零到卓越:京东客服即时通讯系统的技术架构演进历程》 《蘑菇街即时通讯/IM服务器开发之架构选择》 《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《17年的实践:腾讯海量产品的技术方法论》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《现代IM系统中聊天消息的同步和存储方案探讨》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 >> 更多同类文章 …… [2] 有关IM安全的文章: 《即时通讯安全篇(一):正确地理解和使用Android端加密算法》 《即时通讯安全篇(二):探讨组合加密算法在IM中的应用》 《即时通讯安全篇(三):常用加解密算法与通讯安全讲解》 《即时通讯安全篇(四):实例分析Android中密钥硬编码的风险》 《即时通讯安全篇(五):对称加密技术在Android平台上的应用实践》 《即时通讯安全篇(六):非对称加密技术的原理与应用实践》 《传输层安全协议SSL/TLS的Java平台实现简介和Demo演示》 《理论联系实际:一套典型的IM通信协议设计详解(含安全层设计)》 《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》 《来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端安全通信的利器——端到端加密(E2EE)技术详解》 《Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)》 《通俗易懂:一篇掌握即时通讯的消息传输安全原理》 [3] IM开发综合文章: 《IM开发基础知识补课:正确理解前置HTTP SSO单点登陆接口的原理》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《移动端IM开发需要面对的技术问题》 《开发IM是自己设计协议用字节流好还是字符流好?》 《请问有人知道语音留言聊天的主流实现方式吗?》 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《一个低成本确保IM消息时序的方法探讨》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《谈谈移动端 IM 开发中登录请求的优化》 《移动端IM登录时拉取数据如何作到省流量?》 《浅谈移动端IM的多点登陆和消息漫游原理》 《完全自已开发的IM该如何设计“失败重试”机制?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《微信对网络影响的技术试验及分析(论文全文)》 《即时通讯系统的原理、技术和应用(技术论文)》 《开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀》 《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》 《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》 《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》 《基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?》  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?

1、前言 一个完善的IM系统中通常充斥着大量的图片内容,包括:用户头像、图片消息、相册、图片表情等等,那么在做服务端架构设计时该如何存储这些图片呢? 本文分享的是典型Web应用中大量图片的服务端存储加构的演进过程,但基本的技术原理和架构思路对于IM系统而言同样适用,所以在阅读时可以根据自已IM的实际架构情况,酌情吸取适合您的内容即可。文中部分观点可作抛砖引玉之用,可能并非最佳实践,请勿迷信之。 实际上:旧式的PC端IM中,诸如图片消息这种业务形态,可能是通过长连接直接推送过去(所谓的实时图片传输嘛),这种情况理论上是不需要服务端存储的。但现今的主流移动端IM,基于移动网络抖动大、 不稳定的特性和随时随地社交分享的现实,已很少使用实时传输这种技术手段。现在主流IM都是本文所述的这种:通过Http短连接从云(也就是服务端)“拉取”,这种方式的好处是:随时随地分享、对网络稳定性要求低(只要上传者一次上传,服务端可长时间存储,下一个阅读者通过URL按需随读随取即可,再次分享时只要分享URL而无需再次完整传输整个图片)。 以此类推:IM系统中,实际上还存在其它类似于图片的小文件存储需求,比如:语音留言消息中的AMR短音频文件(有些IM中为了音质可能使用的是AAC音频格式,比如易信)、短视频功能中的小视频文件等,这些文件的存储和使用跟图片文件基本类似,所以考虑到通用性,如果能把这些小文件存储也纳入到图片的存储架构中,对于整体系统架构来说(尤其存储部分)就显的更通用。所以本文中虽然以图片存储为切入点,但您实际上完全可以套用到基它小文件的存储上哦。 2、相关文章 ▼ 跟IM数据存储架构有关的文章,有如下几篇,或许对你有用: 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《现代IM系统中聊天消息的同步和存储方案探讨》 ▼ IM开发干货系列文章适合作为IM开发热点问题参考资料(本文是其第11篇): 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》 《移动端IM登录时拉取数据如何作到省流量?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《浅谈移动端IM的多点登陆和消息漫游原理》 《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》(本文) 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 《IM群聊消息的已读回执功能该怎么实现?》 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》 如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。 3、单机时代的图片服务器架构(集中式) 初创时期由于时间紧迫,开发人员水平很有限。 所以通常就直接在website文件所在的目录下,建立1个upload子目录,用于保存用户上传的图片文件: 如果按业务再细分,可以在upload目录下再建立不同的子目录来区分,例如:upload\QA,upload\Face等 在数据库表中保存的也是“upload/qa/test.jpg”这类相对路径; 用户的访问方式如下:http://www.yourdomain.com/upload/qa/test.jpg 程序上传和写入方式: 程序员A通过在web.config中配置物理目录D:\Web\yourdomain\upload  然后通过stream的方式写入文件; 程序员B通过Server.MapPath等方式,根据相对路径获取物理目录  然后也通过stream的方式写入文件。 结果就是: 优点:实现起来最简单,无需任何复杂技术,就能成功将用户上传的文件写入指定目录。保存数据库记录和访问起来倒是也很方便; 缺点:上传方式混乱,严重不利于网站的扩展。 针对上述最原始的架构,主要面临着如下问题: 随着upload目录中文件越来越多,所在分区如果出现容量不足,则很难扩容。只能停机后更换更大容量的存储设备,再将旧数据导入; 在部署新版本(部署新版本前通过需要备份)和日常备份website文件的时候,需要同时操作upload目录中的文件,如果考虑到访问量上升,后边部署由多台Web服务器组成的负载均衡集群,集群节点之间如果做好文件实时同步将是个难题。 4、集群时代的图片服务器架构(实时同步) 一个传统的Web服务端站点下面,新建一个名为upload的虚拟目录,由于虚拟目录的灵活性,能在一定程度上取代物理目录,并兼容原有的图片上传和访问方式。 用户的访问方式依然是: http://www.yourdomain.com/upload/qa/test.jpg 优点:配置更加灵活,也能兼容老版本的上传和访问方式。因为虚拟目录,可以指向本地任意盘符下的任意目录。这样一来,还可以通过接入外置存储,来进行单机的容量扩展。 缺点:部署成由多台Web服务器组成的集群,各个Web服务器(集群节点)之间(虚拟目录下的)需要实时的去同步文件,由于同步效率和实时性的限制,很难保证某一时刻各节点上文件是完全一致的。 基本架构如下图所示:   从上图可看出,整个Web服务器架构已经具备“可扩展、高可用”了,主要问题和瓶颈都集中在多台服务器之间的文件同步上。 上述架构中只能在这几台Web服务器上互相“增量同步”,这样一来,就不支持文件的“删除、更新”操作的同步了。 早期的想法是,在应用程序层面做控制,当用户请求在web1服务器进行上传写入的同时,也同步去调用其它web服务器上的上传接口,这显然是得不偿失的。所以我们选择使用Rsync类的软件来做定时文件同步的,从而省去了“重复造轮子”的成本,也降低了风险性。 同步操作里面,一般有比较经典的两种模型,即推拉模型:所谓“拉”,就是指轮询地去获取更新,所谓推,就是发生更改后主动的“推”给其它机器。当然,也可以采用加高级的事件通知机制来完成此类动作。 在高并发写入的场景中,同步都会出现效率和实时性问题,而且大量文件同步也是很消耗系统和带宽资源的(跨网段则更明显)。 5、集群时代的图片服务器架构改进(共享存储) 沿用虚拟目录的方式,通过UNC(网络路径)的方式实现共享存储(将upload虚拟目录指向UNC)。 用户的访问方式1: http://www.yourdomain.com/upload/qa/test.jpg 用户的访问方式2(可以配置独立域名): http://img.yourdomain.com/upload/qa/test.jpg 支持UNC所在server上配置独立域名指向,并配置轻量级的web服务器,来实现独立图片服务器。 优点: 通过UNC(网络路径)的方式来进行读写操作,可以避免多服务器之间同步相关的问题。相对来讲很灵活,也支持扩容/扩展。支持配置成独立图片服务器和域名访问,也完整兼容旧版本的访问规则。 缺点:但是UNC配置有些繁琐,而且会造成一定的(读写和安全)性能损失。可能会出现“单点故障”。如果存储级别没有raid或者更高级的灾备措施,还会造成数据丢失。 基本架构如下图所示:   在早期的很多基于Linux开源架构的网站中,如果不想同步图片,可能会利用NFS来实现。事实证明,NFS在高并发读写和海量存储方面,效率上存在一定问题,并非最佳的选择,所以大部分互联网公司都不会使用NFS来实现此类应用。当然,也可以通过Windows自带的DFS来实现,缺点是“配置复杂,效率未知,而且缺乏资料大量的实际案例”。另外,也有一些公司采用FTP或Samba来实现。 上面提到的几种架构,在上传/下载操作时,都经过了Web服务器(虽然共享存储的这种架构,也可以配置独立域名和站点来提供图片访问,但上传写入仍然得经过Web服务器上的应用程序来处理),这对Web服务器来讲无疑是造成巨大的压力。所以,更建议使用独立的图片服务器和独立的域名,来提供用户图片的上传和访问。 6、独立图片服务器/独立域名的好处 图片访问是很消耗服务器资源的(因为会涉及到操作系统的上下文切换和磁盘I/O操作)。分离出来后,Web/App服务器可以更专注发挥动态处理的能力。 独立存储,更方便做扩容、容灾和数据迁移; 浏览器(相同域名下的)并发策略限制,性能损失; 访问图片时,请求信息中总带cookie信息,也会造成性能损失; 方便做图片访问请求的负载均衡,方便应用各种缓存策略(HTTP Header、Proxy Cache等),也更加方便迁移到CDN; ...... 我们可以使用Lighttpd或者Nginx等轻量级的web服务器来架构独立图片服务器。 7、我们当前的图片服务器架构 当前图片服务器架构采用分布式文件系统+CDN。 在构建当前的图片服务器架构之前,可以先彻底撇开web服务器,直接配置单独的图片服务器/域名。 但面临如下的问题: 旧图片数据怎么办?能否继续兼容旧图片路径访问规则? 独立的图片服务器上需要提供单独的上传写入的接口(服务API对外发布),安全问题如何保证? 同理,假如有多台独立图片服务器,是使用可扩展的共享存储方案,还是采用实时同步机制? 直到应用级别的(非系统级) DFS(例如FastDFS HDFS MogileFs MooseFS、TFS)的流行,简化了这个问题:执行冗余备份、支持自动同步、支持线性扩展、支持主流语言的客户端api上传/下载/删除等操作,部分支持文件索引,部分支持提供Web的方式来访问。 考虑到各DFS的特点,客户端API语言支持情况(需要支持C#),文档和案例,以及社区的支持度,我们最终选择了FastDFS来部署。 唯一的问题是:可能会不兼容旧版本的访问规则。如果将旧图片一次性导入FastDFS,但由于旧图片访问路径分布存储在不同业务数据库的各个表中,整体更新起来也十分困难,所以必须得兼容旧版本的访问规则。架构升级往往比做全新架构更有难度,就是因为还要兼容之前版本的问题。(给飞机在空中换引擎可比造架飞机难得多) 解决方案如下: 首先,关闭旧版本上传入口(避免继续使用导致数据不一致)。将旧图片数据通过rsync工具一次性迁移到独立的图片服务器上(即下图中描述的Old Image Server)。在最前端(七层代理,如Haproxy、Nginx)用ACL(访问规则控制),将旧图片对应URL规则的请求(正则)匹配到,然后将请求直接转发指定的web 服务器列表,在该列表中的服务器上配置好提供图片(以Web方式)访问的站点,并加入缓存策略。这样实现旧图片服务器的分离和缓存,兼容了旧图片的访问规则并提升旧图片访问效率,也避免了实时同步所带来的问题。 整体架构如图:   8、使用第3方CDN的方案 基于FastDFS的独立图片服务器集群架构,虽然已经非常的成熟,但是由于国内“南北互联”和IDC带宽成本等问题(图片是非常消耗流量的),我们最终还是选择了商用的CDN技术,实现起来也非常容易,原理其实也很简单,我这里只做个简单的介绍。 将img域名cname到CDN厂商指定的域名上,用户请求访问图片时,则由CDN厂商提供智能DNS解析,将最近的(当然也可能有其它更复杂的策略,例如负载情况、健康状态等)服务节点地址返回给用户,用户请求到达指定的服务器节点上,该节点上提供了类似Squid/Vanish的代理缓存服务,如果是第一次请求该路径,则会从源站获取图片资源返回客户端浏览器,如果缓存中存在,则直接从缓存中获取并返回给客户端浏览器,完成请求/响应过程。 由于采用了商用CDN服务,所以我们并没有考虑用Squid/Vanish来自行构建前置代理缓存。 上面的整个集群架构,可以很方便的做横向扩展,能满足一般垂直领域中大型网站的图片服务需求(当然,像taobao这样超大规模的可能另当别论)。经测试,提供图片访问的单台Nginx服务器(至强E5四核CPU、16G内存、SSD),对小静态页面(压缩后大概只有10kb左右的)可以扛住几千个并发且毫无压力。当然,由于图片本身体积比纯文本的静态页面大很多,提供图片访问的服务器的抗并发能力,往往会受限于磁盘的I/O处理能力和IDC提供的带宽。Nginx的抗并发能力还是非常强的,而且对资源占用很低,尤其是处理静态资源,似乎都不需要有过多担心了。可以根据实际访问量的需求,通过调整Nginx的参数,对Linux内核做调优,加入分级缓存策略等手段能够做更大程度的优化,也可以通过增加服务器或者升级服务器配置来做扩展,最直接的是通过购买更高级的存储设备和更大的带宽,以满足更大访问量的需求。 值得一提的是,在“云计算”流行的当下,也推荐高速发展期间的网站,使用“云存储”这样的方案,既能帮你解决各类存储、扩展、备灾的问题,又能做好CDN加速。最重要的是,价格也不贵。 总结,有关图片服务器架构扩展,大致围绕这些问题展开: 容量规划和扩展问题; 数据的同步、冗余和容灾; 硬件设备的成本和可靠性(是普通机械硬盘,还是SSD,或者更高端的存储设备和方案); 文件系统的选择。根据文件特性(例如文件大小、读写比例等)选择是用ext3/4或者NFS/GFS/TFS这些开源的(分布式)文件系统; 图片的加速访问。采用商用CDN或者自建的代理缓存、web静态缓存架构; 旧图片路径和访问规则的兼容性,应用程序层面的可扩展,上传和访问的性能和安全性等。 (原文链接:https://mp.weixin.qq.com/s/0dmZ4kB9PB5_i7zu5JirbQ,有改动) 附录:更多IM开发文章 [1] 有关IM架构设计: 《浅谈IM系统的架构设计》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一套原创分布式即时通讯(IM)系统理论架构方案》 《从零到卓越:京东客服即时通讯系统的技术架构演进历程》 《蘑菇街即时通讯/IM服务器开发之架构选择》 《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《17年的实践:腾讯海量产品的技术方法论》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《现代IM系统中聊天消息的同步和存储方案探讨》 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 [2] 有关IM安全的文章: 《即时通讯安全篇(一):正确地理解和使用Android端加密算法》 《即时通讯安全篇(二):探讨组合加密算法在IM中的应用》 《即时通讯安全篇(三):常用加解密算法与通讯安全讲解》 《即时通讯安全篇(四):实例分析Android中密钥硬编码的风险》 《即时通讯安全篇(五):对称加密技术在Android平台上的应用实践》 《即时通讯安全篇(六):非对称加密技术的原理与应用实践》 《传输层安全协议SSL/TLS的Java平台实现简介和Demo演示》 《理论联系实际:一套典型的IM通信协议设计详解(含安全层设计)》 《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》 《来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端安全通信的利器——端到端加密(E2EE)技术详解》 《Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)》 《通俗易懂:一篇掌握即时通讯的消息传输安全原理》 [3] IM开发综合文章: 《IM开发基础知识补课:正确理解前置HTTP SSO单点登陆接口的原理》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《移动端IM开发需要面对的技术问题》 《开发IM是自己设计协议用字节流好还是字符流好?》 《请问有人知道语音留言聊天的主流实现方式吗?》 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《一个低成本确保IM消息时序的方法探讨》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《谈谈移动端 IM 开发中登录请求的优化》 《移动端IM登录时拉取数据如何作到省流量?》 《浅谈移动端IM的多点登陆和消息漫游原理》 《完全自已开发的IM该如何设计“失败重试”机制?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《微信对网络影响的技术试验及分析(论文全文)》 《即时通讯系统的原理、技术和应用(技术论文)》 《开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀》 《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》 《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》 《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》 《基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?》 >>  来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23

IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理

1、前言 一个安全的信息系统,合法身份检查是必须环节。尤其IM这种以“人”为中心的社交体系,身份认证更是必不可少。 一些PC时代小型IM系统中,身份认证可能直接做到长连接中(也就是整个IM系统都是以长连接为中心:身份鉴权、数据收发、文件传送等等)。但当前主流的IM(尤其新一代的移动端IM)中,都是“长”(指TCP或UDP长连接)、“短”(是指Http短连接)相结合的方式。 一个现代的移动端IM“长”、“短”连接配合内容大致如下: 1)短连接用途1:前置HTTP的SSO单点接口来实现身份认证; 2)短连接用途2:集群式的IM中可能还会有独立(或集成于SSO单独登陆接口中)的SLB接口(即基于HTTP短连接拉取IM服务器集群IP列表); 3)短连接用途3:各种小文件的上传、下载接口实现(头像、图片、语音、文件等)都会是基于Http实现; 4)长连接用途1:用户的实时上、下线状态通知; 5)长连接用途2:实时的加友、加群等指令收发; 6)长连接用途3:服务端发起的其它实时指令推送等。 总之:当今主流的移动IM系统中,“长”、“短”连接分工明确,各自将自身的优势发挥到最大化,优点是:系统分工明确、分层清晰、业务划分合理、负载方案成本低、符合移动网络的特性等。 针对上述主流移动IM系统中“长”、“短”连接的分工方式,其中最为重要也是用户最先接触到的——就是基于Http的SSO单点登陆接口(有的系统里可能并不叫SSO接口,本文讨论的是其广义:即实现身份认证功能的http接口),那么这个SSO接口工作原理是什么?可以怎么来实现?有无最佳实践建议? OK,带着上述的这几个疑问,让我们开启本文的正文部分。 正文内容说明:正文部分介绍SSO单点登陆接口时,是以通用信息系统的角度来阐述原理、逻辑、最佳实践,而非专门针对IM系统,但道理是一模一样的,理解原理后完全可以设计出适合您IM系统的SSO接口。(话外音:其实是懒的重新打字和画图 ^_^)。 2、相关文章 ▼ 带着本文对SSO单点登陆(或者说身份认证)接口的知识,您将能更好的读懂下述技术文章: 《浅谈IM系统的架构设计》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《谈谈移动端 IM 开发中登录请求的优化》 《移动端IM登录时拉取数据如何作到省流量?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 总之,以上几篇精选的文章可以跟本文的知识相辅相成,共同完善您的IM技术开发知识体系,希望对你有用。 ▼ IM开发干货系列文章适合作为IM开发热点问题参考资料(本文是其第10篇): 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》 《移动端IM登录时拉取数据如何作到省流量?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《浅谈移动端IM的多点登陆和消息漫游原理》 《IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理》(本文) 《IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》 《IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》 《IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token》 《IM群聊消息的已读回执功能该怎么实现?》 《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?》 《IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列》 如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。 3、原作者 杨丽:拥有多年互联网应用系统研发经验,曾就职于古大集团,现任职中青易游的系统架构师,主要负责公司研发中心业务系统的架构设计以及新技术积累和培训。现阶段主要关注开源软件、软件架构、微服务以及大数据。 张辉清:10 多年的 IT 老兵,先后担任携程架构师、古大集团首席架构、中青易游 CTO 等职务,主导过两家公司的技术架构升级改造工作。现关注架构与工程效率,技术与业务的匹配与融合,技术价值与创新。 4、单点登录原理简介 假设一个场景:公司内部有财务、OA、订单服务等各类相互独立的应用系统,员工张三对这些系统有操作权限,如果张三想要登录某个系统进行业务操作,那么他需要输入相应的账号与密码。 想象一下:当公司内部有 100 个应用系统,张三是不是要输入 100 次用户名和密码进行登录,然后分别才能进行业务操作呢?显然这是很不好的体验。 因此我们需要引入一个这样的机制:张三只要输入一次用户名和密码登录,成功登录后,他就可以访问财务系统、OA 系统、订单服务等系统——这就是单点登录。 单点登录的英文全称是 Single Sign On,简称是 SSO: 它的意思是说用户只需要登录一次,就可以在个人权限范围内,访问所有相互信任应用的功能模块,不管整个应用群的内部有多么复杂,对用户而言,都是一个统一的整体。用户访问 Web 系统的整个应用群与访问单个系统一样,登录和注销分别只要一次就够了。 举个简单的例子,你登录了百度网页之后,点击跳转到百度贴吧,这时可以发现你已经自动登录了百度贴吧——这就是单独登陆的原理。 5、理论联系实际来讲解SSO单点登陆技术实现 5.1基本介绍 针对本文上半部分的原理介绍,我们以一个真实的信息系统为例,理论联系实际来讲解具体的SSO单点登陆技术实现(实际上,用IM系统的设计思路来看这个例子,可能有点复杂,但知识是相通的,它更有助于对SSO完整知识体系的理解。)   SSO 的技术实现要想做好并不容易,作者认为需求优先级应该先是单点登录和单点注销功能,然后是应用接入的门槛,最后是数据安全性,安全性对于 SSO 也非常重要。SSO 的核心是认证中心,但要实现用户一次登录,到处访问的效果,技术实现需要建立在用户系统、认证中心、权限系统、企业门户的基础上。 各职责如下: 用户系统:负责用户名、密码等帐户信息管理,包括增加、修改、启用、停用用户帐号,同时为认证中心提供对用户名和密码的校验; 认证中心:负责凭证 token 的生成、加密、颁发、验证、销毁、登入 Login、登出 Logout。用户只有拥有凭证并验证通过才能访问企业门户; 权限系统:负责角色管理、资源设置、授权设置、鉴定权限,具体实现可参考 RBAC。权限系统可为企业门户提供用户权限范围内的导航; 企业门户:作为应用系统的集成门户 (Portal),集成了多个应用系统的功能,为用户提供链接导航、用户信息和登出功能等。 5.2服务端功能实现 主要包含以下内容: 登录认证:接收登录帐号信息,让用户系统验证用户的登录信息; 凭证生成:创建授权凭证 token,生成的凭证一般包含用户帐号信息、过期时间等信息,它是一串加密的字符串,加密算法如 AES{凭证明文 +MD5 加信息},可采用 JWT 标准; 凭证颁发:与 SSO 客户端通信,发送凭证给 SSO 客户端; 凭证验证:接收并校验来自 SSO 客户端的凭证有效性,凭证验证包括算法验证和数据验证; 凭证销毁与登出:接收来自 SSO 客户端的登出请求,记录并销毁凭证,跳转至登录页面。 5.3客户端功能实现 客户端的实现逻辑大致如下: 1)请求拦截:拦截应用未登录请求,跳转至登录页面; 2)获取凭证:接收并存储由 SSO 服务端发来的凭证,凭证存储的方式有 Cookie、Session、网址传参、Header 等; 3)提交凭证验证:与 SSO 服务端通信,发出校验凭证有效性的请求; 4)获取用户权限:获取该凭证的用户权限,并返回受保护资源给用户; 5)凭证销毁与登出:销毁本地会话,然后跳转至登出页面。 5.4用户单点登录流程   用户的单点登录流程如下: 1)登录:将用户输入的用户名和密码发送至认证中心,然后认证中心调用用户系统来验证登录信息; 2)生成并颁发凭证:通过登录信息的验证后,认证中心创建授权凭证 token,然后把这个授权凭证 token 返回给 SSO 客户端。SSO 客户端拿到这个 token,进行存储。在后续请求中,在 HTTP 请求数据中都得加上这个 token; 3)凭证验证:SSO 客户端发送凭证 token 给认证中心,认证中心校验这个 token 的有效性。凭证验证有算法验证和数据验证,算法验证可在 SSO 客户端完成。 5.5用户访问流程和单点注销 以上是用户的访问流程,如果用户没有有效的凭证,认证中心将强制用户进入登录流程。对于单点注销,用户如果注销了应用群内的其中一个应用,那么全局 token 也会被销毁,应用群内的所有应用将不能再被访问。 5.6具体接入与集成   例子中的应用接入与集成具体如下: 1)用户系统:接入国内机票平台的用户系统,负责登录认证; 2)权限系统:接入国内机票平台的权限系统; 3)认证中心:负责生成并颁发凭证、销毁凭证,改造国内机票平台的登入、登出; 4)凭证验证:在国内机票、国际机票应用系统中调用 SSO 客户端组件实现凭证的验证; 5)企业门户:由国内机票平台、国际机票平台承担。 6、附加知识:一项被称为JWT的技术   JSON Web Token (JWT) 是目前应用最为广泛的 token 格式,是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。该 token 设计紧凑且安全,特别适用于分布式站点的单点登录、API 网关等场景。 JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息。该 token 也可直接被用于认证,也可被加密。JWT 信息体由 3 部分构成:头 Header+ 载荷 Payload+ 签名 Signature。 JWT具体优点如下: JWT 支持多种语言,C#、Java、JavaScript、Node.js、PHP 等很多语言都可以使用; JWT 可以自身存储一些和业务逻辑有关的所必要的非敏感信息,因为有了 Payload 部分; 利于传输,因为 JWT 的构成非常简单,字节占用很小; 不需要在服务端保存会话信息,不仅省去服务端资源开销,而且使得应用易于扩展。 (原文链接:https://mp.weixin.qq.com/s/ZT1B6ziSSRW41FN33xA3ZA,有改动) 附录:更多IM开发技术文章 [1] 有关IM架构设计: 《浅谈IM系统的架构设计》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一套原创分布式即时通讯(IM)系统理论架构方案》 《从零到卓越:京东客服即时通讯系统的技术架构演进历程》 《蘑菇街即时通讯/IM服务器开发之架构选择》 《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《17年的实践:腾讯海量产品的技术方法论》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《现代IM系统中聊天消息的同步和存储方案探讨》 >> 更多同类文章 …… [2] 有关IM安全的文章: 《即时通讯安全篇(一):正确地理解和使用Android端加密算法》 《即时通讯安全篇(二):探讨组合加密算法在IM中的应用》 《即时通讯安全篇(三):常用加解密算法与通讯安全讲解》 《即时通讯安全篇(四):实例分析Android中密钥硬编码的风险》 《即时通讯安全篇(五):对称加密技术在Android平台上的应用实践》 《即时通讯安全篇(六):非对称加密技术的原理与应用实践》 《传输层安全协议SSL/TLS的Java平台实现简介和Demo演示》 《理论联系实际:一套典型的IM通信协议设计详解(含安全层设计)》 《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》 《来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端安全通信的利器——端到端加密(E2EE)技术详解》 《Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)》 《通俗易懂:一篇掌握即时通讯的消息传输安全原理》 [3] IM开发综合文章: 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《移动端IM开发需要面对的技术问题》 《开发IM是自己设计协议用字节流好还是字符流好?》 《请问有人知道语音留言聊天的主流实现方式吗?》 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《一个低成本确保IM消息时序的方法探讨》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《谈谈移动端 IM 开发中登录请求的优化》 《移动端IM登录时拉取数据如何作到省流量?》 《浅谈移动端IM的多点登陆和消息漫游原理》 《完全自已开发的IM该如何设计“失败重试”机制?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《微信对网络影响的技术试验及分析(论文全文)》 《即时通讯系统的原理、技术和应用(技术论文)》 《开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀》 《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》 《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》 《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》 《基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?》 来源:即时通讯网 - 即时通讯开发者社区!

2018-10-23