Linux编程-并发TCP服务器实现与IO多路复用

TCP‑多进程并发服务器原理

思路:

  1. 父进程循环 accept(),不停等待新客户端连接;
  2. 一旦有客户端接入,fork() 创建子进程;
  3. 子进程负责和该客户端收发数据
  4. 父进程关闭本次连接的connfd,立刻回到 accept,继续等待下一个客户端;
  5. 子进程用完连接后关闭 connfd,退出;
  6. 增加信号处理 SIGCHLD,回收僵尸子进程。

优点

  1. 代码逻辑简单、上手容易 每个客户端一条独立进程,进程之间内存完全隔离;你只需要编写单客户端收发逻辑,不需要处理 IO 多路复用、就绪事件、非阻塞、EAGAIN 等复杂坑。

  2. 天然阻塞 IO,收发互不干扰 子进程里read(connfd)阻塞,完全不会影响父进程accept;各个客户端之间收发完全独立,一个客户端阻塞不会拖垮其他客户端。

  3. 稳定性高、故障隔离好 某一个客户端发生异常、崩溃,只会销毁对应的子进程;不会影响父进程和其他客户端连接

  4. 不用纠结 select 时序陷阱 不会遇到你前面踩过的:select 休眠期间键盘输入丢失、read(0)阻塞卡死 socket 读取一类问题。 如果想要双向聊天,子进程内部再 fork 一次,一条收一条发,轻松实现双向自由发送。

  5. 阻塞 IO 编程难度最低 read/write 默认阻塞,不用设置O_NONBLOCK,不用处理EAGAIN,缓冲区由操作系统 TCP 协议栈自动管理。

缺点

  1. 资源开销大 每接入 1 个客户端就要创建 1 个进程。进程拥有独立地址空间、页表、文件描述符表。 客户端数量非常多(上千)时,CPU、内存开销陡增;进程切换开销变大,服务器性能下降。

  2. 进程间通信麻烦 各个客户端属于不同进程,进程之间不能直接共享数据。 如果需要所有客户端群发消息(聊天室),必须使用 IPC:共享内存、管道、消息队列,实现起来复杂。

  3. 僵尸进程隐患 子进程退出会发送SIGCHLD信号,父进程如果没有waitpid回收,就会产生大量僵尸进程,耗尽 PID 号。

  4. 进程创建慢 fork()创建进程开销大于pthread创建线程;高并发短连接场景,频繁创建销毁进程会造成性能损耗。

练习:多进程并发服务器,实现服务器回发客户端传送来的消息

