网络编程 · 代码笔记3

前言

  为了降低文章冗余度,编程环境以及编译命令在此篇中不再提及,可翻看本系列的前面两篇博文进行了解:
  网络编程 · 代码笔记1
  网络编程 · 代码笔记2

0161epoll_更高效的IO复用技术_条件触发_服务端

  相关代码:

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

#define BUF_SIZE 100
#define EPOLL_SIZE 50

int main(int argc, char const *argv[])
{
    // 创建一个TCP套接字
    int serv_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    printf("serv_sock:%d\n", serv_sock);

    // 初始化服务器地址结构
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));      // 将地址结构清零
    serv_addr.sin_family = AF_INET;                // 设置地址家族为IPv4
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器地址为任意地址
    serv_addr.sin_port = htons(1234);              // 设置服务器端口号为1234

    // 将套接字与服务器地址绑定
    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        perror("bind函数执行失败,绑定服务器信息失败!"); // 绑定失败,打印错误信息
        close(serv_sock);                                 // 关闭套接字
        return 0;                                         // 返回0表示程序执行失败
    }

    // 在套接字上监听,设置最大连接数为20
    if (listen(serv_sock, 20) == -1)
    {
        perror("listen 侦听失败!"); // 监听失败,打印错误信息
        close(serv_sock);            // 关闭套接字
        return 0;                    // 返回0表示程序执行失败
    }

    // 创建一个epoll实例
    int epoll_fd = epoll_create(EPOLL_SIZE);
    printf("epoll_fd:%d\n", epoll_fd);

    // 初始化一个epoll事件结构
    struct epoll_event event;
    event.events = EPOLLIN;    // 设置事件类型为可读
    event.data.fd = serv_sock; // 设置事件相关的文件描述符为服务器套接字

    // 将服务器套接字添加到epoll实例中,以便监听其事件
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, serv_sock, &event);

    // 分配epoll事件数组空间,用来存放有活动的文件描述符的信息
    struct epoll_event *epoll_events_p = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    memset(epoll_events_p, 0, (sizeof(struct epoll_event) * EPOLL_SIZE));

    while (1)
    {
        // 等待epoll事件,当有事件发生时返回事件的个数,-1表示无限等待
        int event_cnt = epoll_wait(epoll_fd, epoll_events_p, EPOLL_SIZE, -1);

        // 如果epoll_wait执行出错,打印错误信息并退出循环
        if (event_cnt == -1)
        {
            printf("epoll_wait() 函数执行错误!\n");
            break;
        }

        // 遍历所有的事件
        for (int i = 0; i < event_cnt; i++)
        {
            // 如果是服务器套接字的事件,表示有新的连接
            if (epoll_events_p[i].data.fd == serv_sock)
            {
                struct sockaddr_in clnt_addr;
                // 初始化客户端地址结构
                memset(&clnt_addr, 0, sizeof(clnt_addr));
                socklen_t clnt_addr_size = sizeof(clnt_addr);
                // 接受新的连接,并获取客户端的套接字
                int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);

                event.events = EPOLLIN;
                event.data.fd = clnt_sock;
                // 将新的客户端套接字添加到epoll的事件集中
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client:%d\n", clnt_sock);
            }

            // 如果是已连接客户端的事件
            else
            {
                char buf[BUF_SIZE] = {0};
                // 读取客户端发送的数据
                int str_len = read(epoll_events_p[i].data.fd, buf, BUF_SIZE);

                // 如果读取的字节数为0,表示客户端关闭了连接
                if (str_len == 0)
                {
                    // 从epoll的事件集中移除该客户端套接字
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events_p[i].data.fd, NULL);
                    // 关闭客户端套接字
                    close(epoll_events_p[i].data.fd);
                    printf("已关闭连接的客户端:%d\n", epoll_events_p[i].data.fd);
                }
                // 如果读取到数据,回显给客户端
                else
                {
                    write(epoll_events_p[i].data.fd, buf, str_len);
                }
            }
        }
    }

    free(epoll_events_p);
    close(serv_sock);
    close(epoll_fd);
    return 0;
}

0162epoll_更高效的IO复用技术_条件触发_客户端

  相关代码:

#include <stdio.h>      // 引入标准输入输出库
#include <string.h>     // 引入字符串操作库
#include <stdlib.h>     // 引入标准库
#include <unistd.h>     // 引入Unix标准函数库
#include <arpa/inet.h>  // 引入IP地址转换库
#include <sys/socket.h> // 引入套接字库

