利用C語言實(shí)現(xiàn)http服務(wù)器(Linux)
這篇文章是我的生產(chǎn)實(shí)習(xí)報(bào)告,在Linux操作系統(tǒng)上實(shí)現(xiàn)的一個(gè)簡單的HTTP服務(wù)器,也算是一個(gè)小項(xiàng)目。請大家多多指教。
一、實(shí)習(xí)目的
本次實(shí)習(xí)緊緊圍繞Linux操作系統(tǒng)基礎(chǔ)知識展開,主要學(xué)習(xí)了Linux系統(tǒng)的常用命令、gcc編譯鏈接過程、多線程通信和同步技術(shù)、socket網(wǎng)絡(luò)通信、HTTP服務(wù)器等內(nèi)容。與此同時(shí),在老師的帶領(lǐng)下進(jìn)行實(shí)操訓(xùn)練,例如:編寫Makefile文件管理工程、實(shí)現(xiàn)靜態(tài)庫和動(dòng)態(tài)庫、模仿系統(tǒng)bash實(shí)現(xiàn)自己的命令解釋器、編寫多線程程序并實(shí)現(xiàn)同步、實(shí)現(xiàn)TCP/UDP服務(wù)器端和客戶端進(jìn)行通信等。
最后通過獨(dú)立完成一個(gè)基于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í)處理多個(gè)客戶端請求。
二、實(shí)習(xí)項(xiàng)目及內(nèi)容
2.1開發(fā)平臺
本項(xiàng)目是基于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項(xiàng)目功能
本項(xiàng)目設(shè)計(jì)的http服務(wù)器是一個(gè)輕量級的服務(wù)器,使用Reactor模式,即主線程只負(fù)責(zé)監(jiān)聽文件描述符上是否有事件發(fā)生,有的話立即將該事件通知工作線程。除此之外,主線程不做其他實(shí)質(zhì)性的工作。讀寫數(shù)據(jù),接受新的連接,以及處理客戶請求均在工作線程中完成。
本項(xiàng)目的基本功能如下:
(1)能接收客戶端的GET請求;
(2)能夠解析客戶端的請求報(bào)文,根據(jù)客戶端要求找到相應(yīng)的資源;
(3)能夠回復(fù)http應(yīng)答報(bào)文;
(4)能夠讀取服務(wù)器中存儲的文件,并返回給請求客戶端,實(shí)現(xiàn)對外發(fā)布靜態(tài)資源;
(5)使用I/O復(fù)用來提高處理請求的并發(fā)度;
(6)服務(wù)器端支持錯(cuò)誤處理,如要訪問的資源不存在時(shí)回復(fù)404錯(cuò)誤等。
2.3技能儲備
為了完成本項(xiàng)目,實(shí)現(xiàn)本項(xiàng)目的具體功能,需要具有一定的技能儲備作為技術(shù)支撐。
首先應(yīng)該掌握Linux操作系統(tǒng)的常用命令,C語言基礎(chǔ),熟練使用vim、gcc編譯器、gdb等工具,Linux平臺上進(jìn)行程序的編寫、編譯以及調(diào)試能力,socket網(wǎng)絡(luò)通信的編程能力,I/O復(fù)用理論知識以及編程能力,多線程編程能力,以及一定的HTML語言能力。
三、項(xiàng)目設(shè)計(jì)
3.1設(shè)計(jì)概述
本項(xiàng)目是基于Linux操作系統(tǒng),使用C語言實(shí)現(xiàn)的輕量級http服務(wù)器。使用socket網(wǎng)絡(luò)編程技術(shù)實(shí)現(xiàn)服務(wù)器端和客戶端之間的通信。同時(shí),為了提高本服務(wù)器的并發(fā)處理性能,本次http服務(wù)器設(shè)計(jì)使用Reactor模式。通過I/O復(fù)用和線程池相結(jié)合,實(shí)現(xiàn)同時(shí)響應(yīng)多個(gè)客戶端的請求,保證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ù)可讀時(shí),epoll_wait 通知主線程。主線程則將socket可讀事件放入消息隊(duì)列。
(4)一旦放入消息隊(duì)列便創(chuàng)建相應(yīng)的線程即工作線程,在線程函數(shù)中處理客戶端信息,然后往epoll內(nèi)核事件表中注冊該socket上的寫就緒事件。
(5)主線程調(diào)用epoll_ wait 等待socket可寫。
(6)當(dāng)socket可寫時(shí),epoll _wait 通知主線程。主線程將socket可寫事件放入消息隊(duì)列。
(7)創(chuàng)建工作線程,往socket上寫入服務(wù)器處理客戶請求的結(jié)果。

