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

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.mzph.cn/web/47705.shtml

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

相关文章

【接口自动化_08课_Pytest+Yaml+Allure框架】

上节课一些内容 的补充 1、openxl这个方法&#xff0c;第一个元素是从1开始的&#xff0c;不是从0开始 回写的列在程序里写的是11&#xff0c;是因为是固定值 一、1. Yaml入门及应用 1、什么是yaml YAML&#xff08;/ˈjməl/&#xff0c;尾音类似camel骆驼&#xff09;是一…

Finding columns with a useful data type 找到合适的数据列的类型

Finding columns with a useful data type 在确定了原始查询的数据列数之后&#xff0c;接下来就是要确定合适的数据列的数据类型。可以用 SELECT a 的方式判断对应的数据列方式&#xff0c;有时候可以通过错误信息判断数据列的类型。如果服务器的响应没有报错&#xff0c;而…

Docker启动PostgreSql并设置时间与主机同步

在 Docker 中启动 PostgreSql 时&#xff0c;需要配置容器的时间与主机同步。可以通过在 Dockerfile 或者 Docker Compose 文件中设置容器的时区&#xff0c;或者使用宿主机的时间来同步容器的时间。这样可以确保容器中的 PostgreSql 与主机的时间保持一致&#xff0c;避免在使…

启动流程和切换流程

启动流程 #mermaid-svg-iUWGw8xl1SyAmoo9 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-iUWGw8xl1SyAmoo9 .error-icon{fill:#552222;}#mermaid-svg-iUWGw8xl1SyAmoo9 .error-text{fill:#552222;stroke:#552222;}#…

C语言·分支和循环语句(超详细系列·全面总结)

前言&#xff1a;Hello大家好&#x1f618;&#xff0c;我是心跳sy&#xff0c;为了更好地形成一个学习c语言的体系&#xff0c;最近将会更新关于c语言语法基础的知识&#xff0c;今天更新一下分支循环语句的知识点&#xff0c;我们一起来看看吧~ 目录 一、什么是语句&#xf…

linux配置podman阿里云容器镜像加速器

1.下载podman yum install -y podman systemctl status podman systemctl start podman 2.获取阿里云个人容器镜像加速器地址 访问阿里云官网&#xff1a;首先&#xff0c;您需要访问阿里云&#xff08;Alibaba Cloud&#xff09;的官方网站。阿里云官网的URL是&#xff1a;…

OS:处理机进程调度

1.BackGround&#xff1a;为什么要进行进程调度&#xff1f; 在多进程环境下&#xff0c;内存中存在着多个进程&#xff0c;其数目往往多于处理机核心数目。这就要求系统可以按照某种算法&#xff0c;动态的将处理机CPU资源分配给处于就绪状态的进程。调度算法的实质其实是一种…

对于品牌方来说,小红书探店应该怎么做?

小红书是中国最大的生活分享社交平台之一&#xff0c;它现在不仅仅是一个购物推荐平台&#xff0c;也是一个探店的好去处。 用户在网络上看到一家心仪的店铺&#xff0c;却又无法亲身到访&#xff0c;对店铺的产品存疑&#xff0c;这时候就会在小红书搜索具体的相关店铺信息。 …

【代码随想录】【算法训练营】【第58天 4】 [卡码104]建造最大岛屿

前言 思路及算法思维&#xff0c;指路 代码随想录。 题目来自 卡码网。 day 58&#xff0c;周四&#xff0c;ding~ 题目详情 [卡码104] 建造最大岛屿 题目描述 卡码104 建造最大岛屿 LeetCode类似题目827 最大人工岛 解题思路 前提&#xff1a; 思路&#xff1a; 重点…

【LeetCode】从前序与中序遍历序列构造二叉树

