UDP内网穿透和打洞原理的C语言代码实现

news/2025/3/18 15:14:01/文章来源:https://www.cnblogs.com/zqingyang/p/18232244

v1.0 2024年6月5日 发布于博客园

目录
  • 序言
    • UDP打洞的原理
    • 应用场景
  • 基本理论
  • 代码实现
    • udp_client_NAT.c
    • udp_server_NAT.c
    • 结果
  • 参考链接

序言

image

UDP打洞(UDP Hole Punching)是一种用于在NAT(网络地址转换)设备后面建立直接P2P(点对点)连接的技术。NAT设备通常会阻止外部设备直接与内部设备通信,因为它们隐藏了内部网络的IP地址。UDP打洞通过利用NAT设备的行为特性来绕过这些限制,从而实现直接通信。

UDP打洞的原理

  1. 初始连接:两个希望进行P2P通信的设备(称为A和B)首先都与一个公共服务器(称为中继服务器)建立连接。中继服务器记录下它们的公共IP地址和端口号。

  2. 交换信息:中继服务器将A的公共IP地址和端口号发送给B,同时将B的公共IP地址和端口号发送给A。

  3. 打洞尝试:A和B使用从中继服务器获得的对方的公共IP地址和端口号,尝试直接向对方发送UDP数据包。由于NAT设备通常会允许内部设备发起的连接通过,因此这些数据包会在NAT设备上打开一个临时的“洞”。

  4. 建立连接:如果A和B的NAT设备都允许这种临时的“洞”,那么A和B就可以通过这些洞进行直接的P2P通信,而不再需要通过中继服务器。

应用场景

UDP打洞技术在许多应用中非常有用,尤其是在需要高效、低延迟的P2P通信时。以下是一些常见的应用场景:

  1. 实时通信应用:如VoIP(网络电话)、视频聊天和在线游戏等。这些应用需要低延迟的通信,而通过中继服务器转发数据会增加延迟。

  2. 文件共享:P2P文件共享网络(如BitTorrent)可以利用UDP打洞技术来建立直接连接,从而提高传输速度和效率。

  3. 远程控制和协作:如远程桌面、在线协作工具等,通过直接P2P连接可以提供更流畅的用户体验。

  4. 物联网(IoT)设备:许多IoT设备位于NAT设备后面,UDP打洞可以使它们更容易与外部服务器或其他设备直接通信。

  5. 游戏主机和客户端:在线游戏通常需要快速的P2P连接来同步游戏状态和动作,UDP打洞技术可以显著改善游戏体验。

UDP打洞是一种非常有用的技术,尤其是在需要高效、低延迟的P2P通信的应用中。它通过巧妙地利用NAT设备的行为特性,使得位于NAT设备后面的设备也能够进行直接的P2P通信。

基本理论

/*** 前提: 服务器具有公网ip, 客户端和服务端已经协商好端口号** 第一步: 客户端发送打洞包给服务器 C---NET--->S (此时客户端看得见服务器, 服务器看不见客户端)*         客户端向服务器发送一个UDP包。NAT设备会为这个连接分配一个公网IP和端口,并将包转发给服务器。** 第二步: 服务器接收并记录客户端信息  (服务器记录客户端的网路信息, 但不知道万恶的运营商有没有关掉这条网路)*         服务器接收到包后,记录下客户端的公网IP和端口。** 第三步: 服务器发送确认信息给客户端: C<---NET---S (服务器沿着原来的网路回传信息, 客户端若收到后维持网路)*         服务器向客户端发送确认消息,确保NAT设备为这对IP和端口建立了映射。** 第四步: 保存映射:C<---NET--->S 需要不断发送保活包, 建议为1/2超时时间 (这条路可通, 不断维持这条路)*          客户端和服务器通过发送UDP包来保持这个映射。只要映射存在,后续的UDP包可以直接穿过NAT设备。*          NAT设备会在一段时间内没有检测到任何活动后关闭映射。这段时间通常被称为“空闲超时时间”或“会话超时时间”。*          UDP超时时间:常见的默认值是30秒、60秒或120秒。*          TCP超时时间: 通常在数分钟到数小时之间。*/

