打印模块
Log.hpp 方便使用
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)
void Log(std::string level, std::string message, std::string file_name, int line)
{
std::cout<<"["<<level<<"]["<<time(nullptr)<<"]["<<message<<"]["<<file_name<<"]["<<line<<"]"<<std::endl;
}
TcpServer.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#define BACKLOG 5
class TcpServer{
private:
int _port; //端口号
int _listen_sock; //监听套接字
static TcpServer* _svr;
int epollfd;
private:
TcpServer(int port)
:_port(port)
,_listen_sock(-1)
{}
TcpServer(const TcpServer&)
{}
public:
static TcpServer* GetInstance(int port)
{
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
if(_svr == nullptr){
pthread_mutex_lock(&lock);
if(_svr == nullptr){
_svr = new TcpServer(port);
_svr->InitServer();
}
pthread_mutex_unlock(&lock);
}
return _svr;
}
void InitServer()
{
Socket();
Bind();
Listen();
LOG(INFO, "tcp_server init ... success");
}
void Socket()
{
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_listen_sock < 0){
LOG(FATAL, "socket error!");
exit(1);
}
int opt = 1;
setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
LOG(INFO, "create socket ... success");
}
void Bind()
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; //云服务器不能直接绑定公网IP
if(bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
LOG(FATAL, "bind error!");
exit(2);
}
LOG(INFO, "bind socket ... success");
}
void Listen()
{
if(listen(_listen_sock, BACKLOG) < 0){
LOG(FATAL, "listen error!");
exit(3);
}
LOG(INFO, "listen socket ... success");
}
int Sock()
{
return _listen_sock;
}
~TcpServer()
{
if(_listen_sock >= 0){
close(_listen_sock);
}
}
};
TcpServer* TcpServer::_svr = nullptr;
HttpServer.hpp
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <signal.h>
#include "TcpServer.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "Log.hpp"
#define PORT 8081
#define MAX_EVENT_NUMBER 1024
class HttpServer{
private:
int _port;
bool _stop;
int epollfd;
struct epoll_event events[MAX_EVENT_NUMBER];
public:
HttpServer(int port)
:_port(port)
{
_stop = false;
epollfd = -1;
}
void InitServer()
{
signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信号,防止写入时崩溃
}
int setnonblocking(int fd)
{
int option = fcntl(fd, F_GETFL) | O_NONBLOCK;
fcntl(fd, F_SETFD, option);
return fcntl(fd, F_GETFL);
}
void addfd(int fd, bool enable_et){
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;
if(enable_et){
ev.events |= EPOLLET;
}
epoll_ctl(this->epollfd, EPOLL_CTL_ADD, fd, &ev);
setnonblocking(fd);
}
void work(int nread, int listenfd)
{
for(int i = 0; i < nread; i++)
{
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int connfd = accept(listenfd, (struct sockaddr*)&peer, &len);
addfd(connfd, true);
}else if(events[i].events & EPOLLIN){
LOG(INFO, "event trigger once");
LOG(INFO, "get a new link");
Task task(sockfd);
ThreadPool::GetInstance()->PushTask(task);
}else{
LOG(INFO, "something else happened");
}
}
}
void Loop()
{
LOG(INFO, "loop begin");
TcpServer* tsvr = TcpServer::GetInstance(_port);
int listen_sock = tsvr->Sock();
epollfd = epoll_create(5);
addfd(listen_sock, true);
while(!_stop){
int nread = epoll_wait(this->epollfd, events, MAX_EVENT_NUMBER, -1);
if(nread < 0){
continue;
}
// struct sockaddr_in peer;
// memset(&peer, 0, sizeof(peer));
// socklen_t len = sizeof(peer);
// int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
// if(sock < 0){
// continue;
// }
// LOG(INFO, "get a new link");
// Task task(sock);
// ThreadPool::GetInstance()->PushTask(task);
//lt(nread, listen_sock);
work(nread, listen_sock);
//int* p = new int(sock);
//pthread_t tid;
//pthread_create(&tid, nullptr, Entrance::HandlerRequest, (void*)p);
//pthread_detach(tid);
}
}
~HttpServer()
{}
};
Task.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include "Protocol.hpp"
class Task{
private:
int _sock;
CallBack _handler; //回调
public:
Task()
{}
Task(int sock)
:_sock(sock)
{}
//处理任务
void ProcessOn()
{
_handler(_sock); //调用CallBack的仿函数
}
~Task()
{}
};
ThreadPool.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include "Task.hpp"
#include "Log.hpp"
#define NUM 6
class ThreadPool{
private:
std::queue<Task> _task_queue; //任务队列
int _num;
bool _stop;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static ThreadPool* _inst;
private:
//构造函数私有
ThreadPool(int num = NUM)
:_num(num)
,_stop(false)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
//拷贝构造函数私有或删除
ThreadPool(const ThreadPool&)=delete;
bool IsEmpty()
{
return _task_queue.empty();
}
bool IsStop()
{
return _stop;
}
void LockQueue()
{
pthread_mutex_lock(&_mutex);
}
void UnLockQueue()
{
pthread_mutex_unlock(&_mutex);
}
void ThreadWait()
{
pthread_cond_wait(&_cond, &_mutex);
}
void ThreadWakeUp()
{
pthread_cond_signal(&_cond);
}
public:
static ThreadPool* GetInstance()
{
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
if(_inst == nullptr){
pthread_mutex_lock(&mtx);
if(_inst == nullptr){
_inst = new ThreadPool();
_inst->InitThreadPool();
}
pthread_mutex_unlock(&mtx);
}
return _inst;
}
static void* ThreadRoutine(void* arg)
{
pthread_detach(pthread_self());
ThreadPool* tp = (ThreadPool*)arg;
while(true){
tp->LockQueue();
while(tp->IsEmpty()){
tp->ThreadWait();
}
Task task;
tp->PopTask(task);
tp->UnLockQueue();
task.ProcessOn(); //处理任务
}
}
bool InitThreadPool()
{
pthread_t tid;
for(int i = 0;i < _num;i++){
if(pthread_create(&tid, nullptr, ThreadRoutine, this) != 0){
LOG(FATAL, "create thread pool error!");
return false;
}
}
LOG(INFO, "create thread pool success!");
return true;
}
void PushTask(const Task& task)
{
LockQueue();
_task_queue.push(task);
UnLockQueue();
ThreadWakeUp();
}
void PopTask(Task& task)
{
task = _task_queue.front();
_task_queue.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
};
ThreadPool* ThreadPool::_inst = nullptr;
Util.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
//工具类
class Util{
public:
static int ReadLine(int sock, std::string& out)
{
char ch = 'X'; //随便设置一个字符,只要不是\n即可
while(ch != '\n'){
ssize_t size = recv(sock, &ch, 1, 0);
if(size > 0){
if(ch == '\r'){
//窥探
recv(sock, &ch, 1, MSG_PEEK);
if(ch == '\n'){
//窥探成功
//\r\n->\n
recv(sock, &ch, 1, 0);
}
else{
//\r->\n
ch = '\n';
}
}
//普通字符或\n
out.push_back(ch);
}
else if(size == 0){
return 0;
}
else{
return -1;
}
}
return out.size();
}
static bool CutString(std::string& target, std::string& sub1_out, std::string& sub2_out, std::string sep)
{
size_t pos = target.find(sep, 0);
if(pos != std::string::npos){
sub1_out = target.substr(0, pos);
sub2_out = target.substr(pos + sep.size());
return true;
}
return false;
}
};
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <sstream>
#include <algorithm>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <sys/wait.h>
#include <fcntl.h>
#include "Util.hpp"
#include "Log.hpp"
#define SEP ": "
#define WEB_ROOT "wwwroot"
#define HOME_PAGE "index.html"
#define HTTP_VERSION "HTTP/1.0"
#define LINE_END "\r\n"
#define PAGE_400 "400.html"
#define PAGE_404 "404.html"
#define PAGE_500 "500.html"
#define OK 200
#define BAD_REQUEST 400
#define NOT_FOUND 404
#define SERVER_ERROR 500
static std::string CodeToDesc(int code)
{
std::string desc;
switch(code){
case 200:
desc = "OK";
break;
case 404:
desc = "Not Found";
break;
default:
break;
}
return desc;
}
static std::string SuffixToDesc(const std::string& suffix)
{
static std::unordered_map<std::string, std::string> suffix_to_desc = {
{".html", "text/html"},
{".css", "text/css"},
{".js", "application/x-javascript"},
{".jpg", "application/x-jpg"},
{".xml", "text/xml"}
};
auto iter = suffix_to_desc.find(suffix);
if(iter != suffix_to_desc.end()){
return iter->second;
}
return "text/html";
}
class HttpRequest{
public:
std::string _request_line; //请求行
std::vector<std::string> _request_header; //请求报头
std::string _blank; //空行
std::string _request_body; //请求正文
//解析完毕之后的结果
std::string _method; //请求方法
std::string _uri; //URI
std::string _version; //版本号
std::unordered_map<std::string, std::string> _header_kv; //请求报头中的键值对
int _content_length; //正文长度
std::string _path; //请求资源的路径
std::string _query_string; //uri中携带的参数
bool _cgi; //是否需要使用CGI模式
public:
HttpRequest()
:_content_length(0)
,_cgi(false)
{}
~HttpRequest()
{}
};
class HttpResponse{
public:
std::string _status_line; //状态行
std::vector<std::string> _response_header; //响应报头
std::string _blank; //空行
std::string _response_body; //响应正文
int _status_code; //状态码
int _fd; //响应的文件
int _size; //响应文件的大小
std::string _suffix; //响应文件的后缀
public:
HttpResponse()
:_blank(LINE_END)
,_status_code(OK)
,_fd(-1)
,_size(0)
{}
~HttpResponse()
{}
};
//读取请求、分析请求、构建响应
//IO通信
class EndPoint{
private:
int _sock;
HttpRequest _http_request;
HttpResponse _http_response;
bool _stop;
private:
//读取请求行
bool RecvHttpRequestLine()
{
auto& line = _http_request._request_line;
if(Util::ReadLine(_sock, line) > 0){
line.resize(line.size() - 1);
LOG(INFO, line);
}
else{
_stop = true; //读取出错,不予处理
}
return _stop;
}
//读取请求报头和空行
bool RecvHttpRequestHeader()
{
std::string line;
while(true){
line.clear(); //每次读取之前清空line
if(Util::ReadLine(_sock, line) <= 0){
_stop = true;
break;
}
if(line == "\n"){
_http_request._blank = line;
break;
}
line.resize(line.size() - 1);
_http_request._request_header.push_back(line);
//LOG(INFO, line);
}
return _stop;
}
//解析请求行
void ParseHttpRequestLine()
{
auto& line = _http_request._request_line;
std::stringstream ss(line);
ss>>_http_request._method>>_http_request._uri>>_http_request._version;
auto& method = _http_request._method;
std::transform(method.begin(), method.end(), method.begin(), toupper);
}
//解析请求报头
void ParseHttpRequestHeader()
{
std::string key;
std::string value;
for(auto& iter : _http_request._request_header){
if(Util::CutString(iter, key, value, SEP))
{
_http_request._header_kv.insert({key, value});
}
}
}
//判定是否需要读取请求正文
bool IsNeedRecvHttpRequestBody()
{
auto& method = _http_request._method;
if(method == "POST"){
auto& header_kv = _http_request._header_kv;
auto iter = header_kv.find("Content-Length");
if(iter != header_kv.end()){
_http_request._content_length = atoi(iter->second.c_str());
return true;
}
}
return false;
}
//读取请求正文
bool RecvHttpRequestBody()
{
if(IsNeedRecvHttpRequestBody()){
int content_length = _http_request._content_length;
auto& body = _http_request._request_body;
char ch = 0;
while(content_length){
ssize_t size = recv(_sock, &ch, 1, 0);
if(size > 0){
body.push_back(ch);
content_length--;
}
else{
_stop = true;
break;
}
}
}
return _stop;
}
//CGI处理
int ProcessCgi()
{
int code = OK;
auto& bin = _http_request._path; //要让子进程执行的目标程序
auto& method = _http_request._method;
//父进程的数据
auto& query_string = _http_request._query_string; //GET
auto& request_body = _http_request._request_body; //POST
int content_length = _http_request._content_length;
auto& response_body = _http_response._response_body;
//站在父进程角度
int input[2];
int output[2];
if(pipe(input) < 0){
LOG(ERROR, "pipe input error!");
code = SERVER_ERROR;
return code;
}
if(pipe(output) < 0){
LOG(ERROR, "pipe output error!");
code = SERVER_ERROR;
return code;
}
pid_t pid = fork();
if(pid == 0){ //child
close(input[0]);
close(output[1]);
//将请求方法通过环境变量传参
std::string method_env = "METHOD=";
method_env += method;
putenv((char*)method_env.c_str());
if(method == "GET"){ //通过环境变量传参
std::string query_env = "QUERY_STRING=";
query_env += query_string;
putenv((char*)query_env.c_str());
LOG(INFO, "GET Method, Add Query_String env");
}
else if(method == "POST"){ //导入正文参数长度
std::string content_length_env = "CONTENT_LENGTH=";
content_length_env += std::to_string(content_length);
putenv((char*)content_length_env.c_str());
LOG(INFO, "POST Method, Add Content_Length env");
}
else{
//Do Nothing
}
dup2(output[0], 0);
dup2(input[1], 1);
execl(bin.c_str(), bin.c_str(), nullptr);
exit(1);
}
else if(pid < 0){
LOG(ERROR, "fork error!");
code = SERVER_ERROR;
return code;
}
else{ //father
close(input[1]);
close(output[0]);
if(method == "POST"){ //将数据写入到管道当中
const char* start = request_body.c_str();
int total = 0;
int size = 0;
while(total < content_length && (size = write(output[1], start + total, request_body.size() - total)) > 0){
total += size;
}
}
//std::string test_string = "2021dragon";
//send(output[1], test_string.c_str(), test_string.size(), 0);
char ch = 0;
while(read(input[0], &ch, 1) > 0){
response_body.push_back(ch);
} //不会一直读,当另一端关闭后会继续执行下面的代码
int status = 0;
pid_t ret = waitpid(pid, &status, 0);
if(ret == pid){
if(WIFEXITED(status)){ //正常退出
LOG(INFO, "正常退出");
if(WEXITSTATUS(status) == 0){ //结果正确
LOG(INFO, "正常退出,结果正确");
code = OK;
}
else{
LOG(INFO, "正常退出,结果不正确");
code = BAD_REQUEST;
}
}
else{
LOG(INFO, "异常退出");
code = SERVER_ERROR;
}
}
//释放文件描述符
close(input[0]);
close(output[1]);
}
return code;
}
//非CGI处理
int ProcessNonCgi()
{
//打开待发送的文件
_http_response._fd = open(_http_request._path.c_str(), O_RDONLY);
if(_http_response._fd >= 0){ //打开文件成功再构建
return OK;
}
return NOT_FOUND;
}
void BuildOkResponse()
{
//构建响应报头
std::string content_type = "Content-Type: ";
content_type += SuffixToDesc(_http_response._suffix);
content_type += LINE_END;
_http_response._response_header.push_back(content_type);
std::string content_length = "Content-Length: ";
if(_http_request._cgi){ //以CGI方式请求
content_length += std::to_string(_http_response._response_body.size());
}
else{ //以非CGI方式请求
content_length += std::to_string(_http_response._size);
}
content_length += LINE_END;
_http_response._response_header.push_back(content_length);
}
void HandlerError(std::string page)
{
_http_request._cgi = false; //正常的网页返回,非CGI返回
_http_response._fd = open(page.c_str(), O_RDONLY);
std::cout<<page.c_str()<<std::endl;
if(_http_response._fd > 0){
std::cout<<page.c_str()<<std::endl;
//构建响应报头
struct stat st;
stat(page.c_str(), &st);
std::string content_type = "Content-Type: text/html";
content_type += LINE_END;
_http_response._response_header.push_back(content_type);
std::string content_length = "Content-Length: ";
content_length += std::to_string(st.st_size);
content_length += LINE_END;
_http_response._response_header.push_back(content_length);
_http_response._size = st.st_size;
}
}
void BuildHttpResponseHelp()
{
int code = _http_response._status_code;
//构建状态行
auto& status_line = _http_response._status_line;
status_line += HTTP_VERSION;
status_line += " ";
status_line += std::to_string(code);
status_line += " ";
status_line += CodeToDesc(code);
status_line += LINE_END;
//构建响应正文,可能包括响应报头
std::string path = WEB_ROOT;
path += "/";
switch(code){
case OK:
BuildOkResponse();
break;
case NOT_FOUND:
path += PAGE_404;
HandlerError(path);
break;
case BAD_REQUEST:
path += PAGE_400;
HandlerError(path);
break;
case SERVER_ERROR:
path += PAGE_500;
HandlerError(path);
break;
default:
break;
}
}
public:
EndPoint(int sock)
:_sock(sock)
,_stop(false)
{}
bool IsStop()
{
return _stop;
}
//读取请求
void RecvHttpRequest()
{
if(!RecvHttpRequestLine()&&!RecvHttpRequestHeader()){ //短路求值
ParseHttpRequestLine();
ParseHttpRequestHeader();
RecvHttpRequestBody();
}
}
//构建响应
void BuildHttpResponse()
{
auto& code = _http_response._status_code;
std::string path;
struct stat st;
size_t pos = 0;
if(_http_request._method != "GET"&&_http_request._method != "POST"){
//非法请求
LOG(WARNING, "method is not right");
code = BAD_REQUEST;
goto END;
}
if(_http_request._method == "GET"){
size_t pos = _http_request._uri.find('?');
if(pos != std::string::npos){
Util::CutString(_http_request._uri, _http_request._path, _http_request._query_string, "?");
_http_request._cgi = true; //上传了参数,需要使用CGI模式
}
else{
_http_request._path = _http_request._uri;
}
}
else if(_http_request._method == "POST"){
_http_request._path = _http_request._uri;
_http_request._cgi = true; //上传了参数,需要使用CGI模式
}
else{
//Do Nothing
}
path = _http_request._path;
_http_request._path = WEB_ROOT;
_http_request._path += path;
if(_http_request._path[_http_request._path.size() - 1] == '/'){
_http_request._path += HOME_PAGE;
}
std::cout<<"debug: "<<_http_request._path.c_str()<<std::endl;
if(stat(_http_request._path.c_str(), &st) == 0){
//资源存在
if(S_ISDIR(st.st_mode)){ //是一个目录,并且不会以/结尾,因为前面已经处理过了
_http_request._path += "/";
_http_request._path += HOME_PAGE;
stat(_http_request._path.c_str(), &st); //path改变,需要重新获取属性
}
else if(st.st_mode&S_IXUSR||st.st_mode&S_IXGRP||st.st_mode&S_IXOTH){ //是一个可执行程序,需要特殊处理
_http_request._cgi = true; //请求的是一个可执行程序,需要使用CGI模式
}
_http_response._size = st.st_size;
}
else{
//资源不存在
LOG(WARNING, _http_request._path + " NOT_FOUND");
code = NOT_FOUND;
goto END;
}
pos = _http_request._path.rfind('.');
if(pos == std::string::npos){
_http_response._suffix = ".html"; //默认设置
}
else{
_http_response._suffix = _http_request._path.substr(pos);
}
if(_http_request._cgi == true){
code = ProcessCgi(); //以CGI的方式进行处理
}
else{
code = ProcessNonCgi(); //简单的网页返回,返回静态网页
}
END:
BuildHttpResponseHelp();
}
//发送响应
void SendHttpResponse()
{
//发送状态行
send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
//发送响应报头
for(auto& iter : _http_response._response_header){
send(_sock, iter.c_str(), iter.size(), 0);
}
//发送空行
send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
//发送响应正文
if(_http_request._cgi){
auto& response_body = _http_response._response_body;
const char* start = response_body.c_str();
size_t size = 0;
size_t total = 0;
while(total < response_body.size()&&(size = send(_sock, start + total, response_body.size() - total, 0)) > 0){
total += size;
}
}
else{
sendfile(_sock, _http_response._fd, nullptr, _http_response._size);
//关闭文件
close(_http_response._fd);
}
}
~EndPoint()
{}
};
class CallBack{
public:
CallBack()
{}
void operator()(int sock)
{
HandlerRequest(sock);
}
void HandlerRequest(int sock)
{
LOG(INFO, "handler request begin");
std::cout<<"get a new link..."<<sock<<std::endl;
#ifdef DEBUG
char buffer[4096];
recv(sock, buffer, sizeof(buffer), 0);
std::cout<<"------------------begin------------------"<<std::endl;
std::cout<<buffer<<std::endl;
std::cout<<"-------------------end-------------------"<<std::endl;
#else
EndPoint* ep = new EndPoint(sock);
ep->RecvHttpRequest();
if(!ep->IsStop()){
LOG(INFO, "Recv No Error, Begin Build And Send");
ep->BuildHttpResponse();
ep->SendHttpResponse();
}
else{
LOG(WARNING, "Recv Error, Stop Build And Send");
}
close(sock);
delete ep;
#endif
LOG(INFO, "handler request end");
}
~CallBack()
{}
};
Makefile
bin=httpserver
cgi=test_cgi
cc=g++
LD_FLAGS=-std=c++11 -lpthread #-DDEBUG=1
curr=$(shell pwd)
src=main.cc
ALL:$(bin) $(cgi)
.PHONY:ALL
$(bin):$(src)
$(cc) -o $@ $^ $(LD_FLAGS)
$(cgi):cgi/test_cgi.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f $(bin) $(cgi)
rm -rf output
.PHONY:output
output:
mkdir -p output
cp $(bin) output
cp -rf wwwroot output
cp $(cgi) output/wwwroot
cp ./cgi/shell_cgi.sh output/wwwroot
cp ./cgi/python_cgi.py output/wwwroot
cp ./cgi/test.html output/wwwroot
main.cc
#include <iostream>
#include <string>
#include <memory>
#include "HttpServer.hpp"
static void Usage(std::string proc)
{
std::cout<<"Usage:\n\t"<<proc<<" port"<<std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2){
Usage(argv[0]);
exit(4);
}
int port = atoi(argv[1]);
std::shared_ptr<HttpServer> svr(new HttpServer(port));
svr->InitServer();
svr->Loop();
return 0;
}
原文链接: link