网络编程之API函数

网络编程中的API(应用程序编程接口)函数是开发人员用于实现网络通信的关键工具。这些API函数提供了各种功能,包括套接字创建、连接管理、数据传输、错误处理等。以下是一些常见的网络编程API函数,主要基于C语言中的BSD套接字API和Windows套接字API(Winsock)。

一、通用API函数

1.1 socket()

socket()函数是用于创建一个新的套接字(socket)的系统调用,在网络编程中广泛使用。以下是对socket()函数的详细解释:

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
int socket(int domain, int type, int protocol);

2、参数解释

  1. domain:指定套接字使用的协议族(Protocol Family)。常用的协议族有:

    • AF_INET:IPv4互联网协议。

    • AF_INET6:IPv6互联网协议。

    • AF_UNIXAF_LOCAL:本地通信协议(在同一台机器上的进程间通信)。

  2. type:指定套接字的类型。常用的套接字类型有:

    • SOCK_STREAM:流式套接字,提供双向的、可靠的、基于连接的字节流。通常用于TCP协议。

    • SOCK_DGRAM:数据报套接字,支持无连接的、固定最大长度的消息。通常用于UDP协议。

    • SOCK_RAW:原始套接字,允许对底层网络协议(如IP或ICMP)的直接访问。

  3. protocol:通常设置为0,表示选择由domaintype参数指定的默认协议。在某些情况下,如果domain支持多种协议,可以通过这个参数来指定特定的协议。

3、返回值

  • 成功时,socket()函数返回一个非负整数,即套接字的文件描述符(socket descriptor),该描述符在后续的套接字操作中用于标识这个套接字。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。

4、常见错误

  • EACCES:权限被拒绝,无法创建指定类型的套接字。

  • EAFNOSUPPORT:不支持指定的地址类型。

  • EINVAL:无效的参数,如不支持的协议或协议不可用。

  • EMFILE:进程文件表溢出,无法打开更多文件(包括套接字)。

  • ENFILE:系统文件表溢出,达到系统允许打开的文件数量上限。

  • ENOBUFSENOMEM:内存不足,无法分配套接字所需的资源。

  • EPROTONOSUPPORT:在指定的协议族中不存在指定的协议类型。

5、使用示例

以下是一个创建IPv4 TCP流式套接字的示例:

int sockfd;  
sockfd = socket(AF_INET, SOCK_STREAM, 0);  
if (sockfd < 0) {  
    // 错误处理  
    perror("socket creation failed");  
    exit(EXIT_FAILURE);  
}

在这个示例中,sockfd将保存新创建的套接字的文件描述符。如果socket()函数失败,将打印错误信息并退出程序。

6、注意事项

  • 在调用socket()函数之前,必须包含头文件<sys/types.h><sys/socket.h>

  • 创建套接字后,通常需要使用bind()函数将套接字绑定到一个本地地址和端口上(对于服务器套接字),或者使用connect()函数连接到远程地址和端口上(对于客户端套接字)。

  • 套接字描述符是一个文件描述符,因此可以使用标准的文件I/O函数(如read()write()close()等)对其进行操作,但更常用的是网络I/O函数(如send()recv()等)。

综上所述,socket()函数是网络编程中创建套接字的基础函数,通过指定协议族、套接字类型和协议来创建一个新的套接字。

1.2 bind()

bind()函数在网络编程中用于将一个套接字(socket)与特定的IP地址和端口号绑定。这样,当服务器监听这个套接字时,它能够接收发送到该IP地址和端口号的网络连接请求。以下是对bind()函数的详细解释:

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2、参数解释

  1. sockfd:套接字文件描述符,由socket()函数返回。这个描述符标识了一个打开的套接字。

  2. addr:指向sockaddr结构体的指针,该结构体包含了要绑定的IP地址和端口号。在实际使用中,通常会使用sockaddr_in(对于IPv4)或sockaddr_in6(对于IPv6)结构体,并将它们强制转换为sockaddr类型。

  3. addrlenaddr参数所指向的地址结构的长度,通常以字节为单位。对于sockaddr_in,这个长度通常是sizeof(struct sockaddr_in);对于sockaddr_in6,则是sizeof(struct sockaddr_in6)

3、返回值

  • 成功时,bind()函数返回0。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。

4、常见错误

  • EADDRINUSE:地址已经在使用中。这通常意味着另一个套接字已经绑定到了请求的IP地址和端口号上。

  • EADDRNOTAVAIL:指定的地址不可用。例如,尝试绑定到一个不属于本机的IP地址。

  • EINVALsockfd不是一个有效的文件描述符,或者addrlen无效。

  • ENOBUFSENOMEM:系统内存不足,无法完成操作。

  • ENOTSOCKsockfd不是一个套接字文件描述符。

5、使用示例

以下是一个将套接字绑定到特定IP地址和端口号的示例:

struct sockaddr_in server_addr;  
int sockfd;  
// 假设sockfd已经通过socket()函数成功创建  
  
// 清除结构体内容  
memset(&server_addr, 0, sizeof(server_addr));  
  
// 设置地址族为AF_INET(IPv4)  
server_addr.sin_family = AF_INET;  
  
// 设置IP地址(这里使用INADDR_ANY表示监听所有IPv4地址)  
server_addr.sin_addr.s_addr = INADDR_ANY;  
  
// 设置端口号(注意:这里使用的是网络字节序)  
server_addr.sin_port = htons(PORT_NUMBER); // PORT_NUMBER是你要绑定的端口号  
  
// 绑定套接字到指定的IP地址和端口号  
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {  
    // 错误处理  
    perror("bind failed");  
    close(sockfd); // 关闭套接字  
    exit(EXIT_FAILURE);  
}

在这个示例中,server_addr结构体被设置为监听所有IPv4地址上的特定端口号。bind()函数被调用以将套接字sockfd绑定到这个地址和端口上。如果bind()失败,将打印错误信息、关闭套接字并退出程序。

6、注意事项

  • 在调用bind()函数之前,必须先调用socket()函数创建一个套接字,并确保sockfd是有效的。

  • 对于服务器来说,通常会在bind()之后调用listen()函数来监听套接字上的连接请求。

  • 客户端通常不需要调用bind()函数,因为它们在连接时会由系统自动分配一个临时的端口号。然而,在某些情况下(如使用特定端口号的客户端应用程序),客户端也可以调用bind()函数。

  • 绑定的IP地址和端口号必须是系统允许的,并且没有被其他应用程序占用。

1.3 listen()

listen()函数在网络编程中用于将套接字设置为监听模式,以便接受传入的连接请求。这个函数通常与服务器套接字一起使用,在调用bind()函数将套接字绑定到特定的IP地址和端口号之后调用。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
int listen(int sockfd, int backlog);

2、参数解释

  1. sockfd:套接字文件描述符,由socket()函数返回。这个描述符标识了一个打开的、已经通过bind()函数绑定到特定IP地址和端口号的套接字。

  2. backlog:指定内核为相应套接字排队的最大连接数。这个值限制了等待处理的连接请求的数量。如果队列已满,新的连接请求可能会被拒绝或丢弃,具体行为取决于系统的实现。

3、返回值

  • 成功时,listen()函数返回0。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。

4、常见错误

  • EBADFsockfd不是一个有效的文件描述符,或者它不是一个套接字。

  • EINVAL:套接字未绑定(即未调用bind()函数),或者套接字已经被设置为监听模式。

  • ENOTSOCKsockfd不是一个套接字文件描述符。

  • EOPNOTSUPP:套接字类型不支持监听操作(例如,尝试在SOCK_DGRAM套接字上调用listen())。

5、使用示例

以下是一个将套接字设置为监听模式的示例:

