linux回声服务器系列(2)_select实现
程序员文章站
2024-03-23 10:14:16
...
在网络编程中,回声服务器(echo server)是一个典型的例子。
在这个系列中将分别采用简易版本、select函数、epoll函数(水平触发和边沿触发)来实现回声服务器。
传送门:
linux回声服务器系列(1)_简单版本
linux回声服务器系列(2)_select实现
linux回声服务器系列(3)_epoll实现
select实现回声服务器
基本功能
采用select函数能实现基本的IO多路复用,实现服务器为多个客户端提供服务的功能。
客户端将用户输入的字符串发送到服务器,服务器收到后原封不动发送回客户端,实现回声。
客户端输入字符q后退出。
服务器结构
服务器监听一个监听描述符listenfd,来获取新的客户端连接。此外,还维护一个client数组,存储每个已连接客户端的套接字描述,如下图。
服务端代码
主函数代码,select的具体处理步骤在select_server函数中。
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
constexpr int PORT = 2345;
constexpr int BUFMAXLEN = 128;
int client[FD_SETSIZE];
int main(int argc, char * argv[])
{
int sockfd, connfd;
char sendBuf[BUFMAXLEN] = {0};
char recvBuf[BUFMAXLEN] = {0};
struct sockaddr_in server_addr, client_addr;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(bind(sockfd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) == -1)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
if(listen(sockfd, SOMAXCONN) == -1)
{
perror("listen failed");
exit(EXIT_FAILURE);
}
std::cout<<"server start... "<<std::endl;
select_server(sockfd);
close(sockfd);
return 0;
}
select函数返回时可能有两种情况:
- 监听描述符可读,有了新的客户端连接,则accept建立连接,获取客户端套接字描述符,存入client数组中。
- client中的套接字描述符有可读事件,则遍历client数组做相应处理。
select_server函数的具体实现:
void select_server(int listenfd)
{
int maxfd, connfd, max_client_index = 0, recv_bytes;
struct sockaddr_in server_addr, client_addr;
fd_set allset, fdset;
size_t i;
socklen_t sock_len = sizeof(sockaddr_in);
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
maxfd = listenfd;
/*初始化客户端套接字描述符数组*/
for(i = 0; i < FD_SETSIZE; i++)
client[i] = -1;
while(1)
{
fdset = allset; //每次都更新内核的监听描述符集
if(select(maxfd + 1, &fdset, NULL, NULL, NULL) < 0)
{
perror("select failed");
exit(EXIT_FAILURE);
}
/*select返回可能有两种情况*/
/*1.监听描述符可读,有了新连接*/
if(FD_ISSET(listenfd, &fdset))
{
if((connfd = accept(listenfd, (struct sockaddr *)(&client_addr), &sock_len)) < 0)
{
perror("accept failed");
exit(EXIT_FAILURE);
}
/*找到client中的空位子填进去*/
for(i = 0; i < FD_SETSIZE; i++)
{
if(client[i] == -1)
{
client[i] = connfd;
break;
}
}
if(i == FD_SETSIZE)
{
std::cout<<"client array full"<<std::endl;
exit(EXIT_FAILURE);
}
std::cout<<"accept client ip: "<<inet_ntoa(client_addr.sin_addr)<<" on socket: "<<connfd<<std::endl;
/*更新索引信息和内核描述符集*/
max_client_index = (i > max_client_index) ? i : max_client_index;
FD_SET(connfd, &allset);
maxfd = (connfd > maxfd) ? connfd : maxfd;
}
/*2.client中的套接字描述符有可读事件*/
for(i = 0; i <= max_client_index; i++)
{
if(client[i] == -1) //未监听
continue;
if(FD_ISSET(client[i], &fdset))
{
/*连接已断开*/
if((recv_bytes = read(client[i], recv_buf, BUFMAXLEN)) == 0)
{
close(client[i]);
FD_CLR(client[i], &allset);
std::cout<<"sockfd "<<client[i]<<" is closed"<<std::endl;
client[i] = -1;
}
else //回写
write(client[i], recv_buf, recv_bytes);
}
}
}
}
客户端代码
客户端代码见linux回声服务器系列(1)_简单版本
运行结果
服务器
客户端1
客户端2
可以看到,服务端能同时处理多个客户端的请求。
select缺点
- 有最大监听描述符个数的限制。
- 调用select每次都需要向内核拷贝整个描述符集信息。
- select返回后并不会告诉你哪些描述符准备好了,需要自行遍历检查。
这些缺点可以通过epoll进行一定程度上的弥补,见下篇。
select优点
与epoll相比,select并非毫无优点。
- 在监听描述符个数较少,且都较活跃的情况下,由于select的实现更简易,效率会比epoll更高。
- select的优点还体现在其良好的兼容性,在几乎所有操作系统下都能使用。