一、并發服務器
1、單循環服務器(順序處理)????????
????????一次只能處理一個客戶端連接,只有當前客戶端斷開連接后,才能接受新的客戶端連接
2、多進程/多線程并發服務器
while(1) {
connfd = accept(listenfd);
pid = fork(); ?// 或 pthread_create()
if (pid == 0) {
// 子進程/線程處理通信
recv(connfd, ...);
send(connfd, ...);
close(connfd);
exit(0); // 或 pthread_exit
}
close(connfd); // 父進程關閉已交給子進程的 connfd
}
優點:
實現真正并發
客戶端可長時間通信
缺點:
創建/銷毀進程或線程開銷大
資源占用高(內存、CPU)
存在僵尸進程問題(需
waitpid()
回收)
二、IO 模型分類(5種)
1、阻塞IO模型
- 常見阻塞IO模型:
- i--讀 scanf、getchar、fgets、read、recv
- o--寫 管道:讀端存在,寫管道 ?寫操作阻塞>>>>內存不足,寫不進去便阻塞了
- 優點:簡單、方便、要等 效率不高
2、?非阻塞IO模型
1)以讀為例:
- 特點:需要不停去看,資源開銷大
2)實現方法
方法一:
open()
時指定int fd = open("fifo", O_RDONLY | O_NONBLOCK);
方法二:運行時用
fcntl()
修改int flag = fcntl(fd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
- 注意事項
- ?適用于
read/write
等系統調用。 對recv()
可使用MSG_DONTWAIT
標志實現非阻塞
3)示例
方法一
方法二
3、信號驅動IO模型
1)使用 SIGIO
信號通知數據到達,異步但支持有限
- 有數據發個信號,然后系統調用
- 通知粒度粗:僅能告知 “有 IO 事件”,無法區分事件類型與細節
2)利用函數:fcntl 實現
3)實現步驟
// 1. 設置文件描述符支持異步通知
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);// 2. 設置信號接收者(當前進程)
fcntl(fd, F_SETOWN, getpid());// 3. 注冊信號處理函數
signal(SIGIO, sig_handler);
- 不足之處
支持的文件類型有限(如 socket、tty)
不適合大量連接場景
實際應用較少
4)示例
4、異步 IO 模型
信號驅動IO和一步IO區別
核心結論 | 信號驅動 IO | 異步 IO |
---|---|---|
異步能力的完整性 | “半異步”:僅解決 “IO 就緒通知”,未解決 “數據拷貝異步” | “全異步”:從 “IO 就緒” 到 “數據拷貝完成” 全程異步 |
內核與應用的職責劃分 | 內核僅通知 “就緒”,數據拷貝需應用程序主動做 | 內核包辦 “就緒檢測 + 數據拷貝”,應用程序僅用結果 |
工業界定位 | 早期異步 IO 的過渡方案,已被淘汰 | 現代高并發 / 高性能 IO 的標準方案 |
????????簡單來說:信號驅動 IO 是 “讓內核喊你‘飯好了’,但你得自己去盛飯”;現代異步 IO 是 “內核把‘飯盛好端到你面前’,你直接吃就行”—— 后者才是真正意義上 “無感知等待、無主動操作” 的異步 IO
5、IO多路復用模型
1)概念
????????用一個線程監控多個文件描述符(fd),當其中任意一個就緒時通知程序進行處理
????????n個客戶端-->>用一個線程或進程服務器去答復
? ? ? ? 優點:避免創建大量線程/進程,節省資源,適合高并發場景(如 Web 服務器)
? ? ? ? 常見函數:select()、
poll()、
epoll()
2)函數介紹
① select
頭文件:????????#include <sys/select.h>
函數原型:
????????????????int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);功能:????????實現IO多路復用
參數:
????????nfds //是關心的文件描述符中最大的那個文件描述符 + 1
????????readfds //代表 要關心 的 讀操作的文件描述符的集合
????????writefds //代表 要關心 的 寫操作的文件描述符的集合 >>> 與read類似
????????exceptfds //代表 要關心 的 異常的文件描述符的集合 >>> 與read類似(error--2)
????????timeout //超時 設置一個超時時間
? ? ? ? ? ? ? ? //NULL 表示select是一個阻塞調用
????{0,0}
:非阻塞效果
????{sec, usec}
:指定超時????????????????時間最小單位寫到ms
返回值:????????
????????????????成功:就緒的 fd 數量(>0)
????????????????超時:返回 0
????????????????失敗:返回 -1
輔助宏函數:
????????????????FD_ZERO(fd_set *set); ? ? ? ?????????// 清空集合
FD_SET(int fd, fd_set *set); ????????// 添加 fd 到集合
FD_CLR(int fd, fd_set *set);???????? // 從集合中移除 fd
FD_ISSET(int fd, fd_set *set);? ? ??// 判斷 fd 是否在集合中
基本實現流程
文字版過程
- 建立一張表 >>>監控 目前只關心讀
- fd_set readfds;一張表
- FD_ZERO(&readfds);清空表(初始化)
- 將要監控的文件描述符 添加到表中
- FD_SET(0,&readfds);//stdin
- FD_SET(fd,&readfds);//建的管道或者文件描述符
- 準備參數
- maxfds 是關心的文件描述符中最大的那個文件描述符 + 1
- int maxfds = fd + 1;
- 每次系統調用只會留下就緒的文件描述符(每次監控都會重新遍歷一遍)
- fd_set backfds; //設置這個等于最初的表
- maxfds 是關心的文件描述符中最大的那個文件描述符 + 1
- 一般在循環內進行系統調用
- 具體內容如下
- 最前面建立tcp網絡連接的基本步驟
- 利用select函數實現步驟
- 加上定時定次功能
優點
內核負責輪詢,減少用戶態頻繁切換
支持跨平臺(Windows/Linux 均可用
缺點
最大監聽數受限:
FD_SETSIZE
默認 1024(Linux)每次調用需重置 fd_set:內核會修改集合,必須每次重新
FD_SET
用戶態與內核態拷貝開銷大
返回后仍需遍歷所有 fd 才能知道哪個就緒
效率隨 fd 數量增長下降明顯
知識點
- stdin????????--->0
- stdout? ? ? --->1
- error? ? ? ? --->2
② poll
頭文件:????????#include <poll.h>
函數原型:? int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:????????????實現IO多路復用
參數:????????
struct pollfd *fds?:struct pollfd {
int ? fd; ? ? ? // 文件描述符
short events; ? // 關注的事件(輸入)
short revents; ?// 實際發生的事件(輸出)
};nfds_t nfds:表示要監控的文件描述符的數量
timeout?:時間值?
返回值:????????
????????????????成功 表示 就緒的數量 ;0 超時情況下表示 沒有就緒實際
????????????????失敗 -1
事件標志:
????????????????POLLIN:數據可讀(等價于
select
的讀)
基本實現流程
優點
無 1024 限制:只要系統允許打開足夠多 fd
無需重置集合:
events
和revents
分離更清晰的事件機制
效率更高:僅遍歷傳入的數組,不遍歷整個 fd 范圍
缺點
每次調用仍需將整個
fds[]
拷貝到內核返回后仍需遍歷全部元素查找就緒 fd
時間復雜度仍是 O(n),連接數多時性能下降
③ epoll
< 水平觸發 >
????????只要緩沖區有數據就持續觸發
結果展現
epoll_create????????????????
函數原型:????????int epoll_create(int size);
功能:? ? ? ? ? ? ??創建 epoll 實例
參數:? ? ? ? ? ? ??
size
:提示內核初始分配空間大小(現已忽略)返回值:? ? ? ? ? 成功??epoll 文件描述符(用于后續操作)
? ? ? ? ? ? ? ? ?? ? ? ??失敗 -1
注意事項:??????使用完需
close(epfd)
epoll_ctl()
???????
函數原型:????????int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:? ? ? ? ? ? ???控制監聽列表
參數:? ? ? ? ? ? ??epfd:epoll 句柄(
epoll_create
返回? ? ? ? ? ? ? ? ? ? ? ? ?op :操作類型
????????????????????????????????EPOLL_CTL_ADD
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??EPOLL_CTL_DEL
????????????????????????????????EPOLL_CTL_MOD
? ? ? ? ? ? ? ? ? ? ? ? ?fd:要監聽的目標文件描述符
? ? ? ? ? ? ? ? ? ? ? ? event:事件結構體? ?
struct epoll_event
返回值:? ? ? ? ? 成功??epoll 文件描述符(用于后續操作)
? ? ? ? ? ? ? ? ?? ? ? ??失敗 -1
struct epoll_event
epoll_event?
結構體:
struct epoll_event {
uint32_t ? ? events; ? // 監聽的事件類型
epoll_data_t data; ? ? // 用戶數據(共用體)
};
typedef union epoll_data {
void ? ?*ptr;
int ? ? ?fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
常見事件類型
事件 | 含義 |
---|---|
EPOLLIN | 可讀 |
EPOLLOUT | 可寫 |
EPOLLRDHUP | 對端關閉連接(TCP 半關閉) |
EPOLLERR | 錯誤(自動監聽) |
EPOLLHUP | 掛起(自動監聽) |
EPOLLET | 邊沿觸發模式(Edge Triggered) |
EPOLLONESHOT | 觸發一次后失效,需重新注冊 |
epoll_wait()
????????
函數原型:????????int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout);功能:? ? ? ? ? ? ??等待事件發生
參數:? ? ? ? ? ? ??epfd:epoll 句柄(
epoll_create
返回? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ?events
:用戶提供的數組,用于接收就緒事件? ? ? ? ? ? ? ? ? ? ? ? ?
maxevents
:最大接收事件數(通常 10~100? ? ? ? ? ? ? ? ? ? ? ?
timeout
:超時(單位 ms )????????????????????????????????
-1
:永久阻塞????????????????????????????????
0
:非阻塞????????????????????????????????
>0
:等待指定毫秒?????????????????????????????????????????????? ? ? ??
返回值:? ? ? ? ? 成功??就緒事件數量(無需遍歷所有 fd)
? ? ? ? ? ? ? ? ?? ? ? ??失敗 -1
tcp 實現epoll并發服務器
封裝添加和刪除函數
完整內容
#include "head.h"int add_fd(int listenfd,int epfd) //將文件描述符添加到 epoll 監控列表
{struct epoll_event ev; //定義結構體ev.events = EPOLLIN; //表示監控可讀事件(文件描述符有數據可讀時觸發)ev.data.fd = listenfd;if (epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev))//epoll_ctl添加、修改、刪除監控的文件描述符{perror("epoll_ctl add fail");return -1;}return 0;
}
int del_fd(int fd,int epfd)
{if (epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)){perror("epoll_ctl del fail");return -1;}return 0;}
int main(void)
{int serfd = socket(AF_INET,SOCK_STREAM,0);if (serfd < 0){perror("fail to socke");return -1;}struct sockaddr_in seraddr;seraddr.sin_family = AF_INET;seraddr.sin_port = htons(50000);seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");if (bind(serfd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){perror("bind fial");return -1;}if (listen(serfd,5) < 0){perror("listen fail");return -1;}struct sockaddr_in cliaddr;bzero(&cliaddr,0);socklen_t len = sizeof(cliaddr);/************正片開始******************/char buf[1024] = {0};int epfd = epoll_create(1); //創建,返回一個用于操作的文件描述符efd//括號內要求大于0的值就行add_fd(serfd,epfd); //添加serfd到epoll監控int t = 3000;struct epoll_event ret_ev[1024]; //可以容納的個數while (1){int ret = epoll_wait(epfd,ret_ev,10,t); //等待事件發生,超時時間為3000ms//ret_ev數組用于存儲發生的事件//就序最多處理 10 個事件printf("ret = %d\n",ret);if (ret < 0) //出錯處理{perror("epoll fail");return -1;}if (ret > 0) //遍歷每個事件{int i = 0;for (i = 0; i < ret; i++){if (ret_ev[i].data.fd == serfd) { int connfd = accept(serfd,(struct sockaddr *)&cliaddr,&len);if (connfd < 0){perror("accept fail");return -1;}printf("----client connect---\n");printf("client ip:%s\n",inet_ntoa(cliaddr.sin_addr));printf("port :%d\n",ntohs(cliaddr.sin_port));//添加到表里add_fd(connfd,epfd);}else //如果不是serfd我們就要開始收數據了{recv(ret_ev[i].data.fd,buf,sizeof(buf),0);printf("buf = %s\n",buf); if (0 == strncmp(buf,"quit",4)){del_fd(ret_ev[i].data.fd,epfd); //從epfd內刪除close(ret_ev[i].data.fd);}}}}}close(serfd);return 0;
}
< 邊緣觸發 >
????????僅在狀態變化時觸發一次(必須配合非阻塞 IO)
? ? ? ? 兩種情況:
????????正常數據:實際只能觸發一次,但數據還在,利用循環可以打印出來,但是讀完數據就沒有了()---n <0
????????quit退出:實際只能觸發一次,但數據還在,利用循環可以打印出來,讀完數據就沒有了,---n= 0
主要改變
注意事項:? ? ? ?
????????????????fd 必須設置為 非阻塞
????????????????必須一次性讀完所有數據(直到 read()
返回 EAGAIN
)
????????????????否則會丟失后續事件
結果展現
具體水平觸發的代碼區別(改動的地方)
完整代碼
#include "head.h"
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <poll.h>
#include <sys/epoll.h>#include <unistd.h>
#include <fcntl.h>int add_fd(int fd, int epfd)
{struct epoll_event ev;ev.events = EPOLLIN | EPOLLET;// EPOLLET(ET)邊緣觸發//$$$--改1--$$$ev.data.fd = fd; //標準輸入 if ( epoll_ctl(epfd,EPOLL_CTL_ADD, fd, &ev)){perror("epoll_ctl add fail");return -1;} return 0;
}int del_fd(int fd, int epfd) //刪除
{//struct epoll_event ev;//ev.events = EPOLLIN;//ev.data.fd = fd; //標準輸入 if ( epoll_ctl(epfd,EPOLL_CTL_DEL, fd, NULL)){perror("epoll_ctl add fail");return -1;} return 0;
}//$$$$$$$$$$--改2--$$$$$$$$$$$$$$$
void set_nonblock(int fd)
{int flags = fcntl(fd,F_GETFL);flags = flags | O_NONBLOCK;fcntl(fd,F_SETFL,flags);return;
}int main(int argc, char const *argv[])
{//step1 socket int fd = socket(AF_INET,SOCK_STREAM,0);if (fd < 0){perror("socket fail");return -1;}struct sockaddr_in seraddr;bzero(&seraddr,sizeof(seraddr));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(50000);seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");//step2 bind if (bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){perror("connect fail");return -1;}//step3 listenif (listen(fd,5) < 0){perror("listen fail");return -1;}struct sockaddr_in cliaddr;bzero(&cliaddr,0);socklen_t len = sizeof(cliaddr);//1.準備表 int epfd = epoll_create(1);if (epfd < 0){perror("epoll_create fail");return -1;}//2.添加 fd add_fd(fd,epfd);char buf[1024] = {0};struct epoll_event ret_ev[10];while (1){int ret =epoll_wait(epfd,ret_ev,10,-1);if (ret < 0){perror("epoll fail");return -1;}if (ret > 0){int i = 0;for (i = 0; i < ret; ++i){if (ret_ev[i].data.fd == fd) //listenfd {int connfd = accept(fd,(struct sockaddr *)&cliaddr,&len);if (connfd < 0){perror("accept fail");return -1;}printf("---client connect---\n");printf("client ip:%s\n",inet_ntoa(cliaddr.sin_addr));printf("port: %d\n",ntohs(cliaddr.sin_port));//設置非阻塞set_nonblock(connfd);//添加到表中add_fd(connfd,epfd);}else //$$$$$$$$$$--改3--$$$$$$$$$$$$$$${while(1){int n = recv(ret_ev[i].data.fd,buf,1,0);printf("n = %d buf = %s\n",n,buf);if (n < 0 && errno != EAGAIN) //正常數據{ perror("recv ");del_fd(ret_ev[i].data.fd,epfd);close(ret_ev[i].data.fd);}if (n== 0 || strncmp(buf,"quit",4) == 0) //退出{del_fd(ret_ev[i].data.fd,epfd);close(ret_ev[i].data.fd);}sleep(1);}}}}} return 0;
}
3)函數對比
特性 | select | poll | epoll |
---|---|---|---|
平臺兼容性 | 高(POSIX) | 高 | 僅 Linux |
最大連接數 | ~1024 | 無限制(但性能差) | 無限制 |
時間復雜度 | O(n) | O(n) | O(1) |
用戶/內核拷貝 | 每次全量拷貝 | 每次全量拷貝 | 共享內存 |
是否修改輸入參數 | 是(需備份) | 否(revents 分離) | 否 |
觸發模式 | 僅 LT | 僅 LT | LT + ET |
遍歷開銷 | 高(需遍歷所有 fd) | 中(遍歷數組) | 低(只處理就緒) |
適用場景 | 小規模連接、跨平臺 | 中小規模連接 | 大規模高并發(如 Nginx) |
4)應用建議
場景 | 推薦方案 |
---|---|
小型工具程序(<100 連接) | select (簡單、跨平臺) |
中等規模服務(幾百連接) | poll 或 select |
高并發服務器(數千以上) | epoll (Linux) |
需跨平臺(如 Windows) | select 或 libevent /libuv 封裝 |
5)總結
整體使用思路:
????????1.準備監控表
????????2.添加監控的文件描述符
????????3.調用函數監控事件發生