1.1 lfd是服务器端调用socket()函数创建的
sock = socket(PF_INET, SOCK_STREAM, 0);
上面的sock会传入listen函数的第一个参数,使得sock成为了监听套接字lfd——所以也相当于是listen的作用使得服务器套接字成为了监听套接字,之前没有指定具体功能。
int listen(int sock, int backlog);//成功时返回0,失败返回-1
第一个参数就是lfd,即监听套接字;backlog 是连接等待队列请求的长度,若为5,则表示最多5个连接请求进入队列——p65-66
即有5个客户端等着连接,服务器会按顺序一个一个处理连接
1.2 客户端请求连接的函数是connect()
int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);//成功时返回0,失败返回-1
//sock,是客户端程序socket()函数创建的套接字,传入这里
//servaddr是保存目标服务器端地址信息的结构体变量的地址serv_addr
//addrlen是sizeof(servaddr),即指针变量长度
这里创建的并不是cfd,因为cfd和lfd是针对服务器说的。客户端那边就一种文件描述符,服务器端有两种,所以加以区分
**1.3 ** 服务器接收客户端的请求,需要调用accept()
这个**accept()返回cfd**,即负责与客户端进行I/O的套接字文件描述符
int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);//成功时返回cfd,失败返回-1
//第一个参数传入lfd
//第二个参数指向保存了发起连接请求的客户端地址信息
第一个参数要把lfd传进去
总的来说,lfd就是服务器端用于监听有没有客户端连接的套接字,或者说监听有没有来自客户端的connect();
而cfd就是负责连接客户端,看看有没有来自客户端的read() / write()这种I/O请求
只有先lfd监听到客户端的连接请求,然后答应连接accept(),才会创建出cfd
要补充一下文件描述符表:
文件描述符的前三个:0, 1, 2分别被定死为 标准输入、标准输出、标准错误,所以其它文件描述符都是从3开始编号的,每个进程会有一个1024长的文件描述符表
一个进程能够同时打开多个文件,对应需要多个文件描述符,所以需要用一个文件描述符表对文件描述符进行管理;通常默认大小为1024,也即能容纳1024个文件描述符;
多进程也好,多进程也好,在基于TCP/IP的服务器-客户端模型中,服务器总是在用listen()监听有没有要连接的客户端,或者调用了accept()函数并一直在阻塞,直到客户端有连接请求connect()。
用前面的lfd和cfd来简述的话: 服务器用一个lfd去监听客户端,有连接的话,对请求队列里的客户端依次答应连接请求,并创建cfd,来一个客户端就创建一个cfd,然后cfd和客户端进行I/O,而lfd继续去监听。——所以TCP/IP那本书把lfd比喻做门卫
存在的问题:
任何事件都要服务器自己做:监听客户端,数据处理。
然后能不能让**内核**也帮着处理一些事情
IO多路复用也叫 IO多路转接
select之于服务器server,相当于秘书之于老板,select是由内核提供的。之前没有select时,server要一直accept()阻塞,等待有客户端的连接。
有了select之后,相当于给各个客户端留电话,谁有事就给秘书打电话,然后秘书告诉老板去调用accept(),创建cfd,和客户端建立连接。
select自己不会创建出cfd,与客户端连接。
所以select做的就是监听,lfd。其实请求连接本质是一个读事件,只是读到的数据是连接请求
服务器不再像以前那样一直accept()阻塞,在等着有客户端连接而是让select监听到有连接请求事件之后再调用accept()。
在多进程或多线程中,相当于开辟多个进程,每个都在那accept(),一个进程对应一个客户端,客户端有事就连接上了,客户端一直没事那该进程调用的accept()就一直在那阻塞,如下图左,有了IO多路复用,就可以一个进程连接多个客户端,如下右:
服务器的服务相当于有三种:
阻塞:像前面说的调用了accept()一直在阻塞,有了连接请求就连上,结束阻塞,没有就一直阻塞
非阻塞忙轮询:不阻塞,一直去问有没有需要连接的,有就调用accept(),没有就一直问
响应式:别人有连接我再调用accept()——就是多路IO复用或者说多路IO转接
#include
#include
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);//成功返回大于0的数,失败返回-1;这个大于0的数是发生事件的文件描述符数,返回0就是没有哪个文件描述符有事件
//maxfd,文件描述符数量,是最大文件描述符的值+1
前面说了,一个进程里文件描述符表是1024大的,而0, 1,2这几个文件描述符被标准输入、标准输出、标准错误占用了,比如我有三个客户端,首先服务器的lfd得占用3,客户端的三个cfd1, cfd2, cfd3则依次是 4, 5, 6,即最大文件描述符是6,那文件描述符的数量就是6 + 1 == 7
//fd_set是一个集合,fd_set的size值在linux下一般被定义为1024,意思是select管理的文件描述符数量不能大于1024,继而文件描述符取值为0~1023
//readset、 writeset、exceptset分别对应文件描述符的三种事件,分别是读事件,写事件,异常事件
//timeout是设置的超时时间,防止陷入无限阻塞
我们传入时,假如文件描述符3,5和 6 之前经常发生读事件,那我们把他们放入read_set里,监听他们的读(因为它们实际上并不是都会发生读事件)
同理,监听文件描述符6的写事件,放进write_set,监听文件描述符7的异常事件。
他们每个集合,或者说fd_set,先暂且理解为就是一个数组,数组下标就是文件描述符0,1,2,3,4,…,如果要监听3和5的读事件,那就把下标为3,5和6处的值改为1,其余为0,然后作为readset传入select()函数第二个参数,同理,写事件,异常事件,也是这样:————位运算0和1
select()返回结果是这三个集合发生事件的总数,如下,文件描述符5和6发生了读事件,4发生了写事件,异常事件没有发生(即使我们监听了7,但它这回没发生异常):
这种情况,三个集合传入select后,返回整型 3(因为就3个文件描述符发生了事件)
显然,select()函数肯定会对我们传入的那几个集合进行修改,比如,我们传入的readset中,下标3,5和6对应的数是1,那fd3没发生,就将其改为0.
下面是fd2没发生,调用select()函数后将fd2所在数值改为0:
补充,如果我们只监听读事件,那除了readset,那另外两个参数我们传入空指针NULL即可,或者0
即先将三种监视对应的fd_set写好,然后确定maxfd,然后设置timeout。
补充下第四个参数struct timeval* timeout计时的结构体:
实际上timeout的参数设置有三种情况:
1.传NULL,就是阻塞状态,一直等下去(不限时嘛)
2.设置一个大于0的时间timeval,就是等待固定时间
3.设置timeval为0,就是非阻塞,检查描述符集合后立刻返回,轮询。——即调用一下select,就去看看有没有事件,没有事件就返回,返回后过会又调用select,又去看有没有事件
fd_set这一集合并不是数组,而是位图(bitmap),这是一种数据结构。在网络编程中,对于位图的操作都会有相应的接口函数。因为他不像数组那样好操作。
如前所述,设置相应的fd_set是使用select的第一步。
在fd_set中更改值的操作由下列**宏**完成:
FD_ZERO(fd_set* fdset);//将fd_set变量的所有位初始化为0————zero
FD_SET(int fd, fd_set* fdset);//将待监听的文件描述符添加到监听集合中,其实就是将fd的值改为1————也就是所谓的注册文件描述符fd的信息
FD_CLR(int fd, fd_set* fdset);//清除文件描述符fd的信息,就是将1改为0————clear
FD_ISSET(int fd, fd_set* fdset);//判断一个文件描述符fd是否在监听集合fdset中,在则返回1,不在则返回0————is set
使用示例如下,首先我们要定义一个fd_set类型的变量set(它可以是readset,writeset、exceptset),将它的地址传入那些宏,所以注意取地址符
如果我们要监听fd1、fd2、fd4、fd5等四个文件描述符,那就需要调用四次FD_SET(),第一个参数就是对应的1, 2,4,5,会将这些位置的值改为1,这就是所谓的**注册**文件描述符的信息
FD_CLR
本意就是clear,将fd的值清0,使用场景:比如我们监听文件描述符fd1,2,4,5,后来fd4断开连接了,我们读fd4发来的数据返回Null,就知道它断开连接了,那就需要把fd4从原来的监听集合中移除
FD_ISSET使用:FD_ISSET(4, &set);
判断文件描述符fd4是否在监听集合set中,本意就是is set。——可以用来验证select函数的调用结果