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

news2024/11/20 16:36:37

IO多路复用机制select实现TCP服务器

  • 一、前言
  • 二、新增使用API函数
    • 2.1、select()函数
    • 2.2、FD_*系列函数
  • 三、实现步骤
  • 四、完整代码
  • 五、TCP客户端
    • 5.1、自己实现一个TCP客户端
    • 5.2、Windows下可以使用NetAssist的网络助手工具
  • 小结

一、前言

手把手教你从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服务器。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1337691.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文献研读|Prompt窃取与保护综述

本文介绍与「Prompt窃取与保护」相关的几篇工作。 目录 1. Prompt Stealing Attacks Against Text-to-Image Generation Models&#xff08;PromptStealer&#xff09;2. Hard Prompts Made Easy: Gradient-Based Discrete Optimization for Prompt Tuning and Discovery&#…

Linux - 记录问题:怎么通过安装包的方式安装gRPC

适用场景 当docker 构建环境不能链接到github 的时候&#xff0c;就可以使用本地构建的方式 完成对应服务的构建需求。 参考案例 使用本地安装包的方式安装 gRPC 注意&#xff1a; 在Docker构建过程中&#xff0c;某些软件包可能会尝试配置时区&#xff0c;这通常需要交互式…

性能优化,让用户体验更加完美(渲染层面)

前言 上一篇我们已经围绕“网络层面”探索页面性能优化的方案&#xff0c;接下来本篇围绕“浏览器渲染层面”继续开展探索。正文开始前&#xff0c;我们思考如下问题&#xff1a; 浏览器渲染页面会经过哪几个关键环节&#xff1f;“渲染层面”的优化从哪几方面着手&#xff1f…

智能三维数据虚拟现实电子沙盘

一、概述 易图讯科技&#xff08;www.3dgis.top&#xff09;以大数据、云计算、虚拟现实、物联网、AI等先进技术为支撑&#xff0c;支持高清卫星影像、DEM高程数据、矢量数据、无人机倾斜摄像、BIM模型、点云、城市白模、等高线、标高点等数据融合和切换&#xff0c;智能三维数…

Git基础学习_p1

文章目录 一、前言二、Git手册学习2.1 Git介绍&前置知识2.2 Git教程2.2.1 导入新项目2.2.2 做更改2.2.3 Git追踪内容而非文件2.2.4 查看项目历史2.2.5 管理分支&#x1f53a;2.2.6 用Git来协同工作2.2.7 查看历史 三、结尾 一、前言 Git相信大部分从事软件工作的人都听说过…

SadTalker数字人增加视频输出mp4质量精度

最近在用数字人简易方案&#xff0c;看到了sadtalker虽然效果差&#xff0c;但是可以作为一个快速方案&#xff0c;没有安装sd的版本&#xff0c;随便找了个一键安装包 设置如上 使用倒是非常简单&#xff0c;但是出现一个问题&#xff0c;就是输出的mp4都出马赛克了 界面上却…

001、安装 Rust

目录 1. 安装 Rust 2. 安装编译器 Visual Studio Code 3. 更新、卸载、文档命令 4. 结语 1. 安装 Rust 安装 Rust 非常简单&#xff0c;首先进入 Rust官网 &#xff0c;然后点击右上角的 Install 。 进入 Install 界面&#xff0c; 它会自动识别你当前的操作系统并给你推荐…

自带AI算法的热红外相机

Tofu AIIR 是识别跟踪与热红外成像一体化的模组&#xff0c;支持热红外视频下的多类型物体检测、识别、跟踪等功能。 产品支持视频编码、设备管理、目标检测、深度学习识别、跟踪等功能&#xff0c;提供多机版与触控版管理软件&#xff0c;为二次开发提供了丰富的SDK接口和开源…

Xshell——Windows将本地文件上传到Linux服务器

1、scp命令 scp是基于ssh的网络文件传输命令&#xff0c;可以将本地文件或文件夹直接上传到服务器指定位置。命令格式&#xff1a; 上传文件 scp -P port filepath usernameip:TargetPath 上传文件夹 scp -r -P port filepath usernameip:TargetPath -P port&#xff1a;用于指…

Spark的生态系统概览:Spark SQL、Spark Streaming

Apache Spark是一个强大的分布式计算框架&#xff0c;用于大规模数据处理。Spark的生态系统包括多个组件&#xff0c;其中两个重要的组件是Spark SQL和Spark Streaming。本文将深入探讨这两个组件&#xff0c;了解它们的功能、用途以及如何在Spark生态系统中使用它们。 Spark …

Redis连接报错-Could not connect to Redis at 127.0.0.1:6379: Connection refused

进入Redis所在路径&#xff0c;命令行输入redis-cli报错&#xff1a;Could not connect to Redis at 127.0.0.1:6379: Connection refused 解决方法&#xff1a; redis-server redis.conf 连接成功&#xff1a;

装饰模式(单一责任)

Decorator&#xff08;装饰模式&#xff1a;单一责任模式&#xff09; 链接&#xff1a;装饰模式实例代码 解析 目的 在某些情况下我们可能会“过度地使用继承来扩展对象的功能”&#xff0c;由于继承为类型引入的静态特质&#xff0c;使得这种扩展方式缺乏灵活性&#xff…

H5调用企业微信扫一扫接口

一、依赖引入 <script src"http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script><!-- <script src"https://res.wx.qq.com/wwopen/js/jsapi/jweixin-1.0.0.js"></script> --><script src"https://open.work.…

ASP.NET MVC的5种AuthorizationFilter

一、IAuthorizationFilter 所有的AuthorizationFilter实现了接口IAuthorizationFilter。如下面的代码片断所示&#xff0c;IAuthorizationFilter定义了一个OnAuthorization方法用于实现授权的操作。作为该方法的参数filterContext是一个表示授权上下文的AuthorizationContext对…

探索前端开发趋势:2023年的新兴技术与发展方向

随着科技的不断发展&#xff0c;前端开发领域也在不断演进。本文将详细介绍2023年前端开发的新兴技术和发展趋势&#xff0c;为开发者们指明前端技术的发展方向和面临的挑战。从WebAssembly、PWA到低代码开发&#xff0c;激动人心的全新前景等你探索。 随着科技的快速发展&…

华锐视点为广汽集团打造VR汽车在线展厅,打破地域限制,尽享购车乐趣

随着科技的飞速发展&#xff0c;我们正在进入一个全新的时代——元宇宙时代。元宇宙是一个虚拟的世界&#xff0c;它不仅能够模拟现实世界&#xff0c;还能够创造出现实世界无法实现的事物。而汽车行业作为人类生活的重要组成部分&#xff0c;也在积极探索与元宇宙的融合&#…

SpringBoot3 整合Kafka

官网&#xff1a;https://kafka.apache.org/documentation/ 消息队列-场景 1. 异步 2. 解耦 3. 削峰 4. 缓冲 消息队列-Kafka 1. 消息模式 消息发布订阅模式&#xff0c;MessageQueue中的消息不删除&#xff0c;会记录消费者的偏移量 2. Kafka工作原理 同一个消费者组里的消…

手机之变@2023:高端化之“殇”、技术革新与新生机

【潮汐商业评论/原创】 消费者越来越不爱换手机了。 “我的手机用3年了&#xff0c;没坏也没卡&#xff0c;使用需求基本都能满足&#xff0c;没什么可换的。现在的手机出再高的配置&#xff0c;但我的需求没那么高&#xff0c;换一次成本也不小&#xff0c;实在换不动了。”…

Bytebase:统一数据库 CI/CD 解决方案 | 开源日报 No.128

bytebase/bytebase Stars: 7.9k License: NOASSERTION Bytebase 是一个数据库 CI/CD 解决方案&#xff0c;为开发人员和 DBA 提供统一的工具来管理不同数据库系统的开发生命周期。其主要功能包括标准化操作流程、SQL 代码审查、GitOps 集成以及数据访问控制等。关键特性和核心…

【Jmeter】Jmeter基础9-BeanShell介绍

3、BeanShell BeanShell是一种完全符合Java语法规范的脚本语言,并且又拥有自己的一些语法和方法。 3.1、Jmeter中使用的BeanShell 在Jmeter中&#xff0c;除了配置元件&#xff0c;其他类型的元件中都有BeanShell。BeanShell 是一种完全符合Java语法规范的脚本语言,并且又拥…