讲一个钓鱼的小故事:
钓鱼一共分为两步一步是等
还有一步是钓
,那么真正有作用的其实是钓,可以把等待理解为无用的。
阻塞IO就是上例中的张三
:在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。
非阻塞IO就是上例中的李四
: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.
信号驱动IO就是上例的王五
: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。
IO多路转接就是上例的赵六
: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.
异步IO就是上例的田七
: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
小结
让IO更高效, 最核心的办法就是让等待的时间尽量少
.IO = 等待 + 拷贝
同步通信 vs 异步通信:
同步和异步关注的是消息通信机制.
阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
fcntl
一个文件描述符, 默认都是阻塞IO,所来所接触的也都是阻塞IO,并没有接触过非阻塞的接口。
函数原型如下:
#include
#include
int fcntl(int fd, int cmd, … /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)(最重要).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)
- 我们此处只是用第三种功能,
获取/设置文件状态标记
, 就可以将一个文件描述符设置为非阻塞
轮询方式读取标准输入
#include
#include
#include
#include
void SetNonBlock(int fd)
{
int f1 = fcntl(fd,F_GETFL); //获取文件描述符的属性
if(f1 < 0)
{
perror("fcntl error!\n");
return;
}
fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
}
int main()
{
char c = 0;
SetNonBlock(0);
while(1)
{
sleep(1);
ssize_t s = read(0,&c,1);
if(s > 0)
{
printf("%c\n",c);
}
else if(s < 0 && errno == EAGAIN)
{
printf("read cond not ok!\n");
}
else
{
perror("read error!\n");
}
printf(".........................\n");
}
return 0;
}
取
出来(这是一个位图).设置
回去. 设置回去的同时, 加上一个O_NONBLOCK参数select的核心作用就是等,就绪事件通知方式,单进程内,同时等待多个文件描述符。
系统提供select函数来实现多路复用输入/输出模型.
select函数原型
select的函数原型如下:
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
参数解释:
参数timeout取值:
关于fd_set结构
位图
”. 使用位图中对应的位来表示要监视的文件描述符.提供了一组操作fd_set的接口, 来比较方便的操作位图
- void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
- int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
- void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
- void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
关于timeval结构
函数返回值:
错误值可能为(但是一般很少情况下才会出错,所以目前先不看):
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数n 为负值。
- ENOMEM 核心内存不足
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.
socket就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT(并不是接收缓冲区只要一有数据,就立马读,而是设置了一个低水位标记,为了保证读取的效率). 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求,这个请求是以读事件报告给操作系统的;
- socket上有未处理的错误;
写就绪
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
main.cc
#include"SelectServer.hpp"
void Usage(std::string proc)
{
cout << "Usage :\n\t" << proc << "port"<< endl;
}
//Server port
int main(int argc,char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
SelectServer *ssvr = new SelectServer(atoi(argv[1]));
ssvr->InitServer();
ssvr->Start();
return 0;
}
Sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define BACKLOG 5
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
cerr << "socket error!" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock,int port)
{
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
cerr << "bind error!"<< endl;
exit(3);
}
}
static void Listen(int sock)
{
if(listen(sock,BACKLOG) < 0)
{
cerr << "listen error!"<< endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock,(struct sockaddr*)&peer,&len);
if(fd < 0)
{
cerr << "accept error!"<<endl;
}
return fd;
}
static void SetsockOpt(int sock)
{
//因为对于server来说,主动断开连接的时候会进入time_wait状态,所以要端口复用
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
}
};
SelectServer.hpp
#pragma once
#include"Sock.hpp"
#define DFL_PORT 8080
#define NUM (sizeof(fd_set)*8)
#define DFL_FD -1
class SelectServer
{
private:
int port;
int lsock;
//需要一个辅助的数组,需要把所有打开的文件描述符都保存起来
int fd_array[NUM];
public:
SelectServer(int _p = DFL_PORT)
:port(_p)
{}
void InitServer()
{
for(int i = 0;i < NUM;++i)
{
fd_array[i] = DFL_FD;
}
lsock = Sock::Socket();
Sock::SetsockOpt(lsock); //将listen socket 设置为端口复用
Sock::Bind(lsock,port);
Sock::Listen(lsock);
fd_array[0] = lsock;
}
//找到数组中默认-1的,然后把这个位置给占掉
void AddFd2Array(int sock)
{
int i = 0;
for( ; i < NUM; i++)
{
if(fd_array[i] ==DFL_FD){
break;
}
}
//有可能不断的来新链接,然后把数组给弄满了
if(i == NUM)
{
cerr << "fd array is full,close sock" << endl;
close(sock);
}
else
{
fd_array[i] = sock;
cout << "fd: " << sock << " add to select..."<< endl;
}
}
void DefFdFromArray(int index)
{
if(index >=0 && index < NUM){
fd_array[index] = DFL_FD;
}
}
void HandlerEvents(fd_set *rfds)
{
for(int i =0;i < NUM;++i)
{
if(fd_array[i] == DFL_FD)
{
continue;
}
//走到这里说明有文件描述符发生了改变,并判断该文件描述符是否属于原来的rfds集合中
//只有你设置了关心的文件描述符,在返回的时候才会发生改变,没有设置的他压根不在乎
if(FD_ISSET(fd_array[i],rfds))
{
//read ready
//其实read ready就绪依旧存在两种情况,一种就是数据准备就绪了,还有一种情况是有了新链接
if(fd_array[i] == lsock)
//link event
{
int sock = Sock::Accept(lsock);
if(sock >= 0){
//sock ok
//获得一个新的文件描述符,一定不敢直接的读取,如果对方不给你发数据,那就直接阻塞了
cout << "get a new link ... "<< endl;
AddFd2Array(sock);
}
}
//date ready event 应该进行IO
else
{
char buffer[1024];
ssize_t s = recv(fd_array[i],buffer,sizeof(buffer),0);
if(s > 0){
buffer[s] = 0;
cout << "client# " << buffer << endl;
}
else if(s == 0){
cout << "client quit" << endl;
close(fd_array[i]);
//还需要把这个文件描述符从全局数组fd_array中删除掉
DefFdFromArray(i);
}
else{
cout << "fd error!"<< endl;
close(fd_array[i]);
DefFdFromArray(i);
}
}
}
}
}
void Start()
{
int maxfd = DFL_FD;//服务器一启动的时候文件描述符就只有lsock一个
//timeout 能够设置三种值 当NULL表示没有timeout,一直阻塞式的等,当0表示非阻塞状态,当是一个确切的数值的时候,表示等特定的时间,如果时间到了没有时间发送,那么就超时返回
for(;;)
{
//对于select来说,每一次都要重新的去设置这个位图结构的fd_set
fd_set rfds;
FD_ZERO(&rfds);
cout << "fd_array: ";
for(int i = 0;i < NUM; i++)
{
if(fd_array[i] != DFL_FD)
{
cout <<fd_array[i]<< " ";
FD_SET(fd_array[i],&rfds);
if(maxfd < fd_array[i])
{
maxfd = fd_array[i];
}
}
}
cout << endl;
//这个timeout也是一个输入、输出型参数,第一次会等待5s,但是由于第一次没有事件发生timeout减为0,第二次及以后都不在等待,所以我们要每次进来都设置一次
printf("begin select...\n");
//struct timeval timeout = {5,0};
switch(select(maxfd+1,&rfds,nullptr,nullptr, nullptr))
{
case 0:
cout << "timeout"<< endl;
break;
case -1:
cerr << "select error"<< endl;
break;
default:
//此时的select的文件描述符时大于0
//success
HandlerEvents(&rfds);
break;
}
}
}
~SelectServer()
{}
};