01 学习目标
- 了解poll操作函数
- 熟练使用epoll多路IO模型
- 了解epoll ET/LT触发模式
- 说出UDP的通信流程
02 通过gdb定位段错误的位置
03 epoll相关的函数介绍
三个函数:
- 该函数生成一个epoll专用的文件描述符
○ int epoll_creae(int size); - 用于控制某个epoll文件描述符事件,可以注册、修改、删除
○ int epoll_ctl(int epfd, int op, int fd,struct epoll_event *event);
○ 参数:
epfd: epoll_create生成的epoll专用描述符
op:
EPOLL_CTL_ADD – 注册
EPOLL_CTL_MOD – 修改
EPOLL_CTL_DEL – 删除
fd: 关联的文件描述符
event: 告诉内核要监听什么事件
fd:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct sockInfo
{
int fd;
struct sockaddr_in addr;
}
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
events:
- EPOLLIN - 读
- EPOLLOUT - 写
- EPOLLERR - 异常
- 等待IO事件发生 - 可以设置阻塞的函数
○ int epoll_wait(int epfd,
struct epoll_event* events, // 数组
int maxevents,
int timeout);
对应select和poll函数
○ 参数:
epfd: 要检测的句柄
events:用于回传待处理事件的数组
maxevents:告诉内核这个events的大小
timeout:为超时时间
-1: 永久阻塞
0: 立即返回
大于0:
04 epoll工作流程
05 epoll模型代码实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event all[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
// 遍历all数组中的前ret个元素
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 将新得到的cfd挂到树上
struct epoll_event temp;
temp.events = EPOLLIN;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[1024] = {0};
int len = recv(fd, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("client disconnected ....\n");
// fd从epoll树上删除
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else
{
printf(" recv buf: %s\n", buf);
write(fd, buf, len);
}
}
}
}
close(lfd);
return 0;
}
06 epoll三种工作模式_epoll水平触发模式
水平触发模式 - 根据读来解释
○ 只要fd对应的缓冲区有数据
○ epoll_wait返回
○ 返回的次数与发送数据的次数没有关系
○ epoll默认的工作模式
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event all[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
printf("================== epoll_wait =============\n");
// 遍历all数组中的前ret个元素
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 将新得到的cfd挂到树上
struct epoll_event temp;
temp.events = EPOLLIN;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[5] = {0};
int len = recv(fd, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("client disconnected ....\n");
// fd从epoll树上删除
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else
{
// printf(" recv buf: %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(fd, buf, len);
}
}
}
}
close(lfd);
return 0;
}
07 epoll三种工作模式_边沿触发模式
epoll_wait 调用次数越多, 系统的开销越大
边沿触发模式 - ET
○ fd - 默认阻塞属性
○ 客户端给server发数据
发一次数据server 的 epoll_wait返回一次
不在乎数据是否读完
如果读不完, 如何全部读出来?
while(recv());
数据读完之后recv会阻塞
解决阻塞问题
◊ 设置非阻塞 - fd
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
struct epoll_event ev;
// 设置边沿触发
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event all[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
printf("================== epoll_wait =============\n");
// 遍历all数组中的前ret个元素
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 将新得到的cfd挂到树上
struct epoll_event temp;
// 设置边沿触发
temp.events = EPOLLIN | EPOLLET;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[5] = {0};
int len = recv(fd, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("client disconnected ....\n");
// fd从epoll树上删除
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else
{
// printf(" recv buf: %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(fd, buf, len);
}
}
}
}
close(lfd);
return 0;
}
08 epoll三种工作模式_边沿非阻塞模式
边沿非阻塞触发
○ 效率最高
○ 如何设置非阻塞
open()
□ 设置flags
□ 必须 O_WDRW | O_NONBLOCK
□ 终端文件: /dev/tty
fcntl
□ int flag = fcntl(fd, F_GETFL);
□ flag |= O_NONBLOCK;
□ fcntl(fd, F_SETFL, flag);
○ 将缓冲区的全部数据都读出
while(recv() > 0)
{
printf():
}
当缓冲区数据读完之后, 返回是否为0?
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
struct epoll_event ev;
// 设置边沿触发
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event all[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
printf("================== epoll_wait =============\n");
// 遍历all数组中的前ret个元素
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 设置文件cfd为非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将新得到的cfd挂到树上
struct epoll_event temp;
// 设置边沿触发
temp.events = EPOLLIN | EPOLLET;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[5] = {0};
int len;
// 循环读数据
while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
{
// 数据打印到终端
write(STDOUT_FILENO, buf, len);
// 发送给客户端
send(fd, buf, len, 0);
}
if(len == 0)
{
printf("客户端断开了连接\n");
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else if(len == -1)
{
if(errno == EAGAIN)
{
printf("缓冲区数据已经读完\n");
}
else
{
printf("recv error----\n");
exit(1);
}
}
#if 0
if(len == -1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("client disconnected ....\n");
// fd从epoll树上删除
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else
{
// printf(" recv buf: %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(fd, buf, len);
}
#endif
}
}
}
close(lfd);
return 0;
}
09 文件描述符突破1024
- select - 突破不了, 需要编译内核
a. 通过数组实现的 - poll和epoll可以突破1024限制
○ poll内部链表
○ epoll红黑树
1.查看受计算机硬件限制的文件描述符上限
○ cat/vi /proc/sys/fs/file-max
2. 通过配置文件修改上限值
○ /etc/security/limits.conf
* soft nofile 8000
* hard nofile 8000
重启或注销虚拟机
10 udp通信流程
1.tcp-面向连接的安全的数据包通信
a.基于流sock_stream
2.udp-面向无连接不安全报文传输
3.通信流程
○服务器端:
(1)创建套接字-socket
第二个参数:SOCK_DGRAM
(2) 绑定IP和端口: bind
□fd
□structsockaddr --服务器
(3) 通信
接收数据:recvfrom
ssize_t recvfrom(int sockfd, void
*buf, size_t len, int flags,struct
sockaddr *src_addr,socklen_t
*addrlen);-同accept的2,3个参数使用方法相同
fd-文件描述符
buf:接收数据缓冲区len:buf的最大容量
flags:0
src_addr:另一端的IP和端口,传出参数
addrlen:传入传出参数
发送数据: sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd: socket函数创建出来的
buf:存储发送的数据
len:发送的数据的长度
flags:0
dest_addr:另一端的IP和端口
addrlen:dest_addr长度
udp服务器端:需要一个套接字, 通信
○客户端:
(1)创建一个用于通信的套接字:socket
(2)通信
□发送数据:sendto
需要先准备好一个结构体:struct sockaddr_in
◊存储服务器的IP和端口
□接收数据:recvform
○udp的数据是不安全的,容易丢包
丢包,丢全部还一部分?□只能丢全部
优点:效率高
11 udp服务器端代码实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, const char* argv[])
{
// 创建套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1)
{
perror("socket error");
exit(1);
}
// fd绑定本地的IP和端口
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8765);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv));
if(ret == -1)
{
perror("bind error");
exit(1);
}
struct sockaddr_in client;
socklen_t cli_len = sizeof(client);
// 通信
char buf[1024] = {0};
while(1)
{
int recvlen = recvfrom(fd, buf, sizeof(buf), 0,
(struct sockaddr*)&client, &cli_len);
if(recvlen == -1)
{
perror("recvform error");
exit(1);
}
printf("recv buf: %s\n", buf);
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client.sin_port));
// 给客户端发送数据
sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&client, sizeof(client));
}
close(fd);
return 0;
}
12 udp客户端代码实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, const char* argv[])
{
// create socket
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1)
{
perror("socket error");
exit(1);
}
// 初始化服务器的IP和端口
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8765);
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
// 通信
while(1)
{
char buf[1024] = {0};
fgets(buf, sizeof(buf), stdin);
// 数据的发送 - server - IP port
sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&serv, sizeof(serv));
// 等待服务器发送数据过来
recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("recv buf: %s\n", buf);
}
close(fd);
return 0;
}