网络编程 · 代码笔记2

前言

1、编程环境

  编码所用IDE:VScode 1.87.0
  编译工具:gcc version 11.4.0

在这里插入图片描述

  运行环境:
    1、Windows Subsystem for Linux (WSL) 2
    2、Ubuntu 22.04.4 LTS

在这里插入图片描述

2、编译命令

  如无特别说明,通用编译命令为:

gcc -o server -g -Wall -std=gnu99 filename.c
或
gcc -o client -g -Wall -std=gnu99 filename.c

3、标题名前缀解释

  XXX1或XXX2中,XXX代表一个主题,凡是具有相同的XXX,都是同一主题。
  1代表服务端内容(代码),2代表客户端内容(代码)。

4、注意事项

  所有代码都需要先运行服务端,而后再运行客户端,或者先运行接收端,再运行发送端。


0111UDP的回声接收端

  注意!此部分内容需要先启动接收端,而后再启动发送端。

  编译命令:

gcc -o receive -g -Wall -std=gnu99 0111UDP的回声接收端.c

  相关代码:

#include <stdio.h>      // 引入标准输入输出库
#include <string.h>     // 引入字符串操作库
#include <stdlib.h>     // 引入标准库,提供多种函数
#include <unistd.h>     // 引入Unix标准函数库
#include <arpa/inet.h>  // 引入IP地址转换函数库
#include <sys/socket.h> // 引入套接字API
#include <netinet/in.h> // 引入网络结构体定义

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

int main(int argc, char const *argv[]) // 程序入口,argc为参数数量,argv为参数数组
{
    int serv_sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字

    struct sockaddr_in serv_addr;             // 定义服务器地址结构体
    memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体为0
    serv_addr.sin_family = AF_INET;           // 设置地址家族为IPv4
    serv_addr.sin_port = htons(1234);         // 设置端口号为1234,htons用于网络字节序转换
    // 利用 INADDR_ANY 自动获取 IP 地址,htonl 函数进行转换
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);                     // 设置IP地址为本地任意地址
    bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // 绑定套接字到本地地址

    struct sockaddr_in clnt_addr;                 // 定义客户端地址结构体
    socklen_t clnt_addr_size = sizeof(clnt_addr); // 定义客户端地址结构体大小变量
    memset(&clnt_addr, 0, sizeof(clnt_addr));     // 初始化客户端地址结构体为0

    char buffer[BUF_SIZE]; // 定义缓冲区数组
    while (1)              // 无限循环,等待客户端数据
    {
        memset(buffer, 0, BUF_SIZE); // 清空缓冲区
        int receive_data_size = recvfrom(serv_sock, buffer, BUF_SIZE, 0, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
        // 从客户端接收数据到缓冲区,并获取客户端地址信息
        printf("发送端传来的数据:%s\n", buffer); // 打印接收到的数据
        sendto(serv_sock, buffer, receive_data_size, 0, (struct sockaddr *)&clnt_addr, clnt_addr_size);
        // 将接收到的数据发送回客户端

        if (strcmp(buffer, "exit") == 0) // 如果接收到的数据为"exit"
        {
            break; // 退出循环
        }
    }

    close(serv_sock);       // 关闭套接字
    printf("关闭连接。\n"); // 打印关闭连接信息

    return 0; // 程序结束
}

0112UDP的回声发送端

  编译命令:

gcc -o send -Wall -g -std=gnu99 0112UDP的回声发送端.c

  相关代码:

#include <stdio.h>      // 引入标准输入输出库
#include <string.h>     // 引入字符串操作库
#include <stdlib.h>     // 引入标准库,提供多种函数
#include <unistd.h>     // 引入Unix标准函数库
#include <arpa/inet.h>  // 引入IP地址转换函数库
#include <sys/socket.h> // 引入套接字API
#include <netinet/in.h> // 引入Internet地址族

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