int sockfd;  
// 假设sockfd已经通过socket()和bind()函数成功创建和绑定  
  
// 将套接字设置为监听模式,最多允许backlog个连接请求排队  
if (listen(sockfd, SOMAXCONN) < 0) {  
    // 错误处理  
    perror("listen failed");  
    close(sockfd); // 关闭套接字  
    exit(EXIT_FAILURE);  
}

在这个示例中,listen()函数被调用以将套接字sockfd设置为监听模式,并允许最多SOMAXCONN个连接请求排队。SOMAXCONN是一个在头文件<sys/socket.h>中定义的常量,表示系统允许的最大连接数队列长度。如果希望使用自定义的队列长度,可以将其替换为所需的整数值。

6、注意事项

  • 在调用listen()函数之前,必须先调用socket()bind()函数。

  • backlog参数的值应该根据服务器的处理能力来设置。如果设置得太小,可能会导致连接请求被拒绝;如果设置得太大,可能会消耗过多的系统资源。

  • 在调用listen()函数之后,服务器通常会使用accept()函数来接受传入的连接请求。

  • 监听套接字通常不会直接用于数据传输,而是用于接受新的连接。一旦接受了一个连接,服务器通常会创建一个新的套接字来与该连接进行通信,而监听套接字则继续接受其他连接请求。

1.4 accept()

accept()函数在网络编程中用于从已完成连接队列的头部返回下一个已完成连接。当服务器套接字处于监听状态时,如果有客户端尝试连接到该服务器,并且连接成功建立,那么这个连接就会被添加到服务器的已完成连接队列中。accept()函数的作用就是从这个队列中取出一个连接,并返回一个新的套接字文件描述符,用于与客户端进行通信。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

2、参数解释

  1. sockfd:监听套接字的文件描述符,该套接字是通过socket()函数创建,并通过bind()函数绑定到特定IP地址和端口号,最后通过listen()函数设置为监听模式的。

  2. addr(可选):指向sockaddr结构体的指针,用于接收客户端的IP地址和端口号信息。如果不需要这个信息,可以设置为NULL

  3. addrlen(可选):指向socklen_t变量的指针,该变量在调用accept()之前应该被初始化为addr所指向的地址结构体的长度。在accept()返回后,该变量会被更新为实际存储在addr中的地址长度。如果addrNULL,则addrlen也可以是NULL

3、返回值

  • 成功时,accept()函数返回一个新的套接字文件描述符,用于与客户端进行通信。这个描述符与监听套接字sockfd是分开的,sockfd继续用于接受其他连接请求。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。

4、常见错误

  • EBADFsockfd不是一个有效的文件描述符,或者它不是一个监听套接字。

  • EAGAINEWOULDBLOCK:套接字是非阻塞的,并且没有已完成连接可供接受。

  • EINTR:调用被信号中断。

  • EINVALsockfd没有设置为监听模式,或者addrlen指针指向的值无效。

  • EMFILE:进程已经打开了文件描述符的最大数量。

  • ENFILE:系统级别的文件描述符限制已达到。

  • ENOTSOCKsockfd不是一个套接字文件描述符。

  • EOPNOTSUPP:套接字类型不支持接受操作(这在标准套接字上通常不会发生)。

5、使用示例

以下是一个使用accept()函数接受客户端连接的示例:

int listen_sockfd, new_sockfd;  
struct sockaddr_in client_addr;  
socklen_t client_addrlen = sizeof(client_addr);  
// 假设listen_sockfd已经通过socket(), bind(), 和 listen()函数成功创建、绑定和设置为监听模式  
  
// 接受一个客户端连接  
new_sockfd = accept(listen_sockfd, (struct sockaddr *)&client_addr, &client_addrlen);  
if (new_sockfd < 0) {  
    // 错误处理  
    perror("accept failed");  
    close(listen_sockfd); // 关闭监听套接字  
    exit(EXIT_FAILURE);  
}  
  
// 此时,new_sockfd可以用于与客户端进行通信  
// listen_sockfd继续用于接受其他客户端连接

6、注意事项

  • accept()函数是阻塞的,这意味着它会等待直到有一个连接被接受。如果希望非阻塞地接受连接,可以将套接字设置为非阻塞模式,或者使用select()poll(), 或 epoll()等函数来检查是否有连接可读。

  • 接受连接后,服务器通常会使用新的套接字文件描述符new_sockfd与客户端进行通信,而监听套接字listen_sockfd则继续用于接受其他连接请求。

  • 在多线程服务器中,每个接受到的连接可以由一个单独的线程来处理。在这种情况下,每个线程都会有自己的套接字文件描述符来与客户端通信。

  • 在处理完与客户端的通信后,服务器应该关闭与客户端通信的套接字文件描述符(new_sockfd),以释放系统资源。但是,监听套接字(listen_sockfd)应该保持打开状态,以便继续接受新的连接请求。

1.5 connect()

connect()函数在网络编程中用于客户端主动发起与服务器端的连接请求。这个函数通常与套接字(socket)一起使用,在调用socket()函数创建一个套接字之后,客户端会调用connect()函数来尝试与服务器建立连接。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2、参数解释

  1. sockfd:客户端套接字的文件描述符,该套接字是通过socket()函数创建的。

  2. addr:指向sockaddr结构体(或其兼容结构体,如sockaddr_in用于IPv4)的指针,包含了服务器的IP地址和端口号信息。

  3. addrlenaddr所指向的地址结构体的长度,通常以字节为单位。这个值可以通过sizeof运算符来获取。

3、返回值

  • 成功时,connect()函数返回0。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。常见的错误包括网络不可达、连接被拒绝、超时等。

4、常见错误

  • EBADFsockfd不是一个有效的文件描述符,或者它不是一个套接字。

  • EAFNOSUPPORT:地址族不被支持。

  • EADDRINUSE:本地地址已在使用中(通常与bind()函数相关,但在某些情况下connect()也可能遇到)。

  • EADDRNOTAVAIL:无法分配请求的地址(例如,尝试绑定到一个不在网络接口上的IP地址)。

  • ECONNREFUSED:连接被拒绝,因为目标主机上的服务器没有运行,或者服务器拒绝接受连接。

  • EINPROGRESS:连接操作正在进行中(非阻塞模式)。在这种情况下,连接的结果将通过select()poll(), 或 epoll()等函数来通知。

  • EINTR:调用被信号中断。

  • EINVAL:无效的参数,如无效的套接字类型或无效的地址长度。

  • EISCONN:套接字已经连接。

  • ENETUNREACH:网络不可达。

  • ENOTSOCKsockfd不是一个套接字文件描述符。

  • ETIMEDOUT:连接尝试超时。

5、使用示例

以下是一个使用connect()函数尝试与服务器建立连接的示例:

int sockfd;  
struct sockaddr_in server_addr;  
  
// 假设sockfd已经通过socket()函数成功创建  
// 填充服务器地址信息  
server_addr.sin_family = AF_INET; // 使用IPv4地址  
server_addr.sin_port = htons(SERVER_PORT); // 服务器端口号(需要转换为网络字节序)  
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr); // 服务器IP地址(需要转换为网络字节序)  
  
// 尝试连接到服务器  
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {  
    // 错误处理  
    perror("connect failed");  
    close(sockfd); // 关闭套接字  
    exit(EXIT_FAILURE);  
}  
  
// 连接成功,可以进行通信了