3.3 socket網(wǎng)絡(luò)編程
本項(xiàng)目通過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)答報(bào)文設(shè)計(jì)
如果客戶端請求響應(yīng)成功,則想客戶端發(fā)送成功應(yīng)答報(bào)文。如下表所示:
表3-1 請求成功的應(yīng)答報(bào)文

如果客戶端請求響應(yīng)失敗,例如服務(wù)器端沒有客戶端所請求的資源,則回復(fù)失敗報(bào)文。如下表所示:
表3-2 請求失敗應(yīng)答報(bào)文

四、代碼實(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)建消息隊(duì)列
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); //向消息隊(duì)列發(fā)送消息
}
}
}
}
}
主函數(shù)中主要調(diào)用各個(gè)封裝好的方法函數(shù),首先調(diào)用創(chuàng)建套接字函數(shù),創(chuàng)建套接字,然后創(chuàng)建消息隊(duì)列。接著創(chuàng)建線程池,子線程同時(shí)執(zhí)行l(wèi)oop_thread線程函數(shù),將在 msgrcv處阻塞,等待獲取消息隊(duì)列中的消息。主線程調(diào)用epoll_create方法創(chuàng)建內(nèi)核事件表,調(diào)用epoll_add函數(shù)添加描述符和事件。接著使用epoll_wait方法獲取就緒描述符,一旦獲取到就緒描述符便向消息隊(duì)列中發(fā)送消息,便可以解除子線程中消息隊(duì)列的阻塞,執(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);//從消息隊(duì)列中讀取消息
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ā)送錯(cuò)誤應(yīng)答報(bào)文函數(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)答報(bào)文函數(shù)
{
printf("主動(dòng)關(guān)閉連接\n");
epoll_del(epfd,c);
close(c);
continue;
}
}
epoll_mod(epfd,c);//調(diào)用重置函數(shù)
}
}
線程將在 msgrcv處阻塞,等待獲取消息隊(duì)列中的消息,判斷描述符類型,進(jìn)行accept操作或recv操作。收到客戶端請求數(shù)據(jù)后再調(diào)用get_filename函數(shù)獲取客戶端請求的資源名稱,再判斷發(fā)送錯(cuò)誤或者正確應(yīng)答報(bào)文。
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("請求報(bào)文錯(cuò)誤\n");
return NULL;
}
printf("請求方法:%s\n",s);
s = strtok_r(NULL," ",&ptr);
if ( s == NULL )
{
printf("請求報(bào)文 無資源名字\n");
return NULL;
}
if ( strcmp(s,"/") == 0 )
{
return "/index.html";
}
return s;
}
通過這個(gè)函數(shù)來解析客戶端請求報(bào)文,獲取資源名稱。
4.1.6發(fā)送正確應(yīng)答報(bào)文函數(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");//分隔報(bào)頭和數(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)答報(bào)文。
4.1.7發(fā)送錯(cuò)誤應(yīng)答報(bào)文函數(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");//分隔報(bào)頭和數(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)答報(bào)文。
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端和移動(dòng)手機(jī)端進(jìn)行網(wǎng)頁測試。
本次測試用例及預(yù)期結(jié)果如下表所示:
表4-1 測試用例及結(jié)果

首先PC端在瀏覽器地址欄輸入服務(wù)器所在的IP地址進(jìn)行訪問,可以成功獲取到服務(wù)器端的index頁面,如圖所示。

點(diǎn)擊下一頁,跳轉(zhuǎn)到test頁面。如圖所示:

在訪問地址后隨意追加錯(cuò)誤訪問信息,即訪問客戶端不存在的資源,得到404err頁面。如圖所示:

訪問客戶端存在的資源,讀取到的相應(yīng)的資源。例如輸入1.116.157.150\3.jpg或2.jpg。得到圖片內(nèi)容,如圖所示:


移動(dòng)手機(jī)端測試同理,同樣可以得到相應(yīng)的結(jié)果如下圖所示:

最后,為測試http服務(wù)器端的并發(fā)性,同時(shí)使用多個(gè)客戶端進(jìn)行連接,同時(shí)訪問服務(wù)器端的資源,均可正常運(yùn)行。服務(wù)器端打印的部分請求信息如下圖:

通過測試結(jié)果顯示,本http服務(wù)器實(shí)現(xiàn)了對外發(fā)布的靜態(tài)資源的功能,并且對于錯(cuò)誤的訪問信息可以進(jìn)行處理回復(fù)。并且,通過多用戶同時(shí)訪問測試結(jié)果顯示,該服務(wù)器具有較好的并發(fā)性,能夠滿足一定客戶端同時(shí)請求資源的需求。
到此這篇關(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ì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
C語言實(shí)現(xiàn)校園導(dǎo)游系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)校園導(dǎo)游系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03