由于我并不需要实现2个客户端的直接通信(增加一个中间服务器即可), 而是在典型的NAT穿透场景中,知道服务器端的公网IP和端口,但不知道客户端的公网IP,可以通过一些技巧来实现UDP打洞。以下是一个可能的方案:

  1. 服务器端:服务器端监听来自客户端的连接请求,并记录客户端的公网IP和端口。
  2. 客户端:客户端向服务器发送一个初始消息,服务器记录该消息的来源IP和端口。
  3. 服务器:服务器将记录的客户端IP和端口返回给客户端。
  4. 双方打洞:客户端和服务器通过发送UDP包到对方的IP和端口来打洞。

代码实现

编译命令:cc udp_client_NAT.c -o udp_client_NAT.out -pthread

udp_client_NAT.c

/*** @file name : udp_client_NAT.c* @brief     : 用于实现基本的UDP客户端和服务器端打洞* @author    : RISE_AND_GRIND@163.com* @date      : 2024/04/07* @version   : 1.0* @note      :* CopyRight (c)  2023-2024   RISE_AND_GRIND@163.com   All Right Reseverd*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <errno.h>#define SERVER_PORT 50001            // 公网服务器的端口
#define SERVER_ADDR "120.79.143.250" // 公网服务器的IP地址
#define BUF_SIZE 1024                // 缓冲区大小(字节)
#define KEEP_ALIVE_INTERVAL 25       // 保活包发送间隔
// 保活包的网路信息
typedef struct
{int sock_fd;                    // 套接字文件描述符struct sockaddr_in socket_addr; // 定义套接字所需的地址信息结构体socklen_t addr_len;             // 目标地址的长度pthread_mutex_t *mutex;         // 互斥锁变量
} KeepAlivePackageArgs_t;/*** @name      keep_alive* @brief     保活线程函数, 用于保持活路* @param     args 线程例程参数, 传入保活包的网络信息* @note*/
void *keep_alive(void *args)
{// 用于传入的是void* 需要强转才能正确指向KeepAlivePackageArgs_t *ka_args = (KeepAlivePackageArgs_t *)args;char keep_alive_msg[] = "KEEP_ALIVE_CLIENT"; // 发送给服务器的保活包, 表示我是客户端, C---NET-->Swhile (1){/*** 对互斥锁进行上锁,如果主线程未上锁,则此次调用会上锁成功,函数调用将立马返回;* 如果互斥锁此时已经被其它线程锁定了,会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。*/pthread_mutex_lock(ka_args->mutex);sendto(ka_args->sock_fd,keep_alive_msg,strlen(keep_alive_msg),MSG_CONFIRM, // 帮助你确认数据包的路径可达性。具体地,内核会尝试确认目标地址是可达的,并且路径是有效的。且避免不必要的探测.(const struct sockaddr *)&ka_args->socket_addr,ka_args->addr_len);pthread_mutex_unlock(ka_args->mutex); // 解锁printf("\n客户端已发服务器送保活包\n");sleep(KEEP_ALIVE_INTERVAL); // 定期保活}
}int main(int argc, char const *argv[])
{char validbuffer[BUF_SIZE]; // 传回的有效数据pthread_mutex_t mutex;pthread_mutex_init(&mutex, NULL); // 初始化套接字文件互斥锁/**********************第一步: 客户端发送打洞包给服务器 C---NET--->S******************************//*****①创建套接字文件描述符****/int client_sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建客户端套接字文件描述符 ipv4 udp 默认协议选择if (0 > client_sock_fd){fprintf(stderr, "客户端UDP套接字文件错误,errno:%d,%s\n", errno, strerror(errno));exit(1);}/****************END***************//****************②发送信息给服务器****************/// 服务器的IP信息结构体struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));// 配置服务器IP信息结构体server_addr.sin_family = AF_INET;                     // ipv4协议簇server_addr.sin_addr.s_addr = inet_addr(SERVER_ADDR); // 服务器公网IPserver_addr.sin_port = htons(SERVER_PORT);            // 服务器端口// 向服务器发送打洞包char buffer[BUF_SIZE] = "HELLO_SERVER";                            // 发送给服务器的打洞包 内容无所谓socklen_t addr_len = sizeof(struct sockaddr_in);                   // 信息结构体长度ssize_t sent_bytes = sendto(client_sock_fd,                        // 客户端套接字文件描述符buffer,                                // 要发送的数据缓冲区strlen(buffer),                        // 要发送的字符串长度MSG_CONFIRM,                           // 确认数据包有效性标志位(const struct sockaddr *)&server_addr, // 指向包含目标地址的 sockaddr 结构体addr_len);                             // 目标地址的长度if (sent_bytes == -1){fprintf(stderr, "发送数据失败, errno:%d, %s\n", errno, strerror(errno));close(client_sock_fd);exit(1);}memset(buffer, 0x0, sizeof(buffer)); // 清空buffer/****************END***************//************************************END*****************************************/// 第二步由服务器完成/**********************第三步: 服务器发送确认信息给客户端: C<---NET---S******************************/// 接收来自服务器的确认消息int n = recvfrom(client_sock_fd,                  // 套接字文件描述符buffer,                          // 接收数据的缓冲区BUF_SIZE,                        // 缓冲区的长度0,                               // MSG_WAITALL 严格等待完整的数据,会一直阻塞,直到接收到指定数量的字节(即 BUF_SIZE)或者发生错误为止。它确保接收到的数据量满足请求的大小。(struct sockaddr *)&server_addr, // 指向存储源地址的 sockaddr 结构体&addr_len);                      // 指向源地址长度的指针buffer[n] = '\0';                                 // 将接收到的数据转换为字符串printf("打洞中, 从服务器收到: %s\n", buffer);/************************************END*****************************************//**********************第四步: 保存映射:C<---NET--->S *******************************************/// 新线程的TIDpthread_t keep_alive_thread;// 定义线程保活包的网络信息KeepAlivePackageArgs_t ka_args;// 设置保活线程参数ka_args.sock_fd = client_sock_fd;ka_args.socket_addr = server_addr;ka_args.addr_len = addr_len;ka_args.mutex = &mutex;// 创建保活线程  并将保活包的网络信息传入线程if (pthread_create(&keep_alive_thread, NULL, keep_alive, (void *)&ka_args) != 0){fprintf(stderr, "创建保活线程错误, errno:%d,%s\n", errno, strerror(errno));close(client_sock_fd);exit(1);}/************************************END*****************************************//************************************正常收发数据部分*******************************************/// // 发送消息给服务器端// const char *message = "我是客户端!";// size_t message_len = strlen(message);// 主循环:接收和处理服务器消息while (1){// 发送消息给服务器端// 从键盘输入字符串char message[BUF_SIZE];printf("请输入要发送给服务器的消息: ");if (fgets(message, BUF_SIZE, stdin) == NULL){perror("fgets error");continue;}// 移除换行符size_t message_len = strlen(message);if (message[message_len - 1] == '\n'){message[message_len - 1] = '\0';message_len--;}// 向服务器发送数据pthread_mutex_lock(&mutex); // 对套接字文件上锁sendto(client_sock_fd, message, message_len, MSG_CONFIRM, (const struct sockaddr *)&server_addr, addr_len);pthread_mutex_unlock(&mutex); // 对套接字文件解锁// 接收来自服务器的消息pthread_mutex_lock(&mutex); // 对套接字文件上锁n = recvfrom(client_sock_fd, buffer, BUF_SIZE, 0, (struct sockaddr *)&server_addr, &addr_len);pthread_mutex_unlock(&mutex); // 对套接字文件解锁if (n < 0){perror("recvfrom error");continue;}buffer[n] = '\0';// 判断是否是保活信息if (strcmp(buffer, "KEEP_ALIVE_SERVER") != 0) // 若收到的不是保活信息, 则为有效信息{// 有效信息, 不是保活信息,处理并存储printf("★收到有效信息: %s\n", buffer);memcpy(validbuffer, buffer, n + 1); // 使用 memcpy 代替 strncpy}else{printf("\n从服务器收到保活信息: %s\n", buffer);}// 清空 buffermemset(buffer, 0, BUF_SIZE);}/************************************END*****************************************/close(client_sock_fd); // 关闭套接字文件return 0;
}
/*** 前提: 服务器具有公网ip, 客户端和服务端已经协商好端口号** 第一步: 客户端发送打洞包给服务器 C---NET--->S (此时客户端看得见服务器, 服务器看不见客户端)*         客户端向服务器发送一个UDP包。NAT设备会为这个连接分配一个公网IP和端口,并将包转发给服务器。** 第二步: 服务器接收并记录客户端信息  (服务器记录客户端的网路信息, 但不知道万恶的运营商有没有关掉这条网路)*         服务器接收到包后,记录下客户端的公网IP和端口。** 第三步: 服务器发送确认信息给客户端: C<---NET---S (服务器沿着原来的网路回传信息, 客户端若收到后维持网路)*         服务器向客户端发送确认消息,确保NAT设备为这对IP和端口建立了映射。** 第四步: 保存映射:C<---NET--->S 需要不断发送保活包, 建议为1/2超时时间 (这条路可通, 不断维持这条路)*          客户端和服务器通过发送UDP包来保持这个映射。只要映射存在,后续的UDP包可以直接穿过NAT设备。*          NAT设备会在一段时间内没有检测到任何活动后关闭映射。这段时间通常被称为“空闲超时时间”或“会话超时时间”。*          UDP超时时间:常见的默认值是30秒、60秒或120秒。*          TCP超时时间: 通常在数分钟到数小时之间。*/

