【Linux网络编程】 HTTP协议

embedded/2025/2/26 11:18:12/

目录

前言

URL

协议格式 

常见的方法

状态码

cookie 

sessionid

token

总结


在这里插入图片描述https://img-blog.csdnimg.cn/a6c0473e16e249c2b9ca02e5b793f35e.gif#pic_center" />

HTTP协议是基于TCP的应用层协议,虽然我们说, 应用层协议是我们程序猿自己定的,但是自己定协议也是比较麻烦要解决两个问题:

  • 序列化与反序列化
  • 数据粘包问题

 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议);

URL

https://i-blog.csdnimg.cn/direct/955d3fb256804b5e9412cb938833617d.png" width="1110" />         浏览器访问百度:www.baidu.com可以跳转到百度;使用终端ping 百度,得到百度的ip,使用ip依然跳转到百度;但是发现并没有端口号这是为什么?

原因:使用的是http协议,只要是http协议,那么服务器所使用的端口号都必须是80,https协议,服务器所使用的端口号必须是443,由此根据协议方案名就可以指定端口号是多少,重要的常用的协议,端口号必须是众所周知的,不可以随意修改;

https://i-blog.csdnimg.cn/direct/975f8da3a855431bb79b8cbc3085ada6.png" width="1002" />

         wd就是搜索的关键信息,其余的参数都是浏览器的一些配置信息;如有多组参数就是要 & 符号进行间隔;

 网址中的名称比如:“ baidu ” 最终会经过应用层协议——DNS进行域名解析;

DNS域名解析:ip地址是一定要的;浏览器会自动的将域名转为ip地址;转完之后浏览器会那ip地址去访问;

 在搜索一些关键信息时,浏览器会将搜索关键字拼接到url上,对于一些特殊的符号(/:#等)会进行编码转换;也就是urlencode和urldecode

转义的规则如下:

         将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式

https://i-blog.csdnimg.cn/direct/1d176f8c4007468c8144875f59133e2d.png" width="1278" />

"+" 被转义成了 "%2B" urldecode就是urlencode的逆过程;

url在线转码工具 - UrlEncoder.cnhttps://csdnimg.cn/release/blog_editor_html/release2.3.8/ckeditor/plugins/CsdnLink/icons/icon-default.png?t=P1C7" />https://www.urlencoder.cn/url%E5%9C%A8%E7%BA%BF%E8%BD%AC%E7%A0%81%E5%B7%A5%E5%85%B7.html

协议格式 

访问百度得到以下信息:

https://i-blog.csdnimg.cn/direct/3d8d7ed7a79b4e3ab5f384b0e185e3d5.png" width="1492" /> 使用telnet工具:

telnet www.baidu.com 80// ctrl + ]
^]// 回车
telnet>
GET / HTTP/1.1
// 两个回车

 查看响应的http报文,报文是一行一行的,因为http报文都是以 行 为单位的,然而无论有多少行,在tcp看来全部都是一行字符串;

http协议版本:http/1.0、http/1.1、http/2.0、到现在最新的是http/3.0;

  • http/1.0:短连接(处理一个请求就关闭)
  • http/1.1:是较为主流,长连接(发送很多请求,全都响应并返回);

 https://i-blog.csdnimg.cn/direct/e262bc763ebd478ab6f53a962bafc03e.png" width="2050" />

 先看响应的首行:

HTTP/1.1 200 OK
// 其他的响应,比如:
HTTP/1.1 400 Bad Request

 他们都符合这样的规则:[http版本]  [状态码]  [状态码描述]

 使用telnet工具发送的请求:

GET / HTTP/1.1

 请求方法(GET)、请求路径(/)、协议版本(http/1.1);

HTTP是如何解决数据粘包问题的?

报头和有效载荷的分离:对于请求与响应:通过空行进行分离;

如何做到读取一个完整的报文?

  • 对于报头:一直读取,直到读取到空行;
  • 对于有效载荷:Content--Length:XXX(正文长度)存在报头中

 收到完整报文了,那序列化于发序列化呢?通常HTTP比较常用的协议是JSON、通过现已经由的序列化库进行序列化和发序列化;关于序列化和反序列化后续会进行介绍;

 如果请求的 url 资源为 “ / ”,请求的就是默认首页(请求行中的url部分),在设计中可以新建一个文件夹来存放web资源,进行跳转;

 上网行为:

  • 获取资源
  • 上传资源 ---把数据上传到服务器(登陆、注册、搜索等),比如:GET/POST结合html使用(html表单)

