从Socket到WebSocket

news2024/11/17 1:41:39

前言

        不知道大家在学习网络编程的时候都是怎样的一种方式,我谨以此文章来记录我自己从头开始学习C++网络编程时的经历,中间有许多我自己的一些想法和思考。当然作为一个刚开始学习的新手来说,有些内容也许不那么正确,只是代表了我在写这篇文章时的看法。对错相信看到本篇文章的人也能够通过自己的知识自行判断。

关于Socket

        我在真正接触TCP/IP网络编程之前,我曾经粗略的浏览过Linux系统编程的课程,当时这门课时长不长,整体介绍了进程线程的概念,涉及了管道,共享内存,消息队列,信号量等知识。并且在课程最后给我们实现了最基本TCP的双端通信。可以说我学习TCP/IP网络编程时已经不算真正意义上的小白了。虽然我希望尽我所能的将这些东西以更简单的方式讲清楚(因为我在学习的时候就发现了网上的大部分教程都是不知道在那里复制了一点相关概念,然后就直接给出了很它们的代码,然而并不会解释这些代码是如何产生的),但是我还是希望你们在学习网络编程之前可以看看相关Linux方面的知识,不至于理解不了常用的名词。

        注意,本片文章着重讲述学习完成socket后向websocket过渡的过程,所以本篇不会着重讲解socket编程,此外这篇文章所有的代码实现均在Linux上实现。

        在开始学习TCP/IP网络编程的阶段,有一本书绕不过去,那就是那本由韩国人尹圣雨所编写的《TCP/IP网络编程》,这本书可以说是我学习网咯编程的启蒙老师,这本书最大的有点就是讲解清晰易懂,示例代码均可以运行,并且注释非常清楚。可以说如果你是刚刚接触网络编程,这本书能够很好的带你入门。我是比较推荐新手看看这本书的。

认识WebSocket

        如果你已经看完了《TCP/IP网络编程》这本书的Linux部分,那么证明你已经有了一些socket编程基础。这时候可能你恰巧需要使用websocket,于是就开始在网络上搜索websocket相关的教程,寄希望发现某个教程能够从最基础开始讲解websocket,从而帮助自己在第一次接触websocket的情况下理解websocket。

        很可惜你失败了,确实找到了一些教程,但是都不是你所需要的。这些教程要不然就是对websocket协议的照本宣科的描述,毫无价值;要么就是使用的js等和java相关的代码来编写,不仅代码中莫名奇妙会出现一些不知道从那里凭空产生的websocket类,而且就连发送接收消息这种websocket协议中的关键性代码的详解也是没有的。

        同样的,我在学习websocket时也遇到了上面的问题,所以我决定自己来写一份使用C++语言的websocket学习记录,希望能够帮助到你们。所以说在这篇文章之后学习websocket的人们是幸运的,因为你们极有可能看到我的这篇文章而免受搜索到大量无用文章的痛苦。

使用websocket第三方库

        如果你尝试使用ChatGPT等AI工具帮你生成一个websocket服务端代码,它们大概率会给出一小段非常精简,并且易于理解的代码。大部分这类代码使用了websocket的第三方库,第三方库将具体的实现方法进行了封装,使得我们在使用起来特别方便。我个人是比较喜欢直接使用boost库来实现websocket服务的。下面我给出一段服务器代码例子

#include <iostream>
#include <boost/asio.hpp>
#include <boost/beast.hpp>