udp_server_NAT.c

/*** @file name : udp_server_NAT.c* @brief     : 用于实现基本的UDP客户端和服务器端打洞* @author    : RISE_AND_GRIND@163.com* @date      : 2024/04/07* @version   : 1.0* @note      :* CopyRight (c)  2023-2024   RISE_AND_GRIND@163.com   All Right Reseverd*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <errno.h>#define CLIENT_PORT 50001      // 客户端的端口
#define BUF_SIZE 1024          // 缓冲区大小(字节)
#define KEEP_ALIVE_INTERVAL 25 // 保活包发送间隔// 保活包的网路信息
typedef struct
{int sock_fd;                    // 套接字文件描述符struct sockaddr_in socket_addr; // 定义套接字所需的地址信息结构体socklen_t addr_len;             // 目标地址的长度pthread_mutex_t *mutex;         // 互斥锁变量
} KeepAlivePackageArgs_t;/*** @name      keep_alive* @brief     保活线程函数, 用于保持活路* @param     args 线程例程参数, 传入保活包的网络信息* @note*/
void *keep_alive(void *args)
{// 用于传入的是void* 需要强转才能正确指向KeepAlivePackageArgs_t *ka_args = (KeepAlivePackageArgs_t *)args;char keep_alive_msg[] = "KEEP_ALIVE_SERVER"; // 发送给客户端的保活包, 表示我是服务器, C<---NET--Sfor (;;){/*** 对互斥锁进行上锁,如果主线程未上锁,则此次调用会上锁成功,函数调用将立马返回;* 如果互斥锁此时已经被其它线程锁定了,会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。*/pthread_mutex_lock(ka_args->mutex);sendto(ka_args->sock_fd,keep_alive_msg,strlen(keep_alive_msg),MSG_CONFIRM, // 帮助你确认数据包的路径可达性。具体地,内核会尝试确认目标地址是可达的,并且路径是有效的。且避免不必要的探测.(const struct sockaddr *)&ka_args->socket_addr,ka_args->addr_len);pthread_mutex_unlock(ka_args->mutex); // 解锁printf("\n服务器已向客户端发送保活包\n");sleep(KEEP_ALIVE_INTERVAL);}
}
int main(int argc, char const *argv[])
{char validbuffer[BUF_SIZE]; // 传回的有效数据pthread_mutex_t mutex;pthread_mutex_init(&mutex, NULL); // 初始化套接字文件互斥锁/**********************第二步: 服务器接收并记录客户端信息 C---NET--->S******************************//*****①创建套接字文件描述符并绑定接收****/int server_sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建客户端套接字文件描述符 ipv4 udp 默认协议选择if (0 > server_sock_fd){fprintf(stderr, "服务器端创建UDP套接字文件错误,errno:%d,%s\n", errno, strerror(errno));exit(1);}// 服务器端的IP信息结构体struct sockaddr_in server_addr;// 配置服务器地址信息 接受来自任何地方的数据 包有效 但只解析50001端口的包memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(CLIENT_PORT);// 绑定socket到指定端口if (bind(server_sock_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){fprintf(stderr, "将服务器套接字文件描述符绑定IP失败, errno:%d,%s\n", errno, strerror(errno));close(server_sock_fd);exit(1);}printf("服务器已经运行, 等待客户端响应中...\n");/****************END***************//****************②接收客户端的打洞包****************/char buffer[BUF_SIZE];               // 存放接收到的数据缓冲区memset(buffer, 0x0, sizeof(buffer)); // 清空bufferstruct sockaddr_in client_addr;socklen_t addr_len = sizeof(struct sockaddr_in);int n = recvfrom(server_sock_fd, buffer, BUF_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);buffer[n] = '\0';printf("解除阻塞, 从客户端收到信息: %s\n", buffer);printf("客户端NAT地址: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));/****************END***************//************************************END*****************************************//**********************第三步: 服务器发送确认信息给客户端: C<---NET---S******************************/// 向客户端发送确认消息char ack_msg[BUF_SIZE];snprintf(ack_msg, BUF_SIZE, "ACK %s %d", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));sendto(server_sock_fd, ack_msg, strlen(ack_msg), MSG_CONFIRM, (const struct sockaddr *)&client_addr, addr_len);memset(buffer, 0x0, sizeof(buffer)); // 清空bufferprintf("打洞完成, 进入交互\n");/************************************END*****************************************//**********************第四步: 保存映射:C<---NET--->S *******************************************/// 新线程的TIDpthread_t keep_alive_thread;// 定义线程保活包的网络信息KeepAlivePackageArgs_t ka_args;// 设置保活线程参数ka_args.sock_fd = server_sock_fd;ka_args.socket_addr = client_addr;ka_args.addr_len = addr_len;ka_args.mutex = &mutex;// 创建保活线程  并将保活包的网络信息传入线程if (pthread_create(&keep_alive_thread, NULL, keep_alive, (void *)&ka_args) != 0){fprintf(stderr, "创建保活线程错误, errno:%d,%s\n", errno, strerror(errno));close(server_sock_fd);exit(1);}/************************************END*****************************************/// 发送消息给客户端const char *message = "我是服务器, 收到请回答!";size_t message_len = strlen(message);// 主循环:接收和响应客户端消息while (1){// 发送消息给客户端pthread_mutex_lock(&mutex); // 对套接字文件上锁sendto(server_sock_fd, message, message_len, MSG_CONFIRM, (const struct sockaddr *)&client_addr, addr_len);pthread_mutex_unlock(&mutex); // 对套接字文件解锁// 接收来自客户端的消息pthread_mutex_lock(&mutex); // 对套接字文件上锁n = recvfrom(server_sock_fd, buffer, BUF_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);pthread_mutex_unlock(&mutex); // 对套接字文件解锁if (n < 0){perror("recvfrom error");continue;}buffer[n] = '\0';// 判断是否是保活信息if (strcmp(buffer, "KEEP_ALIVE_CLIENT") != 0) // 若收到的不是保活信息, 则为有效信息{// 有效信息, 不是保活信息,处理并存储printf("★收到有效信息: %s\n", buffer);memcpy(validbuffer, buffer, n + 1); // 使用 memcpy 代替 strncpy}else{printf("\n从客户端收到保活信息: %s\n", buffer);}// 清空 buffermemset(buffer, 0, BUF_SIZE);}close(server_sock_fd);return 0;
}
/*** 前提: 服务器具有公网ip, 客户端和服务端已经协商好端口号** 第一步: 客户端发送打洞包给服务器 C---NET--->S (此时客户端看得见服务器, 服务器看不见客户端)*         客户端向服务器发送一个UDP包。NAT设备会为这个连接分配一个公网IP和端口,并将包转发给服务器。** 第二步: 服务器接收并记录客户端信息  (服务器记录客户端的网路信息, 但不知道万恶的运营商有没有关掉这条网路)*         服务器接收到包后,记录下客户端的公网IP和端口。** 第三步: 服务器发送确认信息给客户端: C<---NET---S (服务器沿着原来的网路回传信息, 客户端若收到后维持网路)*         服务器向客户端发送确认消息,确保NAT设备为这对IP和端口建立了映射。** 第四步: 保存映射:C<---NET--->S 需要不断发送保活包, 建议为1/2超时时间 (这条路可通, 不断维持这条路)*          客户端和服务器通过发送UDP包来保持这个映射。只要映射存在,后续的UDP包可以直接穿过NAT设备。*          NAT设备会在一段时间内没有检测到任何活动后关闭映射。这段时间通常被称为“空闲超时时间”或“会话超时时间”。*          UDP超时时间:常见的默认值是30秒、60秒或120秒。*          TCP超时时间: 通常在数分钟到数小时之间。*/

