1. 程式人生 > >Linux 網路程式設計——併發伺服器的三種經典實現模型

Linux 網路程式設計——併發伺服器的三種經典實現模型

伺服器設計技術有很多,按使用的協議來分有 TCP 伺服器和 UDP 伺服器,按處理方式來分有迴圈伺服器併發伺服器

迴圈伺服器與併發伺服器模型

在網路程式裡面,一般來說都是許多客戶對應一個伺服器(多對一),為了處理客戶的請求,對服務端的程式就提出了特殊的要求。

目前最常用的伺服器模型有:

·迴圈伺服器:伺服器在同一時刻只能響應一個客戶端的請求

·併發伺服器:伺服器在同一時刻可以響應多個客戶端的請求

UDP 迴圈伺服器的實現方法

UDP 迴圈伺服器每次從套接字上讀取一個客戶端的請求 -> 處理 -> 然後將結果返回給客戶機。

因為 UDP 是

非面向連線的,沒有一個客戶端可以老是佔住服務端。只要處理過程不是死迴圈,或者耗時不是很長,伺服器對於每一個客戶機的請求在某種程度上來說是能夠滿足。

UDP 迴圈伺服器模型為

  1. socket(...); // 建立套接字

  2. bind(...); // 繫結

  3. while(1)

  4. {

  5. recvfrom(...); // 接收客戶端的請求

  6. process(...); // 處理請求

  7. sendto(...); // 反饋處理結果

  8. }

示例程式碼如下:

  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <string.h>

  4. #include <unistd.h>

  5. #include <sys/socket.h>

  6. #include <netinet/in.h>

  7. #include <arpa/inet.h>

  8. int main(int argc, char *argv[])

  9. {

  10. unsigned short port = 8080; // 本地埠

  11. int sockfd;

  12. sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 建立udp套接字

  13. if(sockfd < 0)

  14. {

  15. perror("socket");

  16. exit(-1);

  17. }

  18. // 初始化本地網路資訊

  19. struct sockaddr_in my_addr;

  20. bzero(&my_addr, sizeof(my_addr)); // 清空

  21. my_addr.sin_family = AF_INET; // IPv4

  22. my_addr.sin_port = htons(port); // 埠

  23. my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip

  24. printf("Binding server to port %d\n", port);

  25. // 繫結

  26. int err_log;

  27. err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));

  28. if(err_log != 0)

  29. {

  30. perror("bind");

  31. close(sockfd);

  32. exit(-1);

  33. }

  34. printf("receive data...\n");

  35. while(1)

  36. {

  37. int recv_len;

  38. char recv_buf[512] = {0};

  39. struct sockaddr_in client_addr;

  40. char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16

  41. socklen_t cliaddr_len = sizeof(client_addr);

  42. // 接收客戶端資料

  43. recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);

  44. // 處理資料,這裡只是把接收過來的資料列印

  45. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

  46. printf("\nip:%s ,port:%d\n",cli_ip, ntohs(client_addr.sin_port)); // 客戶端的ip

  47. printf("data(%d):%s\n",recv_len,recv_buf); // 客戶端的資料

  48. // 反饋結果,這裡把接收直接到客戶端的資料回覆過去

  49. sendto(sockfd, recv_buf, recv_len, 0, (struct sockaddr*)&client_addr, cliaddr_len);

  50. }

  51. close(sockfd);

  52. return 0;

  53. }

執行結果如下:

TCP 迴圈伺服器的實現方法

TCP 迴圈伺服器接受一個客戶端的連線,然後處理,完成了這個客戶的所有請求後,斷開連線。TCP 迴圈伺服器一次只能處理一個客戶端的請求,只有在這個客戶的所有請求滿足後,伺服器才可以繼續後面的請求。如果有一個客戶端佔住伺服器不放時,其它的客戶機都不能工作了,因此,TCP 伺服器一般很少用迴圈伺服器模型的。

