Linux网络编程入门

c++在网络编程中特别普及,特别是Linux服务器编程. 相关书籍也有很多,这里简单介绍一下(结合AI总结).

重要的数据结构

套接字地址

PF_INET:指的是协议族(Protocol Family),强调的是协议相关的概念。

AF_INET:指的是地址族(Address Family),关注的是地址格式。

虽然在许多实现中 PF_INETAF_INET 可以互换使用,但理论上它们代表不同的概念。为了保持代码的清晰性和一致性,推荐的做法是在创建套接字时使用 AF_INET 来指代地址族,而保留 PF_INET 用于协议族相关的上下文。不过,由于历史原因和广泛接受的习惯,这种区别在实践中往往被忽略。

在进行网络编程时,sockaddr_insockaddraddrinfo 是三个不同的数据结构,它们各自有不同的用途和特点。

sockaddr

  • 定义

    1
    2
    3
    4
    struct sockaddr {
    sa_family_t sa_family; // 地址族(如AF_INET, AF_INET6)
    char sa_data[14]; // 地址信息,具体格式取决于地址族
    };
  • 用途

    • 这是一个通用的套接字地址结构体,用于表示任何类型的套接字地址。它不特定于任何一种协议或地址家族。
    • 它通常作为函数参数传递,以便支持多种不同类型的地址族(例如IPv4、IPv6等)。
  • 局限性

    • 因为它的sa_data字段是固定大小的字符数组,所以在处理复杂或长度不定的地址信息时不够灵活。

sockaddr_in

  • 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct sockaddr_in {
    sa_family_t sin_family; // 地址族,必须设置为AF_INET
    in_port_t sin_port; // 端口号(使用htons()转换为网络字节序)
    struct in_addr sin_addr; // IPv4地址
    unsigned char sin_zero[8]; // 填充0以使结构体大小与sockaddr相同
    };

    struct in_addr {
    in_addr_t s_addr; // 32位IPv4地址(网络字节序)
    };
  • 用途

    • 专门用于IPv4地址的套接字地址结构体
    • 提供了明确的字段来存储端口号和IP地址,使得处理IPv4地址更加直观和方便。
  • 优点

    • 相较于sockaddr,它提供了更具体的字段,便于操作IPv4地址和端口信息。

sockaddr_in6

1
2
3
4
5
6
7
8
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};

addrinfo

  • 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct addrinfo {
    int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
    int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
    int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
    int ai_protocol; // 使用的协议
    socklen_t ai_addrlen; // 地址长度
    struct sockaddr *ai_addr; // 地址信息
    char *ai_canonname; // 主机规范名称
    struct addrinfo *ai_next; // 下一个addrinfo结构指针
    };

    image-20250227150905975

  • 用途

    • addrinfo 结构体由 getaddrinfo() 函数返回,旨在提供一个统一的方式来处理不同类型的地址信息(包括IPv4和IPv6),并简化了主机名和服务名解析的过程
    • 它可以包含多个结果(通过ai_next链表连接),允许应用程序选择最适合其需求的结果。
  • 优点

    • 支持现代互联网中常见的IPv4和IPv6地址。
    • 能够处理复杂的配置需求,如指定被动监听(AI_PASSIVE)、获取规范主机名等。
    • 更加灵活和强大,适合需要跨平台兼容性和灵活性的应用程序。

sockaddr_storage

sockaddr_storage 是一个在 <netinet/in.h><sys/socket.h> 头文件中定义的数据结构,旨在提供一个足够大的缓冲区来存储任何类型的套接字地址(包括 IPv4、IPv6 等)。它解决了由于不同协议族的地址结构大小不一致所带来的问题,例如 sockaddr_insockaddr_in6 分别用于 IPv4 和 IPv6 地址,它们有不同的大小。

定义与用途

1
2
3
4
struct sockaddr_storage {
sa_family_t ss_family; // 地址族
// 其余字段未指定,实现定义以确保足够的空间和对齐
};
  • ss_family:这是唯一标准化的字段,表示地址族(如 AF_INET 对应 IPv4,AF_INET6 对应 IPv6)。
  • 其他字段:这些字段的具体定义依赖于实现,主要是为了确保 sockaddr_storage 能够容纳所有可能的套接字地址类型,并且保持正确的内存对齐。这意味着它的大小至少要能容纳最大的套接字地址结构(比如 sockaddr_in6)。

主要特点

  1. 统一性:通过使用 sockaddr_storage,可以编写更加通用的代码,避免直接处理特定于协议的地址结构(如 sockaddr_insockaddr_in6),从而提高代码的可移植性和灵活性。
  2. 大小保证sockaddr_storage 的大小被设计为足以容纳系统支持的所有套接字地址结构,这使得它可以安全地转换为任何特定的套接字地址类型。
  3. 对齐要求:除了大小之外,sockaddr_storage 还满足了所有套接字地址结构的对齐要求,这对于高效访问数据至关重要。

总结

  • sockaddr:是一个通用的套接字地址结构体,适用于所有地址族。由于其设计较为通用,实际应用中常被特定于某种地址族的结构体替代,如sockaddr_in
  • sockaddr_in:专用于IPv4地址,提供了对IPv4地址和端口的具体支持,易于理解和使用。
  • addrinfo:提供了一个更高级别的抽象,能够处理IPv4和IPv6地址,并且支持更多的选项和灵活性。它是推荐的方式来进行现代网络编程中的地址解析和套接字创建,特别是当你需要同时支持IPv4和IPv6时。

socketpair

