当前位置: 首页 > 工具软件 > TeamTalk4Mac > 使用案例 >

TeamTalk消息服务器原理及二次开发简介

杜阳泽
2023-12-01

原文:https://my.oschina.net/u/877397/blog/483599

置顶:关于用smart pointer修改的demo源码我放在了我的开源中国的git目录下,

这个地址:http://git.oschina.net/benben-de-eggs/tt-code-fragment

修改基于一个原则,不会动老的接口,也就是说,以前的代码不需要修改成智能指针也一样能编译运行。

文章有点长,而且废话较多,看得不耐烦的同学敬请谅解,本人第一次写博客,有点激动,所以话较多。。。

先说点关于TeamTalk项目变动的消息,由于之前的负责人蓝狐已经离开蘑菇街,并移交了官方的代码和qq 1群的管理权,2群独立出来改名叫TeamTalk非官方BB群,有兴趣或者对蓝狐葛格有个人崇拜情结的同学请加此群号 437335108

“蓝狐是谁?“

”。。。。。“

TeamTalk,简称TT,是蘑菇街开源的一款即时通讯软件。虽然功能相对简单(相比微信,qq之类),但也正是由于这一点,非常适合学习和进行二次开发。

为了给这款不错开源软件做个正面的应用推广,我本人也就牺牲一下个人时间来好好写点关于如何使用TT来进行二次开发的博文,此篇算是第一篇。

TT的客户端横跨几大平台,这恐怕是它最大的特色了,但是大家学习TT可能更多的都是冲着它的服务端去的。因为客户端开发无非是界面的呈现,用户的逻辑,书里网上一找一堆相关的讲解。但是开发一个高并发易伸缩的服务器恐怕就没那么容易找到相关的资料了。尤其是有关消息服务器的开发,鲜有相关书籍涉及之。

TT的服务端其实就是一整套消息服务器,提到消息服务器,喜欢追新的同学可能立刻就会联想到erlang, scala,go这些号称新一代能够支撑大规模高并发牛逼哄哄的语言。

然而TT并没有采用这些新潮的技术,甚至连第三方框架都没有用,而是直接用C++基于socket和io复用的异步机制实现了整个消息服务器。

看到这里,也许有的同学会有点失望,但是我要在这里说明一点,不要被那些新潮的牛逼哄哄的技术所迷惑,其实所有的技术都是万变不离其中的。新的语言,新的框架,所变的其实都只是编程模型和范式,其底层依然是对socket接口的调用,依然是利用TCP/IP协议传输网络数据,依然是利用select, epoll或kqueue来实现异步消息机制。所以当你明白了如何去用C++和最原始最基本的系统调用来实现一整套高并发可扩展的消息服务器时,你就会明白一切新潮的技术,还有那些不是很新潮的,比如像apache, nginx这些web服务器背后能支撑高并发的最基本的原理。

有点扯远了,现在来说一下TT的消息服务器的基本组成。

TT的消息服务器主要由login_server, msg_server, route_server, db_proxy_server组成,其他几个server暂时不谈。

我就不画图了,一方面是觉得画张看上去牛逼哄哄的架构图其实如果不做深入的解释,你也一样看不懂其中的原理,另一方面是我比较懒。。。

这里login_server的名字有点迷惑性,准确的说应该叫消息服务器的负载均衡。之所以这么说,是因为它的主要作用不是用来给你的用户login的,而是给你的用户在login之前选择一个负载较小的msg_server然后login的。而你的用户真正的login操作其实是在msg_server上进行的。

所以TT一开始用户打开app登录的流程是,先用http get的方式去login_server上获取一个负载最小的msg_server的地址,然后打开socket连接那个msg_server。然后一旦连接成功,你就可以发login消息了。

而login消息其实就是由用户名和密码(还有几个其他字段,此处不提)组成的一段消息。发到msg_server上后,msg_server会将其再发给db_proxy_server,db_proxy_server收到这个消息后,会用消息里的用户名去查询数据库,然后将查询的结果与消息里的密码比对,如果比对成功,那么就会向msg_server返回一个OK消息,然后msg_server就会将此用户加入valid user的列表里。之后,你的用户所发的消息就能被msg_server放行了。

这里还有一个route_server,这个是干嘛用的?路由器? 跟家里上网用的那个竖着两个棒子的玩意儿有啥关系?

这里我先解释一下路由的概念,在网络通信里,所谓的路由,就是让你所发的数据报文知道下一个网路节点的路径在哪。比如你家里的那个路由器,它里面其实维护了一张路由表,当你要访问XX网站时,你所发出的请求报文里的ip地址是那个网站的,但是从你家到那个网站并不是只有一根网线连着的,这中间有数不清的网络设备,你家里的那个路由器连到你家小区的路由器,你家小区的路由器再连到电信局里的,电信局子里的再连到。。。这中间每一台路由器都和N台路由器相连,然后你的所发出的报文在每台路由器上究竟是发往哪一台下一个路由器才能最终到达你要访问的网站呢?那就是要查路由器上的路由表,那个路由表会存放到每一个目的网段的下一个路由器的地址。。。