TCP迴圈伺服器模型為:

  1. socket(...);// 建立套接字

  2. bind(...);// 繫結

  3. listen(...);// 監聽

  4. while(1)

  5. {

  6. accept(...);// 取出客戶端的請求連線

  7. process(...);// 處理請求,反饋結果

  8. close(...);// 關閉連線套接字:accept()返回的套接字

  9. }

示例程式碼如下:

  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <string.h>

  4. #include <unistd.h>

  5. #include <sys/socket.h>

  6. #include <netinet/in.h>

  7. #include <arpa/inet.h>

  8. int main(int argc, char *argv[])

  9. {

  10. unsigned short port = 8080; // 本地埠

  11. // 建立tcp套接字

  12. int sockfd = socket(AF_INET, SOCK_STREAM, 0);

  13. if(sockfd < 0)

  14. {

  15. perror("socket");

  16. exit(-1);

  17. }

  18. // 配置本地網路資訊

  19. struct sockaddr_in my_addr;

  20. bzero(&my_addr, sizeof(my_addr)); // 清空

  21. my_addr.sin_family = AF_INET; // IPv4

  22. my_addr.sin_port = htons(port); // 埠

  23. my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip

  24. // 繫結

  25. int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));

  26. if( err_log != 0)

  27. {

  28. perror("binding");

  29. close(sockfd);

  30. exit(-1);

  31. }

  32. // 監聽,套接字變被動

  33. err_log = listen(sockfd, 10);

  34. if(err_log != 0)

  35. {

  36. perror("listen");

  37. close(sockfd);

  38. exit(-1);

  39. }

  40. printf("listen client @port=%d...\n",port);

  41. while(1)

  42. {

  43. struct sockaddr_in client_addr;

  44. char cli_ip[INET_ADDRSTRLEN] = "";

  45. socklen_t cliaddr_len = sizeof(client_addr);

  46. // 取出客戶端已完成的連線

  47. int connfd;

  48. connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);

  49. if(connfd < 0)

  50. {

  51. perror("accept");

  52. continue;

  53. }

  54. // 列印客戶端的ip和埠

  55. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

  56. printf("----------------------------------------------\n");

  57. printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));

  58. // 接收資料

  59. char recv_buf[512] = {0};

  60. int len = recv(connfd, recv_buf, sizeof(recv_buf), 0);

  61. // 處理資料,這裡只是列印接收到的內容

  62. printf("\nrecv data:\n");

  63. printf("%s\n",recv_buf);

  64. // 反饋結果

  65. send(connfd, recv_buf, len, 0);

  66. close(connfd); //關閉已連線套接字

  67. printf("client closed!\n");

  68. }

  69. close(sockfd); //關閉監聽套接字

  70. return 0;

  71. }

執行結果如下:

三種併發伺服器實現方法

一個好的伺服器,一般都是併發伺服器(同一時刻可以響應多個客戶端的請求)。併發伺服器設計技術一般有:多程序伺服器、多執行緒伺服器、I/O複用伺服器等。

多程序併發伺服器

在 Linux 環境下多程序的應用很多,其中最主要的就是網路/客戶伺服器。多程序伺服器是當客戶有請求時,伺服器用一個子程序來處理客戶請求。父程序繼續等待其它客戶的請求。這種方法的優點是當客戶有請求時,伺服器能及時處理客戶,特別是在客戶伺服器互動系統中。對於一個 TCP 伺服器,客戶與伺服器的連線可能並不馬上關閉,可能會等到客戶提交某些資料後再關閉,這段時間伺服器端的程序會阻塞,所以這時作業系統可能排程其它客戶服務程序,這比起迴圈伺服器大大提高了服務效能

TCP多程序併發伺服器 TCP 併發伺服器的思想是每一個客戶機的請求並不由伺服器直接處理,而是由伺服器建立一個子程序來處理。