#define BUF_SIZE 1024 // 定义缓冲区大小

int main(int argc, char const *argv[])
{
    // 创建客户端套接字
    int clnt_sock = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv_addr;             // 定义服务器地址结构体
    memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体

    serv_addr.sin_family = AF_INET;                     // 设置地址家族为IPv4
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器IP地址为本地回环地址
    serv_addr.sin_port = htons(1234);                   // 设置服务器端口号为1234

    // 连接服务器
    connect(clnt_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    while (1)
    {
        // 提示用户输入信息
        fputs("请输入信息,输入“Q”退出:", stdout);

        char message[BUF_SIZE] = {0};                           // 初始化消息缓冲区
        fgets(message, BUF_SIZE, stdin);                        // 从标准输入读取一行数据到消息缓冲区
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) // 判断用户是否输入"Q"或"q"
            break;                                              // 如果是,跳出循环

        write(clnt_sock, message, strlen(message)); // 向服务器发送消息

        int result = read(clnt_sock, message, BUF_SIZE - 1); // 从服务器接收消息
        message[result] = 0;                                 // 设置消息结束符
        printf("来自服务器的信息为:%s\n", message);         // 打印接收到的消息
    }

    close(clnt_sock); // 关闭客户端套接字

    return 0; // 程序结束
}

  运行结果:

在这里插入图片描述

0171epoll_更高效的IO复用技术_边缘触发_服务端

  相关代码:

#include <stdio.h>      // 引入标准输入输出库
#include <stdlib.h>     // 引入标准库,提供多种函数
#include <string.h>     // 引入字符串操作库
#include <unistd.h>     // 引入Unix标准函数库
#include <arpa/inet.h>  // 引入IP地址转换库
#include <sys/socket.h> // 引入套接字库
#include <sys/epoll.h>  // 引入epoll库
#include <fcntl.h>      // 引入文件控制库
#include <errno.h>      // 引入错误号库

#define BUF_SIZE 4    // 定义缓冲区大小
#define EPOLL_SIZE 50 // 定义epoll事件表大小

int main(int argc, char const *argv[])
{
    int serv_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建TCP套接字

    struct sockaddr_in serv_addr;             // 定义服务器地址结构体
    memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体为0

    serv_addr.sin_family = AF_INET;                // 设置地址家族为IPv4
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器IP地址为任意地址
    serv_addr.sin_port = htons(1234);              // 设置服务器端口号为1234

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 绑定服务器信息
    {
        perror("bind函数执行失败,绑定服务器信息失败!"); // 输出错误信息
        close(serv_sock);                                 // 关闭套接字
        return 0;                                         // 程序退出
    }

    if (listen(serv_sock, 20) == -1) // 设置监听套接字
    {
        perror("listen 侦听失败!"); // 输出错误信息
        close(serv_sock);            // 关闭套接字
        return 0;                    // 程序退出
    }

    struct epoll_event event;  // 定义epoll事件结构体
    event.events = EPOLLIN;    // 设置事件为可读
    event.data.fd = serv_sock; // 设置事件相关的文件描述符为服务器套接字

    int epfd = epoll_create(EPOLL_SIZE);               // 创建epoll实例
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); // 将服务器套接字添加到epoll事件表中

    // 分配内存,用于存储epoll事件表
    struct epoll_event *epoll_events_p = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

    while (1)
    {
        // 等待epoll事件发生
        int event_cnt = epoll_wait(epfd, epoll_events_p, EPOLL_SIZE, -1);

        // 如果epoll_wait执行出错,输出错误信息并退出循环
        if (event_cnt == -1)
        {
            printf("epoll_wait() 函数执行错误!\n");
            break;
        }

        printf("epoll_wait 执行成功!\n"); // 如果epoll_wait执行成功,输出成功信息

        for (int i = 0; i < event_cnt; i++)
        {
            // 如果事件发生在服务器套接字上,表示有新的客户端连接
            if (epoll_events_p[i].data.fd == serv_sock)
            {
                struct sockaddr_in clnt_addr;                                              // 定义客户端地址结构体
                socklen_t adr_sz = sizeof(clnt_addr);                                      // 获取地址结构体大小
                int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &adr_sz); // 接受客户端连接

                /* 设置为非阻塞模式,即使没有数据可读,read 调用也会立即返回,而不是阻塞。 */
                int flag = fcntl(clnt_sock, F_GETFL, 0);      // 获取客户端套接字文件状态标志
                fcntl(clnt_sock, F_SETFL, flag | O_NONBLOCK); // 设置客户端套接字为非阻塞模式

                event.events = EPOLLIN | EPOLLET;                  // 设置事件为可读且边缘触发模式
                event.data.fd = clnt_sock;                         // 设置事件相关的文件描述符为客户端套接字
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); // 将客户端套接字添加到epoll事件表中
                printf("connected client:%d\n", clnt_sock);       // 输出客户端连接信息
            }

            // 如果事件发生在其他套接字上,表示有数据可读或连接已关闭
            else
            {
                while (1)
                {
                    char buf[BUF_SIZE] = {0};                                     // 定义缓冲区并初始化为0
                    int str_len = read(epoll_events_p[i].data.fd, buf, BUF_SIZE); // 从套接字读取数据
                    if (str_len < 0)
                    {
                        if (errno == EAGAIN)
                        {
                            break; // 如果读取操作会阻塞,跳出循环
                        }
                    }

                    if (str_len == 0)
                    {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, epoll_events_p[i].data.fd, NULL); // 从epoll事件表中删除套接字
                        close(epoll_events_p[i].data.fd);                                // 关闭套接字
                        printf("已关闭连接的客户端:%d\n", epoll_events_p[i].data.fd);   // 输出关闭连接信息
                        break;
                    }

                    else
                    {
                        write(epoll_events_p[i].data.fd, buf, str_len); // 将读取的数据写回套接字
                    }
                }
            }
        }
    }

    free(epoll_events_p);
    close(serv_sock);
    close(epfd);
    return 0;
}

