TCP服务器的演变过程:IO多路复用机制select实现TCP服务器

发布时间:2023年12月26日

一、前言

手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。

为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。
本节,在上一章节的基础上,将并发的实现改为IO多路复用机制,使用select管理每个新接入的客户端连接,实现发送和接收。

二、新增使用API函数

2.1、select()函数

函数原型:

#include <sys/types.h>
#include <unistd.h>

int select(int maxfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

select函数共有5个参数,其中参数:

  • maxfds:监视对象文件描述符数量。
  • readset:将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值。
  • writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值。
  • exceptset:将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值。
  • timeout:调用select后,为防止陷入无限阻塞状态,传递超时信息。

返回值:

  • 错误返回-1。
  • 超时返回0。

当关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。

2.2、FD_*系列函数

函数原型:

#include <sys/types.h>
#include <unistd.h>

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

(1)FD_CLR函数用于将fd从set集合中清除,即不监控该fd的事件。

(2)FD_SET函数用于将fd添加到set集合中,监控其事件。

(3)FD_ZERO函数用于将set集合重置。

(4)FD_ISSET函数用于判断set集合中的fd是否有事件(读、写、错误)。

三、实现步骤

什么是IO多路复用?通俗的讲就是一个线程,通过记录IO流的状态来管理多个IO。解决创建多个进程处理IO流导致CPU占用率高的问题。

select是io多路复用的一种方式,其他的还有poll、epoll等。
在这里插入图片描述

(1)创建socket。

int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
    printf("errno = %d, %s\n",errno,strerror(errno));
    return SOCKET_CREATE_FAILED;
}

(2)绑定地址。

struct sockaddr_in server;
memset(&server,0,sizeof(server));

server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);

if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
    printf("errno = %d, %s\n",errno,strerror(errno));
    close(listenfd);
    return SOCKET_BIND_FAILED;
}

(3)设置监听。

if(-1==listen(listenfd,BLOCK_SIZE)){
   printf("errno = %d, %s\n",errno,strerror(errno));
   close(listenfd);
   return SOCKET_LISTEN_FAILED;
}

(4)初始化可读文件描述符集合,将监听套接字加入集合。

fd_set writefds,readfds,wset,rset;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(listenfd,&readfds);

(5)从可读文件描述符集合中选择一个就绪的套接字。

wset=writefds;
rset=readfds;

// 从可读文件描述符集合中选择就绪的套接字
int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
        
if(nready==-1)
{
    printf("select errno = %d, %s\n",errno,strerror(errno));
    continue;
}

(6)如果监听套接字有新连接请求,处理新连接。

struct sockaddr_in client;
memset(&client,0,sizeof(client));
socklen_t len=sizeof(client);

int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if(clientfd==-1){
    printf("accept errno = %d, %s\n",errno,strerror(errno));
}
else{
    printf("accept successdul, clientfd = %d\n",clientfd);
    // 将新套接字加入可读文件描述符集合
    FD_SET(clientfd,&readfds);
    if(clientfd>maxfd)
        maxfd=clientfd;
}

(7)处理客户端发来的数据和发送数据到客户端。

        int i=0;
        for(i=listenfd+1;i<=maxfd;i++)
        {
            if(FD_ISSET(i,&rset))
            {
                printf("recv fd=%d\n",i);
                ret=recv(i,buf,BUFFER_LENGTH,0);
                if(ret==0) {
                    // 客户端断开连接
                    printf("connection dropped\n");
                    // 从可读文件描述符集合中移除该套接字
                    FD_CLR(i,&readfds);
                    close(i);
                }
                else if(ret>0)
                {
                    printf("fd=%d recv --> %s\n",i,buf);
                    FD_CLR(i,&readfds);
                    FD_SET(i,&writefds);
                    
                }
                
            }
            else if(FD_ISSET(i,&wset))
            {
                printf("send to fd=%d\n",i);
                ret=send(i,buf,ret,0);
                if(ret==-1)
                {
                    printf("send() errno = %d, %s\n",errno,strerror(errno));
                }
                FD_CLR(i,&writefds);
                FD_SET(i,&readfds);
            }
        }

四、完整代码

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>

#include <errno.h>
#include <string.h>
#include <unistd.h>

#include <sys/select.h>

#define LISTEN_PORT     9999
#define BLOCK_SIZE      10
#define BUFFER_LENGTH   1024

enum ERROR_CODE{
    SOCKET_CREATE_FAILED=-1,
    SOCKET_BIND_FAILED=-2,
    SOCKET_LISTEN_FAILED=-3,
    SOCKET_ACCEPT_FAILED=-4,
    SOCKET_SELECT_FAILED=-5
};