好像越说越复杂了,就此打住。没看懂的同学也没有关系,这里讲的是TT的消息服务器的route,为什么TT的消息服务器需要路由器?那是因为一个消息服务器承载的用户数是有限的,假设你的消息服务器想让1000万用户都能在线自由无障碍交流,但你的一个msg_server只能承受10万用户,怎么办?增加服务器呗,1000万用户,100个msg_server就行了。但是,如果一个msg_server上的用户想跟另一个msg_server上的用户发消息该怎么办?

两个办法,一个是在每个msg_server上维护一个全局的用户所在msg_server的表,如果目的用户不在同一个msg_server上,去查这个表,找到对应的msg_server的地址,然后转发给它。

另一个办法是不管三七二十一,如果目的用户不在当前msg_server上,统统先发给一个中转站,然后让那个中转站选择再发给对应的msg_server。这里这个中转站就是route_server,上面维护了所有用户所在msg_server的映射表,也就是传说中的路由表(看上面对于路由的解释)。那样当你的消息被转发到这个route_server上之后,将会查这个路由表,然后发往对应的msg_server。

到这里,差不多TT的消息服务器的基本机制就讲完了。这里提一个问题,为什么需要独立出route_server和db_proxy_server,能不能把查路由转发和查数据库的任务统统放到msg_server上来做?这样做不是会减少很多代码的复杂度以及少了很多server的开销?(这里不回答这个问题,欢迎在评论里进行讨论)

再来看一个问题,login_server是怎么知道哪个msg_server的负载最小的?

这个问题可以有很多种解决办法,一种是每个msg_server定期把自己当前的用户数写入数据库(redis),然后由login_server定期再去查数据库,这样就知道了。

“什么?还是不知道怎么获取用户最少的那个msg_server?”

“查了数据库后怎么做?”

“循环遍历和比大小会吗?”

“会。。。。。”

另一种方法,也就是TT的做法是让msg_server和login_server定期通讯,每个msg_server定期的给login_server发个消息告诉一下当前的用户数。

“msg_server怎么给login_server发消息?”

“客户端怎么给服务器发消息的?”

“建一个socket连接,然后。。。”

“服务器之间的通讯也是这样的。”

“TT服务器之间的通讯是怎么实现的?”

好吧,现在开始讲TT的网络通信是怎么做的。

这里又要强调一下之前所说过的,所有的网络通信都是基于socket的,不管是什么牛逼语言,牛逼框架,最终发送网络报文都要进行socket系统调用,因为socket是由操作系统内核实现的一组网络通信接口,windows, unix(及其衍生品linux, darwin, mac osx, android, ios)都是这组相同的接口。除非你不用操作系统,自己裸机跑,自己写个网卡驱动,然后再自己实现一下TCP/IP协议栈,然后再。。。

“推荐使用最近很火爆的arduino小板子完成这项艰巨的任务”

“我擦,你还当真要这么干啊。。。”

关于socket就不多说了,否则这样下去没玩没了,都可以出本书了。。。网上和书上讲解socket的书很多,不懂的请自行查阅。

TT并没有使用第三方的框架来构建自己的网络通信,而是选择自己封装socket接口,也就是base目录里面的netlib库。那既然socket是操作系统封装好的网络通信接口,为什么TT还要再包装一层?

这是因为socket接口的调用都是同步的,也就是说当你调用connect或accept接口时,如果目标机器的网络连接比较慢,那个这个调用就会一直不返回,整个程序就会在此处阻塞。如果是客户端应用,那还好,连接慢就让用户等等呗,但服务端,一个连接被阻塞,那后面接进来的用户就得排队等了。所以这个时候一般有两种做法,一种做法就是大家都知道的多线程。排队买票队伍太长了就多开几个窗口呗。但是窗口开很多有个问题,需要雇很多人手帮你卖票,不忙的时候很多卖票的闲着茅坑不拉屎,忙的时候每个队伍里面的人还是要等,还是会有人因为等待太久抱怨。

所以这个时候聪明的老板就会想到另一个办法,只开一个窗口,然后开个休息区,搞几排沙发,安个液晶大彩电放部小电影(纸巾请自备),再给每个人取个号,轮到某个号时再叫那个人,耶~~终于不会有人再抱怨了。

那么如何在程序中实现这种轮到某个人再叫他的机制呢?

这就是传说中的消息机制。操作系统会提供一种侦测消息发生的手段,把这种手段作为系统调用接口提供给用户,用户只需要把需要监听的对象加入到监听队列里,系统会自动在被监听的对象就绪时提醒你“哇塞,可以lu了”

这种侦测消息是否发生的系统调用有select, poll, epoll, kqueue。这里就不细说了,不然又要变成说书了。