int main() {
    // 创建IO上下文
    boost::asio::io_context io_context;

    // 创建TCP端口并监听在9002端口
    boost::asio::ip::tcp::acceptor a(io_context, { boost::asio::ip::tcp::v4(), 9002 });

    // 创建TCP端口并绑定到特定IP地址和端口
    // boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::make_address("192.168.32.124"), 9002);
    // boost::asio::ip::tcp::acceptor a(io_context, endpoint);

    while (true) {
        // 接受客户端连接
        boost::asio::ip::tcp::socket socket(io_context);
        a.accept(socket);

        try {
            // 使用WebSocket流包装TCP套接字
            boost::beast::websocket::stream<boost::asio::ip::tcp::socket&> ws{ socket };

            // 启动WebSocket握手
            ws.accept();

            // 发送消息给客户端
            std::string response = "Hello, WebSocket!";
            ws.write(boost::asio::buffer(response));

            while (true) {
                // 读取客户端发送的消息
                boost::beast::flat_buffer buffer;
                ws.read(buffer);
                std::string received_msg = boost::beast::buffers_to_string(buffer.data());

                // 输出接收到的消息
                std::cout << "Received message from client: " << received_msg << std::endl;

                // 如果收到 "quit" 消息,则关闭连接
                if (received_msg == "quit") {
                    break;
                }

                // 发送消息给客户端
                response = "I received your message: " + received_msg;
                ws.write(boost::asio::buffer(response));
            }

            // 关闭WebSocket连接
            ws.close(boost::beast::websocket::close_code::normal);
        }
        catch (const boost::beast::system_error& se) {
            std::cerr << "Error: " << se.what() << std::endl;
        }
    }

    return 0;
}

        使用第三方库提供的函数能够大大减少我们编写代码的难度,但是作为初学者对于理解websocket协议点三方库就不是那么友好了,因为我们不容易看到具体的处理细节。

使用原生的C++socket编程

        在学习websocket时我们知道,websocket的实质其实还是基于socket进行通信的,只不过在通信的开始需要确认一下请求信息。在确认请求信息过后,之后的数据收发完全就是socekt通信。唯一需要注意的就是收到的数据并不是之前我们的那种简单的字符串了,websocket发送的数据是一个数据帧,简单来说就是我们收到的字符串中包含的不仅仅只有数据,他是遵循websocket协议的的不同字段的拼接。

        简单解释一下这个收到的数据块(也就是字符串),其具体结构如下:

  • FIN(1位):表示这是消息的最后一个数据帧,如果消息可以被分割成多个数据帧,那么只有最后一个数据帧的FIN位为1,其他数据帧的FIN位为0。
  • RSV1, RSV2, RSV3(各1位):预留位,目前没有特定的使用规范,一般情况下应该为0。
  • Opcode(4位):表示数据帧的类型,包括文本帧、二进制帧、关闭连接帧等。
  • Mask(1位):标识是否对数据进行掩码处理,客户端发往服务器的数据帧需要进行掩码处理,而服务器发往客户端的数据帧不需要进行掩码处理。
  • Payload length(7位或16位或64位):表示负载数据的长度,如果长度在0~125字节之间,则使用7位表示;如果长度在126~65535字节之间,则使用16位表示,且紧随其后的两个字节表示真正的长度;如果长度超过65535字节,则使用64位表示。
  • Masking key(0或4字节):如果Mask为1,那么会有4字节的掩码密钥,用于对负载数据进行掩码处理。
  • Payload data(x字节):实际的负载数据内容,长度由Payload length字段指定,如果Mask为1,则需要使用Masking key对这部分数据进行解码。

        这里面我们最需要注意的就是Opcode,Payload length。这个两个字段决定了我们在解析收到的数据的流程。

Opcode, 长度为 4 比特, 该字段将指示 frame 的类型, RFC 6455 定义的 Opcode 共有如下几种:

  • 0x0, 代表当前是一个 continuation frame
  • 0x1, 代表当前是一个 text frame
  • 0x2, 代表当前是一个 binary frame
  • 0x3 ~ 7, 目前保留, 以后将用作更多的非控制类 frame
  • 0x8, 代表当前是一个 connection close, 用于关闭 WebSocket 连接
  • 0x9, 代表当前是一个 ping frame (将在下面讨论)
  • 0xA, 代表当前是一个 pong frame (将在下面讨论)
  • 0xB ~ F, 目前保留, 以后将用作更多的控制类 frame

        实际上最需要记得的就是0x00,0x01,0x02,0x08,分别对应着中间数据,文本数据,二进制数据,和关闭websocket。在收到字符串时,我们首先要取出字符串里面的Opcode,看其符合哪一种,然后依照每种的处理方式继续进行后续数据的处理工作。

        我们这次使用的代码是以收到文本数据为例,下面是一个服务端代码,客户端我们可以使用websocket测试网站websocket在线测试。

