二、非阻塞IO
c++
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//最后一个参数可以设置为MSG_DONTWAIT,可以实现非阻塞的方式进行IO,但是并不方便
可以使用open来设置打开方式为O_NONBLOCK;
c++
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
//可以直接修改文件对象的flag标志位,告诉Linux内核这个文件描述符以现非阻塞方式进行IO;
//第一个参数是文件描述符,第二个参数有多种选项如下;
//复制一个现有的描述符(cmd=F_DUPFD);
//获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD);
//获得/设置文件状态标记(cmd=F_GETFL或F_SETFL);O_NONBLOCK;
//获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN);
//获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW);
Linux平台下,ctrl+d快捷键表示标准输入结束;
设置非阻塞如下:
c++
void setnoblock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
cerr << "fl error:";
return;
}
fcntl(0, F_SETFL, fl | O_NONBLOCK);
}
当非阻塞设置之后,会设置错误码,表示资源暂时不可获取;
c++
#define EWOULDBLOCK EAGAIN //表示操作将会被阻塞,如果是非阻塞方式,则表示条件不就绪,将会再尝试一次;
while (true)
{
printf("please enter# ");
fflush(stdout);
ssize_t n = read(0, buff, sizeof(buff) - 1);
if (n > 0)
{
buff[n - 1] = 0;
cout << "echo: " << buff << endl;
}
else if (n == 0)
{
cout << "read done" << endl;
break;
}
else // 资源暂时不可以获取
{
// 1.当设置成非阻塞的方式时,数据没有就绪,read/write/recv/send等接口就会以出错的方式返回;
// 2.出错并不是意味着真的出错了,而是表示一种资源未就绪的状态;
// 3.通过errno区分是真的出错还是资源未就绪,当错误码为11时就以为着资源未就绪;
if (errno == EWOULDBLOCK/*EAGAIN*/)
{
cerr << "read err, n: " << n << ", errno: " << errno << ", " << strerror(errno) << endl;
sleep(1);
}
else
{
break;
}
}
}
三、多路转接
3.1select
IO = 等待 + 拷贝;
select只负责等待,一次可以等待多个fd;
c++
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
//第一个参数:表示最大文件描述符下标+1;
//返回值:>0,表示有n个fd就绪,==0,表示超时,没有错误也没有fd就绪,<0,表示等待出错;
//第五个参数:给select设置等待时间,1.当等待时间内没有文件描述符就绪就直接返回,值为0,如果有fd就绪就立即返回;2.当时间设置为{0,0}时就意味着非阻塞立即返回;3.当设置为NULL的时候就会阻塞式地等待;4.此参数是输入输出型参数,当提前返回时,此参数显示的就是剩余的快超时时间;
//第二-四个参数:1.fd_set是一个内核提供的数据类型,其实是一个位图类似信号集;2.fd共有三种事件,分别是读时间、写时间、异常事件等;3.关心哪一个事件,就将文件描述符添加到对应的参数位图结构里;4.也是输入输出型参数,输入时,将fd添加到对应参数fd集合里就会让操作系统关心对应的事件,返回时,会告诉用户那些文件描述符就绪;5.比特位的位置表示的是文件描述符的编号,比特位从低到高对应文件描述符从小到大;输入时1/0表示是否对应文件描述符是否关心参数对应事件,输出时1/0表示参数对应事件是否就绪;
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
//如下函数就是对fd_set结构进行修改的接口;
void FD_CLR(int fd, fd_set *set);//将位图中fd对应位置变为0;
int FD_ISSET(int fd, fd_set *set);//判断fd在位图中是否已经被设置;
void FD_SET(int fd, fd_set *set);//将fd添加到位图结构里;
void FD_ZERO(fd_set *set);//将位图清空;
总结:select只负责等待过程中通知事件就绪;
3.2代码实现
要注意不能直接accept,因为accept就是阻塞的等待读事件就绪,所以此时应该使用的是select等待多个文件描述符;
注意等待时间是一个输入输出参数,所以要进行周期地重复设置;
如果事件就绪了,但是没有处理事件,那么select就会一直发送消息,并且当读取fd时是不会阻塞的;
select与多线程的优势就是在于select单执行流就可以使得大量的fd的等待事件重叠,减少IO过程中等待时间的占比,而多线程也可以使得等待时间重叠,但是要创建多个线程;
c++
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Socket.hpp"
class selectserver
{
public:
selectserver(const uint16_t port = defaultport) : port_(port) {}
~selectserver()
{
listensock_.Close();
}
public:
bool init()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
void run()
{
int listensockfd = listensock_.Fd();
for (;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensockfd, &rfds);
timeval time = {1, 0};
int n = select(listensockfd + 1, &rfds, nullptr, nullptr, /*nullptr*/ &time);
switch (n)
{
case 0:
std::cout << "time out, time: " << time.tv_sec << "." << time.tv_usec << std::endl;
break;
case -1:
std::cerr << "select err" << std::endl;
break;
default:
// 表示有事件就绪
std::cout << "get a new link..." << std::endl;
handlerevent();
break;
}
}
}
void handlerevent()
{
}
private:
Sock listensock_;
uint16_t port_;
static const uint16_t defaultport = 8080;
};