文章目录

  • 阻塞式的服务器程序
  • 多线程服务器程序
  • 非阻塞式服务器程序
  • 基于事件响应的服务器程序
  • 事件响应服务器程序的实现`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
服务端输出如下:

客户端输出如下:

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

  1. 【Java网络编程与IO流】Java之Java Servlet详解

    Java网络编程与IO流目录: [Java网络编程与IO流]Java中IO流分为几种?字符流.字节流.缓冲流.输入流.输出流.节点流.处理流 [Java网络编程与IO流]计算机网络常见面试题高频核心考 ...

  2. C语言网络编程函数与相关结构汇总

    持续更新中- 服务器和客户端的一般流程 服务器端:socket()-->bind( )-->listen()-->accept()-->read()/write()---> ...

  3. qn模块java脚本_Qn271 对于网络编程 反射 IO 线程的一些一本入门程序 多多联系会加快 速度 WinSock-NDIS 269万源代码下载- www.pudn.com...

    文件名称: Qn271下载  收藏√  [ 5  4  3  2  1 ] 开发工具: Java 文件大小: 673 KB 上传时间: 2015-05-13 下载次数: 0 提 供 者: 褚晓旦 详细 ...

  4. c语言网络编程百度云,C语言网络编程:C#网络编程(接收文件).pdf

    c语言网络编程:C#网络编程(接收文件) - Part.5 疯狂代码 / ĵ http://DotNet/Article65322.html 本文源代码下载地址 /2009_03/Network-Pa ...

  5. 【Java网络编程与IO流】Java中IO流分为几种?字符流、字节流、缓冲流、输入流、输出流、节点流、处理流

    Java网络编程与IO流目录: [Java网络编程与IO流]Java中IO流分为几种?字符流.字节流.缓冲流.输入流.输出流.节点流.处理流 [Java网络编程与IO流]计算机网络常见面试题高频核心考 ...

  6. C语言网络编程(3)— 通过DNS连接到百度

    C语言网络编程(3)- 通过DNS连接到百度 一.gethostbyname()函数 我们现在认知一台计算机主机通常采用直观可读的名字.例如百度我们会记住 www.baidu.com 而不是他的IP地 ...

  7. Linux操作系统下C语言网络编程(全文23475字,包含了Linux系统下所有网络编程的知识点,附程序代码)

    一.简介 如今网络应用随处可见,web.http.email 等这些都是网络应用程序,他们都有着基于相同的基本编程模型,有着相似的整体逻辑结构,并且还有着相同的编程接口.我们需要了解基本的客户端-服务 ...

  8. 《Go语言网络编程》第一章:体系

    原书地址:http://tumregels.github.io/Network-Programming-with-Go 如果不知道想要构建什么,是不可能创建一个系统的.而且如果不知道它工作的环境,也同 ...

  9. 【Netty】第二章 网络编程和 IO 概念剖析

    [Netty]第二章 网络编程 文章目录 [Netty]第二章 网络编程 一.网络编程 1.模拟阻塞模式下服务器单线程处理请求 2.模拟非阻塞模式下服务器单线程处理请求 3.使用 Selector 改 ...

最新文章

  1. 【Python】*args 和 **kwargs的用法
  2. npm ERR! { Error: EPERM: operation not permitted, mkdir 'C:\Program Files\nodejs\node_cache\_locks'
  3. java 网站开发实例_完整的javaweb项目
  4. 如何跨域来同步不同网站之间的Cookie
  5. Less的@import指令
  6. SQL Server 2016/2014/2012/2008/2005/2000简体中文企业版下载地址
  7. 在装完Linux系统之后自己去修改Swap分区的大小(两种方法)
  8. iOS仿京东分类菜单之UICollectionView内容
  9. golang的https服务器
  10. KEIL软件安装教程
  11. python编程实现语音数据分帧及分帧还原
  12. 树莓派做服务器装什么系统安装,树莓派 安装 群晖系统安装教程
  13. H3C设备网吧万兆光模块解决方案
  14. 大数据的应用场景你知道哪些?
  15. 【流媒体性能测试常用指标】
  16. 数据结构 —— ADT(抽象数据类型)
  17. 单片机开发---ESP32S3移植NES模拟器(二)
  18. iframe背景透明设置方法
  19. ME525手机GPRS上网问题——APN更改
  20. java图片压缩方法(多种)

热门文章

  1. JS中window.event事件使用详解
  2. mybatis 使用resultMap实现数据库的操作
  3. GitHub上整理的一些工具【转载】
  4. Max_user_connections 与Max_connections 与max_connect_errors
  5. [导入]实时数据库的经典书
  6. case when 效率高不高_南京薄蜂窝纸板效率高
  7. 计算机图形学 区域填充,计算机图形学 区域填充算法的实现
  8. mysql数据库验证登陆不上_MySQL数据库连接不上、密码修改问题
  9. 已知小红今年12岁c语言编程,C语言程序设计第轮复习习题.doc
  10. yii2服务器无法加载文件夹,用Yii2做的项目在部署到云服务器后访问无反应,也没有任何数据响应...