http

項目:HTTP 服務器框架

步驟:
一. 背景調研(當前不涉及)
二. 需求分析(最核心的一步)
(有產品經理作出完整的文檔)

實現一個服務器程序
支持HTTP協議的服務器
從請求角度上,支持GET方法和POST方法
從響應的角度上,支持靜態頁面和動態頁面

靜態頁面:返回一個服務器上的本地文件
動態頁面:根據用戶的輸入實時計算生成一個響應結果頁面
12
能夠使用瀏覽器訪問

三. 概要設計(分成幾個模塊,模塊之間的關係)
HTTP服務器框架:

通用的HTTP服務器框架(業界知名的服務器框架有:Nginx 、 httpd(apache))
1). 服務器初始化(socket 的初始化)
2). 處理HTTP請求

從 socket 中讀取數據,並且按照HTTP協議的格式解析數據;
根據輸入的請求的不同決定是按照靜態頁面處理還是動態頁面處理;
如果是按照靜態頁面,直接將服務器上對應的文件返回到客戶端中;
如果時按照動態頁面,就將輸入的信息交給對應的 CGI 程序,由 CGI 程序來計算,生成響應。把響應的結果交還給HTTP服務器框架,由框架完成數據返回到客戶端的過程。
業務相關的 CGI 程序
1). 根據不同的業務,來分別實現不同的 CGI 程序;
2). 讀取 HTTP 服務器框架交給它的參數,根據自身的業務流程計算生成 HTTP 響應數據,交還給框架;

CGI:是一種標準或協議
CGI 優點:

完成了解耦,讓通用的HTTP 服務器框架和業務完全的解耦和
CGI 程序可以用任何的編程語言來實現

CGI 的實現過程:
HTTP 服務器框架創建子進程,子進程根據所請求得 CGI 程序的路徑(HTTP 請求中的url_path)進行程序替換,CGI 程序就可以根據業務進行計算了。
1. HTTP 服務器是通過什麼樣的方式把必要的輸入參數傳給 CGI 程序:進程間通信中的管道 + 環境變量
2. CGI 程序計算的結果通過通過管道寫回給客戶端

四. 詳細設計(模塊的實現細節)
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/wait.h>
#include “http_server.h”

typedef struct sockaddr sockaddr;//定義某種類型
typedef struct sockaddr_in sockaddr_in ;//因爲它在,所以你就可以去給它起一個別名。
//一次從socket中讀取一行數據,把數據放在buf緩衝區中;如果讀取失敗,返回值就是-1;
//遇到\n或\r\n或\r證明讀取完成(一般爲’\n’,但是有些瀏覽器換行符是’\r’,’\r\n’)
int ReadLine(int sock,char buf[],ssize_t size){//在這裏讀取socket中的一行,其實就是讀取我們在瀏覽器中所輸入的一行。
//size表示緩衝區的長度
//1.從socket中一個字符一個字符的讀取
char c=’\0’;
ssize_t i=0;//i表示當前讀了多少個字符
//結束條件;
//a)讀到的長度太長,達到了緩衝區長度的上限
//b)讀到了結束標誌,並把所有結束標誌都轉換成\n(此處要兼容\r,\r\n的情況,統一把這兩種情況
//轉換爲\n

while(i<size-1&&c!='\n'){
	ssize_t  read_size=recv(sock,&c,1,0);
    //一次讀一個字符,把字符讀到c總,0表示flag爲0
	//讀取失敗有兩種
	if(read_size<0){
		return -1;
	}
	if(read_size==0){
		//因爲預期是要讀到\n這樣的換行符,
        //結果還沒有讀得到就先讀了EOF,這種情況我們也暫時認爲是失敗的
		return -1;
	}
    //此時表示大於0的情況
	if(c=='\r')
	{
		recv(sock,&c,1,MSG_PEEK);//要懂MSG_PEEK選項的含義。這次不要似懂非懂。加油。
		if (c=='\n'){//如果下一個字符是\n
			//此時的分隔符就是\r\n,那麼就不要這個字符了,下次再讀取的就是下一行的開始
			recv(sock,&c,1,0);
		}else 
		{
			//當前分隔符確定是\r,此時把分隔符轉換成\n
			c='\n';
		}
	}
	//只要上面的c讀到的是\r,那麼if結束後,c都變成了\n
	//這種方式就是把前面的\r和\r\n兩種情況都統一成了\n
	buf[i++]=c;

}
buf [i]='\0';
return i;//真正想緩衝區中放置的字符的個數

}