int main(int argc, char const *argv[]) // 主函数,argc为参数数量,argv为参数数组
{
    int clnt_sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建一个UDP套接字

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

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

    struct sockaddr_in from_addr;                 // 定义一个来源地址结构
    socklen_t from_addr_size = sizeof(from_addr); // 定义来源地址结构大小
    memset(&from_addr, 0, sizeof(from_addr));     // 初始化来源地址结构为0

    char buffer[BUF_SIZE]; // 定义一个字符数组作为缓冲区
    while (1)              // 无限循环
    {
        memset(buffer, 0, BUF_SIZE);              // 清空缓冲区
        printf("请输入需要发送给接收端的内容:"); // 提示用户输入
        scanf("%s", buffer);                      // 从标准输入读取字符串到缓冲区

        sendto(clnt_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));                   // 向服务器发送数据
        memset(buffer, 0, BUF_SIZE);                                                                                      // 清空缓冲区
        int receive_data_size = recvfrom(clnt_sock, buffer, BUF_SIZE, 0, (struct sockaddr *)&from_addr, &from_addr_size); // 从服务器接收数据
        buffer[receive_data_size] = '\0';                                                                                 // 在接收到的数据末尾添加字符串结束符
        printf("接收到接收端的数据为:%s\n", buffer);                                                                     // 打印接收到的数据

        if (strcmp(buffer, "exit") == 0) // 如果接收到的数据是"exit"
        {
            break; // 退出循环
        }
    }

    close(clnt_sock);       // 关闭套接字
    printf("关闭连接。\n"); // 打印关闭连接的信息

    return 0; // 程序结束
}

  运行结果:

  切记!需要先启动接收端,而后再启动发送端
在这里插入图片描述

0120HTTP服务器

  在编写HTTP服务器的代码之前,首先需要准备2个文件 :1个是home.html,这是客户端浏览器所需要访问的文件,另一个是error.html,这是客户端请求不到文件时,服务器所返回的错误页面文件,这两个HTML文件代码如下:

  home.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #f8f9fa;
        }

        .home-container {
            text-align: center;
        }

        .home-title {
            font-size: 2rem;
            font-weight: bold;
            color: #007bff;
        }

        .home-description {
            font-size: 1.25rem;
            margin-top: 1rem;
        }
    </style>
</head>

<body>
    <div class="home-container">
        <div class="home-title">欢迎来到我的网站</div>
        <div class="home-description">这是一个简单的首页示例。</div>
    </div>
</body>

</html>

  error.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>错误页面</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #f8f9fa;
        }

        .error-container {
            text-align: center;
        }

        .error-code {
            font-size: 5rem;
            font-weight: bold;
            color: #dc3545;
        }

        .error-message {
            font-size: 1.5rem;
            margin-top: 1rem;
        }

        .back-home {
            display: inline-block;
            margin-top: 2rem;
            padding: 0.5rem 1rem;
            background-color: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 0.25rem;
        }
    </style>
</head>

<body>
    <div class="error-container">
        <div class="error-code">404</div>
        <div class="error-message">很抱歉,您访问的页面不存在。</div>
        <a href="./home.html" class="back-home">返回首页</a>
    </div>
</body>

</html>

  这两个文件保存在与HTTP服务器程序相同的当前文件夹下即可,如下图所示。
在这里插入图片描述

  http服务器的代码:

#include <stdio.h>      // 包含标准输入输出库
#include <string.h>     // 包含字符串操作库
#include <stdlib.h>     // 包含标准库,如system调用
#include <unistd.h>     // 包含Unix标准函数,如read、write等
#include <arpa/inet.h>  // 包含IP地址转换函数
#include <sys/socket.h> // 包含套接字相关的函数
#include <netinet/in.h> // 包含网络结构体定义
#include <pthread.h>    // 包含线程相关的函数