6、注意事项

  • 在调用connect()函数之前,必须先调用socket()函数来创建一个套接字。

  • connect()函数可以是阻塞的,也可以是非阻塞的。在阻塞模式下,connect()会等待直到连接成功或失败。在非阻塞模式下,connect()会立即返回,并可能通过EINPROGRESS错误码来指示连接操作正在进行中。在这种情况下,可以使用select()poll(), 或 epoll()等函数来检查连接的状态。

  • 如果connect()函数返回EINPROGRESS错误码,并且使用了非阻塞套接字,那么连接的结果将通过套接字的“写就绪”或“异常就绪”条件来通知。可以通过检查这些条件来确定连接是否成功建立,或者是否发生了错误。

  • 在处理完与服务器的通信后,客户端应该关闭套接字以释放系统资源。

1.6 send() 

send()函数在网络编程中用于在已连接的套接字上发送数据。这个函数通常与socket()connect()(对于客户端)或socket()bind()listen()accept()(对于服务器)等函数一起使用,以建立通信信道,并随后发送和接收数据。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

2、参数解释

  1. sockfd:已连接的套接字文件描述符。

  2. buf:指向包含要发送数据的缓冲区的指针。

  3. len:要发送的数据的字节数。

  4. flags:通常设置为0,但也可以指定一些标志来修改send()的行为。例如,MSG_DONTWAIT(或MSG_NONBLOCK)标志会使send()在非阻塞模式下工作,如果数据不能立即发送,它将返回而不是阻塞。

3、返回值

  • 成功时,send()返回实际发送的字节数,这个值可能小于请求发送的字节数(len),但这种情况通常发生在非阻塞套接字或管道等特殊情况下。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。常见的错误包括套接字未连接、连接已关闭、发送缓冲区已满等。

4、常见错误

  • EBADFsockfd不是一个有效的文件描述符,或者它不是一个套接字。

  • EDESTADDRREQ:套接字是未连接的(通常用于数据报套接字)。

  • EFAULTbuf指针指向的内存区域不可访问。

  • EINTR:调用被信号中断。

  • EINVAL:无效的参数,如无效的套接字类型或无效的标志。

  • EISCONN:套接字已经连接(这个错误通常不会由send()返回,但可能在某些系统上出现)。

  • EMSGSIZE:要发送的数据太大,无法一次性发送(这通常不会发生,因为TCP会分段大数据包)。

  • ENOTCONN:套接字未连接。

  • EPIPE:连接的写端已关闭(例如,对方调用了close()shutdown()的写操作)。

  • EWOULDBLOCK:套接字是非阻塞的,并且数据不能立即发送。

5、使用示例

以下是一个使用send()函数发送数据的示例:

int sockfd; // 假设sockfd已经通过socket()和connect()(对于客户端)或accept()(对于服务器)成功创建并连接  
char *message = "Hello, server!";  
ssize_t bytes_sent;  
  
// 发送数据  
bytes_sent = send(sockfd, message, strlen(message), 0);  
if (bytes_sent < 0) {  
    // 错误处理  
    perror("send failed");  
    close(sockfd); // 关闭套接字  
    exit(EXIT_FAILURE);  
} else if (bytes_sent < strlen(message)) {  
    // 处理部分发送的情况(通常不会发生,除非是非阻塞套接字)  
    fprintf(stderr, "Partial send: %zd bytes sent\n", bytes_sent);  
    // 可能需要再次发送剩余的数据  
}  
  
// 发送成功

6、注意事项

  • send()函数在发送数据时是可靠的,但并不意味着数据会立即到达对方。TCP协议会确保数据的顺序和完整性,但可能会有一些延迟。

  • 对于大数据量的发送,可能需要考虑使用循环来发送数据,直到所有数据都发送完毕。这通常不会发生在阻塞套接字上,因为send()会阻塞直到所有数据都被发送或发生错误。但在非阻塞套接字上,这是常见的做法。

  • 在处理完与服务器的通信后,客户端应该关闭套接字以释放系统资源。服务器通常在处理完所有客户端连接后关闭监听套接字和每个客户端连接套接字。

  • send()函数与write()函数在已连接的套接字上是等效的,但send()提供了更多的标志选项,并且通常在网络编程中更常用。

1.7 sendto()

sendto()函数在网络编程中用于在无需建立连接的套接字(如数据报套接字)上发送数据,或者用于在已连接的套接字上发送数据但希望指定额外的发送选项。与send()函数不同,sendto()允许直接指定目标地址,这使得它特别适用于UDP等无连接协议。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,  
               const struct sockaddr *dest_addr, socklen_t addrlen);

2、参数解释

  1. sockfd:套接字文件描述符。对于UDP套接字,这个描述符通常是通过socket(AF_INET, SOCK_DGRAM, 0)或类似调用创建的。

  2. buf:指向包含要发送数据的缓冲区的指针。

  3. len:要发送的数据的字节数。

  4. flags:通常设置为0,但也可以指定一些标志来修改sendto()的行为。例如,MSG_DONTWAIT(或MSG_NONBLOCK)标志会使sendto()在非阻塞模式下工作。

  5. dest_addr:指向sockaddr结构体(或其兼容结构体,如sockaddr_in用于IPv4)的指针,包含了目标地址和端口号信息。这个参数对于已连接的套接字是可选的,但对于未连接的套接字是必需的。

  6. addrlendest_addr所指向的地址结构体的长度,通常以字节为单位。这个值可以通过sizeof运算符来获取。

3、返回值

  • 成功时,sendto()返回实际发送的字节数,这个值可能小于请求发送的字节数(len),但这种情况通常发生在非阻塞套接字或特殊情况下。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。常见的错误包括无效的套接字、无效的地址、发送缓冲区已满等。

4、常见错误

  • EBADFsockfd不是一个有效的文件描述符,或者它不是一个套接字。

  • EDESTADDRREQ:套接字是未连接的,并且没有提供目标地址(对于需要连接的协议,如TCP,这通常不会发生在使用sendto()的情况下,因为TCP套接字通常不会与sendto()一起使用)。

  • EFAULTbufdest_addr指针指向的内存区域不可访问。

  • EINTR:调用被信号中断。

  • EINVAL:无效的参数,如无效的套接字类型、无效的标志或无效的地址长度。

  • EMSGSIZE:要发送的数据太大,无法一次性发送(对于UDP,这通常意味着数据超过了协议的最大数据报大小)。

  • ENOTSOCKsockfd不是一个套接字文件描述符。

  • EWOULDBLOCK:套接字是非阻塞的,并且数据不能立即发送。

5、使用示例

以下是一个使用sendto()函数发送UDP数据报的示例:

int sockfd;  
struct sockaddr_in dest_addr;  
char *message = "Hello, UDP server!";  
ssize_t bytes_sent;  
  
// 假设sockfd已经通过socket(AF_INET, SOCK_DGRAM, 0)成功创建  
// 填充目标地址信息  
dest_addr.sin_family = AF_INET; // 使用IPv4地址  
dest_addr.sin_port = htons(DEST_PORT); // 目标端口号(需要转换为网络字节序)  
inet_pton(AF_INET, DEST_IP, &dest_addr.sin_addr); // 目标IP地址(需要转换为网络字节序)  
  
// 发送数据  
bytes_sent = sendto(sockfd, message, strlen(message), 0,  
                    (struct sockaddr *)&dest_addr, sizeof(dest_addr));  
if (bytes_sent < 0) {  
    // 错误处理  
    perror("sendto failed");  
    close(sockfd); // 关闭套接字  
    exit(EXIT_FAILURE);  
}  
  
// 发送成功

6、注意事项

  • sendto()函数通常与UDP套接字一起使用,因为它允许直接指定目标地址。对于TCP套接字,通常使用send()函数,因为TCP连接是面向连接的,目标地址在连接建立时就已经确定。

  • 在使用sendto()时,必须确保提供的目标地址是有效的,并且套接字已经通过socket()函数成功创建。

  • 对于大数据量的发送,可能需要考虑使用循环来发送数据,直到所有数据都发送完毕。但是,由于UDP是无连接的、不可靠的协议,发送的数据报可能会丢失、重复或乱序到达,因此通常不建议在UDP上发送大数据。

  • 在处理完通信后,应该关闭套接字以释放系统资源。

