HTTP协议代码实现

目录

一.   服务端模块实现

二.   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));
}

这个函数可以说是大部分服务器都会设置的一个函数,为什么要设置这个函数呢?

一、解决端口快速重用问题

  1. TIME_WAIT状态:在TCP/IP协议中,当一个TCP连接被关闭后,会进入TIME_WAIT状态。这个状态会持续一段时间(通常是2倍的MSL,即最大报文生存时间),以确保该连接在网络中的残余数据包能够消失,避免新连接接收到旧连接的数据包,从而引发错误。然而,这会导致端口在这段时间内无法被立即重用。

  2. 快速重启需求:服务器在重启或重新绑定端口时,如果端口仍处于TIME_WAIT状态,则无法立即绑定该端口,这会影响服务器的快速重启和服务的连续性。通过设置SO_REUSEADDR,服务器可以在端口处于TIME_WAIT状态时重用该端口,从而避免这个问题。

二、支持多实例和多进程

  1. 多实例部署:在需要部署多个服务器实例(如负载均衡、高可用等场景)时,每个实例可能需要绑定到同一个端口上。通过设置SO_REUSEADDR,可以允许不同的服务器实例(或进程)绑定到同一个端口上,只要它们绑定的IP地址不同即可。

  2. 多进程服务:在单个服务器上运行多个进程,每个进程都监听同一个端口时,也需要设置SO_REUSEADDR。这允许不同的进程在相同的端口上监听,从而提供更高的并发处理能力和更好的资源利用率。

三、提升服务器性能

  1. 减少等待时间:通过设置SO_REUSEADDR,服务器可以减少在端口重用方面的等待时间,从而更快地响应新的连接请求,提升服务器的整体性能。

  2. 优化资源利用:在资源受限的环境中,通过允许端口复用,可以更有效地利用有限的端口资源,避免因为端口不足而导致的服务中断或性能下降。

可以知道的是,像一些大公司的服务器如果崩了的话,不可能等待一定时间才去重启,哪怕这个时间很短,但是造成的损失是很大的。所以需要这个函数来立即重启服务器。

而服务端的第三个成员是一个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;
}