int Split(char input[], const char split_char,char output[],int output_size)//在對讀取後的字符串進行切分。
{
//split_char:分隔符;
//使用strtok_r
int i=0;
char *tmp=NULL;//保存上次的切分結果
char * pch;
//使用線程安全的strtok_r代替strtok
//這一點是以後非常容易出錯的一點
pch = strtok_r (input,split_char,&tmp);
while (pch != NULL)
{
if (i>=output_size)
{
return i;
}
output[i++]=pch;//保存在這裏面,這是一個輸出緩衝區的大小。
pch = strtok_r (NULL, split_char,&tmp);//繼續前面的切分繼續切分
}

//printf ("Split\n");
return i;//返回切分的個數

}
//strtok是一個字符串切分函數,一次只能切分一個部分,但是實際中大多期望一次切分完所有的,所以採用
//循環調用的方式。
//strtok的缺陷:可能會導致服務器崩潰,strtok可以接着上一次的切分結果進行切分,說明strtok中保存
//了上一次的切分結果,strtok不能用棧上的變量保存,因爲函數一旦結束,函數棧上的空間就沒有了;
//也不能用堆上的數據保存,因爲不確定變量什麼時候進行釋放。調用一次釋放一次,又會導致內存泄漏
//靜態變量的生命週期是全局的,使用靜態變量來保存,函數結束,靜態變量仍然存在;再次調用strtok,變量
//的值,仍然爲上一次的結果。所以strtok內部採用靜態變量來保存上一次切分的位置。函數內部使用靜態變量
//意味着不可重入,說明此函數也是一個線程不安全函數(多個線程同時調用,可能會出現邏輯上的問題。)
//Split函數是一個多線程的函數,每次收到一個請求,就創建一個線程,線程都要進行first_line解析,
//每次解析都得調用strtok_r進行切分,在一個多線程的環境下調用一個線程不安全函數,很可能導致程序崩潰
//不應該使用線程不安全函數在一個多線程環境下進行調用。
//解決這個問題的辦法:a、加互斥鎖:互斥鎖可行,但是沒必要。枷鎖意味着函數在不同的線程之間要相互競爭
//鎖,會導致程序整體的效率被拖慢,很大一部分時間都在等待鎖
//b、使用strtok_r函數,是strtok的線程安全版本,不再使用內部的緩衝區,而是通過函數參數的形式顯示的
//指定一個緩衝區。這個緩衝區提供一個空間,幫助函數保存上一次的執行結果。
//類似使用內部靜態緩衝區的線程不安全函數:a、inet_ntoa把數字的IP地址轉換爲點分十進制的ip地址,通過
//靜態緩衝區保存點分十進制的字符串,同樣也是線程不安全;爲了解決這個問題通常使用inet_ntop函數解決
//線程不安全問題,不在使用靜態緩衝區來保存結果,而是通過棧上開闢內存,把棧上緩衝區中的內容傳到
//函數裏面,然後基於棧上緩衝區訪問結果而解決問題
//b、random函數,也是在內部有一個靜態緩衝區,也是線程不安全函數,爲了解決這個問題,使用random_r在
//多線程環境下使用