1.8 recv() 

recv()函数在网络编程中用于从已连接的套接字接收数据。它通常与send()函数配对使用,以在客户端和服务器之间建立双向通信。recv()函数可以读取套接字接收缓冲区中的数据,并将其存储到用户提供的缓冲区中。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

2、参数解释

  1. sockfd:已连接的套接字文件描述符。

  2. buf:指向接收数据的缓冲区的指针。这个缓冲区应该有足够的空间来存储可能接收到的数据。

  3. len:缓冲区的大小,即可以接收的最大字节数。

  4. flags:通常设置为0,但也可以指定一些标志来修改recv()的行为。例如,MSG_PEEK标志允许程序查看数据而不从接收缓冲区中移除它,MSG_WAITALL标志(在某些实现中可用)指示recv()应该阻塞,直到接收到请求长度的数据或发生错误。

3、返回值

  • 成功时,recv()返回实际接收到的字节数,这个值可能小于请求接收的字节数(len),特别是当套接字设置为非阻塞模式或接收到的数据小于请求大小时。

  • 当连接正常关闭时,返回0。这表示对方已经调用了close()函数或shutdown()函数的读操作,并且没有更多的数据可以接收。

  • 失败时,返回-1,并设置全局变量errno来指示错误原因。常见的错误包括套接字未连接、连接已关闭、接收缓冲区为空等。

4、常见错误

  • EBADFsockfd不是一个有效的文件描述符,或者它不是一个套接字。

  • ECONNRESET:连接被对方重置(例如,对方调用了close()shutdown()的写操作,并且TCP连接进入了TIME_WAIT状态)。

  • EINTR:调用被信号中断。

  • EINVAL:无效的参数,如无效的套接字类型或无效的标志。

  • ENOTCONN:套接字未连接。

  • EWOULDBLOCK:套接字是非阻塞的,并且没有数据可以立即接收。

5、使用示例

以下是一个使用recv()函数接收数据的示例:

int sockfd; // 假设sockfd已经通过socket()、connect()(客户端)或accept()(服务器)成功创建并连接  
char buffer[BUFFER_SIZE];  
ssize_t bytes_received;  
  
// 接收数据  
bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0);  
if (bytes_received < 0) {  
    // 错误处理  
    perror("recv failed");  
    close(sockfd); // 关闭套接字  
    exit(EXIT_FAILURE);  
} else if (bytes_received == 0) {  
    // 连接已关闭  
    printf("Connection closed by peer\n");  
    close(sockfd); // 关闭套接字  
} else {  
    // 接收数据成功,处理数据  
    buffer[bytes_received] = '\0'; // 确保缓冲区以空字符结尾(如果接收的是字符串)  
    printf("Received: %s\n", buffer);  
    // ... 进一步处理数据 ...  
}

6、注意事项

  • recv()函数在接收数据时是阻塞的,除非套接字被设置为非阻塞模式或使用了select()poll()epoll()等函数来检查套接字是否可读。

  • 在处理接收到的数据时,应该注意缓冲区的大小,并确保不会溢出。如果接收到的数据可能非常大,可能需要使用循环来多次调用recv()函数,直到所有数据都被接收完毕。

  • recv()返回0时,表示连接已经被对方关闭。在这种情况下,应该关闭自己的套接字并释放相关资源。

  • 在使用recv()之前,应该确保套接字已经成功连接。对于服务器来说,这通常意味着已经通过accept()函数接受了一个连接请求。对于客户端来说,这通常意味着已经通过connect()函数成功连接到了服务器。

1.9 recvfrom()

recvfrom() 函数在网络编程中用于从套接字接收数据,与 sendto() 函数相对应,它通常用于无连接的套接字(如 UDP 套接字)上。与 recv() 不同,recvfrom() 允许程序指定一个缓冲区来接收数据,并且它还会返回发送方的地址信息。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,  
                 struct sockaddr *src_addr, socklen_t *addrlen);

2、参数解释

  1. sockfd:套接字文件描述符,通常是通过 socket(AF_INET, SOCK_DGRAM, 0) 或类似调用创建的 UDP 套接字。

  2. buf:指向接收数据的缓冲区的指针。

  3. len:缓冲区的大小,即可以接收的最大字节数。

  4. flags:通常设置为 0,但也可以指定一些标志来修改 recvfrom() 的行为。例如,MSG_PEEK 标志允许程序查看数据而不从接收缓冲区中移除它。

  5. src_addr:指向 sockaddr 结构体(或其兼容结构体,如 sockaddr_in)的指针,用于存储发送方的地址信息。如果不需要这个信息,可以将其设置为 NULL

  6. addrlen:指向一个变量的指针,该变量在调用前应该被设置为 src_addr 所指向的地址结构体的长度(通常通过 sizeof 运算符获取)。在调用后,这个变量将被更新为实际存储在 src_addr 中的地址的长度。如果 src_addr 是 NULL,则 addrlen 也应该是 NULL

3、返回值

  • 成功时,recvfrom() 返回实际接收到的字节数。

  • 当没有数据可接收且套接字是非阻塞的时,返回 -1 并设置 errno 为 EWOULDBLOCK

  • 当连接被对方重置或发生其他错误时,也返回 -1 并设置相应的 errno 值。

4、常见错误

  • EBADFsockfd 不是一个有效的文件描述符,或者它不是一个套接字。

  • EFAULTbufsrc_addr 或 addrlen 指针指向的内存区域不可访问。

  • EINTR:调用被信号中断。

  • EINVAL:无效的参数,如无效的套接字类型、无效的标志或无效的地址长度。

  • ENOTSOCKsockfd 不是一个套接字文件描述符。

  • EMSGSIZE:接收到的数据报太大,无法存储在提供的缓冲区中(这通常不会发生,因为 UDP 数据报的大小有限制,并且 recvfrom() 会根据缓冲区大小截断数据)。

5、使用示例

以下是一个使用 recvfrom() 函数接收 UDP 数据报的示例:

int sockfd;  
struct sockaddr_in src_addr;  
socklen_t addrlen = sizeof(src_addr);  
char buffer[BUFFER_SIZE];  
ssize_t bytes_received;  
  
// 假设 sockfd 已经通过 socket(AF_INET, SOCK_DGRAM, 0) 成功创建  
  
// 接收数据  
bytes_received = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,  
                          (struct sockaddr *)&src_addr, &addrlen);  
if (bytes_received < 0) {  
    // 错误处理  
    perror("recvfrom failed");  
    close(sockfd);  
    exit(EXIT_FAILURE);  
} else {  
    // 接收数据成功,处理数据  
    buffer[bytes_received] = '\0'; // 确保缓冲区以空字符结尾(如果接收的是字符串)  
    printf("Received %zd bytes from %s:%d\n", bytes_received,  
           inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port));  
    printf("Data: %s\n", buffer);  
    // ... 进一步处理数据 ...  
}

6、注意事项

  • recvfrom() 通常与 UDP 套接字一起使用,因为它允许程序接收数据并获取发送方的地址信息。

  • 在使用 recvfrom() 之前,应该确保套接字已经通过 socket() 函数成功创建。

  • 由于 UDP 是无连接的协议,因此每次调用 recvfrom() 都可能接收到来自不同发送方的数据。

  • 如果不需要获取发送方的地址信息,可以将 src_addr 和 addrlen 参数设置为 NULL。但是,在大多数情况下,了解发送方的地址是有用的,因为它允许程序根据发送方做出不同的响应。

  • 当处理完接收到的数据后,应该根据需要关闭套接字以释放系统资源。对于 UDP 套接字来说,这通常发生在程序结束或不再需要接收数据时。