例如这样的登录逻辑。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1550176.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【含文档】基于Springboot+Vue的高校奖助学金系统(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…

柯桥学英语商务口语中老外最爱说的“what‘s up“是什么意思?回答错超尴尬!

“Whats up&#xff1f;” 在看美剧或者和老外聊天的时候 我们总能听到这句话 那你知道这句老外很爱说的 “whats up”是什么意思吗&#xff1f; 快和小编一起来学习吧~ 01 whats up 怎么理解&#xff1f; 很多人觉得Whats up就是更地道的“How are you”&#xff0c;嗯………

【vue3】防抖与节流

1.防抖 &#xff08;多次触发 只执行最后一次&#xff09; 作用&#xff1a; 高频率触发的事件,在指定的单位时间内&#xff0c;只响应最后一次&#xff0c;如果在指定的时间内再次触发&#xff0c;则重新计算时间防抖类似于英雄联盟回城6秒&#xff0c;如果回城中被打断&…

怎么查看员工电脑安装了什么软件

1、使用专业监控软件&#xff1a;安装如金刚钻信息网站行为审计系统、WorkWin等专业的电脑监控软件。这些软件能够实时监控员工的电脑操作&#xff0c;包括安装的软件、运行的程序等。通过软件的管理端&#xff0c;您可以轻松查看员工电脑上安装的所有软件&#xff0c;并可以设…

Library介绍(二)

时序弧&#xff08;timing arc&#xff09; 描述2个节点延迟信息的数据&#xff0c;可以分为net delay和cell delay两大类。 Net delay: drive cell output pin和drived cell input pin之间的net delay&#xff0c;取决于net rc和drive cell驱动能力及drived cell的load。 C…

为什么需要数字集群手持终端?应用优势

在当今社会&#xff0c;无论是应急响应、企业运营还是政府管理&#xff0c;高效、可靠的通讯手段都是成功的关键因素。数字集群手持终端&#xff0c;作为现代通信技术的代表&#xff0c;正成为提升通信效率和确保信息安全的强大工具。据统计&#xff0c;我国的数字集群网络已发…

红蓝攻防实战技术———实战化运营体系的落地

大家好&#xff0c;我是herosunly。985院校硕士毕业&#xff0c;现担任算法研究员一职&#xff0c;热衷于大模型算法的研究与应用。曾担任百度千帆大模型比赛、BPAA算法大赛评委&#xff0c;编写微软OpenAI考试认证指导手册。曾获得阿里云天池比赛第一名&#xff0c;CCF比赛第二…

ZYNQ:点亮LED灯

FPGA 开发流程 1、需求分析&#xff1a;分析需要实现什么功能 2、系统设计&#xff1a;对系统进行设计&#xff0c;需要哪些模块&#xff0c;实现什么功能&#xff0c;数据流怎么走&#xff0c;带宽、工作频率怎么样 3、硬件选项&#xff1a;根据功能、性能需求选择合适的FPGA…

安防区域保护:无线电干扰设备技术详解

在安防区域保护中&#xff0c;无线电干扰设备技术扮演着重要角色&#xff0c;它主要用于通过发射特定频率的无线电波来干扰无人机或其他无线电设备的通信链路、导航信号或控制信号&#xff0c;以达到削弱、阻断甚至控制这些设备运行的目的。以下是对无线电干扰设备技术的详细解…

宝塔环境下MinDoc的安装教程

安装 本教程只适用于CentOS 7&#xff0c;其它系统教程参考&#xff1a;Github地址。 1、下载MinDoc并解压 访问https://github.com/mindoc-org/mindoc/releases下载最新版本并解压 #创建一个目录 mkdir mindoc && cd mindoc#一般宝塔带wget和unzip&#xff0c;如果…

大数据新视界 --大数据大厂之Cassandra 分布式数据库在大数据中的应用与调优

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Open WebUI部署自己的大模型

相关代码地址见文末 1. 概述 Text Generation WebUI 是一个基于网页的界面,提供使用不同语言模型(如Transformers、Llama、ExLlamav2等)进行文本生成的功能。该软件支持以下主要功能: 模型加载:支持从多种框架加载单个或多个LoRA。LoRA微调:在支持的加载器上进行低秩适…

ArcgisEngine开发中,Ifeatureclass.Addfield 报错0x80040655处理方法

1、ArcgisEngine开发中&#xff0c;Ifeatureclass.Addfield 报错0x80040655。如下图所示。 2、经分析&#xff0c;这是由于字段类型错误&#xff0c;经检查&#xff0c;是由于字段名为中文名&#xff0c;超出shp格式的最大字段长度量&#xff0c;看资料说是5个中文字符&#xf…

关于BSV区块链覆盖网络的常见问题解答(上篇)

​​发表时间&#xff1a;2024年9月20日 在BSV区块链上的覆盖网络服务为寻求可扩展、安全、高效交易处理解决方案的开发者和企业家开辟了新的视野。 作为开创性的曼达拉升级的一部分&#xff0c;覆盖网络服务提供了一个强大的框架&#xff0c;用于管理特定类型的交易和数据访问…

城市应急演习演练三维电子沙盘推演系统

深圳易图讯科技(www.3dgis.top)城市应急演习演练三维电子沙盘推演系统&#xff0c;依托大数据、云计算、虚拟现实等先进技术&#xff0c;集成了高清卫星影像、地形数据、实景三维模型等多元信息&#xff0c;真实模拟城市应急场景。该系统通过三维可视化展示和交互操作&#xff…

Pytorch 学习手册

零 相关资料 官方网址 官方网址下的API搜索网站 一 定义 深度学习框架是用于设计、训练和部署深度学习模型的软件工具包。这些框架提供了一系列预定义的组件&#xff0c;如神经网络层&#xff08;卷积层、全连接层等&#xff09;、损失函数、优化器以及数据处理工具&#xf…

Mybatis-Mapper接口方式

目录 配置方式和接口方式的区别 示例&#xff1a;Mapper接口方式实现数据库的CRUD 配置方式和接口方式的区别 Mybatis框架在配置方式的情况下&#xff0c;对数据库的CRUD操作直接通过SqlSession对象来操作&#xff0c;常用的方法有select、insert、update、delete等方法&…

免费送源码:Java+B/S+ssm+MySQL 公众养老服务网上预订系统 计算机毕业设计原创定制

摘 要 本论文主要论述了如何使用JAVA语言开发一个公众养老服务网上预订系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;SSM框架进行开发。在引言中&#xff0c;作者将论述公众养老服务网上预订系统的当前背景以及系统开发…

基于STM32的无人驾驶汽车路径规划与视觉识别系统

目录 引言项目背景环境准备 硬件准备软件安装与配置系统设计 系统架构关键技术代码示例 摄像头图像采集与处理路径规划算法实现实时视觉障碍物检测电机控制与执行应用场景结论 1. 引言 无人驾驶技术是当前自动化和人工智能领域的热门课题之一&#xff0c;涉及到复杂的感知、…

TLS详解

什么是TLS TLS(Transport Layer Security)传输层安全性协议 &#xff0c;它的前身是SSL(Secure Sockets Layer)安全套接层&#xff0c;是一个被应用程序用来在网络中安全的通讯协议&#xff0c; 防止电子邮件、网页、消息以及其他协议被篡改或是窃听。是用来替代SSL的&#xf…