socketpair 是一个用于创建一对互联的套接字描述符的系统调用,它允许在同一主机上的两个进程之间进行双向通信。通常,这些套接字被用于父子进程间的通信,但它们也可以用于任何需要双向(全双工)通信通道的场景

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int socketpair(int domain, int type, int protocol, int sv[2]);
  • 参数
    • domain:指定协议族,通常是 AF_UNIXAF_LOCAL(本地通信),但在某些实现中也可能支持其他域如 AF_INET
    • type:指定套接字类型,常见的有 SOCK_STREAM(提供有序、可靠、双向的连接)和 SOCK_DGRAM(数据报套接字)。
    • protocol:指定使用的协议,通常为 0,表示使用默认协议。
    • sv:指向一个包含两个整数元素的数组,这两个整数将作为返回的套接字描述符。
  • 返回值
    • 成功时,返回 0 并在 sv 数组中填充两个有效的套接字描述符。
    • 失败时,返回 -1 并设置 errno
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>

#define MSG_SIZE 1024

int main() {
int sv[2]; // 存储两个套接字描述符
char buffer[MSG_SIZE];

// 创建一对套接字
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
perror("socketpair");
exit(EXIT_FAILURE);
}

pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) { // 子进程
close(sv[0]); // 关闭不需要的套接字端
const char *msg = "Hello from child process!";
write(sv[1], msg, strlen(msg) + 1); // 发送消息给父进程
close(sv[1]);
exit(EXIT_SUCCESS);
} else { // 父进程
close(sv[1]); // 关闭不需要的套接字端
ssize_t bytes_read = read(sv[0], buffer, MSG_SIZE); // 从子进程接收消息
if (bytes_read > 0) {
printf("Received message from child: %s\n", buffer);
}
close(sv[0]);
wait(NULL); // 等待子进程结束
}

return 0;
}

地址二进制与点分十进制转换

在网络编程中,有时需要将点分十进制表示的IPv4地址(如"192.168.1.1")转换成32位无符号整数形式以便于处理或计算。在C语言中,可以使用以下函数来实现这种转换:

  • inet_addr: 这是一个简单的函数,用于将点分十进制格式的IPv4地址转换为网络字节序的32位长整型值。然而,它不支持IPv6,并且如果输入无效,则返回INADDR_NONE
  • inet_aton: 此函数不仅将点分十进制的IPv4地址转换为二进制形式,还会检查输入的有效性。它接受一个指向in_addr结构的指针作为第二个参数,并在此结构中填充相应的数值。如果转换成功,返回非零值;否则返回0
  • inet_pton: 这是一个更为现代和推荐使用的函数,支持IPv4和IPv6地址的转换。对于IPv4,它的第二个参数是一个指向struct in_addr类型的指针,对于IPv6,则是指向struct in6_addr类型的指针。如果转换成功,返回1;如果输入格式不正确,则返回0;如果遇到系统错误,则返回-1。
特性inet_addrinet_aton
输入格式点分十进制字符串点分十进制字符串
输出格式返回 in_addr_t(32 位整数)填充 struct in_addr 结构体
错误处理错误时返回 INADDR_NONE错误时返回 0
线程安全性安全安全
推荐程度不推荐(已过时)推荐
函数名输入格式输出格式支持的地址类型线程安全性
inet_pton字符串二进制IPv4 和 IPv6安全
inet_aton字符串二进制仅 IPv4安全
inet_ntop二进制字符串IPv4 和 IPv6安全
inet_ntoa二进制字符串仅 IPv4不安全

端口字节序转换

在网络编程中,处理不同系统间的数据传输时,经常需要将数据在主机字节序(Host Byte Order)和网络字节序(Network Byte Order)之间进行转换。这是因为不同的计算机架构可能使用不同的字节序来存储多字节数据类型,如整数。为了确保数据在网络上传输的一致性,通常采用大端字节序(Big Endian),也被称作网络字节序

针对这种需求,有几组常用的函数用于在主机字节序和网络字节序之间进行转换:

  1. htonlhtons:这两个函数分别用于将32位整型(long)和16位整型(short)从主机字节序转换为网络字节序。
    • htonl(uint32_t hostlong): Host to Network Long
    • htons(uint16_t hostshort): Host to Network Short
  2. ntohlntohs:与上述相反,这两个函数用于将32位和16位整型从网络字节序转换为主机字节序。
    • ntohl(uint32_t netlong): Network to Host Long
    • ntohs(uint16_t netshort): Network to Host Short

为了保证网络通信的兼容性,通常采用大端字节序(Big Endian),也称为网络字节序,来表示跨网络传输的数据。

需要转换为网络字节序的数据类型

  1. 端口号:端口号通常是16位的整数,在发送之前应该从主机字节序转换为网络字节序。
  2. IP地址:虽然IP地址通常以字符串形式表示(例如“192.168.0.1”),但在某些情况下,你可能会处理32位整型的IPv4地址或128位的IPv6地址。对于这些情况,如果需要直接操作整型值,则应确保它们是网络字节序。
  3. 序列号、确认号等TCP头部字段:这些字段都是32位的整数,用于TCP协议中的状态跟踪和数据流控制,因此也需要转换为网络字节序。
  4. 其他自定义协议中的多字节字段:如果你设计了一个自定义的应用层协议,并且该协议包含多字节整数字段(如长度指示符、版本号等),那么这些字段也应该按照网络字节序进行编码。

设置套接字等选项