1.8 close() 和 closesocket()

  • 描述:关闭套接字。close()用于POSIX系统,closesocket()用于Windows。

  • 参数

    • sockfd:套接字描述符。

  • 返回值:成功时返回0,失败时返回-1并设置errno(对于close())。

二、高级API函数

2.1 getaddrinfo()

getaddrinfo() 函数是网络编程中用于将主机名和服务名解析为套接字地址结构(如 sockaddr_in 或 sockaddr_in6)的重要工具。它提供了一个比传统的 gethostbyname() 和 getservbyname() 函数更灵活、更强大的接口,并且支持 IPv6。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
#include <netdb.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
  
int getaddrinfo(const char *node, const char *service,  
                const struct addrinfo *hints,  
                struct addrinfo **res);

2、参数解释

  1. node:指向主机名的字符串指针。可以是域名(如 "http://www.example.com"),也可以是点分十进制的 IPv4 地址字符串(如 "192.168.1.1")或 IPv6 地址字符串(如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334")。如果为 NULL,则 service 必须指定一个已知的服务,且返回的地址将用于本地绑定(即 INADDR_ANY 或 IN6ADDR_ANY_INIT)。

  2. service:指向服务名的字符串指针(如 "http" 或 "ftp"),也可以是十进制表示的端口号字符串(如 "80")。如果为 NULL,则必须提供 node 参数。

  3. hints:指向 addrinfo 结构体的指针,用于指定额外的参数,如地址族(AF_INET 或 AF_INET6)、套接字类型(SOCK_STREAM 或 SOCK_DGRAM)和协议(通常为 0,表示自动选择)。如果为 NULL,则使用默认设置。

  4. res:指向 addrinfo 结构体指针的指针,用于存储 getaddrinfo() 返回的结果链表。调用后,这个链表包含了一个或多个 addrinfo 结构体,每个结构体都表示一个可能的地址。

3、返回值

  • 成功时,返回 0。

  • 失败时,返回非零值,并设置全局变量 errno 以指示错误原因。常见的错误包括 EAI_NONAME(主机名或服务名未知)、EAI_FAMILY(地址族不被支持)、EAI_MEMORY(内存分配失败)等。

4、addrinfo 结构体

addrinfo 结构体定义如下:

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;    // 0 or IPPROTO_TCP, IPPROTO_UDP  
    socklen_t        ai_addrlen;     // Length of ai_addr  
    struct sockaddr *ai_addr;        // Pointer to sockaddr  
    char            *ai_canonname;   // Canonical name for node  
    struct addrinfo *ai_next;        // Pointer to next in list  
};

5、使用示例

以下是一个使用 getaddrinfo() 函数解析主机名和服务名,并打印出所有可能的地址的示例:

#include <sys/types.h>  
#include <sys/socket.h>  
#include <netdb.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <arpa/inet.h>  
  
int main() {  
    struct addrinfo hints, *res, *p;  
    int status;  
    char ipstr[INET6_ADDRSTRLEN];  
  
    memset(&hints, 0, sizeof hints);  
    hints.ai_family = AF_UNSPEC;     // AF_INET or AF_INET6 to force version  
    hints.ai_socktype = SOCK_STREAM; // SOCK_STREAM or SOCK_DGRAM  
  
    if ((status = getaddrinfo("www.example.com", "http", &hints, &res)) != 0) {  
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));  
        return 1;  
    }  
  
    printf("IP addresses for www.example.com:\n\n");  
  
    for (p = res; p != NULL; p = p->ai_next) {  
        void *addr;  
        char *ipver;  
  
        // Get the pointer to the address itself,  
        // different fields in IPv4 and IPv6:  
        if (p->ai_family == AF_INET) { // IPv4  
            struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;  
            addr = &(ipv4->sin_addr);  
            ipver = "IPv4";  
        } else { // IPv6  
            struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;  
            addr = &(ipv6->sin6_addr);  
            ipver = "IPv6";  
        }  
  
        // Convert the IP to a string and print it:  
        inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);  
        printf("  %s: %s\n", ipver, ipstr);  
    }  
  
    freeaddrinfo(res); // Free the linked list  
  
    return 0;  
}

6、注意事项

  • 在使用 getaddrinfo() 之前,应该包含必要的头文件,如 <sys/types.h><sys/socket.h> 和 <netdb.h>

  • 调用 getaddrinfo() 后,应该检查返回值以确定是否成功。如果失败,可以使用 gai_strerror() 函数将错误代码转换为人类可读的字符串。

  • 使用完 getaddrinfo() 返回的地址链表后,应该调用 freeaddrinfo() 函数来释放链表占用的内存。

  • getaddrinfo() 支持协议无关性编程,即可以通过设置 hints.ai_family 为 AF_UNSPEC 来同时获取 IPv4 和 IPv6 地址。

  • 当指定服务名时,如果服务名在 /etc/services 文件中不存在,则可能需要提供端口号字符串。

2.2 getnameinfo()

getnameinfo() 函数是网络编程中用于将套接字地址结构(如 sockaddr_in 或 sockaddr_in6)转换为主机名和服务名的工具。它是 getaddrinfo() 函数的反向操作,提供了从套接字地址到人类可读名称的映射。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
#include <netdb.h>  
  
int getnameinfo(const struct sockaddr *sa, socklen_t salen,  
                char *host, size_t hostlen,  
                char *serv, size_t servlen,  
                int flags);

2、参数解释

  1. sa:指向套接字地址结构的指针,该结构包含了要转换的地址信息。

  2. salen:套接字地址结构的长度,通常可以通过 sizeof() 宏来获取。

  3. host:指向存储主机名的字符数组的指针。如果不需要主机名,可以设置为 NULL

  4. hostlen:主机名字符数组的长度。

  5. serv:指向存储服务名的字符数组的指针。如果不需要服务名,可以设置为 NULL

  6. servlen:服务名字符数组的长度。

  7. flags:用于指定函数行为的标志位。常见的标志位包括 NI_NOFQDN(不返回完全限定的域名)、NI_NUMERICHOST(总是返回数字格式的地址)、NI_NAMEREQD(如果无法解析主机名,则返回错误)、NI_NUMERICSERV(总是返回数字格式的服务名)等。

3、返回值

  • 成功时,返回 0。

  • 失败时,返回非零值,并设置全局变量 errno 以指示错误原因。常见的错误包括 EAI_NONAME(无法解析名称)、EAI_FAMILY(地址族不被支持)、EAI_MEMORY(内存分配失败)等。

4、使用示例

以下是一个使用 getnameinfo() 函数将套接字地址结构转换为主机名和服务名,并打印出来的示例:

#include <sys/types.h>  
#include <sys/socket.h>  
#include <netdb.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <arpa/inet.h>  
#include <string.h>  
  
int main() {  
    struct sockaddr_in sa;  
    char host[NI_MAXHOST];  
    char serv[NI_MAXSERV];  
    int result;  
  
    // 初始化 sockaddr_in 结构  
    memset(&sa, 0, sizeof(sa));  
    sa.sin_family = AF_INET;  
    sa.sin_addr.s_addr = inet_addr("8.8.8.8"); // Google 的公共 DNS 服务器地址  
    sa.sin_port = htons(53); // DNS 服务的默认端口  
  
    // 调用 getnameinfo() 函数  
    result = getnameinfo((struct sockaddr *)&sa, sizeof(sa),  
                         host, sizeof(host),  
                         serv, sizeof(serv),  
                         0);  
    if (result != 0) {  
        fprintf(stderr, "getnameinfo: %s\n", gai_strerror(result));  
        return 1;  
    }  
  
    // 打印结果  
    printf("Host: %s\n", host);  
    printf("Service: %s\n", serv);  
  
    return 0;  
}

