Linux 网络编程——并发服务器的三种经典实现模型

服务器设计技术有很多,按使用的协议来分有 TCP 服务器和 UDP 服务器,按处理方式来分有循环服务器并发服务器

循环服务器与并发服务器模型

在网络程序里面,一般来说都是许多客户对应一个服务器(多对一),为了处理客户的请求,对服务端的程序就提出了特殊的要求。


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

·循环服务器:服务器在同一时刻只能响应一个客户端的请求

·并发服务器:服务器在同一时刻可以响应多个客户端的请求

UDP 循环服务器的实现方法

UDP 循环服务器每次从套接字上读取一个客户端的请求 -> 处理 -> 然后将结果返回给客户机。


因为 UDP 是非面向连接的,没有一个客户端可以老是占住服务端。只要处理过程不是死循环,或者耗时不是很长,服务器对于每一个客户机的请求在某种程度上来说是能够满足。


UDP 循环服务器模型为

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

  2. bind(...); // 绑定

  3.  
  4. while(1)

  5. {

  6. recvfrom(...); // 接收客户端的请求

  7. process(...); // 处理请求

  8. sendto(...); // 反馈处理结果

  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.  
  9. int main(int argc, char *argv[])

  10. {

  11. unsigned short port = 8080; // 本地端口

  12.  
  13. int sockfd;

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

  15. if(sockfd < 0)

  16. {

  17. perror("socket");

  18. exit(-1);

  19. }

  20.  
  21. // 初始化本地网络信息

  22. struct sockaddr_in my_addr;

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

  24. my_addr.sin_family = AF_INET; // IPv4

  25. my_addr.sin_port = htons(port); // 端口

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

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

  29.  
  30. // 绑定

  31. int err_log;

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

  33. if(err_log != 0)

  34. {

  35. perror("bind");

  36. close(sockfd);

  37. exit(-1);

  38. }

  39.  
  40. printf("receive data...\n");

  41. while(1)

  42. {

  43. int recv_len;

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

  45. struct sockaddr_in client_addr;

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

  47. socklen_t cliaddr_len = sizeof(client_addr);

  48.  
  49. // 接收客户端数据

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

  51.  
  52. // 处理数据,这里只是把接收过来的数据打印

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

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

  55. printf("data(%d):%s\n",recv_len,recv_buf); // 客户端的数据

  56.  
  57. // 反馈结果,这里把接收直接到客户端的数据回复过去

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

  59. }

  60.  
  61. close(sockfd);

  62.  
  63. return 0;

  64. }

运行结果如下:

TCP 循环服务器的实现方法

TCP 循环服务器接受一个客户端的连接,然后处理,完成了这个客户的所有请求后,断开连接。TCP 循环服务器一次只能处理一个客户端的请求,只有在这个客户的所有请求满足后,服务器才可以继续后面的请求。如果有一个客户端占住服务器不放时,其它的客户机都不能工作了,因此,TCP 服务器一般很少用循环服务器模型的。

TCP循环服务器模型为:

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

  2. bind(...);// 绑定

  3. listen(...);// 监听

  4.  
  5. while(1)

  6. {

  7. accept(...);// 取出客户端的请求连接

  8. process(...);// 处理请求,反馈结果

  9. close(...);// 关闭连接套接字:accept()返回的套接字

  10. }

示例代码如下:

 
  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.  
  9. int main(int argc, char *argv[])

  10. {

  11. unsigned short port = 8080; // 本地端口

  12.  
  13. // 创建tcp套接字

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

  15. if(sockfd < 0)

  16. {

  17. perror("socket");

  18. exit(-1);

  19. }

  20.  
  21. // 配置本地网络信息

  22. struct sockaddr_in my_addr;

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

  24. my_addr.sin_family = AF_INET; // IPv4

  25. my_addr.sin_port = htons(port); // 端口

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

  27.  
  28. // 绑定

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

  30. if( err_log != 0)

  31. {

  32. perror("binding");

  33. close(sockfd);

  34. exit(-1);

  35. }

  36.  
  37. // 监听,套接字变被动

  38. err_log = listen(sockfd, 10);

  39. if(err_log != 0)

  40. {

  41. perror("listen");

  42. close(sockfd);

  43. exit(-1);

  44. }

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

  47.  
  48. while(1)

  49. {

  50.  
  51. struct sockaddr_in client_addr;

  52. char cli_ip[INET_ADDRSTRLEN] = "";

  53. socklen_t cliaddr_len = sizeof(client_addr);

  54.  
  55. // 取出客户端已完成的连接

  56. int connfd;

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

  58. if(connfd < 0)

  59. {

  60. perror("accept");

  61. continue;

  62. }

  63.  
  64. // 打印客户端的ip和端口

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

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

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

  68.  
  69. // 接收数据

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

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

  72.  
  73. // 处理数据,这里只是打印接收到的内容

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

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

  76.  
  77. // 反馈结果

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

  79.  
  80. close(connfd); //关闭已连接套接字

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

  82. }

  83.  
  84. close(sockfd); //关闭监听套接字

  85.  
  86. return 0;

  87. }

运行结果如下:

三种并发服务器实现方法

一个好的服务器,一般都是并发服务器(同一时刻可以响应多个客户端的请求)。并发服务器设计技术一般有:多进程服务器、多线程服务器、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.  
  9. int main(int argc, char *argv[])

  10. {

  11. unsigned short port = 8080; // 本地端口

  12.  
  13. // 创建tcp套接字

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

  15. if(sockfd < 0)

  16. {

  17. perror("socket");

  18. exit(-1);

  19. }

  20.  
  21. // 配置本地网络信息

  22. struct sockaddr_in my_addr;

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

  24. my_addr.sin_family = AF_INET; // IPv4

  25. my_addr.sin_port = htons(port); // 端口

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

  27.  
  28. // 绑定

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

  30. if( err_log != 0)

  31. {

  32. perror("binding");

  33. close(sockfd);

  34. exit(-1);

  35. }

  36.  
  37. // 监听,套接字变被动

  38. err_log = listen(sockfd, 10);

  39. if(err_log != 0)

  40. {

  41. perror("listen");

  42. close(sockfd);

  43. exit(-1);

  44. }

  45.  
  46. while(1) //主进程 循环等待客户端的连接

  47. {

  48.  
  49. char cli_ip[INET_ADDRSTRLEN] = {0};

  50. struct sockaddr_in client_addr;

  51. socklen_t cliaddr_len = sizeof(client_addr);

  52.  
  53. // 取出客户端已完成的连接

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

  55. if(connfd < 0)

  56. {

  57. perror("accept");

  58. close(sockfd);

  59. exit(-1);

  60. }

  61.  
  62. pid_t pid = fork();

  63. if(pid < 0){

  64. perror("fork");

  65. _exit(-1);

  66. }else if(0 == pid){ //子进程 接收客户端的信息,并发还给客户端

  67. /*关闭不需要的套接字可节省系统资源,

  68. 同时可避免父子进程共享这些套接字

  69. 可能带来的不可预计的后果

  70. */

  71. close(sockfd); // 关闭监听套接字,这个套接字是从父进程继承过来

  72.  
  73. char recv_buf[1024] = {0};

  74. int recv_len = 0;

  75.  
  76. // 打印客户端的 ip 和端口

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

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

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

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

  81.  
  82. // 接收数据

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

  84. {

  85. printf("recv_buf: %s\n", recv_buf); // 打印数据

  86. send(connfd, recv_buf, recv_len, 0); // 给客户端回数据

  87. }

  88.  
  89. printf("client closed!\n");

  90.  
  91. close(connfd); //关闭已连接套接字

  92.  
  93. exit(0);

  94. }else if(pid > 0){ // 父进程

  95.  
  96. close(connfd); //关闭已连接套接字

  97.  
  98. }

  99. }

  100.  
  101. close(sockfd);

  102.  
  103. return 0;

  104. }


运行结果如下:

多线程服务器

多线程服务器是对多进程的服务器的改进,由于多进程服务器在创建进程时要消耗较大的系统资源,所以用线程来取代进程,这样服务处理程序可以较快的创建。据统计,创建线程与创建进程要快 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. /************************************************************************

  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.  
  22. // 接收数据

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

  24. {

  25. printf("recv_buf: %s\n", recv_buf); // 打印数据

  26. send(connfd, recv_buf, recv_len, 0); // 给客户端回数据

  27. }

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

  30. close(connfd); //关闭已连接套接字

  31.  
  32. return NULL;

  33. }

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

  36. // 语法格式: void main(void)

  37. // 实现功能: 主函数,建立一个TCP并发服务器

  38. // 入口参数: 无

  39. // 出口参数: 无

  40. //===============================================================

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

  42. {

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

  44. int connfd = 0;

  45. int err_log = 0;

  46. struct sockaddr_in my_addr; // 服务器地址结构体

  47. unsigned short port = 8080; // 监听端口

  48. pthread_t thread_id;

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

  51.  
  52. sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字

  53. if(sockfd < 0)

  54. {

  55. perror("socket error");

  56. exit(-1);

  57. }

  58.  
  59. bzero(&my_addr, sizeof(my_addr)); // 初始化服务器地址

  60. my_addr.sin_family = AF_INET;

  61. my_addr.sin_port = htons(port);

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

  63.  
  64.  
  65. printf("Binding server to port %d\n", port);

  66.  
  67. // 绑定

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

  69. if(err_log != 0)

  70. {

  71. perror("bind");

  72. close(sockfd);

  73. exit(-1);

  74. }

  75.  
  76. // 监听,套接字变被动

  77. err_log = listen(sockfd, 10);

  78. if( err_log != 0)

  79. {

  80. perror("listen");

  81. close(sockfd);

  82. exit(-1);

  83. }

  84.  
  85. printf("Waiting client...\n");

  86.  
  87. while(1)

  88. {

  89. char cli_ip[INET_ADDRSTRLEN] = ""; // 用于保存客户端IP地址

  90. struct sockaddr_in client_addr; // 用于保存客户端地址

  91. socklen_t cliaddr_len = sizeof(client_addr); // 必须初始化!!!

  92.  
  93. //获得一个已经建立的连接

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

  95. if(connfd < 0)

  96. {

  97. perror("accept this time");

  98. continue;

  99. }

  100.  
  101. // 打印客户端的 ip 和端口

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

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

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

  105.  
  106. if(connfd > 0)

  107. {

  108. //由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,值传递。

  109. pthread_create(&thread_id, NULL, (void *)client_process, (void *)connfd); //创建线程

  110. pthread_detach(thread_id); // 线程分离,结束时自动回收资源

  111. }

  112. }

  113.  
  114. close(sockfd);

  115.  
  116. return 0;

  117. }

运行结果如下:

注意,上面例子给线程传参有很大的局限性,最简单的一种情况,如果我们需要给线程传多个参数,这时候我们需要结构体传参,这种值传递编译都通不过,这里之所以能够这么值传递,是因为, 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.  
  3. pthread_mutex_t mutex; // 定义互斥锁,全局变量

  4.  
  5. pthread_mutex_init(&mutex, NULL); // 初始化互斥锁,互斥锁默认是打开的

  6.  
  7. // 上锁,在没有解锁之前,pthread_mutex_lock()会阻塞

  8. pthread_mutex_lock(&mutex);

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

  10.  
  11. //给回调函数传的参数,&connfd,地址传递

  12. pthread_create(&thread_id, NULL, (void *)client_process, (void *)&connfd); //创建线程

  13.  
  14. // 线程回调函数

  15. void *client_process(void *arg)

  16. {

  17. int connfd = *(int *)arg; // 传过来的已连接套接字

  18.  
  19. // 解锁,pthread_mutex_lock()唤醒,不阻塞

  20. pthread_mutex_unlock(&mutex);

  21.  
  22. return NULL;

  23. }

修改的完整代码如下:

 
  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. pthread_mutex_t mutex; // 定义互斥锁,全局变量

  11.  
  12. /************************************************************************

  13. 函数名称: void *client_process(void *arg)

  14. 函数功能: 线程函数,处理客户信息

  15. 函数参数: 已连接套接字

  16. 函数返回: 无

  17. ************************************************************************/

  18. void *client_process(void *arg)

  19. {

  20. int recv_len = 0;

  21. char recv_buf[1024] = ""; // 接收缓冲区

  22. int connfd = *(int *)arg; // 传过来的已连接套接字

  23.  
  24. // 解锁,pthread_mutex_lock()唤醒,不阻塞

  25. pthread_mutex_unlock(&mutex);

  26.  
  27. // 接收数据

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

  29. {

  30. printf("recv_buf: %s\n", recv_buf); // 打印数据

  31. send(connfd, recv_buf, recv_len, 0); // 给客户端回数据

  32. }

  33.  
  34. printf("client closed!\n");

  35. close(connfd); //关闭已连接套接字

  36.  
  37. return NULL;

  38. }

  39.  
  40. //===============================================================

  41. // 语法格式: void main(void)

  42. // 实现功能: 主函数,建立一个TCP并发服务器

  43. // 入口参数: 无

  44. // 出口参数: 无

  45. //===============================================================

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

  47. {

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

  49. int connfd = 0;

  50. int err_log = 0;

  51. struct sockaddr_in my_addr; // 服务器地址结构体

  52. unsigned short port = 8080; // 监听端口

  53. pthread_t thread_id;

  54.  
  55. pthread_mutex_init(&mutex, NULL); // 初始化互斥锁,互斥锁默认是打开的

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

  58.  
  59. sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字

  60. if(sockfd < 0)

  61. {

  62. perror("socket error");

  63. exit(-1);

  64. }

  65.  
  66. bzero(&my_addr, sizeof(my_addr)); // 初始化服务器地址

  67. my_addr.sin_family = AF_INET;

  68. my_addr.sin_port = htons(port);

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

  70.  
  71.  
  72. printf("Binding server to port %d\n", port);

  73.  
  74. // 绑定

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

  76. if(err_log != 0)

  77. {

  78. perror("bind");

  79. close(sockfd);

  80. exit(-1);

  81. }

  82.  
  83. // 监听,套接字变被动

  84. err_log = listen(sockfd, 10);

  85. if( err_log != 0)

  86. {

  87. perror("listen");

  88. close(sockfd);

  89. exit(-1);

  90. }

  91.  
  92. printf("Waiting client...\n");

  93.  
  94. while(1)

  95. {

  96. char cli_ip[INET_ADDRSTRLEN] = ""; // 用于保存客户端IP地址

  97. struct sockaddr_in client_addr; // 用于保存客户端地址

  98. socklen_t cliaddr_len = sizeof(client_addr); // 必须初始化!!!

  99.  
  100. // 上锁,在没有解锁之前,pthread_mutex_lock()会阻塞

  101. pthread_mutex_lock(&mutex);

  102.  
  103. //获得一个已经建立的连接

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

  105. if(connfd < 0)

  106. {

  107. perror("accept this time");

  108. continue;

  109. }

  110.  
  111. // 打印客户端的 ip 和端口

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

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

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

  115.  
  116. if(connfd > 0)

  117. {

  118. //给回调函数传的参数,&connfd,地址传递

  119. pthread_create(&thread_id, NULL, (void *)client_process, (void *)&connfd); //创建线程

  120. pthread_detach(thread_id); // 线程分离,结束时自动回收资源

  121. }

  122. }

  123.  
  124. close(sockfd);

  125.  
  126. return 0;

  127. }

I/O复用服务器

I/O 复用技术是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。它也可用于并发服务器的设计,常用函数 select() 或 epoll() 来实现。详情,请看《select、poll、epoll的区别使用》

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

  2. bind(...); // 绑定

  3. listen(...); // 监听

  4.  
  5. while(1)

  6. {

  7. if(select(...) > 0) // 检测监听套接字是否可读

  8. {

  9. if(FD_ISSET(...)>0) // 套接字可读,证明有新客户端连接服务器

  10. {

  11. accpet(...);// 取出已经完成的连接

  12. process(...);// 处理请求,反馈结果

  13. }

  14. }

  15. close(...); // 关闭连接套接字:accept()返回的套接字

  16. }

示例代码如下:

 
  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.  
  12. #define SERV_PORT 8080

  13. #define LIST 20 //服务器最大接受连接

  14. #define MAX_FD 10 //FD_SET支持描述符数量

  15.  
  16.  
  17. int main(int argc, char *argv[])

  18. {

  19. int sockfd;

  20. int err;

  21. int i;

  22. int connfd;

  23. int fd_all[MAX_FD]; //保存所有描述符,用于select调用后,判断哪个可读

  24.  
  25. //下面两个备份原因是select调用后,会发生变化,再次调用select前,需要重新赋值

  26. fd_set fd_read; //FD_SET数据备份

  27. fd_set fd_select; //用于select

  28.  
  29. struct timeval timeout; //超时时间备份

  30. struct timeval timeout_select; //用于select

  31.  
  32. struct sockaddr_in serv_addr; //服务器地址

  33. struct sockaddr_in cli_addr; //客户端地址

  34. socklen_t serv_len;

  35. socklen_t cli_len;

  36.  
  37. //超时时间设置

  38. timeout.tv_sec = 10;

  39. timeout.tv_usec = 0;

  40.  
  41. //创建TCP套接字

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

  43. if(sockfd < 0)

  44. {

  45. perror("fail to socket");

  46. exit(1);

  47. }

  48.  
  49. // 配置本地地址

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

  51. serv_addr.sin_family = AF_INET; // ipv4

  52. serv_addr.sin_port = htons(SERV_PORT); // 端口, 8080

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

  54.  
  55.  
  56. serv_len = sizeof(serv_addr);

  57.  
  58. // 绑定

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

  60. if(err < 0)

  61. {

  62. perror("fail to bind");

  63. exit(1);

  64. }

  65.  
  66. // 监听

  67. err = listen(sockfd, LIST);

  68. if(err < 0)

  69. {

  70. perror("fail to listen");

  71. exit(1);

  72. }

  73.  
  74. //初始化fd_all数组

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

  76.  
  77. fd_all[0] = sockfd; //第一个为监听套接字

  78.  
  79. FD_ZERO(&fd_read); // 清空

  80. FD_SET(sockfd, &fd_read); //将监听套接字加入fd_read

  81.  
  82. int maxfd;

  83. maxfd = fd_all[0]; //监听的最大套接字

  84.  
  85. while(1){

  86.  
  87. // 每次都需要重新赋值,fd_select,timeout_select每次都会变

  88. fd_select = fd_read;

  89. timeout_select = timeout;

  90.  
  91. // 检测监听套接字是否可读,没有可读,此函数会阻塞

  92. // 只要有客户连接,或断开连接,select()都会往下执行

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

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

  95. if(err < 0)

  96. {

  97. perror("fail to select");

  98. exit(1);

  99. }

  100.  
  101. if(err == 0){

  102. printf("timeout\n");

  103. }

  104.  
  105. // 检测监听套接字是否可读

  106. if( FD_ISSET(sockfd, &fd_select) ){//可读,证明有新客户端连接服务器

  107.  
  108. cli_len = sizeof(cli_addr);

  109.  
  110. // 取出已经完成的连接

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

  112. if(connfd < 0)

  113. {

  114. perror("fail to accept");

  115. exit(1);

  116. }

  117.  
  118. // 打印客户端的 ip 和端口

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

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

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

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

  123.  
  124. // 将新连接套接字加入 fd_all 及 fd_read

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

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

  127. continue;

  128. }else{

  129. fd_all[i] = connfd;

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

  131. break;

  132. }

  133. }

  134.  
  135. FD_SET(connfd, &fd_read);

  136.  
  137. if(maxfd < connfd)

  138. {

  139. maxfd = connfd; //更新maxfd

  140. }

  141.  
  142. }

  143.  
  144. //从1开始查看连接套接字是否可读,因为上面已经处理过0(sockfd)

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

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

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

  148.  
  149. char buf[1024]={0}; //读写缓冲区

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

  151. if(num > 0){

  152.  
  153. //收到 客户端数据并打印

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

  155.  
  156. //回复客户端

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

  158. if(num < 0){

  159. perror("fail to write ");

  160. exit(1);

  161. }else{

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

  163. }

  164.  
  165.  
  166. }else if(0 == num){ // 客户端断开时

  167.  
  168. //客户端退出,关闭套接字,并从监听集合清除

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

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

  171. close(fd_all[i]);

  172. fd_all[i] = -1;

  173.  
  174. continue;

  175. }

  176.  
  177. }else {

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

  179. }

  180. }

  181.  
  182. }

  183.  
  184. return 0;

  185. }

运行结果如下:

本教程示例代码下载请点此处。

参考于:http://blog.chinaunix.net

--------------------- 本文来自 Mike__Jiang 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/tennysonsky/article/details/45671215?utm_source=copy

猜你喜欢

转载自blog.csdn.net/Hamlee67/article/details/82972139