当前位置: 首页 > news >正文

2.1 网络编程 异步网络库zvnet

2.1.1 网络io与io多路复用select/poll/epoll

1. 搭建一个简单的服务端

#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <errno.h> int main() { // 1. create socket int listenFd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(2000); // 2. bind socket int flag = bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); if (-1 == flag) { printf("bind error: %s\n", strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len = sizeof(clientAddr); int clientFd; char buff[1024]; while(1) { memset(buff, 0, sizeof(buff)); // 4. accept client connection clientFd = accept(listenFd,(struct sockaddr*)&clientAddr,&len); // 5. recv data from client recv(clientFd, buff, sizeof(buff), 0); printf("recv data: %s\n", buff); // 6. send data to client memset(buff, 0, sizeof(buff)); sprintf(buff, "hello client, I have received your data"); send(clientFd, buff, strlen(buff), 0); close(clientFd); } close(listenFd); return 0; }

上述代码可以循环的接收客户端连接,并且每一个客户端拥有一次和服务端收发数据的过程。但是有一个致命的确定,比如client1 client2 client3依此连接到服务端(先连接不发数据),此时会出现什么情况呢?如果我client3 client2 client1依此给server发消息,但只有当client1发出消息之后,client3和client2才会收到消息。

分析原因:client1 client2 client3依此连接到服务端时,程序阻塞在recv上,那么其他的client只有三次握手成功,并且阻塞在TCP队列上,并不能建立tcp连接。

2. 改用多线程的方式

#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <errno.h> #include <bits/pthreadtypes.h> void handle_task(void* arg) { int clientFd = *(int*)arg; char buff[1024]; memset(buff, 0, sizeof(buff)); while(1) { memset(buff, 0, sizeof(buff)); int count = recv(clientFd, buff, sizeof(buff), 0); printf("recv data: %s\n", buff); if(0 == count) { printf("client closed connection\n"); break; } memset(buff, 0, sizeof(buff)); sprintf(buff, "hello client, I have received your data"); send(clientFd, buff, strlen(buff), 0); } close(clientFd); } int main() { // 1. create socket int listenFd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(2000); // 2. bind socket int flag = bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); if (-1 == flag) { printf("bind error: %s\n", strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len = sizeof(clientAddr); int clientFd; char buff[1024]; static int count = 0; #if 0 while(1) { memset(buff, 0, sizeof(buff)); // 4. accept client connection clientFd = accept(listenFd,(struct sockaddr*)&clientAddr,&len); printf("accept client connection, count: %d\n", ++count); // 5. recv data from client recv(clientFd, buff, sizeof(buff), 0); printf("recv data: %s\n", buff); // 6. send data to client memset(buff, 0, sizeof(buff)); sprintf(buff, "hello client, I have received your data"); send(clientFd, buff, strlen(buff), 0); close(clientFd); } #elif 1 // with multi thread while(1) { // 4. accept client connection clientFd = accept(listenFd,(struct sockaddr*)&clientAddr,&len); pthread_t thid; pthread_create(&thid, NULL, (void *)handle_task, (void *)&clientFd); } #endif close(listenFd); return 0; }

上述代码在不断开tcp连接时候可以一直进行通信。

弊端:1.如果tcp连接“占着茅坑不拉屎”,那么是对server资源的一种浪费。

这里要明确一个概念,网络IO和Tcp连接。① 只要完成3次握手,即可建立tcp连接,但是只有通过accept获得fd才能进行IO通信; ② accept有一个建立连接的队列,accept是从队列中获取fd的,如果fd在队列里面,那么说明三次握手成功 = 建立tcp连接,如果队列满了,那么无法三次握手成功,只能等着。

3. select的方式(I/O多路复用)

多路复用的意思就是在一个线程中,既能够处理监听事件,又能处理i/o事件

#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <errno.h> #include <bits/pthreadtypes.h> #include <unistd.h> #include <sys/select.h> #define PORT 2001 int main() { // 1. create socket int listenFd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(PORT); // 2. bind socket int flag = bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); if (-1 == flag) { printf("bind error: %s\n", strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len = sizeof(clientAddr); char buff[1024]; // 定义两个文件描述符集合,rfds是总的,rset是临时的 fd_set rfds, rset; FD_ZERO(&rfds); FD_SET(listenFd, &rfds); // 将listenFd放到文件描述符集合中 int maxFd = listenFd; while (1) { rset = rfds; /* param1: select要检查的范围(由于从0开始,所以 + 1) param2: 监听read event param3:监听write event param4:监听exception event param4:是否设置超时时间 */ int nready = select(maxFd + 1, &rset, NULL, NULL, NULL); // 检查是否是listenFd是可读状态,如果可读,则说明有新的连接进来了 if(FD_ISSET(listenFd, &rset)) { int clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &len); printf("accept finshed: %d\n", clientFd); // 将新连接加到fd集合中 FD_SET(clientFd, &rfds); if(clientFd > maxFd) maxFd = clientFd; } // 遍历除了监听集合外的fd是否是可读状态 for (size_t i = listenFd + 1; i < maxFd + 1; i++) { if(FD_ISSET(i, &rset)) { printf("%ld:\n",i); char buffer[1024] = {0}; int count = recv(i, buffer, 1024, 0); printf("buffer:%s\n",buffer); if (count == 0) // disconnect { printf("client disconnect: %ld\n", i); close(i); FD_CLR(i, &rfds); continue; } } } } close(listenFd); return 0; }

