网络socket编程--多路复用

 

IO多路复用

 

一、五种网络I/O模型

在Linux下进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:
同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成
后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册
的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作在没有接收完数据或者没有得到结果之前不
会返回,需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完
成。
在Linux下进行网络编程时,服务器端编程经常需要构造高性能的IO模型,常见的IO模型有五种:

  (1)同步阻塞IO(Blocking IO)
  (2)同步非阻塞IO(Non-blocking IO)
  (3)IO多路复用(IO Multiplexing)
  (4)信号驱动IO(signal driven IO)
  (5)异步IO(Asynchronous IO)

目前操作系统多为采用IO多路复用的模型模拟异步IO的方式,即我们重点介绍IO多路复用。

IO多路复用:IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题,此外poll、epoll都是这种模型。在该种模式下,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。

 

 

二、select多路复用

 

select函数原型如下:
int select (int maxfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select系统调用是用来让我们的程序监视多个文件句柄(socket 句柄)的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变
 

#include <sys/select.h>
#include <sys/time.h>
struct timeval
{
    long tv_sec; //seconds
    long tv_usec; //microseconds
};

FD_ZERO(int fd, fd_set* fds) //清空集合
FD_SET(int fd, fd_set* fds) //将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds) //判断指定描述符是否在集合中
FD_CLR(int fd, fd_set* fds) //将给定的描述符从文件中删除

int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);

说明: select监视并等待多个文件描述符的属性发生变化,它监视的属性分3类,分别是readfds(文件描述符有数据到来可读)、
writefds(文件描述符可写)、和exceptfds(文件描述符异常)。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、
或者有错误异常),或者超时( timeout 指定等待时间)发生函数才返回。当select()函数返回后,可以通过遍历 fdset,来找到
究竟是哪些文件描述符就绪。
1. select函数的返回值是就绪描述符的数目,超时时返回0,出错返回-1;
2. 第一个参数max_fd指待测试的fd个数,它的值是待测试的最大文件描述符加1。Linux内核从0开始到max_fd-1扫描文件描述符,如果有数据出现事件(读、写、异常)将会返回;假设需要监测的文件描述符是8,9,10,那么Linux内核实际也要监测0~7,此时真正带测试的文件描述符是0~10总共11个,即max(8,9,10)+1,所以第一个参数是所有要监听的文件描述符中最大的+1。
3. 中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设为NULL;
4. 最后一个参数是设置select的超时时间,如果设置为NULL则永不超时;
: 待测试的描述集的总个数。 但要注意, 待测试的描述集总是从0, 1, 2, ...开始的。 所以, 假如你要检测的描述符为8,
9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8,
9, 10) + 1
在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数中,这也意味着select所用到的FD_SET是有限的,也正是这个原因select()默认只能同时处理1024个客户端的连接请求:

/linux/posix_types.h:

#define __FD_SETSIZE 1024

下面是select多路复用实现网络socket服务器多路并发流程图:

 

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
socket编程中的多复用是指通过一种机制,使一个进程可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读写操作准备就绪),能够通知程序进行相应的读写操作。在Linux中,常用的多复用机制有select、poll和epoll。其中,select是最古老的多复用机制,poll是select的改进版,而epoll是最新、最高效的多复用机制。多复用机制可以大大提高程序的并发性能,使得程序可以同时处理多个客户端请求。 下面是一个简单的使用select实现多复用的流程图和代码示例: 流程图: ``` 1. 创建socket并绑定端口 2. 将socket设置为非阻塞模式 3. 创建fd_set集合,并将socket加入集合 4. 进入循环,调用select函数,等待文件描述符就绪 5. 如果socket就绪,表示有新的客户端连接请求,调用accept函数接受连接 6. 如果其他文件描述符就绪,表示有客户端发送数据,调用recv函数接收数据并处理 7. 回到步骤4,继续等待文件描述符就绪 ``` 代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/select.h> #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int server_fd, client_fd, max_fd, activity, i, valread, sd; struct sockaddr_in address; char buffer[BUFFER_SIZE] = {0}; fd_set readfds; // 创建socket并绑定端口 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(atoi(argv[1])); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(server_fd, MAX_CLIENTS) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } // 将socket设置为非阻塞模式 int flags = fcntl(server_fd, F_GETFL, 0); fcntl(server_fd, F_SETFL, flags | O_NONBLOCK); // 创建fd_set集合,并将socket加入集合 FD_ZERO(&readfds); FD_SET(server_fd, &readfds); max_fd = server_fd; // 进入循环,调用select函数,等待文件描述符就绪 while (1) { activity = select(max_fd + 1, &readfds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); exit(EXIT_FAILURE); } // 如果socket就绪,表示有新的客户端连接请求,调用accept函数接受连接 if (FD_ISSET(server_fd, &readfds)) { if ((client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept error"); exit(EXIT_FAILURE); } printf("New connection, socket fd is %d, ip is : %s, port : %d\n", client_fd, inet_ntoa(address.sin_addr), ntohs(address.sin_port)); // 将新的客户端socket加入集合 FD_SET(client_fd, &readfds); if (client_fd > max_fd) { max_fd = client_fd; } } // 如果其他文件描述符就绪,表示有客户端发送数据,调用recv函数接收数据并处理 for (i = server_fd + 1; i <= max_fd; i++) { sd = i; if (FD_ISSET(sd, &readfds)) { if ((valread = recv(sd, buffer, BUFFER_SIZE, 0)) == 0) { // 客户端关闭连接 printf("Client disconnected, socket fd is %d\n", sd); close(sd); FD_CLR(sd, &readfds); } else { // 处理客户端发送的数据 printf("Received message from client, socket fd is %d, message is %s\n", sd, buffer); memset(buffer, 0, BUFFER_SIZE); } } } } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值