• HTTP协议1)----对于应用层的详细讲解


    ꧁ 大家好,我是 兔7 ,一位努力学习C++的博主~ ꧂

    ☙ 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步❧

    🚀 如有不懂,可以随时向我提问,我会全力讲解~💬

    🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!👀

    🔥 你们的支持是我创作的动力!⛅

    🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!⭐

    🧸 人的心态决定姿态!⭐

    🚀 本文章CSDN首发!✍

    目录

    0. 前言

    1. 应用层

    1.1 再谈 "协议"

    网络版计算器

    client.cc

    server.cc

    protocol.hpp

    1.2 HTTP协议

    1.2.1 认识URL

    1.2.2 urlencode和urldecode

    1.2.3 HTTP协议格式

    http_server.cc

    http_server.cc

    index.html

    总结:


    0. 前言

            此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

            大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

            感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

    1. 应用层

            我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。

    1.1 再谈 "协议"

            协议是一种 "约定"。socket api的接口,在读写数据时,都是按 "字符串" 的方式来发送接收的。

            如果我们要传输一些 "结构化的数据" 怎么办呢?

    网络版计算器

            例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。

    约定方案一:

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

    约定方案二:

    • 定义结构体来表示我们需要交互的信息;
    • 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体;
    • 这个过程叫做 "序列化" 和 "反序列化";

    举个QQ聊天例子:

    为什么需要序列化?
            答:方便网络发送和接收。

    为什么需要反序列化?
            答:方便上层应用程序正常使用数据。

    client.cc

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7 #include
    8. 8 #include
    9. 9 #include "protocol.hpp"
    10. 10
    11. 11 using namespace std;
    12. 12
    13. 13 int main(int argc, char *argv[])
    14. 14 {
    15. 15 if(argc != 3){
    16. 16 cout << "Usage: " << argv[0] << " server_ip server_port" << endl;
    17. 17 exit(1);
    18. 18 }
    19. 19
    20. 20 int sock = socket(AF_INET, SOCK_STREAM, 0);
    21. 21 if(sock < 0){
    22. 22 exit(2);
    23. 23 }
    24. 24
    25. 25 struct sockaddr_in peer;
    26. 26 memset(&peer, 0, sizeof(peer));
    27. 27 peer.sin_family = AF_INET;
    28. 28 peer.sin_addr.s_addr = inet_addr(argv[1]);
    29. 29 peer.sin_port = htons(atoi(argv[2]));
    30. 30
    31. 31 if(connect(sock, (struct sockaddr*)&peer, sizeof(peer)) < 0){
    32. 32 cerr << "connect failed..." << endl;
    33. 33 exit(3);
    34. 34 }
    35. 35
    36. 36 while(true){
    37. 37 Request rq;
    38. 38 cout << "输入第一个数据# ";
    39. 39 cin >> rq.x;
    40. 40 cout << "输入第二个数据# ";
    41. 41 cin >> rq.y;
    42. 42 cout << "输入你的操作[+-*/%]# ";
    43. 43 cin >> rq.op;
    44. 44
    45. 45 send(sock, &rq, sizeof(rq), 0); //结构化的数据,你怎么没有序列化呢?弱化
    46. 46 Response rsp;
    47. 47 recv(sock, &rsp, sizeof(rsp), 0);
    48. 48
    49. 49 cout <<"status: " << rsp.code << endl;
    50. 50 cout << rq.x << rq.op << rq.y << "=" << rsp.result << endl;
    51. 51 }
    52. 52 }

    server.cc

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7 #include
    8. 8 #include "protocol.hpp"
    9. 9
    10. 10 using namespace std;
    11. 11
    12. 12 void* Routinue(void* arg)
    13. 13 {
    14. 14 pthread_detach(pthread_self());
    15. 15 int sock = *(int*)arg;
    16. 16 delete (int*)arg;
    17. 17
    18. 18 while(true){
    19. 19 Request rq;
    20. 20 Response rsp = {0,0};
    21. 21 ssize_t s = recv(sock, &rq, sizeof(rq), 0);
    22. 22 if(s > 0){
    23. 23 switch(rq.op){
    24. 24 case '+':
    25. 25 rsp.result = rq.x + rq.y;
    26. 26 break;
    27. 27 case '-':
    28. 28 rsp.result = rq.x - rq.y;
    29. 29 break;
    30. 30 case '*':
    31. 31 rsp.result = rq.x * rq.y;
    32. 32 break;
    33. 33 case '/':
    34. 34 if(rq.y == 0){
    35. 35 rsp.code = 1;
    36. 36 }
    37. 37 else{
    38. 38 rsp.result = rq.x / rq.y;
    39. 39 }
    40. 40 break;
    41. 41 case '%':
    42. 42 if(rq.y == 0){
    43. 43 rsp.code = 2;
    44. 44 }
    45. 45 else{
    46. 46 rsp.result = rq.x % rq.y;
    47. 47 }
    48. 48 break;
    49. 49 default:
    50. 50 rsp.code = 3;
    51. 51 break;
    52. 52 }
    53. 53 send(sock, &rsp, sizeof(rsp), 0);
    54. 54 }
    55. 55 else if (s == 0){
    56. 56 cout << "client quit" << endl;
    57. 57 break;
    58. 58 }
    59. 59 else{
    60. 60 break;
    61. 61 }
    62. 62 }
    63. 63 return nullptr;
    64. 64 }
    65. 65
    66. 66 int main(int argc, char* argv[])
    67. 67 {
    68. 68 if(argc != 2){
    69. 69 cout << "Usage: " << argv[0] << " port" << endl;
    70. 70 return 1;
    71. 71 }
    72. 72
    73. 73 int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    74. 74 if(listen_sock < 0){
    75. 75 cerr << "listen error" << endl;;
    76. 76 return 2;
    77. 77 }
    78. 78
    79. 79 struct sockaddr_in local;
    80. 80 memset(&local, 0, sizeof(local));
    81. 81 local.sin_family = AF_INET;
    82. 82 local.sin_port = htons(atoi(argv[1]));
    83. 83 local.sin_addr.s_addr = htonl(INADDR_ANY);
    84. 84
    85. 85 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
    86. 86 cerr << "bind error" << endl;;
    87. 87 return 3;
    88. 88 }
    89. 89
    90. 90 if(listen(listen_sock, 5) < 0){
    91. 91 cerr << "listen error" << endl;
    92. 92 return 4;
    93. 93 }
    94. 94
    95. 95 struct sockaddr_in peer;
    96. 96 for(;;){
    97. 97 socklen_t len = sizeof(peer);
    98. 98 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
    99. 99 if(sock < 0){
    100. 100 continue;
    101. 101 }
    102. 102 pthread_t tid;
    103. 103 int* p = new int(sock);
    104. 104 pthread_create(&tid, nullptr, Routinue, p);
    105. 105 }
    106. 106
    107. 107 }

    protocol.hpp

    1. 1 #pragma once
    2. 2
    3. 3 struct Request{
    4. 4 int x;
    5. 5 int y;
    6. 6 char op;
    7. 7 };
    8. 8
    9. 9 struct Response{
    10. 10 int code;
    11. 11 int result;
    12. 12 };

            我先说一下,首先我们写的代码是有很多bug的,其中第一个bug就是没有做序列化和反序列化,也就是这个客户端和服务器可能在不同的平台运行,客户端可能在Windows下,服务器可能在Linux下,如果发过去的结构体就会有问题,因为我们知道结构体或者类,本身是有内存对齐的,在我们写的结构体中,有可能在Windows下的Request是12字节,而在Linux下是10或者16,这样就很容易出问题。还有就是有可能服务器再读取的时候,期望读12个字节,但是有可能先读10个字节,后两个字节没先读进来,所以这里都会有问题。

            但是这样写很容易让我们理解协议定制,仅此而已,其它的细节不过多深究。

            接下来我们看看结果:

            我们上面的 x y op,分别是运算的符号,而code:不同的数有不同的意义,代表不同的错误信息,而result则是结果,所以,我们刚才就是用我们自己方式约定出来了一套应用层的网络计算器,这就叫做协议。

    1.2 HTTP协议

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

    1.2.1 认识URL

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

            一般来说,登录信息现在是不会显示出来的,因为它现在有单独为它记录的地方。

            标识机器我们用的是公网IP,而IP不适合给人看:

    • 180.101.49.11 VS www.baidu.com
    • 109.244.211.81 VS www.qq.com

            字符串风格的域名,具有更好的自描述性。

            还有就是别人早已经写好的协议,它们通常来说都有明确的端口号,HTTP对应的是80号端口,HTTPS对应的是443号端口,而SSH对应的是22号端口。

            所以众所周知的服务和端口对应关系是明确的,所以服务器端口号是可以省略的。

            因为我们要访问这个服务器的目的是获取某种资源,所以这个路径是很重要的,也就是资源路径。

            然后后面是可以带?的,?后面是参数。


            实际上,上网的大部分行为,都在进行着进程间通信(比如用浏览器去访问NBA),既然是通信,就只有获取信息和发送信息两个方式。

            所以我们对应的生活中,大部分的上网行为,无非两种:

    1. 把服务器上面的资源数据拿到本地(刷短视频,网络小说等)
    2. 把本地的数据推送到服务器(搜索,注册,登录,下单等)

    1.2.2 urlencode和urldecode

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

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

    转义的规则如下:

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

    例如:

            "+" 被转义成了 "%2B"

            urldecode就是urlencode的逆过程;

    urlecode工具

    1.2.3 HTTP协议格式

            为了更好理解,我写了一个简单的网络服务器。

    http_server.cc

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7 #include
    8. 8 #include
    9. 9
    10. 10 int main()
    11. 11 {
    12. 12 int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    13. 13 if(listen_sock < 0){
    14. 14 std::cerr << "socket error" << std::endl;
    15. 15 return 1;
    16. 16 }
    17. 17 struct sockaddr_in local;
    18. 18 memset(&local, 0, sizeof(local));
    19. 19 local.sin_family = AF_INET;
    20. 20 local.sin_port = htons(8081);
    21. 21 local.sin_addr.s_addr = INADDR_ANY;
    22. 22
    23. 23 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
    24. 24 std::cerr << "bind error" << std::endl;
    25. 25 return 2;
    26. 26 }
    27. 27
    28. 28 if(listen(listen_sock, 5) < 0){
    29. 29 std::cerr << "listen error" << std::endl;
    30. 30 return 3;
    31. 31 }
    32. 32
    33. 33 struct sockaddr_in peer;
    34. 34 for(;;){
    35. 35 socklen_t len = sizeof(peer);
    36. 36 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
    37. 37 if(sock < 0){
    38. 38 continue;
    39. 39 }
    40. 40
    41. 41 if(fork() == 0){
    42. 42 if(fork() > 0) exit(0);
    43. 43 close(listen_sock);
    44. 44 //read http request
    45. 45 char buffer[1024];
    46. 46 recv(sock, buffer, sizeof(buffer), 0);
    47. 47 std::cout << "####### http request begin ######" << std::endl;
    48. 48 std::cout << buffer << std::endl;
    49. 49 std::cout << "####### http request end ######" << std::endl;
    50. 50 close(sock);exit(0);
    51. 51 }
    52. 52
    53. 53 close(sock);
    54. 54 waitpid(-1, nullptr, 0);
    55. 55 }
    56. 56
    57. 57
    58. 58 return 0;
    59. 59 }

            我们写的这个服务器,没有做出有效的回馈,相当于只是转发了一下,打印到了屏幕上。

              我们可以看到,我们收到了很多次hppt请求,这里解释一下,本来就只会收到一个的,但是因为我们没有相应,所以浏览器可能在一个时间段内同时向我们发送了多次请求,要么是服务器是多线程的同时向我们发送请求,要么就是没有相应进行超时重传了,所以我们就收到了大量的这样的请求。

            接下来我们细看一下:

    然后我们测试一下请求资源的路径:

            也就是我们要请求服务器上那个路径下的哪个资源。

            HTTP主要的请求方法就是GET和POST。

    response:

     

    我先说一下GET和POST:

    GET方法:

    • 直接获取对应的资源信息,比如网页。
    • GET可以带参数,url?的后面都是GET的参数

    POST方法:

    • 将数据通过正文提交给服务器(不通过url)

    接下来就来用Postman做实验:

    GET

    POST

     

    对比:

            我们发现,POST的报文里比GET多一行Content-length,而且我们看到上面的Content-length是8,正文的字符也是8个,所以这里的Content-length其实就是表示的是正文的字符,又因为GET中没有,所以就更说明了上面GET和POST中的,POST将数据提交给服务器是通过正文传参的。

            接下来我们继续写一个简单的http响应,让其响应一个网页:

    http_server.cc

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6 #include
    7. 7 #include
    8. 8 #include
    9. 9 #include
    10. 10 #include
    11. 11
    12. 12 int main()
    13. 13 {
    14. 14 int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    15. 15 if(listen_sock < 0){
    16. 16 std::cerr << "socket error" << std::endl;
    17. 17 return 1;
    18. 18 }
    19. 19 struct sockaddr_in local;
    20. 20 memset(&local, 0, sizeof(local));
    21. 21 local.sin_family = AF_INET;
    22. 22 local.sin_port = htons(8081);
    23. 23 local.sin_addr.s_addr = INADDR_ANY;
    24. 24
    25. 25 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
    26. 26 std::cerr << "bind error" << std::endl;
    27. 27 return 2;
    28. 28 }
    29. 29
    30. 30 if(listen(listen_sock, 5) < 0){
    31. 31 std::cerr << "listen error" << std::endl;
    32. 32 return 3;
    33. 33 }
    34. 34
    35. 35 struct sockaddr_in peer;
    36. 36 for(;;){
    37. 37 socklen_t len = sizeof(peer);
    38. 38 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
    39. 39 if(sock < 0){
    40. 40 continue;
    41. 41 }
    42. 42
    43. 43 if(fork() == 0){
    44. 44 if(fork() > 0) exit(0);
    45. 45 close(listen_sock);
    46. 46 //read http request
    47. 47 char buffer[1024];
    48. 48 recv(sock, buffer, sizeof(buffer), 0);
    49. 49 std::cout << "####### http request begin ######" << std::endl;
    50. 50 std::cout << buffer << std::endl;
    51. 51 std::cout << "####### http request end ######" << std::endl;
    52. 52
    53. 53 #define PAGE "index.html"
    54. 54 std::ifstream in(PAGE);//默认在构造的时候,就把文件打开了
    55. 55 if(in.is_open()){
    56. 56 in.seekg(0, std::ios::end);//文件指针指向结尾
    57. 57 size_t len = in.tellg();//获取文件长度
    58. 58 in.seekg(0, std::ios::beg);//文件指针从新指向开头
    59. 59 char* file = new char[len];
    60. 60 in.read(file, len);
    61. 61 in.close();
    62. 62
    63. 63 std::string status_line = "http/1.0 200 OK\n";
    64. 64 std::string response_header = "Content-Length: " + std::to_string(len) + "\n";
    65. 65 std::string blank = "\n";
    66. 66
    67. 67 send(sock, status_line.c_str(), status_line.size(), 0);
    68. 68 send(sock, response_header.c_str(), response_header.size(), 0);
    69. 69 send(sock, blank.c_str(), blank.size(), 0);
    70. 70
    71. 71 send(sock, file, len, 0);
    72. 72
    73. 73 delete[] file;
    74. 74 }
    75. 75 close(sock);
    76. 76 exit(0);
    77. 77 }
    78. 78
    79. 79 close(sock);
    80. 80 waitpid(-1, nullptr, 0);
    81. 81 }
    82. 82
    83. 83
    84. 84 return 0;
    85. 85 }

    index.html

    1. 1 <html>
    2. 2 <head>I am head head>
    3. 3 <body>
    4. 4 <h3>I am bodyh3>
    5. 5 <body>
    6. 6 html>

            所以其实是HTTP通过请求得到HTTP响应,然后响应的时候状态行和那一堆响应报头和空行是不会显示出来的,是由浏览器去识别的,正文是由浏览器去显示的,最后看到的就是正文所编写出来的效果,所以其实最后通信的成功是由状态行的状态码和状态描述决定的。

            这里如果会JavaScript的话可以添加HTML的表单等一系列操作,就更能看到在输入的时候提交表单的体现。

            还有就是因为用GET方法的时候,当你输入之后会立马回显到url框中,所以如果数据不敏感,可以使用GET方法,但是如果敏感的话建议使用POST,不是说POST更安全,而是更私密,因为无论是GET还是POST都是明文传送,只不过POST不会立马回显到url框中。

    总结:

    GET:获取资源,获得一个简单的文本,GET也可以传递参数,GET通过url可以传递参数

    POST:推送资源数据,通过正文传递参数

            第二个区别:GET和POST如果传参,GET通过url,POST通过正文,POST会比GET:

    1. 能传递更多的数据
    2. 更私密

            私密不推荐说成安全,因为都不安全,都是明文传送,安全的话还是需要通过加密完成的。

            还有就是HTTP是无状态的,其中第一次、第二次、第三次、第n次没有关系。

    HTTP的方法:

            其中最常用的就是GET方法和POST方法。

    HTTP的状态码:

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

    接下来主要说一下 3XX:

            其中301是永久重定向,302和307是临时重定向。

            它们本质的区别是:影响客户端标签,决定客户端是否需要更新目标地址。

    HTTP常见Header:

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

            然后我们用一下这里的location进行测试一下,我们只需要将我们上面的代码稍作修改即可。

             我们发现,我们用临时重定向完成了任务,但是因为用telnet请求,所以只是一来一回,没有对location后面的字段进行再次请求,但是如果是浏览器的话会继续提取location后面的字段再次进行请求。

    那么接下来再看一下浏览器:

            我们会看到这样一个效果。

    接下来再来讲一下Cookie:

            比方说我们用gitee的时候,当我们输入账号密码后,会有一段时间当我们打开gitee的时候直接就登录进去了,这都是因为Cookie的存在。

            当我们移除这个Cookie的时候,我们再打开gitee就会发现,我们需要重新登录,而且我们会发现Cookie中会少一个文件,这个文件就是用来存储你的用户信息的。

            当然,Cookie文件是可以被不法分子盗取的,然后可以以你的身份去登录,他们一般是通过发送图片或者链接进行钓鱼,其实这都是下载程序,如果你点进去了,那么就会下载木马病毒,然后在你电脑植入,然后自动配置一系列的东西,盗取你的Cookie文件。

            所以其实单纯只有一个Cookie就太简单粗暴了,因为Cookie保存的是你的账号密码,一但它泄漏,那么就有可能就会有人去伪造你的身份,这是其一,其二是个人的隐私就会泄漏。有的人会想,那可以对Cookie进行加密,可以是可以在一定程度上的防御,但是还是会有问题。

            所以当前主流的不只是有一个Cookie文件。

            其实这里的Cookie文件还是可以被盗取的,但是获取不到你的账号密码了,但是可以通过session_id以你的身份登录你的网页,这只是相对安全了,因为在互联网上没有绝对的安全,在安全领域有一个准则,如果破解你这个信息的成本远远大于破解这个信息后获得的收益,那么这个个人信息就是安全的。

            还有就是因为是服务器保管着你的session_id,所以服务器可以添加各种策略来保证安全,我们可能都有一个经历,你在一台电脑上一直登录qq,有陌生人你获取了你的session_id登录,你就会收到一条在哪里登录了该账号,如果不是本人,请改密码之类的。服务器可以做到,如果短时间有长距离的ip地址的跳动,那么服务器就可以清掉session_id和账号的关联,从而不能通过session_id进行登录,那么此时就需要用账号密码登录了,又因为陌生人不知道你的账号密码,那么也就相对安全了很多。而且如果不是自己登录,那么那个陌生人的ip可能就会添加到服务器的黑名单中,然后这个ip地址就被封禁了。

            然后我们自己再改一下我们写的服务器,然后进行测试Cookie。

            当我们用浏览器的时候,我们对其进行抓包。

     

            我们可以看到,浏览器的Cookie文件中,有了Cookie字段,只要有了Cookie文件,每次进行请求的时候都会在报头中添加这个字段,然后服务器可以提取这个字段对其进行某种特殊的操作,在登录的时候显示为自动登录。

    接下来带大家大致了解一下HTTPS:

            其实在最开始的时候用的都是HTTP,那时所有的信息都是人人可以知道,相当于在互联网上lb。HTTPS就是对数据进行了加密。

            HTTP常见的端口号是80,但是HTTPS是443。

            然后其实HTTPS其实是用了对称加密和非对称加密两种加密算法。

     

            在http1.0中,是通过request、response方式通信的,当通信完一次后就断开,但是这样明显太浪费资源了,所以现在主流的http1.1是支持长连接的,所谓的长连接也就是说客户端可以向服务器的缓冲区里一次写入多个http请求,上层在读取的时候可以按照次序依次读取。

            长连接也就是报头中的Connection:keep-alive

             如上就是 HTTP---应用层 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

            再次感谢大家观看,感谢大家支持!

  • 相关阅读:
    常见的数码管中的引脚分布情况
    14:Hadoop数据分析|节点管理|搭建NFS网关服务
    dns隧道的通信原理及特征
    MySQL-事务隔离机制的实现
    【math】Hiden Markov Model 隐马尔可夫模型了解
    1.2.C++项目:仿mudou库实现并发服务器之时间轮的设计
    Swiper系列之轮播图
    Nodejs模块化
    【系统架构】系统质量属性与架构评估
    skywalking集成nacos动态配置
  • 原文地址:https://blog.csdn.net/weixin_69725192/article/details/126693827