#define BUFFLEN 1024          // 定义缓冲区大小
#define SERVER_PORT 1234      // 定义服务器端口号
#define HTTP_FILENAME_LEN 256 // 定义 HTTP 文件名长度
#define SUFFIX_BUFF_SIZE 16   // 存放后缀名缓冲区的大小

// 定义文件类型结构体,用于映射文件后缀到 MIME 类型
struct doc_type
{
    char *suffix; // 文件后缀
    char *type;   // MME类型
};

// 返回内容对应 MIME 类型
struct doc_type file_type[] =
    {
        {"html", "text/html"},
        {"ico", "image/x-icon"},
        {NULL, NULL}};

// HTTP 响应头模板
char *http_res_hdr_tmpl = "HTTP/1.1 200 OK\nServer: orange\n"
                          "Accept-Ranges: bytes\nContent-Length: %d\nConnection: closed\n"
                          "Content-Type: %s\n\n";

/**
 * @brief 解析HTTP请求中的命令,提取文件名和文件后缀名
 *
 * 这个函数从给定的HTTP请求缓冲区中解析出请求的文件名和文件后缀名。
 * 它假设请求行的格式是正确的,并且包含一个完整的HTTP请求行。
 *
 * @param buf 包含HTTP请求行的缓冲区
 * @param file_name 用于存储提取出的文件名的缓冲区
 * @param suffix 用于存储提取出的文件后缀名的缓冲区
 *
 * @note 这个函数没有进行错误检查,因此在格式错误的请求行上调用它可能会导致未定义的行为。
 */
void http_parse_request_cmd(char *buf, char *file_name, char *suffix)
{
    // 查找 URL 开始的位置,空格后面紧跟URL,所以需要+1
    char *begin = strchr(buf, ' ');
    begin += 1;

    // 查找 URL 结束的位置,并令URL结束后的一个空格置0
    char *end = strchr(begin, ' ');
    *end = 0;

    /* 获取请求文件的文件路径 */
    // 文件路径长度
    int file_length = end - begin - 1;
    // 将文件路径复制到 file_name 中, begin + 1是真实的文件路径起始位置
    memcpy(file_name, begin + 1, file_length);
    // 文件路径末尾添加结束符
    file_name[file_length] = 0;

    /* 获得文件后缀名 */
    char *bias = strrchr(begin, '/');
    // 计算最新文件路径的长度
    int suffix_length = end - bias;
    // 提取出带文件后缀名的文件名
    if (*bias == '/')
    {
        ++bias;
        --suffix_length;
    }

    // 找到带文件后缀名的文件名且长度大于0的情况下
    if (suffix_length > 0)
    {
        // 找到文件后缀名的位置
        begin = strchr(file_name, '.');
        // 如果找到了,则将'.'之后的字符串复制到保存文件后缀名的缓冲区中
        if (begin)
        {
            strcpy(suffix, begin + 1);
        }
    }
}

/**
 * @brief 根据文件后缀名获取对应的HTTP内容类型(MIME类型)
 *
 * 这个函数通过比较输入的文件后缀名与预定义的文件类型数组,
 * 来确定相应的HTTP内容类型(MIME类型)。如果找到匹配的文件后缀名,
 * 则返回对应的MIME类型;如果没有找到,则返回NULL。
 *
 * @param suffix 输入的文件后缀名
 *
 * @return 如果找到匹配的MIME类型,则返回该类型的字符串指针;否则返回NULL。
 */
char *http_get_type_by_suffix(const char *suffix)
{
    // 遍历文件类型数组,查找与输入后缀名匹配的MIME类型
    for (struct doc_type *type = file_type; type->suffix != NULL; type++)
    {
        // 如果找到匹配的文件后缀名,返回对应的MIME类型
        if (strcmp(type->suffix, suffix) == 0)
        {
            return type->suffix;
        }
    }

    // 如果没找到匹配的文档类型则返回NULL;
    return NULL;
}