#include <iostream>
#include <string>
#include <cstring>
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <netinet/in.h>
#include <unistd.h>

std::string generate_handshake_response(const std::string& key) {
    const std::string GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    std::string concatenated = key + GUID;
    unsigned char hash[SHA_DIGEST_LENGTH];
    SHA1(reinterpret_cast<const unsigned char*>(concatenated.c_str()), concatenated.length(), hash);

    unsigned char encoded_hash[SHA_DIGEST_LENGTH*2]; // 预留足够的空间以容纳编码后的结果
    int encoded_length = EVP_EncodeBlock(encoded_hash, hash, SHA_DIGEST_LENGTH);

    std::string response_key(reinterpret_cast<char*>(encoded_hash), encoded_length);

    std::string response = "HTTP/1.1 101 Switching Protocols\r\n";
    response += "Upgrade: websocket\r\n";
    response += "Connection: Upgrade\r\n";
    response += "Sec-WebSocket-Accept: " + response_key + "\r\n\r\n";
    return response;
}

// 解析 WebSocket 消息内容
std::string parseWebSocketMessage(const std::string& message) {
    std::string decodedMessage;
    
    // 检查是否有掩码
    bool masked = (message[1] & 0x80) != 0;
    int payloadLength = message[1] & 0x7F;
    int maskOffset = 2;
    
    if (payloadLength == 126) {
        // 16位长度
        payloadLength = (static_cast<uint8_t>(message[2]) << 8) | static_cast<uint8_t>(message[3]);
        maskOffset = 4;
    } else if (payloadLength == 127) {
        // 64位长度,我们假设消息不会很大
        payloadLength = (static_cast<uint64_t>(message[2]) << 56) |
                        (static_cast<uint64_t>(message[3]) << 48) |
                        (static_cast<uint64_t>(message[4]) << 40) |
                        (static_cast<uint64_t>(message[5]) << 32) |
                        (static_cast<uint64_t>(message[6]) << 24) |
                        (static_cast<uint64_t>(message[7]) << 16) |
                        (static_cast<uint64_t>(message[8]) << 8) |
                        static_cast<uint64_t>(message[9]);
        maskOffset = 10;
    }
    
    std::string maskingKey = message.substr(maskOffset, 4);
    std::string payload = message.substr(maskOffset + 4, payloadLength);
    
    // 反掩码操作
    for (int i = 0; i < payloadLength; ++i) {
        decodedMessage.push_back(payload[i] ^ maskingKey[i % 4]);
    }
    
    return decodedMessage;
}

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(9002);

    // 绑定地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    std::cout << "WebSocket server is listening on port 9002" << std::endl;

    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // 接收握手请求
    int valread = read(new_socket, buffer, 1024);
    std::string key;
    std::string handshake_request(buffer, valread);
    std::size_t found = handshake_request.find("Sec-WebSocket-Key: ");
    if (found != std::string::npos) {
        key = handshake_request.substr(found + 19, 24);
        std::string response = generate_handshake_response(key);
        send(new_socket, response.c_str(), response.length(), 0);
        std::cout << "WebSocket handshake completed." << std::endl;
    }
    // std::cout << key << std::endl;

    // 接收和发送WebSocket消息
    while (true) {
        // memset(buffer,'\0',1024);
        valread = read(new_socket, buffer, 1024);

        // 处理WebSocket数据帧,这部分需要根据WebSocket协议规范进行解析
        unsigned char opcode = buffer[0] & 0x0F;
            switch (opcode) {
                case 0x01:  // 文本消息
                    {
                        // 解析文本消息内容
                        std::string str(buffer);
                        std::string message = parseWebSocketMessage(str);

                        //在服务器端回显
                        std::cout << "Received message: " << message << std::endl;
                        //假设收到的数据是文本消息,直接返回相同的消息
                        /*        
                            这行代码构造了返回消息。
                            首先,\x81 是WebSocket帧的控制位,表示这是一个文本消息帧。
                            然后是消息的长度信息,它的长度是一个字节,表示消息的长度。接着是消息的实际内容。
                        */
                        std::string response = std::string("\x81", 1) + static_cast<char>(message.length()) + message;
                        send(new_socket, response.c_str(), response.length(), 0);

                        break;
                    }
                case 0x08:  // 关闭连接
                    {
                        // 处理关闭连接请求
                        // handleCloseRequest();
                        close(new_socket);
                        // close(server_fd);
                        // std::cout << "closed\n";
                        break;
                    }
                // 其他消息类型的处理
                default:
                    {
                        // 其他处理逻辑
                        break;
                    }                
            }        
    }
    return 0;
}

        大家可以看到在我们的主函数中,前面一直都是按照常规的socket编程进行,但是在accept之后,我们对首次接收到的数据(也叫做接收请求头)进行了提取Sec-WebSocket-Key:的操作,并对提取出来的Sec-WebSocket-Key与 WebSocket 魔数 (Magic Number) "258EAFA5-E914-47DA- 95CA-C5AB0DC85B11" 进行字符串连接, 将得到的字符串做 SHA-1 哈希, 将得到的哈希值再做 base64 编码,最终我们把得到的值作为服务器向客户端发送的Sec-WebSocket-Accept:字段值。并且一同发送给客户端的还有
    "HTTP/1.1 101 Switching Protocols\r\n";
    "Upgrade: websocket\r\n";
    "Connection: Upgrade\r\n";