//解析首行
int ParseFirstLine(char first_line[],char**p_url,char **p_method)
{
//需要找到哪部分是url,哪部分是method,此時我們考慮
//一個指針指向方法的首地址,一個指針指向url的首地址的情況
//爲了得到每一個部分,把空格替換成\0,再通過指針指向每一個部分的起始位置,就得到了每一個部分
//對應的字符串,破壞了原有的字符串結構,將其切分成了若干個部分,若干個部分之間不再用空格
//切分,直接用\0切分,我們再得到每一個部分起始位置的指針,就可以通過c風格字符串的方式,得到
//方法、url……
//char **:參數url和method是通過函數參數傳遞,是一個輸出型參數,輸出兩個指針的指向位置,只能
//用二級指針的方式修改指針的指向。所以需要將指針的地址傳遞給函數。
//把首行按空格進行字符串切分
//切分得到的每一個部分,就放在tok數組裏
//返回值,就是tok數組包含幾個元素。

char  *tok[10];
//最後一個參數10表示tok數組中最多可以放幾個元素
int tok_size=Split(first_line," ",tok,10);
if (tok_size!=3)
{
	printf ("Split  failed! tok_size =%d\n",tok_size);//因爲要切分成三部分,方法,url和路徑
	return -1;
}
*p_method=tok[0];
*p_url=tok[1];

// printf ("ParseFirstLine\n");
return 0;

}

//把url字符串進一步切分:url_path指向問號之前、空格之後的字符串;query_string指向問號之後的字符串,這在這裏就是對url字符串進行的是進一步的切分。
//把問號替換成’\0’
int ParseQueryString (char *url,char **p_url_path,char **p_query_string )
{
*p_url_path=url;//從url起始位置開始遍歷
char *p=url;
for (;*p!=’\0’;++p)
{
if (*p==’?’)
{
*p=’\0’;
*p_query_string =p+1;
return 0;
}
}
//循環結束都沒有找到?,說明這個請求不帶query_string
*p_query_string=NULL;
return 0;
}

//解析header,解析header的目的是爲了讀value。
int ParseHeader (int sock ,int *content_length)
{
//1.循環從socket中讀取一行
//2.判斷當前行是不是Content—Length
//3.如果是Content-Length就直接把value讀出來
//4.如果不是就直接丟棄
//5.讀到空行,循環結束。
char buf [SIZE]={0};//定義一個緩衝區
while (1)
{
//1.循環從socket中讀取一行
ssize_t read_size=ReadLine (sock,buf,sizeof (buf));
//sizeof(buf)沒有減1,是什麼原因?ReadLine函數中已經考慮了\0的情況,此時可以不考慮\0
//的情況,但是雙重保證就更合適。
//處理讀失敗的情況
if (read_size<=0)
{
return -1;
}
//處理讀完的情況
//buf裏面只有一個\n,說明讀完了
if (strcmp (buf ,"\n")==0)
{
return 0;
}
//2.判定當前行是不是Content-Length
//如果是Content-Length就直接把value讀出來
//如果不是就直接丟棄
const char *content_length_str="Content-Length: ";
if (content_length!=NULL&&strncmp(buf,content_length_str,strlen(content_length_str))==0)
{
*content_length=atoi(buf+strlen(content_length_str));
//strncmp比較前n個字符;注意“Content-Length”的寫法,只有這種格式。
}
}
return 0;
//更復雜的版本需要把這個字符串以鍵值對的方式存儲(key和value)
}
//剛剛保存時出現了’readonly’ option is set (add ! to override)
//解決方案:按下ESC,輸入:set noreadonly;然就就可以使用:wq正常保存了

void Handler404(int sock)
{
//構造一個完整的HTTP響應
//狀態碼是404
//body部分也應該是一個404相關的錯誤頁面。
const char* first_line=“HTTP/1.1 404 Not Found\n”;
const char* type_line=“Content-Type: text/html;charset=utf-8\n”;//提示瀏覽器按照utf-8方式解碼於下面成雙重保證
const char * blank_line="\n";
//html是一個文本文件,用於說明標題,……詳細的信息
const char * html="<meta http-equiv=“content-type” content=“text/html;charset=utf-8”>"

您的頁面沒找到!!!

”;//提示瀏覽器按照utf8方式解碼//否則會產生亂碼//
//可以通過:set fileencode查看編碼格式,charset叫做字符集
//構造了一個http響應並且把此響應寫會到socket之中,返回瀏覽器。
send (sock,first_line ,strlen(first_line),0);
//send有一個額外的選項flag
send (sock,type_line ,strlen(type_line),0);
send (sock,blank_line ,strlen(blank_line),0);
send (sock,html ,strlen(html),0);
return;
}

