epoll实现web服务器

TCP+HTTP

//web服务端程序--使用epoll模型
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
#include <dirent.h>

#include "pub.h"
#include "wrap.h"

int http_request(int cfd, int epfd);

int main()
{
	//若web服务器给浏览器发送数据的时候, 浏览器已经关闭连接, 
	//则web服务器就会收到SIGPIPE信号 13
	/*   SIGPIPE      13       Term    Broken pipe: write to pipe with no   
                                     readers

	*/
	//signal不可跨平台, signaction可以跨平台
	//signal(act , SIG_IGN);
	struct sigaction act;
	act.sa_handler = SIG_IGN;  //1.忽略 2.阻塞  选择忽略的方法好一些 man 7 signal 查看信号默认处理动作
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGPIPE, &act, NULL);
	
	//进程在哪个目录下执行,那个目录就是默认工作目录
	//老师的案例中,进程在 ~/test/course/day16/20180909 下
	//chdir  : 改变当前进程的工作目录
	//getenv : 获取环境变量的值
	//函数作用:防止进程在U盘目录下执行中,拔掉U盘了
	/*char path[255] = {0};
	sprintf(path, "%s/%s", getenv("HOME"), "webpath");
	chdir(path);  */
	
	/*
		pwd: /home/itcast/webpath
		echo $HOME: /home/itcast
		工作目录 = $HOME+webpath
	*/
	
	
	//创建socket--设置端口复用---bind
	int lfd = tcp4bind(9998, NULL);
	
	//设置监听
	Listen(lfd, 128);

	//创建epoll树
	int epfd = epoll_create(1024);
	if(epfd<0)
	{
		perror("epoll_create error");
		close(lfd);
		return -1;
	}
	
	//将监听文件描述符lfd上树
	struct epoll_event ev;
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	
	int i;
	int cfd;
	int nready;
	int sockfd;
	struct epoll_event events[1024];
	while(1)
	{
		//等待事件发生
		nready = epoll_wait(epfd, events, 1024, -1);
		if(nready<0)
		{
			if(errno==EINTR)
			{
				continue;
			}
			break;
		}
		
		for(i=0; i<nready; i++)
		{
			sockfd = events[i].data.fd;
			//有客户端连接请求
			if(sockfd==lfd)
			{
				//接受新的客户端连接
				cfd = Accept(lfd, NULL, NULL);
				
				//设置cfd为非阻塞,目的是read读cfd时不阻塞
				int flag = fcntl(cfd, F_GETFL);
				flag |= O_NONBLOCK;
				fcntl(cfd, F_SETFL, flag);
				
				//将新的cfd上树
				ev.data.fd = cfd;
				ev.events = EPOLLIN;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
			}
			else 
			{
				//有客户端数据发来
				http_request(sockfd, epfd);
			}			
		}		
	}
}

int send_header(int cfd, char *code, char *msg, char *fileType, int len)
{
	char buf[1024] = {0};
	sprintf(buf, "HTTP/1.1 %s %s\r\n", code, msg); //状态行
	//拼接消息报头
	//具体实现原理://将指针buf向后移动到当前buf字符串的末尾,
	//然后在末尾位置开始拼接新的内容。这样可以确保新拼接的内容不会覆盖已有的内容,从而实现字符串的拼接。
	sprintf(buf+strlen(buf), "Content-Type:%s\r\n", fileType);
	if(len>0)
	{
		sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
	}
	strcat(buf, "\r\n");	//拼接空行
	Write(cfd, buf, strlen(buf));
	return 0;
}

int send_file(int cfd, char *fileName)
{
	//打开文件
	int fd = open(fileName, O_RDONLY);
	if(fd<0)
	{
		perror("open error");
		return -1;
	}
	
	//循环读文件, 然后发送
	int n;
	char buf[1024];
	while(1)
	{
		memset(buf, 0x00, sizeof(buf));
		n = read(fd, buf, sizeof(buf));	//read读文件不会阻塞
		if(n<=0)
		{
			break;
		}
		else 
		{
			Write(cfd, buf, n);
		}
	}
}

