C语言网络编程:多路IO select实现多客户端
文章目錄
- 阻塞式的服務(wù)器程序
- 多線程服務(wù)器程序
- 非阻塞式服務(wù)器程序
- 基于事件響應(yīng)的服務(wù)器程序
- 事件響應(yīng)服務(wù)器程序的實(shí)現(xiàn)`select`
阻塞式的服務(wù)器程序
我們接觸過最多的最基礎(chǔ)的網(wǎng)絡(luò)通信模型為TCP/UDP通信模型,以下為TCP通信模型的基本流程C語言網(wǎng)絡(luò)編程:TCP客戶端實(shí)現(xiàn)
但是以上過程中每個(gè)通信函數(shù)都是阻塞的,而且建立連接之后的數(shù)據(jù)接收發(fā)送同樣是阻塞形式的。send無法發(fā)送時(shí)只能繼續(xù)阻塞,recv接收不到同樣阻塞。這個(gè)過程整個(gè)進(jìn)程都是處于非常被動(dòng)的消耗大量CPU資源的等待過程。這為多客戶端以及多業(yè)務(wù)邏輯的網(wǎng)絡(luò)編程帶來了挑戰(zhàn)。
多線程服務(wù)器程序
此時(shí)很多人推出多線程,即服務(wù)器這里使用多線程方式為每一個(gè)客戶端創(chuàng)建一個(gè)獨(dú)立的連接,如C語言網(wǎng)絡(luò)編程:TCP實(shí)現(xiàn)多線程實(shí)現(xiàn)多客戶端 ,此時(shí)每個(gè)客戶端都能夠獨(dú)立和服務(wù)端進(jìn)行通信。這里不推薦使用多進(jìn)程的方式解決多客戶端以及多業(yè)務(wù)邏輯問題,因?yàn)檫M(jìn)程的開銷遠(yuǎn)大于線程,fork的方式基本是將父進(jìn)程所有的資源接管到子進(jìn)程,如果并發(fā)級(jí)較高,系統(tǒng)資源會(huì)消耗極大。
但是多線程同樣存在問題,每個(gè)客戶端的連接為一個(gè)線程,線程操作本就復(fù)雜,同時(shí)并發(fā)量較高時(shí)對(duì)服務(wù)器的CPU本身也是一種挑戰(zhàn)。
這個(gè)時(shí)候線程池技術(shù)應(yīng)運(yùn)而生,目的是為了降低多線程對(duì)系統(tǒng)CPU資源的開銷,維護(hù)指定數(shù)量的線程來處理連接,當(dāng)建立連接之后“池”內(nèi)指定個(gè)數(shù)的線程負(fù)責(zé)和客戶端通信,當(dāng)釋放連接或者指定時(shí)間內(nèi)為通信,則“池”接收下一輪客戶端連接。
這里的數(shù)據(jù)庫、tomcat、apache等服務(wù)器都有線程池的應(yīng)用。但是線程池本身的規(guī)模需要和服務(wù)器的連接規(guī)模匹配,如果小規(guī)模線程池負(fù)責(zé)大規(guī)模的服務(wù)器連接,這樣對(duì)系統(tǒng)性能反而有反作用。
非阻塞式服務(wù)器程序
我們可以通過對(duì)通信過程中文件描述符的設(shè)置,將其更改為非阻塞的文件描述符
fcntl(fd, F_SETFL, O_NONBLOCK);
如果對(duì)sockfd通信描述符設(shè)置非阻塞標(biāo)記,像我們通信過程中的發(fā)送接收函數(shù)運(yùn)行之后會(huì)立即返回,返回值有如下幾種情況
ret = recv(int sockfd, void *buf, size_t len, int flags)
- ret > 0,表示接受數(shù)據(jù)完畢,返回值即是接受到的字節(jié)數(shù);
- ret = 0,表示連接已經(jīng)正常斷開;
- ret = -1,且 errno 等于 EAGAIN,表示 recv 操作還沒執(zhí)行完成;
- ret = -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系統(tǒng)錯(cuò)誤 errno。
此時(shí)服務(wù)器可以循環(huán)調(diào)用recv函數(shù)去接收數(shù)據(jù),但是recv本身也是系統(tǒng)調(diào)用,如果循環(huán)調(diào)用,同樣會(huì)產(chǎn)生較大的系統(tǒng)開銷。
此時(shí)操作系統(tǒng)同樣提供了更優(yōu)的選擇,select進(jìn)行非阻塞通信的管理
基于事件響應(yīng)的服務(wù)器程序
select 多路io管理的接口基本被所有的unix/linux系統(tǒng)支持,主要接口如下:
#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() 接口可以同時(shí)對(duì)多個(gè)句柄進(jìn)行讀狀態(tài)、寫狀態(tài)和錯(cuò)誤狀態(tài)的探測(cè),所以可以很容易構(gòu)建為多個(gè)客戶端提供獨(dú)立問答服務(wù)的服務(wù)器系統(tǒng)。select最關(guān)鍵的地方是如何動(dòng)態(tài)維護(hù) select() 的三個(gè)參數(shù) readfds、writefds 和 exceptfds。作為輸入?yún)?shù),readfds 應(yīng)該標(biāo)記所有的需要探測(cè)的“可讀事件”的句柄,其中永遠(yuǎn)包括那個(gè)探測(cè) connect() 的那個(gè)“母”句柄;同時(shí),writefds 和 exceptfds 應(yīng)該標(biāo)記所有需要探測(cè)的“可寫事件”和“錯(cuò)誤事件”的句柄 ( 使用 FD_SET() 標(biāo)記 )
比如客戶端的connect操作會(huì)激發(fā)select的一個(gè)“可讀事件”,同時(shí)將對(duì)應(yīng)通信的文件句柄加入到對(duì)應(yīng)可讀事件的FD_SET之中,捕捉到“可讀事件”之后從FD_SET中取出指定的文件句柄即可讀。recv以后需將對(duì)應(yīng)的句柄值加入writefds中,然后繼續(xù)探測(cè)下次的“可寫事件”,同樣,如果 select() 發(fā)現(xiàn)某句柄捕捉到“可寫事件”,則程序應(yīng)及時(shí)做 send() 操作,并準(zhǔn)備好下一次的“可讀事件”探測(cè)準(zhǔn)備。以上過程為一個(gè)select循環(huán),同時(shí)我們可以操作僅僅檢測(cè)可寫或者可讀事件。
使用 select() 的事件驅(qū)動(dòng)模型只用單線程(進(jìn)程)執(zhí)行,占用資源少,不消耗太多 CPU,同時(shí)能夠?yàn)槎嗫蛻舳颂峁┓?wù).
但是如果select模型的循環(huán)體中有較多的操作,則會(huì)導(dǎo)致select事件探測(cè)以及響應(yīng)產(chǎn)生巨大的延時(shí),這將會(huì)是災(zāi)難性的
不過我們當(dāng)前系統(tǒng)提供了很多信號(hào)響應(yīng)以及事件響應(yīng)的庫來供大家使用。signal以及sigaction的信號(hào)處理機(jī)制,以及libevent事件驅(qū)動(dòng)庫;通過這一些庫以及信號(hào)處理函數(shù)我們能夠極大得提升操作系統(tǒng)的響應(yīng)速度,從而避免以上出現(xiàn)的事件響應(yīng)和執(zhí)行出現(xiàn)較大延時(shí)的情況。
事件響應(yīng)服務(wù)器程序的實(shí)現(xiàn)select
這里僅僅使用select實(shí)現(xiàn)服務(wù)端程序,針對(duì)select中的讀事件驅(qū)動(dòng)進(jìn)行周期輪詢。
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 輪詢失敗,接口異常,并設(shè)置errno2. 0 并未檢測(cè)到文件句柄有數(shù)據(jù)3. > 0 檢測(cè)到部分文件句柄有數(shù)據(jù),執(zhí)行獲取操作此時(shí)設(shè)置的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); }
}/*創(chuàng)建連接線程,負(fù)責(zé)接收來自客戶端的連接,并將句柄加入到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和端口號(hào),并將客戶端的句柄加入管理數(shù)組*/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);}
}/*發(fā)送消息線程*/
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;//創(chuàng)建TCP協(xié)議族的面向連接可靠的字節(jié)流,socket文件描述符skfd = socket(AF_INET, SOCK_STREAM, 0);if ( -1 == skfd) {print_err("socket failed",__LINE__,errno);}addr.sin_family = AF_INET; //設(shè)置tcp協(xié)議族addr.sin_port = htons(PORT); //設(shè)置端口號(hào)addr.sin_addr.s_addr = inet_addr(IP); //設(shè)置ip地址/*創(chuàng)建bind,綁定本服務(wù)器的ip和端口號(hào)*/g_ret = bind(skfd, (struct sockaddr*)&addr, sizeof(addr));if ( -1 == g_ret) {print_err("bind failed",__LINE__,errno);}/*將主動(dòng)描述符skfd轉(zhuǎn)為被動(dòng)描述符*/g_ret = listen(skfd, 3);if ( -1 == g_ret ) {print_err("listen failed", __LINE__, errno);}len = sizeof(addr);/*創(chuàng)建三個(gè)線程,用于創(chuàng)建連接,發(fā)送消息,接收消息*/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;
}
客戶端程序這里使用的是標(biāo)準(zhǔn)的tcp阻塞式客戶端程序,可以有多個(gè)這樣的客戶端
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; //設(shè)置tcp協(xié)議族addr.sin_port = htons(PORT); //設(shè)置端口號(hào)addr.sin_addr.s_addr = inet_addr(IP); //設(shè)置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
運(yùn)行如下:
先運(yùn)行server,再運(yùn)行client
服務(wù)端輸出如下:
客戶端輸出如下:
總結(jié)
以上是生活随笔為你收集整理的C语言网络编程:多路IO select实现多客户端的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 结扎还能人工授精吗
- 下一篇: 记一次shell脚本推后台stopped