/**
 * @brief 线程函数,用于处理客户端的HTTP请求并响应
 *
 * 这个函数是一个线程的执行体,它被设计用来处理客户端的HTTP请求。
 * 函数首先分离当前线程,使得线程结束后资源能够被系统自动回收。
 * 然后,它从线程参数中获取客户端的套接字,并读取客户端发送的HTTP请求内容。
 * 接着,函数解析HTTP请求以获取请求的文件路径和文件后缀名,并根据文件后缀名确定MIME类型。
 * 函数会尝试打开请求的文件,如果文件不存在或者无法打开,则会返回一个错误页面。
 * 然后,函数构造HTTP响应头部并发送给客户端,随后发送文件数据。
 * 在所有数据发送完毕后,函数会关闭文件和套接字,并读取任何客户端发来的剩余数据。
 *
 * @param args 一个指向整数(客户端套接字)的指针
 *
 * @return 这个函数不返回任何值
 */
void *thread_func(void *args)
{
    // 线程分离,给系统托管子线程,子线程结束后由系统回收占用资源
    pthread_detach(pthread_self());

    int clnt_sock = *(int *)args; // 从参数中获取客户端的套接字
    char buff[BUFFLEN] = {0};     // 定义缓冲区用于读取和发送数据

    // 读取客户端发送过来的HTTP请求内容
    int num = read(clnt_sock, buff, sizeof(buff));
    if (num > 0)
    {
        // 存储文件路径和文件后缀名的缓冲区
        char file_name[HTTP_FILENAME_LEN] = {0}, suffix[SUFFIX_BUFF_SIZE] = {0};
        // 解析HTTP请求,获取请求的文件路径和后缀
        http_parse_request_cmd(buff, file_name, suffix);
        // 根据文件后缀名获取对应的MIME类型
        char *type = http_get_type_by_suffix(suffix);

        int fp_type = 1; // 根据是否获取到MIME类型设置对应的标志位
        int fp_has = 1;  // 文件是否存在标志
        FILE *fp;        // 打开对应文件的文件指针

        if (type == NULL)
        {
            fp_type = 0; // 文件不存在则置0
            printf("访问的文件不存在!\n");
            type = http_get_type_by_suffix("html");
            fp = fopen("./error.html", "rb"); // 打开错误页面
        }
        else
        {
            fp = fopen(file_name, "rb"); // 尝试打开请求的文件
            if (fp == NULL)
            {
                fp_has = 0;                       // 打开失败则置0
                fp = fopen("./error.html", "rb"); // 打开错误页面
            }
        }

        // 文件指针移动到文件末尾并返回文件所在位置可知文件有多大
        fseek(fp, 0, SEEK_END);
        int file_len = ftell(fp);
        fseek(fp, 0, SEEK_SET);

        // 存储HTTP响应头部
        char http_header[BUFFLEN] = {0};
        // 构造HTTP响应头部并计算HTTP响应头部长度
        int hdr_len = sprintf(http_header, http_res_hdr_tmpl, file_len, type);
        // 向客户端发送HTTP的响应头部
        write(clnt_sock, http_header, hdr_len);

        // 输出当前服务器的处理状态
        if (fp_type == 1)
        {
            if (fp_has == 0)
            {
                printf("服务器不存在 %s 文件\n", file_name);
            }
            else
            {
                printf("%s 文件发送中……", file_name);
            }
        }

        int nCount = 0; // 存储每次读取的字节数
        memset(buff, 0, BUFFLEN);
        // 向客户端发送文件数据
        while ((nCount = fread(buff, 1, BUFFLEN, fp)) > 0)
        {
            // 发送数据,发送完毕后清空缓冲区
            write(clnt_sock, buff, nCount);
            memset(buff, 0, BUFFLEN);
        }

        fclose(fp);                          // 关闭文件
        shutdown(clnt_sock, SHUT_RDWR);      // 关闭连接
        read(clnt_sock, buff, sizeof(buff)); // 读取一些客户端发来的剩余数据,比如TCP四次挥手最后一次的ACK包
        close(clnt_sock);                    // 关闭套接字
    }

    return NULL;
}