0172epoll_更高效的IO复用技术_边缘触发_客户端

  相关代码:

#include <stdio.h>      // 引入标准输入输出库
#include <string.h>     // 引入字符串操作库
#include <stdlib.h>     // 引入标准库,提供多种函数
#include <unistd.h>     // 引入unistd库,提供对POSIX操作系统的API
#include <arpa/inet.h>  // 引入arpa/inet.h库,提供网络编程相关的函数和结构体
#include <sys/socket.h> // 引入sys/socket.h库,提供socket相关的函数
#include <fcntl.h>      // 引入文件控制头文件,提供文件控制相关的函数
#include <errno.h>      // 引入errno.h库,提供错误号相关的宏定义

#define BUF_SIZE 1024 // 定义缓冲区大小为1024字节

int main(int argc, char const *argv[])
{
    int sock = socket(AF_INET, SOCK_STREAM, 0); // 创建一个IPv4的TCP流式socket

    struct sockaddr_in serv_addr;             // 定义一个IPv4的socket地址结构
    memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化serv_addr结构体为0

    serv_addr.sin_family = AF_INET;                     // 设置地址家族为IPv4
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器的IP地址为本地回环地址
    serv_addr.sin_port = htons(1234);                   // 设置服务器的端口号为1234,并进行网络字节序的转换

    // 尝试连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        printf("connect() 函数执行错误,连接服务器失败!\n"); // 连接失败,打印错误信息
        close(sock);                                          // 关闭socket
        return -1;                                            // 返回-1表示程序执行失败
    }

    int flag = fcntl(sock, F_GETFL, 0);      // 获取当前socket的文件状态标志
    fcntl(sock, F_SETFL, flag | O_NONBLOCK); // 设置socket为非阻塞模式

    while (1)
    {
        fputs("输入信息,输入'Q'退出程序:", stdout); // 提示用户输入信息

        char message[BUF_SIZE] = {0};    // 初始化消息缓冲区
        fgets(message, BUF_SIZE, stdin); // 从标准输入读取一行数据到message

        // 如果用户输入的是'q'或'Q',则退出循环
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
        write(sock, message, strlen(message)); // 将用户输入的消息写入socket,发送给服务器

        sleep(2);                   // 程序暂停2秒
        printf("服务器的信息为:"); // 打印提示信息,准备接收服务器响应
        
        while (1)
        {
            memset(message, 0, sizeof(message));             // 清空消息缓冲区
            int str_len = read(sock, message, BUF_SIZE - 1); // 从socket读取数据到message
            if (str_len <= 0)                                // 如果读取的字节数小于等于0
            {
                if (errno == EAGAIN) // 如果错误号是EAGAIN,表示没有数据可读
                {
                    break; // 退出内层循环
                }
            }
            else
            {
                printf("%s", message); // 打印从服务器接收到的消息
            }
        }
    }

    close(sock);
    return 0;
}

  运行结果:

