1. 实例代码以及说明
while(1)
{
fgets(buf, 128, stdin);
buf[strlen(buf) - 1] = '\0';
printf("buf = %s\n", buf);
if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("accept error");
}
printf("客户端[%s - %d]连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
以上代码在运行的过程中,必须先执行fgets,后执行accept,所以如果事先有客户端连接服务器,由于服务器端代码没有来得及执行accept,所以就无法直接连接,必须等fgets执行完毕后才能执accept,
如何实现让这两个阻塞函数独立执行呢?
方法1:阻塞IO,无法解决问题
方法2:非阻塞IO,将fgets和accept都设置为非阻塞,确实可以实现功能,但是非阻塞要轮询每次都判断,没数据就立即返回,所以比较浪费资源
方法3:使用多进程或者多线程,也可以实现想要的功能,但是涉及进程或者线程就要考虑资源释放问题,所以也比较麻烦
方法4:使用IO多路复用,这是专门用于解决这类问题的方法
2. IO多路复用的流程
基本思想是:
先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
IO多路复用的核心思想:
fgets和accept函数在执行之前,让数据先写入到指定文件描述符的缓冲区中,所以如果缓冲区中有数据,判断是哪个文件描述符标识,然后直接执行对应的阻塞函数就解决这个问题了
3. 使用select函数实现IO多路复用
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
功能:
允许一个程序操作多个文件描述符,阻塞等待文件描述符准备就绪,如果有文件描述符准备就绪,则返回立即返回,然后执行相应的IO操作
参数:
nfds:
最大的文件描述符加1
readfds:
保存读操作的文件描述符集合
writefds:
保存写操作的文件描述符集合
exceptfds:
保存其他或者异常操作的文件描述符集合
timeout:
超时
NULL 阻塞
返回值:
成功:
准备就绪的文件描述符的个数
失败:
-1
清空集合set
void FD_ZERO(fd_set *set);
将文件描述符fd添加到集合set中
void FD_SET(int fd, fd_set *set);
将文件描述符fd从集合set中移除
void FD_CLR(int fd, fd_set *set);
判断文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set);
返回值:
1 存在
0 不存在
4. 相关代码
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
//__FILE__: 获取文件名
//__func__:获取函数名
//__LINE__:获取行号
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s-%d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buf[128] = {0};
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转化为无符号32位整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据 "1568" --> 1568
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用select函数实现IO多路复用
//第一步:创建保存要操作的文件描述符的集合并清空
fd_set readfds;
FD_ZERO(&readfds);
int maxfd = sockfd;
int ret;
while(1)
{
//第二步:将要操作的文件描述符添加到集合中
//fgets() --> 0
//accept() --> sockfd
FD_SET(0, &readfds);
FD_SET(sockfd, &readfds);
//第三步:调用select函数阻塞等待文件描述符准备就绪
if((ret = select(maxfd+1, &readfds, NULL, NULL, NULL)) == -1)
{
ERRLOG("select error");
}
printf("ret = %d\n", ret);
//第四步:如果有文件描述符准备就绪,select函数立即返回,
//注意:当select返回之后,第一时间会移除集合中没有准备就绪的
// 文件描述符,所以判断集合中还剩哪个没有被移除,说明就是
// 准备就绪的
//判断哪个文件描述符还在集中,接下来执行相应的IO操作
if(FD_ISSET(0, &readfds) == 1)
{
fgets(buf, 128, stdin);
buf[strlen(buf) - 1] = '\0';
printf("buf = %s\n", buf);
}
if(FD_ISSET(sockfd, &readfds) == 1)
{
if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("accept error");
}
printf("客户端[%s - %d]连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
}
return 0;
}