/**
 * @brief 处理服务器套接字的连接请求
 *
 * 这个函数用于监听服务器套接字,接受客户端的连接请求,并为每个新的连接创建一个线程来处理。
 * 函数无限循环,等待并接受新的连接。当接受到一个连接后,它创建一个新的线程来处理该连接,
 * 并将客户端的套接字传递给线程函数。如果线程创建失败,函数将输出错误信息并终止进程。
 *
 * @param serv_sock 服务器套接字
 */
void handle_connect(int serv_sock)
{
    while (1)
    {

        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);
        // 检查是否成功接收客户端请求
        if (clnt_sock > 0)
        {
            pthread_t threadID;

            // 创建一个新的线程来处理客户端请求,并检查线程是否创建成功
            if (pthread_create(&threadID, NULL, thread_func, &clnt_sock) != 0)
            {
                perror("创建线程失败!");
                exit(-1); // 终止进程,并释放所有资源
            }
        }
    }
}

int main(int argc, char const *argv[])
{
    int serv_sock;
    if ((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("套接字创建失败!");
        return -1;
    }

    // 设置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;
    }

    struct sockaddr_in serv_addr;             // 服务器地址结构体
    memset(&serv_addr, 0, sizeof(serv_addr)); // 清空内容

    // 初始化服务器地址结构体
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    socklen_t serv_addr_size = sizeof(serv_addr);
    // 将套接字绑定到指定的 IP 地址和端口号
    if (bind(serv_sock, (struct sockaddr *)&serv_addr, serv_addr_size) == -1)
    {
        close(serv_sock);
        perror("bind() 执行失败!");
        return -1;
    }

    // 将套接字设置为监听模式,等待客户端连接
    if (listen(serv_sock, SOMAXCONN) == -1)
    {
        close(serv_sock);
        perror("listen() 执行失败!");
        return -1;
    }

    // 处理客户端连接
    handle_connect(serv_sock);

    // 程序结束前关闭套接字,释放资源
    close(serv_sock);

    return 0;
}

  编译运行程序后,启动浏览器,在浏览器中输入网址:

http://127.0.0.1:1234/home.html

  访问home.html成功则会看见这样的页面:
在这里插入图片描述

  如果访问一个不存在的文件,比如将访问网址写成这样:

http://127.0.0.1:1234/test.txt

  则服务器会给客户端浏览器传送error.html网页,客户端浏览器将会看到这样子的界面:
在这里插入图片描述

  顺带附上服务器终端输出内容的截图:
在这里插入图片描述

0130select实现多路复用

  测试方法: 程序运行之后,直接输入内容再回车即可,程序会重新将用户输入的内容输出一遍。

  相关代码:

#include <stdio.h>      // 引入标准输入输出库
#include <unistd.h>     // 引入unistd库,提供对POSIX操作系统API的访问
#include <sys/time.h>   // 引入时间相关的系统调用
#include <sys/select.h> // 引入select()函数的头文件

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

int main(int argc, char const *argv[])
{
    fd_set reads, temps; // 定义两个文件描述符集合
    FD_ZERO(&reads);     // 清空文件描述符集合reads
    FD_SET(0, &reads);   // 将标准输入(文件描述符为0)添加到文件描述符集合reads中

    while (1)
    {
        temps = reads;          // 每次循环都进行重置,将文件描述符集合reads复制到temps中
        struct timeval timeout; // 定义timeval结构体用于select函数的超时设置
        timeout.tv_sec = 1;     // 设置超时时间为1秒
        timeout.tv_usec = 0;    // 设置超时时间为0微秒

        int result = select(1, &temps, 0, 0, &timeout); // 调用select函数,检测文件描述符集合temps是否准备好读操作
        if (result == -1)                               // 如果select函数执行出错
        {
            perror("select() 执行错误!");
            return 0;
        }
        else if (result == 0) // 如果select函数超时
        {
            continue;
        }
        else // 如果select函数检测到文件描述符准备好读操作
        {
            if (FD_ISSET(0, &temps)) // 如果标准输入(文件描述符为0)在temps文件描述符集合中
            {
                char buf[BUF_SIZE] = {0};             // 定义一个大小为BUF_SIZE的字符数组,并初始化为0
                int str_len = read(0, buf, BUF_SIZE); // 从标准输入读取数据到buf数组中,最多读取BUF_SIZE个字节
                buf[str_len] = 0;                     // 在读取到的字符串末尾添加0,表示字符串结束
                printf("%s\n", buf);                  // 输出读取到的字符串
            }
        }
    }
    return 0;
}

  运行结果:

