專業(yè)做公墓 陵園的網(wǎng)站網(wǎng)站seo去哪個(gè)網(wǎng)站找好
這里寫目錄標(biāo)題
- 目標(biāo)
- 實(shí)現(xiàn)的目標(biāo)
- 服務(wù)器代碼(采用epoll實(shí)現(xiàn)服務(wù)器)
- 整體框架
- main函數(shù)
- init_listen_fd函數(shù)(負(fù)責(zé)對lfd初始化的那一系列操作)
- epoll_run函數(shù)
- do_accept函數(shù)
- do_read函數(shù)
- 內(nèi)容
- 補(bǔ)充:http中的getline函數(shù)
- 詳解do_read函數(shù)(借助http協(xié)議進(jìn)行通信的具體數(shù)據(jù)讀寫過程)
- 首先實(shí)現(xiàn)單文件請求
- 思路
- 代碼實(shí)現(xiàn)
- 補(bǔ)充:正則表達(dá)式(簡要介紹)
- 補(bǔ)充:上述代碼中用到的函數(shù)的原型
- strncasecmp
- stat
目標(biāo)
實(shí)現(xiàn)的目標(biāo)
我們要實(shí)現(xiàn),在服務(wù)器上啟動(dòng)服務(wù)之后,通過瀏覽器訪問Ip+port,可以訪問到服務(wù)器某個(gè)目錄的文件
當(dāng)然也可以使用域名訪問,域名就是對IP+port進(jìn)行了包裝,域名需要申請
服務(wù)器代碼(采用epoll實(shí)現(xiàn)服務(wù)器)
整體框架
main函數(shù)
首先,為了更加靈活的進(jìn)行服務(wù)端的啟動(dòng),我們在main函數(shù)中,定義兩個(gè)參數(shù),用于進(jìn)行啟動(dòng)參數(shù)的配置和讀取
首先,判斷argv是否讀到了三個(gè)字符串,如果小于三個(gè)字符串,則啟動(dòng)時(shí)沒有按照規(guī)定進(jìn)行啟動(dòng)參數(shù)設(shè)置,提示啟輸入./server 端口 以及要發(fā)布的文件所在路徑
之后,通過argv[2],拿到輸入的第二個(gè)字符串,即端口,將其atoi,轉(zhuǎn)為字面值一樣的int型,就拿到了開放的端口
之后,因?yàn)閔ttp協(xié)議中,對于從瀏覽器發(fā)送而來的文件的位置,是以啟動(dòng)配置的第三個(gè)參數(shù)為參考根目錄的相對路徑,服務(wù)器要設(shè)法拿到服務(wù)器對應(yīng)的路徑,而拼接路徑又過于繁瑣,所以,使用chdir函數(shù),該函數(shù)可以讓服務(wù)端的工作目錄跳轉(zhuǎn)到某個(gè)目錄下(實(shí)際上就是與cd的作用一樣),所以,chdir(argv[2]),就是將服務(wù)器跳轉(zhuǎn)到第三個(gè)參數(shù)所指明的目錄內(nèi),這樣,從http協(xié)議封裝出來的數(shù)據(jù)包拿到的數(shù)據(jù),可以直接拿到當(dāng)前服務(wù)器使用,因?yàn)榉?wù)器的工作目錄已經(jīng)跳轉(zhuǎn)到第三個(gè)參數(shù)的目錄了
之后使用封裝的函數(shù),根據(jù)傳來的端口,啟動(dòng)epoll監(jiān)聽
init_listen_fd函數(shù)(負(fù)責(zé)對lfd初始化的那一系列操作)
即,根據(jù)傳入進(jìn)來的port(端口)、epfd(epoll樹根)等參數(shù),進(jìn)行l(wèi)fd的創(chuàng)建以及上樹
包括,創(chuàng)建socket、創(chuàng)建服務(wù)器地址結(jié)構(gòu)、設(shè)置端口復(fù)用、給lfd綁定地址、設(shè)置監(jiān)聽上限、將lfd上樹,預(yù)監(jiān)聽其讀事件。
最后返回lfd
epoll_run函數(shù)
根據(jù)傳入的port,首先,創(chuàng)建一個(gè)epoll_event數(shù)組,用于后續(xù)的epoll_wait監(jiān)聽時(shí)返回套接字?jǐn)?shù)組。創(chuàng)建epoll監(jiān)聽樹樹根,之后傳入init_listen_fd函數(shù),創(chuàng)建監(jiān)聽套接字lfd,并將其上樹
在lfd上樹之后,我們就可以開始監(jiān)聽了
while循環(huán)內(nèi),使用epoll_wait開始監(jiān)聽,將監(jiān)聽結(jié)果返回到all_events數(shù)組。
之后拿到返回值,就是all_events數(shù)組的有效元素個(gè)數(shù),也是其循環(huán)上限,
在循環(huán)內(nèi),我們默認(rèn)只處理服務(wù)器的讀事件(也就是只處理接收請求),其他事件不處理
將其元素挨個(gè)取出,轉(zhuǎn)為指針,如果不是事件,直接continue,如果是,進(jìn)行后續(xù)代碼:
讀事件又分為是lfd收到數(shù)據(jù)還是其他cfd受到數(shù)據(jù):
如果是lfd,進(jìn)行accept進(jìn)行新的cfd創(chuàng)建與連接
如果是cfd,那么進(jìn)行數(shù)據(jù)的讀取
do_accept、do_read函數(shù)也是封裝的函數(shù)
do_accept函數(shù)
根據(jù)傳入進(jìn)來的lfd,和epoll的樹根,進(jìn)行新的cfd的創(chuàng)建以及相關(guān)設(shè)置
首先,調(diào)用accept函數(shù),進(jìn)行新的cfd的創(chuàng)建,返回新的cfd,
之后,將其設(shè)置為非阻塞
之后設(shè)置ET模式,這是經(jīng)典的ET+非阻塞,是高效模式
最后將其掛上樹,進(jìn)行監(jiān)聽
do_read函數(shù)
內(nèi)容
補(bǔ)充:http中的getline函數(shù)
參數(shù):第一個(gè)是表示從cfd文件描述符中讀,第二個(gè)是傳出參數(shù),表示讀到的數(shù)據(jù),第三個(gè)是傳出參數(shù)對應(yīng)實(shí)參的開辟空間大小,使用時(shí),直接傳sizeof(第二個(gè)實(shí)參)
其中,recv的第四個(gè)參數(shù)如果是0,那么就是真實(shí)的從緩沖區(qū)拿數(shù)據(jù)讀進(jìn)來
而如果其第四個(gè)參數(shù)是MSG_PEEK,那么就是拷貝讀取,并不會(huì)動(dòng)緩沖區(qū)中的數(shù)據(jù)
上圖中的所有recv,每次都是只讀取一個(gè)字節(jié)
返回值為所讀取到的字節(jié)數(shù),將數(shù)據(jù)讀進(jìn)傳出參數(shù)buf中
詳解do_read函數(shù)(借助http協(xié)議進(jìn)行通信的具體數(shù)據(jù)讀寫過程)
首先實(shí)現(xiàn)單文件請求
思路
1、首先使用封住的getline,讀收到的http協(xié)議的請求行。
2、然后對其進(jìn)行拆分
3、之后判斷文件是否存在,使用stat()函數(shù),如下:
該函數(shù)傳入文件路徑,以及一個(gè)傳出參數(shù)變量,即可通過返回值,判斷文件是否存在,而這里傳路徑時(shí),直接將拆分完的路徑傳入即可,會(huì)因?yàn)榉?wù)器已經(jīng)在main函數(shù)時(shí)被chdir切換為了對應(yīng)的目錄,而瀏覽器也是以服務(wù)器啟動(dòng)時(shí)提供的路徑為參考,所以,可以直接傳入
4、判斷傳來的路徑是文件還是目錄(若是文件進(jìn)行后續(xù)操作,若是目錄,則將目錄信息發(fā)送回去,此處不做研究)
5、使用open函數(shù),打開文件,并將其內(nèi)容讀取到
6、首先封裝http應(yīng)答的數(shù)據(jù)包的協(xié)議頭:
主要包含兩部分,一個(gè)是第一行的版本號、狀態(tài)碼、狀態(tài)描述
另一個(gè)是附加信息中的“文件類型”
7、將數(shù)據(jù)封裝到協(xié)議頭之后,最終生成http應(yīng)答數(shù)據(jù)包,將其發(fā)送回瀏覽器
代碼實(shí)現(xiàn)
更正:145行,sizeof內(nèi)應(yīng)該是line,而不是buf
因?yàn)槲覀儠?huì)頻繁用到將某個(gè)cfd,關(guān)閉,且下樹的操作,所以,可以將該操作封裝為一個(gè)函數(shù),如上圖中的disconnect函數(shù)
之后,對do_read進(jìn)行實(shí)現(xiàn),首先,創(chuàng)建并初始化一個(gè)字符數(shù)組
之后,get_line讀取數(shù)據(jù),傳入“傳出參數(shù)”line,拿到數(shù)據(jù)
之后,對返回值進(jìn)行判斷,如果返回值為0,表示是對端關(guān)閉,所以,我們也進(jìn)行disconnect操作
而如果不是0,則要進(jìn)行數(shù)據(jù)的拆分了,現(xiàn)在在line數(shù)組中的是“GET ./hello.c HTTP/1.1”,即是由三個(gè)字符串組成的字符數(shù)組,接下來是拆分這三個(gè)字符串:
使用sscanf函數(shù):
首先定義三個(gè)字符數(shù)組,分別用于存儲(chǔ)拆分后的結(jié)果
之后,調(diào)用sscanf函數(shù),第一個(gè)參數(shù)傳入要拆分的對象,第二個(gè)參數(shù)需要傳入格式說明符(如%s,%d等),這里我們傳入一個(gè)正則表達(dá)式,大概功能與“%s %s %s”差不多,注意要%s之間是空格,不可忽略
之后:
對于圖中的第一部分,作用是:讀取第一行之后的附加內(nèi)容,不讓其存留于緩沖區(qū)中,防止其影響下一次的讀取。(因?yàn)槲覀冞@里心知肚明收到的肯定是GET請求,所以,并沒有判斷是否讀到了附加內(nèi)容結(jié)束,因?yàn)閷τ诓皇荊ET的請求,將附加內(nèi)容讀掉之后,接下來的“數(shù)據(jù)正文”是不可以直接丟掉的,應(yīng)該存起來留用)
圖中,如果len==‘\n’,本來是想表示讀到了回車,也就是空行的下一行會(huì)自帶換行回車,本意是想判斷是否讀完了整個(gè)附加數(shù)據(jù)(即讀到了空行的下一行),但是這里的判斷操作有點(diǎn)迷,到時(shí)候可視具體情況來定
對于第二部分,是將讀到的第一個(gè)字符串與“GET”對比,查看是否相同,即查看第一個(gè)字符串是否是GET,
在linux中:
使用strncasecmp,該方法是將第一個(gè)參數(shù)與第二個(gè)參數(shù)相比看是否相同,且忽略大小寫,且可以通過第三個(gè)參數(shù)指定比較前多少個(gè)字符
而strcasecmp,沒有n,該方法也是比較第一個(gè)參數(shù)與第二個(gè)參數(shù)是否相同,忽略大小寫,但是無法指定比較前多少個(gè)字符
(若此時(shí)用的是string,那么可以使用c_str()方法,將其轉(zhuǎn)為char指針類型,再進(jìn)行函數(shù)比較,或者將string每個(gè)字符統(tǒng)一轉(zhuǎn)為大寫,再與“GET”比較)
win中忽略大小寫的字符指針之間的比較的函數(shù)是:stricmp()
上圖中,比較讀到的第一個(gè)字符串,與“GET”,且只比較前3個(gè)字符,這里是擔(dān)心因?yàn)椤痋0’而誤判,實(shí)際上這兩個(gè)都被補(bǔ)了‘\0’,但是只比較前三個(gè)會(huì)無需考慮是否有’\0’。
而如果確實(shí)是GET的話,返回值為0,如果返回0,那么就要進(jìn)行相關(guān)GET的操作了,字符指針=path+1,因?yàn)閜ath第一個(gè)字符是‘/’,他并不是文件名的一部分,所以,跳過,直接+1,從第二個(gè)字符讀取
然后封裝一個(gè)函數(shù),將文件名傳入,進(jìn)行相關(guān)的處理
之后對于http_request()函數(shù):
我們已經(jīng)確定了請求是GET,接下來就是對GET做相關(guān)的操作,對于GET請求,我們要判斷文件是否存在,
判斷文件是否存在,使用stat方法,使用方法如上圖,先創(chuàng)建一個(gè)結(jié)構(gòu)體,struct stat類型的結(jié)構(gòu)體,之后,調(diào)用stat方法,第一個(gè)參數(shù):文件名,第二個(gè)參數(shù):結(jié)構(gòu)體地址(該函數(shù)使用時(shí)要保證服務(wù)器當(dāng)前工作目錄切換到了要搜索文件所在的目錄)
如果返回值不是0,那么代表文件不存在,這時(shí)要回發(fā)給瀏覽器404錯(cuò)誤頁面,而服務(wù)器的代碼可以有一個(gè)perror,來進(jìn)行異常顯示,至于要不要exit退出服務(wù)器,并不一定,根據(jù)具體需求。
前面都沒有問題的話,說明文件存在,這里,就要判斷其是文件還是文件夾,使用stat自帶的宏來判斷:
S_ISREG(結(jié)構(gòu)體變量.st_mode),如果為真,則是文件,如果S_ISDIR(結(jié)構(gòu)體變量.st_mode)為真,則是文件夾(或者說是文件目錄)
如果是文件,那么接下來的操作就是
1、回發(fā)http應(yīng)答
2、回發(fā)請求的數(shù)據(jù)內(nèi)容
對于我們封裝的回發(fā)函數(shù)(send_respond):
這里我們應(yīng)該清楚,所謂協(xié)議,實(shí)際上就是一堆字符串,他按照一定的格式,將有效數(shù)據(jù)糅合進(jìn)一堆字符串中,所以,我們回發(fā)時(shí),也是,主要進(jìn)行字符串的拼接操作
首先,對于參數(shù),我們需要:與瀏覽器通信的cfd(因?yàn)槲覀兎?wù)器還是使用socket的epoll服務(wù)器),狀態(tài)碼(用no表示),狀態(tài)碼描述(disp指針接收),文件類型(即附加內(nèi)容中的第四行,這是必不可少的),文件長度
在調(diào)用時(shí),將參數(shù)傳入(這里注意,此前http_request并沒有接收cfd,這里在此處更改,寫代碼的時(shí)候注意)
之后,在send_respond中:
首先定義一個(gè)字符串buf,初始化為0
之后使用sprintf,將字符串拼接到buf中,使用方法見上圖。
而想要再次拼接,且在buf原有的內(nèi)容后面追加的話,繼續(xù)調(diào)用sprintf,但是第一個(gè)參數(shù)傳buf+strlen(buf)
注意,在http中的換行是\r\n
將所有內(nèi)容使用send發(fā)過去之后,還要將第9行的/r/n發(fā)回去
至此,GET的協(xié)議應(yīng)答就發(fā)送完了
而對于,buf+srtlen(buf)的可行性:
strlen會(huì)計(jì)算字符串的字符個(gè)數(shù),且不計(jì)算最后的\0,所以,不管最后有沒有\(zhòng)0,buf+strlen(buf),都會(huì)來到有效數(shù)據(jù)的下一個(gè)位置,
如果有\(zhòng)0,那么就覆蓋其\0,畢竟我們都想要追加內(nèi)容,\0作為結(jié)束符,自然要被覆蓋,且必須被覆蓋,
如果沒有,自然在后面接著寫數(shù)據(jù)即可
發(fā)送完了協(xié)議應(yīng)答,就要發(fā)送下面的數(shù)據(jù)正文了:
打開指定的本地文件,用fd接收(注意這里的fd是代表本地文件,而之前的cfd是通信文件描述符,即套接字,二者不要混淆),之后使用read從打開的本地文件中讀數(shù)據(jù)給buf,然后再使用send將buf中的數(shù)據(jù)寫給cfd
補(bǔ)充:正則表達(dá)式(簡要介紹)
正則表達(dá)式,就是專門對字符串進(jìn)行操作的,他可以表示出你對字符串的任何需求
其中,剛剛的[ ^空格],是匹配除空格之外的任意字符
更多請查看下列網(wǎng)站:
補(bǔ)充:上述代碼中用到的函數(shù)的原型
strncasecmp
stat
下面是使用stat的宏來判斷“使用stat搜索的文件”是文件還是文件夾: