虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用。 HTTP(超文本传输协议)就是其中之一。http应用十分的广泛,几乎每一名程序员(无论前后端 无论C++/Java/Go…)都会接触到!
在互联网世界中, HTTP(HyperText Transfer Protocol, 超文本传输协议) 是一个至关重要的协议。 它定义了客户端(如浏览器) 与服务器之间如何通信, 以交换或传输超文本,超文本支持视频,网页 ,图片等等!
HTTP 协议是客户端与服务器之间通信的基础。 客户端通过 HTTP 协议向服务器发送请求, 服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、 无状态的协议, 即每次请求都需要建立新的连接, 且服务器不会保存客户端的状态信息。
但是有个疑问?http是基于TCP协议的,也就是面向连接的,为什么http确是无连接的协议呢? 因为http会使用Tcp建立的链接,无需再次建立新的链接。就好比之前我们实现的网络计算器,服务端和客户端的连接是通过TCP建立的,但是通信传输Request和Response直接通过Tcp建立的连接即可,无需再次建立连接!
平时我们浏览的网站:百度 , 哔哩哔哩 ,力扣…等等网站都有一个域名
百度 :https://www.baidu.com/
哔哩哔哩:https://www.bilibili.com/
力扣:https://leetcode.cn/
...
这些网址都是https协议,这些网址其实就是URL!
访问时会将网址解析成IP地址!一般成熟的协议名称与端口号是强关联的,称之为知名端口号!
为什么平时访问网站并没有输入端口号? 只有同时具备IP地址和端口号才可以访问到对应的服务器,浏览器发起请求时会自动拼接端口号80!就类似日常生活中报警会自觉想到拨打110 , 火灾会自然的想到拨打119!
通信中离不开“资源”两个字,通信要么是从别处获取资源,要么是向对方发送资源。http协议下的资源是超文本! 网页,图片,音频,视频都是超文本!在进行通信之前,用户想要获取的资源都在后端的云服务器中,云服务器一般都是Linux系统,那么在Linux视角下不就都是文件吗!
为了将这个文件(资源)发给客户端,就必须要找到这个文件,那么怎么找到这个文件呢?当然是通过文件的唯一标识符 — 路径来实现!在URL中后半部分不就是我们的路径吗!这样通过IP地址确定的唯一主机+唯一的路径就可以标识互联网中的唯一的文件资源!
注意第一个斜杠不是Linux服务器的根目录,而是web根目录,web根目录可以是Linux中的任何目录!
所以URL就叫:统一资源定位符
urlencode 和 urldecode
下图是http请求的一个信息:
接下来我们来通过代码实验,来测试一下是否可以获取到这些信息! 首先我们简化一下代码,在传输层直接进行IO,直接在Socket文件中获取数据流,将线程的函数方法修改为以下形式:
// 注意设置为静态函数 , 不然参数默认会有TcpServer* this!!!
static void *Execute(void *args)
{
pthread_detach(pthread_self()); // 线程分离!!!
// 执行Service函数
TcpServer::ThreadData *td = static_cast<TcpServer::ThreadData *>(args);
// 直接进行IO
std::string reqstr;
// 这里默认读取到的是完整的请求
ssize_t n = td->_sockfd->Recv(&reqstr);
if (n > 0)
{
std::string resstr = td->_this->_service(reqstr);
td->_sockfd->Send(resstr);
}
td->_sockfd->Close();
delete td;
return nullptr;
}
回调函数单独设计一个HttpServer
类来获取客户端申请的数据,目前直接进行简单的打印处理就好!
#include <iostream>
#include <string>
class HttpServer
{
public:
HttpServer()
{
}
std::string HandlerHelperRequest(std::string& Requeststr)
{
std::cout<<"------------------"<<std::endl;
std::cout << Requeststr << std::endl;
return std::string();//暂时这样
}
~HttpServer()
{
}
};
然后ServerMain中,将HandlerHelperRequest
作为回调函数构造TcpServer!进行启动即可!那么接下来我们是不是就可以在外界通过IP地址和端口号就可以访问Linux服务器上启动的进程了呢???还不可以,我们需要对Linux云服务器做一些处理,才能让外界成功的访问!
让外界可以访问Linux云服务器需要两步操作:云服务器的安全组设置和服务器操作系统层面的防火墙设置。
云服务器的安全组设置操作步骤如下,这里以阿里云服务器为例:
云服务器设置好时候,接下来就进行服务器操作系统层面的防火墙设置:
对于CentOS:
# 查看防火墙状态
sudo systemctl status firewalld
# 如果需要,启动防火墙服务
sudo systemctl start firewalld
# 检查端口8888是否开放
sudo firewall-cmd --zone=public --query-port=8888/tcp
# 如果端口未开放,添加端口规则
sudo firewall-cmd --zone=public --add-port=8888/tcp --permanent
# 重新加载防火墙规则
sudo firewall-cmd --reload
对于Ubuntu:
# 查看防火墙状态
sudo ufw status
# 如果需要,启用ufw
sudo ufw enable
# 检查端口8888是否开放
sudo ufw allow 8888/tcp
# 如果端口未开放,添加规则
sudo ufw allow 8888/tcp
# 重新加载防火墙规则
sudo ufw reload
这样外界就可以通过IP地址和端口访问到对应的进程了!!!
测试之前我们先获取一个当前机器的IP地址:
使用 curl 命令:
curl ifconfig.me
使用 wget 命令:
wget -qO- ifconfig.me
都可以获取到机器的外网IP! 我们启动程序,等待外部的链接: 可以通过手机或者电脑的浏览器通过IP地址和端口号来进行访问:
进行访问之后,会获取到对应的信息:
可以看到电脑WIndows系统和手机IPhone都成功的访问了我们的服务器!!!非常cool!!!
请求和应答是http协议中双方都认识的结构化数据:
一个基本的http请求的格式是这个样子的,按行为单位!
http的应答与请求的格式很类似:
知道了请求和报文的结构,其本质上还是报文,那么如何将其报头与有效载荷进行分离呢?
我们看到的请求和应答的结构可以看到,报头和报文是通过换行符进行分割的!巧了我们之前不也是这样进行操作的吗!而且只要有正文,就会有对应的content-length:xxx
来帮我我们判断正文的是否完整!
接下来我们简单设计一下HttpRequest
http请求的结构化数据!
首先根据其整体的结构我们可以加入四个成员变量:请求行 ,请求报头, 空行 ,请求正文
// 设计http协议
class HttpRequest
{
public:
HttpRequest()
{
}
void Serialization(std::string &reqstr)
{
}
void Deserialization(std::string &reqstr)
{
}
~HttpRequest()
{
}
private:
std::string _req_line; // 请求行
std::vector<std::string> _req_headers; // 请求报头
std::string _blank_line; // 分割行
std::string _req_body_text; // 正文
};
这是最基本的四块数据,我们先对这四部分进行反序列化。因为他们都是根据分隔符\r\n
进行分割的字符串,所以十分好处理:
std::string GetLine(std::string &reqstr)
{
// 寻找分隔符
auto pos = reqstr.find(base_sep);
if (pos == std::string::npos)
return std::string();
std::string line = reqstr.substr(0, pos);
if (line.empty())
return base_sep;
// 在原字符串中删除
reqstr.erase(0, base_sep.size() + line.size());
return line;
}
void Deserialization(std::string &reqstr)
{
// 进行反序列化
_req_line = GetLine(reqstr);//请求行
do
{
std::string header = GetLine(reqstr);
if (header == "")
break;
else if (header == base_sep)
break;
else
_req_headers.push_back(header);
} while (true);//请求报头
_blank_line = GetLine(reqstr);//空行
_req_body_text = GetLine(reqstr);//请求正文
}
这样就可以将一个字符串切分为四个部分了:
接下来我们可以对数据进行进一步处理:我们加入更加具体的成员变量
std::string _method; //请求方法
std::string _url; //请求路径
std::string _version;//版本
std::unordered_map<std::string , std::string> _kv_headers;//报头
处理很简单:按照字符串结构编写代码即可!
void ParseReqLine()
{
// 优雅的操作!!!
std::stringstream ss(_req_line);
ss >> _method >> _url >> _version;
}
void ParseReqHeader()
{
for (auto &header : _req_headers)
{
auto pos = header.find(line_sep);
if (pos == std::string::npos)
continue;
std::string k = header.substr(0, pos);
std::string v = header.substr(pos + line_sep.size());
if (k.empty() || v.empty())
continue;
_kv_headers.insert(std::make_pair(k, v));
}
}
void Deserialization(std::string &reqstr)
{
// 进行反序列化
//...
//----------------具体数据的处理-------------------
ParseReqLine(); // 请求行的处理!!!
ParseReqHeader(); // 处理报头
}
我们可以将结果打印出来看看:
非常好,我们成功将reqstr进行了反序列化,之后我们再来实现业务逻辑的代码!!!
后续文章,敬请期待!