通讯协议与即时通讯

转自:https://www.jianshu.com/p/ca4aeabf55f6

推送服务一般有三种实现方式:

1.轮询方式

客户端不断的查询服务器,检索新内容。这种方式的缺点十分明显,如果轮询频率过快,会大量消耗网络带宽和电池;

2.长连接方式

客户端和服务端维持一条TCP/IP长连接,服务端向客户端push数据。这种方式可以避免轮询方式带来的性能问题,但是长连接依然会带来耗能问题。目前苹果的APNS和谷歌的GCM都是基于此方案来实现推送服务的;

3.SMS方式

当服务端有新内容的时候,会发送一条类似短信的指令传给客户端,客户端收到后从服务端下载新内容。由于运营商并没有免费开放这种指令,使用需要向运营商缴纳部分费用,所以并没有大量运用起来,但是这种方式非常的高效和及时。

主流推送方案应用比较

1.APNS(Apple Push Notification Service)和GCM(Google Cloud Messaging)

APNS和GCM是iOS和Android两大阵营提出的官方推送方案,这两者的技术架构较为相似。都是由系统来统一的维护一个长连接,所有的APP统一发送心跳和接收推送。

APNS使用的方便性毋庸置疑,但是GCM却在国内举步维艰,具体原因有以下三个:

1)Google与我国政府交恶,导致GMS(Google Mobile Service)在国内无法正常使用,而GCM是依赖于GMS的,所以无法顺利使用。

2)由于国内2G和移动3G的NAT超时时间都小于GCM心跳时间(28分钟),TCP长连接必然无法保活,每次都要等28分钟心跳失败重连后才能收到Push。

3)某些运营商可能限制了5228端口,移动3G/2G下,发现几乎无法连接上GCM服务器,也就无法获得GCM通知,WhatsApp放后台10分钟后,经常很长时间都收不到Push消息。

2.XMPP

XMPP是一种基于标准通用标记语言的子集XML的协议,它继承了在XML环境中灵活的发展性。因此,基于XMPP的应用具有超强的可扩展性。经过扩展以后的XMPP可以通过发送扩展的信息来处理用户的需求,以及在XMPP的顶端建立如内容发布系统和基于地址的服务等应用程序。而且,XMPP包含了针对服务器端的软件协议,使之能与另一个进行通话,这使得开发者更容易建立客户应用程序或给一个配好系统添加功能。

XMPP的优点是:协议成熟,强大,可扩展性强,并且有成熟的开源方案。
XMPP的缺点是:信息冗余量大(信息的格式是 XML),因而费流量,费电。

3.MQTT

MQTT全称叫做Message Queuing Telemetry Transport,意为消息队列遥测传输,是IBM开发的一个即时通讯协议。由于其维护一个长连接以轻量级低消耗著称,所以常用于移动端消息推送服务开发。

MQTT的优点是:协议简洁轻巧,数据冗余量低。并且支持的设备从智能硬件到智能手机无所不包。
MQTT的缺点是:服务器端实现难度大,虽然已经有了C++版本的服务端组件,但是并不开源。而且在推送数量较大时如何处理并发是十分考验后台人员的技术水平的。

MQTT具有如下特性:

  • 使用发布/订阅消息模式,提供一对多消息发布;
  • 对负载内容屏蔽的消息传输;
  • 使用TCP/IP进行网络连接;

主流的MQTT是基于TCP进行连接的,同样也有UDP版本的MQTT,但是不太常用,叫做MQTT-SN。

  • 具有三种消息发布服务质量选项;
    1.“至多一次”,通常app的推送使用的就是这种模式。也就是说,如果移动设备在消息推送的时候没有联网,那么再次联网就不会收到通知了;
    2.“至少一次”,可以确保消息收到,但消息可能会重复;
    3.“只有一次”,确保消息到达一次,比如计费系统, 如果出现消息重复或者丢失会导致系统结果不正确的问题。
  • 小型传输,开销很小(固定长度的头部是2字节),协议交换最小化,以降低网络流量;
    这就是为什么MQTT能以轻量级低消耗著称,所以MQTT特别适用于低开销、低宽带占用的即时通讯场景。
  • 通知有关各方客户端异常中断的机制。

MQTT协议实现方式


1492764064707604.png

在MQTT协议中有三种身份:

  • 发布者(Publish)。发布者其实是客户端,可以进行发布消息;
  • 代理(Broker)。代理指的是服务器,比较有名的是eqmtt,当前,你也可以用其他成熟的框架去搭建MQTT服务;
  • 订阅者(Subscribe)。一般指的是客户端,不过,发布者同时也可以是订阅者。

