什么是Socket
Socket接口是TCP/IP網絡的API,Socket接口定義了許多函數或例程,程序員可以用它們來開發TCP/IP網絡上的應用程序。要學Internet上的TCP/IP網絡編程,必須理解Socket接口。
Socket接口設計者最先是將接口放在Unix操作系統里面的。如果了解Unix系統的輸入和輸出的話,就很容易了解Socket了。網絡的Socket數據傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具有一個類似于打開文件的函數調用Socket(),該函數返回一個整型的Socket描述符,隨后的連接建立、數據傳輸等操作都是通過該Socket實現的。常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數據報式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對于面向連接的TCP服務應用;數據報式Socket是一種無連接的Socket,對應于無連接的UDP服務應用。
Socket建立
為了建立Socket,程序可以調用Socket函數,該函數返回一個類似于文件描述符的句柄。socket函數原型為:
int socket(int domain, int type, int protocol);
domain指明所使用的協議族,通常為PF_INET,表示互聯網協議族(TCP/IP協議族);type參數指定socket的類型:SOCK_STREAM 或SOCK_DGRAM,Socket接口還定義了原始Socket(SOCK_RAW),允許程序使用低層協議;protocol通常賦值"0"。Socket()調用返回一個整型socket描述符,你可以在后面的調用使用它。
Socket描述符是一個指向內部數據結構的指針,它指向描述符表入口。調用Socket函數時,socket執行體將建立一個Socket,實際上"建立一個Socket"意味著為一個Socket數據結構分配存儲空間。Socket執行體為你管理描述符表。
兩個網絡程序之間的一個網絡連接包括五種信息:通信協議、本地協議地址、本地主機端口、遠端主機地址和遠端協議端口。Socket數據結構中包含這五種信息。
Socket配置
通過socket調用返回一個socket描述符后,在使用socket進行網絡傳輸以前,必須配置該socket。面向連接的socket客戶端通過調用Connect函數在socket數據結構中保存本地和遠端信息。無連接socket的客戶端和服務端以及面向連接socket的服務端通過調用bind函數來配置本地信息。
Bind函數將socket與本機上的一個端口相關聯,隨后你就可以在該端口監聽服務請求。Bind函數原型為:
int bind(int sockfd,struct sockaddr *my_addr, int addrlen);
Sockfd是調用socket函數返回的socket描述符,my_addr是一個指向包含有本機IP地址及端口號等信息的sockaddr類型的指針;addrlen常被設置為sizeof(struct sockaddr)。
struct sockaddr結構類型是用來保存socket信息的:
struct sockaddr {
unsigned short sa_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14 字節的協議地址 */
};
sa_family一般為AF_INET,代表Internet(TCP/IP)地址族;sa_data則包含該socket的IP地址和端口號。
另外還有一種結構類型:
struct sockaddr_in {
short int sin_family; /* 地址族 */
unsigned short int sin_port; /* 端口號 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填充0 以保持與struct sockaddr同樣大小 */
};
這個結構更方便使用。sin_zero用來將sockaddr_in結構填充到與struct sockaddr同樣的長度,可以用bzero()或memset()函數將其置為零。指向sockaddr_in 的指針和指向sockaddr的指針可以相互轉換,這意味著如果一個函數所需參數類型是sockaddr時,你可以在函數調用的時候將一個指向sockaddr_in的指針轉換為指向sockaddr的指針;或者相反。
使用bind函數時,可以用下面的賦值實現自動獲得本機IP地址和隨機獲取一個沒有被占用的端口號:
my_addr.sin_port = 0; /* 系統隨機選擇一個未被使用的端口號 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本機IP地址 */
通過將my_addr.sin_port置為0,函數會自動為你選擇一個未占用的端口來使用。同樣,通過將my_addr.sin_addr.s_addr置為INADDR_ANY,系統會自動填入本機IP地址。
注意在使用bind函數是需要將sin_port和sin_addr轉換成為網絡字節優先順序;而sin_addr則不需要轉換。
計算機數據存儲有兩種字節優先順序:高位字節優先和低位字節優先。Internet上數據以高位字節優先順序在網絡上傳輸,所以對于在內部是以低位字節優先方式存儲數據的機器,在Internet上傳輸數據時就需要進行轉換,否則就會出現數據不一致。
下面是幾個字節順序轉換函數:
·htonl():把32位值從主機字節序轉換成網絡字節序
·htons():把16位值從主機字節序轉換成網絡字節序
·ntohl():把32位值從網絡字節序轉換成主機字節序
·ntohs():把16位值從網絡字節序轉換成主機字節序
Bind()函數在成功被調用時返回0;出現錯誤時返回"-1"并將errno置為相應的錯誤號。需要注意的是,在調用bind函數時一般不要將端口號置為小于1024的值,因為1到1024是保留端口號,你可以選擇大于1024中的任何一個沒有被占用的端口號。
連接建立
面向連接的客戶程序使用Connect函數來配置socket并與遠端服務器建立一個TCP連接,其函數原型為:
int connect(int sockfd, struct sockaddr *serv_addr,int addrlen);
Sockfd是socket函數返回的socket描述符;serv_addr是包含遠端主機IP地址和端口號的指針;addrlen是遠端地質結構的長度。Connect函數在出現錯誤時返回-1,并且設置errno為相應的錯誤碼。進行客戶端程序設計無須調用bind(),因為這種情況下只需知道目的機器的IP地址,而客戶通過哪個端口與服務器建立連接并不需要關心,socket執行體為你的程序自動選擇一個未被占用的端口,并通知你的程序數據什么時候到打斷口。
Connect函數啟動和遠端主機的直接連接。只有面向連接的客戶程序使用socket時才需要將此socket與遠端主機相連。無連接協議從不建立直接連接。面向連接的服務器也從不啟動一個連接,它只是被動的在協議端口監聽客戶的請求。
Listen函數使socket處于被動的監聽模式,并為該socket建立一個輸入數據隊列,將到達的服務請求保存在此隊列中,直到程序處理它們。
int listen(int sockfd, int backlog);
Sockfd是Socket系統調用返回的socket 描述符;backlog指定在請求隊列中允許的最大請求數,進入的連接請求將在隊列中等待accept()它們(參考下文)。Backlog對隊列中等待服務的請求的數目進行了限制,大多數系統缺省值為20。如果一個服務請求到來時,輸入隊列已滿,該socket將拒絕連接請求,客戶將收到一個出錯信息。
當出現錯誤時listen函數返回-1,并置相應的errno錯誤碼。
accept()函數讓服務器接收客戶的連接請求。在建立好輸入隊列后,服務器就調用accept函數,然后睡眠并等待客戶的連接請求。
int accept(int sockfd, void *addr, int *addrlen);
sockfd是被監聽的socket描述符,addr通常是一個指向sockaddr_in變量的指針,該變量用來存放提出連接請求服務的主機的信息(某臺主機從某個端口發出該請求);addrten通常為一個指向值為sizeof(struct sockaddr_in)的整型指針變量。出現錯誤時accept函數返回-1并置相應的errno值。
首先,當accept函數監視的socket收到連接請求時,socket執行體將建立一個新的socket,執行體將這個新socket和請求連接進程的地址聯系起來,收到服務請求的初始socket仍可以繼續在以前的 socket上監聽,同時可以在新的socket描述符上進行數據傳輸操作。
數據傳輸
Send()和recv()這兩個函數用于面向連接的socket上進行數據傳輸。
Send()函數原型為:
int send(int sockfd, const void *msg, int len, int flags);
Sockfd是你想用來傳輸數據的socket描述符;msg是一個指向要發送數據的指針;Len是以字節為單位的數據的長度;flags一般情況下置為0(關于該參數的用法可參照man手冊)。
Send()函數返回實際上發送出的字節數,可能會少于你希望發送的數據。在程序中應該將send()的返回值與欲發送的字節數進行比較。當send()返回值與len不匹配時,應該對這種情況進行處理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
recv()函數原型為:
int recv(int sockfd,void *buf,int len,unsigned int flags);
Sockfd是接受數據的socket描述符;buf 是存放接收數據的緩沖區;len是緩沖的長度。Flags也被置為0。Recv()返回實際上接收的字節數,當出現錯誤時,返回-1并置相應的errno值。
Sendto()和recvfrom()用于在無連接的數據報socket方式下進行數據傳輸。由于本地socket并沒有與遠端機器建立連接,所以在發送數據時應指明目的地址。
sendto()函數原型為:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
該函數比send()函數多了兩個參數,to表示目地機的IP地址和端口號信息,而tolen常常被賦值為sizeof (struct sockaddr)。Sendto 函數也返回實際發送的數據字節長度或在出現發送錯誤時返回-1。
Recvfrom()函數原型為:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
from是一個struct sockaddr類型的變量,該變量保存源機的IP地址及端口號。fromlen常置為sizeof (struct sockaddr)。當recvfrom()返回時,fromlen包含實際存入from中的數據字節數。Recvfrom()函數返回接收到的字節數或當出現錯誤時返回-1,并置相應的errno。
如果你對數據報socket調用了connect()函數時,你也可以利用send()和recv()進行數據傳輸,但該socket仍然是數據報socket,并且利用傳輸層的UDP服務。但在發送或接收數據報時,內核會自動為之加上目地和源地址信息。
結束傳輸
當所有的數據操作結束以后,你可以調用close()函數來釋放該socket,從而停止在該socket上的任何數據操作:
close(sockfd);
你也可以調用shutdown()函數來關閉該socket。該函數允許你只停止在某個方向上的數據傳輸,而一個方向上的數據傳輸繼續進行。如你可以關閉某socket的寫操作而允許繼續在該socket上接受數據,直至讀入所有數據。
int shutdown(int sockfd,int how);
Sockfd是需要關閉的socket的描述符。參數 how允許為shutdown操作選擇以下幾種方式:
·0-------不允許繼續接收數據
·1-------不允許繼續發送數據
·2-------不允許繼續發送和接收數據,
·均為允許則調用close ()
shutdown在操作成功時返回0,在出現錯誤時返回-1并置相應errno。
面向連接的Socket實例
代碼實例中的服務器通過socket連接向客戶端發送字符串"Hello, you are connected!"。只要在服務器上運行該服務器軟件,在客戶端運行客戶軟件,客戶端就會收到該字符串。
該服務器軟件代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define SERVPORT 3333 /*服務器監聽端口號 */
#define BACKLOG 10 /* 最大同時連接請求數 */
main()
{
int sockfd,client_fd; /*sock_fd:監聽socket;client_fd:數據傳輸socket */
struct sockaddr_in my_addr; /* 本機地址信息 */
struct sockaddr_in remote_addr; /* 客戶端地址信息 */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket創建出錯!"); exit(1);
}
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(SERVPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \
== -1) {
perror("bind出錯!");
exit(1);
}
if (listen(sockfd, BACKLOG) == -1) {
perror("listen出錯!");
exit(1);
}
while(1) {
sin_size = sizeof(struct sockaddr_in);
if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, \
&sin_size)) == -1) {
perror("accept出錯");
continue;
}
printf("received a connection from %s\n", inet_ntoa(remote_addr.sin_addr));
if (!fork()) { /* 子進程代碼段 */
if (send(client_fd, "Hello, you are connected!\n", 26, 0) == -1)
perror("send出錯!");
close(client_fd);
exit(0);
}
close(client_fd);
}
}
}
服務器的工作流程是這樣的:首先調用socket函數創建一個Socket,然后調用bind函數將其與本機地址以及一個本地端口號綁定,然后調用listen在相應的socket上監聽,當accpet接收到一個連接服務請求時,將生成一個新的socket。服務器顯示該客戶機的IP地址,并通過新的socket向客戶端發送字符串"Hello,you are connected!"。最后關閉該socket。
代碼實例中的fork()函數生成一個子進程來處理數據傳輸部分,fork()語句對于子進程返回的值為0。所以包含fork函數的if語句是子進程代碼部分,它與if語句后面的父進程代碼部分是并發執行的。
客戶端程序代碼如下:
#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define SERVPORT 3333
#define MAXDATASIZE 100 /*每次最大數據傳輸量 */
main(int argc, char *argv[]){
int sockfd, recvbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if (argc < 2) {
fprintf(stderr,"Please enter the server's hostname!\n");
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL) {
herror("gethostbyname出錯!");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket創建出錯!");
exit(1);
}
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr, \
sizeof(struct sockaddr)) == -1) {
perror("connect出錯!");
exit(1);
}
if ((recvbytes=recv(sockfd, buf, MAXDATASIZE, 0)) ==-1) {
perror("recv出錯!");
exit(1);
}
buf[recvbytes] = '\0';
printf("Received: %s",buf);
close(sockfd);
}
客戶端程序首先通過服務器域名獲得服務器的IP地址,然后創建一個socket,調用connect函數與服務器建立連接,連接成功之后接收從服務器發送過來的數據,最后關閉socket。
函數gethostbyname()是完成域名轉換的。由于IP地址難以記憶和讀寫,所以為了方便,人們常常用域名來表示主機,這就需要進行域名和IP地址的轉換。函數原型為:
struct hostent *gethostbyname(const char *name);
函數返回為hosten的結構類型,它的定義如下:
struct hostent {
char *h_name; /* 主機的官方域名 */
char **h_aliases; /* 一個以NULL結尾的主機別名數組 */
int h_addrtype; /* 返回的地址類型,在Internet環境下為AF-INET */
int h_length; /* 地址的字節長度 */
char **h_addr_list; /* 一個以0結尾的數組,包含該主機的所有地址*/
};
#define h_addr h_addr_list[0] /*在h-addr-list中的第一個地址*/
當 gethostname()調用成功時,返回指向struct hosten的指針,當調用失敗時返回-1。當調用gethostbyname時,你不能使用perror()函數來輸出錯誤信息,而應該使用herror()函數來輸出。
無連接的客戶/服務器程序的在原理上和連接的客戶/服務器是一樣的,兩者的區別在于無連接的客戶/服務器中的客戶一般不需要建立連接,而且在發送接收數據時,需要指定遠端機的地址。
阻塞和非阻塞
阻塞函數在完成其指定的任務以前不允許程序調用另一個函數。例如,程序執行一個讀數據的函數調用時,在此函數完成讀操作以前將不會執行下一程序語句。當服務器運行到accept語句時,而沒有客戶連接服務請求到來,服務器就會停止在accept語句上等待連接服務請求的到來。這種情況稱為阻塞(blocking)。而非阻塞操作則可以立即完成。比如,如果你希望服務器僅僅注意檢查是否有客戶在等待連接,有就接受連接,否則就繼續做其他事情,則可以通過將Socket設置為非阻塞方式來實現。非阻塞socket在沒有客戶在等待時就使accept調用立即返回。
#include <unistd.h>
#include <fcntl.h>
……
sockfd = socket(AF_INET,SOCK_STREAM,0);
fcntl(sockfd,F_SETFL,O_NONBLOCK);
……
通過設置socket為非阻塞方式,可以實現"輪詢"若干Socket。當企圖從一個沒有數據等待處理的非阻塞Socket讀入數據時,函數將立即返回,返回值為-1,并置errno值為EWOULDBLOCK。但是這種"輪詢"會使CPU處于忙等待方式,從而降低性能,浪費系統資源。而調用select()會有效地解決這個問題,它允許你把進程本身掛起來,而同時使系統內核監聽所要求的一組文件描述符的任何活動,只要確認在任何被監控的文件描述符上出現活動,select()調用將返回指示該文件描述符已準備好的信息,從而實現了為進程選出隨機的變化,而不必由進程本身對輸入進行測試而浪費CPU開銷。Select函數原型為:
int select(int numfds,fd_set *readfds,fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);
其中readfds、writefds、exceptfds分別是被select()監視的讀、寫和異常處理的文件描述符集合。如果你希望確定是否可以從標準輸入和某個socket描述符讀取數據,你只需要將標準輸入的文件描述符0和相應的sockdtfd加入到readfds集合中;numfds的值是需要檢查的號碼最高的文件描述符加1,這個例子中numfds的值應為sockfd+1;當select返回時,readfds將被修改,指示某個文件描述符已經準備被讀取,你可以通過FD_ISSSET()來測試。為了實現fd_set中對應的文件描述符的設置、復位和測試,它提供了一組宏:
FD_ZERO(fd_set *set)----清除一個文件描述符集;
FD_SET(int fd,fd_set *set)----將一個文件描述符加入文件描述符集中;
FD_CLR(int fd,fd_set *set)----將一個文件描述符從文件描述符集中清除;
FD_ISSET(int fd,fd_set *set)----試判斷是否文件描述符被置位。
Timeout參數是一個指向struct timeval類型的指針,它可以使select()在等待timeout長時間后沒有文件描述符準備好即返回。struct timeval數據結構為:
struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
POP3客戶端實例
下面的代碼實例基于POP3的客戶協議,與郵件服務器連接并取回指定用戶帳號的郵件。與郵件服務器交互的命令存儲在字符串數組POPMessage中,程序通過一個do-while循環依次發送這些命令。
#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define POP3SERVPORT 110
#define MAXDATASIZE 4096
main(int argc, char *argv[]){
int sockfd;
struct hostent *host;
struct sockaddr_in serv_addr;
char *POPMessage[]={
"USER userid\r\n",
"PASS password\r\n",
"STAT\r\n",
"LIST\r\n",
"RETR 1\r\n",
"DELE 1\r\n",
"QUIT\r\n",
NULL
};
int iLength;
int iMsg=0;
int iEnd=0;
char buf[MAXDATASIZE];
if((host=gethostbyname("your.server"))==NULL) {
perror("gethostbyname error");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket error");
exit(1);
}
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(POP3SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1){
perror("connect error");
exit(1);
}
do {
send(sockfd,POPMessage[iMsg],strlen(POPMessage[iMsg]),0);
printf("have sent: %s",POPMessage[iMsg]);
iLength=recv(sockfd,buf+iEnd,sizeof(buf)-iEnd,0);
iEnd+=iLength;
buf[iEnd]='\0';
printf("received: %s,%d\n",buf,iMsg);
iMsg++;
} while (POPMessage[iMsg]);
close(sockfd);
}
posted @
2010-02-24 13:32 暗夜教父 閱讀(271) |
評論 (0) |
編輯 收藏
國內站點:
http://www.gameres.com/ 中國游戲開發技術資源網(國內知名游戲技術站)
http://bbs.gamedev.csdn.net/web/default.aspx 中國游戲開發者CGD(論壇)
http://www.chaosstars.com/ 北京混沌星辰科技有限公司-ChaosStars(之前的開發GBA程序的小組)
http://www.cgfront.com/ 中國游戲@圖形前線(ver6.0) by inova-Tech
http://www.chinaai.org/index.asp 人工智能|模式識別|數字圖像處理—中國人工智能網
http://www.cad.zju.edu.cn/chinagraph/ 中國計算機圖形學教學研究會主頁
http://www.vrforum.cn/home.php 中國VR技術社區 -
www.vrforum.cnhttp://www.modchina.com/ceshi/ 中國MOD制作同盟社
http://bbs.99nets.com/ 99NETS網游模擬中文站
http://www.codingnow.com/2000/index.html 云風工作室
http://blog.codingnow.com/ 云風的 BLOG
http://gd.91.com/Modules/index.aspx 91游戲制作聯盟 -- 專業游戲開發權威網站、游戲制作人社區
http://www.npc6.com/rc/ 游戲人才網
http://www.ylog.net/ 異次元空間-首頁 游戲制作 游戲開發 BLOG空間
http://www.tuyasoft.com/ 涂鴉軟件--國內第一家商業3D引擎公司
http://lightwing.diy.myrice.com/ 琴心劍膽
http://www.dreamwork.cn/ 夢工廠軟件有限公司—DreamWork.CN
http://www.sf.org.cn/ 開發視界
http://www.hyzgame.org/ 絕情電腦游戲創作群
http://www.cnblogs.com/team/CG.html 博客園 - 計算機圖形學
http://www.chinagcn.com/ 歡迎來到游戲創造網!
http://www.npc6.com/ 何苦做游戲-游戲制作de文化...
http://www.d2-life.com/ 第二人生游戲開發俱樂部
http://www.gameassassin.com/ 代碼空間
http://creativesoft.home.shangdu.net/ 創新軟件編程樂園
http://www.gpgame.net/ 金點工作組
http://www.cmzj.com/ Welken Game
http://gd.91.com/zt/ogre/ OGRE引擎研究站--游戲制作聯盟
http://www.ogdev.net/ OGDEV.NET-網絡游戲研發網
http://www.azure.com.cn/ Azure Product(游戲技術,3D圖形學)
http://www.csie.ntu.edu.tw/~r89004/hive/ Hotball的小屋(計算機圖形學)
http://www.sycini.com/ 中國demoscene開發制作資源網
http://www.image2003.com/ 圖像圖形網
http://www.gamaura.com/ Gamaura - 屬于游戲人的專業信息交流平臺
http://www.3donline.cn/ (DxhViewer官方網站)
外國站點:
http://www.gamedev.net/ (國外權威游戲開發技術站點)
http://www.gdmag.com/homepage.htm (知名游戲開發雜志)
http://www.flipcode.com/ (知名圖形學站點,現在只剩下文章)
http://www.gameprogrammer.org/ (游戲開發教程)
http://www.gametutorials.com/ (游戲教程網,好像要收費)
http://www.garagegames.com/ (游戲引擎公司)
http://www.humus.ca/ (牛的網站,3D圖形編程,有很多源碼)
http://www.hugi.scene.org/ (很好的編程電子雜志,多數是DEMO INTRO技術)
http://www.gpgpu.org/ (GPU通用編程)
http://www.iddevnet.com/ (ID SOFT的SDK網站)
http://irrlicht.sourceforge.net/ (很好的開源3D引擎)
http://student.kuleuven.be/~m0216922/CG/index.html (圖形學教程)
http://nehe.gamedev.net/ (知名OPENGL編程教程)
http://www.xmission.com/~nate/index.html (OPENGL例子教學)
http://www.novodex.com/ (物理引擎)
http://developer.nvidia.com/ (顯卡大廠的開發資源網)
http://www.ogre3d.org/ (知名開源3D引擎)
http://www.openal.org/ (優秀的音頻編程庫)
http://www.opengl.org/ (OpenGL)
http://www.msi.unilim.fr/~porquet/glexts/index.html NVIDIA OpenGL Extension Specifications
http://www.typhoonlabs.com/ (SHADER編程)
http://www.clockworkcoders.com/oglsl/extensions.htm (GLSL教程)
http://www.cs.unc.edu/~davemc/Particle/ (粒子引擎)
http://www.markmark.net/clouds/ (實時云的渲染)
http://www.scene.org/ (Demo Intro的大站)
http://www.sgi.com/products/software/opengl/ (SGI公司的OPENGL編程資源)
http://www.shadertech.com/ (SHADER技術)
http://libsh.org/ (一種新圖形硬件編程語言)
http://demo-effects.sourceforge.net/ (開源的圖形特效例子合集合)
http://www.zfx.info/ (3D圖形學,Demo Intro)
http://www.ultimategameprogramming.com/ (游戲編程網,很多代碼)
http://www.beyond3d.com/ (3D硬件資迅站)
http://www.codesampler.com (大量圖形編程例子)
http://www.devmaster.net/ (游戲引擎數據庫,3D圖形編程文章)
http://astronomy.swin.edu.au/~pbourke/geometry/ (計算機圖形學)
http://ps2dev.org/ (PS2,PSP開發)
http://pspdev.ofcode.com/ (PSP開發)
http://www.yaz0r.net/blogs/ (FF10,FF12,王國之心系列游戲模型查看器作者的BLOG)
http://home.gna.org/cal3d/ (開源的3D模型動畫庫)
http://www.ozone3d.net/index.php (Realtime 3D Programming)
http://www.rakkarsoft.com/ (Multiplayer game network engine)
http://www.student.kuleuven.ac.be/~m0216922/CG/index.html
posted @
2010-01-13 12:32 暗夜教父 閱讀(675) |
評論 (0) |
編輯 收藏
用慣了VS,還是想試試VS+OGRE是個啥感覺,于是乎就配置了一下:
1.安裝VS2005 Professional + MSDN
2.安裝VS2005 SP1,不裝據說不能運行
3.安裝DirectX9 SDK Jun2007
4.安裝OGRE1.4.4
5.下載OGRE的時候看到一個debug symbols,于是也下了下來,安裝
6.安裝OgreSDK Wizard80_Eihort_v1_4_2
OK,用向導創建一個工程
編譯.........
通過
運行.........
報了個錯,55555555

