从零开发一个即时通讯 APP

前言

这是我的毕业设计。刚开始确定这个课题的时候,是因为以前有稍微研究过一些 XMPP 协议,在这个基础上做起来应该不难。然后开始选技术的时候还有半年,我想为什么不从更底层做起呢!那就不用 XMPP。当时接触过相关的即时通讯技术还有 WebSocket,那为什么不直接从更底层的 Socket 开始封装呢?
服务端就用 Go 语言吧,用来做 IM 服务器和 HTTP 服务器都很好。

技术选型

既然是基于 Socket,iOS 端我并不准备从 C 语言的 Socket 开发封装起,而是使用一个第三方库 CocoaAsyncSocket。XMPP 的 iOS framework 也是从这个库开始封装。而 Go 语言的 IM 服务端则直接使用原生开发即可,无论是 UDP 还是 TCP 都已经封装得很好。
HTTP 服务器使用的框架是 Gin,已经相当成熟,可以用于大型服务端的开发了。
关于传输的数据格式,XMPP 使用的是 XML,但是体积太大,冗余过多不必要的数据。考虑了很久,好像也没必要自己封装二进制的数据格式,我用的是 Google 的 Protocol Buffer。HTTP 服务器还是使用 JSON。
我还需要存储客户端的 IP 地址,由于需要快速读写,我使用的是 Redis。
AccessToken 验证方式使用的是 JSON Web Token(JWT)

实现思路

我的想法是使用 UDP Socket 来传输数据。至于为什么使用 UDP 呢,一开始的想法是 UDP 比 TCP 快,虽然可能会丢包,但是可以试着优化。关于使用 UDP 来做 IM 这个想法也被一些大神喷过,但是这都是我自己的想法,就这样做着先。
使用 UDP 会丢包,所以我想需要一个回执机制。接收端收到了消息后就给发送端发送一个回执,这个回执包括这条消息的 ID,如果发送方过一段时间还没有接收到回执,则重新发送。而且这个回执还不能丢,所以我使用 TCP 来发送回执。
UDP 是无连接性的,还是要使用 TCP 来连接服务端,表明登录状态。所以 TCP 的作用是连接和发送回执。
具体思路是当客户端登录和重新连接的时候,客户端使用 UDP Socket 绑定端口,然后使用 TCP Socket 来发送 UDP 地址给服务端。服务端把用户的 ID 和 UDP 地址存进 Redis,等发送方发送的消息包含接收端的用户 ID,服务端再从 Redis 取出接收方的 UDP 地址进行转发。
发送图片我是这样实现的,我会把图片上传到七牛云,发送图片的 URL,接收端只需要使用 URL 来加载图片即可。

简单封装一个通讯协议

就叫简单的即时通讯协议,Simple Instant Messaging Protocol,简称 SIMP。
我想的是基于连接的,所以一个用户对应一个 SIMPConnection,每一个 SIMPConnection 是一个单例,使用代理进行回调。

1
- (BOOL)connectionToRemoteHost:(NSString *)host port:(NSInteger)port forUser:(NSString *)userID;

连接需要用户 ID、服务器地址和端口。
在连接的时候就创建 TCP 和 UDP Socket 进行连接,TCP Socket 要发送连接的数据,包括 UDP Socket 的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (BOOL)connectionToRemoteHost:(NSString *)host port:(NSInteger)port forUser:(NSString *)userID {
self.host = host;
self.port = port;
self.userID = userID;
self.tcpSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)];
self.udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
return [self connect];
}

- (BOOL)connect {
NSError *error;
BOOL tcpSuccess = [self.tcpSocket connectToHost:self.host onPort:self.port error:&error];
CheckError(@"TCPSocketConnectToHost", &error);


BOOL udpSuccess = [self.udpSocket connectToHost:self.host onPort:self.port + 1 error:&error];

CheckError(@"UDPSocketConnectToHost", &error);
[self.udpSocket beginReceiving:&error];
CheckError(@"beginReceiving", &error);

[self sendConnectData];
return tcpSuccess && udpSuccess;
}

还有封装一个 SIMPMessage,里面包含 protobuf 的数据。
我的 protobuf 数据是这样的:版本、消息 ID、时间、文字内容、图片 URL、发送方 ID、接收方 ID、消息类型、图片比例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";
message Message {
float version = 1;
uint64 messageId = 2;
uint64 time = 3;
string content = 4;
string imageURL = 5;
string fromUser = 6;
string toUser = 7;
MessageType type = 8;
float imageScale = 9;
enum MessageType {
TEXT = 0;
IMAGE = 1;
AUDIO = 2;
CONNECT = 3;
RECEIPT = 4;
}
}

还有消息队列、群聊等一些我已经有想法但是还没实现的功能。
架构
关于整个APP的流程如下

关于 iOS 端,使用了 MVVM 设计模式结合 RAC,在 Controller 里面只需要组合一下视图和布局,绑定数据即可,把处理数据和大部分逻辑都放在了 ViewModel 里面,结构还算清晰。
关于数据管理,我使用了一个 Redux 思想的全局数据调度中心,实现了单向数据流、数据持久化等。数据持久化用到了 FMDB。但是大部分代码是一个大神写的,很屌。

效果和下一步

目前实现了传输文字和图片,好友添加还是在后台添加(前端还没做),动态模块等。

登录

通讯录

详细资料

个人资料

聊天界面

Demo

先上传到了 GitHub,目前功能还不完善,还会持续开发。
https://github.com/AscenZ/Hey