TCP粘包问题详解和解决方案【C语言】

news2024/9/21 14:38:29

1.什么是TCP粘包

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输协议,它保证了数据的可靠性和顺序性。然而,由于TCP是基于字节流而不是消息的,因此在传输过程中可能会出现粘包(Packing)和拆包(Unpacking)问题。

**粘包问题(TCP粘包现象)**指的是发送方在传输数据时,TCP协议把多个发送的小数据包“粘”在一起,形成一个大的数据包发送;或者接收方在接收数据时,多个小的数据包被“粘”在一起,形成一个大的数据包接收。这种现象的发生是由于TCP协议的工作机制导致的。

原因和机制

  1. TCP工作方式:TCP是基于字节流的协议,它并不了解上层应用发送的消息边界(Message Boundary)。它只负责把接收到的字节流按照顺序交给应用层,因此多个发送的小数据包在传输过程中有可能会被合并成一个大的数据包发送,或者一个大的数据包被拆分成多个小数据包接收。

  2. 发送端的粘包

    • 发送端应用程序往往会先把数据放入TCP发送缓冲区,然后TCP根据自身的发送策略(如Nagle算法等)进行发送,可能会合并多个数据包一起发送,以提高网络利用率和性能。
    • 如果发送端应用程序发送的消息比较小,并且发送速率较快,这些小消息在TCP层可能会被合并成一个大的数据包发送,导致接收方接收到的数据出现粘包现象。
  3. 接收端的粘包

    • 接收端应用程序从TCP接收缓冲区中读取数据时,由于TCP层不了解应用层的消息边界,可能一次性把多个发送的小数据包“粘”在一起交给应用层处理。
    • 如果接收端应用程序处理消息的速度跟不上数据的接收速度,会导致接收到的数据出现粘包现象。

例如:

  • 客户端和服务器之间要进行基于TCP的套接字通信
  • 通信过程中客户端会每次会不定期给服务器发送一个不定长度的有特定含义的字符串。
  • 通信的服务器端每次都需要接收到客户端这个不定长度的字符串,并对其进行解析

根据上面的描述,服务器在接收数据的时候有如下几种情况:

  1. 一次接收到了客户端发送过来的一个完整的数据包
  2. 一次接收到了客户端发送过来的N个数据包,由于每个包的长度不定,无法将各个数据包拆开
  3. 一次接收到了一个或者N个数据包 + 下一个数据包的一部分,还是很悲剧,无法将数据包拆开
  4. 一次收到了半个数据包,下一次接收数据的时候收到了剩下的一部分+下个数据包的一部分,更悲剧,头大了
  5. 另外,还有一些不可抗拒的因素:比如客户端和服务器端的网速不一样,发送和接收的数据量也会不一致

解决方案

粘包问题在实际的网络编程中是常见的,需要采取一些策略来解决或者减少其影响:

  • 消息边界标记:在发送的消息中加入特定的消息边界标记(如换行符 \n),接收端根据消息边界标记来分割接收到的数据,从而识别出完整的消息。有缺陷: 效率低, 需要一个字节一个字节接收, 接收一个字节判断一次, 判断是不是那个特殊字符串

  • 消息长度固定:发送端将每个消息的长度固定,接收端根据固定长度来分割接收到的数据,从而确保每个接收到的数据包含完整的消息。缺点:容易造成空间浪费

  • 消息头部长度字段:发送端在每个消息前加入一个固定长度的消息头部,包含消息的长度信息,接收端根据头部长度字段来读取对应长度的消息数据。这时候数据由两部分组成:数据头+数据块数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节,数据块:当前数据包的内容

  • 使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包

2.解决方案具体实现

这里我们使用消息头+数据块的解决方案,如果使用TCP进行套接字通信,如果发送的数据包粘连到一起导致接收端无法解析,我们通常使用添加包头的方式轻松地解决掉这个问题。关于数据包的包头大小可以根据自己的实际需求进行设定,这里没有啥特殊需求,因此规定包头的固定大小为4个字节,用于存储当前数据块的总字节数。

发送端设计