void PrintRequest (Request*req)
{
printf (“method: %s\n”,req->method);
printf (“url_path: %s\n”,req->url_path);
printf (“query_string: %s\n”,req->query_string);
printf (“content_length: %d\n”,req->content_length);
return;
}

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))
{
//通過這個宏,判定st_mode選項是不是目錄,
return 1;
}
return 0;
}

//http服務器的根目錄:用戶指定的任意一個存在的目錄作爲根目錄,url_path中訪問的文件都是以http服務器的
//根目錄爲基準,取相對路徑。假如:在http_server的路徑下有一個wwroot的目錄,這個目錄下包含了一個
//index.html的文件,如何訪問這個index.html,可以通過/index.html訪問,是在wwwroot下取相對路徑index.html
//若是該目錄下有一個文件image/1.jpg,想要取到1.jpg訪問時path變爲/image/1.jpg
void HandlerFilePath(const char*url_path,char file_path[])
{
//把url_path轉換成本地真實的文件路徑,即進行字符拼接。
//(a,給url_path 加上前綴名(HTTP服務器的根目錄)
//url_path–>/index.html
//file_path—>./wwwroot/index.html
sprintf (file_path,"./wwwroot%s",url_path);
//向file_path的輸出緩衝區中進行拼接
//b)例如url_path是/,此時url_path 其實就是一個目錄。
//如果是目錄的話,就給這個目錄之中追加一個文件(默認文件)index.html
//url_path, /或者 /image/
if (file_path[strlen(file_path)-1]==’/’)
//判定最後一個字符是不是’/’
{
strcat(file_path,“index.html”);
//字符串追加,因爲以’/‘結尾,追加時,不需要加’/’
}
//c)url_path=>/image,
if (IsDir(file_path))
{
strcat (file_path,"/index.html");
}
return ;

}

//幫助獲取文件的大小
ssize_t GetFileSize(const char* file_path)
{
struct stat st;
//stat函數可以獲取一些文件的源信息
int ret=stat(file_path,&st);
//把文件對應的原信息放到結構體裏
if (ret<0)
{
//打開文件失敗,很可能是文件不存在
//此時直接返回文件長度爲0
return 0;
}
return st.st_size;
// st_size是文件的大小
}

int WriteStaticFile (int sock,const char* file_path)
{
//1.打開文件
int fd=open (file_path,O_RDONLY);
//打開文件失敗的情況:a、文件描述符不夠用
//b、文件不存在(用戶輸入的可能不正確)
if (fd<0)
{
perror (“open”);
return 404;
}
//2.把構造出來的HTTP響應寫到socket之中
//1).寫入首行
//2).寫入head
//4).寫入body(文件內容)
const char * first_line=“HTTP/1.1 200 OK\n”;
send (sock,first_line,strlen(first_line ),0);
//插入圖片
//圖片和文本應該分開進行解析,或者讓瀏覽器自動識別
//const char* type_line=“Content-Type: text/html;charset=utf-8\n”;//用此方式,解析照片時會發生錯誤
//const char* type_line=“Content-Type: imag/png;charset=utf-8\n”;
//send (sock,type_line ,strlen(type_line),0);
//兩個都不添,讓瀏覽器自己決定
//(3)寫入空行
const char * blank_line="\n";
send (sock,blank_line ,strlen(blank_line),0);
/*ssize_t file_size=GetFileSize(file_path);
//獲取文件的真實長度
ssize_t i=0;
for(;i<file_size;i++)
{
char c;
//讀一個字節
read(fd,&c,1);
//用戶中調用read就是把文件中的每一個字符都拷貝一份到用戶的緩衝區裏面
//寫一個字節
send (sock,&c,1,0);
//把剛剛讀取到的數據寫回網卡
//這種方式會在用戶空間和內核空間中拷貝多次
}
*/
sendfile(sock,fd,NULL,GetFileSize(file_path));
//sendfile函數,第一個參數時輸出的參數;第一個參數是輸入的參數,第三個參數文件的參數;第四個參
//數是拷貝的字節數,文件本身的大小
//把文件的數據直接在內核中拷回到網卡,只在內核中進行
//3.關閉文件
close (fd);
return 200;
}

