本文轉載自:https://blog.csdn.net/qq_36573828/article/details/82784425
一、具體功能實現
GET方法請求解析
POST方法請求解析
返回請求資源頁面
利用GET方法實現加減法
利用POST方法實現加減法
HTTP請求行具體解析
400、403、404錯誤碼返回的處理
注意:!!本人也是小白一隻,這是剛剛開始學習網絡編程寫的東西,存在很多問題。也未用到RAII等機制,純屬是披着C++皮的C語言項目,類的封裝也不是太好。也未經過壓力、性能等測試。
二、什麼是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。
然後URL就是要請求的資源路徑,協議版本爲HTTP/1.1,頭部字段根據每個頭部字段名都代表着給服務器的一個信息,具體可以根據以下網址查看:https://blog.csdn.net/sinat_22840937/article/details/64438253
(2)HTTP響應
HTTP響應就是服務端返回給客戶端的響應消息。響應格式大概如下:
其中響應首行格式如:HTTP/1.1+狀態響應碼+\r\n 狀態響應碼參考如下:https://baike.baidu.com/item/HTTP狀態碼/5053660?fr=aladdin
這裏大概用的是200,400,403,404,其中頭部字段需要注意content-length,在服務器中響應碼若沒有消息題的長度,瀏覽器就只能通過關閉客戶端纔可以得知消息體的長度,纔可以顯示出消息體的具體表現。而且消息體的長度必須要和消息體吻合。如果服務端發送的消息體長度不正確的話,會導致超時或者瀏覽器一直顯示不了要的資源文件。詳細可以參考博客:https://www.cnblogs.com/lovelacelee/p/5385683.html
四、如何寫出小型 web服務器
1、代碼預備知識
瞭解TCP三次握手和TCP四次揮手
線程同步機制包裝類
線程池創建
epoll多路複用
(1)TCP三次握手
1.服務器需要準備好接受外來連接,通過socket bind listen三個函數完成,然後我們稱爲被動打開。
2.客戶則通過connect發起主動連接請求,這就導致客戶TCP發送一個SYN(同步)分節去告訴服務器客戶將在待建立的連接中發送的數據的初始序列號,通常SYN不攜帶數據,其所在IP數據只有一個IP首部,一個TCP首部以及可能有的TCP選項。
3.服務器確認客戶的SYN後,同時自己也要發送一個SYN分節,它含有服務器將在同一個連接中發送的數據的初始化列序號,服務器在單個分節中發送SYN和對客戶SYN的確認
4.客戶必須去確認服務器的SYN
(2)TCP四次揮手
1.某一個應用進程首先調用close,稱爲該端執行主動關閉,該端的TCP會發送一個FIN分節,表示數據已經發送完畢
2.接到FIN的對端將執行被動關閉,這個FIN由TCP確認,它的接受也作爲一個文件結束符傳遞給接收端應用進程(放在已排隊等候該應用進程接收的任何其他數據之後),因爲FIN的接收意味着接收端應用進程在相應連接上已無額外數據可以接收
3.一段時間後,接收到這個文件結束符的應用進程會調用close關閉它的套接字,這會導致它的TCP也要發送一個FIN
4.接收這個最終FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN
參考網站:https://www.cnblogs.com/Andya/p/7272462.html
(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事件.
下面三篇博客瞭解下:?
https://blog.csdn.net/davidsguo008/article/details/73556811
https://blog.csdn.net/men_wen/article/details/53456491
https://blog.csdn.net/yusiguyuan/article/details/15027821
代碼:
#include<iostream>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
#include<stdlib.h>
#include<assert.h>
#include<sys/epoll.h>
#include"threadpool.h"
//#include"myhttp_coon.h"
using namespace std;
const int port = 8888;
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
void addfd(int epfd, int fd, bool flag)
{
epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
if(flag)
{
ev.events = ev.events | EPOLLONESHOT;
}
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
setnonblocking(fd);
}
int main(int argc, char *argv[])
{
threadpool<http_coon>* pool = NULL;
pool = new threadpool<http_coon>;
http_coon* users = new http_coon[100];
assert(users);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
address.sin_addr.s_addr = htons(INADDR_ANY);
int listenfd = socket(AF_INET,SOCK_STREAM,0);
assert(listenfd >= 0);
int ret;
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd,5);
assert(ret >= 0);
int epfd;
epoll_event events[1000];
epfd = epoll_create(5);
assert(epfd != -1);
addfd(epfd, listenfd, false);//listen不能註冊EPOLLONESHOT事件,否則只能處理一個客戶連接
while(true)
{
int number = epoll_wait(epfd, events, 1000, -1);
if( (number < 0) && (errno != EINTR) )
{
printf("my epoll is failure!\n");
break;
}
for(int i=0; i<number; i++)
{
int sockfd = events[i].data.fd;
if(sockfd == listenfd)//有新用戶連接
{
struct sockaddr_in client_address;
socklen_t client_addresslength = sizeof(client_address);
int client_fd = accept(listenfd,(struct sockaddr*)&client_address, &client_addresslength);
if(client_fd < 0)
{
printf("errno is %d\n",errno);
continue;
}
/*如果連接用戶超過了預定於的用戶總數,則拋出異常*/
/* if(http_coon::m_user_count > MAX_FD)
{
show_error(client_fd, "Internal sever busy");
continue;
}*/
//初始化客戶連接
cout << epfd << " " << client_fd << endl;
addfd(epfd, client_fd, true);
cout << "client_fd:" << client_fd << "****\n";
users[client_fd].init(epfd,client_fd);
}
else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
/*出現異常則關閉客戶端連接*/
users[sockfd].close_coon();
}
else if(events[i].events & EPOLLIN)//可以讀取
{
if(users[sockfd].myread())
{
/*讀取成功則添加任務隊列*/
pool->addjob(users+sockfd);
}
else{
users[sockfd].close_coon();
}
}
else if(events[i].events & EPOLLOUT)//可寫入
{
if(!users[sockfd].mywrite())
{
users[sockfd].close_coon();
}
}
}
}
close(epfd);
close(listenfd);
delete[] users;
delete pool;
return 0;
}
2、主要邏輯思路
1.首先創建和客戶端的連接
2.服務器通過客戶端的HTTP請求解析來判斷返回何種結果.HTTP解析是以行爲單位的,前提條件是根據\r\n來判斷是否完整度入一行,若完整讀入一行了那麼就可以進行解析了。
3.通過HTTP請求的解析後,在寫緩衝區寫如HTTP響應,發送給客戶端(HTTP應答包括一個狀態行,多個頭部字段,一個空行和資源內容,其中前三個部分的內容一般會被web服務器放置在一塊內存中,而文檔的內容通常會被放到另一個單獨的內存中)
4.發送響應首行後,就可以發送主要的消息體了
主要就是封裝在myhttp_coon.h中:
#ifndef _MYHTTP_COON_H
#define _MYHTTP_COON_H
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/socket.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<sys/sendfile.h>
#include<sys/epoll.h>
#include<sys/fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
using namespace std;
#define READ_BUF 2000
class http_coon{
public:
/*NO_REQUESTION是代表請求不完整,需要客戶繼續輸入;BAD_REQUESTION是HTTP請求語法不正確;GET_REQUESTION代表獲得並且解析了一個正確的HTTP請求;FORBIDDEN_REQUESTION是代表訪問資源的權限有問題;FILE_REQUESTION代表GET方法資源請求;INTERNAL_ERROR代表服務器自身問題;NOT_FOUND代表請求的資源文件不存在;DYNAMIC_FILE表示是一個動態請求;POST_FILE表示獲得一個以POST方式請求的HTTP請求*/
enum HTTP_CODE{NO_REQUESTION, GET_REQUESTION, BAD_REQUESTION, FORBIDDEN_REQUESTION,FILE_REQUESTION,INTERNAL_ERROR,NOT_FOUND,DYNAMIC_FILE,POST_FILE};
/*HTTP請求解析的狀態轉移。HEAD表示解析頭部信息,REQUESTION表示解析請求行*/
enum CHECK_STATUS{HEAD,REQUESTION};
private:
char requst_head_buf[1000];//響應頭的填充
char post_buf[1000];//Post請求的讀緩衝區
char read_buf[READ_BUF];//客戶端的http請求讀取
char filename[250];//文件總目錄
int file_size;//文件大小
int check_index;//目前檢測到的位置
int read_buf_len;//讀取緩衝區的大小
char *method;//請求方法
char *url;//文件名稱
char *version;//協議版本
char *argv;//動態請求參數
bool m_linger;//是否保持連接
int m_http_count;//http長度
char *m_host;//主機名記錄
char path_400[17];//出錯碼400打開的文件名緩衝區
char path_403[23];//出錯碼403打開返回的文件名緩衝區
char path_404[40];//出錯碼404對應文件名緩衝區
char message[1000];//響應消息體緩衝區
char body[2000];//post響應消息體緩衝區
CHECK_STATUS status;//狀態轉移
bool m_flag;//true表示是動態請求,反之是靜態請求
public:
int epfd;
int client_fd;
int read_count;
http_coon();
~http_coon();
void init(int e_fd, int c_fd);//初始化
int myread();//讀取請求
bool mywrite();//響應發送
void doit();//線程接口函數
void close_coon();//關閉客戶端鏈接
private:
HTTP_CODE analyse();//解析Http請求頭的函數
int jude_line(int &check_index, int &read_buf_len);//該請求是否是完整的以行\r\n
HTTP_CODE head_analyse(char *temp);//http請求頭解析
HTTP_CODE requestion_analyse(char *temp);//http請求行解析
HTTP_CODE do_post();//對post請求中的參數進行解析
HTTP_CODE do_file();//對GET請求方法中的url 協議版本的分離
void modfd(int epfd, int sock, int ev);//改變socket爲狀態
void dynamic(char *filename, char *argv);//通過get方法進入的動態請求處理
void post_respond();//POST請求響應填充
bool bad_respond();//語法錯誤請求響應填充
bool forbiden_respond();//資源權限限制請求響應的填充
bool succeessful_respond();//解析成功請求響應填充
bool not_found_request();//資源不存在請求響應填充
};
void http_coon::init(int e_fd, int c_fd)
{
epfd = e_fd;
client_fd = c_fd;
read_count = 0;
m_flag = false;
}
http_coon::http_coon()
{
}
http_coon::~http_coon()
{
}
/*關閉客戶端鏈接*/
void http_coon::close_coon()
{
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, 0);
close(client_fd);
client_fd = -1;
}
/*改變事件表中的事件屬性*/
void http_coon::modfd(int epfd, int client_fd, int ev)
{
epoll_event event;
event.data.fd = client_fd;
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
}
/*read函數的封裝*/
int http_coon::myread()
{
bzero(&read_buf,sizeof(read_buf));
while(true)
{
int ret = recv(client_fd, read_buf+read_count, READ_BUF-read_count, 0 );
if(ret == -1)
{
if(errno == EAGAIN || errno == EWOULDBLOCK)//讀取結束
{
break;
}
return 0;
}
else if(ret == 0)
{
return 0;
}
read_count = read_count + ret;
}
strcpy(post_buf,read_buf);
return 1;
}
/*響應狀態的填充,這裏返回可以不爲bool類型*/
bool http_coon::succeessful_respond()//200
{
m_flag = false;
bzero(requst_head_buf,sizeof(requst_head_buf));
sprintf(requst_head_buf,"HTTP/1.1 200 ok\r\nConnection: close\r\ncontent-length:%d\r\n\r\n",file_size);
}
bool http_coon::bad_respond()//400
{
bzero(url, strlen(url));
strcpy(path_400,"bad_respond.html");
url = path_400;
bzero(filename,sizeof(filename));
sprintf(filename,"/home/jialuhu/linux_net/web_sever/%s",url);
struct stat my_file;
if(stat(filename,&my_file)<0)
{
cout << "文件不存在\n";
}
file_size = my_file.st_size;
bzero(requst_head_buf,sizeof(requst_head_buf));
sprintf(requst_head_buf,"HTTP/1.1 400 BAD_REQUESTION\r\nConnection: close\r\ncontent-length:%d\r\n\r\n",file_size);
}
bool http_coon::forbiden_respond()//403
{
bzero(url, strlen(url));
strcpy(path_403,"forbidden_request.html");
url = path_403;
bzero(filename,sizeof(filename));
sprintf(filename,"/home/jialuhu/linux_net/web_sever/%s",url);
struct stat my_file;
if(stat(filename,&my_file)<0)
{
cout << "失敗\n";
}
file_size = my_file.st_size;
bzero(requst_head_buf,sizeof(requst_head_buf));
sprintf(requst_head_buf,"HTTP/1.1 403 FORBIDDEN\r\nConnection: close\r\ncontent-length:%d\r\n\r\n",file_size);
}
bool http_coon::not_found_request()//404
{
bzero(url, strlen(url));
strcpy(path_404,"not_found_request.html");
url = path_404;
bzero(filename,sizeof(filename));
sprintf(filename,"/home/jialuhu/linux_net/web_sever/%s",url);
struct stat my_file;
if(stat(filename,&my_file)<0)
{
cout << "草擬\n";
}
file_size = my_file.st_size;
bzero(requst_head_buf,sizeof(requst_head_buf));
sprintf(requst_head_buf,"HTTP/1.1 404 NOT_FOUND\r\nConnection: close\r\ncontent-length:%d\r\n\r\n",file_size);
}
/*動態請求處理*/
void http_coon::dynamic(char *filename, char *argv)
{
int len = strlen(argv);
int k = 0;
int number[2];
int sum=0;
m_flag = true;
bzero(requst_head_buf,sizeof(requst_head_buf));
sscanf(argv,"a=%d&b=%d",&number[0],&number[1]);
if(strcmp(filename,"/add")==0)
{
sum = number[0] + number[1];
sprintf(body,"<html><body>\r\n<p>%d + %d = %d </p><hr>\r\n</body></html>\r\n",number[0],number[1],sum);
sprintf(requst_head_buf,"HTTP/1.1 200 ok\r\nConnection: close\r\ncontent-length: %d\r\n\r\n",strlen(body));
}
else if(strcmp(filename,"/multiplication")==0)
{
cout << "\t\t\t\tmultiplication\n\n";
sum = number[0]*number[1];
sprintf(body,"<html><body>\r\n<p>%d * %d = %d </p><hr>\r\n</body></html>\r\n",number[0],number[1],sum);
sprintf(requst_head_buf,"HTTP/1.1 200 ok\r\nConnection: close\r\ncontent-length: %d\r\n\r\n",strlen(body));
}
}
/*POST請求處理*/
void http_coon::post_respond()
{
if(fork()==0)
{
dup2(client_fd,STDOUT_FILENO);
execl(filename,argv,NULL);
}
wait(NULL);
}
/*判斷一行是否讀取完整*/
int http_coon::jude_line(int &check_index, int &read_buf_len)
{
cout << read_buf << endl;
char ch;
for( ; check_index<read_buf_len; check_index++)
{
ch = read_buf[check_index];
if(ch == '\r' && check_index+1<read_buf_len && read_buf[check_index+1]=='\n')
{
read_buf[check_index++] = '\0';
read_buf[check_index++] = '\0';
return 1;//完整讀入一行
}
if(ch == '\r' && check_index+1==read_buf_len)
{
return 0;
}
if(ch == '\n')
{
if(check_index>1 && read_buf[check_index-1]=='\r')
{
read_buf[check_index-1] = '\0';
read_buf[check_index++] = '\0';
return 1;
}
else{
return 0;
}
}
}
return 0;
}
/*解析請求行*/
http_coon::HTTP_CODE http_coon::requestion_analyse(char *temp)
{
char *p = temp;
cout << "p=" << p << endl;
for(int i=0; i<2; i++)
{
if(i==0)
{
method = p;//請求方法保存
int j = 0;
while((*p != ' ') && (*p != '\r'))
{
p++;
}
p[0] = '\0';
p++;
cout << "method:" <<method << endl;
// method++;
}
if(i==1)
{
url = p;//文件路徑保存
while((*p != ' ') && (*p != '\r'))
{
p++;
}
p[0] = '\0';
p++;
cout << "url:" << url << endl;
}
}
version = p;//請求協議保存
while(*p != '\r')
{
p++;
}
p[0] = '\0';
p++;
p[0] = '\0';
p++;
cout << version << endl;
if(strcmp(method,"GET")!=0&&strcmp(method,"POST")!=0)
{
return BAD_REQUESTION;
}
if(!url || url[0]!='/')
{
return BAD_REQUESTION;
}
if(strcmp(version,"HTTP/1.1")!=0)
{
return BAD_REQUESTION;
}
status = HEAD;//狀態轉移到解析頭部
return NO_REQUESTION;//繼續解析
}
/*解析頭部信息*/
http_coon::HTTP_CODE http_coon::head_analyse(char *temp)
{
if(temp[0]=='\0')
{
//獲得一個完整http請求
return GET_REQUESTION;
}
//處理其他頭部
else if(strncasecmp(temp,"Connection:", 11) == 0)
{
temp = temp+11;
while(*temp==' ')
{
temp++;
}
if(strcasecmp(temp, "keep-alive") == 0)
{
m_linger = true;
}
}
else if(strncasecmp(temp,"Content-Length:", 15)==0)
{
temp = temp+15;
while(*temp==' ')
{
cout << *temp << endl;
temp++;
}
m_http_count = atol(temp);//content-length需要填充
}
else if(strncasecmp(temp,"Host:",5)==0)
{
temp = temp+5;
while(*temp==' ')
{
temp++;
}
m_host = temp;
}
else{
cout << "can't handle it's hand\n";
}
return NO_REQUESTION;
}
http_coon::HTTP_CODE http_coon::do_file()//GET方法請求,對其請求行進行解析,存寫資源路徑
{
char path[40]="/home/jialuhu/linux_net/web_sever";
char* ch;
if(ch=strchr(url,'?'))
{
argv = ch+1;
*ch = '\0';
strcpy(filename,url);
return DYNAMIC_FILE;
}
else{
strcpy(filename,path);
strcat(filename,url);
struct stat m_file_stat;
if(stat(filename, &m_file_stat) < 0)
{
//cout << "打不開\n";
return NOT_FOUND;//NOT_FOUND 404
}
if( !(m_file_stat.st_mode & S_IROTH))//FORBIDDEN_REQUESTION 403
{
return FORBIDDEN_REQUESTION;
}
if(S_ISDIR(m_file_stat.st_mode))
{
return BAD_REQUESTION;//BAD_REQUESTION 400
}
file_size = m_file_stat.st_size;
return FILE_REQUESTION;
}
}
http_coon::HTTP_CODE http_coon::do_post()//POST方法請求,分解並且存入參數
{
int k = 0;
int star;
char path[34]="/home/jialuhu/linux_net/web_sever";
strcpy(filename,path);
strcat(filename,url);
star = read_buf_len-m_http_count;
argv = post_buf + star;
argv[strlen(argv)+1]='\0';
if(filename!=NULL && argv!=NULL)
{
return POST_FILE;
}
return BAD_REQUESTION;
}
/*http請求解析*/
http_coon::HTTP_CODE http_coon::analyse()
{
status = REQUESTION;
int flag;
char *temp = read_buf;
int star_line = 0;
check_index = 0;
int star = 0;
read_buf_len = strlen(read_buf);
int len = read_buf_len;
while((flag=jude_line(check_index, len))==1)
{
temp = read_buf + star_line;
star_line = check_index;
switch(status)
{
case REQUESTION://請求行分析,包括文件名稱和請求方法
{
cout << "requestion\n";
int ret;
ret = requestion_analyse(temp);
if(ret==BAD_REQUESTION)
{
cout << "ret == BAD_REQUESTION\n";
//請求格式不正確
return BAD_REQUESTION;
}
break;
}
case HEAD://請求頭的分析
{
int ret;
ret = head_analyse(temp);
if(ret==GET_REQUESTION)//獲取完整的HTTP請求
{
if(strcmp(method,"GET")==0)
{
return do_file();//GET請求文件名分離函數
}
else if(strcmp(method,"POST")==0)
{
return do_post();//POST請求參數分離函數
}
else{
return BAD_REQUESTION;
}
}
break;
}
default:
{
return INTERNAL_ERROR;
}
}
}
return NO_REQUESTION;//請求不完整,需要繼續讀入
}
/*線程取出工作任務的接口函數*/
void http_coon::doit()
{
int choice = analyse();//根據解析請求頭的結果做選擇
switch(choice)
{
case NO_REQUESTION://請求不完整
{
cout << "NO_REQUESTION\n";
/*改變epoll的屬性*/
modfd(epfd, client_fd, EPOLLIN);
return;
}
case BAD_REQUESTION: //400
{
cout << "BAD_REQUESTION\n";
bad_respond();
modfd(epfd, client_fd, EPOLLOUT);
break;
}
case FORBIDDEN_REQUESTION://403
{
cout << "forbiden_respond\n";
forbiden_respond();
modfd(epfd, client_fd, EPOLLOUT);
break;
}
case NOT_FOUND://404
{
cout<<"not_found_request"<< endl;
not_found_request();
modfd(epfd, client_fd, EPOLLOUT);
break;
}
case FILE_REQUESTION://GET文件資源無問題
{
cout << "文件file request\n";
succeessful_respond();
modfd(epfd, client_fd, EPOLLOUT);
break;
}
case DYNAMIC_FILE://動態請求處理
{
cout << "動態請求處理\n";
cout << filename << " " << argv << endl;
dynamic(filename, argv);
modfd(epfd, client_fd, EPOLLOUT);
break;
}
case POST_FILE://POST 方法處理
{
cout << "post_respond\n";
post_respond();
break;
}
default:
{
close_coon();
}
}
}
bool http_coon::mywrite()
{
if(m_flag)//如果是動態請求,返回填充體
{
int ret=send(client_fd,requst_head_buf,strlen(requst_head_buf),0);
int r = send(client_fd,body,strlen(body),0);
if(ret>0 && r>0)
{
return true;
}
}
else{
int fd = open(filename,O_RDONLY);
assert(fd != -1);
int ret;
ret = write(client_fd,requst_head_buf,strlen(requst_head_buf));
if(ret < 0)
{
close(fd);
return false;
}
ret = sendfile(client_fd, fd, NULL, file_size);
if(ret < 0)
{
close(fd);
return false;
}
close(fd);
return true;
}
return false;
}
#endif
其中兩個附加功能加法和減法的實現(通過GET方法請求),以及POST方法請求的加法和減法的實現
- 動態請求是什麼樣子(GET)
sum.html文件:
<html>
<head>
<meta charset="utf-8">
<title>sum</title>
</head>
<body>
<form action="add">
a: <input type="text" name="a"><br>
b: <input type="text" name="b"><br>
<input type="submit" value="提交">
</form>
<p>點擊"提交"按鈕,表單數據將被髮送到服務器上的“add”程序上。</p>
</body>
</html>
服務器收到的請求是這樣的,首先是打開sum.html文件
然後在表單上提交要相加的兩個數字
點擊提交後,此時服務器收到的請求是這樣的:
看到了/add?a=33&b=33 這就是通過方法GET提交上來的參數a和b ,此時我們在解析請求行的時候就可以通過問好來判斷是否是GET的動態請求,若是那麼根據sscanf()函數,分離出參數a和b,進行相加後就可以填充HTTP響應發送給瀏覽器了。此處我根據提交的程序名稱來選擇函數,在函數中相加填充返回給瀏覽器。當然我覺得正確的做法是重新寫一個add.c然後執行生產add文件,再在fork()一個子線程通過execl( )函數去執行。
那麼POST請求又是什麼樣子呢,其實POST請求將參數放在了請求
修改後的sum.html文件
<html>
<head>
<meta charset="utf-8">
<title>sum</title>
</head>
<body>
<form action="add" method="post">
a: <input type="text" name="a"><br>
b: <input type="text" name="b"><br>
<input type="submit" value="提交">
</form>
<p>點擊"提交"按鈕,表單數據將被髮送到服務器上的“add”程序上。</p>
</body>
</html>
加入了屬性method="post",此時打開sum.html文件依然是GET方法,只是點擊提交表單後用的是POST方法
和GET不同的是,參數被在請求的數據部分,也就是空行之後,此時若方法是POST的話,根據read_buf_len和Content_Length就可以求出參數在read_buf中的起始位置。然後又可以通過sscanf( )分離參數了,然後fork()一個進程,利用dup2函數,將標準輸出重定向到瀏覽器的sockfd上,再執行execl( )函數。此時我們的add執行文件的.c文件如下:
#include<stdio.h>
#include<string.h>
int main(int argc, char *argv[])
{
char re_head[1000];
char message[1000];
int ret;
int a,b,result;
ret = sscanf(argv[0],"a=%d&b=%d", &a, &b);
//printf("a=%d\t b=%d\n",a,b);
if(ret < 0 || ret != 2)
{
sprintf(message,"<html><body>\r\n");
sprintf(message,"%s<p>failure</p>\r\n",message);
sprintf(message,"%s</body></html>");
sprintf(re_head,"HTTP/1.1 GET\r\n");
sprintf(re_head,"%scontent-length: %d\r\n",re_head,strlen(message));
sprintf(re_head,"%scontent-type: text/html\r\n",re_head);
sprintf(re_head,"%sconection: close\r\n\r\n");
/*錯誤提示消息*/
}
else{
result = a+b;
/*返回正確信息*/
sprintf(message,"<html><body>\r\n");
sprintf(message,"%s<p>%d + %d = %d</p><br>\r\n",message,a,b,result);
sprintf(message,"%s<p>welcome to the word of jialuhu</p><br>\r\n",message);
sprintf(message,"%s</body></html>\r\n",message);
sprintf(re_head,"HTTP/1.1 200 ok\r\n");
sprintf(re_head,"%sContent-length: %d\r\n",re_head,(int)strlen(message));
sprintf(re_head,"%scontent-type: text/html\r\n\r\n",re_head);
// sprintf(re_head,"%sconection: close\r\n\r\n");
}
printf("%s",re_head);
printf("%s",message);
fflush(stdout);
return 0;
}
當然除了加減法,還有很多功能可以去實現。此處就簡單實現了這些功能。還有一些HTML文件,因爲懶癌原因,所以隨便寫了幾個。
五、總結
縱觀博客其實感覺涉及的知識有點雜亂,但是很綜合吧。首先滿足代碼上高性能的需求,利用了線城池和epoll多路複用,其中也包括同步機制的封裝。其次就是HTTP這塊的知識了,包括請求格式響應格式和請求方法和響應狀態碼,很多很多都是零零碎碎平湊一起的。而且感覺這個服務器的實現,也終於明白了瀏覽器和後臺是怎麼溝通交流的,有時候看不如動手實現下,很多東西就會突然明白了。大體模塊就是epoll、線城池、同步機制、邏輯處理。代碼裏肯定也有很多沒有測試出來的bug,但是實現大概三分之二後還是有丟丟開心的吧。