橙色
多進(jìn)程實現(xiàn)并發(fā)服務(wù)器
server_process.c文件內(nèi)容如下:
注意第70行的if(errno == EINTR),如果沒有這個if判斷的話,當(dāng)同時多個客戶端鏈接進(jìn)來,停掉一個客戶端,然后再啟動一個客戶端,就會發(fā)現(xiàn)沒法連接了,accept會報一個錯誤。因為一個客戶端停掉,在服務(wù)器端就相當(dāng)于一個子進(jìn)程終止執(zhí)行,會發(fā)出SIGCHLD信號,被信號捕捉函數(shù)所捕捉,而此時程序正停在accept處阻塞,等待下一個客戶端的連接。當(dāng)信號捕捉函數(shù)處理完再返回accpet時,就會報一個錯誤,該錯誤為EINTR。這個也可以去看accept函數(shù)的介紹,有說明(如下圖)。所以這里要做一個處理,如果errno是EINTR的話,則略過該報錯。
第101行strlen(recvBuf) + 1是很有必要的,strlen在計數(shù)的時候是結(jié)束符’\0’為止,但不包含結(jié)束符。+1之后寫入文件描述符的字符串就會帶上結(jié)束符。如果不帶上結(jié)束符的話,在另一端通過文件描述符讀出的時候,數(shù)據(jù)的最末尾很容易出現(xiàn)一個奇怪的符號。
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recyleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子進(jìn)程都回收了
break;
}else if(ret == 0) {
// 還有子進(jìn)程活著
break;
} else if(ret > 0){
// 被回收了
printf("子進(jìn)程 %d 被回收了\n", ret);
}
}
}
int main() {
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
// 注冊信號捕捉
sigaction(SIGCHLD, &act, NULL);
// 創(chuàng)建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 監(jiān)聽
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 不斷循環(huán)等待客戶端連接
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受連接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) {
continue;
}
perror("accept");
exit(-1);
}
// 每一個連接進(jìn)來,創(chuàng)建一個子進(jìn)程跟客戶端通信
pid_t pid = fork();
if(pid == 0) {
// 子進(jìn)程
// 獲取客戶端的信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客戶端發(fā)來的數(shù)據(jù)
char recvBuf[1024];
while(1) {
int len = read(cfd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
exit(0); // 退出當(dāng)前子進(jìn)程
}
}
close(lfd);
return 0;
}
client.c文件內(nèi)容如下:
// TCP通信的客戶端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.創(chuàng)建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.連接服務(wù)器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024];
int i = 0;
while(1) {
sprintf(recvBuf, "data : %d\n", i++);
// 給服務(wù)器端發(fā)送數(shù)據(jù)
write(fd, recvBuf, strlen(recvBuf)+1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服務(wù)器端斷開連接
printf("server closed...");
break;
}
sleep(1);
}
// 關(guān)閉連接
close(fd);
return 0;
}
多線程實現(xiàn)并發(fā)服務(wù)器
客戶端文件內(nèi)容同上
服務(wù)器端文件內(nèi)容如下:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid; // 線程號
};
struct sockInfo sockinfos[128];
void * working(void * arg) {
// 子線程和客戶端通信 需要cfd 客戶端的信息 線程號
// 獲取客戶端的信息
struct sockInfo * pinfo = (struct sockInfo *)arg;
char cliIp[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客戶端發(fā)來的數(shù)據(jù)
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
}
close(pinfo->fd);
return NULL;
}
int main() {
// 創(chuàng)建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 監(jiān)聽
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 初始化數(shù)據(jù)
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));//將結(jié)構(gòu)體里面所有的成員都初始化為0
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
// 循環(huán)等待客戶端連接,一旦一個客戶端連接進(jìn)來,就創(chuàng)建一個子線程進(jìn)行通信
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受連接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
struct sockInfo * pinfo;
for(int i = 0; i < max; i++) {
// 從這個數(shù)組中找到一個可以用的sockInfo元素
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {
sleep(1);
i=-1;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);//拷貝數(shù)據(jù)
// 創(chuàng)建子線程,因為線程號僅僅在線程創(chuàng)建后才有,所以直接在這里傳入pinfo->tid,就很方便
pthread_create(&pinfo->tid, NULL, working, pinfo);
//這里不能使用pthread_join,因為它是阻塞函數(shù),那么一個子線程沒結(jié)束主線程就只能阻塞在這里,沒辦法創(chuàng)建新的線程
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
BIO模型
阻塞等待:不占用CPU寶貴的時間片,但是每次只能處理一個操作。
當(dāng)對方暫時沒有發(fā)送數(shù)據(jù)時,程序就會阻塞在read處
BIO模型:通過多線程/多進(jìn)程解決每次只能處理一個操作的缺陷。但是線程/進(jìn)程本身需要消耗系統(tǒng)資源,并且線程和進(jìn)程的調(diào)度占用CPU.
NIO模型
非阻塞、忙輪詢:不斷的去催,或者說每隔一端時間就去查看有沒有操作
提高了程序的運行效率、但占用大量CPU資源和系統(tǒng)資源(假設(shè)有1w個客戶端鏈接進(jìn)來,那么服務(wù)器端讀取某一個客戶端的內(nèi)容最慢可能達(dá)到第1w次才能讀到,因為它要依次對這1w個客戶端進(jìn)行輪詢。但可能這1w次輪詢中,僅有一個客戶端的數(shù)據(jù)到達(dá)了,那么其余的9999次遍歷就都浪費了)
I/O多路復(fù)用(I/O多路轉(zhuǎn)接)
把文件中的數(shù)據(jù)寫入到內(nèi)存中就是輸入,把內(nèi)存中的數(shù)據(jù)寫入到文件中就是輸出
? ? ? ?I/O多路復(fù)用使得程序能夠同時監(jiān)聽多個文件描述符,能夠提高程序的性能
,Linux下實現(xiàn)I/O多路復(fù)用的系統(tǒng)調(diào)用有:select、pool和epoll
select
主旨思想
- 首先要構(gòu)造一個關(guān)于文件描述符的列表,將要監(jiān)聽的文件描述符添加到該列表中
- 調(diào)用一個系統(tǒng)函數(shù),監(jiān)聽該列表中的文件描述符,直到這些描述符中的一個或多個進(jìn)行I/O操作時,該函數(shù)才返回。
? ? ? ?a. 這個函數(shù)是阻塞的
? ? ? ?b. 函數(shù)對文件描述符的檢測的操作是由內(nèi)核完成的 - 在返回時,它會告訴進(jìn)程有多少描述符要進(jìn)行I/O操作
圖解原理
前三個是系統(tǒng)固定已經(jīng)占用的
函數(shù)解析
//sizeof(fd_set)=128字節(jié) 也就是1024位
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timerval *timeval);
- 參數(shù):
- nfds:委托內(nèi)核檢測的最大的文件描述符的值+1
- readfds:要檢測的文件描述符的讀的集合,委托內(nèi)核檢測哪些文件描述符的讀的屬性
- 對應(yīng)的是對方發(fā)送過來的數(shù)據(jù),因為讀是被動的接收數(shù)據(jù),檢測的就是讀緩沖區(qū)
- 是一個傳入傳出參數(shù)(比如我想看第5個文件描述符是否可以讀,那我把它置為1,傳入函數(shù),函數(shù)會把這個列表指針交給內(nèi)核,內(nèi)核來檢查,如果該文件描述符確實可以讀,那么內(nèi)核會把它置為1,不可讀,內(nèi)核就會把它置為0)
- writefds:要檢測的文件描述符的寫的集合,委托內(nèi)核檢測哪些文件描述符的寫的屬性
- 委托內(nèi)核檢測寫緩沖區(qū)是不是還可以寫數(shù)據(jù)〈不滿的就可以寫,也就是置為1)
- exceptfds:檢測發(fā)生異常的文件描述符的集合(一般不用)
- timeout:設(shè)置的超時時間
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
- NULL:永遠(yuǎn)等待,直到檢測到了文件描述符有變化
- tv_sec=0 tv_usec=0, 不阻塞
- tv_sec>0 tv_usec>0,阻塞對應(yīng)的時間
- 返回值:
- -1:失敗
- >0(n):檢測的集合中有n個文件描述符發(fā)生了變化
//將參數(shù)文件描述符fd對應(yīng)的標(biāo)志位設(shè)為0
void FD_CLR(int fd, fd_set *set);
//判斷fd對應(yīng)的標(biāo)志位是0還是1,返回值:fa對應(yīng)的標(biāo)志位的值是0,返回0,是1,返回1
int FD_ISSET(int fd, fd_set *set);
//將參數(shù)文件描述符fd對應(yīng)的標(biāo)志位設(shè)為1
void FD_SET(int fd, fd_set *set);
//fd_set一共有1024位,全部初始化為0
void FD_ZERO(fd_set *set);
代碼舉例
客戶端程序:
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 創(chuàng)建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 連接服務(wù)器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服務(wù)器已經(jīng)斷開連接...\n");
break;
}
sleep(1);
}
close(fd);
return 0;
}
服務(wù)器端程序:
這個服務(wù)器端的程序里還是蘊含了很多細(xì)節(jié)需要注意的。
我對第一次循環(huán)進(jìn)行一個分析,先是在rdset中把監(jiān)聽描述符lfd置為了1。接著進(jìn)入了while(1)死循環(huán)。
為了避免循環(huán)中select函數(shù)在傳入rdset時改變了rdset(因為rdset中記錄的是我所需要檢測的文件描述符,應(yīng)該一直是1,但如果將rdset傳入select,在該次循環(huán)中需要被檢測的文件描述符并沒有數(shù)據(jù)傳入,那么就會被內(nèi)核置為0,所以需要tmp),所以在循環(huán)開始將rdset拷貝給tmp。
接著,當(dāng)ret>0時,說明肯定有文件描述符變了,那就先看lfd,看是否是有新的客戶端連接進(jìn)來,如果有的話,則加入到集合rdset中(這里可能會有疑惑,為什么不在FD_SET(cfd, &rdset);后加一行FD_SET(cfd, &tmp);呢?這樣這個新端口傳入的數(shù)據(jù)也就能在該次死循環(huán)中讀取出來了。但考慮到可能這個新端口僅僅只是連接,并沒有傳入數(shù)據(jù),那read讀不到數(shù)據(jù)就會阻塞在這里,因此沒有加,讓它在下一次循環(huán)中再讀是比較保險的)
將這兩個程序運行起來,客戶端無論幾個,服務(wù)器都是能運行的,既沒有借助多線程也沒有借助多進(jìn)程,而是依靠了select函數(shù)。
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main() {
// 創(chuàng)建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監(jiān)聽
listen(lfd, 8);
// 創(chuàng)建一個fd_set的集合,存放的是需要檢測的文件描述符
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1) {
tmp = rdset;
// 調(diào)用select系統(tǒng)函數(shù),讓內(nèi)核幫檢測哪些文件描述符有數(shù)據(jù)
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1) {
perror("select");
exit(-1);
} else if(ret == 0) { //不可能為0,因為上面select設(shè)置的是阻塞,只有當(dāng)文件描述符有變化時才會到這里
continue;
} else if(ret > 0) {
// 說明檢測到了有文件描述符的對應(yīng)的緩沖區(qū)的數(shù)據(jù)發(fā)生了改變
//為什么要檢測lfd是否為1呢?因為第一次發(fā)生了改變肯定是lfd,但后面發(fā)生改變就可能是其他的文件描述符,而不是lfd(也就是說不是有新的文件描述符加進(jìn)來)
if(FD_ISSET(lfd, &tmp)) {
// 表示有新的客戶端連接進(jìn)來了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 將新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
//要檢測的是連接描述符的數(shù)據(jù)有沒有變化,所以不需要檢測監(jiān)聽文件描述符,循環(huán)從lfd+1開始
for(int i = lfd + 1; i <= maxfd; i++) {
if(FD_ISSET(i, &tmp)) {
// 說明這個文件描述符對應(yīng)的客戶端發(fā)來了數(shù)據(jù)
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(i);
FD_CLR(i, &rdset);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
select的缺點
-
每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大
-
同時每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個開銷在fd很多時也很大
-
select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
-
fds集合不能重用,每次都需要重置(其實說的就是上面服務(wù)器端程序定義了兩個fd_set,如果只用一個傳入內(nèi)核,該要檢測的端口這時并沒有數(shù)據(jù)到達(dá),那么就會被內(nèi)核置為0再傳遞出來。那么下次再傳入就不會檢測該端口了,而這顯然是不行的)
poll
poll只針對Linux有效,poll模型是基于select最大文件描述符限制提出的,跟select一樣,只是將select使用的三個基于位的文件描述符(readfds/writefds/exceptfds)封裝成了一個結(jié)構(gòu)體,然后通過數(shù)組的是形式來突破最大文件描述符的限制。
函數(shù)解析
#include <poll.h>
struct pollfd{
int fd; //委托內(nèi)核檢測的文件描述符
short events; //委托內(nèi)核檢測文件描述符的什么事件
short revents; //文件描述符實際發(fā)生的事件
};
//既要檢測讀也要檢測寫該怎么寫?
struct po11fd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
- 參數(shù):
- fds:數(shù)組的首地址
- nfds:這個是第一個參數(shù)數(shù)組中最后一個有效元素的下標(biāo)+1
- timeout:阻塞時長
0:不阻塞
-1:阻塞,當(dāng)檢測到需要檢測的文件描述符有變化,解除阻塞
>0:阻塞時長(單位是毫秒)
- 返回值:
-1:失敗
>0(n):成功, n表示檢測到集合中有n個文件描述符發(fā)生變化
代碼示例
客戶端程序和select中的一樣
服務(wù)器端程序如下:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
// 創(chuàng)建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監(jiān)聽
listen(lfd, 8);
// 初始化檢測的文件描述符數(shù)組
struct pollfd fds[1024];
for(int i = 0; i < 1024; i++) {
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;
int i;
while(1) {
// 調(diào)用poll系統(tǒng)函數(shù),讓內(nèi)核幫檢測哪些文件描述符有數(shù)據(jù)
int ret = poll(fds, nfds + 1, -1);
if(ret == -1) {
perror("poll");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 說明檢測到了有文件描述符的對應(yīng)的緩沖區(qū)的數(shù)據(jù)發(fā)生了改變
if(fds[0].revents & POLLIN) {
// 表示有新的客戶端連接進(jìn)來了
//先看結(jié)構(gòu)體數(shù)組中是否有空位,沒空位的話就等下次再accept新的客戶端,有的話就直接accept
for(i = 1; i < 1024; i++) {
if(fds[i].fd == -1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 將新的文件描述符加入到集合中
fds[i].fd = cfd;
fds[i].events = POLLIN;
// 更新最大的文件描述符的索引
nfds = nfds > i ? nfds : i;
break;
}
}
}
for(int i = 1; i <= nfds; i++) {
if(fds[i].revents & POLLIN) {
// 說明這個文件描述符對應(yīng)的客戶端發(fā)來了數(shù)據(jù)
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(fds[i].fd);
fds[i].fd = -1;
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
epoll(最重要,請重點掌握)
函數(shù)解析
#include <sys/epoll.h>
//創(chuàng)建一個新的epoll示例。在內(nèi)核中創(chuàng)建了一個數(shù)據(jù)。這個數(shù)據(jù)中有兩個比較重要的數(shù)據(jù),一個是需要檢測的文件描述符的信息(紅黑樹〉,還有一個是就緒列表,存放檢測到數(shù)據(jù)發(fā)送改變的文件描述符信息(雙向鏈表〉。
int epoll_create(int size);
- 參數(shù): size : 目前沒有意義了。隨便寫一個數(shù),必須大于0
- 返回值: -1 : 失敗, > 0 : 文件描述符,操作epoll實例的
//對epo11實例進(jìn)行管理:添加文件描述符信息,刪除信息,修改信息
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
- 參數(shù):
- epfd:epoll實例對應(yīng)的文件描述符
- op:要進(jìn)行什么操作
EPOLL_CTL_ADD:添加
EPOLL_CTL_MOD:修改
EPOLL_CTL_DEL:刪除
- fd:要檢測的文件描述符
- event:檢測文件描述符什么事情(如果是刪除操作的話直接NULL就行)
struct epoll_event{
_uint32_t events; // Epoll events
epoll_data data; //user data variable
};
typedef union epoll_data {
void *ptr; //回調(diào)函數(shù)
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
常見的Epoll檢測事件(events):
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET(邊沿模式)如果想要使用邊沿模式并檢測是否可以讀,events可以這么寫:EPOLLIN | EPOLLET
//檢測函數(shù),檢測內(nèi)核中的eventpoll是否有文件描述符改變了,注意events是一個struct epoll_event數(shù)組的指針
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 參數(shù):
- epfd:epo11實例對應(yīng)的文件描述符
- events:傳出參數(shù),是一個struct epoll_event數(shù)組的指針,保存了發(fā)送了變化的文件描述符的信息,
- maxevent:第二個參數(shù)結(jié)構(gòu)體數(shù)組的大小
- timeout:阻塞時間
- 0:不阻塞
- -1:阻塞,直到檢測到fd數(shù)據(jù)發(fā)生變化,解除阻塞
- >0:阻塞的時長(毫秒)
- 返回值:
- 成功,返回發(fā)送變化的文件描述符的個數(shù)>0
- 失敗 -1
代碼舉例
服務(wù)器端:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 創(chuàng)建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監(jiān)聽
listen(lfd, 8);
// 調(diào)用epoll_create()創(chuàng)建一個epoll實例
int epfd = epoll_create(100);
// 將監(jiān)聽的文件描述符相關(guān)的檢測信息添加到epoll實例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
//ret代表的是發(fā)生改變的文件描述符的數(shù)量
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 監(jiān)聽的文件描述符有數(shù)據(jù)達(dá)到,有客戶端連接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有數(shù)據(jù)到達(dá),需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
第59行的if(epevs[i].events & EPOLLOUT)則是為了避免一種情況:當(dāng)我同時檢測文件描述符的讀和寫時,因為下面的代碼都是處理讀這種情況的,所以如果該文件描述符的epevs[i].events是寫的話,則continue,略過這個文件描述符的變動
問題:在最開始調(diào)用epoll_ctl把監(jiān)聽的文件描述符放進(jìn)紅黑樹的時候傳入了&epev,也就是epev的指針,為什么后面?zhèn)魅胄碌奈募枋龇臅r候可以重用這個epev呢,這樣重用epev的話前面?zhèn)魅氲谋O(jiān)聽描述符不就被改動了嘛?還是說其實調(diào)用這個函數(shù)傳入紅黑樹之后,epev里面的數(shù)據(jù)已經(jīng)被拷貝了?
答:當(dāng)調(diào)用epoll_ctl函數(shù)將文件描述符添加到epoll對象中時,epoll會將epoll_event結(jié)構(gòu)體中的數(shù)據(jù)拷貝一份,存儲在自己的內(nèi)存空間中,并將這個拷貝的結(jié)構(gòu)體作為一個節(jié)點插入到紅黑樹中。
這樣做的好處是,當(dāng)文件描述符上的事件發(fā)生時,epoll可以直接從自己的內(nèi)存空間中獲取相應(yīng)的事件信息,而不需要每次都去訪問用戶空間中的epoll_event結(jié)構(gòu)體。這樣可以提高效率,減少系統(tǒng)調(diào)用的次數(shù)。
epoll的兩種工作模式
- Level Triggered(LT)水平觸發(fā):LT (level - triggered)是缺?。ㄈ笔∫簿褪悄J(rèn)的意思)的工作方式,并且同時支持 block和no-block socket。在這種做法中,內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進(jìn)行IO操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你的。
假設(shè)委托內(nèi)核檢測讀事件>檢測fd的讀緩沖區(qū)
讀緩沖區(qū)有數(shù)據(jù)- > epoll檢測到了會給用戶通知
a.用戶不讀數(shù)據(jù),數(shù)據(jù)一直在緩沖區(qū),epoll會一直通知
b.用戶只讀了一部分?jǐn)?shù)據(jù),epoll會通知
c.緩沖區(qū)的數(shù)據(jù)讀完了
- Edge Triggred(ET) 邊緣觸發(fā):ET (edge - triggered)是高速工作方式,只支持no-block socket。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時,內(nèi)核通過epoll告訴你。然后它會假設(shè)你知道文件描述符已經(jīng)就緒,并且不會再為那個文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個文件描述符不再為就緒狀態(tài)了。但是請注意,如果一直不對這個fd作IO操作(從而導(dǎo)致它再次變成未就緒),內(nèi)核不會發(fā)送更多的通知(onlyonce)。
ET模式在很大程度上減少了epoll事件被重復(fù)觸發(fā)的次數(shù),因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務(wù)餓死。
假設(shè)委托內(nèi)核檢測讀事件->檢測fd的讀緩沖區(qū)
讀緩沖區(qū)有數(shù)據(jù)- > epoll檢測到了會給用戶通知
a.用戶不讀數(shù)據(jù),數(shù)據(jù)一致在緩沖區(qū)中,epoll下次檢測的時候就不通知了
b.用戶只讀了一部分?jǐn)?shù)據(jù),epoll不通知
c.緩沖區(qū)的數(shù)據(jù)讀完了,不通知
代碼舉例:
客戶端程序如下:
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 創(chuàng)建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 連接服務(wù)器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
// sprintf(sendBuf, "send data %d", num++);
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服務(wù)器已經(jīng)斷開連接...\n");
break;
}
}
close(fd);
return 0;
}
epoll水平觸發(fā)模式代碼如下:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 創(chuàng)建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監(jiān)聽
listen(lfd, 8);
// 調(diào)用epoll_create()創(chuàng)建一個epoll實例
int epfd = epoll_create(100);
// 將監(jiān)聽的文件描述符相關(guān)的檢測信息添加到epoll實例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 監(jiān)聽的文件描述符有數(shù)據(jù)達(dá)到,有客戶端連接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有數(shù)據(jù)到達(dá),需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
epoll邊沿觸發(fā)模式代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-702332.html
邊沿觸發(fā)模式的代碼與水平觸發(fā)模式的代碼是有所不同的,在前面的概念中,已經(jīng)了解到了邊沿觸發(fā)模式僅會通知一次文件描述符從未就緒變?yōu)榫途w(也就是有數(shù)據(jù)到了)。所以在邊沿觸發(fā)模式下,我們需要一次就讀完發(fā)送方所發(fā)送的所有內(nèi)容。如果沒有讀完的話,文件描述符的狀態(tài)仍會是就緒,而在下次循環(huán)中epoll不會再通知我們,那發(fā)送方所發(fā)送的剩余的我們未讀完的數(shù)據(jù)就丟失了。
(假設(shè)char buf[2],然后發(fā)送方向文件描述符fd中輸入lllhh,那么在LT模式下,首先epoll_wait會檢測到fd中的讀事件就緒。那么開始讀取,因為buf的容量為2,所以先讀取了ll,接著返回到了循環(huán)開頭的epoll_wait,因為沒讀完,該fd中的讀事件仍就處于就緒狀態(tài),再讀,直到讀完為止。而在ET模式下,先讀取了ll,接著返回到了循環(huán)開頭的epoll_wait,即使因為沒讀完該fd中的讀事件仍就處于就緒狀態(tài),但不予理睬。后面的數(shù)據(jù)也就相當(dāng)于丟失了。值得注意的是會丟失的前提是該發(fā)送方此后沒有再發(fā)送數(shù)據(jù),則剩余未讀的在緩沖區(qū)中的數(shù)據(jù)就會丟失。如果說發(fā)送方又一次數(shù)據(jù),比如發(fā)送了rr,那就會讀取出lh,rr仍舊在讀緩沖區(qū)不會被讀出)
所以說ET模式下要保證一次讀完。那么如何一次讀完呢?自然是需要while循環(huán),但又有個問題,當(dāng)讀完數(shù)據(jù)后,read讀不到數(shù)據(jù)了,但發(fā)送方又沒有斷開連接,這是read就會阻塞在這里,從而導(dǎo)致程序無法再繼續(xù)往下運行。所以我們要設(shè)置read函數(shù)不阻塞,其實也就是設(shè)置套接字非阻塞,用到了fcntl函數(shù)。
而通過把套接字設(shè)置為非阻塞從而使read非阻塞,這就又會導(dǎo)致一個問題,當(dāng)某次遍歷已經(jīng)把文件描述符緩沖區(qū)中的數(shù)據(jù)全部讀完之后,下次來讀,read不阻塞,但文件描述符中又沒有數(shù)據(jù),發(fā)送端連接未關(guān)閉,就會報一個EAGAIN的錯誤。也就是程序中第81行。這種情況下不應(yīng)該退出while循環(huán),所以這里用了一個if來判別。
在第74行printf沒辦法數(shù)據(jù)全部讀完后打印出over,所以將74行的printf改為75行的write,第75行是直接將buf的內(nèi)容寫入到標(biāo)準(zhǔn)輸出,將數(shù)據(jù)寫入到終端或命令行界面進(jìn)行顯示。第76行是將buf的內(nèi)容寫入到curfd套接字中,目的是為了完成回射,使客戶端發(fā)送過來的內(nèi)容再被客戶端讀取出來文章來源地址http://www.zghlxwxcb.cn/news/detail-702332.html
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main() {
// 創(chuàng)建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監(jiān)聽
listen(lfd, 8);
// 調(diào)用epoll_create()創(chuàng)建一個epoll實例
int epfd = epoll_create(100);
// 將監(jiān)聽的文件描述符相關(guān)的檢測信息添加到epoll實例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 監(jiān)聽的文件描述符有數(shù)據(jù)達(dá)到,有客戶端連接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 設(shè)置cfd屬性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
epev.events = EPOLLIN | EPOLLET; // 設(shè)置邊沿觸發(fā)
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 循環(huán)讀取出所有數(shù)據(jù)
char buf[5];
int len = 0;
while( (len = read(curfd, buf, sizeof(buf))) > 0) {
// 打印數(shù)據(jù)
// printf("recv data : %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(curfd, buf, len);
}
if(len == 0) {
printf("client closed....");
}else if(len == -1) {
if(errno == EAGAIN) {
write(STDOUT_FILENO, "over.\n", strlen("over.\n") + 1);
}else {
perror("read");
exit(-1);
}
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
到了這里,關(guān)于【Linux】C++項目實戰(zhàn)-高并發(fā)服務(wù)器詳析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!