当前位置: 首页 > 编程日记 > 正文

C语言网络编程:多路IO select实现多客户端

文章目录

          • 阻塞式的服务器程序
          • 多线程服务器程序
          • 非阻塞式服务器程序
          • 基于事件响应的服务器程序
          • 事件响应服务器程序的实现`select`

阻塞式的服务器程序

我们接触过最多的最基础的网络通信模型为TCP/UDP通信模型,以下为TCP通信模型的基本流程C语言网络编程:TCP客户端实现
在这里插入图片描述
但是以上过程中每个通信函数都是阻塞的,而且建立连接之后的数据接收发送同样是阻塞形式的。send无法发送时只能继续阻塞,recv接收不到同样阻塞。这个过程整个进程都是处于非常被动的消耗大量CPU资源的等待过程。这为多客户端以及多业务逻辑的网络编程带来了挑战。

多线程服务器程序

此时很多人推出多线程,即服务器这里使用多线程方式为每一个客户端创建一个独立的连接,如C语言网络编程:TCP实现多线程实现多客户端 ,此时每个客户端都能够独立和服务端进行通信。这里不推荐使用多进程的方式解决多客户端以及多业务逻辑问题,因为进程的开销远大于线程,fork的方式基本是将父进程所有的资源接管到子进程,如果并发级较高,系统资源会消耗极大。

但是多线程同样存在问题,每个客户端的连接为一个线程,线程操作本就复杂,同时并发量较高时对服务器的CPU本身也是一种挑战。

这个时候线程池技术应运而生,目的是为了降低多线程对系统CPU资源的开销,维护指定数量的线程来处理连接,当建立连接之后“池”内指定个数的线程负责和客户端通信,当释放连接或者指定时间内为通信,则“池”接收下一轮客户端连接。

这里的数据库、tomcat、apache等服务器都有线程池的应用。但是线程池本身的规模需要和服务器的连接规模匹配,如果小规模线程池负责大规模的服务器连接,这样对系统性能反而有反作用。

非阻塞式服务器程序

我们可以通过对通信过程中文件描述符的设置,将其更改为非阻塞的文件描述符
fcntl(fd, F_SETFL, O_NONBLOCK);
如果对sockfd通信描述符设置非阻塞标记,像我们通信过程中的发送接收函数运行之后会立即返回,返回值有如下几种情况
ret = recv(int sockfd, void *buf, size_t len, int flags)

  • ret > 0,表示接受数据完毕,返回值即是接受到的字节数;
  • ret = 0,表示连接已经正常断开;
  • ret = -1,且 errno 等于 EAGAIN,表示 recv 操作还没执行完成;
  • ret = -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系统错误 errno。

此时服务器可以循环调用recv函数去接收数据,但是recv本身也是系统调用,如果循环调用,同样会产生较大的系统开销。
此时操作系统同样提供了更优的选择,select进行非阻塞通信的管理

基于事件响应的服务器程序

select 多路io管理的接口基本被所有的unix/linux系统支持,主要接口如下:

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);void FD_CLR(int fd, fd_set *set); //从集合中删除指定的fd描述符
int  FD_ISSET(int fd, fd_set *set); //判断指定的fd描述符是否存在于集合之中
void FD_SET(int fd, fd_set *set);//将指定的fd添加到集合之中
void FD_ZERO(fd_set *set); //初始化集合

由于 select() 接口可以同时对多个句柄进行读状态、写状态和错误状态的探测,所以可以很容易构建为多个客户端提供独立问答服务的服务器系统。select最关键的地方是如何动态维护 select() 的三个参数 readfds、writefds 和 exceptfds。作为输入参数,readfds 应该标记所有的需要探测的“可读事件”的句柄,其中永远包括那个探测 connect() 的那个“母”句柄;同时,writefds 和 exceptfds 应该标记所有需要探测的“可写事件”和“错误事件”的句柄 ( 使用 FD_SET() 标记 )

比如客户端的connect操作会激发select的一个“可读事件”,同时将对应通信的文件句柄加入到对应可读事件的FD_SET之中,捕捉到“可读事件”之后从FD_SET中取出指定的文件句柄即可读。recv以后需将对应的句柄值加入writefds中,然后继续探测下次的“可写事件”,同样,如果 select() 发现某句柄捕捉到“可写事件”,则程序应及时做 send() 操作,并准备好下一次的“可读事件”探测准备。以上过程为一个select循环,同时我们可以操作仅仅检测可写或者可读事件。

