说明
此篇也是遵从当时的文档,不做太多修正。
系统功能模块图
系统流程图
系统数据流图
负载均衡策略图
概要设计
- 服务器端:
- 主线程——网络监听线程;
- 登录和注册线程池;
- 维护游戏房间列表线程;
- 游戏逻辑线程。
- 客户端:
- 大厅进程(接收通信包线程、主逻辑线程);
- 五子棋进程(接收通信包线程、主逻辑线程)。
通信协议:
- 客户端到服务器的登录消息、
- 客户端到服务器的注册消息、
- 客户端到服务器的检测ID消息、
- 客户端到服务器的索求房间列表消息、
客户端到服务器的进某个房间消息;
服务器到客户端的登录回应消息、
- 服务器到客户端的注册回应消息、
- 服务器到客户端的房间列表消息、
服务器到客户端的进某个房间的回应消息;
客户端到服务器的进某个桌子座位的消息、
客户端到服务器的房间聊天消息;
服务器到客户端的进某个桌子座位的回应消息、
服务器到客户端的聊天转发消息;
客户端到服务器的游戏数据消息、
- 客户端到服务器的游戏状态消息、
客户端到服务器的聊天消息;
服务器到客户端的游戏数据转发消息、
- 服务器到客户端的准备回复消息、
- 服务器到客户端的聊天转发消息、
服务器到客户端的桌子玩家列表消息;
服务器到客户端的广播游戏状态消息。
E-R 图
数据库表设计
详细设计之——通信协议的制订
枚举所有要用到的消息标识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| typedef enum { ClientToLogin, ClientToRegister, ClientToCheckUid, ClientToHall_RoomList, ClientToHall_RoomNum,
LoginToClient, RegisterToClient, HallToClient_RoomList, HallToClient_RoomNum,
ClientToRoom_TableNum, ClientToRoom_Chat,
RoomToClient_TableNum, RoomToClient_Chat,
ClientToTable_GameData, ClientToTable_State, ClientToTable_Chat,
TableToClient_GameData, TableToClient_State, TableToClient_Chat, TableToClient_UserList,
Room_Mate_State }MSGTYPE;
|
各个消息的数据结构设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| typedef struct { unsigned int UserID; char PWD[20]; }ClientToLoginMsg;
typedef struct { unsigned int UserID; unsigned int RoomID; int image; char NickName[20]; }ClientToHall_RoomNumMsg;
typedef struct { unsigned int UserID; char PWD[20]; char NickName[20]; int Image; }ClientToRegisterMsg;
typedef struct { int game_rank; int online; int MaxPlayers; int MaxTables; int minLevel; int maxLevel; }shm_roominfo;
typedef struct { unsigned int RoomID; shm_roominfo s_room; }send_roominfo;
|
1 2 3 4 5 6 7
| typedef struct { unsigned int UserID; unsigned int RoomID; int TableID; int SeatID; }ClientToHall_TableNumMsg;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| typedef struct { unsigned int UserID; unsigned int RoomID; int state; }ClientToTable_StateMsg;
typedef struct { int turn; int color; }TableToClient_StateMsg;
typedef struct { int x; int y; int is_win; }TableToClient_GameDataMsg;
typedef struct { unsigned int UserID; unsigned int RoomID; int x; int y; }ClientToTable_GameDataMsg;
typedef struct { unsigned int UserID; unsigned int RoomID; char buffer[50]; }ClientToTable_ChatMsg;
|
详细设计之—— Socket 发包形式统一
基础发包(收包亦然)均为发送字节流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| int ag_writen(SOCKET sock, const void *vptr, size_t n) {
size_t nleft; size_t nwritten; const char *ptr;
ptr = (const char *)vptr; nleft = n; while (nleft > 0) { nwritten = send(sock, ptr, nleft, 0); if (nwritten <= 0) { if (errno == EINTR) nwritten = 0; else return (-1); } nleft -= nwritten; ptr += nwritten; } return (n); }
int ag_readn(SOCKET sock, void *vptr, size_t n) {
size_t nleft; size_t nread; char *ptr;
ptr = (char *)vptr; nleft = n; while (nleft > 0) { nread = recv(sock, ptr, nleft, 0); if (nread < 0) { if (errno == EINTR) nread = 0; else if (errno == EAGAIN) break; else return (-1); } else if (nread == 0) break;
nleft -= nread; ptr += nread; } return (n - nleft); }
|
接收或发送错误时,服务器端直接关闭相应客户端的套接字,以保护服务器。
详细设计之——服务器的设计
配置文件的设计
格式:字段名 + “=” + 字段值,包括数据库连接配置文件、房间配置文件等。
ccache 库的应用
一个共享内存的开源库,修改一些自己系统需求不同的部分。
MySQL 的操作封装
将自己常用的几个操作所用的API封装到一起:
1 2 3 4
| void Sql_connect(MYSQL *mysql, const char *host,const char *user, const char *pwd, const char *db); void RunSelect(MYSQL *mysql, MYSQL_RES *res, const char *sqlstr, char *returned); void RunSql(MYSQL *mysql, const char *sqlstr); void Sql_close(MYSQL *mysql);
|
主程序设计
(1)主线程——分析配置及监听线程:
1
| int main(int argc, char **argv)
|
将配置文件里的信息读到程序里,并开始监听连接。主线程建立两个 cache,用来分别存放在线玩家信息和房间列表及在线信息。
(2)登录和注册线程池:
1 2 3 4 5 6 7 8 9
| void *thread_main(void *args); { switch(MSGTYPE) { case 1: case 2: } }
|
这个线程池的线程数量通常为 CPU 的数量乘以 2。由于各个服务器的 CPU 数量不一样,将这个设计为启动服务器程序时的一个参数。
(3)维护游戏房间列表线程:
1
| void *thread_get_roomlist(void *args);
|
此线程定时遍历房间 cache,刷新在线人数,并遍历用户列表,将在线房间信息发送出去。
(4)游戏逻辑线程:
1 2 3 4 5 6 7 8 9
| void *thread_epoll_gamelogic(void *args); { switch(MSGTYPE) { case 1: case 2: } }
|
接收、处理、发送进出房间、进出桌子、游戏数据、聊天的消息。
(5)统一出错处理的函数:
详细设计之——客户端的设计
大厅进程的设计
(1)主逻辑线程:
- 处理所有客户端大厅的界面操作及发送消息到服务器;
- 处理接收通信包线程转发来的消息。
- 处理五子棋进程发送过来的数据及转发过来的服务器消息。
- 启动五子棋进程时杀死接收通信包线程;
- 关闭五子棋进程时重启接收通信包线程。
(2)接收通信包线程:
1 2 3 4 5 6
| switch(MSGTYPE) { case 1: case 2: }
|
用于接收服务器发过来的消息,根据消息类型直接处理或者转发给主线程。
(3)给五子棋进程的数据:
通过管道,传递子进程初始化的平台需要数据。
五子棋进程
(1)主逻辑线程:
- 处理大厅进程发送过来的数据;
- 处理五子棋界面上的所有操作及发送消息到服务器;
- 处理接收通信包线程发送过来的服务器消息、中间处理后的数据。
(2)接收通信包线程:
1 2 3 4 5 6
| switch(MSGTYPE) { case 1: case 2: }
|
中间处理服务器发送过来的消息及转发一些消息到主线程。
(3)给大厅的一些平台数据转发:
通过给大厅进程发送 WM_COPYDATA
消息来转发同房间玩家的桌子信息实时刷新及房间列表在线列表。
客户端套接字的共用
为了降低开销和简化同步,客户端的两个进程公用同一个套接字,具体设计为:
(1)大厅进程建立一个套接字与服务器建立连接,当新建五子棋进程时,获得子进程的进程号,并发送给大厅进程。
(2)大厅进程根据这个子进程(五子棋进程)号,用函数 WSADuplicateSocket
将自己的套接字复制一个并传递给子进程。子进程利用这个父进程传递过来的套接字跟服务器进行通信。
客户端的数据同步
当大厅进程里建立五子棋进程后,立即关闭大厅自己的接收网络包线程;五子棋进程开启接收网络包线程。
当五子棋玩毕,关掉此进程时,大厅检测到子进程的关闭,立即重启大厅的接收网络包线程。
这样可以屏蔽共享套接字跟服务器通信的互相干扰。