在这里插入图片描述

  注意!客户端发送信息给服务端后会进入阻塞状态 2 秒,期间服务器发送数据过来会存储在客户端的输入缓冲区中,等客户端线程重新获取到cpu时,进入运行状态后才将数据读取出来,并显示到终端中

0180epoll模拟HTTP服务器

  在当前文件夹下需要有两个文件,一个是error.html,另一个是home.html,其相关代码参考 网络编程 · 代码笔记2 的 “0120HTTP服务器”内容。

在这里插入图片描述

  相关代码:

#include <stdio.h>      // 引入标准输入输出库
#include <string.h>     // 引入字符串操作库
#include <stdlib.h>     // 引入标准库,提供多种函数
#include <unistd.h>     // 引入unistd库,提供对POSIX操作系统的API
#include <arpa/inet.h>  // 引入arpa/inet.h库,提供网络编程相关的函数和结构体
#include <sys/socket.h> // 引入sys/socket.h库,提供socket相关的函数
#include <sys/epoll.h>  // 引入sys/epoll.h库,提供epoll相关的函数和结构体
#include <pthread.h>    // 引入pthread.h库,提供多线程编程相关的函数和结构体
#include <stdbool.h>    // 引入stdbool.h库,提供布尔类型的定义
#include <fcntl.h>      // 引入文件控制头文件,提供文件控制相关的函数
#include <errno.h>      // 引入errno.h库,提供错误号相关的宏定义

#define BUFFLEN 1024          // 定义缓冲区的大小为1024字节
#define SERVER_PORT 1234      // 定义服务器的端口号为1234
#define HTTP_FILENAME_LEN 256 // 定义HTTP文件名的长度限制为256字节
#define EPOLL_SIZE 1000       // 定义epoll事件表的大小为1000

/**
 * @brief HTTP响应头模板
 *
 * 用于构造HTTP响应头,包含状态行、服务器信息、内容类型等。
 */
char *http_res_hdr_tmpl = "HTTP/1.1 200 OK\nServer: bianchengbang\n"
                          "Accept-Ranges: bytes\nContent-Length: %d\nConnection: closed\n"
                          "Content-Type: %s\n\n";

/**
 * @brief 客户端信息结构体
 *
 * 用于存储每个客户端的epoll文件描述符和socket文件描述符。
 */
typedef struct info
{
    int epoll_fd; // epoll文件描述符
    int fd;       // socket文件描述符
} info;

/**
 * @brief 文档类型结构体
 *
 * 用于存储文件后缀名和对应的MIME类型。
 */
struct doc_type
{
    char *suffix; // 文件后缀名
    char *type;   // MIME类型
};

/**
 * @brief 文件类型数组
 *
 * 存储了常见的文件后缀名及其对应的MIME类型。
 */
struct doc_type file_type[] = {
    {"html", "text/html"},
    {"ico", "image/x-icon"},
    {"png", "image/png"},
    {"js", "application/x-javascript"},
    {"css", "text/css"},
    {"jpg", "image/jpeg"},
    {"gif", "image/gif"},
    {NULL, NULL}};

/**
 * @brief 错误处理函数
 *
 * @param str 错误信息
 */
void error_die(const char *str)
{
    perror(str); // 打印错误信息
    exit(-1);    // 非正常退出程序
}

/**
 * @brief 将文件描述符添加到epoll的事件表中
 *
 * 这个函数用于将一个文件描述符添加到epoll的事件表中。
 * epoll是Linux内核中的一种I/O多路复用机制,可以高效地处理大量的文件描述符。
 *
 * @param epfd epoll文件描述符
 * @param fd 需要添加的文件描述符
 * @param events 监听的事件类型
 */
void addfd(int epfd, int fd, int events)
{
    struct epoll_event event;                   // 定义一个epoll事件结构体
    event.events = events;                      // 设置要监听的事件类型
    event.data.fd = fd;                         // 设置文件描述符
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); // 将事件添加到epoll的事件表中
}

/**
 * @brief 解析HTTP请求中的命令
 *
 * 这个函数用于解析HTTP请求中的命令,提取文件名和文件后缀。
 *
 * @param buf HTTP请求缓冲区
 * @param file_name 存放文件名的缓冲区
 * @param suffix 存放文件后缀的缓冲区
 */
