网络编程 day4
网络编程 day4
- 10. IO多路复用
- select
- 超时监测
- select实现并发服务器
- poll
- 特点
- 编程步骤
- 函数接口
- 练习
- epoll
- 特点
- 原理
- 编程步骤
- 总结
- 11. 服务器模型
- 分类
- 循环服务器
- 并发服务器
- IO多路复用
- 多线程/多进程
- 总结
10. IO多路复用
select
超时监测
必要性
- 避免进程在没有数据的时候阻塞
- 规定时间内没有完成相应功能,就执行超时的语句
select
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:struct timeval *timeout
NULL:一直阻塞,直到有文件描述符就绪或出错
不同时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值
时间结构体
struct timeval
{
long tv_sec; // 秒
long tv_usec; // 微秒 = 10^-6秒
};
返回值:超时返回0
注:超时检测实际上是- - 的过程,所以需要在循环中重置timeval结构体的内容
select实现并发服务器
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <dirent.h>
int main(int argc, char const *argv[])
{
// 命令行判错
if (argc != 2)
{
printf("argc err\n");
return -1;
}
// 定义变量
int fd_socket = -1; // 连接套接字文件描述符
struct sockaddr_in saddr; // 服务器网络信息
struct sockaddr_in caddr; // 客户端网络信息
int acc = -1; // 通信套接字的文件描述符
int len = sizeof(caddr); // 客户端网络信息结构体大小
char buf[128] = {}; // 存放收发的内容
int ret = -1; // 返回值
fd_set fds; // 创建表
fd_set tempfds; // 备份表
int max = 0; // 最大文件描述符
int sel = -1; // 超时检测
struct timeval tm; // 倒计时
// 1. 创建套接字
fd_socket = socket(AF_INET, SOCK_STREAM, 0);
if (fd_socket < 0)
{
perror("socket err");
return -1;
}
printf("fd_socket : %d\n", fd_socket);
// 2. 指定网络信息
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = INADDR_ANY;
// 3. 绑定套接字
if (bind(fd_socket, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind ok\n");
// 4. 监听套接字
// 将主动套接字变成被动套接字
// 队列1:未连接
// 队列2:已连接
if (listen(fd_socket, 6) < 0)
{
perror("listen err");
return -1;
}
printf("listen ok\n");
// 清空
FD_ZERO(&fds);
FD_ZERO(&tempfds);
// 添加关心的文件描述符
FD_SET(fd_socket, &fds); // 连接文件描述符
max = fd_socket;
while (1)
{
// 设定超时检测计时 // 倒计时的过程是--的过程,会重新赋值,所以在循环里面每次重置时间
tm.tv_sec = 2;
tm.tv_usec = 0;
// 表备份
tempfds = fds;
// 监听表
sel = select(max + 1, &tempfds, NULL, NULL, &tm);
if (sel == 0)
{
printf("time out ……\n");
continue;
}
// 判断哪个文件描述符有动作
if (FD_ISSET(fd_socket, &tempfds))
{
// 接收客户端连接请求
acc = accept(fd_socket, (struct sockaddr *)&caddr, &len);
if (acc < 0)
{
perror("accept err");
return -1;
}
printf("accfd : %d\n", acc);
printf("ip:%s\tport:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// 添加到原始表
FD_SET(acc, &fds);
// 判断最大值和新来的文件描述符大小
if (acc > max)
max = acc;
}
for (int i = fd_socket + 1; i <= max; i++)
{
if (FD_ISSET(i, &tempfds))
{
// 接收消息
ret = recv(i, buf, sizeof(buf), 0);
if (ret < 0) // 接收失败
{
perror("recv err");
return -1;
}
else if (ret == 0) // 客户端关闭
{
printf("client exit\n");
close(i); // 关闭文件描述符
// 将文件描述符从原表中清除
FD_CLR(i, &fds);
// 判断调整最大值
while (!FD_ISSET(max, &fds))
max--;
}
else
{
// 使用通信信息
printf("buf : %s\n", buf);
}
}
}
memset(buf, 0, sizeof(buf));
}
}
poll
特点
- 优化了文件描述符的限制
- poll每次被唤醒之后,需要重新轮询,效率低,耗费CPU
- poll 不需要构造文件描述符的表,采用结构体数组,每次调用也要经过用户空间内拷贝到用户空间
编程步骤
- 创建结构体数组
- 将关心的文件描述符添加到结构体数组中,并赋予事件
- 保存数组内最后一个有效元素下标
- 调用poll函数,监听
- 判断结构体内文件描述符触发的事件
- 根据不同文件描述符发生的不同事件做对应的逻辑处理
函数接口
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能: 监听并等待文件描述符响应
参数:
struct pollfd *fds
关心的文件描述符数组,数组元素个数自己定义struct pollfd fds[N]
nfds_t nfds
最大文件描述符个数,last+1
int timeout
超时检测,直接写时间,单位是毫秒
-1:阻塞 0:不阻塞
返回值:有事件产生 > 0,失败返回 < 0,超时返回0
结构体:
struct pollfd
{
int fd; //文件描述符
short events;//等待的事件触发条件----POLLIN读时间触发
short revents; //实际发生的事件(未产生事件为0 ))
}
练习
输入键盘事件,响应键盘事件,输入鼠标事件,响应鼠标事件(两路IO)
int main(int argc, char const *argv[])
{
// 变量
struct pollfd fds[2]; // 创建结构体数组
int fd_mouse = -1; // 鼠标文件描述符
int last = -1; // 数组内最后一个有效元素的下标
char buf[128] = {};
int rpo = -1;
// 打开鼠标文件
fd_mouse = open("/dev/input/mouse0", O_RDONLY);
if (fd_mouse < 0)
{
perror("open mouse err");
return -1;
}
// 将关心的文件描述符和对应事件添加到结构体数组中
fds[0].fd = 0; // 键盘
fds[0].events = POLLIN;
fds[1].fd = fd_mouse; // 鼠标
fds[1].events = POLLIN;
// 保存数组内最后一个元素的下标
last = 1;
while (1)
{
// poll函数监听
rpo = poll(fds, last + 1, 1000); // 直接写时间,单位是毫秒
if(rpo == 0)
{
printf("time out ……\n");
}
// 判断文件描述符触发事件
if (fds[0].revents == POLLIN)
{
// 对应处理
fgets(buf, sizeof(buf), stdin);
printf("keyboard:%s\n", buf);
}
if (fds[1].revents == POLLIN)
{
read(fd_mouse, buf, sizeof(buf));
printf("mouse:%s\n", buf);
}
memset(buf, 0, sizeof(buf));
}
return 0;
}
epoll
特点
- 监听的文件描述符没有限制
- 属于异步IO, epoll 有事件唤醒后,发生事件的文件描述符会主动调用 callback 拿到对应的文件描述符,不需要轮询,效率高
- epoll不需要构造表,只需要用用户空间复制一次到内核空间
- epoll支持的文件描述符上限是系统能打开的最大文件数目,1GB上限是10万左右
- 每个文件描述符都有一个callback函数,只有产生事件的fd才会主动调用callback,不需要轮询
- epoll处理百万级的并发
原理
- 红黑树和就绪链表在内核空间创建
- epoll_ctl将文件描述符上树
- 发生事件时,对应的文件描述符调用callback,将文件描述符和事件放到链表中
- epoll_wait将文件描述符返回到用户空间
编程步骤
- 创建红黑树和就绪链表——epoll_create
- 将关心的文件描述符和事件上树——epool_ctl
- 阻塞等待事件发生,一旦产生事件,则进行处理——epll_wait
- 根据准备好的文件描述符做对应的逻辑处理
总结
select | poll | epoll | |
---|---|---|---|
监听个数 | 一个文件描述符表最多监听1024个文件描述符 | 结构体数组定义多大就监听多少 | 百万级 |
唤醒 | 每次唤醒都需要轮询 | 每次唤醒都需要轮询 | 红黑树的callback自动回调,不需要轮询 |
效率 | 文件描述符越多,轮询越多,效率越低 | 文件描述符越多,轮询越多,效率越低 | 不需要轮询,效率高 |
结构 | 文件描述符(位表) | 结构体数组 | 红黑树和就绪链表 |
11. 服务器模型
分类
- 循环服务器
- 并发服务器
循环服务器
一个服务器同时只能处理一个客户端请求
socket();
// 指定网络信息
bind();
listen();
while(1)
{
accept();
while(1)
{
recv()/send();
}
close(acceptfd);
}
close(sockfd);
并发服务器
一个服务器在同时可以处理多个客户端请求
IO多路复用
select poll epoll
多线程/多进程
每多一个客户端就多创建一个线程/进程通信
子线程/进程:与客户端通信
主线程/进程:等待下一个客户端连接
多线程
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
// 变量定义
int fd_socket = -1; // 连接套接字文件描述符
struct sockaddr_in saddr; // 服务器网络信息
struct sockaddr_in caddr; // 客户端网络信息
int acc = -1; // 通信套接字的文件描述符
int len = sizeof(caddr); // 客户端网络信息结构体大小
char buf[128] = {}; // 存放收发的内容
pthread_t tid = 0; // 线程id
void *pthreadsocket(void *arg)
{
int ret = -1;
int accfd =*(int *)arg;
while (1)
{
ret = recv(accfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
pthread_exit(NULL);
}
if (ret == 0)
{
printf("client exit\n");
close(accfd);
pthread_exit(NULL);
}
printf("buf:%s\n", buf);
}
}
int main(int argc, char const *argv[])
{
// 1. 创建套接字
fd_socket = socket(AF_INET, SOCK_STREAM, 0);
if (fd_socket < 0)
{
perror("socket err");
return -1;
}
printf("fd_socket : %d\n", fd_socket);
// 2. 指定网络信息
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = INADDR_ANY;
// 3. 绑定套接字
if (bind(fd_socket, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind ok\n");
// 4. 监听套接字
// 将主动套接字变成被动套接字
// 队列1:未连接
// 队列2:已连接
if (listen(fd_socket, 6) < 0)
{
perror("listen err");
return -1;
}
printf("listen ok\n");
while (1)
{
// 接收客户端连接请求
acc = accept(fd_socket, (struct sockaddr *)&caddr, &len);
if (acc < 0)
{
perror("accept err");
return -1;
}
printf("accfd : %d\n", acc);
printf("ip:%s\tport:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pthread_create(&tid, NULL, pthreadsocket, &acc);
pthread_detach(tid);
}
return 0;
}
总结
IO多路复用 | 多线程 | 多进程 | |
---|---|---|---|
优点 | 节省资源 系统开销小 性能高 | 资源开销小 | 服务器稳定 资源独立 安全性高 |
缺点 | 代码复杂 | 安全性差 | 资源开销大 |