九 I/O复用
9.1 select系统调用
select系统的用途: 在一段指定的时间内,监听用户感兴趣的文件描述符上的可读、可写、异常事件
select API
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
参数含义:
🌓 nfds 指定监听文件描述符的总数,通常设置为最大监听文件描述符的值+1,因为文件描述符是从0开=开始计数的.
🚶 readfds,writefds,exceptfds分别指向可读,可写,和异常事件对应的文件描述符集合,应用程序调用时,通过三个参数传入自己感兴趣的文件描述符,返回调用时,内核将修改他们来通知程序那些文件描述符已经就绪.
fd_set结构体仅包含一个整形数组,数组元素的每一位标记一个文件描述符,数量最大为1024,可以使用宏来访问结构体中的位
#include <sys/select.h>
FD_ZERO(fd_set *fd_set); 清楚所有位
FD_SET(int fd, fd_set *fdset); 设置fd中的位
FD_CLR(int fd, fd_set *fdset); 清除fdset中的位
int FD_ISSET(int fd, fd_set* fdset); 测试fdset的位fd是否被设置
📛 timeout参数:用来设置select的超时时间, 他是一个timeval类型的指针,我们不能完全信任timeout返回值,因为调用失败的似乎返回值是不确定的。
struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
}
处理带外数据
带外数据可理解为紧急数据,优先级高的数据
socket上接收普通数据和带外数据都会使得select返回, 但是socket处于不同的就绪状态,普通数据返回可读状态, 带外数据返回异常状态。见代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);//字符串转换为整形函数
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);//转换为二进制网络字节序地址
address.sin_port = htons(port);//主机字节序-->网络字节序
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
if(connfd < 0){
printf("errno is : %d\n", errno);
close(listenfd);
}
char buf[1024];
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);
while(1){
memset(buf, '\0', sizeof(buf));
//每次调用select都要重新在read_fds和exception_fds上设置文件描述符connfd
FD_SET(connfd, &read_fds);
FD_SET(connfd, &exception_fds);
ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
if(ret < 0){
printf("selection failure");
break;
}
if(FD_ISSET(connfd, &read_fds)){
ret = recv(connfd, buf, sizeof(buf) - 1, 0);//减去1是减去 '\0'的长度
if(ret < 0){
break;
}
printf("get %d bytes of normal data: %s\n", ret, buf);
}
//对于异常事件
else if(FD_ISSET(connfd, &exception_fds)){
ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB);//用于接收带外数据
if(ret < 0){
break;
}
printf("get %d bytes of oob data: %s\n", ret, buf);
}
}
close(connfd);
close(listenfd);
return 0;
}
9.2 poll系统调用
poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以检测其中是否有就绪者
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int time_out);
🍕fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读可写和异常事件
struct pollfd{
int fd; 文件描述符
short events; 注册的事件
short revents; 实际发生的事件,由内核填充
}
fd成员为文件描述符,events告诉poll需要监听fd上的那些事件,它是一系列事件的按位或,事件类型可以查看man手册。
🍕nfds参数指定被监听事件集合fds的大小,nfds_t的定义如下:
typedef unsigned long int nfds_t;
🍕timeout参数指定poll的超时值,单位为毫秒,当timeout为-1时,poll调用将永远阻塞,直到某个时间发生;当timeout为0时,poll调用理科返回,返回值的含义与select相同。
9.3 epoll系列系统调用
内核时间表
epoll是linux特有的I/O复用函数,他使用一组函数来实现其功能,epoll把用户关心的文件描述符放在内核中的一个事件表里面,那么这就需要定义一个额外的文件描述符来标识这个事件表。函数为:
#include <sys/epoll.h>
int epoll_create(int size);
函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表,epoll内核事件表操作函数为:
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
其中,fd参数是要操作的文件描述符,op参数则是指定的操作类型,共有三种:
🍔EPOLL_CTL_ADD: 往事件表里注册fd上的事件
🍔EPOLL_CTL_MOD: 修改fd上的注册事件
🍔EPOLL_CTL_DEL: 删除fd上的注册事件
epoll_event结构指针指定事件,结构体定义为:
struct epoll_event{
__uint32_t events;//epoll事件
epoll_data_t data;//用户数据
}
events描述事件类型,epoll支持的事件类型和poll基本相同,宏定义加了个"E",多了EPOLLET和EPOLLONESHOT.这两个事件对提高效率很重要。
其中epoll_data_t定义成了一个联合体,联合体共用一块内存,因此使用不太方便,常常使用其中的fd参数来指定事件的从属文件描述符。epoll_ctl函数成功是返回0,失败则返回-1,并设置errno。
epoll_wait函数
epoll系列系统调用的主要接口函数就是epoll_wait函数,他在一段超时时间内等待一组文件描述符,函数原型为;
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout)
函数调用成功时返回就绪的文件描述符的个数,失败的时返回-1并设置errno。timeout就是超时时间,maxevents参数指定最多监听多少个事件,必须大于零,也存在等于-1的情况,表示该函数无限期阻塞。如果检测到事件,就将就绪的事件从内核事件表,由epfd参数指定的内核事件表中复制到第二个参数events数组中,这就提高了应用程序索引就绪文件描述符的效率。
LT模式和ET模式
epoll对文件描述符的操作有两种模式,电平触发模式(LT)和边缘触发模式(ET),默认工作模式为电平触发模式,当epoll向内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将会以ET模式来处理该文件描述符。自古以来,不论数字电路,单片机,以及网络编程,还有FPGA,皆是边缘触发效率更高,不知为何。
对于电平触发,epoll_wait可以多次报告某一个未处理的事件,直到事件被处理,应用程序可以选择不处理该事件。而对于边缘触发来说,epoll_wait只会报告该事件一次,所以应用程序必须马上处理,没有第二次机会。这也体现了边缘触发效率高的原因:很大程度上降低了同一个事件被触发的次数。
实例代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
//将文件描述符设置成非阻塞的
int setnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
//将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件中
//参数enable_et指定是否对fd启用ET模式
void addfd(int epollfd, int fd, bool enable_et){
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;//可读事件
if(enable_et){
event.events |= EPOLLET;//电平触发模式
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//该函数用来操作epoll的内核事件表
setnonblocking(fd);
}
//LT模式的工作流程
void lt(epoll_event* events, int number, int epollfd, int listenfd){
char buf[BUFFER_SIZE];
for(int i = 0;i < number; i++){
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connfd, false);//对connfd禁用ET模式
}
else if(events[i].events & EPOLLIN){
//只要socket读缓存中还有未读出的数据,这段代码就会被触发
printf("events trigger once\n");
memset(buf, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
if(ret <= 0){
close(sockfd);
continue;
}
printf("get %d bytes of content: %s\n", ret, buf);
}
else{
printf("something else happend, but we dont konw is 啥");
}
}
}
//ET模式的工作流程
void et(epoll_event* events, int number, int epollfd, int listenfd){
char buf[BUFFER_SIZE];
for(int i = 0; i < number; i++){
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connfd, true);//对connfd开启ET模式
}
else if(events[i].events & EPOLLIN){
//这段代码不会被循环触发,所以我们循环读取数据,以确保socket读缓存中
//所有数据读出
printf("events trigger once\n");
while(1){
memset(buf, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
if(ret < 0){
//对于非阻塞IO, 下面的条件成立表示数据读取完毕,此后,epoll就鞥再次触发
//sockfd上的EPOLLIN事件, 以驱动下次读操作
if((errno == EAGAIN) || (errno == EWOULDBLOCK)){
printf("read later\n");
break;
}
close(sockfd);
break;
}
else if(ret == 0){
close(sockfd);
}
else{
printf("get %d bytes of content: %s\n", ret, buf);
}
}
}
else{
printf("something else happend, but we dont konw is 啥");
}
}
}
int main(int argc, char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port_number\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd, listenfd, true);
while(1){
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER,-1);
if(ret < 0){
printf("epoll failure\n");
break;
}
lt(events, ret, epollfd, listenfd);
//et(events, ret, epollfd, listenfd);
}
close(listenfd);
return 0;
}
运行此程序(客户端使用nc命令)
服务端:
ET模式下被触发的次数比LT模式下辈出发的次数要少很多。
注意:每个使用ET模式下的文件描述符都应该是非阻塞的,如果文件描述符是阻塞的,那么读写操作会因为没有后续的事件而一直处于阻塞状态。
EPOLLONESHOT事件
为防止一个线程在处理某一个socket时,该socket又有了新数据可读,导致EPOLLIN再次被触发出现多个线程同时操作一个socket的局面,可以使用EPOLLONESHOT事件来实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或异常事件,且只会触发一次,除非我们使用epoll_ctl函数重置该EPOLLONESHOT事件。如此一来,当某一个线程处理一个socket时,其他线程是不可能有机会操作该socket的。但是,该socket处理完毕后就应该重置该EPOLLONESHOT事件,确保下一次可读时,其EPOLLIN事件能被触发。
实例代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds{
int epollfd;
int sockfd;
};
int setnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
//将fd上的EPOLLIN和EPOLLET注册到epollfd指示的epoll内核时间表
//参数oneshot指定是否注册fd上的epolloneshot事件
void addfd(int epollfd, int fd, bool oneshot){
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
if(oneshot){
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
//重置fd上的事件,这样操作以后,尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发
//fd上的EPOLLIN事件,但是只会被触发一次
void reset_oneshot(int epollfd, int fd){
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);//中间参数代表更改与目标文件描述符fd相关联的事件事件。
}
//工作线程
void* worker(void* arg){
int sockfd = ((fds*)arg)->sockfd;
int epollfd = ((fds*)arg)->epollfd;
printf("start new thread to receive data on fd: %d\n", sockfd);
char buf[BUFFER_SIZE];
memset(buf, '\0', BUFFER_SIZE);
//循环读取socket上的内容,知道遇到 EAGAIN错误
while(1){
int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
if(ret == 0){
close(sockfd);
printf("foreiner closed the connection\n");
break;
}
else if(ret < 0){
if(errno == EAGAIN){
reset_oneshot(epollfd, sockfd);
printf("read later\n");
break;
}
}
else{
printf("get connect: %s\n", buf);
//休眠5秒。模拟数据处理过程
sleep(5);
}
}
printf("end thread receiving data on fd: %d\n", sockfd);
}
int main(int argc, char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
//监听socket,listenfd上是不能注册EPOLLONESHOT事件的,否则应用程序
//只能处理一个客户连接,因为后续的连接将不再触发listenfd上的epoolin事件
addfd(epollfd, listenfd, false);
while(1){
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if(ret < 0){
printf("epoll failure\n");
break;
}
for(int i = 0; i < ret; i++){
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address,
&client_addrlength);
//对每个非监听文件描述符都注册EPOLLONESHOT事件
addfd(epollfd, connfd, true);
}
else if(events[i].events & EPOLLIN){
pthread_t thread;
fds fds_for_new_worker;
fds_for_new_worker.epollfd = epollfd;
fds_for_new_worker.sockfd = sockfd;
//新启动一个工作线程为sockfd服务
pthread_create(&thread, NULL, worker, (void*)
&fds_for_new_worker);
}
else{
printf("some thing happend \n");
}
}
}
close(listenfd);
return 0;
}
代码的含义为,同一个socket在线程处理消息的过程中,如果该socket多次传入信息,则不会更换线程处理。
(客户端)
(服务端)
此段代码并没有显示出重置EPOLLONESHOT事件后可以更换线程的知识,留待以后实验。尽管一个socket在不同时间可能被不同的线程处理,但是同一时刻肯定只有一个线程为他服务,这就保证了连接的完整性,从而避免了很多可能的竞态情况的产生。
9.4 三种IO复用方式的比较
(https://blog.csdn.net/weixin_43748094/article/details/98976208)
(https://mp.weixin.qq.com/s/SuCUybYv4YHn7wY12WBlCQ) 微信公众号文章
9.5 IO复用的高级应用一: 非阻塞connect
connect出错是存在一种errno值为 EINPROGRESS, 这种错误发生在对非阻塞的socket调用connect,而连接又没有立即建立的时候,在这种情况下,可以调用select,poll等函数来监听这个连接失败的socket上的读写事件,当select,poll函数返回时, 利用getsockopt来读取错误码并清除上边的错误。如果错误码为0,表示连接成功建立。
通过非阻塞connect方式,就可以同时发起多个请求一起等待。
代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1023
int setnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
//超时连接函数,参数是服务器IP地址,端口号,和超时时间。
//成功时间返回连接状态的socket,失败返回-1
int unblock_connect(const char* ip, int port, int time){
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
int fdopt = setnonblocking(sockfd);
ret = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
if(ret == 0){
//代表连接成功,恢复sockfd属性,返回之
printf("connect wit server immediately\n");
fcntl(sockfd, F_SETFL, fdopt);
return sockfd;
}
else if(errno != EINPROGRESS){
//如果连接没有建立,那么只有当errno时候上述错误码的时候
//表示连接还在进行,否则出错返回
printf("unblock connect is not support");
return -1;
}
fd_set readfds;
fd_set writefds;
struct timeval timeout;
FD_ZERO(&readfds);
FD_SET(sockfd, &writefds);//设置fd中的位
timeout.tv_sec = time;
timeout.tv_usec = 0;
ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);
if(ret <= 0){
//select出错或者超时,立即返回
printf("connect time out\n");
close(sockfd);
return -1;
}
if( !FD_ISSET(sockfd, &writefds)){
printf("no events on sockfd found\n");
close(sockfd);
return -1;
}
int error = 0;
socklen_t length = sizeof(error);
//调用getsockopt来获取并清除sockfd上的错误
if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0){
printf("get sockrt option failed\n");
close(sockfd);
return -1;
}
if(error != 0){
//错误号不为0表示连接出错
printf("connet failed after select with error: %d \n", error);
close(sockfd);
return -1;
}
//连接成功
printf("connection ready after select with thr socket: %d \n", sockfd);
fcntl(sockfd, F_SETFL, fdopt);
return sockfd;
}
int main(int argc, char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int sockfd = unblock_connect(ip, port, 10);
if(sockfd < 0){
return 1;
}
close(sockfd);
return 0;
}
存在的问题:1 非阻塞的socket可能导致connect始终失败;2 select对处于EINPROGRESS状态下的socket可能不起作用,这些问题没有同意的答案,上面的代码会返回错误码111,留待以后解决。
9.6 IO复用的高级应用二:聊天室程序
实现一个聊天室的程序,以poll为例实现,该聊天室程序可以使所有用户在线群聊,其中:
客户端的功能:
从标准输入终端读入用户数据,并将用户数据发送到服务器;二是标准输出终端打印服务器发给他的数据。服务端的功能是接收客户数据并发送给每一个登录到该服务器上的客户端(数据发送者除外):
客户端程序:
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdiio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>
#define BUFFER_SIZE 64
int main(int argc, char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in server_address;
bzero(&server_address, sizeof(server_address));
server_address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &server_address.sin_addr);
server_address.sin_port = htons(port);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
if(connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0){
printf("connection failed\n");//connect函数用于建立到套接字的连接
close(sockfd);
return 1;
}
pollfd fds[2];
//注册文件描述符0--标准输入,和文件描述符sockfd上的可读事件
fds[0].fd = 0; //文件描述符
fds[0].events = POLLIN; //等待发生的事件
fds[0].revents = 0; //实际发生的事件
fds[1].fd = sockfd;
fds[1].events = POLLIN | POLLRDHUP;//参数2 TCP 连接被对方关闭或者对方关闭了写操作
fds[1].revents = 0;
char read_buf[BUFFER_SIZE];
int pipefd[2];
int ret = pipe(pipefd);
assert(ret != -1);
while(1){
ret = poll(fds, 2, -1);//阻塞的
if(ret < 0){
printf("poll failure\n");
break;
}
if(fds[1].revents & POLLRDHUP){
printf("server close the connection\n");
break;
}
else if(fds[1].revents & POLLIN){
memset(read_buf, '\0', BUFFER_SIZE);
recv(fds[1].fd, read_buf, BUFFER_SIZE-1, 0);
printf("%s\n", read_buf);
}
if(fds[0].revents & POLLIN){
//使用splice将用户输入的数据直接写到sockfd中
ret = splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
ret = splice(pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE);
}
}
close(sockfd);
return 0;
}
服务端程序:
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <poll.h>
#define USER_LIMIT 5 //最大用户数量
#define BUFFER_SIZE 64 //读缓冲区的大小
#define FD_LIMIT 65535 //文件描述符数量限制
//客户数据: 客户端socket地址,待写到客户端的数据的位置,从客户端读入的数据
struct client_data{
sockaddr_in address;
char* write_buf;
char buf[BUFFER_SIZE];
};
int setnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
int main(int argc, char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port_number\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);//监听文件描述符
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));//绑定
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
/*
创建socket数组, 分配 FD_LIMIT 个client_data对象,可以预期:每个可能的socket连接都可以获得
一个这样的对象,并且socket的值可以直接用来索引(作为数组的下标), socket连接对应的client_data
对象
*/
client_data* users = new client_data[FD_LIMIT];
//尽管我们分配了足够多的client_data对象,但为了提高poll的性能,仍有必要限制用户数量
pollfd fds[USER_LIMIT + 1];
int user_counter = 0;
for(int i = 1; i <= USER_LIMIT; ++i){
fds[i].fd = -1;
fds[i].events = 0;
}
fds[0].fd = listenfd;
fds[0].events = POLLIN | POLLERR;//错误
fds[0].revents = 0;
while(1){
ret = poll(fds, user_counter+1, -1);
if(ret < 0){
printf("poll failure\n");
break;
}
for(int i = 0; i < user_counter + 1; ++i){
if(fds[i].fd == listenfd && (fds[i].revents & POLLIN)){
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
if(connfd < 0){
printf("errno is : %d\n", errno);
continue;
}
//如果请求太多,则关闭新的连接
if(user_counter >= USER_LIMIT){
const char* info = "too many users\n";
printf("%s", info);
send(connfd, info, strlen(info), 0);
close(connfd);
continue;
}
//对于新的连接,同时修改fds和 users 数组
//users[connfd]对应于新连接的文件描述符[connfd]的客户数据
user_counter++;
users[connfd].address = client_address;
setnonblocking(connfd);
fds[user_counter].fd = connfd;
fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR;
fds[user_counter].revents = 0;
printf("comes a new users, now have %d users\n", user_counter);
}
else if(fds[i].revents & POLLERR){
printf("get an error from %d\n", fds[i].fd);
char errors[100];
memset(errors, '\0', 100);
socklen_t length = sizeof(errors);
if(getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length) < 0){
printf("get socket option failed");
}
continue;
}
else if(fds[i].revents & POLLRDHUP){
//如果客户端,则服务器也关闭对应连接,并将人数减一
users[fds[i].fd] = users[fds[user_counter].fd];
close(fds[i].fd);
fds[i] = fds[user_counter];
i--;
user_counter--;
printf("a client left\n");
}
else if(fds[i].revents & POLLIN){
int connfd = fds[i].fd;
memset(users[connfd].buf, '\0', BUFFER_SIZE);
ret = recv(connfd, users[connfd].buf, BUFFER_SIZE-1, 0);
printf("get %d bytes of client data %s from %d\n",ret,
users[connfd].buf, connfd);
if(ret < 0){
//如果读操作出错,则关闭连接
if(errno != EAGAIN){
close(connfd);
users[fds[i].fd] = users[fds[user_counter].fd];
fds[i] = fds[user_counter];
i--;
user_counter--;
}
}
else if(ret == 0){}
else{
//如果接受数据,则通知其他socket连接准备写数据
for(int j = 1; j <= user_counter; ++j){
if(fds[j].fd == connfd){
continue;
}
fds[j].events |= ~POLLIN;//可写
fds[j].events |= POLLOUT;
users[fds[j].fd].write_buf = users[connfd].buf;
}
}
}
else if(fds[i].revents & POLLOUT){
int connfd = fds[i].fd;
if(!users[connfd].write_buf){
continue;
}
ret = send(connfd, users[connfd].write_buf, strlen(users[connfd].write_buf), 0);
users[connfd].write_buf = NULL;
//写完需要从新注册fds上的可读事件
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
delete [] users;
close(listenfd);
return 0;
}
9.7 IO复用的高级应用三: 同时处理TCP和UDP服务
一个socket只能监听一个端口,,因此服务器想要监听多个窗口就要同时创建多个socket,并将他们分别绑定到各个端口上,这样一来,IO复用技术就有了用武之地(🥙🥙),,对于同一个端口,服务器要处理该端口上的UDP请求和TCP请求也要创建两个socket,一个是流socket,一个是数据报socket。并将他们都绑定到该端口上。
回射服务器(两个socket,分别处理UDP和TCP请求):
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define TCP_BUFFER_SIZE 512
#define UDP_BUFFER_SIZE 1024
int setnonblocking(int fd){
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
void addfd(int epollfd, int fd){
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
int main(int argc, char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
//创建TCP socket ,并将其绑定到端口port上
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
//创建udp socket,并将其绑定到端口port上
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int udpfd = socket(PF_INET, SOCK_DGRAM, 0);
assert(udpfd >= 0);
ret = bind(udpfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
//注册TCP和UDP上的可读事件
addfd(epollfd, listenfd);
addfd(epollfd, udpfd);
while(1){
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if(number < 0){
printf("epoll failure\n");
break;
}
for(int i = 0; i < number; i++){
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connfd);
}
else if(sockfd == udpfd){
char buf[UDP_BUFFER_SIZE];
memset(buf, '\0', UDP_BUFFER_SIZE);
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
ret = recvfrom(udpfd, buf, UDP_BUFFER_SIZE - 1, 0,
(struct sockaddr*)&client_address, &client_addrlength);
//成功返回收到的字符数,失败返回-1,错误原因存在errno中
if(ret > 0){
sendto(udpfd, buf, UDP_BUFFER_SIZE - 1, 0,
(struct sockaddr*)&client_address, client_addrlength);
//成功返回发送的字符数,失败返回-1,结果存在于errno中
}
}
else if(events[i].events & EPOLLIN){
char buf[TCP_BUFFER_SIZE];
while(1){
memset(buf, '\0', TCP_BUFFER_SIZE);
ret = recv(sockfd, buf, TCP_BUFFER_SIZE-1, 0);
if(ret < 0){
if((errno == EAGAIN) || (errno == EWOULDBLOCK)){
break;
}
close(sockfd);
break;
}
else if(ret == 0){
close(sockfd);
}
else{
send(sockfd, buf, ret, 0);
}
}
}
else{
printf("something else happend\n");
}
}
}
close(listenfd);
return 0;
}
9.8 超级服务: xinetd
(https://blog.csdn.net/lzghxjt/article/details/83018710)