服务器代码:

  1. 程序启动,注册信号处理函数 signal(SIGCHLD,sighandler); 提前告诉内核:以后子进程退出,自动调用 wait 回收,杜绝僵尸进程。

  2. 创建 TCP 监听套接字 socket () 操作系统返回一个文件描述符fd,就是监听套接字。

  3. 填充服务器地址结构体 设置服务器 IP:192.168.31.94,端口:50000注意:IP、端口必须转成网络大端序!

  4. bind () 绑定 把套接字 fd 和 IP + 端口绑定在一起。客户端就可以通过 192.168.31.94:50000 连接服务器。

  5. listen () 开启监听 套接字从主动套接字 → 被动监听套接字,开始等待客户端发起 TCP 连接。

  6. 进入 while (1) 循环,循环接收客户端

    1. accept(fd,...) 阻塞,没有客户端连接时程序停在这里休眠。
    2. 一旦有客户端发起 TCP 三次握手连接成功:
      1. accept 返回一个全新通信文件描述符 connfd(专门用来和这个客户端收发数据)
      2. 获取并打印客户端的 IP 地址、端口号
  7. fork () 创建子进程实现并发

    1. 父进程 (pid>0):不做任何通信工作,直接回到循环开头,再次调用 accept,继续等待下一个客户端连接;
    2. 子进程 (pid=0):接管 connfd,进入收发循环,和当前客户端单独通信。
  8. 子进程通信循环

    1. read(connfd,buf):阻塞读取客户端发来的数据
    2. 如果读到quit:关闭 connfd,子进程 exit 退出;父进程收到 SIGCHLD 信号,调用 wait 回收子进程;
    3. 没有收到 quit:打印收到的消息;拼接回复字符串server+xxx;write 把数据发回客户端。
    4. 循环,等待客户端下一条消息。

    #include<stdio.h>
    #include<sys/types.h> /* See NOTES */
    #include<sys/socket.h>
    #include<string.h>
    #include<arpa/inet.h>
    #include<unistd.h>
    #include<sys/wait.h>
    #include<stdlib.h>
    #include<signal.h>

    /**

    • @brief SIGCHLD信号处理函数,回收僵尸子进程
    • @param sig 触发的信号编号(SIGCHLD)
    • 当子进程退出时,内核会给父进程发送SIGCHLD信号
    • 父进程调用wait(),等待子进程结束,释放子进程资源,避免产生僵尸进程
      */
      void sighandler(int sig)
      {
      wait(NULL);
      }

    int main(int argc, const char *argv[])
    {
    // 注册信号捕捉函数:子进程退出自动调用sighandler回收资源
    signal(SIGCHLD,sighandler);

    复制代码
     // 创建流式套接字,IPv4,TCP协议
     // AF_INET:使用IPv4地址族
     // SOCK_STREAM:流式套接字,对应TCP
     // 0:自动选择对应协议(TCP)
     int fd = socket(AF_INET,SOCK_STREAM,0);
     if(fd < 0)
     {
         // 创建套接字失败打印错误信息
     	perror("socket fail");
     	return -1;
     }
     printf("fd = %d\n",fd);
    
     // 定义服务器地址结构体,存放服务器的IP和端口
     struct sockaddr_in seraddr;
     // 将结构体内存清零,防止出现随机垃圾数据
     bzero(&seraddr,sizeof(seraddr));
     seraddr.sin_family = AF_INET;         // 指定地址族为IPv4
     seraddr.sin_port = htons(50000);      // 绑定端口50000,htons:主机字节序转网络大端序
     seraddr.sin_addr.s_addr = inet_addr("192.168.31.94"); // 绑定本机指定IP地址
    
     // 将套接字文件描述符与IP、端口进行绑定
     // 只有绑定后客户端才能通过这个IP+端口找到服务器
     if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0)
     {
     	perror("bind fail");
     	return -1;
     }
    
     // 开启监听,让套接字变成被动套接字,可以接收客户端连接
     // 第二个参数5:允许挂起的未完成连接最大队列长度为5
     int ret = listen(fd,5);
     if(ret < 0)
     {
     	perror("listen fail");
     	return -1;
     }
     
     struct sockaddr_in cliaddr;   // 保存连接上来的客户端地址信息
     socklen_t addrlen = sizeof(cliaddr); // 客户端地址结构体长度
     
    
    
     while(1)   // 服务器死循环,循环接收新客户端连接
     {
     	// 阻塞等待客户端连接
     	// fd:监听套接字;cliaddr:输出参数,带回客户端IP端口;addrlen:地址长度
     	int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen);
     	if(connfd < 0)
     	{
     		perror("accept fail");
     		return -1;
     	}
     	printf("accept success\n");
     	printf("--------------------\n");
     	printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr)); // 网络字节序IP转字符串打印
     	printf("port	   = %d\n",ntohs(cliaddr.sin_port));       // 网络端口转主机字节序打印
     	printf("--------------------\n");
    
     	// 创建子进程,实现多客户端并发
     	// 父进程:继续回去accept等待下一个客户端
     	// 子进程:专门负责和当前这个客户端收发数据
     	pid_t pid = fork();
     	if(pid == 0)   // ============子进程分支============
     	{
     		// 子进程循环和客户端通信
     		while(1)
     		{
     			char buf[1024] = {'\0'};
     			// 从客户端读取数据,阻塞等待客户端发送消息
     			read(connfd,buf,sizeof(buf));
    
     			// 判断客户端发来quit,关闭连接,子进程退出
     			if(strncmp(buf,"quit",4) == 0)
     			{
     				close(connfd);
     				exit(0);
     			}
    
     			printf("ser:%s\n",buf);  // 服务器打印收到的客户端消息
     			char sbuf[1024];
     			sprintf(sbuf,"server+%s\n",buf); // 拼接回复字符串
     			write(connfd,sbuf,strlen(sbuf)+1); // 把拼接好的数据发回客户端
     		}
     	}
     	// ============父进程分支============
     	// fork返回>0,父进程不会进入上面if,直接回到while循环,再次执行accept
     	// 父进程不负责数据收发,只负责不断等待新客户端接入
     }
     return 0;

    }

