Introduction
本节使用的读写函数使用了上一节封装以后的读写函数,具体实现可以看看上一章。
首先必须清楚客户端和服务端通过TCP是如何交互的。
下图说的很清楚,客户服务端被动打开监听,accept函数一直阻塞到客户到达。客户端使用connect向服务端发送请求,建立3路握手后之后就可以发消息了。
多进程并发服务器模型
如下图客户端通过connect函数和服务器建立连接listenfd是调用listen函数得到的。它会一直监听客户的到达并把客户放到一个队列里面后面accept再从队列里面读。
之后accept接受这个连接之后返回一个已连接描述符。
然后这一步是重要的也是服务器可以同时处理多个客户的原因。
就是每到达一个客户,通过调用fork函数,父进程创建子进程,并且会继承父进程的打开的文件描述符,所以注意在子进程需要把listenfd这个关了,只要父进程监听就可以了。
这里父进程关闭已连接套接字,下一次再调用accept返回已连接描述符,而子进程关闭监听描述符。
实现
下面是代码放到github上面的,觉得可以的话给我加星,谢谢。
具体代码函数功能说明
multiporcess.c 主函数
open_listenfd()这个函数就是服务器就是通过调用几个函数打开监听。后面直接调用这个函数。
/* 打开监听套接字,这个描述符准备在知名端口上准备接受请求*/
int open_listenfd(int port)
{
int listenfd,optval = 1;
struct sockaddr_in serveraddr;
if((listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket error");
exit(-1);
}
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(const void *)&optval,sizeof(int)) < 0)/*这个套接字的地址可以重用,重复绑定 */
{
perror("adress already in use error");
exit(-1);
}
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd(SA*)&serveraddr,sizeof(serveraddr)))
{
perror("bind error");
exit(-1);
}
if(listen(listenfd,LISTENQ) < 0)
{
perror("listen error");
exit(-1);
}
return listenfd;
}
主函数部分
【 需要注意
signal_r函数是一个信号处理函数我之后专门写一篇来说明。
这里简单说一下,fork创建出很多进程,这些进程执行完以后就exit(0)然后发个信号通知主进程,exit函数退出后,进程的有关资源(打开的文件描述符,栈,寄存器,有关信息等)还没有释放掉,如果进程不被回收的话就会占用存储器资源这样的进程就称为僵尸进程。所以解决办法就是主进程使用一个信号处理函数,等待僵尸进程回收。下面这个函数实现了这个功能。】
/*等待多个进程结束*/
void sigchild_handler( int sig )
{
int stat;
while(waitpid(-1,&stat,WNOHANG)>0);
return;
}
int main(int argc,char **argv)
{
int listenfd,connfd,clilen,port;
struct sockaddr_in clientaddr;
if(argc != 2){
fprintf(stderr,"usage:%s <port>\n",argv[0]);
}
port =atoi( argv[1]);
listenfd = open_listenfd(port);/*打开监听套接字*/
printf("has been listen");
signal_r(SIGCHLD,sigchild_handler);/*这是一个信号处理函数*/
while(1){
clilen = sizeof(clientaddr);
/*注意这里在一种会遇到一种中断,我们知道主进程等待客户连接的时候是阻塞在accept上的,那么当有子进程返回信号的时候就会造成accept中断,这种中断我们需要自己重启一下accept*/
if( (connfd =accept(listenfd(SA*)&clientaddr,&clilen)) < 0){
if(errno == EINTR)
continue;
else
perror("accept error");
}
if(fork() == 0){
close(listenfd);/*avoid the waste of the fd*/
do_http(connfd);
exit(0);
/*
上面exit(0);实际上退出时子进程所有打开的描述符都要关。
等价于下面两句
close(connfd);用完的描述符要关闭
exit(0)
*/
}
close(connfd);
}
return 0;
}
do_http.c 业务逻辑部分,可以处理动态和静态页面
void get_filetype(char *filename,char *filetype)
{
if(strstr(filename,".html"))
strcpy(filetype,"text/html");
else if(strstr(filename,".gif"))
strcpy(filetype,"image/gif");
else if(strstr(filename,".jpg"))
strcpy(filetype,"image/jpeg");
else if(strstr(filename,".bmp"))
strcpy(filetype,"image/bmp");
else if(strstr(filename,".mpg"))
strcpy(filetype,"video/mpeg");
else if(strstr(filetype,".png"))
strcpy(filetype,"image/png");
else if(strstr(filename,".mp4"))
strcpy(filetype,"video/mp4");
}
void serve_dynamic(int fd,char *filename,char *cgiargs)
{
char buf[MAXLINE],*list[] = {NULL};
sprintf(buf,"HTTP/1.0 200 OK\r\n");
rio_writen(fd,buf,strlen(buf));
sprintf(buf,"Server: Tiny Web Server\r\n");
rio_writen(fd,buf,strlen(buf));
if(fork() == 0)
{
setenv("QUERY_STRING",cgiargs,1);/*设置环境变量*/
dup2(fd,STDOUT_FILENO);/*标准输出重定向到客户端*/
execve(filename,list,environ);
}
wait(NULL);
}
/*读哪些文件*/
void serve_static(int fd,char * filename,int filesize)
{
int srcfd;
char *srcp,filetype[MAXLINE],buf[MAXLINE];
/*把头部信息发送给客户端*/
get_filetype(filename,filetype);
sprintf(buf,"HTTP/1.0 200 OK\r\n");
sprintf(buf,"%s Server: Tiny Web Server\r\n",buf);
sprintf(buf,"%s Content-length: %d\r\n",buf,filesize);
sprintf(buf,"%s Content-type: %s\r\n\r\n",buf,filetype);
rio_writen(fd,buf,strlen(buf));
/*把发送的文件内容给客户端*/
srcfd =open(filename,O_RDONLY,0);
srcp = mmap(0,filesize,PROT_READ,MAP_PRIVATE,srcfd,0);/*将文件映射到虚拟存储器里面*/
close(srcfd);/*映射到内存就可以把原来的文件描述符关了*/
rio_writen(fd,srcp,filesize);/*从虚拟存储器里面去读*/
munmap(srcp,filesize);
}
void error(int fd,char *cause,char *errnum,char *errmsg,char *reasonmsg )/*把错误号,原因,那个文件出错都显示出来*/
{
char buf[MAXLINE],body[MAXBUF];
sprintf(body,"<html><title> Fantasy Web Error</title>");/*html 体部标记*/
sprintf(body,"%s <body bgcolor=""ffffff"">\r\n",body);
sprintf(body,"%s<p><h1>%s: %s</h1></p>\r\n",body,errnum,errmsg);
//sprintf(body,"%s<p>%s: %s\r\n",body,reasonmsg,cause);
sprintf(body,"%s<p><h2><hr><em> From Fantasy Web Server </em></h2></p>\r\n",body);
sprintf(buf,"HTTP/1.0 %s %s\r\n",errnum,errmsg);
rio_writen(fd,buf,strlen(buf));
sprintf(buf,"Content-type: text/html\r\n");
rio_writen(fd,buf,strlen(buf));
sprintf(buf,"Content-length: %d\r\n\r\n",(int)strlen(body));
rio_writen(fd,buf,strlen(buf));
rio_writen(fd,body,strlen(body));
}
/*读报头的所有信息,web服务器不处理报头
过滤,这个时候第一个请求已经读取了
*/
void read_requestignore(rio_t *rp)
{
char buf[MAXLINE];
int fd;
rio_readlineb(rp,buf,MAXLINE);
while(strcmp(buf,"\r\n"))
{
rio_readlineb(rp,buf,MAXLINE);
FILE *out = fopen("log.txt","w+");
if(out == NULL)
{
perror("cannot open the file ");
exit(-1);
}
fprintf(out,"%s",buf);
fflush(out);
fclose(out);
}
return;
}
/*解析字符串是静态的还是动态的*/
int parse_url(char *url,char *filename,char *cgiargs)
{
char *ptr;
if(!strstr(url,"cgi-bin"))/*找有没有cgi-bin表示是一个静态文件*/
{
strcpy(cgiargs,"");/*标志是静态文件*/
strcpy(filename,".");
strcat(filename,url);
if(url[strlen(url)-1] == '/')/*这种情况取一个默认的文件index.html*/
strcat(filename,"index.html");
return is_not_cgi;
}
else /*是动态的*/
{
ptr = index(url,'?');
if(ptr)
{
strcpy(cgiargs,ptr+1);
*ptr = '\0';
}
else
strcpy(cgiargs ,"");
strcpy(filename,".");
strcat(filename,url);
return is_cgi;
}
}
/* 处理http事物
调用uri_parse函数解析请求
并且serve_static函数处理静态内容
serve_dynamic 运行cgi程序
*/
void do_http(int fd)
{
int is_static;
struct stat sbuf;
char buf[MAXLINE],method[MAXLINE],version[MAXLINE],url[MAXLINE];
char filename[MAXLINE],cgiargs[MAXLINE];
rio_t rio;
rio_readinitb(&rio,fd);/*读函数初始化*/
rio_readlineb(&rio,buf,MAXLINE);/*读函数和描述符连接起来*/
sscanf(buf,"%s %s %s",method,url,version);/*注意到字符串遇到空格停顿*/
if(strcasecmp(method,"GET"))/*不区分大小写的比较两个字符串,没有get 就不行*/
{
error(fd,method,"501","Not Implemented","web is not implemnt this method");
return ;
}
read_requestignore(&rio);/*读取并忽略报头*/
is_static = parse_url(url,filename,cgiargs);/*解析字符串看是静态还是非静态,若果有找到了返回*/
if(stat(filename,&sbuf) < 0)
{
error(fd,filename,"404","Not Found","web server cannot find file");
return;
}
if(is_static)/*判断一下读取的是静态的还是动态的*/
{
if(!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR &sbuf.st_mode))/*判断一下是不是普通文件还有这个文件可不可读*/
{
error(fd,filename,"403","Forbidden","web server cannot read the file");
return;
}
serve_static(fd,filename,sbuf.st_size);/*读文件*/
}
else /*动态的就去调执行的那个文件*/
{
if(!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR &sbuf.st_mode ))/*判断文件是不是普通文件,再看看能不能读*/
{
error(fd,filename,"403","Forbidden","web server cannot run the cgi program");
exit(-1);
}
printf("%s\n",cgiargs);
serve_dynamic(fd,filename,cgiargs);
}
}
最后的代码请去上面链接里面看,注释很详细。