未完成测试,注释多,供参考
//该文件包含了整个HTTP服务器的实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>
typedef struct sockaddr sockaddr;
typedef struct socket_in socket_in;
//定义一个结构体来表示缓冲区
#define SIZE (1024*4)
typedef struct HttpRequest{
char* first_line;
char* method;
char* url;
char* url_path;
char* query_string
int content_length;
}HttpRequest;
//从socket中读取一行数据
//HTTP请求中换行符\n\r \n\r都可以兼容处理
//核心思路:将未知问题转换为已知问题.将\r\n 都转化为\n
int ReadLine(int sock, char output[],ssize_t max_size)
{
//1 一个字符一个字符的从socket中读取数据
char c = '\0';
ssize_t i = 0;//记录了output缓冲区中当前已经写了多少个字符
while (i < max_size)
{
ssize_t read_size = recv(sock, &c, 1, 0);
if (read_size < 0)
{
//此处希望读到完整一行,如果还没读到换行就读到EOF就认为出错
return -1;
}
//2 判断当前字符是不是\r
if (c == '\r')
{
recv(sock, &c, 1, MSG_PEEK);//MSG_PEEK提前看缓冲区的内容,但是不读取
//3 如果当前字符是\r,尝试读取下一个字符
// a)如果下一个字符是\n
if (c == '\n')
{
recv(sock, &c, 1, 0);
}//读取字符将\r\n转化为\n
// b)如果下一个字符不是\n
else
{
c = '\n';//将\r转化为\n
}
//这两种情况都把\r转化成\n
}
//此时所有类型的分割符都被转化为\n
//4 如果当前字符是\n,已将这行读完,结束函数
if (c == '\n')
{
break;
}
//5 如果当前字符是一个普通字符,直接追加到输出结果中
output[i++] = c;
}
output[i] = '\0';
return i;
}
//解析首行,获取到其中的method和url:字符串切分ftok
//首行格式:GET /index.html?a=10&b=20 HTTP/1.1
int ParseFirstLine(char first_line[], char** p_method, char** p_url)
{
char* tok[10] = { 0 };
//使用Split函数对字符串进行切分,n表示切分结果有几个部分
int n=Split(first_line, " ",tok);
if (n != 3)
{
//如果不是3,不符合HTTP协议
printf("Split failed!n=%d\n");
return -1;
}
//此处可以进行更加复杂的校验
*p_method = tok[0];
*p_url = tok[1];
return 0;
}
//进行字符串切分
//strtok内部使用static变量来保存字符串切分的情况,如果有大量的客户端去创建进程,就会
//使static变为共享资源,线程不安全
//使用strtok_r是线程安全版本,内部没有静态变量,需要用户在栈上手动定义变量作为缓冲区去存储.
int Split(char input[], const char*split_char, char* output[])
{
char* tmp = NULL;//如果在tmp前面加static,那么线程就会共用tmp,就不会线程安全
int output_index = 0;
char* p = strtok_r(input,split_char,&tmp);
while (p != NULL)
{
output[output_index++] = p;
p = strtok_r(NULL, split_char,&tmp);
}
return output_index;
}
//url形如: /index.html?a=10&b=20
// http:/.www.baidu.com/index.html?a=10&b=20(不考虑)
int ParseUrl(char url[], char** p_url_path, char** p_query_string)
{
*p_url_path = url;
//查找问号所在位置
char* p = url;
for (; *p != '\0'; ++p)
{
if (*p == '?')
{
//找到了?,将之替换成\0
*p = '\0';
*p_query_string = p + 1;
return 0;
}
}
*p_query_string=NULL;//没有找到
return -1;
}
int ParseHeader(int new_sock, int* content_length)
{
char buf[SIZE] = { 0 };
while (1)
{
ssize_t read_size = ReadLine(new_sock, buf, sizeof(buf)-1);
if (raed_size <= 0)
{
return -1;
}
if (strcmp(buf, "\n") == 0)
{
//读到空行
return 0;
}
//Content_length:100\n
const char* key = "Conten - Length: ";
if (strncmp(buf, key, strlen(key)) == 0)
{
*content_length = atoi(buf + strlen(key));
//break;使用break会出现粘包问题
}
}
return 0;
}
void Handler404(int new_sock)
{
const char* first_line = "HTTP/1.1 404 Not Found!\n";
//此处可以不加header
//Content-Type可以让浏览器自动识别
//Content-Length可以通过关闭socket的 方式告知浏览器已经读完
//body部分是 html的页面
const char* body = "<head><meta http-equiv=\"Content - Type\" "
"content=\"text / html; charset = utf - 8\">"
"</head><h1>404!!!页面被吃了</h1>";
}
int IsDir(const char* file_path)
{
struct stat st;
int ret = stat(file_path, &st);
if (ret < 0)
{
//此处不是目录
return 0;
}
if (S_ISDIR(st.st_mode))
{
return 1;
}
return 0;
}
void HandlerFilePath(const char* url_path, char file_path[])
{
//url_path是以/开头的,所以不需要wwwroot之后显示指明/
sprintf(file_path, "./wwwroot%s", url_path);
//如果url_path指向目录,就在目录后面拼装index.html作为默认访问的文件
//识别url_path指向的是普通文件还是目录
// a)url_path以/结尾,例如:/image/,一定是目录
if (file_path[strlen(file_path) - 1] == '/')
{
strcat(file_path, "index.html");
}
else
{
// b)url_path没有以/ 结尾,此时需要根据文件属性来判定是否是目录:stat函数获取文件属性
if (IsDir(file_path))
{
strcat(file_path, "./index.html");
}
}
}
ssize_t GetFileSize(const char* file_path)//int=2G,太小
{
struct stat st;
int ret = stat(file(file_path, &st));
if (ret < 0)
{
return 0;
}
return st.st_size;
}
int WriteStaticFile(int new_sock, const char* file_path)
{
//1.打开文件,如果打开失败,就返回404
int fd = open(file_path,O_RDONLY)
if (fd < 0)
{
perror("open");
return 404;
}
//2.构造HTTP响应报文
const char* first_line = "HTTP/1.1 200 OK\n";
send(new_sock, first_line, strlen(first_line), 0);
//此处如果更严谨,就需要加一些header
//因为浏览器能够自动识别Content-Type,就没写
//没写conten_length是因为后面立刻关闭了socket
//浏览器能识别数据在哪里结束
const char* blank_line = '\n';
send(new_sock, first_line, strlen(blank_line), 0);
//3.读文件并且写入socket中
//更高效:sendfile把一个文件中的数据读出来,写到另一个中.可以从中间开始
/*char c = '\0';
while (read(new_sock, &c, 1) > 0)
{
send(new_sock, &c, 1, 0);
}*/
ssize_t file_size = GetFileSize(file_path);
sendfile(new_sock, fd, NULL, file_size);
//4.关闭文件
colse(fd);
return 200;
}
int HandlerStaticFile()
{
//1.根据url_path获取到文件的真实目录
//例如,hTTP服务器根目录叫 ./wwwroot
//此时有一个文件叫做 ./wwwroot/image/101.jpg
//在url中写一个path就叫做 /image/101.jpg
char file_path[SIZE] = { 0 };
//根据下面的函数将 /image/101.jpg转化为
//磁盘上的 ./wwwroot/image/101.jpg
HandlerFilePath(req->url_path, file_path);
//2.打开文件,读取文件内容,把文件内容写到socket中
int err_code = WriteStaticFile(new_sock, file_path);
return err_code;
}
int HandlerCGIFather(int new_sock,int father_read,int father_write,const HttpRequest* req)
{
// a)如果是POST请求,把body部分的数据读出来写到管道里,
//剩下的动态生成页面的过程都交给子进程来完成
if (strcasecmp(req->method, "POST"))
{
//根据body的长度决定读取多少个字节
char c = '\0';
int i = 0;
//使用循环防止 read被打断,不加循环即使缓冲区够长,
for (; i < req->content_length; ++i)
{
read(new_sock, &c, 1);
write(father_write, &c, 1);
}
}
// b)构造HTTP响应
const char* first_line = "HTTP/1.1 200 OK\n";
send(new_sock, blank_line, strlen(blank_line),0);
// c)从管道中读取数据(子进程动态生成的页面),把这个页面也写到socket当中,
//此处不方便用sendfile,,主要是数据的长度不容易确定
char c = '\0';
while (raed(father_read, &c, 1) > 0)
{
write(new_sock, &c, 1);
}
// d)进程等待,回收子进程的资源.
//此处如果要进程等待,最好使用waitpid,保证当前回收的子进程就是当年
waitpid(NULL);
return 200;
}
int HandlerCGIChild(int child_read,int child_write,int new_sock, int father_read, int father_write, const HttpRequest* req)
{
//注意:环境变量写在父进程中,虽然子进程能够继承父进程的环境变量,由于同一时刻会有很多个请求,每个请求
//都在请求修改环境变量,会产生类似于线程安全的问题.导致子进程不能正确的获取到这些信息
// a)设置环境变量(METHOD,CONTENT_LENGTH,QUERY_STRING)
// 如果把上面的这几个信息通过管道来告知替换之后的程序,把这个数据也写到socket当中也是可行的,但是此处要遵守CGI标准,
//所以必须使用环境变量传递以上信息.
char method_env[SIZE] = { 0 };
//REQUERY_METHOD=GET
sprintf(method_env, "REQUEST_METHOD=%s", req->method);
putenv(method_env);
if (strcasecmp(req->method,"GET")==0)
{//设置REQUERY_STRING
char query_string_env[SIZE] = { 0 };
sprintf(method_env, "QUERY_STRING=%s", req->query_string);
putenv(query_string_env);
}
else
{
//设置CONTENT_LENGTH
char conten_length_env[SIZE] = { 0 };
sprint(conten_length_env, "CONTENT_LENGTH=%s", req->content_length);
putenv(conten_length_env);
}
// b)把标准输入和标准输出重定向到管道中.此时,CGI读写标准输入输出就相当于读写管道.
dup2(child_read,0);//把后面重定向到前面
dup2(child_write, 1)
// c)子进程进行程序替换(需要先找到是哪个CGI可执行程序,然后再使用exec函数进行替换)
// 替换成功之后,动态页面完全交给CGI程序来计算生成.
char file_path[SIZE] = { 0 };
HandlerFilePath(req->url_path, file_path);
//l lp le
//v vp ve
//第一个参数是可执行程序的路径
//第二个参数,argv[0]
//第三个参数NULL,表示命令行参数结束了
execl(file_path,file_path,NULL)
// d)替换失败的错误处理,子进程就是为了替换而存在的
//如果替换失败,就没有存在的必要了
}
int HandlerCGI(int new_sock)
{
//1.创建一对匿名管道
int fd[1], fd[2];
pipe(fd1);
pipe(fd2);
int father_read = fd[0];
int child_write = fd[1];
int father_write = fd[1];
int child_read = fd[0];
//2.创建子进程fork
pid_t ret = fork();
if (ret > 0)
{
//father
close(child_read);
close(child_write);
HandlerCGIFather();
}
else if (ret == 0)
{
//child;
close(father_read);
close(father_write);
HandlerCGIChild();
}
else
{
perror("fork");
goto END;
}
//3.父进程核心流程
//father
//此处先把不必要的文件描述符关闭掉
//
close(child_write);
close();
//4.子进程的核心流程
//收尾工作
END:
close();
close();
close();
}
//完成具体的请求处理过程
void HandlerRequest(int64_t new_sock)
{
int err_code = 200;//定义一个默认的错误码200,出错置为400
HttpRequest req;
memset(&req, 0, sizeof(req));
//1.解析请求
// a)从socket中读取首行
char first_line[SIZE] = { 0 };
if (ReadLine(new_sock,req.first_line,sizeof(req.first_line )-1) < 0)
{
//todo 错误处理,此处一旦触发逻辑,就简单处理,无脑返回404数据报.正常应该根据不同的错误原因返回不同的数据报
err_code = 404;
goto END;
}
printf("first_line:%s\n", req.first_line);
// b)解析首行,获取到url和method
if (ParseFirstLine(req.first_line,&req.method,&req.url)<0)
{
//错误处理
err_code = 404;
goto END;
}
// c)解析url,获取到url_path和query_string
if (PerseUrl(req.url, &req.url_path, &req.query_string) < 0)
{
//错误处理
err_code = 404;
goto END;
}
// d)解析header,丢弃大部分header,只保留content-Length
if (ParseHeader(new_sock, &req.content_length))
{
//错误处理
err_code = 404;
goto END;
}
//2.根据请求计算响应并能写回客户端
if (strcasecmp(req.method, "GET")==0&&req.query_string==NULL)//strcasecmp忽略大小写的比较
{
// a)处理静态页面
err_code=HandlerStaticFile(new_sock,&req);
}
else if (strcasecmp(req.method, "GET") == 0 && req.query_string != NULL)
{
// b)处理动态页面
err_code=HandlerCGI();
}
else if (strcasecmp(req.method, "POST") == 0) {
// b)处理动态页面
err_code=HandlerCGI();
}
else
{
//错误处理
err_code = 404;
goto END;
}
END:
//收尾工作,主动关闭socket,会进入TIME_WAUT
if (err_code != 200){
Handler404(new_sock);
}
close(new_sock);
}
void* ThreadEntry(void *arg)
{
int64_t new_sock = (int64_t)arg;
HeadlerRequest(new_sock);
return NULL;
}
void HttpServerStart(const char* ip, short port)
{
//1.基本的初始化,基于TCP
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
perror("socket");
return;
}
//设置一个选项 REUSEADDR:地址重用
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(OPT))
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
int ret = bind(listen_sock, (sockaddr*)&addr, sizeof(addr));//一个进程可以绑定多个端口号,通常一个端口号不能被多个进程绑定
if (ret < 0)
{
perror("bind");
return;
}
ret = listen(listen_sock, 5);
if (ret < 0)
{
perror("listen");
return ;
}
printf("HttpServer start OK\n");
//2.进入事件循环
while (1)
{
//此处实现一个多线程版本的服务器
//每个请求都创建一个新的线程处理具体请求
sockaddr_in peer;
socklen_t len = sizeof(peer);
int64-t new_sock = accept(listen_sock, (sockaddr*)&peer, &len);//不能用static,因为N个线程不能共用同一个new_sock
if (new_sock < 0)
{
perror("accept");
continue;
}
pthread_t tid;
pthread_create(&tid, NULL, THreadEntry, (void*)new_sock);
//new_sock怎么传递给线程入口函数,不能写取地址.同一时刻服务器可能会有很多连接线程,每个线程处理一个请求,所有的
//请求共用同一份new_sock一定会出错.正确的处理应该把new_sock按照值的方式传递到线程入口函数中.
pthread_detach(tid);//线程分离
}
}
int main(int argc,char* argv[])
{
if (argc != 3)
{
printf("Usage ./http_server [ip] [port]\n");
return 1;
}
signal(SIGCHLD, SIG_IGN);
HttpServerStart(argv[1], atoi(argv[2]));
return 0;
}