通用的HTTP服務框架
未完成測試,註釋多,供參考
//該檔案包含了整個HTTP伺服器的實現
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>
typedef struct sockaddr sockaddr;
typedef struct socket_in socket_in;
//定義一個結構體來表示緩衝區
#define SIZE (1024*4)
typedef struct HttpRequest{
char* first_line;
char* method;
char* url;
char* url_path;
char* query_string
int content_length;
}HttpRequest;
//從socket中讀取一行資料
//HTTP請求中換行符\n\r \n\r都可以相容處理
//核心思路:將未知問題轉換為已知問題.將\r\n 都轉化為\n
int ReadLine(int sock, char output[],ssize_t max_size)
{
//1 一個字元一個字元的從socket中讀取資料
char c = '\0';
ssize_t i = 0;//記錄了output緩衝區中當前已經寫了多少個字元
while (i < max_size)
{
ssize_t read_size = recv(sock, &c, 1, 0);
if (read_size < 0)
{
//此處希望讀到完整一行,如果還沒讀到換行就讀到EOF就認為出錯
return -1;
}
//2 判斷當前字元是不是\r
if (c == '\r')
{
recv(sock, &c, 1, MSG_PEEK);//MSG_PEEK提前看緩衝區的內容,但是不讀取
//3 如果當前字元是\r,嘗試讀取下一個字元
// a)如果下一個字元是\n
if (c == '\n')
{
recv(sock, &c, 1, 0);
}//讀取字元將\r\n轉化為\n
// b)如果下一個字元不是\n
else
{
c = '\n';//將\r轉化為\n
}
//這兩種情況都把\r轉化成\n
}
//此時所有型別的分割符都被轉化為\n
//4 如果當前字元是\n,已將這行讀完,結束函式
if (c == '\n')
{
break;
}
//5 如果當前字元是一個普通字元,直接追加到輸出結果中
output[i++] = c;
}
output[i] = '\0';
return i;
}
//解析首行,獲取到其中的method和url:字串切分ftok
//首行格式:GET /index.html?a=10&b=20 HTTP/1.1
int ParseFirstLine(char first_line[], char** p_method, char** p_url)
{
char* tok[10] = { 0 };
//使用Split函式對字串進行切分,n表示切分結果有幾個部分
int n=Split(first_line, " ",tok);
if (n != 3)
{
//如果不是3,不符合HTTP協議
printf("Split failed!n=%d\n");
return -1;
}
//此處可以進行更加複雜的校驗
*p_method = tok[0];
*p_url = tok[1];
return 0;
}
//進行字串切分
//strtok內部使用static變數來儲存字串切分的情況,如果有大量的客戶端去建立程序,就會
//使static變為共享資源,執行緒不安全
//使用strtok_r是執行緒安全版本,內部沒有靜態變數,需要使用者在棧上手動定義變數作為緩衝區去儲存.
int Split(char input[], const char*split_char, char* output[])
{
char* tmp = NULL;//如果在tmp前面加static,那麼執行緒就會共用tmp,就不會執行緒安全
int output_index = 0;
char* p = strtok_r(input,split_char,&tmp);
while (p != NULL)
{
output[output_index++] = p;
p = strtok_r(NULL, split_char,&tmp);
}
return output_index;
}
//url形如: /index.html?a=10&b=20
// http:/.www.baidu.com/index.html?a=10&b=20(不考慮)
int ParseUrl(char url[], char** p_url_path, char** p_query_string)
{
*p_url_path = url;
//查詢問號所在位置
char* p = url;
for (; *p != '\0'; ++p)
{
if (*p == '?')
{
//找到了?,將之替換成\0
*p = '\0';
*p_query_string = p + 1;
return 0;
}
}
*p_query_string=NULL;//沒有找到
return -1;
}
int ParseHeader(int new_sock, int* content_length)
{
char buf[SIZE] = { 0 };
while (1)
{
ssize_t read_size = ReadLine(new_sock, buf, sizeof(buf)-1);
if (raed_size <= 0)
{
return -1;
}
if (strcmp(buf, "\n") == 0)
{
//讀到空行
return 0;
}
//Content_length:100\n
const char* key = "Conten - Length: ";
if (strncmp(buf, key, strlen(key)) == 0)
{
*content_length = atoi(buf + strlen(key));
//break;使用break會出現粘包問題
}
}
return 0;
}
void Handler404(int new_sock)
{
const char* first_line = "HTTP/1.1 404 Not Found!\n";
//此處可以不加header
//Content-Type可以讓瀏覽器自動識別
//Content-Length可以通過關閉socket的 方式告知瀏覽器已經讀完
//body部分是 html的頁面
const char* body = "<head><meta http-equiv=\"Content - Type\" "
"content=\"text / html; charset = utf - 8\">"
"</head><h1>404!!!頁面被吃了</h1>";
}
int IsDir(const char* file_path)
{
struct stat st;
int ret = stat(file_path, &st);
if (ret < 0)
{
//此處不是目錄
return 0;
}
if (S_ISDIR(st.st_mode))
{
return 1;
}
return 0;
}
void HandlerFilePath(const char* url_path, char file_path[])
{
//url_path是以/開頭的,所以不需要wwwroot之後顯示指明/
sprintf(file_path, "./wwwroot%s", url_path);
//如果url_path指向目錄,就在目錄後面拼裝index.html作為預設訪問的檔案
//識別url_path指向的是普通檔案還是目錄
// a)url_path以/結尾,例如:/image/,一定是目錄
if (file_path[strlen(file_path) - 1] == '/')
{
strcat(file_path, "index.html");
}
else
{
// b)url_path沒有以/ 結尾,此時需要根據檔案屬性來判定是否是目錄:stat函式獲取檔案屬性
if (IsDir(file_path))
{
strcat(file_path, "./index.html");
}
}
}
ssize_t GetFileSize(const char* file_path)//int=2G,太小
{
struct stat st;
int ret = stat(file(file_path, &st));
if (ret < 0)
{
return 0;
}
return st.st_size;
}
int WriteStaticFile(int new_sock, const char* file_path)
{
//1.開啟檔案,如果開啟失敗,就返回404
int fd = open(file_path,O_RDONLY)
if (fd < 0)
{
perror("open");
return 404;
}
//2.構造HTTP響應報文
const char* first_line = "HTTP/1.1 200 OK\n";
send(new_sock, first_line, strlen(first_line), 0);
//此處如果更嚴謹,就需要加一些header
//因為瀏覽器能夠自動識別Content-Type,就沒寫
//沒寫conten_length是因為後面立刻關閉了socket
//瀏覽器能識別資料在哪裡結束
const char* blank_line = '\n';
send(new_sock, first_line, strlen(blank_line), 0);
//3.讀檔案並且寫入socket中
//更高效:sendfile把一個檔案中的資料讀出來,寫到另一箇中.可以從中間開始
/*char c = '\0';
while (read(new_sock, &c, 1) > 0)
{
send(new_sock, &c, 1, 0);
}*/
ssize_t file_size = GetFileSize(file_path);
sendfile(new_sock, fd, NULL, file_size);
//4.關閉檔案
colse(fd);
return 200;
}
int HandlerStaticFile()
{
//1.根據url_path獲取到檔案的真實目錄
//例如,hTTP伺服器根目錄叫 ./wwwroot
//此時有一個檔案叫做 ./wwwroot/image/101.jpg
//在url中寫一個path就叫做 /image/101.jpg
char file_path[SIZE] = { 0 };
//根據下面的函式將 /image/101.jpg轉化為
//磁碟上的 ./wwwroot/image/101.jpg
HandlerFilePath(req->url_path, file_path);
//2.開啟檔案,讀取檔案內容,把檔案內容寫到socket中
int err_code = WriteStaticFile(new_sock, file_path);
return err_code;
}
int HandlerCGIFather(int new_sock,int father_read,int father_write,const HttpRequest* req)
{
// a)如果是POST請求,把body部分的資料讀出來寫到管道里,
//剩下的動態生成頁面的過程都交給子程序來完成
if (strcasecmp(req->method, "POST"))
{
//根據body的長度決定讀取多少個位元組
char c = '\0';
int i = 0;
//使用迴圈防止 read被打斷,不加迴圈即使緩衝區夠長,
for (; i < req->content_length; ++i)
{
read(new_sock, &c, 1);
write(father_write, &c, 1);
}
}
// b)構造HTTP響應
const char* first_line = "HTTP/1.1 200 OK\n";
send(new_sock, blank_line, strlen(blank_line),0);
// c)從管道中讀取資料(子程序動態生成的頁面),把這個頁面也寫到socket當中,
//此處不方便用sendfile,,主要是資料的長度不容易確定
char c = '\0';
while (raed(father_read, &c, 1) > 0)
{
write(new_sock, &c, 1);
}
// d)程序等待,回收子程序的資源.
//此處如果要程序等待,最好使用waitpid,保證當前回收的子程序就是當年
waitpid(NULL);
return 200;
}
int HandlerCGIChild(int child_read,int child_write,int new_sock, int father_read, int father_write, const HttpRequest* req)
{
//注意:環境變數寫在父程序中,雖然子程序能夠繼承父程序的環境變數,由於同一時刻會有很多個請求,每個請求
//都在請求修改環境變數,會產生類似於執行緒安全的問題.導致子程序不能正確的獲取到這些資訊
// a)設定環境變數(METHOD,CONTENT_LENGTH,QUERY_STRING)
// 如果把上面的這幾個資訊通過管道來告知替換之後的程式,把這個資料也寫到socket當中也是可行的,但是此處要遵守CGI標準,
//所以必須使用環境變數傳遞以上資訊.
char method_env[SIZE] = { 0 };
//REQUERY_METHOD=GET
sprintf(method_env, "REQUEST_METHOD=%s", req->method);
putenv(method_env);
if (strcasecmp(req->method,"GET")==0)
{//設定REQUERY_STRING
char query_string_env[SIZE] = { 0 };
sprintf(method_env, "QUERY_STRING=%s", req->query_string);
putenv(query_string_env);
}
else
{
//設定CONTENT_LENGTH
char conten_length_env[SIZE] = { 0 };
sprint(conten_length_env, "CONTENT_LENGTH=%s", req->content_length);
putenv(conten_length_env);
}
// b)把標準輸入和標準輸出重定向到管道中.此時,CGI讀寫標準輸入輸出就相當於讀寫管道.
dup2(child_read,0);//把後面重定向到前面
dup2(child_write, 1)
// c)子程序進行程式替換(需要先找到是哪個CGI可執行程式,然後再使用exec函式進行替換)
// 替換成功之後,動態頁面完全交給CGI程式來計算生成.
char file_path[SIZE] = { 0 };
HandlerFilePath(req->url_path, file_path);
//l lp le
//v vp ve
//第一個引數是可執行程式的路徑
//第二個引數,argv[0]
//第三個引數NULL,表示命令列引數結束了
execl(file_path,file_path,NULL)
// d)替換失敗的錯誤處理,子程序就是為了替換而存在的
//如果替換失敗,就沒有存在的必要了
}
int HandlerCGI(int new_sock)
{
//1.建立一對匿名管道
int fd[1], fd[2];
pipe(fd1);
pipe(fd2);
int father_read = fd[0];
int child_write = fd[1];
int father_write = fd[1];
int child_read = fd[0];
//2.建立子程序fork
pid_t ret = fork();
if (ret > 0)
{
//father
close(child_read);
close(child_write);
HandlerCGIFather();
}
else if (ret == 0)
{
//child;
close(father_read);
close(father_write);
HandlerCGIChild();
}
else
{
perror("fork");
goto END;
}
//3.父程序核心流程
//father
//此處先把不必要的檔案描述符關閉掉
//
close(child_write);
close();
//4.子程序的核心流程
//收尾工作
END:
close();
close();
close();
}
//完成具體的請求處理過程
void HandlerRequest(int64_t new_sock)
{
int err_code = 200;//定義一個預設的錯誤碼200,出錯置為400
HttpRequest req;
memset(&req, 0, sizeof(req));
//1.解析請求
// a)從socket中讀取首行
char first_line[SIZE] = { 0 };
if (ReadLine(new_sock,req.first_line,sizeof(req.first_line )-1) < 0)
{
//todo 錯誤處理,此處一旦觸發邏輯,就簡單處理,無腦返回404資料報.正常應該根據不同的錯誤原因返回不同的資料報
err_code = 404;
goto END;
}
printf("first_line:%s\n", req.first_line);
// b)解析首行,獲取到url和method
if (ParseFirstLine(req.first_line,&req.method,&req.url)<0)
{
//錯誤處理
err_code = 404;
goto END;
}
// c)解析url,獲取到url_path和query_string
if (PerseUrl(req.url, &req.url_path, &req.query_string) < 0)
{
//錯誤處理
err_code = 404;
goto END;
}
// d)解析header,丟棄大部分header,只保留content-Length
if (ParseHeader(new_sock, &req.content_length))
{
//錯誤處理
err_code = 404;
goto END;
}
//2.根據請求計算響應並能寫回客戶端
if (strcasecmp(req.method, "GET")==0&&req.query_string==NULL)//strcasecmp忽略大小寫的比較
{
// a)處理靜態頁面
err_code=HandlerStaticFile(new_sock,&req);
}
else if (strcasecmp(req.method, "GET") == 0 && req.query_string != NULL)
{
// b)處理動態頁面
err_code=HandlerCGI();
}
else if (strcasecmp(req.method, "POST") == 0) {
// b)處理動態頁面
err_code=HandlerCGI();
}
else
{
//錯誤處理
err_code = 404;
goto END;
}
END:
//收尾工作,主動關閉socket,會進入TIME_WAUT
if (err_code != 200){
Handler404(new_sock);
}
close(new_sock);
}
void* ThreadEntry(void *arg)
{
int64_t new_sock = (int64_t)arg;
HeadlerRequest(new_sock);
return NULL;
}
void HttpServerStart(const char* ip, short port)
{
//1.基本的初始化,基於TCP
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
perror("socket");
return;
}
//設定一個選項 REUSEADDR:地址重用
int opt = 1;
setsockopt(listen_sock, 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_sock, (sockaddr*)&addr, sizeof(addr));//一個程序可以繫結多個埠號,通常一個埠號不能被多個程序繫結
if (ret < 0)
{
perror("bind");
return;
}
ret = listen(listen_sock, 5);
if (ret < 0)
{
perror("listen");
return ;
}
printf("HttpServer start OK\n");
//2.進入事件迴圈
while (1)
{
//此處實現一個多執行緒版本的伺服器
//每個請求都建立一個新的執行緒處理具體請求
sockaddr_in peer;
socklen_t len = sizeof(peer);
int64-t new_sock = accept(listen_sock, (sockaddr*)&peer, &len);//不能用static,因為N個執行緒不能共用同一個new_sock
if (new_sock < 0)
{
perror("accept");
continue;
}
pthread_t tid;
pthread_create(&tid, NULL, THreadEntry, (void*)new_sock);
//new_sock怎麼傳遞給執行緒入口函式,不能寫取地址.同一時刻伺服器可能會有很多連線執行緒,每個執行緒處理一個請求,所有的
//請求共用同一份new_sock一定會出錯.正確的處理應該把new_sock按照值的方式傳遞到執行緒入口函式中.
pthread_detach(tid);//執行緒分離
}
}
int main(int argc,char* argv[])
{
if (argc != 3)
{
printf("Usage ./http_server [ip] [port]\n");
return 1;
}
signal(SIGCHLD, SIG_IGN);
HttpServerStart(argv[1], atoi(argv[2]));
return 0;
}