使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务.

但是如果select模型的循环体中有较多的操作,则会导致select事件探测以及响应产生巨大的延时,这将会是灾难性的
在这里插入图片描述

不过我们当前系统提供了很多信号响应以及事件响应的库来供大家使用。signal以及sigaction的信号处理机制,以及libevent事件驱动库;通过这一些库以及信号处理函数我们能够极大得提升操作系统的响应速度,从而避免以上出现的事件响应和执行出现较大延时的情况。

事件响应服务器程序的实现select

这里仅仅使用select实现服务端程序,针对select中的读事件驱动进行周期轮询。
server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>#define IP "192.168.102.182"
#define PORT 8000int skfd = -1;
struct sockaddr_in addr;
socklen_t len;
int cli[FD_SETSIZE];
int g_cli_count = 0;
int g_ret = -1;void print_err(char *str, int line, int err_no) {printf("%d, %s :%s\n",line,str,strerror(err_no));_exit(-1);
}void *getData(void *arg) {struct timeval tv;tv.tv_sec = 2;tv.tv_usec = 0;int i;while (1) {for(i = 0; i < g_cli_count; ++i ) {fd_set rfds;FD_ZERO(&rfds);int maxfd = 0;int retval = 0;FD_SET(cli[i],&rfds);if (maxfd < cli[i]) {maxfd = cli[i];}/*仅仅轮询读文件句柄,rfds返回值如下:1. -1 轮询失败,接口异常,并设置errno2. 0 并未检测到文件句柄有数据3. > 0 检测到部分文件句柄有数据,执行获取操作此时设置的timeval轮询周期为2秒*/retval = select(maxfd + 1,&rfds, NULL, NULL, &tv);if ( -1 == retval ) print_err("select failed\n",__LINE__,errno);else if (retval == 0) {}else {char buf[1024];bzero(&buf,1024);g_ret = recv(cli[i],buf,sizeof(buf),0);if (-1 == g_ret) print_err("recv failed\n",__LINE__,errno);printf("recv %s\n",buf);}}sleep(1);		}
}/*创建连接线程,负责接收来自客户端的连接,并将句柄加入到FD_SET中*/
void *getConn(void *arg) {while(1) {int conn = accept(skfd, (struct sockaddr *)&addr, &len);if ( -1 == conn ) print_err("accept failed\n",__LINE__,errno);/*建立连接之后打印客户端ip和端口号,并将客户端的句柄加入管理数组*/printf("port = %d, addr = %s\n", ntohs(addr.sin_port),inet_ntoa(addr.sin_addr));cli[g_cli_count ++] = conn;	printf ("client%d accept success is %d",g_cli_count,conn);}
}/*发送消息线程*/
void *sendMess(void *arg) {int i;while(1) {char buf[1024];bzero(&buf, 1024);fgets(buf, sizeof(buf), stdin);for (i = 0; i < g_cli_count; ++i) {if (buf) {g_ret = send(cli[i], buf, sizeof(buf),0);if (-1 == g_ret) print_err("send failed\n",__LINE__,errno);}}	}	
}
int main()
{pthread_t send_id,recv_id,connect_id;//创建TCP协议族的面向连接可靠的字节流,socket文件描述符skfd = socket(AF_INET, SOCK_STREAM, 0);if ( -1 == skfd) {print_err("socket failed",__LINE__,errno);}addr.sin_family = AF_INET; //设置tcp协议族addr.sin_port = htons(PORT); //设置端口号addr.sin_addr.s_addr = inet_addr(IP); //设置ip地址/*创建bind,绑定本服务器的ip和端口号*/g_ret = bind(skfd, (struct sockaddr*)&addr, sizeof(addr));if ( -1 == g_ret) {print_err("bind failed",__LINE__,errno);}/*将主动描述符skfd转为被动描述符*/g_ret = listen(skfd, 3);if ( -1 == g_ret ) {print_err("listen failed", __LINE__, errno);}len = sizeof(addr);/*创建三个线程,用于创建连接,发送消息,接收消息*/pthread_create(&connect_id,NULL,getConn,NULL);pthread_detach(connect_id);pthread_create(&send_id, NULL, sendMess,NULL);pthread_detach(send_id);pthread_create(&recv_id, NULL, getData, NULL);pthread_detach(recv_id);while(1){}return 0;
}