//處理靜態文件
int HandlerStaticFile (int sock ,Request *req)
{
//1根據url_path獲取到文件在服務器上的真實路徑。
char file_path[SIZE]={0};
HandlerFilePath(req->url_path,file_path);//file_path輸出參數
//2。讀取文件,把文件的內容直接寫道socket之中
int err_code=WriteStaticFile (sock,file_path);
return err_code;
}

int HandlerCGIFather(int new_sock,int father_read,int father_write,int child_pid,Request *req){
//1如果是POST請求,就把body寫到管道中
if (strcasecmp(req->method,“POST”)==0){
int i=0;
char c=’\0’;
for (;icontent_length;++i){
//每一次循環都讀取一個字符,讀一個字符向管道中寫一個字符。
read(new_sock,&c,1);
//前面已經對CGI讀取了很多次,在頭部被解析之後才調用,
//緩衝區中首行,haader,空行都已經讀取結束,存放在緩衝區中了
//所以此時讀到的數據是body
write(father_write,&c,1);
}

}
//2構造HTTP響應
const char *  first_line="HTTP/1.1 200 OK\n";
send (new_sock,first_line,strlen(first_line ),0);
const char*  type_line="Content-Type: text/html;charset=utf-8\n";
//const char*  type_line="Content-Type: image/png;charset=utf-8\n";
send (new_sock,type_line ,strlen(type_line),0);
//兩個都不添,讓瀏覽器自己決定
const char *  blank_line="\n";
send (new_sock,blank_line ,strlen(blank_line),0);
//循環的從管道中讀取數據並寫入數據到socket
char  c='\0';
//管道如果所有的寫端全部關閉再嘗試繼續讀,read就會返回0
//管道讀端全部關閉,再嘗試繼續寫,SIGPIPE信號導致進程異常終止
while (read(father_read,&c,1)>0){//從father_read中讀取數據,讀取的數據放到c裏面,
	send(new_sock,&c,1,0);
}
//4回收子進程的資源
waitpid(child_pid,NULL,0);
//wait()//handlerrequet函數是在新線程中執行的,不存在阻塞不能執行多線程的情況。
//一個服務器同一時間收到的請求是非常多的,每一個請求都得創建單獨的線程,每個線程都有
//可能處理CGI的邏輯,就有可能創建子進程
//此處若是調用wait,線程都調用一個wait,wait是無差別的等待,
//可能導致線程1創建的線程被線程一回收
return 200;

}

int HandlerCGIChild(int child_read,int child_write,Request* req){
//1.設置必要的環境變量
char method_env[SIZE]={0};
sprintf (method_env,“REQUEST_METHOD=%s”,req->method);
putenv(method_env);//設置環境變量
//還需要設置QUERY_STRING(get方式時需要設置)或者是CONTENT_LENGTH(post方法需要設置)
if(strcasecmp(req->method,“GET”)==0){
char query_string_env[SIZE]={0};
sprintf (query_string_env,“QUERY_STRING=%s”,req->query_string);
putenv(query_string_env);
}else {
char content_length_env[SIZE]={0};
sprintf (content_length_env,“CONTENT_LENGTH=%d”,req->content_length);
putenv(content_length_env);//設置環境變量
}//名字必須滿足CGI標準
//2把標準輸入輸出重定向到管道里
dup2(child_read,0);//把第二個參數重定向到第一個參數
dup2(child_write,1);
//3對子進程進行程序替換
// url_path: /cgi-bin/test
// file_path: ./wwwroot/cgi-bin/test
//根據url_path獲取對應的file_path
char file_path[SIZE]={0};
HandlerFilePath(req->url_path,file_path);//把req->url_path生成file_path

execl(file_path,file_path,NULL);//
    exit(1);//當程序替換失敗時。一定讓子進程終止,如果子進程不終止,因爲父子進程是同一塊代碼,父進程一直在Listen狀態一直等待,子進程也會等待,所以要直接結束進程,避免一直等待端口數據的返回;

//有 exec l lp le v vp ve,首先看知不知道可執行文件的完整路徑 ,如有就可以不帶P,因爲P是在PATH中找,再看要不要環境變量,如果不用,就不需要帶e,這裏因爲通過putenv ()的方式直接設到了ENV裏面所以不用,說一直接用execl()
return 200;
}

