利用C語言實(shí)現(xiàn)http服務(wù)器(Linux)
這篇文章是我的生產(chǎn)實(shí)習(xí)報告,在Linux操作系統(tǒng)上實(shí)現(xiàn)的一個簡單的HTTP服務(wù)器,也算是一個小項目。請大家多多指教。
一、實(shí)習(xí)目的
本次實(shí)習(xí)緊緊圍繞Linux操作系統(tǒng)基礎(chǔ)知識展開,主要學(xué)習(xí)了Linux系統(tǒng)的常用命令、gcc編譯鏈接過程、多線程通信和同步技術(shù)、socket網(wǎng)絡(luò)通信、HTTP服務(wù)器等內(nèi)容。與此同時,在老師的帶領(lǐng)下進(jìn)行實(shí)操訓(xùn)練,例如:編寫Makefile文件管理工程、實(shí)現(xiàn)靜態(tài)庫和動態(tài)庫、模仿系統(tǒng)bash實(shí)現(xiàn)自己的命令解釋器、編寫多線程程序并實(shí)現(xiàn)同步、實(shí)現(xiàn)TCP/UDP服務(wù)器端和客戶端進(jìn)行通信等。
最后通過獨(dú)立完成一個基于Linux平臺C語言編寫的http服務(wù)器,鞏固課程學(xué)到的Linux平臺上的編程規(guī)范、技術(shù)和技巧,增強(qiáng)對于Linux操作系統(tǒng)的熟練度,培養(yǎng)我們編寫較大型程序的能力,培養(yǎng)底層軟件開發(fā)的能力,并為將來從事Linux平臺開發(fā)、嵌入式開發(fā)等相對高端的軟件開發(fā)工作打下基礎(chǔ)。
本次實(shí)習(xí)具體目的如下:
(1)掌握并熟練使用Linux操作系統(tǒng)常用命令;
(2)熟練使用vim、gcc編譯器、gdb等工具在Linux平臺上進(jìn)行程序的編寫、編譯以及調(diào)試;
(3)使用C語言編寫輕量級http服務(wù)器實(shí)現(xiàn)發(fā)布靜態(tài)頁面功能;
(4)采用線程池和I/O復(fù)用方法實(shí)現(xiàn)同時處理多個客戶端請求。
二、實(shí)習(xí)項目及內(nèi)容
2.1開發(fā)平臺
本項目是基于Linux系統(tǒng)C語言實(shí)現(xiàn)的http服務(wù)器,開發(fā)環(huán)境如下:
開發(fā)平臺:騰訊云服務(wù)器
操作系統(tǒng):Ubuntu Server 20.04 LTS 64bit
CPU:2核
內(nèi)存:4GB
系統(tǒng)盤:60GB SSD云硬盤
2.2項目功能
本項目設(shè)計的http服務(wù)器是一個輕量級的服務(wù)器,使用Reactor模式,即主線程只負(fù)責(zé)監(jiān)聽文件描述符上是否有事件發(fā)生,有的話立即將該事件通知工作線程。除此之外,主線程不做其他實(shí)質(zhì)性的工作。讀寫數(shù)據(jù),接受新的連接,以及處理客戶請求均在工作線程中完成。
本項目的基本功能如下:
(1)能接收客戶端的GET請求;
(2)能夠解析客戶端的請求報文,根據(jù)客戶端要求找到相應(yīng)的資源;
(3)能夠回復(fù)http應(yīng)答報文;
(4)能夠讀取服務(wù)器中存儲的文件,并返回給請求客戶端,實(shí)現(xiàn)對外發(fā)布靜態(tài)資源;
(5)使用I/O復(fù)用來提高處理請求的并發(fā)度;
(6)服務(wù)器端支持錯誤處理,如要訪問的資源不存在時回復(fù)404錯誤等。
2.3技能儲備
為了完成本項目,實(shí)現(xiàn)本項目的具體功能,需要具有一定的技能儲備作為技術(shù)支撐。
首先應(yīng)該掌握Linux操作系統(tǒng)的常用命令,C語言基礎(chǔ),熟練使用vim、gcc編譯器、gdb等工具,Linux平臺上進(jìn)行程序的編寫、編譯以及調(diào)試能力,socket網(wǎng)絡(luò)通信的編程能力,I/O復(fù)用理論知識以及編程能力,多線程編程能力,以及一定的HTML語言能力。
三、項目設(shè)計
3.1設(shè)計概述
本項目是基于Linux操作系統(tǒng),使用C語言實(shí)現(xiàn)的輕量級http服務(wù)器。使用socket網(wǎng)絡(luò)編程技術(shù)實(shí)現(xiàn)服務(wù)器端和客戶端之間的通信。同時,為了提高本服務(wù)器的并發(fā)處理性能,本次http服務(wù)器設(shè)計使用Reactor模式。通過I/O復(fù)用和線程池相結(jié)合,實(shí)現(xiàn)同時響應(yīng)多個客戶端的請求,保證http服務(wù)器的并發(fā)性。
3.2 Reactor模式
Reactor模式是指主線程只負(fù)責(zé)監(jiān)聽文件描述符上是否有事件發(fā)生,有的話立即將該事件通知工作線程。除此之外,主線程不做其他實(shí)質(zhì)性的工作。讀寫數(shù)據(jù),接受新的連接,以及處理客戶請求均在工作線程中完成。
工作流程如下:
(1)主線程往epoll內(nèi)核事件表中注冊socket上的讀就緒事件。
(2)主線程調(diào)用epoll_wait等待socket上有數(shù)據(jù)可讀。
(3)當(dāng)socket上有數(shù)據(jù)可讀時,epoll_wait 通知主線程。主線程則將socket可讀事件放入消息隊列。
(4)一旦放入消息隊列便創(chuàng)建相應(yīng)的線程即工作線程,在線程函數(shù)中處理客戶端信息,然后往epoll內(nèi)核事件表中注冊該socket上的寫就緒事件。
(5)主線程調(diào)用epoll_ wait 等待socket可寫。
(6)當(dāng)socket可寫時,epoll _wait 通知主線程。主線程將socket可寫事件放入消息隊列。
(7)創(chuàng)建工作線程,往socket上寫入服務(wù)器處理客戶請求的結(jié)果。
3.3 socket網(wǎng)絡(luò)編程
本項目通過socket網(wǎng)絡(luò)編程技術(shù)實(shí)現(xiàn)http服務(wù)器端和客戶端實(shí)現(xiàn)通信。并且采用的是TCP協(xié)議。
TCP 提供的是面向連接的、可靠的、字節(jié)流服務(wù)。TCP 的服務(wù)器端和客戶端編程流程如下圖:
3.4 http服務(wù)器應(yīng)答報文設(shè)計
如果客戶端請求響應(yīng)成功,則想客戶端發(fā)送成功應(yīng)答報文。如下表所示:
表3-1 請求成功的應(yīng)答報文
如果客戶端請求響應(yīng)失敗,例如服務(wù)器端沒有客戶端所請求的資源,則回復(fù)失敗報文。如下表所示:
表3-2 請求失敗應(yīng)答報文
四、代碼實(shí)現(xiàn)及運(yùn)行結(jié)果
4.1主要功能實(shí)現(xiàn)
4.1.1 主函數(shù)
int main() { signal(SIGPIPE,sig_fun); sockfd = socket_init();//調(diào)用創(chuàng)建套接字函數(shù) if ( sockfd == -1 ) { exit(0); } msgid = msgget((key_t)1234,IPC_CREAT|0600);//創(chuàng)建消息隊列 if ( msgid == -1 ) { exit(0); } pthread_t id[4]; for( int i = 0; i < 4; i++ ) //循環(huán)創(chuàng)建線程池 { pthread_create(&id[i],NULL,loop_thread,NULL); } epfd = epoll_create(MAXFD);//創(chuàng)建內(nèi)核事件表 if ( epfd == -1 ) { printf("create epoll err\n"); exit(0); } epoll_add(epfd,sockfd);//調(diào)用封裝的函數(shù)添加描述符和事件 struct epoll_event evs[MAXFD]; while( 1 ) { int n = epoll_wait(epfd,evs,MAXFD,-1);//獲取就緒描述符 if( n == -1 ) { continue; } else { struct mess m; m.type = 1; for(int i = 0; i < n; i++ ) { m.c = evs[i].data.fd; if ( evs[i].events & EPOLLIN ) { msgsnd(msgid,&m,sizeof(int),0); //向消息隊列發(fā)送消息 } } } } }
主函數(shù)中主要調(diào)用各個封裝好的方法函數(shù),首先調(diào)用創(chuàng)建套接字函數(shù),創(chuàng)建套接字,然后創(chuàng)建消息隊列。接著創(chuàng)建線程池,子線程同時執(zhí)行l(wèi)oop_thread線程函數(shù),將在 msgrcv處阻塞,等待獲取消息隊列中的消息。主線程調(diào)用epoll_create方法創(chuàng)建內(nèi)核事件表,調(diào)用epoll_add函數(shù)添加描述符和事件。接著使用epoll_wait方法獲取就緒描述符,一旦獲取到就緒描述符便向消息隊列中發(fā)送消息,便可以解除子線程中消息隊列的阻塞,執(zhí)行子線程中的程序,連接客戶端,實(shí)現(xiàn)通信。
4.1.2創(chuàng)建套接字函數(shù)
int socket_init() { int sockfd = socket(AF_INET,SOCK_STREAM,0); if ( sockfd == -1 ) { return -1; } struct sockaddr_in saddr; memset(&saddr,0,sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_port = htons(80); saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); if ( res == -1 ) { printf("bind err\n"); return -1; } res = listen(sockfd,5); if ( res == -1 ) { return -1; } return sockfd; }
將初始化創(chuàng)建套接字函數(shù)封裝。
4.1.3線程函數(shù)
void* loop_thread(void* arg) { while( 1 ) { struct mess m; msgrcv(msgid,&m,sizeof(int),1,0);//從消息隊列中讀取消息 int c = m.c; if ( c == sockfd ) { struct sockaddr_in caddr; int len = sizeof(caddr); int cli = accept(sockfd,(struct sockaddr*)&caddr,&len); if ( cli < 0 ) { continue; } epoll_add(epfd,cli); } else { char buff[1024] = {0}; int n = recv(c,buff,1023,0); if ( n <= 0 ) { epoll_del(epfd,c);//調(diào)用移除描述符函數(shù) close(c); printf("close\n"); continue; } char* filename = get_filename(buff);//調(diào)用資源名獲取函數(shù) if ( filename == NULL ) { send_404status(c);//調(diào)用發(fā)送錯誤應(yīng)答報文函數(shù) epoll_del(epfd,c);//調(diào)用移除描述符函數(shù) close(c); continue; } printf("filename:%s\n",filename); if ( send_httpfile(c,filename) == -1 )//調(diào)用發(fā)送正確應(yīng)答報文函數(shù) { printf("主動關(guān)閉連接\n"); epoll_del(epfd,c); close(c); continue; } } epoll_mod(epfd,c);//調(diào)用重置函數(shù) } }
線程將在 msgrcv處阻塞,等待獲取消息隊列中的消息,判斷描述符類型,進(jìn)行accept操作或recv操作。收到客戶端請求數(shù)據(jù)后再調(diào)用get_filename函數(shù)獲取客戶端請求的資源名稱,再判斷發(fā)送錯誤或者正確應(yīng)答報文。
4.1.4封裝epoll函數(shù)
//添加描述符函數(shù) void epoll_add(int epfd,int fd) { struct epoll_event ev; ev.data.fd = fd; ev.events = EPOLLIN | EPOLLONESHOT; if ( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1) { printf("epoll add err\n"); } } //移除描述符函數(shù) void epoll_del(int epfd, int fd ) { if ( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1 ) { printf("epoll del err\n"); } } //重置描述符函數(shù) void epoll_mod(int epfd, int fd) { struct epoll_event ev; ev.data.fd = fd; ev.events = EPOLLIN | EPOLLONESHOT; if ( epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev) == -1 ) { printf("epoll mod err\n"); } }
4.1.5獲取資源名函數(shù)
char* get_filename(char buff[]) { char* ptr = NULL; char * s = strtok_r(buff," ",&ptr); if ( s == NULL ) { printf("請求報文錯誤\n"); return NULL; } printf("請求方法:%s\n",s); s = strtok_r(NULL," ",&ptr); if ( s == NULL ) { printf("請求報文 無資源名字\n"); return NULL; } if ( strcmp(s,"/") == 0 ) { return "/index.html"; } return s; }
通過這個函數(shù)來解析客戶端請求報文,獲取資源名稱。
4.1.6發(fā)送正確應(yīng)答報文函數(shù)
int send_httpfile(int c, char* filename) { if ( filename == NULL || c < 0 ) { send(c,"err",3,0); return -1 ; } char path[128] = {PATH}; strcat(path,filename);// /home/ubuntu/ligong/day12/index.hmtl int fd = open(path,O_RDONLY); if ( fd == -1 ) { //send(c,"404",3,0); send_404status(c); return -1; } int size = lseek(fd,0,SEEK_END); lseek(fd,0,SEEK_SET); char head_buff[512] = {"HTTP/1.1 200 OK\r\n"}; strcat(head_buff,"Server: myhttp\r\n"); sprintf(head_buff+strlen(head_buff),"Content-Length: %d\r\n",size); strcat(head_buff,"\r\n");//分隔報頭和數(shù)據(jù) 空行 send(c,head_buff,strlen(head_buff),0); printf("send file:\n%s\n",head_buff); int num = 0; char data[1024] = {0}; while( ( num = read(fd,data,1024)) > 0 ) { send(c,data,num,0); } close(fd); return 0; }
如果客戶請求資源可以正常訪問,則調(diào)用該函數(shù)發(fā)送應(yīng)答報文。
4.1.7發(fā)送錯誤應(yīng)答報文函數(shù)
int send_404status(int c) { int fd = open("err404.html",O_RDONLY); if ( fd == -1 ) { send(c,"404",3,0); return 0; } int size = lseek(fd,0,SEEK_END); lseek(fd,0,SEEK_SET); char head_buff[512] = {"HTTP/1.1 404 Not Found\r\n"}; strcat(head_buff,"Server: myhttp\r\n"); sprintf(head_buff+strlen(head_buff),"Content-Length: %d\r\n",size); strcat(head_buff,"\r\n");//分隔報頭和數(shù)據(jù) 空行 send(c,head_buff,strlen(head_buff),0); char data[1024] = {0}; int num = 0; while( ( num = read(fd,data,1024)) > 0 ) { send(c,data,num,0); } close(fd); return 0; }
如果客戶端訪問的在服務(wù)器端資源不存在,則調(diào)用該函數(shù)發(fā)送應(yīng)答報文。
4.1.8 index.htlm
<html> <head> <meta charset=utf8> <title>baixingyu</title> </head> <body background="R-C.jpg"> <center> <h2>bxy</h2> </center> <a href="test.html">下一頁</a> </body> </html>
4.1.9 test.html
<html> <head> <meta charset=utf8> <title>測試</title> </head> <body> <center> <h2>小狗小狗 </center> <a href="index.html">返回</a> </body> </html>
4.1.10 404err.html
<html> <head> <meta charset=utf8> <title>訪問失敗</title> </head> <body background="1.jpg"> <center> <h2>頁面走丟了 </center> </body> </html>
4.2測試及運(yùn)行結(jié)果
為了測試http服務(wù)器是否能夠正常運(yùn)行,并且實(shí)現(xiàn)上文提到的功能,分別采用了PC端和移動手機(jī)端進(jìn)行網(wǎng)頁測試。
本次測試用例及預(yù)期結(jié)果如下表所示:
表4-1 測試用例及結(jié)果
首先PC端在瀏覽器地址欄輸入服務(wù)器所在的IP地址進(jìn)行訪問,可以成功獲取到服務(wù)器端的index頁面,如圖所示。
點(diǎn)擊下一頁,跳轉(zhuǎn)到test頁面。如圖所示:
在訪問地址后隨意追加錯誤訪問信息,即訪問客戶端不存在的資源,得到404err頁面。如圖所示:
訪問客戶端存在的資源,讀取到的相應(yīng)的資源。例如輸入1.116.157.150\3.jpg或2.jpg。得到圖片內(nèi)容,如圖所示:
移動手機(jī)端測試同理,同樣可以得到相應(yīng)的結(jié)果如下圖所示:
最后,為測試http服務(wù)器端的并發(fā)性,同時使用多個客戶端進(jìn)行連接,同時訪問服務(wù)器端的資源,均可正常運(yùn)行。服務(wù)器端打印的部分請求信息如下圖:
通過測試結(jié)果顯示,本http服務(wù)器實(shí)現(xiàn)了對外發(fā)布的靜態(tài)資源的功能,并且對于錯誤的訪問信息可以進(jìn)行處理回復(fù)。并且,通過多用戶同時訪問測試結(jié)果顯示,該服務(wù)器具有較好的并發(fā)性,能夠滿足一定客戶端同時請求資源的需求。
到此這篇關(guān)于利用C語言實(shí)現(xiàn)http服務(wù)器(Linux)的文章就介紹到這了,更多相關(guān)C語言 http服務(wù)器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于C++實(shí)現(xiàn)酒店管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了基于C++實(shí)現(xiàn)酒店管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03C語言實(shí)現(xiàn)校園導(dǎo)游系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)校園導(dǎo)游系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03