項目前言
此項目分爲三部分:
- 第一部分爲tinyhttpd,完成http請求中的get與post請求處理,返回服務器響應(完成動態請求(cgi程序)與靜態請求),目的:瞭解C/S模式的基本框架與服務器與客戶端運行基本流程
- 第二部分爲基於socket與linux的多路複用I/O的epoll方法來編寫的獨立的客戶端和服務器通信程序,服務器能接收客戶端發送的消息並給客戶端回信,目的:熟悉C/S模式,爲以後加入線程池高併發做準備
- 第三部分將前兩部分項目結合並改造爲支持線程池,I/O多路複用epoll的高併發服務器
C/S模式下的常規流程
主要就是這幾個函數結合多線程,I/O複用,併發處理,才能搭建很複雜的web服務器。
項目一:Tinyhttpd
Tinyhttp是一個輕量型Http Server,使用C語言開發,全部代碼只500多行,還包括一個簡單Client.Tinyhttp程序的邏輯爲:一個無線循環,一個請求,創建一個線程,之後線程函數處理每個請求,然後解析HTTP請求,做一些判斷,之後判斷文件是否可執行,不可執行,打開文件,輸出給客戶端(瀏覽器),可執行就創建管道,父子進程進行通信。
Tinyhttpd框架描述:
項目二:客戶端和服務器通信程序
客戶端和服務端都運行在xshell遠程連接的服務器上。分別創建server.c和client.c源文件,編譯運行就行。
/*客戶端*/
#include <fcntl.h> // for open
#include <unistd.h> // for close
#include<stdio.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 4096
int main(int argc, char** argv)
{
int sockfd, n;
char recvline[4096], sendline[4096], buff[4096];
struct sockaddr_in servaddr;
if (argc != 2) {
printf("輸入格式爲 ./client.out ipaddress\n");
exit(0);
}
//創建客戶端socket描述符sockfd,AF_INET指的是ipv4,SOCK_STREAM提供可靠連接(TCP協議),創建失敗返回-1
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
//地址三元組結構體初始化
memset(&servaddr, 0, sizeof(servaddr));
//地址三元組結構體賦值
servaddr.sin_family = AF_INET;
//sin_port端口號,必須要通過 htons 轉換爲網絡格式
servaddr.sin_port = htons(6666);
///inet_pton將點分十進制的ip地址轉化爲用於網絡傳輸的數值格式
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
printf("inet_pton error for %s\n", argv[1]);
exit(0);
}
//連接服務器
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
printf("連接建立成功,請輸入發送給服務器的信息: \n");
while(1)
{
//將輸入的數據存放在數組sendline中,最大4096
fgets(sendline, 4096, stdin);
if (send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
n = recv(sockfd, buff, MAXLINE, 0);
buff[n] = '\0';
printf("收到來自服務器的信息爲: %s\n", buff);
}
close(sockfd);
exit(0);
}
其中,main函數中argc和argv參數在用命令行編譯程序時有用。main( int argc, char* argv[], char **env ) 中第一個參數,int型的argc,爲整型,用來統計程序運行時發送給main函數的命令行參數的個數,在VS中默認值爲1。第二個參數,char*型的argv[],爲字符串數組,用來存放指向的字符串參數的指針數組,每一個元素指向一個參數。
/*服務器端*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <fcntl.h> // for open
#include <unistd.h> // for close
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 4096
int main(int argc, char** argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[4096],sendline[4096];
int n;
//創建服務器socket
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
//綁定服務器的ip與端口號與socket描述符在一起
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
//開始監聽客戶端請求
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
printf("======waiting for client's request======\n");
if ((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {
printf("accept socket error: %s(errno: %d)", strerror(errno), errno);
exit(0);
}
while (1)
{
n = recv(connfd, buff, MAXLINE, 0);
buff[n] = '\0';
printf("收到來自客戶端的信息爲: %s\n", buff);
//給客戶端回覆消息
fgets(sendline, 4096, stdin);
if (send(connfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
}
close(connfd);
close(listenfd);
}
項目三:Tinyhttpd_with_threadpool_epoll
簡介:本項目是在linux下基於Tinyhttpd擴展實現的不到700行的超輕量型Http
Server,基於epoll事件驅動I/O,採用高效的Reactor模型+線程池進行客戶端連接任務管理,支持高併發的靜態與動態http請求。詳見github項目
主要技術知識點
- epoll I/O複用技術(參考資料1)
- 動態請求解析技術Cgi(參考資料1,參考資料2)
- Reactor模式(參考資料)
- 線程池(參考資料)
- Socket網絡編程相關知識(參考資料1,參考資料2)
- http報文格式(參考資料1,參考資料2)
- http請求命令get/post(參考資料1,參考資料2)
- 進程通信(管道pipe)(參考資料1)
壓力測試
用ab壓力對服務器進行壓力測試
參考資料1
參考資料2
參考資料3
參考資料:各類支持高併發的服務器
(1)Zaver
主要思路:
(1)採用框架:
基於事件驅動的單進程 + Non-Blocking模型(Reactor 模型)
(2)框架優勢:
採用單進程模型,避免了系統分配多進程及進程之間通信的開銷,同時降低了內存的耗用。由於採用了單進程模型,爲了更好的提高進程利用率,將默認 Blocking 的 I/O 設置爲 Non-Blocking I/O,即在線程讀/寫數據的過程中,如果緩衝區爲空/緩衝區滿,線程不會阻塞,而是立即返回,並設置 errno;事件循環用作事件通知,如果listenfd上可讀,則調用accept,把新建的fd加入epoll創建的隊列裏面,等工作線程來拿。使用二叉堆來實現timer的功能,用來刪除不活躍鏈接。使用HTTP 狀態機(Parser)來維持當前的解析狀態。
Zaver參考資料1
C++實現高性能Web服務器-zaver
(2)webServer
主要技術及知識點:
- epoll事件驅動I/O
- 使用Reactor模型+線程池進行客戶端連接任務管理
- 使用RAII方法管理內存資源
- 使用eventfd事件進行線程喚醒
- 模仿muduo網絡庫實現的簡單eventLoop循環
- 利用優先隊列實現的定時器
代碼結構:
- Epoll類:實現對系統調用epoll方法的封裝,包括函數epoll_ctl,epoll_wait,實現事件的增加刪除管理,並且將就緒的事件對應的文件描述符添加到消息隊列中等待線程池分配工作線程
- EventLoop類:實現事件循環,若有事件到達,則喚醒阻塞在epoll系統調用上的線程
- Task類:實現一個連接對應一個task指針,以及對應的回調函數
- Thread類:實現基於struct集合的線程信息管理
- ThreadPool類:實現線程池方法,包括創建與分配線程
- EventLoopThread類:實現線程的創建
- Server類:實現服務器流程,啓動端口監聽,創建線程池,設置epoll獲取就緒隊列,用eventfd從線程池中喚醒線程
知識點
- 線程池
- epoll I/O複用技術
- Reactor模式
(3)其他服務器資料
高性能HTTP服務器:設計和思路
利用epoll實現高併發聊天室demo-代碼
利用epoll實現高併發聊天室demo-資料
Linux下C++輕量級Web服務器開發/配套資料