从本章开始,我们要自顶向下的学习网络协议,有了前面的基础,我们再学习http协议就更容易理解了。本章的目标是学习http协议,熟悉http的协议格式,方法,状态码等概念,并且对http常见的报头进行学习。目标已经确定,准备开讲啦… 【网络层状结构复习】
平时我们俗称的 "网址" 其实就是说的 URL。
URL是(Uniform Resource Locator)的缩写,意为统一资源定位符:
我们上一章节学习的 序列化与反序列化 所做的工作是在那一层呢?—— 应用层!
我们再来复盘一下之前的网络编程的过程:
那么有没有已经非常成熟的场景,自定义的协议,这个协议写的不错,然后成了应用层特定协议的标准被别人直接使用呢?有的!!
比如说:http、https、smpt、ftp、DNS…
在解析完协议之后需要被反序列化,变成结构化数据。之后才形成完整的请求,最后才能交给业务被计算。
使用确定协议的时候,一般显示的时会缺省端口号:
所以,浏览器访问指定的URL的时候,浏览器必须给我们自动添加port。
浏览器如何得知,URL匹配的port是谁呢?特定的众所周知服务,端口号必须是确定的!!
例如:httpserver->80
、httpsServer->443
、ssh->22
。
服务和端口是必须绑定的,用户自己写的网络服务bind端口的时候,只能绑定1024之后的端口[1024 ~ n]
。
域名和DNS:
baidu.com
。浏览器加一个服务,DNS服务自动帮我们做的域名解析。域名必须被转化成为IP访问网络服务,服务端必须具有port。网络通信的本质:
socket
。(ip + port
)
URL编码问题:
urlencode
和urldecode
是用于对URL中的特殊字符进行编码和解码的函数。urlencode
函数将URL中的特殊字符转换为%后面跟着两位十六进制数的形式。urldecode
函数则进行相反的操作,它将编码过的URL字符串解码回原始的字符串形式。
发起http请求的时候,有些字段时需要编码的,注意:编码并不等于加密。一个%后面跟两个16进制数,代表的就是一个字符,汉字是三个字符代表一个汉字的,所以两个汉字是6个百分号。
信息在本地编码之后,交给服务器端,服务器再对编码进行解码。所以就需要encode和decode。编码是浏览器做的,而解码是服务端做的。
http是做什么的?
http超文本传输协议,为什么叫超文本的,因为网页是文本的,超在哪呢?超在可以传图片,视频音频。
查阅文档,看音视频都是以网页的形式呈现的,http获取网页资源的视频,音频等也都是文件。
举个栗子:
抓到了百度的网页,这个网页,就是通过百度对应的服务器,通过一些客户端,可以是浏览器或者是一些工具,把它的网页拿下来了,就是文件。
为什么URL可以定位唯一的资源:
ip + port
定位了互联网中唯一的服务。
这么多行,主体是很多http请求的报头属性,请求报头的报头属性非常多,所有的字段都是Key:空格value
。
http是一个基于行的一个协议:
任何协议的request or response
都是:报头 + 有效载荷。
当对http有了宏观结构上的认识之后,也有一些不规范的写法,http请求当中可能会少一些报头,可能会没有正文,但是都必须有请求行和空行。空行存在的最大意义就是将报头和有效载荷分离。
ip具有唯一性,端口在该服务器上也具有唯一性,那么路径就更具有唯一性了。诸多唯一性构建起来就构成了一个URL。
有了之前的套接字编程经验,我们直接写一个简易版的服务端(TCP套接字):
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port),
ip_(ip),
listenSock_(-1)
{
quit_ = false;
}
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind绑定
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5 /*后面再说*/) < 0)
{
exit(3);
}
// 运行别人来连接你了
}
void loop()
{
signal(SIGCHLD, SIG_IGN); // only Linux
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
// 获取链接失败
cerr << "accept error ...." << endl;
continue;
}
// 多进程版本
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
close(listenSock_); // 建议
if (fork() > 0)
exit(0);
// 孙子进程
handlerHttpRequest(serviceSock);
exit(0); // 进入僵尸
}
close(serviceSock);
wait(nullptr);
}
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 安全退出
bool quit_;
};
不清楚的小伙伴看过来 TCP套接字复习。
我们用的是多进程版本的服务器。
提前搞一些宏标识:
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
服务端响应:
void handlerHttpRequest(int sock)
{
char buffer[1024];
ssize_t s = read(sock, buffer, sizeof buffer);
if (s > 0)
cout << buffer;
std::string path = getPath(buffer);
// path = "/a/b/index.html";
// recource = "./wwwroot"; // 我们的web根目录
// recource += path; // 最终拼出来了--> ./wwwroot/a/b/index.html
// 1. 文件在哪里? 在请求的请求行中,第二个字段就是你要访问的文件
// 2. 如何读取
std::string recource = ROOT_PATH;
recource += path;
cout << "[recoure]: " << recource << std::endl;
std::string html = readFile(recource);
std::size_t pos = recource.rfind(".");
std::string suffix = recource.substr(pos);
cout << "[suffix]: " <<suffix << endl;
// 开始响应
std::string response;
// 两百这个状态码代表这次请求时OK的
response = "HTTP/1.0 200 OK\r\n";
if (suffix == ".jpg")
response += "Content-Type: image/jpeg\r\n"; // Content-type标定了正文的类型是什么
else
response += "Content-Type: text/html\r\n";
response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
response += "\r\n"; // 空行
response += html; // 正文
send(sock, response.c_str(), response.size(), 0);
}
首先我们读取收到的序列化数据,提取要获取的数据的路径,根据路径在服务器上找对应的文件,然后将文件提取出来。
随后开始响应,填写响应报头,根据文件的格式填好每个报头字段,将提取到的文件拼接到最后,然后发送给客户端。
Content-type
标定了正文的类型是什么,要根据不同的格式来填Content-type
。 常用对照表
获取路径函数:
std::string getPath(std::string http_request)
{
std::size_t pos = http_request.find(CRLF);
if (pos == std::string::npos)
return "";
// 请求行
std::string request_line = http_request.substr(0, pos);
// GET /a/b/c http/1.1
std::size_t first = request_line.find(SPACE);
if (first == std::string::npos)
return "";
std::size_t second = request_line.rfind(SPACE);
if (second == std::string::npos)
return "";
std::string path = request_line.substr(first + SPACE_LEN, second - (first + SPACE_LEN));
if (path.size() == 1 && path[0] == '/')
path += HOME_PAGE;
return path;
}
注意:
读取文件函数:
std::string readFile(const std::string &recource)
{
std::ifstream in(recource, std::ifstream::binary);
// 检测文件是否成功打开了
if (!in.is_open())
return "404";
std::string content;
std::string line;
while (std::getline(in, line))
content += line;
in.close();
return content;
}
资源文件:
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>测试title>
head>
<body>
<h3>hello my server!h3>
<p>我终于测试完了我的代码p>
<form action="/a/b/c.html" method="post">
Username: <input type="text" name="user"><br>
Password: <input type="password" name="passwd"><br>
<input type="submit" value="Submit">
form>
body>
html>
当然了,我们也不懂前端,随便写一点样例。
主函数:
#include "server.hpp"
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
std::cerr << "example:\n\t" << proc << " 8080\n"
<< std::endl;
}
// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
ServerTcp svr(port);
svr.init();
svr.loop();
return 0;
}
用浏览器获取响应:
telnet是个命令,可以远程的以协议的方式,去登录某种服务:
一般我们浏览器中请求的服务,是一定有人曾经写过这样的服务,部署在Linux上,然后才可以请求。
方法 | 描述 |
---|---|
GET | 请求指定的资源。返回响应主体 |
POST | 向指定资源提交数据进行处理请求。通常用于提交表单或上传文件 |
PUT | 从客户端向服务器传送的数据取代指定的文档的内容 |
DELETE | 请求服务器删除指定的资源 |
HEAD | 获取对应的HTTP报头信息,但不返回响应主体 |
OPTIONS | 返回服务器支持的HTTP方法列表 |
PATCH | 对资源进行部分修改 |
http的请求方法除了GET和POST还有其他方法,但是用的特别少,而且在大部分的服务器当中,基本上很多的方法被注释掉了,被禁掉了。
input标签,会在网页当中给我们构建一个输入框。
URL可以在?
后面带参数,参数是KV的,K对应的就是表单当中的name,V就是往表单当中输入的东西。
GET /index.html http/1.1
。GET or POST
。在HTTP中GET会以明文方式将我们对应的参数信息,拼接到URL中,以这样的方式完成了提参的过程。
HTTP GET方法通常不包含请求正文。HTTP GET方法用于从服务器请求资源,通常通过在URL中指定参数来传递请求参数,而不是在请求正文中传递参数。GET方法将请求参数附加到URL的末尾,形成查询字符串,并将其发送到服务器。
POST方法提交参数,会将参数以明文的方式,拼接到http的正文中来进行提交!
POST方法,请求正文,一般携带的是http请求所带的参数。这个参数类似于C语言C++的字符串。
在浏览器中提交表单的时候,表单的内容以KV的方式拼接到正文了。
小结:
http传输数据能有功能上的满足,但是数据却在网络当中裸奔。所以,这个数据无论如何都是不安全的。
如何选择:
Content-Type
字段来指定数据类型。比如网页端上传一些简历,上传一些视频,或者是上传百度云盘的资料,我们都用的是POST来传,因为POST有详细的字段类型说明。
http常见的Header:
Header | 描述 |
---|---|
Content-Type | 数据类型,指示服务器返回的正文部分的数据类型 |
Content-Length | 有效载荷的长度 |
Host | 客户端告知服务器所请求的资源在哪个主机的哪个端口 |
User-Agent | 声明用户的操作系统和浏览器版本信息 |
Referer | 当前页面是从哪个页面跳转过来的 |
Location | 搭配3xx状态码使用,告诉客户端接下来要去哪里访问 |
Cookie | 在客户端存储少量信息,通常用于实现会话功能 |
Linux网页的跳转本质是Linux目录的跳转,referer代表上一个目录是什么。
http这样的协议几乎是基于纯文本的,而且可展性还非常强,如果未来http想要新功能,新属性,直接在请求和响应报头里添加KV字符串行就可以。
状态码:
状态码范围 | 类别 | 描述 |
---|---|---|
1XX | 信息性状态码 | 服务器已接受请求,需要客户端继续操作 |
2XX | 成功状态码 | 请求已成功处理 |
3XX | 重定向状态码 | 需要完成进一步的操作以完成请求 |
4XX | 客户端错误状态码 | 请求包含语法错误或无法完成请求 |
5XX | 服务器错误状态码 | 服务器在处理请求时发生内部错误或超时等 |
目前被主流浏览器所接受的重定向是301和302。
301和302通常是做什么的呢?
临时重定向:
永久重定向:
void handlerHttpRequest(int sock)
{
char buffer[1024];
ssize_t s = read(sock, buffer, sizeof buffer);
if (s > 0)
cout << buffer;
std::string response = "HTTP/1.1 302 Temporarily Moved\r\n";
// std::string response = "HTTP/1.1 301 Permanently Moved\r\n";
// Location后面填的一定是个网址
response += "Location: https://www.qq.com/\r\n";
response += "\r\n";
send(sock, response.c_str(), response.size(), 0);
}
在响应时状态码填的是301或者302,代表的意思是重定向,给客户端响应,还会给一个新的网址。
因为我们是服务端将从客户端收到的报文打印出来,所以当客户端请求时,会将请求报文打印出来,然后再将带有location的报文发给客户端。
但是我们是打印不出来带有location字段的报文的,因为客户端重定向到了别的服务器了。
浏览器演示图:
telnet演示图:
通过Telnet可以获取远端服务器的响应。当你使用Telnet连接到远程服务器后,你可以输入命令并发送给服务器,然后等待服务器的响应。服务器会返回相应的结果,包括命令执行的输出、状态信息等。
小结:
http协议特点之一:无状态
是什么意思呢?
—— 用户的各种资源请求行为,http本身并不做任何记录!!
HTTP的无状态(Stateless)指的是:
但是用户需要会话保持!!
HTTP的Cookie(也称为HTTP Cookie、Web Cookie或浏览器Cookie)
是一种用于网站与浏览器之间进行状态管理的技术。它通过在用户浏览器中存储少量数据,并在以后的请求中发送给同一网站,实现对用户状态的追踪和识别。
Set-Cookie
字段将一个Cookie
发送给用户。Cookie
存储起来,并在用户以后对同一网站发送请求时,通过HTTP请求头中的Cookie
字段将这个Cookie
发送回服务器。编写响应报文:
第二次以后的请求会携带一个字段叫做cookie
,就代表我们曾经写的内容:
cookie
就是浏览器帮我们维护的一个文件:
在浏览器中查看Cookie:
当第一次客户端请求服务端的时候,将用户账号和密码发给服务端,服务端完成匹配之后,将携带用户账号和密码的Cookie文件返回给客户端,但是这是极其不安全的!
一旦有中间人在服务端返回给客户端的时候,将返回内容截取,那么用户的账号和密码就会泄露。
现在主流的是:cookie + session
服务端在收到客户端请求时,在内部验证信息,一旦验证通过,不再给客户端返回携带用户账号和密码的Cookie文件,而是在服务端本地形成一个session文件!
session里包含了文件的私密信息,包括了这个用户是谁,浏览痕迹是什么,最近一次访问时间是什么时候session有没有过期等等。只要拿着session_id找到session文件,就证明用户处于登录状态,然后服务器就允许该用户去访问指定资源。
依旧存在安全问题:
用户所看到的完整的网页内容,背后可能是无数次http清求!http底层主流采用的就是tcp协议!
长连接(Connection: keep-alive):
http1.1
,响应时也是http1.1
,同时还携带了一个Connection
字段,叫做keep-alive
。http1.1
,并且字段都携带keep-alive
,意味着我们在进行底层连接协商的时候,双方都同意采用长连接方案。http1.0当中是基于短连接的,短连接一次只处理一个http请求!Connection: closed
对于长连接的理解:
一张网页有若干个元素构成,我们在底层发起请求,这个连接暂时不断开,发起若干次http请求,同时把若干次http请求全部都发送到服务器,服务器从一个连接里读到的,不仅仅是一个请求,它可以读到很多请求,然后按照顺序,再把若干个响应全部都返回给客户端,当客户端拿到了完整的网页时,这个连接才断开。
也就是说一次获取若干个元素时,这些若干个元素用一条连接全部请求和响应,不用重复的建立TCP底层的三次握手,四次挥手。这样就能大大的提高效率。
其中双方支持长连接协商的时候,Connection
如果是keep-alive
,客户端发的请求里带了,服务端的响应里带了,就说明双发协商成功,我们双方都认可采用keep-alive
,也叫做连接保活的策略。
因为http1.1有了keep-alive
,为了区分http1.0,所以原来的Connection如果是close,就叫做只支持短连接。
一个连接有多个请求了,如何保证服务端读取请求时,都是一个完整的请求呢?
Content-length
来读取。http是基于tcp的,tcp协议是面向连接的,但是http是无连接的,怎么解释?(重点)
所以,http最核心的特点是超文本传输协议,是一个无连接,无状态的一个应用层协议,这就是http的定义。