int HandlerCGI(int new_sock,Request *req)//第二個參數是格式化的請求數據
{
int err_code=200;
//1.創建一對匿名管道
int fd1[2],fd2[2];
int ret =pipe(fd1);
//創建第一對匿名管道
if (ret<0)
{
return 404;
}
ret=pipe (fd2);
//創建第二對匿名管道
if (ret<0)
{
//走到這一步,說明上面的文件描述符已經分配好了,所以需要關閉前面創建好的文件描述符
close(fd1[0]);
close(fd1[1]);
return 404;
}
//fd1,fd2這種變量名的描述性太差,後面直接用的話
//是非常容易弄混的,所以直接在此處定義幾個
//更加明確的變量名來描述該文件描述符的用途
int father_read=fd1[0];
int child_write=fd1[1];
int father_write=fd2[1];
int child_read=fd2[0];
//2.創建子進程
ret=fork();
//3.父子進程各執行不同的邏輯
if (ret>0){

	//father
	//此處父進程優先關閉這兩個管道的文件描述符
	//是爲了後續父進程從子進程這裏讀數據時,能夠讀到EOF,對於管道來說,所有寫端關閉,
    //繼續讀,纔有EOF,而此時所有寫端,一方面是父進程需要關閉,另一方面子進程也需要關閉。
    //所以此處父進程先關閉不必要的寫端之後,後續子進程用完了
	//直接關閉,父進程也就讀到了EOF
	close (child_read);
	close(child_write);
    //如果寫端不關閉,隨時都有可能有人往管道中寫,此時應保證管道的寫端全都關閉(父進程先
    //關閉,子進程用完了也關閉,兩者都關閉了,才意味着全部寫端端關閉,全部寫端關閉,父進程一直在
    //再去讀,才能讀到read返回的結果
    //因爲父進程先創建管道,再fork,父子進程管道各佔一半
	err_code=HandlerCGIFather(new_sock,father_read,father_write,ret,req);
}else if (ret==0){

	//child
	close (father_read);
	close (father_write);
	err_code=HandlerCGIChild(child_read,child_write,req);
}else{
    //fd[]未初始化時,是一個隨機值,直接關閉會出錯
	perror("fork");
    err_code=404;
	goto END;
}

END:
perror(“fork”);
close (fd1[0]);
close (fd1[1]);
close (fd2[0]);
close (fd2[1]);

//4.收尾工作和錯誤處理
return 200;

}

