01 TeamTalk介绍

01 TeamTalk介绍 TeamTalk是蘑菇街开源的一款企业内部用的即时通讯软件(Enterprise IM),类似腾讯的RTX。网上也有很多的介绍,我这里也有写几遍关于这款产品的“流水账”,一方面对自己这段时间的阅读其代码做个总结,尽量做个既能宏观上从全局来介绍,又不缺少很多有价值的微观细节,另一方面如果对于作为读者的您有些许帮助,那就善莫大焉了。 项目地址github:https://github.com/baloonwj/TeamTalk 如果您打不开github,请移步至百度网盘下载:http://pan.baidu.com/s/1slbJVf3 关于即时通讯软件本身,我相信使用过QQ的都知道是啥。 下载项目解压后目录结构是这样的: 这款即时通讯软件分为服务器端(linux)、pc端、web端、mac端和两个移动端(ios和安卓),源码中使用了大量的开源技术(用项目作者的话说,就是“拿来主义”)。例如通信协议使用了google protobuf,服务器端使用了内存数据库redis,pc端界面库使用的duilib,pc端的日志系统使用的是YAOLOG库、cximage、jsoncpp库等等。在接下来各个端的源码分析中,我们将会深入和细致地介绍。 下一篇我将介绍首先介绍服务器端的程序的编译与部署。

January 11, 2021

02 服务器端的程序的编译与部署