就拿epoll来举个例子,当你创建了一个socket对象后,把它要先设置成非阻塞(nonblock),然后再对它做connect,accept操作时,就会立刻返回。但是这个时候,socket可能还没有连接上,怎么办?把这个socket加入到epoll的监听对象里,然后当socket就绪了,epoll就会返回这个就绪的socket。

所以你的程序通常是这样的

while(true)

  fd = epoll(...)

  read(fd)

上面只是伪代码,具体代码请去TT的base目录里的EventDispatch.cpp里查看。

虽然这段代码很简单,但却是很多系统实现消息机制的模板。也就是说只要是基于消息模型的程序,其实都会有一个监听事件的循环。说到这里,相信大家也就明白了经常听说的io loop或者消息主循环之类的术语是怎么回事了。

很多图形界面的消息响应机制也是这样的,学习过用c写windows窗口程序的同学应该对此很熟悉,用MFC的就比较悲剧了,因为MFC对其用户隐藏了这个消息循环。这也是为什么很多人写了很多年C++界面程序,依然对MFC感到云里雾里的原因。

再举一个跟图形界面有关的例子,很多人都知道浏览器里的javascript是单线程的,但是为什么单线程却能够做像ajax那样异步的操作,很多人以为有另一个线程在帮忙,其实不然。是浏览器在执行了你所有的javascript脚本之后,还偷偷运行了一个像上面一样的监听消息事件的循环。然后一旦有事件发生了,就会进行回调,这样也就实现了单线程异步。

所以,当你明白了这种消息响应背后的机制后,你对很多东西就顿悟了

“没有顿悟的同学,纸巾在那边放着呢,请自行解决。。。”

“周围好多人的,伦家不好意思。。。”

“我是说擦干你的眼泪,想哪儿去了=_=|||”

所以,如果你使用某个语言宣称的很牛逼的某个异步网络通信框架,比如node.js,又或者java的netty等等,如果你不了解这些异步框架背后的东西,那么你永远都无法知道为什么它能高并发的真相,也就很容易陷入究竟是选哪种语言那种框架的无休止的纠结当中。其实啊,他们都一样,吹牛逼的,高并发是操作系统的内核提供保障的,这些语言框架只是在其基础之上提供了一个易于编程的模型和环境。至于真的那么易吗?谁学谁知道,就算你一个框架已经了如指掌,学习另一个框架的曲线依然非常的高,因为这些框架都复杂的跟坨屎一样。为什么会这么复杂?因为它们身为框架,要考虑各种各样的应用场景。

现在好了,TT直接从最基础的底层开始做,并且把真相都暴露给大家了,你在了解真相的同时还可以直接使用它已经做好的netlib库,简洁,明了,真嗨! 

赶紧拿着TT的小代码嗨起来,慢慢的你就出来了~~

好吧,其实TT还有一个异步网络库,那就是push_server里用的那个,这里稍微提一下,push_server肯定是一个C++大神写的

“是谁?”

“你想干嘛?”

“伦家。。。”

“那位大神对男银恐怕不是很有兴趣”

“伦家只是想知道为什么说他是大神。。。”

好吧,因为整个框架完全按照面向对象的模型建模,并且采用了sigslot信号槽(如果用过qt你肯定对这玩意儿不陌生)来绑定事件的响应处理函数。但是本博只是一个简介,讲太多恐怕会被大家喷,所以这里就一带而过了。

其他的server都是用的netlib,而netlib虽然是用c++写的,但其实却是C风格的。所有的接口都是封装成netlib_xxx的函数形式。而异步的操作直接传入一个回调函数,这种做法和node.js的异步是非常相似的(如果你对node.js不熟悉请直接略过)。

这里只举一个connect为例,同样还是以伪代码来说明问题

netlib_connect(ip, port, callback, callback_data)

这个接口需要连接目的ip和port,但是这个函数需要立刻返回,那么如果没有立刻连接上怎么办,这时就需要把所有连接上之后的操作写到一个函数里,然后把这个函数作为callback传入,当netlib底层的消息循环侦测到连接上事件发生后,就会回调这个callback函数。这么说应该很明白了吧。

后面不讲了,太累了。。。

其实相比写文章我更喜欢写代码,这也是为什么编码这么多年,第一次写技术博的原因。

本来是想写一写如何把TT里面用引用计数的管理对象的代码改成用智能指针来实现的,结果一唠叨就写跑题了。

回过来看,只好把文章名字改一改,改成TT的基本原理介绍。

下回再将如何使用netlib创建一个Conn对象,TT里面所有的网络通信代码都是这样的模式,一旦你掌握了,就能很轻易的用这套机制开发其他异步高性能网络通信程序。

但是今天就到此为止了,

“貌似纸巾还没用完,还有人需要吗?”

“廉颇老矣,尚能lu否?”

“不了,谢谢”

 类似资料: