linux网络之Http协议用法及说明
一、认识URL

解析
URI,也叫做统一资源定位符!
http://是协议,作用是规定通信的规?开头,&分割多个参数;给资源传入参数,筛选、定位资源则,http默认端口是80,https默认端口是443
user:pass@是登录认证信息,是用户名,密码,用于登录认证,但是可以省略
www.example.jp是服务器地址,叫做主机域名,而浏览器会通过DNS解析,将域名解析为ip地址,定位到唯一的主机上
:80是端口号,协议默认端口可以不写,作用是定位到主机上的唯一网络服务
/dir/index.htm是带层次的文件路径,是服务器上资源的文件路径,定位主机内部的资源文件,/是路径分隔符
?uid=1是查询字符串(URI),?开头,&分割多个参数,给资源传入参数,筛选、定位资源。
#ch1是片段标识符,#后面内容,定位页面内部某个位置,不会发给服务器,只在浏览器本地生效。
五点核心理解
- 域名→IP:人记域名,浏览器做 DNS 解析转换成 IP 地址,找到网络中的主机。
- 端口号定位服务:IP 找到机器,端口找到机器上运行的网络服务。浏览器会自动补协议对应的默认端口。
- 文件路径:Linux 一切皆文件,web 资源本质就是服务器上的文件,路径指明访问哪个文件。
- 查询字符串
?key=val&key2=val2:同一路径下,靠不同参数区分不同内容,提交参数。 - #片段锚点:页面内跳转,不参与网络请求,浏览器本地跳转页面位置。
总结
1.域名 + 路径:定位一台主机上的某个文件资源
2.协议 + 端口:确定使用哪种服务来收发数据
3.查询字符串:进一步筛选资源、传递参数
4.所有可以用 URL 访问到的资源,属于万维网 WWW 资源。
二、urlencode 和 urldecode(了解)
介绍

在URL里面的部分字符有特殊含义:/ ? : & = # @
这些符号会被浏览器解析为URL结构标记,不能够当做普通参数内容
举例:如果我要传递参数 aaa=7/9&bbb,这里的/、&在 URL 里有特殊语义,如果不编码,服务器就分不清哪些是参数分隔符,哪些是传入的数据,会解析错乱。所以含有特殊字符的参数,必须先做urlencode 转义;服务器收到后做urldecode 还原。
urlencode的编码规则

urldecode的解码规则

什么时候才会触发编码?
查询字符串?key=value的value部分,如果包含& / ? + # 空格,必须编码。
总结
注意:URL 结构符号(? &)本身不要编码,只有参数内容编码。
坑:自己拼接 URL 字符串时,如果直接拼接带特殊符号的字符串,会产生错误 URL,必须手动调用 urlencode。 浏览器会自动对表单、搜索框内容做 urlencode;但是我们写代码构造 http 请求,需要自己实现或者调用库做编码。
三、Http协议的格式
在网络中最常见的两个行为:
(1)从网络中将别人的资源拿下来:GET
(2)将自己的资源传送到网络上:POST

http请求一共四个部分:请求行,请求报头,空行,请求正文
http响应一共四个部分:状态行,响应报头,空行,响应正文
底层本质:http1.1是基于TCP的文本协议,所有报文本质就是字符串,使用了\r\n作为行结束标志!!
HTTP REQUST

3.1请求行
![]()
3.1.1请求方法
代表本次客户端想要对资源进行什么操作,有GET和POST两种操作,GET是获取资源,将参数放在URI字符串,一般不带请求正文;POST是提交资源,将数据放在请求正文。
3.1.2URL(统一资源标识符)
URL:
![]()
重点区分:URI ≠ 完整 URL
URI只写:/search?wd=C%2B%2B,包含资源路径 + 查询参数;不带域名、不带端口。
域名 / 端口放在请求头Host字段里面传递给服务器。
URL包括:
- 协议:
http - 主机 (host):
job.xjtu.edu.cn - 资源路径 (_uri):
/compony/login.do
3.1.3HTTP版本
HTTP/1.1:最主流,支持长连接、Host头部必填。
HTTP/1.0:短连接,请求完成默认断开。
HTTP/2、HTTP/3:改为二进制帧,不再是这种文本报文格式。
3.2请求报头

每一行的格式必须为key:空格 value\r\n,必须严格按照格式来,否则服务器就会解析失败,每个报头占一行,每一行都以\r\n结尾。
几个核心报头深度理解
Host: job.xjtu.edu.cn:Host 告诉服务器,我这次访问的是哪一个域名。没有 Host 头部,服务器不知道返回哪个网站内容。
Connection: keep‑alive:开启长连接。TCP 连接这次请求完成之后不关闭,可以复用这条 socket 继续发多个 HTTP 请求,减少反复建立断开 TCP 的开销。HTTP1.1 默认就是 keep‑alive。 如果值是close,代表本次响应完成就要断开 TCP。
Content‑Length: 36:代表的是请求正文的总长度为36字节,服务器在读的时候,就知道后面要读取36个字节作为body,我们来看一下底部请求正文的长度,正好为36个字节。
Cache‑Control: max‑age=0:缓存控制,告诉服务器,不要用本地缓存,而是向服务器获取最新的资源,一般登录这种敏感操作才会用这种!
Origin: http://job.xjtu.edu.cn:跨域相关头。标识这个请求来源于哪个源(协议 + 域名 + 端口),浏览器发起跨域请求会带上 Origin,服务器用它判断是否允许跨域访问。本次是同域登录,Origin 和 Host 一致。
Upgrade‑Insecure‑Requests: 1:浏览器告诉服务器:优先希望升级成 HTTPS(https 加密访问),1 代表开启该特性。
Content‑Type: application/x‑www‑form‑urlencoded:作用是说明请求正文的数据格式,application/x‑www‑form‑urlencoded:普通表单编码格式,格式就是key=value&key2=value2,value 会做 urlencode 编码。这就是我们前面学的 urlencode 对应的 Content‑Type 类型。如果是传 JSON,这里是application/json。
User‑Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) Chrome/61.0.3163.100 Safari/537.36:简称 UA。告诉服务器客户端是什么程序:Windows 下 Chrome 浏览器版本信息。 服务器可以根据 UA 适配返回不同页面内容。C++ 写网络爬虫的时候,经常伪造 UA,否则服务器识别是脚本直接拒绝。
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/ajpg,*/*;q=0.8:告诉服务器:客户端能够接受哪些类型的响应数据,text/html:html 网页优先,q=0.9 q=0.8:权重系数,数值越小优先级越低,*/*:所有类型都可以接受。
Referer: http://job.xjtu.edu.cn/companyLogin.do:告诉服务器,我是从哪个页面跳转过来发起本次请求。 这里就是登录页面本身。服务器经常用 Referer 做防盗链判断。
Accept‑Encoding: gzip, deflate:告诉服务器:客户端支持接收压缩数据。服务器可以把返回的 html/json 用 gzip 压缩后返回,减少网络传输流量;客户端收到后解压。 服务器如果支持,响应头会多出 Content‑Encoding: gzip。
Accept‑Language: zh‑CN,zh;q=0.8:告诉服务器客户端偏好的语言:简体中文优先。网站可以根据这个返回对应语言页面。
3.3请求正文
![]()
格式依旧是key=value的形式,用&作为分割参数,由于用户名密码都没有特殊字符,所以就不需要编码,字节长度严格和上文的36对应相等!!!
HTTP RESPONSE

3.1状态行
HTTP/1.1 200 OK
HTTP/1.1:响应协议版本,和请求保持一致200:3 位状态码,成功,服务器正常处理完请求OK:人类可读描述,程序解析只看数字200
3.2响应报头
Server: YxlinkWAF:Server响应头告诉客户端服务器软件 / 设备名称。 这里YxlinkWAF是Web 应用防火墙,不是原始 Web 服务器;说明流量经过 WAF 防护设备转发。生产环境常常会伪装 / 删除 Server 头,避免泄露服务器版本信息,防止针对性攻击。
Content‑Type: text/html;charset=UTF‑8:告诉客户端,响应正文是什么类型,什么字符集
Content‑Language: zh‑CN:说明页面内容语言:简体中文。
Transfer‑Encoding: chunked:分块传输编码,当响应体总大小服务器预先不知道的时候(动态 网页、实时输出内容),就不会设置Content‑Length,改用chunked

Date: Fri, 29 Sep 2017 05:10:13 GMT:服务器的GMT 标准时间(格林威治时间,不是北京时间)。 HTTP 协议所有时间头一律使用 GMT 时区。北京时间 = GMT+8。
3.3响应正文
响应正文就是HTML源码了
注意:因为是chunked模式,TCP 流里 HTML 并不是一股脑全部发送,而是被切割成若干 chunk 块传输。抓包工具帮我们把 chunk 块重组,展示成完整 html 文本。
需求1:如何保证正文部分能一字不落的全部读到呢?
报头属性里面有 标注正文部分的长度的属性,所以他只要根据换行规则去读取整个报文,然后一直读到空行就说明报头读完了,然后再根据length字段 直接向后读相应长度的字节 就可以把正文部分也给读完了!
问题1:为什么信息需要有版本号呢?
版本号是便于我们双方标识对应版本号的功能,因为可能有的用户没有更新客户端,所以服务端是有可能会接受到各种不同版本的客户端请求的,而较新的版本可能会存在一些旧版本没有的功能!!(比如有的版本可以发消息,而有的版本还可以发朋友圈)
问题2:状态码有什么含义


四、用代码去理解HTTP1.0
这是一个基于 C++ + TCP 封装实现的极简静态 HTTP 1.0 Web 服务器
客户类:
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类
class InetAddr
{
public:
InetAddr() {}
InetAddr(struct sockaddr_in &addr)
{
SetAddr(addr);
}
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
_addr.sin_port = htons(_port);
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
}
InetAddr(uint16_t port) : _port(port), _ip()
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_addr.s_addr = INADDR_ANY;
_addr.sin_port = htons(_port);
}
void SetAddr(struct sockaddr_in &addr)
{
_addr = addr;
// 网络转主机
_port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
// _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
_ip = ipbuffer;
}
uint16_t Port() { return _port; }
std::string Ip() { return _ip; }
const struct sockaddr_in &NetAddr() { return _addr; }
const struct sockaddr *NetAddrPtr()
{
return CONV(_addr);
}
socklen_t NetAddrLen()
{
return sizeof(_addr);
}
bool operator==(const InetAddr &addr)
{
return addr._ip == _ip && addr._port == _port;
}
std::string StringAddr()
{
return _ip + ":" + std::to_string(_port);
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;Socket.hpp和Tcpserver.hpp
Socket:抽象基类(模板方法模式),定义服务端套接字完整流程骨架,把底层系统调用封装成纯虚接口。
TcpSocket : public Socket:TCP 具体实现子类,override 全部纯虚函数,内部调用 Linux socket 系统调用。
#pragma once
#include <unistd.h>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
static const int gbacklog = 16;
// 基类
namespace SocketModule
{
class Socket
{
public:
virtual ~Socket() {}
virtual void Sockfd() = 0; // 创建套接字
virtual void Bind(uint16_t port) = 0; // 绑定需要端口号
virtual void Listen(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void Close() = 0;
virtual int Recv(std::string *out)=0;
virtual int Send(const std::string &message)=0;
virtual int Connect(const std::string &server_ip, uint16_t port)=0;
public:
void BuildTcpSocket(uint16_t port)
{
Sockfd();
Bind(port);
Listen(gbacklog);
}
};
static const int defaultfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(defaultfd) {}
TcpSocket(int fd) : _sockfd(fd) {}
~TcpSocket() {}
void Sockfd() override
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCK_ERR);
}
LOG(LogLevel::INFO) << "socket success";
}
void Bind(uint16_t port) override
{
InetAddr peer(port);
int n = bind(_sockfd, peer.NetAddrPtr(), peer.NetAddrLen()); // 绑定对象
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
void Listen(int backlog) override
{
// 监听,继续用_sockfd即可
int n = listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success";
}
// unique_ptr:独占所有权,只能有一个指针持有资源,不能拷贝,只能移动。
// shared_ptr`:共享所有权,引用计数,可以拷贝、复制,多个智能指针同时管理同一个对象;计数到 0 自动释放。
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_sockfd, CONV(peer), &len); // 接收对象
if (fd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
return nullptr;
}
// 接收成功
LOG(LogLevel::DEBUG) << "accept success:"<<client->StringAddr();
client->SetAddr(peer); // 用peer把客户端的信息带出来
return std::make_shared<TcpSocket>(fd); // 返回一个TcpSocket的对象
}
void Close() override
{
if (_sockfd >= 0)
{
::close(_sockfd);
}
}
int Recv(std::string *out)override
{
//缓冲区
char buffer[4096];
int n=recv(_sockfd,buffer,sizeof(buffer)-1,0);
if(n>0)
{
buffer[n]=0;
*out+=buffer;
}
return n;
}
int Send(const std::string &message)override
{
return send(_sockfd,message.c_str(),message.size(),0);
}
//因为是客户端一定要绑定服务端的ip地址和port,告诉内核我要连哪一个机器上的哪个程序
int Connect(const std::string &server_ip, uint16_t port) override
{
InetAddr server(server_ip, port);
return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
}
private:
int _sockfd;
};
}有人就有疑问了,服务器与0.0.0.0有关系啊,为啥connect时要传特定的地址呢?
0.0.0.0 是服务端 bind() 的特殊监听地址,不是机器真实 IP