void http_parse_request_cmd(char *buf, char *file_name, char *suffix)
{
    int file_length = 0, suffix_length = 0; // 定义文件名和后缀的长度变量

    char *begin = strchr(buf, ' '); // 查找第一个空格,HTTP请求方法后通常跟有一个空格
    begin += 1;                     // 移动到空格后的位置,即URL的开始

    char *end = strchr(begin, ' '); // 查找第二个空格,URL后通常跟有一个空格
    *end = 0;                       // 将第二个空格置为字符串结束符,以便获取完整的URL

    file_length = end - begin - 1;             // 计算文件名的长度
    memcpy(file_name, begin + 1, file_length); // 将文件名复制到file_name缓冲区
    file_name[file_length] = 0;                // 确保文件名以null字符结束

    char *bias = strrchr(begin, '/'); // 从URL中查找最后一个'/',用于确定文件后缀的位置
    suffix_length = end - bias;       // 计算文件后缀的长度

    // 如果最后一个'/'后面直接是文件名,没有路径
    if (*bias == '/')
    {
        bias++;          // 移动到文件名的开始
        suffix_length--; // 减去'/'的长度
    }

    // 如果找到了文件后缀
    if (suffix_length > 0)
    {
        begin = strchr(file_name, '.'); // 在文件名中查找'.',以确定后缀的开始
        if (begin)                      // 如果找到了'.'
            strcpy(suffix, begin + 1);  // 复制文件后缀到suffix缓冲区
    }
}

/**
 * @brief 根据文件后缀名获取HTTP内容类型
 *
 * @param suffix 文件后缀名
 * @return char* 对应的HTTP内容类型(MIME类型),如果未找到则返回NULL
 */
char *http_get_type_by_suffix(const char *suffix)
{
    struct doc_type *type = NULL;                // 初始化类型指针为NULL
    for (type = file_type; type->suffix; type++) // 遍历文件类型数组
    {
        if (strcmp(type->suffix, suffix) == 0) // 如果找到匹配的后缀名
            return type->type;                 // 返回对应的MIME类型
    }
    return NULL; // 如果没有找到匹配的后缀名,返回NULL
}

/**
 * @brief 初始化服务器
 *
 * @return int 返回服务器socket的文件描述符,如果初始化失败则返回-1
 */
int serv_init(void)
{
    int serv_sock;                                                     // 服务器socket文件描述符
    if ((serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) // 创建TCP套接字
    {
        error_die("套接字创建失败:"); // 如果创建失败,打印错误信息并退出
    }

    struct sockaddr_in serv_addr;             // 服务器socket地址结构
    memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构为0

    serv_addr.sin_family = AF_INET;                // 设置地址家族为IPv4
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器地址为任意可用的IP地址
    serv_addr.sin_port = htons(SERVER_PORT);       // 设置服务器端口号,并进行网络字节序的转换

    // 设置SO_REUSEADDR来允许端口复用
    int reuse = 1;
    if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&reuse, sizeof(reuse)) < 0)
    {
        perror("设置SO_REUSEADDR 1 失败");
        return -1;
    }

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 绑定地址到socket
    {
        error_die("bind() 执行失败:"); // 如果绑定失败,打印错误信息并关闭socket
        close(serv_sock);              // 关闭socket
    }

    if (listen(serv_sock, SOMAXCONN) == -1) // 监听socket,等待客户端连接
    {
        error_die("listen() 执行失败:"); // 如果监听失败,打印错误信息并关闭socket
        close(serv_sock);                // 关闭socket
    }

    return serv_sock; // 返回服务器socket的文件描述符
}

/**
 * @brief 处理客户端HTTP请求的多线程函数
 *        此函数用于网络编程中的HTTP服务器,负责接收客户端的请求,解析请求内容,
 *        根据请求的资源发送相应的文件或错误页面给客户端,并管理socket连接。
 *        函数首先将线程设置为分离状态,然后在一个无限循环中读取客户端socket的数据,
 *        根据读取结果进行不同的处理,包括错误处理、连接断开处理和HTTP请求处理。
 *        在HTTP请求处理中,函数将解析请求中的文件名和后缀,根据后缀获取MIME类型,
 *        如果文件类型未找到或文件不存在,则发送错误页面给客户端。最后,函数将发送
 *        HTTP响应头和文件内容给客户端,并在发送完毕后关闭socket连接。
 *
 * @param args 指向info结构体的指针,包含epoll描述符和客户端socket描述符
 * @return void* 返回NULL
 */