02 服务器端的程序的编译与部署 这篇我们来介绍下TeamTalk服务器端的编译与部署,部署文档在auto_setup下,这里我们只介绍下服务器程序的编译与部署,不包括管理后台的部署,其部署方法在auto_setup\im_server文件夹,其实按官方介绍只要找一台干净的linux系统运行一下auto_setup\im_server\setup.sh程序就可以了,会自动安装mysql(maridb,mysql被oracle收购后,分为两个分支,继续开源的分支改名叫maridb)、nginx和redis。我们暂且不部署web端,所以不需要安装nginx。我这里是手动安装了mysql和redis。然后启动mysql和redis,并手动建立如下库和表。库名叫teamtalk,需要建立以下这些表: --后台管理员表 --password 密码,规则md5(md5(passwd)+salt) CREATE TABLE `IMAdmin` ( `id` mediumint(6) unsigned NOT NULL AUTO_INCREMENT, `uname` varchar(40) NOT NULL COMMENT '用户名', `pwd` char(32) NOT NULL COMMENT '经过md5加密的密码', `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '用户状态 0 :正常 1:删除 可扩展', `created` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `updated` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 --存储语音地址 CREATE TABLE `IMAudio` ( `id` int(11) NOT NULL AUTO_INCREMENT, `fromId` int(11) unsigned NOT NULL COMMENT '发送者Id', `toId` int(11) unsigned NOT NULL COMMENT '接收者Id', `path` varchar(255) COLLATE utf8mb4_bin DEFAULT '' COMMENT '语音存储的地址', `size` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小', `duration` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '语音时长', `created` int(11) unsigned NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_fromId_toId` (`fromId`,`toId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin --存储部门信息 CREATE TABLE `IMDepart` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '部门id', `departName` varchar(64) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '部门名称', `priority` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '显示优先级,相同优先级按拼音顺序排列', `parentId` int(11) unsigned NOT NULL COMMENT '上级部门id', `status` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '状态', `created` int(11) unsigned NOT NULL COMMENT '创建时间', `updated` int(11) unsigned NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_departName` (`departName`), KEY `idx_priority_status` (`priority`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin --发现配置表 CREATE TABLE `IMDiscovery` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `itemName` varchar(64) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '名称', `itemUrl` varchar(64) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT 'URL', `itemPriority` int(11) unsigned NOT NULL COMMENT '显示优先级', `status` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '状态', `created` int(11) unsigned NOT NULL COMMENT '创建时间', `updated` int(11) unsigned NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_itemName` (`itemName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin --群组表 CREATE TABLE `IMGroup` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(256) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '群名称', `avatar` varchar(256) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '群头像', `creator` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建者用户id', `type` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '群组类型,1-固定;2-临时群', `userCnt` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '成员人数', `status` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '是否删除,0-正常,1-删除', `version` int(11) unsigned NOT NULL DEFAULT '1' COMMENT '群版本号', `lastChated` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后聊天时间', `created` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `updated` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_name` (`name`(191)), KEY `idx_creator` (`creator`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='IM群信息' --群成员表 CREATE TABLE `IMGroupMember` ( `id` int(11) NOT NULL AUTO_INCREMENT, `groupId` int(11) unsigned NOT NULL COMMENT '群Id', `userId` int(11) unsigned NOT NULL COMMENT '用户id', `status` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '是否退出群,0-正常,1-已退出', `created` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `updated` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_groupId_userId_status` (`groupId`,`userId`,`status`), KEY `idx_userId_status_updated` (`userId`,`status`,`updated`), KEY `idx_groupId_updated` (`groupId`,`updated`) ) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8 COMMENT='用户和群的关系表' --群消息表,x代表第几张表,目前做了分表有8张:0-7....

January 11, 2021

03 服务器端的程序架构介绍

03 服务器端的程序架构介绍 通过上一节的编译与部署,我们会得到TeamTalk服务器端以下部署程序: db_proxy_server file_server http_msg_server login_server msfs msg_server push_server router_server 这些服务构成的拓扑图如下: 各个服务程序的作用描述如下: LoginServer (C++): 负载均衡服务器,分配一个负载小的MsgServer给客户端使用 MsgServer (C++): 消息服务器,提供客户端大部分信令处理功能,包括私人聊天、群组聊天等 RouteServer (C++): 路由服务器,为登录在不同MsgServer的用户提供消息转发功能 FileServer (C++): 文件服务器,提供客户端之间得文件传输服务,支持在线以及离线文件传输 MsfsServer (C++): 图片存储服务器,提供头像,图片传输中的图片存储服务 DBProxy (C++): 数据库代理服务器,提供mysql以及redis的访问服务,屏蔽其他服务器与mysql与redis的直接交互 HttpMsgServer(C++) :对外接口服务器,提供对外接口功能。(目前只是框架) PushServer(C++): 消息推送服务器,提供IOS系统消息推送。(IOS消息推送必须走apns) 注意:上图中并没有push_server和http_push_server。如果你不调试ios版本的客户端,可以暂且不启动push_server,另外http_push_server也可以暂不启动。 启动顺序: 一般来说,前端的服务会依赖后端的服务,所以一般先启动后端服务,再启动前端服务。建议按以下顺序启动服务: 1、启动db_proxy。 2、启动route_server,file_server,msfs 3、启动login_server 4、启动msg_server 那么我就按照服务端的启动顺序去讲解服务端的一个流程概述。 第一步:启动db_proxy后,db_proxy会去根据配置文件连接相应的MySQL实例,以及redis实例。 第二步:启动route_server,file_server,msfs后,各个服务端都会开始监听相应的端口。 第三步:启动login_server,login_server就开始监听相应的端口(8080),等待客户端的连接,而分配一个负载相对较小的msg_server给客户端。 第四步:启动msg_server(端口8000),msg_server启动后,会去主动连接route_server,login_server,db_proxy_server,会将自己的监听的端口信息注册到login_server去,同时在用户上线,下线的时候会将自己的负载情况汇报给login_server. 各个服务的端口号 (注意:如果出现部署完成后但是服务进程启动有问题或者只有部分服务进程启动了,请查看相应的log日志,请查看相应的log日志,请查看相应的log日志。) 服务 端口 login_server 8080/8008 msg_server 8000 db_proxy_server 10600 route_server 8200 http_msg_server 8400 file_server 8600/8601 服务网络通信框架介绍: 上面介绍的每一个服务都使用了相同的网络通信框架,该通信框架可以单独拿出来做为一个通用的网络通信框架。该网络框架是在一个循环里面不断地检测IO事件,然后对检测到的事件进行处理。流程如下: 使用IO复用技术(linux和windows平台用select、mac平台用kevent)分离网络IO。 对分离出来的网络IO进行操作,分为socket句柄可读、可写和出错三种情况。 当然再加上定时器事件,即检测一个定时器事件列表,如果有定时器到期,则执行该定时器事件。 整个框架的伪码大致如下: while (running) { //处理定时器事件 _CheckTimer(); //IO multiplexing int n = select(socket集合, ...); //事件处理 **if** (某些socket可读) { pSocket->OnRead(); } **if** (某些socket可写) { pSocket->OnWrite(); } **if** (某些socket出错) { pSocket->OnClose(); } } 处理定时器事件的代码如下: void CEventDispatch::_CheckTimer() { uint64_t curr_tick = get_tick_count(); list<TimerItem*>::iterator it; for (it = m_timer_list....

January 11, 2021

04 服务器端db_proxy_server源码分析

04 服务器端db_proxy_server源码分析 从这篇文章开始,我将详细地分析TeamTalk服务器端每一个服务的源码和架构设计。 这篇从db_proxy_server开始。db_proxy_server是TeamTalk服务器端最后端的程序,它连接着关系型数据库mysql和nosql内存数据库redis。其位置在整个服务架构中如图所示: 我们从db_proxy_server的main()函数开始,main()函数其实就是做了以下初始化工作,我整理成如下伪码: int main() { //1. 初始化redis连接 //2. 初始化mysql连接 //3. 启动任务队列,用于处理任务 //4. 启动从mysql同步数据到redis工作 //5. 在端口10600上启动侦听,监听新连接 //6. 主线程进入循环,监听新连接的到来以及出来新连接上的数据收发 } 下面,我们将一一介绍以上步骤。 一、初始化redis连接 CacheManager* pCacheManager = CacheManager::getInstance(); CacheManager* CacheManager::getInstance() { if (!s_cache_manager) { s_cache_manager = new CacheManager(); if (s_cache_manager->Init()) { delete s_cache_manager; s_cache_manager = NULL; } } return s_cache_manager; } int CacheManager::Init() { CConfigFileReader config_file("dbproxyserver.conf"); //CacheInstances=unread,group_set,token,sync,group_member char* cache_instances = config_file.GetConfigName("CacheInstances"); if (!cache_instances) { log("not configure CacheIntance"); return 1; } char host[64]; char port[64]; char db[64]; char maxconncnt[64]; CStrExplode instances_name(cache_instances, ','); for (uint32_t i = 0; i < instances_name.GetItemCnt(); i++) { char* pool_name = instances_name.GetItem(i); //printf("%s", pool_name); snprintf(host, 64, "%s_host", pool_name); snprintf(port, 64, "%s_port", pool_name); snprintf(db, 64, "%s_db", pool_name); snprintf(maxconncnt, 64, "%s_maxconncnt", pool_name); char* cache_host = config_file....

January 11, 2021

05 服务器端msg_server源码分析

05 服务器端msg_server源码分析 在分析msg_server的源码之前,我们先简单地回顾一下msg_server在整个服务器系统中的位置和作用: 各个服务程序的作用描述如下: LoginServer (C++): 负载均衡服务器,分配一个负载小的MsgServer给客户端使用 MsgServer (C++): 消息服务器,提供客户端大部分信令处理功能,包括私人聊天、群组聊天等 RouteServer (C++): 路由服务器,为登录在不同MsgServer的用户提供消息转发功能 FileServer (C++): 文件服务器,提供客户端之间得文件传输服务,支持在线以及离线文件传输 MsfsServer (C++): 图片存储服务器,提供头像,图片传输中的图片存储服务 DBProxy (C++): 数据库代理服务器,提供mysql以及redis的访问服务,屏蔽其他服务器与mysql与redis的直接交互 HttpMsgServer(C++) :对外接口服务器,提供对外接口功能。(目前只是框架) PushServer(C++): 消息推送服务器,提供IOS系统消息推送。(IOS消息推送必须走apns) 从上面的介绍中,我们可以看出TeamTalk是支持分布式部署的一套聊天服务器程序,通过分布式部署可以实现分流和支持高数量的用户同时在线。msg_server是整个服务体系的核心系统,可以部署多个,不同的用户可以登录不同的msg_server。这套体系有如下几大亮点: login_server可以根据当前各个msg_server上在线用户数量,来决定一个新用户登录到哪个msg_server,从而实现了负载平衡; route_server可以将登录在不同的msg_server上的用户的聊天消息发给目标用户; 通过单独的一个数据库操作服务器db_proxy_server,避免了msg_server直接操作数据库,将数据库操作的入口封装起来。 在前一篇文章《服务器端db_proxy_server源码分析》中,我介绍了每个服务如何接收连接、读取数据并解包、以及组装数据包发包的操作,这篇文章我将介绍作为客户端,一个服务如何连接另外一个服务。这里msg_server在启动时会同时连接db_proxy_server,login_server,file_server,route_server,push_server。在msg_server服务main函数里面有如下初始化调用: //连接file_server init_file_serv_conn(file_server_list, file_server_count); //连接db_proxy_server init_db_serv_conn(db_server_list2, db_server_count2, concurrent_db_conn_cnt); //连接login_server init_login_serv_conn(login_server_list, login_server_count, ip_addr1, ip_addr2, listen_port, max_conn_cnt); //连接push_server init_route_serv_conn(route_server_list, route_server_count); //连接push_server init_push_serv_conn(push_server_list, push_server_count); 其中每个连接服务的流程都是一样的。我们这里以第一个连接file_server为例: void init_file_serv_conn(serv_info_t* server_list, uint32_t server_count) { g_file_server_list = server_list; g_file_server_count = server_count; serv_init<CFileServConn>(g_file_server_list, g_file_server_count); netlib_register_timer(file_server_conn_timer_callback, NULL, 1000); s_file_handler = CFileHandler::getInstance(); } template <class T> void serv_init(serv_info_t* server_list, uint32_t server_count) { for (uint32_t i = 0; i < server_count; i++) { T* pConn = new T(); pConn->Connect(server_list[i].server_ip.c_str(), server_list[i].server_port, i); server_list[i]....

January 11, 2021

06 服务器端login_server源码分析

06 服务器端login_server源码分析 login_server从严格意义上来说,是一个登录分流器,所以名字起的有点名不符实。该服务根据已知的msg_server上的在线用户数量来返回告诉一个即将登录的用户登录哪个msg_server比较合适。关于其程序框架的非业务代码我们已经在前面的两篇文章《服务器端db_proxy_server源码分析》和《服务器端msg_server源码分析》中介绍过了。这篇文章主要介绍下其业务代码。 首先,程序初始化的时候,会初始化如下功能: //1. 在8008端口监听客户端连接 //2. 在8100端口上监听msg_server的连接 //3. 在8080端口上监听客户端http连接 其中连接对象CLoginConn代表着login_server与msg_server之间的连接;而CHttpConn代表着与客户端的http连接。我们先来看CLoginConn对象,上一篇文章中也介绍了其业务代码主要在其HandlePdu()函数中,可以看到这路连接主要处理哪些数据包: void CLoginConn::HandlePdu(CImPdu* pPdu) { switch (pPdu->GetCommandId()) { case CID_OTHER_HEARTBEAT: break; case CID_OTHER_MSG_SERV_INFO: _HandleMsgServInfo(pPdu); break; case CID_OTHER_USER_CNT_UPDATE: _HandleUserCntUpdate(pPdu); break; case CID_LOGIN_REQ_MSGSERVER: _HandleMsgServRequest(pPdu); break; default: log("wrong msg, cmd id=%d ", pPdu->GetCommandId()); break; } } 命令号CID_OTHER_HEARTBEAT是与msg_server的心跳包。上一篇文章《服务器端msg_server源码分析》中介绍过,msg_server连上login_server后会立刻给login_server发一个数据包,该数据包里面含有该msg_server上的用户数量、最大可容纳的用户数量、自己的ip地址和端口号。 list<user_conn_t> user_conn_list; CImUserManager::GetInstance()->GetUserConnCnt(&user_conn_list, cur_conn_cnt); char hostname[256] = {0}; gethostname(hostname, 256); IM::Server::IMMsgServInfo msg; msg.set_ip1(g_msg_server_ip_addr1); msg.set_ip2(g_msg_server_ip_addr2); msg.set_port(g_msg_server_port); msg.set_max_conn_cnt(g_max_conn_cnt); msg.set_cur_conn_cnt(cur_conn_cnt); msg.set_host_name(hostname); CImPdu pdu; pdu.SetPBMsg(&msg); pdu.SetServiceId(SID_OTHER); pdu.SetCommandId(CID_OTHER_MSG_SERV_INFO); SendPdu(&pdu); 命令号是CID_OTHER_MSG_SERV_INFO。我们来看下login_server如何处理这个命令的: void CLoginConn::_HandleMsgServInfo(CImPdu* pPdu) { msg_serv_info_t* pMsgServInfo = new msg_serv_info_t; IM::Server::IMMsgServInfo msg; msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength()); pMsgServInfo->ip_addr1 = msg.ip1(); pMsgServInfo->ip_addr2 = msg.ip2(); pMsgServInfo->port = msg.port(); pMsgServInfo->max_conn_cnt = msg.max_conn_cnt(); pMsgServInfo->cur_conn_cnt = msg.cur_conn_cnt(); pMsgServInfo->hostname = msg.host_name(); g_msg_serv_info....

January 11, 2021

07 服务器端msfs源码分析

07 服务器端msfs源码分析 这篇文章是对TeamTalk服务程序msfs的源码和架构设计分析。msfs作用是用来接受teamtalk聊天中产生的聊天图片的上传和下载。还是老规矩,把该服务在整个架构中的位置图贴一下吧。 可以看到,msfs仅被客户端连接,客户端以http的方式来上传和下载聊天图片。 可能很多同学对http协议不是很熟悉,或者说一知半解。这里大致介绍一下http协议,http协议其实也是一种应用层协议,建立在tcp/ip层之上,其由包头和包体两部分组成(不一定要有包体),看个例子: 比如当我们用浏览器请求一个网址http://www.hootina.org/index.php,实际是浏览器给特定的服务器发送如下数据包,包头部分如下: GET /index.php HTTP/1.1\r\n Host: www.hootina.org\r\n Connection: keep-alive\r\n Cache-Control: max-age=0\r\n Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8\r\n User-Agent: Mozilla/5.0\r\n \r\n 这个包没有包体。 从上面我们可以看出一个http协议大致格式可以描述如下: GET或Post请求方法 请求的资源路径 http协议版本号\r\n 字段名1:值1\r\n 字段名2:值2\r\n 字段名3:值3\r\n 字段名4:值4\r\n 字段名5:值5\r\n 字段名6:值6\r\n \r\n 也就是是http协议的头部是一行一行的,每一行以\r\n表示该行结束,最后多出一个空行以\r\n结束表示头部的结束。接下来就是包体的大小了(如果有的话,上文的例子没有包体)。一般get方法会将参数放在请求的资源路径后面,像这样 http://wwww.hootina.org/index.php?变量1=值1&变量2=值2&变量3=值3&变量4=值4 网址后面的问号表示参数开始,每一个参数与参数之间用&隔开 还有一种post的请求方法,这种数据就是将数据放在包体里面了,例如: POST /otn/login/loginAysnSuggest HTTP/1.1\r\n Host: kyfw.12306.cn\r\n Connection: keep-alive\r\n Content-Length: 96\r\n Accept: */*\r\n Origin: https://kyfw.12306.cn\r\n X-Requested-With: XMLHttpRequest\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75\r\n Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n Referer: https://kyfw.12306.cn/otn/login/init\r\n Accept-Encoding: gzip, deflate, br\r\n Accept-Language: zh-CN,zh;q=0.8\r\n \r\n loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=xxxxgjqf&randCode=184%2C55%2C37%2C117 上述报文中loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=2032_scsgjqf&randCode=184%2C55%2C37%2C117 其实包体内容,这个包是我的一个12306买票软件发给12306服务器的报文。这里拿来做个例子。 因为对方收到http报文的时候,如果包体有内容,那么必须告诉对方包体有多大。这个最常用的就是通过包头的Content-Length字段来指定大小。上面的例子中Content-Length等于96,正好就是字符串 loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=xxxxgjqf&randCode=184%2C55%2C37%2C117 的长度,也就是包体的大小。 还有一种叫做http chunk的编码技术,通过对http包内容进行分块传输。这里就不介绍了(如果你感兴趣,可以私聊我)。 常见的对http协议有如下几个误解: html文档的头就是http的头 这种认识是错误的,html文档的头部也是http数据包的包体的一部分。正确的http头是长的像上文介绍的那种。 关于http头Connection:keep-alive字段 一端指定了这个字段后,发http包给另外一端。这个选项只是一种建议性的选项,对端不一定必须采纳,对方也可能在实际实现时,将http连接设置为短连接,即不采纳这个字段的建议。 每个字段都是必须的吗? 不是,大多数字段都不是必须的。但是特定的情况下,某些字段是必须的。比如,通过post发送的数据,就必须设置Content-Length。不然,收包的一端如何知道包体多大。又比如如果你的数据采取了gzip压缩格式,你就必须指定Accept-Encoding: gzip,然对方如何解包你的数据。 好了,http协议就暂且介绍这么多,下面回到正题上来说msfs的源码。 msfs在main函数里面做了如下初始化工作,伪码如下: //1. 建立一个两个任务队列,分别处理http get请求和post请求 //2. 创建名称为000~255的文件夹,每个文件夹里面会有000~255个子目录,这些目录用于存放聊天图片 //3. 在8700端口上监听客户端连接 //4. 启动程序消息泵 第1点,建立任务队列我们前面系列的文章已经介绍过了。...

January 11, 2021

08 服务器端file_server源码分析

08 服务器端file_server源码分析 这篇文章我们来介绍file_server服务的功能和源码实现。TeamTalk支持离线在线文件和离线文件两种传送文件的方式。单纯地研究file_server的程序结构没多大意义,因为其程序结构和其他几个服务结构基本上一模一样,前面几篇文章已经介绍过了。 我们研究teamtalk的file_server是为了学习和借鉴teamtalk的文件传输功能实现思路,以内化为自己的知识,并加以应用。 所以这篇文章,我们将pc客户端的文件传输功能、msg_server转发消息、file_server处理文件数据三个方面结合起来一起介绍。 下面开始啦。 一、连接状况介绍 fileserver开始并不是和客户端连接的,客户端是按需连接file_server的。但是file_server与msg_server却是长连接。先启动file_server,再启动msg_server。msg_server初始化的时候,会去尝试连接file_server的8601端口。连接成功以后,会给file_server发送一个发包询问file_server侦听客户端连接的ip和端口号信息: void CFileServConn::OnConfirm() { log("connect to file server success "); m_bOpen = true; m_connect_time = get_tick_count(); g_file_server_list[m_serv_idx].reconnect_cnt = MIN_RECONNECT_CNT / 2; //连上file_server以后,给file_server发送获取ip地址的数据包 IM::Server::IMFileServerIPReq msg; CImPdu pdu; pdu.SetPBMsg(&msg); pdu.SetServiceId(SID_OTHER); pdu.SetCommandId(CID_OTHER_FILE_SERVER_IP_REQ); SendPdu(&pdu); } file_server收到该数据包后,将自己的侦听客户端连接的ip地址和端口号发包告诉msg_server: void FileMsgServerConn::_HandleGetServerAddressReq(CImPdu* pPdu) { IM::Server::IMFileServerIPRsp msg; const std::list<IM::BaseDefine::IpAddr>& addrs = ConfigUtil::GetInstance()->GetAddressList(); for (std::list<IM::BaseDefine::IpAddr>::const_iterator it=addrs.begin(); it!=addrs.end(); ++it) { IM::BaseDefine::IpAddr* addr = msg.add_ip_addr_list(); *addr = *it; log("Upload file_client_conn addr info, ip=%s, port=%d", addr->ip().c_str(), addr->port()); } SendMessageLite(this, SID_OTHER, CID_OTHER_FILE_SERVER_IP_RSP, pPdu->GetSeqNum(), &msg); } 得到的信息是file_server侦听的ip地址和端口号,默认配置的端口号是8600。也就是说file_server的8600用于客户端连接,8601端口用于msg_server连接。这样,客户端需要传输文件(注意:不是聊天图片,聊天图片使用另外一个服务msfs进行传输),会先告诉msg_server它需要进行文件传输,msg_server收到消息后告诉客户端,你连file_server来传输文件吧,并把file_server的地址和端口号告诉客户端。客户端这个时候连接file_server进行文件传输。我们来具体看一看这个流程的细节信息: 客户端发包给msg_server说要进行文件发送 然后选择一个文件: pc客户端发送文件逻辑: //pc客户端代码(Modules工程SessionLayout.cpp) void SessionLayout::Notify(TNotifyUI& msg) { ... //省略无关代码 else if (msg.pSender == m_pBtnsendfile) //文件传输 { module::UserInfoEntity userInfo; if (!module::getUserListModule()->getUserInfoBySId(m_sId, userInfo)) { LOG__(ERR, _T("SendFile can't find the sid")); return; } CFileDialog fileDlg(TRUE, NULL, NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST , _T("文件|*....

January 11, 2021

09 服务器端route_server源码分析

09 服务器端route_server源码分析 route_server的作用主要是用户不同msg_server之间消息路由,其框架部分和前面的服务类似,没有什么好说的。我们这里重点介绍下它的业务代码,也就是其路由细节: void CRouteConn::HandlePdu(CImPdu* pPdu) { switch (pPdu->GetCommandId()) { case CID_OTHER_HEARTBEAT: // do not take any action, heart beat only update m_last_recv_tick break; case CID_OTHER_ONLINE_USER_INFO: _HandleOnlineUserInfo( pPdu ); break; case CID_OTHER_USER_STATUS_UPDATE: _HandleUserStatusUpdate( pPdu ); break; case CID_OTHER_ROLE_SET: _HandleRoleSet( pPdu ); break; case CID_BUDDY_LIST_USERS_STATUS_REQUEST: _HandleUsersStatusRequest( pPdu ); break; case CID_MSG_DATA: case CID_SWITCH_P2P_CMD: case CID_MSG_READ_NOTIFY: case CID_OTHER_SERVER_KICK_USER: case CID_GROUP_CHANGE_MEMBER_NOTIFY: case CID_FILE_NOTIFY: case CID_BUDDY_LIST_REMOVE_SESSION_NOTIFY: _BroadcastMsg(pPdu, this); break; case CID_BUDDY_LIST_SIGN_INFO_CHANGED_NOTIFY: _BroadcastMsg(pPdu); break; default: log("CRouteConn::HandlePdu, wrong cmd id: %d ", pPdu->GetCommandId()); break; } } 上面是route_server处理的消息类型,我们逐一来介绍: CID_OTHER_ONLINE_USER_INFO 这个消息是msg_server连接上route_server后告知route_server自己上面的用户登录情况。route_server处理后,只是简单地记录一下每个msg_server上的用户数量和用户id: void CRouteConn::_HandleOnlineUserInfo(CImPdu* pPdu) { IM::Server::IMOnlineUserInfo msg; CHECK_PB_PARSE_MSG(msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength())); uint32_t user_count = msg.user_stat_list_size(); log("HandleOnlineUserInfo, user_cnt=%u ", user_count); for (uint32_t i = 0; i < user_count; i++) { IM::BaseDefine::ServerUserStat server_user_stat = msg....

January 11, 2021

1 游戏服务器开发的基本体系与服务器端开发的一些建议

1 游戏服务器开发的基本体系与服务器端开发的一些建议 近年来,我身边的朋友有很多都从web转向了游戏开发。他们以前都没有做过游戏服务器开发,更谈不上什么经验,而从网上找的例子或游戏方面的知识,又是那么的少,那么的零散。当他们进入游戏公司时,显得一脸茫然。如果是大公司还好点,起码有人带带,能学点经验,但是有些人是直接进入了小公司,甚至这些小公司只有他一个后台。他们一肩扛起了公司的游戏后端的研发,也扛起了公司的成败。他们也非常尽力,他们也想把游戏的后端做好。可是就是因为没什么经验,刚开始时以为做游戏服务器和做web差不多,但是经过一段时间之后,才发现代码太多,太乱了,一看代码都想重构,都是踩着坑往前走。 这里我把一些游戏开发方面的东西整理一下,希望能对那些想做游戏服务器开发的朋友有所帮助。 首先,要明确一点,做游戏服务器开发和做传统的web开发有着本质的区别。游戏服务器开发,如果没有经验,一开始根本没有一个明确清析的目标,不像web那样,有些明确的MVC架构,往往就是为了尽快满足策划的需求,尽快的实现功能,尽快能让游戏跑起来。但是随着功能越来越多,在老代码上面修改的越来越频繁,游戏测试时暴露出来的一堆bug,更让人觉得束手无策,这个时候我们想到了重构,想到了架构的设计。 游戏的构架设计非常重要,好的构架代码清析,责任明确,扩展性强,易调试。这些会为我们的开发省去不少时间。**那要怎么样设计游戏的构架呢?可能每个游戏都不一样,但是本质上还是差不多的。 对于游戏服务器的构架设计,我们首先要了解游戏的服务器构架都有什么组成的?**一款游戏到上线,**需要具备哪些功能?**有些人可能会说,只要让游戏跑起来,访问服务器不出问题不就行了吗?答案是不行的,游戏构架本身代表的是一个体系,它包括: 系统初始化 游戏逻辑 数据库系统 缓存系统 游戏日志 游戏管理工具 公共服务组件 这一系统的东西都是不可少的,它们共同服务于游戏的整个运营过程。我们一点点来介绍各个系统的功能。 一,系统初始化 系统初始化是在没有客户端连接的时候,服务器启动时所需要做的工作。基本上就是配置文件的读取,初始化系统参数。 但是我们必须要考虑的是: 系统初始化需要的参数配置在哪儿,是配置在本地服务器,还是配置在数据库; 服务器启动的时候去数据库取; 配置的修改需不需要重启服务器等。 二,游戏逻辑 游戏逻辑是游戏的核心功能实现,也是整个游戏的服务中心,它被开发的好坏,直接决定了游戏服务器在运行中的性能。那在游戏逻辑的开发中我们要注意些什么呢? 游戏是一种网络交互比较强的业务,好的底层通信,可以最大化游戏的性能,增加单台服务器处理的同时在线人数,给游戏带来更好的体验,至少不容易出现因为网络层导致的数据交互卡顿的现象。在这里我推荐使用Netty,它是目前最流行的NIO框架,它的用法可以在我之前的文章中查看,这里不再多说了。 有人疑问,代码也需要分层次?这个是当然了,不同的代码,代表了不同的功能实现。现在的开发语言都是面向对象的,如果我们不加思考,不加整理的把功能代码乱堆一起,起始看起来是快速实现了功能,但是到后期,如果要修改需求,或在原来的代码上增加新的需求,那真是被自己打败了。所以代码一定要分层,主要有以下几层: **协议层,**也叫前后台交互层,它主要负责与前台交互协议的解析和返回数据。在这一层基本上没有什么业务逻辑实现。**与前台交互的数据都在这一层开始,也在这一层终止。**比如你使用了Netty框架,那么Netty的ChannelHandlerContext即Ctx只能出现在这一层,他不能出现到游戏业务逻辑代码的实现中,接收到客户端的请求,在这一层把需要的参数解析出来,再把参数传到业务逻辑方法中,业务逻辑方法处理完后,把要返回给客户端的数据再返回到这一层,在这一层组织数据,返回给客户端,这样就可以把业务逻辑和网络层分离,业务逻辑只关心业务实现,而且也方便对业务逻辑进行单元测试。 业务逻辑层,这里处理真正的游戏逻辑,该计算价格计算价格,该通关的通关,该计时的计时。该保存数据的保存数据。但是这一层不直接操作缓存或数据库,只是处理游戏逻辑计算。因为业务逻辑层是整个游戏事件的处理核心,所以他的处理是否正确直接决定游戏的正确性。所以这一层的代码要尽量使用面向对象的方法去实现。**不要出现重复代码或相似的功能进行复制粘贴,这样修改起来非常不方便,可能是修改了某一处,而忘记了修改另外同样的代码。还要考虑每个方法都是可测试的**,一个方法的行数最好不要超过一百行。另外,可以多看看设计模式的书,它可以帮助我们设计出灵活,整洁的代码。 三,数据库系统 数据库是存储数据库的核心,但是游戏数据在存储到数据库的时候会经过网络和磁盘的IO,它的访问速度相对于内存来说是很慢的。一般来说,每次访问数据库都要和数据库建立连接,访问完成之后,为了节省数据库的连接资源,要再把连接断开。 这样无形中又为服务器增加了开销,在大量的数据访问时,可能会更慢,而游戏又是要求低延时的,这时该怎么办呢?我们想到了数据库连接池,即把访问数据库的连接放到一个地方管理,用完我不断开,用的时候去那拿,用完再放回去。这样不用每次都建立新的连接了。 但是如果要我们自己去实现一套连接池管理组件的话,需要时间不说,对技术的把控也是一个考验,还要再经过测试等等,幸好互联网开源的今天,有一些现成的可以使用,这里推荐Mybatis,即实现了代码与SQL的分离,又有足够的SQL编写的灵活性,是一个不错的选择。 四,缓存系统 游戏中,客户端与服务器的交互是要求低延迟的,延迟越低,用户体验越好。像之前说过的一样,低延迟就是要求服务器处理业务尽量的快,客户端一个请求过来,要在最短的时间内响应结果,最低不得超过500ms,因为加上来回的网络传输耗时,基本上就是600ms-到700ms了,再长玩家就会觉得游戏卡了。 如果直接从数据库中取数据,处理完之后再存回数据库的话,这个性能是跟不上的。在服务器,数据在内存中处理是最快的,所以我们要把一部分常用的数据提前加载到内存中,比如说游戏数据配置表,经常登陆的玩家数据等。这样在处理业务时,就不用走数据库了,直接从内存中取就可以了,速度更快。 游戏中常见的缓存有两种: 直接把数据存储在jvm或服务器内存中 使用第三方的缓存工具,这里推荐Redis,详细的用法可以自己去查询。(本公号内有系列文章,详情见【菜单栏】- 【技术文章】 - 【基础系列】 - 【实战R1,实战R2】) 五,游戏日志 日志是个好东西呀,**一个游戏中更不能少了日志,而且日志一定要记录的详细。**它是玩家在整个游戏中的行为记录,有了这个记录,我们就可以分析玩家的行为,查找游戏的不足,在处理玩家在游戏中的问题时,日志也是一个良好的凭证和快速处理方式。 在游戏中,日志分为: 系统日志,主要记录游戏服务器的系统情况。比如:数据库能否正常连接,服务器是否正常启动,数据是否正常加载; 玩家行为日志,比如玩家发送了什么请求,得到了什么物品,消费了多少货币等等; **统计日志,**这种日志是对游戏中所有玩家某种行为的一种统计,根据这个统计来分析大部分玩家的行为,得出一些共性或不同之处,以方法运营做不同的活动吸引用户消费。 在构架设计中,日志记录一定要做为一种强制行为,因为不强制的话,可能由于某种原因某个功能忘记加日志了,那么当这个功能出问题了,或者运营跟我们要这个功能的一些数据库,就傻眼了。又得加需求,改代码了。日志一定要设计一种良好的格式,日志记录的数据要容易读取,分解。日志行为可以用枚举描述,在功能最后的处理方法里面加上这个枚举做为参数,这样不管谁在调用这个方法时,都要去加参数描述。 俗话说,工欲善其事,必先利其器。**游戏管理工具是对游戏运行中的一系列问题处理的一种工具。**它不仅是给开发人员用,大多数是给运营使用。游戏上线后,我们需要针对线上的问题进行不同的处理。不可能把所有问题都让程序员去处理吧,于是程序员们想到了一个办法,给你们做一个工具,你们爱谁处理谁处理去吧。 六, 游戏管理工具 游戏管理工具是一个不断增涨的系统,因为它很多时候是伴随着游戏中遇到的问题而实现的。 但是根据经验,有一些功能是必须有的,比如: 服务器管理,主要负责服务器的开启,关闭,服务器配置信息,玩家信息查询; 玩家管理,比如踢人,封号; 统计查询,玩家行为日志查询,统计查询,次留率查询,邮件服务,修改玩家数据等。 根据游戏的不同要求,**凡是可以能过工具实现的,都做到游戏管理工具里面。**它是针对所有服务器的管理。 一个好的,全的游戏管理工具,可以提高游戏运营中遇到问题处理的效率,为玩家提供更好的服务。 七,公共组件 公共组件是为游戏运行中提供公共的服务。例如: 充值服务器,我们没必须一个服用一个充值,而且你也不能对外提供多个充值服务器地址,和第三方公司对接,他们绝对不干,这是要疯呀; 还有运营搞活动时的礼包码; 还有注册用户的管理,玩家一个注册账号可以进不同的区等。 这些都是针对所有区服提供的服务,所以要单独做,与游戏逻辑分开,这样方便管理,部署和负载均衡。 还有SDK的登陆验证,现在手游比较多,与渠道对接里要进行验证,这往往是很多http请求,速度慢,所以这个也要拿出来单独做,不要在游戏逻辑中去验证,因为网络IO的访问时间是不可控制的,http是阻塞的请求。 所以,综上来看,一个游戏服务器起码有几个大的功能模块组成: 游戏逻辑工程; 日志处理工程; 充值工程; 游戏管理工具工程; 用户登陆工程; 公共活动工程等。 根据游戏的不同需要,可能还有其它的。所在构架的设计中,一定要考虑到系统的分布式部署,尽量把公共的功能拆出来做,这样可以增强系统的可扩展性。 服务器端开发的一些建议 本文作为游戏服务器端开发的基本大纲,是游戏实践开发中的总结。 第一部分 —— 专业基础,用于指导招聘和实习考核; 第二部分 —— 游戏入门,讲述游戏服务器端开发的基本要点; 第三部分 —— 服务端架构,介绍架构设计中的一些基本原则。 希望能帮到大家! 一、专业基础 1.1网络 1.1.1理解TCP/IP协议 网络传输模型 滑动窗口技术 建立连接的三次握手与断开连接的四次握手 连接建立与断开过程中的各种状态 TCP/IP协议的传输效率 思考: 请解释DOS攻击与DRDOS攻击的基本原理 一个100Byte数据包,精简到50Byte, 其传输效率提高了50% TIMEWAIT状态怎么解释? 1.1.2掌握常用的网络通信模型 Select Epoll,边缘触发与平台出发点区别与应用 Select与Epoll的区别及应用 1....

January 11, 2021