int http_request(int cfd, int epfd)	//接收数据 , 然后处理数据
{
	int n;
	char buf[1024];
	//读取请求行数据, 分析出要请求的资源文件名
	memset(buf, 0x00, sizeof(buf));
	n = Readline(cfd, buf, sizeof(buf));
	if(n<=0) //=0客户端关闭了链接  <0读异常	
	{
		//printf("read error or client closed, n==[%d]\n", n);
		//关闭连接
		close(cfd);
		
		//将文件描述符从epoll树上删除
		epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
		return -1;	
	}
	printf("buf==[%s]\n", buf);
	//GET /hanzi.c HTTP/1.1
	char reqType[16] = {0};
	char fileName[255] = {0};
	char protocal[16] = {0};
	sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocal);
	//printf("[%s]\n", reqType);
	printf("--[%s]--\n", fileName);
	//printf("[%s]\n", protocal);
	
	char *pFile = fileName;
	if(strlen(fileName)<=1) //192.168.186.128:9999 或 192.168.186.128:9999/  则pFile为./
	{
		strcpy(pFile, "./"); //如果写的是localhost:9999 ,则访问./ 即进程工作目录的起始位置
	}
	else 	//其他情况把/去掉   192.168.186.128:9999/baidu.html  则为baidu.html
	{
		因为/hanzi.c 有个/ , 但是访问文件时,要么./ 要么没有/
	//所以可以加个.  或者去掉 /
		pFile = fileName+1;
	}
	
	//转换汉字编码
	strdecode(pFile, pFile);
	printf("[%s]\n", pFile);
	
	//read:当读完后,默认会阻塞卡到那里等待数据
	//只需要请求行, 但是请求消息里还有请求头 空行 消息正文, 他们还会留在内核缓冲区里,所以要循环读完剩余的内核缓冲区数据
	while((n=Readline(cfd, buf, sizeof(buf)))>0);
	//避免产生粘包
	
	//判断文件是否存在
	struct stat st;
	if(stat(pFile, &st)<0)	//若文件不存在,返回错误页
	{
		printf("file not exist\n");
		
		//发送头部信息 - 状态行、消息报头、空行
		send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
		
		//发送文件内容 - 消息正文
		send_file(cfd, "error.html");	
	}
	else 	//若文件存在,判断文件类型是普通?目录?
	{
		//判断文件类型
		//普通文件
		if(S_ISREG(st.st_mode))
		{
			printf("file exist\n");
			//发送头部信息
			send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size);
			//get_mime_type 判断文件类型函数 在pub.c中
				
			//发送文件内容
			send_file(cfd, pFile);
		}
		//目录文件
		else if(S_ISDIR(st.st_mode))
		{
			printf("目录文件\n");
			
			//回复客户端的应答信息
			
			
			char buffer[1024];
			//发送头部信息
			send_header(cfd, "200", "OK", get_mime_type(".html"), 0);	//长度传0相当于没有,要么传对要么传0
			
			//发送html文件内容
			//发送html文件的头部代码
			send_file(cfd, "html/dir_header.html");	
			
			//发送html文件的文件体代码
			//把官方man 3 scandir 的demo拿来改造
			//组织文件列表信息
			struct dirent **namelist;
			int num;
			
			//把argv[1] 换成pFile
			num = scandir(pFile, &namelist, NULL, alphasort);
			if (num < 0)
			{
			   perror("scandir");
			   close(cfd);
			   epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
			   return -1;
			}
			else 
			{
			   while (num--) 	//遍历目录内的文件名
			   {
			       printf("%s\n", namelist[num]->d_name);
			       memset(buffer, 0x00, sizeof(buffer));
			       if(namelist[num]->d_type==DT_DIR)  //目录需要加 /   man readdir 中的结构体有d_type
			       {
			       		sprintf(buffer, "<li><a href=%s/>%s</a></li>", namelist[num]->d_name, namelist[num]->d_name);
			       }
			       else
			       {
			       		sprintf(buffer, "<li><a href=%s>%s</a></li>", namelist[num]->d_name, namelist[num]->d_name);
			       }
			       free(namelist[num]);
			       Write(cfd, buffer, strlen(buffer)); //一条一条发送 缺点:如果目录项多,就写得多
			   }
			   free(namelist);
			}
			
			//发送html文件的尾部代码
			sleep(10);
			send_file(cfd, "html/dir_tail.html");		
		}
	}
	
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值