示例程式碼如下:

  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <string.h>

  4. #include <unistd.h>

  5. #include <sys/socket.h>

  6. #include <netinet/in.h>

  7. #include <arpa/inet.h>

  8. int main(int argc, char *argv[])

  9. {

  10. unsigned short port = 8080; // 本地埠

  11. // 建立tcp套接字

  12. int sockfd = socket(AF_INET, SOCK_STREAM, 0);

  13. if(sockfd < 0)

  14. {

  15. perror("socket");

  16. exit(-1);

  17. }

  18. // 配置本地網路資訊

  19. struct sockaddr_in my_addr;

  20. bzero(&my_addr, sizeof(my_addr)); // 清空

  21. my_addr.sin_family = AF_INET; // IPv4

  22. my_addr.sin_port = htons(port); // 埠

  23. my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip

  24. // 繫結

  25. int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));

  26. if( err_log != 0)

  27. {

  28. perror("binding");

  29. close(sockfd);

  30. exit(-1);

  31. }

  32. // 監聽,套接字變被動

  33. err_log = listen(sockfd, 10);

  34. if(err_log != 0)

  35. {

  36. perror("listen");

  37. close(sockfd);

  38. exit(-1);

  39. }

  40. while(1) //主程序 迴圈等待客戶端的連線

  41. {

  42. char cli_ip[INET_ADDRSTRLEN] = {0};

  43. struct sockaddr_in client_addr;

  44. socklen_t cliaddr_len = sizeof(client_addr);

  45. // 取出客戶端已完成的連線

  46. int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);

  47. if(connfd < 0)

  48. {

  49. perror("accept");

  50. close(sockfd);

  51. exit(-1);

  52. }

  53. pid_t pid = fork();

  54. if(pid < 0){

  55. perror("fork");

  56. _exit(-1);

  57. }else if(0 == pid){ //子程序 接收客戶端的資訊,併發還給客戶端

  58. /*關閉不需要的套接字可節省系統資源,

  59. 同時可避免父子程序共享這些套接字

  60. 可能帶來的不可預計的後果

  61. */

  62. close(sockfd); // 關閉監聽套接字,這個套接字是從父程序繼承過來

  63. char recv_buf[1024] = {0};

  64. int recv_len = 0;

  65. // 列印客戶端的 ip 和埠

  66. memset(cli_ip, 0, sizeof(cli_ip)); // 清空

  67. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

  68. printf("----------------------------------------------\n");

  69. printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));

  70. // 接收資料

  71. while( (recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0 )

  72. {

  73. printf("recv_buf: %s\n", recv_buf); // 列印資料

  74. send(connfd, recv_buf, recv_len, 0); // 給客戶端回資料

  75. }

  76. printf("client closed!\n");

  77. close(connfd); //關閉已連線套接字

  78. exit(0);

  79. }else if(pid > 0){ // 父程序

  80. close(connfd); //關閉已連線套接字

  81. }

  82. }

  83. close(sockfd);

  84. return 0;

  85. }

執行結果如下:

多執行緒伺服器

多執行緒伺服器是對多程序的伺服器的改進,由於多程序伺服器在建立程序時要消耗較大的系統資源,所以用執行緒來取代程序,這樣服務處理程式可以較快的建立。據統計,建立執行緒與建立程序要快 10100 倍,所以又把執行緒稱為“輕量級”程序。執行緒與程序不同的是:一個程序內的所有執行緒共享相同的全域性記憶體、全域性變數等資訊,這種機制又帶來了同步問題

以下是多執行緒伺服器模板:

