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

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   1024enum 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   1024enum 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.mzph.cn/news/579065.shtml

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

相关文章

洛谷——【数据结构1-2】二叉树

文章目录 题目【深基16.例1】淘汰赛题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1基本思路&#xff1a;代码 【深基16.例3】二叉树深度题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1基本思路&#xff1a;代码 [USACO3.4] 美国血统 American Heritage题目描…

算符优先语法分析设计原理与实现

前言&#xff1a; 作者的词法分析程序以及算符优先语法分析设计程序仓库链接 1、目标任务 **[实验项目] **以专题 1 词法分析程序的输出为语法分析的输入&#xff0c;实现算符优先分析算法&#xff0c;完成以下描述算术表达式的算符优先文法的算符优先分析过程。 G[E]:E→E…

Spark编程实验三:Spark SQL编程

目录 一、目的与要求 二、实验内容 三、实验步骤 1、Spark SQL基本操作 2、编程实现将RDD转换为DataFrame 3、编程实现利用DataFrame读写MySQL的数据 四、结果分析与实验体会 一、目的与要求 1、通过实验掌握Spark SQL的基本编程方法&#xff1b; 2、熟悉RDD到DataFram…

代码随想录算法训练营第二十七天 | 回溯算法part4

力扣题目 用时&#xff1a;未知 1、93.复原IP地址 2、78.子集 3、90.子集II 力扣题目记录 93.复原IP地址 这个题和上一个题差不多&#xff0c;只是4段数字&#xff0c;只有三个句点&#xff0c;所以第四段要单独处理判断子串是否合法 class Solution { private:vector<s…

Qt/QML编程学习之心得:实现一个图片浏览器(十八)

