目录
  • 前言
  • 多路复用的原理
  • 基本概念
    • select
    • fd_set
  • 服务器Code
    • 客户端Code
      • 效果演示
        • select服务器
        • 客户端Ⅰ
        • 客户端Ⅱ

      前言

      和之前的udp聊天室有异曲同工之处,这次我们客户端send的是一个封装好了的数据包,recv的是一个字符串,服务器recv的是一个数据包,send的是一个字符串,在用户连接的时候发送一个login请求,然后服务器端处理,并广播到其他客户端去

      多路复用的原理

      Linux下Select多路复用实现简易聊天室示例

      基本概念

      多路复用指的是:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。其实就是一种异步处理的操作,等待可运行的描述符。

      与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

      多路复用大体有三种实现方式分别是:

      select

      poll

      epoll

      本次代码主要是展示select的用法:

      select

      int select(int nfds, fd_set *readfds, fd_set *writefds,
                        fd_set *exceptfds, struct timeval *timeout);
      

      这个是Linux的man手册给出的select的声明

      第一个参数ndfs

      第一个参数是nfds表示的是文件描述集合中的最大文件描述符+1,因为select的遍历使用是[0,nfds)的

      第二个参数readfds

      readfds表示的是读事件的集合

      第三个参数writefds

      writefds表示的是读事件的集合

      第四个参数exceptfds

      exceptfds表示的是异常参数的集合

      第五个参数timeout

      表示的是超时时间,timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

      struct timeval{
        long tv_sec;    //second
        long tv_usec;   //microseconds
        }
      

      fd_set

      fd_set结构体的定义实际包含的是fds_bits位数组,该数组的每个元素的每一位标记一个文件描述符其大小固定,由FD_SETSIZE指定,一般而言FD_SETSIZE的大小为1024

      我们只用关心怎么使用即可:

      下面几个函数就是操作fd_set的函数

      void FD_ZERO(fd_set *fdset);           //清空集合
      
      void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中
      
      void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除
      
      int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 
      

      服务器Code

      实现的功能是:

      客户端连接到客户端时,服务器向其他客户端进行广播上线

      向服务器发送消息,然后服务器向其他客户端广播上线

      客户端退出,服务器向其他客户端广播

      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <fcntl.h>
      #include <netdb.h>
      #include <signal.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <netinet/ip.h>
      #include <arpa/inet.h>
      
      #define N 1024
      int fd[FD_SETSIZE];//用户集合,最大承受量
      
      typedef struct Msg{//消息的结构
          char type;//消息类型
          char name[20];
          char text[N];//消息内容
      }MSG;
      
      typedef struct User{
          int fd;
          struct User *next;
      }USE;
      USE *head;
      
      USE *init() {
          USE *p = (USE *)malloc(sizeof(USE));
          memset(p,0,sizeof(USE));
          p->next = NULL;
          return p;
      }
      
      void Link(int new_fd) {//将新连接加入用户列表里面
          USE *p = head;
          while(p->next) {
              p=p->next;
          }
          USE *k = (USE*)malloc(sizeof(USE));
          k->fd = new_fd;
          k->next = NULL;
          p->next = k;
      }
      
      void login(int fd,MSG msg) {
          USE *p = head;
          char buf[N+30];
          strcpy(buf,msg.name);
          strcat(buf,"上线啦!快来找我玩叭!");
          printf("fd = %d  %s\n",fd,buf);
          while(p->next) {//给其他用户发上线信息
              if(fd != p->next->fd)
                  send(p->next->fd,&buf,sizeof(buf),0);
              p = p->next;
          }
      //    puts("Over login");
      }
      
      void chat(int fd,MSG msg) {
      //    printf("%d\n",msg.text[0]);
          if(strcmp(msg.text,"\n") == 0) return;
          USE *p = head;
          char buf[N+30];
          strcpy(buf,msg.name);
          strcat(buf,": ");
          strcat(buf,msg.text);
          printf("%s\n",buf);
          while(p->next) {//给其他用户发信息
              if(fd != p->next->fd)
                  send(p->next->fd,&buf,sizeof(buf),0);
              p = p->next;
          }
      }
      
      void quit(int fd,MSG msg) {
          USE *p = head;
          char buf[N+30];
          strcpy(buf,msg.name);
          strcat(buf,"伤心的退出群聊!");
          printf("%s\n",buf);
          while(p->next) {//给其他用户发上线信息
              if(fd != p->next->fd)
                  send(p->next->fd,&buf,sizeof(buf),0);
              p = p->next;
          }
      }
      
      
      /*
       * 初始化TCP服务器,返回服务器的socket描述符
       * */
      
      
      int init_tcp_server(unsigned short port) {
          int ret;
          int opt;
          int listen_fd;
          struct sockaddr_in self;        // 监听描述符
      
          listen_fd = socket(AF_INET, SOCK_STREAM, 0);
          if (listen_fd < 0) {
              perror("socket");
              return -1;
          }
          // 配置监听描述符地址复用属性
          opt = 1;
          ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
          if (ret < 0) {
              perror("set socket opt");
              return -1;
          }
      
          // 填充服务器开放接口和端口号信息
          memset(&self, 0, sizeof(self));
          self.sin_family = AF_INET;
          self.sin_port = htons(port);
          self.sin_addr.s_addr = htonl(INADDR_ANY);
          ret = bind(listen_fd, (struct sockaddr *)&self, sizeof(self));
          if (ret == -1) {
              perror("bind");
              return -1;
          }
          // 默认socket是双向,配置成监听模式
          listen(listen_fd, 5);
      
          return listen_fd;
      }
      
      // 监听处理器
      int listen_handler(int listen_fd) {
          int new_fd;
          new_fd = accept(listen_fd, NULL, NULL);
          if (new_fd < 0) {
              perror("accpet");
              return -1;
          }
          return new_fd;
      }
      // 客户端处理器
      int client_handler(int fd) {
          int ret;
          MSG msg;
          // 读一次
          ret = recv(fd, &msg, sizeof(MSG), 0);//读取消息
      //    printf("name = %s\n",msg.name);
          if (ret < 0) {
              perror("recv");
              return -1;
          } else if (ret == 0) {//断开连接
              quit(fd,msg);
              return 0;
          } else {//数据处理
              if(msg.type == 'L') {//登陆处理
                  login(fd,msg);
              }
              else if(msg.type == 'C') {//聊天处理
                  chat(fd,msg);
              }
              else if(msg.type == 'Q') {//退出处理
                  quit(fd,msg);
              }
          }
      //    puts("Over client_handler");
          return ret;
      }
      // 标准输入处理器
      int input_handler(int fd) {
          char buf[1024];
          fgets(buf, sizeof(buf), stdin);
          buf[strlen(buf) - 1] = 0;
          printf("user input: %s\n",buf);
          return 0;
      }
      
      void main_loop(int listen_fd) {
          fd_set current, bak_fds;
          int max_fds;
          int new_fd;
          int ret;
      
          // 把监听描述符、标准输入描述符添加到集合
          FD_ZERO(&current);
          FD_SET(listen_fd, &current);
          FD_SET(0, &current);
          max_fds = listen_fd;
      
          while (1) {
              bak_fds = current;      // 备份集合
              ret = select(max_fds+1, &bak_fds, NULL, NULL, NULL);
              if (ret < 0) {
                  perror("select");
                  break;
              }
              // 判断内核通知哪些描述符可读,分别处理
              for (int i = 0; i <= max_fds; ++i) {
                  if (FD_ISSET(i, &bak_fds)) {
                      if (i == 0) {//服务器的输入端,可以做成广播
                          // 标准输入可读 fgets
                          input_handler(i);
                      } else if (i == listen_fd) {//新连接,也就是有用户上线
                          // 监听描述符可读  accept
                          new_fd = listen_handler(i);
                          if (new_fd < 0) {
                              fprintf(stderr, "listen handler error!\n");
                              return;
                          }
                          if(new_fd >= FD_SETSIZE) {
                              printf("客户端连接过多!");
                              close(new_fd);
                              continue;
                          }
                          // 正常连接更新系统的集合,更新系统的通信录
                          Link(new_fd);//将新的连接描述符放进链表里面
                          FD_SET(new_fd, &current);
                          max_fds = new_fd > max_fds ? new_fd : max_fds;
                      } else {
                          // 新的连接描述符可读  recv
                          ret = client_handler(i);
                          if (ret <= 0) {
                              // 收尾处理
                              close(i);
                              FD_CLR(i, &current);
                          }
                      }
                  }
              }
      //        puts("over loop!\n");
          }
      
      }
      
      int main()
      {
          int listen_fd;
          head = init();
          listen_fd = init_tcp_server(6666);
          if (listen_fd < 0) {
              fprintf(stderr, "init tcp server failed!\n");
              return -1;
          }
          printf("等待连接中...\n");
          main_loop(listen_fd);
          close(listen_fd);
          return 0;
      }
      

      客户端Code

      创建了 一个父子进程,父进程用于接受信息并打印到屏幕,子进程用于输入并发送信息

      //
      // Created by Mangata on 2021/11/30.
      //
      
      #include <stdio.h>
      #include <string.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <fcntl.h>
      #include <netdb.h>
      #include <signal.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <netinet/ip.h>
      #include <arpa/inet.h>
      #define N 1024
      char *ip = "192.168.200.130"; //106.52.247.33
      int port = 6666;
      char name[20];
      
      typedef struct Msg{//消息的结构
          char type;//消息类型
          char name[20];
          char text[N];//消息内容
      }MSG;
      
      /*
       * 初始化TCP客户端,返回客户端的socket描述符
       * */
      int init_tcp_client(const char *host) {
          int tcp_socket;
          int ret;
          struct sockaddr_in dest;
      
          tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
          if (tcp_socket == -1) {
              perror("socket");
              return -1;
          }
      
      
          memset(&dest, 0, sizeof(dest));
          dest.sin_family = AF_INET;
          dest.sin_port = htons(port);
          dest.sin_addr.s_addr = inet_addr(host);
          ret = connect(tcp_socket, (struct sockaddr *)&dest, sizeof(dest));
          if (ret < 0) {
              perror("connect");
              return -1;
          }
      //    int flags = fcntl(tcp_socket, F_GETFL, 0);       //获取建立的sockfd的当前状态(非阻塞)
      //    fcntl(tcp_socket, F_SETFL, flags | O_NONBLOCK);  //将当前sockfd设置为非阻塞
      
          printf("connect %s success!\n", host);
          return tcp_socket;
      }
      
      void login(int fd) {
          MSG msg;
          fputs("请输入您的名字: ",stdout);
          scanf("%s",msg.name);
          strcpy(name,msg.name);
          msg.type = 'L';
          send(fd,&msg,sizeof(MSG),0);
      }
      
      void chat_handler(int client_fd) {
          int ret;
          char buf[N+30];
          pid_t pid = fork();
          if(pid == 0) {
              MSG msg;
              strcpy(msg.name,name);
              while (fgets(buf, sizeof(buf), stdin)) {
                  if (strncmp(buf, "quit", 4) == 0) {// 客户端不聊天了,准备退出
                      msg.type = 'q';
                      send(client_fd,&msg,sizeof(MSG),0);
                      exit(1);
                  }
                  strcpy(msg.text,buf);
                  msg.type = 'C';
                  // 发送字符串,不发送'\0'数据
                  ret = send(client_fd, &msg, sizeof(MSG), 0);
                  if (ret < 0) {
                      perror("send");
                      break;
                  }
                  printf("send %d bytes success!\n", ret);
              }
          }
          else {
              while(1){
                  int rrt = recv(client_fd,&buf,sizeof(buf),0);
                  printf("rrt = %d\n",rrt);
                  if(rrt <= 0) {
                      printf("断开服务器!\n");
                      break;
                  }
      
                  fprintf(stdout,"%s\n",buf);
              }
          }
      
      
      }
      
      int main(int argc,char *argv[])
      {
          int client_socket;
      
          client_socket = init_tcp_client(ip);
          if (client_socket < 0) {
              fprintf(stderr, "init tcp client failed!\n");
              return -1;
          }
          login(client_socket);
          chat_handler(client_socket);
          close(client_socket);
          return 0;
      }
      

      效果演示

      select服务器

      Linux下Select多路复用实现简易聊天室示例

      客户端Ⅰ

      Linux下Select多路复用实现简易聊天室示例

      客户端Ⅱ

      Linux下Select多路复用实现简易聊天室示例

      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。