setsockopt 是一个用于设置套接字选项的函数,它允许开发者对套接字的行为进行精细控制。通过 setsockopt,可以调整套接字的各种属性,例如地址复用接收/发送缓冲区大小、超时时间等。

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  1. sockfd:
    • 套接字描述符。
    • 指定要设置选项的目标套接字。
  2. level:
    • 选项所属的协议层。
    • 常见值包括:
      • SOL_SOCKET: 套接字通用选项(如地址复用、广播等)。
      • IPPROTO_TCP: TCP 协议相关选项。
      • IPPROTO_IP: IP 协议相关选项。
      • IPPROTO_IPV6: IPv6 相关选项。
  3. optname:
    • 具体的选项名称。
    • 根据 level 的不同,可以选择不同的选项。
  4. optval:
    • 指向选项值的指针。
    • 选项值的具体类型和格式取决于 optname
  5. optlen:
    • optval 缓冲区的大小(以字节为单位)
1
2
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

常用选项

  1. SO_REUSEADDR
  • 作用: 允许绑定到已被占用的地址和端口。
  • 场景: 通常用于避免因端口被占用而导致服务器无法启动的问题。

2. SO_REUSEPORT

  • 作用: 允许多个进程或线程绑定到同一个端口。
  • 场景: 适用于多线程或多进程服务器模型。
  • 注意: 需要与 SO_REUSEADDR 配合使用

3. SO_RCVBUFSO_SNDBUF

  • 作用

    :

    • SO_RCVBUF: 设置接收缓冲区大小。
    • SO_SNDBUF: 设置发送缓冲区大小。

4. SO_BROADCAST

  • 作用: 启用广播功能。
  • 场景: 用于 UDP 广播通信

5. SO_KEEPALIVE

  • 作用: 启用 TCP 的保活机制。
  • 场景: 检测长时间空闲的连接是否仍然有效。

6. SO_LINGER

  • 作用: 控制关闭套接字时的行为。
  • 场景: 当需要确保所有数据在关闭前被发送时。

7. TCP_NODELAY

  • 作用: 禁用 Nagle 算法,减少小数据包的延迟。
  • 场景: 对于实时性要求较高的应用(如在线游戏、实时聊天)。

设置文件描述符选项

函数原型

1
2
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
  • fd:目标文件描述符。

  • cmd

    :指定要执行的操作类型,常见的命令包括:

    • F_GETFL:获取文件描述符的状态标志。
    • F_SETFL:设置文件描述符的状态标志。
    • F_GETFD:获取文件描述符的文件描述符标志。
    • F_SETFD:设置文件描述符的文件描述符标志。
    • F_DUPFD:复制文件描述符。
    • F_DUPFD_CLOEXEC:复制文件描述符并设置 FD_CLOEXEC 标志。
  • arg:可选参数,具体取决于 cmd 的值。例如,在使用 F_SETFL 时,arg 是新的状态标志

在网络编程中,将套接字设置为非阻塞模式是一种常见的优化手段。非阻塞模式允许在尝试读取或写入数据时立即返回,而不会阻塞进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <fcntl.h>
#include <unistd.h>

// 将文件描述符设置为非阻塞模式
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0); // 获取当前文件描述符的标志
if (flags == -1) {
perror("fcntl F_GETFL failed");
return -1;
}

// 添加 O_NONBLOCK 标志
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL failed");
return -1;
}

return 0;
}

在非阻塞模式下:

  • 如果没有数据可读,read()recv() 会立即返回 -1,并设置 errnoEAGAINEWOULDBLOCK
  • 如果无法立即写入数据,write()send() 也会返回 -1,并设置 errnoEAGAINEWOULDBLOCK

文件状态标志可以通过 fcntl(fd, F_GETFL) 获取当前设置,并通过 fcntl(fd, F_SETFL, flags) 修改。

  • O_RDONLY, O_WRONLY, O_RDWR:打开文件的模式(只读、只写、读写),通常在打开文件时确定,不能通过 F_SETFL 修改。
  • O_APPEND:每次写入时将数据追加到文件末尾。
  • O_NONBLOCK:设置非阻塞模式。对于文件或设备,这意味着尝试的操作(如读取或写入)如果无法立即完成,则会立即返回而不是阻塞等待。在网络编程中,这通常用于套接字以实现异步I/O。
  • O_ASYNC:当I/O可用时发送信号(通常是 SIGIO)给进程。此功能允许进程异步地处理I/O事件。
  • O_DSYNCO_SYNC:要求同步写入。O_DSYNC 确保数据同步写入磁盘,而 O_SYNC 还包括文件元数据的同步写入。

信号机制

信号是操作系统向进程发送的一种异步通知机制,用于告知进程某个事件已经发生。信号可以被视为一种软件中断,它会打断进程的正常执行流程。

signal

signal() 函数

这是最基本的信号处理函数,用于设置对指定信号的处理方式。然而,它不如 sigaction 灵活和可靠。

1
2
3
4
5
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
  • signum:要捕获或忽略的信号编号。
  • handler:信号处理函数指针,或者 SIG_DFL(默认处理)、SIG_IGN(忽略)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int sig) {
printf("Caught signal %d\n", sig);
exit(0);
}

int main() {
// 设置 SIGINT (Ctrl+C) 的处理程序
if (signal(SIGINT, handle_sigint) == SIG_ERR) {
fprintf(stderr, "Unable to set handler for SIGINT\n");
return 1;
}

while(1) {
printf("Process running...\n");
sleep(1);
}
}

Linux 定义了许多标准信号,例如:

  • SIGINT(2):由用户按下 Ctrl+C 触发,通常用于终止进程。
  • SIGTERM(15):请求终止进程的信号。
  • SIGKILL(9):强制终止进程的信号,无法被捕获或忽略。
  • SIGSEGV(11):段错误(访问非法内存地址)。
  • SIGCHLD(17):子进程状态改变时发送给父进程的信号。
  • SIGUSR1SIGUSR2:用户自定义信号。

信号可以通过以下几种方式触发:

(1) 用户输入

  • 按下 Ctrl+C 会向当前前台进程发送 SIGINT 信号。
  • 按下 Ctrl+\ 会向当前前台进程发送 SIGQUIT 信号。

(2) 系统调用

通过系统调用 killraise 可以向进程发送信号:

  • kill(pid_t pid, int sig):向指定进程 ID 的进程发送信号。
  • raise(int sig):向当前进程自身发送信号。

(3) 硬件异常

当进程访问非法内存地址时,操作系统会发送 SIGSEGV 信号;当进行非法指令操作时,可能会发送 SIGILL 信号。

(4) 软件触发

通过调用 alarm 函数可以设置定时器,超时时会向进程发送 SIGALRM 信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig) {
printf("Received signal: %d\n", sig);
}

int main() {
signal(SIGALRM, handler); // 注册信号处理函数
alarm(3); // 设置 3 秒后发送 SIGALRM 信号
printf("Waiting for the alarm signal...\n");
pause(); // 挂起进程,等待信号
return 0;
}

sigaction

sigaction 是 POSIX 标准定义的一种机制,用于定义进程对特定信号的响应行为。它提供了一种比 signal() 更加灵活和强大的方式来处理信号。sigaction 结构体用于指定如何处理信号、信号处理程序的属性以及信号掩码等信息。

sigaction 提供了比 signal() 更加精细和强大的信号处理功能。

1
2
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 参数:
    • signum:信号编号。
    • act:指向包含新动作的 struct sigaction 结构体的指针。
    • oldact:如果非空,则保存旧的动作。

struct sigaction 结构体

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 用于带有附加信息的信号处理函数
sigset_t sa_mask; // 在执行信号处理器期间需要阻塞的信号集合
int sa_flags; // 控制信号处理的行为标志
void (*sa_restorer)(void); // 已废弃,不应使用
};

sigaction 结构体用于指定如何处理信号、信号处理程序的属性以及信号掩码等信息。

字段说明

  • sa_handler
    • 这是一个指向信号处理函数的指针,或者可以设置为 SIG_DFL(默认信号处理)或 SIG_IGN(忽略信号)。
  • sa_sigaction
    • sa_flags 中设置了 SA_SIGINFO 标志时,此字段将作为信号处理函数使用。与 sa_handler 不同,sa_sigaction 可以接收更多信息,包括一个指向 siginfo_t 结构的指针(包含有关信号的详细信息)和一个指向处理器上下文的指针(通常不使用)。
  • sa_mask
    • 定义了一个信号集,在调用信号处理程序之前,这些信号会被加入到当前的信号屏蔽字中。这意味着在执行信号处理程序期间,这些信号会被暂时阻塞。
  • sa_flags
    • 控制信号处理的行为。常见的标志包括:
      • SA_RESTART:如果信号中断了某个系统调用,则自动重启该系统调用(而不是返回错误)。
      • SA_NOCLDSTOP:仅对 SIGCHLD 信号有效,如果设置了此标志,则子进程停止或恢复时不会发送 SIGCHLD 信号给父进程。
      • SA_NOCLDWAIT:仅对 SIGCHLD 信号有效,阻止创建僵尸进程。
      • SA_SIGINFO:指示使用 sa_sigaction 字段中的信号处理函数,而非 sa_handler。这允许访问扩展的信号信息。
  • sa_restorer
    • 这个字段已废弃,不应该被使用。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handler(int signum, siginfo_t *info, void *context) {
printf("Caught signal %d\n", signum);
// 打印更多关于信号的信息
printf("Signal code: %d\n", info->si_code);
}

int main() {
struct sigaction act;

// 初始化结构体
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO; // 使用带有额外参数的信号处理函数

// 填充信号掩码,这里我们不限制任何额外的信号
sigemptyset(&act.sa_mask);

// 设置 SIGINT 的信号处理程序
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}

printf("Waiting for SIGINT (Ctrl+C)...\n");

// 挂起进程,等待信号
while (1) {
pause(); // 等待信号
}

return 0;
}

stat

stat 函数是 Unix 和类 Unix 操作系统(如 Linux)中的一个系统调用,用于获取文件或文件系统对象的相关信息。它通过填充一个 struct stat 结构体来提供文件的元数据,包括文件大小、权限、创建时间等

1
2
3
4
5
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
  • 参数
    • pathname:要查询的文件或目录的路径。
    • statbuf:指向一个 struct stat 结构体的指针,该结构体将被用来存储文件的状态信息。
  • 返回值
    • 成功时返回 0
    • 失败时返回 -1 并设置 errno 来指示错误类型。

除了 stat,还有其他几个类似的函数可以用于不同的场景:

  • fstat:与 stat 类似,但它接受一个文件描述符而不是路径名作为第一个参数。

    1
    int fstat(int fd, struct stat *statbuf);
  • lstat:与 stat 类似,但如果目标是一个符号链接(symlink),它会返回符号链接本身的信息,而不是它指向的目标文件的信息。

    1
    int lstat(const char *pathname, struct stat *statbuf);

struct stat 包含了大量的关于文件的信息。以下是一些常用的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; // 文件所在设备的标识符
ino_t st_ino; // inode 编号
mode_t st_mode; // 文件类型和访问权限
nlink_t st_nlink; // 硬链接数量
uid_t st_uid; // 文件所有者的用户 ID
gid_t st_gid; // 文件所有者的组 ID
dev_t st_rdev; // 如果文件是一个设备文件,则为其设备编号
off_t st_size; // 文件大小(字节数)
blksize_t st_blksize; // 文件系统的 I/O 块大小
blkcnt_t st_blocks; // 分配给文件的块数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间(在 Unix 中通常为元数据更改时间)
};

struct statst_mode 字段中,文件类型和权限信息被编码在一起。可以通过位操作提取这些信息:

  • 文件类型
    • S_IFMT:文件类型的掩码。
    • S_IFDIR:目录。
    • S_IFCHR:字符设备。
    • S_IFBLK:块设备。
    • S_IFREG:普通文件。
    • S_IFIFO:命名管道(FIFO)。
    • S_IFLNK:符号链接。
    • S_IFSOCK:套接字。
  • 权限
    • S_IRUSR, S_IWUSR, S_IXUSR:用户(拥有者)的读、写、执行权限。
    • S_IRGRP, S_IWGRP, S_IXGRP:组的读、写、执行权限。
    • S_IROTH, S_IWOTH, S_IXOTH:其他人的读、写、执行权限。
1
2
3
if ((sb.st_mode & S_IFMT) == S_IFREG && (sb.st_mode & S_IRUSR)) {
printf("This is a regular file with read permission for the owner.\n");
}

mmap

mmap 是 Unix 和类 Unix 系统(如 Linux)中的一个系统调用,用于将文件或设备的内存映射到进程的地址空间。这种机制允许程序以类似于访问内存的方式访问文件内容,从而简化了文件操作,并且可以提高性能,特别是在处理大文件时。

  • 文件映射:将文件的内容映射到进程的虚拟内存中,使得可以通过指针直接访问文件的数据,而不需要通过常规的文件 I/O 操作(如 readwrite)。
  • 共享内存:允许多个进程共享同一块内存区域,实现高效的进程间通信(IPC)。
  • 匿名映射:创建不与任何文件关联的内存映射,适用于需要动态分配大块内存的情况
1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 参数
    • addr:建议的映射起始地址(通常设为 NULL,让系统选择合适的地址)。
    • length:映射区域的大小(字节数)。
    • prot:指定映射区域的保护标志(如可读、可写、可执行等)。
      • PROT_READ:映射区域可读。
      • PROT_WRITE:映射区域可写。
      • PROT_EXEC:映射区域可执行。
    • flags:控制映射区域的行为。
      • MAP_SHARED:映射区域会被多个进程共享,修改会反映到文件中。
      • MAP_PRIVATE:创建私有副本,修改不会影响原文件。
      • MAP_ANONYMOUS:映射匿名内存(不与文件关联)。
    • fd:要映射的文件描述符(对于匿名映射,应设置为 -1)。
    • offset:从文件开头开始的偏移量(必须是页面大小的倍数)。
  • 返回值

    • 成功时返回指向映射区域的指针。
    • 失败时返回 MAP_FAILED(通常定义为 (void *) -1),并设置 errno
  • munmap:解除内存映射。

    1
    int munmap(void *addr, size_t length);
  • msync:同步内存映射区域到文件或设备。

    1
    int msync(void *addr, size_t length, int flags);

解析传入参数

Linux中getopt 是一个用于解析命令行选项的标准 C 库函数。它使得程序能够处理以短格式(如 -a, -b value)提供的命令行参数。getopt 函数及其扩展版本 getopt_long 为开发者提供了便捷的方式来解析和处理命令行选项。

get_opt函数

1
2
3
#include <unistd.h>

int getopt(int argc, char * const argv[], const char *optstring);
  • 参数

    • argcargv:分别是从 main 函数传递过来的参数计数和参数数组。
    • optstring:包含程序所支持的选项字符组成的字符串。如果某个选项需要参数,则在该选项字符后加上冒号(:),表示该选项需要一个值。
  • 返回值

    • 成功时,返回下一个选项字符。
    • 当所有选项都已处理完毕,返回 -1
    • 如果遇到无效选项或缺少必需的参数,返回 ? 并设置 optopt 变量为无效选项字符。
  • 全局变量

    • optind:指向下一个要处理的 argv 元素的索引。
    • optarg指向当前选项的参数(如果有)。
    • opterr:控制 getopt 是否打印错误消息,默认为 1(开启)。
    • optopt:存储无效选项字符或缺失参数的选项字符。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #include <stdio.h>
    #include <unistd.h>

    int main(int argc, char *argv[]) {
    int opt;
    while ((opt = getopt(argc, argv, "ab:c")) != -1) {
    switch (opt) {
    case 'a':
    printf("Option -a\n");
    break;
    case 'b':
    printf("Option -b with value %s\n", optarg);
    break;
    case 'c':
    printf("Option -c\n");
    break;
    case '?':
    if (optopt == 'b') {
    fprintf(stderr, "Option -b requires an argument.\n");
    } else {
    fprintf(stderr, "Unknown option character `\\x%x'.\n", optopt);
    }
    return 1;
    default:
    abort();
    }
    }

    // 处理非选项参数
    for (int index = optind; index < argc; index++) {
    printf("Non-option argument: %s\n", argv[index]);
    }

    return 0;
    }

getopt_long 函数

