(三)服务器端的程序架构介绍1

通过上一节的编译与部署,我们会得到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事件,然后对检测到的事件进行处理。流程如下:

1. 使用IO复用技术(linux和windows平台用select、mac平台用kevent)分离网络IO。

2. 对分离出来的网络IO进行操作,分为socket句柄可读、可写和出错三种情况。

当然再加上定时器事件,即检测一个定时器事件列表,如果有定时器到期,则执行该定时器事件。

整个框架的伪码大致如下:

[cpp] view plain copy

  1. while (running)
  2. {
  3. //处理定时器事件
  4. _CheckTimer();
  5. //IO multiplexing
  6. int n = select(socket集合, ...);
  7. //事件处理
  8. if (某些socket可读)
  9. {
  10. pSocket->OnRead();
  11. }
  12. if (某些socket可写)
  13. {
  14. pSocket->OnWrite();
  15. }
  16. if (某些socket出错)
  17. {
  18. pSocket->OnClose();
  19. }
  20. }

处理定时器事件的代码如下:

[cpp] view plain copy

  1. void CEventDispatch::_CheckTimer()
  2. {
  3. uint64_t curr_tick = get_tick_count();
  4. list<TimerItem*>::iterator it;
  5. for (it = m_timer_list.begin(); it != m_timer_list.end(); )
  6. {
  7. TimerItem* pItem = *it;
  8. it++; // iterator maybe deleted in the callback, so we should increment it before callback
  9. if (curr_tick >= pItem->next_tick)
  10. {
  11. pItem->next_tick += pItem->interval;
  12. pItem->callback(pItem->user_data, NETLIB_MSG_TIMER, 0, NULL);
  13. }
  14. }
  15. }

即遍历一个定时器列表,将定时器对象与当前时间(curr_tick)做比较,如果当前时间已经大于或等于定时器设置的时间,则表明定时器时间已经到了,执行定时器对象对应的回调函数。

在来看看OnRead、OnWrite和OnClose这三个函数。在TeamTalk源码中每一个socket连接被封装成一个CBaseSocket对象,该对象是一个使用引用计数的类的子类,通过这种方法来实现生存期自动管理。

[cpp] view plain copy

  1. void CBaseSocket::OnRead()
  2. {
  3. if (m_state == SOCKET_STATE_LISTENING)
  4. {
  5. _AcceptNewSocket();
  6. }
  7. else
  8. {
  9. u_long avail = 0;
  10. if ( (ioctlsocket(m_socket, FIONREAD, &avail) == SOCKET_ERROR) || (avail == 0) )
  11. {
  12. m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
  13. }
  14. else
  15. {
  16. m_callback(m_callback_data, NETLIB_MSG_READ, (net_handle_t)m_socket, NULL);
  17. }
  18. }
  19. }

OnRead()方法根据状态标识m_state确定一个socket是侦听的socket还是普通与客户端连接的socket,如果是侦听sokcet则接收客户端的连接;如果是与客户端连接的socket,则先检测socket上有多少字节可读,如果没有字节可读或者检测字节数时出错,则关闭socket,反之调用设置的回调函数。

[cpp] view plain copy

  1. void CBaseSocket::OnWrite()
  2. {
  3. #if ((defined _WIN32) || (defined __APPLE__))
  4. CEventDispatch::Instance()->RemoveEvent(m_socket, SOCKET_WRITE);
  5. #endif
  6. if (m_state == SOCKET_STATE_CONNECTING)
  7. {
  8. int error = 0;
  9. socklen_t len = sizeof(error);
  10. #ifdef _WIN32
  11. getsockopt(m_socket, SOL_SOCKET, SO_ERROR, (char*)&error, &len);
  12. #else
  13. getsockopt(m_socket, SOL_SOCKET, SO_ERROR, (void*)&error, &len);
  14. #endif
  15. if (error) {
  16. m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
  17. } else {
  18. m_state = SOCKET_STATE_CONNECTED;
  19. m_callback(m_callback_data, NETLIB_MSG_CONFIRM, (net_handle_t)m_socket, NULL);
  20. }
  21. }
  22. else
  23. {
  24. m_callback(m_callback_data, NETLIB_MSG_WRITE, (net_handle_t)m_socket, NULL);
  25. }
  26. }

OnWrite()函数则根据m_state标识检测socket是否是尝试连接的socket(connect函数中的socket),用于判断socket是否已经连接成功,反之则是与客户端保持连接的socket,调用预先设置的回调函数。

[cpp] view plain copy

  1. void CBaseSocket::OnClose()
  2. {
  3. m_state = SOCKET_STATE_CLOSING;
  4. m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
  5. }

OnClose()方法将标识m_state设置为需要关闭状态,并调用预先设置的回调函数。

每个服务程序都使用一个stl hash_map来管理所有的socket,键是socket句柄,值是CBaseSocket对象指针:

[cpp] view plain copy

  1. typedef hash_map<net_handle_t, CBaseSocket*> SocketMap;
  2. SocketMap g_socket_map;