对于发送端来说,数据的发送分为以下四步:

  1. 动态申请内存: 根据待发送的数据长度 N申请一块大小为 N+4 的内存,其中4个字节用于存储包头信息。

  2. 写入包头: 将待发送数据的总长度(N)写入申请的内存的前四个字节中,并将其转换为网络字节序(大端序)。

  3. 拷贝数据并发送: 将待发送的数据拷贝到包头后面的地址空间中,然后将整个数据包发送出去。这里需要确保数据包能够完整发送,因此可以设计一个发送函数,确保当前数据包中的数据全部发送完毕。

  4. 释放内存: 发送完毕后,释放申请的堆内存。

示例代码:

/*
函数描述: 发送指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - size: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int writen(int fd, const char* msg, int size) {
    const char* buf = msg; // 指向待发送数据的指针
    int count = size;      // 记录剩余待发送的数据字节数

    while (count > 0) {
        // 尝试发送剩余数据
        int len = send(fd, buf, count, 0);
        if (len == -1) {
            perror("send");
            close(fd);
            return -1; // 发送失败
        } else if (len == 0) {
            continue; // 发送未成功,继续尝试
        }
        buf += len;    // 更新待发送数据的起始地址
        count -= len;  // 更新剩余待发送的数据字节数
    }
    return size; // 全部数据发送完毕,返回发送的总字节数
}

/*
函数描述: 发送带有数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - len: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int sendMsg(int cfd, const char* msg, int len) {
    if (msg == NULL || len <= 0 || cfd <= 0) {
        return -1; // 参数无效
    }

    // 申请内存空间: 数据长度 + 包头4字节(存储数据长度)
    char* data = (char*)malloc(len + 4);
    if (data == NULL) {
        perror("malloc");
        return -1; // 内存申请失败
    }

    // 将数据长度转换为网络字节序(大端序)并存储在包头
    int bigLen = htonl(len);
    memcpy(data, &bigLen, 4);

    // 将待发送的数据拷贝到包头后面
    memcpy(data + 4, msg, len);

    // 发送带有包头的数据包
    int ret = writen(cfd, data, len + 4);

    // 释放申请的内存
    free(data);

    return ret; // 返回发送的字节数
}

接收端设计

在接收端,需要确保每次接收到的都是完整的数据包,避免粘包问题。以下是具体的步骤和代码实现:

  1. 接收4字节的包头,并将其从网络字节序转换为主机字节序,得到即将要接收的数据的总长度。
  2. 根据总长度申请固定大小的堆内存,用于存储待接收的数据。
  3. 根据数据块长度接收固定数量的数据并保存到申请的堆内存中。
  4. 处理接收的数据
  5. 释放存储数据的堆内存

示例代码:

/*
函数描述: 接收指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - buf: 存储待接收数据的内存的起始地址
    - size: 指定要接收的字节数
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int readn(int fd, char* buf, int size) {
    char* pt = buf; // 指向待接收数据的缓冲区
    int count = size; // 记录剩余需要接收的字节数

    while (count > 0) {
        // 尝试接收数据
        int len = recv(fd, pt, count, 0);
        if (len == -1) {
            perror("recv");
            return -1; // 接收失败
        } else if (len == 0) {
            return size - count; // 对方关闭连接,返回已接收的字节数
        }
        pt += len;    // 更新缓冲区指针
        count -= len; // 更新剩余需要接收的字节数
    }
    return size; // 返回实际接收的字节数
}

/*
函数描述: 接收带数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int recvMsg(int cfd, char** msg) {
    // 接收数据头(4个字节)
    int len = 0;
    if (readn(cfd, (char*)&len, 4) != 4) {
        return -1; // 接收数据头失败
    }

    // 将数据头从网络字节序转换为主机字节序,得到数据长度
    len = ntohl(len);
    printf("数据块大小: %d\n", len);

    // 根据读出的长度分配内存,多分配1个字节用于存储字符串结束符'\0'
    char* buf = (char*)malloc(len + 1);
    if (buf == NULL) {
        perror("malloc");
        return -1; // 内存分配失败
    }

    // 接收数据
    int ret = readn(cfd, buf, len);
    if (ret != len) {
        close(cfd);
        free(buf);
        return -1; // 接收数据失败
    }

    buf[len] = '\0'; // 添加字符串结束符
    *msg = buf;

    return ret; // 返回接收的字节数
}

3.TCP循环通信代码

服务端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

int readn(int fd, char* buf, int size);
int recvMsg(int cfd, char** msg);
int sendMsg(int cfd, const char* msg, int len);

int main() {
    int server_sockfd, client_sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 创建服务器套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sockfd < 0) {
        perror("socket");
        return 1;
    }

    // 服务器地址配置
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12345); // 服务器端口
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 绑定地址和端口
    if (bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(server_sockfd);
        return 1;
    }

    // 监听连接
    if (listen(server_sockfd, 5) < 0) {
        perror("listen");
        close(server_sockfd);
        return 1;
    }

    printf("Server is listening on port 12345...\n");

    // 接受客户端连接
    client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
    if (client_sockfd < 0) {
        perror("accept");
        close(server_sockfd);
        return 1;
    }

    char* data;
    char msg[1024];
    while (1) {
        // 接收客户端消息
        int len = recvMsg(client_sockfd, &data);
        if (len < 0) {
            fprintf(stderr, "Failed to receive data\n");
            break;
        }
        
        printf("Client: %s\n", data);

        // 收到"exit"消息,退出循环
        if (strcmp(data, "exit") == 0) {
            free(data);
            break;
        }

        // 获取服务端要发送的消息
        printf("Server: ");
        fgets(msg, sizeof(msg), stdin);
        msg[strcspn(msg, "\n")] = '\0'; // 去掉换行符

        // 发送消息到客户端
        if (sendMsg(client_sockfd, msg, strlen(msg)) < 0) {
            fprintf(stderr, "Failed to send data\n");
            free(data);
            break;
        }

        free(data); // 处理完数据后释放内存

        // 输入"exit"退出循环
        if (strcmp(msg, "exit") == 0) {
            break;
        }
    }

    // 关闭套接字
    close(client_sockfd);
    close(server_sockfd);
    return 0;
}

/*
函数描述: 接收指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - buf: 存储待接收数据的内存的起始地址
    - size: 指定要接收的字节数
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int readn(int fd, char* buf, int size) {
    char* pt = buf; // 指向待接收数据的缓冲区
    int count = size; // 记录剩余需要接收的字节数

    while (count > 0) {
        // 尝试接收数据
        int len = recv(fd, pt, count, 0);
        if (len == -1) {
            perror("recv");
            return -1; // 接收失败
        } else if (len == 0) {
            return size - count; // 对方关闭连接,返回已接收的字节数
        }
        pt += len;    // 更新缓冲区指针
        count -= len; // 更新剩余需要接收的字节数
    }
    return size; // 返回实际接收的字节数
}

/*
函数描述: 接收带数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int recvMsg(int cfd, char** msg) {
    // 接收数据头(4个字节)
    int len = 0;
    if (readn(cfd, (char*)&len, 4) != 4) {
        return -1; // 接收数据头失败
    }

    // 将数据头从网络字节序转换为主机字节序,得到数据长度
    len = ntohl(len);
    printf("数据块大小: %d\n", len);

    // 根据读出的长度分配内存,多分配1个字节用于存储字符串结束符'\0'
    char* buf = (char*)malloc(len + 1);
    if (buf == NULL) {
        perror("malloc");
        return -1; // 内存分配失败
    }

    // 接收数据
    int ret = readn(cfd, buf, len);
    if (ret != len) {
        close(cfd);
        free(buf);
        return -1; // 接收数据失败
    }

    buf[len] = '\0'; // 添加字符串结束符
    *msg = buf;

    return ret; // 返回接收的字节数
}

/*
函数描述: 发送指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - size: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int writen(int fd, const char* msg, int size) {
    const char* buf = msg; // 指向待发送数据的指针
    int count = size;      // 记录剩余待发送的数据字节数

    while (count > 0) {
        // 尝试发送剩余数据
        int len = send(fd, buf, count, 0);
        if (len == -1) {
            perror("send");
            return -1; // 发送失败
        } else if (len == 0) {
            continue; // 发送未成功,继续尝试
        }
        buf += len;    // 更新待发送数据的起始地址
        count -= len;  // 更新剩余待发送的数据字节数
    }
    return size; // 全部数据发送完毕,返回发送的总字节数
}

/*
函数描述: 发送带有数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - len: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int sendMsg(int cfd, const char* msg, int len) {
    if (msg == NULL || len <= 0 || cfd <= 0) {
        return -1; // 参数无效
    }

    // 申请内存空间: 数据长度 + 包头4字节(存储数据长度)
    char* data = (char*)malloc(len + 4);
    if (data == NULL) {
        perror("malloc");
        return -1; // 内存申请失败
    }

    // 将数据长度转换为网络字节序(大端序)并存储在包头
    int bigLen = htonl(len);
    memcpy(data, &bigLen, 4);

    // 将待发送的数据拷贝到包头后面
    memcpy(data + 4, msg, len);

    // 发送带有包头的数据包
    int ret = writen(cfd, data, len + 4);

    // 释放申请的内存
    free(data);

    return ret; // 返回发送的字节数
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

int sendMsg(int cfd, const char* msg, int len);
int recvMsg(int cfd, char** msg);

int main() {
    int sockfd;
    struct sockaddr_in server_addr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return 1;
    }

    // 服务器地址配置
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12345); // 服务器端口
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP地址

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sockfd);
        return 1;
    }

    char msg[1024];
    char* data;
    while (1) {
        // 客户端输入消息
        printf("Client: ");
        fgets(msg, sizeof(msg), stdin);
        msg[strcspn(msg, "\n")] = '\0'; // 去掉换行符

        // 发送数据到服务器
        if (sendMsg(sockfd, msg, strlen(msg)) < 0) {
            fprintf(stderr, "Failed to send data\n");
            break;
        }

        // 接收服务器消息
        if (recvMsg(sockfd, &data) < 0) {
            fprintf(stderr, "Failed to receive data\n");
            break;
        }
        
        printf("Server: %s\n", data);
        free(data);

        // 输入"exit"退出循环
        if (strcmp(msg, "exit") == 0) {
            break;
        }
    }

    // 关闭套接字
    close(sockfd);
    return 0;
}

/*
函数描述: 发送指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - size: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int writen(int fd, const char* msg, int size) {
    const char* buf = msg; // 指向待发送数据的指针
    int count = size;      // 记录剩余待发送的数据字节数

    while (count > 0) {
        // 尝试发送剩余数据
        int len = send(fd, buf, count, 0);
        if (len == -1) {
            perror("send");
            return -1; // 发送失败
        } else if (len == 0) {
            continue; // 发送未成功,继续尝试
        }
        buf += len;    // 更新待发送数据的起始地址
        count -= len;  // 更新剩余待发送的数据字节数
    }
    return size; // 全部数据发送完毕,返回发送的总字节数
}

/*
函数描述: 发送带有数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 待发送的原始数据
    - len: 待发送的原始数据的总字节数
函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1
*/
int sendMsg(int cfd, const char* msg, int len) {
    if (msg == NULL || len <= 0 || cfd <= 0) {
        return -1; // 参数无效
    }

    // 申请内存空间: 数据长度 + 包头4字节(存储数据长度)
    char* data = (char*)malloc(len + 4);
    if (data == NULL) {
        perror("malloc");
        return -1; // 内存申请失败
    }

    // 将数据长度转换为网络字节序(大端序)并存储在包头
    int bigLen = htonl(len);
    memcpy(data, &bigLen, 4);

    // 将待发送的数据拷贝到包头后面
    memcpy(data + 4, msg, len);

    // 发送带有包头的数据包
    int ret = writen(cfd, data, len + 4);

    // 释放申请的内存
    free(data);

    return ret; // 返回发送的字节数
}

