当前位置: 首页 > news >正文

HTTP协议(Linux视角)

目录

urlencode/urldecode

HTTP请求/响应

请求

响应

HTTP服务端实现

服务端和网页分离

HTTP报头常见字段

HTTP请求方法

GET/POST的区别

HTTP状态码

重定向状态码

长连接

会话保持

旧方案:Cookie技术

新方案:Session技术

Http工具

Postman

Fidler


对于应用层的协议,目前已经有很多大佬定制的成熟、好用的协议,例如HTTP(超文本传输协议)就是其中一个

每个网站都有网址(URL),下图简单认识URL的各部分

主机之间的通信需要通过唯一的IP,因此DNS服务器中存储着域名到IP的映射关系,以此找到对应的IP
这里的文件路径并不是从Linux的根目录开始,而是从指定的web根目录(可以是Linux中的任意一个目录)当做根目录

实际上URL中还需要传入端口号,只不过http统一规定为80,https统一规定为443,因此不指定也没关系
早期URL:

urlencode/urldecode

在URL中,如'/',':','@','?','#'等等特殊字符,都有特殊含义,如果URL的参数中出现了这种字符,就需要对其进行转义

例如,在b站中搜索 “CSDN” 的URL,其keyword参数就为CSDN

但如果搜索带有特殊字符或中文:

英文字母会原样输出,但特殊字符或中文就会被转移成%xx的格式

对于一个特殊字符,用%XX表示

对于一个汉字(以UTF-8编码为例),常用汉字用3个%XX(%XX%XX%XX)表示,生僻字用4个%XX(%XX%XX%XX%XX)表示

拿上面C++举例,由于'+'的ASCII为43,十六进制为2B,因此它的urlencode就是%2B

若是汉字,例如'中',它的UTF-8字节序列为E4 B8 AD,将每个字节前带上%,即得%E4%B8%AD

将特殊字符/汉字变为这种带%的十六进制序列的过程,就叫urlencode

而将这种16进制序列再转回原本字符的过程,就是urldecode

HTTP请求/响应

请求

