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;
}