HTTP项目


#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


这里写代码片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值