1.io多路复用:epoll
epoll的提出--》它所支持的文件描述符上限是系统可以最大打开的文件的数目;
eg:1GB机器上,这个上限10万个左右。
每个fd上面有callback(回调函数)函数,只有产生事件的fd才有主动调用callback,不需要轮询。
注意:
Epoll处理高并发,百万级
1. 红黑树: 是特殊的二叉树(每个节点带有属性),Epoll怎样能监听很多个呢?首先创建树的根节点,每个节点都是一个fd以结构体的形式存储(节点里面包含了一些属性,callback函数)
2. 就绪链表: 当某一个文件描述符产生事件后,会自动调用callback函数,通过回调callback函数来找到链表对应的事件(读时间还是写事件)。
1.特点
1.监听的文件描述符没有限制
2.异步io,epoll当有事件唤醒之后,发生事件的文件描述符会主动的调用callback回调函数,拿到对应的文件描述符。不需要轮询,效率高
3.epoll不需要构造表,只需要从用户空间拷贝到内核空间一次
2.编程步骤
1.创建红黑树和就绪链表epoll_create
2.将关心的文件描述符和事件上树epoll_ctl
3.阻塞等待事件产生,一旦产生事件,则会进行处理epoll_wait
4.根据链表中准备好的文件描述符进行处理
3.函数接口
epoll_create(intsize)
功能:创建红黑树根节点(创建epoll实例),同时也会创建就绪列表
返回值:成功时返回一个实例epfd(二叉树句柄),失败时返回-1.
epoll_ctrl(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
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:等待事件产生
内核会查找红黑树中有事件响应的文件描述符, 并将这些文件描述符放入就绪链表
就绪链表中的内容, 执行epoll_wait会同时复制到第二个参数events
参数: epfd:句柄;
events:用来保存从就绪链表中响应事件的集合;
maxevents: 表示每次在链表中拿取响应事件的个数;
timeout:超时时间,毫秒,0立即返回 ,-1阻塞
返回值: 成功: 实际从链表中拿出的文件描述符数目 失败时返回-1
4.总结
select | poll | epoll | |
监听个数 | 一个进程最多监听1024个文件描述符 | 由程序员决定 | 百万级 |
方式 | 每次都需要被唤醒,都需要重新轮询 | 每次都需要被唤醒,都需要重新轮询 | 红黑树内callback自动回调,不需要轮询 |
效率 | 文件描述符越多,轮询越多,效率越低 | 文件描述符越多,轮询也就越多,效率低 | 不需要轮询,效率高 |
原理 |
每次使用select后,都会清空表 每次调用select,都需要拷贝用户空间的表到内核空间 内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环 |
不会清空结构体数组 每次调用poll,都需要拷贝用户空间的结构体到内核空间 内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环 |
不会清空表 epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时) 通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝 |
特点 |
|
|
|
结构 |
文件描述符表(位表) |
结构体数组 |
红黑树和就绪链表 |
开发复杂度 | 低 | 低 | 中 |
2.服务器模型
在网络通信中,通常一个服务器要连接多个客户端
为了处理多个客户端的请求,通常有多种表现形式
1.循环服务器:一个服务器在同一时间只能处理一个客户端的请求
tcp进程:
1.创建套接字socket
2.指定网络信息(服务器的)
3.绑定套接字bind
4.监听套接字listen
while(1)
{
5.接收客户端的连接请求
while(1)
{
接收发送数据
}
关闭套接字
}
2.并发服务器:一个服务器在同一时间可以处理多个客户端的请求
2.1多进程:每有一个客户端连接创建一个进程进行通信
为什么要创建进程-------->通信
什么时间创建进程--------->accept之后fork
子进程;通信
父进程;循环等待下一个客户端连接
进程资源回收------->子进程退出-------》客户端退出
wait(); waitpid();
SIGCHLD:当子进程退出给父进程发的信号
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int sig)
{
wait(NULL);
}
int main(int argc, char const *argv[])
{
pid_t pid;
char buf[128] = {0};
int ret, acceptfd;
// 1.创建套接字(socket)---------------》有手机
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd); // 3
// 2.指定网络信息---------------------------》有号码
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET; // IPV4
saddr.sin_port = htons(atoi(argv[1])); // 端口号
// saddr.sin_addr.s_addr = inet_addr("192.168.50.13"); // 虚拟机IP
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_addr.s_addr = INADDR_ANY;
int len = sizeof(caddr);
// 3.绑定套接字(bind)------------------》绑定手机(插卡)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind ok\n");
// 4.监听套接字(listen)-----------------》待机
if (listen(sockfd, 6) < 0)
{
perror("listen err");
return -1;
}
printf("listen ok\n");
// 5.接收客户端连接连接请求(accept)--》接电话
// tcp服务器一共有两类文件描述符,一类用于连接,一类用于通信
// socket函数返回值:用于连接的文件描述符
// accept函数返回值:用于通信的文件描述符
signal(SIGCHLD, handler);//设置信号处理方式
while (1)
{
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err");
return -1;
}
printf("port:%d ip:%s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
printf("acceptfd:%d\n", acceptfd);
pid = fork();
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
// 6.接收、发送数据(recv send)---》通话
while (1)
{
// read/write()
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
break;
}
else if (ret == 0)
{
printf("client exit\n");
break;
}
else
{
printf("buf:%s\n", buf);
memset(buf, 0, sizeof(buf));
}
}
// 7.关闭套接字(close)-----------------》挂电话
close(acceptfd);
exit(0);
}
else
close(acceptfd);
}
close(sockfd);
return 0;
}
2.2多线程:每一个客户端连接创建一个线程进行通信
为什么要创建线程:通信
什么时候创建线程,acceot之后pthread_create
子线程:通信
主线程:循环等待下一个客户端连接
通信的文件描述符:线程传参
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
void *handler(void *arg)
{
int ret, acceptfd;
acceptfd = *((int *)arg);
char buf[128] = {0};
// 6.接收、发送数据(recv send)---》通话
while (1)
{
// read/write()
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return NULL;
}
else if (ret == 0)
{
printf("client exit\n");
break;
}
else
{
printf("buf:%s\n", buf);
memset(buf, 0, sizeof(buf));
}
// 7.关闭套接字(close)-----------------》挂电话
}
close(acceptfd);
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
int acceptfd;
pthread_t tid;
// 1.创建套接字(socket)---------------》有手机
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd); // 3
// 2.指定网络信息---------------------------》有号码
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET; // IPV4
saddr.sin_port = htons(atoi(argv[1])); // 端口号
// saddr.sin_addr.s_addr = inet_addr("192.168.50.13"); // 虚拟机IP
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_addr.s_addr = INADDR_ANY;
int len = sizeof(caddr);
// 3.绑定套接字(bind)------------------》绑定手机(插卡)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind ok\n");
// 4.监听套接字(listen)-----------------》待机
if (listen(sockfd, 6) < 0)
{
perror("listen err");
return -1;
}
printf("listen ok\n");
// 5.接收客户端连接连接请求(accept)--》接电话
// tcp服务器一共有两类文件描述符,一类用于连接,一类用于通信
// socket函数返回值:用于连接的文件描述符
// accept函数返回值:用于通信的文件描述符
while (1)
{
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err");
return -1;
}
printf("port:%d ip:%s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
printf("acceptfd:%d\n", acceptfd);
pthread_create(&tid, NULL, handler, &acceptfd);
pthread_detach(tid);
}
close(sockfd);
return 0;
}
2.3io多路复用
select,poll,epoll
2.4并发服务器总结
多进程;
优点:服务器更加稳定,父子进程资源相互独立,安全性高
缺点:需要开辟多个进程,大量的消耗资源,系统开销大
多线程:
优点:相对于多进程,资源开销小,多个线程共享同一个进程的资源
缺点:需要开辟多个线程,安全性比较差
io多路复用
优点:节约资源,系统开销小,性能高
缺点:代码复杂度高
FTP
服务器
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <fcntl.h>
void list(int acceptfd);
void putfile(int acceptfd, char *p);
void getfile(int acceptfd, char *p);
int main(int argc, char const *argv[])
{
char buf[128] = {0};
int ret, acceptfd;
// 1.创建套接字(socket)---------------》有手机
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd); // 3
// 2.指定网络信息---------------------------》有号码
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET; // IPV4
saddr.sin_port = htons(atoi(argv[1])); // 端口号
// saddr.sin_addr.s_addr = inet_addr("192.168.50.13"); // 虚拟机IP
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_addr.s_addr = INADDR_ANY;
int len = sizeof(caddr);
// 3.绑定套接字(bind)------------------》绑定手机(插卡)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind ok\n");
// 4.监听套接字(listen)-----------------》待机
if (listen(sockfd, 6) < 0)
{
perror("listen err");
return -1;
}
printf("listen ok\n");
// 5.接收客户端连接连接请求(accept)--》接电话
// tcp服务器一共有两类文件描述符,一类用于连接,一类用于通信
// socket函数返回值:用于连接的文件描述符
// accept函数返回值:用于通信的文件描述符
while (1)
{
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err");
return -1;
}
printf("port:%d ip:%s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
printf("acceptfd:%d\n", acceptfd);
// 6.接收、发送数据(recv send)---》通话
while (1)
{
// read/write()
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return -1;
}
else if (ret == 0)
{
printf("client exit\n");
break;
}
else
{
printf("buf:%s\n", buf);
if (!strcmp(buf, "list"))
list(acceptfd);
if (!strncmp(buf, "put ", 4))
putfile(acceptfd, buf);
if (!strncmp(buf, "get ", 4))
getfile(acceptfd, buf);
if (!strcmp(buf, "quit"))
break;
memset(buf, 0, sizeof(buf));
}
}
// 7.关闭套接字(close)-----------------》挂电话
close(acceptfd);
}
close(sockfd);
return 0;
}
void list(int acceptfd)
{
struct dirent *d;
// 打开目录文件
DIR *dir = opendir(".");
if (dir == NULL)
{
perror("opendir err");
return;
}
// 循环读目录
while ((d = readdir(dir)) != NULL)
{
// 判断是否为普通文件
if (d->d_type == DT_REG)
// 发送给客户端
send(acceptfd, d->d_name, 128, 0);
}
// 发送一个结束的标志
send(acceptfd, "end", 128, 0);
return;
}
void putfile(int acceptfd, char *p)
{
char buf[128] = {0};
int ret;
int fd = open(p + 4, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open err");
return;
}
// 接受客户端传来的文件内容
while (1)
{
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return;
}
else
{
// 写到目标文件里面
if (!strcmp(buf, "end"))
break;
write(fd, buf, strlen(buf));
memset(buf, 0, sizeof(buf));
}
}
close(fd);
return;
}
void getfile(int acceptfd, char *p)
{
char buf[128] = {0};
ssize_t s;
int fd = open(p + 4, O_RDONLY);
if (fd < 0)
{
perror("open err");
return;
}
// 循环读文件
while (1)
{
s = read(fd, buf, sizeof(buf) - 1);
buf[s] = '\0';
if (s == 0)
{
printf("send end\n");
send(acceptfd, "end", sizeof(buf), 0);
break;
}
// 发送给服务器
send(acceptfd, buf, sizeof(buf), 0);
memset(buf, 0, sizeof(buf));
}
close(fd);
return;
}
客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
void show();
void list(int sockfd);
void putfile(int sockfd, char *p);
void getfile(int sockfd, char *p);
int main(int argc, char const *argv[])
{
char buf[128] = {0};
// 1.创建套接字(socket)------------》有手机
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 2.指定(服务器)网络信息--------》有对方的号码
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
// 3.连接(connect)-------------------》拨打电话
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("connect err");
return -1;
}
printf("connect okk\n");
// 4.接收发送消息(recv send)---》通话
while (1)
{
show();
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
// write(sockfd, buf, sizeof(buf));
send(sockfd, buf, sizeof(buf), 0);
if (!strcmp(buf, "list"))
list(sockfd);
if (!strcmp(buf, "quit"))
break;
if (!strncmp(buf, "put ", 4))
putfile(sockfd, buf);
if (!strncmp(buf, "get ", 4))
getfile(sockfd, buf);
}
// 5.关闭套接字(close)------------》挂电话
close(sockfd);
return 0;
}
void show()
{
printf("----------------list----------------\n");
printf("------------put filename------------\n");
printf("------------get filename------------\n");
printf("----------------quit----------------\n");
}
void list(int sockfd)
{
char buf[128] = {0};
int ret;
while (1)
{
ret = recv(sockfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return;
}
if (!strcmp(buf, "end"))
break;
printf("%s ", buf);
}
putchar(10);
return;
}
void putfile(int sockfd, char *p)
{
char buf[128] = {0};
ssize_t s;
int fd = open(p + 4, O_RDONLY);
if (fd < 0)
{
perror("open err");
return;
}
// 循环读文件
while (1)
{
s = read(fd, buf, sizeof(buf) - 1);
buf[s] = '\0';
if (s == 0)
{
printf("send end\n");
send(sockfd, "end", sizeof(buf), 0);
break;
}
// 发送给服务器
send(sockfd, buf, sizeof(buf), 0);
memset(buf, 0, sizeof(buf));
}
close(fd);
return;
}
void getfile(int sockfd, char *p)
{
char buf[128] = {0};
int ret;
int fd = open(p + 4, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open err");
return;
}
// 接受客户端传来的文件内容
while (1)
{
ret = recv(sockfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return;
}
else
{
// 写到目标文件里面
if (!strcmp(buf, "end"))
break;
write(fd, buf, strlen(buf));
memset(buf, 0, sizeof(buf));
}
}
close(fd);
return;
}