注:

① FD_SET(fd,&rfds),fd_set是一个bit位的数组,比如bit arr[1024],就是每一个格子存一个bit位,fd是几久存到对应格子的第几位,比如fd是45,则在arr[45]

② 为啥select返回之后会有一个if和一个for循环呢?如果select返回了,有可能是listenFd被激活了,也有可能是被监听的连接active了,那么select返回了,则就存在两种情况,也可能两种情况同时存在。所以第一个if是判断是否是listenFd被激活,被激活说明有新的连接,要将新连接放到rfds中等待被监听;然后for循环就是遍历剩下被监听的clientFd是否有数据来了。

③ select中的第一个参数必须是maxFd + 1,不然监听不到linstenFd,因为fd是从0开始的。

4. poll的方式(I/O多路复用)

#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <errno.h> #include <bits/pthreadtypes.h> #include <unistd.h> #include <sys/select.h> #include <poll.h> #define PORT 2001 int main() { // 1. create socket int listenFd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(PORT); // 2. bind socket int flag = bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); if (-1 == flag) { printf("bind error: %s\n", strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len = sizeof(clientAddr); char buff[1024]; struct pollfd fds[1024] = { 0 }; fds[listenFd].fd = listenFd; fds[listenFd].events = POLLIN; int maxFd = listenFd; while ( 1 ) { int nReady = poll(fds, maxFd + 1, -1); if(fds[listenFd].revents & POLLIN) { // listenFd可读,将新的fd加入到监听中 printf("have new connect...\n"); int clientFd = accept(listenFd,(struct sockaddr*)&clientAddr,&len); fds[clientFd].fd = clientFd; fds[clientFd].events = POLLIN; if(clientFd > maxFd) maxFd = clientFd; } for (size_t i = listenFd + 1; i < maxFd + 1; i++) { if(fds[i].revents & POLLIN) { // clientFd可读 char buf[1024] = { 0 }; int count = recv(i, buf, sizeof(buf), 0); if(count == 0) { // 关闭clientFd,并移出监听 printf("client disconnect: %ld\n", i); close(i); fds[i].fd = -1; fds[i].events = 0; continue; } else { printf("收到的消息:%s\n",buf); } } } } close(listenFd); return 0; }

注:

① 这里的event有多种状态,POLLIN表示监听读事件,这里为啥要用fds[clientFd].revents和POLLIN做运算呢,因为revents有多种状态,有可读可写异常等等,那么POLLIN是0X0001,如果revents的最后一位是被触发状态,那么说明该event是可读状态。

总结:select和poll

① select和poll都是循环遍历的做法,在select或者poll的时候,会将整个数组从用户空间拷贝到内核空间进行遍历,如果被触发则从内核空间拷贝出来

② 适合io较少的时候使用

③ select在linux2.4之前,因为没有poll和epoll,所以有poll用poll,没有poll用select

5. epoll的方式(I/O多路复用)

#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <errno.h> #include <bits/pthreadtypes.h> #include <unistd.h> #include <sys/select.h> #include <poll.h> #include <sys/epoll.h> #define PORT 2001 int main() { // 1. create socket int listenFd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(PORT); // 2. bind socket int flag = bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); if (-1 == flag) { printf("bind error: %s\n", strerror(errno)); return -1; } // 3. listen listen(listenFd, 10); struct sockaddr_in clientAddr; int len = sizeof(clientAddr); char buff[1024]; // 1是参数,兼容老版本,系统遗留问题,但参数必须大于0 int epfd = epoll_create(1); struct epoll_event ev; ev.data.fd = listenFd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, listenFd, &ev); while ( 1 ) { /* code */ struct epoll_event events[1024] = { 0 }; int nReady = epoll_wait(epfd, events, 1024, -1); for (size_t i = 0; i < nReady; i++) { /* code */ if(events[i].data.fd == listenFd) { int clientFd = accept(listenFd,(struct sockaddr*)&clientAddr,&len); struct epoll_event clientEv; clientEv.data.fd = clientFd; clientEv.events = EPOLLIN; printf("accept finshed: %d\n", clientFd); epoll_ctl(epfd, EPOLL_CTL_ADD, clientFd, &clientEv); continue; } else if(events[i].events & EPOLLIN) { char buffer[1024] = {0}; int count = recv(events[i].data.fd, buffer, 1024, 0); if(count == 0) { printf("client disconnect: %d\n", events[i].data.fd); epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); continue; } printf("recv: %s\n", buffer); count = send(events[i].data.fd, buffer, count, 0); printf("send: %d\n", count); } } } close(listenFd); return 0; }

