转自原文链接:https://blog.csdn.net/myselfzhangji/article/details/103931040
一、那么何为多路IO转接呢?
在linux中,有一条准则,是一切皆文件。socket通信也是利用文件的读写来实现的,那么我们就可以检测文件是否发生读写事件,来判断是否有socket通信。
应用程序调用select函数来通知内核来实现检测文件读、写、异常事件,示意图如下:
select函数原型如下
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数1: 所监听的所有文件描述符中,最大的文件描述符+1
参数2: 所监听的文件描述符"可读"事件,readfds
参数3: 所监听的文件描述符"可写"事件,writefds
参数4: 所监听的文件描述符"异常"事件,
参数5: 超时
返回值:
成功: 所监听的集合中,满足条件的总数;
出错:-1,并设置errno
既然select函数可以分别检测对应文件描述符的读(readfds)、写(writefds)、异常(exceptfds)事件,那么我们就需要首先设置select监测哪些文件描述符的读(readfds)、写(writefds)、异常(exceptfds)事件,linux提供的相关API为:
void FD_ZERO(fd_set *set); //将set清空为0
void FD_CLR(int fd, fd_set *set); //将fd从set集合中清除出去,
int FD_ISSET(int fd, fd_set *set); //判断fd是否在集合中
void FD_SET(int fd, fd_set *set); //将fd设置到set集合中去
FD_SET是设置要监听的文件描述符是哪些,以位图形式表示,假设我们需要监听文件描述符分别是lfd,fd1,fd2,fd3,FD_SET设置之后的readfds如下
select要监听readfds的文件描述符状态,如果fd1和fd3的读事件发生,此时readfds的文件描述符状态如下:
程序步骤如下
- 初始化 readfds 集合
fd_set readfds
FD_ZERO(&readfds);- 将要监听的文件描述符添加到集合中
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
FD_SET(fd3, &readfds);- select开始监听对应文件描述符的事件
select() -------> 满足条件的总数- 判断文件描述符所属事件类型
for() {
FD_ISSET(fd1, &readfds) -----> 返回为1,表示满足
}
server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/shm.h>
#define PORT 8890
#define QUEUE_SIZE 10
#define BUFFER_SIZE 1024
int main(int argc, char **argv)
{
struct sockaddr_in server_sockaddr, client_addr;
socklen_t length = sizeof(client_addr);
char str[INET_ADDRSTRLEN];
pthread_t pid;
int listenfd, client[FD_SETSIZE], sockfd;
int maxfd, maxi, connfd;
int ret = 0, nready;
int reuse = 1;
fd_set rset, allset;
char buf[BUFFER_SIZE] = {0};
int recvlen, i;
//定义IPV4的TCP连接的套接字描述符
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket() fail!\n");
return -1;
}
//使能可以重新使用addr
ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof(reuse));
if (ret < 0) {
perror("setsockopt error");
return -1;
}
//定义sockaddr_in
memset(&server_sockaddr, 0, sizeof(server_sockaddr));
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
server_sockaddr.sin_port = htons(PORT);
//bind成功返回0,出错返回-1
ret = bind(listenfd, (struct sockaddr *)&server_sockaddr,
sizeof(server_sockaddr));
if(ret < 0) {
perror("bind");
return -1;//1为异常退出
}
printf("bind success.\n");
//listen成功返回0,出错返回-1,允许同时帧听的连接数为QUEUE_SIZE
ret = listen(listenfd, QUEUE_SIZE);
if(ret < 0) {
perror("listen");
return -1;
}
printf("listen success.\n");
maxfd = listenfd;
maxi = -1;
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
while(1) {
rset = allset;
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (nready < 0) {
perror("select error!");
return -1;
}
if (FD_ISSET(listenfd, &rset)) { //监听套接字是否有连接请求
connfd = accept(listenfd, (struct sockaddr*)&client_addr,&length);//获得当前接入的ip,内核分配的应用fd
if(connfd < 0) {
perror("connect error!");
return -1;
}
printf("new client accepted, ip : %s, port : %d.\n",
inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
ntohs(client_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++) {
if (client[i] < 0) {
client[i] = connfd;//保存当前接入的ip,内核分配的应用fd
break;
}
}
if (i >= FD_SETSIZE) {
printf("too many client\n");
return -1;
}
FD_SET(connfd, &allset);
if (connfd > maxfd)
maxfd = connfd;
if (i > maxi)
maxi = i;
if (--nready == 0)
continue;
}
for (i = 0; i <= maxi; i++) { //监听已经连接的客户端是否有数据发送过来
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
memset(buf, 0, sizeof(buf));
recvlen = read(sockfd, buf, sizeof(buf));
if (recvlen ==0) {
printf("client[%d] exit\n", client[i]);
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else {
printf("now server recv : %s\n", buf);
write(sockfd, buf, recvlen);
}
}
if (--nready <= 0)
continue;
}
}
printf("closed.\n");
close(listenfd);
return 0;
}
client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define PORT 8890
#define BUFFER_SIZE 1024
#define LOCAL_LOOP_BACK_IP "127.0.0.1"
int main(int argc, char **argv)
{
struct sockaddr_in servaddr;
char sendbuf[BUFFER_SIZE] = {0};
char recvbuf[BUFFER_SIZE] = {0};
int client_fd;
//定义IPV4的TCP连接的套接字描述符
client_fd = socket(AF_INET,SOCK_STREAM, 0);
//set sockaddr_in
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr(LOCAL_LOOP_BACK_IP);
servaddr.sin_port = htons(PORT); //服务器端口
//连接服务器,成功返回0,错误返回-1
if (connect(client_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("connect");
exit(1);
}
printf("connect server(IP:%s).\n",LOCAL_LOOP_BACK_IP);
//客户端将控制台输入的信息发送给服务器端,服务器原样返回信息
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
send(client_fd, sendbuf, strlen(sendbuf),0); ///发送
if(strcmp(sendbuf,"exit\n")==0)
{
printf("client exited.\n");
break;
}
recv(client_fd, recvbuf, sizeof(recvbuf),0); ///接收
printf("client receive: %s\n", recvbuf);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(client_fd);
return 0;
}
(1)我们将各个文件描述符状态以及一些参数标示如下,初始时,我们要一直监听连接事件,监听连接事件是通过server_listen_fd来实现的,所以初始状态如下:
(2)假设此时只有一个client连接事件请求,如下,此时监听到一个连接,会将连接到的文件描述符加入进来,以监听将来是否发生读写数据事件
(3)假设此时有fd2,fd3,fd4都发生连接事件,如下。因为都检测到了连接事件,所以要将这些事件描述符添加进来,以便监测将来可能会发生的读写数据请求
(4)假设此时fd2,fd4发生了数据通信(对客户端而言是发送数据,对服务器端是接收数据,请注意,我们这里的fd1,fd2,fd3,fd4都是server端的文件描述符,因此它们都表示读数据事件发生了),并且同时有fd5的连接请求
发现server还是阻塞等待,这是因为在select函数中,我们没有设置超时等待机制,默认还是阻塞的方式。实际上select函数是可以设置为超时等待的,修改下面章节将。