并发服务器是socket应用编程中最常见的应用模型。并发服务器模型根据连接方式分为长连接和短连接,长连接为通信双方建立连接后一直保持连接,然后一直用此连接进行读写操作;短连接为通信双方每一次交易过程都建立连接和关闭连接。并发服务器模型根据处理方式可分为同步方式和异步方式,同步是客户端发送请求给服务器等待服务器返回处理结果;异步是指客户端发送请求给服务器,不等待服务器返回处理结果,而直接去完成其他的流程,对于处理结果客户端可以事后查询和让服务器进行主动通知。
进程是一个程序的一次运行过程,它是一个动态实体,是独立的任务,它拥有独立的地址空间、执行堆栈、文件描述符等。每个进程拥有独立的地址空间,在进程不存在父子关系的情况下,互不影响。
进程的终止存在两个可能:父进程先于子进程终止(由init进程领养),子进程先于主进程终止。对于后者,系统内核为子进程保留一定的状态信息(进程ID、终止状态、CPU时间等),并向其父进程发送SIGCHLD信号。当父进程调用wait或waitpid函数时,将获取这些信息,获取后内核将对僵尸进程进行清理。如果父进程设置了忽略SIGCHLD信号或对SIGCHLD信号提供了处理函数,即使不调用wait或waitpid函数内核也会清理僵尸进程。
但父进程调用wait函数处理子进程退出信息时,会存在下面所述的问题。在有多个子进程情况下,wait函数只等待最先到达的子进程的终止信息。下图18-7父进程有3个子进程,由于SIGCHLD信号不排队,在SIGCHLD信号同时到来后,父进程的wait函数只执行一次,这样将留下2个僵尸进程,而使用waitpid函数并设置WNOHANG选项可以解决这个问题。
图18-7 多进程信号图
综上所述,在多进程并发的情况下,防止子进程变成僵尸进程常见有如下两种方法:
① 父进程调用signal(SIGCHLD,SIG_IGN)对子进程退出信号进行忽略,或者把SIG_IGN替换为其他处理函数,设置对SIGCHLD信号的处理。
② 父进程调用waitpid(-1,NULL,WNOHANG)对所有子进程SIGCHLD信号进行处理。
图18-8~图18-11画出了并发服务器文件描述符的变化流程图。其中listenfd为服务端的socket监听文件描述符,connfd为accept函数返回的socket连接文件描述符。
服务器调用accept函数后,客户与服务器文件描述符如下图18-8所示。
图18-8 调用accept函数时套接字描述符图
服务器调用accept函数后,客户与服务器文件描述符如下图18-9所示。
图18-9调用accept函数后套接字描述符图
服务器调用fork函数后,客户与服务器文件描述符如下图18-10所示。
图18-10调用fork函数后套接字描述符图
服务端父进程关闭连接套接字,子进程关闭监听套接字,客户与服务器文件描述符状况如下图18-11所示。
图18-11 并发服务器最终连接图
在这里强调的是,并发服务器fork后父进程一定要关闭子进程连接套接字;而子进程要关闭父进程监听套接字,以免误操作。
(1)并发服务器处理流程
并发服务器处理流程如下:
① 客户端首先发起连接。
② 服务端进程accept打开一个新的连接套接字与客户端进行连接,accept在一个while(1)循环内等待客户端的连接。
③ 服务端fork一个子进程,同时父进程close子进程连接套接字,循环等待下一进程。
④ 服务端子进程close父进程监听套接字,并用连接套接字保持与客户端的连接,客户发送数据到服务端,然后阻塞等待服务端返回。
⑤ 子进程进行接收数据,进行业务处理,然后再发送数据给客户端。
⑥ 子进程关闭连接,然后退出。
(2)程序报文协议说明
该程序报文协议模式为常见行业应用软件协议模式,其具体说明如下:
发送和接收报文协议:8位报文长度(不包含本身)+6位交易码+报文内容,交易码标识该交易的类型。
实际应用中服务端进程根据6位交易码调度不同的应用服务。
(3)并发服务器服务端代码
tcpsrv.c源代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MY_PORT 10000
extern int readn(int fd,void *buffer,int length) ;
extern int writen(int fd,void *buffer,int length) ;
int main(int argc ,char **argv)
{
int listen_fd,accept_fd;
struct sockaddr_in server_addr;
struct sockaddr_in cli_addr;
int n;
int cliaddr_len ;
char buffer[1024];
char data[1024] ;
long length ;
int nbytes ;
if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("Socket Error:%s\n",strerror(errno));
return -1;
}
memset(&server_addr,0x00, sizeof(struct sockaddr_in));
memset(&cli_addr,0x00, sizeof(struct sockaddr_in));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(MY_PORT);
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
n=1;
/* 如果服务器终止后,服务器可以第二次快速启动而不用等待一段时间 */
setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
if(bind(listen_fd,(struct sockaddr *)&server_addr,sizeof(server_addr))<0)
{
printf("Bind Error:%s\n",strerror(errno));
return -1;
}
listen(listen_fd,5);
while(1)
{
cliaddr_len= sizeof( cli_addr ) ;
accept_fd=accept(listen_fd, (struct sockaddr *)&cli_addr, &cliaddr_len );
if((accept_fd<0)&&(errno==EINTR))
continue;
else if(accept_fd<0)
{
printf("Accept Error:%s\n",strerror(errno));
continue;
}
if((n=fork())==0)
{
/* 子进程处理客户端的连接 */
fprintf(stdout,"listen_fd:%d accept_fd:%d\n",listen_fd, accept_fd);
close(listen_fd);
memset(buffer, 0x00, sizeof(buffer)) ;
memset(data, 0x00, sizeof(data));
if((nbytes=readn(accept_fd, data, 8 ))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
fprintf(stderr,"data:%s\n",data );
close(accept_fd);
return -1;
}
fprintf(stdout,"data:%s,nbytes=%d\n",data, nbytes );
data[nbytes]='\0' ;
length=atol(data) ;
fprintf(stdout,"data:%s,nbytes=%d\n",data, nbytes );
if((nbytes=readn(accept_fd, data, length ))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
close(accept_fd);
return -1;
}
data[nbytes]='\0' ;
fprintf(stdout,"data:%s,nbytes=%d\n",data, nbytes );
if( strncmp(data, "000000", 6 )==0 )
{
strcpy(buffer, "I am sorry! who am I? I don't know also.") ;
length=strlen(buffer) ;
sprintf(data,"%08ld%6.6s%s", (length+6),"000000", buffer ) ;
if((nbytes=writen(accept_fd,data, (length+6+8)))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
close(accept_fd);
return -1;
}
fprintf(stdout,"data:%s\n",data );
}else{
/*非000000交易请求为非法,沉默是最好的回答*/
}
close(accept_fd);
return 0;
}
else if(n<0)
printf("Fork Error:%s\n\a",strerror(errno));
close(accept_fd);
while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */
}
}
(4)客户端代码
tcpcli.c源代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern int readn(int fd,void *buffer,int length) ;
extern int writen(int fd,void *buffer,int length) ;
int main(int argc, char *argv[])
{
int sockfd;
char buffer[1024];
char data[1024];
long length ;
struct sockaddr_in server_addr;
struct hostent *host;
int portnumber,nbytes;
if(argc!=3)
{
fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
return -1;
}
if((host=gethostbyname(argv[1]))==NULL)
{
fprintf(stderr,"Gethostname error\n");
return -1;
}
if((portnumber=atoi(argv[2]))<0)
{
fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
return -1;
}
/* 客户程序开始建立 sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
return -1;
}
/* 客户程序填充服务端的地址端口信息 */
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(portnumber);
server_addr.sin_addr= *((struct in_addr *)host->h_addr);
/* 客户程序发起连接请求 */
if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr)
)==-1)
{
fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
return -1;
}
memset(buffer, 0x00, sizeof(buffer)) ;
memset(data, 0x00, sizeof(data));
strcpy(buffer, "Hello! who are you? could you tell me?") ;
length=strlen(buffer) ;
/***000000为假设的交易码***/
sprintf(data,"%08ld%6.6s%s", (length+6),"000000", buffer ) ;
length=length+8+6 ;
if((nbytes=writen(sockfd,data, length ))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
close(sockfd);
return -1;
}
printf("I have send:%s\n", data+8);
if((nbytes=readn(sockfd, data, 8 ))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
close(sockfd);
return -1;
}
data[nbytes]='\0' ;
length=atol(data) ;
if((nbytes=readn(sockfd, data, length ))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
close(sockfd);
return -1;
}
data[nbytes]='\0' ;
printf("I have received:%s\n", data);
close(sockfd);
return 0;
}
(5)编译与执行
编译 gcc tcpsrv.c tcpio.c -o tcpsrv。
编译 gcc tcpcli.c tcpio.c -o tcpcli。
在一界面下启动服务端进程 ./tcpsrv。
在另一界面下执行./tcpcli 127.0.0.1 10000,执行结果如下:
I have send:000000Hello! who are you? could you tell me?
I have received:000000I am sorry! who am I? I don't know also.
摘录自《深入浅出Linux工具与编程》