1. 程式人生 > >從零編寫c++之http伺服器(3)-http服務

從零編寫c++之http伺服器(3)-http服務

        http全稱超文字傳輸協議,可除錯性高,擴充套件性也強。上兩個篇章我們已經擁有了epoll事件驅動框架和執行緒池處理網路事件,接下來我們要先寫一個基礎網路套接字,然後在此基礎上擴展出http的套接字。獻上類圖如下

                                            

        可以看到我們有一個最頂層的基類ISocket,擁有一個方法fd返回描述符,增加這個介面時由於事件中心註冊到epoll裡需要int型的描述符。接下來在ISocket基礎上派生出IServer與IClient兩個基類。例項化出兩個TCP型別的套接字類CStreamServer與CStreamClient。然後我們就可以組合的方式擴展出兩個http套接字類,不用繼承的原因是繼承增加耦合,也沒有必要用繼承。由於HttpStream與HttpServer需要收聽事件中心的事件回撥,因此需要繼承IEventHandle。

class ISocket
{
	public:
		virtual ~ISocket(){};
		
		// desc: 獲取套接字描述符
		// param: void
		// return: 套接字描述符
		virtual int fd() = 0;
};

class IClient: public ISocket
{
	public:
		virtual ~IClient(){};

		// desc: 開啟套接字
		// param: void
		// return: 0/成功 -1/失敗
		virtual int start() = 0;

		// desc: 關閉套接字
		// param: void
		// return: 0/成功 -1/失敗
		virtual int close() = 0;

		// desc: 設定套接字為非阻塞
		// param: block/是否非阻塞
		// return: 0/成功 -1/失敗
		virtual int set_nonblock(bool nonblock) = 0;

		// desc: 連線到server
		// param: addr/server地址 port/埠
		// return: 0/成功 -1/失敗
		virtual int connect(const std::string &addr, uint16_t port) = 0;

		// desc: 讀取資料
		// param: buf/存放接收資料緩衝區 len/buf長度 flag/見man recv
		// return: 讀取資料長度
		virtual ssize_t recv(void *buf, size_t len, int flags) = 0;

		// desc: 傳送資料
		// param: buf/存放傳送資料緩衝區 len/buf長度 flag/見man recv
		// return: 傳送資料長度
		virtual ssize_t send(const void *buf, size_t len, int flags) = 0;
};


class IServer: public ISocket
{
	public: 
		virtual ~IServer(){};	

		// desc: 設定套接字為非阻塞
		// param: block/是否非阻塞
		// return: 0/成功 -1/失敗
		virtual int set_nonblock(bool block) = 0;

		// desc: 開啟套接字
		// param: backlog/核心連線佇列最大長度
		// return: 0/成功 -1/失敗
		virtual int start(size_t backlog) = 0;

		// desc: 關閉套接字
		// param: void
		// return: 0/成功 -1/失敗
		virtual int close() = 0;

		// desc: 套接字是否關閉
		// param: void
		// return: true/關閉 false/未關閉
		virtual bool isclose() = 0;

		// desc: 返回一個新的連線, 需要手動釋放記憶體
		// param: void
		// return: NULL/錯誤    NOT NULL/新的連線
		virtual IClient *accept() = 0;
};

        首先我們在HttpServer的start函式中啟動一個CStreamServer。接下來設定為非阻塞套接字,這樣才更高效。然後將套接字註冊到事件中心中去,就可以等待事件通知了。

        有可讀事件到達時會回撥handle_in介面,對於server可讀就是新的連線建立了。這裡需要注意一點就是,一次可讀事件產生並未意味著一個連線建立,在這裡需要迴圈的讀取直到返回EAGAIN或者EWOULDBLOCK(先前設定為非阻塞)。同時由於我們事件中心的epoll檢測是設定為邊緣觸發的。所以這裡不讀取完全是很可怕的。accept讀取連線會返回一個新建的CStreamClien物件,代表著這個新的連線。然後傳入HttpStream建構函式。新建立的HttpStream物件會自行釋放自己。