/*
函数描述: 接收指定的字节数
函数参数:
    - fd: 通信的文件描述符(套接字)
    - buf: 存储待接收数据的内存的起始地址
    - size: 指定要接收的字节数
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int readn(int fd, char* buf, int size) {
    char* pt = buf; // 指向待接收数据的缓冲区
    int count = size; // 记录剩余需要接收的字节数

    while (count > 0) {
        // 尝试接收数据
        int len = recv(fd, pt, count, 0);
        if (len == -1) {
            perror("recv");
            return -1; // 接收失败
        } else if (len == 0) {
            return size - count; // 对方关闭连接,返回已接收的字节数
        }
        pt += len;    // 更新缓冲区指针
        count -= len; // 更新剩余需要接收的字节数
    }
    return size; // 返回实际接收的字节数
}

/*
函数描述: 接收带数据头的数据包
函数参数:
    - cfd: 通信的文件描述符(套接字)
    - msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放
函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1
*/
int recvMsg(int cfd, char** msg) {
    // 接收数据头(4个字节)
    int len = 0;
    if (readn(cfd, (char*)&len, 4) != 4) {
        return -1; // 接收数据头失败
    }

    // 将数据头从网络字节序转换为主机字节序,得到数据长度
    len = ntohl(len);
    printf("数据块大小: %d\n", len);

    // 根据读出的长度分配内存,多分配1个字节用于存储字符串结束符'\0'
    char* buf = (char*)malloc(len + 1);
    if (buf == NULL) {
        perror("malloc");
        return -1; // 内存分配失败
    }

    // 接收数据
    int ret = readn(cfd, buf, len);
    if (ret != len) {
        close(cfd);
        free(buf);
        return -1; // 接收数据失败
    }

    buf[len] = '\0'; // 添加字符串结束符
    *msg = buf;

    return ret; // 返回接收的字节数
}

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

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

