7.2 网络编程_day4
复习:
select的超时时间检测:
超时检测的必要性:
1. 避免进程在没有数据时无限制的阻塞;
2. 规定时间未完成语句应有的功能,则会执行相关功能;
头文件: #include<sys/select.h> #include<sys/time.h>
#include<sys/types.h> #include<unistd.h>
声明: int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:监测是哪些文件描述符产生事件,阻塞等待产生.
参数:nfds: 监测的最大文件描述个数(文件描述符从0开始,这里是个数,记得+1)
readfds: 读事件集合; // 键盘鼠标的输入,客户端连接都是读事件
writefds: 写事件集合; //NULL表示不关心
exceptfds:异常事件集合; //NULL 表示不关心
timeout: 设为NULL,等待直到某个文件描述符发生变化;
设为大于0的值,有描述符变化或超时时间到才返回。
超时时间检测:如果规定时间内未完成函数功能,返回一个超时的信息,我们可以根 据该信息设定相应需
求;
返回值: <0 出错 >0 表示有事件产生;
如果设置了超时检测时间:&tv
<0 出错 >0 表示有事件产生; ==0 表示超时时间已到;
结构体如下:
struct timeval {
long tv_sec; 以秒为单位,指定等待时间
long tv_usec; 以毫秒为单位,指定等待时间
};
void FD_CLR(int fd, fd_set *set); //将set集合中的fd清除掉
int FD_ISSET(int fd, fd_set *set); //判断fd是否在set集合中产生了事件
void FD_SET(int fd, fd_set *set); //将fd加入到集合中
void FD_ZERO(fd_set *set); //清空集合
POLL:
特点:
- 优化文件描述符个数的限制;
(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组容量为1,如果想监听100个,那么这个结构体数组的容量就为100,多少文件描述符由程序员自己来决定) - poll被唤醒之后需要重新轮询一遍驱动,效率比较低(消耗CPU)
- poll不需重新构造文件描述符表(也不需清空表),只需要从用户空间向内核空间拷贝一次数据(效率相对比较高)
poll的流程
使用: 1.创建结构体数组
2.添加结构体成员的文件描述符以及触发方式
3.保存数组内最后一个有效元素的下标
4. 调用函数poll
5.判断结构体内文件描述符是否触发事件
6.根据不同的文件描述符触发不同事件
7. 做对应的逻辑处理
poll函数
机制:
./server:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
//select TCP_server 实现链接多个客户端
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socker is err:");
return -1;
}
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len = sizeof(caddr);
if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) <0)
{
perror("bind is err:");
return -1;
}
if(listen(sockfd,5) < 0)
{
perror("listen is err:");
return -1;
}
//1. 创建结构体数组
struct pollfd fds[100]; //最多接受100个文件描述符
//2.添加关心的文件描述符 并 给文件描述符设置触发方式
fds[0].fd = 0; // 标准输入
fds[0].events = POLLIN; //读事件
fds[1].fd = sockfd;
fds[1].events = POLLIN;
//3.保存数组内最后一个有效元素的下标
int nfds = 1;
char buf[128];
int ret;
while(1)
{
//4.调用poll函数,检测文件描述符产生事件
ret = poll(fds,nfds+1,-1);
if(ret < 0)
{
perror("poll is err:");
return -1;
}
for(int i = 0; i <= nfds; i++)
{
if(fds[i].revents == POLLIN) //如果第三个参数被内核填充,则说明有事件响应
{
if(fds[i].fd == 0) //键盘输入 i == 0
{
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf) -1] == '\n')
buf[strlen(buf) -1] = '\0';
printf("key: %s\n",buf);
}
else if(fds[i].fd == sockfd) // i == 1
{
int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);
if(acceptfd < 0)
{
perror("accept is err:");
return -1;
}
printf("fd: %d, ip: %s, port: %d\n",acceptfd,inet_ntoa(caddr.sin_addr),\
ntohs(caddr.sin_port));
nfds++;// 1 >> 2
fds[nfds].fd = acceptfd;
fds[nfds].events = POLLIN;
break;
}
else
{
int recvbyte = recv(fds[i].fd,buf,sizeof(buf),0);
if(recvbyte < 0)
{
perror("recv is err:");
return -1;
}
else if(recvbyte == 0)
{
printf("%d client is exit\n",fds[i].fd);
//关闭文件描述符
close(fds[i].fd);
//将最后一个数组下标的内容移到当前数组下标
fds[i] = fds[nfds];
//有效数组元素下标-1
nfds--;
//下次循环仍然是本轮循环, 这样可以检测 fds[nfds]的内容
i--;
}
else
{
printf("client: %d %s\n",fds[i].fd,buf);
}
}
}
}
}
close(sockfd);
return 0;
}
关于: 退出客户端, 为什么要进行i-- ,nfds–等操作:
超时时间检测:
epoll实现: (异步)
epoll了解其机制就可以
select,poll都属于 同步IO机制(轮询)
epoll属于异步IO机制(不轮询):
epoll的提出–》它所支持的文件描述符上限是系统可以最大打开的文件的数目;
eg:1GB机器上,这个上限10万个左右。
每个fd上面有callback(回调函数)函数,只有产生事件的fd才有主动调用callback,不需要轮询。
注意:
Epoll处理高并发,百万级
1.红黑树: 是特殊的二叉树(每个节点带有属性),Epoll怎样能监听很多个呢?首先创建树的根节点,每个节点都是一个fd以结构体的形式存储(节点里面包含了一些属性,callback函数)
2.就绪链表: 当某一个文件描述符产生事件后,会自动调用callback函数,通过回调callback函数来找到链表对应的事件(读时间还是写事件)。
epoll特点
1.监听的最大的文件描述符没有个数限制(取决与你自己的系统 1GB - 10万个左右)
2.异步I/O,epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
3.epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.
epoll的流程:
Epoll的使用:
1.创建红黑树 和 就绪链表
2.添加文件描述符和事件信息到树上
3.阻塞等待事件的产生,一旦产生事件,则进行处理
4.根据链中准备处理的文件描述符 进行处理
epoll_create 创建红黑树以及链表
头文件:#include <sys/epoll.h>
声明:int epoll_create(int size);
功能:创建红黑树根节点(创建epoll实例) , 同时也会创建就绪链表
返回值:成功时返回一个实例(二叉树句柄),失败时返回-1。
epoll_ctl 控制epoll属性
声明: int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll属性,比如给红黑树添加节点
参数: 1. epfd: epoll_create函数的返回句柄。//一个标识符
2. op:表示动作类型,有三个宏:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已注册fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
3. 要操作的文件描述符
4. 结构体信息:
typedef union epoll_data {
int fd; //要添加的文件描述符
uint32_t u32; typedef unsigned int
uint64_t u64; typedef unsigned long int
} epoll_data_t;
struct epoll_event {
uint32_t events; 事件
epoll_data_t data; //共用体(看上面)
};
关于events事件:
EPOLLIN: 表示对应文件描述符可读
EPOLLOUT: 可写
EPOLLPRI:有紧急数据可读;
EPOLLERR:错误;
EPOLLHUP:被挂断;
EPOLLET:触发方式,边缘触发;(默认使用边缘触发)
ET模式:表示状态的变化;
NULL: 删除一个文件描述符使用,无事件
返回值:成功:0, 失败:-1
epoll_wait等待事件产生
声明: int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:等待事件产生
内核会查找红黑树中有事件响应的文件描述符, 并将这些文件描述符放入就绪链表
就绪链表中的内容, 执行epoll_wait会同时复制到第二个参数events
参数: epfd:句柄;
events:用来保存从就绪链表中响应事件的集合;
maxevents: 表示每次在链表中拿取响应事件的个数;
timeout:超时时间,毫秒,0立即返回 ,-1阻塞
返回值: 成功: 实际从链表中拿出的数目 失败时返回-1
./server:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
//select TCP_server 实现链接多个客户端
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socker is err:");
return -1;
}
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len = sizeof(caddr);
if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) <0)
{
perror("bind is err:");
return -1;
}
if(listen(sockfd,5) < 0)
{
perror("listen is err:");
return -1;
}
//1.创建红黑数和就绪链表
int epfd = epoll_create(1);
//2.添加文件描述符的属性到树上
struct epoll_event event;
struct epoll_event events[20];
event.data.fd = 0; //终端输入
event.events = EPOLLIN|EPOLLET;
//上树
epoll_ctl(epfd,EPOLL_CTL_ADD,0,&event);
event.data.fd = sockfd; //套接字
event.events = EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
char buf[128];
int ret;
while(1)
{
//3.阻塞等待文件描述符的事件产生
ret = epoll_wait(epfd,events,20,-1);
if(ret < 0)
{
perror("epoll_waite is err:");
return -1;
}
//4.根据结构体数组内的文件描述符 进行相应的逻辑处理
for(int i = 0;i < ret;i++) //ret代表产生事件的文件描述符的数量
{
if(events[i].data.fd == 0) //终端输入
{
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf) -1] == '\n')
buf[strlen(buf) -1] = '\0';
printf("key: %s\n",buf);
}
else if(events[i].data.fd == sockfd) //建立通信
{
int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);
if(acceptfd < 0)
{
perror("accept is err:");
return -1;
}
printf("fd: %d ip: %s port: %d\n",acceptfd,inet_ntoa(caddr.sin_addr),\
ntohs(caddr.sin_port));
.
//新的文件描述符上树
event.data.fd = acceptfd;
event.events = EPOLLIN|EPOLLET;
//上树
epoll_ctl(epfd,EPOLL_CTL_ADD,acceptfd,&event);
}
else
{
int recvbyte =recv(events[i].data.fd,buf,sizeof(buf),0);
if(recvbyte < 0)
{
perror("recv is err:");
return -1;
}
else if(recvbyte == 0)
{
printf("client %d is exit\n",events[i].data.fd);
close(events[i].data.fd);
//树上的文件描述符 下树
epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
}
else
{
printf("client %d: %s\n",events[i].data.fd,buf);
}
}
}
}
close(sockfd);
return 0;
}
超时检测
- 掌握poll代码以及流程和特点, 了解epoll代码,掌握epoll特点
2.画一个表 (IO多路复用的特点以及流程的表)
3.完成多进程/线程实现TCP全双工
4.UDP聊天室