Thinhttpd的流程大致可以分为启动服务器进行监听端口,接受请求,处理请求,最后返回结果。
先要为服务器设置socket,然后建立服务器socket地址(内含端口号),端口号是由用户在GUI处传入。
int httpd = 0; //套接字 int client_sock = 0; //客户套接字 struct sockaddr_in c_sockaddr; //客户套接字地址 u_short port = 8082; //端口号
启动连接,监听端口
httpd = create_connect(&port); //建立连接建立连接的内部实现 create_connect(u_short* port)
int ss = socket(AF_INET, SOCK_STREAM, 0); //建立套接字 struct sockaddr_in s_sockaddr;
bind(ss, (struct sockaddr* )&s_sockaddr, sizeof(s_sockaddr);
listen(ss, QUEUE);当接受到请求,我们会为每个客户生成唯一的socket,防止信息误发。
client_sock = accept(httpd, (struct sockaddr*)&c_sockaddr, &c_sockaddr_len);
我们运用了多线程来处理请求,每当请求到来就会生成新线程处理直至完成,其中系统还会继续接受新的请求。
int pid = fork(); if (pid == 0) { accept_request(client_sock); exit(0); }
好处是可以并发地发送客户访问的内容。
例如用户访问的html里有多张图片,不用并发的话,将会是先获得一张图片的请求,然后阻塞到这张图片发送完毕再接受下一张图片,这样的利用效率太低而且客户会等待很久。
而并发,服务器会同时接受多个图片请求,并一同处理并返回。
处理请求前先要获取请求头,用 '\r\n' 进行分割每一行,把整个请求头都拿出来,请求体会先放着以待CGI判断。
void getRequest(int client, string& buff) { char linebuf[1024]; int numline = recvline(client, linebuf, sizeof(linebuf)); while(numline>2) { string str(linebuf); buff += str; numline = recvline(client, linebuf, sizeof(linebuf)); } }
其中recvline就是取请求的每一行,当遇到空行就会停止,也就是把请求头和体分割。
HttpRequest hr(buff); //取请求方法 method = hr.getMethod(); //取请求uri uri = hr.getUri(); requesturi = hr.getUri(); //取请求版本 version = hr.getVersion(); //取各个请求头部 map<string, vector<string>> headMap = hr.getHeaders();
除了GET方法都要运行cgi
if(method != "GET") cgi = 1;
如果GET后带有请求参数,也会调用cgi
if(hr.hasSource()) { cgi = 1; }
web服务的根目录会由GUI传入,例如:
path = "htdocs";
//拼接路径
path += uri;如果访问的是目录,则会在后面添加index.html
path += idx; //idx = "index.html"如果文件不存在,直接生成Response的对象进行返回404页面
(Response是专门用来返回结果给用户的类)stat是用来获取指定路径的文件或者文件夹的信息
if(stat(p, &st) == -1) { Response response(client, "404"); response.sendHttpHead(); //关闭连接 close(client); }
目前我们只支持php、py、jar这类文件进行调用cgi
string file_type = hr.getSource(); if((file_type == "php") || (file_type == "py") || (file_type == "jar")) cgi = 1;
file_type是由HttpRequest类的方法取得,后面也可以拿来制作返回的content-type
if(!cgi) { cat(client, p, file_type); }
我们也是把GET和非GET请求分开
先要生成CGI对象
CGI *u_cgi = new CGI(path,requesturi,query_string); 构造函数所需参数是
// 具体的路径,一般是 "./" + 根目录 + 请求时的URI(并去掉?后面的内容) string path = "./htdocs/tz.php"; // 请求的 request URI string requestUri = "/tz.php?id=1"; // queryString 请求参数 ?之后 string query_string = "id=1";
u_cgi->run();先把请求体长度content-length取出
因为有些请求带的Content-Length是大写的,搞得我们测试时总会一直在加载,所以我们分了两种情况
map<string,vector<string>>::iterator iter; iter = headMap.find("Content-Length"); // not found Content-Length but have content-length if(iter == headMap.end()) { iter = headMap.find("content-length"); } body_length = stoi((iter->second)[0], nullptr);
请求头长度拿到,我们就可以开始取body,调用getBody函数,会按长度来取body内容
u_cgi->run(method, body_content, body_length); //请求方法、请求体内容、请求体长度
404在前面已经直接返回了
cat函数会把文件的长度读取出来,制作content-length并作为读取结束标志,还会生成Response对象进行返回
fseek(resource,0,SEEK_END); long n = ftell(resource); //获取文件长度 fseek(resource,0,SEEK_SET); Response response(client, state_code); response.sendHttpHead(); //发送response头 response.sendContext(resource, n, file_type); //发送文件内容
在发送文件内容时,我们遇到了很多bug,最终选择了按字符来读取文件内容,并组合成一个4096的字符串进行分包发送
while(length>0) { int i = 0; for(i=0;i<4096 && length>0;i++) { buf[i] = fgetc(file); length--; } send(client,buf,(i),0); }
调用运行cgi后,cgi类会保存执行状态和执行结果
我们先判断执行状态码,
会像404一样直接生成Response类进行返回错误页面。
else if(u_cgi->getStatusCode() == 500) { Response response(client, "500"); response.sendHttpHead(); }
由于cgi返回的结果并不单单是返回内容,还有content-type 和 set-cookies的头部信息,
我们就要先把返回的结果进行进一步处理
先取出body,并且求出body长度,中间同样利用'\r\n'进行分割
string send_str = u_cgi->getOutput(); string::size_type pos = send_str.find_last_of("\r\n"); string res = ""; if(pos != string::npos){ res = send_str.substr(pos+1); } long str_len = res.length(); Response response(client, "200"); response.sendHttpHead(); response.sendString(send_str, str_len);
生成Response类,返回200状态码
利用body长度生成content-length
最后按照顺序返回,这时我们的返回的结果是整个包发送
void Response::sendString(string msg, long body_Length)//返回字符串内容(内部包含部分属性信息) { char *buf = new char[msg.length()]; msgSend(client,buf,"Connection: close\r\n"); sprintf(buf,"Content-Length: %ld\r\n",body_Length); send(client,buf,strlen(buf),0); msgSend(client,buf,msg); }
-
CGI 需要上下文环境,因此接收到 HTTP 请求后,分析出其请求的脚本文件的具体位置,以及处理方式。
-
Linux 下"线程"概念与 Windows 有别,一般把所指向同一位置、使用同样资源与上下文的"线程"称为"线程"
-
在接收到请求时,可以使用Linux的fork()调起另一进程,通过设置环境变量的方式让 CGI 解译程序得以启动,并领用 dup2() 修改进程文件资源表,将子程序调用CGI解译进程的 stdout 重定向到一个匿名管道(pipe)内用于构造 HTTP 响应
-
C语言的 popen 管道为单向管道,只能读或者写。在本例中可以用于GET请求的处理,却无法处理 POST/PUT/PATCH等方式
-
C语言/C++的putenv()函数只会记录最后一组环境变量,使用 setenv() 则可以记录多组(不排除是编译器问题)
-
采用C++的exce函数家族运行外部程序时,第二个参数不得不传递,按照所查找到的资料参数组若无,则可以直接在第二个参数使用 null。导致了本例中 PHP CGI 运行时,往进程启动参数中加多了一次 PHP-CGI (按照老师所说,不行就不钻牛角尖,绕过去!~)
-
Linux PIPE 匿名管道只能传递 64K 的内容(本例实测是918字节) (目前这个暂时没有好的解决方法,和老师讨论无果后也只能绕过去)