而connect是客户端绑定特定的ip地址,需要服务器本地的机器ip地址!!
服务器类
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>
using namespace SocketModule;
using namespace LogModule;
using ioservice=std::function<void(std::shared_ptr<Socket> sock,InetAddr&client)>;
class TcpServer
{
public:
TcpServer(uint16_t port,ioservice service):_port(port),_isrunning(false),_listenptr(std::make_unique<TcpSocket>()),_service(service)
{
_listenptr->BuildTcpSocket(_port);
}
void Start()
{
_isrunning=true;
while(_isrunning)
{
//开始接听
InetAddr client(_port);
auto sock=_listenptr->Accept(&client);//这种接收方式客户端信息我们也有了
if(sock==nullptr)
{
continue;
}
//创建子进程干活
pid_t id=fork();
if(id<0)
{
LOG(LogLevel::ERROR)<<"fork error";
exit(FORK_ERR);
}
else if(id==0)
{
//子进程干的活是服务,压根不需要监听,所以我们得把监听的_fd给关闭
_listenptr->Close();
if(fork()>0)
{
exit(OK);
}
//留下孙子进程执行任务
_service(sock,client);
exit(OK);
}
else
{
//先关闭
sock->Close();
//再等待
pid_t pid=waitpid(id,nullptr,0);
(void)pid;
}
}
_isrunning=false;
}
~TcpServer(){}
private:
uint16_t _port;
std::unique_ptr<TcpSocket> _listenptr;
bool _isrunning;
ioservice _service;
};在讲http和工具类之前,我们先解决几个问题:
问题1:我们返回类型是socket基类,但是可以返回Tcpsocket类型,还有传参时,我们的形参是socket类型,传的实参为什么可以是Tcpsocket类型呢?
这其实是c++的向上转型,只有指针,引用,智能指针才会完成向上转型转化为了socket基类,而值传递只能够完成切片,而当调用函数时,TcpSocket 对象内存的基类子对象部分就有 vptr,智能指针内部保存的是Tcpsocket类型对象的起始地址,然后找到堆上 TcpSocket 对象,对象内部 vptr 去找 TcpSocket 虚表,执行 TcpSocket 重写的 Recv。
延伸:模版方法:
你或许会很好奇,为什么BuildTcpSocketMethod在基类内部,我子类可以直接调用,而且里面的函数是经过重写之后的呢?
因为BuildTcpSocketMethod不是虚函数,他是一个普通成员函数,当继承时,它会被子类调用,所以能够直接使用,不会走虚表查找,对象在堆上创建完整 TcpSocket 对象;对象内存内 vptr 指向 TcpSocket 的虚表,虚表里面存放重写后的:Sockfd、Bind、Listen 的函数地址。进入BuildTcpSocketMethod内部,虽然说在基类的函数内部,但是调用的是虚函数,this 指针是 Socket* 类型,但指向真实的 TcpSocket 对象,通过对象内部的 vptr → TcpSocket 虚表,拿到子类重写的虚函数地址;
问题2:为什么在定义_listenptr时用的是unique_ptr的指针,但是套接字对象却是shared_ptr类型?
unique_ptr 独占所有权,不允许拷贝,只能移动,同一时刻只能有一个智能指针管理这块堆对象,禁止拷贝构造 / 拷贝赋值,监听 TcpSocket 对象,只有 TcpServer 的 _listenptr 拥有它,没有其他地方拷贝、复制这个_listenptr。没有拷贝,就不会违反 unique_ptr 独占规则。
shared_ptr 是共享所有权,引用计数管理,可以多个指针同时指向同一个对象,每来一个客户端就新建一个通信套接字,fork 之后父子进程都拷贝sock(shared_ptr,引用计数 + 1),父子都可以使用,计数归 0 释放。


问题3:为什么设置回调方法时,形参必须是套接字和客户呢?
回调是事件循环触发的,回调函数本身没有捕获外部变量,不知道是哪一个监听 socket来了新连接;传入它,回调内部可以继续执行 accept 接收后续连接,同时shared_ptr保证回调执行期间对象不会被销毁。
accept()系统调用返回的新文件描述符,代表刚接入的客户端;业务代码拿到这个 fd,才能封装成通信 socket,完成读写交互。
简单总结:
sock:标记哪个监听 socket 产生事件;client_fd:拿到新客户端通信句柄。
工具类
#pragma once
#include <iostream>
#include <fstream>
#include <string>
// 工具类
class Util
{
public:
static bool ReadFileContent(const std::string &filename , std::string *out)
{
// version1: 默认是以文本方式读取文件的. 图片是二进制的.
// std::ifstream in(filename);
// if (!in.is_open())
// {
// return false;
// }
// std::string line;
// while(std::getline(in, line))
// {
// *out += line;
// }
// in.close();
// version2 : 以二进制方式进行读取
int filesize = FileSize(filename);
if(filesize > 0)
{
std::ifstream in(filename);
if(!in.is_open())
return false;
out->resize(filesize);
in.read((char*)(out->c_str()), filesize);
in.close();
}
else
{
return false;
}
return true;
}
static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep/*\r\n*/)
{
auto pos = bigstr.find(sep);
if(pos == std::string::npos)
return false;
*out = bigstr.substr(0, pos);
bigstr.erase(0, pos + sep.size());
return true;
}
static int FileSize(const std::string &filename)
{
std::ifstream in(filename, std::ios::binary);
if(!in.is_open())
return -1;
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
in.close();
return filesize;
}
};这是个工具类,全部都是static类型的静态变量,说明这就是单纯的方法,没有什么成员变量,就避免了每次调用都要创建对象的麻烦,有效的节省了空间。
我们首先来看看读取文件内容这个工具

我们的第一个版本是以文本方式进行读取的,先打开文件,然后将文件的每一行读到我们的*out里面,但是以文本方式读取没法读取类似于图片的二进制文件,所以我们又采取了版本2,以二进制方式读取文件,我们首先先拿到文件的大小,如果成功拿到,我们就用ifstream来打开文件,然后先把out的大小定义为filesize(目的是直接给输出 string 预先分配filesize字节内存空间),然后再read(),一次性读取filesize个字节,读到缓冲区里,注意c_str()是const char*类型的,所以我们得把它强转成为(char*)类型的,然后关掉文件流,释放操作系统文件句柄。
我们再来看看截取每一行的操作