在这个示例中,我们创建了一个 sockaddr_in 结构,并将其初始化为 Google 的公共 DNS 服务器地址和 DNS 服务的默认端口。然后,我们调用 getnameinfo() 函数来解析这个地址结构,并将结果打印出来。注意,由于 8.8.8.8 是一个 IP 地址而不是域名,因此 getnameinfo() 在默认情况下可能无法解析出主机名(除非系统中有相应的反向 DNS 记录)。在这种情况下,可以通过设置 NI_NUMERICHOST 标志来强制 getnameinfo() 返回数字格式的地址。

5、注意事项

  • 在使用 getnameinfo() 之前,应该包含必要的头文件,如 <sys/types.h><sys/socket.h> 和 <netdb.h>

  • 调用 getnameinfo() 后,应该检查返回值以确定是否成功。如果失败,可以使用 gai_strerror() 函数将错误代码转换为人类可读的字符串。

  • getnameinfo() 函数支持 IPv4 和 IPv6 地址的解析,具体取决于传入的套接字地址结构的类型。

  • 当指定 host 或 serv 参数为 NULL 时,相应的名称将不会被解析或存储。这可以用于只解析主机名或服务名的情况。

2.3 setsockopt()

setsockopt() 函数是一个用于设置套接字选项的网络编程接口。通过这个函数,程序员可以配置套接字的各种行为特性,比如是否启用TCP的延迟确认、设置套接字的发送和接收缓冲区大小、指定套接字的阻塞或非阻塞模式等。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
int setsockopt(int sockfd, int level, int option_name,  
               const void *option_value, socklen_t option_len);

2、参数解释

  1. sockfd:要设置选项的套接字描述符。

  2. level:选项所在的协议层级。对于大多数套接字选项,这个值会是 SOL_SOCKET,表示套接字层级的选项。对于特定协议的选项(如TCP或IP),这个值会是相应的协议号,如 IPPROTO_TCP 或 IPPROTO_IP

  3. option_name:要设置的选项的名称。这个值是一个整数,对应不同的选项。

  4. option_value:指向包含选项值的缓冲区的指针。这个值的具体类型和含义取决于 option_name

  5. option_lenoption_value 缓冲区的长度,以字节为单位。

3、返回值

  • 成功时,返回 0。

  • 失败时,返回 -1,并设置全局变量 errno 以指示错误原因。

4、常见选项

  • SO_REUSEADDR:允许本地地址和端口号在套接字关闭后立即被重用。对于服务器程序来说,这个选项通常很有用,因为它可以避免在服务器重启时因为“地址已在使用”错误而无法绑定到相同的地址和端口。

  • SO_RCVBUF 和 SO_SNDBUF:分别设置接收和发送缓冲区的大小。这些选项可以影响套接字的吞吐量。

  • SO_KEEPALIVE:启用TCP的保持连接功能。如果一段时间内没有数据传输,TCP会自动发送保活探测包来检查连接是否仍然有效。

  • SO_LINGER:控制套接字关闭时的行为。如果设置了该选项,当调用 close() 关闭套接字时,如果发送缓冲区中还有未发送的数据,系统会尝试发送这些数据,而不是立即关闭连接。

  • SO_NONBLOCK:将套接字设置为非阻塞模式。在非阻塞模式下,套接字操作(如连接、发送、接收)会立即返回,即使操作没有完成。

  • TCP_NODELAY:禁用TCP的Nagle算法。Nagle算法是一种减少小数据包发送次数的优化,但在某些情况下(如实时通信应用)可能会导致延迟。

5、使用示例

以下是一个使用 setsockopt() 函数设置套接字为非阻塞模式的示例:

#include <sys/types.h>  
#include <sys/socket.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <stdio.h>  
  
int set_nonblocking(int sockfd) {  
    int flags, s;  
  
    // 获取当前套接字标志  
    flags = fcntl(sockfd, F_GETFL, 0);  
    if (flags == -1) {  
        perror("fcntl F_GETFL");  
        return -1;  
    }  
  
    // 设置非阻塞标志  
    flags |= O_NONBLOCK;  
    s = fcntl(sockfd, F_SETFL, flags);  
    if (s == -1) {  
        perror("fcntl F_SETFL");  
        return -1;  
    }  
  
    return 0;  
}  
  
int main() {  
    int sockfd;  
    // ... 假设已经创建了套接字 sockfd ...  
  
    // 设置套接字为非阻塞模式  
    if (set_nonblocking(sockfd) == -1) {  
        // 处理错误  
        close(sockfd);  
        return 1;  
    }  
  
    // ... 现在可以使用非阻塞套接字进行通信 ...  
  
    close(sockfd);  
    return 0;  
}

注意:虽然上面的示例使用了 fcntl() 函数来设置非阻塞模式,但 setsockopt() 也可以用于某些套接字选项来设置非阻塞行为(尽管对于非阻塞模式,fcntl() 是更常见的方法)。不过,为了完整性,这里还是展示了如何使用 fcntl() 来设置非阻塞模式,因为这在实践中非常常见。

对于其他套接字选项,你应该查阅相关的系统文档或手册页(如 man setsockopt)来了解如何正确使用 setsockopt() 函数。

2.4 getsockopt()

getsockopt() 函数是网络编程中用于获取套接字选项当前值的接口。与 setsockopt() 函数相反,getsockopt() 不用于设置选项,而是用于查询套接字当前配置的状态。

1、函数原型

#include <sys/types.h>  
#include <sys/socket.h>  
  
int getsockopt(int sockfd, int level, int option_name,  
               void *option_value, socklen_t *option_len);

2、参数解释

  1. sockfd:要查询选项的套接字描述符。

  2. level:选项所在的协议层级。与 setsockopt() 相同,这个值通常是 SOL_SOCKET 表示套接字层级的选项,或者是特定协议的协议号(如 IPPROTO_TCP 或 IPPROTO_IP)。

  3. option_name:要查询的选项的名称。这个值是一个整数,对应不同的选项。

  4. option_value:指向存储选项值的缓冲区的指针。调用 getsockopt() 后,这个缓冲区会被填充为选项的当前值。

  5. option_len:指向一个变量的指针,该变量在调用时包含 option_value 缓冲区的长度(以字节为单位),在返回时包含实际写入缓冲区的字节数。

3、返回值

  • 成功时,返回 0。

  • 失败时,返回 -1,并设置全局变量 errno 以指示错误原因。

4、常见选项

与 setsockopt() 相同,getsockopt() 可以查询许多不同的套接字选项,包括但不限于:

  • SO_REUSEADDR:查询是否允许本地地址和端口号在套接字关闭后立即被重用。

  • SO_RCVBUF 和 SO_SNDBUF:查询接收和发送缓冲区的大小。

  • SO_KEEPALIVE:查询TCP的保持连接功能是否启用。

  • SO_LINGER:查询套接字关闭时的行为。

  • SO_NONBLOCK:查询套接字是否为非阻塞模式。

  • TCP_NODELAY:查询TCP的Nagle算法是否禁用。

5、使用示例

以下是一个使用 getsockopt() 函数查询套接字接收缓冲区大小的示例:

#include <sys/types.h>  
#include <sys/socket.h>  
#include <stdio.h>  
  
int main() {  
    int sockfd;  
    // ... 假设已经创建了套接字 sockfd ...  
  
    int recvbuf_size;  
    socklen_t recvbuf_size_len = sizeof(recvbuf_size);  
  
    // 获取套接字接收缓冲区大小  
    if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (void *)&recvbuf_size, &recvbuf_size_len) == -1) {  
        perror("getsockopt");  
        // 处理错误,例如关闭套接字并返回  
        close(sockfd);  
        return 1;  
    }  
  
    // 打印接收缓冲区大小  
    printf("Receive buffer size: %d bytes\n", recvbuf_size);  
  
    // ... 继续其他操作 ...  
  
    close(sockfd);  
    return 0;  
}