相关文章

javaEE-01-tomcat

文章目录 javaWebTomcat启动 Tomcat 服务器测试服务器是否成功停止tomcat服务器修改服务器的端口号 Idea整合tomcat服务器 javaWeb 所有通过 Java 语言编写可以通过浏览器访问的程序的总称,是基于请求和响应来开发的。 请求: 客户端给服务器发送数据(Request)响应: 服务器给客…

LINUX环境下使用yum安装JDK1.8并配置环境变量

LINUX环境下使用yum安装JDK1.8并配置环境变量 1.查看CentOS自带JDK是否已安装 yum list installed |grep java2.批量卸载JDK rpm -qa | grep java | xargs rpm -e --nodeps3.直接yum安装1.8.0版本openjdk(其他版本请自行修改版本号) yum install java-1.8.0-openjdk* -y4.默…

4 Go语言的操作符

本专栏将从基础开始&#xff0c;循序渐进&#xff0c;由浅入深讲解Go语言&#xff0c;希望大家都能够从中有所收获&#xff0c;也请大家多多支持。 查看相关资料与知识库 专栏地址:Go专栏 如果文章知识点有错误的地方&#xff0c;请指正&#xff01;大家一起学习&#xff0c;…

智慧园区整体一站式解决方案(PPT原件完整版)