目录 一、题目二、解法完整代码 一、题目 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1: 输入: preorder [3,9,20,15,7], inorder [9…

在西藏上大学是一种什么体验?如何解决语言问题?

在西藏地区上大学是一种独特而难忘的经历&#xff0c;它不仅提供了接触壮丽自然风光和深入了解藏族文化的机会&#xff0c;也带来了适应高原气候和生活方式的挑战。学生将在这里体验到丰富的教育资源和学术研究素材&#xff0c;同时在生活和人际交往中培养适应能力和独立性。这…

密码学基础-Hash、MAC、HMAC 的区别与联系

密码学基础-Hash、MAC、HMAC 的区别与联系 Hash Hash 是一种从一段数据中创建小的数字“指纹”的方法。就像一个人的指纹代表一个人的信息一样&#xff0c;Hash 对输入的数据进行整理&#xff0c;生成一个代表该输入数据的“指纹” 数据。通常该指纹数据也可称之为摘要、散列…

文件包含漏洞: 函数,实例[pikachu_file_inclusion_local]

文件包含 文件包含是一种较为常见技术&#xff0c;允许程序员在不同的脚本或程序中重用代码或调用文件 主要作用和用途&#xff1a; 代码重用&#xff1a;通过将通用函数或代码段放入单独的文件中&#xff0c;可以在多个脚本中包含这些文件&#xff0c;避免重复编写相同代码。…

RabbitMQ的学习和模拟实现|Protobuf的介绍和简单使用

protbuf的介绍和简单使用 项目仓库&#xff1a;https://github.com/ffengc/HareMQ protobuf的安装&#xff1a;README-cn.md#环境配置 基于Protobuf的一个小项目&#xff1a;基于protobuf和httplib的在线通讯录项目框架&#xff5c;Protobuf应用小项目 protobuf是什么 Pro…

TinyVue:与 Vue 交往八年的组件库

本文由体验技术团队莫春辉老师原创~ 去年因故停办的 VueConf&#xff0c;今年如约在深圳举行。作为东道主 & 上届 VueConf 讲师的我&#xff0c;没有理由不来凑个热闹。大会结束后&#xff0c;我见裕波在朋友圈转发 Jinjiang 的文章《我和 Vue.js 的十年》&#xff0c;我就…

openlayers 3d 地图 非三维 立体地图 行政区划裁剪 地图背景

这是实践效果 如果没有任何基础 就看这个专栏&#xff1a;http://t.csdnimg.cn/qB4w0 这个专栏里有从最简单的地图到复杂地图的示例 最终效果&#xff1a; 线上示例代码&#xff1a; 想要做这个效果 如果你的行政区划编辑点较多 可能会有卡顿感 如果出现卡顿感需要将边界点相应…

Python爬虫-淘宝搜索热词数据

前言 本文是该专栏的第70篇,后面会持续分享python爬虫干货知识,记得关注。 在本专栏之前,笔者有详细针对“亚马逊Amazon搜索热词”数据采集的详细介绍,对此感兴趣的同学,可以往前翻阅《Python爬虫-某跨境电商(AM)搜索热词》进行查看。 而在本文,笔者将以淘宝为例,获取…

【扩散模型(五)】IP-Adapter 源码详解3-推理代码

系列文章目录 【扩散模型&#xff08;一&#xff09;】中介绍了 Stable Diffusion 可以被理解为重建分支&#xff08;reconstruction branch&#xff09;和条件分支&#xff08;condition branch&#xff09;【扩散模型&#xff08;二&#xff09;】IP-Adapter 从条件分支的视…

【OAuth2系列】集成微信小程序登录到 Spring Security OAuth 2.0

作者&#xff1a;后端小肥肠 创作不易&#xff0c;未经允许严禁转载。 姊妹篇&#xff1a; 【Spring Security系列】权限之旅&#xff1a;SpringSecurity小程序登录深度探索_spring security 微信小程序登录-CSDN博客 目录 1. 前言 2. 总体登录流程 3. 数据表设计 3.1. sys…

Python测试服务器连接的实战代码

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…