一、select
1. select接口
IO复用的作用:
select的返回值是fd_set中产生时间的个数
fd_set就是一个长整型的数组,使用每个bit标记一个文件描述符,最大为FD_SETSIZE个位,select能监听描述符的上限。
一般的,我们使用宏来操作fd_set中的bit
#include<sys/select.h>
FD_ZERO(fd_set* fd_set);
FD_SET(int fd, fd_set* fdset)
FD_CLR(int fd, fd_set* fdset)
int FD_ISSET(int fd, fd_set* fdset)
2. select完整代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<arpa/inet.h>
#define MAX 10
int socket_init(){
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
return -1;
}
struct sockaddr_in ser_addr;
memset(&ser_addr, 0 ,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8888);
ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
assert(res != -1);
res = listen(sockfd, 5);
assert(res != -1);
return sockfd;
}
void fds_init(int* fds){
if(NULL == fds){
return ;
}
for(int i = 0; i < MAX; i++){
fds[i] = -1;
}
}
void fds_del(int* fds, int fd){
if(NULL == fds){
return ;
}
for(int i = 0; i < MAX; i++){
if(fds[i] == fd){
fds[i] = -1;
break;
}
}
}
void fds_add(int* fds, int fd){
if(NULL == fds){
return ;
}
for(int i = 0; i < MAX; i++){
if(fds[i] == -1){
fds[i] = fd;
break;
}
}
}
int main(){
int sockfd = socket_init();
int fds[MAX];
fds_init(fds);
fds_add(fds, sockfd);
fd_set read_fdset;
while(1){
FD_ZERO(&read_fdset);
int maxfd = -1;
for(int i = 0; i < MAX; i++){
if(fds[i] == -1){
continue;
}
FD_SET(fds[i], &read_fdset);
maxfd = fds[i] > maxfd ? fds[i] : maxfd;
}
struct timeval tv = {5, 0};
int n = select(maxfd + 1, &read_fdset, NULL, NULL, &tv);
if( n == -1 ){
continue;
}else if(n == 0){
printf("select timeout!\n");
}else{
for(int i = 0; i < MAX; i++){
if(fds[i] == -1){
continue;
}
if(FD_ISSET(fds[i], &read_fdset)){
if(fds[i] == sockfd){
struct sockaddr_in cli_addr;
int len = sizeof(cli_addr);
int conn = accept(sockfd, (struct sockaddr*)&cli_addr, &len);
if(conn < 0){
printf("客户端连接失败!\n");
continue;
}
printf("客户端:%d 连接成功\n", conn);
fds_add(fds, conn);
}else{
char buff[128];
memset(buff, 0, 128);
int num = recv(fds[i], buff, 127, 0);
if(num <= 0){
printf("客户端:%d 关闭\n", fds[i]);
close(fds[i]);
fds_del(fds, fds[i]);
continue;
}
printf("read:%s\n", buff);
send(fds[i], buff, num, 0);
}
}
}
}
}
return 0;
}
二、Poll
1. poll接口
poll 系统调用和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- poll 系统调用成功返回就绪文件描述符的总数,超时返回 0,失败返回-1
- nfds 参数指定被监听事件集合 fds 的大小。
- timeout 参数指定 poll 的超时值,单位是毫秒,timeout 为-1 时,poll 调用将永久阻塞,直到某个事件发生,timeout 为 0 时,poll 调用将立即返回。
fds 参数是一个 struct pollfd 结构类型的数组,它指定所有用户感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd 结构体定义如下:
struct pollfd{
int fd;
short events;
short revents;
};
其中:
- fd 成员指定文件描述符
- events 成员告诉 poll 监听 fd 上的哪些事件类型,它是一系列事件的按位或
- revents 成员则有内核修改,通知应用程序 fd上实际发生了哪些事件
2. poll支持的事件类型
3. poll完整代码
#define _GNU_SOURCE
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<poll.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<arpa/inet.h>
#define MAX 10
int socket_init(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1){
return -1;
}
struct sockaddr_in ser_addr;
memset(&ser_addr, 0 ,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8888);
ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
assert(res != -1);
res = listen(listenfd, 5);
assert(res != -1);
return listenfd;
}
void fds_init(struct pollfd fds[]){
if(NULL == fds){
return ;
}
for(int i = 0; i < MAX; i++){
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
void fds_add(struct pollfd fds[], int fd){
if(NULL == fds){
return ;
}
for(int i = 0; i < MAX; i++){
if( -1 == fds[i].fd ){
fds[i].fd = fd;
fds[i].events = POLLIN | POLLRDHUP;
fds[i].revents = 0;
break;
}
}
}
void fds_del(struct pollfd fds[], int fd){
if(NULL == fds){
return ;
}
for(int i = 0; i < MAX; i++){
if(fd == fds[i].fd){
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
break;
}
}
}
int main(){
int listenfd = socket_init();
struct pollfd fds[MAX];
fds_init(fds);
fds_add(fds, listenfd);
while(1){
int n = poll(fds, MAX, 5000);
if(n == -1){
printf("poll失败!\n");
continue;
}else if(n == 0){
printf("poll timeout!\n");
continue;
}else{
for(int i = 0; i < MAX; i++){
if(fds[i].fd == -1){
continue;
}
if(fds[i].revents & POLLRDHUP){
fds_del(fds, fds[i].fd);
close(fds[i].fd);
continue;
}
if(fds[i].revents & POLLIN){
if(listenfd == fds[i].fd){
struct sockaddr_in cli_addr;
int len = sizeof(cli_addr);
int conn = accept(listenfd, (struct sockaddr*)&cli_addr, &len);
if(conn < 0){
printf("客户端连接失败!\n");
continue;
}
printf("client %s:%d 连接成功,使用的描述符:%d\n",
inet_ntoa(((struct sockaddr_in)cli_addr).sin_addr),
ntohs(((struct sockaddr_in)cli_addr).sin_port),
conn);
fds_add(fds, conn);
}else{
char buff[128] = {0};
int num = recv(fds[i].fd, buff, 127, 0);
if(num <= 0){
printf("客户端:%d 关闭\n", fds[i].fd);
fds_del(fds, fds[i].fd);
close(fds[i].fd);
continue;
}
printf("buff(%d):%s\n",num, buff);
send(fds[i].fd, buff, num, 0);
}
}
}
}
}
return 0;
}
三、epoll
1. epoll接口
epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、poll 有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像select和poll那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。epoll是一组方法的总称,包括:
- epoll_create(int size): 用于创建内核事件表,底层为红黑树。成功返回内核事件表的文件描述符,失败返回-1。size 参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大,传入的实参合法即可。
- epoll_ctl(int epfd, int op, int fd, struct epoll_event *event): 用于操作内核事件表
- epoll_wait(): 用于在一段超时时间内等待一组文件描述符上的事件
2. epoll完整代码
#define _GNU_SOURCE
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<poll.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#define MAX 10
int socket_init(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1){
return -1;
}
struct sockaddr_in ser_addr;
memset(&ser_addr, 0 ,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8888);
ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
assert(res != -1);
res = listen(listenfd, 5);
assert(res != -1);
return listenfd;
}
void epoll_add(int epoll_fd, int fd){
struct epoll_event event;
event.events = EPOLLIN | EPOLLRDHUP;
event.data.fd = fd;
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1){
perror("epoll_ctl add error\n");
}
}
void epoll_del(int epoll_fd, int fd){
if(epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1){
perror("epoll_ctl delete error\n");
}
}
int main(){
int listenfd = socket_init();
int epoll_fd = epoll_create(MAX);
assert(epoll_fd != -1);
epoll_add(epoll_fd, listenfd);
struct epoll_event events[MAX];
while(1){
int n = epoll_wait(epoll_fd, events, MAX, 5000);
if(n == -1){
perror("epoll wait error\n");
}else if(n == 0){
perror("epoll timeout\n");
}else{
for(int i = 0; i < n; i++){
int cur_fd = events[i].data.fd;
if(events[i].events & POLLRDHUP){
printf("client:%d hup close\n", cur_fd);
epoll_del(epoll_fd, cur_fd);
close(cur_fd);
continue;
}
if(events[i].events & EPOLLIN){
if(cur_fd == listenfd){
struct sockaddr_in cli_addr;
int len = sizeof(cli_addr);
int conn = accept(listenfd, (struct sockaddr*)&cli_addr, &len);
if(conn < 0){
printf("客户端连接失败!\n");
continue;
}
printf("client %s:%d 连接成功,使用的描述符:%d\n",
inet_ntoa(((struct sockaddr_in)cli_addr).sin_addr),
ntohs(((struct sockaddr_in)cli_addr).sin_port),
conn);
epoll_add(epoll_fd, conn);
}else{
char buff[128] = {0};
int num = recv(cur_fd, buff, 127, 0);
if(num <= 0){
printf("客户端:%d 关闭\n", cur_fd);
epoll_del(epoll_fd, cur_fd);
close(cur_fd);
continue;
}
printf("buff(%d):%s\n",num, buff);
send(cur_fd, buff, num, 0);
}
}
}
}
}
return 0;
}
四、LT和ET模式
1. LT和ET的基本概念
epoll 对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(Edge Trigger,边沿触发)模式。LT 模式是默认的工作模式。当往 epoll 内核事件表中注册一个文件描述符上的EPOLLET 事件时,epoll 将以高效的 ET 模式来操作该文件描述符。
对于 LT 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用 epoll_wait 时,还会再次向应用程序通告此事件,直到该事件被处理。
对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序通知这一事件。等事件再次发生,才会再次提醒。 所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因此效率比 LT 模式高。
epoll默认处于LT模式,也就是内核会不断提醒应用程序,知道程序处理完数据
int num = recv(cur_fd, buff, 1, 0);
2. ET的epoll服务器完整代码
- 文件描述符开启 ET模式
- recv设置为 非阻塞,缓冲区没数据的时候返回-1,而不是阻塞
- 循环读取 缓冲区的数据,直到读完
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#define RD_FIN_STR "服务器数据读取完成\n"
#define MAX 10
int socket_init(){
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1){
return -1;
}
struct sockaddr_in ser_addr;
memset(&ser_addr, 0 ,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8888);
ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
assert(res != -1);
res = listen(listenfd, 5);
assert(res != -1);
return listenfd;
}
void set_nonblock(int fd){
int old_feature = fcntl(fd, F_GETFL);
int new_feature = old_feature | O_NONBLOCK;
if(fcntl(fd, F_SETFL, new_feature) == -1){
perror("fcntl error\n");
}
}
void epoll_add(int epoll_fd, int fd){
struct epoll_event event;
event.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
event.data.fd = fd;
set_nonblock(fd);
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1){
perror("epoll_ctl add error\n");
}
}
void epoll_del(int epoll_fd, int fd){
if(epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1){
perror("epoll_ctl delete error\n");
}
}
int main(){
int listenfd = socket_init();
int epoll_fd = epoll_create(MAX);
assert(epoll_fd != -1);
epoll_add(epoll_fd, listenfd);
struct epoll_event events[MAX];
while(1){
printf("开始epoll_wait\n");
int n = epoll_wait(epoll_fd, events, MAX, 5000);
if(n == -1){
perror("epoll wait error\n");
}else if(n == 0){
perror("epoll timeout\n");
}else{
for(int i = 0; i < n; i++){
int cur_fd = events[i].data.fd;
if(events[i].events & POLLRDHUP){
printf("client:%d hup close\n", cur_fd);
epoll_del(epoll_fd, cur_fd);
close(cur_fd);
continue;
}
if(events[i].events & EPOLLIN){
if(cur_fd == listenfd){
struct sockaddr_in cli_addr;
int len = sizeof(cli_addr);
int conn = accept(listenfd, (struct sockaddr*)&cli_addr, &len);
if(conn < 0){
printf("客户端连接失败!\n");
continue;
}
printf("client %s:%d 连接成功,使用的描述符:%d\n",
inet_ntoa(((struct sockaddr_in)cli_addr).sin_addr),
ntohs(((struct sockaddr_in)cli_addr).sin_port),
conn);
epoll_add(epoll_fd, conn);
}else{
while(1){
char buff[128] = {0};
int num = recv(cur_fd, buff, 1, 0);
if(num == -1){
if(errno != EAGAIN && errno != EWOULDBLOCK){
perror("recv error\n");
}else{
printf("数据读取完成\n");
send(cur_fd, RD_FIN_STR, sizeof(RD_FIN_STR), 0);
}
break;
}else if(num == 0){
printf("客户端:%d 关闭\n", cur_fd);
epoll_del(epoll_fd, cur_fd);
close(cur_fd);
break;
}else{
printf("buff(%d):%s\n",num, buff);
send(cur_fd, buff, num, 0);
}
}
}
}
}
}
}
return 0;
}
3. 读取完缓冲区的客户端完整代码
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
void set_nonblock(int fd){
int old_feature = fcntl(fd, F_GETFL);
int new_feature = old_feature | O_NONBLOCK;
if(fcntl(fd, F_SETFL, new_feature) == -1){
perror("fcntl error\n");
}
}
int main(){
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
printf("create socket failed!\n");
return 0;
}
struct sockaddr_in cli_addr;
memset(&cli_addr, 0, sizeof(cli_addr));
cli_addr.sin_family = AF_INET;
cli_addr.sin_port = htons(8888);
cli_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));
assert(res != -1);
set_nonblock(sockfd);
while(1){
char buff[128] = {0};
printf("input:");
fflush(stdout);
fgets(buff, 128, stdin);
if(strcmp(buff, "exit\n") == 0){
break;
}
send(sockfd, buff, strlen(buff), 0);
while(1){
memset(buff, 0 ,128);
sleep(1);
int n = recv(sockfd, buff, 127, 0);
if(n == -1){
if(errno != EAGAIN && errno != EWOULDBLOCK){
perror("recv error\n");
}
break;
}else if(n == 0){
printf("服务端:%d 关闭\n", sockfd);
break;
}else{
printf("buff(%d):%s\n",n, buff);
}
}
}
close(sockfd);
return 0;
}
|