http伺服器小專案
阿新 • • 發佈:2019-01-05
1. http專案整體框架
1.1http協議格式
http請求由三部分組成,分別是:起始行、訊息報頭、請求正文
Request Line<CRLF>
Header-Name: header-value<CRLF>
Header-Name: header-value<CRLF>
//一個或多個,均以<CRLF>結尾
<CRLF>
body//請求正文
- 起始行以一個方法符號開頭,以空格分開,後面跟著請求的URI和協議的版本,格式如下:
Method Request-URI HTTP-Version CRLF
其中 Method表示請求方法;Request-URI是一個統一資源識別符號;HTTP-Version表示請求的HTTP協議版本;CRLF表示回車和換行(除了作為結尾的CRLF外,不允許出現單獨的CR或LF字元)。
請求方法(所有方法全為大寫)有多種,各個方法的解釋如下:
GET 請求獲取Request-URI所標識的資源 POST 在Request-URI所標識的資源後附加新的資料 HEAD 請求獲取由Request-URI所標識的資源的響應訊息報頭 PUT 請求伺服器儲存一個資源,並用Request-URI作為其標識 DELETE 請求伺服器刪除Request-URI所標識的資源 TRACE 請求伺服器回送收到的請求資訊,主要用於測試或診斷 CONNECT 保留將來使用 OPTIONS 請求查詢伺服器的效能,或者查詢與資源相關的選項和需求 應用舉例: GET方法:在瀏覽器的位址列中輸入網址的方式訪問網頁時,瀏覽器採用GET方法向伺服器獲取資源,eg: GET /form.html HTTP/1.1
1.2整體流程
伺服器啟動,等待客戶端請求到來;
客戶端請求到來,建立新執行緒處理該請求;
讀取httpHeader中的method,擷取url,其中GET方法需要記錄url問號之後的引數串;
根據url構造完整路徑,如果是/結尾,則指定為該目錄下的index.html;
獲取檔案資訊,如果找不到檔案,返回404,找到檔案則判斷檔案許可權;
如果是GET請求並且沒有引數,或者檔案不可執行,則直接將檔案內容構造http資訊返回給客戶端;
如果是GET帶引數,POST,檔案可執行,則執行CGI;
GET請求略過httpHeader,POST方法需要記錄httpHeader中的Content-Length:xx;
建立管道用於父子程序通訊,fork產生子程序;
子程序設定環境變數,將標準輸入和輸出與管道相連,並且通過exec執行CGI;
如果是POST,父程序將讀到post內容傳送給子程序,並且接收子程序的輸出,輸出給客戶端
2. 部分專案原始碼
#include"thttpd.c"
static void* msg_request(void *arg)
{
int sock=(int)arg;
pthread_detach(pthread_self());
return (void*)handler_msg(sock);
}
int main(int argc,char* argv[])
{
//daemon(1,0);
if(argc!=3)
{
printf_log("Usage: [local_ip] [local_port]\n",FATAL);
return 1;
}
int lis_sock=startup(argv[1],atoi(argv[2]));
while(1)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(lis_sock,(struct sockaddr*)&peer,&len);
if(sock<0)
{
printf_log("accept failed",WRONING);
continue;
}
pthread_t tid;
if(pthread_create(&tid,NULL,msg_request,(void*)sock)>0)
{
printf_log("pthread_create failed",WRONING);
close(sock);
}
}
return 0;
}
thttpd.c
#include"thttpd.h"
//mysql 關係型資料庫
int startup(const char* _ip,int _port) //建立監聽套接字
{
assert(_ip);
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
printf_log("socket failed",FATAL);
exit(2);
}
int opt=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
printf_log("bind failed",FATAL);
exit(3);
}
if(listen(sock,5)<0)
{
printf_log("listen failed",FATAL);
exit(4);
}
return sock;
}
void printf_log(const char* msg,int level) //記錄日誌
{
openlog("thttpd",LOG_PID|LOG_CONS|LOG_NDELAY,LOG_USER);
switch(level)
{
case NORMAL:
syslog(LOG_NOTICE,msg);
break;
case WRONING:
syslog(LOG_WARNING,msg);
break;
case FATAL:
syslog(LOG_ERR,msg);
break;
}
closelog();
}
static int get_line(int sock,char* buf) //按行讀取請求報頭
{
char ch='\0';
int i=0;
ssize_t ret=0;
while(i<SIZE && ch!='\n')
{
ret=recv(sock,&ch,1,0);
if(ret>0&&ch=='\r')
{
ssize_t s=recv(sock,&ch,1,MSG_PEEK);
if(s>0&&ch=='\n')
{
recv(sock,&ch,1,0);
}
else
{
ch='\n';
}
}
buf[i++]=ch;
}
buf[i]='\0';
return i;
}
static void clear_header(int sock) //清空訊息報頭
{
char buf[SIZE];
int ret=0;
do
{
ret=get_line(sock,buf);
}while(ret!=1&&(strcmp(buf,"\n")!=0));
}
static void show_404(int sock) //404錯誤處理
{
clear_header(sock);
char* msg="HTTP/1.0 404 Not Found\r\n";
send(sock,msg,strlen(msg),0); //傳送狀態行
send(sock,"\r\n",strlen("\r\n"),0); //傳送空行
struct stat st;
stat("wwwroot/404.html",&st);
int fd=open("wwwroot/404.html",O_RDONLY);
sendfile(sock,fd,NULL,st.st_size);
close(fd);
}
void echo_error(int sock,int err_code) //錯誤處理
{
switch(err_code)
{
case 403:
break;
case 404:
show_404(sock);
break;
case 405:
break;
case 500:
break;
defaut:
break;
}
}
static int echo_www(int sock,const char * path,size_t s) //處理非CGI的請求
{
int fd=open(path,O_RDONLY);
if(fd<0)
{
echo_error(sock,403);
return 7;
}
char* msg="HTTP/1.0 200 OK\r\n";
send(sock,msg,strlen(msg),0); //傳送狀態行
send(sock,"\r\n",strlen("\r\n"),0); //傳送空行
if(sendfile(sock,fd,NULL,s)<0)
{
echo_error(sock,500);
return 8;
}
close(fd);
return 0;
}
static int excu_cgi(int sock,const char* method,\
const char* path,const char* query_string) //處理CGI模式的請求
{
char line[SIZE];
int ret=0;
int content_len=-1;
if(strcasecmp(method,"GET")==0) //如果是GET方法的CGI
{
//清空訊息報頭
clear_header(sock);
}
else //POST方法的CGI
{
//獲取post方法的引數大小
do
{
ret=get_line(sock,line);
if(strncmp(line,"Content-Length: ",16)==0) //post的訊息體記錄正文長度的欄位
{
content_len=atoi(line+16); //求出正文的長度
}
}while(ret!=1&&(strcmp(line,"\n")!=0));
}
//因為環境變數有上限,而GET方法傳遞的引數少,所以用環境變數
//POST方法,在正文內傳參,引數多,所以用管道
//將method、query_string、content_len匯出為環境變數
char method_env[SIZE/8];
char query_string_env[SIZE/8];
char content_len_env[SIZE/8];
sprintf(method_env,"METHOD=%s",method);
//把是什麼方法放到環境變數裡,方便子程序執行的cGI判斷是什麼方法。
putenv(method_env);
sprintf(query_string_env,"QUERY_STRING=%s",query_string);
putenv(query_string_env);
sprintf(content_len_env,"CONTENT_LEN=%d",content_len);
putenv(content_len_env);
int input[2]; //站在CGI程式的角度,建立兩個管道
int output[2];
pipe(input);
pipe(output);
pid_t id=fork();
if(id<0)
{
printf_log("fork failed",FATAL);
echo_error(sock,500);
return 9;
}
else if(id==0) //子程序
{
close(input[1]); //關閉input的寫端
close(output[0]); //關閉output的讀端
//將檔案描述符重定向到標準輸入標準輸出
dup2(input[0],0);
dup2(output[1],1);
execl(path,path,NULL); //替換CGI程式
exit(1);
}
else //father
{
close(input[0]); //關閉input的讀端
close(output[1]); //關閉output的寫端
char ch='\0';
if(strcasecmp(method,"POST")==0) //如果是post方法,則父程序需要給子程序傳送引數
{
int i=0;
for( i=0;i<content_len;i++)
{
recv(sock,&ch,1,0); //從sock裡面一次讀一個字元,總共讀conten_len個字元
write(input[1],&ch,1);
}
}
// char* msg="HTTP/1.0 200 OK\r\n\r\n";
// send(sock,msg,strlen(msg),0);
// clear_header(sock);
char* msg="HTTP/1.0 200 OK\r\n";
send(sock,msg,strlen(msg),0); //傳送狀態行
send(sock,"\r\n",strlen("\r\n"),0); //傳送空行
//接收子程序的返回結果
while(read(output[0],&ch,1)) //如果CGI程式結束,寫端關閉,則讀端返回0
{
send(sock,&ch,1,0);
// printf("%c",ch);
}
waitpid(id,NULL,0); //回收子程序
}
return 0;
}
int handler_msg(int sock) //瀏覽器請求處理函式
{
char buf[SIZE];
int count=get_line(sock,buf);
int ret=0;
char method[32];
char url[SIZE];
char *query_string=NULL;
int i=0;
int j=0;
int cgi=0;
//獲取請求方法和請求路徑
while(j<count)
{
if(isspace(buf[j]))
{
break;
}
method[i]=buf[j];
i++;
j++;
}
method[i]='\0';
while(isspace(buf[j])&&j<SIZE) //過濾空格
{
j++;
}
//如果是POST方法一定要用到cgi
//如果是GET方法帶引數也要用到cgi
if(strcasecmp(method,"POST")&&strcasecmp(method,"GET"))
{
printf_log("method failed",FATAL);
echo_error(sock,405);
ret=5;
goto end;
}
if(strcasecmp(method,"POST")==0)
{
cgi=1;
}
i=0;
while(j<count)
{
if(isspace(buf[j]))
{
break;
}
if(buf[j]=='?')
{
query_string=&url[i];
query_string++;
url[i]='\0';
}
else
url[i]=buf[j];
j++;
i++;
}
url[i]='\0';
if(strcasecmp(method,"GET")==0&&query_string!=NULL)
{
cgi=1;
}
//找到請求的資源路徑
char path[SIZE];
sprintf(path,"wwwroot%s",url);
struct stat st;
if(stat(path,&st)<0) //獲取客戶端請求的資源的相關屬性
{
printf_log("stat path faile\n",FATAL);
echo_error(sock,404);
ret=6;
goto end;
}
else
{
printf("S_ISDIR::%d\n",S_ISDIR(st.st_mode));
if(S_ISDIR(st.st_mode))
{
strcat(path,"/index.html");
}
else if(st.st_mode & (S_IXUSR | S_IXOTH | S_IXGRP))
{
cgi=1;
}
}
printf("cgi=%d\n",cgi);
if(cgi) //請求的是可執行程式或者帶引數用cgi程式處理
{
ret=excu_cgi(sock,method,path,query_string); //處理CGI模式的請求
}
else
{
clear_header(sock);
ret=echo_www(sock,path,st.st_size); //如果是GET方法,而且沒有引數,請求的也不是可執行程式,則直接返回資源
}
end:
close(sock);
return ret;
}