示例程式碼如下:

  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <string.h>

  4. #include <unistd.h>

  5. #include <sys/socket.h>

  6. #include <netinet/in.h>

  7. #include <arpa/inet.h>

  8. #include <pthread.h>

  9. /************************************************************************

  10. 函式名稱: void *client_process(void *arg)

  11. 函式功能: 執行緒函式,處理客戶資訊

  12. 函式引數: 已連線套接字

  13. 函式返回: 無

  14. ************************************************************************/

  15. void *client_process(void *arg)

  16. {

  17. int recv_len = 0;

  18. char recv_buf[1024] = ""; // 接收緩衝區

  19. int connfd = (int)arg; // 傳過來的已連線套接字

  20. // 接收資料

  21. while((recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0)

  22. {

  23. printf("recv_buf: %s\n", recv_buf); // 列印資料

  24. send(connfd, recv_buf, recv_len, 0); // 給客戶端回資料

  25. }

  26. printf("client closed!\n");

  27. close(connfd); //關閉已連線套接字

  28. return NULL;

  29. }

  30. //===============================================================

  31. // 語法格式: void main(void)

  32. // 實現功能: 主函式,建立一個TCP併發伺服器

  33. // 入口引數: 無

  34. // 出口引數: 無

  35. //===============================================================

  36. int main(int argc, char *argv[])

  37. {

  38. int sockfd = 0; // 套接字

  39. int connfd = 0;

  40. int err_log = 0;

  41. struct sockaddr_in my_addr; // 伺服器地址結構體

  42. unsigned short port = 8080; // 監聽埠

  43. pthread_t thread_id;

  44. printf("TCP Server Started at port %d!\n", port);

  45. sockfd = socket(AF_INET, SOCK_STREAM, 0); // 建立TCP套接字

  46. if(sockfd < 0)

  47. {

  48. perror("socket error");

  49. exit(-1);

  50. }

  51. bzero(&my_addr, sizeof(my_addr)); // 初始化伺服器地址

  52. my_addr.sin_family = AF_INET;

  53. my_addr.sin_port = htons(port);

  54. my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  55. printf("Binding server to port %d\n", port);

  56. // 繫結

  57. err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));

  58. if(err_log != 0)

  59. {

  60. perror("bind");

  61. close(sockfd);

  62. exit(-1);

  63. }

  64. // 監聽,套接字變被動

  65. err_log = listen(sockfd, 10);

  66. if( err_log != 0)

  67. {

  68. perror("listen");

  69. close(sockfd);

  70. exit(-1);

  71. }

  72. printf("Waiting client...\n");

  73. while(1)

  74. {

  75. char cli_ip[INET_ADDRSTRLEN] = ""; // 用於儲存客戶端IP地址

  76. struct sockaddr_in client_addr; // 用於儲存客戶端地址

  77. socklen_t cliaddr_len = sizeof(client_addr); // 必須初始化!!!

  78. //獲得一個已經建立的連線

  79. connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);

  80. if(connfd < 0)

  81. {

  82. perror("accept this time");

  83. continue;

  84. }

  85. // 列印客戶端的 ip 和埠

  86. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

  87. printf("----------------------------------------------\n");

  88. printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));

  89. if(connfd > 0)

  90. {

  91. //由於同一個程序內的所有執行緒共享記憶體和變數,因此在傳遞引數時需作特殊處理,值傳遞。

  92. pthread_create(&thread_id, NULL, (void *)client_process, (void *)connfd); //建立執行緒

  93. pthread_detach(thread_id); // 執行緒分離,結束時自動回收資源

  94. }

  95. }

  96. close(sockfd);

  97. return 0;

  98. }

執行結果如下:

注意,上面例子給執行緒傳參有很大的侷限性,最簡單的一種情況,如果我們需要給執行緒傳多個引數,這時候我們需要結構體傳參,這種值傳遞編譯都通不過,這裡之所以能夠這麼值傳遞,是因為, int 長度時 4 個位元組, void * 長度也是 4 個位元組。

  1. int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);

  2. pthread_create(&thread_id, NULL, (void *)client_process, (void *)connfd);

如果考慮型別匹配的話,應該是這麼傳參,pthread_create()最後一個引數應該傳地址( &connfd ),而不是值:

  1. int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);

  2. pthread_create(&thread_id, NULL, (void *)client_process, (void *)&connfd);