完成这样一来一回的请求回应操作后,websocket服务端和客户端就正式建立起连接了。

        在代码中我们建立连接以后,主函数进入while死循环,不断接收发送websocket消息。我们下面思考这段代码的逻辑。正如上面介绍数据帧格式的时候所说,首先我们获取数据帧中Opcode的部分,这里使用的是unsigned char opcode = buffer[0] & 0x0F;使用与操作将opcode数据给取了出来,之后使用switch语句判断opcode属于那种情况,这里我实现了0x01文本数据的收发操作。

        确定了数据发送过来的是文本数据,那么这个文本数据有多长,得到数据长度后我们就可以打印输出到服务器上或者回传给客户端了吗。显然远没有这么简单,RFC 6455 规定所有由客户端发往服务端的 WebSocket frame 的 Payload 部分都必须使用掩码覆盖。也就是说我们收到的数据是经过掩码覆盖的加密数据,直接使用会不能识别,必须经过解码操作。得到未被覆盖前的原始数据。

掩码覆盖的算法如下:

  1. 客户端使用熵值足够高的随机数生成器随机生成 32 比特的 Masking-Key
  2. 以字节为步长遍历 Payload, 对于 Payload 的第 i 个字节, 首先做 i MOD 4 得到 j, 则掩码覆盖后的 Payload 的第 i 个字节的值为原先 Payload 第 i 个字节与 Masking-Key 的第 j 个字节做按位异或操作

    // 反掩码操作
    for (int i = 0; i < payloadLength; ++i) {
        decodedMessage.push_back(payload[i] ^ maskingKey[i % 4]);
    }

经过反掩码之后我们就得到了正确的原始数据,使用原始数据就可以正常向客户端发送了。

        看到这里或许你已经又发现了一些特别的地方,是的,原始数据只能用于服务器显示,并不能直接send到客户端,究其原因是因为websocket的收发双方都必须发送严格遵循websocket数据帧格式的数据,所以我们也必须构建出同样的字符串才行,代码中使用std::string response = std::string("\x81", 1) + static_cast<char>(message.length()) + message;这行代码构造了发往客户端的数据。

结语

        至此我们已经完成了一个最基本的websocket服务器模型,相信对初学者的你来说,成功发送并接收websocket数据一定十分兴奋,当然对于之后的学习也不要懈怠。如果本篇教程帮到了你,那么我也感到非常开心。希望我们下下篇博客再见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1717023.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Qt知识】Qt Creator快捷键

