日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

online游戏服务器架构--网络架构

發(fā)布時間:2023/12/13 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 online游戏服务器架构--网络架构 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

啟動:父進程啟動;子進程啟動;網(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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。