void *threadFun(void *args)
{
    // 将当前线程设置为分离状态,线程结束后自动释放资源
    pthread_detach(pthread_self());

    // 将传入的参数转换为info结构指针,并获取epoll文件描述符和客户端socket文件描述符
    info *fds = (info *)args;
    int epoll_fd = fds->epoll_fd;
    int clnt_sock = fds->fd;
    // 释放info结构占用的内存
    free(fds);

    // 循环读取客户端socket的数据
    while (1)
    {
        char buff[BUFFLEN] = {0};
        // 清空缓冲区
        memset(buff, 0, BUFFLEN);

        // 从客户端socket读取数据到缓冲区
        int str_len = read(clnt_sock, buff, BUFFLEN);
        if (str_len < 0)
        {
            // 如果读取错误是由于没有数据可读,打印提示信息
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                printf("循环读完所有数据\n");
            }
            // 关闭客户端socket
            close(clnt_sock);
            // 退出循环
            break;
        }

        // 如果读取到的数据长度为0,表示客户端已断开连接
        else if (str_len == 0)
        {
            // 从epoll实例中移除客户端socket,关闭客户端socket
            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clnt_sock, NULL);
            close(clnt_sock);
            printf("和客户端 %d 断开连接\n", clnt_sock);
        }

        else
        {
            // 定义文件名和后缀名的字符数组,并初始化为0
            char file_name[HTTP_FILENAME_LEN] = {0}, suffix[16] = {0};
            // 解析HTTP请求中的命令,获取文件名和后缀名
            http_parse_request_cmd(buff, file_name, suffix);

            // 定义文件指针,初始化为NULL
            FILE *fp = NULL;
            // 定义标志变量,用于指示文件是否存在(1存在,0不存在)
            int fp_has = 1;
            // 定义标志变量,用于指示文件类型是否正确(1正确,0不正确)
            int fp_type = 1;

            // 根据文件后缀名获取对应的MIME类型
            char *mime_type = http_get_type_by_suffix(suffix);
            // 如果MIME类型为NULL,表示文件类型不匹配
            if (mime_type == NULL)
            {
                // 设置文件类型错误标志
                fp_type = 0;
                // 打印文件类型不匹配的信息
                printf("访问的文件类型(后缀名)不匹配\n");
                // 获取默认的HTML文件的MIME类型
                mime_type = http_get_type_by_suffix("html");
                // 打开默认的错误页面文件
                fp = fopen("error.html", "rb");
            }

            // 如果MIME类型不为NULL,尝试打开请求的文件
            else
            {
                // 尝试打开请求的文件
                fp = fopen(file_name, "rb");
                // 如果文件指针为NULL,表示文件不存在
                if (fp == NULL)
                {
                    // 设置文件不存在标志
                    fp_has = 0;
                    // 打开默认的错误页面文件
                    fp = fopen("error.html", "rb");
                }
            }

            // 将文件指针移动到文件末尾,用于获取文件长度
            fseek(fp, 0, SEEK_END);
            // 获取文件的长度
            int file_len = ftell(fp);
            // 将文件指针移动到文件开头,准备读取文件内容
            fseek(fp, 0, SEEK_SET);

            // 定义用于存储HTTP响应头的字符数组,并初始化为0
            char http_header[BUFFLEN] = {0};
            // sprintf函数将文件长度和MIME类型格式化到http_header中,并返回填充的字符数,即响应头的长度
            int response_header_len = sprintf(http_header, http_res_hdr_tmpl, file_len, mime_type);

            // 将http_header中的响应头发送到客户端,发送的长度为response_header_len
            write(clnt_sock, http_header, response_header_len);

            // 检查文件类型是否正确
            if (fp_type == 1)
            {
                // 检查文件是否存在
                if (fp_has == 0)
                {
                    // 如果文件不存在,打印信息并准备发送errno.html文件
                    printf("----服务器不存在 %s 文件, error.html 文件发送中...\n", file_name);
                    // 将文件名替换为errno.html
                    strcpy(file_name, "error.html");
                }
                else
                {
                    // 如果文件存在,打印信息并准备发送文件
                    printf("----服务器存在 %s 文件,发送中...\n", file_name);
                }
            }

            // 清空缓冲区
            memset(buff, 0, BUFFLEN);
            // 定义用于存储每次读取字节数的变量
            int nCount = 0;
            // 循环读取文件内容到缓冲区,并写入到客户端socket
            while ((nCount = fread(buff, 1, BUFFLEN, fp)) > 0)
            {
                // 将缓冲区中的内容写入到客户端socket
                write(clnt_sock, buff, nCount);
                // 清空缓冲区,准备下一次读取
                memset(buff, 0, BUFFLEN);
            }

            // 打印文件发送完毕的信息
            printf("----文件 %s 发送完毕\n", file_name);
            // 关闭文件指针
            fclose(fp);
            // 关闭客户端socket的写方向,表示不再发送数据
            shutdown(clnt_sock, SHUT_WR);
            // 读取客户端可能发送的任何数据(通常是连接关闭的通知)
            read(clnt_sock, buff, sizeof(buff));
            // 从epoll实例中移除客户端socket
            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clnt_sock, NULL);
            // 关闭客户端socket
            close(clnt_sock);
            // 打印主动断开连接的信息
            printf("主动和客户端 %d 断开连接\n", clnt_sock);
        }
    }
    return NULL;
}