我们把大的字符串传进来,out是缓冲区,sep是分割符(\r\n),然后没有分割符,就返回false,如果找到了分隔符,就把从开头到pos的内容赋值给我们的*out,然后再删除掉我们截取的内容!!

这是filesize的大小,我们用了ifstream,这里先对ifstream做一个简单的介绍

很明显,我们这里是以二进制形式打开的文件,如果打开失败,就返回-1,打开成功后,我们用了seekg,是一个移动读指针,我们先把指针移动到文件末尾,然后tellg返回当前指针的字节位置,也就是文件字节大小,然后再把指针返回到开头位置,然后关闭文件流,返回大小!!!!!!!
HTTP
在此处,我们将http请求和http响应单独封装起来作为两个独立的类!!
HTTPREQUST
代码:
#pragma once
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include <unordered_map>
using namespace SocketModule;
using namespace LogModule;
const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";
const std::string webroot = "./wwwroot"; // 根目录的路径
const std::string homepage = "index.html"; // 首页
const std::string page_404 = "/404.html"; // 错误页面
class HttpRequest
{
public:
HttpRequest()
{
}
std::string Serialize()
{
return std::string();
}
// 分割请求行
bool Praseline(std::string &reqline)
{
std::stringstream ss(reqline);
ss >> _method >> _uri >> _version;
}
// 反序列化
bool DeSerialize(std::string &reqstr)
{
// 获取请求行
std::string reqline;
bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);
if (!res)
{
return false;
}
LOG(LogLevel::DEBUG) << reqline;
// 开始分割
Praseline(reqline);
if (_uri == "/")
{
_uri = webroot + _uri + homepage;
}
else
{
_uri = webroot + _uri;
}
LOG(LogLevel::DEBUG) << "_method: " << _method;
LOG(LogLevel::DEBUG) << "_uri: " << _uri;
LOG(LogLevel::DEBUG) << "_version: " << _version;
// ./wwwroot/XXX.YYY
return true;
}
std::string Uri() { return _uri; }
~HttpRequest()
{
}
private:
std::string _method; // 请求方法
std::string _uri; // 资源
std::string _version; // 版本
std::unordered_map<std::string, std::string> _headers;
std::string blackline;
std::string _text;
};关于uri的一些冷知识:

