文章目录
一、阻塞和非阻塞的概念以及函数实现
阻塞与非阻塞的概念
**阻塞(Blocking)和非阻塞(Non-Blocking)**是计算机编程中描述输入/输出(I/O)操作行为的两种方式。这两种方式主要影响程序如何处理I/O操作(如网络连接、文件读写等),特别是在涉及等待某些资源或数据的情况下。
阻塞模式(Blocking Mode)
-
定义: 在阻塞模式下,当程序调用某个I/O操作时,程序会暂停执行,直到该操作完成。这意味着如果一个操作需要等待,比如网络连接建立、数据读取或写入、输入等待等,程序会一直等待,直到该操作完成后才继续执行后续代码。
-
优点:
- 简单易于理解和实现,因为程序逻辑是线性的。
- 适合处理少量的I/O操作或处理周期较短的操作。
-
缺点:
- 如果I/O操作需要很长时间(例如等待网络响应),程序将被挂起,不能执行其他任务,可能导致程序响应不及时。
非阻塞模式(Non-Blocking Mode)
-
定义: 在非阻塞模式下,I/O操作在无法立即完成时不会让程序等待,而是立即返回一个状态值,指示操作未完成。程序可以继续执行其他任务,并在适当的时候再次尝试完成该I/O操作。
-
优点:
- 提高了程序的并发性,允许程序在等待I/O完成时执行其他任务,适合处理高并发、大量I/O操作的场景。
- 提高了程序的响应性,因为它不必等待单个I/O操作完成。
-
缺点:
- 实现起来相对复杂,因为需要管理和检查I/O操作的状态。
- 需要额外的逻辑来处理未完成的I/O操作。
函数实现
为了实现阻塞和非阻塞模式,我们通常使用套接字编程中的 fcntl
函数来控制文件描述符(如套接字)的阻塞行为。
阻塞模式示例
在默认情况下,套接字是阻塞的。以下是一个典型的阻塞套接字的连接示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 阻塞模式下连接服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Connected to the server in blocking mode.\n");
// 发送和接收数据...
close(sockfd);
return 0;
}
在这个代码中,connect
是阻塞的,它会等待直到连接成功或失败后才返回。
非阻塞模式实现
要将套接字设置为非阻塞模式,可以使用 fcntl
函数:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/select.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置套接字为非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags < 0) {
perror("fcntl(F_GETFL)");
close(sockfd);
exit(EXIT_FAILURE);
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("fcntl(F_SETFL)");
close(sockfd);
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 非阻塞模式下尝试连接服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
if (errno != EINPROGRESS) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
// EINPROGRESS 表示连接正在进行中
}
// 使用 select 等待连接完成
fd_set write_fds;
struct timeval tv;
FD_ZERO(&write_fds);
FD_SET(sockfd, &write_fds);
tv.tv_sec = 5; // 设置超时时间为5秒
tv.tv_usec = 0;
int ret = select(sockfd + 1, NULL, &write_fds, NULL, &tv);
if (ret < 0) {
perror("select");
close(sockfd);
exit(EXIT_FAILURE);
} else if (ret == 0) {
fprintf(stderr, "connect timeout\n");
close(sockfd);
exit(EXIT_FAILURE);
}
// 检查连接是否成功
int err;
socklen_t len = sizeof(err);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
perror("getsockopt");
close(sockfd);
exit(EXIT_FAILURE);
}
if (err != 0) {
fprintf(stderr, "connect failed: %s\n", strerror(err));
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Connected to the server in non-blocking mode.\n");
// 发送和接收数据...
close(sockfd);
return 0;
}
代码说明:
-
设置非阻塞模式:
- 使用
fcntl
函数设置套接字为非阻塞模式。 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK)
将套接字设置为非阻塞模式。
- 使用
-
处理
connect
的EINPROGRESS
错误:- 在非阻塞模式下,
connect
可能会返回-1
,并将errno
设置为EINPROGRESS
,表示连接正在进行中。这是预期的行为,程序不会因此中断。
- 在非阻塞模式下,
-
使用
select
处理连接:- 通过
select
函数等待套接字的写事件(连接成功)或超时。 - 如果
select
指示套接字可写,则表示连接可能已成功。
- 通过
-
检查连接状态:
- 使用
getsockopt
检查连接的实际状态,确保连接确实成功或失败。
- 使用
阻塞与非阻塞的选择
- 阻塞模式适合简单的程序和不需要处理大量并发连接的场景。
- 非阻塞模式适合需要同时处理多个 I/O 操作(如多个客户端连接)且希望程序能响应迅速的场景。结合 I/O 多路复用技术(如
select
、poll
、epoll
)可以显著提高程序的并发能力和响应速度。
二、#include <errno.h>头文件
<errno.h>
头文件是C标准库中的一个文件,用于定义与错误处理相关的宏、变量和函数。它提供了一种标准化的方式来报告和处理程序在运行时发生的各种错误。
<errno.h>
的主要内容
-
errno
变量:errno
是一个全局变量,用于存储最近一次系统调用或库函数调用出错时的错误码。当某个系统调用或库函数返回一个错误(通常是-1
或NULL
),errno
会被设置为一个表示特定错误类型的值。- 由于
errno
是全局变量,所以它可能在不同的系统调用之间被修改,因此在调用返回错误后,应该立即检查errno
的值。
-
错误码宏:
-
<errno.h>
定义了一些标准错误码的宏,这些宏表示各种常见的错误类型。这些错误码通常是整数值,例如EINTR
、ENOMEM
、EINVAL
等。以下是一些常见的错误码:EINTR
(Interrupted function call): 函数调用在执行过程中被信号中断。ENOMEM
(Out of memory): 内存不足,无法分配请求的内存。EINVAL
(Invalid argument): 无效的参数传递给函数。EAGAIN
(Resource temporarily unavailable): 资源暂时不可用,通常需要稍后重试。EBADF
(Bad file descriptor): 无效的文件描述符。EACCES
(Permission denied): 权限被拒绝,通常是尝试访问被禁止的资源。EFAULT
(Bad address): 无效的内存地址传递给函数。EIO
(Input/output error): 输入/输出操作发生错误。
-
-
perror
函数:-
<errno.h>
还提供了perror
函数,用于打印一条描述性错误消息,该消息与errno
的当前值对应。perror
函数的格式是:void perror(const char *s);
-
perror
会在标准错误输出上打印s
参数和一个描述性错误消息,消息与当前的errno
值对应。这在调试程序时特别有用。例如:
FILE *fp = fopen("nonexistent.txt", "r"); if (fp == NULL) { perror("Error opening file"); }
如果文件打开失败,
perror
会打印如下消息:Error opening file: No such file or directory
-
-
strerror
函数:-
<errno.h>
也提供了strerror
函数,该函数返回一个指向描述错误的字符串的指针。strerror
的原型是:char *strerror(int errnum);
-
例如:
printf("Error: %s\n", strerror(errno));
这个函数会返回一个字符串,描述与
errno
对应的错误。
-
使用 <errno.h>
的示例
以下是一个简单的使用 <errno.h>
的示例:
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
// 打印错误码和对应的错误描述
printf("Error code: %d\n", errno);
printf("Error message: %s\n", strerror(errno));
perror("Error occurred");
}
return 0;
}
输出:
Error code: 2
Error message: No such file or directory
Error occurred: No such file or directory
在这个示例中,尝试打开一个不存在的文件会导致 fopen
失败,并且 errno
会被设置为相应的错误码(在这种情况下是 2
,对应的错误消息是 “No such file or directory”)。程序打印了错误码和对应的错误消息。
总结
<errno.h>
是一个非常有用的头文件,它提供了一种标准化的方式来处理程序运行时的错误。通过 errno
变量、错误码宏、以及 perror
和 strerror
函数,程序员可以轻松地检测和报告错误,从而更好地调试和维护代码。