Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)
Posted 呆呆兽学编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)相关的知识,希望对你有一定的参考价值。
⭐️ 本篇博客开始给大家介绍应用层的一个协议——HTTP协议,我会带大家先认识协议,再谈HTTP协议的格式、报头、状态码和方法等相关细节,我还会简单介绍一下在他基础之上扩增一层软件层的协议——HTTPS,它相比HTTP来说,要更安全一些。话不多说,进入今天主要内容~
目录
🌏认识协议
协议是一种“约定”,这种约定是双方都知道的。有了一致的约定,双方才能够正常地进行通信。协议在网络的第一篇博客中也提到过,协议是双方进行通信的基础,在网络通信中存在着各种协议,有了这些协议,网络的通信才能够正常运转。
今天我要用一个例子带大家认识协议——网络计算器。我结合上一篇博客的线程池版本的TCP服务器进行编写这个网络计算器。
大致过程如下:
- 客户端向服务端发送一个请求数据包
- 服务端将请求数据包进行解析,并且进行业务处理,然后返回一个响应数据包给客户端
- 客户端将响应数据包进行解析,得到计算结果
注意: 客户端将请求封装成一个数据包,该过程叫做序列化,服务端将请求数据包进行解析的过程叫做反序列化。目前市面上有json、xml等格式,都可以供程序员进行该操作。
协议定制:
- 请求数据包用一个结构体进行封装,里面有两个操作数和一个操作符
- 响应数据包也用一个结构体进行封装,里面有计算结果和状态码
协议的头文件如下:
/**Protocol.hpp**/
#pragma once
struct request
int _num1;
int _num2;
char _op;
request(int num1, int num2, char op)
:_num1(num1)
,_num2(num2)
,_op(op)
;
struct response
int _code;// 0 正常 1 除以0错误 2 操作符选择错误
int _result;// 结果
response(int code, int result)
:_code(code)
,_result(result)
;
客户端填充请求数据包: 客户端需要让用户输入两个操作数和一个操作符,然后填充好请求数据包(序列化),并且调用send
将请求数据包发送过去,然后reccv
接受服务端发送过来的响应数据包,并且进行解析(反序列化),分析出状态码和结果即可,代码如下(客户端发起请求部分的代码,服务器创建和上篇博客代码一样):
void Request()
std::string msg;
while (1)
request rq(0, 0, 0);
std::cout << "Please Enter first num# ";
std::cin >> rq._num1;
std::cout << "Please Enter second num# ";
std::cin >> rq._num2;
std::cout << "Please Enter(format:num1(+-*/)num2)# ";
std::cin >> rq._op;
send(_sock, &rq, sizeof(rq), 0);
response rp(0, 0);
ssize_t size = recv(_sock, &rp, sizeof(rp), 0);
if (size <= 0)
std::cerr << "read error" << std::endl;
exit(-1);
if (rp._code == 1)
std::cout << "code: " << rp._code << std::endl;
//std::cout << "除零错误" << std::endl;
else if (rp._code == 2)
std::cout << "code: " << rp._code << std::endl;
//std::cout << "非法操作符" << std::endl;
else
std::cout << "code: " << rp._code << std::endl;
std::cout << "result: " << rp._result << std::endl;
服务端响应: 这里使用了线程池为每个服务端提供服务,这里只需要修改Task中的Run方法即可,也就是修改业务处理的部分,代码如下:
static void Service(std::string ip, int port, int sock)
while (1)
request rt(0, 0, 0);
ssize_t size = recv(sock, &rt, sizeof(rt), 0);// 阻塞方式读取
if (size > 0)
// 正常读取size字节的数据
std::cout << "[" << ip << "]:[" << port << "]# "<< rt._num1 << rt._op << rt._num2 << "=?"<< std::endl;
response rp(0, 0);
switch(rt._op)
case '+':
rp._result = rt._num1 + rt._num2;
break;
case '-':
rp._result = rt._num1 - rt._num2;
break;
case '*':
rp._result = rt._num1 * rt._num2;
break;
case '/':
if (rt._num2 == 0)
rp._code = 1;
else
rp._result = rt._num1 / rt._num2;
break;
default:
rp._code = 2;
break;
send(sock, &rp, sizeof(rp), 0);
else if (size == 0)
// 对端关闭
std::cout << "[" << ip << "]:[" << port << "]# close" << std::endl;
break;
else
// 出错
std::cerr << sock << "read error" << std::endl;
break;
close(sock);
std::cout << "service done" << std::endl;
struct Task
int _port;
std::string _ip;
int _sock;
Task(int port, std::string ip, int sock)
:_port(port)
,_ip(ip)
,_sock(sock)
void Run()
Service(_ip, _port, _sock);
;
代码运行效果如下:
正常运算:
错误处理: 状态码为1,表示发生除0错误
🌏HTTP协议
🌲介绍
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议
)的缩写,本质是基于TCP协议来进行文本设置完成协议通信。HTTP协议支持客户端——服务端模式,也就是请求与响应模式,且客户端需要以浏览器的方式访问服务端。
上网的大部分行为就是进行进程间通信,获取信息和发送信息:
- 把服务器资源拿到本地(下载资源、刷视频等)
- 把本地资源推送到服务器(搜索信息)
🌲URL、URI和URN
- URI: 统一资源标识符(Uniform Resource Identifier),用来标识资源的唯一性
- URN: 统一资源名称(Uniform Resource Name),用名字标识资源
- URL: 统一资源定位符(Uniform Resource Locator),给互联网上的每一个文件资源都贴上这样一个唯一标签,并且包含了资源位置信息和访问方式浏览器可以通过URL中的文件位置信息找到对应的资源文件。
结构:
- 协议方案名: 发起请求用到的协议
- 登录信息: 登录认证是用的的信息,通常被忽略
- 服务器地址: 访问资源所在的服务器的地址,也就是域名(字符串风格的)
- 端口号: 服务器绑定的端口号
- 文件路径: 访问资源在目标服务器上的位置信息
- 查询字符串: 查询信息
- 片段标识符: 对某些资源信息的描述与补充
三者关系与区别:
- URL是URI的一种,URL是URI的一种具体表现,包含了资源的位置信息和获取资源的方式
- URN是URI的一种,用特定命名字标识资源,但不包含访问方式。
🌲urlencode和urldecode
urlcode是一种编码方式,urldecode是一种解码方式。在客户端向服务器发起http请求时,为了方便http服务器识别,会将请求进行urlcode编码,同样地httpserver也会对这些字符进行urldecode解码。
编码规则:
- 数字,字母和连字符不作处理
- 中文字符和特殊字符会进行编码
- 要转码的字符会被转为16进制,从右至左取4位,每两位为以为,前面加上%,编码成%XY格式
代码实现如下:
string UrlEncode(const string& szToEncode)
string src = szToEncode;
char hex[] = "0123456789ABCDEF";
string dst;
for (size_t i = 0; i < src.size(); ++i)
unsigned char cc = src[i];
if (isascii(cc))
if (cc == ' ')
dst += "%20";
else
dst += cc;
else
unsigned char c = static_cast<unsigned char>(src[i]);
dst += '%';
dst += hex[c / 16];
dst += hex[c % 16];
return dst;
string UrlDecode(const string& szToDecode)
string result;
int hex = 0;
for (size_t i = 0; i < szToDecode.length(); ++i)
switch (szToDecode[i])
case '+':
result += ' ';
break;
case '%':
if (isxdigit(szToDecode[i + 1]) && isxdigit(szToDecode[i + 2]))
string hexStr = szToDecode.substr(i + 1, 2);
hex = strtol(hexStr.c_str(), 0, 16);
//字母和数字[0-9a-zA-Z]、一些特殊符号[$-_.+!*'(),] 、以及某些保留字[$&+,/:;=?@]
//可以不经过编码直接用于URL
if (!((hex >= 48 && hex <= 57) || //0-9
(hex >=97 && hex <= 122) || //a-z
(hex >=65 && hex <= 90) || //A-Z
//一些特殊符号及保留字[$-_.+!*'(),] [$&+,/:;=?@]
hex == 0x21 || hex == 0x24 || hex == 0x26 || hex == 0x27 || hex == 0x28 || hex == 0x29
|| hex == 0x2a || hex == 0x2b|| hex == 0x2c || hex == 0x2d || hex == 0x2e || hex == 0x2f
|| hex == 0x3A || hex == 0x3B|| hex == 0x3D || hex == 0x3f || hex == 0x40 || hex == 0x5f
))
result += char(hex);
i += 2;
else result += '%';
else
result += '%';
break;
default:
result += szToDecode[i];
break;
return result;
在线工具: https://tool.chinaz.com/tools/urlencode.aspx
🌲HTTP协议格式
🍯请求协议格式
我们可以编写一个简单的基于TCP协议的服务器,并且通过以浏览器作为客户端,对服务器发起请求,并把请求部分打印下来,代码如下:
#include <iostream>
#include <unistd.h>
#include <fstream>
#include <cstring>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <string>
using namespace std;
int main()
// 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
cerr << "socket creat fail" << endl;
exit(1);
cout << "socket creat succes, socket: " << listen_sock << endl;
// 绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8081);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
cerr << "bind fail" << endl;
exit(2);
cout << "bind success" << endl;
// 将套接字设置为监听状态
if (listen(listen_sock, 5) < 0)
cerr << "listen fail" << endl;
exit(3);
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
while (1)
// 获取连接
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0)
cerr << "accept error" << endl;
continue;
// 创建子进程
if (fork() == 0)
close(listen_sock);
if (fork() > 0)
exit(-1);
else
// 孙子进程
while (1)
char buf[1024];
ssize_t size = recv(sock, buf, sizeof(buf), 0);
if (size > 0)
buf[size] = 0;
cout << "#################################### http begin ############################################" << endl;
cout << buf << endl;
cout << "#################################### http end ############################################" << endl;
else if (size == 0)
cout << "close" << endl;
break;
else
cerr << "recv error" << endl;
close(sock);
exit(0);
close(sock);
waitpid(-1, nullptr, 0);
return 0;
下面是通过浏览器对我们的服务器发起请求:
请求内容:
上面是一次请求的内容,大体分为四个部分:请求行、请求报头、空行和请求正文
- 请求行: 请求方法+请求url+http协议版本
- 请求报头: 请求相关属性信息,以
key:value
的形式显示,且用空行分隔每一个属性信息 - 空行: 分隔报头和报文
- 请求正文: 允许为空,且如果请求方法为post,请求报头中会有
Content-Length
属性字段来标识请求正文的长度
回答几个小问题:
1、HTTP协议如何保证报头和有效载荷分离?
通过空行来分离报头和有效载荷,空行之后都是有效载荷的内容,且一般还会有
Content-Length
属性字段来标识正文的长度
2、服务端如何保证自己读取报头完毕?
循环读取,直到读到空行,就说明报头信息读取完毕
🍯响应协议格式
响应的格式如下:
- 响应行: 版本号+状态码+状态码解释
- 响应报头: 响应相关属性信息,以
key:value
的形式显示,且用空行分隔每一个属性信息 - 空行: 分隔报头和报文
- 响应正文: html、JSON、XML等格式的文本
为了更好地让大家看到响应的效果,这里再上面的代码的基础上增加一点响应的部分,其中包含两个字段:Content-Type(正文格式,数据类型)和Content-Length(正文长度) 两个字段
这里我们正文返回一个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>
<!-- 表单域 form 表单域action url 地址 method 提交方式 name 表单名称-->
<form method = "POST" name="注册表" action=以上是关于Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)的主要内容,如果未能解决你的问题,请参考以下文章
Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)
Linux从青铜到王者第二十篇:Linux网络基础第三篇之IP协议
Linux篇第十七篇——网络基础(概念+协议的认识+OSI七层模型+TCP/IP五层模型+网络传输的流程)