对于支持长格式选项(如 --option, --option=value)的应用程序,可以使用 getopt_long 函数。

函数原型

1
2
3
#include <getopt.h>

int getopt_long(int argc, char * const argv[], const char *shortopts, const struct option *longopts, int *longindex);
  • 参数

    • shortopts:与 getopt 相同,定义短格式选项。

    • longopts指向描述长格式选项的struct option

      1
      2
      3
      4
      5
      6
      struct option {
      const char *name; // 长格式选项名
      int has_arg; // 是否需要参数,可能值:no_argument, required_argument, optional_argument
      int *flag; // 若不为 NULL,函数将此指针指向的变量设为 val;若为 NULL,函数返回 val
      int val; // 返回给 `getopt_long` 的值或设置到 `flag` 指向的变量中
      };
    • longindex:如果非 NULL,则指向一个变量,该变量接收匹配的长选项在 longopts 数组中的索引。

  • 返回值

    • 成功时,返回匹配选项的字符(对于短选项)或 val 字段的值(对于长选项)。
    • 当所有选项都已处理完毕,返回 -1
    • 对于无效选项或缺少必需的参数,返回 ?
  • getopt:适用于处理短格式选项的简单场景。通过指定一个选项字符串来定义允许的选项及是否需要参数。

  • getopt_long:扩展了 getopt,支持长格式选项,并允许更灵活地配置每个选项的行为(是否需要参数、如何处理等)。

分散/聚集IO

分散/聚集 I/O(Scatter/Gather I/O)是一种允许在单次系统调用中处理多个数据缓冲区的技术。它特别适用于需要处理多个不连续的数据块的应用场景,如网络通信、数据库操作等。通过分散/聚集 I/O,可以减少系统调用的次数,提高性能和效率。

分散 I/O(Scatter I/O)

分散读取(Scatter Read)指的是从一个输入源(例如文件或套接字)读取数据,并将这些数据分散到多个缓冲区中。这种技术通常用于接收长度未知的数据流,并将其分割成多个部分存储在不同的缓冲区中。

聚集 I/O(Gather I/O)

聚集写入(Gather Write)则是指将多个缓冲区中的数据收集起来,然后一次性写入到输出目标(例如文件或套接字)。这种方法可以简化编程模型,并且通过减少系统调用的数量来提高性能。

在 Unix 和类 Unix 系统(如 Linux)中,分散/聚集 I/O 主要通过 readvwritev 系统调用来实现。

readvwritev 函数

  • readv:从文件描述符读取数据并分散到多个缓冲区。
  • writev:从多个缓冲区收集数据并写入到文件描述符。
1
2
3
4
struct iovec {
void *iov_base; // 指向缓冲区的指针
size_t iov_len; // 缓冲区长度(字节数)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <fcntl.h>
#include <stdio.h>
#include <sys/uio.h>
#include <unistd.h>

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

char buf1[6]; // 存储 "Hello,"
char buf2[8]; // 存储 " World!\n"

struct iovec iov[2];
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2);

ssize_t read_bytes = readv(fd, iov, 2);
if (read_bytes == -1) {
perror("readv");
close(fd);
return 1;
}

buf1[sizeof(buf1)-1] = '\0'; // 确保 buf1 是以 null 结尾的字符串
buf2[sizeof(buf2)-1] = '\0'; // 确保 buf2 是以 null 结尾的字符串

printf("Read %zd bytes: '%s' and '%s'\n", read_bytes, buf1, buf2);

close(fd);
return 0;
}

TCP流程

TCP Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
const char *response = "Hello from server";

// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 监听连接
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}

printf("Server is listening on port %d...\n", PORT);

// 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("Accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 读取客户端数据
int valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Client: %s\n", buffer);

// 发送响应
send(new_socket, response, strlen(response), 0);
printf("Response sent to client.\n");

close(new_socket);
close(server_fd);
return 0;
}

TCP Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from client";

// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将 IP 地址转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(sock);
exit(EXIT_FAILURE);
}

// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection failed");
close(sock);
exit(EXIT_FAILURE);
}

// 发送消息
send(sock, message, strlen(message), 0);
printf("Message sent to server.\n");

// 接收响应
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server: %s\n", buffer);

close(sock);
return 0;
}#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from client";

// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将 IP 地址转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(sock);
exit(EXIT_FAILURE);
}

// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection failed");
close(sock);
exit(EXIT_FAILURE);
}

// 发送消息
send(sock, message, strlen(message), 0);
printf("Message sent to server.\n");

// 接收响应
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server: %s\n", buffer);

close(sock);
return 0;
}

UDP流程

UDP Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50


#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <unistd.h>
const int PORT = 8080;
const int BUFFER_SIZE = 1024;
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
sockaddr_in servaddr, cliaddr;
socklen_t len = sizeof(cliaddr);
const char *response = "Hello from server";
// 创建套接字 SOCK_DGRAM
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));

// 绑定地址和端口
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("Bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d...\n", PORT);

// 服务端绑定套接字后 直接开始读了
// 接收客户端数据
int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0';
printf("Client: %s\n", buffer);

// 发送响应
sendto(sockfd, (const char *)response, strlen(response), 0,
(const struct sockaddr *)&cliaddr, len);
printf("Response sent to client.\n");

close(sockfd);
return 0;
}

UDP Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr;
const char *message = "Hello from client";

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

memset(&servaddr, 0, sizeof(servaddr));

// 设置服务器地址
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = INADDR_ANY;

// 将 IP 地址转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(sockfd);
exit(EXIT_FAILURE);
}