在这个示例中,我们首先创建了一个套接字(假设这个过程已经在其他地方完成),然后调用 getsockopt() 函数来查询该套接字的接收缓冲区大小。如果查询成功,我们将结果打印出来。如果查询失败,我们打印一个错误消息,并关闭套接字。

请注意,实际使用中应该根据具体的套接字选项和需要查询的信息来设置 option_name 和 option_value 缓冲区的类型。此外,调用 getsockopt() 之前,应该确保 option_len 变量包含 option_value 缓冲区的正确长度,并且在函数返回后检查 option_len 的值以了解实际写入的字节数(尽管对于大多数选项来说,这个值通常与提供的缓冲区大小相同)。

2.5 select()

select() 函数是一个在多种操作系统中广泛使用的系统调用,特别是在网络编程和事件驱动编程中。它允许一个程序监视多个文件描述符,以查看它们是否可以进行I/O操作(比如读、写或出现异常)。select() 函数是POSIX标准的一部分,因此在大多数类Unix系统(如Linux、macOS)以及Windows的某些版本(通过Winsock接口)上都是可用的。

1、函数原型

#include <sys/select.h>  
#include <sys/time.h>  
#include <unistd.h>  
  
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

2、参数解释

  1. nfds:指定要监视的文件描述符集合中最大文件描述符加1的值。这通常设置为所有文件描述符中的最大值加1,以确保所有感兴趣的文件描述符都被包括在内。

  2. readfds:指向一个fd_set结构的指针,该结构表示监视读操作的文件描述符集合。如果不需要监视读操作,可以传递NULL

  3. writefds:指向一个fd_set结构的指针,该结构表示监视写操作的文件描述符集合。如果不需要监视写操作,可以传递NULL

  4. exceptfds:指向一个fd_set结构的指针,该结构表示监视异常条件的文件描述符集合(如带外数据到达或套接字错误)。如果不需要监视异常条件,可以传递NULL

  5. timeout:指向一个timeval结构的指针,该结构指定select()函数等待的最长时间。如果传递NULL,则select()将无限期地等待,直到某个文件描述符准备好。

3、返回值

  • 成功时,select()返回准备好的文件描述符的总数(即,在readfdswritefdsexceptfds中至少有一个位被设置的文件描述符的数量)。

  • 失败时,返回-1,并设置全局变量errno以指示错误原因。

4、fd_settimeval结构

  • fd_set:是一个位字段,用于表示文件描述符的集合。可以使用FD_ZERO()FD_SET()FD_CLR()FD_ISSET()宏来操作这些集合。

  • timeval:是一个结构体,用于指定时间间隔。它包含两个成员:tv_sec(秒)和tv_usec(微秒)。

5、使用示例

以下是一个使用select()函数监视套接字读操作的简单示例:

#include <sys/select.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
  
int main() {  
    int sockfd;  
    // ... 假设已经创建了套接字 sockfd,并连接到某个服务器 ...  
  
    fd_set readfds;  
    struct timeval timeout;  
  
    // 初始化文件描述符集合  
    FD_ZERO(&readfds);  
    FD_SET(sockfd, &readfds);  
  
    // 设置超时时间为5秒  
    timeout.tv_sec = 5;  
    timeout.tv_usec = 0;  
  
    // 调用select()函数  
    int retval = select(sockfd + 1, &readfds, NULL, NULL, &timeout);  
    if (retval == -1) {  
        perror("select()");  
        close(sockfd);  
        exit(EXIT_FAILURE);  
    } else if (retval == 0) {  
        printf("Timeout occurred! No data after 5 seconds.\n");  
    } else {  
        // 检查sockfd是否在readfds集合中  
        if (FD_ISSET(sockfd, &readfds)) {  
            char buffer[1024];  
            int bytes_read = read(sockfd, buffer, sizeof(buffer) - 1);  
            if (bytes_read == -1) {  
                perror("read()");  
                close(sockfd);  
                exit(EXIT_FAILURE);  
            } else if (bytes_read == 0) {  
                printf("Connection closed by peer.\n");  
            } else {  
                buffer[bytes_read] = '\0';  
                printf("Received: %s\n", buffer);  
            }  
        }  
    }  
  
    close(sockfd);  
    return 0;  
}

 在这个示例中,我们创建了一个套接字(假设这个过程已经在其他地方完成),并使用select()函数来监视它是否有数据可读。我们设置了一个5秒的超时时间,如果在这段时间内没有数据到达,select()将返回0。如果有数据可读,我们将读取数据并打印出来。如果读取过程中发生错误,我们将打印错误消息并关闭套接字。如果连接被对方关闭,我们将打印一个相应的消息。

2.6 poll() 

poll() 函数是 Unix 和类 Unix 操作系统(如 Linux)中提供的一个系统调用,用于监视多个文件描述符的事件状态。与 select() 函数类似,poll() 允许一个程序同时监视多个文件描述符,以查看它们是否可以进行读、写操作或是否发生了错误等事件。然而,poll() 提供了一些比 select() 更灵活和强大的功能。

1、函数原型

#include <poll.h>  
  
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

2、参数解释

  1. fds:指向一个 struct pollfd 结构数组的指针,该数组中的每个元素都描述了一个要监视的文件描述符及其感兴趣的事件。

  2. nfds:指定 fds 数组中的元素数量,即要监视的文件描述符的数量。

  3. timeout:指定 poll() 函数等待的最长时间(以毫秒为单位)。如果传递 -1,则 poll() 将无限期地等待,直到某个文件描述符准备好。如果传递 0,则 poll() 将立即返回,而不等待。

3、struct pollfd 结构

struct pollfd {  
    int fd;          // 文件描述符  
    short events;    // 等待的事件类型(输入、输出、错误等)  
    short revents;   // 实际发生的事件类型(由 `poll()` 函数返回时填写)  
};
  • fd:要监视的文件描述符。

  • events:指定对文件描述符感兴趣的事件类型,可以是以下值的组合(使用按位或运算符 |):

    • POLLIN:有数据可读。

    • POLLOUT:可以写数据。

    • POLLPRI:有带外(out-of-band)数据可读。

    • POLLERR:发生错误。

    • POLLHUP:挂起(hang up)。

    • POLLNVAL:无效的文件描述符。

  • revents:由 poll() 函数返回时填写,表示实际发生的事件类型。

4、返回值

  • 成功时,poll() 返回准备好的文件描述符的数量(即,在 fds 数组中 revents 字段不为零的元素数量)。

  • 失败时,返回 -1,并设置全局变量 errno 以指示错误原因。

5、使用示例

以下是一个使用 poll() 函数监视套接字读操作的简单示例:

#include <sys/types.h>  
#include <sys/socket.h>  
#include <poll.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
  
int main() {  
    int sockfd;  
    // ... 假设已经创建了套接字 sockfd,并连接到某个服务器 ...  
  
    struct pollfd fds[1];  
    int timeout = 5000; // 5秒超时  
  
    fds[0].fd = sockfd;  
    fds[0].events = POLLIN; // 监视读操作  
    fds[0].revents = 0; // 初始化为0,由poll()函数填写  
  
    // 调用poll()函数  
    int retval = poll(fds, 1, timeout);  
    if (retval == -1) {  
        perror("poll()");  
        close(sockfd);  
        exit(EXIT_FAILURE);  
    } else if (retval == 0) {  
        printf("Timeout occurred! No data after 5 seconds.\n");  
    } else {  
        // 检查sockfd是否在fds数组中,并且是否有数据可读  
        if (fds[0].revents & POLLIN) {  
            char buffer[1024];  
            int bytes_read = read(sockfd, buffer, sizeof(buffer) - 1);  
            if (bytes_read == -1) {  
                perror("read()");  
                close(sockfd);  
                exit(EXIT_FAILURE);  
            } else if (bytes_read == 0) {  
                printf("Connection closed by peer.\n");  
            } else {  
                buffer[bytes_read] = '\0';  
                printf("Received: %s\n", buffer);  
            }  
        }  
    }  
  
    close(sockfd);  
    return 0;  
}

