多人聊天室简易实现(完善中) 多人聊天室1. 预备知识1.1 select()select()可同时监听多个 sockets可通知程序哪些 sockets 有数据可以读取哪些 sockets 可以写入。类似的还有 poll()、epoll()1.1.1 基本语法#includesys/time.h#includesys/types.h#includeunistd.h// 该函数通过 readfds、writefds、exceptfds 监听文件描述符的 sets// 若可读取某个文件描述符只需将 sockfd 新增到 readfds 中// numfds 需设置为文件描述符的最高值加 1intselect(intnumfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*timeout);当select()返回时readfds 会被修改用来反映设置的文件描述符中有哪些数据可以读取。可使用下述的FD_ISSET()宏来获取可读的文件描述符。FD_SET(intfd,fd_set*set);// 将 fd 新增到 setFD_CLR(intfd,fd_set*set);// 从 set 中移除 fdFD_ISSET(intfd,fd_set*set);// 若 fd 在 set 中返回 trueFD_ZERO(fd_set*set);// 将 set 整个清为 0select()同时可通过结构体struct timeval来设置 timeout 的周期structtimeval{inttv_sec;// 秒inttv_usec;// 微秒}2. 简易多人聊天室实现已实现功能输入quit退出无客户端连接1分钟后退出支持多语言输入待完成功能高并发连接2.1 代码实现// multiChat.c#includestdio.h#includeunistd.h#includestdlib.h#includearpa/inet.h#includestring.h#includesys/socket.h#includesys/time.h#includesys/types.h#includetime.h#includenetinet/in.h#includenetdb.h#definePORT9034void*get_in_addr(structsockaddr*sa){if(sa-sa_familyAF_INET){return(((structsockaddr_in*)sa)-sin_addr);}return(((structsockaddr_in6*)sa)-sin6_addr);}intmain(void){fd_set master;// 主要的文件描述符setfd_set read_fds;// 暂存fd setstructaddrinfohints,*ai,*p;structsockaddr_storageremoteaddr;// client addrintyes1;intrv;intlistener;intfdmax;intnewfd;inti,j,v;socklen_taddrlen;charbuf[4096];// 存储 client 数据的缓冲区intnbytes;charremoteIP[INET6_ADDRSTRLEN];intclients0;time_tidle_start;structtimevaltv;memset(hints,0,sizeof(hints));hints.ai_familyAF_UNSPEC;hints.ai_socktypeSOCK_STREAM;hints.ai_flagsAI_PASSIVE;// getaddrinfoif((rvgetaddrinfo(NULL,PORT,hints,ai))!0){fprintf(stderr,selectserver: %s\n,gai_strerror(rv));exit(1);}// 遍历 ai 中获取的地址信息for(pai;p!NULL;pp-ai_next){// socketif((listenersocket(p-ai_family,p-ai_socktype,p-ai_protocol))0){continue;}// 避开错误信息 address already in usesetsockopt(listener,SOL_SOCKET,SO_REUSEADDR,yes,sizeof(int));// bindif(bind(listener,p-ai_addr,p-ai_addrlen)0){close(listener);// 避免文件描述符泄露continue;}break;}if(pNULL){fprintf(stderr,selectserver: failed to bind\n);exit(2);}freeaddrinfo(ai);// 释放链表// listenif(listen(listener,10)-1){perror(listen);exit(3);}// 将 listener 新增到 master setFD_SET(listener,master);// 持续追踪最大的 fdfdmaxlistener;idle_starttime(NULL);// 主要循环for(;;){read_fdsmaster;tv.tv_sec1;tv.tv_usec0;if(select(fdmax1,read_fds,NULL,NULL,tv)-1){perror(select);exit(4);}// 定时器无客户端连接超过60秒则关闭服务器if(clients0time(NULL)-idle_start60){printf(selectserver空闲超时服务器关闭\n);break;}// 在现存的连接中寻找需要读取的数据for(i0;ifdmax;i){if(FD_ISSET(i,read_fds)){if(ilistener){// 有新连接进入addrlensizeof(remoteaddr);// acceptnewfdaccept(listener,(structsockaddr*)remoteaddr,addrlen);if(newfd-1){perror(accept);}else{FD_SET(newfd,master);if(newfdfdmax){fdmaxnewfd;}printf(selectserver新连接来自 %s套接字 %d 已进入聊天室\n,inet_ntop(remoteaddr.ss_family,get_in_addr((structsockaddr*)remoteaddr),remoteIP,INET6_ADDRSTRLEN),newfd);clients;}}else{// 处理来自 client 的数据if((nbytesrecv(i,buf,sizeof(buf),0))0){// 连接关闭if(nbytes0){printf(selectserver套接字 %d 已退出聊天室\n,i);}else{perror(recv);}close(i);FD_CLR(i,master);// 从 master set 中移出clients--;if(clients0)idle_starttime(NULL);}else{buf[strcspn(buf,\r\n)]0;if(strcmp(buf,quit)0){printf(selectserver套接字 %d 已退出聊天室\n,i);close(i);FD_CLR(i,master);clients--;if(clients0)idle_starttime(NULL);nbytessprintf(buf,套接字 %d 已退出聊天室\n,i);}for(j0;jfdmax;j){if(FD_ISSET(j,master)){if(j!listenerj!i){if(send(j,buf,nbytes,0)-1){perror(send);}}}}}}}}}return0;}2.2 编译运行# 编译gcc-omultiChat_demo multiChat.c在同一台终端实验# 运行./multiChat_demo# 开启其他窗口至少两个因为发送的消息并不在服务器端显示# hostname porttelnet127.0.0.19034# 退出quit