1 소개
나는 건국 기념일 전에 작년에 공유했다 불러올 수 있습니다 정기 독자 " 기술적 건조 제품은 : 처음부터 당신에게 백만 메시지 푸시 시스템을 가르 칠 수 있도록 설계 ", 나는 어떤 친구는 여전히 희망, 텍스트의 의사 코드의 일부를 게시하고 있지만 직접 소스 코드의 일부를 실행할 수 있습니다 공유 할 수 있습니다. 글쎄, 나는 가난 말할 것도 없다 의심 (정말 가난 때문에를 ..), I는 용량 라인과 절대적으로 불가능하다 코드, 그래서 큰 싸움을 끌어 준비가이 키보드를 의심 - IM 배포 자유 라인과 세트 밖으로! ^ _ ^!
-이 글은 내가 개발 학습자 용으로 설계된 IM IM 시스템 문서화 CIM 개발자가 IM의 수평 확장 할 수 있습니다 자신을 구축하는 데 도움이되는 몇 가지 구성 요소를 제공하면서, (CROSS-IM 전체 이름).
이 문서와 CIM 코드의 연구를 통해 다음과 같은 정보를 얻을 수 있습니다 :
1) 어떻게 나를 용서 용서, 조금 약함 (처음부터 CIM 고객 IM을 개발하는 방법)
2) 어떻게 IM의 분산 아키텍처를 설계하는;
3) IM 아키텍처를 배포 얼마나 코드 및 관련 기술에 그것들을 달성했다.
CIM 서 소스 주소를 지원하는이 문서 :
메인 이미지 : https://github.com/crossoverJie/cim
대체 거울 : https://github.com/52im/cim
* 팁 : 등 네트워크 프로그래밍, IM 이론, 어느 정도의 지식을 가지고 당신을 필요로이 문서와 CIM 소스를 읽고 당신이가없는 경우 "를 참조하시기 바랍니다 시작 하나가 충분하다 : 0에서 모바일 엔드 IM을 개발 " 그리고 완전히!
동기화에 발표 된이 문서 : http://www.52im.net/thread-2775-1-1.html
2, 저자 소개
![](https://upload-images.jianshu.io/upload_images/1500839-24ce3a37dc0354b3.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/173)
crossoverJie (첸 지에) : (90)는, 지금 충칭 돼지 네트워크 제한에서 정보 공학의 충칭 연구소를 졸업했다.
3, 데모를 실행
두 특별히 기록 된 비디오 프리젠 테이션 (그룹 채팅, 개인 채팅), 당신은 비디오 버전 데모를 보려면 아래 링크를 클릭 할 수 있습니다.
CIM의 속삭임 비디오 프리젠 테이션 : https://www.bilibili.com/video/av39405821
CIM 그룹 채팅 비디오 프리젠 테이션 : https://www.bilibili.com/video/av39405501
4, 건축 디자인
의는 특정 아키텍처를 살펴 보자 :
![](https://upload-images.jianshu.io/upload_images/1500839-8c7937b4372aef30.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/675)
아키텍처 설명 :
1) SpringBoot를 구성하는데 사용되는 다양한 구성 요소를 CIM;
2) 건설의 Netty + 구글 프로토콜 버퍼 하단 통신;
3) 레디 스 각 클라이언트 계정 정보, 온라인 상태의 라우팅 정보를 저장하는 단계;
4) IM-서버 서비스 및 검색에 사육사에 서명합니다.
전반적으로 다음과 같은 모듈로 구성되어 있습니다 :
1) CIM 서버 - IM 서버 : 투명 메시지, 푸시 메시지 기능 전달, 클라이언트 접속을 수신하기위한 수단. 클러스터 배포를 지원합니다;
2) CIM-미래 route-- 메시지 라우팅 서버 : 메시지 라우팅, 메시지 전달, 사용자 로그인, 사용자의 오프라인 및 일부 운영 도구 (온라인 사용자 등)을 처리하기위한 수단;
3) CIM 클라이언트 - IM 클라이언트 : 최종 사용자에게 메시지, 그리고 통신 등 (그룹 채팅, 개인 채팅)를 시작하는 명령을 시작하고, 사용하기 쉬운 일반적인 명령의 일부를 만들었습니다.
도 5를 참조하면, 논리 흐름도
전체 공정은 다음 흐름도 비교적 간단하다 :
![](https://upload-images.jianshu.io/upload_images/1500839-48fa429c59ef6d23.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/626)
프로세스는 다음과 같이 설명된다 :
클라이언트 경로를 개시에 1)의 로그;
2) 클라이언트로 사용 가능한 성공적인 로그인 IM-서버를 선택하고 저장 경로 레디 스에서 사육사에 정보를 기록;
3) IM 서버에게 클라이언트가 성공 후 심박 채 연결 길이를 개시한다;
4) 상기 단말 상태 정보를 오프라인으로 통해 클라이언트 경로를 제거한다.
그것은 다음 단계를 필요로 할 때 우리 자신의 배포 :
1) 상기 미들웨어 기준을 설정 레디 스 , 사육사는 ;
지원 레벨 만이 동일한 사육사 수를 등록 할 필요가 연장되도록 2) 배치 CIM 서버, 이것은 실제 IM 서버는 성능 요건을 충족시키기 위해,이고;
3)部署 cim-forward-route,这是路由服务器,所有的消息都需要经过它。由于它是无状态的,所以也可以利用 Nginx 代理提高可用性;
4)cim-client 真正面向用户的客户端;启动之后会自动连接 IM 服务器便可以在控制台收发消息了。
更多使用介绍可以参考快速启动。
接下来各章将重点看看具体的详细设计实现,比如群聊、私聊消息如何流转;IM 服务端负载均衡;服务如何注册发现等等。
6、IM 服务端
先来看看服务端:主要是实现客户端上下线、消息下发等功能。
首先是服务启动:
![](https://upload-images.jianshu.io/upload_images/1500839-930d0f3654d66f0a.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/482)
![](https://upload-images.jianshu.io/upload_images/1500839-969f76f98ec6d745.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/686)
由于是在 SpringBoot 中搭建的,所以在应用启动时需要启动 Netty 服务。
从 pipline 中可以看出使用了 Protobuf 的编解码(具体报文在客户端中分析,相关知识请见:《Protobuf通信协议详解:代码演示、详细原理介绍等》)。
7、注册发现
需要满足 IM 服务端的水平扩展需求,所以 cim-server 是需要将自身数据发布到注册中心的。这里参考之前分享的《搞定服务注册与发现》有具体介绍。
所以在应用启动成功后需要将自身数据注册到 Zookeeper 中:
![](https://upload-images.jianshu.io/upload_images/1500839-fdd3512a97a55146.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/721)
![](https://upload-images.jianshu.io/upload_images/1500839-70f8e57317f3eb79.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/817)
最主要的目的就是将当前应用的 ip + cim-server-port+ http-port 注册上去:
![](https://upload-images.jianshu.io/upload_images/1500839-25ac80430dea844b.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/856)
上图是我在演示环境中注册的两个 cim-server 实例(由于在一台服务器,所以只是端口不同)。这样在客户端(监听这个 Zookeeper 节点)就能实时的知道目前可用的服务信息。
8、登录
当客户端请求 cim-forward-route 中的登录接口(详见下文)做完业务验证(就相当于日常登录其他网站一样)之后,客户端会向服务端发起一个长连接。
如之前的流程所示:
![](https://upload-images.jianshu.io/upload_images/1500839-e18a69ad303dc647.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/626)
这时客户端会发送一个特殊报文,表明当前是登录信息。服务端收到后就需要将该客户端的 userID 和当前 Channel 通道关系保存起来。
![](https://upload-images.jianshu.io/upload_images/1500839-eb8983de2e6c50eb.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/780)
![](https://upload-images.jianshu.io/upload_images/1500839-3ab93f5f73e43604.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/767)
同时也缓存了用户的信息,也就是 userID 和 用户名。
9、离线消息
当客户端断线后也需要将刚才缓存的信息清除掉。
![](https://upload-images.jianshu.io/upload_images/1500839-671970bf05fecfed.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/771)
同时也需要调用 route 接口清除相关信息(具体接口看下文)。
10、IM 路由
![](https://upload-images.jianshu.io/upload_images/1500839-ccfa8b762d5a18cb.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/728)
从架构图中可以看出,路由层是非常重要的一环;它提供了一系列的 HTTP 服务承接了客户端和服务端。
目前主要是以下几个接口。
10.1 注册接口
![](https://upload-images.jianshu.io/upload_images/1500839-73a3aa3703b5ccd8.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/876)
![](https://upload-images.jianshu.io/upload_images/1500839-d2cb8d1e1041837d.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/592)
由于每一个客户端都是需要登录才能使用的,所以第一步自然是注册。
这里就设计的比较简单,直接利用 Redis 来存储用户信息;用户信息也只有 ID 和 userName 而已。只是为了方便查询在 Redis 中的 KV 又反过来存储了一份 VK,这样 ID 和 userName 都必须唯一。
10.2 登录接口
这里的登录和 cim-server 中的登录不一样,具有业务性质:
具体的流程:
1)登录成功之后需要判断是否是重复登录(一个用户只能运行一个客户端);
2)登录成功后需要从 Zookeeper 中获取服务列表(cim-server)并根据某种算法选择一台服务返回给客户端;
3)登录成功之后还需要保存路由信息,也就是当前用户分配的服务实例保存到 Redis 中。
为了实现只能一个用户登录,使用了 Redis 中的 set 来保存登录信息;利用 userID 作为 key ,重复的登录就会写入失败。
![](https://upload-images.jianshu.io/upload_images/1500839-2d72fd911ea01b1d.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/759)
![](https://upload-images.jianshu.io/upload_images/1500839-0fc1acace33dda9e.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/785)
类似于 Java 中的 HashSet,只能去重保存。
获取一台可用的路由实例也比较简单:
![](https://upload-images.jianshu.io/upload_images/1500839-ccae442488b17fda.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/570)
1)先从 Zookeeper 获取所有的服务实例做一个内部缓存;
2)轮询选择一台服务器(目前只有这一种算法,后续会新增)。
当然要获取 Zookeeper 中的服务实例前自然是需要监听 cim-server 之前注册上去的那个节点。
具体代码如下:
![](https://upload-images.jianshu.io/upload_images/1500839-86819e3b12c0f0b4.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/431)
![](https://upload-images.jianshu.io/upload_images/1500839-1a5d20454bc9c867.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/557)
![](https://upload-images.jianshu.io/upload_images/1500839-27f27f476cab89eb.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/820)
也是在应用启动之后监听 Zookeeper 中的路由节点,一旦发生变化就会更新内部缓存。这里使用的是 Guava 的 cache,它基于 ConcurrentHashMap,所以可以保证清除、新增缓存的原子性。
10.3 群聊接口
这是一个真正发消息的接口,实现的效果就是其中一个客户端发消息,其余所有客户端都能收到!流程肯定是客户端发送一条消息到服务端,服务端收到后在上文介绍的 SessionSocketHolder 中遍历所有 Channel(通道)然后下发消息即可。服务端是单机倒也可以,但现在是集群设计。所以所有的客户端会根据之前的轮询算法分配到不同的 cim-server 实例中。
因此就需要路由层来发挥作用了。
路由接口收到消息后首先遍历出所有的客户端和服务实例的关系。
路由关系在 Redis 中的存放如下:
由于 Redis 单线程的特质,当数据量大时;一旦使用 keys 匹配所有 cim-route:* 数据,会导致 Redis 不能处理其他请求。所以这里改为使用 scan 命令来遍历所有的 cim-route:*。
接着会挨个调用每个客户端所在的服务端的 HTTP 接口用于推送消息。
在 cim-server 中的实现如下:
![](https://upload-images.jianshu.io/upload_images/1500839-5686d9b8316e4863.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/772)
cim-server 收到消息后会在内部缓存中查询该 userID 的通道,接着只需要发消息即可。
10.4 在线用户接口
这是一个辅助接口,可以查询出当前在线用户信息。
![](https://upload-images.jianshu.io/upload_images/1500839-6b0ee12b2a17291f.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/703)
![](https://upload-images.jianshu.io/upload_images/1500839-b241bab01525af51.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/678)
实现也很简单,也就是查询之前保存 ”用户登录状态的那个去重 set “即可。
10.5 私聊接口
之所以说获取在线用户是一个辅助接口,其实就是用于辅助私聊使用的。一般我们使用私聊的前提肯定得知道当前哪些用户在线,接着你才会知道你要和谁进行私聊。
类似于这样:
在我们这个场景中,私聊的前提就是需要获得在线用户的 userID:
![](https://upload-images.jianshu.io/upload_images/1500839-a30541585d4450d8.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/899)
所以私聊接口在收到消息后需要查询到接收者所在的 cim-server 实例信息,后续的步骤就和群聊一致了。调用接收者所在实例的 HTTP 接口下发信息。只是群聊是遍历所有的在线用户,私聊只发送一个的区别。
10.6 下线接口
一旦客户端下线,我们就需要将之前存放在 Redis 中的一些信息删除掉(路由信息、登录状态)。
![](https://upload-images.jianshu.io/upload_images/1500839-394e2061e967d35f.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/793)
![](https://upload-images.jianshu.io/upload_images/1500839-bb593ba174d9169b.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/524)
11、IM 客户端
客户端中的一些逻辑其实在上文已经谈到一些了。
11.1 登录
第一步也就是登录,需要在启动时调用 route 的登录接口,获得 cim-server 信息再创建连接。
![](https://upload-images.jianshu.io/upload_images/1500839-9a7e0d1e84900a2a.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/569)
![](https://upload-images.jianshu.io/upload_images/1500839-146f3bc7b4ff4abc.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/782)
![](https://upload-images.jianshu.io/upload_images/1500839-0a4ce02e5a139c90.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/822)
登录过程中 route 接口会判断是否为重复登录,重复登录则会直接退出程序。
![](https://upload-images.jianshu.io/upload_images/1500839-419f7a9957c1184a.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/919)
接下来是利用 route 接口返回的 cim-server 实例信息(ip+port)创建连接。最后一步就是发送一个登录标志的信息到服务端,让它保持客户端和 Channel 的关系。
![](https://upload-images.jianshu.io/upload_images/1500839-01c356d21206f501.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1031)
11.2 自定义协议
上文提到的一些登录报文、真正的消息报文这些其实都是在我们自定义协议中可以区别出来的。由于是使用 Google Protocol Buffer 编解码,所以先看看原始格式。
![](https://upload-images.jianshu.io/upload_images/1500839-bde77e2f2d2e9494.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/561)
其实这个协议中目前一共就三个字段:
1)requestId 可以理解为 userId;
2)reqMsg 就是真正的消息;
3)type 也就是上文提到的消息类别。
目前主要是三种类型,分别对应不同的业务:
![](https://upload-images.jianshu.io/upload_images/1500839-c2ca852b3f5a611b.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/408)
11.3 心跳
为了保持客户端和服务端的连接,每隔一段时间没有发送消息都需要自动的发送心跳。
目前的策略是每隔一分钟就是发送一个心跳包到服务端:
![](https://upload-images.jianshu.io/upload_images/1500839-c09e558ce8ffa364.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/779)
![](https://upload-images.jianshu.io/upload_images/1500839-95893b8379b4c402.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/759)
这样服务端每隔一分钟没有收到业务消息时就会收到 ping 的心跳包:
![](https://upload-images.jianshu.io/upload_images/1500839-14e941e43e890d11.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/744)
11.4 内置命令
客户端也内置了一些基本命令来方便使用。
![](https://upload-images.jianshu.io/upload_images/1500839-68a811698bd7cf25.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/629)
比如输入 :q 就会退出客户端,同时会关闭一些系统资源。
![](https://upload-images.jianshu.io/upload_images/1500839-79fc8e3f2782e0b3.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/563)
![](https://upload-images.jianshu.io/upload_images/1500839-545677fd36997df4.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/927)
当输入 :olu(onlineUser 的简写)就会去调用 route 的获取所有在线用户接口。
![](https://upload-images.jianshu.io/upload_images/1500839-7d438b205c233ced.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/828)
![](https://upload-images.jianshu.io/upload_images/1500839-c6e4e94825c60976.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/940)
11.5 群聊
群聊的使用非常简单,只需要在控制台输入消息回车即可。这时会去调用 route 的群聊接口。
![](https://upload-images.jianshu.io/upload_images/1500839-61d12af188da6f92.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/746)
11.6 私聊
私聊也是同理,但前提是需要触发关键字;使用 userId;;消息内容 这样的格式才会给某个用户发送消息,所以一般都需要先使用
lu 命令获取所以在线用户才方便使用。
![](https://upload-images.jianshu.io/upload_images/1500839-0c3509ff779d7497.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/661)
11.7 消息回调
为了满足一些定制需求,比如消息需要保存之类的。所以在客户端收到消息之后会回调一个接口,在这个接口中可以自定义实现。
![](https://upload-images.jianshu.io/upload_images/1500839-5439d2bc6a22f39d.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/923)
![](https://upload-images.jianshu.io/upload_images/1500839-3ccdd9dbed25dc07.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/555)
因此先创建了一个 caller 的 bean,这个 bean 中包含了一个 CustomMsgHandleListener 接口,需要自行处理只需要实现此接口即可。
11.8 自定义界面
由于我自己不怎么会写界面,但保不准有其他大牛会写。所以客户端中的群聊、私聊、获取在线用户、消息回调等业务(以及之后的业务)都是以接口形式提供。
也方便后面做页面集成,只需要调这些接口就行了;具体实现不用怎么关心。
12、本文小结
cim 目前只是第一版,BUG 多,功能少(只拉了几个群友做了测试);不过后续还会接着完善,至少这一版会给那些没有相关经验的朋友带来一些思路。
后续计划:
![](https://img2018.cnblogs.com/blog/1834368/201910/1834368-20191014165320974-408144812.jpg)