目录
[1. HTTP协议介绍](#1. HTTP协议介绍)
[1.1 URL介绍](#1.1 URL介绍)
[1.2 urlencode和urldecode](#1.2 urlencode和urldecode)
[1.3 HTTP协议格式](#1.3 HTTP协议格式)
[1.4 HTTP的方法和报头和状态码](#1.4 HTTP的方法和报头和状态码)
[2. 代码验证HTTP协议格式](#2. 代码验证HTTP协议格式)
[2.2 html正式测试](#2.2 html正式测试)
[2.3 再看HTTP方法和报头和状态码](#2.3 再看HTTP方法和报头和状态码)
[2.3.1 方法_GET和POST等](#2.3.1 方法_GET和POST等)
[2.3.2 报头_Cookie和Session等](#2.3.2 报头_Cookie和Session等)
[2.3.3 状态码_重定向等](#2.3.3 状态码_重定向等)
[2.3.4 代码演示](#2.3.4 代码演示)
1. HTTP协议介绍
网络部分到目前为止,基本socket通信写完,包括tcp和udp的网络通信。 学了一般的服务器设计原则和方式,还有自定义协议 + 序列化和反序列化。
此篇写的协议是应用层的协议,你在写的同时,别人有没有可能写?肯定有,这样一来就会存在很多个人写的不同的应用层协议。
所以,已经有大佬针对常见的应用场景,早就写好了常见的协议软件,供我们使用。 hhtp/https就属于这些写好了的常见软件。
HTTP:Hypertext Transfer Protocol 超文本传输协议
HTTPS:Hypertext Transfer Protocol Secure 超文本传输安全协议
http做的事情和我们前面做的事情是一样的,包括:
- 接收完整报文,并且去掉报头。
- 有效载荷反序列化。
- 用户使用结构化数据执行任务。
- 将结构化数据序列化成字符串。
- 给有效载荷增加报头。
- 发生完整报文。
虽然我们现在不知道它是如何实现的,但毫无疑问,http协议肯定比我们写的复杂,而且也更加好用,所以之后我们也不用再自己写了,直接用现成的就可以。
又是这张图,在OSI模型中,网络可以分为七层,而在TCP/IP模型中,网络分为五层,这是将OSI中的上三层,包括会话层,表示层,应用层归为了一层,统称为应用层。
前面写的网络版计算机中,一共也是分为三层:
- 底层网络连接为一层,包括class TcpServer类中的所有成员。这一层对应对应OSI中的会话层。
- 在CalClient.cc的while循环里面的步骤为一层,这一层专门用来处理网络中的数据的。对应OSI中的表示层。
- 具体的计算函数calculator为一层,它在表示层被回调,执行真正的业务逻辑。对应OSI中的应用层。
从上图中可以看到,http就位于TCP\IP模型中的应用层,所以OSI模型中的上三层工作http也会做,也就是上面列举的进行序列化和反序列化等六个内容。
1.1 URL介绍
平时我们俗称的"网址"其实就是说的URL。
上图所示是一个早期使用http协议的网址,以及它隔断字符的意思,现在只是见一见,不作讲解。
如上图所示的URL是本博客主页的网址。 目前大多使用的协议是https协议,对比上面http协议的URL发现没有了端口号,这是因为端口号都默认了,使用http协议的进程,端口号是80。使用https协议的进程,端口号是443。
域名经过域名解析以后就变成了IP地址,所以域名本质上就是一个IP地址,用来标识一台网络主机的唯一性。有IP地址,有端口号,就已经能标识一个进程在网络中的唯一性了。
域名后面的/不要理解为我们Linux机器上的根目录,它是web根目录,也就是说这个根目录是被指定的,可以是任意一个文件夹。
根目录后面的是文件路径,这个路径下放的就是客户端想要的东西,网络请求就是客户端将数据从服务器的这个路径下拿走。
我们平时从网上看到的图片,视频,音频等等网络资源,都放在服务器的磁盘上。
而http协议也可以从服务器拿下来对应的网络资源,这些资源都可以看作是资源文件(本质上也就是文件),又因为服务器上资源种类繁多,但是http都能搞定,所以http被叫做超文本传输协议。
1.2 urlencode和urldecode
看上面贴的博客网址:
如上图所示的url(网址),里面包含有/以及?等字符。
像这样的字符,已经被url当做特殊意义理解了,因此这些字符不能随意出现。
如果url中要包含这些特殊意义的字符,就需要对其做转义处理,就类似C语言中的转义字符一样。但是这里是网络,转义的规则不和C语言一样,它有自己的规则:取出字符的ASCII码:
转成16进制,然后前面加上百分号即可。
比如,+号被转义后成为%2B,这个过程就叫做encode,而将%2B转回到+号就叫做decode。
这个过程并不需要我们自己去做,有需要进行编码和解码的需求在网上直接查就可以,UrlEncode编码/UrlDecode解码 - 站长工具 (chinaz.com)
如上图,将字符C++进行urlencode后的结果是C%2B%2B,同样也可以进行urldecode进行解码。
百度一下C++就是这样的网址:
看到wd=C%2B%2B,其中wd表示关键字,=号后面的内容就是encode后的结果。
当服务器收到url请求后,会自行对特殊字符%xx进行decode。包括汉字也需要进行encode和decode。
1.3 HTTP协议格式
HTTP是基于请求和响应的应用层协议,使用的是TCP套接字,客户端向服务端发送request请求,服务端收到请求后会作response响应返回给客户端。
如上图所示,是HTTP协议的宏观格式,包括客户端的request和服务端的response。
客户端:
请求行: 如上图红色框中所示,包含GET方法,url,还有http协议的版本,如http/1.0或者http/1. 1,这三部分构成请求行,必须使用\r\n来结束这一行。
请求报头: 如上图绿色框中所示,包含的都是请求属性,采用的是键值对的形式,name表示属性的名称,如HOST等,value是属性的内容,如具体的一个网址。每一个属性必须以\r\n来结束。
空行: 如上图紫色框中所示,这一行什么内容都没有,只有\r\n用来表示空行。
请求正文: 如上图最下面的黑色框中所示,包含请求的正文,如username用户名,passwd密码等,同样每个正文后面必须以\r\n来结束。
正文可以省略不写,但是其他三部分必须有,以空行为界,空行后面的是请求正文,空行前面的是请求行和请求报头。
每一部分都是以字符串形式放在报文中,如GET url http/1.0\r\nname: value\r\n\r\n,这一个报文中,包含请求行,一行属性以及空行。
请求行和请求报头两部分可以看成我们自定义协议中的报头,请求正文是有效载荷。
服务端:
状态行: 如上图红色框中所示,包含http/1.1HTTP协议版本,状态码以及状态码描述,最后是\r\n。
响应报头: 如上图绿色框中所示,包含响应属性,和request的请求报头类似。
空行: 如上图紫色框中所示,也是只有一个\r\n。
**响应正文:**如上图黑色框中所示,包含服务器要给客户端返回的数据内容,如html/css/js以及图片,视频,音频等网络资源。
和客户端类似,状态行和响应报头同样类似我们自定义协议中的报头,响应正文是有效载荷。
服务端的response和客户端的request格式相同,只是每一块的内容有所差异。
应用层怎么保证完整的读取了一个请求或者响应?
0首先肯定可以完整的读完一行,因为每一行都是以\r\n结尾的,所以使用while(完整读取一行)的循环可以读完请求行+请求报头,直到空行再停止。
按照我们自定义协议中的逻辑,此时报头(请求行+请求报头)完全读完了,还剩下有效载荷(请求正文)。同样地,在请求报头中有一个属性`Content-Length:XXX正文长度``,根据这个正文长度就可以将所有的请求正文读取完毕。
此时一个完整的请求request就读取到了,同样的方式也可以读取到一个完整的响应response。
请求和响应是怎么做到序列化和反序列化的?序列化和反序列化是由HTTP自己实现的,因为协议中的内容都是字符串,第一行(请求行/状态行) + 请求/响应报头,只要按照\r\n为判断条件,一直循环下去,就可以拼接(序列化)或者拆分(反序列化)。
正文不用进行序列化和反序列化,因为它本身就是字符串,根据Content-Length:XXX正文长度读取相应字节数就可以。
1.4 HTTP的方法和报头和状态码
简单看下 HTTP的方法和状态码和报头,下面在代码中会演示。
HTTP的方法非常多,但是常用的就两种,分别是GET和POST方法。
HTTP常见报头Header:
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上
- User-Agent: 声明用户的操作系统和浏览器版本信息
- referer: 当前页面是从哪个页面跳转过来的
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
HTTP的状态码:
在代码途中会讲解到上面的内容。
telnet链接一下百度首页,用GET方法获取一下资源:(回车后再回车(空行))
此时就拿到了百度首页的资源,下面一大段就是html网页。
2. 代码验证HTTP协议格式
Sock.hpp和Log.hpp仍然用之前TCP套接字写好的:
Log.hpp
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
// #define LOGFILE "./threadpool.log"
#define LOGFILE "./calculator.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...) // 可变参数
{
#ifndef DEBUG_SHOW
if(level== DEBUG)
{
return;
}
#endif
char stdBuffer[1024]; // 标准日志部分
time_t timestamp = time(nullptr); // 获取时间戳
// struct tm *localtime = localtime(×tamp); // 转化麻烦就不写了
snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; // 自定义日志部分
va_list args; // 提取可变参数的 -> #include <cstdarg> 了解一下就行
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof(logBuffer), format, args);
va_end(args); // 相当于ap=nullptr
printf("%s%s\n", stdBuffer, logBuffer);
// FILE *fp = fopen(LOGFILE, "a"); // 追加到文件
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}
Sock.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"
class Sock
{
private:
const static int gbacklog = 20; // listen的第二个参数,现在先不管
public:
Sock()
{}
~Sock()
{}
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
if (listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, listensock: %d", listensock);
return listensock;
}
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
}
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
// 一般情况下:
// const std::string &: 输入型参数
// std::string *: 输出型参数
// std::string &: 输入输出型参数
int Accept(int listensock, std::string *ip, uint16_t *port)
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
return -1;
}
if (port)
*port = ntohs(src.sin_port);
if (ip)
*ip = inet_ntoa(src.sin_addr);
return servicesock;
}
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == 0)
return true;
else
return false;
}
};
敲个和TcpServer.hpp类似的HttpServer.hpp:
HttpServer.hpp
cpp
#pragma once
#include <iostream>
#include <signal.h>
#include <functional>
#include "Sock.hpp"
class HttpServer
{
public:
using func_t = std::function<void(int)>;
private:
int _listensock;
uint16_t _port;
Sock _sock;
func_t _func;
public:
HttpServer(const uint16_t &port, func_t func)
: _port(port)
, _func(func)
{
_listensock = _sock.Socket();
_sock.Bind(_listensock, _port);
_sock.Listen(_listensock);
}
void Start()
{
signal(SIGCHLD, SIG_IGN); // 让子进程退出自动释放
while (true)
{
std::string clientIp;
uint16_t clientPort = 0;
int sockfd = _sock.Accept(_listensock, &clientIp, &clientPort);
if (sockfd < 0)
continue;
if (fork() == 0) // 子进程处理HTTP请求
{
close(_listensock);
_func(sockfd); // 回调处理
close(sockfd);
exit(0);
}
close(sockfd);
}
}
~HttpServer()
{
if (_listensock >= 0)
close(_listensock);
}
};
这代码之前基本都写过了,写个测试代码:
cpp
#include <iostream>
#include <memory>
#include "HttpServer.hpp"
void HandlerHttpRequest(int sockfd)
{
// 1. 读取请求并打印
char buffer[10240];
ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "--------------------\n" << std::endl; // 把请求打印出来,加点分隔符
}
}
static void Usage(const std::string &process) // 使用手册
{
std::cout << "\nUsage: " << process << " port\n" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
httpserver->Start();
return 0;
}
编译运行:
此时服务器已经起来了,没写客户端,怎么测试呢?
顺便一个网页输入xshell里的主机号加冒号再加端口号,这里输入121.199.6.56:8080
(绑定的端口号需要自己到自己的云服务器看看端口有没有开,可以手动添加,本博客常用的就是8080,8081,7070,7071等,这里贴个阿里云添加教程链接:【图文教程】阿里云服务器开放端口设置(超详细)-阿里云开发者社区)
此时网页是这样的:
因为我们并没有给请求任何响应,不过请求已经在客户端打印出来了:
打印了几次应该是浏览器请求失败又请求了几次。现在试着写一个响应,(这里的html知识可以去简单学一学,这里只是直接+=,下面还会自己写网页,很简单,这里就直接用了)
cpp
void HandlerHttpRequest(int sockfd)
{
// 1. 读取请求并打印
char buffer[10240];
ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "--------------------\n" << std::endl; // 把请求打印出来,加点分隔符
}
// 2. 试着构建一个http的响应并发送
std::string HttpResponse = "HTTP/1.1 200 OK\r\n"; // 状态行
HttpResponse += "\r\n"; // 空行
HttpResponse += "<html><h1>Hello Linux NetWork GR_C</h1></html>"; // 正文
send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
Ctrl C关掉上面进程,重复上面步骤:
上面的请求打印已经是刷新浏览器之后的了:
成功运行。
也可以用之前的 telnet 链接,然后输入GET / http/1.1获取请求的第一行:
2.2 html正式测试
上面打印出的请求中,第一行就是请求的资源。
写一个截取请求变为子串的工具类:
Util.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
class Util
{
public:
// aaaa\r\nbbbbb\r\nccc\r\n\r\n
static void cutString(std::string s, const std::string &sep, std::vector<std::string> *out)
{ // 剪切读取到的请求,static使得类名直接访问方法函数
std::size_t start = 0;
while (start < s.size())
{
auto pos = s.find(sep, start); // 从start开始查找sep分隔符
if (pos == std::string::npos) // 找不到,break
break;
std::string sub = s.substr(start, pos - start); // 找到了,子串截取
out->push_back(sub); // 截取到的子串push进vector
start = start + sub.size() + sep.size(); // 让start指向分隔符右侧,不断循环
}
if (start < s.size()) // 后面还有内容也push
out->push_back(s.substr(start));
}
};
测试一下我们的截取功能并打印:
cpp
void HandlerHttpRequest(int sockfd)
{
// 1. 读取请求并打印
char buffer[10240];
ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "--------------------\n" << std::endl; // 把请求打印出来,加点分隔符
}
std::vector<std::string> vline; // 剪切读取到的请求
Util::cutString(buffer, "\n", &vline); // 读取第一行
std::vector<std::string> vblock;
Util::cutString(vline[0], " ", &vblock); // 第一行拆开
std::cout << "#### start ################" << std::endl;
for(auto &iter : vblock)
{
std::cout << "---" << iter << "\n" << std::endl;
}
std::cout << "##### end ###############" << std::endl;
// 2. 试着构建一个http的响应并发送
std::string HttpResponse = "HTTP/1.1 200 OK\r\n"; // 状态行
HttpResponse += "\r\n"; // 空行
HttpResponse += "<html><h1>Hello Linux NetWork GR_C</h1></html>"; // 正文
send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
这里网页再输入121.199.6.56:7070就成功提取并打印了:
如果网页输入121.199.6.56:7070/a/b/c/d.html,请求的路径也成功被打印出来了:
也可以发现我们前面网页输入121.199.6.56:7070请求的是**/(这个不叫作根目录,而叫作web根目录)**
这里新建一个wwwroot目录,在里面建一个index.html文件(框架安装插件后感叹号加回车就出来了)
index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TestWeb</title>
</head>
<body>
<h1>Hello Linux NetWork GR_C html</h1>
<p>hello 哈喽 这里是段落部分......</p>
<p>hello 哈喽 这里是段落部分......</p>
<p>hello 哈喽 这里是段落部分......</p>
</body>
</html>
cpp
#include <iostream>
#include <memory>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Util.hpp"
// 一般http都要有自己的web根目录,下面让请求的资源从web根目录开始,这就是为什么叫作web根目录
#define ROOT "./wwwroot" // ./wwwroot/index.html
#define HOMEPAGE "index.html" // 如果客户端只请求了一个/,返回默认首页
static void Usage(const std::string &process) // 使用手册
{
std::cout << "\nUsage: " << process << " port\n" << std::endl;
}
void HandlerHttpRequest(int sockfd)
{
// 1. 读取请求并打印
char buffer[10240];
ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "--------------------\n" << std::endl; // 把请求打印出来,加点分隔符
}
std::vector<std::string> vline; // 剪切读取到的请求
Util::cutString(buffer, "\n", &vline); // 读取第一行
std::vector<std::string> vblock;
Util::cutString(vline[0], " ", &vblock); // 第一行拆开
// std::cout << "#### start ################" << std::endl;
// for(auto &iter : vblock)
// {
// std::cout << "---" << iter << "\n" << std::endl;
// }
// std::cout << "##### end ###############" << std::endl;
std::string file = vblock[1]; // 就是请求路径,类似/a/b/c.html
std::string target = ROOT; // 定义成自己宏定义的web根目录
if (file == "/") // 如果请求的是web根目录(类似默认目录)
file = "/index.html"; // 默认目录改成这个
target += file;
std::cout << target << std::endl;
std::string content; // 存文件内容的
std::ifstream in(target); // 打开target路径文件
if (in.is_open()) // 成功打开
{
std::string line;
while (std::getline(in, line))
{
content += line;
}
in.close();
}
std::string HttpResponse; // 构建响应,现在先简单用用 // 后面还会讲解
if (content.empty()) // 状态行->没有内容
{
HttpResponse = "HTTP/1.1 301 Moved Permanently\r\n";
}
else // 有内容(正常)
{
HttpResponse = "HTTP/1.1 200 OK\r\n";
}
HttpResponse += "\r\n"; // 空行
HttpResponse += content; //正文
// // 2. 试着构建一个http的响应并发送
// std::string HttpResponse = "HTTP/1.1 200 OK\r\n"; // 状态行
// HttpResponse += "\r\n"; // 空行
// HttpResponse += "<html><h1>Hello Linux NetWork GR_C</h1></html>"; // 正文
send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
httpserver->Start();
return 0;
}
运行后网页输入121.199.6.56:7070就是这样:
都和预料的一样。也可以输入121.199.6.56:7070后输入对应路径获取资源,这里就不演示了。
2.3 再看HTTP方法和报头和状态码
再看下上面的知识:
2.3.1 方法_GET和POST等
上面已经在 telnet链接,用GET方法获取过了资源,下面在代码中使用一下:
这里再wwwroot下建立一个a目录,在a目录里建立b目录,在b目录里写一个test.html:
cpp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TestGET</title>
</head>
<body>
<h1>测试GET</h1>
<p>hello 哈喽 这里是段落部分......</p>
<form name="input" action="/a/b/test.html" method="GET">
Username: <input type="text" name="user">
<br/>
Password: <input type="password" name="pwd">
<br/>
<input type="submit" value="登陆">
</form>
</body>
</html>
把服务端起来,然后用浏览器输入121.199.6.56:7070/a/b/test.html访问这个资源:
随便输入用户名和密码然后点击登录:
此时服务端拿到了用户名和密码,还可以发现在网页上也显示出来了(这说明GET方法是通过url向服务端传参的)。
在test,html里把GET方法改成post方法:(大小写不敏感,改成post试试)
重复上面步骤:
输入后点击登录是这样的:(url上并没有回显参数)
所以,POST方法是通过HTTP的正文提交参数的。
得到的结论:GET方法通过url提交参数。 POST方法通过HTTP请求正文提交参数。
站在客户端的角度,如果使用GET方法,在提交form表单的时候,内容会拼接到url中,在浏览器的网址栏中可以看到,如果是账号密码的话其他人就能够直接看到。
如果使用POST方法,在提交form表单的时候,内容是通过请求正文提交的,在浏览器的网址栏中看不到。
所以,如果提交的数据是比较私密的,如账号密码等,就使用POST方法,如果无所谓的数据就使用POST/GET哪个都行。
但是并不是说POST方法比GET方法安全,仅仅是POST方法无法直观的看到提交的数据。 POST/GET两种方法都是不安全的。
其它方法几乎用不到,这里就不讲解了。
2.3.2 报头_Cookie和Session等
HTTP常见报头Header:
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上
- User-Agent: 声明用户的操作系统和浏览器版本信息
- referer: 当前页面是从哪个页面跳转过来的
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
在使用POST方法发起HTTP请求后,多了几个GET方法中没有的属性,如上图所示。
Connection: close表示短连接,
Connection: keep-alive表示长连接:
- 一个客户端对应一个套接字,客户端的一个请求响应完后,套接字不关闭,只有客户端退出了,或者指定关闭时,套接字才关闭。
- 一个客户端无论有多少个请求,都通过一个套接字和服务器进行网络通信。
Content-Length: XXX\r\n 使用POST方法发起请求时,服务器收到的请求中就有Content-Length这一属性,用来表示请求正文的长度,以字节为单位。
Content-Type: xxx\r\n 表示连接类型,请求和响应中都有。 上图所示的是请求中的Content-Type,由于会用表单提交数据所以它的值如上图红色框中所示。这是浏览器在告诉服务器,它提交的请求类型是form,服务器需要按照表单的处理方式来处理。
Cookie技术:
假设访问CSDN使用的是HTTP协议。
CSDN在第一次登录后,之后打开CSDN就不用再进行登陆了。根据前面学习我们知道,登录时输入的信息其实就是form表单,然后将数据提交给服务器,让服务器进行鉴权,如果权限符合就会返回对应的响应。
HTTP实际上是一种无状态协议,每次请求并不会记录它曾经请求了什么。
所以,在第一次登录CSDN后,在站内进行网页跳转(从一篇文章到另一篇文章)时,理论上需要再次输入账号密码进行登录,让服务器进行鉴权,因为HTTP的每次请求/响应之间是没有任何关系的。但我们在使用浏览器访问CSDN的时候发现并不是这样的,只需要登录一次即可。
这是因为浏览器在我们第一次登录CSDN的时候,将我们的账号密码等登录信息保存了下来。
当我们进行网页跳转或者再次打开CSDN的时候,浏览器自动将保存的用户登录信息添加到了请求报头中,并通过HTTP协议发送给了服务器,服务器进行鉴权并返回对应的响应。
登录还是需要的,只是浏览器帮我们做了这个事。这种技术就叫做Cookie技术。
如上图所示,点击网址前面的小锁,可以查看当前浏览器正在使用的Cookie。
当我们将上图中和CSDN有关的Cookie数据删除后就需重新输入账号密码来登录了。
如上图所示,点击网址前面的小锁,可以查看当前浏览器正在使用的Cookie。
当我们将上图中和CSDN有关的Cookie数据删除后就需重新输入账号密码来登录了。
- 用户在第一次输入账号和密码时,浏览器会进行保存(Cookie),近期再次访问同一个网站(发送http请求),浏览器会自动将用户信息添加到报头中推送给服务器。
- 这样只要用户首次输入密码,一段时间内将不用再做登录操作了。
Cookie又分为内存级和文件级:
- 内存级Cookie:将信息保存在浏览器的缓冲区中,当浏览器被关闭时,意味着进程结束,保存的信息也就没有了,重新打开浏览器后还需要重新登录。
- 文件级Cookie:将信息保存在文件中,文件是放在磁盘上的,无论浏览器怎么打开关闭,文件中的信息都不会删除,在之后发送HTTP请求时,浏览器从该文件中读取信息并加到请求报头中。
根据日常使用浏览器的情况,我们可以知道,大部分情况下的Cookie都是文件级别的,因为关闭了浏览器下次打开不用再重新登录。
Cookie文件也是存在我们电脑上的,具体路径有兴趣可以去找一下。
cookie安全机制,cookie下面的设置可以提高安全性:
- domain:可以访问该Cookie的域名。如果设置为".google.com",则所有以"google.com"结尾的域名都可以访问该Cookie。注意第一个字符必须为"."。
- path:Cookie的使用路径。如果设置为"/sessionWeb/",则只有contextPath为"/sessionWeb"的程序可以访问该Cookie。如果设置为"/",则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为"/"。
- httponly:如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,但不是绝对防止了攻击
- secure:该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。
- expires:指定了coolie的生存期,默认情况下cookie是暂时存在的,他们存储的值只在浏览器会话期间存在,当用户退出浏览器后这些值也会丢失,如果想让cookie存在一段时间,就要为expires属性设置为未来的一个过期日期。现在已经被max-age属性所取代,max-age用秒来设置cookie的生存期
Set-Cookie: qwerty=219ffwef9w0f;
Path=/;
Expires=Wed, 30 Aug 2019 00:00:00 GMT
- 对保存到cookie里面的敏感信息加密
- 设置指定的访问域名
- 设置HttpOnly为true
- 设置Secure为true
- 给Cookie设置有效期
- 给Cookies加个时间戳和IP戳,实际就是让Cookies在同个IP下多少时间内失效
Session技术:
如果我们电脑上的Cookie文件被不法份子盗取,那么它就能以我们的身份去登录我们的CSDN,并且进行一些非法操作。
- 为了保证信息安全,新的做法是将用户的账号密码信息以及浏览痕迹等信息保存在服务器上。
- 每个用户对应一个文件,这个文件被叫做Session文件,由于存在很多的Session文件,所以给每个文件一个名字,叫做Session id。
- 服务器将Session id作为响应返回给用户,此时用户的Cookie中保存的就是这个id值。
如上图所示,当第一次登录时,浏览器的form表单中的用户信息提交到了服务器,服务器创建Session文件并保存用户信息,然后再生成一个id返回给浏览器。
此时浏览器的Cookie保存的就是这个id值。当进行站内页面跳转或者再次打开CSDN的时候,浏览器自动将Cookie中Session id加到请求报头中提交给服务端。
服务端存储用户信息的技术就叫做Session技术。
为什么新的会话保持(Cookie和Session)技术能够提高用户信息的安全性呢?
- 服务端是由专业的人员维护的,服务器中存在病毒以及流氓软件的可能想更小,所以用户信息在服务端会更安全。
- 如果客户端的Cookie中的Session id被盗用,不法分子使用该id向服务端发起请求时,会因为常用IP地址不一样而被服务端强制下线,此时只有手里真正有账号密码的人才能够再次登录。
保证Session安全的策略非常多,有兴趣可以自行了解。
2.3.3 状态码_重定向等
在构建响应的时候,状态行中的状态码直接写的200,状态码描述是OK。那么状态码到底有哪些呢?它们代表的意义是什么? 状态码有五种类型,分别以1~5开头:
状态码 | 类别 | 原因短语 |
---|---|---|
1XX | informa(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常且处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
最常见的一些状态码,如200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)。
这些状态码没什么好说的,如果在代码中要用到可以去找一些比较权威的文档,也可以去查一下表,不过网上的表也不是每个人都遵守的,不过建议大家还是遵守。
下面重点说一下重定向状态码(3XX), 看下网上的一些表:
- 重定向就是将网络请求重新定个方向转到其它位置(跳转网站),此时这个服务器相当于提供了一个引路的服务。
相信都有过这样的经历,打开一个网址以后,自动就弹出一些广告网页,这就是一种重定向。
浏览器发送请求给服务端,服务端返回一个新的url,并且状态码是3XX,浏览器会自动用这个新的url向新地址的服务端发起请求。
所以说,重定向是由客户端完成的,当客户端浏览器收到的响应中状态码是3XX后,它就会自动从响应中寻找返回的新的url并发起请求。
重定向又有两种:
- 永久重定向:状态码为301。
- 临时重定向:状态码为302和307。
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。
如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时直接访问的就是重定向后的网站。
而如果某个网站是临时重定向,那么每次访问该网站时都需要浏览器来帮我们完成重定向跳转到目标网站。
2.3.4 代码演示
在前面写的代码加点代码演示一下:
在b目录里再建一个404.html:(照片可以不带emmm)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面不存在</title>
</head>
<body>
<h1>你访问的页面不存在</h1>
<br/>
<img src="https://img-blog.csdnimg.cn/img_convert/4a13d13d43fdcd24ba8dac4c37e1e115.gif" alt="GR_C博客里的条总" title="条总">
</body>
</html>
HandlerHttpRequest函数:
cpp
void HandlerHttpRequest(int sockfd)
{
// 1. 读取请求并打印
char buffer[10240];
ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << "--------------------\n" << std::endl; // 把请求打印出来,加点分隔符
}
std::vector<std::string> vline; // 剪切读取到的请求
Util::cutString(buffer, "\n", &vline); // 读取第一行
std::vector<std::string> vblock;
Util::cutString(vline[0], " ", &vblock); // 第一行拆开
// std::cout << "#### start ################" << std::endl;
// for(auto &iter : vblock)
// {
// std::cout << "---" << iter << "\n" << std::endl;
// }
// std::cout << "##### end ###############" << std::endl;
std::string file = vblock[1]; // 就是请求路径,类似/a/b/c.html
std::string target = ROOT; // 定义成自己宏定义的web根目录
if (file == "/") // 如果请求的是web根目录(类似默认目录)
file = "/index.html"; // 默认目录改成这个
target += file;
std::cout << target << std::endl;
std::string content; // 存文件内容的
std::ifstream in(target); // 打开target路径文件
if (in.is_open()) // 成功打开
{
std::string line;
while (std::getline(in, line))
{
content += line;
}
in.close();
}
std::string HttpResponse; // 构建响应,现在先简单用用 // 后面还会讲解
if (content.empty()) // 状态行->没有内容
{
HttpResponse = "HTTP/1.1 301 Moved Permanently\r\n";
// HttpResponse += "Location: https://blog.csdn.net/GRrtx?type=blog\r\n"; // 重定向
HttpResponse += "Location: http://121.199.6.56:7070/a/b/404.html\r\n"; // 站内跳转
}
else // 有内容(正常)
{
HttpResponse = "HTTP/1.1 200 OK\r\n";
HttpResponse += ("Content-Type: text/html\r\n"); // 要识别,有些浏览器能自动识别,不过现在是网页,可以是照片和视频等
HttpResponse += ("Content-Length: " + std::to_string(content.size()) + "\r\n");
HttpResponse += "Set-Cookie: 这是一个cookie(小块的东西)\r\n";
}
HttpResponse += "\r\n"; // 空行
HttpResponse += content; //正文
// // 2. 试着构建一个http的响应并发送
// std::string HttpResponse = "HTTP/1.1 200 OK\r\n"; // 状态行
// HttpResponse += "\r\n"; // 空行
// HttpResponse += "<html><h1>Hello Linux NetWork GR_C</h1></html>"; // 正文
send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}
服务器起来,然后在浏览器输入121.199.6.56:7070访问一下:
在浏览器输入121.199.6.56:7070/test404test访问一下不存在的页面:
直接重定向到404.html了。
访问下a/b/test.html然后登录:
此时a/b/test.html里的方法是post,改成get重复上面步骤:
点击登录后也会重定向到我们的404页面,你也可以让它重定向到其它页面。
永久重定向无法演示出来,效果和临时重定向一样。
贴下服务器上收到多个请求的原因:
- 一个网页中有多种类型的资源,而一次请求只能获取一种类型资源。
- 虽然我们在浏览器中只访问一次,但是浏览器会通过多个线程发起多次请求。
- 多次请求的多个响应共同组成了一个网页。
本篇完。
下一篇:网络和Linux网络_6(应用层)HTTPS协议(加密解密+中间人攻击+证书)。