GET:用来获取资源,也可以用来传递参数;
POST:上传数据;

 比如:

https://i-blog.csdnimg.cn/direct/15b6c8b33ce14b958c23546bbcd8ea64.png" width="1936" />

 这两种方式有什么不同?

<input type="password">
<input type="text">

 password类型,在输入文本后效果:

https://i-blog.csdnimg.cn/direct/7fb567ef4a66429a8e827dac4dc92f10.png" width="385" />

 text类型:

https://i-blog.csdnimg.cn/direct/70398a8bd33e4cf7a4697c99dbd7e928.png" width="370" />

 这里只做一些简单的介绍;输入完数据点击提交后,就会将数据发送给指定的文件;

 比如我使用get方法传递,那么他就会把参数拼接在URL,然后发送请求:

http://127.0.0.1:8888/dira/dirb/a.html?password=test&password=123456

 服务端会收到这个请求,执行相应的处理接口;

 使用post方法:把表单的数据添加到了报文的有效载荷中,然后发送请求;

  • url传参字节个数有限制 POST方法没有限制
  • GET方法私密性较差,POST方法好一些

 GET和POST方法都不安全,想要安全,就需要加密解密;

常见的方法

https://i-blog.csdnimg.cn/direct/5186621ee40f421bb88dd0a061cfc81f.png" width="1209" />

  • GET:用于获取资源。可以用来请求网页内容。
  • POST:用于传输实体主体,常用于向服务器提交数据,如表单提交。
  • PUT:用于传输文件,通常是上传文件到服务器或更新资源。
  • HEAD:获取报文首部,不返回实体部分,适用于获取元数据。
  • DELETE:用于删除文件或资源。
  • OPTIONS:用于查询支持的方法,检测服务器能处理哪些请求。
  • TRACE:用于追踪路由(不需要了解)。
  • CONNECT:用于请求用隧道协议连接代理,收到请求不处理,把他交给别人处理。

LINK和UNLINK基本已经废弃

 GET和POST;可在html的表单中使用;

状态码

https://i-blog.csdnimg.cn/direct/a82ea5da61e74cd39f9c6e0e648cb245.png" width="1209" />

最常见的状态码,比如:200(0K),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway);

307:临时重定问,可以用来进行页面的跳转,让用户跳转到目标网页;

301:永久重定向,也就是说使用永久重定向,浏览器会记住这个URL,下次访问旧的URL直接跳转到重定向的URL;

比如:一个网站的域名发生变化(www.a.com->www.b.com),浏览器中搜索出现的还是www.a.com(已经废弃),现在已经更新了新的链接(www.b.com);想在访问www.a.com的同时能跳转到新的链接www.b.com;这时就可以使用301,让搜索引擎更新一下网址 下次爬取时就会到新的url进行爬取;

可以实现一个简易的服务端,负责接收请求,然后重定向到新的网址:

