1、前言
-
本文会用实例的方式,将iOS各种IM的方案都简单的实现一遍。并且提供一些选型、实现细节以及优化的建议。
-
注:文中的所有的代码示例,在github中都有demo: iOS即时通讯,从入门到“放弃”?(demo) 可以打开项目先预览效果,对照着进行阅读。
言归正传,首先我们来总结一下我们去实现IM的方式
第一种方式,使用第三方IM服务
对于短平快的公司,完全可以采用第三方SDK来实现。国内IM的第三方服务商有很多,类似云信、环信、融云、LeanCloud,当然还有其它的很多,这里就不一一举例了,感兴趣的小伙伴可以自行查阅下。
-
第三方服务商IM底层协议基本上都是
TCP
。他们的IM方案很成熟,有了它们,我们甚至不需要自己去搭建IM后台,什么都不需要去考虑。 如果你足够懒,甚至连UI都不需要自己做,这些第三方有各自一套IM的UI,拿来就可以直接用。真可谓3分钟集成... -
但是缺点也很明显,定制化程度太高,很多东西我们不可控。当然还有一个最最重要的一点,就是太贵了...作为真正社交为主打的APP,仅此一点,就足以让我们望而却步。当然,如果IM对于APP只是一个辅助功能,那么用第三方服务也无可厚非。
另外一种方式,我们自己去实现
我们自己去实现也有很多选择: 1)首先面临的就是传输协议的选择,TCP
还是UDP
? 2)其次是我们需要去选择使用哪种聊天协议:
-
基于
Scoket
或者WebScoket
或者其他的私有协议、 -
MQTT
-
还是广为人诟病的
XMPP
?
3)我们是自己去基于OS
底层Socket
进行封装还是在第三方框架的基础上进行封装? 4)传输数据的格式,我们是用Json
、还是XML
、还是谷歌推出的ProtocolBuffer
? 5)我们还有一些细节问题需要考虑,例如TCP的长连接如何保持,心跳机制,Qos机制,重连机制等等...当然,除此之外,我们还有一些安全问题需要考虑。
2、传输协议的选择
接下来我们可能需要自己考虑去实现IM,首先从传输层协议来说,我们有两种选择:TCP
or UDP
?
这个问题已经被讨论过无数次了,对深层次的细节感兴趣的朋友可以看看这篇文章:
这里我们直接说结论吧:对于小公司或者技术不那么成熟的公司,IM一定要用TCP
来实现,因为如果你要用UDP
的话,需要做的事太多。当然QQ就是用的UDP
协议,当然不仅仅是UDP
,腾讯还用了自己的私有协议,来保证了传输的可靠性,杜绝了UDP下各种数据丢包,乱序等等一系列问题。 总之一句话,如果你觉得团队技术很成熟,那么你用UDP
也行,否则还是用TCP
为好。
3、我们来看看各种聊天协议
首先我们以实现方式来切入,基本上有以下四种实现方式:
-
基于
Scoket
原生:代表框架CocoaAsyncSocket
。 -
基于
WebScoket
:代表框架SocketRocket
。 -
基于
MQTT
:代表框架MQTTKit
。 -
基于
XMPP
:代表框架XMPPFramework
。
当然,以上四种方式我们都可以不使用第三方框架,直接基于OS
底层Scoket
去实现我们的自定义封装。下面我会给出一个基于Scoket
原生而不使用框架的例子,供大家参考一下。
首先需要搞清楚的是,其中MQTT
和XMPP
为聊天协议,它们是最上层的协议,而WebScoket
是传输通讯协议,它是基于Socket
封装的一个协议。而通常我们所说的腾讯IM的私有协议,就是基于WebScoket
或者Scoket
原生进行封装的一个聊天协议。
具体这3种聊天协议的对比优劣如下:
协议优劣对比.png
所以说到底,iOS要做一个真正的IM产品,一般都是基于Scoket
或者WebScoket
等,再之上加上一些私有协议来保证的。
3.1 我们先不使用任何框架,直接用OS
底层Socket
来实现一个简单的IM。
我们客户端的实现思路也是很简单,创建Socket
,和服务器的Socket
对接上,然后开始传输数据就可以了。
-
我们学过c/c++或者java这些语言,我们就知道,往往任何教程,最后一章都是讲
Socket
编程,而Socket
是什么呢,简单的来说,就是我们使用TCP/IP
或者UDP/IP
协议的一组编程接口。如下图所示:
我们在应用层,使用socket
,轻易的实现了进程之间的通信(跨网络的)。想想,如果没有socket
,我们要直面TCP/IP
协议,我们需要去写多少繁琐而又重复的代码。
如果有对socket
概念仍然有所困惑的,可以看看这篇文章: 从问题看本质,socket到底是什么?。 但是这篇文章关于并发连接数的认识是错误的,正确的认识可以看看这篇文章: 单台服务器并发TCP连接数到底可以有多少
我们接着可以开始着手去实现IM了,首先我们不基于任何框架,直接去调用OS
底层-基于C的BSD Socket
去实现,它提供了这样一组接口:
//socket 创建并初始化 socket,返回该 socket 的文件描述符,如果描述符为 -1 表示创建失败。
int socket(int addressFamily, int type,int protocol)
//关闭socket连接
int close(int socketFileDescriptor)
//将 socket 与特定主机地址与端口号绑定,成功绑定返回0,失败返回 -1。
int bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength)
//接受客户端连接请求并将客户端的网络地址信息保存到 clientAddress 中。
int accept(int socketFileDescriptor,sockaddr *clientAddress, int clientAddressStructLength)
//客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
int connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength)
//使用 DNS 查找特定主机名字对应的 IP 地址。如果找不到对应的 IP 地址则返回 NULL。
hostent* gethostbyname(char *hostname)
//通过 socket 发送数据,发送成功返回成功发送的字节数,否则返回 -1。
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)
//从 socket 中读取数据,读取成功返回成功读取的字节数,否则返回 -1。
int receive(int socketFileDescriptor,char *buffer, int bufferLength, int flags)
//通过UDP socket 发送数据到特定的网络地址,发送成功返回成功发送的字节数,否则返回 -1。
int sendto(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)
//从UDP socket 中读取数据,并保存发送者的网络地址信息,读取成功返回成功读取的字节数,否则返回 -1 。
int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)
让我们可以对socket进行各种操作,首先我们来用它写个客户端。总结一下,简单的IM客户端需要做如下4件事:
-
客户端调用 socket(...) 创建socket;
-
客户端调用 connect(...) 向服务器发起连接请求以建立连接;
-
客户端与服务器建立连接之后,就可以通过send(...)/receive(...)向客户端发送或从客户端接收数据;
-
客户端调用 close 关闭 socket;
根据上面4条大纲,我们封装了一个名为TYHSocketManager
的单例,来对socket
相关方法进行调用:
TYHSocketManager.h
#import <Foundation/Foundation.h>
@interface TYHSocketManager : NSObject
+ (instancetype)share;
- (void)connect;
- (void)disConnect;
- (void)sendMsg:(NSString *)msg;
@end
TYHSocketManager.m
#import "TYHSocketManager.h"
#import <sys/types.h>
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
@interface TYHSocketManager()
@property (nonatomic,assign)int clientScoket;
@end
@implementation TYHSocketManager
+ (instancetype)share
{
static dispatch_once_t onceToken;
static TYHSocketManager *instance = nil;
dispatch_once(&onceToken, ^{
instance = [[self alloc]init];
[instance initScoket];
[instance pullMsg];
});
return instance;
}
- (void)initScoket
{
//每次连接前,先断开连接
if (_clientScoket != 0) {
[self disConnect];
_clientScoket = 0;
}
//创建客户端socket
_clientScoket = CreateClinetSocket();
//服务器Ip
const char * server_ip="127.0.0.1";
//服务器端口
short server_port=6969;
//等于0说明连接失败
if (ConnectionToServer(_clientScoket,server_ip, server_port)==0) {
printf("Connect to server error\n");
return ;
}
//走到这说明连接成功
printf("Connect to server ok\n");
}
static int CreateClinetSocket()
{
int ClinetSocket = 0;
//创建一个socket,返回值为Int。(注scoket其实就是Int类型)
//第一个参数addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
//第二个参数 type 表示 socket 的类型,通常是流stream(SOCK_STREAM) 或数据报文datagram(SOCK_DGRAM)
//第三个参数 protocol 参数通常设置为0,以便让系统自动为选择我们合适的协议,对于 stream socket 来说会是 TCP 协议(IPPROTO_TCP),而对于 datagram来说会是 UDP 协议(IPPROTO_UDP)。
ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);
return ClinetSocket;
}
static int ConnectionToServer(int client_socket,const char * server_ip,unsigned short port)
{
//生成一个sockaddr_in类型结构体
struct sockaddr_in sAddr={0};
sAddr.sin_len=sizeof(sAddr);
//设置IPv4
sAddr.sin_family=AF_INET;
//inet_aton是一个改进的方法来将一个字符串IP地址转换为一个32位的网络序列IP地址
//如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零。
inet_aton(server_ip, &sAddr.sin_addr);
//htons是将整型变量从主机字节顺序转变成网络字节顺序,赋值端口号
sAddr.sin_port=htons(port);
//用scoket和服务端地址,发起连接。
//客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
//注意:该接口调用会阻塞当前线程,直到服务器返回。
if (connect(client_socket, (struct sockaddr *)&sAddr, sizeof(sAddr))==0) {
return client_socket;
}
return 0;
}
#pragma mark - 新线程来接收消息
- (void)pullMsg
{
NSThread *thread = [[NSThread allo