文章目录
- 一、应用层
- 1.协议
- 2.网络版计算器
- 二、HTTP 协议
- 1. URL
- 2. HTTP 协议格式
- 3.查看 HTTP 请求
- 4.发送 HTTP 响应
- 5. HTTP 的方法
- 6. HTTP 的状态码
- 7. HTTP 的版本
- 8. HTTP 常见 Header
- 9. Cookie 与 session
- 三、HTTP 与 HTTPS
一、应用层
我们程序员写的一个个解决实际问题,满足日常需求的网络程序,都是在应用层。
1.协议
协议是一种约定。网络协议是计算机网络中通信双方都必须遵守的一组约定。
在网络通信中,都是以 “字符串” 的方式来发送和接收数据的。
如果要发送和接收一些结构化的数据,就需要序列化和反序列化。
-
什么是序列化和反序列化?
① 序列化:将结构化的数据按照一个规则转化成字符串。
② 反序列化:将字符串按照相同的规则转化回结构化的数据。 -
为什么要进行序列化和反序列化?
① 为了方便应用层的网络通信。
② 为了方便上层使用内部成员,将应用和网络进行解耦。
结构化的数据,是不便于网络传输的,而字符串便于网络传输。
结构化的数据,以二进制的方式进行收发会有问题,因为客户端和服务器对结构体大小的认识可能不一样,比如每个字段的大小,内存对齐的方式。
结构化的数据,本质就是协议的表现。
2.网络版计算器
程序说明:client 发送一个计算请求给 server ,server 接收到后进行计算,再将响应结果返回给 client 。
我们约定:
① 定义结构体来表示我们需要交互的信息,在这里就是请求和响应。
② 对收发的数据进行序列化和反序列化。
我们会用到 json 组件对结构化的数据进行序列化和反序列化。
Linux 平台下,下载 json 组件的命令:
sudo yum install -y jsoncpp-devel
。
下面的程序包含四个文件:
① Sock.hpp:基本通信函数的实现。
② Protocol.hpp:协议的定制、序列化和反序列化的实现。
③ CalServer.cc:服务端。
④ CalClient.cc:客户端。
- Sock.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
cerr << "socket error!" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock, uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
cerr << "bind error!" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if(listen(sock, 5) < 0)
{
cerr << "listen error!" << endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr*)&peer, &len);
if(fd >= 0){
return fd;
}
return -1;
}
static void Connect(int sock, std::string ip, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
- Protocol.hpp:
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;
// 定制协议的过程,目前就是定制结构化数据的过程
// 我们自己定义的协议,client && server 都必须遵守!这就叫做自定义协议
// 请求格式
typedef struct request
{
int x;
int y;
char op; // "+-*/%"
} request_t;
// 响应格式
typedef struct response
{
int code;
// 只有计算结果,
// 不能区分是正常的计算结果,还是异常的退出结果
// server运算完毕的计算状态:
// code(0:success), code(-1: div 0) ...
int result;
} response_t;
// request_t -> string
std::string SerializeRequest(const request_t &req)
{
// 序列化
Json::Value root; // 可以盛装任何对象,json是一种kv式的序列化方案
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
// FastWriter, StyledWriter
// Json::StyledWriter writer;
Json::FastWriter writer;
std::string json_string = writer.write(root);
return json_string;
}
// string -> request_t
void DeserializeRequest(const std::string &json_string, request_t &out)
{
// 反序列化
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.x = root["datax"].asInt();
out.y = root["datay"].asInt();
out.op = (char)root["operator"].asInt();
}
std::string SerializeResponse(const response_t &resp)
{
Json::Value root;
root["code"] = resp.code;
root["result"] = resp.result;
Json::FastWriter writer;
std::string res = writer.write(root);
return res;
}
void DeserializeResponse(const std::string &json_string, response_t &out)
{
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.code = root["code"].asInt();
out.result = root["result"].asInt();
}
- CalServer.cc:
#include <pthread.h>
#include "Protocol.hpp"
#include "Sock.hpp"
static void Usage(string proc)
{
cout << "Usage: " << proc << " port" << endl;
}
void *HandlerRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
// 业务逻辑,做一个短服务
// request -> 分析处理 -> 构建response -> send(response) -> close(sock)
// 1. 读取请求
char buffer[1024];
request_t req;
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
cout << "get a new request: " << buffer << endl;
std::string str = buffer;
DeserializeRequest(str, req); // 反序列化请求
// 2. 分析请求 && 3. 计算结果
response_t resp = {0, 0};
switch (req.op)
{
case '+':
resp.result = req.x + req.y;
break;
case '-':
resp.result = req.x - req.y;
break;
case '*':
resp.result = req.x * req.y;
break;
case '/':
if (req.y == 0)
resp.code = -1; // 代表除0
else
resp.result = req.x / req.y;
break;
case '%':
if (req.y == 0)
resp.code = -2; // 代表模0
else
resp.result = req.x % req.y;
break;
default:
resp.code = -3; // 代表请求方法异常
break;
}
cout << "request: " << req.x << req.op << req.y << endl;
// 4. 构建响应,并进行返回
std::string send_string = SerializeResponse(resp); // 序列化之后的字符串
write(sock, send_string.c_str(), send_string.size());
cout << "服务结束: " << send_string << endl;
}
// 5. 关闭连接
close(sock);
}
// ./CalServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for (;;)
{
int sock = Sock::Accept(listen_sock);
if (sock >= 0)
{
cout << "get a new client..." << endl;
int *pram = new int(sock);
pthread_t tid;
pthread_create(&tid, nullptr, HandlerRequest, pram);
}
}
return 0;
}
- CalClient.cc:
#include "Protocol.hpp"
#include "Sock.hpp"
void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}
// ./CalClient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int sock = Sock::Socket();
Sock::Connect(sock, argv[1], atoi(argv[2]));
// 业务逻辑
request_t req;
memset(&req, 0, sizeof(req));
cout << "Please Enter Data One# ";
cin >> req.x;
cout << "Please Enter Data Two# ";
cin >> req.y;
cout << "Please Enter Operator# ";
cin >> req.op;
std::string json_string = SerializeRequest(req); // 序列化请求
ssize_t s = write(sock, json_string.c_str(), json_string.size());
char buffer[1024];
s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
response_t resp;
buffer[s] = 0;
std::string str = buffer;
DeserializeResponse(str, resp); // 反序列化
cout << "code[0:success]: " << resp.code << endl;
cout << "result: " << resp.result << endl;
}
return 0;
}
运行测试:
我们上面写的 C/S 模式的在线版本计算器,本质就是一个应用层网络服务:基本通信代码、结构化数据等约定、业务逻辑、序列化和反序列化,这些都是由我们自己实现的。
二、HTTP 协议
虽然说应用层协议是由我们程序员自己定的,但实际上已经有大佬们定义了一些现成的又非常好用的应用层协议,供我们直接参考使用,HTTP 协议(Hyper Text Transfer Protocol,超文本传输协议)就是其中之一。
网络通信、序列化和反序列化、协议细节,这些在 HTTP 协议的内部都实现了。
1. URL
我们请求的图片、视频、音频、文档等,都称之为资源。
服务器的后台是 Linux 。
公网 IP 唯一确认一台主机,而网络资源一定是存在网络中的一台 Linux 机器上!
Linux 保存资源的方式,都是以文件的方式保存的。单 Linux 系统,标识一个唯一资源的方式,是通过路径!
所以,IP + Linux 路径,就可以唯一地确认一个网络资源!
IP:通常以域名的方式呈现,路径可以通过目录名 + / 确认。
例子:
我们所说的 “网址” ,其实就是 URL(Uniform Resource Locator,统一资源定位符)。
① 协议方案名:想要通过什么协议来获得资源。
② 登录信息:现在主流的 URL 已经很少用它,一般是通过表单的方式去提交。
③ 服务器地址:IP 通常以域名的方式呈现,实际上是建立了一个域名和 IP 地址的映射关系。
④ 服务器端口号:要访问服务器的端口,一般是省略的。因为协议方案名和端口号的关系是十分紧密的,在 URL 中指明了协议方案,端口号就不用呈现了。
⑤ 带层次的文件路径:基于 Linux 的文件系统目录结构。
一个基本的 URL 的构成方式是:协议 + 域名 + 资源路径,可能带参。
URL 的作用是,确认全网中唯一的一个资源。
2. HTTP 协议格式
HTTP 协议的序列化和反序列化格式,是 HTTP 协议自身定制的。
HTTP 请求和响应,基本上都是以行(\n)为单位进行构建的。
无论是请求还是响应,基本上都是由 3 或 4 部分组成。
说明:
① 请求行/状态行只有一行内容。
② 请求报头/响应报头有多行内容,是请求/响应属性,这些属性都以冒号分割的键值对的形式按行陈列(即每组属性之间使用 \n 分隔)。
③ 空行就是一个 \n 。
④ 请求正文/响应正文允许为空字符串。若请求正文/响应正文存在,则在请求报头/响应报头中会有一个 Content-Length 属性来标识请求正文/响应正文的长度。
HTTP request 和 HTTP response ,其实就是一个长字符串!它们的读取和发送实际上就是字符串的读取和发送。上面的形式是打印出来之后呈现的结果。
- HTTP 的解包:读字符串时只要读到空行,就是把报头读完了,剩下的部分就是有效载荷。HTTP 的报头和有效载荷,是通过一个特殊字符(空行)来区分的。
- HTTP 的封装:构建 HTTP 请求时,在请求行和请求报头后加上空行,然后再加上请求正文即可。构建 HTTP 响应同理。
- HTTP 的分用:不是 HTTP 解决的,是具体的应用代码解决的。HTTP 需要有接口来帮助上层获取参数。
HTTP 协议的底层是 TCP 协议。
3.查看 HTTP 请求
程序说明:服务端只读取并打印 HTTP 请求,不构建和发送 HTTP 响应。
下面的程序包含两个文件:
① Sock.hpp:基本通信函数的实现。
② Http.cc:服务端。
- Sock.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
cerr << "socket error!" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock, uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
cerr << "bind error!" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if(listen(sock, 5) < 0)
{
cerr << "listen error!" << endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr*)&peer, &len);
if(fd >= 0){
return fd;
}
return -1;
}
static void Connect(int sock, std::string ip, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
- Http.cc:
#include "Sock.hpp"
#include <pthread.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void *HandlerHttpRequest(void *args)
{
int sock = *(int*)args;
delete (int*)args;
pthread_detach(pthread_self());
#define SIZE 1024*10
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
// 这种读法是不正确的,只不过在现在问题没有暴露出来
ssize_t s = recv(sock, buffer, sizeof(buffer), 0); // 跟read函数等价
if(s > 0)
{
buffer[s] = 0;
std::cout << "--------------------http request begin--------------------" << std::endl;
std::cout << buffer; // 查看http的请求格式!for test
std::cout << "---------------------http request end---------------------" << std::endl;
}
close(sock);
return nullptr;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for( ; ; )
{
int sock = Sock::Accept(listen_sock);
if(sock > 0)
{
pthread_t tid;
int *parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
}
运行测试:
使用浏览器连接服务器(浏览器会给服务器发送 HTTP 请求):
服务器接收到 HTTP 请求后,将其打印:
由于我们没有提交数据,所以这里的 HTTP 请求没有请求正文,只有三部分。
4.发送 HTTP 响应
HTTP 请求的 / 并不是根目录,而是叫做 web 根目录。web 根目录下放置的内容,都叫做资源!
我们一般要请求的一定是一个具体的资源:
① 若请求是 / ,意味着我们要请求的是该网站的首页,即 index.html 或 index.htm(一般所有的网站,都要有默认首页)。其实是服务器内部做判断,将访问资源的路径由 / 改成 /index.html 或 /index.htm 。
② 若请求是完整路径,意味着我们要请求的是一个特定的资源。
程序说明:服务端先读取并打印 HTTP 请求,然后发送一个 HTTP 响应。这里我们写死了,无论给服务器发送什么HTTP请求,服务器都只会把首页信息返回回去。
下面的程序包含三个文件:
① Sock.hpp:基本通信函数的实现,跟上面的一样,这里就不贴了。
② Http.cc:服务端。
③ index.html:一个简单的网站首页。
- Http.cc:
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>
// 其中我们这里的wwwroot目录,就叫做web根目录
// 理论上,web根目录可以放在Linux系统的任何目录下,只要能找到就行
// web根目录下的index.html就叫做网站的首页
#define WWWROOT "./wwwroot/"
#define HOME_PAGE "index.html"
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void *HandlerHttpRequest(void *args)
{
int sock = *(int*)args;
delete (int*)args;
pthread_detach(pthread_self());
#define SIZE 1024*10
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
// 这种读法是不正确的,只不过在现在问题没有暴露出来
ssize_t s = recv(sock, buffer, sizeof(buffer), 0); // 跟read函数等价
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer; // 查看http的请求格式!for test
// std::string http_response = "HTTP/1.0 200 OK\n";
// http_response += "Content-Type: text/plain\n"; // text/plain,正文是普通的文本
// http_response += "\n"; // 空行
// http_response += "hello world!";
// send(sock, http_response.c_str(), http_response.size(), 0); // 跟write函数等价
// 这里我们写死了,无论给服务器发送什么HTTP请求,服务器都只会把首页信息返回回去
std::string html_file = WWWROOT;
html_file += HOME_PAGE; // html_file表示文件路径
// stat系统调用:获取指定路径下文件的属性(第二个参数是输出型参数)
struct stat st;
stat(html_file.c_str(), &st);
// 返回的时候,不仅仅是返回正文网页信息,还要包括http响应
std::string http_response = "HTTP/1.0 200 OK\n";
http_response += "Content-Type: text/html; charset=utf8\n"; // 正文部分的数据类型
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size); // 内容长度,就是文件的大小
http_response += "\n";
http_response += "\n"; // 空行
// 接下来才是正文
std::ifstream in(html_file); // 使用文件流对象打开文件
if(!in.is_open())
{
std::cerr << "open html error!" << std::endl;
}
else{
std::string content;
std::string line;
while(std::getline(in, line)){ // 用getline一行一行读取文件内容
content += line;
}
http_response += content;
in.close();
send(sock, http_response.c_str(), http_response.size(), 0); // 跟write函数等价
}
}
close(sock);
return nullptr;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for( ; ; )
{
int sock = Sock::Accept(listen_sock);
if(sock > 0)
{
pthread_t tid;
int *parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
}
- index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h3>hello world!</h3>
</body>
</html>
运行测试:
浏览器给服务器发送 HTTP 请求,并接收到服务器的 HTTP 响应:
服务器接收到 HTTP 请求后,将其打印:
HTTP 协议,如果我们自己写的话,本质是我们要根据协议内容来进行文本分析。
- 读取 HTTP request/response 时,我们要做到的事情是:
① 保证每次读取都是读取完整的一个 HTTP request/response 。
② 保证每次读取都不要将下一个 HTTP request/response 的一部分读到。
以 HTTP request 为例:
读到空行,即可判定已经将报头部分读完。决定请求后面有没有正文,和请求方法有关。
Q:如果有正文,如何保证把正文全部读取完成,而且不要把下一个 HTTP 请求的部分数据读到呢?
A:读到空行,表明已把报头读完,就能提取报头中的一个属性Content-Length: len
(如果有正文,就会有这个属性),该属性表明正文部分有多少个字节,然后再根据 len 决定读取多少个字节的正文。
正文就是有效载荷,换言之,Content-Length 表明了有效载荷的长度。
空行能够做到将报头和有效载荷进行分离(解包)。
同时,若存在 Content-Length ,它会帮助我们读取到完整的 HTTP 请求/响应;若不存在 Content-Length ,就是没有正文的时候,此时的 HTTP 请求/响应就只有三部分,只要读到空行,就读到了完整的 HTTP 请求/响应。
5. HTTP 的方法
其中最常用的是 GET 方法和 POST 方法。
实际上,管理服务器的人会把 GET 方法和 POST 方法暴露出来,一般不会把其它的方法暴露出来(主要是为了防止恶意操作,虽然协议支持)。
GET 方法和 POST 方法:
这里只修改了 index.html 的文件内容,其它文件内容不变。
- GET 方法:如果提交参数,是通过 URL 的方式进行提交的。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>hello 我是首页!</h5>
<h5>hello 我是表单!</h5>
<!-- /a/b/handler_form并不存在,也不处理 -->
<form action="/a/b/handler_from" method="GET">
姓名:<input type="text" name="name"><br/>
密码:<input type="password" name="passwd"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
运行测试:
- POST 方法:如果提交参数,是通过正文的方式进行提交的。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>hello 我是首页!</h5>
<h5>hello 我是表单!</h5>
<!-- /a/b/handler_form并不存在,也不处理 -->
<form action="/a/b/handler_from" method="POST">
姓名:<input type="text" name="name"><br/>
密码:<input type="password" name="passwd"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
运行测试:
这里需要刷新一下浏览器,才能在请求正文中看到 POST 方法提交的参数:
从上面的实验我们可以看到,GET 方法和 POST 方法都可以向服务器提交参数,它们的区别是提交参数时参数所放的位置不同。
总结:
- GET 方法:叫做获取,是最常用的方法。默认一般获取所有的网页,都是 GET 方法,如果要提交参数,它是通过 URL 来进行参数拼接,从而提交给服务端。
- POST 方法:叫做推送,是提交参数比较常用的方法。如果提交参数,一般是通过正文部分提交的(
Content-Length: xxx
表示参数的长度)。 - 如果想获得某种资源,大部分情况下用的是 GET 方法。当提交参数时,既可以使用 GET 方法也可以使用 POST 方法。
- 它们的区别:
① 提交参数时参数所放的位置不同,POST 方法比较私密(私密 != 安全),因为不会回显到浏览器的 URL 输入框。而 GET 方法不私密,因为会将重要信息回显到浏览器的 URL 输入框。
② GET 方法是通过 URL 传参的,而 URL 是有大小限制的,和具体的浏览器有关。POST 方法是通过正文部分传参的,一般没有大小限制。 - 如何选择:
① GET 方法:如果提交的参数不敏感,数量非常少,可以采用 GET 方法。
② POST 方法:否则,就使用 POST 方法。
HTTP 协议处理,本质是文本分析。
所谓的文本分析:
① HTTP 协议本身的字段。
② 提取参数,如果有的话。
GET 或者 POST ,其实是前后端交互的一个重要方式。
6. HTTP 的状态码
最常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway) 。
应用层是人要参与的,人的水平参差不齐,HTTP 的状态码,很多人根本就不清楚如何使用,又因为浏览器的种类太多了,导致大家可能对状态码的支持并不是特别好。
类似于 404 的状态码,对浏览器没有任何指导意义,浏览器就是正常地显示网页,不会根据状态码做处理。
3XX 的状态码是有特殊含义的,重定向:
① 永久重定向:301 ,常用于网站搬迁、域名更换。
② 临时重定向:302 or 307 ,比如注册或登录成功后自动跳转到首页。
重定向的例子:
① 当访问某一个网站的时候,会让我们跳转到另一个网址。
② 当我们访问某种资源的时候,提示我们登录,跳转到了登录页面,登录成功后,会自动跳转回来。
重定向是需要浏览器给我们提供支持的,前提是浏览器必须识别重定向状态码。
要完成重定向,需要服务器的 HTTP 的响应报头存在这么一个属性,Location: 新的地址
,告诉客户端接下来要去哪里访问。
重定向演示:
下面的程序包含两个文件:
① Sock.hpp:基本通信函数的实现,跟上面的一样,这里就不贴了。
② Http.cc:服务端。
- Http.cc:
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>
#define WWWROOT "./wwwroot/"
#define HOME_PAGE "index.html"
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void *HandlerHttpRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
#define SIZE 1024 * 10
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer; // 查看http的请求格式!for test
std::string response = "HTTP/1.0 301 Permanently moved\n";
response += "Location: https://www.qq.com/\n";
response += "\n";
send(sock, response.c_str(), response.size(), 0);
}
close(sock);
return nullptr;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for (;;)
{
int sock = Sock::Accept(listen_sock);
if (sock > 0)
{
pthread_t tid;
int *parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
}
运行测试:
在浏览器的 URL 输入框中输入服务器的 IP 和 port 后,自动跳转到重定向的网站。
7. HTTP 的版本
-
HTTP/1.0:短连接。建立连接 -> 请求 -> 响应 -> 关闭连接,每次 HTTP 请求返回一个资源。
一个大型的网页,是由非常多个资源组成的,于是访问一个大型网页时,就需要进行多次 HTTP 请求。HTTP 协议是基于 TCP 协议的,所以 TCP 要通信,就要建立连接 -> 传输数据 -> 关闭连接,每一次 HTTP 请求都要执行上面的过程。所以,频繁地建立 TCP 连接,带来的开销就会很大,降低了效率。 -
HTTP/1.1 之后:支持长连接。长连接的请求/响应报头中会有
Connection: keep-alive
字段。一个连接可以进行多次请求/响应,不需要频繁地建立 TCP 连接,提高了效率。
8. HTTP 常见 Header
- Content-Type:数据类型(比如 text/html 等)。
- Content-Length:Body 的长度。
- Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
- User-Agent:声明用户的操作系统和浏览器版本信息。
- Referer:当前页面是从哪个页面跳转过来的。
- Location:搭配 3XX 状态码使用,告诉客户端接下来要去哪里访问。
- Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能。
9. Cookie 与 session
HTTP 协议本身是一种无状态的协议,它并不记录发起 HTTP 请求的上下文信息。
当我们在进行各种页面跳转时,本质就是在进行各种 HTTP 请求,但是不管怎么跳转,网站照样认识我们(比如帐号身份信息)。
HTTP 协议主要帮我们解决网络资源获取的问题。要网站认识我们,并不是 HTTP 协议本身要解决的问题,但 HTTP 可以提供一些技术支持,来保证网站具有 “会话保持” 的功能,会话管理的功能由 Cookie 技术支持。
- Cookie:
① 浏览器:Cookie 其实是一个文件,在浏览器中,有两种存在形式:文件版和内存版。该文件里面保存的是用户的私密信息。
② HTTP 协议:如果有该网站对应的 Cookie ,在发起任何请求的时候,都会自动在 request 中携带该 Cookie 信息。
Cookie 演示:
下面的程序包含三个文件:
① Sock.hpp:基本通信函数的实现,跟上面的一样,这里就不贴了。
② Http.cc:服务端。
③ index.html:一个简单的网站首页。
- Http.cc:
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>
#define WWWROOT "./wwwroot/"
#define HOME_PAGE "index.html"
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void *HandlerHttpRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
#define SIZE 1024 * 10
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer; // 查看http的请求格式!for test
std::string html_file = WWWROOT;
html_file += HOME_PAGE; // html_file表示文件路径
// 接下来才是正文
std::ifstream in(html_file); // 使用文件流对象打开文件
if (!in.is_open())
{
std::string http_response = "HTTP/1.0 404 Not Found\n";
http_response += "Content-Type: text/html; charset=utf8\n"; // 正文部分的数据类型
http_response += "\n";
http_response += "<html><p>你访问的资源不存在</p></html>";
send(sock, http_response.c_str(), http_response.size(), 0);
}
else
{
// stat系统调用:获取指定路径下文件的属性(第二个参数是输出型参数)
struct stat st;
stat(html_file.c_str(), &st);
// 返回的时候,不仅仅是返回正文网页信息,还要包括http响应
std::string http_response = "HTTP/1.0 200 OK\n";
http_response += "Content-Type: text/html; charset=utf8\n"; // 正文部分的数据类型
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size); // 内容长度,就是文件的大小
http_response += "\n";
// Set-Cookie:服务器向浏览器设置一个Cookie
// 告诉浏览器把Set-Cookie后面的内容写到浏览器的Cookie文件里
// 从此往后,浏览器再向服务器请求时,都会把这个Cookie信息带上
http_response += "Set-Cookie: id=1111\n";
http_response += "Set-Cookie: password=2222\n";
http_response += "\n"; // 空行
std::string content;
std::string line;
while (std::getline(in, line))
{ // 用getline一行一行读取文件内容
content += line;
}
http_response += content;
in.close();
send(sock, http_response.c_str(), http_response.size(), 0);
}
}
close(sock);
return nullptr;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for (;;)
{
int sock = Sock::Accept(listen_sock);
if (sock > 0)
{
pthread_t tid;
int *parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
}
- index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>hello 我是首页!</h5>
<h5>hello 我是表单!</h5>
<!-- /a/b/handler_form并不存在,也不处理 -->
<form action="/a/b/handler_from" method="POST">
姓名:<input type="text" name="name"><br/>
密码:<input type="password" name="passwd"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
运行测试:
刷新几次浏览器:浏览器检测到要访问网站的 Cookie 是存在的,于是在以后的请求中自动携带上 Cookie 。
若我们把该网站的 Cookie 移除掉,再刷新一下浏览器:
我们就会发现浏览器发送请求时就不再携带 Cookie 了,因为浏览器发现没有该网站对应的 Cookie 信息。
服务器要给浏览器设置 Cookie ,需要在响应报头中加上Set-Cookie: key=value
字段。
我们发现,浏览器每次向目标网站发起请求时,都会自动把曾经给浏览器写入的目标网站的 Cookie 信息携带上。
我们在任何网站登录后,一定会存在对应网站的 Cookie ,如果不想让网站认识我们,就可以把 Cookie 直接移除掉,再刷新网页,于是这个网站就不认识我们了;如果把 Cookie 一直保留着,浏览器在向目标网站发起任何请求时都会携带上 Cookie 字段,支持服务后端对我们的身份进行各种验证,所以在用户看来,只要在某个网站登录过一次,后面再次访问该网站时已无需登录,它已经认识我们了。
① 文件版 Cookie:在浏览器的安装目录以及浏览器使用的某些相关的用户级目录下,Cookie 信息保存在文件里,关闭浏览器甚至电脑重启再打开,都不会影响 Cookie 信息,再打开网站,还认识我们。
② 内存版 Cookie:Cookie 信息在内存中,浏览器一关闭,Cookie 信息就没有了,再打开网站,就不认识我们了。
这就是 Cookie 技术。
别人如果盗取我们的 Cookie 文件,别人就可以以我们的身份进行认证,访问特定的资源。如果保存的是我们的用户名和密码,那么就非常糟糕了。
单纯使用 Cookie 是具有一定安全隐患的。
- session:核心思路是,将用户的私密信息保存在服务端。
因为客户端的 Cookie 文件里面不再保存用户的任何私密信息,所以,即使 Cookie 文件泄露了,也不会导致用户的私密信息被泄露。
仍然存在 Cookie 文件被泄露的风险,这是无法杜绝的。如果 Cookie 文件泄露了,别人就可以拿着它,冒充我们的身份去访问对应的网站。虽然这个问题没法彻底解决,但是有一些衍生的防御方案,比如异地登录、短信认证等。
Cookie + session ,本质:增强用户访问网站或者平台的体验。
三、HTTP 与 HTTPS
HTTP 对数据是没有经过任何加密的,在网络中,数据其实是 “裸” 着的,是不安全的。
HTTPS = HTTP + TLS/SSL 。S 是 Secure 。TLS/SSL 可以理解为 HTTP 数据的加密解密层,也在应用层。HTTPS 对数据是经过加密的,是安全的。
什么叫做安全?安全不是让别人拿不到,而是别人拿到了也没法处理。
什么情况下是比较安全的呢?就是解密的成本远远超过解密之后带来的收益。
简单理解一下加密方式:
-
对称加密,密钥(只有一个)X
用 X 加密,也要用 X 解密。
data ^ X = result
result ^ X = data -
非对称加密,有一对密钥:公钥和私钥。
用公钥加密,但只能用私钥解密,或者用私钥加密,但只能用公钥解密。
一般而言,公钥是全世界公开的,私钥是自己必须私有保存的。