日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

C语言网络编程:多路IO select实现多客户端

發布時間:2023/11/27 生活经验 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C语言网络编程:多路IO select实现多客户端 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

          • 阻塞式的服務器程序
          • 多線程服務器程序
          • 非阻塞式服務器程序
          • 基于事件響應的服務器程序
          • 事件響應服務器程序的實現`select`

阻塞式的服務器程序

我們接觸過最多的最基礎的網絡通信模型為TCP/UDP通信模型,以下為TCP通信模型的基本流程C語言網絡編程:TCP客戶端實現

但是以上過程中每個通信函數都是阻塞的,而且建立連接之后的數據接收發送同樣是阻塞形式的。send無法發送時只能繼續阻塞,recv接收不到同樣阻塞。這個過程整個進程都是處于非常被動的消耗大量CPU資源的等待過程。這為多客戶端以及多業務邏輯的網絡編程帶來了挑戰。

多線程服務器程序

此時很多人推出多線程,即服務器這里使用多線程方式為每一個客戶端創建一個獨立的連接,如C語言網絡編程:TCP實現多線程實現多客戶端 ,此時每個客戶端都能夠獨立和服務端進行通信。這里不推薦使用多進程的方式解決多客戶端以及多業務邏輯問題,因為進程的開銷遠大于線程,fork的方式基本是將父進程所有的資源接管到子進程,如果并發級較高,系統資源會消耗極大。

但是多線程同樣存在問題,每個客戶端的連接為一個線程,線程操作本就復雜,同時并發量較高時對服務器的CPU本身也是一種挑戰。

這個時候線程池技術應運而生,目的是為了降低多線程對系統CPU資源的開銷,維護指定數量的線程來處理連接,當建立連接之后“池”內指定個數的線程負責和客戶端通信,當釋放連接或者指定時間內為通信,則“池”接收下一輪客戶端連接。

這里的數據庫、tomcat、apache等服務器都有線程池的應用。但是線程池本身的規模需要和服務器的連接規模匹配,如果小規模線程池負責大規模的服務器連接,這樣對系統性能反而有反作用。

非阻塞式服務器程序

我們可以通過對通信過程中文件描述符的設置,將其更改為非阻塞的文件描述符
fcntl(fd, F_SETFL, O_NONBLOCK);
如果對sockfd通信描述符設置非阻塞標記,像我們通信過程中的發送接收函數運行之后會立即返回,返回值有如下幾種情況
ret = recv(int sockfd, void *buf, size_t len, int flags)

  • ret > 0,表示接受數據完畢,返回值即是接受到的字節數;
  • ret = 0,表示連接已經正常斷開;
  • ret = -1,且 errno 等于 EAGAIN,表示 recv 操作還沒執行完成;
  • ret = -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系統錯誤 errno。

此時服務器可以循環調用recv函數去接收數據,但是recv本身也是系統調用,如果循環調用,同樣會產生較大的系統開銷。
此時操作系統同樣提供了更優的選擇,select進行非阻塞通信的管理

基于事件響應的服務器程序

select 多路io管理的接口基本被所有的unix/linux系統支持,主要接口如下:

#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() 接口可以同時對多個句柄進行讀狀態、寫狀態和錯誤狀態的探測,所以可以很容易構建為多個客戶端提供獨立問答服務的服務器系統。select最關鍵的地方是如何動態維護 select() 的三個參數 readfds、writefds 和 exceptfds。作為輸入參數,readfds 應該標記所有的需要探測的“可讀事件”的句柄,其中永遠包括那個探測 connect() 的那個“母”句柄;同時,writefds 和 exceptfds 應該標記所有需要探測的“可寫事件”和“錯誤事件”的句柄 ( 使用 FD_SET() 標記 )

比如客戶端的connect操作會激發select的一個“可讀事件”,同時將對應通信的文件句柄加入到對應可讀事件的FD_SET之中,捕捉到“可讀事件”之后從FD_SET中取出指定的文件句柄即可讀。recv以后需將對應的句柄值加入writefds中,然后繼續探測下次的“可寫事件”,同樣,如果 select() 發現某句柄捕捉到“可寫事件”,則程序應及時做 send() 操作,并準備好下一次的“可讀事件”探測準備。以上過程為一個select循環,同時我們可以操作僅僅檢測可寫或者可讀事件。

使用 select() 的事件驅動模型只用單線程(進程)執行,占用資源少,不消耗太多 CPU,同時能夠為多客戶端提供服務.

但是如果select模型的循環體中有較多的操作,則會導致select事件探測以及響應產生巨大的延時,這將會是災難性的

不過我們當前系統提供了很多信號響應以及事件響應的庫來供大家使用。signal以及sigaction的信號處理機制,以及libevent事件驅動庫;通過這一些庫以及信號處理函數我們能夠極大得提升操作系統的響應速度,從而避免以上出現的事件響應和執行出現較大延時的情況。

事件響應服務器程序的實現select

這里僅僅使用select實現服務端程序,針對select中的讀事件驅動進行周期輪詢。
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 輪詢失敗,接口異常,并設置errno2. 0 并未檢測到文件句柄有數據3. > 0 檢測到部分文件句柄有數據,執行獲取操作此時設置的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);		}
}/*創建連接線程,負責接收來自客戶端的連接,并將句柄加入到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和端口號,并將客戶端的句柄加入管理數組*/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);}
}/*發送消息線程*/
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;//創建TCP協議族的面向連接可靠的字節流,socket文件描述符skfd = socket(AF_INET, SOCK_STREAM, 0);if ( -1 == skfd) {print_err("socket failed",__LINE__,errno);}addr.sin_family = AF_INET; //設置tcp協議族addr.sin_port = htons(PORT); //設置端口號addr.sin_addr.s_addr = inet_addr(IP); //設置ip地址/*創建bind,綁定本服務器的ip和端口號*/g_ret = bind(skfd, (struct sockaddr*)&addr, sizeof(addr));if ( -1 == g_ret) {print_err("bind failed",__LINE__,errno);}/*將主動描述符skfd轉為被動描述符*/g_ret = listen(skfd, 3);if ( -1 == g_ret ) {print_err("listen failed", __LINE__, errno);}len = sizeof(addr);/*創建三個線程,用于創建連接,發送消息,接收消息*/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;
}

客戶端程序這里使用的是標準的tcp阻塞式客戶端程序,可以有多個這樣的客戶端
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; //設置tcp協議族addr.sin_port = htons(PORT); //設置端口號addr.sin_addr.s_addr = inet_addr(IP); //設置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
運行如下:
先運行server,再運行client
服務端輸出如下:

客戶端輸出如下:

總結

以上是生活随笔為你收集整理的C语言网络编程:多路IO select实现多客户端的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。