一起來寫web server 09 -- 最終版本
這次的代碼是在前一次代碼的基礎上做一些點滴的修改.實現(xiàn)更好的封裝,更漂亮的抽象.同時也是我們這個web server
的最后一個版本了.這次的版本應該修改的幅度是最大的.
EPOLLONESHOT
即使我們使用
ET
模式,一個socket
上的某個事件還是可能被觸發(fā)多次,這個并發(fā)程序中就會引起一個問題,比如說一個線程(或進程,下同)在讀取完某個socket
上的數(shù)據(jù)后開始處理這些數(shù)據(jù),而在數(shù)據(jù)處理的過程中該socket
上又有新數(shù)據(jù)可讀(EPOLLIN
)再次被觸發(fā),此時另外一個線程被喚醒來讀取這些新的數(shù)據(jù),于是就出現(xiàn)了兩個線程同時操作一個socket
的局面,這當然不是我們期望的,我們期望的是一個socket
連接在任一時刻都只被一個線程處理,這一點可以使用epoll
的EPOLLONESHOT
事件實現(xiàn). --[linux高性能服務器編程]
對于EOLLONESHOT
這個事件,你可以認為,一旦某個線程接收到了注冊了EPOLLONESHOT
的文件描述符的信號,從這個時候器,這個文件描述符相當于從epoll
的監(jiān)聽隊列中移除了,這樣的話,別的事件就不會來打攪你了.總之每次你處理完這個事件,你都要重置這個文件描述符上的EPOLLONESHOT
事件,相當于重新注冊一遍這個文件描述符.這樣的話,當這個文件描述符上事件可讀或者別的什么,可以被再次觸發(fā).
代碼相當簡單,你可以查看我的源碼,這里就不貼了.
Buffer
前面的幾個版本中,我們直接在HttpHandle
這個類中放置字符數(shù)組和index
來處理輸入和輸出,這樣非常不便于管理,所以為了實現(xiàn)更好的抽象,我們重新設計了一個Buffer
類.實現(xiàn)參考了muduo
庫,然后針對我們這個類做了特別的改進.(這個類在linux多線程服務端編程這本書里有很詳細的介紹,你可以看一下.)
我們來看一下這個類:
class Buffer
{
public:
static const size_t kCheapPrepend = 8;
static const size_t kInitialSize = 1024;
private:
std::vector<char> buffer_; /* 用于vector來存儲字符 */
size_t readerIndex_; /* 讀指示器 */
size_t writerIndex_; /* 寫指示器 */
static const char kCRLF[]; /* 其實就是\r\n啦! */
};
看一下它的構造函數(shù):
explicit Buffer(size_t initialSize = kInitialSize)
: buffer_(kCheapPrepend + initialSize)
, readerIndex_(kCheapPrepend)
, writerIndex_(kCheapPrepend)
{
}
需要注意的是buffer_
是一個字符vector
,initialSize
指的是初始化的大小.于別處不同是,readerIndex_
和writeIndex_
并不是從0
開始的,而是在這前面留了一小撮的空間,在我們這個應用里可能沒有什么卵用,但是,別的應用中,用處還是挺大的.
接下來是返回可讀取的字節(jié)數(shù)的函數(shù):
// 不修改類的成員變量的值的函數(shù)都要用const修飾,這是一種好的習慣
size_t readableBytes() const { /* 可讀的字節(jié)數(shù)目 */
return writerIndex_ - readerIndex_;
}
獲取可寫入的字節(jié)數(shù)目.
size_t writableBytes() const { /* 可寫的字節(jié)數(shù)目 */
return buffer_.size() - writerIndex_;
}
peek
函數(shù)返回可以讀的地址,需要注意的是返回的是一個const
對象,即可以讀但是不可以修改.
const char* peek() const { /* 偷看 */
return begin() + readerIndex_; // 從這里開始讀
}
為了方便request
的處理,這個BUffer
類設計了一個findEOF
函數(shù),具體的功能是返回從當前可讀位置開始的第一個\n
字符的地址.
const char* findEOF() const {
const void* eol = memchr(peek(), '\n', readableBytes());
return static_cast<const char*>(eol);
}
基于這個函數(shù),我們可以實現(xiàn)getLine
函數(shù):
bool getLine(char *dest, size_t len) { /* 讀取一行數(shù)據(jù) */
const char* end = findEOL();
if (end == 0) return false; /* 沒有找到換行符 */
const char* start = peek();
assert(end >= start); /* 保證size是一個正數(shù),然后下面static_cast轉換的時候才會正確 */
ptrdiff_t size = end - start - 1;
if (len < static_cast<size_t>(size)) {
return false; /* 空間不夠 */
}
std::copy(start, end - 1, dest); /* 去掉\r\n */
dest[size] = 0;
retrieveUntil(end + 1); /* 丟棄掉包括\n在內的數(shù)據(jù),因為已經被讀了 */
return true;
}
這個函數(shù)很簡單,那就是讀取一行數(shù)據(jù)到dest
所指的字符數(shù)組里面.并且將buffer
的readerIndex_
后移(通過函數(shù)retrieveUntil
實現(xiàn)),如果沒有發(fā)現(xiàn)\n
或者dest
的len
不夠,那就返回false
不會對緩沖區(qū)做任何修改.
同時為了方便寫緩沖區(qū)的輸入,特別添加了appendStr
函數(shù):
void appendStr(const char* format, ...) { /* 格式化輸入 */
char extralbuf[256];
memset(extralbuf, 0, sizeof extralbuf);
va_list arglist;
va_start(arglist, format);
vsnprintf(extralbuf, sizeof extralbuf, format, arglist);
va_end(arglist);
append(extralbuf, strlen(extralbuf));
}
上面的函數(shù)調用append
才正在將數(shù)據(jù)添加到緩沖區(qū)中:
void append(const char* data, size_t len) {
//mylog("before append");
ensureWritableBytes(len);
std::copy(data, data + len, beginWrite());
hasWritten(len);
}
hasWrittern
函數(shù)具體的作用是將writeIndex_
指針后移.這里比較有意思的一個函數(shù)是ensureWritableBytes
函數(shù).它可以實現(xiàn)緩沖區(qū)的動態(tài)擴展.
void ensureWritableBytes(size_t len) { /* 保證有足夠的寫入空間 */
if (writableBytes() < len) {
makeSpace(len);
}
assert(writableBytes() >= len);
}
如果可供寫入的空間不足夠的話,要makeSpace
.
void makeSpace(size_t len) {
if (writableBytes() + prependableBytes() < len + kCheapPrepend) {
buffer_.resize(writerIndex_ + len); /* 重新分配存儲空間 */
}
else { /* 如果剩余的空間大小足夠了! */
assert(kCheapPrepend < readerIndex_);
size_t readable = readableBytes(); /* 可讀的字節(jié)數(shù)目 */
std::copy(begin() + readerIndex_,
begin() + writerIndex_,
begin() + kCheapPrepend);
readerIndex_ = kCheapPrepend;
writerIndex_ = readerIndex_ + readable;
assert(readable == readableBytes());
}
}
如果可供寫的空間不夠,就調用vector
的resize
函數(shù)重新分配空間,否則的話,空間是足夠了,我們要將前面的已經空閑的,但是未被使用的空間回收,具體而言,就是將實際有用的數(shù)據(jù)往前面挪.
好了,具體的一些細節(jié)你可以查看具體的代碼實現(xiàn),這里就不在贅述.
HttpRequest
為了更好地處理連接,我們將處理對方發(fā)送的request
的部分拆分了出來,組成了一個HttpRequest
類,我們來分析一下這個類:
class HttpRequest
{
public:
enum HttpRequestParseState /* HttpRequest所處的狀態(tài) */
{
kExpectRequestLine,
kExpectHeaders,
kExperctBody,
kGotAll,
kError
};
public:
bool keepAlive_; /* 是否繼續(xù)保持連接 */
bool sendFile_; /* 是否要發(fā)送文件 */
bool static_; /* 是否為靜態(tài)頁面 */
std::string method_; /* 方法 */
std::string path_; /* 資源的路徑 */
std::string fileType_; /* 資源的類型 */
private:
static const char rootDir_[]; /* 網(wǎng)頁的根目錄 */
static const char homePage_[]; /* 所指代的網(wǎng)頁 */
... ...
};
這里不得不提的是這幾個狀態(tài),剛開始的時候HttpRequest
是處于 kExpectRequestLine
狀態(tài),也就是渴求請求行,所謂的請求行,就是這個玩意:
GET / Http/1.1\r\n
獲得了請求行之后,立馬轉入kExpectHeaders
,也就是渴求請求頭狀態(tài),這個玩意就是請求行之后的數(shù)據(jù),以一行\r\n
作為終止符.我舉一個栗子:
Accept: image/gif.image/jpeg,*/*\r\n
Accept-Language: zh-cn\r\n
Connection: Keep-Alive\r\n
Host: localhost\r\nUser-Agent: Mozila/4.0(compatible;MSIE5.01;Window NT5.0)\r\n
Accept-Encoding: gzip,deflate\r\n
\r\n
注意,這里我將不可見字符都寫出來了.
如果parse
成功了,,那么就要HttpRequest
立馬轉入kGotAll
狀態(tài),表示萬事俱備,否則,前面的kExpectHeaders
以及kExpectRequestLine
有任何一個出錯,都將進入kError
狀態(tài).
你可能會有疑問,為什么我們要用狀態(tài)機的方式來處理連接?
很簡單,因為用戶的行為可能非常奇葩,他有可能不按套路出牌,最為重要的一點是,我們有的時候并不能一次性就全部讀取到用戶的request
,他可能將一個request
分成若干次來發(fā)送,每次我們讀取不到完整的一行數(shù)據(jù)的時候,我們便要返回,繼續(xù)去監(jiān)聽用戶發(fā)送數(shù)據(jù),然后繼續(xù)返回到我們的HttpRequest
,繼續(xù)運行,這也就導致了我們必須記住之前執(zhí)行到了那個狀態(tài),以便繼續(xù)往下執(zhí)行.用狀態(tài)機是最好的解決辦法.
用于處理用戶發(fā)送的request
的函數(shù):
HttpRequest::HttpRequestParseState HttpRequest::parseRequest(Buffer& buf)
{
bool ok = true;
bool hasMore = true;
while (hasMore) {
if (state_ == kExpectRequestLine) {
const char* crlf = buf.findCRLF(); /* 找到回車換行符 */
if (crlf) { /* 如果找到了! */
ok = processRequestLine(buf);
}
else {
hasMore = false; /* 沒有找到,可能表示對方還沒有發(fā)送完整的一行數(shù)據(jù),要繼續(xù)去監(jiān)聽客戶的寫事件 */
}
if (ok) /* 請求行parse沒有出錯 */
state_ = kExpectHeaders;
else {
state_ = kError; /* 出現(xiàn)錯誤 */
hasMore = false;
}
}
else if (state_ == kExpectHeaders) { /* 處理頭部的信息 */
if (true == (ok = processHeaders(buf))) {
state_ = kGotAll;
hasMore = false;
}
else {
/* 這里做了簡化處理,頭部不會出錯,只要沒有得到\r\n這樣的結尾行都表示用戶數(shù)據(jù)還沒有發(fā)送完畢 */
hasMore = false;
}
}
else if (state_ == kExperctBody) { /* 暫時還未實現(xiàn) */
}
}
return state_;
}
接下來的都是一些小魚小蝦,比如說processRequestLine
函數(shù):
bool HttpRequest::processRequestLine(Buffer& buf)
{
bool succeed = false;
char line[256];
char method[64], path[256], version[64];
buf.getLine(line, sizeof line);
sscanf(line, "%s %s %s", method, path, version);
setMethod(method, strlen(method));
setPath(path, strlen(path));
/* version就不處理了 */
return true;
}
還比如說processHeaders
函數(shù):
bool HttpRequest::processHeaders(Buffer& buf) /* 處理其余的頭部信息 */
{ /* 其余的玩意,我就不處理啦! */
char line[1024];
char key[256], value[256];
while (buf.getLine(line, sizeof line)) {
if (strlen(line) == 0) { /* 只有取到了最后一個才能返回true */
return true;
}
if (strstr(line, "keep-alive")) {
keepAlive_ = true; /* 保持連接 */
}
}
return false;
}
難度不大,這里不再一一贅述.
HttpHandle
HttpHandle
類也變成了一個狀態(tài)機.這個類的入口點只有一個,就是process
函數(shù).
void HttpHandle::process()
{
/*-
* 在process之前,只有這么一些狀態(tài)kExpectRead, kExpectWrite
*/
switch (state_)
{
case kExpectRead: { /* 既然希望讀,那就processRead */
processRead();
break;
}
case kExpectWrite: { /* 既然希望寫,那就processWrite */
processWrite();
break;
}
default: /* 成功,失敗,這些都需要關閉連接 */
removefd(epollfd_, sockfd_);
break;
}
/*-
* 程序執(zhí)行完成后,有這么一些狀態(tài)kExpectRead, kExpectWrite, kError, kSuccess
*/
switch (state_)
{
case kExpectRead: {
modfd(epollfd_, sockfd_, EPOLLIN, true); /* 繼續(xù)監(jiān)聽對方的輸入 */
break;
}
case kExpectWrite: {
modfd(epollfd_, sockfd_, EPOLLOUT, true); /* 監(jiān)聽TCP緩沖區(qū)可寫事件 */
break;
}
default: {
removefd(epollfd_, sockfd_); /* 其余的都關閉連接 */
break;
}
}
}
對于連接的處理也變成了狀態(tài)的轉換,它的狀態(tài)有:
enum HttpHandleState {
kExpectReset, /* 需要初始化 */
kExpectRead, /* 正在處理讀 */
kExpectWrite, /* 正在處理寫 */
kError, /* 出錯 */
kSuccess, /* 成功 */
kClosed /* 對方關閉連接 */
};
剛開始的時候,HttpHandle
處于kExpectReset
狀態(tài),當客戶端connect
過來之后,主線程對HttpHandle
進行初始化,初始化完成之后,進入kExpectRead
狀態(tài),他要一直讀取到對方完整的request
才能進入下一個狀態(tài)kExpectWrite
,這期間,如果客戶端數(shù)據(jù)分多次發(fā)送,HttpHandle
就在這個kExpectRead
打轉,當然如果此時對方關閉了連接,我們進入kClose
狀態(tài),在kExpectWrite
狀態(tài)中,如果數(shù)據(jù)沒有發(fā)送完,一直處于這個狀態(tài),發(fā)送完成的話,進入kSuccess
狀態(tài),整個連接過程中,一旦出錯,都會進入kError
狀態(tài).
在
processRead
過程中:
void HttpHandle::processRead()
{
struct stat sbuf;
if (!read()) { /* 讀取失敗,代表對方關閉了連接 */
state_ = kClosed;
return;
}
/*-
* 試想這樣一種情況,對方并沒有一次性將request發(fā)送完畢,而是只發(fā)送了一部分,你應該如何來處理?正確的方式是繼續(xù)去讀,直到讀到結尾符為止.
* 當然,我這里并沒有處理request是錯誤的情況,這里假設request都是正確的,否則的話,就要關閉連接了.
*/
HttpRequest::HttpRequestParseState state = request_.parseRequest(readBuffer_);
if (state == HttpRequest::kError) /* 如果處理不成功,就要返回 */
{
state_ = kError; /* 解析出錯 */
return;
}
else if (state != HttpRequest::kGotAll){ /* 沒出錯的話,表明對方只發(fā)送了request的一部分,我們需要繼續(xù)讀 */
return;
}
if (strcasecmp(request_.method_.c_str(), "GET")) { /* 只支持Get方法 */
clientError(request_.method_.c_str(), "501", "Not Implemented",
"Tiny does not implement this method");
goto end;
}
if (request_.static_) { /* 只支持靜態(tài)網(wǎng)頁 */
if (stat(request_.path_.c_str(), &sbuf) < 0) {
clientError(request_.path_.c_str(), "404", "Not found",
"Tiny couldn't find this file"); /* 沒有找到文件 */
goto end;
}
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clientError(request_.path_.c_str(), "403", "Forbidden",
"Tiny couldn't read the file"); /* 權限不夠 */
goto end;
}
serveStatic(request_.path_.c_str(), sbuf.st_size);
}
else { /* Serve dynamic content */
clientError(request_.method_.c_str(), "501", "Not Implemented",
"Tiny does not implement this method");
goto end;
}
end:
state_ = kExpectWrite;
return processWrite();
}
在processWrite
過程中:
void HttpHandle::processWrite()
{
int res;
/*-
* 數(shù)據(jù)要作為兩部分發(fā)送,第1步,要發(fā)送writeBuf_里面的數(shù)據(jù).
*/
size_t nRemain = writeBuffer_.readableBytes(); /* writeBuf_中還有多少字節(jié)要寫 */
if (nRemain > 0) {
while (true) {
size_t len = writeBuffer_.readableBytes();
//mylog("1. len = %ld", len);
res = write(sockfd_, writeBuffer_.peek(), len);
if (res < 0) {
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { /* 資源暫時不可用 */
return;
}
state_ = kError;
return;
}
writeBuffer_.retrieve(res);
if (writeBuffer_.readableBytes() == 0) break;
}
}
/*-
* 第2步,要發(fā)送html網(wǎng)頁數(shù)據(jù).
*/
if (sendFile_) {
char *fileAddr = (char *)fileInfo_->addr_;
size_t fileSize = fileInfo_->size_;
while (true) {
res = write(sockfd_, fileAddr + fileWritten_, fileSize - fileWritten_);
if (res < 0) {
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { /* 資源暫時不可用 */
return;
}
state_ = kError; /* 出現(xiàn)了錯誤 */
return;
}
fileWritten_ += res;
if (fileWritten_ == fileInfo_->size_)
break;
}
}
/* 數(shù)據(jù)發(fā)送完畢 */
reset();
if (keepAlive_) /* 如果需要保持連接的話 */
state_ = kExpectRead;
else
state_ = kSuccess;
}
代碼都非常簡單,應該能夠讀懂.
尾聲
到了這里,我的代碼寫的就差不多了,還有一些小問題沒有解決,那就是如果對方一直占用著資源怎么辦?其實我們也有辦法,那就是時間輪或者時間堆,如果過了一段時間客戶端還沒有發(fā)送數(shù)據(jù)過來的話,我們強行關閉連接,至于這些代碼的實現(xiàn),當做一個小小的測驗,留給你吧.
其余的代碼問題都不是很大,可能會有一點錯誤,但會慢慢糾正的.和前面一樣的,參考代碼在這里:https://github.com/lishuhuakai/Spweb
實踐出真知
我讀過UNP
,APUE
,CSAPP
等一大堆的書,我覺得我足夠聰穎,光讀一讀就能夠透知一切,然而某一天,我發(fā)現(xiàn)我錯了,讀了這些書沒多久,書里的只是只是在我的腦海里留下了一層印記,時光如同徐徐的長風,將這些印記一層層拂去,沒過多久,我就忘得差不多了,然后我突然想寫一些代碼了,某一天我欣喜地發(fā)現(xiàn),做過了一遍之后,這些東西長久地留存在我的腦海了,所以我想說的一句是,實踐才能出真知.
當然光寫也是挺傻逼的,有理論的指導,你才能寫出更加漂亮的代碼.程序員永遠都需要理論和實踐兩條腿走路.
我起先也不知道應該做些什么,有些玩意感覺難度太大,一些又沒多大意義,不過不管怎么樣,總比不做要好,下一次,我想寫一個ftp
服務.
Over
!