HTTP 是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。
实现一个服务器程序,支持HTTP协议的服务器,浏览器进行访问
从请求角度上,支持GET和POST方法;从相应角度上,支持静态页面也支持动态页面
静态页面:GET方法且query_string为空。
根据url_path获取到文件的真实路径,打开文件,根据文件内容构造HTTP响应报文。首行+header+空行+文件内容
此处使用sendfile,更高效的把文件中的内容传回给客户端。
动态页面:基于CGI,创建一对匿名管道,fork出子进程。
父进程流程:
a)如果方法是POST,就把body中的数据写到管道中
b) 父进程构造HTTP响应报文。
c) 父进程尝试从管道中读取子进程的计算结果作为HTTP相应中的body
d)父进程进行进程等待。waitpid。。。现在有多个进程,每个进程又创建了子进程,waitpid明确等待回收自己创建的进程
(1)GET方法且query_string为空
(2)POST方法。
子进程流程:a)设置环境变量。REQUENCE_METHOD 、QUERY_STRING 、CONTENT_LENGTH
b)重定向。标准输入输出重定向到管道上
c)根据url_path获取到CGI程序真实路径
d)进程程序替换
e)错误处理
CGI:获取到请求的参数,对参数进行处理
a)从环境变量之中获取到方法。如果是GET方法,从环境变量中获取到query_string。如果是POST方法,需要从环境变量中获取到content_length,再从标准输入中读入body数据
b)从参数中解析出核心数据
c)根据输入参数进行计算,和具体的业务相关
d)根据计算结果生成动态页面
HTTP协议的主要特点可概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
工作流程
(1) 服务器启动,在指定端口或随机选取端口绑定 httpd 服务。
(2)收到一个 HTTP 请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数。
(3)取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。
(4) 格式化 url 到 path 数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页。
(5)如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到(10)。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本。
(6)读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200 状态码写到套接字。
(7) 建立两个管道,cgi_input 和 cgi_output, 并 fork 一个进程。
(8) 在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序。
(9) 在父进程中,关闭 cgi_input 的读取端 和 cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。
(10) 关闭与浏览器的连接,完成了一次 HTTP 请求与回应,因为 HTTP 是无连接的。
关键函数的作用:
ReadLine:按行读取数据, 把回车换行等情况都统一为换行符结束
如果当前字符是\r,读取下一个字符,如果下一个字符是\n,即\r\n的组合,就将它们变为\n
如果下一个字符不是\n,即只有一个\r,那么把它变成\n
如果当前字符是\n,退出函数
如果以上都不是,那就把该字符放入buf中
int ReadLine(int sock,char buf[],size_t max_size){
char c='\0';
ssize_t i=0;
while(i < max_size){
ssize_t read_size = recv(sock,&c,1,0);
if(read_size <= 0){
return -1;
}
if(c=='\r'){
recv(sock,&c,1,MSG_PEEK);
if(c == '\n'){
recv(sock,&c,1,0);
}else{
c = '\n';
}
}
buf[i++] = c;
if(c=='\n'){
break;
}
}
buf[i] = '\0';
return i;
}
HandlerCGI:
创建一对匿名管道,
int HandlerCGI(int new_sock,const HttpRequest* req)
{
int err_code = 200;
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];
pid_t ret = fork();
if(ret > 0){ /father
close(child_read);
close(child_write);
HandlerCGIFather(new_sock,ret,father_read,father_write,req);
}else if(ret ==0){ //child
close(father_read);
close(father_write);
HandlerCGIChild(child_read,child_write,req);
}else{
perror("fork");
err_code = 404;
goto END;
}
END:
if(err_code != 200){
printf("Handler 404\n");
Handler404(new_sock);
}
}
HandlerCGIFather:
a)对于post方法,把body中的数据写到管道中
从socket中读出数据,写到管道中,此处无法用sendfile,因为sendfile只能把数据写到socket之中,直接一个字节一个字节的从socket中读出来,再写到管道中去
b)父进程需要构建一个完整的HTTP协议数据。
对于HTTP协议要求我们按照指定的格式返回数据。CGI程序返回的结果只是body部分。 header,首行等部分需要父进程自己构造
c)从管道中尝试读数据,写回到socket中。判断语句中为0表示读到了 文件结束标志EOF。子进程结束(子进程停止write),父进程就会读到文件尾
father_read对应的是child_write,对于父进程来说,child_write已经关闭了;对于子进程来说,如果CGI程序处理完进程也就退出了。
进程退出的同时会关闭child_write,此时就意味着管道的所有写端都关闭,再尝试读,read返回0
d)进程等待,否则会造成僵尸进程
不能是wait,因为服务器会给每个客户端都创建一个线程,每个线程又有可能创建子进程,此时如果用wait,任何一个子进程结束都可能导致 wait返回,这样的话子进程就不是对应的线程来回收
void HandlerCGIFather(int new_sock,int child_pid,int father_read,\
int father_write,const HttpRequest* req)
{
// a
char c = '\0';
if(strcasecmp(req->method,"POST")==0){
//read data from socket,write in pipe
//can't use sendfile,because sendfile only write data to socket
//one byte,one byte read from socket and write to pipe
ssize_t i=0;
for(;i<req->content_length;++i){
read(new_sock,&c,1);
write(father_write,&c,1);
}
}
//b
const char* first_line = "HTTP/1.1 200 OK\n" ;
const char* blank_line = "\n";
send(new_sock,first_line,strlen(first_line),0);
send(new_sock,blank_line,strlen(blank_line),0);
//c
while(read(father_read,&c,1)>0){
write(new_sock,&c,1);
}
//d
waitpid(child_pid,NULL,0);
}
HandlerCGIChild:
a)exec替换后,子进程里的内容都没有了,创建环境变量。保存CGI想要传递下去的信息。
REQUEST_METHODD , QUERY_STRING , CONTENT_LENGTH
字符串拼接结果相当于是REQUEST_METHOD = GET
b)把子进程的标准输入和标准输出重定向到管道。目的是读取父进程交给的数据,再写回到父进程的管道中
c)进程的程序替换,通过url_path拼装成一个完整的路径,把对应的文件进行替换
之所以采用execl,原因一:此处CGI程序不需要指定命令行参数,l/v不影响
原因二:此处CGI程序对应的全路径中通过putenv的方式 进行设置 的
原因三:此处CGI程序环境变量在子进程中通过putenv的方式进行设置的,如果使用execle的话,就不必使用putenv了,此处的两种方式均可
d)exec执行失败,需要进行错误处理。
如果此处不进行退出,就会出现子进程和父进程监听相同的端口号情况,而此时我们只是希望子进程去调用 CGI程序处理客户端链接这样的事情只应该由父进程来完成
execl之后就把 动态页面生成的逻辑交给了CGI程序
void HandlerCGIChild(int child_read,int child_write,const HttpRequest* req)
{
//a
char method_env[SIZE] = {0};
sprintf(method_env,"REQUEST_METHOD=%s",req->method);
putenv(method_env);
if(strcasecmp(req->method,"GET") == 0){
//set QUERY_STRING
char query_string_env[SIZE] = {0};
sprintf(query_string_env,"QUERY_STRING=%s",req->query_string);
putenv(query_string_env);
}else{
//set CONTENT_LENGTH
char content_length_env[SIZE] = {0};
sprintf(content_length_env,"CONTENT_LENGTH=%d",req->content_length);
putenv(content_length_env);
}
//b
dup2(child_read,0);
dup2(child_write,1);
//c
char file_path[SIZE] = {0};
HanderFilePath(req->url_path,file_path);
execl(file_path,file_path,NULL);
//d
exit(0);
}
HandlerRequest:
1.读取并分析请求
a)从socket中取出HTTP,读取首行
b)分析首行,获取method和url
c)读取并分析头部,只关注content_length
2.通过请求判断是动态还是静态页面
a)GET方法 且 没有 query_string--------静态页面
b)GET方法 且 有 query_string--------动态页面
c)POST---------动态页面
void HandlerRequest(int new_sock)
{
int err_code = 200;
HttpRequest req;
memset(&req,0,sizeof(req));
if(ReadLine(new_sock,req.first_line,sizeof(req.first_line))<0){
printf("ReadLine first_line failed\n");
err_code = 404;
goto END;
}
printf("first_line=%s\n",req.first_line);
if(ParseFirstLine(req.first_line,&req.method,&req.url)<0){
printf("ParseFirstLine failed!first line=%s\n",req.first_line);
err_code = 404;
goto END;
}
if(ParseQueryString(req.url,&req.url_path,&req.query_string)<0){
printf("ParseQueryString failed!url=%s\n",req.url);
err_code = 404;
goto END;
}
if(HandlerHeader(new_sock,&req.content_length)<0){
printf("HandlerHeader failed!");
err_code = 404;
goto END;
}
if(strcmp(req.method,"GET")==0 && req.query_string==NULL){
HandlerStaticFile(new_sock,&req);
}else if(strcmp(req.method,"GET")==0 && req.query_string != NULL){
HandlerCGI(new_sock,&req);
}else if(strcmp(req.method,"POST")==0){
HandlerCGI(new_sock,&req);
}else{
printf("method not supposed!method=POST or GET\n");
err_code = 404;
goto END;
}
END:
if(err_code != 200){
printf("Handler 404\n");
Handler404(new_sock);
}
close(new_sock);
}
HttpServerStart:
1.socket
2.bind
3.listen
4. 进入循环接收客户端请求
5. 创建线程处理更多连接
void HttpServerStart(const char* ip,short port)
{
int listen_socket = socket(AF_INET,SOCK_STREAM,0);
if(listen_socket < 0){
perror("socket");
return;
}
int opt=1;
setsockopt(listen_socket,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_socket,(sockaddr*)&addr,sizeof(addr));
if(ret < 0){
perror("bind");
return;
}
ret = listen(listen_socket,5);
if(ret < 0){
perror("listen");
return;
}
printf("HttpServerStart OK\n");
while(1){
sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_socket,(sockaddr*)&peer,&len);
if(new_sock < 0){
perror("accept");
continue;
}
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,(void*)new_sock);
pthread_detach(tid);
}
}
服务器 性能:
http_load测QPS
进程池-----FastCGI
计算业务-------多线程进行计算