注:

① epoll底层是通过红黑树 + 双向链表构成的,首先红黑树监听事件,如果有事件被激活,双向链表指向被激活的节点上去,然后将链表中数据拷贝到用户空间中,epoll_wait返回值nReady表示拷贝的数量。

总结select、poll、epoll

select:底层用的fd_set位图数组来管理是否有事件处于被激活状态,通过FD_ISSET来查看是否被激活,通过FD_SET来增加监听的fd,通过FD_CLR来删除被监听的事件。位图大小是系统固定,一般是1024大小,意思是被监听的数组大小最多只有1024,是有限的。

poll:用户自己创建struct pollfd fds[] 数组来做的,通过fds[i].revent & POLLIN来判断是否是被触发可读状态,通过fds[i].data.fd = clientFd,fds[i].event=EPOLLIN来设置监听的事件,通过fds[i].data.fd = -1,fds[i].event=0来取消。被监听的数量按理论是和硬件相关的,没有个数限制。

epoll:epoll底层是通过红黑树 + 双向链表构成的,首先红黑树监听事件,如果有事件被激活,双向链表指向被激活的节点上去,然后将链表中数据拷贝到用户空间中。首先"创建一个红黑树",epoll_create(unsiged int i),返回值是一个int epfd,epfd相当于通过这个文件描述符去操控红黑树;增加一个监听事件,epoll_ctl(epfd, EPOLL_CTL_ADD, clientFd, &clientEv);,删除一个监听事件,epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);

6. reactor百万并发

http://www.jsqmd.com/news/483567/

相关文章:

  • Audio Pixel Studio部署教程(Serverless版):Vercel/Cloudflare Pages托管
  • 别再调戏ChatGPT了!OpenClaw正式“破壳”:那个有手的AI,真的来了
  • DeepSeek-OCR部署避坑:首次唤醒慢问题诊断与SSD缓存优化方案
  • 分离式游戏机硬件平台:GD32F470多模无线交互设计
  • RAG生成阶段优化:解决幻觉、多轮对话与引用标注,小白程序员必备收藏!
  • 从抖振到平滑:基于饱和函数sat(s)的准滑动模态滑模控制SIMULINK实践
  • 第四章 第一性原理 vs 类比思维:人类两种终极思考模式
  • 【常亮24天】立创开源:基于STC32F12K54的低功耗迷你桌面时钟4.0版硬件与软件全解析
  • RockyLinux 10.1深度解析:软重启赋能高效运维,后量子加密守护数据新纪元
  • nlp_gte_sentence-embedding_chinese-large实现Python爬虫数据智能处理:自动化采集与清洗
  • 35岁程序员转型指南:避开年龄危机,拥抱AI高薪新赛道
  • 从零开始:Unsloth环境搭建与模型微调完整教程
  • 数据清洗面试问答指南(面试官 vs 实习生)
  • 鸿蒙物联WiFi开关:机械式墙壁开关的非侵入式智能改造方案
  • 第六章 第一性原理:商业世界的本质、价值与决策底层逻辑
  • Global Mapper三维地形与建筑可视化实战指南
  • COMSOL模拟边坡降雨不饱和条件下强度折减的影响研究
  • ESP32-S3语音交互终端:低成本教育级硬件设计
  • 2026 AI浪潮下,程序员的职业突围与机遇,年薪154W!真心建议大家冲一冲新兴领域
  • 深入解析display lldp neighbor与display mac-address的工作原理及网络管理应用
  • SecGPT-14B入门必看:从零搭建网络安全分析大模型服务(含参数详解)
  • Linux网络驱动之Fixed-Link(18)
  • Leather Dress Collection参数详解:Sampling Method对皮革纹理锐度的影响分析
  • 永磁同步电机最大转矩电流比控制(MTPA)+弱磁控制的仿真模型设计与实现
  • 收藏!2026程序员破局指南:高价值赛道已切换,大模型应用开发才是高薪密码
  • 基于SVPWM改进的异步电机/感应电机直接转矩控制的纹波优化“参考文献:[此处可添加具...
  • 【活动获奖作品】基于MPS MP28167-A与CH244K的3A升降压电源适配器设计与调试全记录
  • 甲方云安全:阿里云 / 华为云安全配置最佳实践
  • all-MiniLM-L6-v2进阶使用:多格式模型管理与版本控制策略解析
  • 2 模型预训练、微调、强化学习的格式