Epoll并發(fā)聊天服務(wù)器的實現(xiàn)
一、相關(guān)知識
1.實現(xiàn)并發(fā)通信的三種方式
? 實現(xiàn)并發(fā)通信主要有三種方式:多進程服務(wù)器、多路復(fù)用服務(wù)器(I/O復(fù)用)、多線程服務(wù)器
- 多進程服務(wù)器
? 多進程服務(wù)器指的是利用不同進程處理來自不同客戶端發(fā)來的連接請求,進程之間以輪轉(zhuǎn)的方式運行,由于各個進程之間輪轉(zhuǎn)運行的時間間隔很小,故在用戶看來其實現(xiàn)了并行處理所有的客戶請求。
? 多進程服務(wù)器主要使用fork()函數(shù)進行創(chuàng)建子進程,將主進程和子進程隔離開來對各個客戶端的請求進行分別響應(yīng),fork()函數(shù)的原型為:
#include<unisted.h>
pid_t fork(void);
//成功時返回進程ID,失敗時返回-1
fork()函數(shù)將創(chuàng)建調(diào)用的進程副本,也就是說,并非根據(jù)完全不同的程序創(chuàng)建進程,而是復(fù)制正在運行的、調(diào)用fork函數(shù)的進程
? 在子進程完成其任務(wù)之后,若程序員沒有對其進行銷毀,那么該子進程會變成僵尸進程(Zombie),若父進程未主動要求獲得子進程的結(jié)束狀態(tài)值,操作系統(tǒng)將一直保存,并讓子進程長時間處于僵尸進程狀態(tài)。我們可以使用wait()函數(shù)銷毀僵尸進程:
#include<sys/wait.h>
pid_t wait(int * statloc);
//成功時返回終止的子進程ID,失敗時返回-1
? 調(diào)用此函數(shù)時如果已有子進程終止,那么子進程終止時傳遞的返回值(exit函數(shù)的參數(shù)值、main函數(shù)的return返回值)將保存到該函數(shù)的參數(shù)所指內(nèi)存空間。
? 但函數(shù)參數(shù)指向的單元中還包含其他信息,因此需要通過下列宏進行分離:
- WIFEXITED子進程正常終止時返回“真”
- WEXITSTATUS返回子進程的返回值
int status;
pid_t pid=fork();
.....
if(pid == 0)return 3;
wait(&status);
if(WIFEXITED(status))
//輸出3
printf("Child send: %d \n",WEXITSTATUS(status));
調(diào)用wait函數(shù)時,如果沒有已終止的子進程,那么程序?qū)?strong>阻塞直到有子進程終止,因此需謹慎調(diào)用該函數(shù)!
? 相較于wait()銷毀僵尸進程,waitpid()是更好的函數(shù),并且使用相關(guān)信號(signal()函數(shù))可以在子進程調(diào)用完成后自動銷毀,但是多進程服務(wù)器并不是本文的重點,故不再贅述。
- 多路復(fù)用服務(wù)器
? 下面用兩張圖簡單介紹多進程服務(wù)器與多路復(fù)用服務(wù)器的區(qū)別
? IO多路復(fù)用相對于阻塞式和非阻塞式的好處就是它可以監(jiān)聽多個 socket ,并且不會消耗過多資源。select函數(shù)是實現(xiàn)I/O多路復(fù)用的最簡單也是最重要的函數(shù)。
? 當(dāng)用戶進程調(diào)用 select 時,它會監(jiān)聽其中所有 socket() 直到有一個或多個 socket 數(shù)據(jù)已經(jīng)準(zhǔn)備好,否則就一直處于阻塞狀態(tài)。select()的缺點在于單個進程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,select()所維護的存儲大量文件描述符的數(shù)據(jù)結(jié)構(gòu),隨著文件描述符數(shù)量的增大,其復(fù)制的的開銷也線性增長。同時,由于網(wǎng)絡(luò)響應(yīng)時間的延遲使得大量的tcp鏈接處于非常活躍狀態(tài),但調(diào)用select()會對所有的socket進行一次線性掃描,所以這也浪費了一定的開銷。
? select()函數(shù)的參數(shù)有些復(fù)雜,下面使用注釋對select()的各個參數(shù)進行詳細的解釋
#include<sys/select.h>
#include<sys/time.h>
int select(
int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout);
//maxfd:監(jiān)視對象文件描述符數(shù)量
//readset:將所有關(guān)注”是否存在待讀取數(shù)據(jù)“的文件描述符注冊到fd_set型變量,并傳遞其地址值
//writeset:將所有關(guān)注”是否可傳輸無阻塞數(shù)據(jù)“的文件描述符注冊到fd_set型變量,并傳遞其地址值
//exceptset:將所有關(guān)注”是否發(fā)生異?!暗奈募枋龇缘絝d_set型變量,并傳遞其地址值
//timeout:調(diào)用select函數(shù)后,為防止陷入無限循環(huán)的阻塞,傳遞超時信息
//返回值:發(fā)生錯誤時返回-1,超時返回時返回0,因發(fā)生關(guān)注的事件返回時,返回大于0的值,該值是發(fā)生事件的文件描述符數(shù)
? select函數(shù)使用具體流程:
- 定義select需要的各種參數(shù),例如fd_max、timeout、所需監(jiān)聽的套接字列表等。
- 使用select函數(shù),并對其返回值進行判斷。
- 若返回值為0,那么繼續(xù)循環(huán)使用select監(jiān)視套接字列表;若大于0,使用FD_ISSET確認是哪一個列表發(fā)生了變化,之后對select函數(shù)的返回值(即發(fā)生變化的套接字)進行操作。
? select不合理的兩個缺點:
- 調(diào)用后常見的針對所有文件描述符的循環(huán)語句。
- 每次調(diào)用函數(shù)時都需要向該函數(shù)傳遞監(jiān)視對象信息。
? 調(diào)用select函數(shù)后,不是把發(fā)生變化的文件描述符單獨集中到一起,而是通過觀察作為監(jiān)視對象的fd_set變量的變化,找出發(fā)生變化的文件描述符,因此無法避免針對所有監(jiān)視對象的循環(huán)語句。
? 向系統(tǒng)傳遞監(jiān)視對象信息是select函數(shù)的瓶頸,因為這種應(yīng)用程序與系統(tǒng)層面的交互將對程序造成很大負擔(dān)。
? 由于select函數(shù)是針對套接字的處理,而套接字是由操作系統(tǒng)管理的,故無法繞開應(yīng)用程序與操作系統(tǒng)之間的交互,那么可行的優(yōu)化方法是,僅向操作系統(tǒng)傳遞1次監(jiān)視對象,監(jiān)視范圍或內(nèi)容發(fā)生變化時只通知發(fā)生變化的事項。
? epoll()函數(shù)解決了select()函數(shù)在處理上的瓶頸,其優(yōu)點為:
- 無需編寫以監(jiān)視狀態(tài)變化為目的的針對所有文件描述符的循環(huán)語句。
- 調(diào)用對應(yīng)于select函數(shù)的epoll_wait函數(shù)時無需每次傳遞監(jiān)視對象信息。
? epoll實現(xiàn)需要的三個函數(shù)
- epoll_create:創(chuàng)建保存epoll文件描述符的空間,取代了select中自己創(chuàng)建fd_set的操作,由操作系統(tǒng)負責(zé)保存監(jiān)視對象文件描述符
- epoll_ctl:向空間注冊并注銷文件描述符
- epoll_wait:與select函數(shù)類似,等待文件描述符發(fā)生變化
? epoll方式通過epoll_event結(jié)構(gòu)體將發(fā)生變化的文件描述符單獨集中到一起
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;
#include<sys/epoll.h>
int epoll_create(int size);
//成功時返回epoll文件描述符,失敗時返回-1
//調(diào)用epoll_create時創(chuàng)建的文件描述符保存空間稱為”epoll例程”,通過參數(shù)size傳遞的值決定epoll例程的大小,但該值僅供操作系統(tǒng)參考
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event);
//成功時返回0,失敗時返回-1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//成功時返回發(fā)生事件的文件描述符數(shù),失敗時返回-1
//該函數(shù)的調(diào)用方式如下:
int event_cnt;
struct epoll_event * ep_events;
.....
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);//EPOLL_SIZE是宏常量
.....
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
//調(diào)用函數(shù)后,返回發(fā)生事件的文件描述符數(shù),同時在第二個參數(shù)指向的緩沖中保存發(fā)生事件的文件描述符集合,因此無需像select那樣插入針對所有文件描述符的循環(huán)
? epoll()有兩種不同的觸發(fā)方式,分別是條件觸發(fā)和邊緣觸發(fā)
- 條件觸發(fā):只要輸入緩沖有數(shù)據(jù)就會一直通知該事件,即多次注冊
- 邊緣觸發(fā):輸入緩沖收到數(shù)據(jù)時僅注冊1次該事件,即使輸入緩沖中還留有數(shù)據(jù),也不會再進行注冊
? epoll默認使用條件觸發(fā),即在有限的輸入緩沖下,多個客戶端對其進行連接那么會觸發(fā)多次wait函數(shù)。
將注冊客戶端套接字中的
event.events = EPOLLIN
改為
event.events = EPOLLIN | EPOLLET
就可以實現(xiàn)epoll的邊緣觸發(fā),即從客戶端接收數(shù)據(jù)時,僅注冊1次事件,但是需要進行額外的處理,例如將其變?yōu)榉亲枞J健?/p>
- 多線程服務(wù)器
? 多進程服務(wù)器的瓶頸在于在切換進程時涉及到內(nèi)核態(tài)和用戶態(tài)的切換以及上下文的保存,這種操作在大量的切換過程中十分占用資源,而利用線程可以很好地避開進程切換的瓶頸,多線程的優(yōu)點為:
- 線程的創(chuàng)建和上下文切換比進程的創(chuàng)建和上下文切換更快。
- 線程間交換數(shù)據(jù)時無需特殊技術(shù)。
? 多線程服務(wù)器的編寫涉及到線程庫(pthread.h)以及信號量的互斥同步等操作,在實現(xiàn)起來相對復(fù)雜,這里不在贅述。
2 Epoll的兩種觸發(fā)方式
? epoll()函數(shù)具有兩種觸發(fā)模式:邊緣觸發(fā)(ET, Edge Triggered)和條件觸發(fā)(LT, Level Triggered),這兩種模式定義了epoll()函數(shù)如何響應(yīng)文件描述符的就緒事件。
-
條件觸發(fā)(Level Triggered, LT):
- 在此模式下,只要文件描述符處于就緒狀態(tài)(例如,有數(shù)據(jù)可讀、可寫),
epoll_wait
就會通知這個事件。 - 它更容易理解和使用,因為只要條件滿足,事件就會一直被報告。
- 但是,這可能導(dǎo)致效率問題,尤其是在高負載時。如果應(yīng)用程序沒有讀取所有可用數(shù)據(jù),下次調(diào)用
epoll_wait
時,它仍然會報告相同的文件描述符,可能導(dǎo)致多余的處理。
- 在此模式下,只要文件描述符處于就緒狀態(tài)(例如,有數(shù)據(jù)可讀、可寫),
-
邊緣觸發(fā)(Edge Triggered, ET):
- 在邊緣觸發(fā)模式下,只有文件描述符狀態(tài)發(fā)生變化時(例如,從非就緒變?yōu)榫途w),
epoll_wait
才會通知事件。 - 這種模式對于提高效率非常有用,因為它減少了事件的重復(fù)報告。
- 但是,它也更難正確地使用。應(yīng)用程序必須確保每次都處理所有的可用數(shù)據(jù),因為新的數(shù)據(jù)到來不會再次觸發(fā)事件,除非文件描述符的狀態(tài)再次改變。
- 在邊緣觸發(fā)模式下,只有文件描述符狀態(tài)發(fā)生變化時(例如,從非就緒變?yōu)榫途w),
? 上面的描述可能比較晦澀,用一個簡單的例子來講解兩種觸發(fā)模式的特點:
? 兒子:“媽媽,我收到了500元壓歲錢?!?/em>
? 媽媽:“嗯,真棒!“
? 兒子:”我給隔壁小王買了烤鴨,花了200元?!?/em>
? 媽媽:“嗯,做的好!”
? 兒子:“媽媽,我還買了玩具,剩下50元?!?/em>
? 媽媽:”嗯,你可以自己分配!“
? 從上述對話中可以看出,兒子從收到壓歲錢開始一直向媽媽報告,這就時條件觸發(fā)的原理。如果將兒子比作輸入緩沖,壓歲錢比作輸入數(shù)據(jù),兒子的報告比作事件,可以將條件觸發(fā)簡單地描述為:
條件觸發(fā)方式中,只要輸入緩沖中有數(shù)據(jù)就會一直通知該事件。
? 例如,服務(wù)器端輸入緩沖中收到50字節(jié)的數(shù)據(jù)時,服務(wù)器端操作系統(tǒng)將通知該事件(注冊到發(fā)生變化的文件描述符)。但服務(wù)器端讀取20字節(jié)后還剩30字節(jié)的情況下,仍會注冊事件。
? 下面再看一個例子:
? 兒子:”媽媽,我收到了500元壓歲錢?!?/em>
? 媽媽:“嗯,再接再厲?!?/em>
? 兒子:“。。。。。。”
? 媽媽:“說話呀!壓歲錢呢?”
? 上面的對話就反映了邊緣觸發(fā)的工作模式,即輸入緩沖收到數(shù)據(jù)時僅注冊1次該事件,即使輸入緩沖中還留有數(shù)據(jù),也不會再進行注冊。
? epoll默認以條件觸發(fā)方式工作,select也同樣是以該方式工作的,這兩種觸發(fā)方式各有利弊,使用哪種模式取決于特定的應(yīng)用場景和對性能的需求。邊緣觸發(fā)通常用于高性能服務(wù)器,因為它可以減少系統(tǒng)調(diào)用次數(shù),而條件觸發(fā)則適用于簡單應(yīng)用或者對性能要求不高的場景。
3 線程的使用及線程同步
? 利用線程的高并發(fā)特點使客戶端的I/O操作分離,這樣可以使整個代碼更加清晰,并且能夠提高效率。
? 線程具有單獨的執(zhí)行流,因此需要單獨定義線程的main函數(shù),還需要請求操作系統(tǒng)在單獨的執(zhí)行流中執(zhí)行該函數(shù),完成該功能的函數(shù)如下:
#include<pthread.h>
int pthread_create(pthread_t * restrict thread, const pthread_attr_t * restrict attr, void * (* start_routine)(void *), void * restrict arg);
//成功時返回0,失敗時返回其他值
//thread:保存新創(chuàng)建進程ID的變量地址值
//attr:用于傳遞線程屬性的參數(shù),傳遞NULL時,創(chuàng)建默認屬性的線程
//start_routine:相當(dāng)于線程main函數(shù)的、在單獨執(zhí)行流中執(zhí)行的函數(shù)地址值
//arg:通過第三個參數(shù)傳遞調(diào)用函數(shù)時包含傳遞參數(shù)信息的變量地址值
? 線程相關(guān)代碼在編譯時需要添加-lpthread選項聲明需要連接線程庫,只有這樣才能調(diào)用頭文件pthread.h中聲明的函數(shù)
? 由于線程的執(zhí)行是非阻塞的,所以如果不使用相關(guān)函數(shù)控制線程的執(zhí)行流,必須使用sleep函數(shù)阻塞進程,讓進程等待所有線程都返回之后在返回!
? 下面介紹控制線程執(zhí)行流的函數(shù):
#include<pthread.h>
int pthread_join(pthread_t thread, void ** status);
//成功時返回0,失敗時返回其他值
//ptread:該參數(shù)值ID的線程終止后才會從該函數(shù)返回
//status:保存線程的main函數(shù)返回值的指針變量地址值
? 該函數(shù)可以阻塞進程的執(zhí)行。
? 當(dāng)多個線程同時運行時,就會產(chǎn)生臨界區(qū)的問題。
? 線程安全函數(shù)被多個線程同時調(diào)用時也不會引發(fā)問題,而非線程安全函數(shù)被同時調(diào)用時會引發(fā)問題。
? 在編譯時添加-D_REENTRANT宏使函數(shù)庫中的非線程安全函數(shù)變?yōu)槎x好的線程安全函數(shù)。
? 線程同步是在編寫有關(guān)線程程序時的核心問題,因為線程的調(diào)用順序是隨機的,并且同一個進程的線程共用一個棧區(qū),所以共享變量的使用需要格外謹慎,修改共享變量的代碼我們稱之為臨界區(qū)。對于臨界區(qū)的訪問需要通過同步和互斥來進行保護:
-
同步:需要指定訪問同一內(nèi)存空間的線程執(zhí)行順序的情況
-
互斥:同時訪問同一內(nèi)存空間時發(fā)生的情況
? 互斥的實質(zhì)就是上鎖機制,其依賴互斥量來實現(xiàn),下面介紹互斥量的創(chuàng)建及銷毀函數(shù):
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);
//成功時返回0,失敗時返回其他值
//mutex:創(chuàng)建互斥量時傳遞保存互斥量的變量地址值,銷毀時傳遞需要銷毀的互斥量地址值
//attr:傳遞即將創(chuàng)建的互斥量屬性,沒有特別需要指定的屬性時傳遞NULL
? 接下來介紹利用互斥量鎖住或釋放臨界區(qū)時使用的函數(shù):
int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
//成功時返回0,失敗時返回其他值
? 函數(shù)用法:
pthread_mutex_lock(&mutex);
//臨界區(qū)的開始
//....
//臨界區(qū)的結(jié)束
pthread_mutex_unlock(&mutex);
? 為了提高效率,我們應(yīng)該最大限度減少互斥量lock、unlock函數(shù)的調(diào)用次數(shù)。
? 同步需要使用信號量(semaphore)來完成,下面介紹信號量創(chuàng)建及銷毀的方法。
#include<semaphore.h>
int sem_init(sem_t * sem, int pshared, unsigned int value);
int sem_destroy(sem_t * sem);
//成功時返回0,失敗時返回其他值
//sem:傳遞信號量時傳遞保存信號量的變量地址,銷毀時傳遞需要銷毀的信號變量地址值
//pshared:傳遞其他值時,創(chuàng)建可由多個進程共享的信號量;傳遞0時,創(chuàng)建只允許1個進程內(nèi)部使用的信號量
//value:指定新創(chuàng)建的信號量初始值
? 信號量中相當(dāng)于互斥量lock、unlock的函數(shù),其中post讓信號量增加1,wait讓信號量減少1。
int sem_post(sem_t * sem);
int sem_wait(sem_t * sem);
//成功時返0,失敗時返回其他值
二、代碼實現(xiàn)
? 使用epoll實現(xiàn)聊天服務(wù)器的代碼不是很復(fù)雜,因為最核心的功能——數(shù)據(jù)的收發(fā)已經(jīng)被epoll函數(shù)很好地實現(xiàn)了,我們所要做的就是調(diào)用該函數(shù)并且在此基礎(chǔ)上添加一些簡單的功能。
chat_clnt.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define BUF_SIZE 100
#define NAME_SIZE 20
void *send_msg(void *arg);
void *recv_msg(void *arg);
void error_handling(char *msg);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr;
pthread_t snd_thread, rcv_thread;
void *thread_return;
if (argc != 4)
{
printf("Usage : %s <IP> <port> <name>\n", argv[0]);
exit(1);
}
sprintf(name, "[%s]", argv[3]);
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error");
pthread_create(&snd_thread, NULL, send_msg, (void *)&sock);
pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock);
pthread_join(snd_thread, &thread_return);
pthread_join(rcv_thread, &thread_return);
close(sock);
return 0;
}
void *send_msg(void *arg) // send thread main
{
int sock = *((int *)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
while (1)
{
fgets(msg, BUF_SIZE, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
{
close(sock);
exit(0);
}
sprintf(name_msg, "%s %s", name, msg);
write(sock, name_msg, strlen(name_msg));
}
return NULL;
}
void *recv_msg(void *arg) // read thread main
{
int sock = *((int *)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
int str_len;
while (1)
{
str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
if (str_len == -1)
return (void *)-1;
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}
void error_handling(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
clnt_serv.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_CLNT 256
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i, j;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
int clnt_socks[MAX_CLNT];
int clnt_cnt = 0;
pthread_mutex_t mutex;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
setnonblockingmode(serv_sock);
event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while (1)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1)
{
puts("epoll_wait() error");
break;
}
// puts("return epoll_wait");
for (i = 0; i < event_cnt; i++)
{
if (ep_events[i].data.fd == serv_sock)//client comes!
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
setnonblockingmode(clnt_sock);
event.events = EPOLLIN | EPOLLET;
event.data.fd = clnt_sock;
pthread_mutex_lock(&mutex);
clnt_socks[clnt_cnt++] = clnt_sock;
pthread_mutex_unlock(&mutex);
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}
else
{
while (1)
{
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if (str_len == 0) // close request!
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
pthread_mutex_lock(&mutex);
for (j = 0; j < clnt_cnt; j++)
{
if (ep_events[i].data.fd == clnt_socks[j])
{
while (j < clnt_cnt - 1)
{
clnt_socks[j] = clnt_socks[j + 1];
j++;
}
break;
}
}
clnt_cnt--;
pthread_mutex_unlock(&mutex);
printf("closed client: %d \n", ep_events[i].data.fd);
break;
}
else if (str_len < 0)
{
if (errno == EAGAIN)
break;
}
else
{
pthread_mutex_lock(&mutex);
for (j = 0; j < clnt_cnt; j++)
{
if (clnt_socks[j] != ep_events[i].data.fd)
write(clnt_socks[j], buf, str_len);
}
pthread_mutex_unlock(&mutex);
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void setnonblockingmode(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
Makefile:
CC = gcc
CFLAGS = -Wall -pthread
TARGET_SERVER = chat_serv
TARGET_CLIENT = chat_clnt
all: $(TARGET_SERVER) $(TARGET_CLIENT)
$(TARGET_SERVER): chat_serv.o
$(CC) $(CFLAGS) -o $(TARGET_SERVER) chat_serv.o
$(TARGET_CLIENT): chat_clnt.o
$(CC) $(CFLAGS) -o $(TARGET_CLIENT) chat_clnt.o
chat_serv.o: chat_serv.c
$(CC) $(CFLAGS) -c chat_serv.c
chat_clnt.o: chat_clnt.c
$(CC) $(CFLAGS) -c chat_clnt.c
clean:
rm -f *.o $(TARGET_SERVER) $(TARGET_CLIENT)
三、運行結(jié)果展示
文章來源:http://www.zghlxwxcb.cn/news/detail-773867.html
當(dāng)有客戶接入聊天室的時候會服務(wù)器會顯示其占用的文件描述符;每一個客戶在發(fā)送消息是會在其發(fā)送的消息前附加上其昵稱,這樣更容易識別是哪一個客戶發(fā)來的消息。文章來源地址http://www.zghlxwxcb.cn/news/detail-773867.html
到了這里,關(guān)于epoll并發(fā)服務(wù)器的實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!