// 创建套接字 后直接发送消息 需要连接的地址,不需要connect
// 发送消息
sendto(sockfd, (const char *)message, strlen(message), 0,
(const struct sockaddr *)&servaddr, sizeof(servaddr));
printf("Message sent to server.\n");

// 接收响应
socklen_t len = sizeof(servaddr);
int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&servaddr, &len);
buffer[n] = '\0';
printf("Server: %s\n", buffer);

close(sockfd);
return 0;
}

其他

在windows上的异步I/O机制

异步 I/O(Asynchronous I/O)是一种高效的机制,用于处理文件、网络套接字等设备的输入输出操作.Windows 提供了多种实现异步 I/O 的方式,包括 Overlapped I/OI/O Completion Ports (IOCP)

Overlapped I/O

概述

Overlapped I/O 是 Windows 提供的一种异步 I/O 机制,它通过使用 OVERLAPPED 结构体来标记一个 I/O 操作是否为异步。这种机制适用于文件操作和套接字通信。

  • 如果一个文件句柄或套接字是以重叠(Overlapped)模式打开的,则可以对其进行异步操作。
  • 异步操作完成后,可以通过以下方式通知应用程序:
    • 使用事件对象(Event Object)。
    • 调用回调函数(Completion Routine)。
    • 使用 I/O 完成端口(IOCP)。

关键函数

  • ReadFile / WriteFile:用于读写文件或套接字。
  • GetOverlappedResult:检查异步操作的状态。
  • CancelIoEx:取消挂起的异步 I/O 操作。

示例代码

以下是一个使用 Overlapped I/O 进行异步文件读取的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <windows.h>
#include <stdio.h>

void AsyncFileRead() {
HANDLE hFile = CreateFile(
"example.txt", // 文件名
GENERIC_READ, // 打开文件用于读取
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 打开已存在的文件
FILE_FLAG_OVERLAPPED, // 启用 Overlapped I/O
NULL // 无模板文件
);

if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to open file. Error: %d\n", GetLastError());
return;
}

char buffer[1024];
OVERLAPPED overlapped = {0};
overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 创建事件对象

if (!ReadFile(hFile, buffer, sizeof(buffer), NULL, &overlapped)) {
if (GetLastError() != ERROR_IO_PENDING) {
printf("ReadFile failed. Error: %d\n", GetLastError());
CloseHandle(hFile);
CloseHandle(overlapped.hEvent);
return;
}
}

// 等待异步操作完成
WaitForSingleObject(overlapped.hEvent, INFINITE);

DWORD bytesRead;
if (GetOverlappedResult(hFile, &overlapped, &bytesRead, FALSE)) {
printf("Read %d bytes: %.*s\n", bytesRead, bytesRead, buffer);
} else {
printf("GetOverlappedResult failed. Error: %d\n", GetLastError());
}

CloseHandle(hFile);
CloseHandle(overlapped.hEvent);
}

int main() {
AsyncFileRead();
return 0;
}
  • Overlapped I/O 是 Windows 中最基础的异步 I/O 实现方式之一。它通过 OVERLAPPED 结构体来标记一个 I/O 操作是否为异步。
  • 当使用重叠模式打开文件或套接字时,可以发起异步操作,并且在操作完成前继续执行其他代码。

关键函数

  • CreateFile:创建或打开文件时指定 FILE_FLAG_OVERLAPPED 标志以启用重叠模式。
  • ReadFile / WriteFile:用于读取或写入数据。对于异步操作,最后一个参数应指向一个有效的 OVERLAPPED 结构体。
  • GetOverlappedResult:获取异步操作的结果。
  • WaitForSingleObjectWaitForMultipleObjects:等待异步操作完成。

回调函数

在 Windows 中,回调函数通常通过 ReadFileExWriteFileEx 函数注册,而不是直接使用 ReadFileWriteFile

回调函数的原型必须符合以下格式:

1
2
3
4
5
VOID CALLBACK CompletionRoutine(
DWORD dwErrorCode, // 错误码
DWORD dwNumberOfBytesTransfered, // 转移的字节数
LPOVERLAPPED lpOverlapped // OVERLAPPED 结构体指针
);

使用 ReadFileExWriteFileEx 注册回调函数

  • ReadFileExWriteFileEx 是专门用于异步 I/O 并支持回调函数的 API。
  • 它们需要一个有效的 OVERLAPPED 结构体,并且文件句柄必须以重叠模式打开(即带有 FILE_FLAG_OVERLAPPED 标志)。
  • 异步操作完成后,操作系统会调用指定的回调函数。

  • 回调函数是一种轻量级的异步 I/O 处理方式,通过 ReadFileExWriteFileEx 函数注册回调函数,在操作完成后自动调用。

  • 这种方式要求线程进入可提醒等待状态(Alertable Wait State),例如使用 SleepEx 函数。

关键函数

  • ReadFileEx / WriteFileEx:用于启动异步 I/O 操作并注册回调函数。
  • SleepEx:使当前线程进入可提醒等待状态,以便能够接收异步通知。

3. 示例代码

以下是一个使用回调函数处理异步文件读取的完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <windows.h>
#include <stdio.h>

// 回调函数定义
VOID CALLBACK FileIOCompletionRoutine(
DWORD dwErrorCode,
DWORD dwNumberOfBytesTransfered,
LPOVERLAPPED lpOverlapped)
{
if (dwErrorCode == 0) {
printf("Asynchronous read completed successfully.\n");
printf("Number of bytes read: %d\n", dwNumberOfBytesTransfered);
} else {
printf("Asynchronous read failed with error code: %d\n", dwErrorCode);
}
}

