WebServer项目[三]->linux网络编程基础知识
- 1. I/O多路复用(I/O多路转接)
- 2. select
- 1)select简介
- 2)select详解
- select具体怎么用?
- 那FD_CLR函数是干嘛的?
- 关于 fd_set,它具体是什么?
- 3. poll(改进select)
- 4. epoll
- 5.epoll的两种工作模式
- 6.UDP通信实现
- 7.广播
- 8.组播(多播)
- 9.本地套接字
- 10.阻塞/非阻塞,同步/异步
- 11.Unix/Linux上的五种IO模型
- 异步I/O模型与信号驱动I/O模型的区别(了解即可)
1. I/O多路复用(I/O多路转接)
I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,
Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。
2. select
思路:
-
首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
-
调用一个系统函数(也就是select),监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回。
a.这个函数是阻塞
b.函数对文件描述符的检测的操作是由内核完成的 -
在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
1)select简介
注:sizeof(fd_set) = 128 128 * 8 = 1024
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 参数:
- nfds : 委托内核检测的最大文件描述符的值 + 1
- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
- 一般检测读操作
- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
- 是一个传入传出参数
- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
- exceptfds : 检测发生异常的文件描述符的集合
- timeout : 设置的超时时间
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- NULL : 永久阻塞,直到检测到了文件描述符有变化
- tv_sec = 0 && tv_usec = 0, 不阻塞
- tv_sec > 0 && tv_usec > 0, 阻塞对应的时间
- 返回值 :
- -1 : 失败
- >0(n) : 检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);
2)select详解
select具体怎么用?
elect 函数是 Linux 中常用的 I/O 多路复用函数之一,可以用于同时监视多个文件描述符的读、写和异常事件,当其中任意一个文件描述符发生变化时,select 函数就会返回,并通知应用程序处理相应的事件。
使用 select 函数需要注意以下几个步骤:
- 调用 select 函数前,需要创建一个 fd_set 类型的集合,并将需要监视的文件描述符添加到集合中。fd_set 是一个结构体类型,用于表示待监视的文件描述符集合。
fd_set read_fds; //声明读集合
FD_ZERO(&read_fds); //清空读集合
FD_SET(sockfd, &read_fds); //将 sockfd 添加到读集合中
- 给每个文件描述符设置非阻塞模式,以避免在 select 函数调用期间出现阻塞。这可以通过 fcntl 函数来实现。也可以在 socket 创建时就将其设置为非阻塞模式。
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
- 调用 select 函数并等待事件发生。select 函数的第一个参数是文件描述符的最大值加 1(为啥加一,比如最大值是101,那么select的源码就是一个for循环:for(int i = 0 ; i < 101+1 ;i++) => 刚好可以遍历0~101),第二个、第三个和第四个参数分别是待检查的读、写和异常事件集合。第五个参数是超时时间,如果设置为 NULL,则 select 函数将一直阻塞,直到有事件发生。如果设置为 0,则 select 函数将立即返回,如果没有事件发生,则返回 0。
select(sockfd + 1, &read_fds, NULL, NULL, NULL);
- 检测事件并处理。如果返回值大于 0,说明有事件发生。可以使用 FD_ISSET 宏检查那些文件描述符上的事件是否已经发生,并进行相应的处理。
if (FD_ISSET(sockfd, &read_fds)) {
// sockfd 上有可读事件发生
char buf[BUFSIZ];
int n = read(sockfd, buf, BUFSIZ);
// 处理读取到的数据
}
需要注意的是,使用 select 函数的程序必须以多进程或者多线程的方式来运行,否则 select 函数会阻塞整个程序。同时,select 函数也存在一些局限性,例如最大支持的文件描述符数量有限等问题。
那FD_CLR函数是干嘛的?
FD_CLR 函数是 Linux 中用于从 fd_set 集合中清除指定文件描述符的宏。该函数可以将一个文件描述符从 fd_set 集合中移除,以便在后续的 select 调用中不再监视该文件描述符。
FD_CLR 的调用格式如下:
void FD_CLR(int fd, fd_set *set);
其中,fd 是要从集合中移除的文件描述符,set 是 fd_set 集合。
使用 FD_CLR 函数的步骤如下:
- 在设置 fd_set 集合时,可以使用 FD_SET 宏将文件描述符添加到集合中。
- 如果需要从集合中移除某个文件描述符,可以使用 FD_CLR 宏进行操作。
fd_set fds;
FD_ZERO(&fds); // 初始化集合
FD_SET(sockfd, &fds); // 添加 sockfd 到集合
// 等待 sockfd 上发生事件
select(sockfd + 1, &fds, NULL, NULL, NULL);
// 处理事件,并从集合中移除 sockfd
if (FD_ISSET(sockfd, &fds)) {
// sockfd 上有可读事件发生
char buf[BUFSIZ];
int n = read(sockfd, buf, BUFSIZ);
// 处理读取到的数据
FD_CLR(sockfd, &fds); // 将 sockfd 从集合中移除
}
需要注意的是,在使用 FD_CLR 函数时,必须确保 fd_set 集合已经初始化并且包含了要移除的文件描述符。否则可能会导致未知的错误。
如果需要从集合中移除某个文件描述符,可以使用 FD_CLR 宏进行操作。
关于 fd_set,它具体是什么?
fd_set 是一个结构体类型,它在头文件 sys/select.h 中被定义。该结构体用于表示一个待监视的文件描述符集合。
==fd_set 结构体本身并不存储文件描述符,而是以位图的形式存储,每个位表示对应的文件描述符是否在集合中。在 Linux 中,fd_set 的长度默认为 1024 位,最大支持的文件描述符数量也是 1024 个。==如果需要扩展 fd_set 的长度,可以使用 FD_SETSIZE 宏进行定义。
sizeof(fd_set)=128字节=1024位
fd_set 包含一个数组,数组的每个元素都是 unsigned long 类型,表示 64 个文件描述符的状态。因此,fd_set 最大支持 1024/64=16 个元素,可以用编码方式表示 0~1023 号文件描述符是否在集合中。
fd_set 主要有以下几个宏定义:(上面已讲)
- FD_ZERO(fd_set *set):将 fd_set 集合清空,即将所有位都设置为 0。
- FD_SET(int fd, fd_set *set):将指定的 fd 文件描述符添加到 fd_set 集合中。
- FD_CLR(int fd, fd_set *set):将指定的 fd 文件描述符从 fd_set 集合中移除。
- FD_ISSET(int fd, fd_set *set):判断指定的 fd 文件描述符是否在 fd_set 集合中。
下面是一个简单的 fd_set 示例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/select.h> #include <sys/socket.h> #include <arpa/inet.h> int main() { // 创建 socket int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } // 连接到服务器 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(80); inet_pton(AF_INET, "www.baidu.com", &addr.sin_addr.s_addr); if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { perror("connect"); exit(EXIT_FAILURE); } // 将 sockfd 添加到 fd_set 集合中 fd_set fds; FD_ZERO(&fds); FD_SET(sockfd, &fds); // 等待 sockfd 上发生事件 select(sockfd + 1, &fds, NULL, NULL, NULL); // 处理事件,并从集合中移除 sockfd if (FD_ISSET(sockfd, &fds)) { // sockfd 上有可读事件发生 char buf[BUFSIZ]; int n = read(sockfd, buf, BUFSIZ); // 处理读取到的数据 FD_CLR(sockfd, &fds); // 将 sockfd 从集合中移除 } // 关闭 socket close(sockfd); return 0; }
3. poll(改进select)
#include <poll.h>
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */
short revents; /* 文件描述符实际发生的事件 */
};
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数:
- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
- timeout : 阻塞时长
0 : 不阻塞
-1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
>0 : 阻塞的时长
- 返回值:
-1 : 失败
>0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化
4. epoll
#include <sys/epoll.h>
// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检
//测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向
//链表)。
int epoll_create(int size);
- 参数:
size : 目前没有意义了。随便写一个数,必须大于0
- 返回值:
-1 : 失败 + 错误号
> 0 : 文件描述符,操作epoll实例的
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
- EPOLLIN [其值为 0x001。EPOLLIN 表示对应的文件描述符可以进行读取操作,
当该文件描述符上有数据可读时,epoll_wait() 函数将返回并触发 EPOLLIN 事件,
以表明该文件描述符上已经有数据可读。]
- EPOLLOUT
- EPOLLERR
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息=>放到内核的红黑树上
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数:
- epfd : epoll实例对应的文件描述符
- op : 要进行什么操作
它有三个宏定义作为参数:
EPOLL_CTL_ADD: 添加 它的值为1
EPOLL_CTL_MOD: 修改 它的值为3
EPOLL_CTL_DEL: 删除 它的值为2
- fd : 要检测的文件描述符
- event : 检测文件描述符什么事情
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数:
- epfd : epoll实例对应的文件描述符(作用是对epoll实例进行操作)
- events : 传出参数,保存了发送了变化的文件描述符的信息
- maxevents : 第二个参数结构体数组的大小
- timeout : 阻塞时间
- 0 : 不阻塞
- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
- > 0 : 阻塞的时长(毫秒)
- 返回值:
- 成功,返回发送变化的文件描述符的个数 > 0
- 失败 -1
5.epoll的两种工作模式
Epoll 的工作模式:
-
LT 模式 (水平触发)
假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
b.用户只读了一部分数据,epoll会通知
c.缓冲区的数据读完了,不通知 -
ET 模式(边沿触发)
假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
b.用户只读了一部分数据,epoll不通知
c.缓冲区的数据读完了,不通知
6.UDP通信实现
#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);
- 参数:
- sockfd : 通信的fd
- buf : 要发送的数据
- len : 发送数据的长度
- flags : 0
- dest_addr : 通信的另外一端的地址信息
- addrlen : 地址的内存大小
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- 参数:
- sockfd : 通信的fd
- buf : 接收数据的数组
- len : 数组的大小
- - flags : 0
- src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL
- addrlen : 地址的内存大小
7.广播
向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
a.只能在局域网中使用。
b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。
8.组播(多播)
单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。
单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。
a.组播既可以用于局域网,也可以用于广域网
b.客户端需要加入多播组,才能接收到多播的数据
9.本地套接字
本地套接字的作用:本地的进程间通信
有关系的进程间的通信
没有关系的进程间的通信
本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程。
10.阻塞/非阻塞,同步/异步
阻塞和非阻塞是指程序在执行调用时的状态。如果一个程序在执行某个调用时,如果该调用需要等待某些结果才能继续执行,那么这个调用就是阻塞的。相反,如果该调用不需要等待结果就能继续执行,那么这个调用就是非阻塞的。
同步和异步是指程序在调用完成后的状态。如果一个程序在调用完成后需要等待结果才能继续执行,那么这个调用就是同步的。相反,如果该调用不需要等待结果就能继续执行,那么这个调用就是异步的。
举个例子,如果你在使用浏览器访问一个网站,如果该网站的服务器需要一段时间才能响应你的请求,那么你的浏览器就会被阻塞,直到服务器响应完成。这就是一个阻塞的同步调用。相反,如果你使用 Ajax 技术,浏览器会发起一个异步调用,不会被阻塞,而是可以继续执行其他任务,当服务器响应完成后,浏览器会执行回调函数来处理服务器返回的数据。这就是一个非阻塞的异步调用。
总的来说,阻塞和非阻塞是针对程序在执行调用时的状态,而同步和异步是针对程序在调用完成后的状态。在编写程序时,需要根据实际情况选择合适的调用方式。
11.Unix/Linux上的五种IO模型
-
阻塞式I/O模型(Blocking I/O Model):在阻塞式I/O模型中,当应用程序调用I/O操作时,程序会一直阻塞(即等待)直到I/O操作完成。这意味着应用程序将无法做其他事情,直到I/O操作完成。(容易理解)
-
非阻塞式I/O模型(Non-Blocking I/O Model):在非阻塞式I/O模型中,当应用程序调用I/O操作时,程序会立即返回,而不是一直等待I/O操作完成。如果I/O操作还没有完成,应用程序可以进行其他操作,或者再次尝试I/O操作。(容易理解)
-
I/O复用模型(I/O Multiplexing Model):在I/O复用模型中,应用程序可以同时监视多个I/O操作,等待其中的任何一个I/O操作完成。这样,应用程序可以同时处理多个I/O操作,而不必阻塞或非阻塞地等待每个I/O操作完成。
-
信号驱动I/O模型(Signal-Driven I/O Model):在信号驱动I/O模型中,应用程序发起一个I/O操作后,程序可以继续执行其他任务,而不是一直等待I/O操作完成。当I/O操作完成时,操作系统会发送一个信号给应用程序,应用程序可以捕获该信号并处理I/O操作的结果。
-
异步I/O模型(Asynchronous I/O Model):在异步I/O模型中,应用程序发起一个I/O操作后,程序可以继续执行其他任务,而不必等待I/O操作完成。当I/O操作完成后,操作系统会通知应用程序,并将I/O操作的结果传递给应用程序。
异步I/O模型与信号驱动I/O模型的区别(了解即可)
异步I/O模型和信号驱动I/O模型都是在I/O操作完成后通知应用程序的模型,它们的区别主要在于通知的方式和处理方式。
异步I/O模型是一种完全异步的模型。在异步I/O模型中,应用程序发起一个I/O操作后,程序可以继续执行其他任务,而不必等待I/O操作完成。当I/O操作完成后,操作系统会通知应用程序,并将I/O操作的结果传递给应用程序。应用程序需要在回调函数中处理I/O操作的结果。
信号驱动I/O模型是一种半同步、半异步的模型。在信号驱动I/O模型中,应用程序发起一个I/O操作后,程序可以继续执行其他任务,而不是一直等待I/O操作完成。当I/O操作完成时,操作系统会发送一个信号给应用程序,应用程序可以捕获该信号并处理I/O操作的结果。
因此,异步I/O模型相对于信号驱动I/O模型具有更高的效率和更好的可扩展性,因为它不需要等待I/O操作的完成,并且不需要使用信号来通知应用程序。但是,异步I/O模型的实现比较复杂,需要使用回调函数来处理I/O操作的结果。信号驱动I/O模型的实现相对简单,但是由于使用了信号来通知应用程序,可能会存在一些信号处理的问题。因此,在选择I/O模型时,需要根据具体的需求和实际情况来选择。