以下是Qt Creator中的一些常用快捷键列表&#xff08;持续更新&#xff09;&#xff1a; 基本编辑 多行注释/取消多行注释: Ctrl /编译工程: Ctrl B运行工程: Ctrl R整行上移/下移: Ctrl Shift ↑/↓查找: Ctrl F函数声明和定义切换: F2向下查找: F3头文件和源文件切换:…

SAPUI5基础知识3 - 引导过程(Bootstrap)

1. 背景 在上一篇博客中&#xff0c;我们已经建立出了第一个SAPUI5项目&#xff0c;接下来&#xff0c;我们将为这个项目添加引导过程。 在动手练习之前&#xff0c;让我们先解释一下什么引导过程。 1.1 什么是引导过程&#xff1f; 在计算机科学中&#xff0c;引导过程也称…

苹果如何设置自动循环壁纸?这几种不同的方法你要学会

在使用手机的时候&#xff0c;我们可能会经常更换手机壁纸&#xff0c;遇见好看或者有意义的图片时就想将其设置为壁纸&#xff0c;你知道 iPhone 手机可以设置自动循环壁纸吗&#xff1f;如果你不了解下面就带你一起看看 iPhone 手机如何设置壁纸自动循环。 壁纸程序设置 想…

GIS之arcgis系列06:线划图缓冲区分析

缓冲区工具将在输入要素周围指定距离内创建缓冲区面。 缓冲区例程将遍历输入要素的每个折点并创建缓冲区偏移。 通过这些偏移创建输出缓冲区要素 原理&#xff1a; 01.打开文件 02.确定单位&#xff0c;在文件属性里。 03.工具箱-->分析工具-->邻域分析-->缓冲区。 …

藏汉翻译通小程序——你口袋里的藏语翻译助手!支持高精度藏文OCR文字识别提取,安卓iPhone手机都能用的藏汉翻译小助手!

想要快速学习藏语或者与藏族朋友无障碍沟通吗&#xff1f;藏汉翻译通小程序绝对是你的不二之选&#xff01; 这款小程序不仅界面简洁、操作便捷&#xff0c;更重要的是它集合了多种实用功能于一身。 双语翻译&#xff1a;无论你是藏族还是汉族&#xff0c;只需输入文字&#…

C#中的实体属性详解与示例

文章目录 实体属性的定义实体属性的访问实体属性的示例总结 在C#中&#xff0c;实体属性是面向对象编程的重要组成部分。实体属性允许我们定义对象的特征和行为&#xff0c;并提供了一种方式来访问和管理这些特征。通过实体属性&#xff0c;我们可以封装对象的状态&#xff0c;…

【Linux】Linux基本指令3

目录 1.date指令 2.cal指令 3.find指令&#xff1a;&#xff08;灰常重要&#xff09; -name 4.grep指令——行文本过滤工具 5.zip/unzip指令&#xff1a; 6.tar指令&#xff08;重要&#xff09;&#xff1a;打包/解包&#xff0c;不打开它&#xff0c;直接看内容 7.bc…

OSPF学习笔记(状态机)

1、邻居关系 OSPF设备启动后&#xff0c;会通过OSPF接口向外发送Hello报文&#xff0c;收到Hello报文的OSPF设备会检查报文中所定义的参数&#xff0c;如果双方一致就会形成邻居关系&#xff0c;两端设备互为邻居 2、邻接关系 形成邻居关系后&#xff0c;如果两端设备成功交…

Spring boot集成通义千问大模型

Spring boot集成通义千问大模型 背景 我在用idea进行java开发时发现了通义灵码这款免费的智能代码补全插件&#xff0c;用了一段时间了&#xff0c;感觉很不错。就想着在自己的项目中也能集成通义千问大模型实现智能回答&#xff0c;毕竟对接openai需要解决网络问题&#xff…

[学习笔记]知乎文章-PyTorch的Transformer