客户端程序这里使用的是标准的tcp阻塞式客户端程序,可以有多个这样的客户端
client1.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>#define IP "192.168.102.182"
#define PORT 8000void print_err(char *str, int line, int err_no) {printf("%d, %s :%s\n",line,str,strerror(err_no));_exit(-1);
}int main()
{int skfd = -1, ret = -1;skfd = socket(AF_INET, SOCK_STREAM, 0);if ( -1 == skfd) {print_err("socket failed",__LINE__,errno);}struct sockaddr_in addr;addr.sin_family = AF_INET; //设置tcp协议族addr.sin_port = htons(PORT); //设置端口号addr.sin_addr.s_addr = inet_addr(IP); //设置ip地址ret = connect(skfd,(struct sockaddr*)&addr, sizeof(addr));if(-1 == ret) print_err("connect failed", __LINE__, errno);char buf[100] = {0};char rec[100] = {0};while (1) {bzero(&buf, sizeof(buf));ret = send(skfd,"client1 send",sizeof("client1 send"), 0);if (-1 == ret) {print_err("send failed", __LINE__, errno);}bzero(&rec, sizeof(recv));ret = recv(skfd, &rec, sizeof(rec), 0);if(-1 == ret) print_err("recv failed", __LINE__, errno);else if(ret > 0) printf("recv from server %s\n",rec);}return 0;
}

gcc server.c -o server -pthread
gcc client1.c -o client1 -pthread
运行如下:
先运行server,再运行client
服务端输出如下:
在这里插入图片描述
客户端输出如下:
在这里插入图片描述

相关文章:

MVC 中的 ViewModel

此文章总结自&#xff1a;http://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp.net-mvc-applications ViewModel 这个概念不只是在在MVC模式中有&#xff0c;你会在很多关于MVC、MVP、MVVM的文章中见到这个说法&#xff0c;并且这个概念在任何技术…

java udp tcp协议_【java】TCP和UDP传输协议

TCP协议和UDP协议的比较TCP的全称是Transmission Control Protocol (传输控制协议)传输控制协议&#xff0c;是一种面向连接的协议&#xff0c;类似打电话在通信的整个过程中保持连接保证了数据传递的可靠性和有序性是一种全双工的字节流通信方式服务器压力比较大&#xff0c;资…

dot3_bump_mapping

为什么80%的码农都做不了架构师&#xff1f;>>> //----------------------------------------------------------------------------- // Name: ogl_dot3_bump_mapping.cpp // Author: Kevin Harris // Last Modified: 04/21/05 // Descript…

WPF入门教程-转载

最近为了做炫酷的UI&#xff0c;了解了WPF&#xff0c;之前一直是使用winform的&#xff0c;界面也是古老的不行。在园里找到了一个大佬以前写的教程&#xff0c;备注一下。按照系列教程走下来&#xff0c;可以直接上手了。备忘传送门>>>link&#xff1a;DotNet菜园-W…

记一次shell脚本推后台stopped的问题

我们知道linux 下shell可以被分为交互式脚本和非交互式脚本。 交互式脚本即 输入命令之后shell会等待你的输入&#xff0c;当你输入之后命令会被立即提交从而执行。这个时候我们常见的终端bash&#xff0c;以及login提示等都是交互式命令。 非交互式脚本即shell解释器不需要等待…

封装,继承,多态

一、封装&#xff1a; 封装是实现面向对象程序设计的第一步&#xff0c;封装就是将数据或函数等集合在一个个的单元中&#xff08;我们称之为类&#xff09;。被封装的对象通常被称为抽象数据类型。 封装的意义&#xff1a;   封装的意义在于保护或者防止代码&#xff08;数据…

java将一个数转成36进制的数_编程实现将一个N进制数转换成M进制数。

python: 手写算法版&#xff1a; def conversion_num(num, src, dest): rtn # 1、校验源和目标是否相同 if src dest: rtn num # 2、转成10进制# if src ! 10: num_str str(num) num_str num_str[::-1] exe_num 0 dec_num 0 for num_char in num_str: # 十六进制处理 i…

iOS之Storyboard导航大揭秘(1)

