向酷信即时通讯咨询
售前(售后)咨询,预约演示,详情使用场景
产品概述
更多内容
产品资讯
丹尼尔可以使用的IDEA调试技巧!!!
前天,我采访了一名来自985大学的实习生,问他通常使用什么开发工具。他甚至没有想到IDEA,所以我问我是否用它来调试。如何设置断点条件?这孩子很笨,想了很久,告诉我他从未用过,甚至从未听说过。 作为一名经验丰富的司机,IDEA调试可以说是家常便饭。如果你不能调试,我不相信你读过源代码,所以不要告诉我原理并直接传递它。 基本接口 IDEA的调试运行模式如下: ①在调试模式下启动服务,左按钮在运行模式下启动。在开发过程中,我通常直接启动调试模式,这便于随时调试代码。 ②断点:点击左边行号栏中的左键,或者用快捷键Ctrl+F8按/取消断点,断点行的颜色可以自己设置。 ③调试窗口:当访问请求到达第一个断点后,调试窗口将自动激活。如果没有自动激活,您可以转到设置。 ④调试按钮:共8个按钮,调试的主要功能与这些按钮相对应。将鼠标悬停在按钮上,查看相应的快捷键。 ⑤服务按钮:您可以在此关闭/启动服务并设置断点。 ⑥方法调用栈:线程调试通过的所有方法都显示在这里。如果您选中右上角的[显示所有框架]按钮,其他类库的方法将不会显示,否则这里会有很多方法。 ⑦变量:您可以在变量区域的当前断点之前查看当前方法中的变量。 ⑧观察:要查看变量,您可以拖动变量区域中的变量进行观察。 可变视图 在调试过程中,经常需要观察变量的变化来判断业务逻辑。我们可以在以下四个地方观察它们。 ①观察区域变量中最常用的变量 (2)思想中最人道的地方之一是变量的值被隐藏在变量的后面。 ③观察区域,即眼镜的形状,通常不会展开。下图: 单击“+”添加要观察的变量,单击“-”删除它们。 ④当鼠标悬停在变量上时,变量的值会出现。单击展开查看。 计算表达式 调试业务逻辑时,当某个条件或某个变量的计算值未知时,总是需要判断下一行代码,所以这里需要计算表达式的功能。有两种计算表达式的方法,如下所示: (1)选择要计算的代码,右键单击->求值表达式->求值进行计算。 ②直接点击计算器形状控件,弹出计算窗口,复制代码。注意复制的代码必须是逻辑的,例如,局部变量必须声明。 断点条件设置 对于想阅读Spring源代码的初学者来说,当他们遇到调试UserService的doGetBean的方法时,他们可能会崩溃,因为当容器启动时doGetBean可能会被调用几十次,并且您可以通过在doGetBean方法体中放置断点来使您的生活变得悲惨。 1.设置断点条件有两种方法: (1)直接右键单击断点并添加条件条件。 ②查看断点(ctrl+shift+F8)显示所有断点,并向条件中添加条件。 2.设置异常断点的方法 在设置了异常断点(如空指针异常)后,当出现需要拦截的异常时,程序将自动定位指定的行。下图: ① ctrl+shift+F8显示所有断点,点击+添加Java异常断点。 (2)调试运行,一旦代码有此异常,它将自动定位指定的代码。 线程切换 通常,当我们调试时,会截获一个请求。此时,如果我们想发起另一个请求,我们不能重新发送,因为另一个请求被阻止了。只有当当前线程完成执行时,我们才会离开其他线程。有两种方法可以改变IDEA中的阻塞级别: 1.右键单击断点->选择线程->设为默认,如下图所示: 2.显示所有断点(crtl+shift+F8),选择一个断点,选择线程并设为默认值。下图: 设置阻塞级别,然后可以在线程之间切换,如下图所示: 强行抛出异常 这是Idea在2018年添加的一个新函数,它可以在调试时直接抛出指定的异常。使用方法类似于上面的堆栈放弃框架。右键单击堆栈框架并选择抛出异常,然后输入抛出异常的代码,如抛出新的空指针异常。操作如下: 被迫返回 这是IDEA2015中添加的一个函数,类似于上面的手动异常抛出,但只返回指定的值。使用方法与上述类似。右键单击堆栈框架并选择强制返回,然后输入要返回的值。如果它是无效的,那就更简单了,你不需要输入返回值。下图: 至此,IDEA的一些调试技巧已经被引入,但还不是很完整,还有很多功能强大的功能还没有被引入。如果读者认为陈某写得很好,请表扬他们并分享他们。谢谢你!!! 选自过去期间 帅呆了,你还在搜索整合后的配置吗?老司机教你一个窍门!!! 老司机带你去谈接口限流!!! 春天解决了循环依赖,你真的明白吗? 旧的驱动程序会把你从源代码带到Spring生命周期!!! 老司机带你去手写的衬衫相框!!! 小白高级建筑师缺乏这一套2T视频资源!!!
即时通讯发展干货共享:我如何解决大量离线信息导致客户陷入困境的问题
1.介绍 我已经很久没有写技术文章了。今天的文章不是一篇原则性的文章,而是针对大量离线消息(包括消息漫游)带来的用户体验问题,与大家分享作者开发和实现的即时通讯即时通讯聊天系统升级改造的全过程。 在本文中,我将从以下几个方面介绍它: 1)该即时通讯产品的主要业务和特点; 2)即时消息系统业务状态和难点; 3)升级改造之路; 4)消息确认逻辑的优化。 以下内容都是基于作者个人开发即时消息的宝贵经验。干货满了,我期待你的赞扬。 本文已在“即时通讯技术圈”公开发表。请注意: 即时通讯发展干货共享:如何解决大量离线信息导致客户陷入困境的问题_52im_qr_即时通讯技术圈_ 400px.png。 ▲本文关于公开号码的链接是:https://mp.weixin.qq.com/s/XHdt1IpCrdmaMDxL8WKn3w 2.即时通讯开发干货系列文章 本文是系列文章的第25篇,主要内容如下: 即时消息传递保证机制的实现(一):保证在线实时消息的可靠传递 即时消息传递保证机制的实现(二):保证离线消息的可靠传递 如何确保即时消息的“时间”和“一致性”?》 我应该使用“推”还是“拉”来同步即时消息单聊和群聊的在线状态?》 即时消息群聊新闻如此复杂,如何确保它不丢失或沉重?》 安卓即时通讯智能心跳算法的设计与实现探讨(含示例代码) “当即时消息登录移动终端时,如何通过提取数据来节省流量?》 易于理解:基于集群共享移动终端即时通信接入层的负载均衡方案 浅谈移动即时通信的多点登录和消息漫游原理 构成即时通讯开发的基础知识(一):正确理解前端HTTP单点登录接口的原理 构成即时消息开发的基础知识(2):如何为大量图像文件设计服务器端存储架构?》 补充即时通讯开发的基础知识(3):快速了解服务器端数据库读写分离的原理和实用建议 构成即时消息开发的基础知识(4):正确理解短连接中的Cookie、会话和令牌 如何实现即时通讯群聊信息的阅读回执功能?》 即时消息群聊消息是一份拷贝(即扩散阅读)还是多份拷贝(即扩散写作)?》 构成即时消息开发的基础知识(5):易于理解、正确理解并充分利用MQ消息队列 一种低成本保证即时消息定时的方法探讨 补上一课关于即时消息开发的基础知识(6):您的数据库使用NoSQL还是SQL?读读这个!》 在即时通讯中实现“身边人”功能的原则是什么?如何有效地实现它?》 构成即时通讯开发的基础知识(七):主流移动账户登录方法的原理和设计思路 弥补即时通讯发展的基础知识(8):历史上最流行的,彻底了解字符乱码问题的本质 如何实现即时通讯的扫描码板功能?有一篇文章了解主流应用的扫描代码登陆技术的原理 “即时通讯做手机扫描码登录?让我们先来看看微信扫描码登录功能的技术原理 构成即时通讯发展的基础知识(9):想发展即时通讯集群吗?首先了解什么是RPC!》 “即时消息开发实际的干货:我如何解决大量离线聊天信息导致客户陷入困境的问题”(本文) 此外,如果你是即时通讯开发的初学者,强烈建议你先阅读“初学者:从零开始开发移动即时通讯”。 3.该即时消息产品的主要业务和特点 与传统的互联网行业不同,作者工作的公司(名字不会透露)是一家制作娱乐社交应用的公司,包括小游戏、聊天、朋友圈订阅等。 每个人都应该知道,就技术和产品形式而言,游戏业务与电子商务和旅游业有着本质的不同。 大多数做后端开发的朋友都在开发界面。客户端或浏览器h5通过HTTP向我们后端的控制器接口发出请求,并在后端查询数据库以将JSON返回给客户端。众所周知,HTTP协议具有短连接、无状态、三次握手四次挥动等特点。但是,游戏和实时通信等服务不适合使用HTTP协议。 原因如下: 1)HTTP不能达到实时通信的效果,所以可以被客户端轮询,但是浪费资源太多; 2)三次握手挥了四次,出现了严重的性能问题; 3)无国籍。 例如,当两个用户通过应用聊天时,一方发送消息,另一方需要实时感知消息的到达。当两个或更多的人玩游戏时,玩家需要实时看到彼此的状态。这些场景是无法通过超文本传输协议实现的!因为HTTP只能拉,而聊天和游戏服务需要推。 4.即时消息系统业务状态和难点 4.1业务状况 作者负责整个公司的实时聊天系统。与微信和QQ类似,它具有私人聊天、群聊、发送信息、语音图片、红包等功能。 让我详细介绍一下整个聊天系统是如何工作的。 首先,为了达到实时通信的效果,我们开发了一套基于Netty的长链路网关(扩展阅读:Netty干货共享:北京-东京-麦生产级TCP网关技术实践总结)。采用的协议是MQTT协议,当客户端登录时,应用通过MQTT协议连接到网关(网络服务器)。然后通过MQTT协议将聊天消息推送到网络服务器。网络服务器与网络客户端保持着长期的联系。NettyClient用于处理业务逻辑(如敏感词拦截、数据验证等)。)。最后,消息被推送到NettyServer,然后NettyServer通过mqtt推送到客户端。 其次,如果客户端和服务器想要正常通信,我们需要制定一个统一的协议。以聊天为例,如果我们想和对方聊天,我们需要通过uid和其他信息定位对方的通道(通道(Netty相当于一个套接字连接),然后我们可以将消息发送到正确的客户端。同时,客户端必须通过协议中的数据(uid、groupId等)在私人聊天或群组聊天会话中显示消息。)。 协议中的主要字段如下(我们将数据编码成protobuf格式进行传输): 010203040506070809101112131415161718192021 { " cmd ":" chat "," time":1554964794220," uid":"69212694 "," ClientInfo ":{ " DeviceId ":" B3 b 1519 c-89ec "," deviceInfo":"MI 6X" }," body":{ "v 补充说明:如果您不知道什么是Protobuf格式,请阅读《Protobuf通信协议详细说明:代码演示、详细原理介绍》等。 如上json,协议的主要字段包括: 即时消息开发干货共享:我如何解决大量离线消息导致客户陷入困境的问题_ 1.png。 如果客户端不在线,我们的服务器需要将发送的消息存储在离线消息表中。当对方客户机下次联机时,服务器通过一个长链接将脱机消息推送给客户机。 4.2业务难点 随着业务的蓬勃发展,用户数量越来越多,用户创建的群组数量越来越多,他们加入的群组和朋友数量也越来越多,聊天活动也越来越多。当一些用户不在线时,会生成大量离线消息(特别是对于群聊,有许多离线消息)。 当客户端下次上线时,服务器会将所有离线消息推送给客户端,导致客户端在登录后停留在主页上。此外,产品要求团队成员的数量应该增加(从以前的100人增加到1000人、10000人等)。)。 因此,一些客户端在登录后会因为大量的离线消息而被卡住,用户体验非常糟糕。 我和客户的同事分析了原因: 1)用户登录时,服务器通过循环批量发送所有离线消息,数据量大; 2)当客户端登录并进入主页时,要加载的数据不仅包括离线消息,还包括其他初始化数据; 3)不同价位的客户处理数据的能力有限。在处理聊天消息时,他们需要将消息存储在本地数据库中,刷新用户界面并向服务器回复确认消息,这将消耗大量性能。 (幸运的是,目前在线消息没有性能问题。)。 因此,鉴于上述问题,结合对即时消息系统的雄心勃勃的规划,我们的服务器决定优化离线消息(一点点,客户端的处理能力不够,为什么服务器要优化?服务器的性能远远不是瓶颈。。。). 5.升级改造之路 幸运的是,作者100%参与了系统优化的整个过程,包括技术选择、方案制定和最终代码编写。在此期间,作者想出了各种方案,然后与服务器端和客户端的同事进行了讨论,最后决定了一个稳定的方案。 5.1方案1(已过时的方案) [问题症状]: 客户端登录的主要原因是服务器会向客户端推送大量的离线消息,客户端收到离线消息后会回复服务器确认,然后将消息存储在本地数据库中并刷新用户界面。客户端反馈,即使客户端采用异步模式,也会有严重的性能问题。 [所以我想]: 为什么客户端在收到消息后没有将数据存储在数据库中就回复服务器确认?存储很可能会失败,这本身就是不合理的。这是其中之一。第二,由于服务器推得很紧,客户端被卡住是不合理的,它不关心客户端的处理能力。 [伪代码如下]: 01020304050607080910 int max = 100;//从新库中读取时(最大值> 0){ listoofflinemsglistnew = shardchatofflinemsgdao . getbytouid(uid,20 );if(CollectionUnitils . ISempty(OfflineMSglistNew)){ break;} handleOfflineMsg(uid,offlineMsgListNew,CheckOnLineWhenSendingOfflinemsg);马克斯。} [初步计划]: 既然推动是不合理的,我们可以改变方式。根据不同型号客户端的不同处理能力,服务器会以不同的速度发布。 我们可以将整个过程视为生产者-消费者模型,其中服务器是消息生产者,客户端是消息消费者。收到消息后,客户端将消息存储在本地数据库中,刷新用户界面,然后向服务器发送确认消息。从客户端收到确认消息后,服务器推送下一批消息。 这样,消息发布速度完全根据客户端的处理能力分批分配。但这种方式仍然属于推动方式。 [悲惨的结果]: 然而,理想是充实的,而现实是单薄的。 鉴于这一方案,客户提出了一些问题: 1)虽然客户端不会陷入这种方案,但是如果当前用户有很多离线消息,接收所有离线消息需要很长时间; 2)每次客户端收到消息时,都会刷新界面,客户端很可能会上下跳动。 所以,这个计划被否决了。。。 5.2方案二 [我的想法]: 由于推送的数据量太大,我们能否按需加载?当客户端需要读取离线消息时,服务器会将消息发送给客户端,当不需要时,服务器不会发送消息。 【技术方案】:对于离线消息,我们优化了以下方案 1)我们增加了离线消息计数器的概念:为每个用户的每个会话保存未读消息的元数据(包括未读消息的数量、最新的未读消息、时间戳等数据),该计数器用于客户端显示未读消息的红色气泡。该数据属于增量数据,仅保留离线期间接收的消息元数据。 消息格式如下: 010203040506070809101112 { " session id 1 ":{ " count ":20," lastmsg": ["last n messages"]," tim estamp": 1234567890}," sessionid2": { 即时消息开发干货共享:我如何解决大量离线消息导致客户陷入困境的问题_ 2.png。 2)每次客户端登录时,服务器不推送全部离线消息,只推送离线消息计数器(这部分数据存储在redis中,数据量很小),这个用户数显示在客户端消息列表中未读消息的小红点上。 3)客户端获取这些离线消息计数器数据,遍历会话列表,依次累积未读消息(注意:不是覆盖,服务器在离线后保存客户端的增量数据),然后通知服务器清空离线消息计数器的增量数据。 4)当客户机进入一个会话并启动和加载时,它通过消息的msgId和其他信息向服务器发送一个HTTP请求,然后服务器转到页面查询脱机消息并将其返回给客户机。 5)在接收到消息并将其保存在本地数据库中之后,客户端向服务器发送确认消息,然后服务器在离线消息表中删除离线消息。 [预期结果]: 客户端和服务器端的技术人员认可该方案。我们通过推和拉的方式解决了客户端无法加载离线消息的问题。(在转换之前,它被强制推,在转换之后,它采用推和拉的组合) 流程图如下: 即时消息开发干货共享:我如何解决大量离线消息导致客户陷入困境的问题_ 4.png。 [新问题]: 虽然该方案已经通过,但它带来了一个新的问题:客户端消息聚合的问题。 问题描述如下:登录后,客户端进入会话页面,因为客户端本身存储历史消息,所以当客户端下载并加载新消息时,如何判断是否加载本地历史消息?还是要请求服务器加载脱机邮件? 经过一番思考,服务器和客户端最终达成了一致的计划: 1)在未读消息计数器的小红点逻辑中,服务器将向客户端发送每个会话的最新n条消息; 2)当客户端进入会话时,将根据未读消息计数器的最新N条消息显示主页数据; 3)当客户端每次拉下并加载时,请求服务器,服务器根据时间反转离线消息,返回当前会话的最新离线消息,直到离线消息库中的所有数据都返回给客户端; 4)当离线消息库中没有离线消息时,向客户端返回一个标识符。根据该标识符,当会话页面被下拉并在下一次被加载时,客户端直接请求本地数据库,而不是服务器的离线消息。 6.消息确认逻辑的优化 最后,我们还优化了消息确认的逻辑。 优化前:服务器使用推送模型向客户端推送消息,无论是在线消息还是离线消息,ack的逻辑都是一样的,其中使用了kafka和redis等中间件,过程非常复杂(这里我不详细介绍ack的具体过程,这是不合理的)。 离线消息和在线消息的区别在于我们不存储在线消息,而离线消息将存储在单独的库中。绝对没有必要使用在线消息的ack逻辑来处理离线消息,但是这是不合理的,不仅在处理过程中存在问题,而且浪费了诸如kafka和redis等中间件的性能。 优化后:我们和客户端决定在每次下载和加载离线消息时,将最后一批离线消息的msgId或消息偏移量发送到服务器。服务器根据msgId直接删除发送到离线库中客户端的离线消息,然后将下一批离线消息返回给客户端。 此外,我们还增加了短信漫游功能,用户在切换手机登录后仍然可以找到历史短信,所以我就不详细介绍了。 7.设计优化方案时的文档截图(仅供参考)
什么是一篇文章中的引擎?它能实现即时消息的负载平衡吗
1.介绍 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_1.png Nginx(及其派生物)是一种广泛使用的服务器反向代理和负载平衡方案。从某种意义上说,Nginx几乎是低成本高负载网络服务器的同义词。 Nginx如此受欢迎,许多人理所当然地认为Nginx也可以用来解决即时消息或消息推送场景中的负载平衡问题。 此外,即时通讯的论坛和QQ群中的人们经常会问Nginx是否支持TCP、UDP和WebSocket的负载平衡。 有了以上问题,让我们开始学习这篇文章吧! 2.相关文章 TCP/IP的详细说明-第11章UDP:用户数据报协议 TCP/IP的详细说明-第17章TCP:传输控制协议 TCP/IP的详细说明-第18章TCP连接的建立和终止 TCP/IP的详细说明-第21章TCP的超时和重传 易于理解——对TCP协议的深刻理解(一):理论基础 网络编程中的懒人介绍(3):快速理解TCP协议就足够了 初学者入门:简明的网络套接字教程 网络插座的详细说明(一):对网络插座技术的初步理解 快速理解高性能HTTP服务器的负载均衡技术原理 “腾讯高级建筑师干货总结:阅读大型分布式系统设计的方方面面” 了解分布式架构下的负载平衡技术:分类、原理、算法、常见方案等。 初学者:从零开始理解大型分布式架构的演化历史、技术原理和最佳实践 易于理解:基于集群共享移动终端即时通信接入层的负载均衡方案 3.Nginx的产生 没听说过Nginx吗?那你一定听说过它的“对等”阿帕奇!像Apache一样,Nginx是一个WEB服务器。基于REST体系结构风格,以统一资源标识符(URI)或统一资源定位器(URL)为通信基础,通过HTTP协议提供各种网络服务。 然而,这些服务器在设计之初就受到环境的限制,如用户规模、网络带宽、产品特性等,它们的定位和发展也不尽相同。这也使得每个WEB服务器都有自己独特的特性。 Apache已经发展了很长时间,它是世界上无可争议的最大的服务器。它有很多优点:稳定性、开源、跨平台等。它已经存在了很长一段时间,互联网行业远不如现在。所以它被设计成重量级的。它不支持高度并发的服务器。在Apache上运行成千上万的并发访问将导致服务器消耗大量内存。操作系统在进程或线程之间切换它,这也消耗了大量的CPU资源,导致HTTP请求的平均响应速度降低。 所有这些决定了Apache不能成为高性能的WEB服务器,轻量级高并发服务器Nginx应运而生。 伊戈尔·塞索耶夫是俄国的一名工程师,他在为朗布勒媒体公司工作时用C语言开发了Nginx。作为一个网络服务器,Nginx一直在为兰姆勒媒体提供优秀而稳定的服务。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_522d762853d52.jpg ▲伊戈尔·塞索耶夫,英吉士的创始人 然后,伊戈尔·塞索耶夫打开了Nginx代码的源代码,并授予了一个自由软件许可证。 出于以下原因: 1)Nginx使用事件驱动的架构,这使得它能够支持数百万个TCP连接; 2)高模块化和自由软件许可使得第三方模块层出不穷(这是一个开源时代~); 3)Nginx是一个跨平台的服务器,可以在Linux、Windows、FreeBSD、Solaris、AIX、苹果操作系统等操作系统上运行; 4)这些优秀的设计带来了极大的稳定性。 因此,发动机着火了! 4.Nginx最常见的用途、用法和使用场景 4.1概述 简而言之,Nginx是: 1)免费、开源和高性能的HTTP服务器和反向代理服务器; 2)它也是一个IMAP、POP3和SMTP代理服务器; 3)可以作为发布网站的HTTP服务器; 4)它可以用作负载平衡的反向代理。 4.2什么是代理? 谈到代理,首先,我们应该定义一个概念,这意味着代理是一个代表和渠道。 此时,涉及两个角色,一个是代理角色,另一个是目标角色。代理角色通过该代理访问目标角色以完成某些任务的过程称为代理操作过程。就像我们生活中的专卖店一样,当顾客在阿迪达斯购买一双鞋时,这家专卖店是代理商,代理商角色是阿迪达斯制造商,目标角色是用户。 4.3什么是远期代理? 在讨论反向代理之前,让我们先看看正向代理,它也是最常见的代理模式。我们将从两个方面来解释前向代理的处理模式,并从软件和生活的角度来解释什么是前向代理。 在今天的网络环境下,如果我们因为技术需要而需要访问一些外国网站,此时你会发现我们无法通过浏览器访问外国网站。此时,每个人都可以使用FQ行动来访问它。FQ的主要方式是找到一个可以访问外国网站的代理服务器。我们将向代理服务器发送请求,代理服务器将访问外国网站,然后将访问的数据传递给我们! 上述代理模式称为前向代理: 1)转发代理的最大特点是客户端非常清楚要访问的服务器地址; 2)服务器只知道请求来自哪个代理服务器,而不知道哪个特定客户端; 3)真实的客户端信息在转发代理模式下被屏蔽或隐藏。 让我们来看一个示意图(我将客户端和转发代理放在一起,属于同一个环境,我将在后面介绍): 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_2.jpg 客户端必须设置一个转发代理服务器,但前提是他们知道转发代理服务器的IP地址和代理程序的端口。 如下图所示: 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_3.jpg 总而言之,前向代理“它代表客户端并代表客户端发送请求”,是位于客户端和源服务器之间的服务器。为了从源服务器获得内容,客户端向代理发送请求并指定目标(源服务器),然后代理将请求转发给源服务器并将获得的内容返回给客户端。客户端必须进行一些特殊设置,才能使用转发代理。 转发代理的用途: 1)访问以前无法访问的资源,如谷歌;; 2)它可以被缓存以加速对资源的访问; 3)授权客户端在互联网上访问和认证; 4)代理可以记录用户访问记录(在线行为管理)和隐藏用户信息。 4.4什么是反向代理? 理解什么是正向代理,让我们继续看看如何处理反向代理。 例如,对于中国的一个财富网站来说,每天同一时间连接到该网站的访问者数量已经达到了前所未有的水平,单个服务器远远不能满足人们日益增长的购买欲望。这时,一个熟悉的术语出现了:分布式部署。 分布式部署是通过部署多个服务器来解决访问者数量有限的问题。财富网站的大部分功能也是通过使用Nginx的反向代理直接实现的。在包装了引擎和其他组件之后,它有了一个很高的名字:引擎。对童鞋感兴趣的人可以访问青少年网的官方网站,查看具体信息:http://tengine.taobao.org/. 那么,反向代理以什么方式实现分布式集群操作呢?让我们先看一个示意图(我将服务器和反向代理连接在一起,属于同一个环境,我将在后面介绍),如下图所示。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_4.jpg 从上图可以清楚地看到,多个客户端向服务器发送的请求被Nginx服务器接收,并被分发到后端业务处理服务器,以便按照一定的规则进行处理。在这一点上,请求的来源,也就是客户端,是清楚的,但是不清楚哪个服务器将处理该请求,并且Nginx扮演一个反向代理角色。 客户端没有感知代理,反向代理对外部世界是透明的,因此访问者不知道他们正在访问代理。因为客户端无需任何配置即可访问。 反向代理“代表服务器并代表服务器接收请求”,主要用于服务器集群的分布式部署,它隐藏了服务器的信息。 反向代理的角色: 1)为了保证内部网的安全,通常使用反向代理作为公共网络访问地址,网络服务器是内部网; 2)负载均衡,通过反向代理服务器优化网站负载。 典型项目场景: 通常,在实际项目操作中,前向代理和反向代理可能存在于一个应用场景中,前向代理客户端请求访问目标服务器(反向单利益服务器)和多个实际业务处理服务器的反向代理。 具体拓扑图如下: 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_5.jpg 4.5正向代理和反向代理之间的区别 剪切一个图表来说明正向代理和反向代理之间的区别,如下所示。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_6.jpg 该图如下: 1)在转发代理中,代理和客户端属于同一个局域网(在图中的方框中),隐藏客户端信息; 2)在反向代理中,代理和服务器属于同一个局域网(在图中的方框中),这隐藏了服务器信息。 事实上,代理在两种代理中的作用是发送和接收对服务器的请求和响应,但就结构而言,它恰好是从左到右互换的,因此后一种代理模式称为反向代理。 4.6引擎的负载平衡技术 我们已经定义了所谓的代理服务器的概念。然后,Nginx扮演反向代理服务器的角色。它根据什么规则分发请求?可以针对不同的项目应用场景控制分发规则吗? 这里提到的由客户端发送并由Nginx反向代理服务器接收的请求数就是我们所说的负载。 请求数量根据一定的规则分配到不同的服务器进行处理的规则是一种平衡规则。 因此,根据规则分发服务器接收的请求的过程称为负载平衡。 负载均衡在实际项目运行过程中,有硬件负载均衡和软件负载均衡两种。硬件负载平衡也称为硬负载。例如,F5负载平衡相对昂贵,但是数据的稳定性和安全性得到了很好的保证。中国移动和中国联通等公司将选择硬负载运营;考虑到成本,更多的公司会选择使用软件负载平衡,这是一种利用现有技术和主机硬件实现的消息队列分发机制。 即时通讯初学者:什么是一篇文章中的引擎?它能实现即时消息的负载平衡吗?_7.jpg Nginx支持的负载平衡调度算法如下: 1)权重轮询(默认,通用):根据权重将接收到的请求分配给不同的后端服务器。即使后端服务器在使用过程中出现故障,Nginx也会自动将服务器从队列中移除,请求接受也不会受到任何影响。这样,可以为不同的后端服务器设置一个权重,以调整不同服务器上请求的分配率。权重数据越大,分配给请求的概率就越大;权重值主要根据实际工作环境中不同的后端服务器硬件配置进行调整。 2)ip_hash(公共):根据发起客户端的ip的散列结果来匹配每个请求。在该算法下,具有固定ip地址的客户端将始终访问同一个后端服务器,这在一定程度上解决了集群部署环境中的会话共享问题。 3)公平:智能调整调度算法,并根据后端服务器从请求处理到响应的时间动态分配调度算法。响应时间短、处理效率高的服务器分配请求的概率较高,而响应时间长、处理效率低的服务器分配的请求较少;一种结合前两者优点的调度算法。然而,应该注意的是,默认情况下,Nginx不支持公平算法。如果您想使用这个调度算法,请安装上游公平模块。 4)url_hash:根据访问的url的哈希结果分配请求,每个请求的url将指向后端的一个固定服务器,这可以提高Nginx作为静态服务器时的缓存效率。另外,应该注意的是,默认情况下,Nginx不支持这种调度算法。如果你想使用它,你需要安装Nginx的散列软件包。 5.Nginx支持TCP、UDP和网络套接字的负载平衡 5.1概述 准确地说,对于熟悉Nginx的用户来说,以上章节介绍的内容都是针对Nginx最擅长的Http协议,这也是Nginx最成功的应用场景。随着Nginx的不断升级和演进,开发人员期待Nginx支持仅在HTML5中出现的较低的TCP、UDP和WebSocket协议。幸运的是,这一切都实现了! Nginx支持1.3版的WebSocket反向代理(负载平衡)、1.9.0版的TCP反向代理(负载平衡)和1.9.13版的UDP反向代理(负载平衡)。 原则上,Nginx与UDP或TCP反向代理(负载平衡)是一致的,而WebSocket协议实际上是TCP协议的应用层协议。因此,我们将在这一节介绍Nginx对TCP反向代理(负载平衡)的支持。 对于通常被称为“七层负载平衡”的经典HTTP协议,Nginx实际上在第七层“应用层”工作。对于较低的TCP协议,负载平衡就是我们通常所说的“四层负载平衡”,它在“网络层”和“传输层”工作。例如,LVS(Linux虚拟服务器)和F5(硬件负载平衡设备)也属于“四层负载平衡”。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_2-1.jpg 5.2 the负载均衡的实现原理 当Nginx从侦听端口接收到新的客户端链路时,它会立即执行路由算法,获取要连接的指定服务IP,然后创建新的上游连接来连接到指定的服务器。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_2-2.jpg TCP负载平衡支持Nginx的原始调度算法,包括循环调度(默认,轮询调度),哈希(一致选择)等等。同时,调度信息数据将与鲁棒性检测模块协作,为每个连接选择适当的目标上游服务器。如果使用哈希负载平衡调度方法,您可以使用$remote_addr(客户端IP)来实现简单的持久会话(同一客户端IP的连接总是落在同一服务服务器上)。 与其他上游模块一样,TCP的流模块也支持具有统一负载的自定义转发权重(配置“权重=2”),以及备份和关闭参数,这些参数用于淘汰出现故障的上游服务器。Max_conns参数可以限制服务器的TCP连接数,并根据服务器的容量设置适当的配置值,特别是在高并发情况下,可以达到过载保护的目的。 Nginx监控客户端连接和上游连接。一旦收到数据,Nginx将立即读取并将其推送到上游连接,并且不会在TCP连接中进行数据检测。Nginx为客户端和上游数据写入维护一个内存缓冲区。如果客户端或服务器传输大量数据,缓冲区将适当增加内存大小。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_2-3.jpg 当Nginx从任何一方接收到连接关闭的通知时,或者当TCP连接空闲的时间超过proxy_timeout配置的时间时,连接将被关闭。对于TCP长连接,应选择合适的代理退出时间,并注意监控socke的so_keepalive参数,防止过早断开。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_2 -。png 5.3服务健壮性监控 TCP负载平衡模块支持内置的健壮性检测。如果上游服务器拒绝TCP连接的时间超过proxy_connect_timeout配置的时间,它将被视为无效。在这种情况下,Nginx会立即尝试连接到上游组中的另一台普通服务器。连接失败信息将记录在Nginx的错误日志中。 即时通讯初学者:什么是引擎?它能实现即时消息的负载平衡吗?_2-4.jpg 如果服务器重复出现故障(超过max _ fails或fail_timeout配置的参数),Nginx也会启动该服务器。服务器启动60秒后,Nginx会偶尔尝试重新连接它,以检查它是否恢复正常。如果服务器恢复正常,Nginx将其添加回上游组,并缓慢增加连接请求的比例。 这个位置是“慢慢增加的”,因为通常一个服务有“热数据”,也就是说,超过80%甚至更多的请求实际上被阻塞在“热数据缓存”中,并且只有少数请求被实际处理。当机器刚刚启动时,“热数据缓存”实际上还没有建立。此时,大量请求被爆炸性地转发,这可能导致机器无法“承受”并再次挂起。以mysql为例,我们超过95%的mysql查询通常会进入内存缓存,实际执行的查询并不多。 事实上,无论是单机还是集群,在高并发请求、重启或切换的情况下都存在这种风险。 有两种主要解决方案: 1)请求逐渐增加,从少到多,逐渐积累热数据,最终达到正常服务状态; 2)预先准备好“常用”数据,主动“预热”服务,预热完成后打开服务器的访问。 原则上,TCP负载平衡与LVS是一致的,它的性能将远远高于原来的HTTP负载平衡。然而,它并不比LVS好。LVS被放置在内核模块中,而Nginx在用户模式下工作。此外,Nginx相对较重。 6.Nginx能实现即时消息负载平衡吗? 6.1概述 根据上一节的内容,Nginx可以实现TCP、UDP和WebSocket协议的反向编码(负载均衡)。在这种情况下,基于TCP、UDP或WebSocket协议的即时消息聊天系统能否通过Nginx直接实现即时消息负载均衡? 为了回答这个问题,我们首先来看不同长连接场景中的具体数据趋势。 为了便于描述,以下基于TCP、UDP或网络套接字协议的套接字长连接简称为套接字长连接。 6.2Nginx支持的长连接反向代理数据趋势功能 对于有利于Nginx实现的套接字长连接,数据趋势如下图所示: 即时通讯初学者:什么是一篇文章中的引擎?它能实现即时消息的负载平衡吗?_QQ截图20190606234945.jpg 如上所示,即: 1)客户端使用Nginx反向代理到套接字长连接服务器; 2)客户端可以与套接字长连接服务器通信(即客户端->套接字长连接服务器和套接字长连接服务器->客户端)。 简而言之,Nginx可以实现如下的长连接数据定向能力: 1)客户端到服务器方向(简称c2s):即客户端向长连接服务器发送数据的能力; 2)服务器到客户端方向(简称s2c):长连接服务器向客户端发送数据的能力。 6.3即时消息聊天软件所需的长连接数据趋势功能 对于即时消息聊天应用程序,必要的数据导向功能包括: 1)客户端到服务器方向(简称c2s):即客户端向长连接服务器发送数据的能力; 2)服务器到客户端方向(简称s2c):长连接服务器向客户端发送数据的能力; 3)客户端到客户端方向(简称c2c):客户端向客户端发送数据的能力。 即时消息聊天应用中的三种数据趋势,对应典型的功能逻辑场景: 1)客户端到服务器方向(简称c2s):通常用于客户端向即时消息长连接服务器发起指令,如发起好友请求、发送陌生人聊天消息、发送群聊消息等。 2)服务器到客户端方向(简称s2c):通常用于服务器主动向客户端推送指令,如向客户端传递好友请求、转发陌生人的聊天消息、发送群组聊天消息(发送给所有群组成员)、系统通知等。; 3)客户端到客户端方向(简称c2c):客户端向客户端发送数据的能力。例如,普通的朋友聊天消息从客户端A发送到客户端B(当然,这不一定通过P2P技术实现)。 6.4结论 显然,如前所述,由Nginx实现的TCP、UDP或WebSocket协议的反向代理(负载平衡)只能实现c2s和s2c数据方向,而即时消息聊天应用程序必须需要c2s、s2c和c2c消息方向。数据趋势C2c显然是一个特定于即时消息的场景需求,对于通用解决方案Nginx来说,提供它有点牵强。 我们可以得出结论,我们不能直接通过Nginx实现即时消息负载平衡。 换句话说,如果即时消息的负载平衡可以直接通过Nginx实现,那么在处理高并发性和高吞吐量时,即时消息服务器可以像Http协议一样舒适。 6.5例外 然而,对于即时通讯网络消息推送系统(或类似系统)关注的另一个技术领域,消息推送的负载均衡可以直接通过Nginx实现,因为仅仅消息推送系统只需要c2s和s2c数据方向,而不需要c2c水平数据交互。
即时通讯私有化部署是什么?
即时通讯私有化部署,我们把使用权成为私有化部署系统,全称叫做私人使用服务器部署,就是个人所得私人使用定义,在行业里面做的比较好的有视酷科技旗下的视酷沟通APP,性能稳定,服务好,价格合理 这里我们举例深圳市视酷科技有限公司的服务技术,将运营的APP后台程序换IP程序部署系统,然后在发给你APP使用。 私有化是指公有组织或公有财产的所有权人直接或由其代理人越权将公有组织或公有财产以及这些组织或财产的所有权及其派生权利合法或非法地由公有组织或公有财产的全体公民或某一集体所有转变为个别私人所有的行为及其过程。 中文名 即时通讯私有化部署 外文名 siyouhuabushu 举例产品 A视酷科技视酷沟通APP即时通讯 APP 视酷沟通 在计算领域,把技术变成私人的财富,就变相的理解为私人化使用权, 一个软件开发出现,是源码编写出来的,那么一个程序就分为源码与程序, 程序等于使用权, 源码等于所有权, 即可以分为使用权,所有权,我们把使用权成为私有化部署系统,全称叫做私人使用服务器部署, 1,所有权,手里有源码就是所有权,属于个人财富,随意更改变动, 2,使用权,就是源码不在你的手里,别人把源码设定好访问域名加密打包,再放到你的服务器里面,给你使用,但是自己没有修改的办法,因为程序打包加密了,就不能打开了, A视酷科技视酷沟通APP即时通讯演示图[1] 举例,部署定义[1] 视酷科技的视酷沟通APP里面,登录账号界面有个设置服务器功能,这个功能可以设定你自己的服务器地址IP,前提是你们后台服务器的接口与APP接口是一样的,这样APP里面的功能才能被服务里面的程序调用, 总体程序分类 前端: 安卓,iOS,电脑端pc,web网站端,h5网页端,mac苹果电脑端,APP只是前端展示给我们看的一个程序,没有后台他就是一个展示的模型,就像一个钢铁侠的外壳一套衣服。 后端:服务器是后端写出来的功能,来一个个调用前端功能的实现,整个程序运用起来, 简单说就是钢铁侠的内部心脏,他决定着你能使用什么功能, 后台与前台链接的方法就是人体的血管,通过服务器域名链接通信,部署就是把域名换一下就变成了,一套新的程序了, 功能说明, 需要保持前后端功能同步,这样数亿代码才能有效使用功能正常, 即时通讯系统私有化部署包含领域 军方,企业,商业,任何用到沟通的领域,一些不希望泄露企业内部质料的企业,为了防止商业对手的手段,都选择自己做私有化部署办公,安全,高效,[1]
即时通讯IM技术领域基础篇_VOIP_SIP_OpenFire_doubango
即时通讯IM技术领域提高篇 准备工作(协议选型) xxx项目架构 IM 关键技术点 & 策略机制 典型IM业务场景 存储结构简析 udp协议虽然实时性更好,但是如何处理安全可靠的传输并且处理不同客户端之间的消息交互是个难题,实现起来过于复杂. 目前大部分IM架构都不采用UDP来实现. 但是为啥还需要HTTP呢? 核心的TCP长连接,用来实时收发消息,其他资源请求不占用此连接,保证实时性 http可以用来实现状态协议(可以用php开发) IM进行图片/语言/大涂鸦聊天的时候: http能够很方便的处理 断点续传和分片上传等功能. TCP: 维护长连接,保证消息的实时性, 对应数据传输协议. IM协议选择原则一般是:易于拓展,方便覆盖各种业务逻辑,同时又比较节约流量。节约流量这一点的需求在移动端IM上尤其重要 !!! xmpp: 协议开源,可拓展性强,在各个端(包括服务器)有各种语言的实现,开发者接入方便。但是缺点也是不少:XML表现力弱,有太多冗余信息,流量大,实际使用时有大量天坑。 MQTT: 协议简单,流量少,但是它并不是一个专门为IM设计的协议,多使用于推送. 需要自己在业务上实现群,好友相关等等(目前公司有用MQTT实现通用IM框架). SIP: 多用于VOIP相关的模块,是一种文本协议. sip信令控制比较复杂 私有协议: 自己实现协议.大部分主流IM APP都是是使用私有协议,一个被良好设计的私有协议一般有如下优点:高效,节约流量(一般使用二进制协议),安全性高,难以破解。 xxx项目基本属于私有定制协议, 后期通用IM架构使用MQTT 协议设计的考量: 网络数据大小 —— 占用带宽,传输效率:虽然对单个用户来说,数据量传输很小,但是对于服务器端要承受众多的高并发数据传输,必须要考虑到数据占用带宽,尽量不要有冗余数据,这样才能够少占用带宽,少占用资源,少网络IO,提高传输效率; 网络数据安全性 —— 敏感数据的网络安全:对于相关业务的部分数据传输都是敏感数据,所以必须考虑对部分传输数据进行加密(xxx项目目前提供C++的加密库给客户端使用) 编码复杂度 —— 序列化和反序列化复杂度,效率,数据结构的可扩展性 协议通用性 —— 大众规范:数据类型必须是跨平台,数据格式是通用的 提供序列化和反序列化库的开源协议: pb,Thrift. 扩展相当方便,序列化和反序列化方便(xxx项目目前使用pb) 文本化协议: xml,json. 序列化,反序列化容易,但是占用体积大(一般http接口采用json格式). ... ... 同时支持TCP 和 HTTP 方式, 关联性不大的业务服务独立开来 服务支持平行扩展,平行扩展方便且对用户无感知 cache db层的封装,业务调用方直接调用接口即可. 除了Access server是有状态的,其他服务无状态 各个服务之间,通过rpc通信,可以跨机器. oracle里面都是模块化,有点类似MVC模式, 代码解耦, 功能解耦. 缺点 改进 push server 没有业务,仅仅是转发Access和oracle之间的请求 缺点 改进 Access server和用户紧密连接,维持长连接的同时,还有部分业务 缺点 改进: 为什么有可能会乱序? 对于在线消息, 一发一收,正常情况当然不会有问题 对于离线消息, 可能有很多条. 怎么保证不乱序? 每条消息到服务端后,都会生成一个全局唯一的msgid, 这个msgid一定都是递增增长的(msgid的生成会有同步机制保证并发时的唯一性) 针对每条消息,会有消息的生成时间,精确到毫秒 拉取多条消息的时候,取出数据后,再根据msgid的大小进行排序即可. 消息为什么可能会重复呢? 这种情况下,就可能会需要有重发机制. 客户端和服务端都可能需要有这种机制. 既然有重复机制,就有可能收到的消息是重复的. 怎么解决呢? 保证不重复最好是客户端和服务端相关处理 消息meta结构里面增加一个字段isResend. 客户端重复发送的时候置位此字段,标识这个是重复的,服务端用来后续判断 服务端为每个用户缓存一批最近的msgids(所谓的localMsgId),如缓存50条 服务端收到消息后, 通过判断isResend和此msgid是否在localMsgId list中. 如果重复发送,则服务端不做后续处理. 因为仅仅靠isResend不能够准备判断,因为可能客户端确实resend,但是服务端确实就是没有收到...... 最简单的就是服务端每传递一条消息到接收方都需要一个ack来确保可达 服务端返回给客户端的数据,有可能客户端没有收到,或者客户端收到了没有回应. 考虑一个账号在不同终端登录后的情况. 这里提供两种方案供参考(本质思想一样,实现方式不同) 每个用户的每条消息都一定会分配一个唯一的msgid 服务端会存储每个用户的msgid 列表 客户端存储已经收到的最大msgid 优点: 根据服务器和手机端之间sequence的差异,可以很轻松的实现增量下发手机端未收取下去的消息 对于在弱网络环境差的情况,丢包情况发生概率是比较高的,此时经常会出现服务器的回包不能到达手机端的现象。由于手机端只会在确切的收取到消息后才会更新本地的sequence,所以即使服务器的回包丢了,手机端等待超时后重新拿旧的sequence上服务器收取消息,同样是可以正确的收取未下发的消息。 由于手机端存储的sequence是确认收到消息的最大sequence,所以对于手机端每次到服务器来收取消息也可以认为是对上一次收取消息的确认。一个帐号在多个手机端轮流登录的情况下,只要服务器存储手机端已确认的sequence,那就可以简单的实现已确认下发的消息不会重复下发,不同手机端之间轮流登录不会收到其他手机端已经收取到的消息。 假如手机A拿Seq_cli = 100 上服务器收取消息,此时服务器的Seq_svr = 150,那手机A可以将sequence为[101 - 150]的消息收取下去,同时手机A会将本地的Seq_cli 置为150 手机A在下一次再次上来服务器收取消息,此时Seq_cli = 150,服务器的 Seq_svr = 200,那手机A可以将sequence为[151 - 200]的消息收取下去. 假如原手机A用户换到手机B登录,并使用Seq_cli = 120上服务器收取消息,由于服务器已经确认sequence 每个用户的每条消息都一定会分配一个唯一的msgid 服务端会存储每个用户的msgid 列表 客户端存储已经收到的最大msgid 这两种方式的优缺点? 方式二中,确认机制都是多一次http请求. 但是能够保证及时淘汰数据 方式一中,确认机制是等到下一次拉取数据的时候进行确定, 不额外增加请求, 但是淘汰数据不及时. 心跳功能: 维护TCP长连接,保证长连接稳定性, 对于移动网络, 仅仅只有这个功能吗? 运营商通过NAT(network adddress translation)来转换移动内网ip和外网ip,从而最终实现连上Internet,其中GGSN(gateway GPRS support Node)模块就是来实现NAT的过程,但是大部分运营商为了减少网关NAT的映射表的负荷,若一个链路有一段时间没有通信就会删除其对应表,造成链路中断,因此运营商采取的是刻意缩短空闲连接的释放超时,来节省信道资源,但是这种刻意释放的行为就可能会导致我们的连接被动断开(xxx项目之前心跳有被运营商断开连接的情况,后面改进了心跳策略,后续还将继续改进心跳策略) NAT方案说白了就是将过去每个宽带用户独立分配公网IP的方式改为分配内网IP给每个用户,运营商再对接入的用户统一部署NAT设备,NAT的作用就是将用户网络连接发起的内网IP,以端口连接的形式翻译成公网IP,再对外网资源进行连接。 从mobile 到GGSN都是一个内网,然后在GGSN上做地址转换NAT/PAT,转换成GGSN公网地址池的地址,所以你的手机在Internet 上呈现的地址就是这个地址池的公网地址 心跳保证客户端和服务端的连接保活功能,服务端以此来判断客户端是否还在线 心跳还需要维持移动网络的GGSN 最常见的就是每隔固定时间(如4分半)发送心跳,但是这样不够智能. 4分半的原因就是综合了各家移动运营商的NAT超时时间 心跳时间太短,消耗流量/电量,增加服务器压力. 心跳时间太长,可能会被因为运营商的策略淘汰NAT表中的对应项而被动断开连接 智能心跳策略 为了保证收消息及时性的体验,当app处于前台活跃状态时,使用固定心跳。 app进入后台(或者前台关屏)时,先用几次最小心跳维持长链接。然后进入后台自适应心跳计算。这样做的目的是尽量选择用户不活跃的时间段,来减少心跳计算可能产生的消息不及时收取影响。 维护移动网GGSN(网关GPRS支持节点) 参考微信的一套自适应心跳算法: 精简心跳包,保证一个心跳包大小在10字节之内, 根据APP前后台状态调整心跳包间隔 (主要是安卓) 掉线后,根据不同的状态需要选择不同的重连间隔。如果是本地网络出错,并不需要定时去重连,这时只需要监听网络状态,等到网络恢复后重连即可。如果网络变化非常频繁,特别是 App 处在后台运行时,对于重连也可以加上一定的频率控制,在保证一定消息实时性的同时,避免造成过多的电量消耗。 断线重连的最短间隔时间按单位秒(s)以4、8、16...(最大不超过30)数列执行,以避免频繁的断线重连,从而减轻服务器负担。当服务端收到正确的包时,此策略重置 有网络但连接失败的情况下,按单位秒(s)以间隔时间为2、2、4、4、8、8、16、16...(最大不超过120)的数列不断重试 未读消息索引存在的意义在于保证消息的可靠性以及作为离线用户获取未读消息列表的一个索引结构。 未读消息索引由两部分构成,都存在redis中: 假设A有三个好友B,C,D。A离线。B给A发了1条消息,C给A发了2条消息,D给A发了3条消息,那么此时A的未读索引结构为: hash结构 zset结构 消息上行以及队列更新未读消息索引是指,hash结构对应的field加1,然后将消息id追加到相应好友的zset结构中。 接收ack维护未读消息索引则相反,hash结构对应的field减1,然后将消息id从相应好友中的zset结构中删除。 该流程用户在离线状态的未读消息获取。 该流程主要由sessions/recent接口提供服务。流程如下: 和在线的流程相同,离线客户端读取了未读消息后也要发送接收ack到业务端,告诉它未读消息已经下发成功,业务端负责维护该用户的未读消息索引。 和在线流程不同的是,这个接收ack是通过调用messages/lastAccessedId接口来实现的。客户端需要传一个hash结构到服务端,key为通过sessions/recent接口下发的好友id,value为sessions/recent接口的未读消息列表中对应好友的最大一条消息id。 服务端收到这个hash结构后,遍历它 这样就完成了离线流程中未读消息索引的维护。 如果消息标记为offline,则将消息入库,写缓存(只有离线消息才写缓存),更新未读消息索引,然后调用apns进行推送。 如果消息标记为online,则直接将消息入库即可,因为B已经收到这条消息。 如果消息标记为redeliver,则将消息写入缓存,然后调用apns进行推送。 拆分出来的目的: 真的能够起到这样的效果么? 拆分出来的connd server 还是有可能会需要重启的, 这时候怎么办呢 ?关键性问题还是没有解决 加一层服务,是打算通过共享内存的方式,connd 只管理连接。access 更新升级的时候,用户不会掉线。 目前Access服务不重, 拆分出来真有必要吗? 真要拆分, 那也不是这么拆分, 是在Oracle上做拆分, 类似微服务的那种概念 稳定性不是这么体现,原来 connd 的设计,更薄不承担业务,而现在的 access 还是有一些业务逻辑,那么它升级的可能性就比较高。 access 拆分,目的就是让保持连接的那一层足够薄,薄到怎么改业务,它都不用升级代码(tcp 不会断)。 连接层更稳定 - - - 需要有硬性指标来判断才能确定更稳定,因为Access的服务不重,目前也不是瓶颈点. 减少重启,方便Access服务升级 - - - 不能通过增加一层服务来实现重启升级,需要有其他机制来确保服务端进行升级而不影响TCP长连接上的用户 增加一个服务,就多了一条链路, 就可能会导致服务链路过长,请求经过更多的服务,会导致服务更加不可用. 因为要保证每个服务的可用性都到99.999%(5个9)是很难的,增加一个服务,就会降低整个服务的可用性. 架构改进一定要有数据支撑, 要确实起到效果, 要有数据输出才能证明这个改进是有效果的,要不然花了二个月时间做改进,结果没有用,浪费人力和时间,还降低开发效率 方案: 增加一条信令交互,服务端如果要重启/缩容, 告知连接在此Access上的所有客户端,服务端要升级了,客户端需要重连其他节点 等确定当前Access节点上的所有客户端都连接到其他节点后, 当前Access节点再进行重启/下线/缩容. 怎么扩容? 如果需要扩容,则增加新的节点后,通过etcd进行服务发现注册.客户端通过router server请求数据后,拉取到相关节点. 如果当前3个节点扛不住了,增加2个节点, 这个时候,要能够马上缓解当前3个节点压力,需要怎么做? 服务端发送命令给当前节点上的客户端,让客户端连接到新增节点上. 服务端还需要确定是否有部分连接到其他节点了,然后再有相应的策略. 按照之前的方式,客户端重新登录请求router server,然后再进行连接的话,这是不能够马上缓解压力的,因为新增的节点后, 当前压力还是在之前几个节点 所以, 服务端需要有更好的机制,来由服务端控制 线上机器都有防火墙策略(包括硬件防火墙/软件防火墙) 硬件防火墙: 硬件防火墙设备,很贵,目前有采购,但是用的少 软件防火墙: 软件层面上的如iptable, 设置iptable的防火墙策略 TCP 通道层面上 要能够发送消息, 必须要先登录 要登录, 必须有token,有秘钥 收发消息也可以设置频率控制 socket建连速度的频率控制, 不能让别人一直建立socket连接,要不然socket很容易就爆满了,撑不住了 收发消息频率控制, 不能让别人一直能够发送消息,要不然整个服务就挂掉了 为啥xmpp不适合,仅仅是因为xml数据量大吗 ? 目前也有方案是针对xmpp进行优化处理的. 因此流量大并不是主要缺点 还有一点就是消息不可靠,它的请求及应答机制也是主要为稳定长连网络环境所设计,对于带宽偏窄及长连不稳定的移动网络并不是特别优化 因此设计成支持多终端状态的XMPP在移动领域并不是擅长之地 为啥mqtt不适合? 为啥xxx项目没有用mqtt ? mqtt 适合推送,不适合IM, 需要业务层面上额外多做处理, 目前已经开始再用 xxx项目不用mqtt是历史遗留问题,因为刚开始要迅速开展,迅速搭建架构实现,因此用来蘑菇街的teamtalk. 如果后续选型的话, 如果没有历史遗留问题,那么就会选择使用mqtt 除了数据量大, 还要考虑协议的复杂度, 客户端和服务端处理协议的复杂度? 协议要考虑容易扩展, 方便后续新增字段, 支持多平台 要考虑客户端和服务端的实现是否简单 编解码的效率 服务需要能够跨机房,尤其是有状态的节点. 需要储备多机房容灾,防止整个机房挂掉. 维持TCP长连接,包括心跳/超时检测 收包解包 防攻击机制 等待接收消息回应(这个之前没有说到,就是把消息发送给接收方后还需要接收方回应) 如何保证消息不丢,不重? 怎么设计消息防丢失机制? 对于长连接, 怎管理这些长连接?
在安卓即时通讯应用中实现@人功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]
1.介绍 最近,有一个需求:评论@ people(是的,它是即时聊天或微博应用中的@ people功能),如下图所示: @安卓即时通讯应用中的人物功能实现:模仿微博、QQ、微信,零入侵,高可伸缩[图形+源代码] _ wechatimg43.jpg @安卓即时通讯应用中的人物功能实现:模仿微博、QQ、微信,零入侵,高可伸缩[图形+源代码]_微信 ▲ @微信群聊界面中的人物功能▲@ QQ群聊界面中的人物功能 互联网上的一些文章共享了类似功能的逻辑,但是几乎所有的文章都扩展了EditText类,所以这种实现方式肯定不能进入我的首发阵容。你认为这是因为它不符合面向对象的六个原则吗?错了,只是因为它不够优雅!不够优雅!不够优雅! 那么,如果只有饮水机代码呢?当然是: 阅读fuking源代码 努力工作有回报。我读了一遍编辑文本的源代码,然后创造了这个“优雅”的轮子(开玩笑的,编辑文本的源代码怎么能被称为富金源代码,他有一个父亲叫TextView)。别废话了,上泡菜。 在此之前,你需要记住一个与文本相关的想法:一切都是跨越! 2.添加标签文本样式,并将其绑定到标签的业务数据 每个人都知道文本风格与可展性有关。 这里也使用可展性。我已经定义了一个DataBindingSpan接口,它有两个主要功能: 1)让用户提供一个CharSequence对象作为标签,它决定了标签文本的样式和内容; 2)提供一种方法来返回由DataBindingSpan对象绑定的业务数据。 1234 interface DataBindingSpan { fun SpannedText():CharSequence fun BindingData():T } 示例代码: 010203040506070809101112 class SpanNableData(私有值跨度:字符串):DataBindingSpan { override fun SpanNedText():CharSequence { return SpanNableString(跨度)。应用{设置跨度(前背景颜色跨度(颜色。红色),0,长度,跨度。SPAN _ EXCLIVE _ EXCLIVE)} }覆盖有趣的绑定数据():字符串{返回范围} } 这个类只包装一个字符串。spannedText()返回一个字符串,该字符串将标签文本的颜色更改为红色,而bindingData()将该字符串作为业务数据返回。 你也可以把它变成别的东西。用户对象很好。SpannedText()返回用户名,bindingData()返回用户标识,这样就可以很容易地实现@ person函数的业务数据绑定的相关逻辑。 3.确保绑定在文本上的数据的安全性和可靠性 当我们将Span绑定到文本时,我们需要确保文本和数据在文本更改时的安全性、可靠性和一致性。 事实上,自从数据库发布以来,我们一直在处理这件事。正如SpannableData所示,当spannedText()返回一个Spannable对象时,spanned。span _ exclusive _ exclusive用作标志。它不能在头部和尾部扩展跨度的范围,只能在中间插入。同时,当Span所涵盖的文本被删除时,Span也将被删除。也就是说,它天生具有一定的数据安全性和可靠性。这将为我们节省很多东西。 当然,跨越。span _ exclusive _ exclusive没有完全的安全性。毕竟,它不能阻止中间插入。我们必须自己做这件事。那么,我们应该怎么做来禁止中间插入呢? 这种需求产生了两个问题: 1)当普通文本发生变化时,如何监控跨度起始位置的变化? 2)如何禁止在跨度内插入光标? 对于第一个问题,我在网上看到了一个想法。维护跨度起始位置管理器跨度管理器,然后使用文本查看器监控文本的变化。文本的任何更改都会导致spanrangmanager重新计算span的位置。 当然,如果我使用这种方法,我就不会有这个博客。事实上,安卓系统有一个很好的跨度管理器,那就是跨度生成器。与此同时,SDK提供了一个监听程序SpanWatcher来监听SpannableStringBuilder中Span的变化。感兴趣的学生可以看看他的源代码。 其次,我们需要确保文本和数据之间的一致性,并禁止将光标插入到跨度覆盖文本的中间。 有三种方法: 1)普通文本,当标签文本被破坏(删除、插入、追加)时,绑定数据失效,这是微信的做法; 2)普通文本将标签文本作为一个整体,不能在标签中插入光标,防止数据被破坏。这是微博的做法; 3)占位符被不可分割的范围(如图像范围)取代,这是QQ的惯例。 微博和微信都必须监控和处理软键盘删除键、文本变化、光标活动、文本选择状态和跨度变化。QQ要简单得多,这将在后面讨论。 4.微博的实践 4.1倾听并处理光标活动、选定状态和量程位置变化 对于游标活动和所选状态监听,如果标签文本功能是通过继承编辑文本来实现的,那么方法OnSelectonChanged(int Selstart,int Selend)可以监听游标活动。然而,这种方法怎么能被认为是优雅的呢? 我应该怎样做才能“优雅地”实现它?还是那句话: 阅读fuking源代码 两个角色: 选择 SpanWatcher 如果有一篇文章叫做“选择如何管理文本光标活动和选择状态?”,那么它一定能够回答这个问题。 这里将不详细描述选择的内部实现,您只需要知道两件事: 1)所选状态有一个起点(起点)和一个终点(终点),起点和终点反映在文本中,实际上是两个非点点:起点和终点; 2)光标处于特殊选中状态,起点和终点在同一位置。 因为所选状态的实现是跨度,所以它与视图无关,而是与跨度有关。也就是说,我们可以在不使用自己的API的情况下管理EditText的游标活动和选择状态(请注意这些词,他是“优雅实现”的基石)。 选择管理光标活动。那么,什么是SpanWatcher?如前所述,它是一个监听器,用于监听SpannableStringBuidler中的跨度变化。有一些非常相似的东西,文本观察者。是的,他们有同一个父亲,诺科普斯潘。他们倾听文本变化和跨度变化。 以下是SpanWatcher的源代码: 0102030405060708091011121314151617181920212223/* * *当此类型的对象附加到Spannable时,将调用其方法*来通知它其他标记对象已经*添加、更改或移除。*/公共接口SpanWatcher扩展了NoCopySpan { /** *调用此方法是为了通知您指定的对象*已附加到指定的文本范围。*/public void on panadded(可扩展文本、对象内容、int开始、int结束);/** *调用此方法是为了通知您指定的对象*已从指定的文本范围中分离。*/公共空在panRemoved上(可展开文本、对象内容、int开始、int结束);/** *调用此方法是为了通知您指定的对象*已从文本的范围ostart…oend *重新定位到新的范围nstart…nend。*/ public void onSpanChanged(可扩展文本、对象内容、int ostart、int oend、int nstart、int nend);} 我们已经知道光标是一种跨度。也就是说,我们可以通过SpanWatcher监听光标的活动,并意识到当光标在Span内移动时,它将再次移动到Span的最近边,并且光标永远不能插入Span内。这样,我们就可以实现将标注文本作为一个整体的想法。 下面是代码实现: 0102030405060708091011112131415161718192021222324252627282930313233343536373839 package com . iyao import Android . text . Selection import Android . text . SpanWat cher import Android . text . Spannaleimport kotlin . math . abimport kotlin . reflect . kClass class Selection .选择_结束&选择!= n start){ selEnd = n start text . GetSpans(n start,nend,kClass.java)。firstOrNull()?。运行{ val spanStart = text . GetSpanStart(this)val spanEnd = text . GetSpanEnd(this)val index = if(ABS(selEnd-spanEnd)> ABS(selEnd-spanStart))spanStart否则spanEnd Selection . set Selection(text,Selection . GetSelectionStart(text,index) } } if (what === Selection。选择_开始和选择开始!= n start){ SelStart = n start text . GetSpans(n start,nend,kClass.java)。firstOrNull()?。运行{ val spanStart = text . GetSpanStart(this)val spanEnd = text . GetSpanEnd(this)val index = if(ABS(SelStart-SpanEnd)> ABS(SelStart-SpanStart))spanStart否则spanEnd selection . set selection(text,index,selection . GetSelectionEnd(text))} }覆盖娱乐已覆盖已删除(text:Spanable?什么:有吗?,开始:Int,结束:Int) { }覆盖已添加的乐趣(文本:可扩展?什么:有吗?,开始:Int,结束:Int) { }} 现在,我们只需要将这个跨度添加到setText()之前的文本中。 4.2听软键盘的删除键并处理所选状态 现在,Span覆盖的文本被视为一个整体,光标不能插入,但是当我们从Span的尾部删除文本时,它仍然被一个字一个字地删除。我们的要求是当删除Span文本时,整个Span都可以被删除,所以我们需要监控键盘的删除键。 0102030405060708091011112131415161718192021 package com . iyao import Android . text . selection import Android . text . spanable class key codedeletehelper private constructor(){ companion object { fun OnDedown(text:Spanable):布尔值{ val selectionStart = selection . GetSelectionStart(text)val selectionId = selection . GetSelectionEnD(text)text . GetSpans(selectionStart,selectionEn)第一个或全部{ text.getSpanEnd(it) ==选择开始}?。运行{返回(选择开始==选择结束)。还有{ val spanStart = text . GetSpanStart(此)val spanEnd = text.getSpanEnd(此)选择.设置选择(text,SpanStart,spanEnd) } }返回false } }} 让我们使用它: 0102030405060708091011编辑文本. setOnKeyListener { v,键码,事件-> if(键码==键码事件。KEYCODE _ DEL & & event . action = = KeYEvent。ACTION _ DOWN){ return @ SetOnKeyListener KeyCodeDeleteHelper . OnDeldown((v as editText)。text)} return @ SetOnKeyListener false }//Get数据valstring = edittext . text . let { it . getspans(0,it.length,databindingspan:: class.java)}。映射{it.bindingdata ()} 现在你可以达到和微博一样的效果。一切都很顺利。 然而,当你运行它的时候,你会发现选择panWatcher一点效果都没有。轮子已经造好了,你告诉我轴承坏了。 而且,当您在编辑文本上打印跨度时,您找不到选择范围监视器。这表明选择面板监视器在设置文本()期间被清除。我们能在setText()之后设置它吗?如果你这样做,你会发现一个新问题。由setText()添加的文本无效。似乎我们不能通过setText()添加内容,我们只能使用getText()来添加内容。此外,我们必须完全禁用setText(),因为每次调用都会清除选择面板监视器。 这个方法看起来不错,但是如果有人不熟悉这个特性呢?告诉他你不能用setText()?还是通过内联方法或继承为编辑文本添加新方法?所有这些都很好,但唯一的缺点是它不是我想要的优雅。我希望它像普通的编辑文本一样使用setText()方法。 要考虑的问题是,选择“泛观察者”在哪里消失了?我要把这个方位拿回来。 4.3优雅实现车轮的轴承:可编辑。工厂 setText()方法中的SelectionSpanWatcher消失。我需要阅读它的源代码。 编辑文本覆盖getText(),settext(字符序列文本,缓冲区类型)方法: 0102030405060708091011121314151617 @ Override public可编辑GetText(){ CharSequence text = super . GetText();//这只能在施工期间发生。if (text == null) {返回null;}如果(可编辑的文本实例){返回(可编辑)super . GetText();} super.setText(文本,缓冲类型。可编辑);返回(可编辑)super . GetText();} @覆盖公共空设置文本(字符序列文本,缓冲类型){ super.setText(文本,缓冲类型。可编辑);} 从源代码的角度来看,重写的唯一目的是将BufferType设置为BufferType.EDITABLE。 我们都知道文本视图有三种文本模式: 1)缓冲类型。正常静态文本模式,该模式下的文本不可编辑,也没有丰富的文本样式; 2)缓冲类型。SPANNABLE是一种带有文本样式的模式,不能编辑。当TextView.isTextSelectable()返回true时,文本视图的文本模式; 3)缓冲类型的文本模式。可编辑可编辑文本,可使用文本样式编辑。 这里不具体提及这三种模式的相关内容。只需要知道编辑文本的模式是BufferType.EDITABLE。 那么,缓冲类型之间是什么关系。可编辑和“方位”?这很重要。 在阅读上面的源代码片段时,我想知道是否有人注意到设置文本(CharSequence)传入了一个CharSequence对象,文本视图#getText()返回了一个CharSequence对象,而编辑文本#getText()返回了一个可编辑对象。它是何时以及如何完成转换的?这将是一个突破吗? 根据可编辑getText()的源代码,它被转换为super.settext(文本、缓冲类型、可编辑)。 在TextView源代码中,settext(字符序列文本、缓冲区类型、布尔型通知前、整型)有这样一个流分支: 010203040506070809101112私有void setText(CharSequence text,BufferType类型,布尔notifyBefore,int oldlen) { if (type == BufferType。可编辑|| getKeyListener()!= null | | NeedEditableForNotification){...可编辑t = mEditableFactory.newEditable可编辑(文本);text = t;...}...mBufferType =类型;setTextInternal(文本);...} 因此,分配给编辑文本的字符序列对象首先由媒体工厂转换成可编辑对象,最后分配给编辑文本。媒体工厂的类型是可编辑的。工厂,它是一个静态的内部类。 让我们看看可编辑的实现。工厂是: 01020304050607080910111213141516171819202122232425/* * *工厂,由TextView用来创建新的{ @ link Editable。您可以对其进行子类化,以提供{@link SpannableStringBuilder}以外的功能。* * @参见安卓. widget . TextView # SeteditableFactory(工厂)*/公共静态类Factory {私有静态可编辑。工厂实例=新的可编辑。工厂();/** *返回标准的可编辑工厂。*/公共静态可编辑。工厂getInstance() {返回实例;} /** *从指定的*字符序列返回一个新的SpannedStringBuilder。您可以覆盖它以提供*一种不同类型的跨区表。*/ public可编辑的新可编辑的(字符序列源){返回新的SpannableStringBuilder(源);}} 一个非常简单的转换,它将CharSequence对象转换为SpannableStringBuilder的对象,后者是可编辑的子类。 让我们看看这个构造函数: 0102030405060708091011121314151617 public SpannableStringBuilder(CharSequence text,int start,int end) {...mText = Arrayutils . NewUnpaddchararray(GrowingArrayutils . GrowSize(src len));...if(spanded的文本实例){ spanded sp =(spanded)文本;对象[]跨度= sp.getSpans(开始、结束、对象类);对于(int ii = 0;ii 这就是轴承损坏的原因。 前面提到的SpanWatcher继承了NoCopySpan,NoCopySpan是一个标记接口。其功能是标记跨度不能被复制。在构造时,SpannableStringBuilder会忽略所有NoCopySpan及其子类。因此,选择panWatcher不会分配给编辑文本的文本。 因为没有拷贝NoCopySpan,所以我们可以在构建SpannableStringBuilder之后重置它。可编辑的注释。工厂给了我希望。它可以被重写并重新注入到编辑文本中。 1 ndroid . widget . TextView # SeteditableFactory(工厂) 下面是重写的可编辑。工厂,用于将NoCopySpan重置为SpannableStringBuilder: 0102030405060708091011121314151617 package com . iyao import Android . text . editableimport Android . text . nocopyspanimport Android . text . SpannableStringBuilder import Android . text . SpannedImport Android . text . style . BackgroundColorSpan类NoCopySpanEditableFactory(私有vararg val spans: NoCopySpan):可编辑。工厂(){覆盖有趣的新的可编辑的(来源:字符序列):可编辑的(返回SpannableStringBuilder.valueOf(来源)。应用{跨度. forEach {设置跨度(它,0,源.长度,跨度。SPAN_INCLUSIVE_INCLUSIVE) } } } 没错,有17行代码。这是这个轮子的新轴承。现在让我们重复使用它。 使用editText.setEditableFactory()安装新轴承并让车轮运转: 1234567 edittext . seteditablefactory(nocopyspanediatablefactory(selectionPanwatcher(DataBindingSpan::class)))edittext . setonKeyListener { v,keyCode,事件-> if (keyCode == KeyEvent。KEYCODE _ DEL & & event . action = = KeYEvent。ACTION _ DOWN){ return @ SetOnKeyListener KeyCodeDeleteHelper . OnDeldown((v as editText)。text)} return @ SetOnKeyListener false } 一个“优雅”的实现诞生了,你可以在微博这样的评论中使用@ people。 运行效果: 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_1.gif 5.微信的实践 微信应该以更简单的方式处理。它们不禁止在覆盖范围的文本中插入光标,但是当覆盖范围的文本发生变化时,会清除范围和数据。他们还想通过监控删除按钮来实现Span的整体删除,但性能与微博略有不同。 微信三部曲。 首先,定义一个接口来判断Span是否无效: 1234567 package com . iyao import Android . text . Spanable interface RemovedOnDirtysPan { fun IsDirty(text:Spanable):Boolean } 第二,让SpannableData实现这个接口。当然,你也可以让RemoveOnDirtySpan继承DataBindingSpan,尽管我认为这不符合“六大”。 010203040506070809101112131415161718 class SpanNableData(私有值范围:字符串):DataBindingSpan,RemoveOnDirtySpan { override fun SpanNedText():CharSequence { return SpanNableString(范围)。应用{设置跨度(前背景颜色跨度(颜色。红色),0,长度,跨度。SPAN_EXCLUSIVE_EXCLUSIVE) } }覆盖趣味绑定数据():字符串{返回跨度}覆盖趣味标识(文本:Spannable):布尔值{值spanStart =文本。getSpanStart(此)值spanEnd =文本。getSpanEnd(此)返回spanStart > = 0 & & spanEnd > = 0 & >文本。子字符串(spanStart,spanEnd)!= spanned } } 最后,重写一个DirtySpanWatcher来删除无效的跨度: 01020304050607080910111213141516171819202122232425262728 package com . iyao import Android . text . SpanWatcherImport Android . text . Spanable class DirtySpanWa tcher(private val Remove谓词:(Any) ->布尔值):SpanWatcher { override fun OnSpanchanged(text:Spanable,what: Any,ostart: Int,oend: Int,nstart筛选{ removePredicate.invoke(it) }。forEach { text.removeSpan(it) } }覆盖娱乐项目已覆盖(text:Spanable,what: Any,start: Int,end: Int) { }覆盖娱乐项目已添加(text:Spanable,what: Any,start: Int,end: Int) { } } 现在,让微信运行起来: 123456789 edittext . seteditablefactory(NocopysPanediatableFactory(DirtysPanwatcher { it is ForeGroundColorSpan | | it is RemoveOnDirtysPan }))edittext . SetOnKeyListener { v,keyCode,事件-> if (keyCode == KeyEvent。KEYCODE _ DEL & & event . action = = KeYEvent。ACTION _ DOWN){ KeyCodeDeleteHelper . OnDeldown((v as editText)。text)} return @ SetOnKeyListener false } 应该注意的是,微信和微博有一点点不同。微博已经被确认和删除两次,但微信没有。代码上唯一的不同是微信没有return@setOnKeyListener。 运行效果: 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_2.gif 6.QQ的实践 QQ太简单了,我不想谈论它。在这里写一个简单的演示来演示。 QQ还需要使用数据绑定服务,即使你不需要它。它的核心是图像跨度: 010203040506070809101112 class SpanNableData(私有值跨度:字符串):DataBindingSpan { override fun SpanNedText():CharSequence { return SpanNableString(「@ $ spanded」)。应用{设置跨度(图像跨度(标签可绘制(@ $跨度),颜色=颜色。LTGRAY),跨区),0,长度-1,跨区。SPAN _ EXCLIVE _ EXCLIVE)} }覆盖有趣的绑定数据():字符串{返回范围} } 现在,我们只需要实现一个可绘制的绘图文本。在这里,我的名字是LabelDrawable,这可能不准确: 0102030405060708091011121314151617181920212223242526 class标签可绘制(val text: CharSequence,private val TextPaint:TextPaint = TextPaint(绘制)。反别名标志)。应用{ textSize = 42f this.color = Color。DKGRAY文本对齐=绘画。Align.CENTER},color:Int):ColorDrawable(color){ init { CalculateBounds()}覆盖有趣的绘图(画布:画布){ super.draw(画布)画布. drawText(文本,0,文本.长度,边界. CENTER()。toFloat(),bounds.centerY()。toFloat()+GetBaseline Offset(TextPaint . FontMetrics),textPaint) }私人趣味计算边界(){ TextPaint . GetTextBounds(text . ToString),0,text.length,Bounds)边界. inset(-8,-4)边界. offset(8,0) }私人趣味GetBaseline Offset(FontMetrics:Paint)。FontMetrics):浮动{返回(fontMetrics .下降- fontMetrics .上升)/ 2 - fontMetrics .下降}} 像使用普通扳手一样使用他。 运行效果: 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_3.gif 如果你想做得更好,你需要处理多行文本,如测量,布局和绘图。给点提示,文本视图截图也是可绘制的。如果有视图,即使它没有附加到窗口,我们也可以手动调用measure(),layout(),draw()方法来获取可绘制视图的屏幕截图,以便添加到图像Span,但是这不能响应触摸事件。 7.获取文本中绑定的数据 只需使用以下代码: 123 val strings = EdItText . text . let { it . GetSpans(0,it.length,DataBindingSpan::class.java)}。映射{ it.bindingData() } 8.下载本文的源代码附件 轻松掌握(52 im.net)。zip (434 KB,下载次数:39次,价格:1枚金币) 9.题外话:本文的代码是由Kotlin编写的,但是我希望Java版本由@ people实现。我该怎么办? 是的,科特林还没有被广泛使用,所以它不能被使用。 然而,@,一个看似简单的函数,在没有bug的情况下实际上有点困难,或者代码量并不太小。 那么,我在哪里可以找到@ person函数的可靠的Java版本呢? 答案就在这里:你可以下载网易云信的官方开源即时消息演示,它有@ function的完整代码实现: 在安卓即时通讯应用中实现@ person功能:模仿微博、QQ、微信,零入侵,高可扩展性[图形+源代码]_55.jpg ▲ @ person函数完整的源代码位置 别告诉我这是违法的。他们说这是开源的。。。 网易云信即时通讯演示下载地址:点击这里进入。 网易云信即时通讯演示的Github地址:https://github.com/netease-im/NIM_Android_Demo 好的,我没有向网易云信收取任何福利费用。我之所以建议你“撕掉”它的源代码,是因为在我评估了主流第三方即时通讯开源的演示代码之后,@ person函数写得相当好,只有网易云信没有选择。
Agora:从演示到实用,仍有10,000个网络rtcs
因为音频和视频通话=音频和视频处理+网络传输,并且公共互联网不是实时的, 因此,真正可用的实时音频和视频服务的开发,从演示到生产,仍然需要10,000个网络广播终端 前言 在WebRTC开源之前,实时音频和视频通信听起来如此先进:回声消除、噪声抑制...看到傅里叶变换时感到头痛的工程师很难解决这些专业领域的问题 谷歌收购了GIPS。在开发了网络广播电视项目之后,开发者可以自己放弃网络音频和视频交流 在下载、编译和集成之后,当工程师们第一次听到通过互联网发送的提要时,他们会非常兴奋。demo距离10,000人的现场直播只有一步之遥 然而,电信行业需要四个9的可用性,而刚才令人兴奋的“喂养”不能得到一个9 一家公司在展会上展示了跨国音频和视频,许多电话无法接通。自嘲说我们还没有做网络优化 这相当于互联网创业时期的“糟糕的程序员”,本质上是演示产品和真实产品之间的差距,以及外行和局内人之间的差距 小红说在家用WIFI聊天和斗地主没有压力,也不可能用你的音频和视频通话 如果你想开发和分享微信的功能,百度文档会一步一步来做;然而,找不到这样的文档,并且通过这样做可以解决用户报告的这些问题,然后可以在电信级实现音频和视频呼叫 教育、社交和约会应用只需要音频和视频通话。上述问题将迫使用户使用更稳定的skype或微信进行交流 技术原因造成的用户流失是每个工程师都不想看到的 实时音频和视频的难点是什么? 因为音频和视频通话=音频和视频处理+网络传输,公共互联网不是为实时通信而设计的 难点如下: 协议:tcp有不可容忍的延迟,udp有丢包延迟抖动紊乱 政治方面:各国出口的光缆很少,带宽也受到严格限制 业务:由于成本的原因,运营商之间的网络传输非常糟糕 用户设备:无线路由器只支持802.11G的实时通信模式。多台路由器使用同一频段会造成信号污染;2G网络的上行带宽仅为20kbps 体系结构:公共网络中的每个节点都不可靠。后台工程师熟悉的mtr命令可以分析哪个路由节点的数据包丢失率高。如果此时正在传输音频和视频,质量将不可避免地受到影响 如果你想在这样一个公共互联网上传输音频和视频数据,但你不做任何网络传输工作,如果你没有遇到问题,你可以购买彩票 如何进行网络传输?老师没有谈论它,并且在网上找不到它。有没有一种深深的无力感 如何解决它? 我们可以从以下几个方面着手: 质量评估:如果语音卡成功,我们必须首先通过网络参数来评估语音质量 数据统计:用户的使用情况如何?它需要一个完善的数据统计模型和支持系统,否则开发人员会盲目 智能接入:影响质量的原因——不同的互联网服务提供商会有不同的丢包率,需要多线服务器 智能路由:例如,随着海外用户的增加,当电信用户与美国用户通话时,会有很大的数据包丢失。如果没有像美国电信那样的多线服务器,它在通过日本转发后可能不会丢失。这是智能路由。 虚拟专线:智能接入和智能路由可以匹配网络专线的质量,称为虚拟专线 丢包对抗:用户投诉明显减少,仍有一些用户自身网络不强 用户X一直在使用2G,而用户Y在公司的很多无线网络中都有信号污染,所以需要一个丢包对策机制 网络可用性:用户无法通过虹桥机场,公共场所WIFI有很多限制,因此需要考虑网络可用性 后台高可用性:用户很好,但各种互联网公司事件让运营商担心他们的服务器电源也被挖掘机切断,因此他们需要后台高可用性。
自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)
1.写在前面 我一直想写一篇关于分享的文章,但是我太忙了,没有时间。今天,我终于离开了公司。我计划在再次找工作之前好好休息几天。我决定冷静下来,有空的时候写一篇文章。毕竟,我从我的前任那里学到了很多。 在工作了五年半之后,在过去的三四年里,我一直在做与社会相关的项目,包括直播、即时通讯、视频分享、社区论坛和其他产品。我知道即时通讯技术在项目中的重要性,并借此机会总结开源共享的精神,所以我写了这篇文章。 *重要提示:本文不是一篇即时通讯理论文章,但文章的内容都是由实战代码组织的。如果你对即时通讯(即时通讯)技术理论知之甚少,建议你仔细阅读它:“初学者足够了:从头开始开发移动即时通讯”。这个网站上的其他文章,“拿起键盘是干的:用我的双手开发一个分布式即时消息系统”,“适合初学者:从头开始开发一个即时消息服务器(基于Netty,带有完整的源代码)”和“适合初学者:教你用Go(带有源代码)构建一个高性能和可扩展的即时消息系统”,也值得一读。 本文的实际内容将涉及以下即时通讯技术内容: 1)Protobuf序列化; 2)TCP拆包和粘贴; 3)长连接握手认证; 4)心跳机制; 5)重连机制; 6)消息重传机制; 7)读写超时机制; 8)离线消息; 9)线程池。 不想阅读本文的学生可以直接从Github下载本文的源代码: 1)原地址:https://github.com/FreddyChen/NettyChat 2)备用地址:https://github。com/52im/nettychat 接下来,让我们言归正传。 2.本文的阅读对象 这篇文章适合没有开发经验的白人小开发者。它将教你如何从头开始开发一个典型的基于Netty+TCP+Protobuf的即时通讯(即时消息)系统。它非常适合从头开始的安卓开发者。 本文不适合没有编程的潜在开发人员,因为即时通讯(IM)系统属于特定的业务领域,如果您几乎不能编写一般的逻辑代码,不建议您阅读本文。这篇文章显然不是编程语言的入门教程。 此外,本网站上另一篇类似的文章“拉起键盘是干的:一个徒手的分布式即时通讯系统”也值得一读。 3.关于作者 自己开发即时通讯有这么难吗?教你自己创建一个简单的即时消息版本的Andriod(源代码)_WX20190721-165501@2x.jpg) 这篇文章的原始内容最初由FreddyChen共享。作者现在从事安卓程序开发。他的技术博客地址是https://金爵。im/user/5bd 7 aff be 51d 4547 f 763 Fe 72 4.为什么要使用传输控制协议? 在这里,我们需要简单地解释一下TCP/UDP的区别,并简单地总结一下。 优势: 1)TCP:其优点是稳定可靠。在数据传输之前,会有一个三次握手来建立连接。数据传输时,将有确认、窗口、重传和拥塞控制机制。数据传输后,连接将被断开,以节省系统资源。 2)UDP:它的优点是比TCP更快、更安全。UDP是一种无状态传输协议,没有TCP所拥有的各种机制,因此它可以非常快速地传输数据。如果没有TCP的这些机制,被攻击使用的机制会更少,但是无法避免被攻击。 缺点: 1)TCP:它的缺点是速度慢、效率低、系统资源高、易受攻击。TCP需要在传输数据之前建立连接,这将消耗时间。此外,当传输数据时,确认机制、重传机制和拥塞机制将消耗大量时间,并且所有传输连接都应该在每个设备上维护。 2)UDP:缺点是不可靠和不稳定,因为没有TCP机制。UDP传输数据时,如果网络质量不好,很容易丢失数据包,导致数据丢失。 适用场景: 1)TCP:当对网络通信质量有要求时,如HTTP、HTTPS、FTP等文件传输协议,POP、SMTP等邮件传输协议。 2)UDP:当网络通信质量不高时,要求网络通信速度快。 至于WebSocket,稍后可能会写一篇特别的文章来介绍它。总而言之,我们决定采用TCP协议。 有关TCP和UDP的比较和选择的详细文章,请参见: 简单介绍了TCP和UDP的区别 为什么QQ使用UDP而不是TCP?》 "移动即时通讯协议选择:UDP还是TCP?》 网络编程中的懒人介绍(4):快速理解TCP和UDP的区别 网络编程中的懒人入门(5):快速理解为什么UDP有时优于TCP 安卓程序员必须知道的网络通信传输层协议——UDP和TCP 或者,如果您对TCP和UDP协议知之甚少,您可以阅读本文: TCP/IP的详细说明-第11章UDP:用户数据报协议 TCP/IP的详细说明-第17章TCP:传输控制协议 TCP/IP的详细说明-第18章TCP连接的建立和终止 TCP/IP的详细说明-第21章TCP的超时和重传 脑残网络编程导论(1):学习TCP三次握手和四波动画 技术过去:TCP/IP协议改变世界(珍贵的多画面,手机注意) 易于理解——对TCP协议的深刻理解(一):理论基础 网络编程中的懒人介绍(3):快速理解TCP协议就足够了 更高层次:优秀安卓程序员必须了解的网络基础。 5.为什么使用原蟾? 有三种常见的和可选的应用网络传输协议,即json/xml/protobuf,它们是旧的规则。让我们先分别看看这三种格式的优缺点。 附言:如果您不知道什么是protobuf,建议您详细阅读:Protobuf通信协议详细说明:代码演示、详细原理介绍等。 优势: 1)json:它的优点是比xml格式小,传输效率比XML高得多,可读性也不错。 2)xml:它的优点是可读性强,解析方便。 3)protobuf:优点是传输效率快(据说当数据量大时,传输效率比xml和json快10-20倍)。序列化后,体积比Json和XML小,并且支持跨平台多语言。消息格式升级和兼容性还不错,序列化和反序列化速度非常快。 缺点: 1)json:缺点是传输效率不是特别高(比xml快,但比protobuf慢得多)。 2)xml:缺点是低效率和过度的资源消耗。 3)原虫:缺点是使用不方便。 在需要大量数据传输的场景中,如果数据量很大,protobuf可以明显减少数据量和网络IO,从而减少网络传输所消耗的时间。考虑到作为一种社交产品,消息数据量将非常大,为了节省流量,protobuf是一个不错的选择。 有关即时消息相关协议格式选择的更多文章,请进一步阅读: 如何选择即时通讯应用的数据传输格式 强烈建议使用Protobuf作为您的即时通讯应用程序数据传输格式 综合评估:Protobuf的性能比JSON快五倍吗?》 移动即时通信发展中面临的技术问题(包括通信协议选择) 简要描述移动即时消息开发的难点:架构设计、通信协议和客户端 理论与实践相结合:典型即时通信协议的详细设计 分享技术实践,如58户实时信息系统的协议设计 详细解释如何在节点中使用谷歌的原型 技术素养:新一代基于UDP的低延迟网络传输层协议 “金蝶手写团队分享:仍在使用JSON?Protobuf使数据传输更经济、更快(原理) “金蝶手写团队分享:仍在使用JSON?Protobuf使数据传输越来越快(实战) > >更多类似的文章... 6.为什么使用Netty? 首先,让我们看看什么是内蒂。网络上的介绍:Netty是一个基于JBOSS提供的Java NIO的开源框架。Netty提供异步无阻塞、事件驱动、高性能、高可靠性和高度可定制的网络应用程序和工具,可用于开发服务器和客户端。 附言:如果你不知道经典的信息作战、网络作战或网络作战框架,请阅读以下文章: 历史上最强大的Java NIO简介:如果你担心开始和放弃,请阅读这篇文章!》 “放开我!让您在一分钟内了解Java NIO和经典IO之间的区别 初学者:网络的学习方法和高级策略——一个Java高性能NIO框架 NIO框架的详细说明:Netty的高性能 为什么不使用Java生物? 1)一个连接一个线程:由于线程数量有限,消耗大量资源,最终无法满足高并发连接的需求。 2)低性能:频繁的上下文切换导致CUP的利用率低。 3)可靠性差:由于所有的IO操作都是同步的,即使对于业务线程,业务线程的IO操作也可能被阻塞,这将导致系统过于依赖网络的实时性和外部组件的处理能力,从而大大降低了可靠性。 为什么不使用Java NIO呢? 1)NIO的类库和API相当复杂。要使用它进行开发,您需要掌握选择器、字节缓冲、服务器套接字通道、套接字通道等。 2)需要许多额外的编程技巧来帮助NIO的使用。例如,因为NIO涉及反应器线程模型,所以有必要熟悉多线程和网络编程来编写高质量的NIO程序。 3)要具有高可靠性,工作量和难度都很大,因为服务器需要面对频繁的客户端访问和断开、网络闪烁、半包读写、故障缓存和网络阻塞等问题,这些问题会严重影响我们的可靠性,用本机NIO解决起来相当困难。 4)JDK NIO BUG - epoll空轮询中的著名错误,当select返回0时,将导致选择器的空轮询,并导致100%的CUP。这位官员说,这个问题在JDK1.6之后已经解决了,但实际上,发生的可能性降低了,而且没有从根本上解决。 为什么使用Netty? 1)应用编程接口简单易用,开发门槛低; 2)功能强大,预置多种编解码功能,支持多种主流协议; 3)定制能力强,可以通过ChannelHandler灵活扩展通信框架; 4)高性能。与许多NIO主流框架相比,Netty具有最高的综合性能; 5)稳定性高,解决了BUGJDK NIO; 6)经历了大规模商业应用的测试,质量和可靠性得到了很好的验证。 为什么不使用第三方软件开发工具包,如融云、环欣和腾讯TIM? 这是一个意见问题。有时,这是因为公司的技术选择,因为使用第三方SDK意味着消息数据需要存储在第三方服务器上。此外,可伸缩性和灵活性肯定不如我们自己开发的那些。还有一个小问题,那就是充电。例如,融云的免费版只支持100个注册用户,超过100个就要收费,群聊支持者的数量有限,等等... 自己开发即时通讯有这么难吗?手工1.jpg教你一个简单的安卓版本的即时消息(带源代码)。 ▲以上截图内容来自云即时通讯官方网站 Mina实际上与Netty非常相似,大多数API都是相同的,因为它们是由同一作者开发的。然而,我觉得米娜没有妮蒂成熟。在使用Netty的过程中,如果出现问题,很容易找到解决方案,因此Netty是一个不错的选择。 注意:关于MINA和Netty框架之间的关系和比较,请参见下面的文章了解详细信息: 关于“为什么选择Netty”的11个问题和答案 关于开源NIO框架的流言蜚语——首先是MINA还是Netty?》 选择内蒂还是米娜:深入研究与比较(一) 选择内蒂还是米娜:深入研究和比较(2) 好吧,我们废话少说,开始吧。 7.准备工作 首先,我们创建一个新项目,然后在项目中创建一个安卓库。模块名称暂时为im_lib,如图所示: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-1.jpg。 然后,在分析了我们的消息结构之后,每条消息应该有一个消息唯一的id、发送者id、接收者id、消息类型、发送时间等等。经过分析,一般的消息类型分类如下: MsgId:消息Id 发件人Id:发件人id ToId:收件人Id MsgType:消息类型 MsgContentType:消息内容类型 Timestamp:消息时间戳 状态报告:状态报告 扩展:扩展字段 根据以上所述,我编制了一张思维导图供你参考: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-2.jpg。 上面的图片比原始图片大。如果你看不清楚,请下载一张清晰的大图: 信息结构——思维导图[清晰的大图]。zip (314.71 KB,下载次数:175) 这是最基本的部分。当然,您可以根据自己的需要定制适合自己的消息结构。 我们根据用户定义的消息类型编写原型文件: 01020304050607080910111213141516171819 syntax = " proto 3 ";//指定原型版本选项。原蟾蜍";//指定包名选项。//指定生成的类名消息消息{ Head Head = 1;//消息头字符串正文= 2;//消息正文}消息头{字符串MsGid = 1;//消息id int 32 MSgType = 2;//消息类型int 32 MsgContentType = 3;//消息内容类型字符串FromId = 4;//消息发送者id字符串ToID = 5;//消息接收方id int 64timestamp = 6;//消息时间戳int 32 StatusReport = 7;//状态报告字符串extend = 8;//扩展字段,json}以键/值形式存储} 然后执行命令(我使用的mac和windows命令应该类似): 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 7-3.jpg。 然后,我们将看到一个java类将在与原型文件相同的目录中生成,这就是我们需要使用的: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-4.jpg。 我们打开扫描: 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_7-5.jpg 有很多事情,所以不要担心它们。这是谷歌为我们生成的protobuf类。直接用吧。如何使用它? 只需直接使用这个类文件,并将其复制到我们开始指定的项目包的路径中: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-6.jpg。 添加依赖项后,您可以看到MessageProtobuf类文件没有报告错误。顺便介绍一下内蒂的罐子包和法斯特森的: 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_7-7.jpg 建议使用最终版的jar包。如果您以后熟悉它,可以使用简化的jar包。 至此,准备工作结束了。现在,让我们编写java代码来实现即时通讯的功能。 8.代码封装 为什么你需要封装?直截了当地说,这是为了解耦,并且在不改变调用位置的情况下,将来切换到不同的框架实现是很方便的。 以栗子为例,早期流行的安卓图片加载框架是通用图像加载器。后来,由于某些原因,原作者停止了项目的维护。目前,流行的图片加载框架是毕加索或格莱德,因为有很多地方可以调用图片加载功能。如果在早期使用通用图像加载器时没有进行一些封装,那么现在就必须切换到Glide,而且变化会非常非常大,可能会有遗漏,风险非常高。 那么,解决方案是什么? 非常简单,我们可以使用工厂设计模式进行一些包装。有三种工厂模式:工厂方法模式、抽象工厂模式和工厂方法模式。在这里,我使用工厂方法模式进行包装。具体差异请参考“我对三种设计模式的理解:简单工厂、工厂方法和抽象工厂”。 让我们分析一下,ims(即时消息服务,以下简称ims)应该具有初始化、建立连接、重新连接、关闭连接、释放资源、判断长连接是否关闭、发送消息等功能。 基于以上分析,我们可以抽象出一个接口: 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_8-1.jpg。 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_8-2.jpg。 OnEventListener是一个与应用层交互的侦听器: 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 8-3.jpg。 IMConnectStatusCallback是im的连接状态回调侦听器: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_8-4.jpg。 然后编写一个Netty tcp实现类: 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_8-5.jpg 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_8-6.jpg。 接下来,编写一个工厂方法: 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_8-7.jpg 封装部分到此结束。接下来,它实现了。 9.初始化 我们首先实现init (vector server urllist,onevent侦听器,imsconnectstatuscallback回调)方法,初始化一些参数,并建立第一个连接。 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_9-1.jpg MsgDispatcher是消息转发器,负责将收到的消息转发到应用层: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_9-2.jpg。 ExecutorServiceFactory是一个线程池工厂,负责调度重新连接和心跳线程: 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 9-3.jpg。 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 9-4.jpg。 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_9-5.jpg 10.连接和重新连接 resetConnect()方法用作连接的起点,第一个连接和重新连接逻辑都在resetConnect()方法中进行逻辑处理。 让我们看一眼: 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_10-1.jpg。 可以看出,当第一次连接时,即当连接失败一个周期后重新连接时,线程将休眠一段时间,因为此时网络条件可能不是很好。然后,判断ims是否关闭或者是否正在进行重新连接操作。由于重新连接操作是由子线程执行的,因此需要一些并发处理来避免重复的重新连接。 重新连接任务开始后,分四个步骤执行: 1)更改重新连接状态标识; 2)向应用层回调连接状态; 3)关闭先前打开的连接通道;; 4)使用线程池执行新的重新连接任务。 ResetConnectRunnable是一个重新连接任务,核心重新连接逻辑放在这里执行: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-2.jpg。 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-3.jpg。 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-4.jpg。 ToServer()是服务器实际连接的位置: 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_10-5.jpg。 InitBootstrap()用于初始化Netty引导: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-6.jpg。 注意:将NioEventLoopGroup的线程数设置为4可以满足QPS超过一百万的情况。如果应用程序需要承受数千万的流量,它需要额外调整线程的数量。(请参考:“netty的实际百万流量NioEventLoopGroup线程数配置”) 接下来,让我们看一下TCPChannelInitializerHanlder: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-7.jpg。 其中,protobufEncoder和ProtobufDecoder增加了对Protobuf的支持,LoginAuthRespHandler是接收服务器握手认证消息响应的处理程序,HeartbeatRespHandler是接收服务器心跳消息响应的处理程序,TCPReadHandler是从服务器接收其他消息后的处理程序。让我们别管它。我们将重点分析纵向字段预处理器和纵向字段基帧解码器,这需要扩展到解包和绑定TCP。 11.解包和粘贴TCP 什么是TCP解包?为什么要进行TCP解包? 简而言之,我们都知道,TCP以“流”的形式传输数据,为了提高TCP的性能,发送方会将待发送的数据刷入缓冲区,等待缓冲区满,然后将缓冲区中的数据发送给接收方。类似地,接收器也将具有接收数据的缓冲机制。解包意味着当套接字读取时,它不会读取一个完整的数据包,而只会读取其中的一部分。 什么是TCP粘性数据包?为什么会出现TCP粘性数据包? 同上,粘贴包是指在读取套接字时,读取实际意义上的两个或多个数据包的内容,并同时将它们作为一个数据包进行处理。 引用一张图片来解释三种情况:拆包、粘贴和正常状态: 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_11-1.jpg。 了解TCP拆包/卡的原因,如何解决? 一般来说,有四种解决方案: 1)消息固定长度; 2)使用回车换行作为消息结束标志; 3)特殊分隔符用作消息结束标志,如\ t \ n等。,而回车换行实际上是一种特殊的分隔符; 4)消息分为消息头和消息体,消息的总长度由消息头中的字段标识。 Netty为上述四种场景封装了以下四个相应的解码器: 1)固定长度的帧解码器,固定长度的消息解码器; 2)LineBasedFrameDecoder,回车换行符消息解码器; 3)DELimITERBASEDFRAMEDCODER,特殊分隔符消息解码器; 4)长度字段基本帧解码器,一个自定义长度的消息解码器。 我们使用的是LengthFieldBasedframeCoder自定义长度消息解码器,它与LengthFieldRepeater编码器一起使用。对于参数配置,建议参考文章“netty -最常见的TCP粘性包解决方案:纵向字段基础帧编码器和纵向字段中继器”,并详细解释。 我们的配置是消息头的长度是2字节,所以消息包的最大长度需要小于65536字节。netty将消息内容的长度存储在消息头字段中,接收者可以根据消息头字段获得该消息的总长度。当然,netty提供的LengthFieldBasedFrameDecoder已经打包了处理逻辑。我们只需要配置长度字段偏移量、长度字段长度、长度调整、初始化标签条,就可以解决TCP的解包和粘贴问题。与本机nio相比,这是netty的便利之处,本机nio需要自己处理解包/粘贴问题。 12.长连接握手认证 然后,让我们来看看LoginAuthHandler和HeartbeatRespHandler。 当客户机和服务器之间的连接成功建立时,客户机主动向服务器发送一个登录验证消息,引入与当前用户相关的参数,如令牌。服务器收到此消息后,会向数据库查询用户信息。如果是合法有效的用户,它会向客户端返回登录成功消息;否则,它会向客户端返回登录失败消息。这里,它是接收到服务器返回的登录状态后的处理程序。 例如: 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_12-1.jpg。 可以看出,在收到服务器握手消息响应后,状态将从扩展字段中取出。如果状态=1,则表示握手成功。此时,将首先向服务器发送心跳消息,然后使用Netty的IdleStateHandler读写超时机制定期向服务器发送心跳消息,以维持长连接并检测长连接是否仍然存在。 当客户端收到服务器成功登录的消息时,它会主动向服务器发送心跳消息。心跳消息可以是一个空包,包越小越好。服务器从客户端接收到心跳数据包后,会将其原样返回给客户端。这里,它是接收服务器返回的心跳消息响应的处理程序。 例如: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_12-2.jpg。 这相对简单。当您收到心跳消息响应时,您不需要处理任务,因此可以直接打印出来供我们分析。 13、心跳机制和读写超时机制 心跳包定期发送,也可以自己定义一个周期,比如“移动即时通讯练习:在安卓版实现微信智能心跳机制”。为简单起见,这里规定当它应用于前台时,心跳包将在8秒内发送,当它被切换到后台时,它将在30秒内发送一次,这可以根据您的实际情况进行修改。心跳数据包用于维护长连接,并检测长连接是否断开。 附注:关于心跳保持活力的更多文章,请参阅: "安卓端消息推送概述:实现原理、心跳保持、遇到的问题等." “为什么基于TCP的移动即时消息仍然需要心跳保持机制?》 “微信团队原创分享:安卓版微信背景保活战斗分享(网络保活文章)” “移动即时通讯实践:WhatsApp、Line和微信心跳策略分析” 然后,我们使用Netty的读写超时机制来实现心跳消息管理处理程序: 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_13-1.jpg。 可以看出,读超时/写超时/读和写超时可以通过回调userEventTriggered()方法来判断,下面的代码将被粘贴。 首先,我们可以在READER_IDLE事件中检测到在指定时间内是否没有收到服务器心跳数据包响应,如果是,它将触发重新连接操作。在WRITER_IDEL事件中,可以检测客户端是否在指定时间内没有向服务器发送心跳数据包,如果是,它将主动发送心跳数据包。发送心跳数据包是在子线程中执行的,因此我们可以使用以前编写的工作线程池来进行线程管理。 AddHeartbeatHandler()代码如下: 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_13-2.jpg。 从图中可以看出,在IdleStateHandler中配置的读取超时是心跳间隔的3倍,也就是说,当3次心跳没有响应时,长连接被视为断开,并触发重新连接操作。写超时是心跳间隔的长度,这意味着每个心跳间隔都会发送一个心跳数据包。没有使用读写超时,因此它被配置为0。 onconnectstatuscallback(int connectstatus)是一个连接状态回调,以及一些常见的逻辑处理: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-3.jpg 连接成功后,立即发送一条握手消息,再次梳理一下整体流程: 1)客户端根据服务端返回的host及port,进行第一次连接; 2)连接成功后,客户端向服务端发送一条握手认证消息(1001); 3)服务端在收到客户端的握手认证消息后,从扩展字段里取出用户token,到本地数据库校验合法性; 4)校验完成后,服务端把校验结果通过1001消息返回给客户端,也就是握手消息响应; 5)客户端收到服务端的握手消息响应后,从扩展字段取出校验结果。若校验成功,客户端向服务端发送一条心跳消息(1002),然后进入心跳发送周期,定期间隔向服务端发送心跳消息,维持长连接以及实时检测链路可用性,若发现链路不可用,等待一段时间触发重连操作,重连成功后,重新开始握手/心跳的逻辑。 看看TCPReadHandler收到消息是怎么处理的: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-4.jpg 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-5.jpg 可以看到,在channelInactive()及exceptionCaught()方法都触发了重连,channelInactive()方法在当链路断开时会调用,exceptionCaught()方法在当出现异常时会触发,另外,还有诸如channelUnregistered()、channelReadComplete()等方法可以重写,在这里就不贴了,相信聪明的你一眼就能看出方法的作用。 我们仔细看一下channelRead()方法的逻辑,在if判断里,先判断消息类型,如果是服务端返回的消息发送状态报告类型,则判断消息是否发送成功,如果发送成功,从超时管理器中移除,这个超时管理器是干嘛的呢? 下面讲到消息重发机制的时候会详细地讲。在else里,收到其他消息后,会立马给服务端返回一个消息接收状态报告,告诉服务端,这条消息我已经收到了,这个动作,对于后续需要做的离线消息会有作用。如果不需要支持离线消息功能,这一步可以省略。最后,调用消息转发器,把接收到的消息转发到应用层即可。 代码写了这么多,我们先来看看运行后的效果,先贴上缺失的消息发送代码及ims关闭代码以及一些默认配置项的代码。 发送消息: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-6.jpg 关闭ims: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-7.jpg ims默认配置: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-8.jpg 还有,应用层实现的ims client启动器: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-9.jpg 由于代码有点多,不太方便全部贴上,如果有兴趣可以下载本文的完整demo进行体验。 额,对了,还有一个简易的服务端代码,如下: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-10.jpg 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-11.jpg 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-12.jpg 14、运行调试 我们先来看看连接及重连部分(由于录制gif比较麻烦,体积较大,所以我先把重连间隔调小成3秒,方便看效果)。 启动服务端: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_14-1.gif 启动客户端: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_14-2.gif 可以看到,正常的情况下已经连接成功了,接下来,我们来试一下异常情况。 比如服务端没启动,看看客户端的重连情况: 这次我们先启动的是客户端,可以看到连接失败后一直在进行重连,由于录制gif比较麻烦,在第三次连接失败后,我启动了服务端,这个时候客户端就会重连成功。 然后,我们再来调试一下握手认证消息即心跳消息: 可以看到,长连接建立成功后,客户端会给服务端发送一条握手认证消息(1001),服务端收到握手认证消息会,给客户端返回了一条握手认证状态消息,客户端收到握手认证状态消息后,即启动心跳机制。gif不太好演示,下载demo就可以直观地看到。 接下来,在讲完消息重发机制及离线消息后,我会在应用层做一些简单的封装,以及在模拟器上运行,这样就可以很直观地看到运行效果。 15、消息重发机制 消息重发,顾名思义,即使对发送失败的消息进行重发。考虑到网络环境的不稳定性、多变性(比如从进入电梯、进入地铁、移动网络切换到wifi等),在消息发送的时候,发送失败的概率其实不小,这时消息重发机制就很有必要了。 有关即时通讯(IM)应用中的消息送达保证机制,可以详细阅读以下文章: 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM群聊消息如此复杂,如何保证不丢不重?》 《完全自已开发的IM该如何设计“失败重试”机制?》 我们先来看看实现的代码逻辑。 MsgTimeoutTimer: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-1.jpg 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-2.jpg MsgTimeoutTimerManager: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-3.jpg 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-4.jpg 然后,我们看看收消息的TCPReadHandler的改造: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-5.jpg 最后,看看发送消息的改造: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-6.jpg 说一下逻辑吧:发送消息时,除了心跳消息、握手消息、状态报告消息外,消息都加入消息发送超时管理器,立马开启一个定时器,比如每隔5秒执行一次,共执行3次,在这个周期内,如果消息没有发送成功,会进行3次重发,达到3次重发后如果还是没有发送成功,那就放弃重发,移除该消息,同时通过消息转发器通知应用层,由应用层决定是否再次重发。如果消息发送成功,服务端会返回一个消息发送状态报告,客户端收到该状态报告后,从消息发送超时管理器移除该消息,同时停止该消息对应的定时器即可。 另外,在用户握手认证成功时,应该检查消息发送超时管理器里是否有发送超时的消息,如果有,则全部重发: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-7.jpg 16、离线消息 由于离线消息机制,需要服务端数据库及缓存上的配合,代码就不贴了,太多太多。 我简单说一下实现思路吧:客户端A发送消息到客户端B,消息会先到服务端,由服务端进行中转。 这个时候,客户端B存在两种情况: 1)长连接正常,就是客户端网络环境良好,手机有电,应用处在打开的情况; 2)废话,那肯定就是长连接不正常咯。这种情况有很多种原因,比如wifi不可用、用户进入了地铁或电梯等网络不好的场所、应用没打开或已退出登录等,总的来说,就是没有办法正常接收消息。 如果是长连接正常,那没什么可说的,服务端直接转发即可。 如果长连接不正常,需要这样处理: 服务端接收到客户端A发送给客户端B的消息后,先给客户端A回复一条状态报告,告诉客户端A,我已经收到消息,这个时候,客户端A就不用管了,消息只要到达服务端即可。然后,服务端先尝试把消息转发到客户端B,如果这个时候客户端B收到服务端转发过来的消息,需要立马给服务端回一条状态报告,告诉服务端,我已经收到消息,服务端在收到客户端B返回的消息接收状态报告后,即认为此消息已经正常发送,不需要再存库。 如果客户端B不在线,服务端在做转发的时候,并没有收到客户端B返回的消息接收状态报告,那么,这条消息就应该存到数据库,直到客户端B上线后,也就是长连接建立成功后,客户端B主动向服务端发送一条离线消息询问,服务端在收到离线消息询问后,到数据库或缓存去查客户端B的所有离线消息,并分批次返回,客户端B在收到服务端的离线消息返回后,取出消息id(若有多条就取id集合),通过离线消息应答把消息id返回到服务端,服务端收到后,根据消息id从数据库把对应的消息删除即可。 以上是单聊离线消息处理的情况,群聊有点不同,群聊的话,是需要服务端确认群组内所有用户都收到此消息后,才能从数据库删除消息,就说这么多,如果需要细节的话,可以私信我。 更多有关离线消息处理思路的文章,可以详细阅读: 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《IM群聊消息如此复杂,如何保证不丢不重?》 《浅谈移动端IM的多点登陆和消息漫游原理》 不知不觉,NettyTcpClient中定义了很多变量,为了防止大家不明白变量的定义,还是贴上代码吧: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_16-1.jpg 17、应用层封装 这个就见仁见智啦,每个人代码风格不同,我把自己简单封装的代码贴上来吧。 MessageProcessor消息处理器: 001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103package com.freddy.chat.im; import android.util.Log; import com.freddy.chat.bean.AppMessage;import com.freddy.chat.bean.BaseMessage;import com.freddy.chat.bean.ContentMessage;import com.freddy.chat.im.handler.IMessageHandler;import com.freddy.chat.im.handler.MessageHandlerFactory;import com.freddy.chat.utils.CThreadPoolExecutor; /** * @ProjectName: NettyChat * @ class name:MessageProcessor.java * @ PackageName:com . freddy . chat .im * * @描述:消息处理器 * * @author: FreddyChen * @date: 2019/04/10 03:27 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public class MessageProcessor implements IMessageProcessor { private static final String TAG = MessageProcessor.class.getSimpleName(); private MessageProcessor() { } private static class MessageProcessorInstance { private static final IMessageProcessor INSTANCE = new MessageProcessor(); } public static IMessageProcessor getInstance() { return MessageProcessorInstance.INSTANCE; } /** * 接收消息 * @param message */ @Override public void receiveMsg(final AppMessage message) { CThreadPoolExecutor.runInBackground(new Runnable() { @Override public void run() { try { IMessageHandler messageHandler = MessageHandlerFactory.getHandlerByMsgType(message.getHead().getMsgType()); if (messageHandler != null) { messageHandler.execute(message); } else { Log.e(TAG, "未找到消息处理handler,msgType=" + message.getHead().getMsgType()); } } catch (Exception e) { Log.e(TAG, "消息处理出错,reason=" + e.getMessage()); } } }); } /** * 发送消息 * * @param message */ @Override public void sendMsg(final AppMessage message) { CThreadPoolExecutor.runInBackground(new Runnable() { @Override public void run() { boolean isActive = IMSClientBootstrap.getInstance().isActive(); if (isActive) { IMSClientBootstrap.getInstance().sendMessage(MessageBuilder.getProtoBufMessageBuilderByAppMessage(message).build()); } else { Log.e(TAG, "发送消息失败"); } } }); } /** * 发送消息 * * @param message */ @Override public void sendMsg(ContentMessage message) { this.sendMsg(MessageBuilder.buildAppMessage(message)); } /** * 发送消息 * * @param message */ @Override public void sendMsg(BaseMessage message) { this.sendMsg(MessageBuilder.buildAppMessage(message)); }} IMSEventListener与ims交互的listener: 001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163/** * @ProjectName: NettyChat * @ class name:IMSEventListener.java * @ PackageName:com . freddy . chat .im * * @描述:侦听器与ims交互 * * @author: FreddyChen * @date: 2019/04/07 23:55 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public class IMSEventListener implements OnEventListener { private String userId; private String token; public IMSEventListener(String userId, String token) { this.userId = userId; this.token = token; } /** * 接收ims转发过来的消息 * * @param msg */ @Override public void dispatchMsg(MessageProtobuf.Msg msg) { MessageProcessor.getInstance().receiveMsg(MessageBuilder.getMessageByProtobuf(msg)); } /** * 网络是否可用 * * @return */ @Override public boolean isNetworkAvailable() { ConnectivityManager cm = (ConnectivityManager) NettyChatApp.sharedInstance().getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = cm.getActiveNetworkInfo(); return info != null && info.isConnected(); } /** * 设置ims重连间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getReconnectInterval() { return 0; } /** * 设置ims连接超时时长,0表示默认使用ims的值 * * @return */ @Override public int getConnectTimeout() { return 0; } /** * 设置应用在前台时ims心跳间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getForegroundHeartbeatInterval() { return 0; } /** * 设置应用在后台时ims心跳间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getBackgroundHeartbeatInterval() { return 0; } /** * 构建握手消息 * * @return */ @Override public MessageProtobuf.Msg getHandshakeMsg() { MessageProtobuf.Msg.Builder builder = MessageProtobuf.Msg.newBuilder(); MessageProtobuf.Head.Builder headBuilder = MessageProtobuf.Head.newBuilder(); headBuilder.setMsgId(UUID.randomUUID().toString()); headBuilder.setMsgType(MessageType.HANDSHAKE.getMsgType()); headBuilder.setFromId(userId); headBuilder.setTimestamp(System.currentTimeMillis()); JSONObject jsonObj = new JSONObject(); jsonObj.put("token", token); headBuilder.setExtend(jsonObj.toString()); builder.setHead(headBuilder.build()); return builder.build(); } /** * 构建心跳消息 * * @return */ @Override public MessageProtobuf.Msg getHeartbeatMsg() { MessageProtobuf.Msg.Builder builder = MessageProtobuf.Msg.newBuilder(); MessageProtobuf.Head.Builder headBuilder = MessageProtobuf.Head.newBuilder(); headBuilder.setMsgId(UUID.randomUUID().toString()); headBuilder.setMsgType(MessageType.HEARTBEAT.getMsgType()); headBuilder.setFromId(userId); headBuilder.setTimestamp(System.currentTimeMillis()); builder.setHead(headBuilder.build()); return builder.build(); } /** * 服务端返回的消息发送状态报告消息类型 * * @return */ @Override public int getServerSentReportMsgType() { return MessageType.SERVER_MSG_SENT_STATUS_REPORT.getMsgType(); } /** * 客户端提交的消息接收状态报告消息类型 * * @return */ @Override public int getClientReceivedReportMsgType() { return MessageType.CLIENT_MSG_RECEIVED_STATUS_REPORT.getMsgType(); } /** * 设置ims消息发送超时重发次数,0表示默认使用ims的值 * * @return */ @Override public int getResendCount() { return 0; } /** * 设置ims消息发送超时重发间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getResendInterval() { return 0; }} MessageBuilder消息转换器: 001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146package com.freddy.chat.im; import com.freddy.chat.bean.AppMessage;import com.freddy.chat.bean.BaseMessage;import com.freddy.chat.bean.ContentMessage;import com.freddy.chat.bean.Head;import com.freddy.chat.utils.StringUtil;import com.freddy.im.protobuf.MessageProtobuf; /** * @ProjectName: BoChat * @ class name:MessageBuilder.java * @ PACkageName:com . bochat . app . message * * @描述:消息转换 * * @author: FreddyChen * @date: 2019/02/07 17:26 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public class MessageBuilder { /** * 根据聊天消息,生成一条可以能够传输通讯的消息 * * @param msgId * @param type * @param subType * @param fromId * @param toId * @param extend * @param content * @return */ public static AppMessage buildAppMessage(String msgId, int type, int subType, String fromId, String toId, String extend, String content) { AppMessage message = new AppMessage(); Head head = new Head(); head.setMsgId(msgId); head.setMsgType(type); head.setMsgContentType(subType); head.setFromId(fromId); head.setToId(toId); head.setExtend(extend); message.setHead(head); message.setBody(content); return message; } /** * 根据聊天消息,生成一条可以能够传输通讯的消息 * * @param msg * @return */ public static AppMessage buildAppMessage(ContentMessage msg) { AppMessage message = new AppMessage(); Head head = new Head(); head.setMsgId(msg.getMsgId()); head.setMsgType(msg.getMsgType()); head.setMsgContentType(msg.getMsgContentType()); head.setFromId(msg.getFromId()); head.setToId(msg.getToId()); head.setTimestamp(msg.getTimestamp()); head.setExtend(msg.getExtend()); message.setHead(head); message.setBody(msg.getContent()); return message; } /** * 根据聊天消息,生成一条可以能够传输通讯的消息 * * @param msg * @return */ public static AppMessage buildAppMessage(BaseMessage msg) { AppMessage message = new AppMessage(); Head head = new Head(); head.setMsgId(msg.getMsgId()); head.setMsgType(msg.getMsgType()); head.setMsgContentType(msg.getMsgContentType()); head.setFromId(msg.getFromId()); head.setToId(msg.getToId()); head.setExtend(msg.getExtend()); head.setTimestamp(msg.getTimestamp()); message.setHead(head); message.setBody(msg.getContent()); return message; } /** * 根据业务消息对象获取protoBuf消息对应的builder * * @param message * @return */ public static MessageProtobuf.Msg.Builder getProtoBufMessageBuilderByAppMessage(AppMessage message) { MessageProtobuf.Msg.Builder builder = MessageProtobuf.Msg.newBuilder(); MessageProtobuf.Head.Builder headBuilder = MessageProtobuf.Head.newBuilder(); headBuilder.setMsgType(message.getHead().getMsgType()); headBuilder.setStatusReport(message.getHead().getStatusReport()); headBuilder.setMsgContentType(message.getHead().getMsgContentType()); if (!StringUtil.isEmpty(message.getHead().getMsgId())) headBuilder.setMsgId(message.getHead().getMsgId()); if (!StringUtil.isEmpty(message.getHead().getFromId())) headBuilder.setFromId(message.getHead().getFromId()); if (!StringUtil.isEmpty(message.getHead().getToId())) headBuilder.setToId(message.getHead().getToId()); if (message.getHead().getTimestamp() != 0) headBuilder.setTimestamp(message.getHead().getTimestamp()); if (!StringUtil.isEmpty(message.getHead().getExtend())) headBuilder.setExtend(message.getHead().getExtend()); if (!StringUtil.isEmpty(message.getBody())) builder.setBody(message.getBody()); builder.setHead(headBuilder); return builder; } /** * 通过protobuf消息对象获取业务消息对象 * * @param protobufMessage * @return */ public static AppMessage getMessageByProtobuf( MessageProtobuf.Msg protobufMessage) { AppMessage message = new AppMessage(); Head head = new Head(); MessageProtobuf.Head protoHead = protobufMessage.getHead(); head.setMsgType(protoHead.getMsgType()); head.setStatusReport(protoHead.getStatusReport()); head.setMsgContentType(protoHead.getMsgContentType()); head.setMsgId(protoHead.getMsgId()); head.setFromId(protoHead.getFromId()); head.setToId(protoHead.getToId()); head.setTimestamp(protoHead.getTimestamp()); head.setExtend(protoHead.getExtend()); message.setHead(head); message.setBody(protobufMessage.getBody()); return message; }} AbstractMessageHandler抽象的消息处理handler,每个消息类型对应不同的messageHandler: 010203040506070809101112131415161718192021222324package com.freddy.chat.im.handler; import com.freddy.chat.bean.AppMessage; /** * @ProjectName: NettyChat * @ class name:AbstractMessageHandler.java * @ PackageName:com . freddy . chat .im。处理者 * * @描述:抽象消息处理程序 * * @author: FreddyChen * @date: 2019/04/10 03:41 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public abstract class AbstractMessageHandler implements IMessageHandler { @Override public void execute(AppMessage message) { action(message); } protected abstract void action(AppMessage message);} SingleChatMessageHandler单聊消息处理handler: 010203040506070809101112131415161718192021222324252627282930313233343536373839404142package com.freddy.chat.im.handler; import android.util.Log; import com.freddy.chat.bean.AppMessage;import com.freddy.chat.bean.SingleMessage;import com.freddy.chat.event.CEventCenter;import com.freddy.chat.event.Events; /** * @ProjectName: NettyChat * @ class name:SingleChatMessageHandler.java * @ PackageName:com . freddy . chat .im。处理者 * * @描述:类描述 * * @author: FreddyChen * @date: 2019/04/10 03:43 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public class SingleChatMessageHandler extends AbstractMessageHandler { private static final String TAG = SingleChatMessageHandler.class.getSimpleName(); @Override protected void action(AppMessage message) { Log.d(TAG, "收到单聊消息,message=" + message); SingleMessage msg = new SingleMessage(); msg.setMsgId(message.getHead().getMsgId()); msg.setMsgType(message.getHead().getMsgType()); msg.setMsgContentType(message.getHead().getMsgContentType()); msg.setFromId(message.getHead().getFromId()); msg.setToId(message.getHead().getToId()); msg.setTimestamp(message.getHead().getTimestamp()); msg.setExtend(message.getHead().getExtend()); msg.setContent(message.getBody()); CEventCenter.dispatchEvent(Events.CHAT_SINGLE_MESSAGE, 0, 0, msg); }} GroupChatMessageHandler群聊消息处理handler: 0102030405060708091011121314151617181920212223242526package com.freddy.chat.im.handler; import android.util.Log; import com.freddy.chat.bean.AppMessage; /** * @ProjectName: NettyChat * @ class name:GroupChatMessageHandler.java * @ PackageName:com . freddy . chat .im。处理者 * * @描述:类描述 * * @author: FreddyChen * @date: 2019/04/10 03:43 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public class GroupChatMessageHandler extends AbstractMessageHandler { private static final String TAG = GroupChatMessageHandler.class.getSimpleName(); @Override protected void action(AppMessage message) { Log.d(TAG, "收到群聊消息,message=" + message); }} MessageHandlerFactory消息handler工厂: 0102030405060708091011121314151617181920212223242526272829303132333435363738394041424344package com.freddy.chat.im.handler; import android.util.SparseArray; import com.freddy.chat.im.MessageType; /** * @ProjectName: NettyChat * @ class name:MessageHandlerFactory.java * @ PackageName:com . freddy . chat .im。处理者 * * @描述:消息处理处理程序工厂 * * @author: FreddyChen * @date: 2019/04/10 03:44 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public class MessageHandlerFactory { private MessageHandlerFactory() { } private static final SparseArray HANDLERS = new SparseArray(); static { /** 单聊消息处理handler */ HANDLERS.put(MessageType.SINGLE_CHAT.getMsgType(), new SingleChatMessageHandler()); /** 群聊消息处理handler */ HANDLERS.put(MessageType.GROUP_CHAT.getMsgType(), new GroupChatMessageHandler()); /** 服务端返回的消息发送状态报告处理handler */ HANDLERS.put(MessageType.SERVER_MSG_SENT_STATUS_REPORT.getMsgType(), new ServerReportMessageHandler()); } /** * 根据消息类型获取对应的处理handler * * @param msgType * @return */ public static IMessageHandler getHandlerByMsgType(int msgType) { return HANDLERS.get(msgType); }} MessageType消息类型枚举: 0102030405060708091011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283package com.freddy.chat.im; /** * @ProjectName: NettyChat * @ class name:MessageType.java * @ PackageName:com . freddy . chat .im * * @描述:消息类型 * * @author: FreddyChen * @date: 2019/04/08 00:04 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public enum MessageType { /* * 握手消息 */ HANDSHAKE(1001), /* * 心跳消息 */ HEARTBEAT(1002), /* * 客户端提交的消息接收状态报告 */ CLIENT_MSG_RECEIVED_STATUS_REPORT(1009), /* * 服务端返回的消息发送状态报告 */ SERVER_MSG_SENT_STATUS_REPORT(1010), /** * 单聊消息 */ SINGLE_CHAT(2001), /** * 群聊消息 */ GROUP_CHAT(3001); private int msgType; MessageType(int msgType) { this.msgType = msgType; } public int getMsgType() { return this.msgType; } public enum MessageContentType { /** * 文本消息 */ TEXT(101), /** * 图片消息 */ IMAGE(102), /** * 语音消息 */ VOICE(103); private int msgContentType; MessageContentType(int msgContentType) { this.msgContentType = msgContentType; } public int getMsgContentType() { return this.msgContentType; } }} IMSConnectStatusListenerIMS连接状态监听器: 0102030405060708091011121314151617181920212223242526272829package com.freddy.chat.im; import com.freddy.im.listener.IMSConnectStatusCallback; /** * @ProjectName: NettyChat * @ class name:IMSConnectStatusListener.java * @ PackageName:com . freddy . chat .im * * @描述:类描述 * * @author: FreddyChen * @date: 2019/04/08 00:31 * @ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL] */public class IMSConnectStatusListener implements IMSConnectStatusCallback { @Override public void onConnecting() { } @Override public void onConnected() { } @Override public void onConnectFailed() { }} 由于每个人代码风格不同,封装代码都有自己的思路,所以,在此就不过多讲解,只是把自己简单封装的代码全部贴上来,作一个参考即可。 只需要知道,接收到消息时,会回调OnEventListener的dispatchMsg(MessageProtobuf.Msg msg)方法: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_17-1.jpg 发送消息需要调用imsClient的sendMsg(MessageProtobuf.Msg msg)方法: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_17-2.jpg 即可,至于怎样去封装得更好,大家自由发挥吧。由于代码较多,这里就不一一贴出来了,请自行从github下载完整工程源码:https://github.com/52im/NettyChat 18、最终运行 运行一下,看看效果吧: 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_16a42c85e653b88c11aaa.gif 运行步骤是: 1)首先,启动服务端。 2)然后,修改客户端连接的ip地址为192.168.0.105(这是我本机的ip地址),端口号为8855,fromId,也就是userId,定义成100001,toId为100002,启动客户端A。 3)再然后,fromId,也就是userId,定义成100002,toId为100001,启动客户端B。 4)客户端A给客户端B发送消息,可以看到在客户端B的下面,已经接收到了消息。 5)用客户端B给客户端A发送消息,也可以看到在客户端A的下面,也已经接收到了消息。 至于,消息收发测试成功。至于群聊或重连等功能,就不一一演示了,还是那句话,下载demo体验一下吧:https://github.com/52im/NettyChat。 由于gif录制体积较大,所以只能简单演示一下消息收发,具体下载demo体验吧。如果有需要应用层UI实现(就是聊天页及会话页的封装)的话,我再分享出来吧。 19、写在最后 终于写完了,这篇文章大概写了10天左右,有很大部分的原因是自己有拖延症,每次写完一小段,总静不下心来写下去,导致一直拖到现在,以后得改改。第一次写技术分享文章,有很多地方也许逻辑不太清晰,由于篇幅有限,也只是贴了部分代码,建议大家把源码下载下来看看。一直想写这篇文章,以前在网上也尝试过找过很多im方面的文章,都找不到一篇比较完善的,本文谈不上完善,但包含的模块很多,希望起到一个抛砖引玉的作用,也期待着大家跟我一起发现更多的问题并完善,最后,如果这篇文章对你有用,希望在github上给我一个star哈。。。 应大家要求,精简了netty-all-4.1.33.Final.jar包,原netty-all-4.1.33.Final.jar包大小为3.9M。 经测试发现目前im_lib库只需要用到以下jar包: netty-buffer-4.1.33.Final.jar netty-codec-4.1.33.Final.jar netty-common-4.1.33.Final.jar netty-handler-4.1.33.Final.jar netty-resolver-4.1.33.Final.jar netty-transport-4.1.33.Final.jar 所以,抽取以上jar包,重新打成了netty-tcp-4.1.33-1.0.jar(已经上传到github工程了),目前自测没有问题,如果发现bug,请告诉我,谢谢。
即时通讯
即时通 讯软件是经过即时通讯技能来贯彻在线谈天说地、 交流的软 件,现阶段礼仪之邦最盛行的 有QQ、MSN、POPO、UC、等,而域外重在使用ICQ、MSN。 即时通讯(Instant ):经过即时通讯效能,你何尝不可分晓你的四座宾朋是否正值线上,及与他俩即时通讯。即时通讯比传递电子邮件所需岁时更短,并且比拨电话更便宜,属实是大网年间最有利于的通讯道道儿。 即时通讯(Instant ,古称IM)是一个终端服务,许两人或多人使唤网路立时的传送文字新闻、档案、语音与视频交流。即时通讯是一个极点连往一个即时通讯网路的劳务。即时通讯两样於e-mail在於它的攀谈是立即的。大多数的即时通讯服务提供了presence 的特色——显得联络人名单,联络人是不是在缐上与能否与联络人搭腔。 在最初的即时通讯程式中,使用者输入的每一个字元都会应时来得在两者的萤幕,且每一个字元的刨除与修定通都大邑立刻的反响在萤幕上。这种模式较之行使e-mail更像是机子叙谈。在现在时的即时通讯程式中,搭腔中的另一方通常只会在本地端按下送出键(Enter或是Ctrl+Enter)后才会观展新闻。 在网际网路上受欢迎的即时通讯劳务涵盖了MSN 、AOL Instant 、Yahoo! 、NET Service、Jabber、ICQ与QQ。 这些服务有赖於浩繁千方百计更久的(与常见)的缐上闲扯媒婆,如Internet Relay Chat一码事资深。 1970年代最初,一种更早的即时通讯形式是柏拉图系统(PLATO system)。此后在1980年间,UNIX/Linux的交谈旋踵谍报被科普的应用於总工程师与文化界,1990年间即时通讯更超过了网际网路交流。1996年11月,ICQ是首个周遍被非UNIX/Linux使用者用於网际网路的即时通讯软体。在ICQ的引见日后,并且在游人如织地方有定准数量的即时通讯法门进化,且各式的即时通讯程式有单独的签订,鞭长莫及相互之间互通。这引导使用者再就是履行两个之上的即时通讯软体,还是他俩方可施用帮忙多商定的终极软体,如Gaim、Trillian或Jabber。 最近,过剩即时通讯劳务开班提供视讯议会的效应,网络电话(VoIP),与网路会议劳务从头粘连为富有影像议会与顿然快讯的效应。於是,这些传媒的独家变的益发混淆。 即时通讯软体最早的即时通讯软体是ICQ,ICQ是英文中I seek you的重音,意思是我找你。四名以色列青年于1996年7月确立店家,并在11月份发布了前期的ICQ版本,在六个月内有85万用户注册利用。 早期的ICQ很不安生,虽说,还是屡遭千夫的迎接,雅虎也推出Yahoo! pager,美国在线也将保有即时通讯职能的AOL封装在Netscape ,往后摩托罗拉更将Windows 内建於 Windows XP作业系统中。 腾讯公司盛产的腾讯QQ也长足成为中原最大的即时消息软件。 即时消息软件也面临着互联互通、免费或收款题目的添麻烦。 即时通讯家什是免费的,收费很难办成的。不收款有人用,收贷就很是少人用了。貌似的都是诱导式的收费道道儿的。 Hi、MSN、QQ、FastMsg、UC等 QQ MSN 庄重圈圈 我就知底这么着多.
产品问答
酷信即时通讯有哪些服务
共3个回答来自用户 0whm0
酷信即时通讯(KuXin Instant Messaging)主要提供以下几种服务: 1. 即时消息传递:酷信即时通讯允许用户实时发送和接收文本、图片、视频等多种形式的消息,使得用户可以快速方便地进行聊天交流。 2. 群组聊天:酷信即时通讯支持创建和加入群组,用户可以与多个人同时进行群组聊天,方便团队协作和群体交流。 3. 语音通话:酷信即时通讯提供语音通话功能,用户可以通过网络实现高质量的语音通讯,免去了通话费用。 4. 视频通话:除了语音通话,酷信即时通讯还支持视频通话,用户可以通过网络实时进行面对面的视频沟通,增加沟通效率和互动性。 5. 文件传输:酷信即时通讯允许用户传输和接收各种文件,包括文档、图片、音频、视频等,方便用户之间的文件共享。 6. 离线消息:如果用户不在线或者应用未打开,酷信即时通讯可以将未读消息保存并在用户重新登录时进行推送,确保消息不会丢失。 7. 安全与隐私:酷信即时通讯注重用户数据的保护,采用安全的加密技术和隐私权限控制,保障用户的通讯内容和个人信息的安全。 总的来说,酷信即时通讯提供了全面的即时通讯服务,包括消息传递、群组聊天、语音通话、视频通话、文件传输等功能,满足用户的不同交流需求。
来自用户 BdR3HZ
酷信即时通讯提供了以下几项服务: 1. 即时消息传递:用户可以通过酷信发送即时消息,与其他用户进行实时的文字交流。 2. 语音通话:酷信支持语音通话功能,用户可以进行高质量的语音通话。 3. 视频通话:酷信还支持视频通话功能,用户可以进行面对面的实时视频交流。 4. 群组聊天:用户可以创建群组,在群组中进行多人聊天,并分享文档、图片等多媒体文件。 5. 文件传输:酷信支持文件传输功能,用户可以通过酷信发送和接收各种类型的文件,如文档、图片、音频等。 6. 表情和贴纸:酷信提供了丰富多样的表情和贴纸,用户可以在聊天中使用表情和贴纸来丰富交流内容。 7. 加密保护:酷信采用了端到端加密技术,确保用户的消息和通话内容在传输过程中得到保护,防止信息被窃听。 总之,酷信即时通讯提供了综合的即时通讯服务,满足用户之间的实时交流和沟通需求。
来自用户 Mazt6oI
酷信即时通讯提供了以下服务: 1. 即时消息传递:酷信即时通讯允许用户通过文字、图片、语音等方式发送即时消息。用户可以与其他用户或群组进行单聊或群聊。 2. 视频通话:酷信即时通讯支持视频通话功能,用户可以与其他用户进行实时的视频通话,实现面对面的沟通。 3. 文件传输:酷信即时通讯允许用户在聊天过程中发送和接收各种类型的文件,如图片、音频、视频、文档等。 4. 表情和贴纸:酷信即时通讯提供了丰富的表情和贴纸选择,用户可以通过表情和贴纸来丰富聊天内容,增加趣味性。 5. 群组功能:酷信即时通讯支持创建和管理群组,用户可以邀请其他用户加入群组进行多人聊天和协作。 6. 提醒和通知:酷信即时通讯提供了消息提醒和通知功能,用户可以设置接收消息时的提醒方式,如声音、震动等。 7. 安全性和隐私保护:酷信即时通讯采用了高级的加密技术,确保消息的安全传输。同时,用户可以根据自己的需求设置聊天的隐私保护级别。 总之,酷信即时通讯提供了全面的即时通讯服务,满足了用户在沟通中的各种需求。
酷信即时通讯评价怎么样
共3个回答酷信即时通讯怎么用
共3个回答商务咨询
运营咨询
电话沟通