MQTT客户端
一般来说,客户端可以实现一下功能:

  • 给其他客户端发布订阅的信息;
  • 订阅其他客户端发布的信息;
  • 退订和订阅主题;
  • 断开服务器连接。

MQTT服务端
MQTT服务端也称为消息代理,经常你会听到broker这个词。它可以实现一下功能:

  • 接收来自客户端的网络连接;
  • 接受客户发布的应用信息;
  • 处理来自客户端主题订阅和退订请求;
  • 向订阅的客户端转发应用程序消息。

MQTT协议中的方法
MQTT和HTTP一样,也定义了一些动作,来表示对确定资源进行操作。

  • Connect,等待于服务器建立连接;
  • Disconnect,等待客户端完成所做的工作,并与服务器断开TCP/IP会话;
  • Subscribe,主题订阅;
  • UnSubscribe,主题取消订阅;
  • Publish,发送消息。

4.HTTP轮询

HTTP轮询就是在一个给定的时间间隔后,定时向服务器发送请求,查看是否有新的数据。

HTTP轮询的优点是:实现简单、可控性强,部署硬件成本低。
HTTP轮询的缺点是:实时性差,只有时间到了才会向服务器查看是否有新的数据。两次请求之间的时间间隔过大,则失去了即时推送的意义。但如果设置的时间间隔较短的,又会费电费流量。

5.第三方推送

在推送这一分支领域有许许多多的第三方推送服务,例如:极光,个推等。

优点是:集成方便。
缺点是:大量推送数据后,付费服务是在所难免。而且因为是通用共享云,所以你的服务质量是否有保证,也就不能要求太多了,必竟你一毛钱也没出或者也不打算出。

IM实现

第一种方式,使用第三方IM服务
国内IM的第三方服务商有很多,类似云信、环信、融云、LeanCloud

  • 第三方服务商IM底层协议基本上都是TCP。他们的IM方案很成熟,有了它们,我们甚至不需要自己去搭建IM后台,什么都不需要去考虑。
    如果你足够懒,甚至连UI都不需要自己做,这些第三方有各自一套IM的UI,拿来就可以直接用。真可谓3分钟集成...
  • 但是缺点也很明显,定制化程度太高,很多东西我们不可控。当然还有一个最最重要的一点,就是太贵了...作为真正社交为主打的APP,仅此一点,就足以让我们望而却步。

另外一种方式,我们自己去实现
我们自己去实现也有很多选择:
1)首先面临的就是传输协议的选择,TCP还是UDP?
结论吧:对于小公司或者技术不那么成熟的公司,IM一定要用TCP来实现,因为如果你要用UDP的话,需要做的事太多。当然QQ就是用的UDP协议,当然不仅仅是UDP,腾讯还用了自己的私有协议,来保证了传输的可靠性,杜绝了UDP下各种数据丢包,乱序等等一系列问题。

2)其次是我们需要去选择使用哪种聊天协议:
基于Scoket或者WebScoket或者其他的私有协议、
MQTT
还是广为人诟病的XMPP?

基于Scoket原生:代表框架 CocoaAsyncSocket。
基于WebScoket:代表框架 SocketRocket。
基于MQTT:代表框架 MQTTKit。
基于XMPP:代表框架 XMPPFramework。

其中MQTT和XMPP为聊天协议,它们是最上层的协议,而WebScoket是传输通讯协议,它是基于Socket封装的一个协议。而通常我们所说的腾讯IM的私有协议,就是基于WebScoket或者Scoket原生进行封装的一个聊天协议。
说到底,iOS要做一个真正的IM产品,一般都是基于Scoket或者WebScoket等,再之上加上一些私有协议来保证的

3)我们是自己去基于OS底层Socket进行封装还是在第三方框架的基础上进行封装?

4)传输数据的格式,我们是用Json、还是XML、还是谷歌推出的ProtocolBuffer?
使用 ProtocolBuffer 减少 Payload
滴滴打车40%;
携程之前分享过,说是采用新的Protocol Buffer数据格式+Gzip压缩后的Payload大小降低了15%-45%。数据序列化耗时下降了80%-90%。
采用高效安全的私有协议,支持长连接的复用,稳定省电省流量
【高效】提高网络请求成功率,消息体越大,失败几率随之增加。
【省流量】流量消耗极少,省流量。一条消息数据用Protobuf序列化后的大小是 JSON 的1/10、XML格式的1/20、是二进制序列化的1/10。同 XML 相比, Protobuf 性能优势明显。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。
【省电】省电
【高效心跳包】同时心跳包协议对IM的电量和流量影响很大,对心跳包协议上进行了极简设计:仅 1 Byte 。
【易于使用】开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持java、c++、python、Objective-C等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。语言支持:原生支持c++、java、python、Objective-C等多达10余种语言。 2015-08-27 Protocol Buffers v3.0.0-beta-1中发布了Objective-C(Alpha)版本, 2016-07-28 3.0 Protocol Buffers v3.0.0正式版发布,正式支持 Objective-C。
【可靠】微信和手机 QQ 这样的主流 IM 应用也早已在使用它(采用的是改造过的Protobuf协议)

