教父的告白
一切都是纸老虎
posts - 82,  comments - 7,  trackbacks - 0
什么是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 暗夜教父 阅读(260) | 评论 (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.cn
http://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 暗夜教父 阅读(667) | 评论 (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 暗夜教父 阅读(497) | 评论 (0)编辑 收藏
     摘要: 简介:本教程基于Ogre Wiki上的Basic Tutorial系列,并依据笔者使用的vs2005+sp1+OgreSDK1.4.3开发环境简化整理而来,其中穿插着笔者自己的理解。这是教程的第一部分,也是我的学习笔记。正文:凡是翻译过几篇技术类文章的人都深知从头至尾忠实重现作者的原意是一件多么令人头疼的事情。当我从诸多曾经许诺要翻译的文章中爬出来的时候,我决定这次不做那样一个“傻子&...  阅读全文
posted @ 2009-12-25 16:44 暗夜教父 阅读(6017) | 评论 (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 暗夜教父 阅读(564) | 评论 (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 暗夜教父 阅读(731) | 评论 (0)编辑 收藏

在ubuntu下安装subversion服务器

在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 subversion

sudo 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 subversion

cd /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 菜单上选择“系统->系统管理->用户和组”;
  • 切换到“组”标签;
  • 点击“添加组”按钮;
  • 组名为“subversion”;
  • 将您自己和“www-data”(Apache 用户)加入组成员中;
  • 点击“OK”以确认修改,关闭该程序。

或者使用命令完成上述功能(增加组,并且把用户加到组里):

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 协议访问(http://)

要通过 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 暗夜教父 阅读(873) | 评论 (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 暗夜教父 阅读(347) | 评论 (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 暗夜教父 阅读(791) | 评论 (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 暗夜教父 阅读(565) | 评论 (0)编辑 收藏
仅列出标题
共9页: 1 2 3 4 5 6 7 8 9 

<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

常用链接

留言簿(2)

随笔分类

随笔档案

文章分类

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