void HandlerRequest(int new_sock)
{
int err_code=200;//錯誤碼初始200,默認沒錯
//1.讀取並解析請求(反序列化)
Request req;
memset (&req,0,sizeof (req));
//a)從socket中讀取出首行
if (ReadLine(new_sock,req.first_line,sizeof (req.first_line))<0)
{
//失敗處理
err_code =404;
goto END;
}
//b)解析首行,從首行中解析出url和method
if (ParseFirstLine(req.first_line,&req.url,&req.method))
{
//失敗處理
err_code =404;
goto END;

}
//c)解析url,從url中解析出url_path和query_string
if (ParseQueryString(req.url,&req.url_path,&req.query_string))
{
	//失敗處理
	err_code =404;
	goto END;

}
//d)解析Header,丟棄了大部分header,只讀取Content—Length
if (ParseHeader(new_sock,&req.content_length))
{
	//失敗處理
	err_code =404;
	goto END;

}
PrintRequest (&req);
//2.靜態/動態方式生成頁面
//3.把生成結果寫回客戶端上
if (strcasecmp(req.method,"GET")==0&&req.query_string==NULL)
{
	//a)如果是GET方式請求,並且沒有query_string,
	//那麼返回靜態頁面
	err_code=HandlerStaticFile(new_sock,&req);
}else  if (strcasecmp(req.method,"GET")==0&&req.query_string!=NULL)
{
	//b)如果是GET方式請求,並且有query_string,
	//那麼返回動態頁面
	err_code=HandlerCGI(new_sock,&req);
}else  if (strcasecmp(req.method,"POST")==0)
{
	//c)如果請求是POST類型的(一定是帶參的,參數是通過body來傳給服務器的),那麼也返回動態頁面
	err_code=HandlerCGI(new_sock,&req);
}else  
{
	//失敗處理
	err_code =404;
	goto END;
}

//錯誤處理;直接返回一個404的HTTP響應

END:
if (err_code !=200)
{
Handler404(new_sock);
}
close(new_sock);
return;

}

void ThreadEntry(voidarg)// 因爲在64位機中指針佔8個字節,如果強轉回int會有丟失數據的危險
//void*是一個指針,32位機器下佔4個字節,64位機器下佔8個字節,而一個int在32位和64位機器下都爲4個字節
//在64位機器下,需要改爲int64_t
{
int new_sock=(int)arg;
//使用HandlerRequest完成具體的處理請求過程這個過程單獨提取出來是爲了解耦和
//相當於線程入口函數只有一個包裝,真正幹活的是這個函數,這樣最大的好處還是解耦和
//一旦需要把服務器改成多進程或者IO多路複用的形式,整體代碼的改動都是比較小的
HandlerRequest(new_sock);
return NULL;
}

//服務器啓動
void HttpServerStart(const char* ip,short port)
{
int listen_sock=socket(AF_INET,SOCK_STREAM,0);
if (listen_sock<0)
{
perror(“socket”);
return ;
}
//加上這個選項是爲了重用TIME_WAIT連接//解決出現大量TIME_WAIT的現象
//服務器出現大量的TIME_WAIT狀態,說明是服務器率先斷開了連接,爲了大量TIME_WAIT連接之後還能夠
//有新的連接可用,那麼就需要重用已有的TIME_WAIT,加上SO_REUSEADDR進行重用
//服務器出現大量的CLOSE_WAIT狀態意味着代碼出現了bug,四次揮手只出現了兩次,沒有正確的分配socket
//導致服務器出錯,文件泄漏,需要找到對應的代碼進行修復。
int opt=1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt));
//加上這個函數是爲了使端口處於TIME-WAIT時複用地址.
//第一個參數表示要對哪一個參數進行設置,第二、三個參數決定了要給哪個選項設置一個值
//最後兩個參數決定了要設置什麼樣的值,SO_REUSEADDR重用addr連接。opt爲1表示開啓選項
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 ("ServerInit  OK\n");
while (1)
{
	sockaddr_in peer;
	socklen_t  len=sizeof (peer);
	int   new_sock=accept(listen_sock,(sockaddr*)&peer,&len);
	if (new_sock<0)
	{
		perror("accept");
		continue;
	}
	//使用多線程的方式來實現TCP服務器
	pthread_t  tid;
	//       printf ("ThreadEntry\n");
    //把new_sock傳遞給線程入口函數
	pthread_create(&tid,NULL,ThreadEntry,(void *)new_sock);
    //回收線程
	pthread_detach(tid);
}

}
//主函數的參數 ./http_server [ip] [port]
int main (int argc,char* argv[])
{
if (argc!=3)
{
printf (“Usage ./http_server [ip] [port]\n”);
return 1;
}
HttpServerStart( argv[1],atoi(argv[2]));
return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章