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 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(¤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 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易聊天室內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 學習Linux網絡編程基本函數
- 使用C語言實現本地socke通訊的方法
- Vscode搭建遠程c開發環境的圖文教程
- epoll多路復用的一個實例程序(C實現)
- 探析如何使用SystemTap觀測TCP Backlog