结果

打洞模块编写成功, 可实现内网客户端与服务器相互UDP通信

image

image

参考链接

  • P2P通信原理与实现(C语言) - 知乎 (zhihu.com)
  • UDP内网穿透和打洞原理与代码实现 - 知乎 (zhihu.com)

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

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

相关文章

低代码智能通信:腾讯云短信助力,快速构建高效消息应用

前言 ​ 随着信息技术的飞速发展,现代社会对信息传达的及时性、准确性与便捷性要求越来越高。尤其在移动互联网时代,用户对于服务的体验要求不断提升,这促使各类网站、APP、小程序等服务平台必须持续优化其交互方式,以满足用户日益增长的需求。 ​ 在此背景下,短信作为一种…

.NET C# 程序自动更新组件

实现一个轻量级独立自动更新组件,可稍作修改集成到大家自己项目中,比如:WPF/Winform/Windows服务引言 本来博主想偷懒使用AutoUpdater.NET组件,但由于博主项目有些特殊性和它的功能过于多,于是博主自己实现一个轻量级独立自动更新组件,可稍作修改集成到大家自己项目中,比…

IC设计企业致力于解决的HPC数据防泄漏,到底该怎么做?

对于半导体IC设计企业来说,芯片设计、验证、仿真使用HPC环境现在已逐渐成为趋势,主要原因在于原来的工作流程存在较多的缺陷: 性能瓶颈:仿真、设计、验证、生产过程中,前端仿真需要小文件高并发低时延的读写和巨量元数据处理能力,后端仿真存储需要提供很大的读写带宽满足…