在这里插入图片描述

0141select多路复用技术实现回声客户端_服务端

  相关代码:

#include <stdio.h>      // 标准输入输出库
#include <stdlib.h>     // 提供了多种通用工具函数
#include <string.h>     // 字符串操作函数库
#include <sys/time.h>   // 提供了时间相关的函数和数据结构
#include <sys/socket.h> // 提供套接字相关的函数,如socket()、bind()
#include <netinet/in.h> // 定义了 sockaddr_in 结构体,用于IPv4套接字地址
#include <sys/select.h> // 提供了select()函数,用于IO多路复用
#include <unistd.h>     // 提供了基本的系统调用函数
#include <arpa/inet.h>  // 提供了IP地址转换函数,如inet_addr()、htons()

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

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

    struct sockaddr_in serv_addr;             // 定义一个IPv4套接字地址结构
    memset(&serv_addr, 0, sizeof(serv_addr)); // 将结构体清零

    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)
    {
        printf("bind() 函数执行出错,服务器绑定信息失败!\n");
        close(serv_sock);
        return 0;
    }

    // 开始监听,设置最大连接数为20
    if (listen(serv_sock, 20) == -1)
    {
        printf("listen 函数执行出错,服务器侦听连接失败!\n");
        close(serv_sock);
        return 0;
    }

    int max_fd = serv_sock; // 设置最大文件描述符为服务器套接字

    fd_set reads, back_reads;  // 创建一个文件描述符集合
    FD_ZERO(&reads);           // 清空文件描述符集合
    FD_SET(serv_sock, &reads); // 将服务器套接字添加到集合中

    int break_flag = 1;     // 退出死循环标志
    while (break_flag == 1) // 循环处理客户端连接和消息
    {

        back_reads = reads; // 复制文件描述符集合,用于select函数

        struct timeval timeout; // 设置select函数的超时时间
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        int fdNum = select(max_fd + 1, &back_reads, 0, 0, &timeout); // 使用select等待IO事件
        if (fdNum == -1)
        {
            printf("%d\n", __LINE__);
            printf("select 函数执行错误!\n");
            break;
        }
        else if (fdNum == 0) // 如果select超时,继续下一次循环
        {
            continue;
        }
        else
        {
            // 遍历文件描述符集合
            for (int i = 0; i < max_fd + 1; i++)
            {
                if (FD_ISSET(i, &back_reads)) // 判断是否有文件描述符在活跃
                {
                    if (i == serv_sock) // 如果是服务器套接字,则处理新的连接
                    {
                        struct sockaddr_in clnt_addr;
                        socklen_t clnt_addr_size = sizeof(clnt_addr);
                        int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); // 接受新的连接

                        FD_SET(clnt_sock, &reads);                        // 将新的客户端套接字添加到集合中
                        max_fd = max_fd > clnt_sock ? max_fd : clnt_sock; // 更新最大文件描述符
                        printf("客户端连接成功,socket为:%d\n", clnt_sock);
                    }
                    else // 如果是客户端套接字,则处理客户端消息
                    {
                        char buf[BUF_SIZE] = {0};
                        int str_len = read(i, buf, BUF_SIZE - 1); // 从客户端读取消息
                        printf("客户端发来的数据:%s", buf);

                        // 如果客户端关闭连接
                        if (str_len == 0)
                        {
                            FD_CLR(i, &reads); // 从集合中移除客户端套接字
                            close(i);          // 关闭客户端套接字
                            printf("客户端已关闭链接。\n");
                        }
                        else
                        {
                            write(i, buf, str_len); // 将收到的消息写回客户端
                            if (strcmp(buf, "exit\n") == 0)
                            {
                                printf("检测到退出指令,正在退出中……\n");
                                break_flag = 0;
                            }
                        }
                    }
                }
            }
        }
    }

    close(serv_sock); // 关闭服务器的socket
    printf("服务端退出程序完毕。\n");
    return 0;
}

