目录
1、i/o复用概述:
通过一种机制,让单个进程可以监视多个文件描述符,一旦某个描述符就绪,能够通知程序进行相应的读写操作
解释:
i/o复用与非阻塞机制类似,但比非阻塞较好。i/o复用通过循环的请求,但循环请求的对象不单一,一旦满足就进行相应的操作。比如:一个服务端建立,多个对象可以连接服务端,进行相应的操作,也可以采取退出,与服务端断开连接操作。
i/o复用不需要创建进程与线程,也不需要对进程进行维护。其优势就是在于系统开销小
i/o复用有三种:select、epoll、poll(介绍前2种,select是普通的i/o复用,epoll是更高效的i/o复用,poll是select与epoll之间的过度)
2、select机制:
(1)、select过程建立如下:
1、将需要进行io操作的文件描述符fd添加到select中
2、阻塞等待select系统调用返回
3、数据到达后,select函数返回
4、用户线程正式发起read请求,读取数据并继续执行
select最大优势在于,用户可以在一个线程里可以同时处理多个i/o复用的请求
(2)、select的相关函数:
select函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你,系统提供select函数来实现多路复用输入/输出模型。
int select(int maxfd, //最大套接字的值+1
fd_set *rdset,
fd_set *wrset,
fd_set *exset,
struct timeval *timeout
);
参数解释:
fd_set代表文字描述符的集合,2,3,4分别表示读数据、写数据、执行数据集合。内核当中,3个数据会集合成一个,传入时候通常只需要2给参数,其余的给空就行
struct temval *timeout表示时间设置具体结构如下:
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
返回值如果为-1表示出错,返回值为0表示超时,返回值为其他值的话则表示客户端与服务端成功去的联系。
对于fd_set之类的设置给出了以下3种设置方式
1、将指定的文件描述符集清空
FD_ZERO(fd_set *fdset);
2、用于在文件描述符集合中增加一个新的文件描述符
FD_SET(fd_set *fdset);
3、用于测试指定的文件描述符是否在该集合中
FD_ISSET(int fd,fd_set *fdset);
(3)、select的相关代码演示:
1、服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#define MAXBUF 1024
int main(int argc,char* argv[]){
int sockfd;//用来表示socket句柄
int new_fd;//用来表示客户端连接进来后的句柄
struct sockaddr_in my_addr,client_addr;//分别为服务端和客户端的操作
socklen_t len = sizeof(struct sockaddr_in); //存储(struct sockaddr_in )的内存大小
unsigned int myport,lisnum;//设置端口号和监听的个数
char buf[MAXBUF + 1];
int retval,maxfd;
//select 机制
fd_set rfds; //fd_set本质为一个 long 类型的数组 8 * 128 = 1024
/*目的:将每个位置表示为0,将对应操作位置改成1*/
struct timeval tv;//超时时间
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket error\n");
return 1;
}
// argv[1]给ip地址,argv[2]给端口号
if(argv[2])
{
myport = atoi(argv[2]);
}else{
myport = 7838;
}
if(argv[3])
{
lisnum = atoi(argv[3]);
}else{
lisnum = 2;
}
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(myport);
if(argv[1])
my_addr.sin_addr.s_addr = inet_addr(argv[1]);
else
my_addr.sin_addr.s_addr = INADDR_ANY;
if((bind(sockfd,(struct sockaddr*)&my_addr,len)) == -1)
{
perror("bind error!\n");
return 1;
}
if(listen(sockfd,lisnum) == -1)
{
perror("listen error!\n");
return 1;
}
/*以上前期服务端的准备工作完成,等待客户链接*/
while (1)
{
printf("wait for connect\n");
if((new_fd = accept(sockfd,(struct sockaddr*)&client_addr,&len)) == -1)
{
perror("accept error!\n");
return 1;
}else{
printf("client ip : %s,port : %d, sock : %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
new_fd);
}
//以上为阻塞,等待客户端发信息给服务端
while (1)
{
//每一次循环将rfds数组整体清0;
FD_ZERO(&rfds);
FD_SET(0,&rfds);
FD_SET(new_fd,&rfds);
maxfd = new_fd;
tv.tv_sec = 100;
tv.tv_usec = 0;
retval = select(maxfd + 1,&rfds,NULL,NULL,&tv);
if(retval == -1)
{
perror("select error!\n");
return 1;
}else if(retval == 0) continue;
else {
if(FD_ISSET(0,&rfds))//如果是标准输入返回
{
memset(buf, 0 ,MAXBUF + 1);
fgets(buf,MAXBUF,stdin);
if(!strncasecmp(buf,"quit",4))//判断输入是否为quit(不分大小写)
{
printf("will quit\n") ;
break;
//该break仅退出select机制内循环,链接并未结束
}
len = send(new_fd,buf,strlen(buf) - 1,0);//将输入的发给用户端
if(len < 0)
{
printf("send error\n");
break;
}
}
if(FD_ISSET(new_fd,&rfds))//判断客户端是否发送消息来
{
memset(buf, 0 ,MAXBUF + 1);
len = recv(new_fd,buf,MAXBUF,0);
if(len < 0)
{
printf("recv error\n");
break;
}else{
printf("receive from client message is :%s,byte:%d",buf,len);
}
}
}
}
close(new_fd);
printf("need other connect (no->quit)");
fflush(stdout);
memset(buf,0,MAXBUF + 1);
fgets(buf,MAXBUF,stdin);
if(!strncasecmp(buf,"no",2))
{
printf("quit\n");
break;
}
}
close(sockfd);
return 0;
}
2、客户端发送接受代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<errno.h>
#define MAXBUF 1024
int main(int agrc,char *argv[])
{
int sockfd;
struct sockaddr_in My_clientaddr;
socklen_t len = sizeof(struct sockaddr_in);
char buf[MAXBUF + 1];
int retval,maxfd;
fd_set rfds;
struct timeval tv;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket error!\n");
return 1;
}
My_clientaddr.sin_family = AF_INET;
My_clientaddr.sin_port = htons(atoi(argv[2]));
if(argv[1])
My_clientaddr.sin_addr.s_addr = inet_addr(argv[1]);
else
My_clientaddr.sin_addr.s_addr = INADDR_ANY;
if(connect(sockfd,(struct sockaddr *)&My_clientaddr,sizeof(My_clientaddr)) != 0)
{
perror("connect error\n");
return 1;
}
printf("prepare for receiving.......\n");
while (1)
{
//每一次循环将rfds数组整体清0;
FD_ZERO(&rfds);
FD_SET(0,&rfds);
FD_SET(sockfd,&rfds);
maxfd = sockfd;
tv.tv_sec = 100;
tv.tv_usec = 0;
retval = select(maxfd + 1,&rfds,NULL,NULL,&tv);
if(retval == -1)
{
perror("select error!\n");
return 1;
}else if(retval == 0) continue;
else {
if(FD_ISSET(0,&rfds))//如果是标准输入返回
{
memset(buf, 0 ,MAXBUF + 1);
fgets(buf,MAXBUF,stdin);
if(!strncasecmp(buf,"quit",4))//判断输入是否为quit(不分大小写)
{
printf("will quit\n") ;
break;
//该break仅退出select机制内循环,链接并未结束
}
len = send(sockfd,buf,strlen(buf) - 1,0);//将输入的发给用户端
if(len < 0)
{
printf("send error\n");
break;
}
}
if(FD_ISSET(sockfd,&rfds))//判断客户端是否发送消息来
{
memset(buf, 0 ,MAXBUF + 1);
len = recv(sockfd,buf,MAXBUF,0);
if(len < 0)
{
printf("recv error\n");
break;
}else{
printf("receive from client message is :%s,byte:%d",buf,len);
}
}
}
}
close(sockfd);
return 0;
}
效果展示:
3、epoll机制:
一个文件是需要消耗资源的,所以对于文件的监听来说是有限度的。select里面如果监听1024个左右的文件句柄的时候可能会出一些问题,如果超过1024出现的问题更大。每个进程对于file的监控是有限的。所以如果能够脱离文件就会更合适一些。
为解决上面的问题,实现大规模并发服务器,采取了下方的epoll机制
epoll 是通过 epoll_fd 对象来实现。
epoll_fd 简单理解为一个单个的文件,文件中存在多个 epoll_event 对象,每个 epoll_event 都表示一个事件,这些事件都可以添加到epoll_fd对象中。linux 对于事件的处理很方便,所以 epoll 可以实现一个高并发的监听。
1、epoll实现过程:
1.1、创建 epoll_fd 对象
1.2、设置 epoll_event 对象
1.2.1、创建 epoll_event 对象
1.2.2、设置 epoll_event 对象
对象.events = EPOLLIN;//数据的读取
对象.data.fd = listen_socket;
1.2.3、使用epoll_event对象
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event);
1.3、使用 epoll_fd 对象
int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout);
2、epoll_event 结构:
struct epoll_event {
__uint32_t events; //事件类型
epoll_data_t data; //数据
};
EPOLLIN: 表示对应的文件描述符可以读 EPOLLOUT
EPOLLET:表示被 epoll 设为边缘触发
3、相关函数:
3.1、进行各种控制操作以改变已打开文件的的各种属性
int fcntl(int fd,
int cmd,
long arg);
O_NONBLOCK:表示非阻塞i/o
3.2、获取或者设置与某个套接字关联的选项
int setsockopt(int sock,
int level,
int optname,
const void *optval,
socklen_t optlen);
level:SOL_SOCKET
optname:SO_REUSEADDR
4、相关命令:
可以作为 client 发起 tcp 或 udp 连接
nc ip地址 端口号 就可以实现
因此可以不需要写用户端代码