练习:多线程并发服务器,实现服务器回发客户端传送来的消息

  1. socket () 创建监听套接字 fd 操作系统返回监听文件描述符,专门用来等待客户端连接。

  2. 填充服务器地址结构体 seraddr 配置服务器 IP:192.168.31.94,端口:50000;端口使用htons转换成网络大端字节序。

  3. bind () 绑定套接字与 IP 端口 客户端就可以通过 192.168.31.94:50000 找到服务器程序。

  4. listen () 开启监听 套接字由主动套接字 → 被动监听套接字,开始监听客户端 TCP 连接请求。

  5. 进入 while (1) 死循环,循环接收客户端

    • accept(fd,...) 阻塞休眠,没有客户端连接时程序停在这里。
    • 客户端发起 TCP 连接、三次握手完成后,accept 返回全新通信套接字connfd,这个 fd 专门用来和该客户端收发数据。
    • 打印客户端 IP 和端口信息。
  6. pthread_create 创建子线程处理客户端

  • 主线程(main) :创建完子线程后,不会阻塞;立刻回到循环头部,再次执行accept等待下一个客户端接入。
  • 子线程 :执行pthread_cli函数,接管connfd,专门负责和当前客户端循环收发消息。
  1. pthread_detach(tid) 标记线程为分离线程。当子线程执行完毕退出时,系统自动回收线程资源,不需要主线程调用pthread_join等待回收,避免产生僵尸线程。

  2. 子线程通信循环 pthread_cli

  • read(connfd,buf)阻塞读取客户端发送的数据;

  • 如果读到字符串quit:关闭connfd套接字,跳出循环,pthread_exit(NULL)线程结束;

  • 没有收到 quit:服务器打印收到的消息,拼接回复字符串server+xxxwrite发送回客户端;

  • 继续循环等待客户端下一条消息。

    #include<stdio.h>
    #include<sys/types.h> /* See NOTES */
    #include<sys/socket.h>
    #include<string.h>
    #include<arpa/inet.h>
    #include<unistd.h>
    #include<sys/wait.h>
    #include<stdlib.h>
    #include<signal.h>
    #include<pthread.h>

    /**

    • @brief 子线程回调函数:专门和一个客户端进行数据通信

    • @param arg 传入通信套接字connfd的地址

    • @return 线程返回值
      */
      void *pthread_cli(void * arg)
      {
      // 取出主线程传过来的通信文件描述符
      int connfd = *(int *)arg;

      // 和客户端循环收发数据
      while(1)
      {
      char buf[1024] = {'\0'};
      // 阻塞读取客户端发送过来的数据
      read(connfd,buf,sizeof(buf));

      复制代码
       // 如果客户端发送quit,关闭套接字,退出循环,线程结束
       if(strncmp(buf,"quit",4) == 0)
       {
       	close(connfd);
       	break;
       }
      
       printf("ser:%s\n",buf);
       char sbuf[1024];
       // 拼接回复字符串
       sprintf(sbuf,"server+%s\n",buf);
       // 将响应数据写回客户端
       write(connfd,sbuf,strlen(sbuf)+1);

      }
      // 线程正常退出
      pthread_exit(NULL);
      }

    int main(int argc, const char *argv[])
    {
    // 创建流式套接字,IPv4,TCP协议
    // AF_INET:IPv4;SOCK_STREAM:TCP流式套接字;0自动匹配TCP协议
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd < 0)
    {
    // 创建套接字失败打印错误信息
    perror("socket fail");
    return -1;
    }
    printf("fd = %d\n",fd);

    复制代码
      // 定义服务器地址结构体,存放服务器IP、端口信息
      struct sockaddr_in seraddr;
      // 将结构体内存清零,清除随机脏数据
      bzero(&seraddr,sizeof(seraddr));
      seraddr.sin_family = AF_INET;         // 指定地址族为IPv4
      seraddr.sin_port = htons(50000);      // 绑定端口50000,htons主机字节序转网络大端序
      seraddr.sin_addr.s_addr = inet_addr("192.168.31.94"); // 绑定本机指定IP地址
    
      // 将监听套接字fd与IP、端口进行绑定
      if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0)
      {
      	perror("bind fail");
      	return -1;
      }
    
      // 开启监听,套接字变为被动套接字,等待客户端连接
      // 第二个参数5:未完成连接队列最大长度
      int ret = listen(fd,5);
      if(ret < 0)
      {
      	perror("listen fail");
      	return -1;
      }
      
      struct sockaddr_in cliaddr;   // 存储接入客户端的IP、端口
      socklen_t addrlen = sizeof(cliaddr);  // 客户端地址结构体大小
      
      // 服务器主循环,不断等待新客户端接入
      while(1)
      {
      	// accept阻塞等待客户端TCP连接,三次握手成功后返回通信套接字connfd
      	int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen);
      	if(connfd < 0)
      	{
      		perror("accept fail");
      		return -1;
      	}
      	printf("accept success\n");
      	printf("--------------------\n");
      	printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr)); // 打印客户端IP
      	printf("port	   = %d\n",ntohs(cliaddr.sin_port));       // 打印客户端端口
      	printf("--------------------\n");
    
      	pthread_t tid;
      	// 创建子线程,让子线程处理当前这个客户端的通信业务
      	// 参数:线程id,线程属性,线程入口函数,传给线程函数的参数
      	pthread_create(&tid,NULL,pthread_cli,(void *)&connfd);
      	// 设置线程分离态:线程结束后自动释放自身资源,不需要主线程pthread_join回收
      	pthread_detach(tid);
      
      }
    
      return 0;

    }