对原生socket接口进行封装:

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>#define Convert(addrptr) ((struct sockaddr *)addrptr)namespace Net_Work
{enum{SocketError = 1,BindError,ListenError};const static int defaultsockfd = -1;const int backlog = 5;// 封装一个基类,Socket接口类class Socket{public:virtual ~Socket() {}virtual void CreateSocketOrDie() = 0;virtual void BindSocketOrDie(uint16_t port) = 0;virtual void ListenSocketOrDie(int backlog) = 0;virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0; // 返回文件描述符以及客户端的信息(参数为输出型参数)virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;virtual int GetSockFd() = 0;virtual void SetSockFd(int sockfd) = 0;virtual void CloseSockFd() = 0;virtual bool Recv(std::string *buffer, int size) = 0;virtual void Send(std::string &send_str) = 0;public:void BuildListenSocketMethod(uint16_t port, int backlog){// bind监听(服务端) ip地址不需要设置为固定的ip,只需指定端口即可(传端口号)CreateSocketOrDie();BindSocketOrDie(port);ListenSocketOrDie(backlog);}bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport){// 客户端连接时需要服务端的ip地址和端口号CreateSocketOrDie();return ConnectServer(serverip, serverport);}void BuildNormalSocketMethod(int sockfd){SetSockFd(sockfd);}};class TcpSocket : public Socket{public:TcpSocket(int sockfd = defaultsockfd): _sockfd(sockfd){}~TcpSocket(){}// override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错void CreateSocketOrDie() override{_sockfd = ::socket(AF_INET, SOCK_STREAM, 0); // 创建套接字if (_sockfd < 0)exit(SocketError);}void BindSocketOrDie(uint16_t port) override{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(port);int n = ::bind(_sockfd, Convert(&local), sizeof(local));if (n < 0)exit(BindError);}void ListenSocketOrDie(int backlog) override{// backlog:内核允许在等待连接队列中排队的最大连接数int n = ::listen(_sockfd, backlog);if (n < 0)exit(ListenError);}Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override// 返回文件描述符以及客户端的信息(参数为输出型参数){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newsockfd = accept(_sockfd, Convert(&peer), &len);if (newsockfd < 0)return nullptr;*peerport = ntohs(peer.sin_port);*peerip = inet_ntoa(peer.sin_addr);Socket *s = new TcpSocket(newsockfd);return s;}bool ConnectServer(std::string &serverip, uint16_t serverport) override{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverip.c_str());server.sin_port = htons(serverport);socklen_t len = sizeof(server);int n = ::connect(_sockfd, Convert(&server), len);// std::cout << errno << strerror(errno) << std::endl;if (n == 0)return true;elsereturn false;}int GetSockFd() override{return _sockfd;}void SetSockFd(int sockfd) override{_sockfd = sockfd;}void CloseSockFd() override{if (_sockfd > defaultsockfd)close(_sockfd);}bool Recv(std::string *buffer, int size) override{char inbuffer[size];ssize_t n = recv(_sockfd, inbuffer, size-1, 0);if(n > 0){inbuffer[n] = 0;*buffer += inbuffer;//这里读取时是+=不会覆盖原有剩余的报文return true;}//else if(n < 0 || n == 0) return false;return false;}void Send(std::string &send_str) override{send(_sockfd, send_str.c_str(), send_str.size(), 0);}private:int _sockfd;};
}

 实现一个简单的TCPServer,接收到请求后直接返回重定向的响应:

#pragma once#include "Socket.hpp"
#include <pthread.h>
#include <iostream>
#include <functional>using func_t = std::function<std::string(std::string &request)>;class TcpServer;class ThreadData
{
public:ThreadData(TcpServer *tcp_this, Net_Work::Socket *sockp): _this(tcp_this), _sockp(sockp){}public:TcpServer *_this;Net_Work::Socket *_sockp;
};class TcpServer
{
public:TcpServer(uint16_t port): _port(port), _listensocket(new Net_Work::TcpSocket()) //, _handle_request(handel_request){// 创建套接字,bind、监听_listensocket->BuildListenSocketMethod(_port, Net_Work::backlog);}// 创建线程去执行任务static void *ThreadRun(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);std::string http_request;while (true){// 读取数据--不关心数据是什么,只读取,对数据进行处理//接收成功后进行处理if (td->_sockp->Recv(&http_request, 1024)) // 一次读取1024自己(可自主设置){// 报文处理// std::string http_response = td->_this->_handle_request(http_request); // 回调(将接收的数据进行处理std::string http_response = "HTTP/1.1 307 Temporary Redirect\r\n""Location: https://www.qq.com/\r\n""\r\n";// std::string http_response = "HTTP/1.1 301 Moved Permanently\r\n"//                 "Location: https://www.baidu.com/\r\n"//                 "\r\n";if (!http_response.empty()){// 发送td->_sockp->Send(http_response);}}}td->_sockp->CloseSockFd();delete td->_sockp;delete td;return nullptr;}void Loop(){while (true){std::string peerip;uint16_t peerport;// 连接Net_Work::Socket *newsock = _listensocket->AcceptConnection(&peerip, &peerport); // 返回新的fdif (newsock == nullptr)continue;std::cout << "获取一个新连接, sockfd: " << newsock->GetSockFd() << " client info: " << peerip << ": " << peerport << std::endl;pthread_t tid;ThreadData *td = new ThreadData(this, newsock);// 创建新线程去执行,把新的fd交给新的线程去执行pthread_create(&tid, nullptr, ThreadRun, td);}}~TcpServer(){delete _listensocket;}private:int _port;Net_Work::Socket *_listensocket;// public:
//     func_t _handle_request; // 初始化时填入,请求服务要做的任务
};

 main函数启动:

int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage : " << argv[0] << " port" << std::endl;return 0;}uint16_t localport = std::stoi(argv[1]);// std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerHttpRequest));std::unique_ptr<TcpServer> svr(new TcpServer(localport));// 执行服务svr->Loop();
}

 原理:

https://i-blog.csdnimg.cn/direct/058af51c86334eb2bf0f1c01a64cf135.png" width="1443" />

浏览器识别状态码为307,然后又识别到Location字段,他就知道要跳转的网页是是什么,从报头中提取内容跳转到qq.com;

cookie 

         HTTP是无状态的;什么意思?HTTP 协议对事务处理没有记忆能力,服务器不会在不同请求之间记住客户端的相关信息或状态。每个请求都是独立的,服务器无法自动识别多个请求是否来自同一个客户端以及它们之间的关联;通俗点讲:第二次访问同一个网址时,服务端依然是按照第一次访问时的状态进行响应,常见的情况:网络比较卡,然后一直点刷新,每次刷新就会有一个http请求,服务端会一个一个的对请求进行响应;

 但是你会发现一个问题:比如你在网页上登录哔哩哔哩,然后你关闭浏览器,下次再次使用浏览器访问,账号是登录状态;它直接就认识你都账号,为什么?不是无状态吗?

服务端响应时是没有记忆的,但是客户端可以,这一切都是浏览器帮我们记录存储的;

https://i-blog.csdnimg.cn/direct/f12afe4169764f559914adbc36df9bae.png" width="1321" />

 首次访问时,会需要会显示需要登录,在登录页面输入账号密码,发送给服务端进行认证,认证通过后,服务端就会发送响应,响应中有这两个字段:

  • Set-Cookie username;
  • Set-Cookie password;

         浏览器拿到后发现server响应的http报头有Cookie字段,就会对Cookie字段进行保存;下次进行请求时,浏览器会自动将保存的Cookie字段添加到http请求报头中;

 Cookie分两种:

  • 文件级:浏览器安装时会有安装目录,它会把cookie添加到安装目录的某个文件中
  • 内存级:在堆区申请一块空间进行存储 把数据保存到浏览器进程的上下文中 

如何分辨时文件级还是内存级?

打开浏览器登陆一次,把浏览器关了(进程退出),再次访问如果需要重新登录,那就是内存级;如果再次登录还是可以识别那就是文件级;

https://i-blog.csdnimg.cn/direct/e7e0ee3e04a74a1b9da9b72048d21fcf.png" width="721" />

https://i-blog.csdnimg.cn/direct/733234b3f9a84aaa97ad4befd2c84928.png" width="523" />

sessionid

         比如:你开了一个某视频会员,你的室友想要看电影,这时你可以把你的某视频的cookie信息给他,如果网址预防性不好,允许行同时多人在线,那么他就可以直接用你的账户进行观看;

比如:日常使用电脑时,电脑中了病毒,那病毒主要干什么呢?黑客主要就是搜集你浏览器中所有的cookie文件; 有了cookie数据,他就可以以你的身份进行访问;如果cookie中保存的是敏感的信息(用户名,密码)这就问题更大了账号极有可能会被盗;

有什么解决办法吗?

有的,cookie不直接存储账号密码不就行了;

https://i-blog.csdnimg.cn/direct/42b2001d1ec34abf93ec1ea854b110ae.png" width="1713" />

把用户信息不保存在client端,而是保存在server端,以后访问时认证使用sessionid即可;

那还是存在问题,用户敏感数据没了,但是黑客依然可以通过cookie,以你的身份访问资源,但是这种方式可以减小账号被盗的风险;当然也会有检测方式,判断是否异常登录:

比如:前一秒账户的ip显示在北京,下一秒就到了缅北;那么server端就可以让cookie失效;所以为什么某些程序需要定位权限,他的sessionid很可能与ip地址相绑定 一旦发生很大的变化,就可以让认证失效同时也可以设置cookie的有效时间,来进一步的限制;

token

         sessioid在单个服务端使用比较好,如果是分布式系统呢?服务端的主机有很多个,比如你这次访问的和下次访问的可能不是同一台服务器,那sessionid不久不行了吗?如果想要在多台服务器中都可以使用,那就必须让每台服务器都存储sessionid;

https://i-blog.csdnimg.cn/direct/8be57598207a4ea2a50aa3a712c69643.png" width="1236" />

 这样很浪费空间,有什么办法可以解决吗?有的那就是token;

 token由三部分组成:

https://i-blog.csdnimg.cn/direct/02defcb43e224b65960f226ab3db7c0f.png" width="541" />

 使用原理:

比如:使用非对称加密方式,验证用户信息的服务器持有一个私钥,其他服务器持有一个公钥;

浏览器发送请求给服务端:

  • 服务端将收到的消息(一般是用户信息,账号密码)进行验证,然后生成一个hash值;
  • 同时服务端会使用自己的私钥对数据的hash值进行签名;
  • 将身份信息,以及签名组成token返回给浏览器;

        浏览器接收到token会将token进行保存,下次访问时会带上token,

验证方接收到Token后,首先提取出其中的哈希值和签名。验证方使用相同的哈希算法计算出原始用户信息的哈希值。然后,验证方使用公钥对Token中的签名进行解密,得到一个哈希值。最后,将解密得到的哈希值与自己计算的哈希值进行比较,以确认两者是否相同,从而验证Token的有效性和数据的完整性。

这样服务端就无需存储用户的会话状态了,只需验证token即可;

        如果安全要求高的场景可结合hash算法,将消息及token一起hash运算得到哈希值,验证方收到后验证消息是否被篡改;

关于cookie和sessionid,token的关系:cookie中可以存储sessionid以及token;


总结

        以上便是本文的全部内容,希望对你有所帮助,感谢阅读!


http://www.ppmy.cn/embedded/167247.html

相关文章

Java入门——猜测数字游戏

题目&#xff1a; 程序随机给出一个1-1000的整数&#xff0c;然后让你猜是什么数。你可以猜任何数字&#xff0c;游戏会提示过大或过小&#xff0c;从而缩小结果范围。经过几次猜测和提示&#xff0c;终于给出了答案。在游戏过程中&#xff0c;记录游戏结束时需要猜对的次数&a…

自动化反编译微信小程序工具-e0e1-wx

一、项目地址 https://github.com/eeeeeeeeee-code/e0e1-wx 二、简介 1.还在一个个反编译小程序吗&#xff1f;2.还在自己一个个注入hook吗&#xff1f;3.还在一个个查看找接口、查找泄露吗&#xff1f;现在有自动化辅助渗透脚本了&#xff0c;自动化辅助反编译、自动化注入…

绩效管理与业务流程

绩效管理本质就是价值管理&#xff0c;或者说是能力管理&#xff0c;也就是通过一系列的科技手段去发现、证明一个人的能力和价值&#xff0c;然后给予科学、合理的利益分配。业务流程就是把企业的每一个零部件或者说齿轮都有效组合起来形成一个有机体为市场提供自己的独特价值…

封装响应体、自定义异常、全局异常处理、工具类返回响应体

异常设置 创建自己的异常继承运行异常 使用 构造函数 接收错误信息 和 错误code代码 理解&#xff1a; 在运行时 用户输入错误 所以要继承运行异常 断言类 抛出异常 assert a 1 那么 xxx throwIf 创建断言工具类 工具类 方法接收 布尔类型&#xff08;判断&#xff09; 运…

ubuntu安装docker docker/DockerHub 国内镜像源/加速列表【持续更新】

ubuntu安装docker & docker镜像代理【持续更新】 在Ubuntu上安装Docker&#xff0c;你可以选择两种主要方法&#xff1a;从Ubuntu的仓库安装&#xff0c;或者使用Docker的官方仓库安装。下面我会详细解释这两种方法。 方法一&#xff1a;从Ubuntu的仓库安装Docker 这种方…

项目实战--网页五子棋(匹配模块)(5)

上期我们实现了websocket后端的大部分代码&#xff0c;这期我们实现具体的匹配逻辑 1. 定义Mather类 我们新建一个Matcher类用来实现匹配逻辑 Component public class Matcher {//每个匹配队列代表不同的段位,这里约定每一千分为一个段位private ArrayList<Queue<User…

Elasticsearch中的CURL请求详解

在Elasticsearch(简称ES)的日常管理和操作中&#xff0c;CURL命令因其轻量级和高效性而备受青睐。CURL是一个利用URL语法在命令行方式下工作的开源文件传输工具&#xff0c;可以简单实现常见的GET/POST请求&#xff0c;是开发者与ES进行交互的重要桥梁。本文将详细介绍在ES中常…

低延迟,高互动:EasyRTC的全场景实时通信解决方案

在数字化时代&#xff0c;实时通信技术已成为连接人与人、人与设备的重要桥梁。无论是在线教育、远程医疗、智能家居&#xff0c;还是企业协作&#xff0c;高效的实时互动体验都是提升效率和满意度的关键。而 EasyRTC&#xff0c;作为领先的实时通信解决方案&#xff0c;凭借其…