QML中有个重要控件,经常使用就是image,通常可以用它来显示一张图片。如果想结合openfiledialog来让image显示图片,也就是做一个简易的图片浏览器,怎么弄呢? DefaultFileDialog.qml: import QtQuick 2.0 import QtQuick.Dialogs 1.0FileDialog {id: fileDialogtitle: &qu…

2024免费的数据恢复软件EasyRecovery14自己操作就能恢复的方法

而今天小编为大家还是带来了同系列软件easyrecovery14&#xff0c;这是easyrecovery数据恢复软件中的技术员版本&#xff0c;不仅包含家庭版和专业版的所有功能&#xff0c;而且还旨在简化技术人员的数据恢复过程。软件拥有强大的数据恢复功能&#xff0c;支持使用的恢复场景有…

KNN与KD树博客总结

目录 总结小结&#xff1a; 总结 原始篇&#xff1a;KNN算法及其优缺点算法思想改进篇&#xff1a;KD树&#xff08;KNN的plus版算法实现第一篇&#xff1a;平衡二叉树的构建&#xff08;递归算法实现第二篇&#xff1a;KD树的构建&#xff08;递归算法实现第三篇&#xff1a;…

CentOS 7 设置网络

CentOS 7 设置网络 正常情况 ①登陆进去之后使用下面的命令修改文件 echo ONBOOTyes >> /etc/sysconfig/network-scripts/ifcfg-ens33②如果是虚拟机重启后使用如下命令进行查看IP地址 ip addr注&#xff1a;到这里如果显示有两部分&#xff0c;则代表网络设置成功&a…

华为设备VRP系统管理

为了满足企业业务对网络的需求&#xff0c;网络设备中的系统文件需要不断进行升级。另外&#xff0c;网络设备中的配置文件也需要时常进行备份&#xff0c;以防设备故障或其他灾害给业务带来损害。在升级和备份系统文件或配置文件时&#xff0c;经常会使用FTP和TFTP来传输文件。…

服务器系统时间不同步如何处理

在分布式计算环境中&#xff0c;服务器系统时间的同步至关重要。然而&#xff0c;由于各种原因&#xff0c;服务器系统时间不同步的问题时有发生,这可能会导致严重的问题&#xff0c;如日志不准确、证书验证失败等。下面我们可以一起探讨下造成服务器系统时间不同的原因以及解决…

【Vue2+3入门到实战】(5)Vue基础之Computed计算属性 详细示例

目录 一、今日学习目标1.computed计算属性 二、computed计算属性1.概念2.语法3.注意4.案例5.代码准备 三、computed计算属性 VS methods方法1.computed计算属性2.methods计算属性3.计算属性的优势4.总结 四、计算属性的完整写法五、综合案例-成绩案例六、Computed计算属性总结 …

【测试开发】测试分类相关知识

文章目录 目录 文章目录 前言 一、测试分类 1.按照测试对象划分&#xff1a; 界面测试&#xff1a; 可靠性测试&#xff1a; 容错性测试&#xff1a; 文档测试 &#xff1a; 兼容性测试 &#xff1a; 易用性测试 &#xff1a; 安装卸载测试 &#xff1a; 安全测试 &#xff1a;…

揭秘Pod状态与生命周期管理的秘密(中)

上一篇文章中主要介绍了Pod的基础概念与使用、删除。本文将带你一起学习Pod的几种容器(Init、Pause) 点击 这里 可以查看所有相关文章。 Init 容器 本文讲解 Init 容器的基本概念&#xff0c;这是一种专用的容器&#xff0c;在应用程序容器启动之前运行&#xff0c;用来包含…

Spring Boot 3.2 新特性之 HTTP Interface

SpringBoot 3.2引入了新的 HTTP interface 用于http接口调用&#xff0c;采用了类似 openfeign 的风格。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-http-interface 一、概述 HTTP Interface 是一个类似于 openfeign 的同步接口调…

住宅代理妙用:网络抓取的必备工具

什么是住宅代理&#xff1f; 要准确理解什么是住宅代理&#xff0c;首先需要了解什么是住宅IP。IP 地址是连接到网络时分配给单个设备的唯一标识符。这允许设备或端点直接相互通信&#xff0c;而无需跨线。 住宅IP是指分配给特定设备&#xff08;例如计算机、手机、平板电脑等…

新版IDEA中Git的使用(二)

说明&#xff1a;前面介绍了在新版IDEA中Git的基本操作&#xff0c;本文介绍关于分支合并、拉取等操作&#xff1b; 例如&#xff0c;现在有一个项目&#xff0c;分支如下&#xff1a; main&#xff1a;主分支&#xff1b; dev&#xff1a;开发分支&#xff1b; test&#x…

CNVD原创漏洞审核和处理流程

一、CNVD原创漏洞审核归档和发布主流程 &#xff08;一&#xff09;审核和归档流程 审核流程分为一级、二级、三级审核&#xff0c;其中一级审核主要对提交的漏洞信息完整性进行审核&#xff0c;漏洞符合可验证&#xff08;通用型漏洞有验证代码信息或多个互联网实例、事件型…

CCRC信息安全认证指什么?

CCRC信息安全认证&#xff0c;全称为中国网络安全审查技术与认证中心&#xff08;China Information Security Certification Center&#xff09;认证&#xff0c;它是中国信息安全领域的一项权威资质认证。该认证由中央机构编制委员会办公室批准成立&#xff0c;隶属于国家市场…

k8s的二进制部署1

k8s的二进制部署&#xff1a;源码包部署 k8smaster01&#xff1a;192.168.176.61 kube-apiserver kube-controller-manager kube-scheduler etcd k8smaster01&#xff1a;192.168.176.62 kube-apiserver kube-controller-manager kube-scheduler node节点01&#xff1a;192.…

promise的使用和实例方法

前言 异步,是任何编程都无法回避的话题。在promise出现之前,js中也有处理异步的方案,不过还没有专门的api能去处理链式的异步操作。所以,当大量的异步任务逐个执行,就变成了传说中的回调地狱。 function asyncFn(fn1, fn2, fn3) {setTimeout(() > {//处理第一个异步任务fn1…