软件全套资料部分文档清单&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说明书&#xff0c;需求调研计划&#xff0c;用户需求调查单&#xff0c;用户需求说明书&#xff0c;概要设计说明书&#xff0c;技术解决方…

SQL labs-SQL注入(四,sqlmap对于post传参方式的注入)

本文仅作为学习参考使用&#xff0c;本文作者对任何使用本文进行渗透攻击破坏不负任何责任。 序言&#xff1a;本文主要讲解基于SQL labs靶场&#xff0c;sqlmap工具进行的post传参方式的SQL注入。 传参方式有两类&#xff0c;一类是直接在url栏内进行url编码后进行的传参&am…

批量打断相交线——ArcGIS 解决方法

在数据处理&#xff0c;特别是地理空间数据处理或是任何涉及图形和线条分析的场景中&#xff0c;有时候需要把相交的线全部从交点打断一个常见的需求。这个过程对于后续的分析、编辑、或是可视化展现都至关重要&#xff0c;因为它可以确保每条线都是独立的&#xff0c;避免了因…

LabVIEW放大器自动测量系统

开发了一个基于LabVIEW平台的多路前置放大器自动测量系统的开发与实施。该系统集成了硬件控制与软件编程&#xff0c;能够实现放大器各项性能指标的快速自动测量&#xff0c;有效提高了测试的精确性和效率。系统设计采用了虚拟仪器技术&#xff0c;结合了先进的测量与控制策略&…

Redis 7.x 系列【30】集群管理命令

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 概述2. 集群信息2.1 CLUSTER INFO 3. 节点管理3.1 CLUSTER MYID3.2 CLUSTER NODES3…

UI界面卡顿检测工具--UIHaltDetector

引言&#xff1a; 在日常工作当中&#xff0c;我们经常会遇到软件的界面出现卡顿的问题&#xff0c;而为了确定卡顿原因&#xff0c;我特地写了一个UI界面卡顿的小工具&#xff1a;UIHaltDetector&#xff1b;该工具可以在检测到目标窗口出现卡顿的时候直接打印堆栈日志和输出…

Windows系统安全加固方案:快速上手系统加固指南 (下)

