这篇文章主要介绍“Linux下Select多路复用如何实现简易聊天室”,在日常操作中,相信很多人在Linux下Select多路复用如何实现简易聊天室问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Linux下Select多路复用如何实现简易聊天室”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
前言
和之前的udp聊天室有异曲同工之处,这次我们客户端send的是一个封装好了的数据包,recv的是一个字符串,服务器recv的是一个数据包,send的是一个字符串,在用户连接的时候发送一个login请求,然后服务器端处理,并广播到其他客户端去
多路复用的原理
基本概念
多路复用指的是:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。其实就是一种异步处理的操作,等待可运行的描述符。
与多进程和多线程技术相比,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 1024int 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; }}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(¤t); FD_SET(listen_fd, ¤t); FD_SET(0, ¤t); 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, ¤t); max_fds = new_fd > max_fds ? new_fd : max_fds; } else { // 新的连接描述符可读 recv ret = client_handler(i); if (ret <= 0) { // 收尾处理 close(i); FD_CLR(i, ¤t); } } } }// 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 1024char *ip = "192.168.200.130"; //106.52.247.33int port = 6666;char name[20];typedef struct Msg{//消息的结构 char type;//消息类型 char name[20]; char text[N];//消息内容}MSG;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多路复用如何实现简易聊天室”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!