一.I/O复用概述
/O复用使得多个程序能够同时监听多个文件描述符,对提高程序的性能有很大帮助。以下情况适用于I/O复用技术:
◼ TCP 服务器同时要处理监听套接字和连接套接字。
◼ 服务器要同时处理 TCP 请求和 UDP 请求。
◼ 程序要同时处理多个套接字。
◼ 客户端程序要同时处理用户输入和网络连接。
◼ 服务器要同时监听多个端口
需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当
多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一
个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以
配合使用多线程或多进程等编程方法
二.select机制
1.select接口介绍
select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符的可读、
可写和异常等事件。
select 系统调用的原型如下:
#include <sys/select.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct ti
meval *timeout);
/*
select 成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内
没有任何文件描述符就绪,select 将返回 0。select 失败是返回-1.如果在 select 等待
期间,程序接收到信号,则 select 立即返回-1,并设置 errno 为 EINTR。
maxfd 参数指定的被监听的文件描述符的总数。它通常被设置为 select 监听的所
有文件描述符中的最大值+1
readfds、writefds 和 exceptfds 参数分别指向可读、可写和异常等事件对应的文件
描述符集合。应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件
描述符。select 返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪
fd_set 结构如下:
#define __FD_SETSIZE 1024
typedef long int __fd_mask;
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
通过下列宏可以访问 fd_set 结构中的位:
FD_ZERO(fd_set *fdset); // 清除 fdset 的所有位
FD_SET(int fd, fd_set *fdset); // 设置 fdset 的位 fd
FD_CLR(int fd, fd_set *fdset); // 清除 fdset 的位 fd
int FD_ISSET(int fd, fd_set *fdset);// 测试 fdset 的位 fd 是否被设置
timeout 参数用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指
针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。timeval
结构的定义如下:
struct timeval
{
long tv_sec; //秒数
long tv_usec; // 微秒数
};//struct timeval tv = {5,0};
如果给 timeout 的两个成员都是 0,则 select 将立即返回。如果 timeout 传递
NULL,则 select 将一直阻塞,直到某个文件描述符就绪
*/
2.设计思路图解
3.测试代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/select.h>
#include<time.h>
#define STDIN 0
int main()
{
int fd = STDIN;//键盘
fd_set fdset;//集合,收集描述符
while(1)//因为不止检测一次
{
FD_ZERO(&fdset);//清空集合,每个位置0:FD_ZERO
FD_SET(fd,&fdset);//将描述符fd添加到集合fdset
struct timeval tv = {5,0};//超时时间
int n = select(fd+1,&fdset,NULL,NULL,&tv);//可能阻塞
if(n ==-1)//select执行失败
{
printf("select err\n");
}
else if(n==0)//超市,没有找到可用事件描述符
{
printf("tme out\n");
}
else
{
if(FD_ISSET(fd,&fdset))
{
char buff[128]={0};
int num = read(fd,buff,127);
printf("num=%d,buff=%s\n",num,buff);
}
}
}
}
~
~
~
4.tcp通过select实现并发连接
SER.C
#include<stdio.h> // 标准输入输出库
#include<stdlib.h> // 标准库,提供一些函数如malloc, free, rand等
#include<string.h> // 字符串操作库
#include<unistd.h> // UNIX标准函数库
#include<sys/select.h>// 选择库,提供select函数
#include<time.h> // 时间库
#include<sys/socket.h>// 套接字库
#include<arpa/inet.h> // 提供inet_addr等函数
#include<netinet/in.h>// 提供一些网络相关的宏
#define MAXFD 10 // 定义最大文件描述符数量
// 初始化socket函数
int socket_init();
// 初始化文件描述符数组
void fds_init(int fds[]){
for(int i=0; i<MAXFD; i++){
fds[i] = -1; // 将所有文件描述符初始化为-1,表示未被使用
}
}
// 将新的文件描述符添加到数组中
void fds_add(int fds[], int fd){
for(int i=0; i<MAXFD; i++){
if(fds[i] == -1){ // 找到数组中第一个未使用的文件描述符位置
fds[i] = fd; // 添加文件描述符
break; // 退出循环
}
}
}
// 从未使用的文件描述符数组中删除指定的文件描述符
void fds_del(int fds[], int fd){
for(int i=0; i<MAXFD; i++){
if(fds[i] == fd){ // 找到要删除的文件描述符
fds[i] = -1; // 将其设置为-1,表示未使用
break; // 退出循环
}
}
}
// 接受客户端连接请求并添加到文件描述符数组
void accept_client(int sockfd, int fds[]){
int c = accept(sockfd, NULL, NULL); // 接受连接
if(c < 0){
return; // 如果返回-1,表示出错
}
printf("accept c = %d\n", c);
fds_add(fds, c); // 添加到文件描述符数组
}
// 接收客户端发送的数据
void recv_date(int c, int fds[]){
char buff[128] = {0}; // 创建缓冲区
int n = recv(c, buff, 127, 0); // 接收数据
if(n < 0){
printf("cli close\n");
close(c); // 如果接收失败,关闭连接
fds_del(fds, c); // 从数组中删除该文件描述符
return;
}
if(n == 0){
printf("time out(%d)\n", n); // 如果超时
}
printf("buff(c=%d)=%s\n", c, buff); // 打印接收到的数据
send(c, "ok", 2, 0); // 发送确认消息
}
// 主函数
int main(){
int sockfd = socket_init(); // 初始化socket
if(sockfd == -1){
exit(1); // 如果初始化失败,退出程序
}
int fds[MAXFD]; // 文件描述符数组
fds_init(fds); // 初始化数组
fds_add(fds, sockfd); // 将监听的socket添加到数组
fd_set fdset; // 创建文件描述符集合
while(1){ // 无限循环等待事件
FD_ZERO(&fdset); // 清空文件描述符集合
int maxfd = -1; // 存储最大的文件描述符
// 遍历文件描述符数组,将所有文件描述符添加到集合中
for(int i=0; i<MAXFD; i++){
if(fds[i] == -1){
continue; // 如果文件描述符未使用,跳过
}
FD_SET(fds[i], &fdset); // 添加到集合
if(fds[i] > maxfd){ // 更新最大文件描述符
maxfd = fds[i];
}
}
struct timeval tv = {5,0}; // 设置超时时间
// 使用select等待直到有文件描述符准备好IO操作或超时
int n = select(maxfd+1, &fdset, NULL, NULL, &tv);
if(n == -1){
printf("select err\n"); // 错误
} else if(n == 0){
printf("time out\n"); // 超时
} else{
// 遍历文件描述符数组,检查哪些文件描述符准备好了IO操作
for(int i=0; i<MAXFD; i++){
if(fds[i] == -1){
continue; // 如果文件描述符未使用,跳过
}
if(FD_ISSET(fds[i], &fdset)){ // 检查文件描述符是否被设置
if(fds[i] == sockfd){ // 如果是监听的socket
accept_client(sockfd, fds); // 接受新的连接
} else{ // 如果是已连接的客户端
recv_date(fds[i], fds); // 接收数据
}
}
}
}
}
}
// 创建socket并绑定到端口
int socket_init(){
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建socket
if(sockfd == -1){
return -1; // 创建失败返回-1
}
struct sockaddr_in saddr; // 服务器地址结构
memset(&saddr, 0, sizeof(saddr)); // 清零
saddr.sin_family = AF_INET; // 地址族
saddr.sin_port = htons(6000); // 端口
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // IP地址
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 绑定
if(res == -1){
printf("bind err\n");
return -1; // 绑定失败返回-1
}
if(listen(sockfd, 5) == -1){ // 开始监听,设置队列长度为5
return -1; // 监听失败返回-1
}
return sockfd; // 返回socket文件描述符
}
CLI.C
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
exit(1);
}
struct sockaddr_in saddr;//代表服务器的端口
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1)
{
printf("connct err\n");
exit(1);
}
while(1)
{
printf("input: ");
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
}
close(sockfd);
exit(0);
}