5)我们还有一些细节问题需要考虑,例如TCP的长连接如何保持,心跳机制,Qos机制,重连机制等等...当然,除此之外,我们还有一些安全问题需要考虑。

1.我们先不使用任何框架,直接用OS底层Socket来实现一个简单的IM。

我们客户端的实现思路也是很简单,创建Socket,和服务器的Socket对接上,然后开始传输数据就可以了。

Socket编程,而Socket是什么呢,简单的来说,就是我们使用TCP/IP 或者UDP/IP协议的一组编程接口。Socket是网络上运行的两个程序间双向通讯的一端,它既可以接受请求,也可以发送请求,利用它可以较为方便的编写网络上数据的传递。
1.socket与进程的关系
1).socket与进程间的关系:socket 用来让一个进程和其他的进程互通信息(IPC),而Socket接口是TCP/IP网络的API接口函数。
2).进程间通信(本机内)
进程间通信(不同计算机,要联网)

2、socket与文件的关系——如何理解socket是种特殊的I/O?
1)Socket最先应用于Unix操作系统,如果了解Unix系统的I/O的话,就很容易了解Socket了,因为Socket数据传输其实就是一种特殊的I/O。
2)可对其进行文件操作
3)有文件描述符。而文件描述符的本质是一个非负整数。只是用于区分。类似的还有进程ID。

首先我们不基于任何框架,直接去调用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;

服务端需要做的工作简单的总结下:
服务器调用 socket(...) 创建socket;
服务器调用 listen(...) 设置缓冲区;
服务器通过 accept(...)接受客户端请求建立连接;
服务器与客户端建立连接之后,就可以通过 send(...)/receive(...)向客户端发送或从客户端接收数据;
服务器调用 close 关闭 socket;

心跳

心跳就是用来检测TCP连接的双方是否可用。那又会有人要问了,TCP不是本身就自带一个KeepAlive机制吗?
这里我们需要说明的是TCP的KeepAlive机制只能保证连接的存在,但是并不能保证客户端以及服务端的可用性.比如会有以下一种情况:

某台服务器因为某些原因导致负载超高,CPU 100%,无法响应任何业务请求,但是使用 TCP 探针则仍旧能够确定连接状态,这就是典型的连接活着但业务提供方已死的状态。

这个时候心跳机制就起到作用了:

  • 我们客户端发起心跳Ping(一般都是客户端),假如设置在10秒后如果没有收到回调,那么说明服务器或者客户端某一方出现问题,这时候我们需要主动断开连接。
  • 服务端也是一样,会维护一个socket的心跳间隔,当约定时间内,没有收到客户端发来的心跳,我们会知道该连接已经失效,然后主动断开连接。

NAT超时

我们真正需要心跳机制的原因其实主要是在于国内运营商NAT超时。
原来这是因为IPV4引起的,我们上网很可能会处在一个NAT设备(无线路由器之类)之后。
NAT设备会在IP封包通过设备时修改源/目的IP地址. 对于家用路由器来说, 使用的是网络地址端口转换(NAPT), 它不仅改IP, 还修改TCP和UDP协议的端口号, 这样就能让内网中的设备共用同一个外网IP.
NAT设备会根据NAT表对出去和进来的数据做修改, 比如将192.168.0.3:8888发出去的封包改成120.132.92.21:9202, 外部就认为他们是在和120.132.92.21:9202通信. 同时NAT设备会将120.132.92.21:9202收到的封包的IP和端口改成192.168.0.3:8888, 再发给内网的主机, 这样内部和外部就能双向通信了, 但如果其中192.168.0.3:8888 == 120.132.92.21:9202这一映射因为某些原因被NAT设备淘汰了, 那么外部设备就无法直接与192.168.0.3:8888通信了。

我们的设备经常是处在NAT设备的后面, 比如在大学里的校园网, 查一下自己分配到的IP, 其实是内网IP, 表明我们在NAT设备后面, 如果我们在寝室再接个路由器, 那么我们发出的数据包会多经过一次NAT.
国内移动无线网络运营商在链路上一段时间内没有数据通讯后, 会淘汰NAT表中的对应项, 造成链路中断。
而国内的运营商一般NAT超时的时间为5分钟,所以通常我们心跳设置的时间间隔为3-5分钟。

PingPong机制