既然是协议,就一定会有请求与响应
HTTP的请求分为四大部分:请求行、请求报头、空行、请求正文

  • 请求行:以空格为分隔的三列,分别表示请求方法(例如GET为获取数据,POST为提交数据)、URL(不包含http://和域名的非完整URL,只有路径和查询参数)、HTTP版本(例如http/1.1代表1.1版本,现已退役,主流网站/APP都为h2或h3,也就是http2或http3版本)
  • 请求报头:每行为以":"分隔的KV键值对(分隔符为':'+' '),即为要告诉服务器的元数据,当读到空行时,代表请求报头部分结束
  • 请求正文:承载要告诉服务端的核心数据,例如在登录时提交的用户名与密码。
    但并不是所有的HTTP请求都有正文,例如当请求方法为GET,即获取资源时,例如百度的搜索,参数通常拼接在 URL 后面,而不是放在正文里。

响应

当客户端的请求通过TCP连接传输过去后,当处理完后,服务端也会再发送回响应,大体格式和请求类似:

  • 响应行:以空格为分隔的三列,分别表示HTTP版本状态码(类似于程序的退出码,告诉对方成功与否。例如200代表成功,404代表找不到指定资源)、状态码描述(类似于strerror(),每个状态码都有对应的描述,例如200为ok,404为Not Found)
  • 响应报头:响应报头中的KV键值对属性决定了要如何接收/解压/解析/展示响应正文。

  • 响应正文:服务端返回给客户端的数据,若报头的Content-Type为text/html,就代表响应正文要当作HTML来解析,其他同理

HTTP怎么保证应用层读到的是一个完整的请求/响应呢?

请求/响应行和请求/响应报头可以通过while(行不为空)读取完,当跳出while时,就代表现在的光标在空行开头。
在请求/响应报头中,Content-Length字段的值为正文长度,通过读取该字段就可以正好读完请求/响应正文

HTTP是怎么序列化/反序列化的?

HTTP自己的序列化只是将这四部分作为字符串拼接起来再发送....嗯没错就这么简单(HTTP/1.1标准)

在Linux云服务器中写好服务端后,就可以用浏览器充当客户端进行通讯了:
只要在浏览器的URL处输入公网IP:端口号即可向服务端发送请求

请求行的 / 代表请求web根目录,http请求若没有请求指定的资源,web server会有默认的首页(例如index.html)

请求/响应都会发送http版本,这就交换了通信双方的版本。使用客户端的用户,因为更新 or 不更新的问题,客户端会有很多http版本,只要交换了通信双方版本,服务端就可以知道哪些功能是对方客户端协议有的

又或者说:HTTP 版本号的交换,就是通信双方在对齐“协议规范”。服务端通过版本号,就能精准掌握对方在“协议层面”支持哪些功能集合(如多路复用、分块传输等),从而决定采用哪种底层通信规则。而在该协议框架内具体“用不用”某个功能,则交由 Header 来灵活协商

User-Agent为客户端的信息,包括系统,安卓/WIn/Mac都可以显示出来,服务端就可以根据不同的系统返回不同的结果

HTTP服务端实现

当客户端向服务端发送请求后,服务端可以返回响应,这里以HTML为例

例如,可以用一个现成的HTML,利用wget命令下载资源,这里以哔哩哔哩首页为例

此时只需在服务端中构建响应并发送给客户端即可

//HttpServer.cpp: #include <iostream> #include <fstream> #include "HttpServer.hpp" #include "log.hpp" #include "protocol.hpp" using namespace std; using namespace Server; void usage(string proc) // 使用手册 { cout << GREEN << "\nUsage: \n\t" << ED << RED << proc << " [port]\n\n" << ED; } void Get(const Request &request, Response &response) { cout << "----------http start------------------\n"; cout << request.inbuffer << endl; // 这里采用硬编码,仅供测试用 std::string resp_line = "HTTP/1.1 200 OK\r\n"; // 响应行 // 响应报头 std::string resp_header = "Content-Type: text/html\r\n"; // 告诉客户端,正文为html std::string resp_blank = "\r\n"; // 空行 ifstream ifs("index.html"); if (!ifs.is_open()) { LogMessage(ERROR, (char *)"打开文件失败"); return; } // std::string resp_body; std::string resp_body{std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>()}; // 响应正文 response.outbuffer += resp_line += resp_blank += resp_body; cout << "---------- http end ------------------\n"; } int main(int argc, char *argv[]) { if (argc != 2) { usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); // 字符串port转整数 HttpServer server(Get, port); server.init(); server.start(); return 0; } //HttpServer.hpp: #include <iostream> #include <functional> #include <string> #include <cstring> #include <cerrno> #include <csignal> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include "log.hpp" #include "protocol.hpp" namespace Server { // typedef function<void (std::string)> func_t;//回调函数类型 // typedef std::function<void(const Request &, Response &)> func_t; using func_t = std::function<void(const Request &, Response &)>; // 将请求处理为响应 const int gbacklog = 5; // 全局的全连接队列长度 void handlerEnter(int sockfd, func_t func) { // 读取到完整的Http请求 Request request; Response response; char buffer[1024]; size_t n = recv(sockfd, buffer, sizeof(buffer), 0); // 假设一次读取完整请求 if (n > 0) { buffer[n] = 0; request.inbuffer = buffer; // func(Request, Response); 传入请求,传出响应 func(request, response); // 循环发送,确保所有数据都发出去 // send一次性发送的话,内核缓冲区可能遭不住,因此循环发送 const std::string &data = response.outbuffer; size_t total_sent = 0; while (total_sent < data.size()) { ssize_t sent = send(sockfd, data.c_str() + total_sent, data.size() - total_sent, 0); if (sent <= 0) { LogMessage(ERROR, (char *)"发送数据失败"); break; } total_sent += sent; } // send(sockfd, response.outbuffer.c_str(), response.outbuffer.size(), 0); } else { LogMessage(ERROR, (char *)"客户端退出..."); exit(0); } // exit(0); } class HttpServer { public: HttpServer(func_t func, uint16_t port) : _func(func), _port(port) {} HttpServer(func_t func, std::string ip, uint16_t port) : _func(func), _ip(ip), _port(port) { } void init() { // 创建监听套接字 _ListenSock = socket(AF_INET, SOCK_STREAM, 0); if (_ListenSock == -1) { LogMessage(FATAL, (char *)"socket创建监听套接字失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(SOCKET_ERR); } LogMessage(DEBUG, (char *)"socket创建监听套接字成功"); // bind绑定ip+port struct sockaddr_in ServerAddr; memset(&ServerAddr, 0, sizeof(ServerAddr)); ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(_port); if (inet_pton(AF_INET, _ip.c_str(), &ServerAddr.sin_addr) != 1) { LogMessage(FATAL, (char *)"点分十进制ip转网络序列失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(INETPN_ERR); } LogMessage(DEBUG, (char *)"点分十进制ip转网络序列成功"); if (bind(_ListenSock, (struct sockaddr *)&ServerAddr, sizeof(ServerAddr)) != 0) { LogMessage(FATAL, (char *)"bind绑定失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(BIND_ERR); } LogMessage(DEBUG, (char *)"bind绑定成功"); // 开启监听状态 if (listen(_ListenSock, gbacklog) != 0) { LogMessage(FATAL, (char *)"listen监听状态开启失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(LISTEN_ERR); } LogMessage(DEBUG, (char *)"listen监听状态开启成功"); } void start() { signal(SIGCHLD, SIG_IGN); // OS自动回收子进程资源 while (true) { // 建立连接 struct sockaddr_in ClientAddr; memset(&ClientAddr, 0, sizeof(ClientAddr)); socklen_t socklen = sizeof(ClientAddr); int sockfd = accept(_ListenSock, (struct sockaddr *)&ClientAddr, &socklen); if (sockfd == -1) { LogMessage(FATAL, (char *)"accept建立新连接失败, 错误码: %d, 错误描述:%s", errno, strerror(errno)); exit(ACCEPT_ERR); } LogMessage(DEBUG, (char *)"accept建立新连接成功,sockfd = %d", sockfd); pid_t pid = fork(); if (pid == 0) // 子进程 { close(_ListenSock); // 关掉无用文件描述符 if (fork() > 0) // 子进程本身退出 exit(0); // 孙子进程,被OS领养,不等待也不会变成僵尸进程 handlerEnter(sockfd, _func); } close(sockfd); // 父进程关掉该文件描述符,防止文件描述符被用完 } } private: int _ListenSock; // listen监听套接字 std::string _ip = "0.0.0.0"; // 默认接收所有ip uint16_t _port; // 服务器端口号 func_t _func; // // func_t _callback; }; }

在浏览器URL处ip:port的方式访问,就可以看到服务端返回的html了(由于图片资源在本服务器没有,所以加载不出来)

服务端和网页分离

若不想将html放在服务端的内存中,也就是让html和服务端分离,通过请求行的URL字段决定服务端返回给客户端的响应

需要让服务端通过请求的资源路径,将指定资源读取

//HttpServer.cpp: std::string resp_body; if (!Util::readfile(request._path, resp_body)) Util::readfile(html_404, resp_body); //Util.hpp: static bool readfile(const std::string file, std::string &outbuffer) // 将文件内容读取到outbuffer中 { std::ifstream ifs(file, std::ios::binary); if (!ifs.is_open()) // 不存在该文件(资源) return false; // 一次性全部读取 std::ostringstream oss; oss << ifs.rdbuf(); outbuffer = oss.str(); ifs.close(); return true; }

一个用户看到的网页结果,可能是由多个资源整合而成,因此要获取一张完整的网页效果,浏览器需要发起多次http请求
所以正文不一定是html,也有可能是图片/视频等等,因此需要根据不同的后缀名填写报头字段Content-Type,再根据资源的大小填充Content-Length

std::string ContDesc(std::string suffix) // 根据后缀返回Content-Type的值 { std::string ct = "Content-Type: "; if (suffix == ".html") ct += "text/html"; else if (suffix == ".jpg" || suffix == ".jpeg") ct += "image/jpeg"; else if (suffix == ".gif") ct += "image/gif"; else if (suffix == ".ico") ct += "image/x-icon"; // [TODO] 每个类型都写一个if ct += "\r\n"; return ct; } // 响应报头 // std::string resp_header = "Content-Type: text/html\r\n"; // 告诉客户端,正文为html 硬编码 std::string resp_header = ContDesc(request._suffix); if(request._size > 0) //若正文大小大于0,则填充Content-Length { resp_header += "Content-Length: " + std::to_string(request._size) + "\r\n"; } //protocol.hpp: class Request { public: void parse() // 从请求中解析请求行 { std::string req_line = Util::getOneLine(_inbuffer, sep_line); if (req_line.empty()) return; std::istringstream ist(req_line); ist >> _method >> _url >> _version; // 提取出请求行的三个字段 _path = wwwroot + _url; if (_path[_path.size() - 1] == '/') // 说明没有指定访问的资源,返回默认首页 _path += default_page; // 将请求的资源的后缀提取出来 // ./wwwroot/index.html 提取 .html // ./wwwroot/image/1.jpg 提取 .jpg auto pos = _path.rfind("."); if (pos == std::string::npos) _suffix = ".html"; // 缺省值为.html else _suffix = _path.substr(pos); // 将请求的资源的大小放到_size中 struct stat st; int n = stat(_path.c_str(), &st); if (n == 0) _size = st.st_size; else _size = -1; } public: std::string _inbuffer; // 整个请求 std::string _method; // 请求方法 std::string _url; // 文件路径 std::string _version; // http版本 std::string _path; // 真正的服务器目录 std::string _suffix; // 请求资源的后缀名(Content-Type) int _size; // 正文大小(Content-Length的值) }; class Response { public: std::string _outbuffer; };

HTTP报头常见字段

  • Content-Type: 数据类型(text/html等)
  • Content-Length: Body的长度
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • referer: 当前页面是从哪个页面跳转过来的;
  • location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问(重定向);
  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

HTTP请求方法

在上面服务端中,请求都是GET,表示获取资源,而还有POST,表示上传资源,这两种方法是最常用的

例如此时要再网页中通过表单输入用户名&密码,点击登录

<form action="/login" method="GET"> <div class="form-group"> <label for="username">用户名</label> <input type="text" id="username" name="username" placeholder="请输入用户名" required> </div> <div class="form-group"> <label for="password">密码</label> <input type="password" id="password" name="password" placeholder="请输入密码" required> </div> <button type="submit" class="login-btn">登 录</button> </form>

这段表单的请求资源为/login,请求方法为GET,点击登录后,会自动发送http请求,该请求以?隔开url和参数

由于我们现在的服务器没有做相关处理,因此访问/login资源会被替换成404界面

服务端也会收到该请求:

若将请求方法改为POST,点击登录后,发送的http请求就不会附带参数

参数在该请求的正文中

GET/POST的区别

  • GET通过url传递参数,POST通过http请求的正文提交参数
  • POST方法的参数一般来说用户看不到,私密性好(私密性 != 安全性
    无论是GET还是POST都不安全,要安全就需要https加密
  • 若GET方法,参数就不能太大,否则url会非常冗余
    但POST方法就无需担心参数长度,即使是超文本也没问题

当我们提交了指定路径(例如/login)后,服务器会通过例如if (path = "\login")这样的判断,来引导程序走向专门用于登录的代码,而不是默认的获取资源(例如fork后执行execl程序替换,交给指定程序执行)

请求方法有很多,但一般来说只会用到GET和POST

HTTP状态码

  • 2开头的,例如200,201,204等,都表示请求已成功被服务器接收,理解并接受
  • 4开头的,例如400,403,404等,都表示请求包含语法错误或无法实现,问题出在客户端(浏览器/App)
  • 5开头的,例如500,502,503等,表示服务器在处理请求的过程中发生了错误,问题出在服务端
  • 1开头的,例如100,101等,这类状态码很少在日常开发中直接接触,主要用于协议层的通信。
  • 3开头的,例如301,302,304,表示需要客户端采取进一步的操作才能完成请求(重定向

重定向状态码

下面详细介绍一下3号开头的重定向状态码

当在浏览器登录时,登录完后通常要跳转到首页,这个操作其中就有重定向的参与:服务端向浏览器发送重定向响应,浏览器识别到后再请求新的url

而重定向又分为永久重定向(301/308)临时重定向(302/303/307)

永久重定向会让浏览器记住这个跳转,在下次访问旧url时,不会问服务器,而是直接在本地跳转到新url;还会自动将用户保存的旧书签更新为新url

而临时重定向就不会

永久重定向一般用于永久更换域名、HTTP强制跳转HTTPS等等,其他情况都用临时重定向

若要将上面实现的服务端改为临时重定向到指定网站,只需修改如下两行即可

std::string resp_line = "HTTP/1.1 302 Found\r\n"; // 响应行 resp_header += "Location: https://www.bilibili.com/\r\n";

启动服务端后,当用telnet测试时,会发现它的Location字段和302状态码

现在再从浏览器进入时,就会自动跳转到bilibili.com

长连接

Http网页中可能包含多个元素,需要向服务端发送多个请求,而Http是基于TCP的,若每个元素都要建立一条TCP链接,开销会很大。

因此引入了长连接优化机制,长连接让一个 TCP 连接可以复用,连续发送多个 HTTP 请求/响应,避免频繁建立和断开连接

长连接需要客户端与服务端都支持才可以启用:

  • 若是HTTP/1.0,需要请求和响应中都包含Connection: keep alive报头字段
  • 若是HTTP/1.1,默认双方都开启了长连接,若想关闭,就显示声明Connection: close

会话保持

会话保持不是HTTP协议具备的,而是为了弥补HTTP缺陷而在应用层(浏览器)引入的“补丁”

HTTP协议本身是无状态的,即协议本身不记录前一次请求和后一次请求之间的任何关联。但这样用户在浏览器中登录了某个网站后,自动重定向到指定url,此时这个新url也不认识用户,还需要再一次登录

因此需要会话保持:用户登录一次某网站后,该网站会一直记住登录信息,后续再访问该网站时无需重复登录

旧方案:Cookie技术

在用户第一次访问该网站时,会要求注册/登录,当注册/登录成功后,浏览器会加密保存用户信息(账号密码等等),保存的信息称之为Cookie,并在后续访问同一网站时自动推送(将Cookie夹在请求中),每次访问网站时服务端也会有身份验证,此时读取请求中的Cookie

Cookie分为文件级Cookie内存级Cookie,文件级在浏览器被关闭后仍然有效,内存级仅在当前浏览器中有效,关闭即失效

而Cookie技术的安全风险太大:Cookie存储在本地,容易被木马病毒窃取、当黑客获取到Cookie文件后,就可以伪装成合法用户访问服务,并且Cookie中的账号密码也容易被获取

新方案:Session技术

为了提高安全性,现在用户的私密数据不存储在本地,而是服务器上。在登录成功后,服务端创建唯一的Session文件(包含了用户的认证信息、浏览痕迹等私密信息)并返回给用户一个Session ID(作为Cookie返回),后续请求只需携带该Session ID,服务端会验证ID合法性

由于攻击者只能获取到Session ID,而无法直接获取账号密码等信息,这在一定程度上改善了用户信息泄露的问题。同时,大型公司会投入资源维护服务器的安全性,包括防漏洞、防攻击等措施,进一步降低信息泄露的风险。

虽然攻击者拿到Session ID后依然可以伪装成合法用户,但当前已有许多成熟的技术防范:

  • 异常行为检测:IP地址突变、长期未用账号突然活跃、短时大量联系历史好友等等
  • 主动防御机制:强制Session失效、多因素认证触发、蜜罐漏洞诱捕黑客

要想在我们实现的服务端上推送给浏览器Cookie,需要在响应报头中添加Set-Cookie

真正生成的Cookie应该是通过算法实现的,但这里仅为测试所以手动写一个:

resp_header += "Set-Cookie: 1145141919810aaaaaa\r\n";

之后在浏览器访问该网站时:

默认的Cookie到期时间是会话结束,要手动设置就在Set-Cookie字段后加Max-Age属性(当有两个以上属性时,Cookie本身也需要有属性名):

resp_header += "Set-Cookie: tmpcookie=1145141919810aaaaaa; Max-Age=120\r\n";

在服务器中也可以看到,后续浏览器的每次请求都会附带Cookie:

Http工具

Postman

Postman是一个接口调试工具,用于模拟浏览器发送请求并查看响应

由于在服务端发送响应时通常都会对html进行压缩(去掉无意义的\r\n\t等),用这类工具就可以查看美化(重新为html加上格式符)后的效果

当我们请求自己的服务端时,获取的Cookie信息:

服务端的请求显示:

若用GET方式自己添加参数向服务端发请求,服务端会收到:

若用POST方式自己添加参数向服务端发请求,服务端可以正常收到:

Fidler

Fidler是一个专门用于HTTP的本地抓包工具,它充当中间人劫持浏览器的请求转发给目标服务器

若浏览器向服务端发送POST请求,依旧可以通过抓包软件抓到正文中的参数:

Fidler作为中间人,浏览器需要告知它要将请求发送到哪个服务器上,因此在Fidler中显示的请求,url部分是带上了IP地址的,而服务端接收到的请求只有后面的路径部分

http://www.jsqmd.com/news/1276998/

相关文章:

  • Git从入门到精通:全面指南
  • 2026年 东莞有铅锡膏/助焊剂品牌厂家:焊接性能与工艺稳定性深度解析 - 卓企推荐
  • Tiva™ TM4C129X EPI接口配置详解:从SDRAM到异步SRAM的实战指南
  • 基于YOLOv10的实时水藻检测系统开发实践
  • AB类立体声耳机音频放大器:TPA6110A2DGNR
  • 可以生成 word 的 Claude,搭配 AI 导出鸭优化文件转换,对比多种导出方式,解决排版错乱、格式变形各类办公难题
  • AI代理系统中的Agent Skills技术解析与实践
  • 终极指南:Spotube如何实现插件化元数据管理,打造你的专属音乐流媒体体验
  • 三步开启未来预测:用群体智能引擎看见每一个“如果“
  • COMSOL双温方程激光烧蚀模拟技术与工程实践
  • 鼠标一拖,批量搞定:LinkClump让你的网页浏览效率翻倍
  • 解锁Windows家庭版远程桌面:RDP Wrapper使用指南
  • 三步解锁九大网盘全速下载:开源脚本LinkSwift终极指南
  • 2026年 高温针筒锡膏品牌供应厂家:精准点胶与耐热稳定的专业选择 - 卓企推荐
  • 计算机毕业设计之基于springboot的购物网站
  • AI记忆系统设计:OpenClaw如何解决大模型记忆困境
  • GPipe流水线并行技术解析与优化实践
  • 【HR总监严审的AI总结标准】:揭秘2024年升职加薪必备的7项AI生成硬指标
  • 如何免费解锁Cursor Pro完整功能:3步实现AI编程助手无限使用
  • 猕猴桃检测数据集解析与应用实践
  • VMware Workstation 17 Pro 安装 Ubuntu 22.04 LTS 虚拟机完整教程
  • 2026实力之选:重庆臻进财科技有限公司,景区/民宿/防腐/移动木屋建造品牌机构 - 品牌发掘
  • 怎么判断外贸询盘是不是高质量商机?林芳老师的询盘分级SOP - 外贸圈集团
  • DSBGA封装解析:从LM3561实例看芯片封装设计与SMT工艺要点
  • Smithbox终极指南:零编程基础打造你的魂系游戏MOD
  • LyricsX终极指南:3分钟打造你的Mac智能歌词助手
  • Moneta Markets亿汇:把信息披露习惯做扎实,长期观察者更容易感受到的要点
  • 终极指南:Heimer跨平台思维导图软件的完整使用教程
  • Claude 出来的内容如何去除 #** 符号 结合 AI 导出鸭实操解析
  • TEL 2L80-000212-V1 控制器