小型web伺服器的實現(C++)
一、具體功能實現
- GET方法請求解析
- POST方法請求解析
- 返回請求資源頁面
- 利用GET方法實現加減法
- 利用POST方法實現加減法
- HTTP請求行具體解析
- 400、403、404錯誤碼返回的處理
二、什麼是web伺服器
- web伺服器就是在物理伺服器基礎上的具有服務端功能的網路連線程式,簡而言之就是處理客戶端發來的各種請求然後根據伺服器的邏輯處理返回一個結果給客戶端。在web伺服器和客戶端之間的通訊是基於HTTP協議進行的。而客戶端可以是瀏覽器也可以是支援HTTP協議的APP。
- 那麼瀏覽器應該怎麼連線上自己的web伺服器呢,最簡單的web伺服器就是通過TCP三次握手建立連線後,伺服器直接返回一個結果給瀏覽器。瀏覽器和伺服器是通過TCP三路握手建立連線的。瀏覽器在通過URL(統一資源定位符,就是我們俗稱的網路地址)去請求伺服器的連線,並且通過URL中的路徑請求伺服器上的資源。舉個栗子就是這樣的:
最簡單的web伺服器:
#include<stdio.h> #include<stdlib.h> #include<sys/socket.h> #include<sys/types.h> #include<sys/stat.h> #include<sys/sendfile.h> #include<fcntl.h> #include<netinet/in.h> #include<arpa/inet.h> #include<assert.h> #include<unistd.h> #include<string.h> const int port = 8888; int main(int argc,char *argv[]) { if(argc<0) { printf("need two canshu\n"); return 1; } int sock; int connfd; struct sockaddr_in sever_address; bzero(&sever_address,sizeof(sever_address)); sever_address.sin_family = PF_INET; sever_address.sin_addr.s_addr = htons(INADDR_ANY); sever_address.sin_port = htons(8888); sock = socket(AF_INET,SOCK_STREAM,0); assert(sock>=0); int ret = bind(sock, (struct sockaddr*)&sever_address,sizeof(sever_address)); assert(ret != -1); ret = listen(sock,1); assert(ret != -1); while(1) { struct sockaddr_in client; socklen_t client_addrlength = sizeof(client); connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); if(connfd<0) { printf("errno\n"); } else{ char request[1024]; recv(connfd,request,1024,0); request[strlen(request)+1]='\0'; printf("%s\n",request); printf("successeful!\n"); char buf[520]="HTTP/1.1 200 ok\r\nconnection: close\r\n\r\n";//HTTP響應 int s = send(connfd,buf,strlen(buf),0);//傳送響應 //printf("send=%d\n",s); int fd = open("hello.html",O_RDONLY);//訊息體 sendfile(connfd,fd,NULL,2500);//零拷貝傳送訊息體 close(fd); close(connfd); } } return 0; }
最簡單的html檔案:
<html>
<body bgcolor="blue">
this is the html.
<hr>
<p>hello word! waste young! </p><br>
</body>
</html>
執行web.c檔案,生成執行檔案a.out,在終端執行後,我們在瀏覽器的網址欄中輸入:http://localhost:8888 然後確認後,就會返回hello.html的檔案頁面
這裡的URL,localhost:實際就是hostname,然後8888是埠,如果在埠後面再加上比如/hello.html這樣的路徑就表示請求伺服器上的一個hello.html,這裡請求方法是GET,所以要求伺服器返回該資源的頁面。
那麼此時再來看下伺服器接收到的東西,就是HTTP請求。
第一行就是請求行,請求行的格式是這樣的:請求方法+空格+URL+空格+協議版本+\r+\n 這裡的請求方法是GET ,URL是/(在這裡,URL就相當於資源的路徑,若在網址欄輸入的是http://localhost:8888/hello.html的話,這裡瀏覽器傳送過來的URL就是/hello.html),協議版本是HTTP/1.1(現在多數協議版本都是這個)。
第二行到最後一行都是請求頭部,請求頭部的格式是這樣的: 頭部欄位:+空格+數值+\r+\n 然後多個頭部子段組織起來就是請求頭部,在最後的頭部欄位的格式中需要有兩個換行符號,最後一行的格式是:頭部欄位:+空格+數值+\r+\n+\r+\n 因為在後面還要跟著請求資料,為了區分請求資料和請求頭的結束,就多了一個換行符。
三、HTTP請求和響應
(1)HTTP請求
簡而言之就是客戶端傳送給服務端的請求。請求格式上面略提到了一點點,大概的格式就如下所示:
其中的細節就很多了,但是主要的是請求方法。其中頭部欄位有很多,大家可以上網百度。主要實現的就是GET方法和POST方法,其中GET方法是請求資源,但是不改變伺服器上資源的,POST方法的話就會請求更改伺服器上的資源。除了這兩個方法外,還有PUT,DELETE,HEAD,TRACE等等。對應增刪查改的就是PUT、DELETE、POST、GET。
(2)HTTP響應
HTTP響應就是服務端返回給客戶端的響應訊息。響應格式大概如下:
這裡大概用的是200,400,403,404,其中頭部欄位需要注意content-length,在伺服器中響應碼若沒有訊息題的長度,瀏覽器就只能通過關閉客戶端才可以得知訊息體的長度,才可以顯示出訊息體的具體表現。而且訊息體的長度必須要和訊息體吻合。如果服務端傳送的訊息體長度不正確的話,會導致超時或者瀏覽器一直顯示不了要的資原始檔。詳細可以參考部落格:https://www.cnblogs.com/lovelacelee/p/5385683.html
四、如何寫出小型 web伺服器
1、程式碼預備知識
- 瞭解TCP三次握手和TCP四次揮手
- 執行緒同步機制包裝類
- 執行緒池建立
- epoll多路複用
(1)TCP三次握手
- 伺服器需要準備好接受外來連線,通過socket bind listen三個函式完成,然後我們稱為被動開啟。
- 客戶則通過connect發起主動連線請求,這就導致客戶TCP傳送一個SYN(同步)分節去告訴伺服器客戶將在待建立的連線中傳送的資料的初始序列號,通常SYN不攜帶資料,其所在IP資料只有一個IP首部,一個TCP首部以及可能有的TCP選項。
- 伺服器確認客戶的SYN後,同時自己也要傳送一個SYN分節,它含有伺服器將在同一個連線中傳送的資料的初始化列序號,伺服器在單個分節中傳送SYN和對客戶SYN的確認
- 客戶必須去確認伺服器的SYN
(2)TCP四次揮手
- 某一個應用程序首先呼叫close,稱為該端執行主動關閉,該端的TCP會發送一個FIN分節,表示資料已經發送完畢
- 接到FIN的對端將執行被動關閉,這個FIN由TCP確認,它的接受也作為一個檔案結束符傳遞給接收端應用程序(放在已排隊等候該應用程序接收的任何其他資料之後),因為FIN的接收意味著接收端應用程序在相應連線上已無額外資料可以接收
- 一段時間後,接收到這個檔案結束符的應用程序會呼叫close關閉它的套接字,這會導致它的TCP也要傳送一個FIN
- 接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN
(3)執行緒池的建立
我用的是半同步/半反應堆執行緒池。該執行緒池通用性比較高,主執行緒一般往工作佇列中加入任務,然後工作執行緒等待後並通過競爭關係從工作佇列中取出任務並且執行。而且應用到伺服器程式中的話要保證客戶請求都是無狀態的,因為同一個連線上的不同請求可能會由不同的執行緒處理。
ps:若工作佇列為空,則執行緒就處於等待狀態,就需要同步機制的處理。
程式碼:
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include<iostream>
#include<list>
#include<cstdio>
#include<semaphore.h>
#include<exception>
#include<pthread.h>
#include"myhttp_coon.h"
#include"mylock.h"
using namespace std;
template<typename T>
/*執行緒池的封裝*/
class threadpool
{
private:
int max_thread;//執行緒池中的最大執行緒總數
int max_job;//工作佇列的最大總數
pthread_t *pthread_poll;//執行緒池陣列
std::list<T*> m_myworkqueue;//請求佇列
mylocker m_queuelocker;//保護請求佇列的互斥鎖
sem m_queuestat;//由訊號量來判斷是否有任務需要處理
bool m_stop;;//是否結束執行緒
public:
threadpool();
~threadpool();
bool addjob(T* request);
private:
static void* worker(void *arg);
void run();
};
/*執行緒池的建立*/
template <typename T>
threadpool<T> :: threadpool()
{
max_thread = 8;
max_job = 1000;
m_stop = false;
pthread_poll = new pthread_t[max_thread];//為執行緒池開闢空間
if(!pthread_poll)
{
throw std::exception();
}
for(int i=0; i<max_thread; i++)
{
cout << "Create the pthread:" << i << endl;
if(pthread_create(pthread_poll+i, NULL, worker, this)!=0)
{
delete [] pthread_poll;
throw std::exception();
}
if(pthread_detach(pthread_poll[i]))//將執行緒分離
{
delete [] pthread_poll;
throw std::exception();
}
}
}
template <typename T>
threadpool<T>::~threadpool()
{
delete[] pthread_poll;
m_stop = true;
}
template <typename T>
bool threadpool<T>::addjob(T* request)
{
m_queuelocker.lock();
if(m_myworkqueue.size()> max_job)//如果請求佇列大於了最大請求佇列,則出錯
{
m_queuelocker.unlock();
return false;
}
m_myworkqueue.push_back(request);//將請求加入到請求佇列中
m_queuelocker.unlock();
m_queuestat.post();//將訊號量增加1
return true;
}
template <typename T>
void* threadpool<T>::worker(void *arg)
{
threadpool *pool = (threadpool*)arg;
pool->run();
return pool;
}
template <typename T>
void threadpool<T> :: run()
{
while(!m_stop)
{
m_queuestat.wait();//訊號量減1,直到為0的時候執行緒掛起等待
m_queuelocker.lock();
if(m_myworkqueue.empty())
{
m_queuelocker.unlock();
continue;
}
T* request = m_myworkqueue.front();
m_myworkqueue.pop_front();
m_queuelocker.unlock();
if(!request)
{
continue;
}
request->doit();//執行工作佇列
}
}
#endif
(4)同步機制的包裝類
因為採用了執行緒池,就相當於用了多執行緒程式設計,此時就需要考慮各個執行緒對公共資源的訪問的限制,因為方便之後的程式碼採用了三種包裝機制,分別是訊號量的類,互斥鎖的類和條件變數的類。在伺服器中我使用的是訊號量的類。其中訊號量的原理和System V IPC訊號量一樣(不抄書了,直接拍照了。。。)
程式碼實現:
#ifndef _MYLOCK_H
#define _MYLOCK_H
#include<iostream>
#include<list>
#include<cstdio>
#include<semaphore.h>
#include<exception>
#include<pthread.h>
#include"myhttp_coon.h"
using namespace std;
/*封裝訊號量*/
class sem{
private:
sem_t m_sem;
public:
sem();
~sem();
bool wait();//等待訊號量
bool post();//增加訊號量
};
//建立訊號量
sem :: sem()
{
if(sem_init(&m_sem,0,0) != 0)
{
throw std ::exception();
}
}
//銷燬訊號量
sem :: ~sem()
{
sem_destroy(&m_sem);
}
//等待訊號量
bool sem::wait()
{
return sem_wait(&m_sem) == 0;
}
//增加訊號量
bool sem::post()
{
return sem_post(&m_sem) == 0;
}
/*封裝互斥鎖*/
class mylocker{
private:
pthread_mutex_t m_mutex;
public:
mylocker();
~mylocker();
bool lock();
bool unlock();
};
mylocker::mylocker()
{
if(pthread_mutex_init(&m_mutex, NULL) != 0)
{
throw std::exception();
}
}
mylocker::~mylocker()
{
pthread_mutex_destroy(&m_mutex);
}
/*上鎖*/
bool mylocker::lock()
{
return pthread_mutex_lock(&m_mutex)==0;
}
/*解除鎖*/
bool mylocker::unlock()
{
return pthread_mutex_unlock(&m_mutex) == 0;
}
/*封裝條件變數*/
class mycond{
private:
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
public:
mycond();
~mycond();
bool wait();
bool signal();
};
mycond::mycond()
{
if(pthread_mutex_init(&m_mutex,NULL)!=0)
{
throw std::exception();
}
if(pthread_cond_init(&m_cond, NULL)!=0)
{
throw std::exception();
}
}
mycond::~mycond()
{
pthread_mutex_destroy(&m_mutex);
pthread_cond_destroy(&m_cond);
}
/*等待條件變數*/
bool mycond::wait()
{
int ret;
pthread_mutex_lock(&m_mutex);
ret = pthread_cond_wait(&m_cond,&m_mutex);
pthread_mutex_unlock(&m_mutex);
return ret == 0;
}
/*喚醒等待條件變數的執行緒*/
bool mycond::signal()
{
return pthread_cond_signal(&m_cond) == 0;
}
#endif
(5)epoll多路複用
epoll系列系統呼叫函式(#include<sys/epoll.h>):
int epoll_create(int size);建立核心事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);操作epoll的核心事件表
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);一段時間內等待一組檔案描述符上的就緒事件
除此這些函式外,還需要了解epoll的LT模式和ET模式還有EPOLLONESHOT事件.
下面三篇部落格瞭解下: