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

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


摘要: 簡(jiǎn)介:本教程基于Ogre Wiki上的Basic Tutorial系列,并依據(jù)筆者使用的vs2005+sp1+OgreSDK1.4.3開(kāi)發(fā)環(huán)境簡(jiǎn)化整理而來(lái),其中穿插著筆者自己的理解。這是教程的第一部分,也是我的學(xué)習(xí)筆記。正文:凡是翻譯過(guò)幾篇技術(shù)類文章的人都深知從頭至尾忠實(shí)重現(xiàn)作者的原意是一件多么令人頭疼的事情。當(dāng)我從諸多曾經(jīng)許諾要翻譯的文章中爬出來(lái)的時(shí)候,我決定這次不做那樣一個(gè)“傻子&...
閱讀全文
建議讀者對(duì)應(yīng) HGE 的官方的例子:Tutorial 02 - Using input, sound and rendering
來(lái)閱讀本文
渲染:
在 HGE 中,四邊形是一種圖元,對(duì)應(yīng)了結(jié)構(gòu)體 hgeQuad,另外還有三角形圖元,對(duì)應(yīng)
hgeTriple,為了渲染,我們現(xiàn)在需要使用 hgeQuad 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體如下:
struct hgeQuad
{
hgeVertex v[4]; // 頂點(diǎn)描述了這個(gè)四邊形
HTEXTURE tex; // 紋理的句柄或者為0
int
blend; // 混合模式(blending mode)
};
HGE 中圖元對(duì)應(yīng)的結(jié)構(gòu)體總含有這3個(gè)部分:頂點(diǎn),紋理句柄,混合模式
struct hgeVertex
{
float x,
y; // 屏幕的 x,y 坐標(biāo)
float z; //
Z-order,范圍 [0, 1]
DWORD col; //
頂點(diǎn)的顏色
float tx,
ty; // 紋理的 x,y 坐標(biāo)(賦值前需要規(guī)格化坐標(biāo)間隔,使得
tx,ty 取值范圍在[0,1])
};
規(guī)格化坐標(biāo)間隔在后面的例子中會(huì)談到。不過(guò)先要談到的一點(diǎn)是 tx,ty 的值超過(guò) 1 也是合法的
1. 顏色的表示:
顏色使用32位表示,從左開(kāi)始,8位為 Alpha 通道,8位紅色,8位綠色,8位藍(lán)色
對(duì)于后24位,如果全部為0,表示黑色,如果全部為1,表示白色
2. 定義顏色的運(yùn)算:
我們把顏色看成一個(gè)四維向量,即 alpha 通道,紅色,綠色,藍(lán)色這四個(gè)分量
<1> 顏色是可以相乘的
顏色的相乘是對(duì)應(yīng)的四個(gè)分量分別相乘的結(jié)果,即:alpha 通道的值與 alpha
通道的值相乘,紅色的值與紅色的值相乘,綠色的值與綠色的值相乘,藍(lán)色的值與藍(lán)色的值相乘。
<2> 顏色是可以相加的
同上,對(duì)應(yīng)分量相加。
顏色的每個(gè)分量使用浮點(diǎn)數(shù)表示,范圍是[0-1],相加操作可能導(dǎo)致溢出,一種處理的方式就是,如果溢出,則設(shè)定值為1。
3. 混合模式:
1)BLEND_COLORADD
表示頂點(diǎn)的顏色與紋理的紋元(texel)顏色相加,這使得紋理變亮,可見(jiàn)頂點(diǎn)顏色為 0x00000000
將不造成任何影響。
2)BLEND_COLORMUL
表示頂點(diǎn)的顏色與紋理的紋元顏色相乘,這使得紋理變暗,可見(jiàn)頂點(diǎn)顏色為 0xFFFFFFFF 將不造成任何影響。
注意:必須在1),2)中做一個(gè)選擇,且只能選擇1),2)中的一個(gè)。處理的對(duì)象是紋理顏色和頂點(diǎn)顏色。
這里有一個(gè)技巧:
如果我們需要在程序中顯示一個(gè)氣球,這個(gè)氣球的顏色不斷變化,這時(shí)候我們并不需要準(zhǔn)備多張不同顏色的氣球紋理,而只需要一張白色的氣球紋理,設(shè)置
blend 為 BLEND_COLORMUL,白色的R,G,B值被表示成
1.0,也就是說(shuō),紋理顏色和頂點(diǎn)顏色相乘的結(jié)果是頂點(diǎn)的顏色,那么就可以通過(guò)修改頂點(diǎn)顏色,得到任意顏色的氣球了。
3)BLEND_ALPHABLEND
渲染時(shí),將對(duì)象的像素顏色(而非頂點(diǎn)的顏色)與當(dāng)前屏幕的對(duì)應(yīng)像素顏色進(jìn)行 alpha 混合。alpha 混合使用到 alpha
通道,對(duì)于兩個(gè)像素顏色進(jìn)行如下操作,得到一個(gè)顏色:
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使用的是對(duì)象像素的顏色的 alpha 通道。可見(jiàn)如果對(duì)象像素顏色 alpha 通道為
0,那么結(jié)果就是只有當(dāng)前屏幕的像素顏色,也就是常常說(shuō)的 100% 透明,因此,我們可以理解 alpha
混合就是一個(gè)是圖像透明的操作,0 表示完全透明,255 表示完全不透明。
4)BLEND_ALPHAADD
渲染時(shí),將對(duì)象的像素顏色與當(dāng)前屏幕的對(duì)應(yīng)像素顏色相加,結(jié)果是有了變亮的效果。
注意:這里的3),4)必選其一,且只能選其一。處理的對(duì)象是對(duì)象像素顏色和屏幕像素顏色。選擇
BLEND_ALPHABLEND 并且設(shè)定對(duì)象的 alpha 通道為 FF,可使此組參數(shù)無(wú)效。
5)BLEND_ZWRITE
渲染時(shí),寫(xiě)像素的 Z-order 到 Z-buffer
6)BLEND_NOZWRITE
渲染時(shí),不寫(xiě)像素的 Z-order 到 Z-buffer
這里一樣是二者選一
設(shè)置舉例:
quad.blend=BLEND_ALPHAADD | BLEND_COLORMUL |
BLEND_ZWRITE; // quad 為 hgeQuad 變量
4. HGE 渲染
1)定義和初始化 hgeQuad 結(jié)構(gòu)體:
hgeQuad quad; // 定義四邊形
2)初始化 hgeQuad 變量:
// 設(shè)置混合模式
quad.blend=BLEND_ALPHAADD | BLEND_COLORMUL | BLEND_ZWRITE;
// 加載紋理
quad.tex = pHGE->Texture_Load("particles.png");
注意,讀取硬盤(pán)上資源的時(shí)候,可能會(huì)失敗,因此通常都需要檢查,例如:
if (!quad.tex)
{
MessageBox(NULL, "Load particles.png",
"Error", 0);
}
// 初始化頂點(diǎn)
for(int i=0;i<4;i++)
{
// 設(shè)置頂點(diǎn)的 z 坐標(biāo)
quad.v[i].z=0.5f;
// 設(shè)置頂點(diǎn)的顏色,顏色的格式為 0xAARRGGBB
quad.v[i].col=0xFFFFA000;
}
// 這里假定載入的紋理大小為
128*128,現(xiàn)在截取由點(diǎn)(96,64),(128,64),(128,96),(96,96)這四個(gè)點(diǎn)圍成的圖形。
quad.v[0].tx=96.0/128.0; quad.v[0].ty=64.0/128.0; //
規(guī)格化坐標(biāo)間隔
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;
注意,對(duì)于 hgeQuad 結(jié)構(gòu)體,頂點(diǎn) quad.v[0] 表示左上那個(gè)點(diǎn),quad.v[1]
表示右上的點(diǎn),quad.v[2] 表示右下的點(diǎn),quad.v[3] 表示左下的點(diǎn)。
// 設(shè)置 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)設(shè)置渲染函數(shù)(render function):
System_SetState(HGE_RENDERFUNC,RenderFunc);
RenderFunc 原型和幀函數(shù)一樣:
bool RenderFunc();
4)編寫(xiě) RenderFunc 函數(shù):
bool RenderFunc()
{
pHGE->Gfx_BeginScene(); //
在如何渲染之前,必須調(diào)用這個(gè)函數(shù)
pHGE->Gfx_Clear(0); //
清屏,使用黑色,即顏色為 0
pHGE->Gfx_RenderQuad(&quad);
// 渲染
pHGE->Gfx_EndScene(); //
結(jié)束渲染,并且更新窗口
return false; // 必須返回 false
}
補(bǔ)充:Load 函數(shù)是和 Free 函數(shù)成對(duì)出現(xiàn)的,即在硬盤(pán)上加載了資源之后,需要 Free 它們,例如:
quad.tex = pHGE->Texture_Load("particles");
// ...
pHGE->Texture_Free(quad.tex);
這里不得不談一下規(guī)格化坐標(biāo)間隔,而這之前,需要說(shuō)說(shuō) Texture_GetWidth(xxx) 和
Texture_GetHeight(xxx) 函數(shù),如果這樣調(diào)用:Texture_GetWidth(xxx)
獲取的是處于顯存中的紋理寬度,而 Texture_GetWidth(xxx, true)
獲取到的是圖像文件的寬度,需要特別主義的是,對(duì)于同一張紋理來(lái)說(shuō),這兩個(gè)值可能是不一樣的,那么在規(guī)格化坐標(biāo)間隔的時(shí)候,應(yīng)該明確的是,對(duì)于一個(gè)
w*h 圖像的圖片,那么對(duì)于圖中點(diǎn)(x,y)應(yīng)該轉(zhuǎn)換成為:
tx = x / pHGE->GetWidth(xxx);
ty = y / pHGE->GetHeight(xxx);
而不能寫(xiě)成:
tx = x / pHGE->GetWidth(xxx, true);
ty = y / pHGE->GetHeight(xxx, true);
這里要注意一下 x,y 的含義
最后再談一下 tx 和 ty,實(shí)際上 tx,ty 大于 1 也是合法的,例如:
tx = 800 / 64;
ty = 600 / 64;
這會(huì)使得圖片重復(fù),而具體的含義,可以通過(guò)實(shí)現(xiàn)來(lái)體會(huì)
音效:
使用音效是很簡(jiǎn)單的
1. 載入音效:
HEFFECT hEffect = pHGE->Effect_Load("sound.mp3");
2. 播放:
pHGE->Effect_PlayEx(hEffect);
或者 pHGE->Effect_Play(hEffect);
1)Effect_Play 函數(shù)只接受一個(gè)參數(shù)就是音效的句柄 HEFFECT xx;
2)Effect_PlayEx 函數(shù)較為強(qiáng)大,一共有四個(gè)參數(shù):
HCHANNEL Effect_PlayEx(
HEFFECT
effect, // 音效的句柄
int volume =
100, // 音量,100為最大,范圍是[0, 100]
int pan =
0, // 范圍是[-100, 100],-100表示只使用左聲道,100表示只使用右聲道
float pitch =
1.0, // 播放速度,1.0
表示正常速度,值越大播放速度越快,值越小播放越慢。這個(gè)值要大于0才有效(不可以等于0)
bool loop =
false // 是否循環(huán)播放,false表示不循環(huán)
);
輸入:
僅僅需要調(diào)用函數(shù) pHGE->Input_GetKeyState(HGEK_xxx);
來(lái)判斷輸入,應(yīng)該在幀函數(shù)中調(diào)用它,例如:
bool FrameFunc()
{
if
(pHGE->Input_GetKeyState(HGEK_LBUTTOM))
// ...
if (pHGE->Input_GetKeyState(HGEK_UP))
// ...
}
2009-12-7 22:19:36 LinkTalk.NET(909327571)
暗夜教父, 你說(shuō)的6萬(wàn)連接,每個(gè)連接每秒發(fā)1K包嗎?
2009-12-7 22:19:49 暗夜教父(199033)
en
2009-12-7 22:19:49 jack(357794482)
就是后面函數(shù)執(zhí)行完成之后的結(jié)果給前面的原子嗎
2009-12-7 22:20:54 LinkTalk.NET(909327571)
機(jī)器配置如何?還有6萬(wàn)個(gè)客戶端如何模擬的?
2009-12-7 22:21:41 暗夜教父(199033)
機(jī)器是AMD 雙核 3200+
2009-12-7 22:21:50 LinkTalk.NET(909327571)
另外就是每個(gè)連接上的時(shí)間間隔為1秒,沒(méi)有延遲嗎?
2009-12-7 22:22:12 暗夜教父(199033)
內(nèi)存是4G,操作系統(tǒng)ubuntu 9.10
2009-12-7 22:22:53 暗夜教父(199033)
我人為的沒(méi)做這種延遲
2009-12-7 22:23:04 暗夜教父(199033)
服務(wù)器和客戶端都是這個(gè)配置
2009-12-7 22:23:16 暗夜教父(199033)
客戶端是用erlang模擬并發(fā)
2009-12-7 22:23:39 暗夜教父(199033)
不過(guò)創(chuàng)建6W個(gè)連接花費(fèi)了一定的時(shí)間
2009-12-7 22:24:09 暗夜教父(199033)
對(duì)哦,這樣就不是6萬(wàn)個(gè)一起并發(fā)出來(lái)的
2009-12-7 22:24:09 LinkTalk.NET(909327571)
大概多久?
2009-12-7 22:24:19 暗夜教父(199033)
沒(méi)統(tǒng)計(jì)時(shí)間
2009-12-7 22:24:42 暗夜教父(199033)
這里存在問(wèn)題了
2009-12-7 22:25:27 暗夜教父(199033)
其實(shí)并發(fā)應(yīng)該是 連接數(shù) / 創(chuàng)建并發(fā)的時(shí)間
2009-12-7 22:25:27 LinkTalk.NET(909327571)
什么問(wèn)題?
2009-12-7 22:25:49 LinkTalk.NET(909327571)
這個(gè)無(wú)所謂
2009-12-7 22:26:08 LinkTalk.NET(909327571)
只要你全部連接創(chuàng)建好后一直保持住就可以了
2009-12-7 22:26:23 暗夜教父(199033)
一直保持也不是6萬(wàn)同時(shí)
2009-12-7 22:26:32 暗夜教父(199033)
創(chuàng)建連接有時(shí)差
2009-12-7 22:26:35 LinkTalk.NET(909327571)
6萬(wàn)個(gè)如果能夠1k/s的保持48小時(shí)的話,非常牛逼了
2009-12-7 22:26:38 暗夜教父(199033)
那么發(fā)生數(shù)據(jù)也有時(shí)差
2009-12-7 22:27:53 LinkTalk.NET(909327571)
你通過(guò)什么方式做到間隔一秒發(fā)一次數(shù)據(jù)的?
2009-12-7 22:28:32 LinkTalk.NET(909327571)
sleep嗎?
2009-12-7 22:29:19 LinkTalk.NET(909327571)
你有沒(méi)有看一下網(wǎng)卡的帶寬消耗
2009-12-7 22:29:37 LinkTalk.NET(909327571)
按道理應(yīng)該1k * 6萬(wàn) = 60M/s
2009-12-7 22:30:12 LinkTalk.NET(909327571)
如果達(dá)到60M以上基本算是真正的穩(wěn)定的實(shí)現(xiàn)了6萬(wàn)并發(fā)
2009-12-7 22:30:34 暗夜教父(199033)
client那邊是我同事寫(xiě)的
2009-12-7 22:30:39 暗夜教父(199033)
我不知道是不是sleep
2009-12-7 22:30:52 暗夜教父(199033)
帶寬消耗我還也真沒(méi)看
2009-12-7 22:30:53 LinkTalk.NET(909327571)
erlang里面好像也就只有sleep了
2009-12-7 22:31:06 暗夜教父(199033)
我估計(jì)不是
2009-12-7 22:31:52 LinkTalk.NET(909327571)
另外我懷疑當(dāng)并發(fā)高了以后,sleep會(huì)不準(zhǔn),真正的間隔肯定會(huì)大于sleep的時(shí)間
2009-12-7 22:32:50 暗夜教父(199033)
恩,確實(shí)有待驗(yàn)證
2009-12-7 22:33:04 暗夜教父(199033)
不過(guò)只有用應(yīng)用來(lái)驗(yàn)證了
2009-12-7 22:33:14 暗夜教父(199033)
等項(xiàng)目上線了,看看實(shí)際情況
2009-12-7 22:33:17 LinkTalk.NET(909327571)
嗯,呵呵
2009-12-7 22:33:24 LinkTalk.NET(909327571)
你們這個(gè)用在什么項(xiàng)目上馬?
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)
不過(guò)風(fēng)險(xiǎn)也很大
2009-12-7 22:35:15 LinkTalk.NET(909327571)
為什么?
2009-12-7 22:35:24 暗夜教父(199033)
沒(méi)做過(guò)
2009-12-7 22:35:28 暗夜教父(199033)
沒(méi)有成熟項(xiàng)目的經(jīng)驗(yàn)
2009-12-7 22:35:29 LinkTalk.NET(909327571)
呵呵
2009-12-7 22:35:32 暗夜教父(199033)
什么都有可能發(fā)生
2009-12-7 22:35:45 LinkTalk.NET(909327571)
傳統(tǒng)的游戲服務(wù)器,單臺(tái)到兩三萬(wàn)并發(fā)已經(jīng)很牛X了
2009-12-7 22:35:50 暗夜教父(199033)
未知因素太多
2009-12-7 22:36:00 暗夜教父(199033)
我估計(jì)6W還能上
2009-12-7 22:36:00 LinkTalk.NET(909327571)
而且硬件還不差的情況下
2009-12-7 22:36:11 LinkTalk.NET(909327571)
游戲的數(shù)據(jù)通信量比較大
2009-12-7 22:36:22 LinkTalk.NET(909327571)
嗯
2009-12-7 22:36:30 暗夜教父(199033)
沒(méi)過(guò)6W是因?yàn)榭蛻舳说亩丝诨旧蠜](méi)了
2009-12-7 22:36:45 LinkTalk.NET(909327571)
這個(gè)不會(huì)啊
2009-12-7 22:36:51 LinkTalk.NET(909327571)
客戶端端口沒(méi)有關(guān)系的
2009-12-7 22:36:55 暗夜教父(199033)
有吧
2009-12-7 22:36:59 LinkTalk.NET(909327571)
因?yàn)镮P不一樣
2009-12-7 22:37:11 暗夜教父(199033)
不是。我測(cè)試的時(shí)候
2009-12-7 22:37:15 LinkTalk.NET(909327571)
同一個(gè)IP有65535個(gè)端口的限制
2009-12-7 22:37:18 暗夜教父(199033)
6萬(wàn)個(gè)連接從一臺(tái)機(jī)器來(lái)的
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)
服務(wù)器端只監(jiān)聽(tīng)一個(gè)端口 應(yīng)該不會(huì)有端口數(shù)限制吧?
2009-12-7 22:38:24 LinkTalk.NET(909327571)
服務(wù)器端不會(huì)
2009-12-7 22:38:50 david(258667581)
做游戲的話 不是都雍和宮服務(wù)器嗎
2009-12-7 22:38:55 david(258667581)
都做的服務(wù)器嗎
2009-12-7 22:39:24 暗夜教父(199033)
。。。
2009-12-7 22:39:32 暗夜教父(199033)
我是做測(cè)試
2009-12-7 22:39:36 david(258667581)
另外 如果端口socket屬性設(shè)置reuse為true 是否可以超過(guò)6W端口
2009-12-7 22:39:48 LinkTalk.NET(909327571)
不會(huì)
2009-12-7 22:39:53 暗夜教父(199033)
服務(wù)器端是一臺(tái)機(jī)器,客戶端也是一臺(tái)機(jī)器
2009-12-7 22:39:56 LinkTalk.NET(909327571)
那個(gè)是針對(duì)不同的protocol
2009-12-7 22:40:02 暗夜教父(199033)
客戶端發(fā)起6W個(gè)連接
2009-12-7 22:40:19 暗夜教父(199033)
就要占用6W多個(gè)端口
2009-12-7 22:40:18 david(258667581)
不同的protocol是什么意思
2009-12-7 22:40:35 LinkTalk.NET(909327571)
tcp 和 udp 可以reuse同一個(gè)port
2009-12-7 22:41:09 LinkTalk.NET(909327571)
erlang其實(shí)我只是大概的了解
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再連接另外一個(gè)程序?
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)
我已經(jīng)用C#實(shí)現(xiàn)了
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)
自己實(shí)現(xiàn)消息的調(diào)度
2009-12-7 22:42:15 LinkTalk.NET(909327571)
還有實(shí)現(xiàn)異步編程接口
2009-12-7 22:42:41 david(258667581)
actor模式是什么意思
2009-12-7 22:42:59 LinkTalk.NET(909327571)
一種軟件設(shè)計(jì)模式
2009-12-7 22:43:16 LinkTalk.NET(909327571)
erlang/scala等并發(fā)平臺(tái)都是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)
因?yàn)槲沂煜#
2009-12-7 22:44:39 LinkTalk.NET(909327571)
也打算用java模擬
2009-12-7 22:45:01 LinkTalk.NET(909327571)
同時(shí)也因?yàn)镃#/Java有大量的開(kāi)發(fā)人員和豐富的第三方擴(kuò)展
2009-12-7 22:45:25 LinkTalk.NET(909327571)
同時(shí)也有很爽的IDE
2009-12-7 22:45:26 LinkTalk.NET(909327571)
:)
2009-12-7 22:45:42 david(258667581)
是 但是感覺(jué)如果你是要測(cè)性能的話 c#的性能可能跟不上erlang啊
2009-12-7 22:45:49 david(258667581)
用c的都會(huì)好些
2009-12-7 22:46:01 LinkTalk.NET(909327571)
其實(shí)erlang語(yǔ)言本身性能不見(jiàn)得高,因?yàn)槭悄_本語(yǔ)言
2009-12-7 22:46:15 LinkTalk.NET(909327571)
高并發(fā)是因?yàn)榧兿鬟f,可以有效的避免死鎖
2009-12-7 22:46:27 LinkTalk.NET(909327571)
傳統(tǒng)語(yǔ)言比如c/c++要避免死鎖比較難
2009-12-7 22:46:49 LinkTalk.NET(909327571)
給大家看老外的一個(gè)測(cè)試數(shù)據(jù)
2009-12-7 22:46:57 LinkTalk.NET(909327571)
erlang其實(shí)算是執(zhí)行效率相對(duì)比較差的
2009-12-7 22:47:20 david(258667581)
哪里 erlang的性能應(yīng)該是可以跟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)
這個(gè)里面有很多語(yǔ)言的性能測(cè)試比較
2009-12-7 22:48:23 LinkTalk.NET(909327571)
erlang算是比較差的
2009-12-7 22:49:02 LinkTalk.NET(909327571)
傳統(tǒng)語(yǔ)言達(dá)不到高并發(fā)是因?yàn)闊o(wú)法有效的避免死鎖,還有cpu調(diào)度做得不好
2009-12-7 22:50:17 暗夜教父(199033)
恩,C如果使用erlang的模式
2009-12-7 22:50:26 暗夜教父(199033)
絕對(duì)不會(huì)輸
2009-12-7 22:50:29 LinkTalk.NET(909327571)
嗯
2009-12-7 22:50:37 LinkTalk.NET(909327571)
erlang本身就是C寫(xiě)的
2009-12-7 22:50:41 暗夜教父(199033)
恩
2009-12-7 22:50:54 LinkTalk.NET(909327571)
我記得國(guó)內(nèi)好像有個(gè)牛人在研究用C++模擬erlang
2009-12-7 22:51:00 LinkTalk.NET(909327571)
好像是盛大的一個(gè)架構(gòu)師
2009-12-7 22:51:13 暗夜教父(199033)
貌似現(xiàn)在做linux下做服務(wù)器端得,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)
我記得好像云風(fēng)把大話西游的服務(wù)器端改成C了
2009-12-7 22:52:28 小生啊牙(86753957)
51.com的服務(wù)器就是用C++模擬erlang的
2009-12-7 22:52:50 小生啊牙(86753957)
使用協(xié)程
2009-12-7 22:53:59 LinkTalk.NET(909327571)
嗯,協(xié)程可以在傳統(tǒng)的面向過(guò)程的線程上模擬異步操作
2009-12-7 22:55:05 LinkTalk.NET(909327571)
其實(shí)高并發(fā)只是一種設(shè)計(jì)模式
2009-12-7 22:55:19 LinkTalk.NET(909327571)
erlang把這個(gè)設(shè)計(jì)模式固化并強(qiáng)制到語(yǔ)法里了
2009-12-7 22:56:22 LinkTalk.NET(909327571)
C#2.0開(kāi)始有個(gè)新的特性,叫iterator,通過(guò)yield關(guān)鍵字來(lái)實(shí)現(xiàn)coroutine(協(xié)程)
2009-12-7 22:56:39 LinkTalk.NET(909327571)
并且在語(yǔ)法上也可以用連貫的形式來(lái)實(shí)現(xiàn)異步的操作
2009-12-7 22:56:49 LinkTalk.NET(909327571)
和erlang的形式類似
2009-12-7 22:57:10 LinkTalk.NET(909327571)
java里面目前只有anonymous class可以實(shí)現(xiàn)異步調(diào)用
2009-12-7 22:57:44 LinkTalk.NET(909327571)
但是那個(gè)語(yǔ)法寫(xiě)起來(lái)有些牽強(qiáng),花括號(hào)會(huì)越嵌越深
2009-12-7 22:59:19 LinkTalk.NET(909327571)
我用C#實(shí)現(xiàn)的actor模式也可以處理每分鐘大概200萬(wàn)條消息(在PC上)
2009-12-7 22:59:24 LinkTalk.NET(909327571)
AMD雙核
2009-12-7 23:00:28 LinkTalk.NET(909327571)
也測(cè)試過(guò)HTTP 請(qǐng)求,大概可以達(dá)到1萬(wàn)多并發(fā),不過(guò)是6秒一個(gè)請(qǐng)求(sleep6秒,實(shí)際會(huì)延遲到10秒以上),cpu占用40-60
2009-12-7 23:01:54 LinkTalk.NET(909327571)
每分鐘200萬(wàn)消息是最簡(jiǎn)單的ping/pong測(cè)試
2009-12-7 23:06:12 david(258667581)
以前沒(méi)聽(tīng)說(shuō)過(guò) 協(xié)程 呵呵
2009-12-7 23:06:33 LinkTalk.NET(909327571)
是否有協(xié)程無(wú)所謂的
2009-12-7 23:07:14 LinkTalk.NET(909327571)
關(guān)鍵是純消息傳遞(避免死鎖)還有線程合理有效的調(diào)度(實(shí)現(xiàn)高效的異步處理)
2009-12-7 23:07:35 david(258667581)
恩 關(guān)鍵是避開(kāi)鎖
2009-12-7 23:08:04 LinkTalk.NET(909327571)
并最好再能夠在語(yǔ)法上將異步操作用順序化的代碼來(lái)表示
2009-12-7 23:08:22 LinkTalk.NET(909327571)
實(shí)在不行也無(wú)所謂,可以用event handler的方式
2009-12-7 23:08:48 david(258667581)
能否舉一個(gè)異步操作的具體應(yīng)用場(chǎng)景?
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)
簡(jiǎn)單的原理就是調(diào)用函數(shù)遞交或注冊(cè)一個(gè)IO請(qǐng)求到系統(tǒng)內(nèi)核
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)
系統(tǒng)IO內(nèi)核收到數(shù)據(jù)或相關(guān)的事件會(huì)觸發(fā)當(dāng)初注冊(cè)的回調(diào)函數(shù)
2009-12-7 23:10:59 LinkTalk.NET(909327571)
調(diào)用請(qǐng)求 和 數(shù)據(jù)返回或事件觸發(fā) 不在同一個(gè)操作系統(tǒng)線程上完成,就稱為異步
2009-12-7 23:11:13 LinkTalk.NET(909327571)
異步的IO才比較高效
2009-12-7 23:11:37 LinkTalk.NET(909327571)
操作系統(tǒng)的線程數(shù)有限制
2009-12-7 23:11:42 david(258667581)
這些都不難 無(wú)論是java和c#還是c,都已經(jīng)有專門(mén)的api支撐了
2009-12-7 23:11:51 david(258667581)
關(guān)鍵是業(yè)務(wù)上的操作
2009-12-7 23:11:55 LinkTalk.NET(909327571)
一般上了千以后,線程的效率就比較差了
2009-12-7 23:11:58 david(258667581)
鎖的都是業(yè)務(wù)
2009-12-7 23:12:02 LinkTalk.NET(909327571)
而且很耗cpu和內(nèi)存
2009-12-7 23:12:23 LinkTalk.NET(909327571)
所以要通過(guò)合理的調(diào)度和異步操作來(lái)分享寶貴的操作系統(tǒng)線程
2009-12-7 23:12:51 LinkTalk.NET(909327571)
嚴(yán)格遵守actor模式可以有效的避免死鎖
2009-12-7 23:13:04 david(258667581)
[表情]
2009-12-7 23:14:34 LinkTalk.NET(909327571)
我也在摸索和嘗試,erlang的消息調(diào)度和分布式支持是目前最好的
2009-12-7 23:14:49 LinkTalk.NET(909327571)
scala也不錯(cuò),twitter就放棄erlang轉(zhuǎn)向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)
分布式以后是趨勢(shì) 所以感覺(jué)erlang的生命力應(yīng)該還是很強(qiáng)的
2009-12-7 23:15:44 LinkTalk.NET(909327571)
嗯,分布式其實(shí)其他語(yǔ)言也可以做的
2009-12-7 23:16:05 LinkTalk.NET(909327571)
只是erlang已經(jīng)做了十幾年了
2009-12-7 23:16:19 LinkTalk.NET(909327571)
其他語(yǔ)言也肯定會(huì)逐漸支持的
2009-12-7 23:16:25 david(258667581)
恩
2009-12-7 23:16:43 LinkTalk.NET(909327571)
但是我個(gè)人比較覺(jué)得遺憾的是erlang的語(yǔ)法和編程模式
2009-12-7 23:17:08 LinkTalk.NET(909327571)
如果erlang代碼上到一定的量以后維護(hù)和調(diào)試就相當(dāng)麻煩了
2009-12-7 23:17:11 david(258667581)
語(yǔ)法習(xí)慣了就好 關(guān)鍵是對(duì)于結(jié)構(gòu)的支持
2009-12-7 23:17:17 david(258667581)
可讀性太差
2009-12-7 23:17:31 LinkTalk.NET(909327571)
嗯,數(shù)據(jù)結(jié)構(gòu)表現(xiàn)力也不夠豐富
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)
開(kāi)發(fā)環(huán)境也沒(méi)跟上 沒(méi)有很好的IDE
2009-12-7 23:19:16 LinkTalk.NET(909327571)
嗯
2009-12-7 23:19:37 LinkTalk.NET(909327571)
腳本語(yǔ)言重構(gòu)起來(lái)就相當(dāng)麻煩了
2009-12-7 23:19:57 T.t.T!Ck.¢#(121787333)
有一個(gè)老外也用C#來(lái)模擬erlang的模式
2009-12-7 23:20:06 LinkTalk.NET(909327571)
因?yàn)闊o(wú)類型,無(wú)法反射元數(shù)據(jù),沒(méi)有辦法自動(dòng)生成文檔,更難重構(gòu)
2009-12-7 23:20:07 Lenn(28663)
ERLANG跟Java一樣是編譯態(tài)語(yǔ)言,怎么成腳本語(yǔ)言了
2009-12-7 23:20:22 LinkTalk.NET(909327571)
erlang嚴(yán)格來(lái)講是腳本
2009-12-7 23:20:31 LinkTalk.NET(909327571)
弱類型的基本都是腳本
2009-12-7 23:20:44 LinkTalk.NET(909327571)
包括php也號(hào)稱支持編譯
2009-12-7 23:20:47 LinkTalk.NET(909327571)
其實(shí)還是腳本
2009-12-7 23:21:16 Lenn(28663)
bin code只有200多條指令,屬于典型的中間太語(yǔ)言,效率不會(huì)比Java差多少
2009-12-7 23:21:34 LinkTalk.NET(909327571)
嗯
2009-12-7 23:21:48 LinkTalk.NET(909327571)
我覺(jué)得任何東西都有得必有失
2009-12-7 23:21:57 LinkTalk.NET(909327571)
一方面太強(qiáng)了,比如有其他的缺陷
2009-12-7 23:22:06 LinkTalk.NET(909327571)
就看自己的喜好和具體的應(yīng)用場(chǎng)景了
2009-12-7 23:22:08 Lenn(28663)
圖形太弱
2009-12-7 23:22:47 Lenn(28663)
調(diào)試起來(lái)最爽,業(yè)務(wù)想對(duì)了基本不會(huì)編程從出錯(cuò)
2009-12-7 23:23:16 LinkTalk.NET(909327571)
嗯
2009-12-7 23:24:26 Lenn(28663)
我們就是一個(gè)案例,用JAVA做的東西,現(xiàn)在還不停改代碼,Erlang那一塊,要改也只是幾行一會(huì)的事情
2009-12-7 23:25:17 jack(357794482)
其實(shí)這種事情要看你對(duì)語(yǔ)言的處理能力
2009-12-7 23:25:20 LinkTalk.NET(909327571)
嗯
2009-12-7 23:25:35 LinkTalk.NET(909327571)
google的首席架構(gòu)師是搞java的
2009-12-7 23:25:41 LinkTalk.NET(909327571)
其實(shí)這個(gè)看具體情況的
2009-12-7 23:26:17 Lenn(28663)
用什么還有歷史原因,比如新的facebook很多用Erlang
2009-12-7 23:26:33 Lenn(28663)
因?yàn)樽龊玫臇|西更改語(yǔ)言是個(gè)大問(wèn)題
2009-12-7 23:26:36 LinkTalk.NET(909327571)
但是更新的twitter由erlang轉(zhuǎn)向了scala
2009-12-7 23:26:58 LinkTalk.NET(909327571)
另外erlang十幾年了,到今天才紅,也是有一定的原因的
2009-12-7 23:27:13 LinkTalk.NET(909327571)
erlang有非常突出的優(yōu)勢(shì),但是也存在一些不夠完美的地方
2009-12-7 23:29:04 Lenn(28663)
現(xiàn)在社區(qū)也挺活躍,業(yè)務(wù)有比較強(qiáng)的優(yōu)勢(shì)
2009-12-7 23:29:37 LinkTalk.NET(909327571)
嗯,是的;)
2009-12-7 23:29:57 LinkTalk.NET(909327571)
所以就算其他語(yǔ)言的開(kāi)發(fā)人員也開(kāi)始了解
2009-12-7 23:30:02 LinkTalk.NET(909327571)
學(xué)習(xí)、
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)
嗯
在ubuntu下安裝subversion服務(wù)器
Tuesday, 6. March 2007, 13:29:44
subversion, ubuntu
在ubuntu下安裝subversion服務(wù)器
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服務(wù)器
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
(其中兩個(gè)最常用的位置之一是:/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(第一個(gè)用戶的時(shí)候)
(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認(rèn)證領(lǐng)域:<
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
傳輸文件數(shù)據(jù).
提交后的修訂版為 1。
* enjoy it .
簡(jiǎn)介
如果您對(duì) Subversion 還比較陌生,本節(jié)將給您一個(gè)關(guān)于 Subversion 的簡(jiǎn)要介紹。
Subversion 是一款開(kāi)放源代碼的版本控制系統(tǒng)。使用 Subversion,您可以重新加載源代碼和文檔的歷史版本。Subversion 管理了源代碼在各個(gè)時(shí)期的版本。一個(gè)文件樹(shù)被集中放置在文件倉(cāng)庫(kù)中。這個(gè)文件倉(cāng)庫(kù)很像是一個(gè)傳統(tǒng)的文件服務(wù)器,只不過(guò)它能夠記住文件和目錄的每一次變化。
[編輯]假設(shè)
首先我們假設(shè)您能夠在 Ubuntu 中操作 Linux 的命令、編輯文件、啟動(dòng)和停止服務(wù)。當(dāng)然,我們還認(rèn)為您的 Ubuntu 正在運(yùn)行中,您可以使用 sudo 操作并且您打算使用 Subversion。
我們假設(shè)您可能需要使用所有可能的方法訪問(wèn) SVN 文件倉(cāng)庫(kù)。同時(shí)我們也認(rèn)為您應(yīng)該已經(jīng)配置好了您的 /etc/apt/sources.list 文件。
[編輯]本文涉及的范圍
要通過(guò) HTTP 協(xié)議訪問(wèn) SVN 文件倉(cāng)庫(kù),您需要安裝并配置好 Web 服務(wù)器。Apache 2 被證實(shí)可以很好的與 SVN 一起工作。關(guān)于 Apache 2 的安裝超出了本文的范圍,盡管如此,本文還是會(huì)涉及如何配置 Apache 2 使用 SVN。
類似的,要通過(guò) HTTPS 協(xié)議訪問(wèn) SVN 文件倉(cāng)庫(kù),您需要在您的 Apache 2 中安裝并配置好數(shù)字證書(shū),這也不在本文的討論范圍之中。
幸運(yùn)的,Subversion 已經(jīng)包含在 main 倉(cāng)庫(kù)中。所以,要安裝 Subversion,您只需要簡(jiǎn)單的運(yùn)行:
$ sudo apt-get install subversion
$ sudo apt-get install libapache2-svn
如果系統(tǒng)報(bào)告了依賴關(guān)系的錯(cuò)誤,請(qǐng)找出相應(yīng)的軟件包并安裝它們。如果存在其它問(wèn)題,也請(qǐng)自行解決。如果您是再不能解決這些問(wèn)題,可以考慮通過(guò) Ubuntu 的網(wǎng)站、Wiki、論壇或郵件列表尋求支持。
[編輯]服務(wù)器配置
您應(yīng)該已經(jīng)安裝了上述的軟件包。本節(jié)將闡述如何創(chuàng)建 SVN 文件倉(cāng)庫(kù)以及如何設(shè)置項(xiàng)目的訪問(wèn)權(quán)限。
[編輯]創(chuàng)建 SVN 倉(cāng)庫(kù)
許多位置都可以放置 Subversion 文件倉(cāng)庫(kù),其中兩個(gè)最常用的是:/usr/local/svn 以及 /home/svn。為了在下面的描述中簡(jiǎn)單明了,我們假設(shè)您的 Subversion 文件倉(cāng)庫(kù)放在 /home/svn,并且你的項(xiàng)目名稱是簡(jiǎn)單的“myproject”。
同樣的,也有許多常用的方式設(shè)置文件倉(cāng)庫(kù)的訪問(wèn)權(quán)限。然而,這也是安裝過(guò)程中最經(jīng)常出現(xiàn)錯(cuò)誤的地方,因此我們會(huì)對(duì)此進(jìn)行一個(gè)詳細(xì)說(shuō)明。典型的情況下,您應(yīng)該創(chuàng)建一個(gè)名為“Subversion”的組來(lái)?yè)碛形募}(cāng)庫(kù)所在的目錄。下面是一個(gè)快速的操作說(shuō)明,有關(guān)內(nèi)容請(qǐng)參考相關(guān)文檔的詳細(xì)說(shuō)明:
- 在 Ubuntu 菜單上選擇“系統(tǒng)->系統(tǒng)管理->用戶和組”;
- 將您自己和“www-data”(Apache 用戶)加入組成員中;
- 點(diǎn)擊“OK”以確認(rèn)修改,關(guān)閉該程序。
或者使用命令完成上述功能(增加組,并且把用戶加到組里):
sudo addgroup subversion
sudo usermod -G subversion -a www-data
再或者直接使用命令編輯組文件"sudo vi /etc/group",增加組和成員(不推薦):
$ sudo vi /etc/group
結(jié)果看上去,像這樣。
$ cat /etc/group|grep subversion
subversion:x:1001:www-data,exp
您需要注銷然后再登錄以便您能夠成為 subversion 組的一員,然后就可以執(zhí)行簽入文件(Check in,也稱提交文件)的操作了。
現(xiàn)在執(zhí)行下面的命令
$ sudo mkdir /home/svn
$ cd /home/svn
$ sudo mkdir myproject
$ sudo chown -R root:subversion myproject
下面的命令用于創(chuàng)建 SVN 文件倉(cāng)庫(kù):
$ sudo svnadmin create /home/svn/myproject
賦予組成員對(duì)所有新加入文件倉(cāng)庫(kù)的文件擁有相應(yīng)的權(quán)限:
$ sudo chmod -R g+rws myproject
如果上面這個(gè)命令在創(chuàng)建SVN文件倉(cāng)庫(kù)之前運(yùn)行,你可能在后續(xù)Check in的時(shí)候遇到如下錯(cuò)誤:
Can't open '/home/svn/myproject/db/txn-current-lock': Permission denied
查看txn-current-lock文件的權(quán)限和用戶以及組信息,應(yīng)該類似于:
$ ls -l /home/svn/myproject/db/txn-current-lock
-rw-rwSr-- 1 root subversion 0 2009-06-18 15:33 txn-current-lock
除了權(quán)限以外,用戶及其組如果不對(duì),則仍然會(huì)遇到上述問(wèn)題,可以再次運(yùn)行命令:
$ sudo chown -R root:subversion myproject
[編輯]訪問(wèn)方式
Subversion 文件倉(cāng)庫(kù)可以通過(guò)許多不同的方式進(jìn)行訪問(wèn)(Check Out,簽出)——通過(guò)本地硬盤(pán),或者通過(guò)各種網(wǎng)絡(luò)協(xié)議。無(wú)論如何,文件倉(cāng)庫(kù)的位置總是使用 URL 來(lái)表示。下表顯示了不同的 URL 模式對(duì)應(yīng)的訪問(wèn)方法:
模式 | 訪問(wèn)方法 |
file:/// | 直接訪問(wèn)本地硬盤(pán)上文件倉(cāng)庫(kù) |
http:// | 通過(guò) WebDAV 協(xié)議訪問(wèn)支持 Subversion 的 Apache 2 Web 服務(wù)器 |
https:// | 類似 http://,支持 SSL 加密 |
svn:// | 通過(guò)自帶協(xié)議訪問(wèn) svnserve 服務(wù)器 |
svn+ssh:// | 類似 svn://,支持通過(guò) SSH 通道 |
本節(jié)中,我們將看到如何配置 SVN 以使之能夠通過(guò)所有的方法得以訪問(wèn)。當(dāng)然這里我們之討論基本的方法。要了解更高級(jí)的用途,我們推薦您閱讀《使用 Subversion 進(jìn)行版本控制》在線電子書(shū)。
[編輯]直接訪問(wèn)文件倉(cāng)庫(kù)(file://)
這是所有訪問(wèn)方式中最簡(jiǎn)單的。它不需要事先運(yùn)行任何 SVN 服務(wù)。這種訪問(wèn)方式用于訪問(wèn)本地的 SVN 文件倉(cāng)庫(kù)。語(yǔ)法是:
$ svn co file:///home/svn/myproject
或者
$ svn co file://localhost/home/svn/myproject
注意:如果您并不確定主機(jī)的名稱,您必須使用三個(gè)斜杠(///),而如果您指定了主機(jī)的名稱,則您必須使用兩個(gè)斜杠(//).
對(duì)文件倉(cāng)庫(kù)的訪問(wèn)權(quán)限基于文件系統(tǒng)的權(quán)限。如果該用戶具有讀/寫(xiě)權(quán)限,那么他/她就可以簽出/提交修改。如果您像前面我們說(shuō)描述的那樣設(shè)置了相應(yīng)的組,您可以簡(jiǎn)單的將一個(gè)用戶添加到“subversion”組中以使其具有簽出和提交的權(quán)限。
[編輯]通過(guò) WebDAV 協(xié)議訪問(wèn)(http://)
要通過(guò) WebDAV 協(xié)議訪問(wèn) SVN 文件倉(cāng)庫(kù),您必須配置您的 Apache 2 Web 服務(wù)器。您必須加入下面的代碼片段到您的 /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>
如果需要用戶每次登錄時(shí)都進(jìn)行用戶密碼驗(yàn)證,請(qǐng)將<LimitExcept GET PROPFIND OPTIONS REPORT>與</LimitExcept>兩行注釋掉。
當(dāng)您添加了上面的內(nèi)容,您必須重新起動(dòng) Apache 2 Web 服務(wù)器,請(qǐng)輸入下面的命令:
sudo /etc/init.d/apache2 restart
接下來(lái),您需要?jiǎng)?chuàng)建 /etc/subversion/passwd 文件,該文件包含了用戶授權(quán)的詳細(xì)信息。要添加用戶,您可以執(zhí)行下面的命令:
sudo htpasswd -c /etc/subversion/passwd user_name
它會(huì)提示您輸入密碼,當(dāng)您輸入了密碼,該用戶就建立了。“-c”選項(xiàng)表示創(chuàng)建新的/etc/subversion/passwd文件,所以u(píng)ser_name所指的用戶將是文件中唯一的用戶。如果要添加其他用戶,則去掉“-c”選項(xiàng)即可:
sudo htpasswd /etc/subversion/passwd other_user_name
您可以通過(guò)下面的命令來(lái)訪問(wèn)文件倉(cāng)庫(kù):
$ svn co http://hostname/svn/myproject myproject --username user_name
它會(huì)提示您輸入密碼。您必須輸入您使用 htpasswd 設(shè)置的密碼。當(dāng)通過(guò)驗(yàn)證,項(xiàng)目的文件就被簽出了。
警告:密碼是通過(guò)純文本傳輸?shù)摹H绻鷵?dān)心密碼泄漏的問(wèn)題,我們建議您使用 SSL 加密,有關(guān)詳情請(qǐng)看下一節(jié)。
[編輯]通過(guò)具有安全套接字(SSL)的 WebDAV 協(xié)議訪問(wèn)(https://)
通過(guò)具有 SSL 加密的 WebDAV 協(xié)議訪問(wèn) SVN 文件倉(cāng)庫(kù)(https://)非常類似上節(jié)所述的內(nèi)容,除了您必須為您的 Apache 2 Web 服務(wù)器設(shè)置數(shù)字證書(shū)之外。
您可以安裝由諸如 Verisign 發(fā)放的數(shù)字簽名,或者您可以安裝您自己的數(shù)字簽名。
我們假設(shè)您已經(jīng)為 Apache 2 Web 服務(wù)器安裝和配置好了相應(yīng)的數(shù)字證書(shū)?,F(xiàn)在按照上一節(jié)所描述的方法訪問(wèn) SVN 文件倉(cāng)庫(kù),別忘了把 http:// 換成https://。如何,幾乎是一模一樣的!
[編輯]通過(guò)自帶協(xié)議訪問(wèn)(svn://)
當(dāng)您創(chuàng)建了 SVN 文件倉(cāng)庫(kù),您可以修改 /home/svn/myproject/conf/svnserve.conf 來(lái)配置其訪問(wèn)控制。
例如,您可以取消下面的注釋符號(hào)來(lái)設(shè)置授權(quán)機(jī)制:
# [general]
# password-db = passwd
現(xiàn)在,您可以在“passwd”文件中維護(hù)用戶清單。編輯同一目錄下“passwd”文件,添加新用戶。語(yǔ)法如下:
username = password
#(注意行開(kāi)始不要有多余空格)
要了解詳情,請(qǐng)參考該文件。
現(xiàn)在,您可以在本地或者遠(yuǎn)程通過(guò) svn:// 當(dāng)文 SVN 了,您可以使用“svnserve”來(lái)運(yùn)行 svnserver,語(yǔ)法如下:
$ svnserve -d --foreground -r /home/svn
# -d -- daemon mode
# --foreground -- run in foreground (useful for debugging)
# -r -- root of directory to serve
要了解更多信息,請(qǐng)輸入:
$ svnserve --help
當(dāng)您執(zhí)行了該命令,SVN 就開(kāi)始監(jiān)聽(tīng)默認(rèn)的端口(3690)。您可以通過(guò)下面的命令來(lái)訪問(wèn)文件倉(cāng)庫(kù):
$ svn co svn://hostname/myproject myproject --username user_name
基于服務(wù)器的配置,它會(huì)要求輸入密碼。一旦通過(guò)驗(yàn)證,就會(huì)簽出文件倉(cāng)庫(kù)中的代碼。
要同步文件倉(cāng)庫(kù)和本地的副本,您可以執(zhí)行 update 子命令,語(yǔ)法如下:
$ cd project_dir
$ svn update
要了解更多的 SVN 子命令,您可以參考手冊(cè)。例如要了解 co (checkout) 命令,請(qǐng)執(zhí)行:
$ svn co --help
或者這樣
$ svn --help commit
或者直接
? svn help co
checkout (co): 從版本庫(kù)簽出工作副本。
使用: checkout URL[@REV]... [PATH]
。。。。。
一個(gè)實(shí)例:
? killall svnserve; svnserve -d -r /home/svn/
/home/svn/lj12-source/conf ? dog *
authz:[groups]
authz:lj12 = veexp
authz:[lj12-source:/] <-注意寫(xiě)法。
authz:veexp = rw
authz:@lj12 = rw
authz:* = passwd:[users] <-2個(gè)用戶和密碼。
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
認(rèn)證領(lǐng)域: <svn://localhost:3690> a712643f-661e-0410-8ad4-f0554cd88977
用戶名: veexp “veexp”的密碼:
A lj12-source/tim.h A lj12-source/en.c
......
認(rèn)證失敗的密碼緩沖記錄位置,明文密碼。到1.6版本,可能使用keyring管理。如果調(diào)試密碼,直接刪除如下文件就可。
~/.subversion/auth/svn.simple/:
eea34a6f7baa67a3639cacd6a428dba4
[編輯]通過(guò)具被SSH隧道保護(hù)的自帶協(xié)議訪問(wèn)(svn+ssh://)
配置和服務(wù)器進(jìn)程于上節(jié)所述相同。我們假設(shè)您已經(jīng)運(yùn)行了“svnserve”命令。
我們還假設(shè)您運(yùn)行了 ssh 服務(wù)并允許接入。要驗(yàn)證這一點(diǎn),請(qǐng)嘗試使用 ssh 登錄計(jì)算機(jī)。如果您可以登錄,那么大功告成,如果不能,請(qǐng)?jiān)趫?zhí)行下面的步驟前解決它。
svn+ssh:// 協(xié)議使用 SSH 加密來(lái)訪問(wèn) SVN 文件倉(cāng)庫(kù)。如您所知,數(shù)據(jù)傳輸是加密的。要訪問(wèn)這樣的文件倉(cāng)庫(kù),請(qǐng)輸入:
$ svn co svn+ssh://hostname/home/svn/myproject myproject --username user_name
注意:在這種方式下,您必須使用完整的路徑(/home/svn/myproject)來(lái)訪問(wèn) SVN 文件倉(cāng)庫(kù)
基于服務(wù)器的配置,它會(huì)要求輸入密碼。您必須輸入您用于登錄 ssh 的密碼,一旦通過(guò)驗(yàn)證,就會(huì)簽出文件倉(cāng)庫(kù)中的代碼。
您還應(yīng)該參考 SVN book 以了解關(guān)于 svn+ssh:// 協(xié)議的詳細(xì)信息。
(呵)近一段時(shí)間由于工作需要,終于開(kāi)始玩Linux了,今天搞了一天的MySQL編譯安裝,記錄下來(lái),備忘吧?。?/p>
(卡)安裝環(huán)境:VmWare5(橋接模式) + RedHat E AS 4 + 已安裝了開(kāi)發(fā)工具以及相關(guān)開(kāi)發(fā)包(安裝Linux系統(tǒng)時(shí)自己要定制的),并測(cè)試成功
(!)先給出MySQL For Linux 源碼下載地址,是xx.tar.zg格式的
http://www.filewatcher.com/m/mysql-5.0.45.tar.gz.24433261.0.0.html
(1)
-------------預(yù)備工作----------
1:假如下載的文件名為:mysql-5.0.45.tar.gz
2:假如copy到 /home下
3:groupadd mysql #添加mysql組
4:useradd -g mysql mysql #添加一個(gè)mysql用戶
5:cd /home #進(jìn)入到該目錄
-----------------------編譯過(guò)程-----------------------
6:tar zxvf mysql-5.0.45.tar.gz #解壓后,在該目錄下會(huì)出現(xiàn)一個(gè)同名的文件夾
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 #參數(shù)設(shè)置,可以先不明白,以后再修改配置
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 . #改當(dāng)前目錄的捅有者為root。注意,最后有個(gè) . 啊,表示當(dāng)前目錄
16:chown -R mysql /usr/local/mysql/var #-R表示遞歸之下的所有目錄
17:chgrp -R mysql /usr/local/mysql #改變目錄所屬為mysql
18:bin/mysqld_safe --user=mysql & #啟動(dòng)mysql
-----------------------------------------更改mysql的root用戶密碼----------------------------
19:bin/mysqladmin -uroot password 123456 #在mysql政黨啟動(dòng)的情況下,更改root用戶的登錄密碼
20:bin/mysql -uroot -p #輸入此命令后,會(huì)提示你輸入root用戶密碼123456,
21:show databases; #如果查出所有數(shù)據(jù)庫(kù),就恭喜你了
------------------------------------------------------把mysql加入到系統(tǒng)服務(wù)中-------------------------------------
22:cp /usr/local/mysql/share/mysql/mysql.server /etc/init.d/mysqld
chkconfig --add mysqld #加入到系統(tǒng)服務(wù)中,就可以通過(guò)service mysqld start|stop|status|restart等進(jìn)行管理,很是方便,就不用再到/usr/local/mysql5.0.45/bin/啟動(dòng)mysql了
------------------------------------------------------------------配置mysql環(huán)境變量------------------------------------------------
23:cd /root #回到你的個(gè)人主目錄,我這里是用root登陸的
cp .bashrc .bashrc.bak #備份一下吧
vi .bashrc
在最后加入:export PATH=/usr/local/mysql/bin:$PATH:.
source ~/.bashrc #回到終端再輸入此命令,以使剛修改的起作用,~代表用戶主目錄
env #查看一下是否生效
24:此是用來(lái)替換23步的一種方法
cp /usr/local/mysql/bin/mysql /usr/bin/mysql #把mysql常用的工具目錄加入到系統(tǒng)變量目錄中去,自己選擇性加,這樣做主要是可以直接運(yùn)行該工具,而不需要切換到該目錄下,類似于添加環(huán)境變量了
-------------------------------------------------------------------------------讓Linux開(kāi)放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用戶開(kāi)啟mysql遠(yuǎn)程訪問(wèn)權(quán)限--------------------------------------------
26:shutdown -hr now #重啟
ps -e | grep mysql #查看mysql是否已隨開(kāi)機(jī)啟動(dòng),或者輸入:service mysqld status
mysql -uroot -p #進(jìn)入mysql
輸入root用戶的密碼
grant all on *.* to root@'%' identified by '123456';
#grant 權(quán)限 on 數(shù)據(jù)庫(kù)名.表名 to 用戶@登錄主機(jī) identified by "用戶密碼";
flush privileges; #為了開(kāi)發(fā)方便,可以讓root用戶具有遠(yuǎn)程訪問(wèn)的權(quán)限
#最后,再附上一個(gè)很好用的mysql客戶端,http://download.csdn.net/source/924456
(2)默認(rèn)的mysql數(shù)據(jù)庫(kù)目錄是 /usr/local/mysql-5.0.45/var
我們?cè)诎惭b時(shí)指定了安裝目錄為/usr/local/mysql-5.0.45,除了在這里安裝所要的文件外,還有一部分用戶常用的,可執(zhí)行二進(jìn)制文件被放到了/usr/bin中,其實(shí),在/usr/local/mysql-5.0.45/bin下,全都有這些命令了,之所以要在/usr/bin中把那幾個(gè)命令考過(guò)來(lái),就是為了方便,相當(dāng)于設(shè)置環(huán)境變量了,你可以echo $PATH一下,里面一定有/usr/bin這個(gè)值的。
明白了安裝過(guò)程,刪除mysql也就不足為難了
(3)通過(guò)一個(gè)完整的例子,自己會(huì)學(xué)到很多東西,linux常用命令還真需要自己來(lái),整理記錄
談這個(gè)話題之前,首先要讓大家知道,什么是服務(wù)器。在網(wǎng)絡(luò)游戲中,服務(wù)器所扮演的角色是同步,廣播和服務(wù)器主動(dòng)的一些行為,比如說(shuō)天氣,NPC AI之類的,之所以現(xiàn)在的很多網(wǎng)絡(luò)游戲服務(wù)器都需要負(fù)擔(dān)一些游戲邏輯上的運(yùn)算是因?yàn)闉榱朔乐箍蛻舳说淖鞅仔袨?。了解到這一點(diǎn),那么本系列的文章將分為兩部分來(lái)談?wù)劸W(wǎng)絡(luò)游戲服務(wù)器的設(shè)計(jì),一部分是講如何做好服務(wù)器的網(wǎng)絡(luò)連接,同步,廣播以及NPC的設(shè)置,另一部分則將著重談?wù)勀男┻壿嫹旁诜?wù)器比較合適,并且用什么樣的結(jié)構(gòu)來(lái)安排這些邏輯。
服務(wù)器的網(wǎng)絡(luò)連接
大多數(shù)的網(wǎng)絡(luò)游戲的服務(wù)器都會(huì)選擇非阻塞select這種結(jié)構(gòu),為什么呢?因?yàn)榫W(wǎng)絡(luò)游戲的服務(wù)器需要處理的連接非常之多,并且大部分會(huì)選擇在Linux/Unix下運(yùn)行,那么為每個(gè)用戶開(kāi)一個(gè)線程實(shí)際上是很不劃算的,一方面因?yàn)樵贚inux/Unix下的線程是用進(jìn)程這么一個(gè)概念模擬出來(lái)的,比較消耗系統(tǒng)資源,另外除了I/O之外,每個(gè)線程基本上沒(méi)有什么多余的需要并行的任務(wù),而且網(wǎng)絡(luò)游戲是互交性非常強(qiáng)的,所以線程間的同步會(huì)成為很麻煩的問(wèn)題。由此一來(lái),對(duì)于這種含有大量網(wǎng)絡(luò)連接的單線程服務(wù)器,用阻塞顯然是不現(xiàn)實(shí)的。對(duì)于網(wǎng)絡(luò)連接,需要用一個(gè)結(jié)構(gòu)來(lái)儲(chǔ)存,其中需要包含一個(gè)向客戶端寫(xiě)消息的緩沖,還需要一個(gè)從客戶端讀消息的緩沖,具體的大小根據(jù)具體的消息結(jié)構(gòu)來(lái)定了。另外對(duì)于同步,需要一些時(shí)間校對(duì)的值,還需要一些各種不同的值來(lái)記錄當(dāng)前狀態(tài),下面給出一個(gè)初步的連接的結(jié)構(gòu):
typedef connection_s {
user_t *ob; /* 指向處理服務(wù)器端邏輯的結(jié)構(gòu) */
int fd; /* socket連接 */
struct sockaddr_in addr; /* 連接的地址信息 */
char text[MAX_TEXT]; /* 接收的消息緩沖 */
int text_end; /* 接收消息緩沖的尾指針 */
int text_start; /* 接收消息緩沖的頭指針 */
int last_time; /* 上一條消息是什么時(shí)候接收到的 */
struct timeval latency; /* 客戶端本地時(shí)間和服務(wù)器本地時(shí)間的差值 */
struct timeval last_confirm_time; /* 上一次驗(yàn)證的時(shí)間 */
short is_confirmed; /* 該連接是否通過(guò)驗(yàn)證過(guò) */
int ping_num; /* 該客戶端到服務(wù)器端的ping值 */
int ping_ticker; /* 多少個(gè)IO周期處理更新一次ping值 */
int message_length; /* 發(fā)送緩沖消息長(zhǎng)度 */
char message_buf[MAX_TEXT]; /* 發(fā)送緩沖區(qū) */
int iflags; /* 該連接的狀態(tài) */
} connection_t;
服務(wù)器循環(huán)的處理所有連接,是一個(gè)死循環(huán)過(guò)程,每次循環(huán)都用select檢查是否有新連接到達(dá),然后循環(huán)所有連接,看哪個(gè)連接可以寫(xiě)或者可以讀,就處理該連接的讀寫(xiě)。由于所有的處理都是非阻塞的,所以所有的Socket IO都可以用一個(gè)線程來(lái)完成。
由于網(wǎng)絡(luò)傳輸?shù)年P(guān)系,每次recv()到的數(shù)據(jù)可能不止包含一條消息,或者不到一條消息,那么怎么處理呢?所以對(duì)于接收消息緩沖用了兩個(gè)指針,每次接收都從text_start開(kāi)始讀起,因?yàn)槔锩鏆埩舻目赡苁巧洗谓邮盏降亩嘤嗟陌霔l消息,然后text_end指向消息緩沖的結(jié)尾。這樣用兩個(gè)指針就可以很方便的處理這種情況,另外有一點(diǎn)值得注意的是:解析消息的過(guò)程是一個(gè)循環(huán)的過(guò)程,可能一次接收到兩條以上的消息在消息緩沖里面,這個(gè)時(shí)候就應(yīng)該執(zhí)行到消息緩沖里面只有一條都不到的消息為止,大體流程如下:
while ( text_end – text_start > 一條完整的消息長(zhǎng)度 )
{
從text_start處開(kāi)始處理;
text_start += 該消息長(zhǎng)度;
}
memcpy ( text, text + text_start, text_end – text_start );
對(duì)于消息的處理,這里首先就需要知道你的游戲總共有哪些消息,所有的消息都有哪些,才能設(shè)計(jì)出比較合理的消息頭。一般來(lái)說(shuō),消息大概可分為主角消息,場(chǎng)景消息,同步消息和界面消息四個(gè)部分。其中主角消息包括客戶端所控制的角色的所有動(dòng)作,包括走路,跑步,戰(zhàn)斗之類的。場(chǎng)景消息包括天氣變化,一定的時(shí)間在場(chǎng)景里出現(xiàn)一些東西等等之類的,這類消息的特點(diǎn)是所有消息的發(fā)起者都是服務(wù)器,廣播對(duì)象則是場(chǎng)景里的所有玩家。而同步消息則是針對(duì)發(fā)起對(duì)象是某個(gè)玩家,經(jīng)過(guò)服務(wù)器廣播給所有看得見(jiàn)他的玩家,該消息也是包括所有的動(dòng)作,和主角消息不同的是該種消息是服務(wù)器廣播給客戶端的,而主角消息一般是客戶端主動(dòng)發(fā)給服務(wù)器的。最后是界面消息,界面消息包括是服務(wù)器發(fā)給客戶端的聊天消息和各種屬性及狀態(tài)信息。
下面來(lái)談?wù)勏⒌慕M成。一般來(lái)說(shuō),一個(gè)消息由消息頭和消息體兩部分組成,其中消息頭的長(zhǎng)度是不變的,而消息體的長(zhǎng)度是可變的,在消息體中需要保存消息體的長(zhǎng)度。由于要給每條消息一個(gè)很明顯的區(qū)分,所以需要定義一個(gè)消息頭特有的標(biāo)志,然后需要消息的類型以及消息ID。消息頭大體結(jié)構(gòu)如下:
type struct message_s {
unsigned short message_sign;
unsigned char message_type;
unsigned short message_id
unsigned char message_len
}message_t;
服務(wù)器的廣播
服務(wù)器的廣播的重點(diǎn)就在于如何計(jì)算出廣播的對(duì)象。很顯然,在一張很大的地圖里面,某個(gè)玩家在最東邊的一個(gè)動(dòng)作,一個(gè)在最西邊的玩家是應(yīng)該看不到的,那么怎么來(lái)計(jì)算廣播的對(duì)象呢?最簡(jiǎn)單的辦法,就是把地圖分塊,分成大小合適的小塊,然后每次只象周圍幾個(gè)小塊的玩家進(jìn)行廣播。那么究竟切到多大比較合適呢?一般來(lái)說(shuō),切得塊大了,內(nèi)存的消耗會(huì)增大,切得塊小了,CPU的消耗會(huì)增大(原因會(huì)在后面提到)。個(gè)人覺(jué)得切成一屏左右的小塊比較合適,每次廣播廣播周圍九個(gè)小塊的玩家,由于廣播的操作非常頻繁,那么遍利周圍九塊的操作就會(huì)變得相當(dāng)?shù)念l繁,所以如果塊分得小了,那么遍利的范圍就會(huì)擴(kuò)大,CPU的資源會(huì)很快的被吃完。
切好塊以后,怎么讓玩家在各個(gè)塊之間走來(lái)走去呢?讓我們來(lái)想想在切換一次塊的時(shí)候要做哪些工作。首先,要算出下個(gè)塊的周圍九塊的玩家有哪些是現(xiàn)在當(dāng)前塊沒(méi)有的,把自己的信息廣播給那些玩家,同時(shí)也要算出下個(gè)塊周圍九塊里面有哪些物件是現(xiàn)在沒(méi)有的,把那些物件的信息廣播給自己,然后把下個(gè)塊的周圍九快里沒(méi)有的,而現(xiàn)在的塊周圍九塊里面有的物件的消失信息廣播給自己,同時(shí)也把自己消失的消息廣播給那些物件。這個(gè)操作不僅煩瑣而且會(huì)吃掉不少CPU資源,那么有什么辦法可以很快的算出這些物件呢?一個(gè)個(gè)做比較?顯然看起來(lái)就不是個(gè)好辦法,這里可以參照二維矩陣碰撞檢測(cè)的一些思路,以自己周圍九塊為一個(gè)矩陣,目標(biāo)塊周圍九塊為另一個(gè)矩陣,檢測(cè)這兩個(gè)矩陣是否碰撞,如果兩個(gè)矩陣相交,那么沒(méi)相交的那些塊怎么算。這里可以把相交的塊的坐標(biāo)轉(zhuǎn)換成內(nèi)部坐標(biāo),然后再進(jìn)行運(yùn)算。
對(duì)于廣播還有另外一種解決方法,實(shí)施起來(lái)不如切塊來(lái)的簡(jiǎn)單,這種方法需要客戶端來(lái)協(xié)助進(jìn)行運(yùn)算。首先在服務(wù)器端的連接結(jié)構(gòu)里面需要增加一個(gè)廣播對(duì)象的隊(duì)列,該隊(duì)列在客戶端登陸服務(wù)器的時(shí)候由服務(wù)器傳給客戶端,然后客戶端自己來(lái)維護(hù)這個(gè)隊(duì)列,當(dāng)有人走出客戶端視野的時(shí)候,由客戶端主動(dòng)要求服務(wù)器給那個(gè)物件發(fā)送消失的消息。而對(duì)于有人總進(jìn)視野的情況,則比較麻煩了。
首先需要客戶端在每次給服務(wù)器發(fā)送update position的消息的時(shí)候,服務(wù)器都給該連接算出一個(gè)視野范圍,然后在需要廣播的時(shí)候,循環(huán)整張地圖上的玩家,找到坐標(biāo)在其視野范圍內(nèi)的玩家。使用這種方法的好處在于不存在轉(zhuǎn)換塊的時(shí)候需要一次性廣播大量的消息,缺點(diǎn)就是在計(jì)算廣播對(duì)象的時(shí)候需要遍歷整個(gè)地圖上的玩家,如果當(dāng)一個(gè)地圖上的玩家多得比較離譜的時(shí)候,該操作就會(huì)比較的慢。
服務(wù)器的同步
同步在網(wǎng)絡(luò)游戲中是非常重要的,它保證了每個(gè)玩家在屏幕上看到的東西大體是一樣的。其實(shí)呢,解決同步問(wèn)題的最簡(jiǎn)單的方法就是把每個(gè)玩家的動(dòng)作都向其他玩家廣播一遍,這里其實(shí)就存在兩個(gè)問(wèn)題:1,向哪些玩家廣播,廣播哪些消息。2,如果網(wǎng)絡(luò)延遲怎么辦。事實(shí)上呢,第一個(gè)問(wèn)題是個(gè)非常簡(jiǎn)單的問(wèn)題,不過(guò)之所以我提出這個(gè)問(wèn)題來(lái),是提醒大家在設(shè)計(jì)自己的消息結(jié)構(gòu)的時(shí)候,需要把這個(gè)因素考慮進(jìn)去。而對(duì)于第二個(gè)問(wèn)題,則是一個(gè)挺麻煩的問(wèn)題,大家可以來(lái)看這么個(gè)例子:
比如有一個(gè)玩家A向服務(wù)器發(fā)了條指令,說(shuō)我現(xiàn)在在P1點(diǎn),要去P2點(diǎn)。指令發(fā)出的時(shí)間是T0,服務(wù)器收到指令的時(shí)間是T1,然后向周圍的玩家廣播這條消息,消息的內(nèi)容是“玩家A從P1到P2”有一個(gè)在A附近的玩家B,收到服務(wù)器的這則廣播的消息的時(shí)間是T2,然后開(kāi)始在客戶端上畫(huà)圖,A從P1到P2點(diǎn)。這個(gè)時(shí)候就存在一個(gè)不同步的問(wèn)題,玩家A和玩家B的屏幕上顯示的畫(huà)面相差了T2-T1的時(shí)間。這個(gè)時(shí)候怎么辦呢?
有個(gè)解決方案,我給它取名叫 預(yù)測(cè)拉扯,雖然有些怪異了點(diǎn),不過(guò)基本上大家也能從字面上來(lái)理解它的意思。要解決這個(gè)問(wèn)題,首先要定義一個(gè)值叫:預(yù)測(cè)誤差。然后需要在服務(wù)器端每個(gè)玩家連接的類里面加一項(xiàng)屬性,叫l(wèi)atency,然后在玩家登陸的時(shí)候,對(duì)客戶端的時(shí)間和服務(wù)器的時(shí)間進(jìn)行比較,得出來(lái)的差值保存在latency里面。還是上面的那個(gè)例子,服務(wù)器廣播消息的時(shí)候,就根據(jù)要廣播對(duì)象的latency,計(jì)算出一個(gè)客戶端的CurrentTime,然后在消息頭里面包含這個(gè)CurrentTime,然后再進(jìn)行廣播。并且同時(shí)在玩家A的客戶端本地建立一個(gè)隊(duì)列,保存該條消息,只到獲得服務(wù)器驗(yàn)證就從未被驗(yàn)證的消息隊(duì)列里面將該消息刪除,如果驗(yàn)證失敗,則會(huì)被拉扯回P1點(diǎn)。然后當(dāng)玩家B收到了服務(wù)器發(fā)過(guò)來(lái)的消息“玩家A從P1到P2”這個(gè)時(shí)候就檢查消息里面服務(wù)器發(fā)出的時(shí)間和本地時(shí)間做比較,如果大于定義的預(yù)測(cè)誤差,就算出在T2這個(gè)時(shí)間,玩家A的屏幕上走到的地點(diǎn)P3,然后把玩家B屏幕上的玩家A直接拉扯到P3,再繼續(xù)走下去,這樣就能保證同步。更進(jìn)一步,為了保證客戶端運(yùn)行起來(lái)更加smooth,我并不推薦直接把玩家拉扯過(guò)去,而是算出P3偏后的一點(diǎn)P4,然后用(P4-P1)/T(P4-P3)來(lái)算出一個(gè)很快的速度S,然后讓玩家A用速度S快速移動(dòng)到P4,這樣的處理方法是比較合理的,這種解決方案的原形在國(guó)際上被稱為(Full plesiochronous),當(dāng)然,該原形被我篡改了很多來(lái)適應(yīng)網(wǎng)絡(luò)游戲的同步,所以而變成所謂的:預(yù)測(cè)拉扯。
另外一個(gè)解決方案,我給它取名叫 驗(yàn)證同步,聽(tīng)名字也知道,大體的意思就是每條指令在經(jīng)過(guò)服務(wù)器驗(yàn)證通過(guò)了以后再執(zhí)行動(dòng)作。具體的思路如下:首先也需要在每個(gè)玩家連接類型里面定義一個(gè)latency,然后在客戶端響應(yīng)玩家鼠標(biāo)行走的同時(shí),客戶端并不會(huì)先行走動(dòng),而是發(fā)一條走路的指令給服務(wù)器,然后等待服務(wù)器的驗(yàn)證。服務(wù)器接受到這條消息以后,進(jìn)行邏輯層的驗(yàn)證,然后計(jì)算出需要廣播的范圍,包括玩家A在內(nèi),根據(jù)各個(gè)客戶端不同的latency生成不同的消息頭,開(kāi)始廣播,這個(gè)時(shí)候這個(gè)玩家的走路信息就是完全同步的了。這個(gè)方法的優(yōu)點(diǎn)是能保證各個(gè)客戶端之間絕對(duì)的同步,缺點(diǎn)是當(dāng)網(wǎng)絡(luò)延遲比較大的時(shí)候,玩家的客戶端的行為會(huì)變得比較不流暢,給玩家?guī)?lái)很不爽的感覺(jué)。該種解決方案的原形在國(guó)際上被稱為(Hierarchical master-slave synchronization),80年代以后被廣泛應(yīng)用于網(wǎng)絡(luò)的各個(gè)領(lǐng)域。
最后一種解決方案是一種理想化的解決方案,在國(guó)際上被稱為Mutual synchronization,是一種對(duì)未來(lái)網(wǎng)絡(luò)的前景的良好預(yù)測(cè)出來(lái)的解決方案。這里之所以要提這個(gè)方案,并不是說(shuō)我們已經(jīng)完全的實(shí)現(xiàn)了這種方案,而只是在網(wǎng)絡(luò)游戲領(lǐng)域的某些方面應(yīng)用到這種方案的某些思想。我對(duì)該種方案取名為:半服務(wù)器同步。大體的設(shè)計(jì)思路如下:
首先客戶端需要在登陸世界的時(shí)候建立很多張廣播列表,這些列表在客戶端后臺(tái)和服務(wù)器要進(jìn)行不及時(shí)同步,之所以要建立多張列表,是因?yàn)橐獜V播的類型是不止一種的,比如說(shuō)有l(wèi)ocal message,有remote message,還有g(shù)lobal message 等等,這些列表都需要在客戶端登陸的時(shí)候根據(jù)服務(wù)器發(fā)過(guò)來(lái)的消息建立好。在建立列表的同時(shí),還需要獲得每個(gè)列表中廣播對(duì)象的latency,并且要維護(hù)一張完整的用戶狀態(tài)列表在后臺(tái),也是不及時(shí)的和服務(wù)器進(jìn)行同步,根據(jù)本地的用戶狀態(tài)表,可以做到一部分決策由客戶端自己來(lái)決定,當(dāng)客戶端發(fā)送這部分決策的時(shí)候,則直接將最終決策發(fā)送到各個(gè)廣播列表里面的客戶端,并對(duì)其時(shí)間進(jìn)行校對(duì),保證每個(gè)客戶端在收到的消息的時(shí)間是和根據(jù)本地時(shí)間進(jìn)行校對(duì)過(guò)的。那么再采用預(yù)測(cè)拉扯中提到過(guò)的計(jì)算提前量,提高速度行走過(guò)去的方法,將會(huì)使同步變得非常的smooth。該方案的優(yōu)點(diǎn)是不通過(guò)服務(wù)器,客戶端自己之間進(jìn)行同步,大大的降低了由于網(wǎng)絡(luò)延遲而帶來(lái)的誤差,并且由于大部分決策都可以由客戶端來(lái)做,也大大的降低了服務(wù)器的資源。由此帶來(lái)的弊端就是由于消息和決策權(quán)都放在客戶端本地,所以給外掛提供了很大的可乘之機(jī)。
下面我想來(lái)談?wù)勱P(guān)于服務(wù)器上NPC的設(shè)計(jì)以及NPC智能等一些方面涉及到的問(wèn)題。首先,我們需要知道什么是NPC,NPC需要做什么。NPC的全稱是(Non-Player Character),很顯然,他是一個(gè)character,但不是玩家,那么從這點(diǎn)上可以知道,NPC的某些行為是和玩家類似的,他可以行走,可以戰(zhàn)斗,可以呼吸(這點(diǎn)將在后面的NPC智能里面提到),另外一點(diǎn)和玩家物件不同的是,NPC可以復(fù)生(即NPC被打死以后在一定時(shí)間內(nèi)可以重新出來(lái))。其實(shí)還有最重要的一點(diǎn),就是玩家物件的所有決策都是玩家做出來(lái)的,而NPC的決策則是由計(jì)算機(jī)做出來(lái)的,所以在對(duì)NPC做何種決策的時(shí)候,需要所謂的NPC智能來(lái)進(jìn)行決策。
下面我將分兩個(gè)部分來(lái)談?wù)凬PC,首先是NPC智能,其次是服務(wù)器如何對(duì)NPC進(jìn)行組織。之所以要先談NPC智能是因?yàn)橹挥挟?dāng)我們了解清楚我們需要NPC做什么之后,才好開(kāi)始設(shè)計(jì)服務(wù)器來(lái)對(duì)NPC進(jìn)行組織。
NPC智能
NPC智能分為兩種,一種是被動(dòng)觸發(fā)的事件,一種是主動(dòng)觸發(fā)的事件。對(duì)于被動(dòng)觸發(fā)的事件,處理起來(lái)相對(duì)來(lái)說(shuō)簡(jiǎn)單一些,可以由事件本身來(lái)呼叫NPC身上的函數(shù),比如說(shuō)NPC的死亡,實(shí)際上是在NPC的HP小于一定值的時(shí)候,來(lái)主動(dòng)呼叫NPC身上的OnDie() 函數(shù),這種由事件來(lái)觸發(fā)NPC行為的NPC智能,我稱為被動(dòng)觸發(fā)。這種類型的觸發(fā)往往分為兩種:
一種是由別的物件導(dǎo)致的NPC的屬性變化,然后屬性變化的同時(shí)會(huì)導(dǎo)致NPC產(chǎn)生一些行為。由此一來(lái),NPC物件里面至少包含以下幾種函數(shù):
class NPC {
public:
// 是誰(shuí)在什么地方導(dǎo)致了我哪項(xiàng)屬性改變了多少。
OnChangeAttribute(object_t *who, int which, int how, int where);
Private:
OnDie();
OnEscape();
OnFollow();
OnSleep();
// 一系列的事件。
}
這是一個(gè)基本的NPC的結(jié)構(gòu),這種被動(dòng)的觸發(fā)NPC的事件,我稱它為NPC的反射。但是,這樣的結(jié)構(gòu)只能讓NPC被動(dòng)的接收一些信息來(lái)做出決策,這樣的NPC是愚蠢的。那么,怎么樣讓一個(gè)NPC能夠主動(dòng)的做出一些決策呢?這里有一種方法:呼吸。那么怎么樣讓NPC有呼吸呢?
一種很簡(jiǎn)單的方法,用一個(gè)計(jì)時(shí)器,定時(shí)的觸發(fā)所有NPC的呼吸,這樣就可以讓一個(gè)NPC有呼吸起來(lái)。這樣的話會(huì)有一個(gè)問(wèn)題,當(dāng)NPC太多的時(shí)候,上一次NPC的呼吸還沒(méi)有呼吸完,下一次呼吸又來(lái)了,那么怎么解決這個(gè)問(wèn)題呢。這里有一種方法,讓NPC異步的進(jìn)行呼吸,即每個(gè)NPC的呼吸周期是根據(jù)NPC出生的時(shí)間來(lái)定的,這個(gè)時(shí)候計(jì)時(shí)器需要做的就是隔一段時(shí)間檢查一下,哪些NPC到時(shí)間該呼吸了,就來(lái)觸發(fā)這些NPC的呼吸。
上面提到的是系統(tǒng)如何來(lái)觸發(fā)NPC的呼吸,那么NPC本身的呼吸頻率該如何設(shè)定呢?這個(gè)就好象現(xiàn)實(shí)中的人一樣,睡覺(jué)的時(shí)候和進(jìn)行激烈運(yùn)動(dòng)的時(shí)候,呼吸頻率是不一樣的。同樣,NPC在戰(zhàn)斗的時(shí)候,和平常的時(shí)候,呼吸頻率也不一樣。那么就需要一個(gè)Breath_Ticker來(lái)設(shè)置NPC當(dāng)前的呼吸頻率。
那么在NPC的呼吸事件里面,我們?cè)趺礃觼?lái)設(shè)置NPC的智能呢?大體可以概括為檢查環(huán)境和做出決策兩個(gè)部分。首先,需要對(duì)當(dāng)前環(huán)境進(jìn)行數(shù)字上的統(tǒng)計(jì),比如說(shuō)是否在戰(zhàn)斗中,戰(zhàn)斗有幾個(gè)敵人,自己的HP還剩多少,以及附近有沒(méi)有敵人等等之類的統(tǒng)計(jì)。統(tǒng)計(jì)出來(lái)的數(shù)據(jù)傳入本身的決策模塊,決策模塊則根據(jù)NPC自身的性格取向來(lái)做出一些決策,比如說(shuō)野蠻型的NPC會(huì)在HP比較少的時(shí)候仍然猛撲猛打,又比如說(shuō)智慧型的NPC則會(huì)在HP比較少的時(shí)候選擇逃跑。等等之類的。
至此,一個(gè)可以呼吸,反射的NPC的結(jié)構(gòu)已經(jīng)基本構(gòu)成了,那么接下來(lái)我們就來(lái)談?wù)勏到y(tǒng)如何組織讓一個(gè)NPC出現(xiàn)在世界里面。
NPC的組織
這里有兩種方案可供選擇,其一:NPC的位置信息保存在場(chǎng)景里面,載入場(chǎng)景的時(shí)候載入NPC。其二,NPC的位置信息保存在NPC身上,有專門(mén)的事件讓所有的NPC登陸場(chǎng)景。這兩種方法有什么區(qū)別呢?又各有什么好壞呢?
前一種方法好處在于場(chǎng)景載入的時(shí)候同時(shí)載入了NPC,場(chǎng)景就可以對(duì)NPC進(jìn)行管理,不需要多余的處理,而弊端則在于在刷新的時(shí)候是同步刷新的,也就是說(shuō)一個(gè)場(chǎng)景里面的NPC可能會(huì)在同一時(shí)間內(nèi)長(zhǎng)出來(lái)。而對(duì)于第二種方法呢,設(shè)計(jì)起來(lái)會(huì)稍微麻煩一些,需要一個(gè)統(tǒng)一的機(jī)制讓NPC登陸到場(chǎng)景,還需要一些比較麻煩的設(shè)計(jì),但是這種方案可以實(shí)現(xiàn)NPC異步的刷新,是目前網(wǎng)絡(luò)游戲普遍采用的方法,下面我們就來(lái)著重談?wù)勥@種方法的實(shí)現(xiàn):
首先我們要引入一個(gè)“靈魂”的概念,即一個(gè)NPC在死后,消失的只是他的肉體,他的靈魂仍然在世界中存在著,沒(méi)有呼吸,在死亡的附近漂浮,等著到時(shí)間投胎,投胎的時(shí)候把之前的所有屬性清零,重新在場(chǎng)景上構(gòu)建其肉體。那么,我們?cè)趺磥?lái)設(shè)計(jì)這樣一個(gè)結(jié)構(gòu)呢?首先把一個(gè)場(chǎng)景里面要出現(xiàn)的NPC制作成圖量表,給每個(gè)NPC一個(gè)獨(dú)一無(wú)二的標(biāo)識(shí)符,在載入場(chǎng)景之后,根據(jù)圖量表來(lái)載入屬于該場(chǎng)景的NPC。在NPC的OnDie() 事件里面不直接把該物件destroy 掉,而是關(guān)閉NPC的呼吸,然后打開(kāi)一個(gè)重生的計(jì)時(shí)器,最后把該物件設(shè)置為invisable。這樣的設(shè)計(jì),可以實(shí)現(xiàn)NPC的異步刷新,在節(jié)省服務(wù)器資源的同時(shí)也讓玩家覺(jué)得更加的真實(shí)。
(這一章節(jié)已經(jīng)牽扯到一些服務(wù)器腳本相關(guān)的東西,所以下一章節(jié)將談?wù)劮?wù)器腳本相關(guān)的一些設(shè)計(jì))
補(bǔ)充的談?wù)剢l(fā)式搜索(heuristic searching)在NPC智能中的應(yīng)用。
其主要思路是在廣度優(yōu)先搜索的同時(shí),將下一層的所有節(jié)點(diǎn)經(jīng)過(guò)一個(gè)啟發(fā)函數(shù)進(jìn)行過(guò)濾,一定范圍內(nèi)縮小搜索范圍。眾所周知的尋路A*算法就是典型的啟發(fā)式搜索的應(yīng)用,其原理是一開(kāi)始設(shè)計(jì)一個(gè)Judge(point_t* point)函數(shù),來(lái)獲得point這個(gè)一點(diǎn)的代價(jià),然后每次搜索的時(shí)候把下一步可能到達(dá)的所有點(diǎn)都經(jīng)過(guò)Judge()函數(shù)評(píng)價(jià)一下,獲取兩到三個(gè)代價(jià)比較小的點(diǎn),繼續(xù)搜索,那些沒(méi)被選上的點(diǎn)就不會(huì)在繼續(xù)搜索下去了,這樣帶來(lái)的后果的是可能求出來(lái)的不是最優(yōu)路徑,這也是為什么A*算法在尋路的時(shí)候會(huì)走到障礙物前面再繞過(guò)去,而不是預(yù)先就走斜線來(lái)繞過(guò)該障礙物。如果要尋出最優(yōu)化的路徑的話,是不能用A*算法的,而是要用動(dòng)態(tài)規(guī)劃的方法,其消耗是遠(yuǎn)大于A*的。
那么,除了在尋路之外,還有哪些地方可以應(yīng)用到啟發(fā)式搜索呢?其實(shí)說(shuō)得大一點(diǎn),NPC的任何決策都可以用啟發(fā)式搜索來(lái)做,比如說(shuō)逃跑吧,如果是一個(gè)2D的網(wǎng)絡(luò)游戲,有八個(gè)方向,NPC選擇哪個(gè)方向逃跑呢?就可以設(shè)置一個(gè)Judge(int direction)來(lái)給定每個(gè)點(diǎn)的代價(jià),在Judge里面算上該點(diǎn)的敵人的強(qiáng)弱,或者該敵人的敏捷如何等等,最后選擇代價(jià)最小的地方逃跑。下面,我們就來(lái)談?wù)剬?duì)于幾種NPC常見(jiàn)的智能的啟發(fā)式搜索法的設(shè)計(jì):
Target select (選擇目標(biāo)):
首先獲得地圖上離該NPC附近的敵人列表。設(shè)計(jì)Judge() 函數(shù),根據(jù)敵人的強(qiáng)弱,敵人的遠(yuǎn)近,算出代價(jià)。然后選擇代價(jià)最小的敵人進(jìn)行主動(dòng)攻擊。
Escape(逃跑):
在呼吸事件里面檢查自己的HP,如果HP低于某個(gè)值的時(shí)候,或者如果你是遠(yuǎn)程兵種,而敵人近身的話,則觸發(fā)逃跑函數(shù),在逃跑函數(shù)里面也是對(duì)周圍的所有的敵人組織成列表,然后設(shè)計(jì)Judge() 函數(shù),先選擇出對(duì)你構(gòu)成威脅最大的敵人,該Judge() 函數(shù)需要判斷敵人的速度,戰(zhàn)斗力強(qiáng)弱,最后得出一個(gè)主要敵人,然后針對(duì)該主要敵人進(jìn)行路徑的Judge() 的函數(shù)的設(shè)計(jì),搜索的范圍只可能是和主要敵人相反的方向,然后再根據(jù)該幾個(gè)方向的敵人的強(qiáng)弱來(lái)計(jì)算代價(jià),做出最后的選擇。
Random walk(隨機(jī)走路):
這個(gè)我并不推薦用A*算法,因?yàn)镹PC一旦多起來(lái),那么這個(gè)對(duì)CPU的消耗是很恐怖的,而且NPC大多不需要長(zhǎng)距離的尋路,只需要在附近走走即可,那么,就在附近隨機(jī)的給幾個(gè)點(diǎn),然后讓NPC走過(guò)去,如果碰到障礙物就停下來(lái),這樣幾乎無(wú)任何負(fù)擔(dān)。
Follow Target(追隨目標(biāo)):
這里有兩種方法,一種方法NPC看上去比較愚蠢,一種方法看上去NPC比較聰明,第一種方法就是讓NPC跟著目標(biāo)的路點(diǎn)走即可,幾乎沒(méi)有資源消耗。而后一種則是讓NPC在跟隨的時(shí)候,在呼吸事件里面判斷對(duì)方的當(dāng)前位置,然后走直線,碰上障礙物了用A*繞過(guò)去,該種設(shè)計(jì)會(huì)消耗一定量的系統(tǒng)資源,所以不推薦NPC大量的追隨目標(biāo),如果需要大量的NPC追隨目標(biāo)的話,還有一個(gè)比較簡(jiǎn)單的方法:讓NPC和目標(biāo)同步移動(dòng),即讓他們的速度統(tǒng)一,移動(dòng)的時(shí)候走同樣的路點(diǎn),當(dāng)然,這種設(shè)計(jì)只適合NPC所跟隨的目標(biāo)不是追殺的關(guān)系,只是跟隨著玩家走而已了。
在這一章節(jié),我想談?wù)勱P(guān)于服務(wù)器端的腳本的相關(guān)設(shè)計(jì)。因?yàn)樵谏弦徽鹿?jié)里面,談NPC智能相關(guān)的時(shí)候已經(jīng)接觸到一些腳本相關(guān)的東東了。還是先來(lái)談?wù)勀_本的作用吧。
在基于編譯的服務(wù)器端程序中,是無(wú)法在程序的運(yùn)行過(guò)程中構(gòu)建一些東西的,那么這個(gè)時(shí)候就需要腳本語(yǔ)言的支持了,由于腳本語(yǔ)言涉及到邏輯判斷,所以光提供一些函數(shù)接口是沒(méi)用的,還需要提供一些簡(jiǎn)單的語(yǔ)法和文法解析的功能。其實(shí)說(shuō)到底,任何的事件都可以看成兩個(gè)部分:第一是對(duì)自身,或者別的物件的數(shù)值的改變,另外一個(gè)就是將該事件以文字或者圖形的方式廣播出去。那么,這里牽扯到一個(gè)很重要的話題,就是對(duì)某一物件進(jìn)行尋址。恩,談到這,我想將本章節(jié)分為三個(gè)部分來(lái)談,首先是服務(wù)器如何來(lái)管理動(dòng)態(tài)創(chuàng)建出來(lái)的物件(服務(wù)器內(nèi)存管理),第二是如何對(duì)某一物件進(jìn)行尋址,第三則是腳本語(yǔ)言的組織和解釋。其實(shí)之所以到第四章再來(lái)談服務(wù)器的內(nèi)存管理是因?yàn)樵谇皫渍抡勥@個(gè)的話,大家對(duì)其沒(méi)有一個(gè)感性的認(rèn)識(shí),可能不知道服務(wù)器的內(nèi)存管理究竟有什么用。
4.1、服務(wù)器內(nèi)存管理
對(duì)于服務(wù)器內(nèi)存管理我們將采用內(nèi)存池的方法,也稱為靜態(tài)內(nèi)存管理。其概念為在服務(wù)器初始化的時(shí)候,申請(qǐng)一塊非常大的內(nèi)存,稱為內(nèi)存池(Memory pool),同時(shí)也申請(qǐng)一小塊內(nèi)存空間,稱為垃圾回收站(Garbage recollecting station)。其大體思路如下:當(dāng)程序需要申請(qǐng)內(nèi)存的時(shí)候,首先檢查垃圾回收站是否為空,如果不為空的話,則從垃圾回收站中找一塊可用的內(nèi)存地址,在內(nèi)存池中根據(jù)地址找到相應(yīng)的空間,分配給程序用,如果垃圾回收站是空的話,則直接從內(nèi)存池的當(dāng)前指針位置申請(qǐng)一塊內(nèi)存;當(dāng)程序釋放空間的時(shí)候,給那塊內(nèi)存打上已經(jīng)釋放掉的標(biāo)記,然后把那塊內(nèi)存的地址放入垃圾回收站。
下面具體談?wù)勗摲椒ǖ脑敿?xì)設(shè)計(jì),首先,我們將采用類似于操作系統(tǒng)的段頁(yè)式系統(tǒng)來(lái)管理內(nèi)存,這樣的好處是可以充分的利用內(nèi)存池,其缺點(diǎn)是管理起來(lái)比較麻煩。嗯,下面來(lái)具體看看我們?cè)趺礃觼?lái)定義頁(yè)和段的結(jié)構(gòu):
typedef struct m_segment_s
{
struct m_segment_s *next; /* 雙線鏈表 + 靜態(tài)內(nèi)存可以達(dá)到隨機(jī)訪問(wèn)和順序訪問(wèn)的目的,
真正的想怎么訪問(wèn),就怎么訪問(wèn)。 */
struct m_segment_s *pre; int flags; // 該段的一些標(biāo)記。
int start; // 相對(duì)于該頁(yè)的首地址。
int size; // 長(zhǎng)度。
struct m_page_s *my_owner; // 我是屬于哪一頁(yè)的。
char *data; // 內(nèi)容指針。
}m_segment_t;
typedef struct m_page_s
{
unsigned int flags; /* 使用標(biāo)記,是否完全使用,是否還有空余 */
int size; /* 該頁(yè)的大小,一般都是統(tǒng)一的,最后一頁(yè)除外 */
int end; /* 使用到什么地方了 */
int my_index; /* 提供隨機(jī)訪問(wèn)的索引 */
m_segment_t *segments; // 頁(yè)內(nèi)段的頭指針。
}m_page_t;
那么內(nèi)存池和垃圾回收站怎么構(gòu)建呢?下面也給出一些構(gòu)建相關(guān)的偽代碼:
static m_page_t *all_pages;
// total_size是總共要申請(qǐng)的內(nèi)存數(shù),num_pages是總共打算創(chuàng)建多少個(gè)頁(yè)面。
void initialize_memory_pool( int total_size, int num_pages )
{
int i, page_size, last_size; // 算出每個(gè)頁(yè)面的大小。
page_size = total_size / num_pages; // 分配足夠的頁(yè)面。
all_pages = (m_page_t*) calloc( num_pages, sizeof(m_page_t*) );
for ( i = 0; i < num_pages; i ++ )
{
// 初始化每個(gè)頁(yè)面的段指針。
all_pages[i].m_segment_t = (m_segment_t*) malloc( page_size );
// 初始化該頁(yè)面的標(biāo)記。
all_pages[i].flags |= NEVER_USED;
// 除了最后一個(gè)頁(yè)面,其他的大小都是page_size 大小。
all_pages[i].size = page_size;
// 初始化隨機(jī)訪問(wèn)的索引。
all_pages[i].my_index = i;
// 由于沒(méi)有用過(guò),所以大小都是0
all_pages[i].end = 0;
}
// 設(shè)置最后一個(gè)頁(yè)面的大小。
if ( (last_size = total_size % num_pages) != 0 )
all_pages[i].size = last_size;
}
下面看看垃圾回收站怎么設(shè)計(jì):
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位來(lái)儲(chǔ)存首相對(duì)地址,低8位來(lái)儲(chǔ)存長(zhǎng)度。
garbage_station[i] = (int*) calloc( page_size, sizeof( unsigned short ));
memset( garbage_station[i], 0, sizeof( garbage_station[i] ));
}
}
也許這樣的貼代碼會(huì)讓大家覺(jué)得很不明白,嗯,我的代碼水平確實(shí)不怎么樣,那么下面我來(lái)用文字方式來(lái)敘說(shuō)一下大體的概念吧。對(duì)于段頁(yè)式內(nèi)存管理,首先分成N個(gè)頁(yè)面,這個(gè)是固定的,而對(duì)于每個(gè)頁(yè)面內(nèi)的段則是動(dòng)態(tài)的,段的大小事先是不知道的,那么我們需要回收的不僅僅是頁(yè)面的內(nèi)存,還包括段的內(nèi)存,那么我們就需要一個(gè)二維數(shù)組來(lái)保存是哪個(gè)頁(yè)面的那塊段的地址被釋放了。然后對(duì)于申請(qǐng)內(nèi)存的時(shí)候,則首先檢查需要申請(qǐng)內(nèi)存的大小,如果不夠一個(gè)頁(yè)面大小的話,則在垃圾回收站里面尋找可用的段空間分配,如果找不到,則申請(qǐng)一個(gè)新的頁(yè)面空間。
這樣用內(nèi)存池的方法來(lái)管理整個(gè)游戲世界的內(nèi)存可以有效的減少內(nèi)存碎片,一定程度的提高游戲運(yùn)行的穩(wěn)定性和效率。
4.2、游戲中物件的尋址
第一個(gè)問(wèn)題,我們?yōu)槭裁匆獙ぶ??加入了腳本語(yǔ)言的概念之后,游戲中的一些邏輯物件,比如說(shuō)NPC,某個(gè)ITEM之類的都是由腳本語(yǔ)言在游戲運(yùn)行的過(guò)程中動(dòng)態(tài)生成的,那么我們通過(guò)什么樣的方法來(lái)對(duì)這些物件進(jìn)行索引呢?說(shuō)得簡(jiǎn)單一點(diǎn),就是如何找到他們呢?有個(gè)很簡(jiǎn)單的方法,全部遍歷一次。當(dāng)然,這是個(gè)簡(jiǎn)單而有效的方法,但是效率上的消耗是任何一臺(tái)服務(wù)器都吃不消的,特別是在游戲的規(guī)模比較大之后。
那么,我們?cè)趺磥?lái)在游戲世界中很快的尋找這些物件呢?我想在談這個(gè)之前,說(shuō)一下Hash Table這個(gè)數(shù)據(jù)結(jié)構(gòu),它叫哈希表,也有人叫它散列表,其工作原理是不是順序訪問(wèn),也不是隨機(jī)訪問(wèn),而是通過(guò)一個(gè)散列函數(shù)對(duì)其key進(jìn)行計(jì)算,算出在內(nèi)存中這個(gè)key對(duì)應(yīng)的value的地址,而對(duì)其進(jìn)行訪問(wèn)。好處是不管面對(duì)多大的數(shù)據(jù),只需要一次計(jì)算就能找到其地址,非常的快捷,那么弊端是什么呢?當(dāng)兩個(gè)key通過(guò)散列函數(shù)計(jì)算出來(lái)的地址是同一個(gè)地址的時(shí)候,麻煩就來(lái)了,會(huì)產(chǎn)生碰撞,其的解決方法非常的麻煩,這里就不詳細(xì)談其解決方法了,否則估計(jì)再寫(xiě)個(gè)四,五章也未必談得清楚,不過(guò)如果大家對(duì)其感興趣的話,歡迎討論。
嗯,我們將用散列表來(lái)對(duì)游戲中的物件進(jìn)行索引,具體怎么做呢?首先,在內(nèi)存池中申請(qǐng)一塊兩倍大于游戲中物件總數(shù)的內(nèi)存,為什么是兩倍大呢?防止散列表碰撞。然后我們選用物件的名稱作為散列表的索引key,然后就可以開(kāi)始設(shè)計(jì)散列函數(shù)了。下面來(lái)看個(gè)例子:
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是需要進(jìn)行索引的字符串指針,maxn是字符串可能的最大長(zhǎng)度,返回值是相對(duì)地址。
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;
}
具體的算法就不說(shuō)了,上面的那一大段東西不要問(wèn)我為什么,這個(gè)算法的出處是CACM 33-6中的一個(gè)叫Peter K.Pearson的鬼子寫(xiě)的論文中介紹的算法,據(jù)說(shuō)速度非常的快。有了這個(gè)散列函數(shù),我們就可以通過(guò)它來(lái)對(duì)世界里面的任意物件進(jìn)行非??斓膶ぶ妨?。
4.3、腳本語(yǔ)言解釋
在設(shè)計(jì)腳本語(yǔ)言之前,我們首先需要明白,我們的腳本語(yǔ)言要實(shí)現(xiàn)什么樣的功能?否則隨心所欲的做下去寫(xiě)出個(gè)C的解釋器之類的也說(shuō)不定。我們要實(shí)現(xiàn)的功能只是簡(jiǎn)單的邏輯判斷和循環(huán),其他所有的功能都可以由事先提供好的函數(shù)來(lái)完成。嗯,這樣我們就可以列出一張工作量的表單:設(shè)計(jì)物件在底層的保存結(jié)構(gòu),提供腳本和底層間的訪問(wèn)接口,設(shè)計(jì)支持邏輯判斷和循環(huán)的解釋器。
下面先來(lái)談?wù)勎锛诘讓拥谋4娼Y(jié)構(gòu)。具體到每種不同屬性的物件,需要采用不同的結(jié)構(gòu),當(dāng)然,如果你愿意的話,你可以所有的物件都采同同樣的結(jié)構(gòu),然后在結(jié)構(gòu)里面設(shè)計(jì)一個(gè)散列表來(lái)保存各種不同的屬性。但這并不是一個(gè)好方法,過(guò)分的依賴散列表會(huì)讓你的游戲的邏輯變得繁雜不清。所以,盡量的區(qū)分每種不同的物件采用不同的結(jié)構(gòu)來(lái)設(shè)計(jì)。但是有一點(diǎn)值得注意的是,不管是什么結(jié)構(gòu),有一些東西是統(tǒng)一的,就是我們所說(shuō)的物件頭,那么我們?cè)趺磥?lái)設(shè)計(jì)這樣一個(gè)物件頭呢?
typedef struct object_head_s
{
char* name;
char* prog;
}object_head_t;
其中name是在散列表中這個(gè)物件的索引號(hào),prog則是腳本解釋器需要解釋的程序內(nèi)容。下面我們就以NPC為例來(lái)設(shè)計(jì)一個(gè)結(jié)構(gòu):
typedef struct npc_s
{
object_head_t header; // 物件頭
int hp; // NPC的hp值。
int level; // NPC的等級(jí)。
struct position_s position; // 當(dāng)前的位置信息。
unsigned int personality; // NPC的個(gè)性,一個(gè)unsigned int可以保存24種個(gè)性。
}npc_t;
OK,結(jié)構(gòu)設(shè)計(jì)完成,那么我們?cè)趺磥?lái)設(shè)計(jì)腳本解釋器呢?這里有兩種法,一種是用虛擬機(jī)的模式來(lái)解析腳本語(yǔ)言,另外一中則是用類似匯編語(yǔ)言的那種結(jié)構(gòu)來(lái)設(shè)計(jì),設(shè)置一些條件跳轉(zhuǎn)和循環(huán)就可以實(shí)現(xiàn)邏輯判斷和循環(huán)了,比如:
set name, "路人甲";
CHOOSE: random_choose_personality; // 隨機(jī)選擇NPC的個(gè)性
compare hp, 100; // 比較氣血,比較出的值可以放在一個(gè)固定的變量里面
ifless LESS; // hp < 100的話,則返回。
jump CHOOSE; // 否則繼續(xù)選擇,只到選到一個(gè)hp < 100的。
LESS: return success;
這種腳本結(jié)構(gòu)就類似CPU的指令的結(jié)構(gòu),一條一條指令按照順序執(zhí)行,對(duì)于腳本程序員(Script Programmer)也可以培養(yǎng)他們匯編能力的說(shuō)。
那么怎么來(lái)模仿這種結(jié)構(gòu)呢?我們拿CPU的指令做參照,首先得設(shè)置一些寄存器,CPU的寄存器的大小和數(shù)量是受硬件影響的,但我們是用內(nèi)存來(lái)模擬寄存器,所以想要多大,就可以有多大。然后提供一些指令,包括四則運(yùn)算,尋址,判斷,循環(huán)等等。接下來(lái)針對(duì)不同的腳本用不同的解析方法,比如說(shuō)對(duì)NPC就用NPC固定的腳本,對(duì)ITEM就用ITEM固定的腳本,解析完以后就把結(jié)果生成底層該物件的結(jié)構(gòu)用于使用。
而如果要用虛擬機(jī)來(lái)實(shí)現(xiàn)腳本語(yǔ)言的話呢,則會(huì)將工程變得無(wú)比之巨大,強(qiáng)烈不推薦使用,不過(guò)如果你想做一個(gè)通用的網(wǎng)絡(luò)游戲底層的話,則可以考慮設(shè)計(jì)一個(gè)虛擬機(jī)。虛擬機(jī)大體的解釋過(guò)程就是進(jìn)行兩次編譯,第一次對(duì)關(guān)鍵字進(jìn)行編譯,第二次生成匯編語(yǔ)言,然后虛擬機(jī)在根據(jù)編譯生成的匯編語(yǔ)言進(jìn)行逐行解釋,如果大家對(duì)這個(gè)感興趣的話,可以去www.mudos.org上下載一份MudOS的原碼來(lái)研究研究。 大體的思路講到這里已經(jīng)差不多了,下面將用unreal(虛幻)為實(shí)例,談一談網(wǎng)絡(luò)游戲服務(wù)器的設(shè)計(jì)。
同步在網(wǎng)絡(luò)游戲中是非常重要的,它保證了每個(gè)玩家在屏幕上看到的東西大體是一樣的。其實(shí)呢,解決同步問(wèn)題的最簡(jiǎn)單的方法就是把每個(gè)玩家的動(dòng)作都向其他玩家廣播一遍,這里其實(shí)就存在兩個(gè)問(wèn)題:1,向哪些玩家廣播,廣播哪些消息。2,如果網(wǎng)絡(luò)延遲怎么辦。事實(shí)上呢,第一個(gè)問(wèn)題是個(gè)非常簡(jiǎn)單的問(wèn)題,不過(guò)之所以我提出這個(gè)問(wèn)題來(lái),是提醒大家在設(shè)計(jì)自己的消息結(jié)構(gòu)的時(shí)候,需要把這個(gè)因素考慮進(jìn)去。而對(duì)于第二個(gè)問(wèn)題,則是一個(gè)挺麻煩的問(wèn)題,大家可以來(lái)看這么個(gè)例子:
比如有一個(gè)玩家A向服務(wù)器發(fā)了條指令,說(shuō)我現(xiàn)在在P1點(diǎn),要去P2點(diǎn)。指令發(fā)出的時(shí)間是T0,服務(wù)器收到指令的時(shí)間是T1,然后向周圍的玩家廣播這條消息,消息的內(nèi)容是“玩家A從P1到P2”有一個(gè)在A附近的玩家B,收到服務(wù)器的這則廣播的消息的時(shí)間是T2,然后開(kāi)始在客戶端上畫(huà)圖,A從P1到P2點(diǎn)。這個(gè)時(shí)候就存在一個(gè)不同步的問(wèn)題,玩家A和玩家B的屏幕上顯示的畫(huà)面相差了T2-T1的時(shí)間。這個(gè)時(shí)候怎么辦呢?
有個(gè)解決方案,我給它取名叫 預(yù)測(cè)拉扯,雖然有些怪異了點(diǎn),不過(guò)基本上大家也能從字面上來(lái)理解它的意思。要解決這個(gè)問(wèn)題,首先要定義一個(gè)值叫:預(yù)測(cè)誤差。然后需要在服務(wù)器端每個(gè)玩家連接的類里面加一項(xiàng)屬性,叫TimeModified,然后在玩家登陸的時(shí)候,對(duì)客戶端的時(shí)間和服務(wù)器的時(shí)間進(jìn)行比較,得出來(lái)的差值保存在TimeModified里面。還是上面的那個(gè)例子,服務(wù)器廣播消息的時(shí)候,就根據(jù)要廣播對(duì)象的TimeModified,計(jì)算出一個(gè)客戶端的CurrentTime,然后在消息頭里面包含這個(gè)CurrentTime,然后再進(jìn)行廣播。并且同時(shí)在玩家A的客戶端本地建立一個(gè)隊(duì)列,保存該條消息,只到獲得服務(wù)器驗(yàn)證就從未被驗(yàn)證的消息隊(duì)列里面將該消息刪除,如果驗(yàn)證失敗,則會(huì)被拉扯回P1點(diǎn)。然后當(dāng)玩家B收到了服務(wù)器發(fā)過(guò)來(lái)的消息“玩家A從P1到P2”這個(gè)時(shí)候就檢查消息里面服務(wù)器發(fā)出的時(shí)間和本地時(shí)間做比較,如果大于定義的預(yù)測(cè)誤差,就算出在T2這個(gè)時(shí)間,玩家A的屏幕上走到的地點(diǎn)P3,然后把玩家B屏幕上的玩家A直接拉扯到P3,再繼續(xù)走下去,這樣就能保證同步。更進(jìn)一步,為了保證客戶端運(yùn)行起來(lái)更加smooth,我并不推薦直接把玩家拉扯過(guò)去,而是算出P3偏后的一點(diǎn)P4,然后用(P4-P1)/T(P4-P3)來(lái)算出一個(gè)很快的速度S,然后讓玩家A用速度S快速移動(dòng)到P4,這樣的處理方法是比較合理的,這種解決方案的原形在國(guó)際上被稱為(Full plesiochronous),當(dāng)然,該原形被我篡改了很多來(lái)適應(yīng)網(wǎng)絡(luò)游戲的同步,所以而變成所謂的:預(yù)測(cè)拉扯。
另外一個(gè)解決方案,我給它取名叫 驗(yàn)證同步,聽(tīng)名字也知道,大體的意思就是每條指令在經(jīng)過(guò)服務(wù)器驗(yàn)證通過(guò)了以后再執(zhí)行動(dòng)作。具體的思路如下:首先也需要在每個(gè)玩家連接類型里面定義一個(gè)TimeModified,然后在客戶端響應(yīng)玩家鼠標(biāo)行走的同時(shí),客戶端并不會(huì)先行走動(dòng),而是發(fā)一條走路的指令給服務(wù)器,然后等待服務(wù)器的驗(yàn)證。服務(wù)器接受到這條消息以后,進(jìn)行邏輯層的驗(yàn)證,然后計(jì)算出需要廣播的范圍,包括玩家A在內(nèi),根據(jù)各個(gè)客戶端不同的TimeModified生成不同的消息頭,開(kāi)始廣播,這個(gè)時(shí)候這個(gè)玩家的走路信息就是完全同步的了。這個(gè)方法的優(yōu)點(diǎn)是能保證各個(gè)客戶端之間絕對(duì)的同步,缺點(diǎn)是當(dāng)網(wǎng)絡(luò)延遲比較大的時(shí)候,玩家的客戶端的行為會(huì)變得比較不流暢,給玩家?guī)?lái)很不爽的感覺(jué)。該種解決方案的原形在國(guó)際上被稱為(Hierarchical master-slave synchronization),80年代以后被廣泛應(yīng)用于網(wǎng)絡(luò)的各個(gè)領(lǐng)域。
最后一種解決方案是一種理想化的解決方案,在國(guó)際上被稱為Mutual synchronization,是一種對(duì)未來(lái)網(wǎng)絡(luò)的前景的良好預(yù)測(cè)出來(lái)的解決方案。這里之所以要提這個(gè)方案,并不是說(shuō)我們已經(jīng)完全的實(shí)現(xiàn)了這種方案,而只是在網(wǎng)絡(luò)游戲領(lǐng)域的某些方面應(yīng)用到這種方案的某些思想。我對(duì)該種方案取名為:半服務(wù)器同步。大體的設(shè)計(jì)思路如下:
首先客戶端需要在登陸世界的時(shí)候建立很多張廣播列表,這些列表在客戶端后臺(tái)和服務(wù)器要進(jìn)行不及時(shí)同步,之所以要建立多張列表,是因?yàn)橐獜V播的類型是不止一種的,比如說(shuō)有l(wèi)ocal message,有remote message,還有g(shù)lobal message 等等,這些列表都需要在客戶端登陸的時(shí)候根據(jù)服務(wù)器發(fā)過(guò)來(lái)的消息建立好。在建立列表的同時(shí),還需要獲得每個(gè)列表中廣播對(duì)象的TimeModified,并且要維護(hù)一張完整的用戶狀態(tài)列表在后臺(tái),也是不及時(shí)的和服務(wù)器進(jìn)行同步,根據(jù)本地的用戶狀態(tài)表,可以做到一部分決策由客戶端自己來(lái)決定,當(dāng)客戶端發(fā)送這部分決策的時(shí)候,則直接將最終決策發(fā)送到各個(gè)廣播列表里面的客戶端,并對(duì)其時(shí)間進(jìn)行校對(duì),保證每個(gè)客戶端在收到的消息的時(shí)間是和根據(jù)本地時(shí)間進(jìn)行校對(duì)過(guò)的。那么再采用預(yù)測(cè)拉扯中提到過(guò)的計(jì)算提前量,提高速度行走過(guò)去的方法,將會(huì)使同步變得非常的smooth。該方案的優(yōu)點(diǎn)是不通過(guò)服務(wù)器,客戶端自己之間進(jìn)行同步,大大的降低了由于網(wǎng)絡(luò)延遲而帶來(lái)的誤差,并且由于大部分決策都可以由客戶端來(lái)做,也大大的降低了服務(wù)器的資源。由此帶來(lái)的弊端就是由于消息和決策權(quán)都放在客戶端本地,所以給外掛提供了很大的可乘之機(jī)。
綜合以上三種關(guān)于網(wǎng)絡(luò)同步派系的優(yōu)缺點(diǎn),綜合出一套關(guān)于網(wǎng)絡(luò)游戲傳輸同步的較完整的解決方案,我稱它為綜合同步法(colligate synchronization)。大體設(shè)計(jì)思路如下:
首先將服務(wù)器需要同步的所有消息從劃分一個(gè)優(yōu)先等級(jí),然后按照3/4的比例劃分出重要消息和非重要消息,對(duì)于非重要消息,把決策權(quán)放在客戶端,在客戶端邏輯上建立相關(guān)的決策機(jī)構(gòu)和各種消息緩存區(qū),以及相關(guān)的消息緩存區(qū)管理機(jī)構(gòu),如下圖所示:

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

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