int main(int argc,char **argv)
{
    // 1.
    int listenfd=socket(AF_INET,SOCK_STREAM,0);
    if(listenfd==-1){
        printf("errno = %d, %s\n",errno,strerror(errno));
        return SOCKET_CREATE_FAILED;
    }

    // 2.
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));

    server.sin_family=AF_INET;
    server.sin_addr.s_addr=htonl(INADDR_ANY);
    server.sin_port=htons(LISTEN_PORT);

    if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
        printf("errno = %d, %s\n",errno,strerror(errno));
        close(listenfd);
        return SOCKET_BIND_FAILED;
    }

    // 3.
    if(-1==listen(listenfd,BLOCK_SIZE)){
        printf("errno = %d, %s\n",errno,strerror(errno));
        close(listenfd);
        return SOCKET_LISTEN_FAILED;
    }

    printf("listen port: %d\n",LISTEN_PORT);
    
    fd_set writefds,readfds,wset,rset;
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);
    FD_SET(listenfd,&readfds);

    char buf[BUFFER_LENGTH]={0};
    int ret=0;
    int maxfd=listenfd;
    while(1)
    {
        wset=writefds;
        rset=readfds;

        // 从可读文件描述符集合中选择就绪的套接字
        int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
        
        if(nready==-1)
        {
            printf("select errno = %d, %s\n",errno,strerror(errno));
            continue;
        }

        // 如果监听套接字有新连接请求,处理新连接
        if(FD_ISSET(listenfd,&rset))
        {
            // 4.
            printf("accept , listenfd = %d\n",listenfd);
            struct sockaddr_in client;
            memset(&client,0,sizeof(client));
            socklen_t len=sizeof(client);
            
            int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
            if(clientfd==-1){
                printf("accept errno = %d, %s\n",errno,strerror(errno));
            }
            else{
                printf("accept successdul, clientfd = %d\n",clientfd);
                // 将新套接字加入可读文件描述符集合
                FD_SET(clientfd,&readfds);
                if(clientfd>maxfd)
                    maxfd=clientfd;
            }

            
        }
        printf("listenfd=%d.maxfd=%d\n",listenfd,maxfd);
        int i=0;
        for(i=listenfd+1;i<=maxfd;i++)
        {
            if(FD_ISSET(i,&rset))
            {
                printf("recv fd=%d\n",i);
                ret=recv(i,buf,BUFFER_LENGTH,0);
                if(ret==0) {
                    // 客户端断开连接
                    printf("connection dropped\n");
                    // 从可读文件描述符集合中移除该套接字
                    FD_CLR(i,&readfds);
                    close(i);
                }
                else if(ret>0)
                {
                    printf("fd=%d recv --> %s\n",i,buf);
                    FD_CLR(i,&readfds);
                    FD_SET(i,&writefds);
                    
                }
                
            }
            else if(FD_ISSET(i,&wset))
            {
                printf("send to fd=%d\n",i);
                ret=send(i,buf,ret,0);
                if(ret==-1)
                {
                    printf("send() errno = %d, %s\n",errno,strerror(errno));
                }
                FD_CLR(i,&writefds);
                FD_SET(i,&readfds);
            }
        }
    }

    
    close(listenfd);

    return 0;
}

编译命令:

gcc -o server server.c

五、TCP客户端

5.1、自己实现一个TCP客户端

自己实现一个TCP客户端连接TCP服务器的代码:

#include <stdio.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <errno.h>
#include <string.h>

#include <unistd.h>
#include <stdlib.h>

#define BUFFER_LENGTH   1024

enum ERROR_CODE{
    SOCKET_CREATE_FAILED=-1,
    SOCKET_CONN_FAILED=-2,
    SOCKET_LISTEN_FAILED=-3,
    SOCKET_ACCEPT_FAILED=-4
};

int main(int argc,char** argv)
{
    if(argc<3)
    {
        printf("Please enter the server IP and port.");
        return 0;
    }
    printf("connect to %s, port=%s\n",argv[1],argv[2]);

    int connfd=socket(AF_INET,SOCK_STREAM,0);
    if(connfd==-1)
    {
        printf("errno = %d, %s\n",errno,strerror(errno));
        return SOCKET_CREATE_FAILED;

    }
    struct sockaddr_in serv;
    serv.sin_family=AF_INET;
    serv.sin_addr.s_addr=inet_addr(argv[1]);
    serv.sin_port=htons(atoi(argv[2]));
    socklen_t len=sizeof(serv);
    int rwfd=connect(connfd,(struct sockaddr*)&serv,len);
    if(rwfd==-1)
    {
        printf("errno = %d, %s\n",errno,strerror(errno));
        close(rwfd);
        return SOCKET_CONN_FAILED;
    }
    int ret=1;
    while(ret>0)
    {
        char buf[BUFFER_LENGTH]={0};
        printf("Please enter the string to send:\n");
        scanf("%s",buf);
        send(connfd,buf,strlen(buf),0);

        memset(buf,0,BUFFER_LENGTH);
        printf("recv:\n");
        ret=recv(connfd,buf,BUFFER_LENGTH,0);
        printf("%s\n",buf);
        
    }
    close(rwfd);
    return 0;
}

编译:

gcc -o client client.c

5.2、Windows下可以使用NetAssist的网络助手工具

在这里插入图片描述
下载地址:http://old.tpyboard.com/downloads/NetAssist.exe

小结

至此,我们实现了一个使用IO多路复用机制实现的服务器,这时的TCP服务器可以使用一个线程就能处理多个客户端连接。通过记录IO流的状态来管理多个IO,解决创建多个进程处理IO流导致CPU占用率高的问题。

我们总结一下select的使用流程:

1、定义io管理状态变量:fd_set rfds,wfds;

2、初始化变量:FD_ZERO();

3、设置io流状态,最初只有监听的fd,将其设置:FD_SET(listenfd,rfds);

4、在循环中select。

5、FD_ISSET()判断端口是否有连接。

6、FD_ISSET()判断可读、可写状态。

select是io多路复用的一种方式,其他的还有poll、epoll等。下一章节我们将使用更高效的IO多路复用器epoll来实现TCP服务器。
在这里插入图片描述

文章来源:https://blog.csdn.net/Long_xu/article/details/135230145
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。