网络编程(三)TCP IO多路转接服务器编程(select)

同系列文章:
1,

一,select知识引入

前面已经介绍过了
1)单线程的服务器,可以处理单个客户端与服务端的交流;
2)多线程的服务器,可以处理多个客户端与服务端的交流。
根据前面的知识,我们知道了如果想要处理多个客户端与服务器的交流的话可以使用多线程的技术,而这篇博客就来介绍一下处理多个客户端与服务器的交流的一个新的技术:IO多路复用——select方法。

二,select基本概念

2.1 select概念理解前先总结一下通信过程(便于后文理解select)

1,listen()监听函数过后,服务器的ip与端口就会暴露在网络中,网络中连接的各个客户端就可以连接该服务器,而所有的连接请求都会存储在监听文件描述符对应的读缓冲区中,每执行一次accept,就会从该监听文件描述符对应的读缓冲区中读取一个连接。
2,如果是多线程的方法,只需要在该循环的每次循环的时候accept建立成功一个连接便开启一个子线程处理该客户端。
3,而在子线程中,服务端与对应客户端交流的时候主要通过write与read函数(或者send与recv函数),当使用write给客户端发送数据的时候,程序会将数据写入服务器中与该客户端文件描述符有关的数据写缓冲区中,内核发现缓冲区中有数据以后就会选择一个时机尽快发送;当使用read函数读取该客户端发送过来的数据的时候,程序也是查看服务器中与该客户端文件描述符有关的数据读缓冲区,如果有数据,就从读缓冲区中读数据。
4,仔细阅读我上面描述的1,2,3的一个过程,我们可以将这些过程总结一下,那就是不管是监听文件描述符,或者是普通通信文件描述符,这些文件描述符都对应了自己的读缓冲区与写缓冲区。而accept函数运行主要是查看监听文件描述符中的读缓冲区;read与write分别是查看普通通信文件描述符中的读缓冲区写缓冲区。并且一个服务器中有一个监听文件描述符,有多个通信文件描述符。
5,明确了4这样一个总结的过程,我们再来了解一下select:
select函数是一种IO多路转接函数,它委托内核检测服务器端所有的文件描述符(通信和监听两类),这个检测过程会导致进程/线程的阻塞,如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出
6,如果传出的文件描述符为监听文件描述符,那就使用accept函数处理;如果传出的文件描述符为通信文件描述符,那就使用read与write函数处理。

2.2 select函数的用法

2.2.1 select函数原型

使用select这种I0多路转接方式需要调用一个同名函数 select,这个函数是跨平台的,Linux 、Mac、Windows都是支持的。程序猿通过调用这个函数可以委托内核帮助我们检测若干个文件描述符集合的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态:(这也是我为什么前面要特别强调读缓冲区写缓冲区要有一个区分的概念的原因)
1,读缓冲区: 检测里边有没有数据,如果有数据该缓冲区对应的文件描述符就绪
2,写缓冲区︰检测写缓冲区是否可以写(有没有容量),如果有容量可以写,缓冲区对应的文件描述符就绪
3,读写异常:检测读写缓冲区是否有异常,如果有该缓冲区对应的文件描述符就绪
委托检测的文件描述符被遍历检测完毕之后,已就绪的这些满足条件的文件描述符会通过select()的参数作为传出参数分3个集合传出,程序猿得到这几个集合之后就可以分情况依次处理了。
注意:我前面将“集合”两个字加了黑体,这是我觉得读者可能读到这里暂时会有疑惑的一点,这个集合即表示多个文件描述符的集合,这是一种新的数据类型(fd_set类型),内核可以读取该数据类型的值来判断该集合中有什么文件描述符。具体的该数据结构的原理和用法我在后文会介绍,现在暂时将其当作一个普通集合即可。

下面先来看—下这个函数的函数原型:

#include <sys/select.h>
struct timeval {
   
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval * timeout);

