目录
一. 服务端模块实现
二. HTTP协议模块实现
2.1 HTTP请求部分:
2.2 HTTP响应部分:
2.3 工厂类部分:
2.4 HTTP服务端部分:
2.5 回头处理业务处理函数:
三. 调用服务端模块实现
四. 具体效果
五. 改进HTTP服务端模块
本篇博客我们来讲如何实现一个HTTP,此篇涉及前端和后端。快快准备好小板凳来听吧!!!
前面我们已经讲了HTTP原理部分,不懂的小伙伴可以点击补缺。(点此查看)
一. 服务端模块实现
首先我们来讲服务端的实现。此篇代码会贯彻高内聚低耦合的编码思想。因此我们将服务端封装成类。
class TcpServer
{
public:TcpServer(int port, http_t http_service): _localaddr("0", port), _listensock(make_unique<TcpSocket>()){_listensock->BuildListenSocket(_localaddr);}~TcpServer(){}private:InetAddr _localaddr;unique_ptr<Socket> _listensock;bool _isrunning;
};
首先,介绍一下类成员,第一个成员是我们封装的类。目的是为了方便代码编写。来看看吧:
#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>using namespace std;class InetAddr
{
private:void GetAddress(string* ip,uint16_t* port){*port=ntohs(_addr.sin_port);//网络字节序转为主机字节序*ip=inet_ntoa(_addr.sin_addr);//将网络字节序IP转为点分式十进制IP}
public:InetAddr(const struct sockaddr_in &addr):_addr(addr){GetAddress(&_ip,&_port);}InetAddr(const string& ip,uint16_t port):_ip(ip),_port(port){_addr.sin_family=AF_INET;_addr.sin_port=htons(_port);_addr.sin_addr.s_addr=inet_addr(_ip.c_str());}InetAddr(){}bool operator==(const InetAddr& addr){if(_ip==addr._ip && _port==addr._port){return true;}return false;}struct sockaddr_in Addr(){return _addr;}string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr(){}
private:struct sockaddr_in _addr;string _ip;uint16_t _port;
};
第二个类成员通过智能指针包装了我们封装的类---Socket,因为我们为了提高代码自主性。来看看Socket类具体是怎么实现的吧:
#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <functional>
#include <sys/wait.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#include "InetAddr.hpp"
#include "Log.hpp"// 模板方法模式namespace socket_ns
{class Socket;const static int gbacklog = 8;using socket_sptr = shared_ptr<Socket>;enum{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR,USAGE_ERROR};// unique_ptr<Socket> listensock=make_unique<TcpSocket>();// listensock->BuildListenSocket();// unique_ptr<Socket> clientsock=make_unique<TcpSocket>();// clientsock->BuildClientSocket();// clientsock->Send();// clientsock->Recv();class Socket{public:virtual void CreateSocketOrDie() = 0;virtual void BindSocketOrDie(InetAddr &addr) = 0;virtual void ListenSocketOrDie() = 0;//地址复用virtual socket_sptr Accepter(InetAddr *addr) = 0;virtual bool Connectcor(InetAddr &addr) = 0;virtual void SetSocketAddrReuse() = 0;//设置地址复用,一般服务器必须设置virtual int Sockfd() = 0;virtual int Recv(string *out) = 0;virtual int Send(const string &in) = 0;virtual void Close() = 0;// virtual void Recv()=0;// virtual void Send()=0;// virtual void other()=0;public:void BuildListenSocket(InetAddr &addr){CreateSocketOrDie();SetSocketAddrReuse();BindSocketOrDie(addr);ListenSocketOrDie();}bool BuildClientSocket(InetAddr &addr){CreateSocketOrDie();return Connectcor(addr);}};class TcpSocket : public Socket{public:TcpSocket(int fd = -1): _sockfd(fd){}void CreateSocketOrDie() override{// 1.创建流式套接字_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "socket error\n");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, sockfd is: %d\n", _sockfd);}void BindSocketOrDie(InetAddr &addr){// 2.bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(addr.Port());local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);}LOG(DEBUG, "bind success,sockfd is: %d\n", _sockfd);}void ListenSocketOrDie() override{int n = ::listen(_sockfd, gbacklog);if (n < 0){LOG(FATAL, "listen error\n");exit(LISTEN_ERROR);}LOG(DEBUG, "listen success,sockfd is: %d\n", _sockfd);}socket_sptr Accepter(InetAddr *addr){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");return nullptr;}*addr = peer;socket_sptr sock = make_shared<TcpSocket>(sockfd);return sock;}bool Connectcor(InetAddr &addr){// 构建目标主机的socket信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(addr.Port());server.sin_addr.s_addr = inet_addr(addr.Ip().c_str());int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){cerr << "connect error" << endl;return false;}return true;}void SetSocketAddrReuse() override{int opt=1;::setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT,&opt,sizeof(opt));}int Sockfd() override{return _sockfd;}int Recv(string *out) override{char inbuffer[4096];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){inbuffer[n] = 0;*out += inbuffer; //??? +=}return n;}int Send(const string &in) override{int n = ::send(_sockfd, in.c_str(), in.size(), 0);return n;}void Close() override{if (_sockfd > -1){::close(_sockfd);}}private:int _sockfd;};
}
对于Socket类,最为值得说道的是地址复用函数---SetSocketAddrReuse。
void SetSocketAddrReuse() override
{int opt=1;::setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT,&opt,sizeof(opt));
}
这个函数可以说是大部分服务器都会设置的一个函数,为什么要设置这个函数呢?
一、解决端口快速重用问题
TIME_WAIT状态:在TCP/IP协议中,当一个TCP连接被关闭后,会进入TIME_WAIT状态。这个状态会持续一段时间(通常是2倍的MSL,即最大报文生存时间),以确保该连接在网络中的残余数据包能够消失,避免新连接接收到旧连接的数据包,从而引发错误。然而,这会导致端口在这段时间内无法被立即重用。
快速重启需求:服务器在重启或重新绑定端口时,如果端口仍处于TIME_WAIT状态,则无法立即绑定该端口,这会影响服务器的快速重启和服务的连续性。通过设置SO_REUSEADDR,服务器可以在端口处于TIME_WAIT状态时重用该端口,从而避免这个问题。
二、支持多实例和多进程
多实例部署:在需要部署多个服务器实例(如负载均衡、高可用等场景)时,每个实例可能需要绑定到同一个端口上。通过设置SO_REUSEADDR,可以允许不同的服务器实例(或进程)绑定到同一个端口上,只要它们绑定的IP地址不同即可。
多进程服务:在单个服务器上运行多个进程,每个进程都监听同一个端口时,也需要设置SO_REUSEADDR。这允许不同的进程在相同的端口上监听,从而提供更高的并发处理能力和更好的资源利用率。
三、提升服务器性能
减少等待时间:通过设置SO_REUSEADDR,服务器可以减少在端口重用方面的等待时间,从而更快地响应新的连接请求,提升服务器的整体性能。
优化资源利用:在资源受限的环境中,通过允许端口复用,可以更有效地利用有限的端口资源,避免因为端口不足而导致的服务中断或性能下降。
可以知道的是,像一些大公司的服务器如果崩了的话,不可能等待一定时间才去重启,哪怕这个时间很短,但是造成的损失是很大的。所以需要这个函数来立即重启服务器。
而服务端的第三个成员是一个bool类型的成员,如果该值为真,则服务器运行,为假则关闭服务器。
服务端我们的思想是:实现基本的套接字建立,而让上层去实现具体的http服务。其实这个思想我们在前面很多博客都已经讲过,可以看出这个思想是很实用的。
我们再来实现具体的服务端具体细节。在此之前我还封装了一个类。
struct ThreadData
{
public:ThreadData(socket_sptr fd, InetAddr addr, TcpServer *s): sockfd(fd), clientaddr(addr), self(s){}public:socket_sptr sockfd;InetAddr clientaddr;TcpServer *self;
};
封装这个类是为了后续调用上层处理函数的时候能更方便。服务端细节如下:
using http_t = function<>; // 处理方法,留待后续补充class TcpServer
{
public:TcpServer(int port, http_t http_service): _localaddr("0", port), _listensock(make_unique<TcpSocket>()), _http_service(http_service), _isrunning(false){_listensock->BuildListenSocket(_localaddr);}static void *HandlerSock(void *args){//留待后续实现}void Loop(){_isrunning = true;// 4.不能直接接收数据,应该先获取连接while (_isrunning){InetAddr peeraddr;socket_sptr normalsock = _listensock->Accepter(&peeraddr);if (normalsock == nullptr)continue;// 采用多线程// 此处不能像多进程一样关闭文件描述符,因为多线程文件描述符表是共享的pthread_t t;ThreadData *td = new ThreadData(normalsock, peeraddr, this);pthread_create(&t, nullptr, HandlerSock, td); // 将线程分离}_isrunning = false;}~TcpServer(){}private:InetAddr _localaddr;unique_ptr<Socket> _listensock;bool _isrunning;http_t _http_service;
};
具体的服务处理函数留待本博客后续讲解,先往下看吧。
二. HTTP协议模块实现
此处我们需要实现HTTP协议,首先我们知道HTTP是有请求与响应的。
2.1 HTTP请求部分:
我们仍然封装成一个类来实现具体细节:
static const string sep = "\r\n";
static const string header_sep = ": ";
static const string wwwroot = "wwwroot";
static const string homepage = "index.html";static const string httpversion="HTTP/1.0";
static const string space=" ";
static const string filesuffixsep=".";
static const string args_sep="?";//http内部,要根据目标要访问的资源的文件后缀,区分清楚文件类型,通过Content-Type告诉浏览器,我的response的正文的类型!
class HttpRequest
{
private:string GetOneline(string &req){if (req.empty())return req;auto pos = req.find(sep);if (pos == string::npos)return string();string line = req.substr(0, pos);req.erase(0, pos + sep.size());return line.empty() ? sep : line;}bool ParseHeaderHelper(const string &line, string *k, string *v){auto pos = line.find(header_sep);if (pos == string::npos){return false;}*k = line.substr(0, pos);*v = line.substr(pos + header_sep.size());return true;}public:HttpRequest(): _blank_line(sep), _path(wwwroot)//所有请求的资源默认都会去wwwroot路径下找{}void Serialize() // 此处没写,是因为直接拿的浏览器作为客户端做实验,序列化浏览器自动就做了{}void Deserialize(string &req){_req_line = GetOneline(req);while (true){string line = GetOneline(req);if (line.empty())break;else if (line == sep){_req_text = req;break;}else{_req_header.emplace_back(line);}}ParseReqLine();ParseHeader();}bool ParseReqLine(){if (_req_line.empty())return false;stringstream ss(_req_line);ss >> _method >> _url >> _version;// /index.html?use=zhangsan&passwd=123456if(strcasecmp("get",_method.c_str())==0){auto pos=_url.find(args_sep);if(pos!=string::npos){LOG(INFO,"change begin: url: %s,_args: %s\n",_url.c_str(),_args.c_str());_args=_url.substr(pos+args_sep.size());_url.resize(pos);LOG(INFO,"change done: url: %s,_args: %s\n",_url.c_str(),_args.c_str());}}_path += _url;// 判断一下是不是请求的/---wwwroot/if (_path[_path.size() - 1] == '/')//默认没有指明请求的什么资源时,则拼上首页(wwwroot/index.html){_path += homepage;}auto pos=_path.rfind(filesuffixsep);if(pos==string::npos){_suffix=".unknown";}else{_suffix=_path.substr(pos);}LOG(INFO,"client want get %s, _suffix: %s\n",_path.c_str(),_suffix.c_str());return true;}bool ParseHeader(){for (auto header : _req_header){string k, v;if (ParseHeaderHelper(header, &k, &v)){_headers.insert(make_pair(k, v));}}return true;}void Print(){cout << "===" << _req_line << endl;for (auto &header : _req_header){cout << "***" << header << endl;}cout << _blank_line;cout << _req_text << endl;cout << "method ### " << _method << endl;cout << "url ### " << _url << endl;cout << "path ### " << _path << endl;cout << "httpversion ### " << _version << endl;for (auto& header : _headers){cout << "@@@" << header.first << " - " << header.second << endl;}}string Path(){return _path;}string Suffix(){return _suffix;}bool IsExec(){return !_args.empty() || !_req_text.empty();}string Args(){return _args;}string Text(){return _req_text;}string Method(){return _method;}string Url(){return _url;}~HttpRequest(){}private:// 原始协议内容string _req_line; // 请求行vector<string> _req_header;//请求报头string _blank_line;//空行string _req_text;//请求正文// 期望解析的结果string _method; // 请求方法string _url;string _args; //参数string _path; // 实际访问资源路径string _version; // http版本string _suffix;//文件后缀unordered_map<string, string> _headers;
};
我们拿出HTTP请求具体格式:
首先来分析成员变量:
成员变量:
变量都是HTTP请求格式里面的内容,可以对照代码中注释理解。此处重点说一下_suffix成员与_headers成员以及_url成员与_path成员的区别。
_suffix成员:文件后缀。为什么要加这个呢?因为服务器接收请求之后,需要根据这个文件后缀明确文件类型,进而确定HTTP响应的正文部分是什么类型。
_headers成员:即请求报头的Key与Value。这个很好理解,请求报头中Key与Value是一对一对的,所以想到用unordered_map类型存储。
_url成员与_path成员的区别:
前面博客已经提及过url相当于平时输入得网址,url与实际路径的关系就是:url是包含实际路径的。所以我们设置了两个成员变量。
然后说回成员函数,有读者会发现我们并没有实现序列化,而是只实现了反序列化。这是为什么呢?
其实是因为我们选用的是浏览器作为客户端,而浏览器会自动进行序列化,所以不需实现序列化。
然后来说说这个反序列化函数,反序列化函数需要做的就是将发送过来的序列化字符串变成结构化数据,在这个请求类中说就是根据发送过来的字符串填写相应的成员变量。
首先我们来看看第一个函数:
static const string sep = "\r\n";string GetOneline(string &req)
{if (req.empty())return req;auto pos = req.find(sep);if (pos == string::npos)return string();string line = req.substr(0, pos);req.erase(0, pos + sep.size());return line.empty() ? sep : line;
}
此函数的目的在于获取一行数据,也就是在发送过来的字符串中截取一行的数据。
而我们定义的规则是,一行数据结束后,需要加上 "\r\n" 来分割。所以我们就用这个来作为行与行的依据。
那么反序列化函数前几行代码就自然而然解释得通了:
_req_line = GetOneline(req);
while (true)
{string line = GetOneline(req);if (line.empty())break;else if (line == sep){_req_text = req;break;}else{_req_header.emplace_back(line);}
}
首先将最开始的一行给成员变量_req_line,表示请求行。然后就是读取请求报头的部分。
由于并不知道有多少行请求报头,所以采用循环读取的方式,如果该行请求报头不为空或者不是分隔符"\r\n" ,就填写进_req_header成员变量中。
然后就需要对请求行与请求报头做处理了。首先是对请求行做处理。
static const string args_sep="?";bool ParseReqLine()
{if (_req_line.empty())return false;stringstream ss(_req_line);ss >> _method >> _url >> _version;// /index.html?use=zhangsan&passwd=123456if(strcasecmp("get",_method.c_str())==0){auto pos=_url.find(args_sep);if(pos!=string::npos){LOG(INFO,"change begin: url: %s,_args: %s\n",_url.c_str(),_args.c_str());_args=_url.substr(pos+args_sep.size());_url.resize(pos);LOG(INFO,"change done: url: %s,_args: %s\n",_url.c_str(),_args.c_str());}}_path += _url;// 判断一下是不是请求的/---wwwroot/if (_path[_path.size() - 1] == '/')//默认没有指明请求的什么资源时,则拼上首页(wwwroot/index.html){_path += homepage;}auto pos=_path.rfind(filesuffixsep);if(pos==string::npos){_suffix=".unknown";}else{_suffix=_path.substr(pos);}LOG(INFO,"client want get %s, _suffix: %s\n",_path.c_str(),_suffix.c_str());return true;
}
首先我们采用了标准输入流的子类stringstream(点此了解详情),此输入流优点在于能自动根据空格分隔来依次填写变量内容。
我们考虑到如果请求方法是Get的话,则我们需要提取出查询参数_args,就像/index.html?use=zhangsan&passwd=123456一样,?后面的就是查询参数,需要单独提取出来。那为什么单单get方法需要这样,而Post方法却不需要呢。
是因为Get方法设计初衷是幂等的,意味着多次执行相同的GET请求不会对资源产生副作用。因此,GET请求的参数通常放在URL中,以便可以被缓存、书签化和重新发送而不会导致数据变更。
而Post方法则不同,Post方法设计初衷不是幂等的,多次执行相同的POST请求可能会导致不同的结果(如数据重复提交)。POST请求的参数放在HTTP请求的主体(body)中,而不是URL中,这是为了允许发送大量的数据,并且不暴露敏感信息在URL上。所以不需单独处理url。
然后继续对请求报头做处理:
bool ParseHeaderHelper(const string &line, string *k, string *v)
{auto pos = line.find(header_sep);if (pos == string::npos){return false;}*k = line.substr(0, pos);*v = line.substr(pos + header_sep.size());return true;
}bool ParseHeader()
{for (auto header : _req_header){string k, v;if (ParseHeaderHelper(header, &k, &v)){_headers.insert(make_pair(k, v));}}return true;
}
对请求报头做处理是比较简单的事,只需取出Key和Value,并将这两部分插入到成员变量_headers中即可。
由此我们就简单实现了HTTP请求的处理,继续来说HTTP响应的处理。
2.2 HTTP响应部分:
我们也封装成一个类来实现细节:
class HttpResponse
{
public:HttpResponse():_version(httpversion),_blank_line(sep){}void AddStatusLine(int code,const string& desc)//添加状态行{_code=code;_desc=desc;}void AddHeader(const string &k,const string &v)//添加报头{LOG(DEBUG,"AddHeader: %s->%s\n",k.c_str(),v.c_str());_headers[k]=v;}void AddText(string text){_resp_text=text;}string Serialize(){_status_line=_version+space+to_string(_code)+space+_desc+sep;for(auto &header: _headers){_resp_header.emplace_back(header.first+header_sep+header.second+sep);}//序列化string respstr=_status_line;for(auto &header:_resp_header){respstr+=header;}respstr+=_blank_line;respstr+=_resp_text;return respstr;}// 同样也不用做反序列化,是响应给浏览器,会自动反序列化~HttpResponse() {}private:// 构建应答的必要字段string _version;//版本int _code;//状态码string _desc;//状态描述unordered_map<string,string> _headers;//报头// 应答的结构化字段string _status_line;vector<string> _resp_header;string _blank_line;string _resp_text;
};
HTTP响应的处理相对于HTTP请求显得就很简单了,我们着重讲一下序列化部分。此处刚好与HTTP请求是相反的,HTTP响应不需事先反序列化,因为响应给浏览器,而浏览器会自动做反序列化。
static const string header_sep = ": ";
static const string space=" ";
static const string sep = "\r\n";string Serialize()
{_status_line=_version+space+to_string(_code)+space+_desc+sep;for(auto &header: _headers){_resp_header.emplace_back(header.first+header_sep+header.second+sep);}//序列化string respstr=_status_line;for(auto &header:_resp_header){respstr+=header;}respstr+=_blank_line;respstr+=_resp_text;return respstr;
}
序列化只需将结构化数据转换成字符串即可,操作还是很简单的,只需记得在每一行的结束加上分隔符 "\r\n" 即可。
2.3 工厂类部分:
我们额外分装了一个封装类:
class Factory
{
public:static shared_ptr<HttpRequest> BuildHttpRequest(){return make_shared<HttpRequest>();}static shared_ptr<HttpResponse> BuildHttpResponse(){return make_shared<HttpResponse>();}
};
为的是方便创建指针,并且这个指针还是智能指针,指针里面的模板参数是HTTP请求与HTTP响应。
2.4 HTTP服务端部分:
我们最后就要实现的是HTTP服务端,前面一直讲的是HTTP请求与响应,那么就应该有一个专门的模块来发起请求和处理响应,这也就是我们在服务端模块里面说的上层处理模块。
我们先来实际看一下具体的HTTP请求与HTTP,所以设计一个初始版本的HTTP服务端:
class HttpServer
{
public:HttpServer(){_mime_type.insert(make_pair(".html","text/html"));_mime_type.insert(make_pair(".css","text/css"));_mime_type.insert(make_pair(".js","application/x-javascript"));_mime_type.insert(make_pair(".png","image/png"));_mime_type.insert(make_pair(".jpg","image/jpeg"));_mime_type.insert(make_pair(".unknown","text/html"));_code_to_desc.insert(make_pair(100,"Continue"));_code_to_desc.insert(make_pair(200,"OK"));_code_to_desc.insert(make_pair(301,"Moved Permanently"));_code_to_desc.insert(make_pair(302,"Found"));_code_to_desc.insert(make_pair(404,"Not Found"));_code_to_desc.insert(make_pair(500,"Internal Server Error"));}string HandlerHttpRequest(string req){cout << "-----------------" << endl;cout << req << endl;string response = "HTTP/1.0 200 OK\r\n";response += "\r\n";response += "<html><body><h1>hello world!</h1></body></html>";return response;}~HttpServer(){}private:unordered_map<string,string> _mime_type;//文件后缀对应的类型unordered_map<int,string> _code_to_desc;
};
首先还是来解释一下成员变量:
成员变量:
_mime_type成员:这个就是我们的文件后缀与其对应的文件类型组成的unordered_map,前面说过每个后缀都有其对应的文件类型,所以我们在这里手动添加几种。
_code_to_desc成员:这个是我们的状态码与其对应的描述组成的unordered_map,在上篇博客讲过每种状态码对应的描述不一样,所以在这里添加几种样例。
在构造函数中的代码都是填充这两个成员的细节。
在 HandlerHttpRequest 函数中实现的就是具体的HTTP请求处理部分,需要返回HTTP响应,也就是我们的前端部分。
string response = "HTTP/1.0 200 OK\r\n";
response += "\r\n";
response += "<html><body><h1>hello world!</h1></body></html>";
- 第一部分
"HTTP/1.0 200 OK\r\n"
是HTTP响应的状态行,表示服务器使用HTTP 1.0协议,响应状态码为200(OK),表示请求已成功处理。\r\n
是HTTP消息中的换行符。- 第二部分
"\r\n"
是一个空行,HTTP协议要求状态行之后必须有一个空行,以分隔状态行和响应头(尽管在这个例子中,响应头部分被省略了)。- 第三部分
"<html><body><h1>hello world!</h1></body></html>"
是响应体,包含了HTML格式的文本,当这个响应被Web浏览器接收到时,会显示“hello world!”的标题。
2.5 回头处理业务处理函数:
我们再回头处理一下服务端模块中的服务处理函数:
using http_t = function<string(string request)>; // 处理方法struct ThreadData
{
public:ThreadData(socket_sptr fd, InetAddr addr, TcpServer *s): sockfd(fd), clientaddr(addr), self(s){}public:socket_sptr sockfd;InetAddr clientaddr;TcpServer *self;
};class TcpServer
{
public:TcpServer(int port, http_t http_service): _localaddr("0", port), _listensock(make_unique<TcpSocket>()), _http_service(http_service), _isrunning(false){_listensock->BuildListenSocket(_localaddr);}static void *HandlerSock(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);string request,response;ssize_t n = td->sockfd->Recv(&request);//读取请求if (n > 0){response=td->self->_http_service(request);td->sockfd->Send(response);//发送请求}td->sockfd->Close();delete td;return nullptr;}void Loop(){_isrunning = true;// 4.不能直接接收数据,应该先获取连接while (_isrunning){InetAddr peeraddr;socket_sptr normalsock = _listensock->Accepter(&peeraddr);if (normalsock == nullptr)continue;// Version 2:采用多线程// 此处不能像多进程一样关闭文件描述符,因为多线程文件描述符表是共享的pthread_t t;ThreadData *td = new ThreadData(normalsock, peeraddr, this);pthread_create(&t, nullptr, HandlerSock, td); // 将线程分离}_isrunning = false;}~TcpServer(){}private:InetAddr _localaddr;unique_ptr<Socket> _listensock;bool _isrunning;http_t _http_service;
};
我们的 http_t 类型就是处理业务函数,可以发现他其实就是我们HTTP服务端里面的HandlerHttpRequest 函数。那应该如何用这个处理函数呢?
我们来看看具体的HandlerSock函数:
static void *HandlerSock(void *args)
{pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);string request,response;ssize_t n = td->sockfd->Recv(&request);//读取请求if (n > 0){response=td->self->_http_service(request);td->sockfd->Send(response);//发送响应}td->sockfd->Close();delete td;return nullptr;
}
其实就是读取发送过来的请求,并且将响应发送回去。
三. 调用服务端模块实现
继续实现服务端的调用问题:
// ./httpserver serverport
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return 1;}uint16_t port = stoi(argv[1]);HttpServer httpservice;TcpServer tcpsvr(port,bind(&HttpServer::HandlerHttpRequest,&httpservice,placeholders::_1));tcpsvr.Loop();return 0;
}
很简单,其实就是创建一个服务端对象,再用这个对象调用 Loop 函数即可。来看看具体效果:
四. 具体效果
首先运行起来服务端,然后再浏览器上输入对应的IP地址加端口号(就是上图中红色方框内部,读者们记得输入自己的IP哦):
就会显示出来了,那我们的HTTP请求的格式是什么样的呢?
就是这两个,诶有读者会发现我不是只请求了一次吗,为什么会有两个请求呢?
其实第一个是我们的HTTP请求,而第二个请求是我们申请图标的请求:
就像我们百度的时候,左边的这个小爪子。
五. 改进HTTP服务端模块
实际运用中,肯定不能像前面一样写一个简单的前端代码,而是一个完整的页面。因此要完善HTTP请求与响应的结构。
我们用重定向状态码 302 来演示:
string HandlerHttpRequest(string req)
{auto request = Factory::BuildHttpRequest();request->Deserialize(req);int contentsize=0;string text=ReadFileContent(request->Path(),&contentsize);string suffix=request->Suffix();int code=302;auto response=Factory::BuildHttpResponse();response->AddStatusLine(code,_code_to_desc[code]);//http协议已经给我们规定好了不同文件后缀对应的Content-Typeresponse->AddHeader("Content-Type",_mime_type[suffix]);response->AddHeader("Location","https://www.qq.com/");return response->Serialize();
}
我们先来说说 ReadFileContent 函数:
string ReadFileContent(const string &path,int *size)
{//要按照二进制打开,不能用文本方式打开,因为会有图片等等ifstream in(path,ios::binary);if(!in.is_open()){return string();}in.seekg(0,in.end);int filesize=in.tellg();in.seekg(0,in.beg);string content;content.resize(filesize);in.read((char*)content.c_str(),filesize);in.close();*size=filesize;return content;
}
这里要注意的是,我们读取要按照二进制方式打开,不能用文本方式打开。因为读取的数据可能是图片等等,用文本方式是不能读取图片的。
我们再来看看改进的HTTP服务端模块到底是怎么实现的吧:
首先是构建出HTTP请求,然后根据请求中的路径读取数据,并获取其对应的文件后缀,即其文件类型。然后设置状态码为 302,并构建出响应,并且构建好响应报头。最后序列化后返回即可。
来看看效果吧,我们可以预测一下,如果是正确的,我们用浏览器访问就应该跳转到 腾讯官网:
可以发现确实是如此,因此我们这个结构是正确的。
既然说到重定向我们就继续说说我们平时看视频的时候,如果有些视频需要会员才能观看,而你刚好又不是会员,你看了几分钟然后就自动跳转到会员充值页面,这是怎么实现的呢?
其实这就是运用了重定向的原理:
string HandlerHttpRequest(string req)
{ auto request = Factory::BuildHttpRequest();request->Deserialize(req);auto response=Factory::BuildHttpResponse();string newurl="https://www.baidu.com/";int code=0;if(request->Path()=="wwwroot/redir"){code=301;response->AddStatusLine(code,_code_to_desc[code]);response->AddHeader("Location",newurl);}else{code=200;int contentsize=0;string text=ReadFileContent(request->Path(),&contentsize);response->AddStatusLine(code,_code_to_desc[code]);response->AddHeader("Content-Length",to_string(contentsize));response->AddText(text);}string suffix=request->Suffix();response->AddHeader("Content-Type",_mime_type[suffix]);return response->Serialize();
}
如果请求的路径是 "wwwroot/redir" ,那么就会进行重定向到百度官网。
我们再来看一个改进版本,此版本对请求中包含了特定的逻辑进行了处理,例如登录请求:
string HandlerHttpRequest(string req)
{auto request = Factory::BuildHttpRequest();request->Deserialize(req);if(request->IsExec()){auto response=_funcs[request->Path()](request);return response->Serialize();}else{auto response=Factory::BuildHttpResponse();int code=200;int contentsize=0;string text=ReadFileContent(request->Path(),&contentsize);if(text.empty())//访问内容为空,即访问路径错误了{code=404;response->AddStatusLine(code,_code_to_desc[code]);string text404=ReadFileContent("wwwroot/404.html",&contentsize);response->AddHeader("Content-Length",to_string(contentsize));response->AddHeader("Content-Type",_mime_type[".html"]);response->AddText(text404);}else{string suffix=request->Suffix();response->AddStatusLine(code,_code_to_desc[code]);response->AddHeader("Content-Length",to_string(contentsize));response->AddHeader("Content-Type",_mime_type[suffix]);response->AddText(text);}return response->Serialize();}auto request = Factory::BuildHttpRequest();request->Deserialize(req);auto response=Factory::BuildHttpResponse();string newurl="https://www.baidu.com/";// string newurl="https://8.130.126.156:8888/3.html";//原理类似于看视频看一会跳转到充值会员页面int code=0;if(request->Path()=="wwwroot/redir"){code=301;response->AddStatusLine(code,_code_to_desc[code]);response->AddHeader("Location",newurl);}else{code=200;int contentsize=0;string text=ReadFileContent(request->Path(),&contentsize);response->AddStatusLine(code,_code_to_desc[code]);response->AddHeader("Content-Length",to_string(contentsize));response->AddText(text);}string suffix=request->Suffix();response->AddHeader("Content-Type",_mime_type[suffix]);return response->Serialize();
}
其中_funcs是新增的HTTP服务端成员变量:
unordered_map<string,func_t> _funcs;
这样当请求中有特定的逻辑时,就会去执行特定的逻辑:
shared_ptr<HttpResponse> Login(shared_ptr<HttpRequest> req)
{LOG(DEBUG,"===================\n");string userdata;if(req->Method()=="GET"){userdata=req->Args();}else if(req->Method()=="POST"){userdata=req->Text();}LOG(DEBUG,"enter data handler,data is: %s\n",userdata.c_str());auto response=Factory::BuildHttpResponse();response->AddStatusLine(200,"OK");response->AddHeader("Content-Type","text/html");response->AddText("<html><h1>handler data done</h1></html>");LOG(DEBUG,"==================\n");return response;
}
例如这样的登录逻辑。