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