【Linux篇】网络编程基础(笔记)

目录

一、服务器模型

[1. C/S 模型](#1. C/S 模型)

[2. P2P模型](#2. P2P模型)

二、服务器编程框架

[1. I/O处理单元](#1. I/O处理单元)

[2. 逻辑单元](#2. 逻辑单元)

[3. 网络存储单元](#3. 网络存储单元)

[4. 请求队列](#4. 请求队列)

三、网络编程基础API

[1. socket 地址处理 API](#1. socket 地址处理 API)

(1)主机字节序和网络字节序

(2)通用socket地址

[① sa_family成员](#① sa_family成员)

[② sa_data成员](#② sa_data成员)

(3)专用socket地址

(4)IP地址转换函数

[① inet_addr函数](#① inet_addr函数)

[② inet_aton函数](#② inet_aton函数)

[③ inet_ntoa函数](#③ inet_ntoa函数)

[④ inet_pton函数](#④ inet_pton函数)

[⑤ inet_ntop函数](#⑤ inet_ntop函数)

[2. 创建socket](#2. 创建socket)

[① domain参数](#① domain参数)

[② type参数](#② type参数)

[③ protocol参数](#③ protocol参数)

[3. 命名socket](#3. 命名socket)

[4. 监听socket](#4. 监听socket)

[① sockfd参数](#① sockfd参数)

[② backlog参数](#② backlog参数)

[5. 接受连接](#5. 接受连接)

[① sockfd参数](#① sockfd参数)

[② addr参数用来](#② addr参数用来)

[6. 发起连接](#6. 发起连接)

[① sockfd参数](#① sockfd参数)

[② serv_addr参数](#② serv_addr参数)

[③ addrlen参数](#③ addrlen参数)

[7. 关闭连接](#7. 关闭连接)

[① sockfd参数](#① sockfd参数)

[② howto参数](#② howto参数)

[8. 数据读写(TCP)](#8. 数据读写(TCP))

[① recv](#① recv)

[② send](#② send)

[9. 地址信息函数](#9. 地址信息函数)

[① getsockname](#① getsockname)

[② getpeername](#② getpeername)

[10. 网络信息API](#10. 网络信息API)

[① name参数](#① name参数)

[② addr参数](#② addr参数)

[③ len参数](#③ len参数)

[④ type参数](#④ type参数)

四、高效的事件处理模式

[1. Reactor模式](#1. Reactor模式)

[2. Proactor模式](#2. Proactor模式)

五、I/O复用

[1. select系统调用](#1. select系统调用)

[① nfds参数](#① nfds参数)

[② readfds、writefds和exceptfds参数](#② readfds、writefds和exceptfds参数)

[③ timeout参数](#③ timeout参数)

[2. poll系统调用](#2. poll系统调用)

[① fds参数](#① fds参数)

[② nfds参数](#② nfds参数)

[③ timeout参数](#③ timeout参数)

[3. epoll系列系统调用](#3. epoll系列系统调用)

[① fd参数](#① fd参数)

[② op参数](#② op参数)

[③ event参数](#③ event参数)


一、服务器模型

1. C/S 模型

TCP/IP 协议在 设计和 实现上 并没有 客户端 和服务器的 概念,在通信过程中 所有机器 都是对等的。但 由于资源都 被数据提供者 所垄断,所以 几乎所有的 网络应用 程序都 很自然地 采用了 C/S(客户端/服务器)模型(所有客户端 都通过 访问服务器来 获取所需的资源)。

采用C/S模型 的 TCP服务器 和TCP客户端的工作流程:

① 服务器启动 后,首先 创建一个(或多个)监听 socket ,并调用 bind 函数 将其绑定 到服务器 感兴趣的 端口上 ,然后调用 listen 函数 等待客户连接
② 服务器 稳定运行之后,客户端 就可以 调用 connect 函数 向服务器 发起连接了。由于 客户连接 请求是 随机到达的 异步事件,服务器 需要 使用某种 I/O 模型 来监听 这一事件。
I/O模型有多种,图中 服务器使用的 是 I/O 复用 技术之一的 select 系统调用。
③ 当监听到 连接请求后,服务器 就 调用 accept 函数 接受 它,并 分配一个 逻辑单元 为新的连接 服务。逻辑单元 可以是 新创建的 子进程、子线程 或者 其他。
图中服务器 给客户端分配的 逻辑单元是 由 fork 系统调用 创建的 子进程。
④ 逻辑单元 读取客户 请求,处理 该请求,然后 将处理结果 返回给 客户端
⑤ 客户端 接收到 服务器 反馈的 结果之后,可以 继续向服务器 发送请求,也可以 立即 主动关闭连接。如果 客户端 主动关闭连接,则服务器 执行被动 关闭连接。
⑥ 至此,双方的通信结束。

需要注意的是,服务器在 处理一个 客户请求的 同时还会 继续监听 其他客户请求,否则就 变成了 效率低下的 串行服务器了、图中 服务器 同时监听 多个客户请求 是 通过 select 系统 调用 实现的。

2. P2P模型

P2P(Peer to Peer,点对点)模型 摒弃了 以服务器为 中心的格局,让 网络上 所有 主机 重新回归 对等的地位。P2P 模型 使得 每台机器 在消耗服务的 同时 也给别人 提供服务,这样 资源能够 充分、自由地共享。 云计算机群 可以看作 P2P 模型 的一个典范。但 P2P 模型 的缺点 也很明显:当用户之间传输的请求过多时,网络的负载将加重

从编程角度来讲,P2P 模型 可以看作 C/S 模型的扩展:每台主 机既是 客户端,又是 服务器。

二、服务器编程框架

服务器程序 种类繁多,但其 基本框架都一样,不同 之处在于 逻辑处理

1. I/O处理单元

I/O 处理单元 是服务器 管理客户连接的 模块。它 通常要完成 以下工作:++等待 并接受 新的客户连接,接收 客户数据,将服务器 响应数据返回 给客户端。++

***但是,数据的收发 不一定在 I/O 处理单元 中执行,也 可能在 逻辑单元中 执行,具体 在何处执行 取决于事件 处理模式。*对于 一个服务器机群 来说,I/O 处理单元 是一个 专门的 接入 服务器。它实现负载均衡,从 所有逻辑服务器中 选取负荷最小的 一台来为 新客户服务。

2. 逻辑单元

逻辑单元通常是一个 进程或线程。它 分析并处理 客户数据,然后 将结果 传递给 I/O 处理单元或者 直接发送给 客户端(具体 使用哪种方式取决于 事件处理模式)。

对服务器机群 而言,一个逻辑单元本身 就是一台 逻辑服务器。服务器 通常拥有 多个逻辑单元,以 实现对多个 客户任务的 并行处理。

3. 网络存储单元

网络存储单元 可以是 数据库、缓存 和 文件,甚至是 一台独立的 服务器。但它 不是 必须的,比如 ssh、telnet 等登录服务 就不需要 这个单元。

4. 请求队列

请求队列 是各单元之间的 通信方式的 抽象。I/O 处理单元 接收到 客户请求时,需要 以某种方式 通知一个 逻辑单元来 处理该请求。同样,多个逻辑单元 同时访问一个 存储单元时,也 需要采用 某种机制来协调 处理竞态条件。

对于服务器机群 而言,请求队列 是各台服务器之间 预先 建立的、静态的、永久 的 TCP 连接。这种 TCP 连接 能提高服务器之间 交换数据的 效率,因为 它避免了 动态建立 TCP 连接导致的 额外的 系统开销。

三、网络编程基础API

1. socket 地址处理 API

(1)主机字节序和网络字节序

字节序问题:现代CPU的累加器 一次都能 装载(至少)4字节(32位机),即一个整数。那么这 4 字节 在内存中排列的 顺序 将影响它被 累加器装载成的 整数的值

字节序分为 大端字节序(big endian)和小端字节序(little endian)。

大端字节序 是 指一个整数的高位字节(23~31 bit)存储在 内存的 低地址处,低位字节(0~7bit)存储在 内存的 高地址处。

小端字节序 则是 指整数的 高位字节存储 在内存的 高地址处,而 低位字节则存储 在内存的 低地址处。
当格式化的数据(比如32 bit整型数 和16 bit短整型数)在 两台使用 不同字节序的 主机之间直接 传递时,接 收端必然将被 错误地解释。解决问题的方法是:发**++送端总是 把要发送的 数据 转化成 大端字节序 数据 后再发送,而 接收端知道 对方传送过来的 数据总是 采用 大端字节序,所以接收端 可以 根据自身 采用的字节序决定 是否对接收到的 数据 进行转换(小端机转换,大端机不转换)++**。
因此 大端字节序 也称为 网络字节序。现代 PC 大多采用 小端字节序,因此 小端字节序又被称为 主机字节序。
Linux提供了如下 4 个函数来完成 主机字节序 和 网络字节序之间的 转换:

cpp 复制代码
#include<netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);  // "host to network long",即将长整型(32 bit)的主机字节序数据转化为网络字节序数据。
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

这 4 个函数中,长整型函数 通常用来转换 IP 地址,短整型 函数用 来 转换端口号(任何格式化的 数据 通过网络传输时,都应该使用这些 函数来 转换字节序)。

(2)通用socket地址

cpp 复制代码
#include<bits/socket.h>
struct sockaddr
{
    sa_family_t sa_family;
    char sa_data[14];
}
① sa_family成员

**地址族类型(sa_family_t)的变量。地址族类型 通常 与协议族类型 对应。**宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者 与前者有 完全相同的值,所以 二者通常混用。

② sa_data成员

用于存放 socket 地址值。但是,不同的协议族的 地址值 具有不同的 含义 和 长度。

(3)专用socket地址

cpp 复制代码
#include<sys/un.h>
struct sockaddr_un
{
    sa_family_t sin_family;  /*地址族:AF_UNIX*/
    char sun_path[108];      /*文件路径名*/
};

TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址 结构体,它们分别用于IPv4 和 IPv6。

cpp 复制代码
struct sockaddr_in {
    sa_family_t sin_family;    /* 地址族:AF_INET */
    u_int16_t sin_port;        /* 端口号,要用网络字节序表示 */
    struct in_addr sin_addr;   /* IPv4地址结构体,见下面 */
};
struct in_addr {
    u_int32_t s_addr;          /* IPv4地址,要用网络字节序表示* /
};


struct sockaddr_in6 {
    sa_family_t sin6_family;   /* 地址族:AF_INET6 */
    u_int16_t sin6_port;       /* 端口号,要用网络字节序表示 */
    u_int32_t sin6_flowinfo;   /* 流信息,应设置为0 */
    struct in6_addr sin6_addr; /* IPv6地址结构体,见下面 */
    u_int32_t sin6_scope_id;   /* scope ID,尚处于实验阶段 */
};
struct in6_addr {
    unsigned char sa_addr[16]; /* IPv6地址,要用网络字节序表示 */
};

所有专用 socket 地址 类型的 变量 在实际使用时 都需要 转化为 通用 socket 地址 类型sockaddr(强制转换即可),因为所有 socket 编程接口 使用的地址参数的 类型 都是 sockaddr。

(4)IP地址转换函数

cpp 复制代码
#include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char*inet_ntoa(struct in_addr in);
① inet_addr函数

将用 点分 十进制字符串 表示的 IPv4 地址转化为 用网络字节序 整数 表示的 IPv4 地址。它失败时返回 INADDR_NONE。

② inet_aton函数

完成和 inet_addr 同样的功能,但是 将转化结果 存储于参数 inp 指向的 地址结构中。它成功时 返回 1,失败则 返回 0。

③ inet_ntoa函数

将 用网络字节序 整数 表示的 IPv4 地址 转化为 用点分 十进制字符串 表示的 IPv4 地址。
注:该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。

cpp 复制代码
#include<arpa/inet.h>
int inet_pton(int af, const char* src, void* dst);
const char*inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
④ inet_pton函数

将用字符串 表示的 IP 地址 src(用点分 十进制字符串 表示的 IPv4 地址 或用 十六进制字符 串表示的 IPv6 地址)转换成 用网络字节序整数 表示的 IP 地址,并把 转换结果 存储于 dst 指向的内存中。其中,af 参数指定 地址族,可以是 AF_INET 或者 AF_INET6。inet_pton 成功 时 返回 1,失败 则返回 0 并设置 errno。

⑤ inet_ntop函数

进行相反的 转换,前三个参数的 含义 与 inet_pton 的参数 相同,最后 一个参数 cnt 指定目标存储单元的 大小。inet_ntop 成功时 返回目标存储单元的 地址,失败则返回 NULL 并设置errno。

2. 创建socket

socket 就是可读、可写、可控制、可关闭的文件描述符。

cpp 复制代码
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
① domain参数

告诉系统 使用哪个 底层协议族。对 TCP/IP 协议族 而言,该参数应该 设置为 PF_INE(Protocol Family of Internet,用于 IPv4)或 PF_INET6(用于IPv6)。

② type参数

指定服务类型。服务类型主要有 SOCK_STREAM 服务(流服务)和 SOCK_UGRAM(数据报)服务。
对 TCP/IP 协议族而言,其值取 SOCK_STREAM 表示传输层使用 TCP协议,取SOCK_DGRAM 表示传输层使用 UDP 协议。

③ protocol参数

是在前 两个参数构成的 协议集合下,再 选择一个 具体的协议。几乎 在所有情况下,我们 都应该把它设置为 0,表示使用 默认协议。
socket 系统调用成功时返回一个 socket 文件描述符,失败则 返回-1 并 设置errno。

3. 命名socket

将一个 socket 与 socket 地址绑定称为 给 socket 命名。

在 服务器程序中,通常 要命名 socket,因为 只有命名后 客户端才能 知道该 如何连接 它。客户端则 通常不需要命名 socket,而是 采用 匿名方式,即 使用操作系统 自动分配的 socket地址。

cpp 复制代码
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

bind 将 my_addr 所指的 socket 地址分配给 未命名的 sockfd 文件描述符,addrlen 参数 指出该 socket 地址的长度。bind 成功时 返回 0,失败则 返回 -1并 设置errno。

4. 监听socket

还需使用系统调用 来创建一个监听队列以 存放待处理的 客户连接。

cpp 复制代码
#include<sys/socket.h>
int listen(int sockfd, int backlog);
① sockfd参数

指定被监听的 socket。

② backlog参数

提示 内核监听队列的 最大长度。监听队列的 长度如果超过 backlog,服务器 将不受理 新的客户连接,客户端也将收到 ECONNREFUSED 错误信息。

5. 接受连接

cpp 复制代码
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
① sockfd参数

是执行过 listen 系统调用的 监听 socket。

② addr参数用来

获取 被接受连接的远端 socket 地址,该 socket 地址的长度 由 addrlen 参数指出。
accept 成功时 返回一个 新的连接 socket,该 socket 唯一地标识了 被接受的 这个连接,服务器可通过读写该 socket 来与被接受连接对应的 客户端通信。
accept失败时返回-1并设置errno。

accept 只是 从监听队列中 取出连接,而 不论连接处于 何种状态(如 ESTABLISHED 状态 和 CLOSE_WAIT 状态),更不关心 任何网络状况的 变化。
我们 把执行过 listen 调用、处于 LISTEN 状态的 socket 称为 监听 socket,而 所有 处于 ESTABLISHED 状态的 socket 则称为连接 socket

6. 发起连接

cpp 复制代码
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
① sockfd参数

由socket系统调用返回一个 socket。

② serv_addr参数

是 服务器监听的 socket 地址。

③ addrlen参数

指定 这个地址的 长度。
connect 成功时 返回 0。一旦 成功建立 连接,sockfd 就唯一地标识了 这个连接,客户端 就可以 通过读写 sockfd 来与服务器通信。
connect 失败则返回 -1 并设置errno。

7. 关闭连接

cpp 复制代码
#include<unistd.h>
int close(int fd);

fd 参数 是待关闭的 socket。

close 系统 调用 并非总是 立即关闭一个连接,而是 将 fd 的引用 计数 减 1。只有 当 fd 的引用 计数为 0 时,才真 正关闭连接。多 进程程序 中,一次 fork 系统调用 默认将 使父进程中 打开的 socket 的引用计数 加 1,因此 必须在 父进程和子进程 中 都对 该 socket 执行 close 调用 才能将连接关闭。

如果 无论如何都要立即 终止连接(而不是将 socket 的引用 计数 减 1),可以使用 shutdown系统调用:

cpp 复制代码
#include<sys/socket.h>
int shutdown(int sockfd, int howto);
① sockfd参数

待关闭的 socket。

② howto参数

决定 shutdown 的行为。


由此可见,shutdown 能够分别关闭 socket 上的 读或写,或者 都关闭。而 close 在关闭连接时只能将 socke t上的读 和 写同时关闭。
shutdown成功时返回0,失败则返回-1并设置errno。

8. 数据读写(TCP)

对文件的 读写操作 read 和 write 同样适用于 socket。但是 socket 编程接口 提供了 几个专门用于 socket 数据读写的 系统调用,它们 增加了对数据 读写的 控制。

cpp 复制代码
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
① recv

读取 sockfd 上的数据,buf 和 len 参数 分别指定 读缓冲区的 位置 和 大小,flags 参数 通常设置为 0 即可。
recv 成功 时 返回实际 读取到的数据的 长度,它可能 小于期望的 长度l en。因此 可能要 多次调用 recv,才能 读取到完整的 数据。
recv 可能 返回 0,这意味着 通信对方 已经关闭 连接了。recv 出错时 返回 -1 并 设置errno。

② send

往 sockfd 上写入数据,buf 和 len参数分别指定 写 缓冲区的位置 和 大小。send 成功时 返回实际 写入的数据的长度,失败则 返回 -1 并设置 errno。
flags 参数 为数据收 发提供了 额外的控制,它可以 取表5-4所示选项中的 一个 或 几个的逻辑或。

9. 地址信息函数

想知道一个连接 socket 的本端 socket 地址,以及 远端的 socket 地址。

cpp 复制代码
#include<sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);

① getsockname

获取 sockfd 对应的 本端 socket 地址,并将其 存储于 address 参数指定的 内存中,该 socket 地址的 长度则 存储于 address_len 参数 指向的 变量中。如果实际 socket 地址的 长度 大于 address 所指内存区的 大小,那么该 socket 地址将 被截断。
getsockname 成功时 返回 0,失败返回 -1 并设置 errno。

② getpeername

获取sockfd对应的远端socket地址。
其参数及 返回值的含义 与 getsockname 的参数及 返回值相同。

10. 网络信息API

socket 地址的 两个要素,即 IP 地址 和 端口号,都是用 数值表示的。这 不便于记忆,也不便于扩展(比如从 IPv4 转移 到 IPv6)。因此 在这我们 ++考虑用 服务名称 来 代替端口号。++

cpp 复制代码
#include<netdb.h>
struct hostent* gethostbyname(const char* name);  // 根据主机名称获取主机的完整信息
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);  // 根据IP地址获取主机的完整信息。
① name参数

指定目标主机的主机名,

② addr参数

指定目标主机的 IP 地址。

③ len参数

指定 addr 所指 IP 地址的长度。

④ type参数

指定 addr 所指 IP 地址的类型,其合法取值包括 AF_INET(用于 IPv4 地址)和 AF_INET6(用于 IPv6 地址)。
gethostbyname 函数通常先 在本地的 /etc/hosts 配置文件中 查找主机,如果 没有找到,再去访 问 DNS 服务器。
这两个函数返回的 都是 hostent 结构体类型的 指针。

cpp 复制代码
#include<netdb.h>
struct hostent {
    char* h_name;        /* 主机名 */
    char** h_aliases;    /* 主机别名列表,可能有多个 */
    int h_addrtype;      /* 地址类型(地址族)*/
    int h_length;        /* 地址长度 */
    char** h_addr_list   /* 按网络字节序列出的主机IP地址列表 */
};

四、高效的事件处理模式

1. Reactor模式

++Reactor 要求主线程(I/O 处理单元)只负责 监听文件描述上 是 否有事件发生,有的 话 就立即 将该事件通知 工作线程(逻辑单元)。除此之外,主线程 不做任何其他 实质性的工作。读写 数据,接受 新的连接,以及处理 客户请求 均在工作线程 中完成。++

(1)主线程 往 epoll 内核事件表 中注册 socket 上的 读就绪事件。
(2)主线程 调用 epoll_wait 等待 socket 上有数据 可读。
(3)当 socket 上有数据 可读时,epoll_wait 通知 主线程。主线程 则将 socket 可读 事件放入 请求队列。
(4)睡眠 在请求队列上的 某个工作线程 被唤醒,它从 socket 读取数据,并 处理 客户请求,然后往 epoll 内核事件表中 注册该 socket 上的 写就绪事件。
(5)主线程 调用 epoll_wait 等待 socket 可写。
(6)当 socket 可写时,epoll_wait 通知 主线程。主线程 将 socket 可写事件 放入 请求队列。
(7)睡眠 在请求队列上 的某个 工作线程 被唤醒,它往 socket 上写入 服务器处理客户 请求的结果。

2. Proactor模式

++Proactor模式将所有 I/O 操作都交给 主线程 和 内核来处理,工作线程 仅仅负责业务逻辑。++

(1)主线程调用 aio_read 函数 向内核 注册 socket 上的 读完成事件,并告诉 内核 用户读缓冲区 的位置,以及 读操作完成 时如何 通知 应用程序(以 信号为例)。
(2)主线程继续处理其他逻辑。
(3)当 socket 上的数据 被读入 用户缓冲区后,内核将 向 应用程序 发送一个信号,以 通知应用 程序数据 已经可用。
(4)应用程序 预先定义好 的信号处理函数 选择一个 工作线程 来 处理客户 请求。工作 线程处理完 客户请求之后,调用 aio_write 函数 向内核 注册 socket 上的 写完成事件,并 告诉内核 用户 写缓冲区的 位置,以及 写操作 完成 时如何 通知 应用程序(以信号为例)。
(5)主线程 继续 处理其他逻辑。
(6)当用户缓冲区的 数据 被写入 socket 之后,内核将 向应用程序 发送一个 信号,以 通知应用程序 数据 已经发送 完毕。
(7)应用程序 预先定义好 的信号处理函数 选择一个 工作线程 来 做善后处理,比如 决定是否关闭 socket。

五、I/O复用

1. select系统调用

select 系统调用的 用途是:++在 一段指定时间 内,监听 用户 感兴趣的 文件描述符上的 可读、可写 和 异常 等事件。++

cpp 复制代码
#include<sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
① nfds参数

指定 被监听的 文件描述符的 总数。它通常 被设置 为 select 监听的 所有文件描述符中 的 最大值 加 1,因为 文件描述符是 从 0 开始计 数的。

② readfds、writefds和exceptfds参数

分别指向 可读、可写 和 异常 等事件 对应的 文件描述符 集合。应用 程序调用 select 函数时,通过这 3 个参数 传入 自己感兴趣的 文件描述符。
select 调用返回时,内核 将修改它们 来通 知应用程序 哪些文件描述符 已经 就绪。
**fd_set 结构体仅 包含一个 整型数组,该数组的 每个元素的每一位(bit)标记 一个 文件描述符。fd_set 能容纳的 文件描述符数量由FD_SETSIZE指 定,这就限制了 select 能同时 处理的文件 描述符 的总量。**我们可以 使用下面的一系列宏来访问 fd_set结 构体中的位:

cpp 复制代码
#include<sys/select.h>
FD_ZERO(fd_set* fdset);               /* 清除 fdset 的所有位 */
FD_SET(int fd, fd_set* fdset);        /* 设置 fdset 的位fd */
FD_CLR(int fd, fd_set* fdset);        /* 清除 fdset 的位fd */
int FD_ISSET(int fd, fd_set* fdset);  /* 测试 fdset 的位fd是否被设置 */
③ timeout参数

用来设置 select 函数的 超时时间。它是一个 timeval 结构类型的 指针,采用 指针参数是 因为内核 将修改它以告诉 应用程序 select 等待了 多久。

cpp 复制代码
struct timeval {
    long tv_sec;   /* 秒数 */
    long tv_usec;  /* 微秒数 */
};

由以上 定义可见,select 给我们提供了 一个微秒级 的 定时方式。如果给 timeout 变量的 tv_sec 成员 和 tv_usec 成员都 传递 0,则 select 将立即 返回。如果给 timeout 传递 NULL,则 select 将一直 阻塞,直到 某个文件描述符 就绪。
select 成功时 返回就绪(可读、可写和异常)文件描述符的 总数。如果 在超时时间 内没有任何 文件描述符 就绪,select 将返回 0。select 失败时 返回 -1 并设置 errno。如果 在 select 等待期间,程序 接收到信号,则 select 立即 返回 -1,并设置 errno 为 EINTR。

2. poll系统调用

poll 系统调用 和 select类似,也是在 指定时间内轮询 一定数量的 文件描述符,以 测试其中是否 有就绪者。

cpp 复制代码
#include<poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
① fds参数

是一个 pollfd 结构类型的 数组,它 指定所有 我们感兴趣的 文件描述符上 发生的 可读、可写和异常等 事件。

cpp 复制代码
struct pollfd {
int fd;         /* 文件描述符 */
short events;   /* 注册的事件 */
short revents;  /* 实际发生的事件,由内核填充 */
};

其中,fd 成员 指定文件 描述符;events 成员告诉 poll 监听 fd 上的 哪些事件,它是 一系列事件的 按位或;revents 成员则 由内核修改,以通知应用 程序 fd 上实际发生了 哪些事件。

② nfds参数

指定被监听事件集合 fds 的大小。

③ timeout参数

指定 poll 的超时值,单位是 毫秒。当 timeout 为 -1 时,poll 调用将 永远阻塞,直到 某个 事件发生;当 timeout 为 0 时,poll 调用 将立即 返回。
poll系统调用的返回值的含义与select相同。

3. epoll系列系统调用

**++epoll 使用一组函数 来 完成任务,而 不是 单个函数。 epoll 把用户关心的 文件描述符 上的 事件 放在内核里的 一个事件表 中,从而 无须像 select 和 poll 那样 每次调用都要 重复 传入 文件描述符集 或 事件集。但 epoll 需要 使用一个 额外的 文件描述符,来唯 一标识 内核中的 这个事件表。++**这个文件描述符 使用 如下 epoll_create 函数来创建:

cpp 复制代码
#include<sys/epoll.h>
int epoll_create(int size)

以下函数用来操作 epoll 的内核事件表:

cpp 复制代码
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)
① fd参数

是要操作的文件描 述符。

② op参数

指定 操作类型。

③ event参数

指定事件,它是epoll_event结构指针类型。

cpp 复制代码
struct epoll_event {
    __uint32_t events;    /* epoll事件 */
    epoll_data_t data;    /* 用户数据 */
};

其中 events 成员描述 事件类型。epoll支 持的 事件类型 和 poll 基本相同。data 成员 用于存储用户 数据。

cpp 复制代码
typedef union epoll_data {
    void* ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

epoll_data_t 是一个联合体,其 4 个成员中 使用最多的是 fd,它 指定事件所 从属的 目标文件描述符
epoll_ctl 成功时 返回 0,失败则 返回 -1 并设置 errno。


​​​​​​​关于 epoll 的剩余内容,近期学习后补充。

相关推荐
_.Switch几秒前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
2401_850410831 分钟前
文件系统和日志管理
linux·运维·服务器
qq_254674413 分钟前
工作流初始错误 泛微提交流程提示_泛微协同办公平台E-cology8.0版本后台维护手册(11)–系统参数设置
网络
JokerSZ.5 分钟前
【基于LSM的ELF文件安全模块设计】参考
运维·网络·安全
XMYX-035 分钟前
使用 SSH 蜜罐提升安全性和记录攻击活动
linux·ssh
UestcXiye1 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
亦枫Leonlew1 小时前
三维测量与建模笔记 - 3.3 张正友标定法
笔记·相机标定·三维重建·张正友标定法
考试宝1 小时前
国家宠物美容师职业技能等级评价(高级)理论考试题
经验分享·笔记·职场和发展·学习方法·业界资讯·宠物
一只哒布刘2 小时前
NFS服务器
运维·服务器