进程版 VS 线程版对比

项目 fork 多进程服务器 pthread 多线程服务器
资源开销 大,每个进程独立地址空间 小,线程共享主线程地址空间
文件描述符 fork 会复制 fd 副本 所有线程共享文件描述符
数据隔离 进程间数据天然隔离 共享全局变量,需要加锁
回收资源 SIGCHLD + wait 回收僵尸进程 pthread_detach 回收线程

什么是 IO 多路复用

一个进程,通过一块内核监控,同时监听多个文件描述符 (fd),哪个 fd 就绪(可读 / 可写 / 异常),就去处理哪一个。

  1. 不需要创建进程、线程;单进程就可以并发处理多个客户端
  2. 三大模型:selectpollepoll
  3. 三种并发方案对比
方案 并发方式 优点 缺点
fork 多进程服务器 一客户端一子进程 编程简单,进程隔离稳定 资源开销巨大;进程切换消耗高;僵尸进程问题
pthread 多线程服务器 一客户端一线程 开销比进程小 线程竞态、共享资源需要锁;多线程调度开销
IO 多路复用 (select/epoll) 单进程监控所有 fd 资源开销最小;没有进程线程切换开销 代码逻辑复杂;单进程 CPU 只能跑一个业务,耗时任务会阻塞整个服务器

阻塞 IO‑Blocking‑IO

发起 IO 调用后,线程 / 进程立刻休眠,一直等到数据就绪、完成读写,函数才返回。

read(fd,buf,len) 读取一个没有数据的 socket 举例:

  1. 调用read(),内核发现缓冲区没有数据
  2. 用户线程被挂起、让出 CPU,阻塞等待
  3. 内核收到网络数据,拷贝到用户缓冲区
  4. read 返回,程序继续往下执行