本文使用的软件版本&#xff1a; IOS&#xff1a;6.1 XCode&#xff1a;4.6 Storyboard&#xff08;故事板&#xff09;是XCode4.2才开始支持的&#xff0c;为了使设计View更容易。尽管用以前的nib&#xff08;xib&#xff09;拖拖拽拽也没问题&#xff0c;不过却需要 在各种文…

nginx的gzip压缩功能

我们在开发网站的时候&#xff0c;应该要考虑到pv&#xff0c;因为pv比较大可能会造成服务器带宽不够用&#xff0c;进而导致用户体验变差。 这个时候我们就可以考虑用nginx的gzip功能。 在nginx中开启gzip压缩功能很简单&#xff0c;之需要在nginx的配置文件nginx.conf中配置以…

C语言的单链表逆序和指定范围逆序

文章目录前言逆序指定范围逆序源码实现前言 关于链表的逆置&#xff0c;是考察对链表指针的理解。知道了如何不实用额外空间&#xff0c;同时使用O(n)复杂度对链表进行逆序之后将会对链表有好理解。 同时关于如何在指定范围内对链表逆置同样可以进一步加深理解 逆序 基本过程…

mysql udf 性能_适当的mysql udf

问题不在于参数的类型,而是在调用str_ucwords_init时它是NULL(因为在检索任何行之前调用了str_ucwords_init).要使str_ucwords与字段一起使用,您必须通过在_init函数中将initid-> maybe_null设置为1并在str_ucwords中将* null_value设置为1(并且结果为NULL,尽管这可能不是必…

让Windows7运行速度更快的BIOS优化设置教程

和以前使用WindowsXP一样&#xff0c;很多用户都在设法提高windows7的系统运行速速&#xff0c;比较常见的方法大多是对系统服务进行优化&#xff0c;去掉一些可有可无的系统服务&#xff0c;还有就是优化资源管理器菜单等。除此之外&#xff0c;还有一些“不常见的偏方”&…

开源 免费 java CMS - FreeCMS1.2-功能说明-网上调查

2019独角兽企业重金招聘Python工程师标准>>> 下载地址&#xff1a;http://code.google.com/p/freecms/ 网上调查 从FreeCMS 1.2 开始支持 Admin和站点管理员可以管理站点下所有网上调查&#xff0c;普通用户只可管理自己添加的网上调查。 1. 网上调查管理 从左…

Python 之 杂谈(迭代器iter)、偏函数

1、 l [1,2,3,b,5,6] def func():return l.pop() x iter(func,b) print(x.__next__()) print(x.__next__()) print(x.__next__())执行结果&#xff1a; 遇到“b”就停下 2、偏函数 from functools import partial def add(x,y):return xy func partial(add,1)#将1固定传给x…

C语言的单链表创建:头插法/尾插法

文章目录前言链表头插法链表尾插法源码实现前言 接下来一段时间&#xff0c;将对数据结构进行复习&#xff0c;总的来说数据结构自大学之后忘记得有点吓人&#xff0c;为了防止脑容量本就小得脑袋更小&#xff0c;必须得持续性得温故了。 链表数据结构得提出 是为了弥补数组上…

java配置文件实现方式_java相关:详解Spring加载Properties配置文件的四种方式

java相关&#xff1a;详解Spring加载Properties配置文件的四种方式发布于 2020-4-29|复制链接摘记: 一、通过 context:property-placeholder 标签实现配置文件加载1、用法示例&#xff1a; 在spring.xml配置文件中添加标签..一、通过 context:property-placeholder 标签实现配置…

objective-c abort() 与 exit() 函数的区别

exit()函数 调用exit会让用户感觉程序崩溃了&#xff0c;不会有按Home键返回时的平滑过渡和动画效果&#xff1b;另外&#xff0c;使用exit可能会丢失数据&#xff0c;因为调用exit并不会调用-applicationWillTerminate:方法和UIApplicationDelegate方法&#xff1b; abort() a…

用户、组的管理常用到的命令介绍

在LINUX系统管理中&#xff0c;我们经常添加删除修改用户和组的信息&#xff0c;所以我们来学习下创建删除修改用户和组 下面我们就来简单的说下什么是组、什么是用户的概念&#xff1f; 用户&#xff1a; 其实简单的理解就是文件或者目录创建的一种标识。 组&#xff1a; 组简…

react生命周期函数

在react中&#xff0c;生命周期函数指的是组件在加载前&#xff0c;加载后&#xff0c;以及组件更新数据和组件销毁时触发的一系列方法。通常分为以下几类&#xff1a; 组件加载的时候触发的函数&#xff1a;constructor 、componentWillMount、 render 、componentDidMount 组…

C语言的单链表求交点

单链表求交点&#xff0c;问题如下&#xff1a; 使用o(1)的空间复杂度&#xff0c;求两个链表相交的时候的交点&#xff0c;这个思想就类似于使用o(1)的空间复杂度和o(n)的时间复杂度来求链表的第k个节点。 过程如下&#xff1a; 获取两个链表的长度将较长的链表移动和短链表…

jquery中如何以逗号分割字符串_百度知道

jquery中如何以逗号分割字符串_百度知道javascript本身就是带split方法的定义和用法split() 方法用于把一个字符串分割成字符串数组。语法stringObject.split(separator,howmany)参数 描述separator 必需。字符串或正则表达式&#xff0c;从该参数指定的地方分割 stringObject。…

mysql 前台启动_从Windows命令行启动MySQL

可以从命令行手动启动MySQL服务器。可以在任何版本的Windows中实现。要想从命令行启动mysqld服务器&#xff0c;你应当启动控制台窗口(或“DOS window”)并输入命令&#xff1a;C&#xff1a;\> C:\Program Files\MySQL\MySQL Server 5.0\bin\mysqld根据系统中MySQL安装位置…

设置datagridview的数据源为(DATASET)SQL查寻结果

private void button5_Click(object sender, EventArgs e)02 {03 if (MessageBox.Show("确认删除该行吗&#xff1f;", "删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question) DialogResult.Yes )04 {05 SqlConnection conn new SqlConnection();0…

vim中文手册

http://vimcdoc.sourceforge.net/doc/help.html转载于:https://www.cnblogs.com/answercard/p/10125611.html

C语言单链表求环,并返回环的起始节点

若链表中存在环&#xff0c;找出其中的环所在的节点&#xff0c;否则&#xff0c;返回NULL 在没有C set容器的优势前提下&#xff0c;我们对这样的环型的寻找以及定位可以利用快慢指针来实现。 有环的存在&#xff0c;类似与操场跑圈&#xff0c;必然存在快慢之分。有了快慢&a…

CSS3无前缀脚本prefixfree.js与Animatable使用介绍

要求 必备知识 本文要求基本了解 JAVASCRIPT 和 和 CSS3 基本知识。 运行环境 桌面端:IE9 &#xff0c;Opera 10&#xff0c;火狐3.5 &#xff0c;Safari 4和Chrome浏览器;移动端:移动Safari&#xff0c;Android浏览器&#xff0c;Chrome浏览器和Opera Mobile。 演示地址 演示…

mysql的优化之table_open_cache 篇_mysql性能优化之table_open_cache

表现&#xff1a;数据库查询效率慢&#xff0c;show processlist 发现比较多的查询正在opening table。进一步确认&#xff0c;执行以下语句&#xff1a;mysql> show global status like open%tables%;------------------------| Variable_name | Value |----------------…

AIX系统日志学习笔记之一

AIX系统上线之后&#xff0c;难免会出现错误&#xff0c;为了应对错误&#xff0c;aix提供了很多处理错误的方法和日志记录机制&#xff0c;为修复故障和系统提供方便。 Errdemon是aix的一个守护进程&#xff0c;该进程会实时检查/dev/drror设备文件&#xff0c;查看是否有新的…

C语言的单链表分割

已知链表头指针head与数值x&#xff0c;将所有小于x的节点放在大于或等于x 的节点前&#xff0c;且保持这些节点的原来的相对位置。 这个过程有点类似于快速排序&#xff0c;寻找一个阈值&#xff0c;比该阈值小的放左边&#xff0c;比该阈值大的放右边。只是由数组遍历变为来…

java面试时候算法题多吗_java程序员面试中最容易被问到的18个算法题(附答案!)...

作者&#xff1a;cpp软件架构狮链接&#xff1a;https://www.toutiao.com/i6618515311836529156/(点击阅读原文前去围观)算法是比较复杂又基础的学科&#xff0c;每个学编程的人都会学习大量的算法。而根据统计&#xff0c;以下这18个问题是面试中最容易遇到的&#xff0c;本文…