1,nfds参数后面讲,首先可以看到第二,三,四个参数分别是fd_set类型的,表示select函数接下来要检测第二个参数表示的集合的读缓冲区有哪些已经就绪,第三个参数表示的集合的写缓冲区有哪些已经就绪,第四个参数表示的集合的读写缓冲区有哪些有异常。这些第二,三,四个参数个别可以直接置为NULL,表示不去检测某个集合对应的读或者写缓冲区。第五个参数表示select检测的时间,如果为0则表示如果检测到没有某个集合里面的文件描述符符合条件,进程就在此阻塞。第二,三,四个参数也是传出参数,在select函数执行完以后,这些集合里面存放的就都是那些对应缓冲区就绪的文件描述符。
2,nfds参数:文件描述符的类型为int类型,而前面第二,三,四参数传入了大于等于1个文件描述符的集合,即多个int类型元素的集合,这里的nfds正是传入的这些集合当中文件描述符的值最大的那个文件描述符fdmax+1。

2.2.2 使用select函数

接下来读者们一定迫不及待想用select函数来进行编程了,下面就是使用select函数来实现多客户端处理的一个流程:
1,先按照正常的单线程服务器编程的前面流程一般,生成一个监听文件描述符,然后将该监听文件描述符放入一个fd_set集合中,并且作为第二个参数readfds(查看该集合中读缓冲区就绪的文件描述符)传入select函数。
2,制作一个while(1)循环,在该循环内部不断执行select函数,然后查看传出的readfds集合中的监听文件描述符是否就绪。
3,如果就绪,则此次accept以后将接收到一个客户端通信描述符,可以将此通信文件描述符放入readfds集合中,下次循环就会也检测该文件描述符的状态。
4,然后查看该集合中其他的所有的非监听文件描述符(即通信文件描述符)哪些就绪了,依次处理已就绪了的客户端的交流。并且处理每个客户端的时候不用向以前一样在内部while(1)。
仔细思考这个while循环的流程,我们可以发现,在整个while内部,都是先检测一下是否有新连接,然后依次处理一遍有数据到来的客户端。如此如此,不断循环。
注意:上文是编写一个select io多路转接服务器的整个程序运行流程,不过看了整个流程以后,读者们一定意识到了,上面流程中涉及到很多和fd_set集合有关的操作,而现在只需要理解并且学会使用fd_set这个数据结构,整个流程就可以没有障碍地编写下来了,所以最后再详细介绍一下fd_set这个数据结构,然后开始编程。

2.2 fd_set数据结构

我们先来看一下fd_set数据结构的原型

typedef struct{
   
    long int fds_bits[32];
}fd_set;

可以看出,fd_set是一个数据长度为32的long类型的数组,有读者要问了,fd_set不是表示一个集合的数据结构吗?读者不用着急,实际上long类型数据占据4个字节,即总共128个字节,即这块内存共1024位,这并不是巧合,而是故意为之,其实这块内存中的每一个bit位和文件描述符的值是一一对应的,这样就可以用最小的存储空间将要存储的在1024之内的所有的文件描述符都表示在这个fd_set数据结构当中了。只要对应的bit位为1,则代表与之对应的文件描述符在该集合中。当然,前面讲的东西是fd_set这个东西为什么能够作为一个集合使用的原理,我们作为系统编程的程序员当然不用关心怎样建立某一个文件描述符到这块内存的映射关系,而是有一个专门操作这个数据类型的接口:

// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0        
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);

再补充一点:
select函数中第一个参数为什么要求输入待检测的集合中的文件描述符的最大值maxfd+1?因为select函数内部需要从0开始遍历到maxfd+1,如果fd_set集合内部内存的对应的那一bit位为1(即集合中存在该元素),内核就去检测其对应的读/写缓冲区是否就绪,如果就绪,则这一bit位就还是1,如果未就绪,则将这一bit位改为0,即相当于从集合中删去这个文件描述符。

三, select多路转接服务器例子

1,服务端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
   
    // 1. 创建监听的fd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5000);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(lfd
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值