欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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数组,存储每个已连接客户端的套接字描述,如下图。

linux回声服务器系列(2)_select实现

服务端代码

主函数代码,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函数返回时可能有两种情况:

  1. 监听描述符可读,有了新的客户端连接,则accept建立连接,获取客户端套接字描述符,存入client数组中。
  2. 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)_简单版本

运行结果

服务器
linux回声服务器系列(2)_select实现
客户端1
linux回声服务器系列(2)_select实现
客户端2
linux回声服务器系列(2)_select实现
可以看到,服务端能同时处理多个客户端的请求。

select缺点

  1. 有最大监听描述符个数的限制。
  2. 调用select每次都需要向内核拷贝整个描述符集信息。
  3. select返回后并不会告诉你哪些描述符准备好了,需要自行遍历检查。

这些缺点可以通过epoll进行一定程度上的弥补,见下篇。

select优点

与epoll相比,select并非毫无优点。

  1. 在监听描述符个数较少,且都较活跃的情况下,由于select的实现更简易,效率会比epoll更高。
  2. select的优点还体现在其良好的兼容性,在几乎所有操作系统下都能使用。