您现在的位置是:首页 >技术杂谈 >【Linux】一文读懂HTTP协议:从原理到应用网站首页技术杂谈
【Linux】一文读懂HTTP协议:从原理到应用
🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
👉HTTP协议👈
在网络版计算器一文中,我们通过手动地定制协议来加深对协议的认识。虽然我说应用层协议是由程序猿自己定,但实际上已经有大佬们定义了一些现成的、又非常好用的应用层协议,供我们直接参考使用,其中 HTTP 协议就是其中之一。
什么是HTTP协议
HTTP(超文本传输协议)是一种应用层协议,用于在客户端和服务器之间传输超文本。它是 Web 的基础,可用于检索和提交信息,例如 HTML 文件、图像、样式表等。HTTP 是无状态的,也就是说每个请求都是独立的,服务器不会存储任何有关先前请求的信息。HTTP 协议常用于浏览器与 Web 服务器之间的通信。
认识URL
平时我们俗称的网址,其实就是说的 URL。URL,全称是Uniform Resource Locator,即统一资源定位符,它是互联网上用来定位资源的标准方式。URL 是由多个部分组成,通常包含以下信息:
- 协议:例如 http、https、ftp、file 等。
- 域名:指向某个 IP 地址的可读性更好的别名,例如 www.example.com。
- 端口:应用程序使用的端口号。我们所请求的网络服务对应的端口号都是众所周知的,如 HTTP 服务的默认端口号是80,而 HTTPS 服务的默认端口号是443。
- 路径:资源在服务器上的路径。
- 参数:向脚本传递参数。
- 锚点:页面内部的位置。
URL 通常被用于定位 Web 页面、图像、视频、音频、文件等网络资源。它是一种标准化的格式,可以在浏览器中输入 URL,以访问特定的网络资源。
我们平时上网的目的无非两种:一、我们想要获取资源,二、我们想要上传资源。假设我们现在想要获取资源,在我们没有获取到资源之前,这个资源在服务器上。而一个服务器上可能存在多种资源(本质就是文件),那服务器是如何找到我们需要的资源,并将该资源通过网络交给我们呢?
其实我们在向服务器请求资源时,就会在 URL 内部带上资源所在的路径,这样服务器就可以通过该路径找到我们所需要的资源并交给我们。
urlencode 和 urldecode
在 URL 中,某些字符具有特殊含义。这些字符包括保留字符(如 /、?、& 等)和非 ASCII 字符(如中文、日文等)。因此,如果要在 URL 中包含这些字符,需要将它们进行编码。URL 编码是一种将 URL 中的特殊字符转换为标准 ASCII 字符的方法。
urlencode 是 URL 编码的过程,它将 URL 中的非 ASCII 字符和保留字符进行编码,以便在 URL 中安全地传输。具体来说,urlencode 会将非 ASCII 字符转换成它们的 UTF-8 编码,然后将每个字节转换成 %XX 的形式,其中 XX 是两个十六进制数字表示的字节值。转换的规则:将需要转码的字符转为十六进制,然后从右到左,取4位(不足4位直接处理),每两位做一位,前面加上 %,编码成 %XX。
例如,“hello, 世界” 在进行 urlencode 之后会被转换为 “hello%2C%20%E4%B8%96%E7%95%8C”。
urldecode 是将 URL 编码的字符串还原为原始字符串的过程。它将 %XX 形式的编码转换为相应的字节,并将 UTF-8 编码的字节序列还原为原始的 Unicode 字符。
HTTP协议格式
在 HTTP 协议中,客户端向服务器发送请求,服务器接收并响应请求。请求和响应都有特定的格式。
HTTP请求报文格式
请求通常由请求行、请求报头、空行和请求正文四部分组成。请求行包括请求方法、请求 URL 和 HTTP 协议版本;请求报头是一组键值对,用来描述客户端发送的请求的一些信息,例如请求的 Host、User-Agent 等。;请求正文是可选的,可以没有,通常只有在请求方法为 POST 或 PUT 时才会有请求体,用于传输客户端提交的数据。
注:请求中的 HTTP 协议版本是客户端告知服务端,客户端所采用的的 HTTP 协议版本。
HTTP相应报文格式
响应报文也由三部分组成:状态行、响应报头、空行和响应正文。状态行包括 HTTP 协议版本、状态码和状态码描述。响应报头和请求报头类似,也是一组键值对,用于描述服务器发送的响应的一些信息,例如响应的 Content-Type、Content-Length 等。响应正文用于传输服务器返回的数据。
注:响应中的 HTTP 协议版本是服务端告知客户端,服务端所采用的 HTTP 协议版本。
我们知道,每一层协议都需要考虑封装和解包的问题,也就是如何区分报头和有效载荷(正文)的问题?那 HTTP 是如何区分报头和有效载荷的呢?很明显,HTTP 是通过 (区分一行的内容) 和空行来区分报头和有效载荷的。
现在可以将报头和有效载荷区分开来了,那如何得知有效载荷的大小呢?如果有效载荷存在,那么报头中会有一个 Content-Length 属性来标识有效载荷的大小。
HTTP Demo
Log.hpp
#pragma once
#include <cstdio>
#include <cstdarg>
#include <string>
#include <iostream>
#include <ctime>
// 日志等级
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOGFILE "./Calculate.log"
const char* levelMap[] =
{
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
void logMessage(int level, const char* format, ...)
{
// 只有定义了DEBUG_SHOW,才会打印debug信息
// 利用命令行来定义即可,如-D DEBUG_SHOW
#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] ", levelMap[level], timestamp);
char logBuffer[1024]; // 自定义部分
va_list args; // va_list就是char*的别名
va_start(args, format); // va_start是宏函数,让args指向参数列表的第一个位置
// vprintf(format, args); // 以format形式向显示器上打印参数列表
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args); // va_end将args弄成nullptr
FILE *fp = fopen(LOGFILE, "a");
// printf("%s%s
", stdBuffer, logBuffer);
fprintf(fp, "%s%s
", stdBuffer, logBuffer);
fclose(fp);
}
Sock.hpp
#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string>
#include <cstring>
#include "Log.hpp"
class Sock
{
private:
const static int backlog = 20;
public:
Sock() {}
// 返回值是创建的套接字
int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
logMessage(FATAL, "Create Socket Error! Errno:%d Strerror:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "Create Socket Success! Socket:%d", sock);
return sock;
}
// 绑定端口号
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);
local.sin_addr.s_addr = inet_addr(ip.c_str());
if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "Bind Error! Errno:%d Strerror:%s", errno, strerror(errno));
exit(3);
}
}
// 将套接字设置为监听套接字
void Listen(int listenSock)
{
if (listen(listenSock, backlog) < 0)
{
logMessage(FATAL, "Listen Error! Errno:%d Strerror:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "Init Server Success!");
}
// 接收链接,返回值是为该连接服务的套接字
// ip和port是输出型参数,返回客户端的ip和port
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(FATAL, "Accept Error! Errno:%d Strerror:%s", errno, strerror(errno));
return -1;
}
if (ip)
*ip = inet_ntoa(src.sin_addr);
if (port)
*port = ntohs(src.sin_port);
return serviceSock;
}
// 发起连接
bool Connet(int sock, const std::string &serverIP, const int16_t &serverPort)
{
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_pton(AF_INET, serverIP.c_str(), &server.sin_addr);
if (connect(sock, (struct sockaddr *)&server, sizeof server) == 0)
return true;
else
return false;
}
~Sock() {}
};
HttpServer.hpp
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include "Sock.hpp"
using func_t = std::function<void(int)>;
class HttpServer
{
public:
HttpServer(const uint16_t& port, func_t func)
: _port(port)
, _func(func)
{
_listenSock = _sock.Socket();
_sock.Bind(_listenSock, _port);
_sock.Listen(_listenSock);
}
~HttpServer()
{
if(_listenSock >= 0)
close(_listenSock);
}
void Start()
{
signal(SIGCHLD, SIG_IGN);
while(true)
{
std::string clientIP;
uint16_t clientPort;
int sockfd = _sock.Accept(_listenSock, &clientIP, &clientPort);
if(sockfd < 0) continue;
// 创建子进程去处理请求
if(fork() == 0)
{
close(_listenSock);
_func(sockfd);
close(sockfd);
exit(0);
}
}
}
private:
int _listenSock;
uint16_t _port;
Sock _sock;
func_t _func; // 回调函数
};
HttpServer.cc
#include <iostream>
#include <memory>
#include "HttpServer.hpp"
void Usage(const std::string proc)
{
std::cout << "
Usage" << proc << " Port" << std::endl;
}
void HandlerHttpRequest(int sock)
{
// 1、读取请求
char buffer[10240];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
// 将接收到的数据直接当成字符串进行打印
if(s > 0)
{
buffer[s] = '