iOS即时通讯,从入门到“放弃”?

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、我们来看看各种聊天协议

首先我们以实现方式来切入,基本上有以下四种实现方式:

  1. 基于Scoket原生:代表框架 CocoaAsyncSocket

  2. 基于WebScoket:代表框架 SocketRocket

  3. 基于MQTT:代表框架 MQTTKit

  4. 基于XMPP:代表框架 XMPPFramework

当然,以上四种方式我们都可以不使用第三方框架,直接基于OS底层Scoket去实现我们的自定义封装。下面我会给出一个基于Scoket原生而不使用框架的例子,供大家参考一下。

首先需要搞清楚的是,其中MQTTXMPP为聊天协议,它们是最上层的协议,而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件事:

  1. 客户端调用 socket(...) 创建socket;

  2. 客户端调用 connect(...) 向服务器发起连接请求以建立连接;

  3. 客户端与服务器建立连接之后,就可以通过send(...)/receive(...)向客户端发送或从客户端接收数据;

  4. 客户端调用 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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值