online游戏服务器架构--网络架构
啟動:父進程啟動;子進程啟動;網(wǎng)絡架構。
每個父進程攜帶N個子進程,子進程負責處理業(yè)務邏輯和其它數(shù)據(jù),而父進程只是將客戶端的請求路由到各個子進程,路由的策略非常簡單,父進程將請求包按照輪流的法則分發(fā)到這N個子進程。
子進程接收到請求包的時候,它便開始處理,處理完后再將結果反還給父進程。注意,子進程并不處理網(wǎng)絡連接,它并不知道請求包的源的信息,它只處理業(yè)務,相反地,父進程并不知道請求包的內(nèi)容,它的任務就是處理連接。
父子進程之間通過共享內(nèi)存進行通信,具體來講就是父進程將請求包放入和對應子進程共享的內(nèi)存中,然后通過一個管道喚醒子進程,子進程探測到管道消息以后就從共享內(nèi)存將請求拉出來然后進行處理,處理完畢后再將結果放回到共享內(nèi)存,然后同樣喚醒父進程,父進程被喚醒之后便拉出子進程的回復數(shù)據(jù),最后通過它自己保存的連接返回給客戶端。
這個服務器解除了接收數(shù)據(jù)和處理數(shù)據(jù)之間的耦合,便于進行任何一邊的擴展,不像那種消息映射服務器,直接在本進程內(nèi)部通過分發(fā)回調(diào)函數(shù)來處理業(yè)務邏輯,或者用線程的方式進行處理,線程的方式雖然解決了吞吐量的問題,但是無法解決穩(wěn)定性的問題,必須默認所有的數(shù)據(jù)都是安全的或者開發(fā)出繁復的處理邏輯來處理異常情況,額外增加了服務器的負擔。子進程的關于業(yè)務邏輯的處理方式非常類似于那種消息映射服務器,不同之處在于,典型的消息映射服務器是從網(wǎng)絡上將數(shù)據(jù)拉回,而該online服務器卻是從共享內(nèi)存中將數(shù)據(jù)拉回,多了共享內(nèi)存這么一個中間層。
關于業(yè)務邏輯的處理還有一個類似的層次,就是online子進程和數(shù)據(jù)庫之間的關系,它們通過一個數(shù)據(jù)庫代理(DBProxy)來將子進程的處理邏輯和數(shù)據(jù)庫之間的耦合解除,并且這數(shù)據(jù)庫代理還可以隱藏數(shù)據(jù)庫的訪問接口,只有代理知道后端連接了什么數(shù)據(jù)庫,而處理邏輯不必知道,它只需要將訪庫請求作為網(wǎng)絡請求發(fā)送給數(shù)據(jù)庫代理就好了,然后用消息映射服務器的方式處理數(shù)據(jù)庫代理的回復。數(shù)據(jù)庫只管保存數(shù)據(jù),而不管這些數(shù)據(jù)之間的除了關系模型之外的額外事宜,比如有效性驗證之類的,所有的數(shù)據(jù)驗證和處理工作在online子進程那里進行。這樣處理的優(yōu)點就是易于擴展新業(yè)務,缺點就是要來回幾次的進程訪庫,因為每次只取當次的數(shù)據(jù),在業(yè)務處理過程中可能還需要別的數(shù)據(jù)…不過缺點可以通過高速網(wǎng)絡和高性能數(shù)據(jù)庫以及數(shù)據(jù)庫代理服務器來彌補。
for ( ; i != bc->bind_num; ++i ) {
bind_config_elem_t* bc_elem = &(bc->configs[i]);
shmq_create(bc_elem); //通過mmap的方式創(chuàng)建共享內(nèi)存
… //錯誤處理
} else if (pid > 0) {
close_shmq_pipe(bc, i, 0);
do_add_conn(bc_elem->sendq.pipe_handles[0], PIPE_TYPE_FD, 0, bc_elem);
} else {
run_worker_process(bc, i, i + 1);
}
}
run_worker_process函數(shù)開始了子進程的歷程,可以看到最后這個函數(shù)調(diào)用了一個叫做net_loop的無限循環(huán),這個函數(shù)在父進程初始化完畢后也最終調(diào)用,原型如下:
int net_loop(int timeout, int max_len, int is_conn);
該函數(shù)通過最后一個參數(shù)is_conn來區(qū)分是子進程還是父進程,函數(shù)內(nèi)部實現(xiàn)也是通過該參數(shù)一分為二的,online的父進程負責網(wǎng)絡收發(fā),主要是基于epoll的,epoll已經(jīng)被證明擁有很高的性能,在linux平臺上的性能測試已經(jīng)超越了原來的poll/select模型,甚至比windows的IO完成端口在大負載,高并發(fā)環(huán)境下表現(xiàn)更加出色。在net_loop中用epoll_wait等待有事件的文件描述符,然后判斷文件描述符的類型(套結字在創(chuàng)建之初就將描述符和類型等信息打包成一個數(shù)據(jù)結構了),如果是管道文件的事件,那么肯定是不需要處理數(shù)據(jù)的,僅僅察看事件類型以及判斷是否父進程就可以判斷發(fā)生了什么事了,由于子進程根本就不會將套結字描述符加入到epoll監(jiān)控向量,因此子進程只能有管道類型的事件發(fā)生,注意這里不涉及online子進程和DB的通信。接下來的net_loop中關于epoll的處理流程就是父進程的事了,具體過程就是處理套結字類型的文件描述符了,就是從套結字接收數(shù)據(jù),然后放到和一個子進程共享的內(nèi)存區(qū)域中,最后往子進程管道里寫一個數(shù)據(jù),告訴子進程現(xiàn)在該處理業(yè)務邏輯了,子進程在net_loop中監(jiān)控到管道事件之后,最終調(diào)用net_loop最后的handle_recv_queue()函數(shù),該函數(shù)開始處理業(yè)務邏輯:
if(!is_conn) {
#ifdef USE_CMD_QUEUE
handle_cmd_busy_sprite(); //handle the busy sprite list first
#endif
handle_recv_queue();
handle_timer();
}
以上是net_loop的大致流程,對于父進程如何將請求路由給子進程有兩種選擇,一種是父進程網(wǎng)絡服務器按照某種策略比如負載均衡采取輪換路由,另一種就是將選擇留給用戶,用戶登錄online之前首先登錄一個switch服務器,自行選擇online子進程,每個online子進程都有一個ID,用戶選擇后就用這個ID作為數(shù)據(jù)打包,另外switch服務器上的online子進程鏈表中包含了足夠的其對應于父進程的IP地址和端口信息,然后向online子進程對應的父進程發(fā)送LOGIN包,父進程在net_loop中最終調(diào)用net_recv,然后解出LOGIN包,由于該包中包含了其子進程的id,而這個id又和其與子進程的共享內(nèi)存相關聯(lián),一個數(shù)據(jù)結構最起碼關聯(lián)了父進程接收的套結字描述符,子進程ID,父子進程的共享內(nèi)存緩沖區(qū)這三個元素。
關鍵數(shù)據(jù)結構:
typedef struct bind_config_elem {
int online_id;
char online_name[16];
char bind_ip[16]; //邦定的ip地址
in_port_t bind_port; //邦定的端口
char gameserv_ip[16]; //游戲服務器的ip
in_port_t gameserv_port;
char gameserv_test_ip[16];
in_port_t gameserv_test_port;
struct shm_queue sendq; //發(fā)送緩沖區(qū),被分割成一個一個的塊,因此叫隊列
struct shm_queue recvq; //接收緩沖區(qū),被分割成一個一個的塊,因此叫隊列
} bind_config_elem_t;
該結構描述了每一個傳輸套結字都應該擁有的一個結構,也就是每一個子進程一個這樣的結構
typedef struct bind_config {
int online_start_id;
int bind_num;
bind_config_elem_t configs[MAX_LISTEN_FDS];
} bind_config_t;
這個結構是上面結構的容器,main中的bind_config_elem_t* bc_elem = &(bc->configs[i]);體現(xiàn)了一切,所有的一切都是從配置文件中讀取的。
typedef struct shm_head {
volatile int head;
volatile int tail;
atomic_t blk_cnt;
} __attribute__ ((packed)) shm_head_t;
這個結構分割了一個緩沖區(qū),將一個連續(xù)的緩沖區(qū)分割成了一個隊列
struct shm_queue {
shm_head_t* addr;
u_int length;
int pipe_handles[2];
};
這個結構代表了一個緩沖區(qū),分割的過程在shm_head_t中體現(xiàn)。
struct epinfo {
struct fdinfo *fds;
struct epoll_event *evs;
struct list_head close_head;
struct list_head etin_head;
int epfd;
int maxfd;
int fdsize;
int count;
};
這個結構代表了epoll事件。
在LOGIN包被父進程解析到的時候:
if ((ntohl(proto->cmd) == PROTO_LOGIN) && (epi.fds[fd].bc_elem == 0) )為真,接著:
uint16_t online_id = ntohs(*(uint16_t*)(proto->body)); //得到用戶選擇的online_id
…
epi.fds[fd].bc_elem = &(bc->configs[online_id - bc->online_start_id]); //得到該id對應的config結構體。
得到了bind_config_elem_t結構體之后就可以將請求包轉(zhuǎn)發(fā)到從該結構體中取出的共享內(nèi)存緩沖區(qū)了,然后將請求包放到這個內(nèi)存中。所有的請求包中,LOGIN請求包是父進程直接處理的,后續(xù)的游戲邏輯請求包由子進程處理,另外子進程雖然不處理網(wǎng)絡連接,但是對于和數(shù)據(jù)庫代理服務器和switch中心跳服務器的連接還是要自己處理的,因此子進程中也有網(wǎng)絡相關的內(nèi)容,在net_rcv中有以下片斷:
if (!is_conn) {
handle_process(epi.fds[fd].cb.recvptr, epi.fds[fd].cb.rcvprotlen, fd, 0);
}
這個就是直接處理數(shù)據(jù)庫代理以及心跳的處理過程。另外關于網(wǎng)絡架構中還有一點就是鏈表的使用,在net_rcv中首先調(diào)用do_read_conn讀取網(wǎng)絡數(shù)據(jù),但是一旦當前積壓的未處理的數(shù)據(jù)達到了一個最大值的時候,后續(xù)的請求就要丟到鏈表中,然后在下一輪net_loop中接收新的數(shù)據(jù)前優(yōu)先處理之;在net_loop中有一句:
if (is_conn) handle_send_queue();
該句的意思就是說,如果是父進程,那么首先處理發(fā)送隊列,這些發(fā)送隊列中的數(shù)據(jù)都是子進程放入的請求包的回復,父進程優(yōu)先將這些回復返回給各個客戶端:
static inline void handle_send_queue()
{
struct shm_block *mb;
struct shm_queue *q;
int i = 0;
for ( ; i != bindconf.bind_num; ++i ) {
q = &(bindconf.configs[i].sendq);
while ( shmq_pop(q, &mb) == 0 ) {
schedule_output(mb);
}
}
}
雖然這個過程比較優(yōu)先,但是更優(yōu)先是前面說的過程,就是處理積壓鏈表,下面片斷在上面的之前調(diào)用:
list_for_each_safe (p, l, &epi.close_head) { //優(yōu)先便利需要關閉的套結字,第一時間關閉連接
fi = list_entry (p, struct fdinfo, list);
if (fi->cb.sendlen > 0)
do_write_conn (fi->sockfd);
do_del_conn (fi->sockfd, 0);
}
list_for_each_safe (p, l, &epi.etin_head) { //優(yōu)先處理積壓隊列,提高響應速度
fi = list_entry (p, struct fdinfo, list);
if (net_recv(fi->sockfd, max_len, is_conn) == -1)
do_del_conn(fi->sockfd, is_conn);
}
該服務器中大量運用了鏈表,此鏈表的定義就是list_head,是從linux內(nèi)核中抽取出來的。
接收新連接的時候,在net_loop中:
if (epi.fds[fd].type == LISTEN_TYPE_FD) {
while (do_open_conn(fd, is_conn) > 0);
接收了新的連接,并且加入了一個列表,將新連接的套結字描述符和一個空的bind_config_elem_t相關聯(lián),注意此時并沒有初始化這個bind_config_elem_t,因為在LOGIN包到來之前還不知道和哪一個bind_config_elem_t相關聯(lián),該函數(shù)僅僅初始化了一個epi結構。
?
總結
以上是生活随笔為你收集整理的online游戏服务器架构--网络架构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis——发布和订阅
- 下一篇: UNIX(多线程):24---哪些STL