在这个示例中,我们创建了一个套接字(假设这个过程已经在其他地方完成),并使用 poll() 函数来监视它是否有数据可读。我们设置了一个5秒的超时时间,如果在这段时间内没有数据到达,poll() 将返回0。如果有数据可读,我们将读取数据并打印出来。如果读取过程中发生错误,我们将打印错误消息并关闭套接字。如果连接被对方关闭,我们将打印一个相应的消息。

6、注意事项

  • 在使用 poll() 函数之前,应确保已经包含了正确的头文件 <poll.h>

  • poll() 函数不会清空 fds 数组,因此在多次调用时不需要重新初始化数组中的元素。这与 select() 函数不同,select() 函数在每次调用后都会清空其监视的文件描述符集合。

  • poll() 函数在处理大量文件描述符时比 select() 函数更高效,因为它不需要在每次调用时都重新构建文件描述符集合。然而,在处理极大量文件描述符(例如,成千上万个)时,epoll() 函数可能是一个更好的选择,因为它提供了更高的效率和更好的可扩展性。

2.7 epoll()

epoll是Linux内核为处理大量并发网络连接而提供的一种高效I/O事件通知机制。它相较于传统的select和poll函数,具有更高的效率和更好的可扩展性。以下是关于epoll的详细介绍:

1、epoll概述

epoll是Linux特有的一种I/O事件通知机制,它在Linux 2.6内核版本中首次引入。epoll通过一组函数来实现其功能,包括epoll_create()、epoll_ctl()和epoll_wait()。这些函数允许用户空间程序监视多个文件描述符上的I/O事件,并在这些事件发生时得到通知。

2、epoll函数

  • epoll_create()

该函数用于创建一个epoll实例,并返回一个文件描述符,该描述符用于后续对epoll实例的操作。

int epoll_create(int size);
  • size:指定要监听的文件描述符数量,但这个参数在Linux 2.6.8及以后的版本中已被忽略,因为epoll内部使用了更高效的数据结构。

  • 返回值:成功时返回一个非负整数,作为epoll实例的文件描述符;失败时返回-1,并设置errno。

  • epoll_ctl()

该函数用于向epoll实例中添加、修改或删除要监视的文件描述符及其事件类型。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:由epoll_create()返回的文件描述符。

  • op:要执行的操作,可以是EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或EPOLL_CTL_DEL(删除)。

  • fd:要监视的文件描述符。

  • event:指向一个epoll_event结构的指针,该结构指定了要监视的事件类型。

  • 返回值:成功时返回0;失败时返回-1,并设置errno。

  • epoll_wait()

该函数用于等待并返回发生在epoll实例中已注册文件描述符上的I/O事件。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd:由epoll_create()返回的文件描述符。

  • events:指向一个epoll_event数组的指针,用于接收发生的事件。

  • maxevents:数组的大小,即一次能处理的最大事件数。

  • timeout:等待事件的超时时间(毫秒)。如果为-1,则无限期等待;如果为0,则立即返回。

  • 返回值:成功时返回发生的事件数;失败时返回-1,并设置errno。

3、epoll_event结构

epoll_event结构用于指定和接收I/O事件。

struct epoll_event {  
    __uint32_t events;   // 事件类型  
    epoll_data_t data;   // 与事件相关的用户数据  
};

  • events:表示感兴趣的事件和被触发的事件类型。常见的事件类型包括EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLPRI(带外数据可读)、EPOLLERR(错误)、EPOLLHUP(挂起)等。

  • data:是一个联合体,可以保存触发事件的某个文件描述符相关的数据。它可以是文件描述符本身(int fd)、一个指向用户数据的指针(void *ptr)或其他类型的数据。

4、epoll的特点与优势

  1. 效率:epoll使用了一种称为“事件驱动”的模型,它避免了传统I/O复用模型(如select和poll)中每次调用都需要重新扫描整个文件描述符集合的缺点。epoll只需在事件发生时通知用户空间程序,从而大大提高了效率。

  2. 可扩展性:epoll能够高效地处理大量并发网络连接,因为它使用了高效的数据结构和算法来管理文件描述符。这使得epoll成为处理大规模并发网络应用(如Web服务器、聊天服务器等)的理想选择。

  3. 易用性:虽然epoll的API相对复杂一些,但一旦掌握了它的使用方法,就可以轻松地实现高效的I/O事件处理。此外,Linux社区和开源项目中也提供了许多关于epoll的使用示例和库函数,方便开发者学习和使用。

5、使用示例

以下是一个简单的epoll使用示例,它演示了如何创建一个epoll实例、添加一个文件描述符进行监视以及等待并处理事件。

#include <sys/epoll.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
  
// ...(省略了部分代码,如错误处理函数等)  
  
int main() {  
    int listen_fd, epoll_fd, nfds, conn_fd;  
    struct sockaddr_in server_addr;  
    struct epoll_event ev, events[MAX_EVENTS];  
  
    // 创建监听套接字并设置非阻塞模式  
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);  
    setnonblocking(listen_fd);  
    // ...(省略了绑定和监听设置等代码)  
  
    // 创建epoll实例  
    epoll_fd = epoll_create1(0);  
    if (epoll_fd == -1) {  
        perror("epoll_create1");  
        exit(EXIT_FAILURE);  
    }  
  
    // 向epoll实例中添加监听套接字  
    ev.events = EPOLLIN | EPOLLET;  
    ev.data.fd = listen_fd;  
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {  
        perror("epoll_ctl: listen_fd");  
        exit(EXIT_FAILURE);  
    }  
  
    // 进入事件循环  
    while (1) {  
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);  
        if (nfds == -1) {  
            perror("epoll_wait");  
            exit(EXIT_FAILURE);  
        }  
  
        for (int n = 0; n < nfds; ++n) {  
            if (events[n].data.fd == listen_fd) {  
                // 处理新的连接请求  
                conn_fd = accept(listen_fd, NULL, NULL);  
                if (conn_fd == -1) {  
                    perror("accept");  
                    continue;  
                }  
                setnonblocking(conn_fd);  
                ev.events = EPOLLIN | EPOLLET;  
                ev.data.fd = conn_fd;  
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {  
                    perror("epoll_ctl: conn_fd");  
                    close(conn_fd);  
                }  
            } else {  
                // 处理已连接套接字上的数据  
                int fd = events[n].data.fd;  
                // ...(省略了数据读取和处理代码)  
            }  
        }  
    }  
  
    close(listen_fd);  
    close(epoll_fd);  
    return 0;  
}

在这个示例中,我们首先创建了一个监听套接字,并将其设置为非阻塞模式。然后,我们创建了一个epoll实例,并向其中添加了监听套接字以监视其上的I/O事件。接下来,我们进入了一个事件循环,在该循环中我们调用epoll_wait()函数来等待并处理发生在epoll实例中的事件。如果发生的是新的连接请求,我们接受该连接并将其添加到epoll实例中以监视其上的数据读取事件。如果发生的是数据读取事件,我们则读取并处理数据。 

猜你喜欢

转载自blog.csdn.net/a8039974/article/details/143206981