重写学习 localStorage 与 sessionStorage

localStorage 与 sessionStorage localStorage 与 sessionStorage 很多小伙伴对它们俩都很熟悉了;最熟悉的莫过下面这2条 1,localStorage 存储的数据没有时间限制,理论上永久有效;除非手动清除。sessionStorage 存储的数据在关闭当前页面后失效; 2,有存储大小限制,两者存储大…

ASP.NET Web应用程序升级最新的MSBuild格式后,Visual Studio 2022中如何调试?

摘要 把ASP.NET的Web应用程序,Project文件从 <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">改为 <Project Sdk="Microsoft.NET.Sdk.Web">之后,升…

AI带你玩转音乐,使你成为真正音乐人(AI歌曲制作)

利用AI工具快速完成一首歌: 歌由词和曲组成。 词可以利用文心一言 输入:写一首赞扬国家繁华发展的歌词 这样一首歌的词部分已经出来。 曲部分: 利用https://suno.com/create生成: 拷贝文心一言生成的词 选择定制模式Custom Mode有更多的风格可以制作点击生成就可以完成曲制…

零基础写框架(2):故障排查和日志基础

关于从零设计 .NET 开发框架 作者:痴者工良 教程说明: 仓库地址:https://github.com/whuanle/maomi 文档地址:https://maomi.whuanle.cn 作者博客: https://www.whuanle.cn https://www.cnblogs.com/whuanle 故障排查和日志 .NET 程序进行故障排查的方式有很多,笔者个人总…