👉 特点:调用不返回,卡死在这里等

非阻塞 IO‑Non‑Blocking‑IO

IO 调用立刻返回 ,不管数据有没有准备好。 如果数据没就绪,函数返回错误标记(Linux 里一般返回‑1,errno = EAGAIN / EWOULDBLOCK),不会卡住线程

同样 read(fd,buf,len),socket 设为非阻塞:

  1. 调用 read (),内核缓冲区没有数据
  2. 马上返回,报错 EAGAIN,不阻塞线程
  3. 用户代码可以去干别的事,之后轮询 (polling) 再次调用 read 尝试读取

👉 特点:不会卡住,但是需要不停循环重试,也就是轮询。

fcntl 函数原型

复制代码
#include <unistd.h>
	#include <fcntl.h>
	int fcntl(int fd, int cmd, ... /* arg */ );
	功能:
	      修改指定文件的属性信息。
	参数:
	      fd  要调整的文件描述符
		  cmd 要调整的文件属性宏名称
		  ... 可变长的属性值参数。
	返回值:
	     成功   不一定,看cmd
	     失败   -1;

	eg:
	    修改文件的非阻塞属性:
		int flag ;
		flag  = fcntl(fd,F_GETFL,0);  ///获取fd文件的默认属性到flag变量中。
		flag  = flag | O_NONBLOCK;    ///将变量的值调整并添加非阻塞属性
		fcntl(fd,F_SETFL,flag);       ///将新属性flag设置到fd对应的文件生效。

		以上代码执行后的阻塞IO将变成非阻塞方式。

学会设置文件IO的属性之后,我们继续学习,信号驱动IO原理。当对应的文件描述符可以执行 IO操作时。进程请求内核为自己发送一个信号。这就可以实现,进程执行任何其他的任务直到 I/O 就绪为止,此时内核会发送信号给进程。发送的信号为SIGIO信号,通过前面学习的signal()函数对信号处理进行注册sighandler即可实现,信号驱动IO

工作原理

  1. 进程给文件描述符设置 O_NONBLOCK + O_ASYNC(异步信号标志)

  2. 使用fcntl(F_SETOWN) 指定:数据就绪时,内核该通知哪个进程 / 线程

  3. 进程设置 SIGIO 信号处理函数(signal/sigaction)

  4. 进程就可以去干别的事情,不需要阻塞等待,也不用轮询

  5. 当内核缓冲区数据就绪时 → 内核发送 SIGIO 信号通知你的程序

  6. 程序收到信号,在信号回调函数里面执行 read/recv,读取数据

  7. 核心:内核主动发信号通知你数据来了,而不是你不停轮询问内核。

    int flag;
    flag = fcntl(connfd,F_GETFL,0);
    flag = flag | O_NONBLOCK;
    fcntl(connfd,F_SETFL,flag);

    O_NONBLOCK → 非阻塞开关
    O_ASYNC → 异步信号驱动开关