/**
 * @brief 处理客户端连接请求的主循环函数
 *        此函数用于创建epoll实例,监听服务端socket上的活动,接受新的客户端连接,
 *        并为每个新的客户端连接创建一个线程来处理HTTP请求。
 *
 * @param serv_sock 服务端socket的文件描述符
 */
void handle_connect(int serv_sock)
{
    // 创建一个epoll实例,并返回epoll文件描述符
    int epoll_fd = epoll_create(1);
    // 将服务端socket添加到epoll实例中,监听EPOLLIN事件(可读事件)
    addfd(epoll_fd, serv_sock, EPOLLIN);
    // 分配一个epoll事件结构体数组,大小为EPOLL_SIZE,用于存储epoll_wait返回的事件
    struct epoll_event *epoll_events_p = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

    while (1)
    {
        // 等待epoll事件发生,超时时间为10000毫秒(10秒)
        // 存储事件的数组为epoll_events_p,数组大小为EPOLL_SIZE
        // 返回值event_cnt表示发生事件的数量
        int event_cnt = epoll_wait(epoll_fd, epoll_events_p, EPOLL_SIZE, 10000);

        // 如果epoll_wait返回-1,表示发生错误,打印错误信息并退出循环
        if (event_cnt == -1)
        {
            perror("epoll_wait() 运行失败:");
            break;
        }

        // 如果event_cnt为0,表示没有事件发生,打印监听中的信息并继续下一次循环
        if (event_cnt == 0)
        {
            continue;
        }

        // 遍历epoll_wait返回的事件数组
        for (int i = 0; i < event_cnt; i++)
        {
            // 如果事件发生在服务端socket上,表示有新的客户端连接请求
            if (epoll_events_p[i].data.fd == serv_sock)
            {
                struct sockaddr_in clnt_addr;
                // 定义客户端地址结构大小
                socklen_t clnt_addr_size = sizeof(clnt_addr);
                // 接受客户端连接,创建客户端socket,并获取客户端地址信息
                int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);

                // 设置客户端socket为非阻塞模式
                // 在非阻塞模式下,read() 和 write() 系统调用会立即返回,
                // 即使没有数据可读或缓冲区没有空间可写。
                fcntl(clnt_sock, F_SETFL, O_NONBLOCK);

                // 将客户端socket添加到epoll实例中,并设置监听EPOLLIN | EPOLLET | EPOLLONESHOT事件
                // EPOLLIN:监听socket的可读事件,即当socket上有数据可读时触发通知
                // EPOLLET:使用边缘触发(ET)模式,epoll仅在文件描述符状态改变时触发一次通知
                // EPOLLONESHOT:一次触发后关闭事件,防止多线程同时处理同一个socket的事件,避免竞态条件
                addfd(epoll_fd, clnt_sock, EPOLLIN | EPOLLET | EPOLLONESHOT);

                // 打印客户端连接成功的信息
                printf("客户端%d连接成功\n", clnt_sock);
            }

            // 如果事件发生在其他已连接的客户端socket上,为这个客户端创建线程处理HTTP请求
            else
            {
                // 分配info结构体内存,用于存储epoll文件描述符和客户端socket文件描述符
                info *fds = (info *)malloc(sizeof(info));
                fds->epoll_fd = epoll_fd;
                fds->fd = epoll_events_p[i].data.fd;
                pthread_t threadID;

                // 创建线程,执行threadFun函数处理客户端请求
                int ret = pthread_create(&threadID, NULL, threadFun, (void *)fds);
                // 如果线程创建失败,打印错误信息并退出程序
                if (ret != 0)
                {
                    printf("线程创建失败\n");
                    exit(0);
                }
            }
        }
    }

    free(epoll_events_p);
}