所以在删除或者新增socket时,实际上就是从这个hash_map中删除或者向这个hash_map中增加对象。多线程操作,需要一个锁来进行保护:

[cpp] view plain copy

  1. void CEventDispatch::AddEvent(SOCKET fd, uint8_t socket_event)
  2. {
  3. CAutoLock func_lock(&m_lock);
  4. if ((socket_event & SOCKET_READ) != 0)
  5. {
  6. FD_SET(fd, &m_read_set);
  7. }
  8. if ((socket_event & SOCKET_WRITE) != 0)
  9. {
  10. FD_SET(fd, &m_write_set);
  11. }
  12. if ((socket_event & SOCKET_EXCEP) != 0)
  13. {
  14. FD_SET(fd, &m_excep_set);
  15. }
  16. }

代码CAutoLock func_lock(&m_lock);即保护该hash_map的锁对象。

而管理以上功能的是一个单例类CEventDispatch,所以不难才出CEventDispatch提供的接口:

[cpp] view plain copy

  1. class CEventDispatch
  2. {
  3. public:
  4. virtual ~CEventDispatch();
  5. void AddEvent(SOCKET fd, uint8_t socket_event);
  6. void RemoveEvent(SOCKET fd, uint8_t socket_event);
  7. void AddTimer(callback_t callback, void* user_data, uint64_t interval);
  8. void RemoveTimer(callback_t callback, void* user_data);
  9. void AddLoop(callback_t callback, void* user_data);
  10. void StartDispatch(uint32_t wait_timeout = 100);
  11. void StopDispatch();
  12. bool isRunning() {return running;}
  13. static CEventDispatch* Instance();
  14. protected:
  15. CEventDispatch();
  16. private:
  17. void _CheckTimer();
  18. void _CheckLoop();
  19. typedef struct {
  20. callback_t callback;
  21. void* user_data;
  22. uint64_t interval;
  23. uint64_t next_tick;
  24. } TimerItem;
  25. private:
  26. #ifdef _WIN32
  27. fd_set m_read_set;
  28. fd_set m_write_set;
  29. fd_set m_excep_set;
  30. #elif __APPLE__
  31. int m_kqfd;
  32. #else
  33. int m_epfd;
  34. #endif
  35. CLock m_lock;
  36. list<TimerItem*> m_timer_list;
  37. list<TimerItem*> m_loop_list;
  38. static CEventDispatch* m_pEventDispatch;
  39. bool running;
  40. };

其中StartDispatch()和StopDispatcher()分别用于启动和停止整个循环流程。一般在程序初始化的时候StartDispatch(),在程序退出时StopDispatcher()。

原文发布于微信公众号 - 高性能服务器开发(easyserverdev)

原文发表时间:2018-03-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏转载gongluck的CSDN博客

打开文件open()函数的使用方法详解

头文件:#include <sys/types.h>    #include <sys/stat.h>    #include <fcntl.h> 定义函数...

2915
来自专栏向治洪

Android打包遇到的那些坑

说说今天打包遇到的坑,由于线上有个支付的bug需要紧急修复,而我们的项目又没有使用热修复,所以只能通过编译打包等传统流程,还好android上线比较快。 说说我...

21310
来自专栏软件开发

HTML5 学习总结(五)——WebSocket与消息推送

B/S结构的软件项目中有时客户端需要实时的获得服务器消息,但默认HTTP协议只支持请求响应模式,这样做可以简化Web服务器,减少服务器的负担,加快响应速度,因为...

4038
来自专栏小筱月

java 开发 websocket 网页端聊天室

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

5632
来自专栏smy

php获取用户真实IP和防刷机制

X-Forwarded-For(XFF):  是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段, 格式:c...

4702
来自专栏三丰SanFeng

Linux进程间通信(一) - 管道

管道(pipe) 普通的Linux shell都允许重定向,而重定向使用的就是管道。 例如:ps | grep vsftpd .管道是单向的、先进先出的、无结构...

2507
来自专栏向治洪

android打包方法超过65k错误

近日,Android Developers在Google+上宣布了新的Multidex支持库,为方法总数超过65K的Android应用提供了官方支持。 如果...

1805
来自专栏PingCAP的专栏

使用 Ansible 安装部署 TiDB

多机部署 TiDB 太繁琐?快来尝试我们的 TiDB 一键安装吧。本文介绍基于 Ansible Playbook 实现 TiDB 和监控组件的自动化安装和配置,...

1.4K0
来自专栏bboysoul

网站信息收集工具RED_HAWK

安装使用很简单,首先安装上php,然后git clone下来用php运行就好了 git clone https://github.com/Tuhinshubh...

1142
来自专栏Young Dreamer

webpack中tree-shaking技术介绍

之前介绍过webpack3的新特性,里面提到webpack2支持了ES6的import和export,不需要将ES6的模块先转成CommonJS模块,然后再进行...

3035

扫码关注云+社区

领取腾讯云代金券