#include <stdio.h>
#include <string.h>
#include <stdlib.h> //NULL
#include <singal.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>// socket bind listen accept connect send recv
#include <netinet/in.h>
#include <arpa/inet.h>//uint16_t ntohs (uint32_t netlong)
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#define SIZE 10240
#define PAGE_404 "wwwroot/404.html"
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct HttpTmp {
char first_line[SIZE];
char* method;
char* url;
char* url_path;
char* query_string;
int content_length;
//char* Content_Type;
} HttpTmp;
int HandlerStaticFile(int new_sock, const HttpTmp* req);
int ReadLine(int sock, char buf[], ssize_t max_size){
//按行从socket中读取数据
//换行符可能有:\n,\r,\r\n
//循环从socket中读取字符,一次读一个
char c = '\0';
ssize_t i = 0;
while (i < max_size){
size_t read_size = recv(sock, &c, 1, 0);
if (read_size <= 0){
printf("Http Format error\n");
return -1;
}
if (c == '\r'){
recv(sock, &c, 1, MSG_PEEK);//MSG_PEEK读取数据但不删除
if (c == '\n'){
recv(sock, &c, 1, 0);
}
else{
c = '\n';
}
}
buf[i++] = c;
if (c == '\n'){
break;
}
}
buf[i] = '\0';
return 0;
}
//strtok 线程不安全
//Split切分完毕后,就会破坏掉原有的字符串,把其中的分割符替换成 \0
ssize_t Split(char first_line[], const char* split_char, char* output[]){
int count = 0;
//此处的 tmp必须是栈上的变量
char* tmp = NULL;
char* p = strtok_r(first_line, split_char, &tmp);
while (p != NULL){
output[count++] = p;
p = strtok_r(NULL, split_char, &tmp);
}
return count;
}
int ParseFirstLine(char first_line[], char** method_ptr, char** url_ptr){
char* arr_ptr[20] = { NULL };
ssize_t n = Split(first_line, " ", arr_ptr);
if (n != 3){
printf("first_line Split error! n= %ld\n ", n);
return -1;
}
*method_ptr = arr_ptr[0];
*url_ptr = arr_ptr[1];
return 0;
}
int ParseQueryString(char url[], char** url_path_ptr, char** query_string_ptr){
//没有考虑带域名
*url_path_ptr = url;
char* p = url;
for (; *p != '\0'; ++p){
if (*p == '?'){
*p = '\0';
*query_string_ptr = p + 1;
return 0;
}
}
*query_string_ptr = NULL;
return 0;
}
int HandlerHeader(int new_sock, int* content_length_ptr){
char buf[SIZE] = { 0 };
const char* p = "Content-Length: ";
while (1){
if (ReadLine(new_sock, buf, sizeof(buf)) < 0){
printf("ReadLine failed!\n");
return -1;
}
if (strcmp(buf, "\n") == 0){
return 0;
}
if (strncmp(buf, p, strlen(p)) == 0){
*content_length_ptr = atoi(buf + strlen(p));
//1.找到content_length的值
//2.把接受缓冲区收到的数据都去除,删除,避免粘包问题
}
}//while
}
void Handler404(int new_sock){
struct stat st;
const char* first_line = "HTTP/1.1 404 NOT FOUND\r\n";
//send(new_sock, first_line, strlen(first_line), 0);
const char* blank_line = "\r\n";
//const char* body = "<head><meta http-equiv=\"Content-Type\" \
content=\"text/html;charset=utf-8\"></head><h1>Not Found</h1>";
//const char* body = "<h1>Not Found</h1>";
//char content_length_arr[SIZE] = { 0 };
//sprintf(content_length_arr, "Content-Length: %lu\n", strlen(body));
const char* type = "Content-Type: text/html;charset=ISO-8859-1\r\n";
//const char* type = "Content-Type: text/html;charset=utf-8\r\n";
send(new_sock, first_line, strlen(first_line), 0);
//send(new_sock, content_length_arr, strlen(content_length_arr), 0);
send(new_sock, type, strlen(type), 0);
send(new_sock, blank_line, strlen(blank_line), 0);
//send(new_sock, body, strlen(body), 0);
int fd = open(PAGE_404, O_RDONLY);
stat(PAGE_404,&st);
sendfile(new_sock, fd, NULL, st.st_size);
close(fd);
}
int IsDir(const char* file_path){
struct stat st;
int ret = stat(file_path, &st);
if (ret < 0){
perror("stat\n");
return 0;
}
if (S_ISDIR(st.st_mode)){
return 1;
}
//else if((st.st_mode & S_IXUSR) ||
// (st.st_mode & S_IXGRP) ||
// (st.st_mode & S_IXOTH)){
// ;
// }
return 0;
}
void HandlerFilePath(const char* url_path, char* file_path){
sprintf(file_path, "wwwroot%s", url_path);//
if (url_path[strlen(url_path) - 1] == '/'){
strcat(file_path, "index.html");
}
if (IsDir(file_path)){
strcat(file_path, "/index.html");
}
return;
}
size_t GetFileSize(const char* file_path){
FILE* fp = fopen(file_path, "r");
fseek(fp, 0, SEEK_END);
size_t len = ftell(fp);
fclose(fp);
return len;
}
int WriteStaticFile(int new_sock, char* file_path){
//如果打开失败,则文件有可能不存在
int fd = open(file_path, O_RDONLY);
if (fd < 0){
perror("open\n");
return 404;
}
size_t file_size = GetFileSize(file_path);
//给 socket 写入的数据其实是一个HTTP响应
const char *first_line = "HTTP/1.1 200 OK\n";
//此处需要返回的header重点是两个方面:
//a)Constent-Type,可以忽略,浏览器能自动识别数据类型
//b) Content-Length,也可以省略,紧接着就会关闭socket
char header[SIZE] = { 0 };
sprintf(header, "Content-Length: %lu\n", file_size);
char type[SIZE] = { 0 };
sprintf(type, "Content-Type: text/html;charset=ISO-8859-1\r\n");
//const char* type_line = "Content-Type: text/html;charset=utf-8\r\n";
const char *blank_line = "\n";
send(new_sock, first_line, strlen(first_line), 0);
send(new_sock, header, strlen(header), 0);
send(new_sock, type,strlen(type) ,0);
send(new_sock, blank_line, strlen(blank_line), 0);
//由于接下来的数据拷贝如果采用 write/read 来进行拷贝
//会涉及到频繁的访问 IO设备,导致效率下降
//所以这里使用一个特殊的函数,直接在内核中,一次拷贝就解决问题
//需要注意的是:这个函数第一个参数必须是一个 socket
sendfile(new_sock, fd, NULL, file_size);
close(fd);
return 200;
}
int HandlerStaticFile(int new_sock, const HttpTmp* req){
char file_path[SIZE] = { 0 };
HandlerFilePath(req->url_path, file_path);
int err_code = WriteStaticFile(new_sock, file_path);
return err_code;
}
void HandlerCGIFather(int new_sock, int child_pid, int father_read, int father_write, const HttpTmp* req){
//2. 父进程需要构造一个完整的HTTP协议数据,对于HTTP协议要求我们按照指定的格式返回数据
//对于CGI要求CGI程序返回的结果只是BODY部分,HTTP请求的其他部分需要父进程自己构造
const char* first_line = "HTTP/1.1 200 OK\r\n";
//Content-Type 和 Content-Length部分省略
const char* type_line = "Content-Type: text/html;charset=ISO-8859-1\r\n";
//const char* type_line = "Content-Type: text/html;charset=utf-8\r\n";
const char* blank_line = "\r\n";
send(new_sock, first_line, strlen(first_line), 0);
send(new_sock, type_line,strlen(type_line),0);
send(new_sock, blank_line, strlen(blank_line), 0);
//1. 对于 POST 把body中的数据写入到管道中
char c = '\0';
if (strcasecmp(req->method, "POST") == 0){
//从socket中读出数据,写入管道中
//此处无法使用sendfile, 因为这个函数只能把数据写到socket中。
//所以这里采用一个字节一个字节的从socket中读出来,再写到管道中
ssize_t i;
for (i = 0; i < req->content_length; ++i){
read(new_sock, &c, 1);
write(father_write, &c, 1);
}
}
//3. 从管道中尝试读取数据,写回到socket中,father_read对应的是child_write,对于父进程来说
//child_write 已经关闭了,对于子进程来说,如果CGI程序处理完进程就推出了,进程退出就会关闭
//child_write,此时就意味着管道的所有写端都关闭,再尝试读,read返回0
while (read(father_read, &c, 1) > 0){
write(new_sock, &c, 1);
}
//4. 进行进程等待
//这里不能使用wait,因为服务器会给每一个客户都创建一个线程,每个线程又很可能创建子进程
//如果是wait等待,那么任何一个子进程结束都可能导致wait返回,这样子进程就不是由对应的线程
//来回收了
waitpid(child_pid, NULL, 0);
close(father_read);
close(father_write);
}
int HandlerCGIChild(int child_read, int child_write, const HttpTmp* req){
//1. 创建环境变量,以至于进程替换之后依然可用那些必须的数据
//2. 重定向,将子进程的标准输入和标准输出重定向到管道
dup2(child_read, 0);
dup2(child_write, 1);
//REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH
char method_env[SIZE] = { 0 };
//拼接字符串:REQUEST_METHOD=GET
sprintf(method_env, "REQUEST_METHOD=%s", req->method);
putenv(method_env);
if (strcasecmp(req->method, "GET") == 0){
//设置QUERY_STRING
char query_string_env[SIZE] = { 0 };
sprintf(query_string_env, "QUERY_STRING=%s", req->query_string);
putenv(query_string_env);
}
else{
//设置CONTENT_LENGTH
char content_length_env[SIZE] = { 0 };
sprintf(content_length_env, "CONTENT_LENGTH=%d", req->content_length);
putenv(content_length_env);
}
//3. 进程的程序替换
char file_path[SIZE] = { 0 };
sprintf(file_path, "wwwroot%s", req->url_path);
execl(file_path, file_path, NULL);//第一个参数是路径,第二个参数是命令行参数,第三个参数是NULL结尾
//4. 错误处理:如果execl执行失败
//如果此处不退出,则会出现子进程和父进程监听相同端口号的情况,而我们只希望子进程取调用CGI程序,处理
//客户端连接这样的事情应该只由父进程来完成
return 404;
}
//CGI 是一种协议, 约定了HTTP服务器如何生成动态页面,HTTP服务器需要创建子进程,子进程进行
//程序替换,
int HandlerCGI(int new_sock, const HttpTmp* req){
int err_code = 200;
//1.创建一对管道
int fd1[2], fd2[2];
pipe(fd1);
pipe(fd2);
int father_read = fd1[0];
int child_write = fd1[1];
int father_write = fd2[1];
int child_read = fd2[0];
//2.创建子进程
pid_t ret = fork();
//3.父进程流程
if (ret > 0){
close(child_read);
close(child_write);
HandlerCGIFather(new_sock, ret, father_read, father_write, req);
}
else if (ret == 0){
//4.子进程流程
close(father_read);
close(father_write);
err_code = HandlerCGIChild(child_read, child_write, req);
}
else{
err_code = 404;
}
return err_code;
}
void HandlerRequest(int new_sock){
//1.请求并分析
int err_code = 200;
// a.从socket中读取HTTP请求的首行
HttpTmp req;
memset(&req, 0, sizeof(req));
if (ReadLine(new_sock, req.first_line, sizeof(req.first_line) - 1) < 0){
printf("ReadLine First_line failed\n");
err_code = 404;
goto FLAG;
}
printf("first_line=%s\n", req.first_line);
// b.解析首行,获取到方法,url,版本号(不用)
if (ParseFirstLine(req.first_line, &req.method, &req.url) < 0){
printf("ParseFirstLine faild! frist_line= %s\n", req.first_line);
err_code = 404;
goto FLAG;
}
// c.对url进行解析,解析处其中的url_path,query_string
if (ParseQueryString(req.url, &req.url_path, &req.query_string) < 0){
printf("ParseQueryString failed! url= %s\n", req.url);
err_code = 404;
goto FLAG;
}
// d.读取并解析header部分(只解析content_length)
if (HandlerHeader(new_sock, &req.content_length)< 0){
printf("HandlerHeader failed! \n");
err_code = 404;
goto FLAG;
}
//2.根据请求的详细信息情况执行静态页面逻辑还是动态页面逻辑
// a.如果是GET请求,并且没有 query_string就是静态页面
// b.如果是GET请求,并且有query_string认为是动态页面
// c.如果POST请求,都认为是动态页面
if ((strcasecmp(req.method, "GET") == 0) && (req.query_string == NULL)){
err_code = HandlerStaticFile(new_sock,&req);
}
else if ((strcasecmp(req.method, "GET") == 0) && (req.query_string != NULL)){
err_code = HandlerCGI(new_sock,&req);
}
else if (strcasecmp(req.method, "POST") == 0){
err_code = HandlerCGI(new_sock,&req);
}
else{
printf("method not support! method= %s\n", req.method);
err_code = 404;
}
FLAG:
if (err_code != 200){
Handler404(new_sock);
}
//只是短连接,短链接就是每次客户端(浏览器)给服务器发送请求之前,都是新建立一个socket进行连接。
//对于短链接来说,如果响应写完啦,就可以关闭new_sock
//由于是服务器断开连接,进入TIME_WAIT,端口号被占用
//需要设置setsockopt REUSEADDR 来重用TIME_WAIT连接
close(new_sock);
}
void* ThreadEntry(void* arg){
//线程入口函数,负责这一次请求的完整性
int new_sock = (int64_t)arg;
HandlerRequest(new_sock);
return NULL;
}
void HttpServerStart(const char* ip, short port){
// 1.创建 tcp socket
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0){
perror("socket\n");
return;
}
//设置 REUSEADDR,处理后面短连接主动关闭 socket问题
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2.绑定端口号
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\n");
return;
}
// 3.监听socket
ret = listen(listen_sock, 10);
if (ret<0){
perror("listen\n");
return;
}
printf("Server start OK!\n");
// 4.进入循环,处理客户端的连接
while (1){
sockaddr_in peer;
socklen_t len = sizeof(peer);
int64_t new_sock = accept(listen_sock, (sockaddr*)&peer, &len);
if (new_sock < 0){
perror("accept\n");
continue;
}
//使用多线程的方式来完成多个连接的并行处理
pthread_t tid;
pthread_create(&tid, NULL, ThreadEntry, (void*)new_sock);
pthread_detach(tid);
}
}
int main(int argc, char* argv[]){
if (argc != 3){
printf("Usage ./http_server [ip] [port]\n");
return 1;
}
singal(SIGPIPE,SIG_IGN);
HttpServerStart(argv[1], atoi(argv[2]));
return 0;
}
CGI
这里写代码片