目录
前言
本文主要带着各位一起了解我们应用层一种重要的协议------HTTP协议;看完本文,你会了解,URL是什么、HTTP协议的报文组成、HTTP协议部分字段、GET与POST方法;
1、认识URL
想必大家可能都认识URL,通常我们把这个成为网址,实际上其全名为统一资源定位符。下面,我来带着大家一起认识URL;
其中端口号可以省略,因为这是众所周知的了,比如http协议默认端口号为80,https协议默认端口号为443,关于域名,就是IP地址,域名通过域名解析后会转化成IP地址,一个域名只有一个IP地址,而一个IP地址可以对应多个域名;我们通过域名(IP)+ 端口号的方式找到全网唯一进程,然后接着我们通过端口号后路径+资源文件名可以找到全网唯一一份资源,url同时也可以携带参数,?后为参数,其中参数之间用&分隔开;
补充:
encode与uncode
在我们url中可能携带一些符号与中文,那么对于这种情况又是如何进行处理的呢?我们做如下实现,我们在浏览器搜索框中输入搜索abcde,然后查看URL;
注意:不同搜索引擎生成的URL连接也不同,这里我分别用微软的bing和百度来进行搜索,分别生成如下两个URL;
我们可以查看到我们的检索词条出现在了URL的参数上,对于bing来说是q=检索词,对于百度来说是wd=检索词;接着我们在搜索C++;bing搜索结果如下;
我们发现,q后面的检索词条变成了C%2B%2B,我们将2B看作十六进制数,然后转换成十进制数为43,我们对比ASCII表发现,正是+;
我们将这个过程称为URL的Encode,具体规则为将数字和特殊字符转成ASCII值,以两个十六进制数为一位,然后转成%+ASCII值;
所谓的Uncode也就是刚才那个过程的逆向过程,网上有很多的URL Encode与 Uncode工具,大家也可以尝试着试一试;下面贴出了其中一个;
2、HTTP请求与响应的报文格式
(1)请求格式
请求格式主要由四部分构成,首先是请求行,请求行有三个字段,每个字段都由空格划分,其中第一个字段为请求方法,常见的方法有post、get方法;接着是URL,表示请求的资源,最后一个为协议版本,表示客户端使用的协议版本号,如HTTP/1.0,该行结束后有一个\r\n 表示结束;接着是请求报头,请求报头由若干个key value结构构成,表示这次请求报文所携带的属性;接着是一行换行符,这个换行符是为了以便区分正文与报头部分内容;
有了上面的理解,我们是可以很容易区分报头部分和正文部分,但是我们应该如何完整提取正文部分呢?我们又不知道正文部分有多大,实际上,在我们请求属性中,有一个属性叫Content-Length,该字段标识请求正文的大小;这样我们就可以完整的提取出一个报文了,如Conten-Length: 1024;
(2)响应格式
响应报文的格式与请求报文格式也类似,由四部分组成,分别为状态行、响应报头、空行以及相应正文;其中状态行中也有三个字段,以空格分割,以\r\n为结尾;第一个字段为协议版本,表示服务器端的协议版本号,第二个字段状态码,第三个字段为状态描述,如200,对应状态描述为 OK;接着又是若干个KeyValue结构的响应报头,表示该报文的若干属性;接着一行空行一分割报头与响应正文内容;最后就是响应正文,用来保存一些反馈给客户端的资源;
百闻不如一见,今天就带着各位写一个简单的小程序来观察HTTP协议;
3、HTTP测试代码
我们这个demo代码有如下文件,我们简单的就想看一个HTTP请求是什么样子,我们通过浏览器发送HTTP请求,我们收到请求后打印出来即可;
下面展示我们所写代码;
cpp
// Makefile文件
httpserver:HttpServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f httpserver
cpp
// Err.hpp文件
#pragma once
#define ARGS_ERR 1 // 参数错误
#define SOCK_ERR 2 // 创建套接字失败
#define BIND_ERR 3 // 绑定失败
#define LISTEN_ERR 4 // 监听失败
#define ACCEPT_ERR 5 // 接受连接失败
#define CONNECT_ERR 6 // 连接失败
cpp
// Log.hpp文件
#pragma once
#include <cstdarg>
#include <cstdio>
#include <ctime>
#define Info 1
#define Debug 2
#define Warning 3
#define Error 4
#define Fatal 5
const char *getLevel(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
}
return "Unknow";
}
bool LogMessage(int level, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
char buf[1024];
int n = vsprintf(buf, fmt, args);
if (n < 0)
return false;
// 获取时间
time_t timestamp = time(nullptr);
struct tm *cur_time = localtime(×tamp);
// 打印输出
printf("[%s][%d-%d-%d %d:%d:%d] %s\n", getLevel(level), 1900 + cur_time->tm_year, cur_time->tm_mon,\
cur_time->tm_mday, cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, buf);
// 往文件打印
// FILE* pf = fopen("./Log/server.log", "a");
// fprintf(pf, "[%s][%d-%d-%d %d:%d:%d] %s\n", getLevel(level), 1900 + cur_time->tm_year, cur_time->tm_mon,\
// cur_time->tm_mday, cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, buf);
// fclose(pf);
return true;
}
cpp
// Sock.hpp文件
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"
class Sock
{
static const int g_backlog = 10;
public:
Sock(){}
int Socket()
{
int listenSock = socket(AF_INET, SOCK_STREAM, 0);
if(listenSock < 0)
{
LogMessage(Fatal, "create socket fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(SOCK_ERR);
}
LogMessage(Info, "create socket success, listenSock:%d", listenSock);
return listenSock;
}
int Bind(int listenSock, const std::string& ip, uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(ip.c_str());
local.sin_port = htons(port);
int n = bind(listenSock, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LogMessage(Fatal, "bind sock fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(BIND_ERR);
}
LogMessage(Info, "bind socket success!");
return n;
}
int Listen(int listenSock)
{
int n = listen(listenSock, g_backlog);
if(n < 0)
{
LogMessage(Fatal, "listen sock fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(LISTEN_ERR);
}
LogMessage(Info, "listen socket success!");
return n;
}
int Accept(int listenSock, std::string* ip, uint16_t* port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sockfd = accept(listenSock, (struct sockaddr*)&peer, &len);
if(sockfd < 0) return -1;
if(ip)
*ip = inet_ntoa(peer.sin_addr);
if(port)
*port = ntohs(peer.sin_port);
LogMessage(Info, "Accept socket success, sockfd:%d", sockfd);
return sockfd;
}
int Connect(int sockfd, const std::string ip, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
LogMessage(Info, "connect socket success!");
return n;
}
};
cpp
// HttpServer.hpp文件
#pragma once
#include <functional>
#include <unistd.h>
#include <signal.h>
#include "Sock.hpp"
#include "Log.hpp"
using func_t = std::function<void(int)>;
class HttpServer
{
public:
HttpServer(uint16_t port, func_t func, const std::string& ip = "0.0.0.0")
:_func(func)
{
_listenSock = _sock.Socket();
_sock.Bind(_listenSock, ip, port);
_sock.Listen(_listenSock);
}
void start()
{
std::cout << "start begin\n";
signal(SIGCHLD, SIG_IGN);
while(true)
{
uint16_t port;
std::string ip;
int sockfd = _sock.Accept(_listenSock, &ip, &port);
if(sockfd == -1)
{
LogMessage(Error, "Accept fail, errno:%d, errstr:%s", errno, strerror(errno));
continue;
}
if(fork() == 0)
{
// child
close(_listenSock);
_func(sockfd);
close(sockfd);
exit(0);
}
// father
close(sockfd);
}
}
private:
Sock _sock;
int _listenSock;
func_t _func; // 回调函数
};
cpp
// HttpServer.cc文件
#include <iostream>
#include <vector>
#include <fstream>
#include <string>
#include <memory>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include "HttpServer.hpp"
#include "Err.hpp"
#include "Utility.hpp"
#define ROOT "./WWWROOT"
static void Usage(const std::string& str)
{
std::cout << "Usage:\n" << str << " serverPort" << std::endl;
}
void transaction(int sockfd)
{
char buf[1024];
// 暂不考虑不能获取一个完整报文
int n = recv(sockfd, buf, sizeof(buf) - 1, 0);
if(n < 0)
{
LogMessage(Error, "recv fail, errno:%d, errstr:%s", errno, strerror(errno));
return ;
}
buf[n] = 0;
// 打印请求
std::cout << buf << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(ARGS_ERR);
}
std::unique_ptr<HttpServer> phs(new HttpServer(atoi(argv[1]), transaction));
phs->start();
return 0;
}
编译上述代码后我们运行我们的服务器并设置端口为8888,接着在浏览器中输入我们的IP:端口号,接着我们会发现确实会打印,如下所示;
我们发现请求中中的URL连接为反斜杠,这是因为我们在浏览器中未输入任何文件资源路径,因此表示默认访问web根目录,所以显示反斜杠;再仔细观察,发现我们使用的是HTTP/1.1协议,且我们使用get请求方法;接着是一个请求报头,存放都是本报文的属性;
4、HTTP的方法
前面我们提到的get与post是HTTP方法中的一种,接下来我来详细介绍一些HTTP常见方法;
get:获取资源;post:上传资源;
head:获取报文头部
我们上网无非两个目的,一个是获取网络资源,通过这种我们采用get方法,另一个是将我们的数据资源上传,对于这种情况我们既使用get方法,又使用post方法,那么这两种方法到底有什么区别呢?
(1)get与post的区别
我们对上述代码进行拓展,当客户端访问服务端时,我们默认返回首页资源文件,通常这个首页资源文件未HTML文件,如下所示;
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
</head>
<body>
<h1>这是首页</h1>
<form action="./page1.html" method="get">
User:<input type="text" name="User">
<br>
Password:<input type="password", name="password">
<br>
<input type="submit", name="提交">
</form>
</body>
</html>
**注意:**上述文件,我们存在一个专门的目录下,这个目录我们便称之为web根目录,如下所示;
我们再添加一个文件Utility.hpp,该文件放着一些比较实用的函数,目前有切割字符串,如下所示;
cpp
// Utility.hpp文件
#pragma once
#include <vector>
#include <string>
class Utility
{
public:
static void cutString(const std::string& str, const std::string& sep, std::vector<std::string>* v)
{
size_t start = 0;
size_t pos = 0;
while(true)
{
pos = str.find(sep, start);
if(pos == std::string::npos)
break;
v->push_back(str.substr(start, pos - start));
start = pos + sep.size();
}
if(start < str.size())
{
v->push_back(str.substr(start, pos - start));
}
}
};
我们再对业务处理函数进行一些修改,我们收到HTTP请求后,提取访问的资源路径,并找到对应资源文件,返回资源文件内容;
我们果然获取了一张网页,我们可以按下F12,返回的正是我们缩写的HTML源码;我们点击提交按钮,如下所示;
我们发现我们的刚才所提交的数据都完整的放在了URL中,我们将方法改为post,再次提交查看结果;如下所示;
我们发现,此时我们提交的数据并没有放在URL中,那么我们提交的数据又放在了哪里呢?我们使用post方法是否更加安全呢?
对于更加安全这种说法我并不是很赞同,我认为post方法相对于get方法,更加私密,post提交的数据并不会放在URL中,而会放在请求正文中,如下所示;
5、HTTP的状态码
关于HTTP的状态码,我们仅仅需要做一些了解即可;常见的有如下五种开头的;
1xx:信息性状态码
2xx:成功状态码
3xx:重定向状态码
4xx:客户端错误状态码
5xx:服务器错误状态码
这里介绍一些常见状态码;
状态码 对应描述 意义
200 OK 成功运行
301 Moved Permanently 永久移动
307 Temporary Redirect 临时移动
404 Not Found 找不到对应资源
6、HTTP报头属性
这里主要介绍一些比较常见的属性,如下所示;
Content-Length:正文大小;
Content-Type:正文资源的类型(比如HTML/Png/Jpg/CSS等);
Host:客户端请求资源的IP与端口号;
User-Agent:声明用户的操作系统和浏览器版本信息;
Reffer:从哪个页面跳转过来的;
location:搭配3xx字段,告诉客户端接下来应该去哪;
Cookie:用于客户端存储少量信息,通常用于会话功能的实现;
这里我想着重介绍其中Cookie这个字段;该字段用于会话管理,要理解会话管理首先要知道我们为什么要进行会话管理?关于为什么要进行会话管理首先我们得清楚,HTTP协议的特点,HTTP协议具有无连接,无状态的特点;所谓无连接,就是HTTP请求与响应过程中,并没有建立连接;可有的小伙伴就疑惑了,HTTP不是基于TCP协议吗?怎么会没有连接呢?其实,TCP的连接是基于传输层的连接,跟我们应用层HTTP协议没有任何关系;关于HTTP的无状态就是我们HTTP协议并不会记录上一次请求或响应,也不会知道对方是否曾经向我发送过请求;
在聊聊我们的会话管理,我们用一个例子来理解我们的会话管理;例如我们的CSDN,当我们进行登录后,即使我们关闭浏览器后,我们再打开CSDN官网,我们会发现我们仍然是登录状态,这就是所谓的会话管理;若没有会话管理,则我们每次访问一个需要登录的资源时都需要重新登录;因为HTTP是无状态的,服务端并不知道你的信息,因此无法知道你是否有权限访问对应资源;因此我们每次请求对应资源都要进行身份验证,这样对用户来说,非常影响体验,因此早期诞生了如下方案;
在我们输入账号和密码之前,本地客户端会生成一个cookie文件来保存账号和密码(也有可能保存在内存中),接着每次当我们发送http请求时,客户端首先会从cookie文件中获取账号和密码,这样每次请求就都不用用户手动进行身份验证了;
可是这样也同时带来了不少的安全隐患,若用户的设备被植入了木马病毒,此时,黑客可以轻松获取cookie文件中的内容,此时用户的私人信息就会遭受泄露;这样是我们所不能容忍的;因此又有了另一种方案,如下所示;
当我们进行账号密码登录时,我们的客户端会发送http请求,当服务端收到http请求后,对用户身份进行验证,若验证通过,则会生成一个唯一session id,服务端会用该id生成一个类,并再里面保存该用户的信息,做完这一系列工作后,再将这个sid返回给客户端,客户端将这个sid保存在本地的cookie文件或内存中,从此以后客户端都用这个sid进行访问;
这时肯定有人就有疑惑了,那这个sid不同样也可以被盗取吗?若黑客将我们的sid盗取不同样也可以登录我们的账户吗?确实如此,当时即便登录了我们的账户,也无法知道我们的账户密码等私密信息,因此该信息保存在服务端,而且,实际上,服务端也可以获取客户端登录位置,若登录位置有异常通常会给客户端发送信息,想必大家也会见过这种现象吧,例如企鹅;
说了这么多,不是一次实操,我们可以通过在响应中设置set-Cookie属性,我们再查看之后的访问中,客户端是否会发送Cookie信息(客户端为浏览器);