练习:用信号驱动io实现服务器回发客户端发送来的信息

  1. 客户端发送数据到服务器

  2. 内核检测到套接字缓冲区有数据,发送 SIGIO 信号给服务器进程

  3. 进程收到信号,打断当前主程序流程,跳转到 sighandler()

  4. 在信号函数内部调用read(cfd)读取客户端数据

  5. 判断如果收到quit,关闭套接字、进程退出;

  6. 否则拼接字符串server+xxx,write 回写给客户端

  7. 信号处理函数执行完毕,回到主循环继续等待下一个客户端连接

    int cfd = -1; // 全局变量:保存客户端文件描述符,信号处理函数需要访问

    /**

    • @brief SIGIO 信号的回调处理函数

    • 复制代码
         当客户端有数据到达,驱动发送SIGIO信号触发此函数,读取客户端数据并回复
    • @param signo 触发的信号编号
      */
      void sighandler(int signo)
      {
      char buf[1024] = {'\0'};
      // 从客户端套接字读取数据
      int ret = read(cfd,buf,sizeof(buf));

      printf("ret = %d buf = %s\n",ret,buf);

      // 如果客户端发送 quit,则关闭连接、退出进程
      if(strncmp(buf,"quit",4) == 0)
      {
      close(cfd);
      exit(0);
      }

      printf("ser:%s\n",buf);
      char sbuf[1024];
      sprintf(sbuf,"server+%s\n",buf);
      // 将拼接好的数据发回客户端
      write(cfd,sbuf,strlen(sbuf)+1);

      return ;
      }

      // 提前注册SIGIO异步IO信号的回调函数
      // 当套接字有IO事件(收到数据),内核发送SIGIO信号,调用sighandler
      signal(SIGIO,sighandler);

      // 循环等待客户端连接
      while(1)
      {
      // 5.阻塞等待客户端连接,获取客户端套接字文件描述符
      int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen);
      if(connfd < 0)
      {
      perror("accept fail");
      return -1;
      }

      复制代码
       cfd = connfd;   // 将客户端描述符赋值给全局变量,供信号函数使用
      
       printf("accept success\n");
       printf("--------------------\n");
       printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr));
       printf("port	   = %d\n",ntohs(cliaddr.sin_port));
       printf("--------------------\n");
      
       // 6.F_SETOWN:设置该文件描述符的信号接收者为本进程
       // 当connfd产生IO事件时,内核把SIGIO信号发给当前进程PID
       fcntl(connfd,F_SETOWN,getpid());
      
       // 7.获取套接字原来的文件状态标志
       int flag;
       flag = fcntl(connfd,F_GETFL,0);
       // 8.追加 O_ASYNC(异步IO标志),开启信号驱动IO
       // 开启之后:套接字有数据可读 → 内核发送SIGIO信号 → 执行sighandler
       fcntl(connfd,F_SETFL,flag|O_ASYNC);

      }

信号IO能够实现非阻塞非轮询的异步操作IO,但同时也存在多路IO无法处理的问题,因此引入IO的多路复用函数select()

select 属于IO 多路复用 。 进程把一批文件描述符 (fd) 交给内核;然后阻塞在select(),由内核监视这些 fd;只要其中一个或多个 fd 就绪 (可读 / 可写 / 异常),select 就返回,用户程序再遍历所有 fd 处理 IO。

复制代码
进程 → select(阻塞) → 内核监听多个fd
                ↓
        某个fd数据就绪 → select唤醒返回
                ↓
        用户循环扫描所有fd,找出就绪的

select函数原型:

复制代码
int select(       int nfds, 
            fd_set *readfds, 
			fd_set *writefds,
            fd_set *exceptfds, 
			struct timeval *timeout);
功能:
   实现IO多路复用
   监控多路IO,看哪一路IO就绪
参数:
  @nfds           //最大的文件描述符+1(表示的是三个集合中最大的文件描述符加+1)
  @readfds        //存放 关心 读操作 的哪些文件描述符
  @writefds       //存放 关心 写操作 的哪些文件描述符
  @exceptfds      //存放 关心 出错操作 的哪些文件描述符
  @timeout        //设置一个超时时间 
                  //NULL    --- 表示select 阻塞调用
				  //时间为0  --- 表示select 非阻塞 
                  延时时间的结构体struct timeval {
					   long    tv_sec;         /* seconds */
					   long    tv_usec;        /* microseconds */
				   };
                 说明:
				     这个超时时间
					 递减,直到减成0
					 t中的值 就已经是0
返回值:
  成功 返回 !!!就绪的文件描述符的数量!!!     因此要备份nfds
  失败 返回-1 && errno会被设置

同时,为了适配select函数的执行,有以下的宏函数