0142select多路复用技术实现回声客户端_客户端

  相关代码:

#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; // 程序结束
}

  运行结果:

在这里插入图片描述

0151poll实现IO多路复用_服务端

  相关代码:

#include <stdio.h>
#include <stdlib.h>     // exit()
#include <string.h>     // memset()
#include <sys/socket.h> // socket()、bind()
#include <netinet/in.h> // sockaddr_in
#include <arpa/inet.h>  // inet_addr()、htons()
#include <unistd.h>     // close()、read()、write()
#include <poll.h>       // poll()

#define MAX 100       // 定义pollfd数组的最大长度为100
#define BUF_SIZE 1024 // 定义缓冲区大小为1024字节

int pfd_count = 0; // 记录当前pollfd数组中已使用的元素数量

/**
 * @brief  初始化pollfd数组中所有元素。
 * @param  pfd[] 要初始化的pollfd结构体数组。
 * @note   该函数将所有文件描述符设置为-1,表示没有设置文件描述符,并清除每个元素的事件和返回事件。
 */
void pfd_init(struct pollfd pfd[MAX])
{
    for (int i = 0; i < MAX; i++)
    {
        pfd[i].fd = -1;     // 将文件描述符设置为-1(表示没有设置文件描述符)。
        pfd[i].events = 0;  // 清除事件。
        pfd[i].revents = 0; // 清除返回事件。
    }
}

/**
 * @brief  向pollfd数组中添加一个带有指定事件的文件描述符。
 * @param  pfd[] pollfd结构体数组。
 * @param  fd    要添加到数组的文件描述符。
 * @param  events 要监视的事件的按位或。
 * @note   如果数组已满,该函数将打印错误消息并退出程序。
 */
void pfd_add(struct pollfd pfd[MAX], int fd, short events)
{
    if (pfd_count == MAX)
    {
        printf("pollfd数组已满,pfd_add 函数执行错误!\n"); // 数组满时的错误消息。
        exit(-1);                                          // 以失败状态退出程序。
    }
    pfd[pfd_count].fd = fd;         // 分配文件描述符。
    pfd[pfd_count].events = events; // 分配要监视的事件。
    pfd[pfd_count].revents = 0;     // 清除该pollfd的返回事件。
    ++pfd_count;                    // 增加活动文件描述符的数量。
}

/**
 * @brief  从pollfd数组中删除一个文件描述符。
 * @param  pfd[] pollfd结构体数组。
 * @param  fd    要从数组中删除的文件描述符。
 * @note   该函数在数组中搜索文件描述符,如果找到,则将其字段重置为默认值。
 */
