目录
一.服务器代码
以下各函数代码功能是接收客户端的数据并原样发送给客户端。
1.select方式
FD_ZERO(*fd_set),用来清空fd_set集合。
FD_SET(*fd_set,fd),把fd加到fd_set集合。
FD_ISSET(*fd_set,fd),测试fd是否在集合fd_set中。
int select(int maxfd, fd_set * readset, fd_set * writeset,fd_set * excepset, struct timeval * tv);
参数maxfd是当前最大文件描述符+1。
参数readset是要测试的读文件描述符集合。
参数writeset是要测试的写文件描述符集合。
参数excset是要测试的异常描述符号集合。
参数tv是设置超时时间,如果是NULL将一直等待;如果结构体填0,那将立即查询并返回。
函数返回值:如果有就绪的文件描述符则返回其个数,如果超时则返回0,如果出差返回-1。
void echo_select(int listfd)
{
sockaddr_storage cliaddr;
socklen_t cliaddrlen = 0;
fd_set fsetall;
FD_ZERO(&fsetall);
FD_SET(listfd, &fsetall);
int maxfd = listfd;
unordered_set<int> socketfds;
char buff[1024];
int nready = 0;
for (;;)
{
fd_set fsr = fsetall;
nready = select( maxfd + 1, &fsr, NULL, NULL, NULL);
if(nready < 0)
{
nready = 0;
sys_error("select");
}
if(FD_ISSET(listfd, &fsr))
{
bzero(&cliaddr, sizeof(cliaddr));
int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
if(confd <= 0)
sys_error("accept");
if(confd > maxfd)
maxfd = confd;
PrintClientInfo((sockaddr*)&cliaddr);
socketfds.insert(confd);
FD_SET(confd, &fsetall);
if(--nready == 0)
continue;
}
for(auto iter = socketfds.begin(); iter != socketfds.end();)
{
int clifd = *iter;
if(FD_ISSET(clifd, &fsr))
{
ssize_t readn = read(clifd, buff, sizeof(buff));
if(readn <= 0)
{
int errnum = errno;
if(errnum != 0)
{
if(EAGAIN == errnum || EINTR == errnum || EWOULDBLOCK == errnum)
continue;
else if(ECONNRESET == errnum)
{
// client close. nothing to do.
}
else
cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
}
iter = socketfds.erase(iter);
FD_CLR(clifd, &fsetall); //unregister
close(clifd);
}
else if(readn > 0)
{
iter++;
ssize_t wn = write(clifd, buff, readn);
if(wn < 0)
sys_error("socket write");
}
if(--nready == 0)
break;
}
else
iter++;
}
}
}
2.poll方式
struct pollfd {
int fd; //需要测试的文件描述符,-1则跳过测试
short events; //POLLIN,POLLOUT 需要测试的条件
short revents; //POLLERROR 的返回可不需要在events中填写
};
int poll(struct pollfd * fds, nfds_t fdslen, int time)
参数fds和fdslen指定了需要测试的struct pollfd数组。
参数time设置超市时间。如果-1则一直等待直到有一个描述符就绪,0立即返回。
函数返回值:如果有就绪的文件描述符则返回其个数,如果超时则返回0,如果出差返回-1。
void echo_poll(int listfd)
{
struct pollfd fds[OPEN_FD_MAX];
fds[0].fd = listfd;
fds[0].events = POLLIN;
for(int i = 1; i < arraysize(fds); ++i)
fds[i].fd = -1;
int realsize = 1;
int nready = 0;
sockaddr_storage cliaddr;
socklen_t cliaddrlen = 0;
char buff[1024];
for(;;)
{
assert(nready == 0);
nready = poll(fds, realsize, -1);
int kk = nready;
if(nready < 0)
{
sys_error("poll");
nready = 0;
}
if(fds[0].revents & POLLIN)
{
bzero(&cliaddr, sizeof(cliaddr));
int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
if(confd <= 0)
sys_error("accept");
PrintClientInfo((sockaddr*)&cliaddr);
//绑定新的连接到一个空闲的位置
for(int i = 1; i < arraysize(fds); ++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = confd;
//fds[i].events = (POLLIN | POLLERR);// 不需要设置监听POLLERR,当POLLERR发生时总会在fds[i].revents中返回
fds[i].events = POLLIN;
if(i >= realsize)
realsize = i + 1;
break;
}
}
if(--nready == 0)
continue;
}
for(int i = 1; i < realsize; ++i)
{
if(fds[i].fd == -1)
continue;
if((fds[i].revents & (POLLIN | POLLERR)) > 0 )
{
while(true)
{
ssize_t readn = read(fds[i].fd, buff, sizeof(buff));
if(readn > 0)
{
write(fds[i].fd, buff, readn);
}
else
{
int errnum = errno;
if(errnum != 0)
{
if(EINTR == errnum)
continue;
else if(EWOULDBLOCK == errnum || EAGAIN == errnum)
break;
else if(ECONNRESET == errnum)
{
// client close. nothing to do.
}
else
cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
}
close(fds[i].fd);
fds[i].fd = -1; //unregister
}
break;
}
if(--nready == 0)
break;
}
}
assert(nready == 0);
}
}
3.epoll方式
epoll_create打开一个epoll文件描述符,供下面2函数使用。
iepoll_ctl添加、修改、删除需要测试的文件描述符。
epoll_wait进行测试。
/*
EPOLLET模式。
1.如果用这个模式监听listfd,需要while accept。
2.如果用这个模式监听socketfd,需要while read
3.如果使用while相应的得使用fcntl设置成非阻塞模式
*/
void echo_epoll(int listfd, bool bet)
{
int epfd = epoll_create(OPEN_FD_MAX);
epoll_event tmpet;
tmpet.data.fd = listfd;
if(bet)
{
tmpet.events = EPOLLIN|EPOLLET;
SetNoBlock(listfd);
}
else
tmpet.events = EPOLLIN;
int retepollctl = false;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, listfd, &tmpet) < 0)
sys_error("epoll_ctl");
epoll_event epoll_events[OPEN_FD_MAX];
sockaddr_storage cliaddr;
socklen_t cliaddrlen = 0;
char buff[1024];
for(;;)
{
int nready = epoll_wait(epfd, epoll_events, OPEN_FD_MAX, -1);
if(nready < 0)
{
sys_error("epoll_wait");
nready = 0;
continue;
}
for(int i = 0; i < nready; i++)
{
epoll_event* pitem = &epoll_events[i];
if(pitem->data.fd == listfd)
{
do{
bzero(&cliaddr, sizeof(cliaddr));
int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
if(confd < 0)
{
if(errno == EWOULDBLOCK)
break;
else
sys_error("accept");
}
PrintClientInfo((sockaddr*)&cliaddr);
tmpet.data.fd = confd;
if(bet)
{
tmpet.events = EPOLLIN | EPOLLET;
SetNoBlock(confd); // EPOLLET must use noblock.
}
else
tmpet.events = EPOLLIN;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, confd, &tmpet) < 0)
sys_error("epoll_ctl");
}
while(bet);
}
else
{
do
{
int readn = read(pitem->data.fd, buff, sizeof(buff));
if(readn <= 0)
{
int errnum = errno;
if(errnum != 0)
{
if(EINTR == errnum)
continue;
if(EAGAIN == errnum || EWOULDBLOCK == errnum)
break;
else if(ECONNRESET == errnum)
{
// client close. nothing to do.
}
else
cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
}
// step1. if first call close, epoll_ctl will call error, errno to bo Bad file descriptor.
tmpet.data.fd = pitem->data.fd;
tmpet.events = EPOLLIN | EPOLLET;
if(epoll_ctl(epfd, EPOLL_CTL_DEL, pitem->data.fd, &tmpet) < 0)
sys_error("epoll_ctl");
// step2.
close(pitem->data.fd);
}
else
write(pitem->data.fd, buff, readn);
}
while (bet);
}
}
}
}
4.kqueue方式
kqueue();
int kevent(int kq, //kqueue();返回的文件描述符
const struct kevent *changelist, //需要测试的列表
int nchanges,//需要测试的列表的元素个数
struct kevent *eventlist,//监测到就绪后返回的列表
int nevents,//返回列表的元素个数
const struct timespec *timeout);//超时时间,NULL一直等待,0立马返回
void echo_kqueue(int listfd, bool bet)
{
int kq = kqueue();
struct kevent ke;
if(bet)
EV_SET(&ke, listfd, EVFILT_READ, EV_ADD|EV_CLEAR, 0, 0, 0);
else
EV_SET(&ke, listfd, EVFILT_READ, EV_ADD, 0, 0, 0);
kevent(kq, &ke, 1, NULL, 0, NULL);
SetNoBlock(listfd);
struct kevent eventlist[1024];
sockaddr_storage cliaddr;
socklen_t cliaddrlen = 0;
char buff[1024];
for(;;)
{
int nready = kevent(kq, NULL, 0, eventlist, arraysize(eventlist), NULL);
if(nready < 0)
sys_error("kevent");
for(int i = 0;i < nready; ++i)
{
struct kevent act = eventlist[i];
int actfd = (int)act.ident;
if(actfd == listfd)
{
bzero(&cliaddr, sizeof(cliaddr));
do
{
int confd = accept(listfd, (sockaddr*)&cliaddr, &cliaddrlen);
if(confd > 0)
{
PrintClientInfo((sockaddr*)&cliaddr);
if(bet)
{
EV_SET(&ke, confd, EVFILT_READ, EV_ADD|EV_CLEAR, 0, 0, 0);
SetNoBlock(confd);
}
else
EV_SET(&ke, confd, EVFILT_READ, EV_ADD, 0, 0, 0);
kevent(kq, &ke, 1, NULL, 0, NULL); // 如果返回列表设置为0,函数会立马返回,相当于只注册,类似epoll_ctl。
}
else
{
if(errno == EWOULDBLOCK)
break;
else
sys_error("accept");
}
}while(bet);
}
else
{
do
{
ssize_t readn = read(actfd, buff, sizeof(buff));
if(readn > 0)
{
write(actfd, buff, readn);
}
else
{
int errnum = errno;
if(errnum != 0)
{
if(EINTR == errnum)
continue;
if(EAGAIN == errnum || EWOULDBLOCK == errnum)
break;
else if(ECONNRESET)
{
//client close. nothing to do.
}
else
cout << "errno:"<< errnum <<" " << strerror(errnum) << " readn:" << readn << endl;
}
/*
EV_DELETE Removes the event from the kqueue. Events which are attached
to file descriptors are automatically deleted on the last close of the descriptor.
*/
//manual unregister. in this example this step can cancel.
EV_SET(&ke, actfd, EVFILT_READ, EV_DELETE, 0, 0, 0);
kevent(kq, &ke, 1, NULL, 0, NULL);
//close will auto unregister the kevent.
close(actfd);
break;
}
}while(bet);
}
}
}
}
二.重要总结
1.设置非阻塞时,不能影响原来标志位.
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
2.listen(int listenfd,int backlog);backlog参数是设置新连接队列的长度限制,即半连接队列(未完成3次握手)和全连接队列的连接数总和。
backlog=0,在不同系统中实现不同,可尝试用 char* ptr = getenv("LISTENQ");取环境变量的值。
3.io阻塞函数会被系统信号中断,errno被设置为EINT.
4.如果一个socket一端已经关闭,另一端第一次写会返回ECONNRESET,再次写会EPIPE。可捕获信号.
fcntl(socketfd, F_SETOWN, getpid()); //先设置属主
signal(SIGPIPE, del_pipe);//再设置信号捕捉函数
5.select每次调用会修改fd_set,所以每次调用前需要重设需要监听的fd_set.
6.poll的events无需设置监听POLLERR,当错误产生时会在revents中返回,移除监听把fd设置为-1即可.
7.epoll用ET模式时一次事件只触发一次,所以需要用while循环,用循环为了避免阻塞需要把描述符设置为非阻塞.
例如socket缓冲区收到100字节,读取50字节后,调用epoll_wait,如果是ET模式不会再次触发,非ET模式会再次触发。如果监听套接字也是ET模式,accept不用while循环的话,只能获取一个就绪连接,导致其他的连接不能及时响应。
8.kqueue的EV_CLEAR类似epoll的POLLET.
9. kqueue的kevent函数类似epoll的epoll_ctl和epoll_wait二者组合.
kevent(int kq,const struct kevent *changelist, int nchanges,struct kevent *eventlist, int nevents, const struct timespec *timeout);
如果参数nchanges不为0,nevents设置为0,相当于注册类似epoll_ctl,如果参数nchanges为0,nevents不为0,相当于等待事件触发类似epoll_wait。
10.epoll和kqueue中当描述符要关闭时,确保在调用close前先unregister事件.
在epoll中,先close(fd);再epoll_ctl(,EPOLL_CTL_ADD,fd)。如果fd是最后的关闭(比如fork需要父子进程都close),此处epoll_ctl将出错,errno=EBADF,Bad file descriptor。
在kqueue中 Events which are attached to file descriptors are automatically deleted on the last close of the descriptor.
11.服务器使用getaddrinfo函数,这个函数把协议相关性隐藏在库函数内部,适配ipv4和ipv6.
12.kqueue官方文档
https://man.freebsd.org/cgi/man.cgi?kqueue#EXAMPLES
三.测试
写了个客户端程序,同时启动100个线程模拟并发,进行数据的发送和读取。
1.服务编译与运行
通过参数指定用哪种模式运行。
注意select,poll是跨平台的, epoll是linux实现,kqueue是macos实现。
参数epoll,epollet,kqueue,kqueueet分别模拟了水平触发模式和边缘触发模式。
[root@local apue.3e]# g++ service.cpp -o service
[root@local apue.3e]# ./service epollet
run epoll_et model.
^C
[root@local apue.3e]# ./service poll
run poll model.
^C
[root@local apue.3e]# ./service select
run select model.
^C
[root@local apue.3e]# ./service epoll
run epoll model.
yadou@yadou-mac Debug % ./service kqueue
run kqueue model.
^C
yadou@yadou-mac Debug % ./service kqueueet
run kqueue_et model.
2.客户端编译与运行
因为使用了多线程所以编译需要链接线程库,-lpthread.
[root@local apue.3e]# g++ client.cpp -lpthread -o client
[root@local apue.3e]# ./client
finish:100 success:100 fail:0
send:204500
recive:204500
3.源码下载地址
https://download.csdn.net/download/yadoufeng/87743624
下载源码后可以修改服务器和客户端的缓冲大小,观察边缘触发模式和水平触发模式。
以验证重要总结第7点。