复制代码
为了配合select函数执行,有如下宏函数:
	void  FD_CLR(int fd, fd_set *set);
	功能:将指定的set集合中编号为fd的描述符号删除。

	int   FD_ISSET(int fd, fd_set *set);
	功能:判断值为fd的描述符是否在set集合中,
		  如果在则返回真,否则返回假。

	void  FD_SET(int fd, fd_set *set);
	功能:将指定的fd描述符,添加到set集合中。

	void  FD_ZERO(fd_set *set);
	功能:将指定的set集合中所有描述符删除。

set其实就是一个表,类型为fd_set的表,用来存储要监控的文件描述如有哪些,有一个,就用一次宏函数FD_SET写入到集合里,然后在调用select来监控。因为每次调用select都会返回响应了的文件描述符的集合,所以传入set都需要进行备份,不然循环监控,响应一次之后,后面都不监控了,只监控上一次select中响应了的文件描述符fd。

作业:用select多路复用IO实现服务器与客户端点对点实时聊天,双向通信。

  1. 初始化 select 文件描述符集合 监听两个可读事件:

    1. fd=0:键盘标准输入,服务器控制台输入消息发给客户端。
    2. connfd:客户端套接字,接收客户端发来的消息。
  2. fcntl 设置 0、connfd 为非阻塞 IO read、fgets、write 不会卡住程序。

  3. 进入 while (1) 永久循环

    1. 每次循环先把备份好的backfds重新赋值给rdfdsselect 会修改 rdfds,就绪以外的 fd 会被清除,所以每次都要重置
    2. 调用select(),超时时间 1000 微秒。
      1. ret<0:select 出错,程序退出
      2. ret==0:超时,没有键盘输入、也没有客户端消息;回到循环开头重新 select
      3. ret>0:有文件描述符就绪,开始遍历检查就绪 fd
  4. 遍历判断就绪的文件描述符

    1. 如果就绪 fd == 0:读取键盘输入,发送字符串给客户端;输入quit关闭套接字,程序退出。
    2. 如果就绪 fd == connfd:读取客户端发来的数据,打印在服务器终端;客户端发送quit,服务器关闭套接字退出。
  5. 一轮收发完成后,回到 while 循环顶部,继续等待下一次可读事件。

    #include<stdio.h>
    #include<sys/types.h> /* See NOTES */
    #include<sys/socket.h>
    #include<string.h>
    #include<arpa/inet.h>
    #include<unistd.h>
    #include<sys/wait.h>
    #include<stdlib.h>
    #include<signal.h>
    #include<fcntl.h>

    int main(int argc, const char *argv[])
    {
    // 1.创建TCP流式套接字,IPv4协议族,TCP(SOCK_STREAM),协议号自动填0
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd < 0)
    {
    // 创建套接字失败打印错误信息
    perror("socket fail");
    return -1;
    }
    printf("fd = %d\n",fd);

    复制代码
     // 定义服务器地址结构体,存放服务器IP+端口
     struct sockaddr_in seraddr;
     // 将结构体内存全部清零,防止垃圾数据
     bzero(&seraddr,sizeof(seraddr));
     seraddr.sin_family = AF_INET;         // 指定地址族为IPv4
     seraddr.sin_port = htons(50000);      // 绑定端口50000,主机字节序转网络大端序
     seraddr.sin_addr.s_addr = inet_addr("192.168.31.94"); // 绑定本机指定IP地址
    
     // 将套接字fd与IP、端口进行绑定
     if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0)
     {
     	perror("bind fail");
     	return -1;
     }
    
     // 开启监听,设置内核等待队列最大挂起连接数为5
     int ret = listen(fd,5);
     if(ret < 0)
     {
     	perror("listen fail");
     	return -1;
     }
     
     struct sockaddr_in cliaddr;         // 用来保存连接上来的客户端地址信息
     socklen_t addrlen = sizeof(cliaddr);
     // 阻塞等待客户端连接,成功返回新通信文件描述符connfd
     int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen);
     if(connfd < 0)
     {
     	perror("accept fail");
     	return -1;
     }
    
     printf("accept success\n");
     printf("--------------------\n");
     printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr));  //打印客户端IP
     printf("port	   = %d\n",ntohs(cliaddr.sin_port));          //打印客户端端口
     printf("--------------------\n");
    
     fd_set rdfds;        // 可读文件描述符集合,select要用
     FD_ZERO(&rdfds);     // 清空文件描述符集合
     FD_SET(0,&rdfds);    // 将标准输入(键盘,fd=0)加入监听集合
     FD_SET(connfd,&rdfds);// 将客户端通信套接字connfd加入监听集合
    
     fd_set backfds = rdfds; // 备份原始的文件描述符集合(select每次会清空就绪位)
     char buf[1024];         // 收发数据缓冲区
     int nfds = connfd + 1;  // select第一个参数:最大文件描述符+1
    
     //==== 设置标准输入为非阻塞 ====
     int flag = fcntl(0,F_GETFL);        // 获取0号文件描述符原有属性
     fcntl(0,F_SETFL,flag | O_NONBLOCK); // 添加非阻塞标志
    
     //==== 设置客户端套接字connfd为非阻塞 ====
     flag = fcntl(connfd,F_GETFL);
     fcntl(connfd,F_SETFL,flag | O_NONBLOCK);
    
    
     // 循环监听键盘输入 和 客户端发来的数据
     while(1)
     {
     	rdfds = backfds;   // 每次循环重置监听集合,select会修改rdfds
     	struct timeval t = {0,1000}; // select超时时间:0秒 + 1000微秒
     	int ret = select(nfds,&rdfds,NULL,NULL,&t); //select只监听可读事件
     	if(ret < 0)
     	{
     		perror("select fail");
     		return -1;
     	}
    
     	// ret>0:有文件描述符就绪(有数据可读)
     	if(ret > 0)
     	{
     		int i = 0;
     		for(i=0;i<nfds;i++)  //遍历所有被监听的fd
     		{
     			if(FD_ISSET(i,&rdfds)) //判断i这个文件描述符是否就绪
     			{
     				if(i == 0)   //就绪的是键盘标准输入
     				{
     					fgets(buf,sizeof(buf),stdin);          //读取键盘输入
     					write(connfd,buf,strlen(buf)-1);       //发送给客户端,去掉回车符
     					if(strncmp(buf,"quit",4) == 0)         //输入quit就退出程序
     					{
     						close(fd);
     						close(connfd);
     						exit(0);
     					}
     				}else if(i == connfd) //就绪的是客户端套接字,客户端发来了消息
     				{
     					int ret = read(connfd,buf,sizeof(buf)); //读取客户端数据
     					if(strncmp(buf,"quit",4) == 0)          //客户端发quit,服务器退出
     					{
     						close(fd);
     						close(connfd);
     						exit(0);
     					}
     					printf("cli to ser:%s\n",buf);          //打印客户端消息
     				}
     			}
     		}
     	}
         // ret==0:select超时,没有任何fd就绪,回到循环继续select
     }
    
     return 0;

    }

相关推荐
也曾看到过繁星23 分钟前
Linux---vim 编辑器
linux·编辑器·vim
深念Y25 分钟前
# CC-Switch + Claude/Codex 折腾教训记录
运维·服务器·网络·ai·agent·web·ccsiwtch
是小李呀28 分钟前
Linux 服务器从零部署 kkFileView 完全指南(Docker 方案)
linux
OpenCloudOS32 分钟前
高危|Linux 内核 XFS reflink 本地提权漏洞修复指南
linux·网络·安全
黑泽明*37 分钟前
云计算与服务器基础入门指南
服务器·云计算·perl
小HANN1 小时前
保姆级实战:CentOS7 搭建 LAMP 环境部署 WordPress 个人博客
linux·运维·服务器·经验分享
mayaairi1 小时前
JS DOM与事件处理完全指南
服务器·前端·javascript
前端 贾公子1 小时前
Linux云服务器安装与PostgreSQL安装
运维·服务器
严谨的麻辣烫1 小时前
批量静态 IP 如何管理?用 Python 建立一个简单的 IP 资源监控方案
运维·服务器·网络·python·tcp/ip