HTTPRESPONSE
class HttpResponse
{
public:
HttpResponse() : blackline(glinespace), _version("HTTP/1.0")
{
}
// 序列化
std::string Serialize()
{
// 拼接成字符串
std::string repline = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;
// 拼接响应报头
std::string repkv;
for (auto &header : _headers)
{
std::string line = header.first + glinesep + header.second + glinespace;
repkv += line;
}
return repline + repkv + blackline + _text;
}
// 设置要打开的本地资源文件路径,例如 `./wwwroot/index.html`。
void SetTargetFile(const std::string &target)
{
_targetfile = target;
}
// 设置退出码
void Setcode(int code)
{
_code = code;
switch (_code)
{
case 200:
_desc = "OK";
break;
case 404:
_desc = "Not Found";
break;
default:
break;
}
}
// 设置请求报头
void SetHeader(const std::string &key, const std::string &value)
{
auto iter = _headers.find(key);
if (iter != _headers.end())
return;
_headers.insert(std::make_pair(key, value));
}
std::string Uri2Suffix(const std::string &targetfile)
{
// 现在开始截取后缀
auto pos = targetfile.rfind(".");
if (pos == std::string::npos)
{
return "text/html";
}
std::string suffix = targetfile.substr(pos); // 从pos接到最后
if (suffix == ".html" || suffix == ".htm")
return "text/html";
else if (suffix == ".jpg")
return "image/jpeg";
else if (suffix == "jpg")
return "image/jpg";
else
return "";
}
bool MakeResponse()
{
if (_targetfile == "./wwwroot/favicon.ico")
{
LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";
return false;
}
int filesize = 0;
bool res = Util::ReadFileContent(_targetfile, &_text); // 浏览器请求的资源,一定会存在吗?出错呢?
if(!res)
{
//走404页面,首先清空文本
_text="";
LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";
Setcode(404);
_targetfile=webroot+page_404;//重新给_targetfile
filesize=Util::FileSize(_targetfile);
Util::ReadFileContent(_targetfile,&_text);
std::string suffix=Uri2Suffix(_targetfile);
//重读
SetHeader("Content-type",suffix);
SetHeader("Content-length",std::to_string(filesize));
}
else
{
//读取_text
LOG(LogLevel::DEBUG)<<"read success:"<<_targetfile;
Setcode(200);
filesize=Util::FileSize(_targetfile);
std::string suffix=Uri2Suffix(_targetfile);
SetHeader("Content-length",std::to_string(filesize));
SetHeader("Content-type",suffix);
}
return true;
}
bool Deserialize(std::string &reqstr)
{
return true;
}
~HttpResponse() {}
private:
std::string _version; // HTTP版本默认 "HTTP/1.0"
int _code;
std::string _desc;
std::unordered_map<std::string, std::string> _headers;
std::string blackline;
std::string _text; // 响应body,读取的html/图片文件内容
// 其他属性
std::string _targetfile; // 要读取的本地磁盘文件路径 ./wwwroot/xxx.html
};HTTP处理请求
class Http
{
public:
Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port))
{
}
void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
// 收到请求
std::string httpreqstr;
// 假设:概率大,读到了完整的请求
// bug!
int n = sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串, 其实我们的recv也是有问题的。tcp是面向字节流的.
if (n > 0)
{
std::cout << "##########################" << std::endl;
std::cout << httpreqstr;
std::cout << "##########################" << std::endl;
// 对报文完整性进行审核 -- 缺
// 所以,今天,我们就不在担心,用户访问一个服务器上不存在的资源了.
// 我们更加不担心,给用户返回任何网页资源(html, css, js, 图片,视频)..., 这种资源,静态资源!!
HttpRequest req;
req.DeSerialize(httpreqstr);
HttpResponse resp;
resp.SetTargetFile(req.Uri());
if (resp.MakeResponse())
{
std::string response_str = resp.Serialize();
sock->Send(response_str);
}
// HttpResponse resp;
// resp._version = "HTTP/1.1";
// resp._code = 200; // success
// resp._desc = "OK";
// //./wwwroot/a/b/c.html
// LOG(LogLevel::DEBUG) << "用户请求: " << filename;
// bool res = Util::ReadFileContent(filename, &(resp._text)); // 浏览器请求的资源,一定会存在吗?出错呢?
// (void)res;
}
// #ifndef DEBUG
// #define DEBUG
#ifdef DEBUG
// 收到请求
std::string httpreqstr;
// 假设:概率大,读到了完整的请求
sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串, 其实我们的recv也是有问题的。tcp是面向字节流的.
std::cout << httpreqstr;
// 直接构建http应答. 内存级别+固定
HttpResponse resp;
resp._version = "HTTP/1.1";
resp._code = 200; // success
resp._desc = "OK";
std::string filename = webroot + homepage; // "./wwwroot/index.html";
bool res = Util::ReadFileContent(filename, &(resp._text));
(void)res;
std::string response_str = resp.Serialize();
sock->Send(response_str);
#endif
// 对请求字符串,进行反序列化
}
void Start()
{
tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client)
{this->HandlerHttpRquest(sock, client); });
}
~Http()
{
}
private:
std::unique_ptr<TcpServer> tsvrp;
};结果:


总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
linux或windows环境下pytorch的安装与检查验证(解决runtimeerror问题)
这篇文章主要介绍了linux或windows环境下pytorch的安装与检查验证(解决runtimeerror问题),需要的朋友可以参考下2019-12-12


最新评论