Linux网络编程之select多路转接服务器
一、select函数简介
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1。
readfds:读 文件描述符监听集合,传入、传出参数。
writefds:写 文件描述符监听集合,传入、传出参数,不监听传NULL。
exceptfds:异常 文件描述符监听集合,传入、传出参数,不监听传NULL。
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数
0: 没有满足监听条件的文件描述符
-1: errno
二、select相关函数参数分析
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) 位图所有二进制位置零
select多路IO转接:
原理:借助内核,select 来监听,客户端连接、数据通信事件。
void FD_ZERO(fd_set *set);
--- 清空一个文件描述符集合
fd_set rset;
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set);
--- 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);
void FD_CLR(int fd, fd_set *set);
--- 将一个文件描述符从监听集合中 移除。
FD_CLR(4, &rset);
int FD_ISSET(int fd, fd_set *set);
--- 判断一个文件描述符是否在监听集合中
返回值: 在:1 不在:0
FD_ISSET(4, &rset);
三、基于select的多路转接服务器
服务端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, maxi;
/* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int nready, client[FD_SETSIZE];
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
/* 起初 listenfd 即为最大文件描述符 */
maxfd = listenfd;
/* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
maxi = -1;
for (i = 0; i < FD_SETSIZE; i++)
/* 用-1初始化client[] */
client[i] = -1;
FD_ZERO(&allset);
/* 构造select监控文件描述符集 */
FD_SET(listenfd, &allset);
while (1) {
/* 每次循环时都重新设置select监控信号集 */
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
/* 说明有新的客户端链接请求 */
if (FD_ISSET(listenfd, &rset)) {
clie_addr_len = sizeof(clie_addr);
/* Accept 不会阻塞 */
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
/* 找client[]中没有使用的位置 */
if (client[i] < 0) {
/* 保存accept返回的文件描述符到client[]里 */
client[i] = connfd;
break;
}
/* 达到select能监控的文件个数上限 1024 */
if (i == FD_SETSIZE) {
fputs("too many clients\n", stderr);
exit(1);
}
/* 向监控文件描述符集合allset添加新的文件描述符connfd */
FD_SET(connfd, &allset);
if (connfd > maxfd)
/* select第一个参数需要 */
maxfd = connfd;
if (i > maxi)
/* 保证maxi存的总是client[]最后一个元素下标 */
maxi = i;
if (--nready == 0)
continue;
}
/* 检测哪个clients 有数据就绪 */
for (i = 0; i <= maxi; i++) {
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
/* 当client关闭链接时,服务器端也关闭对应链接 */
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {
Close(sockfd);
/* 解除select对此文件描述符的监控 */
FD_CLR(sockfd, &allset);
client[i] = -1;
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0)
/* 跳出for, 但还在while中 */
break;
}
}
}
Close(listenfd);
return 0;
}
客户端:
/* client.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
if (argc != 2) {
printf("Enter: ./client server_IP\n");
exit(1);
}
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
printf("------------connect ok----------------\n");
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
}
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
四、基于select的多路转接服务器优缺点
select优缺点:
缺点:监听上限受文件描述符限制,最大 1024。
检测满足条件的fd, 自己添加业务逻辑提高小, 提高了编码难度。
优点:跨平台,win、linux、macOS、Unix、类Unix、mips。