但是,如果按地址傳遞的話,又會有這麼一個問題,假如有多個客戶端要連線這個伺服器,正常的情況下,一個客戶端連線對應一個 connfd,相互之間獨立不受影響,但是,假如多個客戶端同時連線這個伺服器,A 客戶端的連線套接字為 connfd,伺服器正在用這個 connfd 處理資料,還沒有處理完,突然來了一個 B 客戶端,accept()之後又生成一個 connfd, 因為是地址傳遞, A 客戶端的連線套接字也變成 B 這個了,這樣的話,伺服器肯定不能再為 A 客戶端伺服器了,這時候,我們就需要考慮多工的互斥或同步問題了,這裡通過互斥鎖來解決這個問題,確保這個connfd值被一個臨時變數儲存過後,才允許修改。

  1. #include <pthread.h>

  2. pthread_mutex_t mutex; // 定義互斥鎖,全域性變數

  3. pthread_mutex_init(&mutex, NULL); // 初始化互斥鎖,互斥鎖預設是開啟的

  4. // 上鎖,在沒有解鎖之前,pthread_mutex_lock()會阻塞

  5. pthread_mutex_lock(&mutex);

  6. int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);

  7. //給回撥函式傳的引數,&connfd,地址傳遞

  8. pthread_create(&thread_id, NULL, (void *)client_process, (void *)&connfd); //建立執行緒

  9. // 執行緒回撥函式

  10. void *client_process(void *arg)

  11. {

  12. int connfd = *(int *)arg; // 傳過來的已連線套接字

  13. // 解鎖,pthread_mutex_lock()喚醒,不阻塞

  14. pthread_mutex_unlock(&mutex);

  15. return NULL;

  16. }

修改的完整程式碼如下:

  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <string.h>

  4. #include <unistd.h>

  5. #include <sys/socket.h>

  6. #include <netinet/in.h>

  7. #include <arpa/inet.h>

  8. #include <pthread.h>

  9. pthread_mutex_t mutex; // 定義互斥鎖,全域性變數

  10. /************************************************************************

  11. 函式名稱: void *client_process(void *arg)

  12. 函式功能: 執行緒函式,處理客戶資訊

  13. 函式引數: 已連線套接字

  14. 函式返回: 無

  15. ************************************************************************/

  16. void *client_process(void *arg)

  17. {

  18. int recv_len = 0;

  19. char recv_buf[1024] = ""; // 接收緩衝區

  20. int connfd = *(int *)arg; // 傳過來的已連線套接字

  21. // 解鎖,pthread_mutex_lock()喚醒,不阻塞

  22. pthread_mutex_unlock(&mutex);

  23. // 接收資料

  24. while((recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0)

  25. {

  26. printf("recv_buf: %s\n", recv_buf); // 列印資料

  27. send(connfd, recv_buf, recv_len, 0); // 給客戶端回資料

  28. }

  29. printf("client closed!\n");

  30. close(connfd); //關閉已連線套接字

  31. return NULL;

  32. }

  33. //===============================================================

  34. // 語法格式: void main(void)

  35. // 實現功能: 主函式,建立一個TCP併發伺服器

  36. // 入口引數: 無

  37. // 出口引數: 無

  38. //===============================================================

  39. int main(int argc, char *argv[])

  40. {

  41. int sockfd = 0; // 套接字

  42. int connfd = 0;

  43. int err_log = 0;

  44. struct sockaddr_in my_addr; // 伺服器地址結構體

  45. unsigned short port = 8080; // 監聽埠

  46. pthread_t thread_id;

  47. pthread_mutex_init(&mutex, NULL); // 初始化互斥鎖,互斥鎖預設是開啟的

  48. printf("TCP Server Started at port %d!\n", port);

  49. sockfd = socket(AF_INET, SOCK_STREAM, 0); // 建立TCP套接字

  50. if(sockfd < 0)

  51. {

  52. perror("socket error");

  53. exit(-1);

  54. }

  55. bzero(&my_addr, sizeof(my_addr)); // 初始化伺服器地址

  56. my_addr.sin_family = AF_INET;

  57. my_addr.sin_port = htons(port);

  58. my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

  59. printf("Binding server to port %d\n", port);

  60. // 繫結

  61. err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));

  62. if(err_log != 0)

  63. {

  64. perror("bind");

  65. close(sockfd);

  66. exit(-1);

  67. }

  68. // 監聽,套接字變被動

  69. err_log = listen(sockfd, 10);

  70. if( err_log != 0)

  71. {

  72. perror("listen");

  73. close(sockfd);

  74. exit(-1);

  75. }

  76. printf("Waiting client...\n");

  77. while(1)

  78. {

  79. char cli_ip[INET_ADDRSTRLEN] = ""; // 用於儲存客戶端IP地址

  80. struct sockaddr_in client_addr; // 用於儲存客戶端地址

  81. socklen_t cliaddr_len = sizeof(client_addr); // 必須初始化!!!

  82. // 上鎖,在沒有解鎖之前,pthread_mutex_lock()會阻塞

  83. pthread_mutex_lock(&mutex);

  84. //獲得一個已經建立的連線

  85. connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);

  86. if(connfd < 0)

  87. {

  88. perror("accept this time");

  89. continue;

  90. }

  91. // 列印客戶端的 ip 和埠

  92. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

  93. printf("----------------------------------------------\n");

  94. printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));

  95. if(connfd > 0)

  96. {

  97. //給回撥函式傳的引數,&connfd,地址傳遞

  98. pthread_create(&thread_id, NULL, (void *)client_process, (void *)&connfd); //建立執行緒

  99. pthread_detach(thread_id); // 執行緒分離,結束時自動回收資源

  100. }

  101. }

  102. close(sockfd);

  103. return 0;

  104. }