参考资料&#xff1a; PyTorch的Transformer register_buffer的作用是&#xff1a;登记成员变量&#xff0c;它会自动成为模型中的参数&#xff0c;随着模型移动&#xff08;gpu/cpu&#xff09;而移动&#xff0c;但是不会随着梯度进行更新。 参考资料&#xff1a;【Torch API…

【云原生】kubernetes中pod的生命周期、探测钩子的实战应用案例解析

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

向量数据库引领 AI 创新——Zilliz 亮相 2024 亚马逊云科技中国峰会

2024年5月29日&#xff0c;亚马逊云科技中国峰会在上海召开&#xff0c;此次峰会聚集了来自全球各地的科技领袖、行业专家和创新企业&#xff0c;探讨云计算、大数据、人工智能等前沿技术的发展趋势和应用场景。作为领先的向量数据库技术公司&#xff0c;Zilliz 在本次峰会上展…

Pont在小程序开发的使用

Pont是一个很好的前后端桥&#xff0c;但是有个问题。默认产生的代码&#xff0c;无法支持微信小程序开发。根本原因是因为使用了window给全局的对象注入了API和refs属性&#xff0c;由于小程序没有window属性&#xff0c;当然就无法使用了&#xff0c;解决办法也比较简单。只需…

轻松入门:HTML网页制作指南 进阶篇

一.表格标签 1.1表格的主要作用 表格不是用来布局页面的,而是用来展示数据的。 1.2基本语法 <table><tr><td>单元格内的文字</td>...</tr>...</table>说明&#xff1a; 1.<table> </table> 是用于定义表格的标签。 2.<t…

Iphone自动化指令每隔固定天数打开闹钟关闭闹钟(一)

注意&#xff1a;因为是第一次用iphone的快捷指令&#xff0c;不是很明白&#xff0c;所以之后多次运行发现有bug&#xff0c;所以快捷指令部分在下一章重新写&#xff0c;我用两个日期测试了&#xff0c;没问题&#xff0c;这一章可以当做熟悉快捷指令的一些操作用&#xff0c…

【协议开发系列】梳理关于TCP和UDP两种协议的区别和使用场景

起源 前二天项目上在核对外部对接服务的五元组列表的时候&#xff0c;有一位客户提问对于同样的服务同时支持tcp和udp二种方式&#xff0c;有什么优点和缺点&#xff0c;应该如何选择&#xff1f;这个问题突然让我愣了一下&#xff0c;确实好久没有“温故”了&#xff0c;相关…

【哈希】用哈希桶封装unordered_map unordered_set

&#x1f389;博主首页&#xff1a; 有趣的中国人 &#x1f389;专栏首页&#xff1a; C进阶 &#x1f389;其它专栏&#xff1a; C初阶 | Linux | 初阶数据结构 小伙伴们大家好&#xff0c;本片文章将会讲解 用哈希桶封装 unordered_map & unordered_set 的相关内容。 如…

pdf拆分成有图和无图的pdf(方便打印)

pdf拆分成有图和无图的pdf(方便打印) 原因 打印图片要彩印&#xff0c;每次都要手动弄&#xff0c;打印的时候很麻烦&#xff1b; 随着打印次数的增加&#xff0c;时间就越来越多 为解决此问题&#xff0c;使用python写一个exe解决这个问题 历程 找一个python的GUI界面找到 t…

USART串口外设

USART介绍 USART&#xff1a;另外我们经常还会遇到串口&#xff0c;叫UART&#xff0c;少了个S&#xff0c;就是通用异步收发器&#xff0c;一般我们串口很少使用这个同步功能&#xff0c;所以USART和UART使用起来&#xff0c;也没有什么区别。 其实这个STM32的USART同步模式&a…

MySQL 索引的使用

本篇主要介绍MySQL中索引使用的相关内容。 目录 一、最左前缀法则 二、索引失效的场景 索引列运算 字符串无引号 模糊查询 or连接条件 数据分布 一、最左前缀法则 当我们在使用多个字段构成的索引时&#xff08;联合索引&#xff09;&#xff0c;需要考虑最左前缀法则…