【计算机网络】应用层协议(序列化与反序列化/HTTP/HTTPS)

目录

[应用层协议 - 序列化与反序列化](#应用层协议 - 序列化与反序列化)

[例子 - 网络计算器](#例子 - 网络计算器)

json

[HTTP 协议](#HTTP 协议)

[认识 URL](#认识 URL)

[http 协议格式](#http 协议格式)

[html 文件](#html 文件)

[简易 http 服务器](#简易 http 服务器)

[HTTPS 协议](#HTTPS 协议)

加密与解密

常见加密方式

数据摘要

加密方案

[CA 认证](#CA 认证)

其他应用层协议


应用层协议 - 序列化与反序列化

我们程序员写的一个个解决我们实际问题、满足我们日常需求的网络程序, 都是在应用层。之前在套接字编程的 TCP 服务器,我们规定服务器是一个 "echo" 服务器,或规定用户发送的数据是 Linux 指令,这些其实都可以称作应用层协议了,只是比较草率。协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢? 例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算,最后再把结果返回给客户端

约定方案一:

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;

约定方案二:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串, 这个过程叫做 "序列化 " 。接收到数据的时候再按照相同的规则把字符串转化回结构体;这个过程叫做 "反序列化"。
cpp 复制代码
// proto.h 定义通信的结构体
typedef struct Request {
	int a;
	int b;
} Request;

typedef struct Response {
	int sum;
    int code; // 错误码
} Response;

// client.c 客户端核心代码
Request request;
Response response;

scanf("%d,%d", &request.a, &request.b);
write(fd, request, sizeof(Request));
read(fd, response, sizeof(Response));

// server.c 服务端核心代码
Request request;
read(client_fd, &request, sizeof(request));
Response response;
response.sum = request.a + request.b;
write(client_fd, &response, sizeof(response));
  • BUG:由于结构体的内存对齐问题,同一个结构体,在不同的编译器编译后,大小可能不一样! VS中默认对齐数为 8

    Linux gcc : 没有默认对齐数 , 对齐数就是结构体成员的自身大小

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是ok的. 这种约定, 就是 应用层协议

TCP 套接字也是文件,也可以用 write/read 进行数据的收发。write 仅仅负责将数据写入(拷贝到)内核的 TCP 协议的发送缓冲区,用 write 写入后并不代表数据已经发送到其他人,TCP 协议既然叫"传输控制协议",数据什么时候发送、发送多少数据、出错后如何处理,全部由 TCP 协议控制,用户不必操心。而用 read 读取的数据,读取的是否是一个完整的请求,是否是多个请求(用户向服务器一次发送多个请求,read 一次性将接收缓冲区全部读取),就完全不确定了,需要在应用层定制协议解决

例子 - 网络计算器

用基于 TCP 协议的服务器实现一个简易的网络计算器,目的是比较详细的展示序列化和反序列化的过程,以便更好的理解序列化和反序列化。

  • 用类封装套接字,避免代码冗余
  • 为了使代码简单,服务器采用多进程的方式提供访问,即父进程 accept,子进程提供服务。
  • 序列化协议定制:假设定义的协议是:Requst 结构体序列化之后是"有效载荷的长度+\n+"a op b"+\n" ,比如用户输入 "10 + 20" 序列化之后就是"7\n10 + 20\n",如果是多个报文,比如 "7\n10 + 20\n7\n10 + 20\n7\n10 + 20\n",这样 read 就可以识别是否是完整报文(下文说明)。选择 \n 作为分隔符的原因是有效载荷不可能出现 \n,以及提高调试时的可读性。协议的定制是十分灵活的,比如上面有效载荷之后可以带两个\n,更加提高调试时的可读性,在"有效载荷的长度"之前,还可以加上用数字表示的"协议类型",使得网络计算器不仅可以处理整型还可以处理浮点数等。现在我们假设 Requst 结构体序列化之后是"有效载荷的长度+\n+"a op b"+\n"。同样的道理,Response 结构体序列化之后的格式可以是:"有效载荷的长度+\n+"result code"。序列化时,将有效载荷序列化(requst -> "x op y" 由下面的成员函数 Serialize 完成)和添加报头("x op y" -> len\n"x op y"\n,由下面的 Encode 函数完成)的工作分开。
  • 反序列化协议定制:现在我们假设服务器收到一个请求,如果是完整的请求,一定是形如 len\n"x op y"\n 的形式的。将报文的有效载荷与报头分离,由下面的 Dedode 函数完成,Dedode 函数会根据 len 和 len 的长度检查是否是完整报文。将有效载荷解析成实际数据类型的工作由 request 的 Deserialize 成员函数完成。
  • 服务器如何读取报文:如果客户端只向服务器发送了半个请求、一个请求或多个请求,服务器该如何处理?解决方法是服务器的 inbuffer 采用追加的方式读入请求。如果客户端只向服务器发送了半个请求:解析失败,返回空字符串,服务器继续读取请求;如果客户端向服务器发送一个或多个请求:解析成功,服务器向客户端发送序列化后的 response 字符串,并且 inbuffer erase 处理过的请求,直到 inbuffer 只剩半个请求或为空,返回空字符串,服务器继续读取请求。

Protocol.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

// #define MySelf 1

const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";

// 该函数给有效载荷添加报头形成报文
std::string Encode(std::string &content) content:有效载荷
{
    // 添加有效载荷长度
    std::string package = std::to_string(content.size());

    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}

// 该函数将报文的有效载荷提取到 content
bool Decode(std::string &package, std::string *content)
{
    // 提取有效载荷长度
    std::size_t pos = package.find(protocol_sep);
    if(pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);
    std::size_t len = std::stoi(len_str);
    
    // 检查是否是完整报文
    // 现在知道 len 的长度和有效载荷的长度
    // 那么整个报文的长度应该是 len 的长度 + 有效载荷的长度 + 2
   
    std::size_t total_len = len_str.size() + len + 2;
    if(package.size() < total_len) return false;

    *content = package.substr(pos+1, len);
    
    // earse 移除刚刚成功解析的报文 
    package.erase(0, total_len);

    return true;
}


// json, protobuf
class Request
{
public:
    Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper)
    {}
    Request()
    {}
public:
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        
        // 构建报文的有效载荷
        // struct => string, "x op y"
        
        std::string s = std::to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += std::to_string(y);
        *out = s;
        
        return true;
#else
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        
        return true;
#endif
    }
    bool Deserialize(const std::string &in) // "x op y"
    {
#ifdef MySelf

        // 提取 x
        std::size_t left = in.find(blank_space_sep);
        if (left == std::string::npos) return false;
        std::string part_x = in.substr(0, left);

        // 提取 y
        std::size_t right = in.rfind(blank_space_sep);
        if (right == std::string::npos) return false;
        std::string part_y = in.substr(right + 1);

        // 提取 op
        // op 一定是单个字符
        if (left + 2 != right) return false;
        op = in[left + 1];
        
        x = std::stoi(part_x);
        y = std::stoi(part_y);
        
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        x = root["x"].asInt();
        y = root["y"].asInt();
        op = root["op"].asInt();
        
        return true;
#endif
    }
    void DebugPrint()
    {
        std::cout << "新请求构建完成:  " << x << op << y << "=?" << std::endl;
    }
public:
    // x op y
    int x;
    int y;
    char op; // 支持 + - * / %
};

class Response
{
public:
    Response(int res, int c) : result(res), code(c)
    {}

    Response()
    {}
public:
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        
        // "result code"
        // 构建报文的有效载荷
        
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += std::to_string(code);
        *out = s;
        
        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif
    }
    bool Deserialize(const std::string &in) // "result code"
    {
#ifdef MySelf
        std::size_t pos = in.find(blank_space_sep);
        if (pos == std::string::npos) return false;
        
        std::string part_result = in.substr(0, pos);
        std::string part_code = in.substr(pos+1);

        result = std::stoi(part_result);
        code = std::stoi(part_code);

        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        result = root["result"].asInt();
        code = root["code"].asInt();
        return true;
#endif

    }
    void DebugPrint()
    {
        std::cout << "结果响应完成, result: " << result << ", code: "<< code << std::endl;
    }
public:
    int result;
    int code; // 错误码: 0 表示可信,否则表明对应的错误原因
};

上面的代码已经改成 json 的版本,下面讲解如何使用 json

json

  • 上面手写序列化和反序列化的逻辑太麻烦了,有没有现成的方案可以用呢?当然有,比如 json,不过使用 json 之前要先安装第三方库:sudo yum install -y jsoncpp-devel,安装完成后验证安装:
bash 复制代码
[hxh@VM-16-12-centos ~]$ ls /usr/include/jsoncpp
json
[hxh@VM-16-12-centos ~]$ ls /usr/include/jsoncpp/json
assertions.h  autolink.h  config.h  features.h  forwards.h  json.h  reader.h  value.h  version.h  writer.h
[hxh@VM-16-12-centos ~]$ ls /lib64/libjsoncpp.so -l
lrwxrwxrwx 1 root root 15 Mar 29 13:48 /lib64/libjsoncpp.so -> libjsoncpp.so.0

使用方法:

cpp 复制代码
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
    /////// 序列化 ////////

    Json::Value root;
    // Json::Value 是 jsoncpp 中表示 JSON 数据的核心类
    // 它可以表示 JSON 中的任何类型:对象、数组、字符串、数字、布尔值、null,甚至其他的 Json
    // root 默认是一个空的 JSON 对象 {}

    // 添加 键值对
    root["x"] = 100;
    root["y"] = 200;
    root["op"] = '+';
    root["desc"] = "this is a + oper";

    // 此时 root 对应的 JSON 结构为:
    // {
    //     "x" : 100,
    //     "y" : 200,
    //     "op" : "+",
    //     "desc" : "this is a + oper"
    // }

    Json::FastWriter w;
    // Json::FastWriter 是 jsoncpp 提供的序列化器之一
    // 特点:生成紧凑格式的 JSON 字符串(无缩进、无换行)
    // 输出是单行字符串,适合网络传输或存储
    //新版 jsoncpp (1.9.0+) 推荐使用 StreamWriterBuilder 替代 FastWriter


    std::string res = w.write(root);
    // write() 方法将 Json::Value 对象转换为 JSON 字符串
    // 返回类型是 std::string
    // 这个操作就是序列化(Serialization):将内存中的数据结构转换为可存储/传输的字符串格式

    std::cout << res << std::endl;
    //输出:{"x":100,"y":200,"op":"+","dect":"this is a + oper"}

    //如果换成其他 Writer,输出格式会不同:
    Json::StyledWriter sw; //  (格式化输出)
    res = sw.write(root);
    std::cout << res << std::endl;
    // 输出:
    // {
    //     "x" : 100,
    //     "y" : 200,
    //     "op" : "+",
    //     "desc" : "this is a + oper"
    // }

    /////// 反序列化 ////////

    Json::Value v;
    // 创建一个空的 Json::Value 对象 v
    // 这个对象将作为容器,存储解析后的 JSON 数据
    // 初始状态是空对象 {}

    Json::Reader r;
    // Json::Reader 是 jsoncpp 中的解析器类
    // 负责将 JSON 格式的字符串解析为 Json::Value 对象
    // 注意:在新版 jsoncpp 中,Json::Reader 已被标记为废弃,推荐使用 Json::CharReader
    
    r.parse(res, v);
    // parse() 方法执行反序列化操作
    // 参数1 res:输入型参数,要解析的 JSON 字符串(假设是之前序列化得到的字符串)
    // 参数2 v:输出型参数,解析结果的存储位置(引用传递)
    // 返回值:bool 类型,成功返回 true,失败返回 false(此处未检查返回值)

    int x = v["x"].asInt();
    int y = v["y"].asInt();
    char op = v["op"].asString()[0];
    std::string desc = v["desc"].asString();
    Json::Value temp = v["test"];

    std::cout << x << std::endl;
    std::cout << y << std::endl;
    std::cout << op << std::endl;
    std::cout << desc << std::endl;

    return 0;
}

HTTP 协议

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

认识 URL

平时我们俗称的 "网址" 其实就是说的 URL,全称统一资源定位符,一个完整的 URL,应该包括:

  • 注意:现在的 URL 的登陆信息和服务器端口号通常省略。带层次的文件路径的第一个 / 是 web 根目录(不一定是 LInux 的根目录)。
  • 问题:在浏览器访问服务器时(在 url 输入框输入),为什么只输入了 IP 地址,就能访问?原来浏览器默认使用 http 协议,在该协议中,会如果不输入端口号,会缺省访问服务器的 80 端口号(https 缺省访问 443 端口号)。输入 IP 地址后,自动转化成 http://IP地址/。也可以在 IP 地址后面加上 :(冒号)后跟指定端口号。还可以输入指定的路径。比如 IP 地址:端口号/a/b/c。
  • urlencode和urldecode:在 URL 中,有些字符有特定功能,比如 & 是分隔符,还有 :+ / 等等,如果在浏览器搜索栏(不是 URL 的输入框,这里的搜索栏比如是百度的搜索栏)搜索时,故意搜索这些特殊字符,会出现什么现象呢?比如搜索 aaaa:+/bbbb,搜索结果的 URL 是 https://www.baidu.com/s?ie=utf-8\&f=8\&rsv_bp= 1& rsv_idx=1&tn =68018901_58_ oem_d g&wd=aaaa%3A%2B%2Fbbbb&fenlei=256&,注意到 : + / 被转化成了 %3A、%2B、%2F。像 / ? : 等这样的字符, 已经被 url 当做特殊意义理解了. 因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.转义的规则: 将需要转码的字符转为 16 进制,然后从右到左,取 4 位(不足 4 位直接处理),每 2 位做一位,前面加上 %,编码成 %XY 格式。结论:如果在搜索时出现特殊字符,会先进行 encode,然后服务器 decode。

http 协议格式

HTTP 协议(HyperText Transfer Protocol)的格式分为请求消息响应消息 两种,它们的结构类似,都由起始行头部字段空行消息体四部分组成。它们共同遵守的规则有:

  • 换行符 :每行以 \r\n(CRLF)结束,空行就是一个单独的 \r\n

  • ASCII 字符:方法名、URI、版本、头部字段名必须是 ASCII 字符

  • 大小写:方法名和头部字段名不区分大小写(通常大写),但 URI 和消息体内容区分

HTTP 请求消息格式

bash 复制代码
<方法> <请求URL> <http版本>
<头部字段1>: <值>
<头部字段2>: <值>
...
<空行>
[消息体]
  • 第一行:起始行

由三个部分组成,每个部分用空格隔开。

<方法> :GET、POST、PUT、DELETE 等,但最常见的是 GET,POST 方法

方法 用途 安全性 幂等性 可缓存 请求体 响应体
GET 获取资源 ✅ 安全 ✅ 幂等 ✅ 可缓存 ❌ 无 ✅ 有
POST 创建资源 ❌ 不安全 ❌ 不幂等 ❌ 不可缓存 ✅ 有 ✅ 有
PUT 完整更新资源 ❌ 不安全 ✅ 幂等 ❌ 不可缓存 ✅ 有 ⚠️ 可能有
PATCH 部分更新资源 ❌ 不安全 ❌ 不幂等 ❌ 不可缓存 ✅ 有 ⚠️ 可能有
DELETE 删除资源 ❌ 不安全 ✅ 幂等 ❌ 不可缓存 ⚠️ 可能有 ⚠️ 可能有
HEAD 获取元数据 ✅ 安全 ✅ 幂等 ✅ 可缓存 ❌ 无 ❌ 无
OPTIONS 查询支持的方法 ✅ 安全 ✅ 幂等 ❌ 不可缓存 ⚠️ 可能有 ✅ 有
CONNECT 建立代理隧道 ❌ 不安全 ❌ 不幂等 ❌ 不可缓存 ⚠️ 可能有 ❌ 无
TRACE 回显请求(调试) ✅ 安全 ✅ 幂等 ❌ 不可缓存 ❌ 无 ✅ 有

GET 方法:

特点

  • 只读操作,不应产生副作用

  • 参数通过 URL 传递(有长度限制)

  • 可被缓存

  • 可被书签保存

POST 方法

特点

  • 数据在请求体中

  • 可能产生副作用

  • 不幂等(多次请求会产生多个资源)

  • 不会被缓存

用户向服务器发送数据,通常是通过表单发送的。表单就是网页的各种输入框或者按钮。

表单按提交方式可以分为:

类型 method 属性 特点 适用场景
GET 表单 method="GET" 将用户通过表单输入的数据附在 URL 后,可见、可缓存、有长度限制(用 ?分割用户要访问的文件路径和输入的数据,用 & 分割不同的数据项),如果用户要访问的文件是可执行程序,就可以用程序替换的方式执行,用管道等 IPC 技术将用户输入的数据发送给可执行程序。由于该方法将数据回显在 URL 中,所以该方法不私密,让人觉得不安全。 搜索、筛选、查询
POST 表单 method="POST" 将用户通过表单输入的数据放在在消息体中,不可见、无长度限制。由于该方法不将数据回显在 URL,所以让人觉得更私密一些。但其实 GET 和 POST 方法都是不安全的,因为它们都没有对数据进行加密。 登录、注册、上传、修改数据

<请求URL> :资源路径,如 /index.htmlhttp://example.com/index.html 可以携带参数如 usrname=123&&password=456

<http 版本>HTTP/1.0HTTP/1.1HTTP/2.0HTTP/3.0

版本 发布年份 核心特点 当前使用情况
HTTP/0.9 1991 只有 GET,无头部,无状态码 已废弃
HTTP/1.0 1996 增加方法、头部、状态码 基本废弃
HTTP/1.1 1997 持久连接、管道化、分块传输 广泛使用
HTTP/2.0 2015 二进制分帧、多路复用、服务器推送 日益普及
HTTP/3.0 2022 基于 UDP (QUIC),更快连接 逐步采用
  • 接下来的行:头部字段

<头部字段>、<值>:它们是 k-v 结构,格式是:<头部字段>: <值>

头部字段 作用 示例
Host 服务器域名和端口(HTTP/1.1 必需) Host: example.com:8080
User-Agent 客户端信息(客户的OS信息,使用的什么浏览器等等,反爬虫相关) User-Agent: curl/7.68.0
Content-Type 消息体数据类型,每种文件类型都对应一个 Content-Type,比如.html对应 text/html,png对应image/png,更多映射关系可以查看Content-Type映射表 Content-Type: application/json
Content-Length 消息体字节数 Content-Length: 348
Accept 客户端可接收的响应类型 Accept: text/html,application/json
Connection 是长链接还是短链接 Connection: keep-alive
location 重定向。搭配3xx状态码使用, 告诉客户端接下来要去哪里访问 location:https://www.XXX.com
Set-Cookie 用于在客户端存储登陆信息,只要一次登陆(用户向服务器发送 POST 请求,服务器查询数据库发现你曾经注册过,返回的 http 响应有 location 重定向到首页,还有 Set-Cookie 消息)。下一次自动登陆(客户端接收到服务器的登陆响应,记住 Set-Cookie,下一次请求时添加头部字段Set-Cookie,用户无需再次登陆)Set-Cookie 的内容存储在 cookie 文件中, cookie 文件分为内存极和文件级 Set-Cookie:usrname=hxh&&password=1234

<空行> :一个单独的 \r\n,用于分隔报头和有效载荷

问题:我怎么知道我读取到的是一个完整的报文?答案:我只要读到了空行,说明我读取完一个报文的报头,根据报头的大小和报头的 Content-Length字段算出该报文的完整大小(与上面网络计算器的协议相似)

  • 最后一行:消息体 (有效载荷)

[消息体]:用户要上传的数据(可以没有,比如用户只想获取数据)

HTTP 响应消息格式

cpp 复制代码
<http版本> <状态码> <原因短语>
<头部字段1>: <值>
<头部字段2>: <值>
...
<空行>
[消息体]

HTTP 响应消息格式与请求消息格式十分相似,不同的是:

<状态码>:服务器对客户端请求的响应结果的三位数字代码,常见如 200(OK)、404(Not Found)、403(Forbidden,无权限)、302(Redirect,重定向),504(Bad Gateway等待服务器超时)

常见状态码:

状态码 含义 常见场景
200 OK 成功响应
301 永久移动 域名迁移、HTTPS强制
302 临时移动 临时跳转、登录后重定向
304 未修改 缓存复用
400 错误请求 JSON格式错误、参数缺失
401 未授权 未登录、Token过期
403 禁止访问 无权限
404 未找到 路径错误、资源删除
429 过多请求 接口限流
500 服务器内部错误 代码异常、数据库连接失败
502 网关错误 Nginx/PHP-FPM通信失败
503 服务不可用 停机维护、负载过高

<原因短语>:状态码的文字描述,如 "OK"、"Not Found"

[消息体]:服务器响应客户的请求,返回资源,比如有html/css/js/图片/音频等等。

用 telnet 指令向 www.baidu.com 发送一个最简单的请求(方法:Get,请求 URL:/ 即百度首页,版本:HTTP/1.1,头部字段和消息体全为空),观察发送回来的响应信息:

长连接与短链接

短链接:

工作流程:

每次HTTP请求/响应都需要重新建立和断开TCP连接。

  1. 客户端发起TCP三次握手 → 建立连接

  2. 客户端发送HTTP请求 → 服务器返回HTTP响应

  3. 客户端或服务器发起TCP四次挥手 → 断开连接

  4. 下一个请求,重复步骤1-3

特点:

  • 开销大:每个请求都有额外的TCP握手(1个RTT)和挥手开销。

  • 并发限制:浏览器通常限制同一域名下短连接的最大并发数(如6-8个)。

  • 简单:服务器处理简单,连接用完即关,不占用过多内存。

适用场景:

  • 早期HTTP/1.0(默认行为)。

  • 请求频率极低的应用,比如文件上传/下载(下载完即断开)。

  • 服务器资源非常受限的环境。

长链接

工作流程:

一个TCP连接上可以连续发送多个HTTP请求/响应,直到任意一方主动断开。

  1. 客户端发起TCP三次握手 → 建立连接

  2. 客户端发送HTTP请求1 → 服务器返回HTTP响应1

  3. 客户端发送HTTP请求2 → 服务器返回HTTP响应2 (复用同一连接

  4. ...

  5. 空闲一段时间后(超时),或一方发送 Connection: close,才断开连接。

比如客户端向京东服务器申请访问首页,服务器响应请求,发送了首页的 html 文件,客户端(浏览器)根据该 html 文件又连续向京东服务器发送访问商品图片、视频的请求,最终一个包含文字、图片、视频的京东首页呈现。

特点:

  • 性能高:减少多次握手和慢启动时间,页面加载速度更快。

  • 降低延迟:后续请求无需等待建立新连接。

  • 资源占用:服务器需要维持连接状态,占用内存和文件描述符。

  • 队头阻塞(Head-of-Line Blocking):在HTTP/1.1中,请求必须串行处理,一个慢响应会阻塞后续请求。

如何启用:

  • HTTP/1.0 :需要在头部字段中添加 Connection: keep-alive(非标准但广泛支持)。

  • HTTP/1.1 :默认启用长连接,除非显式指定 Connection: close

  • HTTP/2 / HTTP/3:基于长连接设计,进一步解决了队头阻塞问题(多路复用)。

适用场景:

  • 现代网页浏览(一个页面需要请求HTML、CSS、JS、图片等几十个资源)

  • RESTful API 调用(频繁的客户端-服务器通信)。

  • 需要保持会话状态的场景。

html 文件

当使用浏览器请求一个网页时,服务器返回的 HTTP 响应中,消息体通常就是 HTML 文件的内容:

html 复制代码
<!DOCTYPE html>           <!-- 文档类型声明,表示使用 HTML5 -->
<html lang="zh-CN">       <!-- 根元素,lang 属性声明语言 -->
<head>                    <!-- 头部:元数据,不显示在页面中 -->
    <meta charset="UTF-8">        <!-- 字符编码声明 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>页面标题</title>       <!-- 浏览器标签页显示的标题 -->
    <link rel="stylesheet" href="style.css">  <!-- 引入 CSS -->
</head>
<body>                    <!-- 主体:页面可见内容 -->
    <header>头部内容</header>
    <main>主要内容</main>
    <footer>底部内容</footer>
    
    <script src="script.js"></script>  <!-- 引入 JavaScript -->
</body>
</html>

常见 HTML 元素

元素 作用 示例
<h1>~<h6> 标题 <h1>一级标题</h1>
<p> 段落 <p>这是一段文字</p>
<a> 超链接 <a href="https://example.com">链接</a>
<img> 图片 <img src="photo.jpg" alt="描述">
<div> 块级容器 <div class="container">
<span> 行内容器 <span class="highlight">高亮</span>
<ul>/<ol> 列表 <ul><li>项目1</li></ul>
<form> 表单 <form action="/submit" method="post">
<input> 输入框 <input type="text" name="username">

简易 http 服务器

下面实现一个简易的 http 服务器,理解 http 请求到 http 响应的整个大致过程:

  • 在当前目录创建一个名为 wwwroot 的目录,作为 web 根目录,用户指定访问 /a/b/c 目录,经过服务器的拼接,变成访问 wwwroot/a/b/c 目录,但用户认为 / 就是 web 根目录。用户如果要访问 web 根目录,难道我们要把 wwwroot 的所有内容都返回给用户吗?当然不是,我们只需给用户返回首页的 html 文件即可。
  • wget 指令:一个强大的命令行下载工具,支持 HTTP、HTTPS、FTP 等协议,可以递归下载、断点续传、后台下载等
bash 复制代码
# 下载单个文件
wget https://example.com/file.zip

# 指定保存文件名
wget -O myfile.zip https://example.com/file.zip

# 指定保存目录
wget -P /path/to/dir https://example.com/file.zip

# 例子:
wget www.baidu.com
# 当前目录下载了一个 index.html ,也就是百度的首页文件
  • 使用我们封装的 tcp socket 接口,避免代码冗余
  • Http_server.hpp 的 HttpRequest 的成员函数 Parse 使用了 stringstrem:stringstream 是 C++ 标准库中用于字符串流处理的类,定义在 <sstream> 头文件中。它可以将字符串作为流进行输入输出操作,类似于 cincout,但操作对象是字符串
cpp 复制代码
#include <sstream>
#include <iostream>
#include <string>

// 创建 stringstream 对象
std::stringstream ss;

// 写入数据
ss << "Hello " << 42 << " " << 3.14;

// 读取数据 默认以空格为分隔符
std::string str;
int num;
double pi;
ss >> str >> num >> pi;

std::cout << str << ", " << num << ", " << pi << std::endl;
// 输出: Hello, 42, 3.14
  • 下面的代码中我们使用 8080 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口, 但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号

Http_server.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>


#include "Socket.hpp"
#include "Log.hpp"

const std::string wwwroot = "./wwwroot"; // web 根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html"; // 首页

static const int defaultport = 8082;

class HttpServer;

class ThreadData
{
public:
    ThreadData(int fd, HttpServer* s) : sockfd(fd), svr(s)
    {}

public:
    int sockfd;
    HttpServer* svr;
};

class HttpRequest
{
public:
    // 初步解析
    void Deserialize(std::string req)
    {
        // 将起始行和头部字段按行为单位读取到 req_header(string数组) 中
        while (true)
        {
            std::size_t pos = req.find(sep);
            if (pos == std::string::npos) break;

            std::string temp = req.substr(0, pos);
            if (temp.empty()) break;

            req_header.push_back(temp);

            // 读取一行就删除一行
            req.erase(0, pos + sep.size());
        }

        //剩下的就是消息体(应该用Content_Lenth,这里从简)
        text = req;
    }

    // 进一步解析
    void Parse()
    {
        std::stringstream ss(req_header[0]);
        ss >> method >> url >> http_version;

        file_path = wwwroot; // ./wwwroot
        if (url == "/" || url == "/index.html") {

            // 用户想要访问首页
            file_path += "/";
            file_path += homepage; // ./wwwroot/index.html
        }
        else file_path += url; // /a/b/c/d.html->./wwwroot/a/b/c/d.html

        // 解析出文件后缀
        auto pos = file_path.rfind(".");
        if (pos == std::string::npos) suffix = ".html"; // 如果没有后缀,默认是 .html 文件
        else suffix = file_path.substr(pos);
    }

    // 打印解析后的结果
    void DebugPrint()
    {
        for (auto& line : req_header)
        {
            std::cout << "--------------------------------" << std::endl;
            std::cout << line << "\n\n";
        }

        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << file_path << std::endl;
        std::cout << text << std::endl;
    }
public:

    // 初步解析的结果
    std::vector<std::string> req_header; // 起始行 + 头部字段
    std::string text; // 消息体

    // 进一步解析之后的结果
    std::string method;
    std::string url;
    std::string http_version;
    std::string file_path; // 用户要访问的文件路径

    std::string suffix; // 文件后缀
};

class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport) : port_(port)
    {
        content_type.insert({ ".html", "text/html" });
        content_type.insert({ ".png", "image/png" });
    }
    bool Start()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        for (;;)
        {
            std::string clientip;
            uint16_t clientport;

            int sockfd = listensock_.Accept(&clientip, &clientport);
            if (sockfd < 0) continue;

            lg(Info, "get a new connect, sockfd: %d", sockfd);

            pthread_t tid;
            ThreadData* td = new ThreadData(sockfd, this);
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }
    static std::string ReadHtmlContent(const std::string& htmlpath)
    {
        // 坑
        std::ifstream in(htmlpath, std::ios::binary); // binary:以二进制方式读取
        if (!in.is_open()) return ""; // 资源不存在

        // 获取文件大小
        in.seekg(0, std::ios_base::end);
        auto len = in.tellg();
        in.seekg(0, std::ios_base::beg);

        std::string content;
        content.resize(len);

        in.read((char*)content.c_str(), content.size());

        in.close();

        return content;
    }
    std::string SuffixToDesc(const std::string& suffix)
    {
        auto iter = content_type.find(suffix);

        if (iter == content_type.end()) return content_type[".html"]; // 找不到默认是 html 文件
        else return content_type[suffix];
    }
    void HandlerHttp(int sockfd)
    {
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // bug
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
            // 假设我们读取到的就是一个完整的,独立的 http 请求

            HttpRequest req;
            req.Deserialize(buffer);
            req.Parse();
            //req.DebugPrint();

            // 返回响应的过程

            std::string text; // 用户要访问的 html 文件

            bool ok = true;

            text = ReadHtmlContent(req.file_path); // 失败?
            if (text.empty()) // 资源不存在
            {
                ok = false;
                std::string err_html = wwwroot;
                err_html += "/";
                err_html += "err.html"; // 我们制作的 404 Not Found 页面
                text = ReadHtmlContent(err_html);
            }

            // 构建起始行
            std::string response_line;
            if (ok)
                response_line = "HTTP/1.0 200 OK\r\n";
            else
                response_line = "HTTP/1.0 404 Not Found\r\n";

            // 构建头部字段
            std::string response_header = "Content-Length: " + std::to_string(text.size()) + "\r\n";
            response_header += "Content-Type: " + SuffixToDesc(req.suffix) + "\r\n";
            response_header += "Set-Cookie: name=haha" + "\r\n";
            /*发送多个cookie,应该分别发送*/
            response_header += "Set-Cookie: password=1234" + "\r\n";
            // 往后客户使用浏览器再次访问,浏览器会自动打包所有cookie:Set-Cookie: name=haha; password=1234
            // 然后发送给服务器

            // 空行
            std::string blank_line = "\r\n";

            // 构建 http 响应消息
            std::string response = response_line;
            response += response_header;
            response += blank_line;
            response += text;

            send(sockfd, response.c_str(), response.size(), 0);
        }
        close(sockfd);
    }
    static void* ThreadRun(void* args)
    {
        pthread_detach(pthread_self());

        ThreadData* td = static_cast<ThreadData*>(args);
        td->svr->HandlerHttp(td->sockfd);
        delete td;
        return nullptr;
    }
    ~HttpServer()
    {}

private:
    Sock listensock_;
    uint16_t port_;
    std::unordered_map<std::string, std::string> content_type; // 映射表
};

HTTPS 协议

HTTP 协议内容都是按照⽂本的⽅式明⽂传输的. 这就导致在传输过程中出现⼀些窃听、篡改的情况.HTTPS 也是⼀个应⽤层协议. 只是在 HTTP 和 TCP 这一层之间,加了一个安全层(SSL/TLS 协议,有加密解密功能),确保数据在网络上传输时是加密的.

特性 HTTP HTTPS
传输安全 明文传输,可被窃听 密文传输,不可直接读取
身份验证 无法验证服务器身份,易被钓鱼 通过CA数字证书验证,确保是真实网站
数据完整性 数据在传输中可被篡改而不被发现 防止数据被篡改,一旦被改会被发现
默认端口 80 443
协议基础 直接基于 TCP 先进行 TLS/SSL 握手,再基于 TCP
SEO与浏览器标识 浏览器会显示"不安全" 浏览器显示安全锁🔒,利于搜索排名

加密与解密

  • 加密就是把明文 (要传输的信息)进行⼀系列变换, ⽣成密文 .
  • 解密就是把密文 再进行⼀系列变换, 还原成明文
  • 在这个加密和解密的过程中, 往往需要⼀个或者多个中间的数据, 辅助进行 这个过程, 这样的数据称为密钥

为什么要加密和解密?例子:中间人攻击

  • 从前有个叫做"天天动听"的软件,在浏览器点击下载它时,弹出的下载链接是"QQ浏览器"的下载链接。由于我们通过⽹络传输的任何的数据包都会经过运营商的⽹络设备(路由器, 交换机等), 那么运营商的⽹络设备就可以解析出你传输的数据内容, 并进⾏篡改.点击 "下载按钮", 其实就是在给服务器发送了⼀个 HTTP 请求, 获取到的 HTTP 响应其实就包含了该APP 的下载链接. 运营商劫持之后, 就发现这个请求是要下载天天动听, 那么就⾃动的把交给⽤⼾的响应给篡改成 "QQ浏览器" 的下载地址了
  • 所以:因为http的内容是明⽂传输的,明⽂数据会经过路由器、wifi热点、通信服务运营商、代理服务器等多个物理节点,如果信息在传输过程中被劫持,传输的内容就完全暴露了。劫持者还可以篡改传输的信息且不被双⽅察觉,这就是中间⼈攻击 ,所以我们才需要对信息进⾏加密

常见加密方式

对称加密

  • 同⼀个密钥可以同时⽤作信息的加密和解密,这种加密⽅法称为对称加密,也称为单密钥加密,特征:加密和解密所⽤的密钥是相同的
  • 常⻅对称加密算法(了解):DES、3DES、AES、TDEA、Blowfish、RC2等
  • 特点:算法公开、计算量⼩、加密速度快、加密效率⾼

非对称加密

  • 需要两个密钥来进⾏加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。既可以公钥加密私钥解密,也可以公钥解密私钥加密。公钥解密私钥加密:拥有私钥的人可以加密,但谁都可以解密(无意义)。公钥加密私钥解密:谁都可以加密,但是只有拥有私钥的人才能解密。公钥和私钥只是用来区分该密钥是被公开还是保密的名称,假设有两个密钥 A 和 B,A 和 B 都可以对数据进行加密和解密,但 A 加密的数据只能由 B 解密,B 加密的数据只能由 A 解密。如果 A 公开,那么 A 就叫公钥,否则就叫密钥。 A 成为公钥还是 B 成为公钥都可以,只要保证一个公开一个保密即可。
  • 常⻅⾮对称加密算法(了解):RSA,DSA,ECDSA
  • 特点:算法强度复杂、安全性依赖于算法与密钥,但是由于其算法复杂,⽽使得加密解密速度没有对称加密解密的速度快。

数据摘要

  • 数字摘要(也叫数据指纹),其基本原理是利⽤单向散列函数(Hash函数)对信息进⾏运算,⽣成⼀串具有唯一性的固定⻓度的数字指纹(字符串)。哪怕是修改原文的一个标点符号,生成的数据指纹差别都会非常大。数字指纹用来判断数据有没有被篡改
  • 摘要常⻅算法:有MD5、SHA1、SHA256、SHA512等,算法把⽆限的映射成有限,因此可能会有碰撞(两个不同的信息,算出的摘要相同,但是概率⾮常低)
  • 摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,因为不能从摘要反推原信息,通常⽤来进⾏数据对⽐
  • 其他应用:去重。比如实现网盘"秒传"功能,当一个用户上传资源,该资源生成的数据指纹已经在数据库中存在了,此时直接复用该资源。

加密方案

加密的⽅式有很多, 但是整体可以分成两⼤类: 对称加密 和 ⾮对称加密,直接讲解现成的成熟方案无法理解该方案形成的前因后果,我们先自己设计加密方案,通过不断的改进,最终接近成熟方案

方案 1 - 只使用对称加密

  • 如果通信双⽅都各⾃持有同⼀个密钥X,且没有别⼈知道,这两⽅的通信安全当然是可以被保证的(除⾮密钥被破解)引⼊对称加密之后, 即使数据被截获, 由于⿊客不知道密钥是啥, 因此就⽆法进⾏解密, 也就不知道请求的真实内容是啥了
  • 但事情没这么简单. 服务器同⼀时刻其实是给很多客⼾端提供服务的. 这么多客⼾端, 每个⼈⽤的秘钥都必须是不同的(如果是相同那密钥就太容易扩散了, ⿊客就也能拿到了). 因此服务器就需要维护每个客⼾端和每个密钥之间的关联关系, 这也是个很⿇烦的事情
  • ⽐较理想的做法, 就是能在客⼾端和服务器建⽴连接的时候, 双⽅协商确定这次的密钥是啥。但是如果直接把密钥明⽂传输, 那么⿊客也就能获得密钥了~~ 此时后续的加密操作就形同虚设了。
  • 但是要想对密钥进⾏对称加密, 就仍然需要先协商确定⼀个 "密钥的密钥". 这就成了 "先有鸡还是先有蛋" 的问题了. 此时密钥的传输再⽤对称加密就⾏不通了

方案 2 - 只使用非对称加密

  • 鉴于⾮对称加密的机制,如果服务器先把公钥以明⽂⽅式传输给客户端,之后客户端向服务器传数据前都先⽤这个公钥加密好再传,从客⼾端到服务器信道似乎是安全的(其实有安全问题),因为只有服务器有相应的私钥能解开公钥加密的数据。
  • 但是服务器到客户端的这条路怎么保障安全?因为客户端只有公钥,它只能解密用私钥加密的数据,如果服务器⽤它的私钥加密数据传给客户端,那么客户端可以⽤公钥解密它,中间人也可以⽤公钥解密它。

方案 3 - 双方都使用非对称加密

  1. 服务端拥有公钥 S 与对应的私钥 S',客⼾端拥有公钥 C 与对应的私钥 C'
  2. 客⼾和服务端交换公钥
  3. 客⼾端给服务端发信息:先⽤S对数据加密再发送,只能由服务器解密
  4. 服务端给客⼾端发信息:先⽤C对数据加密再发送,只能由客⼾端解密

这样貌似也行,但是效率太低,并且依旧有安全问题

方案 4 - 非对称加密 + 对称加密 (解决方案 3 的效率问题)

  1. 服务端具有⾮对称公钥 S 和私钥 S'
  2. 客⼾端发起 https 请求,获取服务端公钥 S
  3. 客⼾端在本地⽣成对称密钥 C, 通过公钥 S 加密, 发送给服务器.
  4. 由于中间的⽹络设备没有私钥, 即使截获了数据, 也⽆法还原出内部的原⽂, 也就⽆法获取到对称密钥(真的吗?)
  5. 服务器通过私钥 S' 解密, 还原出客⼾端发送的对称密钥 C. 并且使⽤这个对称密钥加密给客⼾端返回的响应数据.
  6. 后续客⼾端和服务器的通信都只⽤对称加密即可. 由于该密钥只有客⼾端和服务器两个主机知道, 其他主机/设备不知道密钥即使截获数据也没有意义

由于对称加密的效率⽐⾮对称加密⾼很多, 因此只是在开始阶段协商密钥的时候使⽤⾮对称加密, 后续的传输仍然使⽤对称加密.该方案已经很接近真实 https 数据加密方式了,但仍然有安全问题,

⽅案 2,⽅案 3,⽅案 4都存在⼀个问题,如果最开始,中间⼈就已经开始攻击了呢?

  1. 服务器具有⾮对称加密算法的公钥 S,私钥 S'
  2. 中间⼈具有⾮对称加密算法的公钥 M,私钥 M'
  3. 客⼾端向服务器发起请求,服务器明⽂传送公钥 S 给客⼾端
  4. 中间⼈劫持数据报⽂,提取公钥 S 并保存好,然后将被劫持报⽂中的公钥 S 替换成为⾃⼰的公钥 M,并将伪造报⽂发给客⼾端
  5. 客⼾端收到报⽂,提取公钥 M (⾃⼰当然不知道公钥被更换过了),⾃⼰形成对称秘钥C,⽤公钥 M 加密 C,形成报⽂发送给服务器
  6. 中间⼈劫持后,直接⽤⾃⼰的私钥 M' 进⾏解密,得到通信秘钥 C,再⽤曾经保存的服务端公钥 S 加密后,将报⽂推送给服务器
  7. 服务器拿到报⽂,⽤⾃⼰的私钥 S' 解密,得到通信秘钥 C
  8. 双⽅开始采⽤ C 进⾏对称加密,进⾏通信。但是⼀切都在中间⼈的掌握中,劫持数据,进⾏窃听甚⾄修改,都是可以的

上⾯的攻击⽅案,同样适⽤于⽅案2,⽅案3
问题本质出在客⼾端无法确定收到的含有公钥的数据报文,就是目标服务器发送过来的!

CA 认证

服务端在使⽤ HTTPS 前,需要向 CA 机构申领⼀份数字证书,数字证书⾥含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书⾥获取公钥就⾏了,证书就如身份证,证明服务端公钥的权威性

CA 证书的具体内容

CA 证书可以理解成是⼀个结构化的字符串, 包含证书发布机构、证书有效期、服务器生成的公钥、证书所有者、签名等等

客户端怎么确认 CA 证书 1、是权威机构 CA 颁发的,而不是服务器伪造的 2、如果是,那么该证书是否被篡改过呢?

我们先看看 CA 证书的申请过程和它起作用的过程:

其中请求文件可以在网上自动(CSR在线生成工具)生成,我们只需要填写相关信息。形成CSR之后,后续就是向 CA 进⾏申请认证,不过⼀般认证过程很繁琐,⽹络有各种提供证书申请的服

务商,如果真的需要,直接找这些服务商即可。

数据签名

当服务端申请CA证书的时候,CA机构会对该服务端进⾏审核,并专⻔为该⽹站形成数字签名,过程如下:

  1. CA机构拥有⾮对称加密的私钥A和公钥A',(注意:这里的公钥和私钥与上面讲解的非对称加密的公钥和私钥毫无关系!它们只是名字相同罢了。CA 机构会公开自己的公钥,客户端本身内置了很多权威 CA 机构的公钥)
  2. CA机构对服务端申请的证书明⽂数据进⾏ hash,形成数据摘要
  3. 然后对数据摘要⽤CA私钥A'加密,得到数字签名S
  4. 服务端申请的证书明⽂和数字签名S 共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了

数据签名在证书生命周期的作用

客户端在成功申请服务器的证书后,通过数字签名对证书进行检查,看看证书是否被篡改。

方案 5 - 非对称加密 + 对称加密 + CA 认证(解决方案 4 的安全问题)

  1. 服务端具有 CA 证书(内含公钥 S)和私钥 S'
  2. 客⼾端发起 https 请求,获取服务端 CA 证书
  3. 客户端对 CA 证书进行认证
  4. 认证成功后,客⼾端在本地⽣成对称密钥 C, 通过公钥 S 加密, 发送给服务器.
  5. 由于中间的⽹络设备没有私钥, 即使截获了数据, 也⽆法还原出内部的原⽂, 也就⽆法获取到对称密钥(这回是真的安全了)
  6. 服务器通过私钥 S' 解密, 还原出客⼾端发送的对称密钥 C. 并且使⽤这个对称密钥加密给客⼾端返回的响应数据.
  7. 后续客⼾端和服务器的通信都只⽤对称加密即可. 由于该密钥只有客⼾端和服务器两个主机知道, 其他主机/设备不知道密钥即使截获数据也没有意义

CA 证书如何保证数据的安全

引入 CA 认证以后,如果中间人想篡改服务器发送给客户端的证书,并且不想让客户端发现,就必须将篡改后的证书重新生成数据摘要,然后将这个新的数据摘要替换旧的数据摘要,如果要替换旧的数据摘要,当然可以用 CA 的公钥解密,但是如果想让客户端成功解密,就必须用 CA 证书的私钥对数据摘要加密,而中间人在一定能力范围内是绝不可能得到 CA 的私钥的。并且客户端只认 CA 机构的公钥,不会使用其他人提供的公钥。现在中间人唯一的可能攻击方式就是申请一个真的证书,然后将服务器发送的证书整个调包,但证书含有域名信息,世界上不存在两个完全一样的域名,中间人伪造的网站与真正的网站一定有所差别,客户端有时也会发现并怀疑。如果中间人都做到如此地步了,那就与技术无关了,该受到法律的制裁了。

其他应用层协议

协议 全称/主要用途 传输层协议 端口号
HTTP/HTTPS 网页浏览(超文本传输协议/安全版本) TCP 80 / 443
FTP 文件传输(File Transfer Protocol) TCP 20(数据), 21(控制)
SMTP 电子邮件发送(Simple Mail Transfer Protocol) TCP 25
POP3 电子邮件接收(Post Office Protocol v3) TCP 110
IMAP 电子邮件接收与管理(Internet Message Access Protocol) TCP 143
DNS 域名解析(Domain Name System) UDP(通常)/ TCP 53
DHCP 动态主机配置协议(Dynamic Host Configuration Protocol) UDP 67(服务器), 68(客户端)
SSH 安全远程登录(Secure Shell) TCP 22
Telnet 不安全的远程登录 TCP 23
SNMP 网络管理(Simple Network Management Protocol) UDP 161/162
NTP 网络时间同步(Network Time Protocol) UDP 123
相关推荐
指针刺客2 小时前
网络协议之WebSocket
网络·websocket·网络协议
aaa最北边2 小时前
计算机网络-断开连接的四次挥手底层细节
java·网络·计算机网络
We་ct2 小时前
EventSource & WebSocket & HTTP
前端·javascript·网络·websocket·网络协议·http·面试
帐篷Li2 小时前
ONVIF Server 功能完善开发计划
网络·网络协议·http
门思科技2 小时前
KS31:4-20mA设备如何低成本接入LoRaWAN实现无线化改造
服务器
蘑菇小白2 小时前
网络:TCP
网络·tcp/ip·udp
青枣八神2 小时前
如何让手机访问电脑本地的前端服务器网页(Vite等前端项目)
服务器·前端·web·手机访问
习惯就好zz2 小时前
RK3588 Android 12 修改 NTP 服务器:从资源覆盖到时间同步验证
android·运维·服务器·aosp·ntp
汤愈韬2 小时前
ip-prefix(IP前缀列表)
linux·服务器·网络协议·tcp/ip