void AsyncFileReadWithCallback() {
HANDLE hFile = CreateFile(
"example.txt", // 文件名
GENERIC_READ, // 打开文件用于读取
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 打开已存在的文件
FILE_FLAG_OVERLAPPED, // 启用 Overlapped I/O
NULL // 无模板文件
);

if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to open file. Error: %d\n", GetLastError());
return;
}

char buffer[1024];
OVERLAPPED overlapped = {0};

// 启动异步读取操作
BOOL result = ReadFileEx(
hFile, // 文件句柄
buffer, // 缓冲区
sizeof(buffer), // 要读取的字节数
&overlapped, // OVERLAPPED 结构体
FileIOCompletionRoutine // 回调函数
);

if (!result) {
printf("ReadFileEx failed. Error: %d\n", GetLastError());
CloseHandle(hFile);
return;
}

// 等待异步操作完成
SleepEx(INFINITE, TRUE); // 进入可提醒等待状态,使回调函数得以执行

CloseHandle(hFile);
}

int main() {
AsyncFileReadWithCallback();
return 0;
}

I/O Completion Ports (IOCP)

概述

I/O Completion Ports(简称 IOCP)是 Windows 提供的一种高性能的异步 I/O 机制,特别适合于需要处理大量并发连接的服务器程序。IOCP 的核心思想是将多个 I/O 操作绑定到一个完成端口,并由一个线程池来处理完成的通知

  • IOCP 的优点:
    • 高效地管理多个并发 I/O 操作。
    • 自动负载均衡,多个工作线程可以高效协作。
    • 支持大规模并发连接。

关键函数

  • CreateIoCompletionPort:创建或关联一个完成端口。
  • PostQueuedCompletionStatus:向完成端口队列提交自定义的完成包。
  • GetQueuedCompletionStatus:从完成端口队列中获取完成通知。

示例代码

以下是一个简单的 IOCP 示例,展示如何使用 IOCP 处理异步文件读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <windows.h>
#include <stdio.h>

typedef struct {
OVERLAPPED overlapped;
char buffer[1024];
} IO_CONTEXT;

DWORD WINAPI WorkerThread(LPVOID lpParam) {
HANDLE hCompletionPort = (HANDLE)lpParam;
DWORD bytesTransferred;
ULONG_PTR completionKey;
LPOVERLAPPED pOverlapped;

while (true) {
BOOL result = GetQueuedCompletionStatus(
hCompletionPort, &bytesTransferred, &completionKey, &pOverlapped, INFINITE);

if (!result || bytesTransferred == 0) {
printf("Operation failed or completed.\n");
break;
}

IO_CONTEXT *context = (IO_CONTEXT *)pOverlapped;
printf("Read %d bytes: %.*s\n", bytesTransferred, bytesTransferred, context->buffer);
}

return 0;
}

void AsyncFileReadWithIOCP() {
HANDLE hFile = CreateFile(
"example.txt",
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED | FILE_FLAG_NO_BUFFERING,
NULL
);

if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to open file. Error: %d\n", GetLastError());
return;
}

HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (!hCompletionPort) {
printf("Failed to create completion port. Error: %d\n", GetLastError());
CloseHandle(hFile);
return;
}

CreateIoCompletionPort(hFile, hCompletionPort, (ULONG_PTR)hFile, 0);

HANDLE hThread = CreateThread(NULL, 0, WorkerThread, hCompletionPort, 0, NULL);
if (!hThread) {
printf("Failed to create worker thread. Error: %d\n", GetLastError());
CloseHandle(hFile);
CloseHandle(hCompletionPort);
return;
}

IO_CONTEXT context = {0};
context.overlapped.Offset = 0;

if (!ReadFile(hFile, context.buffer, sizeof(context.buffer), NULL, &context.overlapped)) {
if (GetLastError() != ERROR_IO_PENDING) {
printf("ReadFile failed. Error: %d\n", GetLastError());
CloseHandle(hFile);
CloseHandle(hCompletionPort);
CloseHandle(hThread);
return;
}
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hFile);
CloseHandle(hCompletionPort);
CloseHandle(hThread);
}

int main() {
AsyncFileReadWithIOCP();
return 0;
}

关键函数

  • CreateIoCompletionPort:创建一个新的完成端口或将其与现有句柄关联。
  • PostQueuedCompletionStatus:手动向完成端口队列中添加状态信息。
  • GetQueuedCompletionStatus:从完成端口队列中检索下一个已完成的操作的状态。

3. 对比 Overlapped I/O 和 IOCP

特性Overlapped I/OIOCP
适用场景小规模异步操作大规模并发 I/O 操作
性能较低更高
复杂度较低较高
线程管理需要手动管理线程自动管理线程池
扩展性有限非常强

4. 总结

  • Overlapped I/O:适合小型应用或需要简单异步 I/O 的场景,易于实现但扩展性较差。
  • IOCP:适合高性能服务器程序,支持大规模并发连接,具有更高的效率和扩展性,但实现起来更复杂。

  • 简单场景:如果您的应用只需要简单的异步 I/O 操作,Overlapped I/O 可能是最容易实现的选择。

  • 高并发场景:如果您正在开发一个需要处理大量并发连接的应用程序(如 Web 服务器),那么 IOCP 是最佳选择,因为它提供了更好的性能和扩展性。
  • 轻量级需求:如果希望避免复杂的线程管理并且对性能的要求不是极高,可以考虑使用回调函数的方式。
-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道