首页 > 其他分享 >UDP内网穿透和打洞原理的C语言代码实现

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

时间:2024-06-05 09:11:20浏览次数:20  
标签:UDP addr 保活 C语言 mutex 服务器 内网 客户端

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

目录

序言

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    : [email protected]
 * @date      : 2024/04/07
 * @version   : 1.0
 * @note      :
 * CopyRight (c)  2023-2024   [email protected]   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-->S
    while (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); // 服务器公网IP
    server_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 *******************************************/
    // 新线程的TID
    pthread_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);
        }

        // 清空 buffer
        memset(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    : [email protected]
 * @date      : 2024/04/07
 * @version   : 1.0
 * @note      :
 * CopyRight (c)  2023-2024   [email protected]   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--S

    for (;;)
    {
        /**
         * 对互斥锁进行上锁,如果主线程未上锁,则此次调用会上锁成功,函数调用将立马返回;
         * 如果互斥锁此时已经被其它线程锁定了,会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。
         */
        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)); // 清空buffer

    struct 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)); // 清空buffer
    printf("打洞完成, 进入交互\n");
    /************************************END*****************************************/
    /**********************第四步: 保存映射:C<---NET--->S *******************************************/
    // 新线程的TID
    pthread_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);
        }

        // 清空 buffer
        memset(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

参考链接

标签:UDP,addr,保活,C语言,mutex,服务器,内网,客户端
From: https://www.cnblogs.com/zqingyang/p/18232244

相关文章

  • C语言Kruskal算法求最小生成树
    Kruskal算法求出最小生成树。图形算法描述先找最小权值边为1的边有(V1,V4),(V2,V9),保证不产生回路就可以成功选择边除去上一次找的边后,在找权值最小的边为2的有(V2,V3),(V4,V3),(V5,V6),(V9,V8),连接不产生回路的边除去之前找过的边,后面再看权值最小的边为3的边有(V1,V3),(V7,V8),(V9,V7)按顺......
  • 渗透测试——工作组内网信息收集(1)
    目录1、工作组信息收集(在这里我以自己物理机win11举例)(1)用户信息(2)系统信息 (3)网络信息 (5)RDP远程桌面 (6)获取杀软信息 参考下面这个文章,找找对应的杀毒软化进程(7)代理信息​编辑 (8)WIFI密码指定获取密码 获取所有连过的wifi密码 (9)回收站信息 (10)谷歌浏览器(这个......
  • C语言第三篇-数组
    数组定义数组是一组相同类型元素的集合;也就是一组数。数组的创建方式chararr3[10];//char是指数组的元素类型;10是一个常量表达式,用来指定数组的大小floatarr4[5];//float是指数组的元素类型;5是一个常量表达式,用来指定数组的大小doublearr5[2......
  • C语言数据结构实现-顺序表基本操作
    顺序表,全名顺序存储结构,是线性表的一种。通过《什么是线性表》一节的学习我们知道,线性表用于存储逻辑关系为“一对一”的数据,顺序表自然也不例外。不仅如此,顺序表对数据的物理存储结构也有要求。顺序表存储数据时,会提前申请一整块足够大小的物理空间,然后将数据依次存储起来,存储时......
  • 扫雷游戏(C语言)(超详细!新手小白入!)
    扫雷游戏详细过程一.前言二.游戏的分析和设计1.厘清整体思路2.棋盘的构建与思路3.初始化及打印棋盘4.布置雷5.排查雷三.扫雷游戏的扩展一.前言游戏介绍这是一款经典的扫雷游戏,玩家可以任意点击一个小方框,若不是雷,则会显示周边有几个雷,并把雷的个数显示出来,若是雷,......
  • UDP协议的应用——域名解析
    设计程序实现解析www.baidu.com的域名,把获取到的百度的IP地址全部输出到终端并验证是否正确/*************************************************************************************************************************** filename: udp_cs.c* author :Dazz* d......
  • 操作系统入门系列-MIT6.828(操作系统工程)学习笔记(四)---- C语言与计算机架构(Programmin
    系列文章目录操作系统入门系列-MIT6.S081(操作系统)学习笔记(一)----操作系统介绍与接口示例操作系统入门系列-MIT6.828(操作系统工程)学习笔记(二)----课程实验环境搭建(wsl2+ubuntu+quem+xv6)操作系统入门系列-MIT6.828(操作系统工程)学习笔记(三)----xv6初探与实验一(Lab:Xv6and......
  • _weak c语言在实际使用中有什么核心作用
    C语言中__weak关键字的核心作用在C语言中,__weak关键字通常用于声明弱符号,它允许在链接阶段出现多个同名函数的情况下,选择性地指定某个函数具有较低的优先级。如果在代码中使用了__weak修饰的函数,并且在链接时没有找到其他同名的强符号函数,那么编译器会自动链接到这个弱符号函......
  • 网络编程练习题---利用UDP协议实现组播通信
    目录题目解析代码实现题目解析由于该题需要实现组播通信,所以我们需要将套接字文件句柄设置为组播属性,并将需要通信的用户端IP地址,加入组中。由于组播通信需要实现一对多发送消息,所以还需要将套接字文件句柄的广播属性一并开启。由于该题实现过程使用到了线程相关函数接口,所......
  • C语言杂谈:从Hello world说起 #include| main| printf| return
    #include<stdio.h>intmain(){ printf("Hellowworld"); return0;}        打印出“Helloworld”的这个程序相信每个人都是见过的,这段代码非常的简单,没有调用函数,没有使用指针,没有各种杂七杂八的东西,但我相信,第一次看见这个代码的朋友一定会有很多疑问。 ......