瘋掉,翻遍google也沒找出個結果來
無奈,拖了N天之后,到今天想想,重裝一下OGRE吧,這次沒裝上那個debug symbols
編譯,運行,成功!!!
HOHO~仔細一看那個東西的文件名:Ogre_PDBs_vc8_v1.4.3,可能是跟OGRE版本不一致的原因吧
天知道ogre3d.org為什么把它們放在一塊,這不是誤導人么???
看看AppWizard的示例程序,效果不錯哈:


posted @
2009-12-25 16:45 暗夜教父 閱讀(504) |
評論 (0) |
編輯 收藏
摘要: 簡介:本教程基于Ogre Wiki上的Basic Tutorial系列,并依據筆者使用的vs2005+sp1+OgreSDK1.4.3開發環境簡化整理而來,其中穿插著筆者自己的理解。這是教程的第一部分,也是我的學習筆記。正文:凡是翻譯過幾篇技術類文章的人都深知從頭至尾忠實重現作者的原意是一件多么令人頭疼的事情。當我從諸多曾經許諾要翻譯的文章中爬出來的時候,我決定這次不做那樣一個“傻子&...
閱讀全文
posted @
2009-12-25 16:44 暗夜教父 閱讀(6059) |
評論 (1) |
編輯 收藏
建議讀者對應 HGE 的官方的例子:Tutorial 02 - Using input, sound and rendering
來閱讀本文
渲染:
在 HGE 中,四邊形是一種圖元,對應了結構體 hgeQuad,另外還有三角形圖元,對應
hgeTriple,為了渲染,我們現在需要使用 hgeQuad 結構體,這個結構體如下:
struct hgeQuad
{
hgeVertex v[4]; // 頂點描述了這個四邊形
HTEXTURE tex; // 紋理的句柄或者為0
int
blend; // 混合模式(blending mode)
};
HGE 中圖元對應的結構體總含有這3個部分:頂點,紋理句柄,混合模式
struct hgeVertex
{
float x,
y; // 屏幕的 x,y 坐標
float z; //
Z-order,范圍 [0, 1]
DWORD col; //
頂點的顏色
float tx,
ty; // 紋理的 x,y 坐標(賦值前需要規格化坐標間隔,使得
tx,ty 取值范圍在[0,1])
};
規格化坐標間隔在后面的例子中會談到。不過先要談到的一點是 tx,ty 的值超過 1 也是合法的
1. 顏色的表示:
顏色使用32位表示,從左開始,8位為 Alpha 通道,8位紅色,8位綠色,8位藍色
對于后24位,如果全部為0,表示黑色,如果全部為1,表示白色
2. 定義顏色的運算:
我們把顏色看成一個四維向量,即 alpha 通道,紅色,綠色,藍色這四個分量
<1> 顏色是可以相乘的
顏色的相乘是對應的四個分量分別相乘的結果,即:alpha 通道的值與 alpha
通道的值相乘,紅色的值與紅色的值相乘,綠色的值與綠色的值相乘,藍色的值與藍色的值相乘。
<2> 顏色是可以相加的
同上,對應分量相加。
顏色的每個分量使用浮點數表示,范圍是[0-1],相加操作可能導致溢出,一種處理的方式就是,如果溢出,則設定值為1。
3. 混合模式:
1)BLEND_COLORADD
表示頂點的顏色與紋理的紋元(texel)顏色相加,這使得紋理變亮,可見頂點顏色為 0x00000000
將不造成任何影響。
2)BLEND_COLORMUL
表示頂點的顏色與紋理的紋元顏色相乘,這使得紋理變暗,可見頂點顏色為 0xFFFFFFFF 將不造成任何影響。
注意:必須在1),2)中做一個選擇,且只能選擇1),2)中的一個。處理的對象是紋理顏色和頂點顏色。
這里有一個技巧:
如果我們需要在程序中顯示一個氣球,這個氣球的顏色不斷變化,這時候我們并不需要準備多張不同顏色的氣球紋理,而只需要一張白色的氣球紋理,設置
blend 為 BLEND_COLORMUL,白色的R,G,B值被表示成
1.0,也就是說,紋理顏色和頂點顏色相乘的結果是頂點的顏色,那么就可以通過修改頂點顏色,得到任意顏色的氣球了。
3)BLEND_ALPHABLEND
渲染時,將對象的像素顏色(而非頂點的顏色)與當前屏幕的對應像素顏色進行 alpha 混合。alpha 混合使用到 alpha
通道,對于兩個像素顏色進行如下操作,得到一個顏色:
R(C)=alpha*R(B)+(1-alpha)*R(A)
G(C)=alpha*G(B)+(1-alpha)*G(A)
B(C)=alpha*B(B)+(1-alpha)*B(A)
這里的BLEND_ALPHABLEND使用的是對象像素的顏色的 alpha 通道。可見如果對象像素顏色 alpha 通道為
0,那么結果就是只有當前屏幕的像素顏色,也就是常常說的 100% 透明,因此,我們可以理解 alpha
混合就是一個是圖像透明的操作,0 表示完全透明,255 表示完全不透明。
4)BLEND_ALPHAADD
渲染時,將對象的像素顏色與當前屏幕的對應像素顏色相加,結果是有了變亮的效果。
注意:這里的3),4)必選其一,且只能選其一。處理的對象是對象像素顏色和屏幕像素顏色。選擇
BLEND_ALPHABLEND 并且設定對象的 alpha 通道為 FF,可使此組參數無效。
5)BLEND_ZWRITE
渲染時,寫像素的 Z-order 到 Z-buffer
6)BLEND_NOZWRITE
渲染時,不寫像素的 Z-order 到 Z-buffer
這里一樣是二者選一
設置舉例:
quad.blend=BLEND_ALPHAADD | BLEND_COLORMUL |
BLEND_ZWRITE; // quad 為 hgeQuad 變量
4. HGE 渲染
1)定義和初始化 hgeQuad 結構體:
hgeQuad quad; // 定義四邊形
2)初始化 hgeQuad 變量:
// 設置混合模式
quad.blend=BLEND_ALPHAADD | BLEND_COLORMUL | BLEND_ZWRITE;
// 加載紋理
quad.tex = pHGE->Texture_Load("particles.png");
注意,讀取硬盤上資源的時候,可能會失敗,因此通常都需要檢查,例如:
if (!quad.tex)
{
MessageBox(NULL, "Load particles.png",
"Error", 0);
}
// 初始化頂點
for(int i=0;i<4;i++)
{
// 設置頂點的 z 坐標
quad.v[i].z=0.5f;
// 設置頂點的顏色,顏色的格式為 0xAARRGGBB
quad.v[i].col=0xFFFFA000;
}
// 這里假定載入的紋理大小為
128*128,現在截取由點(96,64),(128,64),(128,96),(96,96)這四個點圍成的圖形。
quad.v[0].tx=96.0/128.0; quad.v[0].ty=64.0/128.0; //
規格化坐標間隔
quad.v[1].tx=128.0/128.0; quad.v[1].ty=64.0/128.0;
quad.v[2].tx=128.0/128.0; quad.v[2].ty=96.0/128.0;
quad.v[3].tx=96.0/128.0; quad.v[3].ty=96.0/128.0;
注意,對于 hgeQuad 結構體,頂點 quad.v[0] 表示左上那個點,quad.v[1]
表示右上的點,quad.v[2] 表示右下的點,quad.v[3] 表示左下的點。
// 設置 hgeQuad 在屏幕中的位置
float x=100.0f, y=100.0f;
quad.v[0].x=x-16; quad.v[0].y=y-16;
quad.v[1].x=x+16; quad.v[1].y=y-16;
quad.v[2].x=x+16; quad.v[2].y=y+16;
quad.v[3].x=x-16; quad.v[3].y=y+16;
3)設置渲染函數(render function):
System_SetState(HGE_RENDERFUNC,RenderFunc);
RenderFunc 原型和幀函數一樣:
bool RenderFunc();
4)編寫 RenderFunc 函數:
bool RenderFunc()
{
pHGE->Gfx_BeginScene(); //
在如何渲染之前,必須調用這個函數
pHGE->Gfx_Clear(0); //
清屏,使用黑色,即顏色為 0
pHGE->Gfx_RenderQuad(&quad);
// 渲染
pHGE->Gfx_EndScene(); //
結束渲染,并且更新窗口
return false; // 必須返回 false
}
補充:Load 函數是和 Free 函數成對出現的,即在硬盤上加載了資源之后,需要 Free 它們,例如:
quad.tex = pHGE->Texture_Load("particles");
// ...
pHGE->Texture_Free(quad.tex);
這里不得不談一下規格化坐標間隔,而這之前,需要說說 Texture_GetWidth(xxx) 和
Texture_GetHeight(xxx) 函數,如果這樣調用:Texture_GetWidth(xxx)
獲取的是處于顯存中的紋理寬度,而 Texture_GetWidth(xxx, true)
獲取到的是圖像文件的寬度,需要特別主義的是,對于同一張紋理來說,這兩個值可能是不一樣的,那么在規格化坐標間隔的時候,應該明確的是,對于一個
w*h 圖像的圖片,那么對于圖中點(x,y)應該轉換成為:
tx = x / pHGE->GetWidth(xxx);
ty = y / pHGE->GetHeight(xxx);
而不能寫成:
tx = x / pHGE->GetWidth(xxx, true);
ty = y / pHGE->GetHeight(xxx, true);
這里要注意一下 x,y 的含義
最后再談一下 tx 和 ty,實際上 tx,ty 大于 1 也是合法的,例如:
tx = 800 / 64;
ty = 600 / 64;
這會使得圖片重復,而具體的含義,可以通過實現來體會
音效:
使用音效是很簡單的
1. 載入音效:
HEFFECT hEffect = pHGE->Effect_Load("sound.mp3");
2. 播放:
pHGE->Effect_PlayEx(hEffect);
或者 pHGE->Effect_Play(hEffect);
1)Effect_Play 函數只接受一個參數就是音效的句柄 HEFFECT xx;
2)Effect_PlayEx 函數較為強大,一共有四個參數:
HCHANNEL Effect_PlayEx(
HEFFECT
effect, // 音效的句柄
int volume =
100, // 音量,100為最大,范圍是[0, 100]
int pan =
0, // 范圍是[-100, 100],-100表示只使用左聲道,100表示只使用右聲道
float pitch =
1.0, // 播放速度,1.0
表示正常速度,值越大播放速度越快,值越小播放越慢。這個值要大于0才有效(不可以等于0)
bool loop =
false // 是否循環播放,false表示不循環
);
輸入:
僅僅需要調用函數 pHGE->Input_GetKeyState(HGEK_xxx);
來判斷輸入,應該在幀函數中調用它,例如:
bool FrameFunc()
{
if
(pHGE->Input_GetKeyState(HGEK_LBUTTOM))
// ...
if (pHGE->Input_GetKeyState(HGEK_UP))
// ...
}
posted @
2009-12-23 16:31 暗夜教父 閱讀(578) |
評論 (0) |
編輯 收藏
2009-12-7 22:19:36 LinkTalk.NET(909327571)
暗夜教父, 你說的6萬連接,每個連接每秒發1K包嗎?
2009-12-7 22:19:49 暗夜教父(199033)
en
2009-12-7 22:19:49 jack(357794482)
就是后面函數執行完成之后的結果給前面的原子嗎
2009-12-7 22:20:54 LinkTalk.NET(909327571)
機器配置如何?還有6萬個客戶端如何模擬的?
2009-12-7 22:21:41 暗夜教父(199033)
機器是AMD 雙核 3200+
2009-12-7 22:21:50 LinkTalk.NET(909327571)
另外就是每個連接上的時間間隔為1秒,沒有延遲嗎?
2009-12-7 22:22:12 暗夜教父(199033)
內存是4G,操作系統ubuntu 9.10
2009-12-7 22:22:53 暗夜教父(199033)
我人為的沒做這種延遲
2009-12-7 22:23:04 暗夜教父(199033)
服務器和客戶端都是這個配置
2009-12-7 22:23:16 暗夜教父(199033)
客戶端是用erlang模擬并發
2009-12-7 22:23:39 暗夜教父(199033)
不過創建6W個連接花費了一定的時間
2009-12-7 22:24:09 暗夜教父(199033)
對哦,這樣就不是6萬個一起并發出來的
2009-12-7 22:24:09 LinkTalk.NET(909327571)
大概多久?
2009-12-7 22:24:19 暗夜教父(199033)
沒統計時間
2009-12-7 22:24:42 暗夜教父(199033)
這里存在問題了
2009-12-7 22:25:27 暗夜教父(199033)
其實并發應該是 連接數 / 創建并發的時間
2009-12-7 22:25:27 LinkTalk.NET(909327571)
什么問題?
2009-12-7 22:25:49 LinkTalk.NET(909327571)
這個無所謂
2009-12-7 22:26:08 LinkTalk.NET(909327571)
只要你全部連接創建好后一直保持住就可以了
2009-12-7 22:26:23 暗夜教父(199033)
一直保持也不是6萬同時
2009-12-7 22:26:32 暗夜教父(199033)
創建連接有時差
2009-12-7 22:26:35 LinkTalk.NET(909327571)
6萬個如果能夠1k/s的保持48小時的話,非常牛逼了
2009-12-7 22:26:38 暗夜教父(199033)
那么發生數據也有時差
2009-12-7 22:27:53 LinkTalk.NET(909327571)
你通過什么方式做到間隔一秒發一次數據的?
2009-12-7 22:28:32 LinkTalk.NET(909327571)
sleep嗎?
2009-12-7 22:29:19 LinkTalk.NET(909327571)
你有沒有看一下網卡的帶寬消耗
2009-12-7 22:29:37 LinkTalk.NET(909327571)
按道理應該1k * 6萬 = 60M/s
2009-12-7 22:30:12 LinkTalk.NET(909327571)
如果達到60M以上基本算是真正的穩定的實現了6萬并發
2009-12-7 22:30:34 暗夜教父(199033)
client那邊是我同事寫的
2009-12-7 22:30:39 暗夜教父(199033)
我不知道是不是sleep
2009-12-7 22:30:52 暗夜教父(199033)
帶寬消耗我還也真沒看
2009-12-7 22:30:53 LinkTalk.NET(909327571)
erlang里面好像也就只有sleep了
2009-12-7 22:31:06 暗夜教父(199033)
我估計不是
2009-12-7 22:31:52 LinkTalk.NET(909327571)
另外我懷疑當并發高了以后,sleep會不準,真正的間隔肯定會大于sleep的時間
2009-12-7 22:32:50 暗夜教父(199033)
恩,確實有待驗證
2009-12-7 22:33:04 暗夜教父(199033)
不過只有用應用來驗證了
2009-12-7 22:33:14 暗夜教父(199033)
等項目上線了,看看實際情況
2009-12-7 22:33:17 LinkTalk.NET(909327571)
嗯,呵呵
2009-12-7 22:33:24 LinkTalk.NET(909327571)
你們這個用在什么項目上馬?
2009-12-7 22:33:27 LinkTalk.NET(909327571)
上面
2009-12-7 22:33:30 LinkTalk.NET(909327571)
游戲嗎?
2009-12-7 22:33:31 暗夜教父(199033)
恩,游戲的
2009-12-7 22:34:48 LinkTalk.NET(909327571)
[表情]
2009-12-7 22:34:59 暗夜教父(199033)
不過風險也很大
2009-12-7 22:35:15 LinkTalk.NET(909327571)
為什么?
2009-12-7 22:35:24 暗夜教父(199033)
沒做過
2009-12-7 22:35:28 暗夜教父(199033)
沒有成熟項目的經驗
2009-12-7 22:35:29 LinkTalk.NET(909327571)
呵呵
2009-12-7 22:35:32 暗夜教父(199033)
什么都有可能發生
2009-12-7 22:35:45 LinkTalk.NET(909327571)
傳統的游戲服務器,單臺到兩三萬并發已經很牛X了
2009-12-7 22:35:50 暗夜教父(199033)
未知因素太多
2009-12-7 22:36:00 暗夜教父(199033)
我估計6W還能上
2009-12-7 22:36:00 LinkTalk.NET(909327571)
而且硬件還不差的情況下
2009-12-7 22:36:11 LinkTalk.NET(909327571)
游戲的數據通信量比較大
2009-12-7 22:36:22 LinkTalk.NET(909327571)
嗯
2009-12-7 22:36:30 暗夜教父(199033)
沒過6W是因為客戶端的端口基本上沒了
2009-12-7 22:36:45 LinkTalk.NET(909327571)
這個不會啊
2009-12-7 22:36:51 LinkTalk.NET(909327571)
客戶端端口沒有關系的
2009-12-7 22:36:55 暗夜教父(199033)
有吧
2009-12-7 22:36:59 LinkTalk.NET(909327571)
因為IP不一樣
2009-12-7 22:37:11 暗夜教父(199033)
不是。我測試的時候
2009-12-7 22:37:15 LinkTalk.NET(909327571)
同一個IP有65535個端口的限制
2009-12-7 22:37:18 暗夜教父(199033)
6萬個連接從一臺機器來的
2009-12-7 22:37:18 LinkTalk.NET(909327571)
哦,了解
2009-12-7 22:37:23 LinkTalk.NET(909327571)
嗯
2009-12-7 22:37:54 david(258667581)
服務器端只監聽一個端口 應該不會有端口數限制吧?
2009-12-7 22:38:24 LinkTalk.NET(909327571)
服務器端不會
2009-12-7 22:38:50 david(258667581)
做游戲的話 不是都雍和宮服務器嗎
2009-12-7 22:38:55 david(258667581)
都做的服務器嗎
2009-12-7 22:39:24 暗夜教父(199033)
。。。
2009-12-7 22:39:32 暗夜教父(199033)
我是做測試
2009-12-7 22:39:36 david(258667581)
另外 如果端口socket屬性設置reuse為true 是否可以超過6W端口
2009-12-7 22:39:48 LinkTalk.NET(909327571)
不會
2009-12-7 22:39:53 暗夜教父(199033)
服務器端是一臺機器,客戶端也是一臺機器
2009-12-7 22:39:56 LinkTalk.NET(909327571)
那個是針對不同的protocol
2009-12-7 22:40:02 暗夜教父(199033)
客戶端發起6W個連接
2009-12-7 22:40:19 暗夜教父(199033)
就要占用6W多個端口
2009-12-7 22:40:18 david(258667581)
不同的protocol是什么意思
2009-12-7 22:40:35 LinkTalk.NET(909327571)
tcp 和 udp 可以reuse同一個port
2009-12-7 22:41:09 LinkTalk.NET(909327571)
erlang其實我只是大概的了解
2009-12-7 22:41:22 LinkTalk.NET(909327571)
我打算用C#和Java模擬erlang
2009-12-7 22:41:26 david(258667581)
程序a試用端口2000 tcp連 然后 程序b用2000端口udp再連接另外一個程序?
2009-12-7 22:41:36 david(258667581)
模擬?
2009-12-7 22:41:40 LinkTalk.NET(909327571)
嗯
2009-12-7 22:41:47 LinkTalk.NET(909327571)
我已經用C#實現了
2009-12-7 22:41:47 david(258667581)
怎么模擬?
2009-12-7 22:41:59 LinkTalk.NET(909327571)
就是Actor模式
2009-12-7 22:42:07 LinkTalk.NET(909327571)
自己實現消息的調度
2009-12-7 22:42:15 LinkTalk.NET(909327571)
還有實現異步編程接口
2009-12-7 22:42:41 david(258667581)
actor模式是什么意思
2009-12-7 22:42:59 LinkTalk.NET(909327571)
一種軟件設計模式
2009-12-7 22:43:16 LinkTalk.NET(909327571)
erlang/scala等并發平臺都是actor模式
2009-12-7 22:44:06 david(258667581)
不懂
2009-12-7 22:44:25 david(258667581)
為什么要用c#模擬?
2009-12-7 22:44:33 LinkTalk.NET(909327571)
因為我熟悉C#
2009-12-7 22:44:39 LinkTalk.NET(909327571)
也打算用java模擬
2009-12-7 22:45:01 LinkTalk.NET(909327571)
同時也因為C#/Java有大量的開發人員和豐富的第三方擴展
2009-12-7 22:45:25 LinkTalk.NET(909327571)
同時也有很爽的IDE
2009-12-7 22:45:26 LinkTalk.NET(909327571)
:)
2009-12-7 22:45:42 david(258667581)
是 但是感覺如果你是要測性能的話 c#的性能可能跟不上erlang啊
2009-12-7 22:45:49 david(258667581)
用c的都會好些
2009-12-7 22:46:01 LinkTalk.NET(909327571)
其實erlang語言本身性能不見得高,因為是腳本語言
2009-12-7 22:46:15 LinkTalk.NET(909327571)
高并發是因為純消息傳遞,可以有效的避免死鎖
2009-12-7 22:46:27 LinkTalk.NET(909327571)
傳統語言比如c/c++要避免死鎖比較難
2009-12-7 22:46:49 LinkTalk.NET(909327571)
給大家看老外的一個測試數據
2009-12-7 22:46:57 LinkTalk.NET(909327571)
erlang其實算是執行效率相對比較差的
2009-12-7 22:47:20 david(258667581)
哪里 erlang的性能應該是可以跟c叫板的
2009-12-7 22:48:04 LinkTalk.NET(909327571)
http://shootout.alioth.debian.org/u32q/benchmark.php?test=all&lang=csharp&lang2=hipe&box=1
2009-12-7 22:48:16 LinkTalk.NET(909327571)
這個里面有很多語言的性能測試比較
2009-12-7 22:48:23 LinkTalk.NET(909327571)
erlang算是比較差的
2009-12-7 22:49:02 LinkTalk.NET(909327571)
傳統語言達不到高并發是因為無法有效的避免死鎖,還有cpu調度做得不好
2009-12-7 22:50:17 暗夜教父(199033)
恩,C如果使用erlang的模式
2009-12-7 22:50:26 暗夜教父(199033)
絕對不會輸
2009-12-7 22:50:29 LinkTalk.NET(909327571)
嗯
2009-12-7 22:50:37 LinkTalk.NET(909327571)
erlang本身就是C寫的
2009-12-7 22:50:41 暗夜教父(199033)
恩
2009-12-7 22:50:54 LinkTalk.NET(909327571)
我記得國內好像有個牛人在研究用C++模擬erlang
2009-12-7 22:51:00 LinkTalk.NET(909327571)
好像是盛大的一個架構師
2009-12-7 22:51:13 暗夜教父(199033)
貌似現在做linux下做服務器端得,C比C++多了
2009-12-7 22:51:20 LinkTalk.NET(909327571)
嗯
2009-12-7 22:51:29 LinkTalk.NET(909327571)
linux下c多
2009-12-7 22:51:36 暗夜教父(199033)
我記得好像云風把大話西游的服務器端改成C了
2009-12-7 22:52:28 小生啊牙(86753957)
51.com的服務器就是用C++模擬erlang的
2009-12-7 22:52:50 小生啊牙(86753957)
使用協程
2009-12-7 22:53:59 LinkTalk.NET(909327571)
嗯,協程可以在傳統的面向過程的線程上模擬異步操作
2009-12-7 22:55:05 LinkTalk.NET(909327571)
其實高并發只是一種設計模式
2009-12-7 22:55:19 LinkTalk.NET(909327571)
erlang把這個設計模式固化并強制到語法里了
2009-12-7 22:56:22 LinkTalk.NET(909327571)
C#2.0開始有個新的特性,叫iterator,通過yield關鍵字來實現coroutine(協程)
2009-12-7 22:56:39 LinkTalk.NET(909327571)
并且在語法上也可以用連貫的形式來實現異步的操作
2009-12-7 22:56:49 LinkTalk.NET(909327571)
和erlang的形式類似
2009-12-7 22:57:10 LinkTalk.NET(909327571)
java里面目前只有anonymous class可以實現異步調用
2009-12-7 22:57:44 LinkTalk.NET(909327571)
但是那個語法寫起來有些牽強,花括號會越嵌越深
2009-12-7 22:59:19 LinkTalk.NET(909327571)
我用C#實現的actor模式也可以處理每分鐘大概200萬條消息(在PC上)
2009-12-7 22:59:24 LinkTalk.NET(909327571)
AMD雙核
2009-12-7 23:00:28 LinkTalk.NET(909327571)
也測試過HTTP 請求,大概可以達到1萬多并發,不過是6秒一個請求(sleep6秒,實際會延遲到10秒以上),cpu占用40-60
2009-12-7 23:01:54 LinkTalk.NET(909327571)
每分鐘200萬消息是最簡單的ping/pong測試
2009-12-7 23:06:12 david(258667581)
以前沒聽說過 協程 呵呵
2009-12-7 23:06:33 LinkTalk.NET(909327571)
是否有協程無所謂的
2009-12-7 23:07:14 LinkTalk.NET(909327571)
關鍵是純消息傳遞(避免死鎖)還有線程合理有效的調度(實現高效的異步處理)
2009-12-7 23:07:35 david(258667581)
恩 關鍵是避開鎖
2009-12-7 23:08:04 LinkTalk.NET(909327571)
并最好再能夠在語法上將異步操作用順序化的代碼來表示
2009-12-7 23:08:22 LinkTalk.NET(909327571)
實在不行也無所謂,可以用event handler的方式
2009-12-7 23:08:48 david(258667581)
能否舉一個異步操作的具體應用場景?
2009-12-7 23:09:08 LinkTalk.NET(909327571)
很多都是異步操作(那樣IO才可以做到高效)
2009-12-7 23:09:19 LinkTalk.NET(909327571)
比如epoll和windows的iocp
2009-12-7 23:09:22 LinkTalk.NET(909327571)
都是異步的
2009-12-7 23:09:44 LinkTalk.NET(909327571)
簡單的原理就是調用函數遞交或注冊一個IO請求到系統內核
2009-12-7 23:09:45 david(258667581)
是不是都是底層的
2009-12-7 23:09:55 LinkTalk.NET(909327571)
然后不需要阻塞,立即返回
2009-12-7 23:10:17 LinkTalk.NET(909327571)
系統IO內核收到數據或相關的事件會觸發當初注冊的回調函數
2009-12-7 23:10:59 LinkTalk.NET(909327571)
調用請求 和 數據返回或事件觸發 不在同一個操作系統線程上完成,就稱為異步
2009-12-7 23:11:13 LinkTalk.NET(909327571)
異步的IO才比較高效
2009-12-7 23:11:37 LinkTalk.NET(909327571)
操作系統的線程數有限制
2009-12-7 23:11:42 david(258667581)
這些都不難 無論是java和c#還是c,都已經有專門的api支撐了
2009-12-7 23:11:51 david(258667581)
關鍵是業務上的操作
2009-12-7 23:11:55 LinkTalk.NET(909327571)
一般上了千以后,線程的效率就比較差了
2009-12-7 23:11:58 david(258667581)
鎖的都是業務
2009-12-7 23:12:02 LinkTalk.NET(909327571)
而且很耗cpu和內存
2009-12-7 23:12:23 LinkTalk.NET(909327571)
所以要通過合理的調度和異步操作來分享寶貴的操作系統線程
2009-12-7 23:12:51 LinkTalk.NET(909327571)
嚴格遵守actor模式可以有效的避免死鎖
2009-12-7 23:13:04 david(258667581)
[表情]
2009-12-7 23:14:34 LinkTalk.NET(909327571)
我也在摸索和嘗試,erlang的消息調度和分布式支持是目前最好的
2009-12-7 23:14:49 LinkTalk.NET(909327571)
scala也不錯,twitter就放棄erlang轉向scala
2009-12-7 23:15:00 LinkTalk.NET(909327571)
但是scala在分布式支持方面不及erlang
2009-12-7 23:15:30 LinkTalk.NET(909327571)
但是scala有最大的好處就是基于JVM,可以利用java的各種好處
2009-12-7 23:15:33 david(258667581)
分布式以后是趨勢 所以感覺erlang的生命力應該還是很強的
2009-12-7 23:15:44 LinkTalk.NET(909327571)
嗯,分布式其實其他語言也可以做的
2009-12-7 23:16:05 LinkTalk.NET(909327571)
只是erlang已經做了十幾年了
2009-12-7 23:16:19 LinkTalk.NET(909327571)
其他語言也肯定會逐漸支持的
2009-12-7 23:16:25 david(258667581)
恩
2009-12-7 23:16:43 LinkTalk.NET(909327571)
但是我個人比較覺得遺憾的是erlang的語法和編程模式
2009-12-7 23:17:08 LinkTalk.NET(909327571)
如果erlang代碼上到一定的量以后維護和調試就相當麻煩了
2009-12-7 23:17:11 david(258667581)
語法習慣了就好 關鍵是對于結構的支持
2009-12-7 23:17:17 david(258667581)
可讀性太差
2009-12-7 23:17:31 LinkTalk.NET(909327571)
嗯,數據結構表現力也不夠豐富
2009-12-7 23:17:35 LinkTalk.NET(909327571)
都是tuple
2009-12-7 23:17:43 LinkTalk.NET(909327571)
眼睛要看花了
2009-12-7 23:19:09 david(258667581)
開發環境也沒跟上 沒有很好的IDE
2009-12-7 23:19:16 LinkTalk.NET(909327571)
嗯
2009-12-7 23:19:37 LinkTalk.NET(909327571)
腳本語言重構起來就相當麻煩了
2009-12-7 23:19:57 T.t.T!Ck.¢#(121787333)
有一個老外也用C#來模擬erlang的模式
2009-12-7 23:20:06 LinkTalk.NET(909327571)
因為無類型,無法反射元數據,沒有辦法自動生成文檔,更難重構
2009-12-7 23:20:07 Lenn(28663)
ERLANG跟Java一樣是編譯態語言,怎么成腳本語言了
2009-12-7 23:20:22 LinkTalk.NET(909327571)
erlang嚴格來講是腳本
2009-12-7 23:20:31 LinkTalk.NET(909327571)
弱類型的基本都是腳本
2009-12-7 23:20:44 LinkTalk.NET(909327571)
包括php也號稱支持編譯
2009-12-7 23:20:47 LinkTalk.NET(909327571)
其實還是腳本
2009-12-7 23:21:16 Lenn(28663)
bin code只有200多條指令,屬于典型的中間太語言,效率不會比Java差多少
2009-12-7 23:21:34 LinkTalk.NET(909327571)
嗯
2009-12-7 23:21:48 LinkTalk.NET(909327571)
我覺得任何東西都有得必有失
2009-12-7 23:21:57 LinkTalk.NET(909327571)
一方面太強了,比如有其他的缺陷
2009-12-7 23:22:06 LinkTalk.NET(909327571)
就看自己的喜好和具體的應用場景了
2009-12-7 23:22:08 Lenn(28663)
圖形太弱
2009-12-7 23:22:47 Lenn(28663)
調試起來最爽,業務想對了基本不會編程從出錯
2009-12-7 23:23:16 LinkTalk.NET(909327571)
嗯
2009-12-7 23:24:26 Lenn(28663)
我們就是一個案例,用JAVA做的東西,現在還不停改代碼,Erlang那一塊,要改也只是幾行一會的事情
2009-12-7 23:25:17 jack(357794482)
其實這種事情要看你對語言的處理能力
2009-12-7 23:25:20 LinkTalk.NET(909327571)
嗯
2009-12-7 23:25:35 LinkTalk.NET(909327571)
google的首席架構師是搞java的
2009-12-7 23:25:41 LinkTalk.NET(909327571)
其實這個看具體情況的
2009-12-7 23:26:17 Lenn(28663)
用什么還有歷史原因,比如新的facebook很多用Erlang
2009-12-7 23:26:33 Lenn(28663)
因為做好的東西更改語言是個大問題
2009-12-7 23:26:36 LinkTalk.NET(909327571)
但是更新的twitter由erlang轉向了scala
2009-12-7 23:26:58 LinkTalk.NET(909327571)
另外erlang十幾年了,到今天才紅,也是有一定的原因的
2009-12-7 23:27:13 LinkTalk.NET(909327571)
erlang有非常突出的優勢,但是也存在一些不夠完美的地方
2009-12-7 23:29:04 Lenn(28663)
現在社區也挺活躍,業務有比較強的優勢
2009-12-7 23:29:37 LinkTalk.NET(909327571)
嗯,是的;)
2009-12-7 23:29:57 LinkTalk.NET(909327571)
所以就算其他語言的開發人員也開始了解
2009-12-7 23:30:02 LinkTalk.NET(909327571)
學習、
2009-12-7 23:30:04 Lenn(28663)
圖形方面卻絲毫不行
2009-12-7 23:30:05 LinkTalk.NET(909327571)
或模擬erlang
2009-12-7 23:30:08 LinkTalk.NET(909327571)
嗯
posted @
2009-12-07 23:49 暗夜教父 閱讀(739) |
評論 (0) |
編輯 收藏
在ubuntu下安裝subversion服務器
Tuesday, 6. March 2007, 13:29:44
subversion, ubuntu
在ubuntu下安裝subversion服務器
originally by: zengpuzhang <zengpuzhang@gmail.com>
Use the subversion for apache2 on ubunut 5.10.
* Install apache2
sudo apt-get install apache2
* It will download apache2 apache2-common apache2-mpm-worker apache2-utils
* Install subversion
sudo apt-get install subversion
* Install libapache2-svn
sudo apt-get install libapache2-svn
在ubuntu下安裝
subversion服務器
originally by: zengpuzhang <zengpuzhang@gmail.com>
Use the
subversion for apache2 on ubunut 5.10.
* Install apache2
sudo
apt-get install apache2
* It will download apache2 apache2-common apache2-mpm-worker apache2-utils
* Install
subversionsudo
apt-get install
subversion* Install libapache2-svn
sudo
apt-get install libapache2-svn
* Create
subversion home foder and project
(其中兩個最常用的位置之一是:/usr/local/svn)
cd /home/
sudo mkdir svn
cd svn
sudo svnadmin create project
cd /home
sudo chown www-data.www-data svn -R
* Configure the apache and
subversioncd /etc/apache2
sudo mkdir authz
cd authz
sudo touch project.authz
sudo touch dav_svn.passwd
sudo vi /etc/apache2/mods-enabled/dav_svn.conf
* Add the content like this:
<Location /svn/project>
DAV svn
SVNPath /home/svn/project
AuthzSVNAccessFile /etc/apache2/authz/project.authz
AuthType Basic
AuthName "Project
Subversion Repository"
AuthUserFile /etc/apache2/authz/dav_svn.passwd
Require valid-user
</Location>
* Save the dav_svn.conf and edit the project.authz
sudo vi /etc/apache2/authz/project.authz
* Add the content like this
[/]
zengpuzhang=rw
* Sava the project.authz and create a user name zengpuzhang
sudo htpasswd2 -c /etc/apache2/authz/dav_svn.passwd zengpuzhang(第一個用戶的時候)
(sudo htpasswd2 -m /etc/apache2/authz/dav_svn.passwd xxxx , 以后的用戶)
* Input the user`s password
* Restart the apache
sudo /etc/init.d/apache2 restart
* Done!
*
subversion checked svn co
$svn co
http://192.168.10.163/svn/project認證領域:<
http://192.168.10.163:80> Project
Subversion Repository
用戶登錄名:zengpuzhang
“zengpuzhang”的密碼:
取出修訂版 0。
*
subversion checked svn add
$cd project
$touch test
$svn add test
A test
*
subversion checked svn ci
$svn ci -m “just a test”
新增 test
傳輸文件數據.
提交后的修訂版為 1。
* enjoy it .
簡介
如果您對 Subversion 還比較陌生,本節將給您一個關于 Subversion 的簡要介紹。
Subversion 是一款開放源代碼的版本控制系統。使用 Subversion,您可以重新加載源代碼和文檔的歷史版本。Subversion 管理了源代碼在各個時期的版本。一個文件樹被集中放置在文件倉庫中。這個文件倉庫很像是一個傳統的文件服務器,只不過它能夠記住文件和目錄的每一次變化。
首先我們假設您能夠在 Ubuntu 中操作 Linux 的命令、編輯文件、啟動和停止服務。當然,我們還認為您的 Ubuntu 正在運行中,您可以使用 sudo 操作并且您打算使用 Subversion。
我們假設您可能需要使用所有可能的方法訪問 SVN 文件倉庫。同時我們也認為您應該已經配置好了您的 /etc/apt/sources.list 文件。
[編輯]本文涉及的范圍
要通過 HTTP 協議訪問 SVN 文件倉庫,您需要安裝并配置好 Web 服務器。Apache 2 被證實可以很好的與 SVN 一起工作。關于 Apache 2 的安裝超出了本文的范圍,盡管如此,本文還是會涉及如何配置 Apache 2 使用 SVN。
類似的,要通過 HTTPS 協議訪問 SVN 文件倉庫,您需要在您的 Apache 2 中安裝并配置好數字證書,這也不在本文的討論范圍之中。
幸運的,Subversion 已經包含在 main 倉庫中。所以,要安裝 Subversion,您只需要簡單的運行:
$ sudo apt-get install subversion
$ sudo apt-get install libapache2-svn
如果系統報告了依賴關系的錯誤,請找出相應的軟件包并安裝它們。如果存在其它問題,也請自行解決。如果您是再不能解決這些問題,可以考慮通過 Ubuntu 的網站、Wiki、論壇或郵件列表尋求支持。
[編輯]服務器配置
您應該已經安裝了上述的軟件包。本節將闡述如何創建 SVN 文件倉庫以及如何設置項目的訪問權限。
[編輯]創建 SVN 倉庫
許多位置都可以放置 Subversion 文件倉庫,其中兩個最常用的是:/usr/local/svn 以及 /home/svn。為了在下面的描述中簡單明了,我們假設您的 Subversion 文件倉庫放在 /home/svn,并且你的項目名稱是簡單的“myproject”。
同樣的,也有許多常用的方式設置文件倉庫的訪問權限。然而,這也是安裝過程中最經常出現錯誤的地方,因此我們會對此進行一個詳細說明。典型的情況下,您應該創建一個名為“Subversion”的組來擁有文件倉庫所在的目錄。下面是一個快速的操作說明,有關內容請參考相關文檔的詳細說明:
- 在 Ubuntu 菜單上選擇“系統->系統管理->用戶和組”;
- 將您自己和“www-data”(Apache 用戶)加入組成員中;
或者使用命令完成上述功能(增加組,并且把用戶加到組里):
sudo addgroup subversion
sudo usermod -G subversion -a www-data
再或者直接使用命令編輯組文件"sudo vi /etc/group",增加組和成員(不推薦):
$ sudo vi /etc/group
結果看上去,像這樣。
$ cat /etc/group|grep subversion
subversion:x:1001:www-data,exp
您需要注銷然后再登錄以便您能夠成為 subversion 組的一員,然后就可以執行簽入文件(Check in,也稱提交文件)的操作了。
現在執行下面的命令
$ sudo mkdir /home/svn
$ cd /home/svn
$ sudo mkdir myproject
$ sudo chown -R root:subversion myproject
下面的命令用于創建 SVN 文件倉庫:
$ sudo svnadmin create /home/svn/myproject
賦予組成員對所有新加入文件倉庫的文件擁有相應的權限:
$ sudo chmod -R g+rws myproject
如果上面這個命令在創建SVN文件倉庫之前運行,你可能在后續Check in的時候遇到如下錯誤:
Can't open '/home/svn/myproject/db/txn-current-lock': Permission denied
查看txn-current-lock文件的權限和用戶以及組信息,應該類似于:
$ ls -l /home/svn/myproject/db/txn-current-lock
-rw-rwSr-- 1 root subversion 0 2009-06-18 15:33 txn-current-lock
除了權限以外,用戶及其組如果不對,則仍然會遇到上述問題,可以再次運行命令:
$ sudo chown -R root:subversion myproject
[編輯]訪問方式
Subversion 文件倉庫可以通過許多不同的方式進行訪問(Check Out,簽出)——通過本地硬盤,或者通過各種網絡協議。無論如何,文件倉庫的位置總是使用 URL 來表示。下表顯示了不同的 URL 模式對應的訪問方法:
模式 | 訪問方法 |
file:/// | 直接訪問本地硬盤上文件倉庫 |
http:// | 通過 WebDAV 協議訪問支持 Subversion 的 Apache 2 Web 服務器 |
https:// | 類似 http://,支持 SSL 加密 |
svn:// | 通過自帶協議訪問 svnserve 服務器 |
svn+ssh:// | 類似 svn://,支持通過 SSH 通道 |
本節中,我們將看到如何配置 SVN 以使之能夠通過所有的方法得以訪問。當然這里我們之討論基本的方法。要了解更高級的用途,我們推薦您閱讀《使用 Subversion 進行版本控制》在線電子書。
[編輯]直接訪問文件倉庫(file://)
這是所有訪問方式中最簡單的。它不需要事先運行任何 SVN 服務。這種訪問方式用于訪問本地的 SVN 文件倉庫。語法是:
$ svn co file:///home/svn/myproject
或者
$ svn co file://localhost/home/svn/myproject
注意:如果您并不確定主機的名稱,您必須使用三個斜杠(///),而如果您指定了主機的名稱,則您必須使用兩個斜杠(//).
對文件倉庫的訪問權限基于文件系統的權限。如果該用戶具有讀/寫權限,那么他/她就可以簽出/提交修改。如果您像前面我們說描述的那樣設置了相應的組,您可以簡單的將一個用戶添加到“subversion”組中以使其具有簽出和提交的權限。
要通過 WebDAV 協議訪問 SVN 文件倉庫,您必須配置您的 Apache 2 Web 服務器。您必須加入下面的代碼片段到您的 /etc/apache2/mods-available/dav_svn.conf中:
<Location /svn/myproject>
DAV svn
SVNPath /home/svn/myproject
AuthType Basic
AuthName "myproject subversion repository"
AuthUserFile /etc/subversion/passwd
<LimitExcept GET PROPFIND OPTIONS REPORT>
Require valid-user
</LimitExcept>
</Location>
如果需要用戶每次登錄時都進行用戶密碼驗證,請將<LimitExcept GET PROPFIND OPTIONS REPORT>與</LimitExcept>兩行注釋掉。
當您添加了上面的內容,您必須重新起動 Apache 2 Web 服務器,請輸入下面的命令:
sudo /etc/init.d/apache2 restart
接下來,您需要創建 /etc/subversion/passwd 文件,該文件包含了用戶授權的詳細信息。要添加用戶,您可以執行下面的命令:
sudo htpasswd -c /etc/subversion/passwd user_name
它會提示您輸入密碼,當您輸入了密碼,該用戶就建立了。“-c”選項表示創建新的/etc/subversion/passwd文件,所以user_name所指的用戶將是文件中唯一的用戶。如果要添加其他用戶,則去掉“-c”選項即可:
sudo htpasswd /etc/subversion/passwd other_user_name
您可以通過下面的命令來訪問文件倉庫:
$ svn co http://hostname/svn/myproject myproject --username user_name
它會提示您輸入密碼。您必須輸入您使用 htpasswd 設置的密碼。當通過驗證,項目的文件就被簽出了。
警告:密碼是通過純文本傳輸的。如果您擔心密碼泄漏的問題,我們建議您使用 SSL 加密,有關詳情請看下一節。
[編輯]通過具有安全套接字(SSL)的 WebDAV 協議訪問(https://)
通過具有 SSL 加密的 WebDAV 協議訪問 SVN 文件倉庫(https://)非常類似上節所述的內容,除了您必須為您的 Apache 2 Web 服務器設置數字證書之外。
您可以安裝由諸如 Verisign 發放的數字簽名,或者您可以安裝您自己的數字簽名。
我們假設您已經為 Apache 2 Web 服務器安裝和配置好了相應的數字證書。現在按照上一節所描述的方法訪問 SVN 文件倉庫,別忘了把 http:// 換成https://。如何,幾乎是一模一樣的!
[編輯]通過自帶協議訪問(svn://)
當您創建了 SVN 文件倉庫,您可以修改 /home/svn/myproject/conf/svnserve.conf 來配置其訪問控制。
例如,您可以取消下面的注釋符號來設置授權機制:
# [general]
# password-db = passwd
現在,您可以在“passwd”文件中維護用戶清單。編輯同一目錄下“passwd”文件,添加新用戶。語法如下:
username = password
#(注意行開始不要有多余空格)
要了解詳情,請參考該文件。
現在,您可以在本地或者遠程通過 svn:// 當文 SVN 了,您可以使用“svnserve”來運行 svnserver,語法如下:
$ svnserve -d --foreground -r /home/svn
# -d -- daemon mode
# --foreground -- run in foreground (useful for debugging)
# -r -- root of directory to serve
要了解更多信息,請輸入:
$ svnserve --help
當您執行了該命令,SVN 就開始監聽默認的端口(3690)。您可以通過下面的命令來訪問文件倉庫:
$ svn co svn://hostname/myproject myproject --username user_name
基于服務器的配置,它會要求輸入密碼。一旦通過驗證,就會簽出文件倉庫中的代碼。
要同步文件倉庫和本地的副本,您可以執行 update 子命令,語法如下:
$ cd project_dir
$ svn update
要了解更多的 SVN 子命令,您可以參考手冊。例如要了解 co (checkout) 命令,請執行:
$ svn co --help
或者這樣
$ svn --help commit
或者直接
? svn help co
checkout (co): 從版本庫簽出工作副本。
使用: checkout URL[@REV]... [PATH]
。。。。。
一個實例:
? killall svnserve; svnserve -d -r /home/svn/
/home/svn/lj12-source/conf ? dog *
authz:[groups]
authz:lj12 = veexp
authz:[lj12-source:/] <-注意寫法。
authz:veexp = rw
authz:@lj12 = rw
authz:* = passwd:[users] <-2個用戶和密碼。
passwd:veexp = icep
passwd:test = test
svnserve.conf:[general]
svnserve.conf:anon-access = none
svnserve.conf:auth-access = write
svnserve.conf:password-db = passwd
svnserve.conf:authz-db = authz <-如果不啟用authz,則test也可以取出。
? svn co svn://localhost/lj12-source --username veexp
認證領域: <svn://localhost:3690> a712643f-661e-0410-8ad4-f0554cd88977
用戶名: veexp “veexp”的密碼:
A lj12-source/tim.h A lj12-source/en.c
......
認證失敗的密碼緩沖記錄位置,明文密碼。到1.6版本,可能使用keyring管理。如果調試密碼,直接刪除如下文件就可。
~/.subversion/auth/svn.simple/:
eea34a6f7baa67a3639cacd6a428dba4
[編輯]通過具被SSH隧道保護的自帶協議訪問(svn+ssh://)
配置和服務器進程于上節所述相同。我們假設您已經運行了“svnserve”命令。
我們還假設您運行了 ssh 服務并允許接入。要驗證這一點,請嘗試使用 ssh 登錄計算機。如果您可以登錄,那么大功告成,如果不能,請在執行下面的步驟前解決它。
svn+ssh:// 協議使用 SSH 加密來訪問 SVN 文件倉庫。如您所知,數據傳輸是加密的。要訪問這樣的文件倉庫,請輸入:
$ svn co svn+ssh://hostname/home/svn/myproject myproject --username user_name
注意:在這種方式下,您必須使用完整的路徑(/home/svn/myproject)來訪問 SVN 文件倉庫
基于服務器的配置,它會要求輸入密碼。您必須輸入您用于登錄 ssh 的密碼,一旦通過驗證,就會簽出文件倉庫中的代碼。
您還應該參考 SVN book 以了解關于 svn+ssh:// 協議的詳細信息。
posted @
2009-11-26 15:01 暗夜教父 閱讀(878) |
評論 (0) |
編輯 收藏
(呵)近一段時間由于工作需要,終于開始玩Linux了,今天搞了一天的MySQL編譯安裝,記錄下來,備忘吧!!
(卡)安裝環境:VmWare5(橋接模式) + RedHat E AS 4 + 已安裝了開發工具以及相關開發包(安裝Linux系統時自己要定制的),并測試成功
(!)先給出MySQL For Linux 源碼下載地址,是xx.tar.zg格式的
http://www.filewatcher.com/m/mysql-5.0.45.tar.gz.24433261.0.0.html
(1)
-------------預備工作----------
1:假如下載的文件名為:mysql-5.0.45.tar.gz
2:假如copy到 /home下
3:groupadd mysql #添加mysql組
4:useradd -g mysql mysql #添加一個mysql用戶
5:cd /home #進入到該目錄
-----------------------編譯過程-----------------------
6:tar zxvf mysql-5.0.45.tar.gz #解壓后,在該目錄下會出現一個同名的文件夾
7:cd /home/mysql-5.0.45
8:./configure --prefix=/usr/local/mysql --with-charset=utf8 --with-collation=utf8_general_ci --with-extra-charsets=latin1 #參數設置,可以先不明白,以后再修改配置
9:make
10:make install
11:cp support-files/my-medium.cnf /etc/my.cnf #如果/etc/my.cnf已存在,則先備份,再刪除
12:vi /etc/my.cnf #將log-bin=mysql-bin注釋掉
----------------------------安裝并初步配置mysql--------------------------
13:cd /usr/local/mysql
14:bin/mysql_install_db --user=mysql #初始化mysql
15:chown -R root . #改當前目錄的捅有者為root。注意,最后有個 . 啊,表示當前目錄
16:chown -R mysql /usr/local/mysql/var #-R表示遞歸之下的所有目錄
17:chgrp -R mysql /usr/local/mysql #改變目錄所屬為mysql
18:bin/mysqld_safe --user=mysql & #啟動mysql
-----------------------------------------更改mysql的root用戶密碼----------------------------
19:bin/mysqladmin -uroot password 123456 #在mysql政黨啟動的情況下,更改root用戶的登錄密碼
20:bin/mysql -uroot -p #輸入此命令后,會提示你輸入root用戶密碼123456,
21:show databases; #如果查出所有數據庫,就恭喜你了
------------------------------------------------------把mysql加入到系統服務中-------------------------------------
22:cp /usr/local/mysql/share/mysql/mysql.server /etc/init.d/mysqld
chkconfig --add mysqld #加入到系統服務中,就可以通過service mysqld start|stop|status|restart等進行管理,很是方便,就不用再到/usr/local/mysql5.0.45/bin/啟動mysql了
------------------------------------------------------------------配置mysql環境變量------------------------------------------------
23:cd /root #回到你的個人主目錄,我這里是用root登陸的
cp .bashrc .bashrc.bak #備份一下吧
vi .bashrc
在最后加入:export PATH=/usr/local/mysql/bin:$PATH:.
source ~/.bashrc #回到終端再輸入此命令,以使剛修改的起作用,~代表用戶主目錄
env #查看一下是否生效
24:此是用來替換23步的一種方法
cp /usr/local/mysql/bin/mysql /usr/bin/mysql #把mysql常用的工具目錄加入到系統變量目錄中去,自己選擇性加,這樣做主要是可以直接運行該工具,而不需要切換到該目錄下,類似于添加環境變量了
-------------------------------------------------------------------------------讓Linux開放3306端口-------------------------------------------
25:service iptables stop
vi /etc/sysconfig/iptables
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 3306 -j ACCEPT
service iptables start
-------------------------------------------------------------------------------給root用戶開啟mysql遠程訪問權限--------------------------------------------
26:shutdown -hr now #重啟
ps -e | grep mysql #查看mysql是否已隨開機啟動,或者輸入:service mysqld status
mysql -uroot -p #進入mysql
輸入root用戶的密碼
grant all on *.* to root@'%' identified by '123456';
#grant 權限 on 數據庫名.表名 to 用戶@登錄主機 identified by "用戶密碼";
flush privileges; #為了開發方便,可以讓root用戶具有遠程訪問的權限
#最后,再附上一個很好用的mysql客戶端,http://download.csdn.net/source/924456
(2)默認的mysql數據庫目錄是 /usr/local/mysql-5.0.45/var
我們在安裝時指定了安裝目錄為/usr/local/mysql-5.0.45,除了在這里安裝所要的文件外,還有一部分用戶常用的,可執行二進制文件被放到了/usr/bin中,其實,在/usr/local/mysql-5.0.45/bin下,全都有這些命令了,之所以要在/usr/bin中把那幾個命令考過來,就是為了方便,相當于設置環境變量了,你可以echo $PATH一下,里面一定有/usr/bin這個值的。
明白了安裝過程,刪除mysql也就不足為難了
(3)通過一個完整的例子,自己會學到很多東西,linux常用命令還真需要自己來,整理記錄
posted @
2009-11-05 17:16 暗夜教父 閱讀(354) |
評論 (0) |
編輯 收藏
談這個話題之前,首先要讓大家知道,什么是服務器。在網絡游戲中,服務器所扮演的角色是同步,廣播和服務器主動的一些行為,比如說天氣,NPC AI之類的,之所以現在的很多網絡游戲服務器都需要負擔一些游戲邏輯上的運算是因為為了防止客戶端的作弊行為。了解到這一點,那么本系列的文章將分為兩部分來談談網絡游戲服務器的設計,一部分是講如何做好服務器的網絡連接,同步,廣播以及NPC的設置,另一部分則將著重談談哪些邏輯放在服務器比較合適,并且用什么樣的結構來安排這些邏輯。
服務器的網絡連接
大多數的網絡游戲的服務器都會選擇非阻塞select這種結構,為什么呢?因為網絡游戲的服務器需要處理的連接非常之多,并且大部分會選擇在Linux/Unix下運行,那么為每個用戶開一個線程實際上是很不劃算的,一方面因為在Linux/Unix下的線程是用進程這么一個概念模擬出來的,比較消耗系統資源,另外除了I/O之外,每個線程基本上沒有什么多余的需要并行的任務,而且網絡游戲是互交性非常強的,所以線程間的同步會成為很麻煩的問題。由此一來,對于這種含有大量網絡連接的單線程服務器,用阻塞顯然是不現實的。對于網絡連接,需要用一個結構來儲存,其中需要包含一個向客戶端寫消息的緩沖,還需要一個從客戶端讀消息的緩沖,具體的大小根據具體的消息結構來定了。另外對于同步,需要一些時間校對的值,還需要一些各種不同的值來記錄當前狀態,下面給出一個初步的連接的結構:
typedef connection_s {
user_t *ob; /* 指向處理服務器端邏輯的結構 */
int fd; /* socket連接 */
struct sockaddr_in addr; /* 連接的地址信息 */
char text[MAX_TEXT]; /* 接收的消息緩沖 */
int text_end; /* 接收消息緩沖的尾指針 */
int text_start; /* 接收消息緩沖的頭指針 */
int last_time; /* 上一條消息是什么時候接收到的 */
struct timeval latency; /* 客戶端本地時間和服務器本地時間的差值 */
struct timeval last_confirm_time; /* 上一次驗證的時間 */
short is_confirmed; /* 該連接是否通過驗證過 */
int ping_num; /* 該客戶端到服務器端的ping值 */
int ping_ticker; /* 多少個IO周期處理更新一次ping值 */
int message_length; /* 發送緩沖消息長度 */
char message_buf[MAX_TEXT]; /* 發送緩沖區 */
int iflags; /* 該連接的狀態 */
} connection_t;
服務器循環的處理所有連接,是一個死循環過程,每次循環都用select檢查是否有新連接到達,然后循環所有連接,看哪個連接可以寫或者可以讀,就處理該連接的讀寫。由于所有的處理都是非阻塞的,所以所有的Socket IO都可以用一個線程來完成。
由于網絡傳輸的關系,每次recv()到的數據可能不止包含一條消息,或者不到一條消息,那么怎么處理呢?所以對于接收消息緩沖用了兩個指針,每次接收都從text_start開始讀起,因為里面殘留的可能是上次接收到的多余的半條消息,然后text_end指向消息緩沖的結尾。這樣用兩個指針就可以很方便的處理這種情況,另外有一點值得注意的是:解析消息的過程是一個循環的過程,可能一次接收到兩條以上的消息在消息緩沖里面,這個時候就應該執行到消息緩沖里面只有一條都不到的消息為止,大體流程如下:
while ( text_end – text_start > 一條完整的消息長度 )
{
從text_start處開始處理;
text_start += 該消息長度;
}
memcpy ( text, text + text_start, text_end – text_start );
對于消息的處理,這里首先就需要知道你的游戲總共有哪些消息,所有的消息都有哪些,才能設計出比較合理的消息頭。一般來說,消息大概可分為主角消息,場景消息,同步消息和界面消息四個部分。其中主角消息包括客戶端所控制的角色的所有動作,包括走路,跑步,戰斗之類的。場景消息包括天氣變化,一定的時間在場景里出現一些東西等等之類的,這類消息的特點是所有消息的發起者都是服務器,廣播對象則是場景里的所有玩家。而同步消息則是針對發起對象是某個玩家,經過服務器廣播給所有看得見他的玩家,該消息也是包括所有的動作,和主角消息不同的是該種消息是服務器廣播給客戶端的,而主角消息一般是客戶端主動發給服務器的。最后是界面消息,界面消息包括是服務器發給客戶端的聊天消息和各種屬性及狀態信息。
下面來談談消息的組成。一般來說,一個消息由消息頭和消息體兩部分組成,其中消息頭的長度是不變的,而消息體的長度是可變的,在消息體中需要保存消息體的長度。由于要給每條消息一個很明顯的區分,所以需要定義一個消息頭特有的標志,然后需要消息的類型以及消息ID。消息頭大體結構如下:
type struct message_s {
unsigned short message_sign;
unsigned char message_type;
unsigned short message_id
unsigned char message_len
}message_t;
服務器的廣播
服務器的廣播的重點就在于如何計算出廣播的對象。很顯然,在一張很大的地圖里面,某個玩家在最東邊的一個動作,一個在最西邊的玩家是應該看不到的,那么怎么來計算廣播的對象呢?最簡單的辦法,就是把地圖分塊,分成大小合適的小塊,然后每次只象周圍幾個小塊的玩家進行廣播。那么究竟切到多大比較合適呢?一般來說,切得塊大了,內存的消耗會增大,切得塊小了,CPU的消耗會增大(原因會在后面提到)。個人覺得切成一屏左右的小塊比較合適,每次廣播廣播周圍九個小塊的玩家,由于廣播的操作非常頻繁,那么遍利周圍九塊的操作就會變得相當的頻繁,所以如果塊分得小了,那么遍利的范圍就會擴大,CPU的資源會很快的被吃完。
切好塊以后,怎么讓玩家在各個塊之間走來走去呢?讓我們來想想在切換一次塊的時候要做哪些工作。首先,要算出下個塊的周圍九塊的玩家有哪些是現在當前塊沒有的,把自己的信息廣播給那些玩家,同時也要算出下個塊周圍九塊里面有哪些物件是現在沒有的,把那些物件的信息廣播給自己,然后把下個塊的周圍九快里沒有的,而現在的塊周圍九塊里面有的物件的消失信息廣播給自己,同時也把自己消失的消息廣播給那些物件。這個操作不僅煩瑣而且會吃掉不少CPU資源,那么有什么辦法可以很快的算出這些物件呢?一個個做比較?顯然看起來就不是個好辦法,這里可以參照二維矩陣碰撞檢測的一些思路,以自己周圍九塊為一個矩陣,目標塊周圍九塊為另一個矩陣,檢測這兩個矩陣是否碰撞,如果兩個矩陣相交,那么沒相交的那些塊怎么算。這里可以把相交的塊的坐標轉換成內部坐標,然后再進行運算。
對于廣播還有另外一種解決方法,實施起來不如切塊來的簡單,這種方法需要客戶端來協助進行運算。首先在服務器端的連接結構里面需要增加一個廣播對象的隊列,該隊列在客戶端登陸服務器的時候由服務器傳給客戶端,然后客戶端自己來維護這個隊列,當有人走出客戶端視野的時候,由客戶端主動要求服務器給那個物件發送消失的消息。而對于有人總進視野的情況,則比較麻煩了。
首先需要客戶端在每次給服務器發送update position的消息的時候,服務器都給該連接算出一個視野范圍,然后在需要廣播的時候,循環整張地圖上的玩家,找到坐標在其視野范圍內的玩家。使用這種方法的好處在于不存在轉換塊的時候需要一次性廣播大量的消息,缺點就是在計算廣播對象的時候需要遍歷整個地圖上的玩家,如果當一個地圖上的玩家多得比較離譜的時候,該操作就會比較的慢。
服務器的同步
同步在網絡游戲中是非常重要的,它保證了每個玩家在屏幕上看到的東西大體是一樣的。其實呢,解決同步問題的最簡單的方法就是把每個玩家的動作都向其他玩家廣播一遍,這里其實就存在兩個問題:1,向哪些玩家廣播,廣播哪些消息。2,如果網絡延遲怎么辦。事實上呢,第一個問題是個非常簡單的問題,不過之所以我提出這個問題來,是提醒大家在設計自己的消息結構的時候,需要把這個因素考慮進去。而對于第二個問題,則是一個挺麻煩的問題,大家可以來看這么個例子:
比如有一個玩家A向服務器發了條指令,說我現在在P1點,要去P2點。指令發出的時間是T0,服務器收到指令的時間是T1,然后向周圍的玩家廣播這條消息,消息的內容是“玩家A從P1到P2”有一個在A附近的玩家B,收到服務器的這則廣播的消息的時間是T2,然后開始在客戶端上畫圖,A從P1到P2點。這個時候就存在一個不同步的問題,玩家A和玩家B的屏幕上顯示的畫面相差了T2-T1的時間。這個時候怎么辦呢?
有個解決方案,我給它取名叫 預測拉扯,雖然有些怪異了點,不過基本上大家也能從字面上來理解它的意思。要解決這個問題,首先要定義一個值叫:預測誤差。然后需要在服務器端每個玩家連接的類里面加一項屬性,叫latency,然后在玩家登陸的時候,對客戶端的時間和服務器的時間進行比較,得出來的差值保存在latency里面。還是上面的那個例子,服務器廣播消息的時候,就根據要廣播對象的latency,計算出一個客戶端的CurrentTime,然后在消息頭里面包含這個CurrentTime,然后再進行廣播。并且同時在玩家A的客戶端本地建立一個隊列,保存該條消息,只到獲得服務器驗證就從未被驗證的消息隊列里面將該消息刪除,如果驗證失敗,則會被拉扯回P1點。然后當玩家B收到了服務器發過來的消息“玩家A從P1到P2”這個時候就檢查消息里面服務器發出的時間和本地時間做比較,如果大于定義的預測誤差,就算出在T2這個時間,玩家A的屏幕上走到的地點P3,然后把玩家B屏幕上的玩家A直接拉扯到P3,再繼續走下去,這樣就能保證同步。更進一步,為了保證客戶端運行起來更加smooth,我并不推薦直接把玩家拉扯過去,而是算出P3偏后的一點P4,然后用(P4-P1)/T(P4-P3)來算出一個很快的速度S,然后讓玩家A用速度S快速移動到P4,這樣的處理方法是比較合理的,這種解決方案的原形在國際上被稱為(Full plesiochronous),當然,該原形被我篡改了很多來適應網絡游戲的同步,所以而變成所謂的:預測拉扯。
另外一個解決方案,我給它取名叫 驗證同步,聽名字也知道,大體的意思就是每條指令在經過服務器驗證通過了以后再執行動作。具體的思路如下:首先也需要在每個玩家連接類型里面定義一個latency,然后在客戶端響應玩家鼠標行走的同時,客戶端并不會先行走動,而是發一條走路的指令給服務器,然后等待服務器的驗證。服務器接受到這條消息以后,進行邏輯層的驗證,然后計算出需要廣播的范圍,包括玩家A在內,根據各個客戶端不同的latency生成不同的消息頭,開始廣播,這個時候這個玩家的走路信息就是完全同步的了。這個方法的優點是能保證各個客戶端之間絕對的同步,缺點是當網絡延遲比較大的時候,玩家的客戶端的行為會變得比較不流暢,給玩家帶來很不爽的感覺。該種解決方案的原形在國際上被稱為(Hierarchical master-slave synchronization),80年代以后被廣泛應用于網絡的各個領域。
最后一種解決方案是一種理想化的解決方案,在國際上被稱為Mutual synchronization,是一種對未來網絡的前景的良好預測出來的解決方案。這里之所以要提這個方案,并不是說我們已經完全的實現了這種方案,而只是在網絡游戲領域的某些方面應用到這種方案的某些思想。我對該種方案取名為:半服務器同步。大體的設計思路如下:
首先客戶端需要在登陸世界的時候建立很多張廣播列表,這些列表在客戶端后臺和服務器要進行不及時同步,之所以要建立多張列表,是因為要廣播的類型是不止一種的,比如說有local message,有remote message,還有global message 等等,這些列表都需要在客戶端登陸的時候根據服務器發過來的消息建立好。在建立列表的同時,還需要獲得每個列表中廣播對象的latency,并且要維護一張完整的用戶狀態列表在后臺,也是不及時的和服務器進行同步,根據本地的用戶狀態表,可以做到一部分決策由客戶端自己來決定,當客戶端發送這部分決策的時候,則直接將最終決策發送到各個廣播列表里面的客戶端,并對其時間進行校對,保證每個客戶端在收到的消息的時間是和根據本地時間進行校對過的。那么再采用預測拉扯中提到過的計算提前量,提高速度行走過去的方法,將會使同步變得非常的smooth。該方案的優點是不通過服務器,客戶端自己之間進行同步,大大的降低了由于網絡延遲而帶來的誤差,并且由于大部分決策都可以由客戶端來做,也大大的降低了服務器的資源。由此帶來的弊端就是由于消息和決策權都放在客戶端本地,所以給外掛提供了很大的可乘之機。
下面我想來談談關于服務器上NPC的設計以及NPC智能等一些方面涉及到的問題。首先,我們需要知道什么是NPC,NPC需要做什么。NPC的全稱是(Non-Player Character),很顯然,他是一個character,但不是玩家,那么從這點上可以知道,NPC的某些行為是和玩家類似的,他可以行走,可以戰斗,可以呼吸(這點將在后面的NPC智能里面提到),另外一點和玩家物件不同的是,NPC可以復生(即NPC被打死以后在一定時間內可以重新出來)。其實還有最重要的一點,就是玩家物件的所有決策都是玩家做出來的,而NPC的決策則是由計算機做出來的,所以在對NPC做何種決策的時候,需要所謂的NPC智能來進行決策。
下面我將分兩個部分來談談NPC,首先是NPC智能,其次是服務器如何對NPC進行組織。之所以要先談NPC智能是因為只有當我們了解清楚我們需要NPC做什么之后,才好開始設計服務器來對NPC進行組織。
NPC智能
NPC智能分為兩種,一種是被動觸發的事件,一種是主動觸發的事件。對于被動觸發的事件,處理起來相對來說簡單一些,可以由事件本身來呼叫NPC身上的函數,比如說NPC的死亡,實際上是在NPC的HP小于一定值的時候,來主動呼叫NPC身上的OnDie() 函數,這種由事件來觸發NPC行為的NPC智能,我稱為被動觸發。這種類型的觸發往往分為兩種:
一種是由別的物件導致的NPC的屬性變化,然后屬性變化的同時會導致NPC產生一些行為。由此一來,NPC物件里面至少包含以下幾種函數:
class NPC {
public:
// 是誰在什么地方導致了我哪項屬性改變了多少。
OnChangeAttribute(object_t *who, int which, int how, int where);
Private:
OnDie();
OnEscape();
OnFollow();
OnSleep();
// 一系列的事件。
}
這是一個基本的NPC的結構,這種被動的觸發NPC的事件,我稱它為NPC的反射。但是,這樣的結構只能讓NPC被動的接收一些信息來做出決策,這樣的NPC是愚蠢的。那么,怎么樣讓一個NPC能夠主動的做出一些決策呢?這里有一種方法:呼吸。那么怎么樣讓NPC有呼吸呢?
一種很簡單的方法,用一個計時器,定時的觸發所有NPC的呼吸,這樣就可以讓一個NPC有呼吸起來。這樣的話會有一個問題,當NPC太多的時候,上一次NPC的呼吸還沒有呼吸完,下一次呼吸又來了,那么怎么解決這個問題呢。這里有一種方法,讓NPC異步的進行呼吸,即每個NPC的呼吸周期是根據NPC出生的時間來定的,這個時候計時器需要做的就是隔一段時間檢查一下,哪些NPC到時間該呼吸了,就來觸發這些NPC的呼吸。
上面提到的是系統如何來觸發NPC的呼吸,那么NPC本身的呼吸頻率該如何設定呢?這個就好象現實中的人一樣,睡覺的時候和進行激烈運動的時候,呼吸頻率是不一樣的。同樣,NPC在戰斗的時候,和平常的時候,呼吸頻率也不一樣。那么就需要一個Breath_Ticker來設置NPC當前的呼吸頻率。
那么在NPC的呼吸事件里面,我們怎么樣來設置NPC的智能呢?大體可以概括為檢查環境和做出決策兩個部分。首先,需要對當前環境進行數字上的統計,比如說是否在戰斗中,戰斗有幾個敵人,自己的HP還剩多少,以及附近有沒有敵人等等之類的統計。統計出來的數據傳入本身的決策模塊,決策模塊則根據NPC自身的性格取向來做出一些決策,比如說野蠻型的NPC會在HP比較少的時候仍然猛撲猛打,又比如說智慧型的NPC則會在HP比較少的時候選擇逃跑。等等之類的。
至此,一個可以呼吸,反射的NPC的結構已經基本構成了,那么接下來我們就來談談系統如何組織讓一個NPC出現在世界里面。
NPC的組織
這里有兩種方案可供選擇,其一:NPC的位置信息保存在場景里面,載入場景的時候載入NPC。其二,NPC的位置信息保存在NPC身上,有專門的事件讓所有的NPC登陸場景。這兩種方法有什么區別呢?又各有什么好壞呢?
前一種方法好處在于場景載入的時候同時載入了NPC,場景就可以對NPC進行管理,不需要多余的處理,而弊端則在于在刷新的時候是同步刷新的,也就是說一個場景里面的NPC可能會在同一時間內長出來。而對于第二種方法呢,設計起來會稍微麻煩一些,需要一個統一的機制讓NPC登陸到場景,還需要一些比較麻煩的設計,但是這種方案可以實現NPC異步的刷新,是目前網絡游戲普遍采用的方法,下面我們就來著重談談這種方法的實現:
首先我們要引入一個“靈魂”的概念,即一個NPC在死后,消失的只是他的肉體,他的靈魂仍然在世界中存在著,沒有呼吸,在死亡的附近漂浮,等著到時間投胎,投胎的時候把之前的所有屬性清零,重新在場景上構建其肉體。那么,我們怎么來設計這樣一個結構呢?首先把一個場景里面要出現的NPC制作成圖量表,給每個NPC一個獨一無二的標識符,在載入場景之后,根據圖量表來載入屬于該場景的NPC。在NPC的OnDie() 事件里面不直接把該物件destroy 掉,而是關閉NPC的呼吸,然后打開一個重生的計時器,最后把該物件設置為invisable。這樣的設計,可以實現NPC的異步刷新,在節省服務器資源的同時也讓玩家覺得更加的真實。
(這一章節已經牽扯到一些服務器腳本相關的東西,所以下一章節將談談服務器腳本相關的一些設計)
補充的談談啟發式搜索(heuristic searching)在NPC智能中的應用。
其主要思路是在廣度優先搜索的同時,將下一層的所有節點經過一個啟發函數進行過濾,一定范圍內縮小搜索范圍。眾所周知的尋路A*算法就是典型的啟發式搜索的應用,其原理是一開始設計一個Judge(point_t* point)函數,來獲得point這個一點的代價,然后每次搜索的時候把下一步可能到達的所有點都經過Judge()函數評價一下,獲取兩到三個代價比較小的點,繼續搜索,那些沒被選上的點就不會在繼續搜索下去了,這樣帶來的后果的是可能求出來的不是最優路徑,這也是為什么A*算法在尋路的時候會走到障礙物前面再繞過去,而不是預先就走斜線來繞過該障礙物。如果要尋出最優化的路徑的話,是不能用A*算法的,而是要用動態規劃的方法,其消耗是遠大于A*的。
那么,除了在尋路之外,還有哪些地方可以應用到啟發式搜索呢?其實說得大一點,NPC的任何決策都可以用啟發式搜索來做,比如說逃跑吧,如果是一個2D的網絡游戲,有八個方向,NPC選擇哪個方向逃跑呢?就可以設置一個Judge(int direction)來給定每個點的代價,在Judge里面算上該點的敵人的強弱,或者該敵人的敏捷如何等等,最后選擇代價最小的地方逃跑。下面,我們就來談談對于幾種NPC常見的智能的啟發式搜索法的設計:
Target select (選擇目標):
首先獲得地圖上離該NPC附近的敵人列表。設計Judge() 函數,根據敵人的強弱,敵人的遠近,算出代價。然后選擇代價最小的敵人進行主動攻擊。
Escape(逃跑):
在呼吸事件里面檢查自己的HP,如果HP低于某個值的時候,或者如果你是遠程兵種,而敵人近身的話,則觸發逃跑函數,在逃跑函數里面也是對周圍的所有的敵人組織成列表,然后設計Judge() 函數,先選擇出對你構成威脅最大的敵人,該Judge() 函數需要判斷敵人的速度,戰斗力強弱,最后得出一個主要敵人,然后針對該主要敵人進行路徑的Judge() 的函數的設計,搜索的范圍只可能是和主要敵人相反的方向,然后再根據該幾個方向的敵人的強弱來計算代價,做出最后的選擇。
Random walk(隨機走路):
這個我并不推薦用A*算法,因為NPC一旦多起來,那么這個對CPU的消耗是很恐怖的,而且NPC大多不需要長距離的尋路,只需要在附近走走即可,那么,就在附近隨機的給幾個點,然后讓NPC走過去,如果碰到障礙物就停下來,這樣幾乎無任何負擔。
Follow Target(追隨目標):
這里有兩種方法,一種方法NPC看上去比較愚蠢,一種方法看上去NPC比較聰明,第一種方法就是讓NPC跟著目標的路點走即可,幾乎沒有資源消耗。而后一種則是讓NPC在跟隨的時候,在呼吸事件里面判斷對方的當前位置,然后走直線,碰上障礙物了用A*繞過去,該種設計會消耗一定量的系統資源,所以不推薦NPC大量的追隨目標,如果需要大量的NPC追隨目標的話,還有一個比較簡單的方法:讓NPC和目標同步移動,即讓他們的速度統一,移動的時候走同樣的路點,當然,這種設計只適合NPC所跟隨的目標不是追殺的關系,只是跟隨著玩家走而已了。
在這一章節,我想談談關于服務器端的腳本的相關設計。因為在上一章節里面,談NPC智能相關的時候已經接觸到一些腳本相關的東東了。還是先來談談腳本的作用吧。
在基于編譯的服務器端程序中,是無法在程序的運行過程中構建一些東西的,那么這個時候就需要腳本語言的支持了,由于腳本語言涉及到邏輯判斷,所以光提供一些函數接口是沒用的,還需要提供一些簡單的語法和文法解析的功能。其實說到底,任何的事件都可以看成兩個部分:第一是對自身,或者別的物件的數值的改變,另外一個就是將該事件以文字或者圖形的方式廣播出去。那么,這里牽扯到一個很重要的話題,就是對某一物件進行尋址。恩,談到這,我想將本章節分為三個部分來談,首先是服務器如何來管理動態創建出來的物件(服務器內存管理),第二是如何對某一物件進行尋址,第三則是腳本語言的組織和解釋。其實之所以到第四章再來談服務器的內存管理是因為在前幾章談這個的話,大家對其沒有一個感性的認識,可能不知道服務器的內存管理究竟有什么用。
4.1、服務器內存管理
對于服務器內存管理我們將采用內存池的方法,也稱為靜態內存管理。其概念為在服務器初始化的時候,申請一塊非常大的內存,稱為內存池(Memory pool),同時也申請一小塊內存空間,稱為垃圾回收站(Garbage recollecting station)。其大體思路如下:當程序需要申請內存的時候,首先檢查垃圾回收站是否為空,如果不為空的話,則從垃圾回收站中找一塊可用的內存地址,在內存池中根據地址找到相應的空間,分配給程序用,如果垃圾回收站是空的話,則直接從內存池的當前指針位置申請一塊內存;當程序釋放空間的時候,給那塊內存打上已經釋放掉的標記,然后把那塊內存的地址放入垃圾回收站。
下面具體談談該方法的詳細設計,首先,我們將采用類似于操作系統的段頁式系統來管理內存,這樣的好處是可以充分的利用內存池,其缺點是管理起來比較麻煩。嗯,下面來具體看看我們怎么樣來定義頁和段的結構:
typedef struct m_segment_s
{
struct m_segment_s *next; /* 雙線鏈表 + 靜態內存可以達到隨機訪問和順序訪問的目的,
真正的想怎么訪問,就怎么訪問。 */
struct m_segment_s *pre; int flags; // 該段的一些標記。
int start; // 相對于該頁的首地址。
int size; // 長度。
struct m_page_s *my_owner; // 我是屬于哪一頁的。
char *data; // 內容指針。
}m_segment_t;
typedef struct m_page_s
{
unsigned int flags; /* 使用標記,是否完全使用,是否還有空余 */
int size; /* 該頁的大小,一般都是統一的,最后一頁除外 */
int end; /* 使用到什么地方了 */
int my_index; /* 提供隨機訪問的索引 */
m_segment_t *segments; // 頁內段的頭指針。
}m_page_t;
那么內存池和垃圾回收站怎么構建呢?下面也給出一些構建相關的偽代碼:
static m_page_t *all_pages;
// total_size是總共要申請的內存數,num_pages是總共打算創建多少個頁面。
void initialize_memory_pool( int total_size, int num_pages )
{
int i, page_size, last_size; // 算出每個頁面的大小。
page_size = total_size / num_pages; // 分配足夠的頁面。
all_pages = (m_page_t*) calloc( num_pages, sizeof(m_page_t*) );
for ( i = 0; i < num_pages; i ++ )
{
// 初始化每個頁面的段指針。
all_pages[i].m_segment_t = (m_segment_t*) malloc( page_size );
// 初始化該頁面的標記。
all_pages[i].flags |= NEVER_USED;
// 除了最后一個頁面,其他的大小都是page_size 大小。
all_pages[i].size = page_size;
// 初始化隨機訪問的索引。
all_pages[i].my_index = i;
// 由于沒有用過,所以大小都是0
all_pages[i].end = 0;
}
// 設置最后一個頁面的大小。
if ( (last_size = total_size % num_pages) != 0 )
all_pages[i].size = last_size;
}
下面看看垃圾回收站怎么設計:
int **garbage_station;
void init_garbage_station( int num_pages, int page_size )
{
int i;
garbage_station = (int**) calloc( num_pages, sizeof( int* ) );
for ( i = 0; i < num_pages; i ++)
{
// 這里用unsigned short的高8位來儲存首相對地址,低8位來儲存長度。
garbage_station[i] = (int*) calloc( page_size, sizeof( unsigned short ));
memset( garbage_station[i], 0, sizeof( garbage_station[i] ));
}
}
也許這樣的貼代碼會讓大家覺得很不明白,嗯,我的代碼水平確實不怎么樣,那么下面我來用文字方式來敘說一下大體的概念吧。對于段頁式內存管理,首先分成N個頁面,這個是固定的,而對于每個頁面內的段則是動態的,段的大小事先是不知道的,那么我們需要回收的不僅僅是頁面的內存,還包括段的內存,那么我們就需要一個二維數組來保存是哪個頁面的那塊段的地址被釋放了。然后對于申請內存的時候,則首先檢查需要申請內存的大小,如果不夠一個頁面大小的話,則在垃圾回收站里面尋找可用的段空間分配,如果找不到,則申請一個新的頁面空間。
這樣用內存池的方法來管理整個游戲世界的內存可以有效的減少內存碎片,一定程度的提高游戲運行的穩定性和效率。
4.2、游戲中物件的尋址
第一個問題,我們為什么要尋址?加入了腳本語言的概念之后,游戲中的一些邏輯物件,比如說NPC,某個ITEM之類的都是由腳本語言在游戲運行的過程中動態生成的,那么我們通過什么樣的方法來對這些物件進行索引呢?說得簡單一點,就是如何找到他們呢?有個很簡單的方法,全部遍歷一次。當然,這是個簡單而有效的方法,但是效率上的消耗是任何一臺服務器都吃不消的,特別是在游戲的規模比較大之后。
那么,我們怎么來在游戲世界中很快的尋找這些物件呢?我想在談這個之前,說一下Hash Table這個數據結構,它叫哈希表,也有人叫它散列表,其工作原理是不是順序訪問,也不是隨機訪問,而是通過一個散列函數對其key進行計算,算出在內存中這個key對應的value的地址,而對其進行訪問。好處是不管面對多大的數據,只需要一次計算就能找到其地址,非常的快捷,那么弊端是什么呢?當兩個key通過散列函數計算出來的地址是同一個地址的時候,麻煩就來了,會產生碰撞,其的解決方法非常的麻煩,這里就不詳細談其解決方法了,否則估計再寫個四,五章也未必談得清楚,不過如果大家對其感興趣的話,歡迎討論。
嗯,我們將用散列表來對游戲中的物件進行索引,具體怎么做呢?首先,在內存池中申請一塊兩倍大于游戲中物件總數的內存,為什么是兩倍大呢?防止散列表碰撞。然后我們選用物件的名稱作為散列表的索引key,然后就可以開始設計散列函數了。下面來看個例子:
static int T[] =
{
1, 87, 49, 12, 176, 178, 102, 166, 121, 193, 6, 84, 249, 230, 44, 163,
14, 197, 213, 181, 161, 85, 218, 80, 64, 239, 24, 226, 236, 142, 38, 200,
110, 177, 104, 103, 141, 253, 255, 50, 77, 101, 81, 18, 45, 96, 31, 222,
25, 107, 190, 70, 86, 237, 240, 34, 72, 242, 20, 214, 244, 227, 149, 235,
97, 234, 57, 22, 60, 250, 82, 175, 208, 5, 127, 199, 111, 62, 135, 248,
174, 169, 211, 58, 66, 154, 106, 195, 245, 171, 17, 187, 182, 179, 0, 243,
132, 56, 148, 75, 128, 133, 158, 100, 130, 126, 91, 13, 153, 246, 216, 219,
119, 68, 223, 78, 83, 88, 201, 99, 122, 11, 92, 32, 136, 114, 52, 10,
138, 30, 48, 183, 156, 35, 61, 26, 143, 74, 251, 94, 129, 162, 63, 152,
170, 7, 115, 167, 241, 206, 3, 150, 55, 59, 151, 220, 90, 53, 23, 131,
125, 173, 15, 238, 79, 95, 89, 16, 105, 137, 225, 224, 217, 160, 37, 123,
118, 73, 2, 157, 46, 116, 9, 145, 134, 228, 207, 212, 202, 215, 69, 229,
27, 188, 67, 124, 168, 252, 42, 4, 29, 108, 21, 247, 19, 205, 39, 203,
233, 40, 186, 147, 198, 192, 155, 33, 164, 191, 98, 204, 165, 180, 117, 76,
140, 36, 210, 172, 41, 54, 159, 8, 185, 232, 113, 196, 231, 47, 146, 120,
51, 65, 28, 144, 254, 221, 93, 189, 194, 139, 112, 43, 71, 109, 184, 209,
};
// s是需要進行索引的字符串指針,maxn是字符串可能的最大長度,返回值是相對地址。
inline int whashstr(char *s, int maxn)
{
register unsigned char oh, h;
register unsigned char *p;
register int i;
if (!*s)
return 0;
p = (unsigned char *) s;
oh = T[*p]; h = (*(p++) + 1) & 0xff;
for (i = maxn - 1; *p && --i >= 0; )
{
oh = T[oh ^ *p]; h = T[h ^ *(p++)];
}
return (oh << 8) + h;
}
具體的算法就不說了,上面的那一大段東西不要問我為什么,這個算法的出處是CACM 33-6中的一個叫Peter K.Pearson的鬼子寫的論文中介紹的算法,據說速度非常的快。有了這個散列函數,我們就可以通過它來對世界里面的任意物件進行非常快的尋址了。
4.3、腳本語言解釋
在設計腳本語言之前,我們首先需要明白,我們的腳本語言要實現什么樣的功能?否則隨心所欲的做下去寫出個C的解釋器之類的也說不定。我們要實現的功能只是簡單的邏輯判斷和循環,其他所有的功能都可以由事先提供好的函數來完成。嗯,這樣我們就可以列出一張工作量的表單:設計物件在底層的保存結構,提供腳本和底層間的訪問接口,設計支持邏輯判斷和循環的解釋器。
下面先來談談物件在底層的保存結構。具體到每種不同屬性的物件,需要采用不同的結構,當然,如果你愿意的話,你可以所有的物件都采同同樣的結構,然后在結構里面設計一個散列表來保存各種不同的屬性。但這并不是一個好方法,過分的依賴散列表會讓你的游戲的邏輯變得繁雜不清。所以,盡量的區分每種不同的物件采用不同的結構來設計。但是有一點值得注意的是,不管是什么結構,有一些東西是統一的,就是我們所說的物件頭,那么我們怎么來設計這樣一個物件頭呢?
typedef struct object_head_s
{
char* name;
char* prog;
}object_head_t;
其中name是在散列表中這個物件的索引號,prog則是腳本解釋器需要解釋的程序內容。下面我們就以NPC為例來設計一個結構:
typedef struct npc_s
{
object_head_t header; // 物件頭
int hp; // NPC的hp值。
int level; // NPC的等級。
struct position_s position; // 當前的位置信息。
unsigned int personality; // NPC的個性,一個unsigned int可以保存24種個性。
}npc_t;
OK,結構設計完成,那么我們怎么來設計腳本解釋器呢?這里有兩種法,一種是用虛擬機的模式來解析腳本語言,另外一中則是用類似匯編語言的那種結構來設計,設置一些條件跳轉和循環就可以實現邏輯判斷和循環了,比如:
set name, "路人甲";
CHOOSE: random_choose_personality; // 隨機選擇NPC的個性
compare hp, 100; // 比較氣血,比較出的值可以放在一個固定的變量里面
ifless LESS; // hp < 100的話,則返回。
jump CHOOSE; // 否則繼續選擇,只到選到一個hp < 100的。
LESS: return success;
這種腳本結構就類似CPU的指令的結構,一條一條指令按照順序執行,對于腳本程序員(Script Programmer)也可以培養他們匯編能力的說。
那么怎么來模仿這種結構呢?我們拿CPU的指令做參照,首先得設置一些寄存器,CPU的寄存器的大小和數量是受硬件影響的,但我們是用內存來模擬寄存器,所以想要多大,就可以有多大。然后提供一些指令,包括四則運算,尋址,判斷,循環等等。接下來針對不同的腳本用不同的解析方法,比如說對NPC就用NPC固定的腳本,對ITEM就用ITEM固定的腳本,解析完以后就把結果生成底層該物件的結構用于使用。
而如果要用虛擬機來實現腳本語言的話呢,則會將工程變得無比之巨大,強烈不推薦使用,不過如果你想做一個通用的網絡游戲底層的話,則可以考慮設計一個虛擬機。虛擬機大體的解釋過程就是進行兩次編譯,第一次對關鍵字進行編譯,第二次生成匯編語言,然后虛擬機在根據編譯生成的匯編語言進行逐行解釋,如果大家對這個感興趣的話,可以去www.mudos.org上下載一份MudOS的原碼來研究研究。 大體的思路講到這里已經差不多了,下面將用unreal(虛幻)為實例,談一談網絡游戲服務器的設計。
posted @
2009-10-29 17:47 暗夜教父 閱讀(800) |
評論 (0) |
編輯 收藏
同步在網絡游戲中是非常重要的,它保證了每個玩家在屏幕上看到的東西大體是一樣的。其實呢,解決同步問題的最簡單的方法就是把每個玩家的動作都向其他玩家廣播一遍,這里其實就存在兩個問題:1,向哪些玩家廣播,廣播哪些消息。2,如果網絡延遲怎么辦。事實上呢,第一個問題是個非常簡單的問題,不過之所以我提出這個問題來,是提醒大家在設計自己的消息結構的時候,需要把這個因素考慮進去。而對于第二個問題,則是一個挺麻煩的問題,大家可以來看這么個例子:
比如有一個玩家A向服務器發了條指令,說我現在在P1點,要去P2點。指令發出的時間是T0,服務器收到指令的時間是T1,然后向周圍的玩家廣播這條消息,消息的內容是“玩家A從P1到P2”有一個在A附近的玩家B,收到服務器的這則廣播的消息的時間是T2,然后開始在客戶端上畫圖,A從P1到P2點。這個時候就存在一個不同步的問題,玩家A和玩家B的屏幕上顯示的畫面相差了T2-T1的時間。這個時候怎么辦呢?
有個解決方案,我給它取名叫 預測拉扯,雖然有些怪異了點,不過基本上大家也能從字面上來理解它的意思。要解決這個問題,首先要定義一個值叫:預測誤差。然后需要在服務器端每個玩家連接的類里面加一項屬性,叫TimeModified,然后在玩家登陸的時候,對客戶端的時間和服務器的時間進行比較,得出來的差值保存在TimeModified里面。還是上面的那個例子,服務器廣播消息的時候,就根據要廣播對象的TimeModified,計算出一個客戶端的CurrentTime,然后在消息頭里面包含這個CurrentTime,然后再進行廣播。并且同時在玩家A的客戶端本地建立一個隊列,保存該條消息,只到獲得服務器驗證就從未被驗證的消息隊列里面將該消息刪除,如果驗證失敗,則會被拉扯回P1點。然后當玩家B收到了服務器發過來的消息“玩家A從P1到P2”這個時候就檢查消息里面服務器發出的時間和本地時間做比較,如果大于定義的預測誤差,就算出在T2這個時間,玩家A的屏幕上走到的地點P3,然后把玩家B屏幕上的玩家A直接拉扯到P3,再繼續走下去,這樣就能保證同步。更進一步,為了保證客戶端運行起來更加smooth,我并不推薦直接把玩家拉扯過去,而是算出P3偏后的一點P4,然后用(P4-P1)/T(P4-P3)來算出一個很快的速度S,然后讓玩家A用速度S快速移動到P4,這樣的處理方法是比較合理的,這種解決方案的原形在國際上被稱為(Full plesiochronous),當然,該原形被我篡改了很多來適應網絡游戲的同步,所以而變成所謂的:預測拉扯。
另外一個解決方案,我給它取名叫 驗證同步,聽名字也知道,大體的意思就是每條指令在經過服務器驗證通過了以后再執行動作。具體的思路如下:首先也需要在每個玩家連接類型里面定義一個TimeModified,然后在客戶端響應玩家鼠標行走的同時,客戶端并不會先行走動,而是發一條走路的指令給服務器,然后等待服務器的驗證。服務器接受到這條消息以后,進行邏輯層的驗證,然后計算出需要廣播的范圍,包括玩家A在內,根據各個客戶端不同的TimeModified生成不同的消息頭,開始廣播,這個時候這個玩家的走路信息就是完全同步的了。這個方法的優點是能保證各個客戶端之間絕對的同步,缺點是當網絡延遲比較大的時候,玩家的客戶端的行為會變得比較不流暢,給玩家帶來很不爽的感覺。該種解決方案的原形在國際上被稱為(Hierarchical master-slave synchronization),80年代以后被廣泛應用于網絡的各個領域。
最后一種解決方案是一種理想化的解決方案,在國際上被稱為Mutual synchronization,是一種對未來網絡的前景的良好預測出來的解決方案。這里之所以要提這個方案,并不是說我們已經完全的實現了這種方案,而只是在網絡游戲領域的某些方面應用到這種方案的某些思想。我對該種方案取名為:半服務器同步。大體的設計思路如下:
首先客戶端需要在登陸世界的時候建立很多張廣播列表,這些列表在客戶端后臺和服務器要進行不及時同步,之所以要建立多張列表,是因為要廣播的類型是不止一種的,比如說有local message,有remote message,還有global message 等等,這些列表都需要在客戶端登陸的時候根據服務器發過來的消息建立好。在建立列表的同時,還需要獲得每個列表中廣播對象的TimeModified,并且要維護一張完整的用戶狀態列表在后臺,也是不及時的和服務器進行同步,根據本地的用戶狀態表,可以做到一部分決策由客戶端自己來決定,當客戶端發送這部分決策的時候,則直接將最終決策發送到各個廣播列表里面的客戶端,并對其時間進行校對,保證每個客戶端在收到的消息的時間是和根據本地時間進行校對過的。那么再采用預測拉扯中提到過的計算提前量,提高速度行走過去的方法,將會使同步變得非常的smooth。該方案的優點是不通過服務器,客戶端自己之間進行同步,大大的降低了由于網絡延遲而帶來的誤差,并且由于大部分決策都可以由客戶端來做,也大大的降低了服務器的資源。由此帶來的弊端就是由于消息和決策權都放在客戶端本地,所以給外掛提供了很大的可乘之機。
綜合以上三種關于網絡同步派系的優缺點,綜合出一套關于網絡游戲傳輸同步的較完整的解決方案,我稱它為綜合同步法(colligate synchronization)。大體設計思路如下:
首先將服務器需要同步的所有消息從劃分一個優先等級,然后按照3/4的比例劃分出重要消息和非重要消息,對于非重要消息,把決策權放在客戶端,在客戶端邏輯上建立相關的決策機構和各種消息緩存區,以及相關的消息緩存區管理機構,如下圖所示:

上圖簡單說明了對于非重要消息,客戶端的大體處理流程,其中有一個客戶端被動行為值得大家注意,其中包括對服務器發過來的某些驗證代碼做返回,來確保消息緩存中的消息和服務器端是一致的,從而有效的防止外掛來篡改本地消息緩存。其中的消息來源是包括本地的客戶端響應玩家的消息以及遠程服務器傳遞過來的消息。
對于重要消息,比如說戰斗或者是某些牽扯到玩家一些比較敏感數據的操作,則采用另外一套方案,該方案首先需要在服務器和客戶端之間建立一套Ping System,然后服務器保存和用戶的及時的ping值,當ping比較小的時候,響應玩家消息的同時先不進行動作,而是先把該消息反饋給服務器,并且阻塞,服務器收到該消息,進行邏輯驗證之后向所有該詳細廣播的有效對象進行廣播(包括消息發起者),然后客戶端收到該消息的驗證,才開始執行動作。而當ping比較大的時候,客戶端響應玩家消息的同時立刻進行動作,并且同時把該消息反饋給服務器,值得注意的是這個時候還需要在本地建立一個無驗證消息的隊列,把該消息入隊,執行動作的同時等待服務器的驗證,還需要保存當前狀態。服務器收到客戶端的請求后,進行邏輯驗證,并把消息反饋到各個客戶端,帶上各個客戶端校對過的本地時間。如果驗證通過不過,則通知消息發起者,該消息驗證失敗,然后客戶端自動把已經在進行中的動作取消,恢復原來狀態。如果驗證通過,則廣播到的各個客戶端根據從服務器獲得校對時間進行對其進行拉扯,保證在該行為完成之前完成同步。

至此,一個比較成熟的網絡游戲的同步機制已經初步建立起來了,接下來的邏輯代碼就根據各自不同的游戲風格以及側重點來寫了。
同步是網絡游戲最重要的問題,如何同步也牽扯到各個方面的問題,比如說游戲的規模,游戲的類型以及各種各樣的方面,對于規模比較大的游戲,在同步方面可以下很多的工夫,把消息分得十分的細膩,對于不同的消息采用不同的同步機制,而對于規模比較小的游戲,則可以采用大體上一樣的同步機制,究竟怎么樣同步,沒有個定式,是需要根據自己的不同情況來做出不同的同步決策的
網游同步算法之導航推測(Dead Reckoning)算法:
在了解該算法前,我們先來談談該算法的一些背景資料。大家都知道,在網絡傳輸的時候,延遲現象是很普遍的,而在基于Server/Client結構下的網絡游戲的同步也就成了很頭疼的問題,在保證客戶端響應用戶本地指令流暢的情況下,沒法有效的保證的同步的及時性。同樣,在軍方也有類似的事情發生,即使是同一LAN里面的機器,也會因為傳輸的延遲,導致一些運算的失誤,介于此,美國國防部投入了大量的資金用于研究一種比較的好的方案來解決分布式系統中的延遲問題,特別是一個叫分布式模擬運動(Distributed Interactive Simulation)的系統,這套系統呢,其中就提出了一套號稱是Latency Hiding & Bandwidth Reduction的方案,命名為Dead Reckoning。呵呵,來頭很大吧,恩,那么我們下面就來看看這套系統的一些觀點,以及我們如何把它運用到我們的網絡游戲的同步中。
首先,這套同步方案是基于我那篇《網絡游戲的同步》一文中的Mutual Synchronization同步方案的,也就是說,它并不是Server/Client結構的,而是基于客戶端之間的同步的。下面我們先來說一些本文中將用到的名詞概念:
網狀網絡:客戶端之間構成的網絡
節點:網狀網絡中的每個客戶端
極限誤差:進行同步的時候可能產生的誤差的極值
恩,在探討其原理的之前,我們先來看看我們需要一個什么樣的環境。首先,需要一個網狀網絡,網狀網絡如何構成呢?當有新節點進入的時候,通知該網絡里面的所有節點,各節點為該客戶端在本地創建一個副本,登出的時候,則通知所有節點銷毀本地關于該節點的副本。然后每個節點該保存一些什么數據呢?首先有一個很重要的包需要保存,叫做協議數據包(PDU Protocol Data Unit),PDU包含節點的一些相關的運動信息,比如當前位置,速度,運動方向,或者還有加速度等一些信息。除PDU之外,還有其他信息需要保存,比如說節點客戶端人物的HP,MP之類的。然后,保證每個節點在最少8秒之內要向其它節點廣播一次PDU信息。最后,設置一個極限誤差值。到此,其環境就算搭建完成了。下面,我們就來看看相關的具體算法:
假設在節點A有一個小人(路人甲),開始跑路了,這個時候,就像所有的節點廣播一次他的PDU信息,包括:速度(S),方向(O),加速度(A)。那么所有的節點就開始模擬路人甲的運動軌跡和路線,包括節點A本身(這點很重要),同時,路人甲在某某玩家的控制下,會不時的改變一下方向,讓其跑路的路線變得不是那么正規。在跑路的過程中,節點A有一個值在不停的記錄著其真實坐標和在后臺模擬運動的坐標的差值,當差值大于極限誤差的時候,則計算出當前的速度S,方向O和速度A(算法將在后面介紹),并廣播給網絡中其他所有節點。其他節點在收到這條消息之后呢,就可以用一些很平滑的移動把路人甲拉扯過去,然后重新調整模擬跑路的數據,讓其繼續在后臺模擬跑路。
很顯然,如果極限誤差定義得大了,其他節點看到的偏差就會過大,如果極限偏差定義得小了,網絡帶寬就會增大。如果定義這個極限誤差,就該根據各種數據的重要性來設計了。如果是回合制的網絡游戲,那么在走路上把極限誤差定義得大些無所謂,可以減少帶寬。但是如果是及時打斗的網絡游戲,那么就得把極限誤差定義得小一些,否則會出現某人看到某人老遠把自己給砍死的情況。
Dead Reckoning的主要算法有9種,但是只有兩種是解決主要問題的,其他的基本上只是針對不同的坐標系的一些不同的算法,這里就不一一介紹了。好,那么我們下面來看傳說中的最主要的兩種算法:
第一:目標點 = 原點 + 速度 * 時間差
第二:目標點 = 原點 + 速度 * 時間差 + 1/2 * 加速度 * 時間差
呵呵,傳說中的算法都是很經典的,雖然我們早在初中物理的時候就學過。
該算法的好處呢,正如它開始所說的,Latency Hiding & Bandwidth Reduction,從原則上解決了網絡延遲導致的不同步的問題,并且有效的減少了帶寬,不好的地方就是該算法基本上只能使用于移動中的同步,當然,移動的同步是網絡游戲中同步的最大的問題。
該方法結合我在《網絡游戲的同步》一文中提出的綜合同步法的構架可以基本上解決掉網絡游戲中走路同步的問題。相關問題歡迎大家一起討論。
有關導航推測算法(Dead Reckoning)中的平滑處理:
根據我上篇文章所介紹的,在節點A收到節點B新的PDU包時,如果和A本地的關于B的模擬運動的坐標不一致時,怎么樣在A的屏幕上把B拽到新的PDU包所描敘的點上面去呢,上文中只提了用“很平滑的移動”把B“拉扯”過去,那么實際中應該怎么操作呢?這里介紹四種方法。
第一種方法,我取名叫直接拉扯法,大家聽名字也知道,就是直接把B硬生生的拽到新的PDU包所描敘的坐標上去,該方法的好處是:簡單。壞處是:看了以下三種方法之后你就不會用這種方法了。
第二種方法,叫直線行走(Linear),即讓B從它的當前坐標走直線到新的PDU包所描敘的坐標,行走速度用上文中所介紹的經典算法:
目標點 = 原點 + 速度 * 時間差 + 1/2 * 加速度 * 時間差算出:
首先算出從當前坐標到PDU包中描敘的坐標所需要的時間:
T = Dest( TargetB – OriginB ) / Speed
然后根據新PDU包中所描敘的坐標信息模擬計算出在時間T之后,按照新的PDU包中的運動信息所應該達到的位置:
_TargetB = NewPDU.Speed * T
然后根據當前模擬行動中的B和_TargetB的距離配合時間T算出一個修正過的速度_S:
_S = Dest( _TargetB – OriginB ) / T
然后在畫面上讓B以速度_S走直線到Target_B,并且在走到之后調整其速度,方向,加速度等信息為新的PDU包中所描敘的。
這種方法呢,非常的土,會讓物體在畫面上移動起來變得非常的不現實,經常會出現很生硬的拐角,而且對于經常要修改的速度_S,在玩家A的畫面上,玩家B的行動會變得非常的詭異。其好處是:比第一種方法要好。
第三種方法,叫二次方程行走(Quadratic),該方法的原理呢,就是在直線行走的過程中,加入二次方程來計算一條曲線路徑,讓Dest( _TargetB – OriginB )的過程是一條曲線,而不是一條直線,恩,具體的實現方法,就是在Linear方法的計算中,設定一個二次方程,在Dest函數計算距離的時候根據設定的二次方程來計算,這樣一來,可以使B在玩家A屏幕上的移動變得比較的有人性化一些。但是該方法的考慮也是不周全的,僅僅只考慮了TargetB到_TargetB的方向,而沒有考慮新的PDU包中的方向描敘,那么從_TargetB開始模擬行走的時候,仍然是會出現比較生硬的拐角,那么下面提出的最終解決方案,將徹底解決這個問題。
最后一種方法叫:立方體抖動(Cubic Splines),這個東東比較復雜,它需要四個坐標信息作為它的參數來進行運算,第一個參數Pos1是OriginB,第二個參數Pos2是OriginB在模擬運行一秒以后的位置,第三個參數Pos3是到達_TargetB前一秒的位置,第四個參數pos4是_TargetB的位置。
Struct pos {
Coordinate X;
Coordinate Y;
}
Pos1 = OriginB
Pos2 = OriginB + V
Pos3 = _TargetB – V
Pos4 = _TargetB
運動軌跡中(x, y)的坐標。
x = At^3 + Bt^2 + Ct + D
y = Et^3 + Ft^2 + Gt + H
(其中時間t的取值范圍為0-1,在Pos1的時候為0,在Pos4的時候為1)
x(0-3)代表Pos1-Pos4中x的值,y(0-3)代表Pos1-Pos4中y的值
A = x3 – 3 * x2 +3 * x1 – x0
B = 3 * x2 – 6 * x1 + 3 * x0
C = 3 * x1 – 3 * x0
D = x0
E = y3 – 3 * y2 +3 * y1 – y0
F = 3 * y2 – 6 * y1 + 3 * y0
G = 3 * y1 – 3 * y0
H = y0
上面是公式,那么下面我們來看看如何獲得Pos1-Pos4:首先,Pos1和 Pos2的取值會比較容易獲得,根據OriginB配合當前的速度和方向可以獲得,然而Pos3和Pos4呢,怎么獲得呢?如果在從Pos1到Pos4的過程中有新的PDU到達,那么我們定義它為NewPackage。
Pos3 = NewPackage.X + NewPackage.Y * t + 1/2 * NewPackage.a * t^2
Pos4 = Pos3 – (NewPackage.V + NewPackage.a * t)
如果沒有NewPackage的情況下,則Pos3和Pos4按照開始所規定的方法獲得。
至此,關于導航推測的算法大致介紹完畢。
歡迎討論,聯系作者:QQ 181194 MSN: xiataiyi@hotmail.com
參考文獻《Defeating Lag with Cubic Splines》
posted @
2009-10-29 14:18 暗夜教父 閱讀(575) |
評論 (0) |
編輯 收藏