|
根据之前我们讨论的,我们的网络模块使用面向对象的C++编写,在Demo版中不考虑加密/解密等其他因素,仅仅考虑消息的发送和接收。我想,暂时也不考虑随着人物的移动如何发送广播……以下是我这几天的研究成果。
套接字是一种网络API,用于不同进程的通信。它支持多种通信协议,并定义了多种类型,如SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_SEQPACKET等。其中最常用的两种是SOCK_STREAM、SOCK_DGRAM。SOCK_STREAM用于提供面向连接的传输,而SOCK_DGRAM用于提供面向无连接的传输。
采用TCP套接字可实现基于TCP/IP协议、面向连接的通信模式。TCP套接字的实现分为TCP服务器和TCP客户。其模式相对固定,我们仅需理解其基本过程,要编程可套用相应模板。TCP服务器的基本过程是:创建套接字socket()、绑定套接字bind()、监听listen()并接受连接请求accept()、读/写数据send()recv()和终止连接shutdown()。TCP客户的基本过程是:创建套接字socket()、连接服务器connect()、读写数据send()recv()和终止连接shutdown()。但却并不实用,无法满足复杂的网络应用。
[code:1]
/************************************************
TCP服务器
************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main()
{
int sockfd,connect_sock;
/*Create TCP socket*/
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("Creating socket failed.");
exit(1);
}
/*Bind socket to address */
......
/*listen client's reqirement*/
......
loop
{
/*accept connection*/
if((connect_sock=accept(sockfd,NULL,NULL))==-1)
{
perrpr("Acception failed.");
exit(1);
}
......
}
}
/************************************************
TCP客户
************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main()
{
int sockfd;
/*Create TCP socket*/
if((sockfd=socket(AF_INET,SOCK_STREAM,,0))==-1)
{
perror("Creating socket failed.");
exit(1);
}
/* Connect to server and receive data from the server.*/
......
}
[/code:1]
套接字的另一主要用途是UDP。采用UDP套接字可实现基于TCP/IP协议面向无连接的通信模式。于TCP套接字相比UDP套接字不能提供可靠的数据传输,但UDP套接字的实现更加简单,并且具有更高的传输效率。因而同样也得到广泛的应用,如DNS、NFS、SNMP。与TCP套接字相同,其实现也分为UDP服务器和UDP客户端。UDP服务器的基本过程包括:创建套接字、绑定套接字、读/写数据和关闭套接字。UDP客户的基本过程是:创建套接字、读写数据和关闭套接字。与TCP套接字不同,UDP套接字读/写函数,recvfrom()和sendto()函数都有与套接字地址相关的参数。对于UDP服务器而言,用该参数来区分不同的客户端。而对于客户端,则用此确定相应的服务器。——在Linux内部UDP的函数就是通过TCP函数实现功能的,所以我还是倾向于使用TCP函数,当然也不绝对,要听听大家的意见了……
[code:1]
/************************************************
UDP服务器
************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main()
{
int sockfd;
/*Create UDP socket*/
if((sockfd=socket(AF_INET,SOCK_DGRAM,0))==-1)
{
perror("Creating socket failed.");
exit(1);
}
/*Bind socket to address */
loop
{
/*RECEIVE CLIENT'S DATA*/
...
/*handle client's information*/
...
/*send information to client*/
...
}
/*close socket*/
}
/************************************************
UDP客户
************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main()
{
int sockfd;
/*Create UDP socket*/
if((sockfd=socket(AF_INET,SOCK_DGRAM,0))==-1)
{
perror("Creating socket failed.");
exit(1);
}
/* send data to the server.*/
...
/* receive data from the server.*/
...
/*close UDP socket*/
close(sockfd);
}
[/code:1]
Linux系统主要提供三种方式以实现并发服务器:进程、线程及I/O多路服用。
采用多进程实现并发服务器是一种最传统的方法。由于各进程有独立的地址空间,具有可靠性高的特点。但却导致系统开销大,切换时间长,并且进程间共享数据较为复杂。
[code:1]
/************************************************
多进程并发服务器
************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
main()
{
int listenfd,connfd;
pid_t pid;
int BACKLOG=5;
/* Create TCP socket */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("Creating socket failed.");
exit(1);
}
/*Bind socket to address */
bind(listenfd,...);
/*listen client's reqirement*/
listen(listenfd,BACKLOG);
while(1)
{
/*accept connection*/
if((connfd=accept(aockfd,NULL,NULL)==-1)
{
perrpr("Acception failed.");
exit(1);
}
/*create child process to service the client.*/
if(pid=fork())>0)
{
/*parent process*/
close(connfd);
...
continue;
}
else if (pid==0){
/*child process*/
close(listenfd);
...
exit(0);
}
else{
printf("fork error\n");
exit(0);
}
}
[/code:1]
虽然多线程具有系统开销小,切换速度快的等特点,但由于多个线程共用相同的内存区,从而造成多线程比多进程更严重的可重入问题。尤其是开发服务器的编程,如果不注意这个问题,会产生很多难以调试的bug。其根本解决方法是:为线程专有的数据分配专用的数据区。可采用系统提供的方法或主线程为每个新产生的线程分配数据区。
[code:1]
/************************************************
多线程并发服务器
************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
/*function to be executed by the new thread */
void* start_routine(void * arg);
main()
{
int listenfd,connfd;
pthread_t thread;
type arg;
int BACKLOG=5;
/* Create TCP socket */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("Creating socket failed.");
exit(1);
}
/*Bind socket to address */
bind(listenfd,...);
/*listen client's connecting reqirement*/
listen(listenfd,BACKLOG);
while(1)
{
/*accept connection*/
if((connfd=accept(aockfd,NULL,NULL)==-1)
{
perrpr("Acception failed.");
exit(1);
}
/*create child process to service the client.*/
.../*put parameters to arg*/
if(pthread_create(&thread,NULL,start_routine,(void *)&arg)){
/*handle exception*/
...
exit(0);
}
}
}
[/code:1]
I/O多路复用具有极小的系统开销,因此其性能很高,但其程序结构较为复杂。
[code:1]
/****************************************
select()函数实现I/O多路复用
****************************************/
...
int fd,allfd[MAX];
fd_set fdRSet,fdWSet;
struct timewal timeout=...;
int maxfd=MAXSOCKET+1;
...
while(1){
FD_ZERO(&fdRSet);
FD_SET(fd,&fdTSet);
FD_ZERO(&fdWSet);
FD_SET(fd,&fdWSet);
switch(select(maxfd,&fdTSet,&fdWset,&timeout)){
case -1:handle exception;
case 0:handle timeout;
default:
for(All of socket){
if(FD_ISSET(allfd[i],&fdRSet)){
Read from the socket.
...
}
if(FD_ISSET(allfd[i],&fdWSet)){
Write to the socket.
...
}
...
}
}
}
[/code:1]
由此看来,每种实现方法各有其有缺点,考虑到网络游戏的服务器端要有支持几百个客户端的连接,多进程实现并发服务器的方法可以不予考虑,至于后两种选择,由于我们准备采用面相对象的编程方法,以及先开发一个Demo版的游戏,我倾向于使用多线程的方法,不知大家有什么更好的建议?
在多进程/多线程环境中,线程及进程同步是十分重要的。该技术解决了有并发而产生的同步问题,使得并发服务器能可靠的运行。多线程并发服务器虽然有很多优点,但是存在一些问题,需要十分注意。除了前面讲到的线程安全性问题外,另一个重要的方面就是线程同步。
对于复杂的网络服务器,同步可能会异常复杂,处理不好就会导致系统的可靠性下降。尤其是多线程并发服务器,多个线程使用同一地址空间。虽然,线程间通信简单,却由于共享数据,则必须处理同步。在Linux系统中,提供互斥锁和条件变量来实现线程的同步。互斥锁用于保护共享数据的完整性,条件变量则用于协调多个线程的运行以提高运行效率。同时,线程退出的同步亦不能忽视,通常利用pthread_join()函数来实现。在处理同步时,要注意防止死锁。并且在长时间的操作(例如I/O)上还要尽量不要加锁,这样会对性能造成负影响,真的好麻烦啊:)给出一个实例吧:(程序太长,还是以后再说吧~~)
还有就是异常处理。写得有一些长,呵呵,希望朋友们有耐心看完……:)
异常处理是网络编程中最复杂的部分。异常主要有超时异常、服务器异常、客户异常……在实际的网络开发中,对异常的处理占有相当大的部分。这是由网络的特点决定的。在网络上,各主机系统的运行是独立的,网络本身也非常复杂,他包括了路由、信道、防火墙等等。网络的使用情况也随时变化,造成网络拥挤程度的不同。由于这些因素,经常造成通信中断、IP包传输延迟过长或丢失包等异常情况。如果处理不好,会直接影响系统性能。这些异常,在编程中,通常引起一些I/O操作的超时。
另一类异常是由本地系统引起的,通常是由于系统资源分配造成的。如socket()和bind()函数的调用异常。这些异常影响系统的稳定性,处理不好,可能导致系统崩溃,可能有点夸张……
无论是服务器还是客户,处理异常的关键是处理与网络I/O操作相关的系统调用。通常有如下三类处理方法:对于致命的错误,需要终止程序;对于一般性错误,处理完异常后,重新执行调用;对于超时异常,根据网络应用情况进行处理……I/O操作超时的处理主要用alarm()或select()函数来实现。
在网络应用开发中,随着需求的改变及性能的提高,程序变得越来越复杂,越来越大。从而导致编译和连接时间长。我们的网络模块到后来也不能超过这种情况,为更有效地重用代码,应该建立相应的类库。这也是C++的优势,也是sjinny选择C++的原因吧~为了更有效地重用代码,应该建立相应的类库。库是包括不同对象的文件。它可作为一个实体进行连接,因而可以极大地提高连接速度。据了解,Linux系统可以创建两种库:静态链接库和动态链接库。我想我们可以考虑建立自己地Socket类库,可极大地提高网络软件开发的效率,而且可开发出具有商业价值的类库,哈哈……
暂时就想到这么多……还有守护进程、多接口……过几天再讨论啦,不知道大家对我的想法有什么好的意见? |
|