May 2, 2018

Telegram网络层源码分析

最近看了一下Telegram网络层的源码,本来想网上找一下现成的结论,降低一点学习成本,但是并没有发现相关的资料。于是自己梳理了一下telegram是如何发送网络请求和响应回包的,这里做个总结。

连接的建立

先上一张图:
ConnectionsManager
这张图描述了telegram客户端和server的连接时如何建立的。Java层的ConnectionsManager是一个线程安全的单例,其实只是个wrapper,真正的逻辑都是转交给C++层的ConnectionsManager类处理的。 C++层的ConnectionsManager对象,在TgNetWrapper.cpp中初始化,也同样是个单例。这里贴一下C++层ConnectionsManager类的init方法:

这里有两个关键的方法调用,一个是loadConfig(),一个是pthread_create()。顺着时间线往下看,loadConfig()调用了initDataCenter()方法,这个方法实现如下:

可以看到,主要是一些hardcode的IP和端口,其实在连接上第一个数据中心之后,客户端就会被分发到最优的数据中心接入,这里写这么多,可以看作一种fallback逻辑。
接下来使用了一个pthread_create的调用,新的线程中会执行ThreadProc()方法,这个方法会先调用sendPing(),实现如下:

这里调用了DataCenter的getPushConnection()和getGenericConnection()方法,这里把connection类型区分开的本意是下载,推送等使用不同的连接,这样可以防止一些耗时任务一直占用连接,同一个数据中心,可以给不同类型的连接分配不同的IP和端口,来提高网络连接的效率。
接下来是一个循环,循环中不断调用select()方法,来处理请求,这个方法的实现如下:

这里有一个epoll_wait()的调用,直觉上来说,这就是处理网络请求的核心,所以我们也从这里开始分析,先看一下这个epolFd上注册的fd:

这里分析一下,epolFd上注册的fd一共有两类:

  1. socket。这里可以看到,socket连接在创建时,会向epoll注册自己,在关闭时会把自己从epoll的监视列表移除。而adjustWriteOp()方法是在socket发送数据时调用,我们知道,对于socket的send,只是把数据发送到了缓冲区,如果缓冲区满会触发EAGAIN,这个时候就要监听EPOLLOUT,表示缓冲区可写,adjustWriteOp()方法完成的就是这个操作。
  2. 用于事件通知的fd,这里有eventFdpipeFd。这两个用于事件通知的fd不会同时使用,先初始化eventFd,pipeFd仅仅是在eventFd无法成功初始化时的备选方案。这两个fd具体用在哪里可以看下面的代码段。

wakeup()方法调用的地方在这里:

这个scheduleTask()方法是写得最耐人寻味的地方。我本来以为,这只是个网络请求的任务队列,后来查了一下这个方法的调用,发现这个队列的东西真是包罗万象,什么加载文件,加载配置,设置dns,设置语言等任务都放到了这个队列里,感觉上是一个jni层全局的调度器。而对于网络请求,都是放在requestQueue中,查看sendRequest()方法不难看出来,处理网络请求是在processRequestQueue()方法中进行的。
现在再来看select()方法,流程就清楚多了,epoll_wait会在以下几种情况下被唤醒:

  1. socket连接有数据需要接收
  2. socket缓冲区可写,上一次发送的数据没有完成
  3. 有任务在pendingTasks队列中

当线程被唤醒,不再阻塞之后,会首先去处理pendingTasks队列中的请求。然后调用epoll返回的EventObject对象的onEvent()方法依次处理,这个EventObject方法其实可以看作一个wrapper,会根据EventObjectType把逻辑分发到各个对象上,代码如下:

对于epoll,会用到的类型是EventObjectTypeConnection, EventObjectTypePipe, EventObjectTypeEvent.
完成EventObject的处理之后,接下来会关闭超时连接,这里的超时时间是12秒,然后要处理长连接的心跳。这里对于长连接心跳的处理如下:

  1. 关闭超时的长连接。这里对于超时的判定有两个条件,一是发送心跳包没有收到回包,已经过去了30s,二是距离上次发送心跳包时间已经过了190s。
  2. 判断当前时间距离上一次发送心跳包是否已经超过了180s,如果是的话,则发送长连接心跳包。这里可以得出结论,Telegram的长连接心跳周期是3分钟
    接下的处理非常良心,Telegram在检测到当前队列没有上传下载任务和向server请求盐值的任务时,会关闭和数据中心的socket连接。这里有一个阈值是10s,可以认为,如果把应用切到后台,在完成当前队列里的上传和下载请求10s后,Telegram就会关闭长连接,停止发心跳。这个操作非常省电,反观国内大把app,各种黑科技保活。
    那么有一个问题,Telegram是如何保证休眠后还能接收到消息呢?答案是GCM,从AndroidManifest.xml里很容易找到GCM对应service的实现类:

这里的ConnectionsManager.getInstance().resumeNetworkMaybe()就是尝试重新恢复长连接的操作,逻辑很简单,就不再赘述。
最后一步,对于和DataCenter的普通连接,也发送心跳保活,这里的心跳周期是19s,然后每小时更新一次DataCenter的配置。再调用processRequestQueue()方法开始处理请求队列中的任务。
这里整理了一个比较直观的流程图,说明了发起请求的过程。
select

Telegram网络模块的分析就到这里了。