void pfd_del(struct pollfd pfd[MAX], int fd)
{
    for (int i = 0; i < MAX; i++)
    {
        if (pfd[i].fd == fd)
        {
            pfd[i].fd = -1;     // 重置文件描述符为-1。
            pfd[i].events = 0;  // 清除事件。
            pfd[i].revents = 0; // 清除返回事件。
            break;
        }
    }
}

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

    struct sockaddr_in serv_addr;                // 定义服务器地址结构体
    socklen_t serv_addr_len = sizeof(serv_addr); // 获取服务器地址结构体的大小
    memset(&serv_addr, 0, sizeof(serv_addr));    // 初始化服务器地址结构体为0

    serv_addr.sin_family = AF_INET;                // 设置地址家族为IPv4
    serv_addr.sin_port = htons(1234);              // 设置端口号为1234,htons用于网络字节序转换
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置IP地址为本地任意地址

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

    // 将套接字设置为监听模式,并设置最大连接数为20
    if (listen(serv_sock, 20))
    {
        perror("listen 侦听失败!"); // 如果监听失败,打印错误信息
        close(serv_sock);            // 关闭套接字
        return -1;                   // 返回-1表示监听失败
    }

    struct pollfd pfd[MAX];          // 定义pollfd结构体数组,用于IO多路复用
    pfd_init(pfd);                   // 初始化pollfd数组
    pfd_add(pfd, serv_sock, POLLIN); // 将服务器套接字添加到pollfd数组中,并设置监听读事件

    // 进入一个无限循环,用于持续监听和处理网络事件
    while (1)
    {
        // 声明一个整型变量result,用于存储poll函数的返回值
        int result;

        // 调用poll函数等待I/O事件,如果返回-1表示出错
        if ((result = poll(pfd, MAX, 5000)) == -1)
        {
            printf("poll() 函数执行错误!所在行:%d\n", __LINE__); // 打印错误信息,并输出错误发生的行号
            exit(1);                                               // 遇到错误时退出程序
        }

        // 如果poll函数返回0,表示等待超时
        if (result == 0)
        {
            printf("超时,重新监听。\n"); // 打印超时信息
            continue;                     // 继续下一次循环,重新监听
        }

        // 遍历pollfd数组,处理每个文件描述符的事件
        for (int i = 0; i < MAX; i++)
        {
            // 如果文件描述符为-1,表示无效,跳过
            if (pfd[i].fd == -1)
            {
                continue; // 继续下一个文件描述符
            }

            // 如果revents字段包含POLLIN,表示有数据可读
            if (pfd[i].revents & POLLIN)
            {
                // 如果是服务器socket有数据可读,表示有新的连接
                if (pfd[i].fd == serv_sock)
                {
                    struct sockaddr_in clnt_addr;                 // 声明一个sockaddr_in结构体,用于存储客户端地址信息
                    socklen_t clnt_addr_size = sizeof(clnt_addr); // 客户端地址结构的大小

                    int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); // 接受新的连接,并获取客户端socket
                    pfd_add(pfd, clnt_sock, POLLIN);                                                   // 将新的客户端socket添加到pollfd数组中,以便监听其事件
                    printf("已连接的客户端socket为:%d\n", clnt_sock);                                 // 打印已连接的客户端socket编号
                }

                // 如果不是服务器socket,表示是客户端socket有数据可读
                else
                {
                    char buf[BUF_SIZE] = {0};                         // 声明一个字符数组用于存储读到的数据,并初始化为0
                    int str_len = read(pfd[i].fd, buf, BUF_SIZE - 1); // 从客户端socket读取数据
                    printf("客户端发来的数据:%s\n", buf);            // 打印客户端发来的数据

                    // 如果读取的字节数为0,表示客户端断开了连接
                    if (str_len == 0)
                    {
                        printf("客户端socket %d 失去连接\n", pfd[i].fd); // 打印失去连接的信息
                        close(pfd[i].fd);                                // 关闭客户端socket
                        pfd_del(pfd, pfd[i].fd);                         // 从pollfd数组中删除对应的socket
                    }

                    // 如果读取到了数据
                    else
                    {
                        write(pfd[i].fd, buf, str_len); // 将数据写回客户端,实现回声服务
                    }
                }
            }
        }
    }

    // 关闭服务端的socket
    close(serv_sock);

    return 0;
}

0152poll实现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; // 程序结束
}

  运行结果:

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值