I/O複用伺服器

I/O 複用技術是為了解決程序或執行緒阻塞到某個 I/O 系統呼叫而出現的技術,使程序不阻塞於某個特定的 I/O 系統呼叫。它也可用於併發伺服器的設計,常用函式 select() 或 epoll() 來實現。詳情,請看《select、poll、epoll的區別使用》

  1. socket(...); // 建立套接字

  2. bind(...); // 繫結

  3. listen(...); // 監聽

  4. while(1)

  5. {

  6. if(select(...) > 0) // 檢測監聽套接字是否可讀

  7. {

  8. if(FD_ISSET(...)>0) // 套接字可讀,證明有新客戶端連線伺服器

  9. {

  10. accpet(...);// 取出已經完成的連線

  11. process(...);// 處理請求,反饋結果

  12. }

  13. }

  14. close(...); // 關閉連線套接字:accept()返回的套接字

  15. }

示例程式碼如下:

  1. #include <stdio.h>

  2. #include <unistd.h>

  3. #include <stdlib.h>

  4. #include <errno.h>

  5. #include <string.h>

  6. #include <sys/socket.h>

  7. #include <sys/types.h>

  8. #include <netinet/in.h>

  9. #include <arpa/inet.h>

  10. #include <sys/select.h>

  11. #define SERV_PORT 8080

  12. #define LIST 20 //伺服器最大接受連線

  13. #define MAX_FD 10 //FD_SET支援描述符數量

  14. int main(int argc, char *argv[])

  15. {

  16. int sockfd;

  17. int err;

  18. int i;

  19. int connfd;

  20. int fd_all[MAX_FD]; //儲存所有描述符,用於select呼叫後,判斷哪個可讀

  21. //下面兩個備份原因是select呼叫後,會發生變化,再次呼叫select前,需要重新賦值

  22. fd_set fd_read; //FD_SET資料備份

  23. fd_set fd_select; //用於select

  24. struct timeval timeout; //超時時間備份

  25. struct timeval timeout_select; //用於select

  26. struct sockaddr_in serv_addr; //伺服器地址

  27. struct sockaddr_in cli_addr; //客戶端地址

  28. socklen_t serv_len;

  29. socklen_t cli_len;

  30. //超時時間設定

  31. timeout.tv_sec = 10;

  32. timeout.tv_usec = 0;

  33. //建立TCP套接字

  34. sockfd = socket(AF_INET, SOCK_STREAM, 0);

  35. if(sockfd < 0)

  36. {

  37. perror("fail to socket");

  38. exit(1);

  39. }

  40. // 配置本地地址

  41. memset(&serv_addr, 0, sizeof(serv_addr));

  42. serv_addr.sin_family = AF_INET; // ipv4

  43. serv_addr.sin_port = htons(SERV_PORT); // 埠, 8080

  44. serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip

  45. serv_len = sizeof(serv_addr);

  46. // 繫結

  47. err = bind(sockfd, (struct sockaddr *)&serv_addr, serv_len);

  48. if(err < 0)

  49. {

  50. perror("fail to bind");

  51. exit(1);

  52. }

  53. // 監聽

  54. err = listen(sockfd, LIST);

  55. if(err < 0)

  56. {

  57. perror("fail to listen");

  58. exit(1);

  59. }

  60. //初始化fd_all陣列

  61. memset(&fd_all, -1, sizeof(fd_all));

  62. fd_all[0] = sockfd; //第一個為監聽套接字

  63. FD_ZERO(&fd_read); // 清空

  64. FD_SET(sockfd, &fd_read); //將監聽套接字加入fd_read

  65. int maxfd;

  66. maxfd = fd_all[0]; //監聽的最大套接字

  67. while(1){

  68. // 每次都需要重新賦值,fd_select,timeout_select每次都會變

  69. fd_select = fd_read;

  70. timeout_select = timeout;

  71. // 檢測監聽套接字是否可讀,沒有可讀,此函式會阻塞

  72. // 只要有客戶連線,或斷開連線,select()都會往下執行

  73. err = select(maxfd+1, &fd_select, NULL, NULL, NULL);

  74. //err = select(maxfd+1, &fd_select, NULL, NULL, (struct timeval *)&timeout_select);

  75. if(err < 0)

  76. {

  77. perror("fail to select");

  78. exit(1);

  79. }

  80. if(err == 0){

  81. printf("timeout\n");

  82. }

  83. // 檢測監聽套接字是否可讀

  84. if( FD_ISSET(sockfd, &fd_select) ){//可讀,證明有新客戶端連線伺服器

  85. cli_len = sizeof(cli_addr);

  86. // 取出已經完成的連線

  87. connfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len);

  88. if(connfd < 0)

  89. {

  90. perror("fail to accept");

  91. exit(1);

  92. }

  93. // 列印客戶端的 ip 和埠

  94. char cli_ip[INET_ADDRSTRLEN] = {0};

  95. inet_ntop(AF_INET, &cli_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);

  96. printf("----------------------------------------------\n");

  97. printf("client ip=%s,port=%d\n", cli_ip,ntohs(cli_addr.sin_port));

  98. // 將新連線套接字加入 fd_all 及 fd_read

  99. for(i=0; i < MAX_FD; i++){

  100. if(fd_all[i] != -1){

  101. continue;

  102. }else{

  103. fd_all[i] = connfd;

  104. printf("client fd_all[%d] join\n", i);

  105. break;

  106. }

  107. }

  108. FD_SET(connfd, &fd_read);

  109. if(maxfd < connfd)

  110. {

  111. maxfd = connfd; //更新maxfd

  112. }

  113. }

  114. //從1開始檢視連線套接字是否可讀,因為上面已經處理過0(sockfd)

  115. for(i=1; i < maxfd; i++){

  116. if(FD_ISSET(fd_all[i], &fd_select)){

  117. printf("fd_all[%d] is ok\n", i);

  118. char buf[1024]={0}; //讀寫緩衝區

  119. int num = read(fd_all[i], buf, 1024);

  120. if(num > 0){

  121. //收到 客戶端資料並列印

  122. printf("receive buf from client fd_all[%d] is: %s\n", i, buf);

  123. //回覆客戶端

  124. num = write(fd_all[i], buf, num);

  125. if(num < 0){

  126. perror("fail to write ");

  127. exit(1);

  128. }else{

  129. //printf("send reply\n");

  130. }

  131. }else if(0 == num){ // 客戶端斷開時

  132. //客戶端退出,關閉套接字,並從監聽集合清除

  133. printf("client:fd_all[%d] exit\n", i);

  134. FD_CLR(fd_all[i], &fd_read);

  135. close(fd_all[i]);

  136. fd_all[i] = -1;

  137. continue;

  138. }

  139. }else {

  140. //printf("no data\n");

  141. }

  142. }

  143. }

  144. return 0;

  145. }

執行結果如下:

--------------------- 本文來自 Mike__Jiang 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/tennysonsky/article/details/45671215?utm_source=copy