可以通过IO多路服用技术,实现全双工服务器
什么是IO多路复用
同时对多个文件描述符进行监听,当文件描述符发生相应事件的时候,返回这个文件描述符
IO多路复用基本思想(机制)
- 先构建一张关于文件描述符的表,然后调用一个函数
- 返回在这些文件描述符一个或多个发生I/O操作时才会返回,否则阻塞
- 通过函数返回,判断对哪一个文件描述符进行操作
**实现IO多路复用的方式 **
select
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
fd_set readfds
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:等待对应文件描述符表产生操作,只保留表中有事件发生的文件描述符
nfds 表的大小
readfds 等待件描述符表读事件发生
writefds 等待件描述符表写事件发生
exceptfds 等待件描述符表异常事件发生
void FD_CLR(int fd, fd_set *set);
在表中删除文件描述符fd
int FD_ISSET(int fd, fd_set *set);
判断表里的文件描述符是不是 fd
void FD_SET(int fd, fd_set *set);
将fd写入表中
void FD_ZERO(fd_set *set);
清空表
步骤:
- 定义表
fd_set rfs,tfd;
- 清空表
FD_ZERO(&rfs)
- 填表
FD_SET(0,&rfs)
- 监听表
tfd = rfd
select(max+1,&tfd,NULL,NULL,NULL)
- 判断表
FD_ISSET(0,&tfd)
例子
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
if (argc != 3)
{
perror("usage:<ip><port>");
return -1;
}
// 1套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// 2绑定
struct sockaddr_in ad;
// 从终端输入获取IP和端口
ad.sin_addr.s_addr = inet_addr(argv[1]);
ad.sin_port = htons(atoi(argv[2]));
ad.sin_family = AF_INET;
socklen_t len = sizeof(ad);
if (bind(sfd, (struct sockaddr *)&ad, len) < 0)
{
perror("file");
return -1;
}
listen(sfd, 8);
// 创建表
fd_set readfds, tempfds;
FD_ZERO(&readfds);
//填充表
FD_SET(0, &readfds);
FD_SET(sfd, &readfds);
// 记录有几个接入的端口
int max = sfd;
char buf[64] = "";
while (1)
{
tempfds = readfds;
int ret = select(max + 1, &tempfds, NULL, NULL, NULL);
// 链接套接字接收到链接信息
if (FD_ISSET(sfd, &tempfds))
{
int acceptfd = accept(sfd, (struct sockaddr *)&ad, &len);
// 加入表
FD_SET(acceptfd, &readfds);
if (max < acceptfd)
{
max = acceptfd;
}
}
if (FD_ISSET(0, &tempfds))
{
// 发送前先清空
memset(buf, 0, 64);
// 获取缓存区字符
gets(buf);
// 客户端文件文件描述符从4开始
// 信息发送给所有文件
for (int i = 4; i <= max; i++)
{
// 如果读取表中有该文件描述符
if (FD_ISSET(i, &readfds))
{
// 发送
send(i, buf, 64, 0);
}
}
}
// 判断每一个文件描述符在缓存区有无消息
for (int i = 4; i <= max; i++)
{
if (FD_ISSET(i, &tempfds))
{
int ret = recv(i, buf, 64, 0);
if(ret<0)
{
perror("ret err");
return -1;
}
else if (ret > 0)
{
printf("接收到%s\n", buf);
}
else // =0 端口退出
{
printf("%d,quit\n", i);
// 关闭文件描述符
close(i);
FD_CLR(i, &readfds);
while (!FD_ISSET(max, &readfds))
{
max--;
}
}
memset(buf, 0, 64);
}
}
}
return 0;
}
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds,int timeout);
功能:监视并等待多个文件描述符的属性变化
参数:
struct pollfd *fds :结构体数组
nfds_t nfds :数组大小
int timeout:指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回.
设置为-1,则会在事件发生前一直阻塞
结构体:
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};
步骤
- 创建表(结构体数组)
- 填充表(数组)
- 监听表
- 判断表
例子
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <poll.h>
int main(int argc, char const *argv[])
{
if (argc != 3)
{
perror("file");
return -1;
}
// 1套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// 2绑定
struct sockaddr_in ad;
// ad.sin_addr.s_addr = inet_addr(argv[1]);
ad.sin_addr.s_addr =INADDR_ANY;
ad.sin_family = AF_INET;
ad.sin_port = htons(atoi(argv[2]));
socklen_t len = sizeof(ad);
if (bind(sfd, (struct sockaddr *)&ad, len) < 0)
{
perror("file");
return -1;
}
listen(sfd, 8);
char buf[64] = "";
// 创建表
#define N 10
struct pollfd fds[N];
int last = -1; // 初始表长度
fds[++last].fd = 0; /* */
fds[last].events = POLLIN;
fds[++last].fd = sfd;
fds[last].events = POLLIN;
while (1)
{
// 结构体,大小,无定时.无操作时一直阻塞
poll(fds, last + 1, -1);
for (int i = 0; i <= last; i++)
{
// 如果文件描述符i有输入
if (fds[i].revents == POLLIN)
{
// 判断输入来源
// 如果是链接套接字
if (fds[i].fd == sfd)
{
// 创建通信套接字
int afd = accept(sfd, (struct sockaddr *)&ad, &len);
fds[++last].fd = afd;
fds[last].events = POLLIN;
fds[last].revents = 0;
}
// 如果是标准输入
else if (fds[i].fd == 0)
{
memset(buf, 0, 64);
gets(buf);
// 注意,我们要将信息发送给所有客户端,所有需要重新遍历,注意开始位置
for (int j = 2; j <= last; j++)
{
int s = send(fds[j].fd, buf, 64, 0);
}
}
// 接收,如果不是标准输入,也不是连接套接字输入,那就是有端口发来了消息,属于信息套接字有输入
else
{
int ret = recv(fds[i].fd, buf, 64, 0);
if (ret < 0)
{
perror("ret err");
}
else if (ret > 0)
{
printf("%s\n", buf);
memset(buf, 0, 64);
}
// =0 有端口退出
else
{
printf("%d:quit\n", fds[i].fd);
close(fds[i].fd);
fds[i--] = fds[last--];
}
}
}
}
}
return 0;
}
select和poll优缺点
select
优点:
- 轻量级,跨平台(LINUX,MACOS,Windows都可用),可以同时监听多个文件描述符。
缺点:
- 最大监听文件描述符数量为1024,最大监听1020个客户端,
- 每次要进行轮询比较消耗CPU
- 每次都要拷贝一遍表
poll
优点:
- 对比select,能监听的文件描述符数可以更多,能监听的文件描述符数取决于数组的大小,数组大小取决于内存。
- 不需要每次都重新构建文件描述符表,传入一次即可。
缺点:
- 需要轮询
- 只能用在UNIX原生系统下,不支持跨平台