int HttpServer::start(size_t backlog)
{
	if(!m_server.isclose())	 //has start
		return 0;
		
	if(m_server.start(backlog) < 0)
		return -1;

	if(m_server.set_nonblock(true) < 0)
		errsys("set socket non block failed\n");

	return register_event(m_server);
}
void HttpServer::handle_in(int fd)
{
	/*
	* 讀取所有建立的連線
	*/
	do{
		Socket::IClient *newconn = m_server.accept();
		if(newconn == NULL){
			if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR){
				break;
			}else{
				errsys("sockfd[%d] accept error\n", fd);
				close();
				return;
			}
		}
		
		trace("socket[%d] accept a conn\n", fd);
		HttpStream *httpstream = new HttpStream(newconn);	// self release 
		assert(httpstream != NULL);

	}while(true);
}

      新的連線會接收到請求,然後我們對資料使用CHttpRequest物件進行解析。然後我們開始根據http的方法進行處理。這裡只處理了GET方法。然後讀取出檔案後開始構造CHttpRespose物件,需要注意一點是CHttpRespose裡配置的Body最大是64k,這裡沒有進行分包處理。因此不宜傳輸大檔案試驗。構造好CHttpRespose回覆包後我們呼叫其serialize進行序列化後傳送出去,這樣就完成了一個http事務。同時這裡為了處理簡單,採取了短連結。即回覆報文中Connection頭部為close。

void HttpStream::handle_in(int fd)
{
	Pthread::CGuard guard(m_readbuffmutex);
	ssize_t nread = m_client->recv(m_readbuff, READBUFF_LEN, MSG_DONTWAIT);
	if((nread < 0 && nread != EAGAIN) || nread == 0){	// error or read EOF
		close();
		return;
	}else if(nread < 0 && nread == EAGAIN){ 		
		errsys("non data to read\n");
		return; 
	}	
	m_readbuff[nread] = 0x00;

	Http::CHttpRequest httprequest;
	if(httprequest.load_packet(m_readbuff, nread) < 0){
		error("parse package error\n");
		return;
	}

	trace("socket[%d] receive <--- %ld bytes\n", fd, nread);
	Http::IHttpRespose *respose = handle_request(httprequest);
	if(respose != NULL){
		m_client->send(respose->serialize(), respose->size(), 0);
		delete respose;
	}
}
void HttpStream::handle_close(int fd)
{
	trace("socket[%d] handle close\n", fd);
	delete this;
}
Http::IHttpRespose *HttpStream::handle_request(Http::IHttpRequest &request)
{
	const std::string &method = request.method();
	const std::string &url = request.url();

	std::string dname, bname;
	split_url(url, dname, bname);

	Http::CHttpRespose *respose = new Http::CHttpRespose;	
	std::string filepath = http_path_handle(dname, bname);
	if(method == "GET"){

		std::string extention = extention_name(filepath);
		if(extention.empty() || access(filepath.c_str(), R_OK) < 0){

			errsys("access %s error\n", filepath.c_str());
			
			respose->set_version(HTTP_VERSION);
			respose->set_status("404", "Not Found");
			respose->add_head(HTTP_HEAD_CONNECTION, "close");
			return respose;
		}

		
		struct stat filestat;
		stat(filepath.c_str(), &filestat);
		const size_t filesize = filestat.st_size;

		char *fbuff = new char[filesize];
		assert(fbuff != NULL);

		FILE *fp = fopen(filepath.c_str(), "rb");
		if(fp == NULL || fread(fbuff, filesize, 1, fp) != 0x01){
		
			delete fbuff;

			respose->set_version(HTTP_VERSION);
			respose->set_status("500", "Internal Server Error");
			respose->add_head(HTTP_HEAD_CONNECTION, "close");
			return respose;
		}
		
		fclose(fp);

		char sfilesize[16] = {0x00};
		snprintf(sfilesize, sizeof(sfilesize), "%ld", filesize);

		respose->set_version(HTTP_VERSION);
		respose->set_status("200", "OK");
		respose->add_head(HTTP_HEAD_CONTENT_TYPE, http_content_type(extention));
		respose->add_head(HTTP_HEAD_CONTENT_LEN, sfilesize);
		respose->add_head(HTTP_HEAD_CONNECTION, "close");
		respose->set_body(fbuff, filesize);
		delete fbuff;
	}

	return respose;
}

        最終完成程式碼後執行make編譯,在Bin目錄下生成名為panda的執行程式。執行後在瀏覽器上輸入ip和8080埠即可開啟網頁, have fun!