这里写目录标题 一、概述二、IP协议安全配置启用SYN攻击保护 三、文件权限3.1 关闭默认共享3.2 查看共享文件夹权限3.3 删除默认共享 四、服务安全4.1禁用TCP/IP 上的NetBIOS4.2 ### 禁用不必要的服务 五、安全选项5.1启动安全选项5.2禁用未登录前关机 六、其他安全配置**6.1防…

基于 HTML+ECharts 实现的数据可视化大屏案例(含源码)

数据可视化大屏案例&#xff1a;基于 HTML 和 ECharts 的实现 数据可视化已成为企业决策和业务分析的重要工具。通过直观、动态的图表展示&#xff0c;数据可视化大屏能够帮助用户快速理解复杂的数据关系&#xff0c;发现潜在的业务趋势。本文将介绍如何利用 HTML 和 ECharts 实…

基于JAVA+SpringBoot+Vue的前后端分离的医院后勤管理系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 医院后勤管理系统是一…

LLama 405B 技术报告解读

LLama 405B 技术报告解读 果然传的消息都是真的&#xff0c;meta在24号凌晨发布了llama 3的405B版本&#xff0c;这次还是做一个技术报告解读。 值得一提的是&#xff0c;在技术报告的开头&#xff0c;meta特意强调了一个 Managing complexity&#xff0c;大意是管控复杂度。…

echarts实现在市级行政区点击县级行政区,显示单个县级行政区地图数据

因需兼容ie&#xff0c;此处所有变量声明都用var。如无需支持&#xff0c;可另做let修改。 这里以常州市为例,我们可以去阿里云提供的地理工具去截取地图json数据DataV.GeoAtlas地理小工具系列 点击所选区域&#xff0c;右侧会对应显示json数据&#xff0c;再次点击右侧红框内…

【Hec-Ras】案例1:韩国Seung-gi stream稳定流/非稳定流模拟

Hec-Ras案例1&#xff1a;韩国Seung-gi stream 研究区域&#xff1a;Seung-gi stream&#xff08;韩国&#xff09;研究数据降水数据&#xff08;Rainfall data&#xff09; 步骤1&#xff1a;创建工程文件/打开已有工程文件步骤2&#xff1a;参数调整步骤2.1&#xff1a;数据导…

Android .rc规则详解与init 启动

系列文章请扫关注公众号&#xff01; 简介 Android的init进程是启动各种服务的核心进程&#xff0c;并处理属性设置等。怎么启动各个服务和监听属性的呢&#xff1f;启动过程中会解析rc文件,并存下来。当系统属性更改或启动某项服务时&#xff0c;init就会按照rc中的设置运行对…

SpringBoot集成Tomcat、DispatcherServlet

通过 SpringBoot 自动配置机制&#xff0c;导入配置类 利用 SpringBoot 自动配置机制&#xff0c;SpringBoot 会导入一个类型为 ServletWebServerFactoryAutoConfiguration 的配置类 ServletWebServerFactoryAutoConfiguration ServletWebServerFactoryAutoConfigurations 类上…

软考中级网络工程师考什么?应该怎么正确备考

网络工程师软考中级难易度50%&#xff0c;不太难。但是如果准备不足就悬了&#xff0c;赶紧备考起来吧。 网络工程师每年考两次&#xff0c;相比其他的软考考试一年中考的机会又多了一次&#xff0c;而且软考网工也是挺热门的科目&#xff0c;每年很多人报考&#xff0c;相对的…

CoAP——Libcoap安装和使用(Ubuntu22.04)

1、简介 CoAP&#xff08;Constrained Application Protocol&#xff09;是一种专为受限设备和网络设计的应用层协议。它类似于HTTP&#xff0c;但具有更轻量级的特性&#xff0c;适合用于物联网&#xff08;IoT&#xff09;环境中的低功耗和低带宽设备。Libcoap是一个轻量级的…

RK3568 Linux 平台开发系列讲解(内核入门篇):如何高效地阅读 Linux 内核设备驱动

在嵌入式 Linux 开发中,设备驱动是实现操作系统与硬件之间交互的关键。对于 RK3568 这样的平台,理解和阅读 Linux 内核中的设备驱动程序至关重要。 1. 理解内核架构 在阅读设备驱动之前,首先要了解 Linux 内核的基本架构。内核主要由以下几个部分组成: 内核核心:处理系…