> 文章列表 > WebServer项目(三)->linux网络编程基础知识

WebServer项目(三)->linux网络编程基础知识

WebServer项目(三)->linux网络编程基础知识

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

思路:

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。

  2. 调用一个系统函数(也就是select),监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回。
    a.这个函数是阻塞
    b.函数对文件描述符的检测的操作是由内核完成的

  3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
    WebServer项目(三)->linux网络编程基础知识

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详解

WebServer项目(三)->linux网络编程基础知识
WebServer项目(三)->linux网络编程基础知识


select具体怎么用?

elect 函数是 Linux 中常用的 I/O 多路复用函数之一,可以用于同时监视多个文件描述符的读、写和异常事件,当其中任意一个文件描述符发生变化时,select 函数就会返回,并通知应用程序处理相应的事件。

使用 select 函数需要注意以下几个步骤:

  1. 调用 select 函数前,需要创建一个 fd_set 类型的集合,并将需要监视的文件描述符添加到集合中fd_set 是一个结构体类型,用于表示待监视的文件描述符集合
fd_set read_fds; //声明读集合
FD_ZERO(&read_fds); //清空读集合
FD_SET(sockfd, &read_fds); //将 sockfd 添加到读集合中
  1. 给每个文件描述符设置非阻塞模式,以避免在 select 函数调用期间出现阻塞。这可以通过 fcntl 函数来实现。也可以在 socket 创建时就将其设置为非阻塞模式。
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
  1. 调用 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);
  1. 检测事件并处理。如果返回值大于 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 函数的步骤如下:

  1. 在设置 fd_set 集合时,可以使用 FD_SET 宏将文件描述符添加到集合中。
  2. 如果需要从集合中移除某个文件描述符,可以使用 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 主要有以下几个宏定义:(上面已讲

  1. FD_ZERO(fd_set *set):将 fd_set 集合清空,即将所有位都设置为 0。
  2. FD_SET(int fd, fd_set *set):将指定的 fd 文件描述符添加到 fd_set 集合中。
  3. FD_CLR(int fd, fd_set *set):将指定的 fd 文件描述符从 fd_set 集合中移除。
  4. 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() {// 创建 socketint 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);// 处理事件,并从集合中移除 sockfdif (FD_ISSET(sockfd, &fds)) {// sockfd 上有可读事件发生char buf[BUFSIZ];int n = read(sockfd, buf, BUFSIZ);// 处理读取到的数据FD_CLR(sockfd, &fds); // 将 sockfd 从集合中移除}// 关闭 socketclose(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

WebServer项目(三)->linux网络编程基础知识

#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: 添加   它的值为1EPOLL_CTL_MOD: 修改   它的值为3EPOLL_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通信实现

WebServer项目(三)->linux网络编程基础知识

#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.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。
WebServer项目(三)->linux网络编程基础知识

8.组播(多播)

单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。
单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。
a.组播既可以用于局域网,也可以用于广域网
b.客户端需要加入多播组,才能接收到多播的数据

WebServer项目(三)->linux网络编程基础知识

9.本地套接字

本地套接字的作用:本地的进程间通信
有关系的进程间的通信
没有关系的进程间的通信
本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程。

10.阻塞/非阻塞,同步/异步

阻塞和非阻塞是指程序在执行调用时的状态。如果一个程序在执行某个调用时,如果该调用需要等待某些结果才能继续执行,那么这个调用就是阻塞的。相反,如果该调用不需要等待结果就能继续执行,那么这个调用就是非阻塞的。
同步和异步是指程序在调用完成后的状态。如果一个程序在调用完成后需要等待结果才能继续执行,那么这个调用就是同步的。相反,如果该调用不需要等待结果就能继续执行,那么这个调用就是异步的。
举个例子,如果你在使用浏览器访问一个网站,如果该网站的服务器需要一段时间才能响应你的请求,那么你的浏览器就会被阻塞,直到服务器响应完成。这就是一个阻塞的同步调用。相反,如果你使用 Ajax 技术,浏览器会发起一个异步调用,不会被阻塞,而是可以继续执行其他任务,当服务器响应完成后,浏览器会执行回调函数来处理服务器返回的数据。这就是一个非阻塞的异步调用。
总的来说,阻塞和非阻塞是针对程序在执行调用时的状态,而同步和异步是针对程序在调用完成后的状态。在编写程序时,需要根据实际情况选择合适的调用方式。
WebServer项目(三)->linux网络编程基础知识

11.Unix/Linux上的五种IO模型

  1. 阻塞式I/O模型(Blocking I/O Model):在阻塞式I/O模型中,当应用程序调用I/O操作时,程序会一直阻塞(即等待)直到I/O操作完成。这意味着应用程序将无法做其他事情,直到I/O操作完成。(容易理解)

  2. 非阻塞式I/O模型(Non-Blocking I/O Model):在非阻塞式I/O模型中,当应用程序调用I/O操作时,程序会立即返回,而不是一直等待I/O操作完成。如果I/O操作还没有完成,应用程序可以进行其他操作,或者再次尝试I/O操作。(容易理解)

  3. I/O复用模型(I/O Multiplexing Model):在I/O复用模型中,应用程序可以同时监视多个I/O操作,等待其中的任何一个I/O操作完成。这样,应用程序可以同时处理多个I/O操作,而不必阻塞或非阻塞地等待每个I/O操作完成。

  4. 信号驱动I/O模型(Signal-Driven I/O Model):在信号驱动I/O模型中,应用程序发起一个I/O操作后,程序可以继续执行其他任务,而不是一直等待I/O操作完成。当I/O操作完成时,操作系统会发送一个信号给应用程序,应用程序可以捕获该信号并处理I/O操作的结果。

  5. 异步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模型时,需要根据具体的需求和实际情况来选择。