int main()
{
    int serv_sock = serv_init();

    handle_connect(serv_sock);
    close(serv_sock);
    return 0;
}

  运行结果:

  在浏览器中输入以下网址:

http://127.0.0.1:1234/home.html

  显示界面:
在这里插入图片描述
  服务器输出:
在这里插入图片描述

0191基于UDP的多播_接收端

  相关代码:

#include <stdio.h>      // 引入标准输入输出头文件
#include <stdlib.h>     // 引入标准库定义头文件
#include <string.h>     // 引入字符串操作头文件
#include <unistd.h>     // 引入Unix标准函数头文件
#include <sys/types.h>  // 引入系统数据类型头文件
#include <sys/socket.h> // 引入套接字API头文件
#include <arpa/inet.h>  // 引入网络地址操作头文件

#define MAX_BUF_SIZE 1024           // 定义最大缓冲区大小
#define PORT 1234                   // 定义端口号
#define MULTICAST_GROUP "224.1.1.2" // 定义多播组地址

int main(int argc, char const *argv[])
{
    // 创建UDP socket
    int sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 使用IPv4协议,UDP传输协议创建套接字
    if (sock_fd < 0)
    {
        perror("socket"); // 输出创建socket失败的原因
        exit(1);          // 非正常退出程序
    }

    // 设置socket端口复用
    int reuse = 1;
    if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse)) < 0)
    {
        perror("setsockopt SO_REUSEADDR"); // 输出设置socket选项失败的原因
        exit(1);                           // 非正常退出程序
    }

    // 绑定socket到本地地址和端口
    struct sockaddr_in local_addr;              // 定义IPv4的socket地址结构
    memset(&local_addr, 0, sizeof(local_addr)); // 初始化地址结构为0

    local_addr.sin_family = AF_INET;                // 设置地址家族为IPv4
    local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置地址为任意可用的接口地址
    local_addr.sin_port = htons(PORT);              // 设置端口为之前定义的端口号

    if (bind(sock_fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0)
    {
        perror("bind"); // 输出绑定socket失败的原因
        exit(1);        // 非正常退出程序
    }

    // 加入多播组
    struct ip_mreq mreq;            // 定义多播组结构
    memset(&mreq, 0, sizeof(mreq)); // 初始化多播组结构为0

    mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP); // 设置多播组地址
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);          // 设置加入多播组的接口为任意接口
    if (setsockopt(sock_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
    {
        perror("setsockopt IP_ADD_MEMBERSHIP"); // 输出设置多播组失败的原因
        exit(1);                                // 非正常退出程序
    }

    // 接收数据
    char buf[MAX_BUF_SIZE];                                         // 定义接收数据缓冲区
    int recv_len = recvfrom(sock_fd, buf, sizeof(buf), 0, NULL, 0); // 从socket接收数据
    if (recv_len < 0)
    {
        perror("recvfrom"); // 输出接收数据失败的原因
        exit(1);            // 非正常退出程序
    }

    buf[recv_len] = '\0'; // 确保接收到的数据以null字符结尾
    printf("%s\n", buf);  // 打印接收到的数据

    // 关闭socket
    close(sock_fd); // 关闭socket文件描述符
    return 0;       // 正常退出程序
}

0192基于UDP的多播_发送端

  相关代码:

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

#define MULTICAST_GROUP "224.1.1.2"
#define PORT 1234
#define TTL 64
#define BUF_SIZE 1024

int main()
{
    struct sockaddr_in addr;
    char buf[BUF_SIZE] = "这是来源于发送端的新消息。";

    // 创建UDP套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        perror("socket");
        exit(1);
    }

    // 设置多播TTL
    int ttl = TTL;
    if ((setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl))) < 0)
    {
        perror("setsockopt");
        exit(1);
    }

    // 设置目标地址和端口
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(MULTICAST_GROUP);
    addr.sin_port = htons(PORT);

    // 发送数据
    if ((sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&addr, sizeof(addr))) < 0)
    {
        perror("sendto");
        exit(1);
    }

    // 关闭套接字
    close(sock);

    return 0;
}

  运行结果:
  打开一个发送端和多个接收端,当发送端发送信息的时候,多个接收端将会接收到相同的信息
在这里插入图片描述

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值