我们在这心跳间隔的3-5分钟如果连接假在线(例如在地铁电梯这种环境下)。那么我们岂不是无法保证消息的即时性么?这显然是我们无法接受的,所以业内的解决方案是采用双向的PingPong机制。
当服务端发出一个Ping,客户端没有在约定的时间内返回响应的ack,则认为客户端已经不在线,这时我们Server端会主动断开Scoket连接,并且改由APNS推送的方式发送消息。
同样的是,当客户端去发送一个消息,因为我们迟迟无法收到服务端的响应ack包,则表明客户端或者服务端已不在线,我们也会显示消息发送失败,并且断开Scoket连接。

还记得我们之前CocoaSyncSockt的例子所讲的获取消息超时就断开吗?其实它就是一个PingPong机制的客户端实现。我们每次可以在发送消息成功后,调用这个超时读取的方法,如果一段时间没收到服务器的响应,那么说明连接不可用,则断开Scoket连接

重连机制

理论上,我们自己主动去断开的Scoket连接(例如退出账号,APP退出到后台等等),不需要重连。其他的连接断开,我们都需要进行断线重连。
一般解决方案是尝试重连几次,如果仍旧无法重连成功,那么不再进行重连。
接下来的WebScoket的例子,我会封装一个重连时间指数级增长的一个重连方式,可以作为一个参考。

WebScoket最具代表性的一个第三方框架SocketRocket
//重连机制
- (void)reConnect
{
    [self disConnect]; // 断开连接
    //超过一分钟就不再重连 所以只会重连5次 2^5 = 64
    if (reConnectTime > 64) {
        return;
    }
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        webSocket = nil;
        [self initSocket];
    });
    //重连时间2的指数级增长
    if (reConnectTime == 0) {
        reConnectTime = 2;
    }else{
        reConnectTime *= 2;
    }
}

MQTT:

MQTT是一个聊天协议,它比webScoket更上层,属于应用层。
它的基本模式是简单的发布订阅,也就是说当一条消息发出去的时候,谁订阅了谁就会受到。其实它并不适合IM的场景,例如用来实现有些简单IM场景,却需要很大量的、复杂的处理。
比较适合它的场景为订阅发布这种模式的,例如微信的实时共享位置,滴滴的地图上小车的移动、客户端推送等功能。
首先我们来看看基于MQTT协议的框架-MQTTKit:

需要说一下的是:
1)当我们连接成功了,我们需要去订阅自己clientID的消息,这样才能收到发给自己的消息。
2)其次是这个框架为我们实现了一个QOS机制,那么什么是QOS呢?

QoS(Quality of Service,服务质量)指一个网络能够利用各种基础技术,为指定的网络通信提供更好的服务能力, 是网络的一种安全机制, 是用来解决网络延迟和阻塞等问题的一种技术。
在这里,它提供了三个选项:
typedef enum MQTTQualityOfService : NSUInteger {
AtMostOnce,
AtLeastOnce,
ExactlyOnce
} MQTTQualityOfService;
分别对应最多发送一次,至少发送一次,精确只发送一次。
QOS(0),最多发送一次:如果消息没有发送过去,那么就直接丢失。
QOS(1),至少发送一次:保证消息一定发送过去,但是发几次不确定。
QOS(2),精确只发送一次:它内部会有一个很复杂的发送机制,确保消息送到,而且只发送一次。

IM一些其它问题

1.IM的可靠性:
我们之前穿插在例子中提到过:
心跳机制、PingPong机制、断线重连机制、还有我们后面所说的QOS机制。这些被用来保证连接的可用,消息的即时与准确的送达等等。
上述内容保证了我们IM服务时的可靠性,其实我们能做的还有很多:比如我们在大文件传输的时候使用分片上传、断点续传、秒传技术等来保证文件的传输。

2.安全性:
我们通常还需要一些安全机制来保证我们IM通信安全。
例如:防止 DNS 污染、帐号安全、第三方服务器鉴权、单点登录等等

3.一些其他的优化:
类似微信,服务器不做聊天记录的存储,只在本机进行缓存,这样可以减少对服务端数据的请求,一方面减轻了服务器的压力,另一方面减少客户端流量的消耗。
我们进行http连接的时候尽量采用上层API,类似NSUrlSession。而网络框架尽量使用AFNetWorking3。因为这些上层网络请求都用的是HTTP/2 ,我们请求的时候可以复用这些连接。

音视频通话

IM应用中的实时音视频技术,几乎是IM开发中的最后一道高墙。原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的。
实时音视频技术上的实现内容主要包括:音视频的采集、编码、网络传输、解码、播放等环节。



作者:勇敢的_心_
链接:https://www.jianshu.com/p/ca4aeabf55f6
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/blueangle17/article/details/80595029