使用Visual Studio分析.NET Dump

前言 内存泄漏和高CPU使用率是在日常开发中经常遇到的问题,它们可能会导致应用程序性能下降甚至崩溃。今天我们来讲讲如何使用Visual Studio 2022分析.NET Dump,快速找到程序内存泄漏问题。 什么是Dump文件? Dump文件又叫内存转储文件或者叫内存快照文件。用于存储程序运行时…

springboot模块化开发项目搭建

1.New一个Project,命名,Next,Finish 2.根据需要修改Maven配置 3.初始化后,删除无用文件4.选中项目创建Module,命名,Next,一般模块包括common、dao、service、web、entrance(入口文件),也可以增加订单、会员等各种业务模块,各模块之间依赖引用即可5.删除无用文件,删…

组合数学中的食用工具

背景: 教授在打概率和期望中的《灯蹬登》,需要推式子。众所周知,一个正确的式子不光要可以解释已有的数据,还要能预测未知数据的结果。在这样的情况下,组合数学的工具是必不可少的。我们通过这个工具实现了三种计算器无法直接实现的功能:输入\(A,m,n\),表示求\(A^m_n\)的…

读AI未来进行式笔记03自然语言处理技术

自然语言处理1. AI伙伴 1.1. 作为AI能力的集大成者,AI伙伴融合了各种复杂的AI技术 1.2. 人类唯一可能超越AI的领域,只可能在机器无法触及之处,那是属于人类感性与直觉的领域 1.3. 要读懂人类,需要漫长而平缓的学习过程 1.4. AI塑造了我们,我们反过来也塑造了AI 1.5. AI的“…