🔥博客简介:开了几个专栏,针对 Linux 和 rtos 系统,嵌入式开发和音视频开发,结合多年工作经验,跟大家分享交流嵌入式软硬件技术、音视频技术的干货。
✍️系列专栏:C/C++、Linux、rtos、嵌入式开发、流媒体、数据结构、网络协议、开源库、CMake、Makefile、架构设计模式等。
文章目录
《HTTP总结与全梳理》系列,主要是针对HTTP协议知识点进行全面的梳理总结。此篇文章主要对HTTP协议进行详解,并列举一个简单的示例demo,该系列后续文章包括HTTP 开发调试方法抓包方法、HTTP数据上传,HTTP数据下载等文章。
一、前言
嵌入式开发工作中我们常用到一些网络协议,本章我们总结http协议,后续文章我们会陆续总结https等网络协议。在总结http协议之前,我们先熟悉一下什么是超文本。
超文本的概念最早由Ted Nelson在1960年代提出,并在1990年代因万维网(World Wide Web)的发展而广为人知。
超文本(Hypertext)是一种通过HTML(超文本标记语言)组织和呈现信息的方式。允许读者通过点击或选择文本中的链接来跳转到相关的信息或内容,这种链接通常称为"超链接"(Hyperlinks)。
在超文本中,内容可以包含文本、图像、视频等多种媒体形式,并且这些内容之间可以通过超链接进行连接和跳转。这种非线性的信息组织方式使得用户能够灵活地浏览和查找信息,而不是按照传统的线性文本顺序进行阅读。
例如,在一个网页中,您会发现许多链接,当您点击一个链接时,它会带您到另一个相关的页面或内容,这就是典型的超文本应用。
二、HTTP 概述
1. HTTP 发展历程
HTTP的发展可以追溯到1990年代初期。它经历了几个主要版本的演变,每个版本都带来了显著的改进和优化。以下是HTTP历次发展的详细历程:
1. HTTP/0.9(1991年)
HTTP的第一个版本是在1991年由WWW之父蒂姆·伯纳斯-李(Tim Berners-Lee)提出的。这是一个极其简陋的协议,只有一个GET方法,用于请求服务器上的HTML文档。它没有HTTP头部,因此无法传输元数据,所有的响应都被视为纯文本。该版本缺少如下现代HTTP特性:
方法: 只有一个GET,没有POST、PUT等。
协议头: 没有请求和响应头。
状态码: 没有标准化的状态码。2. HTTP/1.0(1996年)
HTTP/1.0是第一个广泛使用的版本,并在1996年通过RFC 1945标准化。它引入了以下显著改进:
更多的方法: 除了GET,新增了POST和HEAD方法。
协议头: 引入了HTTP头部信息,使请求和响应更加灵活与可扩展。
状态码: 标准化了状态码,如200 OK、404 Not Found等。
多媒体支持: 支持图片、文件下载等多种媒体类型的传输。3. HTTP/1.1(1997年)
HTTP/1.1首次在RFC 2068中定义,并在随后通过RFC 2616进行了修订和完善。它成为了至今最为广泛应用的HTTP版本,带来了多个关键特性和性能改进:
持久连接 :默认保持连接,允许多个请求和响应复用同一个TCP连接,减少了连接建立和关闭的开销。
分块传输编码: 支持服务器在文档生成的同时发送响应,从而实现动态内容的高效传输。
增强的缓存机制: 引入了强缓存控制(如Cache-Control头)和条件请求(如If-Modified-Since头)。
Host头: 支持在一个服务器上托管多个域名,从而实现虚拟主机功能。
管道化: 允许在同一连接上并行发送多个请求,而不用等待前一个请求的响应,但这个特性在实践中未得到广泛使用。4. HTTP/2.0(2015年)
HTTP/2由IETF在2015年发布(RFC 7540),其设计目标是改善HTTP/1.x的性能瓶颈,主要通过以下技术实现:
二进制分帧: 使用二进制协议替代文本协议,提升了解析速度和传输效率。
多路复用: 在同一个TCP连接上并发地传输多个请求和响应,减少了延迟并提高了网络利用率。
头部压缩: 采用HPACK算法压缩HTTP头部,减少头部信息的传输开销。
服务器推送: 允许服务器主动向客户端推送资源,以提前加载页面所需资源,提升响应速度。5. HTTP/3(2022 还在完善中)
HTTP/3是最新一代的HTTP协议,目前正处在标准化过程当中,部分草案已经由IETF发布。HTTP/3的关键区别在于它基于QUIC(Quick UDP Internet Connections)协议,而不是传统的TCP。QUIC是Google开发的一种基于UDP的协议,旨在解决TCP的延迟和连接可靠性问题。HTTP/3的主要特点包括:
基于UDP的QUIC协议 :通过使用UDP来替代TCP,减少了连接建立时间和抗网络抖动的能力。
快速重连: 在网络改变或中断时,实现快速重连而不需要重新建立连接。
改进的流控制 :在HTTP/3中,多个流之间完全独立,减少了拥塞和阻塞问题。未来的发展
HTTP仍在不断演进中,以适应日益增长的互联网需求和新兴的网络技术。每个新版本的发布都致力于解决前一个版本中的缺陷和优化网络性能。
2. HTTP 简述
HTTP全称为 Hypertext Transfer Protocol,中文含义为"超文本传输协议"。 HTTP协议是一个基于请求/响应模式的应用层协议。
HTTP往往是基于TCP/IP协议传输数据,但也有HTTP是基于UDP协议来传输数据。通过上面讲述的HTTP发展历程可知,HTTP1.0、HTTP1.1、HTTP2.0均是基于TCP实现,而HTTP3则是基于UDP实现。
HTTP协议定义了客户端与服务器之间的通信规范,以实现对各种资源(如文本、图像、音频、视频等)的传输和访问。通常,由HTTP客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如"HTTP/1.1 200 OK",以及返回的内容,如请求的文件、错误消息、或者其它信息。
OSI参考模型和TCP/IP分层模型示意图为:
三、HTTP 的特性
无状态
HTTP是无状态协议,这意味着每个请求都是独立的,服务器不保留任何有关之前请求的信息。这种设计使得HTTP具有很高的灵活性和可扩展性,但也带来了会话管理的挑战,通常通过Cookies、Session等机制实现。
无连接
HTTP的"无连接"特性指的是每次HTTP请求与响应之间并不要求客户端和服务器之间保持长时间的通信连接。具体来说,客户端在发送请求并接收到服务器的响应后,连接就会被立即关闭,这样的设计使得每个请求/响应对都独立于其他请求/响应对。
虽然HTTP协议本身是无连接的,但其具体实现上有所不同:
短连接短连接(短链接)是HTTP/1.0协议默认使用的一种连接方式。特性如下:
- 每个请求/响应对结束后立刻关闭连接: 在HTTP/1.0中,客户端和服务器的每次请求/响应对都会建立一个新的TCP连接,并在请求完成后关闭连接。
- 高连接开销: 由于每次请求都要重新建立和关闭连接,短连接会导致较高的连接开销。
- 适用于低频率的请求: 短连接适用于那些不频繁发送请求的情况,但当请求频繁时,会带来额外的负载和延时。
长连接
长连接(长链接)是HTTP/1.1及以上版本默认支持和推荐的连接方式。长连接并不是一直保持连接不断开,而是在一定的时间范围内保持连接,以便可以复用已有的连接来处理多个请求和响应。特性如下:
- 连接保持一段时间: 长连接在请求完成后不会立即关闭,可以保持TCP连接一定时间(通常这个时间由服务器设置的一段超时时间决定)。在此连接期间,客户端可以继续发送多个请求。HTTP/1.1及以上版本默认使用长连接,Connection: keep-alive不需要显式指定。如果不使用长连接,需要明确指定Connection: close不使用长连接。
- 减少连接开销: 由于减少了重复的TCP握手和连接建立、关闭的过程,长连接显著降低了连接开销,提高了请求效率和响应速度。
- 支持多个请求/响应对: 在一个长连接上,客户端可以发送多个请求,每个请求可以在不同的时候被发送。例如,浏览器可以在一个长连接上不断地获取网页上的多个资源(图片、CSS文件、JavaScript等)。
四、HTTP 工作流程
HTTP协议采用了请求/响应模型,定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
以下是 HTTP 请求/响应的步骤:
1. 客户端连接到Web服务器 一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.baidu.com。
2. 发送HTTP请求 通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。
3. 服务器接受请求并返回HTTP响应 Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
4. 释放连接TCP连接 若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
5. 客户端浏览器解析HTML内容。客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应。
五、HTTP 请求方法
方法 | 描述 |
---|---|
GET | 用于获取服务器上的资源。当客户端发送GET请求时,服务器将返回相应请求的资源 ,GET请求的参数会附在URL后面,因此可能会受到长度限制 |
HEAD | 类似于GET请求,但只返回资源的报头信息,而不返回实际的资源内容。HEAD请求常用于检查资源的元数据,例如最后的修改时间、ETag等 |
POST | 用于向服务器提交数据,并请求服务器进行处理。POST请求通常用于向服务器发送表单数据、上传文件、进行用户登录等操作。相对于GET请求,POST请求没有长度限制,并且不会将参数暴露在URL中,而是放在请求体中进行传输 |
PUT | 用于创建或更新服务器上的资源。PUT请求类似于POST请求,但PUT请求要求在指定的URL上创建或更新资源。如果资源已存在,则会进行更新;如果资源不存在,则会进行创建 |
DELETE | 用于删除服务器上的资源。DELETE请求会删除指定URL对应的资源。删除后,相应的URL将不再存在。 |
CONNECT | 主要用于代理服务器,指示代理服务器与目标服务器建立隧道连接 |
OPTIONS | 用于获取目标URL所支持的请求方法。服务器收到OPTIONS请求后,会返回该URL所支持的请求方法列表 |
TRACE | 用于将请求上的报头信息回显给客户端,用于调试或诊断信息 |
在实际嵌入式开发中,上述方法大部分都使用不到,最常见的HTTP方法是GET、PUT、POST。
六、HTTP 请求格式
客户端发送一个HTTP请求到服务器的请求消息格式包括:请求行、请求头、空行、请求体(请求数据)四部分组成
下图示例:
请求头里面的这些键值对含义,看下表:
请求头 | 作用 | 示例 |
---|---|---|
Host | 指定请求的目标主机的域名和可选的端口号 | Host: www.example.com |
User-Agent | 包含发出请求的浏览器或客户端的信息 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) |
Accept | 告知服务器客户端能够处理的内容类型 | Accept: text/html;image/apng |
Accept-Language | 指定客户端首选的自然语言 | Accept-Language: zh-CN |
Accept-Encoding | 指定客户端支持的内容编码方式(如压缩算法) | Accept-Encoding: gzip, deflate |
Cookie | 包含之前由服务器发送并存储在客户端的cookie | Cookie: sessionId=abc123; logged_in=true |
Referer | 告知服务器请求来源(引荐资源)的URL | Referer: https://www.google.com/ |
Connection | 控制连接的处理 | Connection: keep-alive |
Content-Type | 告知服务器请求主体包含的数据的类型 | Content-Type: application/json |
Content-Length | 表示请求主体的长度(以字节为单位) | Content-Length: 16 |
Authorization | 包含认证信息,用于身份验证 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
Cache-Control | 指示请求和响应的缓存机制 | Cache-Control: no-cache |
Range | 请求特定范围的资源部分,它是断点续传的基础 | Range: bytes=500-999 |
需要注意的是,这个Content-Length表示请求体里面的数据长度,即"name=ueno&age=37"长度正好为16
七、HTTP 响应格式
HTTP响应也由四部分组成,分别是:响应行、响应头、空行和响应体(响应正文)。
下图示例:
响应头里面的这些键值对含义,看下表:
响应头 | 作用 | 示例 |
---|---|---|
Date | 指明响应被发送的日期和时间 | Date: Wed, 21 Oct 2023 07:28:00 GMT |
Server | 包含处理请求的服务器软件的信息 | Server: Apache/2.4.41 (Ubuntu) |
Content-Type | 指示响应主体的数据类型 | Content-Type: text/html; charset=UTF-8 |
Content-Length | 表示响应主体的长度(以字节为单位 | Content-Length: 169 |
Content-Encoding | 指示服务器对响应主体使用的编码方式 | Content-Encoding: gzip |
Last-Modified | 表示所请求资源的最后修改日期和时间 | Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT |
ETag | 提供资源的特定版本标识符,用于缓存优化 | ETag: "5d8c72a5edda3f30b6cf15e66f3591b2" |
Accept-Ranges | 指示服务器是否支持范围请求及范围单位 | Accept-Ranges: bytes |
Cache-Control | 告诉客户端如何缓存响应 | Cache-Control: no-cache, no-store, must-revalidate |
Expires | 指定响应过期的日期和时间,过期后需要重新请求 | Expires: Wed, 21 Oct 2024 07:28:00 GMT |
Set-Cookie | 设置HTTP cookie,发送到客户端以存储信息 | Set-Cookie: sessionId=abc123; Path=/; HttpOnly |
Location | 用于重定向响应,告知客户端重新请求的位置(URL) | Location: https://www.example.com/newpage |
Connection | 控制当前连接的管理 | Connection: keep-alive |
Transfer-Encoding | 指示响应主体的传输编码方式,用于分块传输 | Transfer-Encoding: chunked |
Retry-After | 告知客户端多久之后可以再次请求资源,通常用于503状态码 | Retry-After: 120 |
需要注意的是,所有HTTP响应的第一行都是状态行,即响应行就是状态行。状态行内容依次是当前HTTP版本号,3位数字组成的状态码,以及描述状态的短语,彼此由空格分隔。状态码的第一个数字代表当前响应的类型,如下图:
关于HTTP详细的状态码可查看开发文档 HTTP 响应状态码
关于HTTP更多内容,详细可查看开发文档 HTTP 开发文档
八、HTTP 的优缺点
以下是HTTP的优点和缺点:
优点:
- 简单且易于实现: HTTP协议结构简单,快速且易于实现,对于开发和调试非常方便。
- 广泛支持: 几乎所有的现代互联网应用和设备都支持HTTP,使其成为一个非常通用的协议。
- 无状态性: HTTP是无状态协议,这意味着每个请求都是独立的,不依赖于前面的请求。这简化了服务器设计和应用逻辑。
- 兼容性好: HTTP可以使用不同的传输层(如TCP、QUIC等),这使得它在不同的网络环境中具有良好的兼容性。
- 支持多种媒体类型: HTTP可以传输各种类型的数据,如HTML文档、图像、视频、JSON、XML等。
缺点:
- 无状态性: 尽管无状态性使得HTTP简单易实现,但它也导致了每次请求/响应之间不能保留客户端的状态,需要使用cookie、session等技术来实现状态保存。
- 安全性较低: 原生的HTTP不加密,明文传输,数据在传输过程中容易被截取和篡改。HTTPS(HTTP + SSL/TLS)可以解决这个问题,但增加了复杂度和性能开销。
- 性能问题: HTTP/1.1的请求/响应模型使得每次请求都要建立新的TCP连接或复用已有连接,但仍会带来延迟和开销。在HTTP/2和HTTP/3中有所改进,但也增加了复杂度。
- 冗长的头部: HTTP协议的请求和响应中,头部信息较为冗长,占用了额外的带宽。
- 资源误用: 无状态性和握手开销使得HTTP在大量短连接情况下,比较消耗服务器资源。
九、示例
示例通过http的GET方法连接www.example.com,并获取其响应打印出来。www.example.com的界面如下:
1、目录结构
bash
project
└── http.c
2、http.c源码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#define BUFFER_SIZE 4096
void error_handling(const char *message)
{
perror(message);
exit(EXIT_FAILURE);
}
int main()
{
const char *hostname = "www.example.com";
const char *portnum = "80";
const char *path = "/";
int sockfd;
struct addrinfo hints, *res;
char buffer[BUFFER_SIZE];
int bytes_received;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(hostname, portnum, &hints, &res) != 0)
{
error_handling("getaddrinfo() error");
}
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd == -1)
{
error_handling("socket() error");
}
if (connect(sockfd, res->ai_addr, res->ai_addrlen) == -1)
{
error_handling("connect() error");
}
freeaddrinfo(res);
snprintf(buffer, sizeof(buffer),
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Connection: close\r\n"
"\r\n",
path, hostname);
printf("*********Request*********\n");
printf("%s", buffer);
if (write(sockfd, buffer, strlen(buffer)) == -1)
{
error_handling("write() error");
}
while ((bytes_received = read(sockfd, buffer, BUFFER_SIZE - 1)) > 0)
{
buffer[bytes_received] = '\0';
printf("*********Response*********\n");
printf("%s", buffer);
}
if (bytes_received == -1)
{
error_handling("read() error");
}
close(sockfd);
return 0;
}
3、编译运行
在project目录,执行命令,生成可执行文件app
bash
gcc -o app http.c
运行结果
请求报文:
响应报文: