深入解析HTTP协议与应用层编程

目录

本节重点

应用层

再谈协议

编写一个自定义协议的网络计算器

序列化和反序列化代码

添加报头和去掉报头

服务器主要核心逻辑代码

客户端主要核心逻辑代码

序列化和反序列方案

HTTP协议

认识URL

HTTP超文本通信的整个大致流程

HTTP发展历史

HTTP的协议格式

方法说明

状态说明

Header说明

高频请求头

高频响应头

编写一个HTTP协议的网络服务器

实用小工具telnet

前端学习网站

框架流程(理解GET和POST)

讲解状态码301和307

长链接

会话保持

cookie信息泄漏问题

实用小工具postman、fiddler、curl

本节重点

  1. 理解应用层的作用, 初识HTTP协议
  2. 学会使用一些分析网络问题的工具和方法

应用层

之前谈过应用层就是程序员编写的一层,下面的传输层、网络层、数据链路层由os帮我们完成

应用层即A告诉B实际要完成什么任务,比如下载电影,比如下单等,而下面几层是如何保证将数据传输到B

再谈协议

协议就是一种约定:只有双方遵循一种协议,才能互相通信,就像两个会说话的人,一个说北方话一个说南方话那就沟通不了,只有两边都说普通话才行,对于socket api接口,在读写数据时,都是按“字符串”的方式发送接受的(字符串只是个形象描述),如果我们要传输一些“结构化的数据”怎么办

注意:接下来谈的是应用层的协议,我们要制作一个协议,客户端遵循服务器遵循,双方通过协议正常解析对方发来的消息/任务,才能进行下一步

假设你不使用结构化的数据:

比如张三20北京市朝阳区

1. 你这样无法区分是张三2还是张三,是0岁还是20岁(有歧义)

2. 你需要单独规定用什么特殊字符来分隔每个字段(张三|20|北京市朝阳区)

3. 但是有些字段本身就包含|,比如北京市|朝阳区,那你解析的时候就会出现问题,而且字段为null怎么办?

4. 如果需要增加新的字段,比如电话,你所有的接收端的逻辑又要修改一遍

“纯字符串传输” 在极简单场景下可行,但会引入「解析歧义、扩展性差、兼容性低」的致命问题 

序列化的本质不是 “转字符串”,而是给数据加「结构化约定」,让通信双方能无歧义地解析数据

流程:

结构化数据--->序列化成一种特定的结构化约定---->传输------>反序列化解析成结构化数据

编写一个自定义协议的网络计算器

整个过程:我们需要将结构化的数据通过序列化,然后write写到tcp的发送缓冲区,不是由我们调用发送函数就发送到网络,而是拷贝到tcp的发送缓冲区,由os自主决定什么时候发,发多少,发错怎么办(有对应的机制,后面详讲),由于缓冲区是分开的,无论是客户端还是服务端发送和接收是互不干扰的,所以叫全双工。

tcp放到接收缓冲区之前由os帮我们处理好tcp层的报文,也就是解包+分用,我们接收缓冲区拿到的就是我们应用层规定的协议

当服务端的read忙其他时没有及时读,一下子积累了很多的报文,那我如何保证我读到的一个完整的报文,而不是半个,一个半呢???

此时我们应用层协议就需要发挥作用了,报文=报头+有效载荷,需要明确报文和报文的边界

1. 定长:直接用头几个字节来描述有效载荷有多长

2.特殊符合:分隔不同的报文

3.自描述方式:我是什么类型、有哪些字段、每个字段多长

这里采用定长的方式

总结:

我们需要定义协议,报文长啥样,服务端接收的时候可以进行区分报文和报文的边界(此时需要我们添加报头的设计)

序列化和反序列化:各个字段之间如何分开(这里采用特殊字符,自己编写)

两个结构体代表结构化数据:request请求(操作数x,y,操作符op),response回复(返回码表示计算成功失败,返回结果)

业务逻辑:

序列化和反序列化代码

注意:我们编写的是自己的,没有用第三方的,我这里使用了特殊字符比如空格来区分不同字段

//这里是request的序列化和反序列化  
 bool serialize(std::string *out){
        *out = "";
        // 结构化 -> "x op y";
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);

        *out = x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;

        return true;
    }

    // "x op yyyy";
    bool deserialize(const std::string &in)
    {
        // "x op y" -> 结构化
        auto left = in.find(SEP);
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;
        if (right - (left + SEP_LEN) != 1)
            return false;

        std::string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - start
        std::string y_string = in.substr(right + SEP_LEN);

        if (x_string.empty())
            return false;
        if (y_string.empty())
            return false;
        x = std::stoi(x_string);
        y = std::stoi(y_string);
        op = in[left + SEP_LEN];
        return true;
    }


//这里是respon的序列化和反序列化
    bool serialize(std::string *out){

        *out = "";
        std::string ec_string = std::to_string(exitcode);
        std::string res_string = std::to_string(result);

        *out = ec_string;
        *out += SEP;
        *out += res_string;

        return true;
    }
    bool deserialize(const std::string &in){
        // "exitcode result"
        auto mid = in.find(SEP);
        if (mid == std::string::npos)
            return false;
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);
        if (ec_string.empty() || res_string.empty())
            return false;

        exitcode = std::stoi(ec_string);
        result = std::stoi(res_string);

        return true;
    }

添加报头和去掉报头

我们从接收缓冲区读取的数据,需要区分不同的报文,所以需要这两个函数,这里我采用的是头部添加四个字节,这个四个字节叫报头,报头中描述有效载荷有多长,这样就能计算出整个报文多大

// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    return send_string;
}

// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;
    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

服务器主要核心逻辑代码

 void handlerEntery(int sock, func_t func)
    {
        std::string inbuffer;
        while (true)
        {
            // 1. 读取:"content_len"\r\n"x op y"\r\n
            // 1.1 你怎么保证你读到的消息是 【一个】完整的请求
            std::string req_text, req_str;
            // 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n
            if (!recvPackage(sock, inbuffer, &req_text))
                return;
            std::cout << "带报头的请求:\n" << req_text << std::endl;
            if (!deLength(req_text, &req_str))
                return;
            std::cout << "去掉报头的正文:\n" << req_str << std::endl;
            

            // 2. 对请求Request,反序列化
            // 2.1 得到一个结构化的请求对象
            Request req;
            if (!req.deserialize(req_str))
                return;

            // 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑
            // 3.1 得到一个结构化的响应
            Response resp;
            func(req, resp); // req的处理结果,全部放入到了resp, 回调是不是不回来了?不是!

            // 4.对响应Response,进行序列化
            // 4.1 得到了一个"字符串"
            std::string resp_str;
            resp.serialize(&resp_str);

            std::cout << "计算完成, 序列化响应: " <<  resp_str << std::endl;

            // 5. 然后我们在发送响应
            // 5.1 构建成为一个完整的报文
            std::string send_string = enLength(resp_str);
            std::cout << "构建完成完整的响应\n" <<  send_string << std::endl;
        
            send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说
        }
    }

客户端主要核心逻辑代码

 void start()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            std::cerr << "socket connect error" << std::endl;
        }
        else
        {
            std::string line;
            std::string inbuffer;
            while (true)
            {
                std::cout << "mycal>>> ";
                std::getline(std::cin, line);  // 1+1
                Request req = ParseLine(line); // "1+1"
                std::string content;
                req.serialize(&content);
                std::string send_string = enLength(content);
                std::cout << "sendstring:\n" << send_string << std::endl;
                send(_sock, send_string.c_str(), send_string.size(), 0); // bug?? 不管

                std::string package, text;
                //  "content_len"\r\n"exitcode result"\r\n
                if (!recvPackage(_sock, inbuffer, &package))
                    continue;
                if (!deLength(package, &text))
                    continue;
                // "exitcode result"
                Response resp;
                resp.deserialize(text);
                std::cout << "exitCode: " << resp.exitcode << std::endl;
                std::cout << "result: " << resp.result << std::endl;
            }
        }
    }

注意:代码中的bug是因为send无法保证一次性发一个完整的过去,有时候发送缓冲区满了就发不了了,比如100字节,可能只发了30字节,需要检查返回值

总结:序列化和反序列化自己写起来麻烦并且很挫,改动也麻烦,无非就是做一个工作,如何描述结构化的数据《--------》特定的结构化约定(可以想象成一种带特殊分隔符分隔字段的字符串,但这个说法不对)

所以对于序列化和反序列化有现成的解决方案,绝对不会自己去写,但协议需要,你需要规定x op y,需要规定报文

序列化和反序列方案

1.json

2.protobuf

3.xml

方案格式类型可读性(人类是否能直接看懂)示例(描述 “姓名张三、年龄 20”)
JSON文本格式高(键值对结构,直接可读){"name":"张三","age":20}
Protobuf二进制格式低(必须通过工具解析)二进制字节流(如 0A 06 E5 BC A0 E4 B8 89 10 14
XML文本格式高(标签化结构,语义清晰但冗余)<User><name>张三</name><age>20</age></User>
方案序列化 / 反序列化性能数据体积(同等内容)核心原因
JSON中等较大(冗余字段名)文本格式 + 键值对冗余
Protobuf极高(比 JSON 快 10~20 倍)极小(仅存字段编号 + 值)二进制格式 + 无冗余字段名
XML极大(冗余标签)文本格式 + 标签嵌套冗余
方案开发成本跨语言兼容性版本兼容性(新增字段)
JSON极低(几乎所有语言内置解析库)极高(全语言支持)高(旧版本可忽略未知字段)
Protobuf中等(需定义.proto文件 + 编译)极高(官方支持多语言)极高(字段编号不变即可兼容)
XML高(需处理标签解析 + Schema 约束)高(全语言支持)中等(需兼容标签结构)
方案典型场景
JSON前后端交互、轻量级接口对接、普通业务数据传输
Protobuf高并发微服务、游戏服务器、物联网(低带宽)、大数据传输
XML

传统企业系统(如 SOAP 接口)、配置文件

综上,我们已经学习了应用层协议(自个编写的)

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

HTTP协议

通过之前的自定义应用层协议,可以很明确的知道http协议做的事情跟我们自定义的一样

会话层:也就是我们的socket连接

表示层:添加报头,序列化反序列化等

应用层:你的业务逻辑,比如计算器业务

只不过是http协议人家已经帮我们做了这部分工作,直接拿来使用即可

http和https都是全球性的超文本信息交互生态系统(web)的通信协议

认识URL

平时我们俗称的“网址”其实就是说的URL

urlencode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.

比如:某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.

转义的规则如下:

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

这里可以看到+已经是%2B,因为+的ASCII是43,转成16进制就是2B

urlencode就是将字符变成%XY格式

urldecode就是%XY格式变成字符

网上有工具自行查询一下

这个是允许携带特殊字符,使用urlencode,后面服务器收到请求之后在使用urldecode解码即可

HTTP超文本通信的整个大致流程

域名即ip地址

http的端口通常是80,https的端口通常是443(一般端口需要显示出来,只是浏览器省略)

域名后面/:注意这个是web根目录,不是你的Linux服务器的根目录,概念不一样

后面的一串路径:是你申请的资源的位置,比如这里是访问某个人的朋友圈,那就是一串id,得到之后后面返回给你id对应的朋友圈

由于所有的资源,无论是视频还是文本,http都能够搞定,所以叫做超文本协议

web:全球性的超文本信息交互生态系统

web是一个生态系统,它包含通信协议http和https,还有web客户端(比如浏览器),web服务器,web应用(网页版微信)

超文本:超文本就是支持视频文本等多样的数据形式,还有超链接,支持跳转,比如你点一个链接可以跳到别的网页

HTTP发展历史

版本协议类型核心特性解决的核心痛点
HTTP/0.9文本仅 GET 方法,纯文本传输无(协议雏形)
HTTP/1.0文本多方法、多资源类型、状态码支持图片等二进制资源传输
HTTP/1.1文本持久连接、管道化、Host 必选减少 TCP 连接开销,支持虚拟主机
HTTP/2二进制多路复用、头部压缩、服务器推送彻底解决 HTTP 层队头阻塞
HTTP/3基于 QUIC连接迁移、UDP 传输、内置 TLS 1.3解决 TCP 层队头阻塞,提升安全性

HTTP 协议的演进始终围绕 「提升性能」和「增强安全」 两个核心目标:

  1. 从「文本协议」到「二进制协议」,提升解析效率;
  2. 从「短连接」到「持久连接」再到「多路复用」,提升并发能力;
  3. 从「明文传输」到「强制加密」(HTTP/3 内置 TLS),强化通信安全;
  4. 从「依赖 TCP」到「基于 QUIC」,解决底层传输的队头阻塞问题。

这里主要讲解1.0,提一嘴1.1,2.0不讲

HTTP的协议格式

request第一行:[方法] + url + [版本]

GET就是方法,版本就是HTTP/1.0这样

下面多行:这是报头

这里面的字段都是name: value\r\n的形式(注意:后面有个空格)

比如有个字段Content-Length: 36,这个表示有效载荷的大小 

报头最后一行是\r\n,表示报头结束,后面是有效载荷

每一个协议需要解决的问题:判断哪里是报头哪里是有效载荷

有效载荷(body):这个部分允许为空字符串

因为http的格式是\r\n,所以这里它使用分隔符来标记,这样能够拼接成字符串(序列化),后面服务器接收后再找\r\n就能够转换成结构化的数据(反序列化),为什么这里可以用分隔符,本质是因为你的请求中如果有特殊字符它早就通过urlencode转码了

response:第一行[版本] [状态码] [状态描述]

比如我们写一个函数,如果函数出错它需要设置错误码,让外面获取知道错误,这里的状态码也是告诉客户端完成的怎么样,类似我们的网络计算器中的exitcode

方法说明

方法核心语义是否安全是否幂等允许请求体典型使用场景
GET读取 / 查询资源✅ 安全✅ 幂等❌(规范)访问网页、查询数据(如 /api/user?id=1
POST创建 / 提交资源❌ 不安全❌ 非幂等✅ 允许提交表单、创建数据(如 /api/user 创建新用户)
PUT全量更新资源(替换)❌ 不安全✅ 幂等✅ 允许更新用户全部信息(如 /api/user/1 替换用户 1)
DELETE删除资源❌ 不安全✅ 幂等❌(规范)删除数据(如 /api/user/1 删除用户 1)
PATCH增量更新资源(部分修改)❌ 不安全✅ 幂等✅ 允许修改用户手机号(仅改一个字段,如 /api/user/1
HEAD仅获取响应头(无响应体)✅ 安全✅ 幂等❌ 禁止检查资源是否存在、获取文件大小 / 更新时间
OPTIONS探测服务端支持的方法✅ 安全✅ 幂等❌ 禁止跨域请求(CORS)预检、查询接口支持的方法
TRACE回显请求(调试用)✅ 安全✅ 幂等❌ 禁止排查请求传输过程中的修改(极少用,易被攻击)
  1. 安全性:执行该方法是否会「修改服务端数据」—— 安全方法(GET/HEAD/OPTIONS/TRACE)仅读取数据,不会改变服务端状态;不安全方法(POST/PUT/DELETE/PATCH)会修改数据。
  2. 幂等性:多次执行同一请求,是否得到相同的结果(服务端状态一致)—— 幂等方法(GET/PUT/DELETE/PATCH)多次调用和一次调用效果相同,非幂等方法(POST)多次调用可能创建多个资源(如重复提交表单创建两个用户)。

最常用的是GET和POST

状态说明

类别首位数字核心语义典型场景
信息型1xx临时响应,请求已接收,继续处理100 Continue(预检通过)
成功型2xx请求已成功处理200 OK、201 Created、204 No Content
重定向3xx需要客户端进一步操作(跳转)301 永久重定向、302 临时重定向、304 缓存命中
客户端错误4xx客户端请求有误,服务端无法处理400 参数错误、401 未授权、403 禁止访问、404 资源不存在
服务端错误5xx服务端处理请求时出错500 服务器内部错误、502 网关错误、503 服务不可用
业务场景推荐状态码原因说明
用户登录成功200 OK通用成功,返回用户信息
注册新用户成功201 Created资源创建成功,响应头返回新用户 URL
删除用户成功204 No Content成功但无需返回内容,减少带宽消耗
访问需要登录的接口401 Unauthorized未授权,提示用户登录
普通用户访问管理员接口403 Forbidden已授权但无权限,拒绝访问
接口参数校验失败400 Bad Request客户端参数错误,明确指出错误字段
服务端代码抛空指针异常500 Internal Server Error服务端未捕获异常,返回通用错误
高峰期接口限流429 Too Many Requests客户端请求过频,提示重试时间

服务维护中暂停访问

资源不存在

503 Service Unavailable

404Not Found

服务不可用,响应头加 Retry-After 提示重试时间

访问不存在的页面、查询不存在的 ID

HTTP 状态码的核心价值是「标准化的结果标识」—— 客户端(浏览器 / APP)可直接根据状态码做自动化处理(如 401 跳登录、304 用缓存、503 重试),无需解析响应体。

Header说明

高频请求头
头部名核心语义典型场景
Host指定请求的服务器域名 + 端口(HTTP/1.1 必选)Host: www.example.com(虚拟主机核心,区分同一 IP 下的不同网站)
User-Agent(UA)客户端标识(浏览器 / 设备 / 系统)User-Agent: Mozilla/5.0 (Windows NT 10.0; Chrome/120.0.0.0)(服务端适配不同客户端)
Content-Type请求体的数据格式application/json(JSON 数据)、application/x-www-form-urlencoded(表单)、multipart/form-data(文件上传)
Content-Length请求体的字节长度Content-Length: 27(告诉服务端需读取多少字节的请求体)
Authorization身份凭证(登录令牌)Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9(JWT 令牌)、Basic dXNlcjE6密码Base64(基础认证)
Cookie客户端携带的 Cookie(服务端之前设置的)Cookie: uid=123; token=abc(维持登录状态)
Accept客户端支持的响应数据格式Accept: application/json, text/html(优先要 JSON,其次 HTML)
Accept-Encoding客户端支持的压缩方式Accept-Encoding: gzip, deflate(服务端压缩响应体,节省带宽)
Referer请求的来源页面(从哪个页面跳转过来)Referer: https://www.example.com/login(防盗链、统计来源)
Cache-Control客户端的缓存要求Cache-Control: no-cache(不使用本地缓存,强制请求服务端)
Origin跨域请求的源(协议 + 域名 + 端口)Origin: https://blog.example.com(CORS 跨域预检核心)
高频响应头
头部名核心语义典型场景
Content-Type响应体的数据格式 + 字符编码Content-Type: application/json; charset=utf-8(告诉客户端解析为 JSON,编码 UTF-8)
Content-Length响应体的字节长度Content-Length: 1024(客户端按长度读取响应体)
Set-Cookie服务端给客户端设置 CookieSet-Cookie: uid=123; Path=/; HttpOnly; Max-Age=3600(HttpOnly 防 XSS,Max-Age 设置有效期)
Cache-Control服务端的缓存规则Cache-Control: max-age=86400(客户端缓存 24 小时)、no-store(禁止缓存)
Location重定向的目标地址Location: https://www.new-example.com(配合 301/302 重定向)
Server服务端软件标识Server: nginx/1.21.0(显示服务器类型,可隐藏提升安全性)
Access-Control-Allow-Origin跨域允许的源Access-Control-Allow-Origin: *(允许所有域)、Access-Control-Allow-Origin: https://blog.example.com(仅允许指定域)
Access-Control-Allow-Methods跨域允许的请求方法Access-Control-Allow-Methods: GET, POST, PUT(配合 OPTIONS 预检)
Content-Encoding响应体的压缩方式Content-Encoding: gzip(客户端需先解压再解析)
ETag资源的唯一标识(缓存校验)ETag: "abc123"(配合 304,客户端请求时带If-None-Match: "abc123",服务端判断资源是否修改)
Last-Modified资源最后修改时间(缓存校验)Last-Modified: Mon, 15 Dec 2025 11:00:00 GMT(配合 304,客户端带If-Modified-Since校验)

编写一个HTTP协议的网络服务器

服务端代码


#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>

#define DEFAULT_IP "0.0.0.0"
#define DEFAULT_PORT 7676
#define BACKLOG 5
class HttpServer{
    private:
    uint16_t _port;//端口号
    std::string _ip;//ip地址
    int _listenfd;//监听套接字
    public:
    HttpServer(uint16_t port=DEFAULT_PORT,std::string ip=DEFAULT_IP)
    :_port(port)
    ,_ip(ip)
    {}
    //初始化服务器
    void init(){
        _listenfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字,这个是ipv4,tcp
        if(_listenfd<0){
            std::cout<<"socket error:"<<errno<<strerror(errno)<<std::endl;
            exit(1);//直接退出
        }

        struct sockaddr_in _addr;
        bzero(&_addr,sizeof(_addr));//清零
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());

        if (bind(_listenfd, (struct sockaddr *)&_addr, sizeof(_addr)) < 0)
        {
            // bind失败
            std::cerr << "bind error:" << errno << strerror(errno) << std::endl;
        }

        // listen监听模式
        if (listen(_listenfd,BACKLOG) < 0)
        {
            // listen失败
            std::cerr << "listen error:" << errno << strerror(errno) << std::endl;
        }
        std::cout<<"server init _listenfd="<<_listenfd<<std::endl;
    }
    void start(){
        while(true){
         struct sockaddr_in client_addr;
                socklen_t client_addr_len = sizeof(client_addr);
                //接收链接
                std::cout<<"accept..."<<std::endl;
                int client_fd = accept(_listenfd,(struct sockaddr*)&client_addr,&client_addr_len);
                if(client_fd<0){
                    //accept失败
                    std::cerr<<"accept error:"<<errno<<strerror(errno)<<std::endl;
                    continue;
                }
                std::cout<<"client_fd="<<client_fd<<std::endl;
                std::string client_ip=inet_ntoa(client_addr.sin_addr);
                uint16_t client_port = ntohs(client_addr.sin_port);
                char inbuf[1024] = {0};
                int len = read(client_fd, inbuf, sizeof(inbuf) - 1);
                if (len <= 0){
                    // 关闭套接字
                    close(client_fd);
                    break;
                }
                // 处理业务逻辑
                std::cout << inbuf << std::endl;
                // 构建一个响应返回出去
                char outbuf[1024] = {0};
                const char *html = "<html><body><h1>Hello World</h1></body></html>"; // 这个用于返回响应,html内容,浏览器会解析
                sprintf(outbuf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s", strlen(html), html);
                write(client_fd, outbuf, strlen(outbuf));
                close(client_fd); // 关闭
               
        }
    }
};

客户端代码

#include "http_server.hpp"
int main(){
    HttpServer server;
    server.init();
    server.start();
}

这只是一个简单的框架,简单的HTTP协议的框架,因为协议部分别人已经写好了,也就是应用层的协议别人做了,我们只需要获取各个字段去解析是啥意思,然后后端再去执行业务逻辑就行

浏览器(客户端)→ 构造HTTP请求(包含请求行/请求头/请求体)
→ 基于TCP发送给服务器 → 服务器解析HTTP请求 → 处理业务 
→ 构造HTTP响应 → 基于TCP返回给浏览器 
→ 浏览器解析响应 → 渲染页面/执行交互

可以看到占有的文件描述符是4,因为3是socket创建出来的去监听了,所以是从小到大,所以只能是4去连接

GET:查询,请求某个资源

/favicon.ico:表示浏览器要获取服务器根目录下的 favicon.ico 文件(这是网页的 “图标文件”,浏览器会自动请求这个文件,用来显示标签页图标);(一般来说就把你的网页的首页放在这里,别人访问的时候就可以去显示)

HTTP/1.1:协议及版本

HOST:请求我们的服务器的什么,8080端口

Connection:这个是浏览器希望是长连接(注意我们的代码是close,短链接,所以会被服务器主动四次挥手断开连接)

User_Agent:表示 “发送请求的客户端是 Windows 10 系统 + Edge 浏览器(基于 Chrome 内核)”;服务器可以通过这个字段做 “浏览器 / 系统适配”(比如给移动端返回适配的页面)。

Accept:表示客户端能够收到的资源类型

为什么服务器发送的版本是1.1不是2等版本?

这个发送的协议的版本号取决于浏览器自身,不是由我们决定的,是浏览器自动适配的,因为我们的c++服务器返回去的是HTTP 1.1,如果支持更高版本,也是由浏览器和服务器之间通信协商完成的

这个请求的/favicon.ico是啥???

这里博主没有贴出来第一次,第一次请求没有这个目录,只有根目录

正常的浏览器会请求两次:第一次你输入ip:端口,请求web根目录,第二次浏览器自动请求小图标的资源,你需要放在/favicon.ico中,但是由于博主没有放,所以没有

这个请求时是长连接怎么解释???

这个是浏览器希望是长连接,但是是长还是短取决于我们的服务器,你也可以编写成长的

这里是一发一个请求响应之后关闭,你长连接的话可以使用一个连接去处理多个请求之后在关,这是取决于你的服务器的代码

如何理解web根目录???

请求的时候会携带请求的路径,比如之前我们请求的是/,那服务器收到之后就可以解析

一般来说服务器不会把web服务放在根目录下,比如你的进程放在/a/b/c下面,那别人传来一个/的时候你就需要解析,因为是/所以拼接再一次变成/a/b/c/,但是此时是一个路径,不是具体的文件,你需要特殊处理,判断一下如果前端传来/,那就去找/a/b/c/index.html,如果申请的是/register.html,那最后服务器拼接的时候就变成/a/b/c/register.html,然后服务器构建响应返回出去

所以web根目录其实就是一开始的路径/a/b/c,这是服务端做的识别

一般来说,会把所有的静态文件放在这个wwwroot目录下,后面请求啥我就加上./wwwroot即可,Web 根目录只存浏览器能直接请求、服务器能直接返回的静态文件,绝对不存服务器代码(如 C++/Java/PHP 源码)、配置文件(如数据库密码)、系统敏感文件!

所谓的前后端交互

前端:写好这个wwwroot目录,所有的样式前端去完成,

后端:处理静态资源请求:根据请求路径(比如/login.html),从wwwroot里读取对应的文件返回;处理动态业务请求:接收前端的接口请求(比如/api/login),执行业务逻辑(验证账号密码),返回 JSON 格式的动态数据;

注意:一个网页不是仅仅有一个请求就能够完成的,一个网页包含很多资源(图片/文字等等),一个资源就需要一个http请求,所以一次连接需要多个请求才能完成网页的渲染

实用小工具telnet

telnet 是 TCP 协议的极简客户端工具,核心作用是「建立 TCP 连接、发送原始数据、接收服务器响应」—— 对咱们写 C++ HTTP 服务器来说,它是排查「端口连通性」和「HTTP 协议交互」的 “利器”,比浏览器更纯粹(不处理 HTML、不自动发 /favicon.ico 请求),能直接看到底层的请求 / 响应数据。

telnet ip 端口:这个就是使用这个小工具来连接你的服务器,后面就是建立成功

然后你可以自定义请求,这样可以避开浏览器自动帮你构建的,可以用来测试

然后输入[   这个服务端就会构建响应返回去给你,可以看到最纯粹的底层响应数据

前端学习网站

w3school 在线教程

可以去这个网站学习关于一个html的语法,简单搭建一个自己的网站

框架流程(理解GET和POST)

无非就两种情况:

1.获取资源:就是我们前面说的

2.上传资源(交互):html中有一种表单(可以在w3shool网站查询),就比如你登录时提交用户名和密码,后面需要把账户和密码发给服务器,服务器查询之后是对的再返回合法的界面,否则就是登录失败返回错误

这是一个表单,学过html的应该知道,我们填写相应的信息之后就会构建一次请求交给服务器

action:你要提交到哪个资源路径下

method:提交方法

下面就是表单:展示给用户看就是需要提交账户和密码

submit:就是提交按钮,写完之后就能够点击登录按钮

这样浏览器就会构建一次响应,后端就能够收到,然后解析进行判断

这个登录action最后会拼接到你的web根目录下形成url,?后面是你提交的参数,前面是提交到哪里(访问的资源),由于我们是随便写的,所以访问的资源不存在。

对于GET方法,浏览器会把提交的参数放在url中,?后面就是我们提交的参数

对于POST方法,浏览器不会把提交的参数放在url中,会放在body部分,然后浏览器上端的就不会有参数(用户名和密码)

那对于其他的方法重要吗???有必要学吗???

其实没必要,我们只需要根据url中的?前面是申请的资源,后面是参数

有些情况可能是c++编写我们的服务器,而后端处理数据可以交给python等语言

通过url传参,url?前面可以告诉我们访问什么资源,?后面传参,最后程序执行完

我们在构建响应返回出去

对于这个方法我们可以构建一个哈希map,然后把全部的方法都存进去,形成一个string-func_t的kv模型,未来客户端只要传一个string(解析url),就可以返回一个func_t方法,这就叫功能路由

讲解状态码301和307

有时候我们需要进行跳转页面,比如你有两个网站,一个是以前开发的,后面又做了一个新的,但是大家都是只知道旧的,并且旧的客户很多,你需要把旧的移到新的

此时就需要重定向

301:永久重定向,资源 “永久搬家” 了(比如旧网址废弃,换成新网址);浏览器会永久缓存这个重定向关系(下次再访问旧地址,直接跳新地址,不会再问服务器)。

307:临时重定向,资源 “临时搬家”(比如服务器维护,临时把请求转到备用服务器);浏览器不会缓存这个重定向关系(每次访问旧地址,都会先问服务器 “要不要重定向”)。

构建响应的时候有一个字段Location

Location重定向的目标地址Location: https://www.new-example.com(配合 301/302 重定向)

长链接

场景:以前的http1.0其实频繁的请求没问题,因为图片啊啥的都很小,但现在不一样,图片大,频繁的请求就慢,每次都要建立链接去申请资源,所以http请求就发展,为了减少tcp面向连接频繁创建连接的问题------>长连接技术,也就是用同一条链接,当你请求完一个之后不释放,还接着用这个链接请求

Connection: keep-alive是支持长连接,只有服务器返回给客户端说支持,客户端也要告诉服务器支持,双方协商都支持才可以,close是短链接
 

会话保持

现象:比如你登录bilibili时你已经登录过了,然后在新建一个哔哩哔哩在登这时候就不用扫码它已经记住你了,即使你关掉浏览器,在重新打开哔哩哔哩也可以记住,但换一个浏览器就不认识了,只要你在同一个浏览器当中就记住

http的定位是超文本传输,也就是什么资源都可以从服务器中拉取到本地

由于http协议是无状态的,也就是不会记录你以前的请求,比如你这次请求登录,下一次请求查订单,服务器默认不知道这两个请求是同一个用户发的,http根本不记录用户的登录状态然后维护起来,http不猜测也不记录

会话保持” 是 ** 让服务器 “记住用户状态”** 的技术(比如用户登录后,后续请求不用重复输入账号密码),核心解决 “HTTP 是无状态协议” 的痛点

Cookie 是实现浏览器端状态存储的核心技术,也是会话保持的基础 —— 简单说,它是「服务器下发给浏览器、浏览器自动保存并随后续请求携带的小型文本数据」,用来解决 HTTP 无状态的问题。

  1. 用户首次登录
    • 浏览器发送 POST /api/login 请求,携带账号密码;
    • 服务器验证通过后,生成 Set-Cookie 响应头(比如 Set-Cookie: sessionid=abc123; Path=/; HttpOnly)(服务器可以通过这个字段控制控制 Cookie 的有效期、作用范围、安全性,常用属性),返回给浏览器;
  2. 浏览器处理
    • 自动解析 Set-Cookie 头,把 sessionid=abc123 保存到本地;
  3. 用户后续请求
    • 浏览器访问该域名下的任意路径(比如 /api/userinfo),会自动在请求头里加 Cookie: sessionid=abc123
  4. 服务器识别用户
    • 解析请求头里的 Cookie,通过 sessionid 找到对应的用户信息,确认 “这是已登录用户”。

浏览器是一个进程,你关闭浏览器就相当于关闭进程

如果是cookie内存,那你的进程关闭,cookie就没了,你存储的信息就没了,下一次还需要登录

如果是cookie文件,进程关闭不影响文件,下一次启动还会有

以下是实验,先登录记录cookie,然后删掉cookie,再接着跳转页面

已经登录过

删除cookie

但是此时发现跳转网页看视频,不会弹出来登录信息,这个原因的本质是不只是cookie这种验证方式,b站还做了很多种验证方式

比如B 站 Web 端登录后,除了下发普通登录 Cookie,还会返回refresh_token字段并由浏览器相关机制存储。这个refresh_token相当于 “Cookie 刷新凭证”,有效期比普通 Cookie 更长。当你手动删除 Cookie 后,B 站网页会通过前端逻辑检查 Cookie 状态,若发现 Cookie 缺失或失效,会自动调用专门接口,用保存的refresh_token重新获取新的 Cookie。在你跳转网页的短时间内,这个刷新流程会静默完成,所以你察觉不到需要重新登录。

还有很多种情况,这个取决于他们是如何处理的

可以找一些小网站,小网站就不会有多种方式,博主尝试csdn是会重新登录的,大家试试

cookie信息泄漏问题

如果cookie文件当中存储了账户和密码,此时就会很危险,如果中了木马病毒,账户密码泄漏,非法用户就可以使用你的cookie去登录服务器,服务器无法判断是非法用户

如何解决这个因为中了木马病毒而导致信息泄漏问题???

不是因为cookie文件,是因为我们把隐私信息放在了客户端,不要觉得自己的安全意识高,用户对自己的信息的保护能力是有限的(qq号被盗就是因为有些人点击了一些非法链接,下载了木马病毒)

解决方案:不要把信息放在客户端,把信息放在服务器

流程:

客户端发送用户名+密码去登录,服务器收到请求之后核对(查询数据库),错误返回失败,正确就存储,随机生成一个sessionid,这个id和用户信息绑定,然后存储在一个session文件

然后给客户端返回Set-Cookie:sessionid,后续浏览器解析这个字段,然后把sessionid存储在浏览器的cookie文件当中,跳转页面的时候,浏览器直接把这个sessionid附带传给服务器,服务器会去session文件当中,去核对

解决问题:

解决了客户端明文存储账户和密码的问题,防止中了木马病毒之后,泄漏个人信息

但是如果依旧会被盗取sessionid,然后服务器依旧无法判断是否为非法用户

但区别在于:sessionid的风险可控,偷明文密码的风险是毁灭性的

sessionid被盗取,只能浏览个人主页或者发发消息等操作

密码被盗取,别人可以改你的密码,”永久掌控账户“

服务器防范措施:

sessionid被盗取,服务器也是有相应的措施的,比如ip对比,一旦发现异地登录就让你校验,一旦发现设备更改,比如之前用苹果17,一下子切换苹果11,比如用户操作习惯等等只需要让sessionid失效即可,Set-Cookie中有个字段Max-Age中可以设置id过期时间

实用小工具postman、fiddler、curl

Postman 是一款专门用于测试 HTTP/HTTPS API 的工具,简单说就是「模拟前端发送请求、验证后端接口是否正常工作」的利器,是后端开发、测试、前端联调的必备工具。

我们可以不用先写前端页面,直接使用这个工具去测试我们的接口是否可以正常使用

Fiddler 是一款免费的 HTTP/HTTPS 抓包工具,核心作用是「监听、捕获、修改客户端与服务器之间的所有 HTTP 流量」—— 简单说,它是夹在浏览器 / 客户端和服务器之间的 “中间人”,能清晰看到请求和响应的每一个细节,比 Postman 更侧重 “流量分析 / 调试”,和 Postman 互补使用。

注意:fiddler是充当了中间层,也就是代理,浏览器先发给fiddler,fiddler先解析拿到之后在转发给服务器,因为这个fiddler是在你的本地机器上,所以能够抓取你测试机器发出去的数据包

这两款软件可以自行下载研究

curl 是一款命令行形式的 HTTP / 网络工具(也有库版本 libcurl),核心作用是「在终端里发送 HTTP/HTTPS 请求、传输数据」,无需图形界面,是后端开发 / 运维调试接口、自动化脚本的必备工具 —— 和 Postman(可视化界面)、Fiddler(抓包)定位不同,但能完美互补

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值