Linux网路编程(基于Centos7)

基本概念

GNU计划

GNU 计划的最终目标是打造出一套完全自由(即自由使用、自由更改、自由发布)、开源的操作系统,并初步将其命名为GNU 操作系统

GNU 计划:“打造一套自由、开源的操作系统”的初衷,但该操作系统并非完全产自 GNU 计划,因此其被称为 GNU/Linux 操作系统(人们更习惯称为 Linux 操作系统

GCC

GCC 的全拼为GNU C Compiler,即 GUN 计划诞生的 C 语言编译器。

最初 GCC 的定位确实只用于编译 C 语言。但经过这些年不断的迭代,GCC 的功能得到了很大的扩展,它不仅可以用来编译 C 语言程序,还可以处理 C++、Go、Objective -C 等多种编译语言编写的程序。

与此同时,由于之前的 GNU C Compiler 已经无法完美诠释 GCC 的含义,所以其英文全称被重新定义为 GNU Compiler Collection(即GNU编译器套件)

Centos安装GCC编译器

  1. 安装完虚拟机操作系统后,首先需要联网。具体的方式见如下blog:

(4条消息) VMware 虚拟机里连不上网的五种解决方案_虚拟机无法上网_菜鸟也秃头的博客-CSDN博客

  1. 下载xshell,并且用xshell连接虚拟机。xshell连接不上虚拟机的问题见如下:我当时是直接还原了虚拟机的网络的默认配置就可以了,然后电脑重新开机。

(4条消息) Xshell连接不上虚拟机的解决办法汇总_虚拟机ssh连不上_落花流水i的博客-CSDN博客

  1. 下载GCC编译器,需要用root用户登陆虚拟机。
    1. 要能够在CentOS系统上添加新的存储库并安装软件包,您必须以root或者具有sudo权限的用户登录。
    2. 默认的CentOS存储库包含一个名为Development Tools的软件组,该软件组包含GCC编译器以及编译软件所需的许多库和其他工具。
    3. sudo yum group install "Development Tools"命令将会安装包括GCC编译器运行在内的开发工具,包括gccg++make
    4. 若还需要安装使用GNU/Linux进行开发的手册页请运行命令sudo yum install man-pages
    5. 安装完成后,通过使用将打印GCC版本的gcc --version命令验证GCC编译器是否已成功安装。

文件的上传与下载

  1. rz命令(上传文件到Linux)

    1. rz 直接输入 rz 之后回车就会打开你本地文件夹,选择文件就可以上传文件到Linux
  2. sz命令(下载文件到windows)

    1. sz Test.war。输入 sz 文件名,就会打开你本地文件夹,选择之后就会将 sz 后边写的文件保存到你windows本地

Linux下的文件操作

open函数

open函数的原型如下:

#include 
int open(const char *pathname, int flags);

参数说明:

  • pathname:要打开的文件路径名。
  • flags:打开文件的标志位,用来指定文件的打开方式和操作权限。

返回值:

  • 成功:返回文件描述符(非负整数),用于之后对文件的读写操作。
  • 失败:返回-1,并设置errno来指示具体的错误原因。

close函数

#include 
int close(int fd);

参数说明:

  • fd:要关闭的文件描述符。
  • 返回值:
    • 成功:返回0。
    • 失败:返回-1,并设置errno来指示具体的错误原因。

write函数

write函数用于向文件描述符写入数据。

它的原型如下:

#include 
ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:要写入数据的文件描述符。
  • buf:指向要写入的数据缓冲区的指针。
  • count`:要写入的数据字节数。

返回值:

  • 成功:返回实际写入的字节数。
  • 失败:返回-1,并设置errno来指示具体的错误原因。

read函数

read函数用于从文件描述符读取数据。它的原型如下:

#include 
ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:要读取数据的文件描述符。
  • buf:指向存放读取数据的缓冲区的指针。
  • count:要读取的数据最大字节数。

返回值:

  • 成功:返回实际读取的字节数。如果读取到文件末尾,返回0。
  • 失败:返回-1,并设置errno来指示具体的错误原因。

用read函数判断套接字读完了输入缓冲中的全部数据:

  1. 使用非阻塞套接字: 首先,将套接字设置为非阻塞模式,这样在读取数据时,read函数不会一直等待,而是立即返回。如果输入缓冲中没有数据可读,则read函数会返回一个错误码,指示无数据可用。
  2. 循环读取数据: 在非阻塞模式下,循环调用read函数,直到其返回值小于等于0。read函数返回值为0表示连接被关闭,返回值小于0表示发生了错误。如果read返回值大于0,表示成功读取了一些数据。由于是非阻塞模式,可能一次读取并未读完全部数据,所以需要进行循环读取,直到read返回0或一个负值。
  3. ps:这种情况下,read一直读取数据,所以最后一次肯定是将数据全部读完了的,然后在下一次读取数据的时候,输入缓冲区已经没有数据了,因此在下一次读取数据的时候,就会返回负数!

TCP套接字中的IO缓冲

write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。

准确来讲,write函数调用瞬间,数据讲移至输出缓冲,read函数调用瞬间,从输入缓冲中读取数据。

这些I/O缓冲特性:

  1. I/O缓冲在每个TCP套接字中单独存在。
  2. I/O缓冲在创建套接字时自动生成。
  3. 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
  4. 关闭套接字将丢失输入缓冲中的数据。

ps:不会出现输入缓冲的大小比传输的数据的大小还要小的情况。因为TCP中有滑动窗口机制控制数据流。

多播

多播(Multicast)是一种网络通信方式,用于将数据从一个发送者发送给一组接收者。与广播(Broadcast)不同,广播将数据发送给网络上的所有主机,而多播只将数据发送给特定的一组主机,这组主机称为多播组。多播允许在一个发送者和多个接收者之间进行高效的一对多通信

多播使用UDP的特性来实现一对多的数据传输。在多播中,数据包被发送到一个特定的多播组地址,而不是单个主机地址。所有加入了该多播组的主机都能接收到这个数据包。这种方式可以在一个发送者和多个接收者之间进行高效的一对多通信,节约了带宽和服务器资源。

实现多播的sender和receiver:sender只需设置数据报的最大生存时间,然后向特定的多播组发送数据,而receiver需要先加入多播组,然后再从多播组中接收数据。

多播的sender和receiver必须使用同一个端口!

多播组成员

ip_mreq结构体

ip_mreq结构体是用于设置和获取 IP 多播组成员的结构体,定义在 头文件中。

结构体定义如下:

struct ip_mreq {
    struct in_addr imr_multiaddr;  // 多播组的 IP 地址
    struct in_addr imr_interface;  // 加入多播组的接口的 IP 地址
};

其中,imr_multiaddr 表示要加入或离开的多播组的 IP 地址imr_interface 表示加入多播组的网络接口的 IP 地址。这两个字段都是 struct in_addr 类型,用于存储 IPv4 地址。

使用 ip_mreq 结构体,可以通过 setsockopt 函数来设置和获取 IP 多播组成员。通过设置 IP_ADD_MEMBERSHIP 选项可以将主机加入到指定的多播组,通过设置 IP_DROP_MEMBERSHIP 选项可以将主机从多播组中移除。

举例说明:

假设有一个 IPv4 的多播组地址为 239.255.1.1,主机的网络接口地址为 192.168.1.100。现在我们要将主机加入到这个多播组中(注意:我们只需要将所有要接收数据的主机加入到多播组地址,然后发送者发送数据到多播组地址即可),可以使用以下代码:

#include 
#include 
#include 

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return -1;
    }

    // 将当前的主机加入到多播的组地址
    struct ip_mreq mreq;
    mreq.imr_multiaddr.s_addr = inet_addr("239.255.1.1");  // 设置多播组地址
    mreq.imr_interface.s_addr = inet_addr("192.168.1.100");  // 将主机的地址加入到多播组地址
	// 设置套接字的多播地址
    if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
        perror("setsockopt");
        return -1;
    }

    printf("Joined multicast group.\n");

    // 接下来可以使用 sockfd 来进行多播数据的收发操作

    return 0;
}

setsockopt函数

setsockopt 函数用于设置套接字选项,它可以设置套接字的各种属性,例如超时时间、缓冲区大小、是否启用广播等。setsockopt 的函数原型如下:

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数解析:

  • sockfd:套接字文件描述符,用于标识要设置选项的套接字。
  • level:选项所属的协议层,常用的值有:
    • SOL_SOCKET:套接字选项层,用于设置套接字的各种属性。
    • IPPROTO_IP:IP 协议层,用于设置 IP 协议的选项。
    • IPPROTO_TCP:TCP 协议层,用于设置 TCP 协议的选项。
    • IPPROTO_UDP:UDP 协议层,用于设置 UDP 协议的选项。
    • IPPROTO_IPV6:IPv6 协议层,用于设置 IPv6 协议的选项。
  • optname:要设置的选项名称,表示要设置的具体属性,具体取值与所属的协议层和选项类型有关。
  • optval:指向存放选项值的缓冲区,该缓冲区的类型和大小取决于所要设置的选项。
  • optlen:选项值缓冲区的大小,单位是字节。

sendto函数

sendto 函数是用于通过 UDP 套接字向指定的目标地址发送数据报的系统调用。它可以在不需要建立连接的情况下直接发送数据,适用于无连接的数据传输。该函数的原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  • sockfd:表示要发送数据的套接字描述符。
  • buf:指向要发送数据的缓冲区
  • len:表示要发送数据的长度
  • flags:指定发送数据的可选参数,可以为 0 或者包含以下标志的按位或组合:
    • MSG_CONFIRM:确保数据报能够成功交付到目标,或者返回错误。
    • MSG_DONTROUTE:不使用路由表来发送数据,直接发送到目标地址。
    • MSG_DONTWAIT:非阻塞发送数据,即使在发送缓冲区已满的情况下,也立即返回。
    • MSG_EOR:表示数据报的末尾。
    • MSG_MORE:在数据报链的末尾指示还有更多数据报。
    • MSG_NOSIGNAL:如果目标进程已经关闭,不产生 SIGPIPE 信号,而是返回错误。
  • dest_addr:指向目标地址的结构体指针,通常是 struct sockaddr_instruct sockaddr_in6 类型,用于指定接收方的地址和端口号。
  • addrlen:表示目标地址结构体的大小

sendto 函数的返回值为发送的数据字节数,如果出现错误,则返回 -1,并设置相应的错误码到 errno 变量。

以下是一个简单的示例,使用 sendto 函数向指定的目标地址发送 UDP 数据报:

#include 
#include 
#include 
#include 

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return -1;
    }

    // 设置目标地址信息
    struct sockaddr_in dest_addr;
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 目标 IP 地址
    dest_addr.sin_port = htons(12345); // 目标端口号

    const char* message = "Hello, UDP!";
    ssize_t num_bytes_sent = sendto(sockfd, message, strlen(message), 0,
                                    (struct sockaddr*)&dest_addr, sizeof(dest_addr));

    if (num_bytes_sent == -1) {
        perror("sendto");
    } else {
        printf("Sent %zd bytes to %s:%d\n", num_bytes_sent,
               inet_ntoa(dest_addr.sin_addr), ntohs(dest_addr.sin_port));
    }

    close(sockfd);
    return 0;
}

recvfrom函数

recvfrom 函数是用于通过 UDP 套接字接收数据报的系统调用。它可以从指定的源地址接收数据,适用于无连接的数据传输。该函数的原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:表示要接收数据的套接字描述符。
  • buf:指向接收数据的缓冲区。
  • len:表示接收数据缓冲区的大小。
  • flags:指定接收数据的可选参数,可以为 0 或者包含以下标志的按位或组合:
    • MSG_CMSG_CLOEXEC:接收的控制消息中的文件描述符将被设置为 close-on-exec,即在执行 exec 调用时自动关闭。
    • MSG_DONTWAIT:非阻塞接收数据,即使在接收缓冲区中没有数据时也立即返回。
    • MSG_ERRQUEUE:接收错误消息,仅适用于特定类型的套接字(如 RAW 套接字)。
    • MSG_OOB:接收带外数据。
    • MSG_PEEK:从接收队列中查看数据,但不将数据从队列中删除。
    • MSG_TRUNC:如果接收缓冲区不够大,截断接收数据而不是丢弃多余的部分。
  • src_addr:指向用于保存源地址的结构体指针,通常是 struct sockaddr_instruct sockaddr_in6 类型。
  • addrlen:指向一个整数,表示 src_addr 结构体的大小,函数将返回实际接收到的源地址结构体大小。

recvfrom 函数的返回值为实际接收的数据字节数,如果出现错误,则返回 -1,并设置相应的错误码到 errno 变量。

以下是一个简单的示例,使用 recvfrom 函数从 UDP 套接字中接收数据报:

#include 
#include 
#include 
#include 

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return -1;
    }

    // 绑定到指定的地址和端口
    struct sockaddr_in my_addr;
    my_addr.sin_family = AF_INET;
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到任意本地地址
    my_addr.sin_port = htons(12345); // 绑定端口号
	// 将UDP套接字与本机进行绑定
    if (bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr)) < 0) {
        perror("bind");
        close(sockfd);
        return -1;
    }

    // 接收数据
    char buffer[1024];
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    ssize_t num_bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                                         (struct sockaddr*)&client_addr, &client_addr_len);

    if (num_bytes_received == -1) {
        perror("recvfrom");
    } else {
        buffer[num_bytes_received] = '\0';
        printf("Received %zd bytes from %s:%d: %s\n", num_bytes_received,
               inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);
    }

    close(sockfd);
    return 0;
}

广播

广播是一种网络通信方式,它可以向同一网络中的所有主机传输数据。广播数据包会被网络中的所有设备接收,包括路由器和交换机,但是只有与广播发送者在同一网络的主机才会处理这个广播消息。

广播也是基于UDP完成的。

实现广播的区别和实现多播的区别仅仅在于

  1. 广播的ip地址和多播的组地址不同。
  2. 广播设置套接字的可选项与多播不同。

直接广播和本地广播

根据传输数据时使用的IP地址的形式来分:广播分为直接广播**(主机地址全部设置为1)和本地广播(网络地址和主机地址都全部设置为1)**。

广播的实现:sender需要设置更改套接字的默认设置,使套接字支持广播。receiver只需要正常接收数据即可。

实现广播的发送和接收

广播发送端(Sender)

#include 
#include 
#include 
#include 
#include 

#define BROADCAST_PORT 12345

int main() {
    int sender_socket;
    struct sockaddr_in broadcast_addr;
    const char* broadcast_message = "Hello, this is a broadcast message!";
    
    // 创建UDP套接字
    sender_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (sender_socket < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项,允许广播
    int broadcast_enable = 1;
    if (setsockopt(sender_socket, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable)) < 0) {
        perror("Error setting socket options");
        close(sender_socket);
        exit(EXIT_FAILURE);
    }
	// broadcast_addr结构体设置广播的ip地址和端口号
    memset(&broadcast_addr, 0, sizeof(broadcast_addr));
    broadcast_addr.sin_family = AF_INET;
    broadcast_addr.sin_port = htons(BROADCAST_PORT);
    broadcast_addr.sin_addr.s_addr = INADDR_BROADCAST;

    // 发送广播消息
    if (sendto(sender_socket, broadcast_message, strlen(broadcast_message), 0, (struct sockaddr*)&broadcast_addr, sizeof(broadcast_addr)) < 0) {
        perror("Error sending broadcast");
    } else {
        printf("Broadcast message sent: %s\n", broadcast_message);
    }

    close(sender_socket);
    return 0;
}

广播接收端(Receiver)

#include 
#include 
#include 
#include 

#define BROADCAST_PORT 12345

int main() {
    int receiver_socket;
    struct sockaddr_in receiver_addr, sender_addr;
    socklen_t sender_addr_len;
    char buffer[1024];

    // 创建UDP套接字
    receiver_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (receiver_socket < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }
	
    memset(&receiver_addr, 0, sizeof(receiver_addr));
    receiver_addr.sin_family = AF_INET;
    receiver_addr.sin_port = htons(BROADCAST_PORT);
    receiver_addr.sin_addr.s_addr = INADDR_ANY;

    // 绑定接收端口和地址
    if (bind(receiver_socket, (struct sockaddr*)&receiver_addr, sizeof(receiver_addr)) < 0) {
        perror("Error binding socket");
        close(receiver_socket);
        exit(EXIT_FAILURE);
    }

    printf("Waiting for broadcast messages...\n");

    while (1) {
        // 接收广播消息
        sender_addr_len = sizeof(sender_addr);
        ssize_t bytes_received = recvfrom(receiver_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&sender_addr, &sender_addr_len);

        if (bytes_received < 0) {
            perror("Error receiving broadcast");
        } else {
            buffer[bytes_received] = '\0';
            printf("Received broadcast message: %s from %s:%d\n", buffer, inet_ntoa(sender_addr.sin_addr), ntohs(sender_addr.sin_port));
        }
    }

    close(receiver_socket);
    return 0;
}

基于UDP回声客户端和服务端的实现

已连接UDP套接字和未连接UDP套接字

sendto函数传输数据中过程大致的3个阶段:

  1. 向UDP套接字注册目标IP和端口号。
  2. 传输数据。
  3. 删除UDP套接字中注册的目标地址和端口号。

如果基于UDP的传输的过程中,我们要多次传输数据到同一个目标主机,那么我们可以利用connect函数将UDP套接字编程已连接套接字,这样会提高效率。

如果我们创建了已连接套接字,那么在传输数据的时候,就可以与TCP套接字一样,每次调用sendto函数时候只需传输数据。因为已经指定了收发对象,所以不仅可以使用sendto,recvfrom函数,还可以使用write,read函数进行通信。

UDP回声服务端

#include 
#include 
#include 
#include 
#include 

#define PORT 12345
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len;
    char buffer[BUFFER_SIZE];

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    // 设置server_addr结构体
    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(PORT);

    // 绑定套接字到指定的IP地址和端口
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Error binding socket");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
	// 基于UDP套接字的服务端不需要进行listen和accept。
    // 而且只需要一个套接字就能与客户端进行通信。
    printf("UDP Echo Server listening on port %d...\n", PORT);

    while (1) {
        // 接收来自客户端的数据
        client_addr_len = sizeof(client_addr);
        ssize_t bytes_received = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0, (struct sockaddr*)&client_addr, &client_addr_len);
        if (bytes_received < 0) {
            perror("Error receiving data");
        } else {
            buffer[bytes_received] = '\0';
            printf("Received data from client: %s\n", buffer);

            // 将接收到的数据回送给客户端
            sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&client_addr, sizeof(client_addr));
        }
    }

    close(sockfd);
    return 0;
}

UDP回声客户端

#include 
#include 
#include 
#include 
#include 

#define SERVER_IP "127.0.0.1"
#define PORT 12345
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    ssize_t bytes_sent, bytes_received;

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    // 设置server_addr结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(PORT);
	// 基于UDP的客户端不需要调用connect函数建立连接,可以直接发送数据。
    while (1) {
        // 从标准输入读取用户输入的数据
        printf("Enter data to send (q to quit): ");
        fgets(buffer, BUFFER_SIZE, stdin);

        // 检查是否输入了 "q",如果是则退出客户端
        if (strcmp(buffer, "q\n") == 0) {
            break;
        }

        // 发送数据到服务器
        bytes_sent = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
        if (bytes_sent < 0) {
            perror("Error sending data");
        }

        // 等待接收回声数据 这里设置NULL表示我客户端不关心是谁发送过来的数据
        // 如果说想要接收是谁发送过来的数据的信息,那么就需要传入发送者的结构体。
        bytes_received = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0, NULL, NULL);
        if (bytes_received < 0) {
            perror("Error receiving data");
        } else {
            buffer[bytes_received] = '\0';
            printf("Received echo from server: %s\n", buffer);
        }
    }

    close(sockfd);
    return 0;
}

基于TCP回声客户端和服务端的实现

socket函数

在Linux中,socket函数用于创建一个新的套接字,并返回套接字描述符。它的原型如下:

#include 
#include 
int socket(int domain, int type, int protocol);

参数说明:

  • domain:指定通信的协议族,可以是PF_INET(IPv4)、PF_INET6(IPv6)等。
    • AF = Address Family
    • PF = Protocol Family
    • AF_INET = PF_INET
  • type:指定套接字类型,可以是SOCK_STREAM(面向连接的流套接字,如TCP)、SOCK_DGRAM(无连接的数据报套接字,如UDP)、SOCK_RAW(原始套接字,用于直接访问网络协议)等。
    • 面向连接的通信特点:
      • 数据是以数据流的形式进行传输的,即发送方将数据按照顺序发送,接收方按照相同的顺序接收,保证数据的完整性和顺序性。因此,面向连接的通信中不存在数据边界,也就是说发送方可以多次发送数据,接收方可以多次(或者调用一个read接收)接收数据,并且数据之间没有明显的分隔符
    • 无连接的通信特点:
      • 在无连接的通信中,数据是以数据报的形式进行传输的,每个数据报都是独立的,具有自己的边界(也就是说发送一个数据包,必须对应调用一次read来读取整个数据包!),因此在无连接的通信中存在数据边界。每个数据报都包含了目的地址、源地址、协议类型等信息,接收方根据这些信息来辨别不同的数据报。
  • protocol:指定协议类型,通常为0,表示根据domaintype参数选择默认协议。

返回值:

  • 成功:返回一个新的套接字描述符。
  • 失败:返回-1,并设置errno来指示具体的错误原因。

getsockopt函数

getsockopt 函数用于获取套接字选项的值。它的原型如下:

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

参数解释:

  • sockfd:要查询选项的套接字描述符。
  • level:选项所属的协议层。常用的值有 SOL_SOCKET 表示套接字级别的选项,以及其他协议特定的值,比如 IPPROTO_TCP 表示 TCP 协议特定的选项。
  • optname:要查询的协议层里面的选项的名称,表示要查询的具体选项。
  • optval:用于存储选项值的缓冲区。
  • optlen:传入时表示 optval 缓冲区的大小,传出时表示实际获取到的选项值的大小。

setsockopt函数

setsockopt 函数用于设置套接字选项的值。它的原型如下:

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数解释:

  • sockfd:要设置选项的套接字描述符。
  • level:选项所属的协议层。常用的值有 SOL_SOCKET 表示套接字级别的选项,以及其他协议特定的值,比如 IPPROTO_TCP 表示 TCP 协议特定的选项。
  • optname:选项的名称,表示要设置的具体选项。
  • optval:指向存储选项值的缓冲区的指针。
  • optlen:指定 optval 缓冲区的大小。

举例说明:

#include 
#include 
#include 
#include 
#include 

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    int keepalive = 1;
    socklen_t len = sizeof(keepalive);

    // 设置 SO_KEEPALIVE 选项的值为 1(开启)  如果设置失败返回负数
    if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, len) < 0) {
        perror("setsockopt");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("SO_KEEPALIVE option set to ON\n");

    close(sockfd);
    return 0;
}

各种可选项

SO_TYPE

这个可选项用于获取套接字的类型信息。如果是TCP套接字,那么获取的常数值为1。如果是UDP套接字,获取的常数值为2。

SO_SNDBUF、SO_RCVBUF
  1. SO_SNDBUF

    • SO_SNDBUF 选项用于设置套接字发送缓冲区的大小
    • 发送缓冲区是用于存储待发送数据的内存区域。
    • 可以使用 setsockopt 函数来设置该选项的值,传递的参数是一个整数,表示要设置的缓冲区大小(字节)。
    • 默认情况下,操作系统会根据系统的配置自动设置缓冲区大小,但在某些情况下,可能需要手动设置该选项来优化网络性能,例如在高负载情况下或者需要发送大量数据时。
  2. SO_RCVBUF

    • SO_RCVBUF 选项用于设置套接字接收缓冲区的大小
    • 接收缓冲区是用于存储接收到的数据的内存区域。
    • 同样可以使用 setsockopt 函数来设置该选项的值,传递的参数也是一个整数,表示要设置的缓冲区大小(字节)。
    • 默认情况下,操作系统会根据系统的配置自动设置接收缓冲区大小,但在某些情况下,可能需要手动设置该选项来优化网络性能,例如在需要接收大量数据时。

    举例说明:

    #include 
    #include 
    #include 
    #include 
    
    int main() {
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0) {
            perror("socket");
            exit(EXIT_FAILURE);
        }
    
        int sndbuf_size = 1024 * 1024; // 设置发送缓冲区大小为 1 MB
        int rcvbuf_size = 1024 * 1024; // 设置接收缓冲区大小为 1 MB
    
        // 设置发送缓冲区大小
        if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, sizeof(sndbuf_size)) < 0) 	  {
            perror("setsockopt");
            close(sockfd);
            exit(EXIT_FAILURE);
        }
    
        // 设置接收缓冲区大小
        if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size)) < 0) 	  {
            perror("setsockopt");
            close(sockfd);
            exit(EXIT_FAILURE);
        }
    
        printf("SO_SNDBUF and SO_RCVBUF options set successfully\n");
    
        close(sockfd);
        return 0;
    }
    
SO_REUSEADDR

Time-wait:指的是主动关闭TCP连接的套接字在完成TCP四次挥手后,套接字会等待一段时间(通常是2倍的MSL,最长时间为2分钟),然后才会彻底关闭。

原因:在TCP连接关闭后,有可能还有延迟到达的数据报文,这些数据报文需要在Time-wait状态期间被处理。

套接字在Time-wait过程时,相应的端口是正在使用的状态。

SO_REUSEADDR 是套接字选项中的一个选项,用于设置在bind()函数中允许地址重用。它允许多个套接字在同一端口上绑定,即使之前绑定的套接字仍然处于 TIME_WAIT 状态。通常情况下,一个套接字在释放后会在一段时间内处于 TIME_WAIT 状态,这段时间内不能重新绑定相同的地址和端口。

使用 SO_REUSEADDR 选项可以在某些情况下解决地址已被占用的问题,特别是在服务器端需要频繁重启或者在调试时。

举例说明:

假设有一个服务器程序,在异常情况下会意外退出,然后重新启动。如果服务器程序在启动时需要绑定相同的地址和端口,但之前的套接字可能还处于 TIME_WAIT 状态,导致无法绑定,就可以使用 SO_REUSEADDR 选项。

int option = 1;
// 设置 SO_REUSEADDR 选项
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)) == -1)
    error_handling("setsockopt() error");
TCP_NODELAY

**Nagle算法:**Nagle 算法是一种优化算法,它会延迟发送小数据包,将多个小数据包合并成一个较大的数据包再发送,以减少网络传输的开销。而且使用Nagle算法的时候,只有收到前一条数据的ACK信息时,才会发送下一数据。

TCP_NODELAY 是套接字选项中的一个选项,用于禁用 Nagle 算法。虽然这个算法在某些情况下可以提高网络传输效率,但对于某些实时性要求高的应用,如在线游戏或视频通话,这种延迟可能会导致性能下降。

TCP_NODELAY 的作用就是禁用 Nagle 算法(将TCP_NODELAY选项的值改为1)使得数据发送时不进行合并,而是立即发送。这样可以降低发送数据的延迟,提高实时性。

举例说明:

int option = 1;
// 设置 TCP_NODELAY 选项
if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &option, sizeof(option)) == -1)
    error_handling("setsockopt() error");

套接字的缓冲

在网络编程中,套接字内部通常会有一个由字节数组构成的缓冲区,用于存储即将发送或接收的数据。这个缓冲区可以被看作是一个临时存储区域,用于暂时保存数据,然后在适当的时候进行发送或接收。

对于发送数据,当应用程序调用send函数发送数据时,数据不会立即被发送到网络,而是先被复制到套接字的发送缓冲区中。操作系统会根据网络的状况和传输策略来决定何时发送缓冲区中的数据。如果发送缓冲区已满或者网络状况不佳,数据可能会滞留在发送缓冲区中,直到有空闲空间或者网络状况改善时才被发送出去。

对于接收数据,当应用程序调用recv函数接收数据时,数据也不是直接从网络读取到应用程序中,而是先被复制到套接字的接收缓冲区中。然后应用程序从接收缓冲区中读取数据。如果接收缓冲区中没有数据,recv函数会等待,直到有数据到达接收缓冲区或者出现错误。

sockaddr_in结构体

sockaddr_in是用于表示IPv4地址的结构体,其定义如下:

struct sockaddr_in {
    short sin_family;           // 地址族,AF_INET表示IPv4    占用2字节
    unsigned short sin_port;    // 端口号					  占用2字节	
    struct in_addr sin_addr;    // IP地址					   占用4字节
    char sin_zero[8];           // 未使用,通常设置为0		   占用8字节
};

现在来解析一下每个字段的含义:

  1. sin_family:表示地址族,通常设置为AF_INET,表示IPv4地址族。
  2. sin_port:表示端口号,用于标识应用程序运行的网络服务。端口号是一个16位的整数,范围是0~65535。
  3. sin_addr:表示IP地址,是一个struct in_addr类型的结构体。
  4. sin_zero:为了保持与struct sockaddr结构体的大小相同而保留的字段,通常设置为全0。
  • in_addr结构体定义如下:
struct in_addr {
    unsigned long s_addr; // 32位的IPv4地址,使用网络字节序存储
};

IPv4地址是一个32位的无符号整数,通常使用网络字节序(大端字节序)存储(Inter和AMD系列的CPU都采用小端序标准)。可以使用inet_addr函数将点分十进制的IP地址转换为unsigned long类型的网络字节序整数。

字节序的转换

htons函数:把short类型数据从主机字节序转化为网络字节序 在传入端口的时候需要对端口进行转换

htonl函数:把long类型的数据从主机字节序转化为网络字节序 在传入ip地址的时候需要对ip地址进行转换

inet_addr函数

inet_addr函数用于将点分十进制的IP地址转换为**网络字节序(大端字节序)**的32位IPv4地址。它的声明如下:

in_addr_t inet_addr(const char *cp);

参数cp是一个指向包含点分十进制IP地址的字符串的指针。点分十进制IP地址由四个十进制数字组成,每个数字之间用.分隔,例如:“127.0.0.1”。

函数返回值是一个in_addr_t类型的整数,它是一个无符号32位整数,表示转换后的IPv4地址,使用网络字节序存储

而且当转换失败的时候,会返回INADDR_NONE

inet_aton函数

inet_aton函数用于将点分十进制的IP地址转换为网络字节序(大端字节序)的32位IPv4地址(与inet_addr函数类似)。它的声明如下:

int inet_aton(const char *cp, struct in_addr *inp);

参数cp是一个指向包含点分十进制IP地址的字符串的指针。点分十进制IP地址由四个十进制数字组成,每个数字之间用.分隔,例如:“127.0.0.1”。

参数inp是一个指向struct in_addr结构体的指针。struct in_addr结构体用于存储32位IPv4地址

函数返回值是一个整数,如果转换成功,返回1;如果转换失败,返回0。

下面是一个使用inet_aton函数的例子:

#include 
#include 

int main() {
    const char *ip_str = "192.168.1.100";
    struct in_addr ip_addr;

    if (inet_aton(ip_str, &ip_addr) == 1) {
        // 打印转换后的IPv4地址
        printf("IPv4 address: %u\n", ip_addr.s_addr);
    } else {
        printf("Invalid IP address\n");
    }

    return 0;
}

inet_ntoa函数

inet_ntoa函数用于将网络字节序(大端字节序)的32位IPv4地址转换为点分十进制的IP地址。它的声明如下:

char *inet_ntoa(struct in_addr in);

参数in是一个struct in_addr类型的结构体,用于存储网络字节序的32位IPv4地址。

函数返回值是一个指向表示点分十进制IP地址的静态缓冲区的指针。注意,由于返回值指向的是静态缓冲区,所以每次调用inet_ntoa函数时,返回值会被覆盖。因此,如果你希望保留转换后的IP地址,请将返回值复制到自己定义的缓冲区中

下面是一个使用inet_ntoa函数的例子:

#include 
#include 

int main() {
    struct in_addr ip_addr;
    ip_addr.s_addr = 16777343; // 由于是网络字节序,等同于点分十进制的IP地址:"127.0.0.1"

    // 将网络字节序的IPv4地址转换为点分十进制IP地址
    char *ip_str = inet_ntoa(ip_addr);

    // 打印转换后的IP地址
    printf("IPv4 address: %s\n", ip_str);

    return 0;
}

atoi函数

atoi函数用于将字符串转换为整数(int类型)。它的声明如下:

int atoi(const char *str);

参数str是一个指向以空字符(‘\0’)结尾的字符串的指针,表示要转换的字符串。这个字符串可以包含空白字符(空格、制表符、换行符等),并且可以以正负号开头,后面跟着一个或多个数字字符。

函数返回值是转换后的整数值。如果字符串不能被正确转换为整数(例如,字符串中包含非数字字符),atoi函数将返回0。

下面是一个使用atoi函数的例子:

#include 
#include 

int main() {
    const char *str = "12345";
    int num = atoi(str);

    printf("The converted integer is: %d\n", num);  // 12345

    return 0;
}

gethostbyname函数

gethostbyname 函数用于通过主机名获取主机的相关信息,包括主机的IP地址等。通过域名获取主机的ip。

函数原型为:

struct hostent *gethostbyname(const char *name);

参数解析:

  • name: 表示要查询的主机名,可以是主机名或者IP地址的字符串形式。

返回值:

  • 成功:返回一个指向 hostent 结构体的指针,包含主机的相关信息。
  • 失败:返回 NULL,并设置 h_errno 变量来表示错误码。

hostent 结构体的定义如下:

struct hostent {
    char  *h_name;        // 官方主机名(域名)
    char **h_aliases;     // 主机的别名列表
    int    h_addrtype;    // 地址类型,通常为 AF_INET(IPv4)或 AF_INET6(IPv6)
    int    h_length;      // 地址长度,通常为 4(IPv4)或 16(IPv6)
    char **h_addr_list;   // 地址列表,一个指向IP地址的指针数组,以NULL结尾
};
// 可以通过遍历拿到所有的ip
// 字符串指针数组中的元素实际指向的是in_addr结构体变量地址值而非字符串。
for(int i = 0;host->h_addr_list[i];i++){
    printf("%s\n",inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
}

gethostbyaddr函数

利用IP地址获取域名相关信息。

它的函数原型如下:

struct hostent *gethostbyaddr(const char* addr, socklen_t len, int type);

参数解析:

  • addr:指向存储 IP 地址的缓冲区的指针,可以是IPv4地址结构 struct in_addr 或IPv6地址结构 struct in6_addr
  • len:指定地址的长度,对于 IPv4 地址,len 应为 sizeof(struct in_addr),对于 IPv6 地址,len 应为 sizeof(struct in6_addr)
  • type:指定地址类型,应为 AF_INET(IPv4)或 AF_INET6(IPv6)。

sockaddr结构体

struct sockaddr是一个通用的地址结构体,在网络编程中经常用于存储和表示不同地址族(IPv4、IPv6等)的地址信息。由于它是一个通用的结构体,不能直接使用,通常会通过类型转换为特定的地址结构体,如struct sockaddr_in用于IPv4地址,struct sockaddr_in6用于IPv6地址。

struct sockaddr的声明如下:

struct sockaddr {
    sa_family_t sa_family; // 地址族(Address family)   // 2字节
    char sa_data[14]; // 地址数据					     // 14字节
};
  • sa_family:表示地址族,是一个短整数(2字节),用于指示地址的类型,如AF_INET表示IPv4地址族,AF_INET6表示IPv6地址族,等等。不同的地址族对应不同的地址结构体。
  • sa_data:存储地址数据的数组,大小为14字节。对于IPv4地址,保存有2字节的端口号和4字节的IP地址,剩下8个字节用于保留。

由于struct sockaddr是一个通用的结构体,它主要在函数参数中使用,用于传递不同地址族的地址信息。在实际使用中,会根据需要将其转换为特定的地址结构体,并使用特定的地址结构体来访问和处理具体的地址信息。

listen函数

listen函数用于将套接字设置为监听状态,使其可以接受连接请求。它的原型如下:

int listen(int sockfd, int backlog);
  • sockfd:需要设置为监听状态的套接字的文件描述符
  • backlog等待连接队列的最大长度。当有多个连接请求到达时,如果已经有 backlog 个请求等待处理,新的连接请求将会被拒绝。

返回值listen函数的返回值是一个整数,用于表示函数执行的状态。如果函数调用成功,返回值为0。如果函数调用失败,返回值为-1

accept函数

accept函数用于接受客户端的连接请求,并创建一个新的套接字用于与客户端进行通信。它的原型如下:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd监听状态的套接字的文件描述符
  • addr:一个指向 struct sockaddr 类型的指针,用于保存客户端的地址信息
  • addrlen:一个指向 socklen_t 类型的指针,用于指定 addr 的长度,同时也用于返回实际客户端地址的长度

返回值accept 函数的返回值是一个整数,表示接受连接的套接字的文件描述符。如果函数调用成功,返回值是一个新的套接字文件描述符,用于与客户端建立连接;如果函数调用失败,返回值为-1

connect函数

connect 函数用于向服务器发起连接请求。它的原型如下:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数解析:

  • sockfd: 需要连接的套接字文件描述符。它应该是之前调用 socket 函数创建的套接字,且类型为 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP)。
  • addr: 指向目标服务器的地址结构体指针,可以是 struct sockaddr_instruct sockaddr_in6,具体取决于所使用的地址族。
  • addrlen: addr 指向的地址结构体的长度,可以使用 sizeof(struct sockaddr_in)sizeof(struct sockaddr_in6) 获取。

返回值:

  • 如果连接成功,返回值为0。
  • 如果连接失败,返回值为-1,并设置全局变量 errno 来指示错误的原因。

注意:在调用connect函数的时候,如果服务器端还没有调用accept函数接受连接,那么客户端将会阻塞在这里。而且,在调用connect函数的时候,客户端的ip地址和端口号会由操作系统自动分配,无需我们手动绑定。

INADDR_ANY

INADDR_ANY是一个常量,它表示服务器绑定的IP地址,通常用于服务器程序在多个网络接口上监听连接。具体来说,INADDR_ANY代表所有可用的网络接口地址

在IPv4中,INADDR_ANY的值是0,表示服务器可以在任意网络接口上接受连接请求。当服务器使用bind函数绑定套接字时,如果将sin_addr设置为INADDR_ANY,则服务器将监听所有可用的网络接口,可以通过任意可用的IP地址进行访问。

TCP回声服务端

#include 
#include  // 处理字符串的头文件
// atoi() 函数位于 头文件中。
// atoi() 函数用于将字符串(表示整数值)转换为对应的整数。
// 函数名称中的 "ato" 表示 "ASCII to",即将 ASCII 字符串转换为整数。
#include  // 用于提供对 POSIX 操作系统 API 的访问。它定义了许多常用的系统调用和符号常量,提供了对操作系统底层功能的访问。
#include 
//  头文件提供了一些用于处理 IP 地址和端口号的函数和宏定义。
// inet_addr():将点分十进制的 IP 地址转换为网络字节序的整数表示。
// htonl() 函数用于将无符号长整型数(32位)从主机字节序转换为网络字节序。
// htons() 函数用于将一个无符号短整型数(16位)从主机字节序转换为网络字节序。
#include 
//  头文件提供了一些用于套接字编程的函数和结构体定义。其中常用的函数包括:
/*
socket():创建套接字。
bind():将套接字与地址绑定。用于将一个套接字(socket)绑定到一个特定的IP地址和端口号,以便在该地址和端口上监听和接收网络数据。
listen():启动套接字监听模式。
accept():接受客户端连接请求。
connect():连接到服务器。
read():从套接字读取数据。
write():向套接字写入数据。
close():关闭套接字。
*/

#define BUF_SIZE 1024
void error_handing(char *message);

int main(int argc, char *argv[])
{
    // serv_sock用来建立连接的套接字的文件描述符
    // clnt_sock用来与客户端通信的套接字
    int serv_sock, clnt_sock;
    char message[BUF_SIZE];
    struct sockaddr_in serv_adr, clnt_adr; // 服务器和客户端的地址结构体。
    socklen_t clnt_adr_sz;                 // clnt_adr_sz客户端地址结构体的大小。
    int str_len;
    if (argc != 2)
    {
        printf("Usage: %s  \n", argv[0]);
    }

    // 创建套接字  PF_INET 表示使用 IPv4,SOCK_STREAM 表示使用 TCP 协议。
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handing("socket() error");
    }
    // 初始化为0
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET; // 表示使用的地址族
    // INADDR_ANY表示绑定到所有可用的网络接口上,即服务器将监听所有网络接口上的传入连接。这样,服务器可以接受来自任何IP地址的连接请求,而不限定于特定的IP地址。
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 表示服务器地址(转成long类型)
    serv_adr.sin_port = htons(atoi(argv[1]));     // 表示服务器端口号(转成short类型)

    // 将套接字与主机的ip地址和端口号进行绑定
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handing("bind() error");
    }

    if (listen(serv_sock, 5) == -1)
    {
        error_handing("listen() error");
    }

    clnt_adr_sz = sizeof(clnt_adr); // 客户端地址结构体的大小。
    int i;
    for (i = 0; i < 5; i++)
    {
        // clnt_sock是用来与客户端通信的套接字
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
        if (clnt_sock == -1)
        {
            error_handing("accept() error");
        }
        else
            printf("Connected client %d \n", i + 1);
		// 套接字是在阻塞的状态下的!
        while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
        {
            printf("Message from client:", message);
            puts('\n');
            write(clnt_sock, message, str_len);
        }
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}

void error_handing(char *message)
{
    fputs(message, stderr);
    fputc("\n", stderr);
}

TCP回声客户端

#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024
void error_handing(char *message);

int main(int argc, char *argv[])
{
    int sock; // 客户端的套接字文件描述符
    char message[BUF_SIZE];
    int str_len;                 // 读取到的数据的长度。
    struct sockaddr_in serv_adr; // 服务器的地址结构体。

    if (argc != 3)
    {
        printf("Usage : %s   \n", argv[0]);
        exit(1);
    }
    sock = socket(PF_INET, SOCK_STREAM, 0); // PF_INET 表示使用 IPv4,SOCK_STREAM 表示使用 TCP 协议。
    if (sock == -1)
    {
        error_handing("socket() error");
    }
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET; // 表示使用的地址族
    // s_addr表示服务器的 IP 地址
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]); // 将点分十进制形式的 IP 地址转换为网络字节序的二进制形式。
    serv_adr.sin_port = htons(atoi(argv[2]));      // sin_port 表示服务器的端口号。

    if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handing("connect() error!");
    }
    else
    {
        puts("Connected....");
    }
    while (1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);

        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
        {
            break;
        }
        str_len = write(sock, message, strlen(message));
        /*
        recv_len = 0;
        while(recv_len < str_len){
        	recv_cnt = read(sock,&message[recv_len],BUF_SIZE-1);
        	if(recv_cnt == -1) {
        		printf("read() error");
        	}
        	recv_len += recv_cnt;
        }
        */
        str_len = read(sock, message, BUF_SIZE - 1);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
    close(sock);
    return 0;
}

void error_handing(char *message)
{
    fputs(message, stderr);
    fputc("\n", stderr);
}

多任务并发服务器

代表性的并发服务器端实现模型和方法:

  1. 多进程服务器:通过创建多个进程提供服务。
  2. 多路复用服务器:通过捆绑并统一管理I/O对象提供服务。
  3. 多线程服务器:通过生成与客户端等量的线程提供服务。

CPU核的数量与可同时运行的进程数相同。若进程数超过核数,进程将分时使用CPU资源。但因为CPU运转速度极快,我们会感到所有进程同时运行。

僵尸进程

僵尸进程是指在进程已经结束执行(通过调用exit系统调用或者返回main函数)后**(注意:此时操作系统会接收到这个值,但是并不会对其进行资源的释放,需要父进程对子进程进行资源释放),但其父进程还未对其进行资源回收和进程退出状态获取的进程。僵尸进程仍然保留在系统进程表中,占用了进程ID等系统资源,但已经没有运行代码。僵尸进程不会消耗CPU资源,但会占用一些系统资源**,如果大量产生僵尸进程,可能会导致系统资源耗尽。

僵尸进程产生的原因是父进程没有及时调用wait或waitpid等系统调用来获取子进程的退出状态,导致子进程的退出状态不能被及时回收。在父进程未回收子进程退出状态的情况下,子进程的退出状态信息会保留在系统进程表中,成为僵尸进程。

下面举一个简单的例子来说明僵尸进程的产生:

#include 
#include 
#include 

int main() {
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("fork error");
        exit(1);
    } else if (pid == 0) {
        // 子进程执行的代码
        printf("Child process (PID: %d) is running.\n", getpid());
        sleep(5);
        printf("Child process (PID: %d) is exiting.\n", getpid());
    } else {
        // 父进程执行的代码
        printf("Parent process (PID: %d) is running.\n", getpid());
        printf("Parent process (PID: %d) is waiting for the child to exit.\n", getpid());
        // 父进程未调用wait或waitpid来回收子进程退出状态
        sleep(10);
        printf("Parent process (PID: %d) is exiting.\n", getpid());
    }
    
    return 0;
}

基本函数

wait()函数

wait函数用于父进程等待子进程的终止,并获取子进程的退出状态。它的原型如下:

#include 
#include 
pid_t wait(int *status);
  • status 是一个指向整型变量的指针,用于存储子进程的退出状态信息。如果不关心子进程的退出状态,可以将 status 设置为 NULL

返回值:

  • 若调用成功,返回终止子进程的进程ID(PID);
  • 若调用失败,返回 -1,并设置 errno 错误码。

wait函数会挂起父进程的执行,直到有子进程退出。如果有多个子进程同时退出,父进程只会等待并回收一个子进程,其他子进程继续成为僵尸进程,需要再次调用 wait 来回收。

WIFEXITEDWEXITSTATUS 是用于处理子进程退出状态的宏,它们定义在 头文件中。

  1. WIFEXITED(status) 宏:

    • 用于检查子进程是否正常终止(即是否通过调用 exit() 或者返回 main 函数来退出)。
    • 参数 statuswait 函数获取到的子进程退出状态。
    • 如果子进程正常终止,宏返回非零值;否则,返回0。
  2. WEXITSTATUS(status) 宏:

    • 用于获取子进程的退出状态码
    • 参数 statuswait 函数获取到的子进程退出状态。
    • 如果子进程正常终止,宏返回子进程的退出状态码;否则,其行为是未定义的(undefined behavior)。

waitpid()函数

waitpid() 函数用于等待指定的子进程结束、销毁该子进程并获取其状态信息。它是一个系统调用函数,位于 头文件中。

函数原型如下:

#include 
#include 
pid_t waitpid(pid_t pid, int *status, int options);
  • pid 参数指定要等待的子进程的进程 ID。可以传入不同的值来指定不同的等待条件:
    • -1:等待任何子进程,类似于 wait() 函数。
    • 0:等待与当前调用进程的进程组 ID 相同的任何子进程。
    • 大于 0:等待指定进程 ID 的子进程。
  • status 参数是一个指向整型变量的指针,用于存储子进程的退出状态信息
  • options 参数是一个整数,用于指定额外的选项。常用的选项包括:
    • WNOHANG:以非阻塞方式等待子进程结束,即如果没有子进程结束,则立即返回
    • WUNTRACED等待停止的子进程,但不等待被跟踪的子进程。
    • 若该参数传入0表示以默认行为等待子进程结束,并在子进程结束后阻塞父进程,直到获取子进程的退出状态信息。
  • 返回值waitpid() 函数的返回值为子进程的进程 ID,或者特定的错误代码,如 -1 表示等待错误,0 表示使用了 WNOHANG 选项且没有已结束的子进程。
  1. WIFEXITED(status) 宏:

WIFEXITED(status) 宏用于判断子进程是否正常退出。它是一个宏函数,用于检查子进程的退出状态是否是正常终止的状态。如果子进程是通过调用 exit() 函数或返回 main() 函数中的整数值来正常结束的,则该宏将返回非零值(true)。否则,如果子进程不是正常终止的,例如由于信号导致的非正常退出,则该宏将返回零值(false)。

  1. WEXITSTATUS(status) 宏:

WEXITSTATUS(status) 宏用于获取子进程的退出状态码。它是一个宏函数,用于从 waitpid() 返回的状态信息中提取子进程的退出状态码。如果子进程是通过调用 exit(code) 函数或返回 main() 函数中的整数值 code 来正常结束的,则该宏将返回 code 的值。如果子进程不是正常终止的,调用该宏将产生未定义的结果。

举例说明:

#include 
#include 
#include 
#include 

int main() {
    pid_t pid1, pid2;
    
    pid1 = fork();
    if (pid1 < 0) {
        perror("fork error");
        exit(1);
    } else if (pid1 == 0) {
        // 子进程1执行的代码
        printf("Child process 1 (PID: %d) is running.\n", getpid());
        sleep(3);
        printf("Child process 1 (PID: %d) is exiting.\n", getpid());
        exit(123); // 子进程1以状态码 123 退出
    }
    
    pid2 = fork();
    if (pid2 < 0) {
        perror("fork error");
        exit(1);
    } else if (pid2 == 0) {
        // 子进程2执行的代码
        printf("Child process 2 (PID: %d) is running.\n", getpid());
        sleep(5);
        printf("Child process 2 (PID: %d) is exiting.\n", getpid());
        exit(456); // 子进程2以状态码 456 退出
    }
    
    // 父进程执行的代码
    printf("Parent process (PID: %d) is running.\n", getpid());
    printf("Parent process (PID: %d) is waiting for child processes to exit.\n", getpid());
    
    int status;
    pid_t child_pid;
    
    // 使用waitpid等待子进程1结束  0表示父进程阻塞在此处,并等待子进程结束。
    child_pid = waitpid(pid1, &status, 0);
    if (child_pid == -1) {
        perror("waitpid error");
        exit(1);
    } else {
        if (WIFEXITED(status)) {
            int exit_status = WEXITSTATUS(status);
            printf("Child process 1 (PID: %d) exited with status: %d\n", child_pid, exit_status);
        } else {
            printf("Child process 1 (PID: %d) terminated abnormally.\n", child_pid);
        }
    }
    
    // 使用waitpid等待子进程2结束
    child_pid = waitpid(pid2, &status, 0);
    if (child_pid == -1) {
        perror("waitpid error");
        exit(1);
    } else {
        if (WIFEXITED(status)) {
            int exit_status = WEXITSTATUS(status);
            printf("Child process 2 (PID: %d) exited with status: %d\n", child_pid, exit_status);
        } else {
            printf("Child process 2 (PID: %d) terminated abnormally.\n", child_pid);
        }
    }
    
    printf("Parent process (PID: %d) is exiting.\n", getpid());
    
    return 0;
}

fork()函数

fork 函数是在 POSIX 系统编程中常用的一个系统调用,用于创建一个新的进程(子进程)作为当前进程(父进程)的副本。

fork 调用后,父进程和子进程将同时继续执行从 fork 调用开始的位置,但是在父进程和子进程中的返回值是不同的,这可以用于区分父进程和子进程的执行路径

函数原型如下:

#include 
pid_t fork(void);

返回值:在父进程中,fork 返回新创建的子进程的进程 ID(PID),在子进程中,fork 返回 0,如果出现错误,则返回 -1。

fork执行后,子进程关闭服务器端套接字

在并发服务器中,当使用 fork 创建新的子进程时,父进程和子进程都会继续执行从 fork 调用处开始的代码。由于父子进程共享文件描述符(可以理解为每个文件描述符指向特定的套接字),包括服务器套接字,如果不关闭子进程中的服务器套接字,将导致以下问题:

  1. 端口占用:服务器套接字绑定到特定的端口,并开始监听客户端连接。如果子进程不关闭服务器套接字,那么子进程也会继续监听同一个端口。这将导致两个进程都在使用相同的端口,造成冲突和错误。
  2. 连接共享:如果子进程不关闭服务器套接字,它仍然可以接受客户端的连接请求。这将导致多个进程同时处理相同的客户端连接,导致并发问题和数据混乱

为了避免以上问题,通常在子进程中需要关闭服务器套接字。这样,子进程只负责处理客户端连接,而不再监听新的连接请求。父进程仍然继续监听,并处理新的客户端连接

以下是一个简单的示例,演示了在并发服务器中使用 fork 创建子进程,并在子进程中关闭服务器套接字:

注意:套接字并非进程所有,从严格意义上讲,套接字属于操作系统,只是进程拥有代表相应套接字的文件描述符

#include 
#include 
#include 
#include 
#include 

#define PORT 8080
#define MAX_CLIENTS 10

void handle_client(int client_socket) {
    // 处理客户端连接的逻辑
    // ...
    close(client_socket);
}

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_address, client_address;
    socklen_t client_address_len = sizeof(client_address);

    // 创建服务器套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket() error");
        exit(1);
    }

    // 配置服务器地址和端口
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(PORT);

    // 绑定服务器套接字到指定地址和端口
    if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        perror("bind() error");
        exit(1);
    }

    // 开始监听连接请求
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        perror("listen() error");
        exit(1);
    }

    while (1) {
        // 接受客户端连接  accept函数返回的时候,证明了创建了一个用于通信的套接字,而且client_socket是指向这个套接字的文件描述符。
        client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_address_len);
        if (client_socket == -1) {
            perror("accept() error");
            continue;
        }

        // 创建子进程处理客户端连接
        pid_t child_pid = fork();
        if (child_pid == -1) {
            perror("fork() error");
            close(client_socket);
            continue;
        } else if (child_pid == 0) {
            // 子进程逻辑,关闭服务器套接字并处理客户端连接
            close(server_socket);
            handle_client(client_socket);
            exit(0);
        } else {
            // 父进程逻辑,继续监听新的连接请求
            close(client_socket);
        }
    }

    close(server_socket);

    return 0;
}

shutdown函数

shutdown 函数用于关闭套接字的发送或接收功能,即禁止套接字进行数据传输。它可以用于优雅地关闭网络连接,通常在客户端和服务器之间的通信结束时使用。

函数原型为:

int shutdown(int sockfd, int how);

参数解析:

  • sockfd: 表示套接字的文件描述符,指定要关闭的套接字。
  • how表示关闭的方式:
    • SHUT_RD:关闭套接字的接收功能,即禁止接收数据
    • SHUT_WR:关闭套接字的发送功能,即禁止发送数据
    • SHUT_RDWR:同时关闭套接字的发送和接收功能,即禁止发送和接收数据

使用的情况:当服务端持续向客户端发送数据,客户端持续接收数据的时候,客户端不知道何时数据发送结束。那么此时,当服务端调用shutdown(sock,SHUT_WR)时候,会向客户端传递EOF表示文件传输结束,然后客户端知道了结束之后,可以向服务器端再发送相应的字符串,而此时服务端还未关闭接收数据的流,因此还可以接收到客户端发送的数据。

ps:当服务端发送EOF(End of File)时,客户端调用 read 函数会返回0。这表示服务端已经关闭了连接,没有更多的数据可供读取了。在这种情况下,客户端可以根据返回值为0来判断连接已经关闭,并进行相应的处理,如关闭套接字等。

send函数

send 函数用于在一个已连接的套接字上发送数据。它是在网络编程中常用的一个系统调用函数,用于将数据从一个套接字发送给另一个套接字,通常用于 TCP 连接中。

函数原型如下:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd: 已连接套接字的文件描述符,即发送数据的套接字。

  • buf: 指向要发送的数据的缓冲区的指针。

  • len: 要发送的数据的字节数。

  • flags: 选项标志,用于控制发送的行为,可以设置为 0 或一些其他标志,常见参数:

    • MSG_DONTWAIT调用I/O函数时不阻塞,用于使用非阻塞I/O
    • MSG_OOB,用于发送或接收带外数据(Out-of-Band data)。带外数据是一种在正常数据流之外传输的特殊数据,通常用于紧急通知或控制信息。(收到MSG_OOB紧急信息时,操作系统会产生SIGURG信号,并调用注册的信号处理函数)(通过MSG_OOB可选项传递数据时只返回1个字节,而且也不会加快数据传输速度)
    • MSG_PEEK 查看缓冲区中的数据而不移除它

返回值:

  • 成功:返回实际发送的字节数(可能小于 len)。
  • 失败:返回 -1,并设置 errno 来指示错误的原因。

send 函数将数据从 buf 指向的缓冲区复制到套接字的发送缓冲区,然后由操作系统负责将数据发送出去。如果发送的数据长度超过了发送缓冲区的大小,数据将被截断。如果发送缓冲区已满,数据可能会阻塞,直到缓冲区有足够的空间发送数据。

recv函数

recv 函数用于在一个已连接的套接字上接收数据。它是在网络编程中常用的一个系统调用函数,用于从一个套接字接收数据。

函数原型如下:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd: 已连接套接字的文件描述符,即接收数据的套接字
  • buf: 指向接收数据的缓冲区的指针。
  • len: 缓冲区的大小,表示期望接收的数据的最大字节数。
  • flags: 选项标志,用于控制接收的行为,可以设置为 0 或一些其他标志,如 MSG_WAITALLMSG_PEEK 等,通常设置为 0。
    • MSG_OOB,用于接收紧急的数据,当收到MSG_OOB紧急信息时,操作系统将产生SIGURG信号,并调用注册的信号处理函数。在使用 MSG_OOB 选项发送数据时,只能发送1字节的带外数据,即每次发送的数据长度限制为1字节
    • MSG_PEEK,用于验证输入缓冲中是否存在接收的数据。即使读取了输入缓冲的数据也不会删除
    • MSG_DONTWAIT调用I/O函数时不阻塞,用于使用非阻塞I/O

返回值:

  • 成功:返回实际接收的字节数。
  • 连接关闭:返回 0,表示对方已经关闭了连接。
  • 失败:返回 -1,并设置 errno 来指示错误的原因。

getpid函数

getpid 函数是一个系统调用,用于获取当前进程的进程ID(Process ID)。它是C标准库中的一个函数,通常位于 头文件中。

函数原型如下:

#include 
#include 
pid_t getpid(void);

返回值:

  • 成功:返回当前进程的进程ID。
  • 失败:返回 -1,并设置 errno 来指示错误的原因。

writev函数

writev 函数是一个用于写入数据到文件描述符的系统调用函数。它允许将多个散布(scatter)的数据块一次性写入到文件描述符中,减少了多次调用 write 函数的开销,提高了写入效率。

函数原型如下:

#include 
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

参数说明:

  • fd:表示文件描述符,即要写入数据的目标文件或套接字。
  • iov:是一个指向 iovec 结构体数组的指针,每个结构体指定了一个散布的数据块的起始地址和长度。
  • iovcnt:表示 iov 数组的元素个数,即要写入的散布数据块的个数。

返回值:

  • 成功:返回实际写入的字节数。
  • 失败:返回 -1,并设置 errno 来指示错误的原因。

struct iovec 结构体定义如下:

struct iovec {
    void *iov_base; // 散布数据块的起始地址
    size_t iov_len; // 散布数据块的长度
};

writev 函数的作用是将 iovec 数组中指定的散布数据块依次写入到文件描述符 fd 中。它可以用于一次性写入多个不连续的数据块,例如在网络编程中可以同时发送多个缓冲区的数据,或者在文件操作中将多个缓冲区的内容写入到文件中。

下面是一个简单的例子,使用 writev 函数将多个缓冲区的数据写入到文件中:

#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 10

int main() {
    int fd, iovcnt;
    struct iovec iov[3];
    ssize_t bytes_written;

    char buf1[] = "Hello, ";
    char buf2[] = "writev function!";
    char buf3[] = "\n";

    iov[0].iov_base = buf1;
    iov[0].iov_len = strlen(buf1);

    iov[1].iov_base = buf2;
    iov[1].iov_len = strlen(buf2);

    iov[2].iov_base = buf3;
    iov[2].iov_len = strlen(buf3);

    fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open() error");
        exit(EXIT_FAILURE);
    }
	// iovcnt表示结构体数组的数量
    iovcnt = sizeof(iov) / sizeof(struct iovec);
	
    bytes_written = writev(fd, iov, iovcnt);
    if (bytes_written == -1) {
        perror("writev() error");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("Total bytes written: %ld\n", bytes_written);

    close(fd);
    return 0;
}

readv函数

readv 函数是一个用于从文件描述符读取数据的系统调用函数。允许从文件描述符中一次性读取多个散布的数据块,减少了多次调用 read 函数的开销,提高了读取效率。

函数原型如下:

#include 
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

参数说明:

  • fd:表示文件描述符,即要读取数据的源文件或套接字。
  • iov:是一个指向 iovec 结构体数组的指针,每个结构体指定了一个散布的数据块的起始地址和长度。
  • iovcnt:表示 iov 数组的元素个数,即要读取的散布数据块的个数。

返回值:

  • 成功:返回实际读取的字节数。
  • 连接关闭:返回 0,表示对方已经关闭了连接。
  • 失败:返回 -1,并设置 errno 来指示错误的原因。

举例:从标准输入中读取数据

#include 
#include 
#include 
#include 

#define BUF_SIZE 10

int main() {
    int iovcnt;
    struct iovec iov[1];
    ssize_t bytes_read;

    char buf[BUF_SIZE];
/*
文件描述符0:标准输入(stdin)
文件描述符1:标准输出(stdout)
文件描述符2:标准错误(stderr)
*/
    iov[0].iov_base = buf;
    iov[0].iov_len = BUF_SIZE;

    iovcnt = sizeof(iov) / sizeof(struct iovec);

    bytes_read = readv(STDIN_FILENO, iov, iovcnt);
    if (bytes_read == -1) {
        perror("readv() error");
        exit(EXIT_FAILURE);
    }

    printf("Total bytes read: %ld\n", bytes_read);
    printf("Data read from stdin:\n");
    // %.*s:表示要打印一个字符串,其中 .* 是一个占位符,用于指定字符串的最大输出长度。
    printf("%.*s\n", (int)bytes_read, buf);

    return 0;
}

信号处理

signal函数

signal 函数用于设置信号处理函数,它的原型如下:

typedef void (*sighandler_t)(int);
// 一个指向参数为int,返回值为void* 的函数指针,重命名为:sighandler_t
sighandler_t signal(int signum, sighandler_t handler);

参数解析:

  • signum:表示要设置的信号的编号,可以是标准信号(如SIGINTSIGTERM等)或用户自定义信号。
  • handler:是一个函数指针,指向信号处理函数。它的类型为 sighandler_t,它是一个指向函数的指针,该函数接受一个 int 类型的参数(信号编号),并返回 void

sigaction结构体

用于设置和修改信号处理函数的行为。在 POSIX 系统编程中,它是对信号处理函数进行设置和更改的重要工具。该结构体在 头文件中定义。

结构体定义如下:这个结构体里面包含了要屏蔽的信号集sa_mask信号处理函数sa_handler

struct sigaction {
    void (*sa_handler)(int);         // 指定信号处理函数的地址
    void (*sa_sigaction)(int, siginfo_t *, void *); // 用于传递额外的信号信息的信号处理函数(可选)
    sigset_t sa_mask;                // 信号屏蔽字,在信号处理期间阻塞的信号集
    int sa_flags;                    // 用于指定信号处理函数的选项
    void (*sa_restorer)(void);       // 用于特殊处理的保留字段(不常用)
};
  • sigemptyset函数:用于初始化一个空的信号集(在初始化的时候,将sa_mask设置为空表示不屏蔽任何的信号)。

函数原型如下:

#include 
int sigemptyset(sigset_t *set);
  • sigaction函数:用于设置和修改信号处理函数的行为(用来关联信号和信号处理函数的)

函数原型如下:

#include 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:指定要设置或修改的信号的编号,如 SIGINTSIGTERM 等(子进程终止时将产生SIGCHLD信号)。可以通过在 头文件中查找信号名称对应的来获得信号编号。
  • act:指向 struct sigaction 结构体的指针,用于设置新的信号处理行为
  • oldact:指向 struct sigaction 结构体的指针,用于存储原先的信号处理行为

返回值:函数调用成功,返回值为 0。函数调用失败,返回值为 -1。

sigaction 函数允许我们设置信号的处理方式,具体有以下几种选择(act为sigaction结构体的变量):

  1. 捕捉信号并执行自定义的信号处理函数:设置 act->sa_handler 为自定义的信号处理函数的地址。
  2. 忽略信号:设置 act->sa_handlerSIG_IGN,表示忽略该信号。
  3. 恢复信号的默认处理方式:设置 act->sa_handlerSIG_DFL,表示恢复信号的默认处理方式。

多进程并发服务器

server端

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 8888
#define BUF_SIZE 1024

void error_handling(char *message);

int main() {
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_size;
    char buffer[BUF_SIZE];
    int str_len;

    // 创建套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1) {
        error_handling("socket() error");
    }

    // 初始化服务器地址结构
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(PORT);

    // 绑定套接字与服务器地址
    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        error_handling("bind() error");
    }

    // 监听套接字
    if (listen(serv_sock, 5) == -1) {
        error_handling("listen() error");
    }

    printf("Waiting for connections...\n");

    while (1) {
        clnt_addr_size = sizeof(clnt_addr);
        // 接受客户端连接请求
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
        if (clnt_sock == -1) {
            continue;
        }

        // 创建子进程处理客户端连接
        pid_t pid = fork();
        if (pid == -1) {
            error_handling("fork() error");
        } else if (pid == 0) { // 子进程
            close(serv_sock); // 子进程关闭监听套接字
            while ((str_len = read(clnt_sock, buffer, BUF_SIZE)) != 0) {
                write(clnt_sock, buffer, str_len); // 回传客户端数据
            }
            close(clnt_sock);
            printf("Client disconnected.\n");
            return 0;
        } else { // 父进程
            close(clnt_sock); // 父进程关闭与客户端的连接的套接字文件描述符
        }
    }

    close(serv_sock);

    return 0;
}

void error_handling(char *message) {
    perror(message);
    exit(EXIT_FAILURE);
}

客户端基于多进程实现I/O分割

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024

void error_handling(const char *message);

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("Usage: %s  \n", argv[0]);
        exit(1);
    }

    int sock;
    struct sockaddr_in serv_addr;
    pid_t pid;
    char message[BUF_SIZE];

    // 创建套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        error_handling("socket() error");
    }

    // 设置服务器地址结构
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    // 连接服务器
    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        error_handling("connect() error");
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        error_handling("fork() error");
    }

    if (pid == 0) {  // 子进程用于接收消息并输出
        while (1) {
            memset(message, 0, sizeof(message));
            int str_len = read(sock, message, BUF_SIZE - 1);
            if (str_len == 0) {
                break;
            }
            printf("Received message: %s\n", message);
        }
    } else {  // 父进程用于发送消息
        while (1) {
            memset(message, 0, sizeof(message));
            printf("Enter message (Q to quit): ");
            fgets(message, BUF_SIZE, stdin);
            if (strcmp(message, "Q\n") == 0 || strcmp(message, "q\n") == 0) {
                break;
            }
            write(sock, message, strlen(message));
        }
    }

    // 关闭套接字
    close(sock);
    return 0;
}

void error_handling(const char *message) {
    perror(message);
    exit(1);
}

进程的通信

IPC(Inter-Process Communication)是指进程间通信的技术和机制。

通过管道实现进程通信

管道并非属于进程的资源,而是和套接字一样,属于操作系统。

pipe 函数
用于创建一个管道,用于实现进程间的单向通信。它创建了一个字节流管道,提供了一个读取端和一个写入端。

函数原型如下:

int pipe(int pipefd[2]);

参数 pipefd 是一个整型数组,长度为 2,用于存储管道的文件描述符。pipefd[0] 用于读取数据,pipefd[1] 用于写入数据(相当于说:pipefd[0]用于指向管道的出口,pipefd[1]用于指向管道的入口)。

返回值是函数执行的状态,成功时返回 0,失败时返回 -1。

  • 管道是一个字节流管道,数据被视为无格式的字节流,没有消息边界。
  • 管道是半双工的,即只能在一个方向上传输数据。
  • 管道默认是阻塞的,即写入端和读取端的操作都会阻塞进程,直到操作完成。

从上述的特点可以看出:如果只创建一个管道,那么进程之间不可能同时进行数据的双向传递

通过管道实现进程的单向通信:

#include 
#include 
#include 

int main() {
    int pipefd[2]; // 用于存储管道的文件描述符
    pid_t childpid;
    char buffer[256];

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    childpid = fork();

    if (childpid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    // 子进程
    if (childpid == 0) {
        close(pipefd[1]); // 子进程关闭写端
        read(pipefd[0], buffer, sizeof(buffer));
        printf("子进程收到消息: %s\n", buffer);
        close(pipefd[0]);
    }
    // 父进程
    else {
        close(pipefd[0]); // 父进程关闭读端
        char message[] = "Hello, child!";
        write(pipefd[1], message, sizeof(message));
        close(pipefd[1]);
    }

    return 0;
}

实现进程双向通信任务

创建两个管道

#include 
#include 
#include 

#define BUFFER_SIZE 1024

int main() {
    int pipefd1[2]; // 管道1,用于父进程向子进程发送数据
    // 那么需要关闭父进程的读取端(pipefd1[0]),需要关闭子进程的发送端(pipefd1[1])
    int pipefd2[2]; // 管道2,用于子进程向父进程发送数据
    // 那么需要关闭父进程的发送端(pipefd2[1]),需要关闭子进程的读取端(pipefd2[0])
    pid_t child_pid;
    char buffer[BUFFER_SIZE];

    // 创建管道1
    if (pipe(pipefd1) == -1) {
        perror("pipe() error");
        exit(1);
    }

    // 创建管道2
    if (pipe(pipefd2) == -1) {
        perror("pipe() error");
        exit(1);
    }

    // 创建子进程
    child_pid = fork();
    if (child_pid == -1) {
        perror("fork() error");
        exit(1);
    } else if (child_pid == 0) {
        // 子进程逻辑:从管道1读取数据并向管道2写入数据
        close(pipefd1[1]); // 关闭管道1的写入端
        close(pipefd2[0]); // 关闭管道2的读取端

        // 从管道1读取数据
        ssize_t nbytes = read(pipefd1[0], buffer, BUFFER_SIZE);
        if (nbytes == -1) {
            perror("read() error");
            exit(1);
        }

        printf("Child process received from parent: %s", buffer);

        // 向管道2写入数据
        const char *message = "Hello, parent process!";
        nbytes = write(pipefd2[1], message, strlen(message));
        if (nbytes == -1) {
            perror("write() error");
            exit(1);
        }

        close(pipefd1[0]);
        close(pipefd2[1]);
        exit(0);
    } else {
        // 父进程逻辑:向管道1写入数据并从管道2读取数据
        close(pipefd1[0]); // 关闭管道1的读取端
        close(pipefd2[1]); // 关闭管道2的写入端

        // 向管道1写入数据
        const char *message = "Hello, child process!";
        ssize_t nbytes = write(pipefd1[1], message, strlen(message));
        if (nbytes == -1) {
            perror("write() error");
            exit(1);
        }

        // 从管道2读取数据
        nbytes = read(pipefd2[0], buffer, BUFFER_SIZE);
        if (nbytes == -1) {
            perror("read() error");
            exit(1);
        }

        printf("Parent process received from child: %s", buffer);

        close(pipefd1[1]);
        close(pipefd2[0]);
    }

    return 0;
}

I/O复用

并发服务器的第二种实现方法。

I/O复用是一种高效的编程技术,它允许一个进程可以同时监听多个I/O事件,而无需阻塞等待每个事件的完成。通过使用I/O复用,一个进程可以同时监视多个文件描述符(如套接字、管道、文件等),并在其中任何一个文件描述符就绪时立即对其进行处理,而不必逐个轮询每个文件描述符。

I/O复用的主要目的是提高程序的并发性和响应性。通常,在传统的阻塞I/O模型中,当一个文件描述符上的I/O操作没有完成时,进程会被阻塞,无法处理其他的事件。而通过使用I/O复用,进程可以同时处理多个事件,提高了程序的效率。

在Unix/Linux中,常用的I/O复用机制包括selectpollepoll等。这些机制允许一个进程同时监视多个文件描述符,并在有任何一个文件描述符就绪时返回,然后进程可以针对就绪的文件描述符进行读写等操作。

总的来说,I/O复用是一种利用操作系统提供的机制,实现多个I/O事件的同时监听和处理的编程技术,使得程序可以更高效地处理并发任务。它在网络编程、服务器编程等场景中广泛应用。

fd_set设置文件描述符

在使用 select 函数等IO复用机制时,需要对文件描述符集合进行初始化和设置,告诉系统要监视哪些文件描述符。为此,可以使用一些宏函数来操作文件描述符集合(fd_set)。

在 C 语言中,fd_set 是一个位图,用于表示一组文件描述符。以下是常用的对文件描述符集合进行操作的宏函数:

  • FD_ZERO(fd_set *fdset):将文件描述符集合初始化为空集,即清空所有位。

  • FD_SET(int fd, fd_set *fdset):将指定的文件描述符 fd 添加到文件描述符集合 fdset 中。

  • FD_CLR(int fd, fd_set *fdset):将指定的文件描述符 fd 从文件描述符集合 fdset 中移除,即清除对应的位。

  • FD_ISSET(int fd, fd_set *fdset):判断指定的文件描述符 fd 是否在文件描述符集合 fdset 中。如果在集合中,则返回非零值,否则返回0。

以下是一个简单的示例,演示如何使用这些宏函数来操作文件描述符集合:

#include 
#include 
#include 
#include 

int main() {
    fd_set readfds;
    int fd1 = 0; // 标准输入的文件描述符
    int fd2 = 3; // 假设另一个文件描述符为3

    FD_ZERO(&readfds); // 初始化文件描述符集合为空集

    FD_SET(fd1, &readfds); // 将标准输入的文件描述符添加到集合中
    FD_SET(fd2, &readfds); // 将另一个文件描述符添加到集合中

    // 使用 FD_ISSET 判断文件描述符是否在集合中
    if (FD_ISSET(fd1, &readfds)) {
        printf("fd1 is in the set.\n");
    }

    if (FD_ISSET(fd2, &readfds)) {
        printf("fd2 is in the set.\n");
    }

    return 0;
}

select函数

select 是一种IO复用机制,用于同时监视多个文件描述符的状态,以确定是否有可读、可写或异常事件就绪。它可以使单个线程同时处理多个I/O事件,避免了为每个事件创建单独的线程,从而提高了程序的性能和效率。

函数原型如下:

#include 
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:表示待监视的文件描述符的最大值加1(监视的文件描述符的数量),即文件描述符的范围为0到nfds-1。通常可以通过找出最大的文件描述符值然后加1来确定nfds
  • readfds:指向一个fd_set类型的集合,用于监视可读事件
  • writefds:指向一个fd_set类型的集合,用于监视可写事件
  • exceptfds:指向一个fd_set类型的集合,用于监视异常事件(如带外数据)。
  • timeout:指向一个timeval结构体的指针,用于设置select超时时间。如果为NULL,则select将一直阻塞直到有事件就绪。如果timeouttv_sectv_usec成员都为0,则select立即返回,也就是非阻塞模式。

select函数返回值:

  1. 如果返回值为 -1,表示 select 函数调用出错,此时通常可以通过查看 errno 来获取具体的错误信息,例如:errno 的值为 EINTR 表示 select 函数由于信号中断而返回。
  2. 如果返回值为 0,表示超时,即在指定的超时时间内没有任何文件描述符就绪。
  3. 如果返回值大于 0,表示有一些文件描述符就绪。此时可以使用 FD_ISSET 宏来检查哪些文件描述符处于就绪状态。

解释:调用select函数后,除发生变化的文件描述符(文件描述符变化是指监视的文件描述符中发生了相应的监视事件)对应位外,剩下的所有位将初始化为0?

在调用 select 前,我们手动将需要监视的文件描述符对应位设置为1。然后调用 select 后,只有那些发生状态变化的文件描述符对应位保留为1(即说明这些文字描述符有对应的事件),其余的文件描述符对应位被初始化为0。

fd_set 是一个位图,用于表示一组文件描述符。在调用select之前,需要使用宏函数对fd_set进行初始化和设置,以告诉select要监视哪些文件描述符

select 函数在返回时会修改fd_set(即会将文件描述符原来为1的所有位均变为0(这些文件描述符是没有发生状态变化的!)),以标记哪些文件描述符已经就绪。可以使用宏函数来检查和处理就绪的文件描述符。

以下是一个简单的示例,演示如何使用 select 函数来监视标准输入是否有数据到达

#include 
#include 
#include 
#include 

int main() {
    fd_set readfds;
    int maxfd;
    char buffer[1024];

    while (1) {
        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds); // 监视标准输入
        // 其他需要监视的文件描述符也可以添加到readfds中
		// STDIN_FILENO 是一个常量,它定义了标准输入(Standard Input)的文件描述符。
        // 标准输入的文件描述符是一个整数,用于在程序中标识标准输入。
        // 在 POSIX 系统中,标准输入的文件描述符通常是 0。
        maxfd = STDIN_FILENO + 1; // 文件描述符的最大值加1

        // 设置超时时间为5秒
        struct timeval timeout;
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
		
        int ready = select(maxfd, &readfds, NULL, NULL, &timeout);
        if (ready == -1) {
            perror("select() error");
            exit(1);
        } else if (ready == 0) {
            printf("Timeout: No data received.\n");
        } else {
            if (FD_ISSET(STDIN_FILENO, &readfds)) {
                // 标准输入有数据到达
                int nbytes = read(STDIN_FILENO, buffer, sizeof(buffer));
                if (nbytes == -1) {
                    perror("read() error");
                    exit(1);
                }
                printf("Received data from stdin: %s\n", buffer);
            }
            // 处理其他就绪的文件描述符
        }
    }

    return 0;
}

利用select实现I/O复用服务器端

#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_CLIENTS 5
#define BUFFER_SIZE 1024

void handleClientMessage(int client_socket, char *buffer)
{
    // 在这里处理客户端发送的消息,此处仅将消息原样回复给客户端
    write(client_socket, buffer, strlen(buffer));
}

int main(int argc, char *argv[])
{
    int server_socket, client_socket, max_fd, activity, i, valread;
    int client_sockets[MAX_CLIENTS] = {0}; // client_sockets数组用于存储所有的与客户端通信的套接字的文件描述符
    char buffer[BUFFER_SIZE];

    // 创建套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1)
    {
        perror("socket() error");
        exit(EXIT_FAILURE);
    }
    // 绑定ip地址和端口号
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(atoi(argv[1]));

    // 绑定套接字到指定地址和端口
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    {
        perror("bind() error");
        exit(EXIT_FAILURE);
    }

    // 监听连接请求
    if (listen(server_socket, 5) == -1)
    {
        perror("listen() error");
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %s...\n", argv[1]);

    fd_set readfds; // 文件描述符集合
    int addrlen = sizeof(server_addr);

    while (1)
    {
        FD_ZERO(&readfds);               // 初始化文件描述符集合
        FD_SET(server_socket, &readfds); // 添加服务器套接字到集合中
        max_fd = server_socket;

        // 添加客户端套接字到集合中
        for (i = 0; i < MAX_CLIENTS; i++)
        {
            client_socket = client_sockets[i];   //client_socket表示套接字文件描述符的编号
            if (client_socket > 0) // 表示当前这个与客户连接的套接字存在
            {
                FD_SET(client_socket, &readfds);
            }
            if (client_socket > max_fd)
            {
                max_fd = client_socket;
            }
        }
        // 设置超时时间为5秒
        struct timeval timeout;
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;

        // 使用 select 函数进行 I/O 复用  监视
        activity = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
        if (activity == -1)
        {
            perror("select() error");
            exit(EXIT_FAILURE);
        }
        else if (activity == 0)
        {
            // 超时
            printf("Timeout: No activity.\n");
            continue;
        }

        // 检查服务器套接字是否就绪,表示有新的连接请求
        if (FD_ISSET(server_socket, &readfds))
        {
            client_socket = accept(server_socket, (struct sockaddr *)&server_addr, (socklen_t *)&addrlen);
            if (client_socket == -1)
            {
                perror("accept() error");
                exit(EXIT_FAILURE);
            }

            // 将新的客户端套接字添加到客户端套接字数组中
            for (i = 0; i < MAX_CLIENTS; i++)
            {
                if (client_sockets[i] == 0)
                {
                    // 注册与客户端连接的套接字文件描述符
                    client_sockets[i] = client_socket;
                    break;
                }
            }
            printf("New client connected: socket fd is %d\n", client_socket);
        }

        // 检查客户端套接字是否就绪,表示有客户端发送消息
        for (i = 0; i < MAX_CLIENTS; i++)
        {
            client_socket = client_sockets[i];
            if (FD_ISSET(client_socket, &readfds))
            {
                valread = read(client_socket, buffer, BUFFER_SIZE);
                if (valread == 0)
                {
                    // 客户端断开连接
                    close(client_socket);
                    client_sockets[i] = 0;
                    printf("Client disconnected: socket fd is %d\n", client_socket);
                }
                else
                {
                    // 处理客户端发送的消息
                    buffer[valread] = '\0';
                    handleClientMessage(client_socket, buffer); // 与客户端通信的为client_socket,客户端发送的数据为buffer
                }
            }
        }
    }

    // 关闭服务器套接字
    close(server_socket);

    return 0;
}

epoll优于select的原因

epoll_create函数

epoll_create 函数用于创建一个 epoll 实例,并返回一个文件描述符(该文件描述符指向这个epoll实例),用于操作这个 epoll 实例。它的参数如下:

int epoll_create(int size);

size:表示 epoll 实例能够管理的文件描述符的最大数量,但在实际使用中并不会限制这个数量,因为内核会动态调整。该参数在新版本的 Linux 内核中已经被忽略,可以将其设置为任意非负数。

epoll_ctl函数

epoll_ctl 函数用于控制 epoll 实例,包括添加、修改或删除文件描述符等。其参数如下:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:epoll 实例的文件描述符,即由 epoll_create 返回的文件描述符。
  • op:表示操作类型,可以是以下值之一:
    • EPOLL_CTL_ADD:将文件描述符添加到 epoll 实例中,使其可以被监视。
    • EPOLL_CTL_MOD:修改已经添加到 epoll 实例中的文件描述符的事件类型,即修改对该文件描述符感兴趣的事件。
    • EPOLL_CTL_DEL:从 epoll 实例中删除文件描述符,停止对该文件描述符的监视。
  • fd:要操作的文件描述符,即要添加、修改或删除的文件描述符。
  • event:指向 epoll_event 结构体的指针,用于指定要监听的事件类型

将文件描述符fd注册到epoll实例epfd中,并且在需要xx(读取数据)的情况下产生相应的事件。

为什么epoll_ctl函数中fd参数已经指定了文件描述符,然后在event参数中还要指定文件描述符?这两个文件描述符不是一致的吗?对于 epoll_ctl 函数,的确会出现一个疑惑:为什么在 event 参数中还需要指定文件描述符,而不直接使用 fd 参数中的文件描述符?实际上,这两个文件描述符是一致的,只是为了增加代码的可读性和灵活性而设置了两个参数。

epoll_event结构体

struct epoll_event 是用于描述事件的结构体,它包含以下成员:

struct epoll_event {
    uint32_t events;  // 监听的事件类型,可以是 EPOLLIN、EPOLLOUT、EPOLLRDHUP、EPOLLPRI 等
    epoll_data_t data;  // 用户数据,用于保存和事件关联的额外信息
};
  • events:用于指定要监听的事件类型,可以是以下标志的组合:
    • EPOLLIN:表示对应的文件描述符可以读取数据(可读事件)。
    • EPOLLOUT:表示对应的文件描述符可以写入数据(可写事件)。
    • EPOLLRDHUP:表示对端关闭连接,或者发送了 FIN,或者半关闭连接(半关闭事件)。
    • EPOLLPRI:表示有紧急数据可读(带外数据可读事件)。
    • EPOLLERR:表示有错误发生(错误事件)。
    • EPOLLHUP:表示有挂起的事件(挂起事件)。
    • EPOLLET:使用边缘触发模式。
    • EPOLLONESHOT:只监听一次事件,事件触发后需要重新添加到 epoll 实例中。
  • data:用于保存和事件关联的额外信息,是一个联合体 epoll_data_t,它可以是文件描述符或指针。
typedef union epoll_data {
    void *ptr;  // 指针
    int fd;     // 文件描述符
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

epoll_wait函数

epoll_wait 函数用于等待事件的发生,并将发生的事件返回给用户。它的参数解析如下:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd:epoll 实例的文件描述符,即 epoll 描述符,用于指定要等待事件的 epoll 实例。
  • events:用于保存发生的事件的数组,是一个 struct epoll_event 结构体指针。
  • maxeventsevents 数组的大小,即最多可以保存多少个事件
  • timeout超时时间,以毫秒为单位。指定在等待事件发生时的超时时间,若超时时间为 0,则表示阻塞等待事件发生,直到有事件发生为止;若超时时间为 -1,则表示永久阻塞,直到有事件发生为止;若超时时间大于 0,则表示最多等待指定时间后立即返回,无论是否有事件发生。

举例:

#include 
#include 

#define MAX_EVENTS 10

int main() {
    int epoll_fd = epoll_create(1); // 创建一个 epoll 实例
    struct epoll_event events[MAX_EVENTS]; // 保存发生的事件的数组(结构体数组)

    // 设置关心的事件类型为 EPOLLIN (可读事件)
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = STDIN_FILENO; // 标准输入文件描述符

    // 将标准输入文件描述符添加到 epoll 实例中
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &event);

    printf("等待标准输入事件发生...\n");

    // 等待事件发生
    int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    if (num_events == -1) {
        perror("epoll_wait");
        return 1;
    }

    // 遍历发生的事件
    for (int i = 0; i < num_events; i++) {
        if (events[i].data.fd == STDIN_FILENO) {
            printf("标准输入事件发生!\n");
        }
    }

    return 0;
}

基于epoll的回声服务器端实现

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024
#define EPOLL_SIZE 50

void error_handling(const char *message);

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s \n", argv[0]);
        return 1;
    }

    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_size;
    char buffer[BUF_SIZE];

    // 创建监听套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1) {
        error_handling("socket() error");
    }

    // 设置服务器地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    // 绑定地址和端口
    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        error_handling("bind() error");
    }

    // 监听套接字
    if (listen(serv_sock, 5) == -1) {
        error_handling("listen() error");
    }

    // 创建 epoll 实例
    int epoll_fd = epoll_create(EPOLL_SIZE);
    if (epoll_fd == -1) {
        error_handling("epoll_create() error");
    }

    struct epoll_event events[EPOLL_SIZE];  // 用于存储发生的事件的结构体数组
    struct epoll_event event;               // 用于注册的事件

    // 将监听套接字加入到 epoll 实例中
    event.events = EPOLLIN;      // 读取数据事件
    event.data.fd = serv_sock;   // 绑定服务端的套接字
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, serv_sock, &event) == -1) {
        error_handling("epoll_ctl() error");
    }

    printf("回声服务器启动,等待客户端连接...\n");

    while (1) {
        int event_cnt = epoll_wait(epoll_fd, events, EPOLL_SIZE, -1);  // 等待事件发生
        if (event_cnt == -1) {
            error_handling("epoll_wait() error");
        }

        for (int i = 0; i < event_cnt; i++) {
            int sockfd = events[i].data.fd;

            // 新的客户端连接
            if (sockfd == serv_sock) {
                clnt_addr_size = sizeof(clnt_addr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
                if (clnt_sock == -1) {
                    error_handling("accept() error");
                }

                printf("客户端连接:%s:%d\n", inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));

                event.events = EPOLLIN | EPOLLET; // 边缘触发模式
                event.data.fd = clnt_sock;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, clnt_sock, &event) == -1) {
                    error_handling("epoll_ctl() error");
                }
            }
            // 客户端消息处理
            else {
                memset(buffer, 0, sizeof(buffer));
                int recv_len = recv(sockfd, buffer, BUF_SIZE, 0);
                if (recv_len == 0) {
                    // 客户端关闭连接
                    printf("客户端关闭:%d\n", sockfd);
                    epoll_ctl(epoll_fd, EPOLL _CTL_DEL, sockfd, NULL);
                    close(sockfd);
                }
                else if (recv_len < 0) {
                    // 接收数据出错
                    error_handling("recv() error");
                }
                else {
                    // 回声处理
                    send(sockfd, buffer, recv_len, 0);
                }
            }
        }
    }

    close(serv_sock);
    close(epoll_fd);

    return 0;
}

void error_handling(const char *message) {
    perror(message);
    exit(1);
}

fcntl函数

fcntl 函数是用于对文件描述符进行操作的系统调用,它可以用来改变文件描述符的属性。其函数原型如下:

#include 
int fcntl(int fd, int cmd, ... /* arg */ );

参数解析:

  • fd: 文件描述符,表示要操作的文件描述符。
  • cmd: 表示要执行的命令,可以是以下之一:
    • F_DUPFD: 复制文件描述符。后面跟一个整数参数,表示从该整数值开始搜索未用的文件描述符,并返回复制的文件描述符。
    • F_GETFD: 获取文件描述符的标志。
    • F_SETFD: 设置文件描述符的标志。后面跟一个整数参数,表示要设置的标志。
    • F_GETFL: 获取文件状态标志。
    • F_SETFL: 设置文件状态标志。后面跟一个整数参数,表示要设置的标志
    • 其他命令,用于文件锁、文件性能等操作。
  • arg: 根据不同的命令,可能需要传递额外的参数。

可以通过这个函数,设置套接字文件描述符的状态为非阻塞状态,这样在调用read & write函数的时候,不会阻塞。

多线程

编译多线程的程序的时候,需要加上 -lpthread选项

pthread_create函数

pthread_create是用于创建线程的函数,其参数如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • thread:指向pthread_t类型的指针,用于存储新创建线程的ID。在成功创建线程后,新线程的ID将存储在这个指针指向的位置
  • attr:指向pthread_attr_t类型的指针,用于指定新线程的属性。可以通过这个参数来设置线程的属性,例如栈大小、调度策略等。如果传递NULL,将使用默认的线程属性。
  • start_routine:是一个函数指针,指向新线程要执行的函数。这个函数应该是一个返回void *类型,接受一个void *类型参数的函数。新线程将从这个函数开始执行。
  • arg:是一个void *类型的参数,用于传递给start_routine函数。这个参数可以用来向新线程传递数据

返回值:

  • 成功创建线程: 如果pthread_create成功创建了一个新线程,它将返回0。这表示新线程已经成功启动,并且在thread参数指向的位置存储了新线程的ID。

  • 创建线程失败: 如果pthread_create创建线程失败,它会返回一个非零的错误码,用于指示失败的原因。常见的错误码包括:

    • EAGAIN:资源不足,无法创建新线程。

    • EINVAL:传递给pthread_create的参数无效,例如线程属性参数或线程函数指针为空。

    • EPERM:没有权限创建新线程。

pthread_join函数

pthread_join函数用于等待指定的线程终止,并且将线程的返回值存储在一个指针指向的位置。该函数的参数如下:

int pthread_join(pthread_t thread, void **retval);
  • thread:要等待的线程的ID,通常是由pthread_create函数返回的线程ID。

  • retval:指向指针的指针,用于存储线程的返回值。线程函数可以通过return语句返回一个值,该值将被传递给等待线程的retval指向的位置。如果不关心线程的返回值,可以将retval设置为NULL

pthread_join函数会阻塞当前线程,直到指定的线程终止。一旦目标线程终止,pthread_join将会返回,然后可以通过retval指向的位置获取线程的返回值。

举例:假设我们有一个简单的线程函数,用于计算从1到N的和,并且我们想在主线程中创建一个新线程来执行这个函数,并获取计算结果。示例代码如下:

#include 
#include 

void *calculateSum(void *arg) {
    int N = *(int *)arg;
    int sum = 0;
    
    for (int i = 1; i <= N; i++) {
        sum += i;
    }
    
    // 通过返回指针传递计算结果
    int *result = malloc(sizeof(int));
    *result = sum;
    return (void *)result;
}

int main() {
    pthread_t thread;
    int N = 100;
    void *result;
    
    // 创建新线程,并传递N作为参数
    int result_create = pthread_create(&thread, NULL, calculateSum, &N);
    if (result_create != 0) {
        fprintf(stderr, "Error creating thread: %d\n", result_create);
        return -1;
    }
    
    // 等待新线程执行结束,并获取计算结果
    int result_join = pthread_join(thread, &result);
    if (result_join != 0) {
        fprintf(stderr, "Error joining thread: %d\n", result_join);
        return -1;
    }
    
    // 获取计算结果并打印
    int sum = *(int *)result;
    printf("Sum from 1 to %d is: %d\n", N, sum);
    
    // 释放存储结果的内存
    free(result);
    
    return 0;
}

线程安全函数与非线程安全函数

线程安全函数和非线程安全函数是涉及多线程编程时的两个重要概念。它们与并发执行的多个线程同时访问相同的共享数据有关

  1. 线程安全函数(Thread-Safe Functions): 线程安全函数是指在多线程环境下可以安全地被多个线程同时调用,而不会产生竞争条件或导致不确定的结果。这些函数内部通常使用了同步机制(如互斥锁、信号量等)来保护共享数据,确保多线程访问时的正确性。

线程安全函数的设计目标是保证在多线程环境下,不管有多少个线程同时调用这个函数,它都能正确地完成任务而不会造成数据混乱或不一致。

  1. 非线程安全函数(Non-Thread-Safe Functions): 非线程安全函数是指在多线程环境下不能被多个线程同时调用,否则可能会导致竞争条件、数据不一致或程序崩溃等问题。这些函数在设计时并没有考虑多线程并发访问的情况,没有采取措施保护共享数据。

当多个线程同时调用非线程安全函数并访问共享数据时,很可能会出现数据错乱、覆盖或不可预期的行为,从而导致程序运行不稳定。

对于非线程安全函数,平台同时提供了具有相同功能的线程安全函数。eg:gethostbyname函数就不是线程安全函数,但是gethostbyname_r是线程安全函数。因此我们可以选择替代。然后若我们想让程序自动将gethostbyname函数替换成gethostbyname_r函数,可以声明头文件前定义_REENTRANT宏。

多线程同时访问内存空间的问题

任何内存空间,只要被同时访问,都可能发生问题。

为了解决问题,应该要保证:当某一个线程访问变量时,应该阻止其他线程访问,直到该线程完成访问。这就是线程同步的问题。

临界区:函数内同时运行多个线程时引起问题的多条语句构成的代码块。

#include 
#include 

int sharedVariable = 0;

void *incrementVariable(void *arg) {
    for (int i = 0; i < 100000; i++) {
        sharedVariable++;  // 临界区代码!
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    
    // 创建两个线程,同时对共享变量进行增加操作
    pthread_create(&thread1, NULL, incrementVariable, NULL);
    pthread_create(&thread2, NULL, incrementVariable, NULL);
    
    // 等待两个线程执行结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    
    // 打印共享变量的值
    printf("Final value of sharedVariable: %d\n", sharedVariable);
    
    return 0;
}

线程同步

互斥量

pthread_mutex_init函数

函数pthread_mutex_init用于初始化一个互斥锁(mutex),使其成为可用状态,以便用于多线程环境中的同步操作。该函数的参数如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex:指向pthread_mutex_t类型的指针,用于指定要初始化的互斥锁对象。pthread_mutex_t是一个结构体,用于表示互斥锁的属性和状态。

  • attr:指向pthread_mutexattr_t类型的指针,用于指定互斥锁的属性。如果传递NULL,则使用默认的互斥锁属性。

pthread_mutex_destroy函数

函数pthread_mutex_destroy用于销毁一个互斥锁(mutex),在互斥锁不再需要使用时调用该函数进行资源的释放。该函数的参数如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

mutex:指向pthread_mutex_t类型的指针,用于指定要销毁的互斥锁对象pthread_mutex_t是一个结构体,表示互斥锁的属性和状态。

pthread_mutex_lock函数

函数pthread_mutex_lock用于在多线程环境中获取一个互斥锁(mutex),以便进入临界区,保护共享资源的访问。该函数的参数如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

mutex:指向pthread_mutex_t类型的指针,用于指定要获取的互斥锁对象。pthread_mutex_t是一个结构体,表示互斥锁的属性和状态。

pthread_mutex_unlock函数

函数pthread_mutex_unlock用于在多线程环境中释放一个互斥锁(mutex),以便离开临界区,允许其他线程获取该锁并进入临界区。该函数的参数如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

mutex:指向pthread_mutex_t类型的指针,用于指定要释放的互斥锁对象。pthread_mutex_t是一个结构体,表示互斥锁的属性和状态。

pthread_mutex_lock(&mutex);
// 临界区开始
// ...
// 临界区结束
pthread_mutex_unlock(&mutex);
// 如果没有调用unlock释放锁,那么其他线程将会一直阻塞在lock函数中,这种情况称为死锁。

利用互斥锁解决多线程同时访问内存空间的问题

#include 
#include 

int sharedVariable = 0;
pthread_mutex_t mutex;  // 声明互斥锁对象

void *incrementVariable(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);  // 获取互斥锁,在临界区之前加锁
        sharedVariable++;  // 临界区代码!
        pthread_mutex_unlock(&mutex);  // 在临界区之后释放锁
    }
    return NULL;
}

int main() {
    // 初始化互斥锁
    int result = pthread_mutex_init(&mutex, NULL);
    if (result != 0) {
        perror("pthread_mutex_init");
        return -1;
    }

    pthread_t thread1, thread2;
    
    // 创建两个线程,同时对共享变量进行增加操作
    pthread_create(&thread1, NULL, incrementVariable, NULL);
    pthread_create(&thread2, NULL, incrementVariable, NULL);
    
    // 等待两个线程执行结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    
    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);
    
    // 打印共享变量的值
    printf("Final value of sharedVariable: %d\n", sharedVariable);
    
    return 0;
}
信号量

sem_init函数

sem_init函数用于初始化一个信号量(Semaphore),使其成为可用状态,以便用于多线程或多进程环境中的同步和互斥操作。该函数的参数如下:

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:指向sem_t类型的指针,用于指定要初始化的信号量对象。sem_t是一个不透明的数据类型,用于表示信号量。

  • pshared:表示信号量的共享模式。如果该值为0,则表示信号量是线程间共享的,只能用于同一进程内的不同线程之间的同步。如果该值为非0(通常是1),则表示信号量是进程间共享的,可以用于不同进程之间的同步。

  • value:表示信号量的初始值。该值必须是一个非负整数,用于指定信号量的初始资源数量。

sem_destroy函数

函数sem_destroy用于销毁一个信号量(Semaphore),在信号量不再需要使用时调用该函数进行资源的释放。该函数的参数如下:

int sem_destroy(sem_t *sem);
  • sem:指向sem_t类型的指针,用于指定要销毁的信号量对象。sem_t是一个不透明的数据类型,用于表示信号量。

sem_destroy函数用于销毁一个先前通过sem_init初始化的信号量对象。一旦信号量不再使用,可以调用该函数进行清理,以释放相关的资源。

sem_post函数

函数sem_post用于增加(释放)一个信号量的值,从而使其他等待该信号量的线程或进程有机会获取该信号量并继续执行。该函数的参数如下:

int sem_post(sem_t *sem);
  • sem:指向sem_t类型的指针,用于指定要增加(释放)的信号量对象。sem_t是一个不透明的数据类型,用于表示信号量。

sem_post函数用于增加(释放)一个先前通过sem_init初始化的信号量对象的值。如果在某个时刻,有其他线程或进程正在等待该信号量,那么sem_post的调用将会使其中的一个线程或进程被唤醒,并有机会获取该信号量继续执行。

需要注意的是,sem_post不会阻塞当前线程或进程,它只是增加信号量的值。通常在释放共享资源或完成某项任务时使用sem_post来通知其他线程或进程可以继续进行。

sem_wait函数

函数sem_wait用于尝试获取(减少)一个信号量的值,如果信号量的值大于0,则减少其值并继续执行;如果信号量的值为0,则当前线程或进程将被阻塞,直到信号量的值大于0为止。该函数的参数如下:

int sem_wait(sem_t *sem);
  • sem:指向sem_t类型的指针,用于指定要获取(减少)的信号量对象。sem_t是一个不透明的数据类型,用于表示信号量。

sem_wait函数用于减少一个先前通过sem_init初始化的信号量对象的值。如果信号量的值大于0,则减少信号量的值并继续执行后续代码。如果信号量的值为0,则当前线程或进程将被阻塞,直到有其他线程或进程调用sem_post增加信号量的值,使得当前线程或进程可以继续执行。

需要注意的是,sem_wait是一个阻塞函数,即如果信号量的值为0,当前线程或进程会被阻塞,直到获取到信号量为止。因此,sem_wait通常用于等待某个资源的释放或完成某项任务的信号。

sem_wait(&sem);   // 信号量变为0
// 临界区的开始
// .....
// 临界区的结束
sem_post(&sem);

线程销毁

  1. pthread_join函数:调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。
  2. pthread_detach函数:
int pthread_detach(pthread_t thread);

thread:要设置为分离状态的线程标识符(Thread ID),即线程创建成功后,由pthread_create函数返回的线程标识符。

调用上述函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。

多线程服务器端

server端的代码

#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int clients[MAX_CLIENTS];   // 存储套接字文件描述符的数组
int num_clients = 0;       // 表示连接的客户端的数量
pthread_mutex_t clients_mutex;  // 锁对象

void *handle_client(void *arg) {   // 子线程开始执行的函数
    int client_socket = *((int *)arg);
    char buffer[BUFFER_SIZE];

    while (1) {
        int bytes_received = recv(client_socket, buffer, sizeof(buffer), 0);
        if (bytes_received <= 0) {
            // Client disconnected or error occurred
            close(client_socket);

            // Remove the client from the list of connected clients
            pthread_mutex_lock(&clients_mutex);
            for (int i = 0; i < num_clients; i++) {
                if (clients[i] == client_socket) {
                    clients[i] = clients[num_clients - 1];  // 如果说数组中的某一个位置的套接字文件描述符关闭了。那么就将最后一个套接字文件描述符复制到这个位置上,同时将连接的数量减少1
                    num_clients--;
                    break;
                }
            }
            pthread_mutex_unlock(&clients_mutex);

            return NULL;
        }

        // Broadcast the message to all other connected clients
        pthread_mutex_lock(&clients_mutex);
        for (int i = 0; i < num_clients; i++) {
            if (clients[i] != client_socket) {
                send(clients[i], buffer, bytes_received, 0); // 向其他的客户端发送数据
            }
        }
        pthread_mutex_unlock(&clients_mutex);
    }

    return NULL;
}

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // Initialize the mutex for clients list  初始化锁
    pthread_mutex_init(&clients_mutex, NULL);

    // Create the server socket
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // Configure the server address
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8888);

    // Bind the server socket to the specified address and port
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // Listen for incoming connections
    if (listen(server_socket, 5) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port 8888...\n");

    while (1) {
        // Accept a new connection from a client 接收一个客户端的请求
        // client_socket用来与客户端进行通信的套接字文件描述符
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
        if (client_socket < 0) {
            perror("accept");
            continue;
        }

        // Add the new client to the list of connected clients
        // 因为需要操作到套接字的数组,因此主线程需要对这个操作进行上锁
        pthread_mutex_lock(&clients_mutex);
        if (num_clients < MAX_CLIENTS) {
            clients[num_clients++] = client_socket;
            pthread_t thread;
            pthread_create(&thread, NULL, handle_client, &client_socket);  // 创建子线程,执行相应的函数。
            pthread_detach(thread);  // 子进程结束的时候,自动销毁、
        } else {
            close(client_socket);
        }
        pthread_mutex_unlock(&clients_mutex); // 主线程释放锁,保证子线程操作
    }

    // Close the server socket and clean up
    close(server_socket);
    pthread_mutex_destroy(&clients_mutex);  // 销毁互斥锁

    return 0;
}

标准I/O函数与系统函数

c语言标准I/O函数

fopen函数

fopen 函数用于打开文件,并返回一个指向 FILE 结构体的指针,以便后续对文件进行读写操作。以下是 fopen 函数的参数解析:

FILE *fopen(const char *filename, const char *mode);
  • filename: 是要打开的文件名,可以是相对路径或绝对路径。可以是字符串常量或字符数组。
  • mode: 是打开文件的模式,是一个字符串,用来指定文件的打开方式。常用的模式如下:
    • "r": 只读方式打开文件。文件必须存在,否则打开失败。
    • "w": 写入方式打开文件。如果文件存在,内容将被清空,如果文件不存在,则创建新文件。
    • "a": 追加方式打开文件。如果文件存在,数据将被写入到文件末尾,如果文件不存在,则创建新文件。
    • "rb": 二进制只读方式打开文件。与 "r" 相似,但以二进制模式打开文件。
    • "wb": 二进制写入方式打开文件。与 "w" 相似,但以二进制模式打开文件。
    • "ab": 二进制追加方式打开文件。与 "a" 相似,但以二进制模式打开文件。

fgetc函数

fgetc 函数用于从指定的文件中读取一个字符,并返回该字符的ASCII 值。以下是 fgetc 函数的参数解析:

int fgetc(FILE *stream);
  • stream: 是一个指向 FILE 结构体的指针,用于指定要读取字符的文件

返回值:

  • 成功读取一个字符时,返回该字符的 ASCII 值(范围为 0 到 255)。
  • 到达文件末尾时,返回 EOF(End of File),其值通常为 -1。

举例:

#include 

int main() {
    FILE *file;
    int ch;

    // 打开文件 example.txt 以只读方式
    file = fopen("example.txt", "r");

    // 检查文件是否打开成功
    if (file == NULL) {
        printf("无法打开文件。\n");
        return 1;
    }

    // 读取文件内容并输出到屏幕
    while ((ch = fgetc(file)) != EOF) {
        putchar(ch);
    }

    // 关闭文件
    fclose(file);

    return 0;
}

fgets函数

函数fgets是C标准库中用于从文件流中读取一行字符串的函数,其原型如下:

char *fgets(char *str, int n, FILE *stream);
  • str:指向一个字符数组(字符串缓冲区)的指针,用于存储读取的字符串。

  • n:要读取的最大字符数(包括终止符\0)。即str指向的字符数组最多能够存储n-1个字符的内容,最后一个字符用于存储字符串终止符\0

  • stream:要读取的文件流的指针,通常是一个指向已打开的文件的指针。

fputs函数

fputs 函数用于将一个字符串写入到指定的文件中。以下是 fputs 函数的参数解析:

int fputs(const char *str, FILE *stream);
  • str: 是一个指向以 null 结尾的字符串的指针,表示要写入文件的字符串。
  • stream: 是一个指向 FILE 结构体的指针,用于指定要写入字符串的文件

返回值:

  • 成功写入字符串时,返回非负值。
  • 发生错误时,返回 EOF(End of File),其值通常为 -1。

举例:

#include 

int main() {
    FILE *file;
    const char *str = "Hello, World!\n";

    // 打开文件 output.txt 以写入方式
    file = fopen("output.txt", "w");

    // 检查文件是否打开成功
    if (file == NULL) {
        printf("无法打开文件。\n");
        return 1;
    }

    // 将字符串写入文件
    if (fputs(str, file) == EOF) {
        printf("写入文件时发生错误。\n");
        return 1;
    }

    // 关闭文件
    fclose(file);

    return 0;
} 

fflush函数

fflush 函数用于刷新指定的流。它是标准C库中的一个函数,位于 头文件中。fflush 函数的参数解析如下:

int fflush(FILE *stream);
  • stream:是一个指向 FILE 结构体的指针,表示要刷新的流。如果传入 NULL,则会刷新所有的标准I/O流。

返回值:

  • 成功时,返回0。
  • 失败时,返回 EOF,并设置 errno 变量来指示错误。

举例:

#include 

int main() {
    FILE *file = fopen("example.txt", "w");

    if (file == NULL) {
        perror("无法打开文件");
        return 1;
    }

    fputs("Hello, World!", file);

    // 刷新流缓冲区 使用 fflush 函数刷新文件流缓冲区,确保数据被写入文件
    int result = fflush(file);

    if (result == EOF) {
        perror("刷新文件流失败");
        fclose(file);
        return 1;
    }

    // 关闭文件
    fclose(file);

    return 0;
}

file结构体指针与文件描述符的转换

fdopen函数

fdopen 函数用于将一个文件描述符(整数值)转换为对应的 FILE* 流。这样可以在标准I/O函数中使用这个流来读取或写入数据。以下是 fdopen 函数的参数解析:

FILE *fdopen(int fd, const char *mode);
  • fd:是一个文件描述符(file descriptor),即整数值,表示要转换的文件描述符。
  • mode:是一个以字符串形式表示的文件访问模式,类似于 fopen 函数的模式字符串。常用的模式有 "r"(只读模式)、"w"(只写模式)、"a"(追加模式)等。

返回值:

  • 成功时,返回指向对应 FILE 结构体的指针。
  • 失败时,返回 NULL

举例说明:

假设我们已经有一个文件描述符,我们想要使用标准I/O函数来读取或写入数据,可以使用 fdopen 函数将文件描述符转换为 FILE* 流,然后使用标准I/O函数操作该流。下面是一个示例:

#include 
#include 
#include 

int main() {
    int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);

    if (fd == -1) {
        perror("无法打开文件");
        return 1;
    }

    // 使用 fdopen 将文件描述符 fd 转换为 FILE* 流
    FILE *file = fdopen(fd, "w");

    if (file == NULL) {
        perror("无法转换文件描述符为 FILE* 流");
        close(fd);
        return 1;
    }

    // 使用标准 I/O 函数写入数据
    fprintf(file, "这是一个示例文件。\n");

    // 关闭 FILE* 流
    fclose(file);

    return 0;
}

fileno函数

fileno 函数用于获取给定 FILE* 流对应的文件描述符(file descriptor)。它是标准C库中的一个函数,位于 头文件中。fileno 函数的参数解析如下:

int fileno(FILE *stream);
  • stream:是一个指向 FILE 结构体的指针,表示要获取文件描述符的流。

返回值:

  • 成功时,返回与给定流对应的文件描述符(整数值)。
  • 失败时,返回 -1,并设置 errno 变量来指示错误。

举例说明:

#include 

int main() {
    FILE *file = fopen("example.txt", "r");

    if (file == NULL) {
        perror("无法打开文件");
        return 1;
    }

    // 使用 fileno 函数获取 FILE* 流对应的文件描述符
    int fd = fileno(file);

    if (fd == -1) {
        perror("获取文件描述符失败");
        fclose(file);
        return 1;
    }

    printf("文件描述符:%d\n", fd);

    // 使用标准 I/O 函数读取数据
    char buffer[100];
    fgets(buffer, sizeof(buffer), file);
    printf("文件内容:%s\n", buffer);

    // 关闭 FILE* 流
    fclose(file);

    return 0;
}

基于套接字的标准I/O函数使用

基于套接字的标准I/O函数使用的例子:server端

#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024

int main() {
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_len;
    char buffer[BUF_SIZE];

    // 创建套接字
    serv_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1) {
        perror("socket() 失败");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind() 失败");
        close(serv_sock);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(serv_sock, 5) == -1) {
        perror("listen() 失败");
        close(serv_sock);
        exit(EXIT_FAILURE);
    }

    clnt_addr_len = sizeof(clnt_addr);

    while (1) {
        // 接受连接
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_len);
        if (clnt_sock == -1) {
            perror("accept() 失败");
            close(serv_sock);
            exit(EXIT_FAILURE);
        }

        // 使用 fdopen 创建套接字的标准I/O流
        // r+ 模式是以读写方式打开文件的一种模式
        FILE *clnt_stream = fdopen(clnt_sock, "r+");

        if (clnt_stream == NULL) {
            perror("fdopen() 失败");
            close(clnt_sock);
            continue;
        }

        // 从客户端读取数据,并回送相同的数据
        while (fgets(buffer, BUF_SIZE, clnt_stream) != NULL) {
            fputs(buffer, clnt_stream);
            fflush(clnt_stream);  // fflush刷新缓冲区,是为了保证让数据立刻发送到客户端
        }

        // 关闭套接字的标准I/O流
        fclose(clnt_stream);
        close(clnt_sock);
    }

    // 关闭服务器套接字
    close(serv_sock);
    return 0;
}

文件描述符的复制

dup函数

dup 函数用于复制文件描述符,创建一个新的文件描述符,使其指向与原始文件描述符相同的文件或套接字。

函数原型:

int dup(int oldfd);

参数 oldfd:要复制的旧文件描述符。

返回值:若成功,返回新的文件描述符(大于等于0);若出错,返回-1。

举例说明:

#include 
#include 
#include 

int main() {
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    int new_fd = dup(fd);
    if (new_fd == -1) {
        perror("Error duplicating file descriptor");
        close(fd);
        return 1;
    }

    printf("Original file descriptor: %d\n", fd);
    printf("Duplicated file descriptor: %d\n", new_fd);

    close(fd);
    close(new_fd);
    return 0;
}
dup2函数

dup2 函数与 dup 函数类似,也是用于复制文件描述符,但 dup2 允许指定新的文件描述符的值。

函数原型:

int dup2(int oldfd, int newfd);

参数 oldfd:要复制的旧文件描述符。 参数 newfd:新的文件描述符的值。

返回值:若成功,返回新的文件描述符(大于等于0);若出错,返回-1。

举例说明:

#include 
#include 
#include 

int main() {
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }

    int new_fd = dup2(fd, 10);
    if (new_fd == -1) {
        perror("Error duplicating file descriptor");
        close(fd);
        return 1;
    }

    printf("Original file descriptor: %d\n", fd);
    printf("Duplicated file descriptor: %d\n", new_fd);

    close(fd);
    close(new_fd);
    return 0;
}

复制文件描述符后“流”的分离

使用方法很简单,直接在 “基于套接字的标准I/O函数使用”里面进行修改即可!

#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024

int main() {
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_len;
    char buffer[BUF_SIZE];

    // 创建套接字
    serv_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1) {
        perror("socket() 失败");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind() 失败");
        close(serv_sock);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(serv_sock, 5) == -1) {
        perror("listen() 失败");
        close(serv_sock);
        exit(EXIT_FAILURE);
    }

    clnt_addr_len = sizeof(clnt_addr);

    while (1) {
        // 接受连接
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_len);
        if (clnt_sock == -1) {
            perror("accept() 失败");
            close(serv_sock);
            exit(EXIT_FAILURE);
        }

        // 使用 fdopen 创建套接字的标准I/O流
        // r+ 模式是以读写方式打开文件的一种模式
        FILE *readfp = fdopen(clnt_sock, "r");
		FILE *writefp = fdopen(dup(clnt_sock),"w");
        if (readpf == NULL || writefp == ) {
            perror("fdopen() 失败");
            close(clnt_sock);
            continue;
        }

        // 从客户端读取数据,并回送相同的数据
        while (fgets(buffer, BUF_SIZE, readfp) != NULL) {
            fputs(buffer, writefp);
            fflush(writefp);  // fflush刷新缓冲区,是为了保证让数据立刻发送到客户端
        }

        // 关闭套接字的标准I/O流
        fclose(readfp);
        fclose(writefp);
        close(clnt_sock);
    }

    // 关闭服务器套接字
    close(serv_sock);
    return 0;
}

制作HTTP服务器

strstr函数

函数strstr是C标准库中的字符串处理函数之一,用于在一个字符串中查找另一个子字符串的第一次出现,并返回第一次出现的子字符串的地址。其原型如下:

char *strstr(const char *haystack, const char *needle);
  • haystack:指向要搜索的源字符串(父字符串)的指针。

  • needle:指向要查找的目标子字符串的指针。

strstr函数会在haystack字符串中查找第一次出现的needle子字符串,并返回第一次出现的子字符串的地址。如果找到匹配的子字符串,则返回子字符串在haystack中的地址;如果没有找到匹配的子字符串,则返回NULL。

strtok函数

函数strtok是C标准库中的字符串处理函数,用于将字符串分割成多个子字符串(标记),其原型如下:

char *strtok(char *str, const char *delim);
  • str:指向要分割的字符串的指针。在第一次调用时,传入要分割的字符串;在后续调用中,传入NULL。
  • delim:指向一个包含分隔符(分割字符)的C字符串的指针。该参数决定了如何将原始字符串分割成多个子字符串。

strtok函数在第一次调用时,会将str指向的字符串按照delim中指定的分隔符进行分割,并返回第一个子字符串的地址。随后的调用中,传入NULL作为第一个参数,可以继续获取后续的子字符串,直到字符串中没有剩余的子字符串为止。

strncmp函数

函数strncmp是C标准库中的字符串处理函数之一,用于比较两个字符串的前若干个字符是否相等。其原型如下:

int strncmp(const char *str1, const char *str2, size_t n);
  • str1:指向第一个字符串的指针。

  • str2:指向第二个字符串的指针。

  • n:要比较的最大字符数。

strncmp函数会比较str1str2所指向的字符串的前n个字符是否相等。比较是以字符的ASCII码值为依据的,如果前n个字符都相等,则返回0;如果在前n个字符中发现不相等的字符,则返回不相等字符的差值(str1[i] - str2[i],其中i为第一个不相等的字符位置);如果在前n个字符之前就遇到了字符串的结束符\0,则返回str1[i] - str2[i],其中i\0所在的位置。

需要注意的是,strncmp不会检查字符串的终止符\0是否相等,它只比较前n个字符,即使其中包含了终止符\0

HTTP服务器

#include 
#include 
#include 
#include 
#include 
#include 

void *handle_client(void *arg) {
    int client_socket = *((int *)arg);  // client_socket为与客户端通信的套接字文件描述符
    // 将套接字转换为文件流
    FILE *stream = fdopen(client_socket, "r+");
    if (stream == NULL) {
        perror("fdopen");
        close(client_socket);
        return NULL;
    }

    // 读取HTTP请求头
    char request[1024];
    if (fgets(request, sizeof(request), stream) == NULL) {
        perror("fgets");
        fclose(stream);
        close(client_socket);
        return NULL;
    }

    // 仅处理GET请求
    if (strncmp(request, "GET ", 4) != 0) {
        const char *response = "HTTP/1.1 501 Not Implemented\r\nContent-Length: 0\r\n\r\n";
        fputs(response, stream);
        fclose(stream);
        close(client_socket);
        return NULL;
    }

    // 发送HTTP响应头
    const char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
    fputs(response, stream);

    // 发送HTML页面内容
    const char *html_content = "

Hello, World!

"
; fputs(html_content, stream); // 关闭文件流和套接字 fclose(stream); close(client_socket); return NULL; } int main() { int server_socket, client_socket; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len = sizeof(client_addr); // 创建服务器套接字 server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket < 0) { perror("socket"); exit(EXIT_FAILURE); } // 配置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8888); // 绑定服务器套接字到地址和端口 if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_socket, 5) < 0) { perror("listen"); exit(EXIT_FAILURE); } printf("Server listening on port 8888...\n"); while (1) { // 接受新连接 client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len); if (client_socket < 0) { perror("accept"); continue; } // 创建新线程处理请求 子线程执行handle_client函数 pthread_t thread; if (pthread_create(&thread, NULL, handle_client, &client_socket) != 0) { perror("pthread_create"); close(client_socket); } // 分离线程,使其在结束时自动释放资源 pthread_detach(thread); } // 关闭服务器套接字 close(server_socket); return 0; }

你可能感兴趣的:(linux,运维,服务器)