Linux下Select多路復(fù)用實現(xiàn)簡易聊天室示例
前言
和之前的udp聊天室有異曲同工之處,這次我們客戶端send的是一個封裝好了的數(shù)據(jù)包,recv的是一個字符串,服務(wù)器recv的是一個數(shù)據(jù)包,send的是一個字符串,在用戶連接的時候發(fā)送一個login請求,然后服務(wù)器端處理,并廣播到其他客戶端去
多路復(fù)用的原理

基本概念
多路復(fù)用指的是:通過一種機(jī)制,可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。其實就是一種異步處理的操作,等待可運(yùn)行的描述符。
與多進(jìn)程和多線程技術(shù)相比,I/O多路復(fù)用技術(shù)的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不必創(chuàng)建進(jìn)程/線程,也不必維護(hù)這些進(jìn)程/線程,從而大大減小了系統(tǒng)的開銷。
多路復(fù)用大體有三種實現(xiàn)方式分別是:
select
poll
epoll
本次代碼主要是展示select的用法:
select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
這個是Linux的man手冊給出的select的聲明
第一個參數(shù)ndfs
第一個參數(shù)是nfds表示的是文件描述集合中的最大文件描述符+1,因為select的遍歷使用是[0,nfds)的
第二個參數(shù)readfds
readfds表示的是讀事件的集合
第三個參數(shù)writefds
writefds表示的是讀事件的集合
第四個參數(shù)exceptfds
exceptfds表示的是異常參數(shù)的集合
第五個參數(shù)timeout
表示的是超時時間,timeout告知內(nèi)核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結(jié)構(gòu)用于指定這段時間的秒數(shù)和微秒數(shù)。
struct timeval{
long tv_sec; //second
long tv_usec; //microseconds
}
fd_set
fd_set結(jié)構(gòu)體的定義實際包含的是fds_bits位數(shù)組,該數(shù)組的每個元素的每一位標(biāo)記一個文件描述符其大小固定,由FD_SETSIZE指定,一般而言FD_SETSIZE的大小為1024
我們只用關(guān)心怎么使用即可:
下面幾個函數(shù)就是操作fd_set的函數(shù)
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); // 檢查集合中指定的文件描述符是否可以讀寫
服務(wù)器Code
實現(xiàn)的功能是:
客戶端連接到客戶端時,服務(wù)器向其他客戶端進(jìn)行廣播上線
向服務(wù)器發(fā)送消息,然后服務(wù)器向其他客戶端廣播上線
客戶端退出,服務(wù)器向其他客戶端廣播
#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{//消息的結(jié)構(gòu)
char type;//消息類型
char name[20];
char text[N];//消息內(nèi)容
}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) {//給其他用戶發(fā)上線信息
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) {//給其他用戶發(fā)信息
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) {//給其他用戶發(fā)上線信息
if(fd != p->next->fd)
send(p->next->fd,&buf,sizeof(buf),0);
p = p->next;
}
}
/*
* 初始化TCP服務(wù)器,返回服務(wù)器的socket描述符
* */
int init_tcp_server(unsigned short port) {
int ret;
int opt;
int listen_fd;
struct sockaddr_in self; // 監(jiān)聽描述符
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
return -1;
}
// 配置監(jiān)聽描述符地址復(fù)用屬性
opt = 1;
ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
if (ret < 0) {
perror("set socket opt");
return -1;
}
// 填充服務(wù)器開放接口和端口號信息
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;
}
// 默認(rèn)socket是雙向,配置成監(jiān)聽模式
listen(listen_fd, 5);
return listen_fd;
}
// 監(jiān)聽處理器
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 {//數(shù)據(jù)處理
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;
}
// 標(biāo)準(zhǔn)輸入處理器
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;
// 把監(jiān)聽描述符、標(biāo)準(zhǔn)輸入描述符添加到集合
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;
}
// 判斷內(nèi)核通知哪些描述符可讀,分別處理
for (int i = 0; i <= max_fds; ++i) {
if (FD_ISSET(i, &bak_fds)) {
if (i == 0) {//服務(wù)器的輸入端,可以做成廣播
// 標(biāo)準(zhǔn)輸入可讀 fgets
input_handler(i);
} else if (i == listen_fd) {//新連接,也就是有用戶上線
// 監(jiān)聽描述符可讀 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;
}
// 正常連接更新系統(tǒng)的集合,更新系統(tǒng)的通信錄
Link(new_fd);//將新的連接描述符放進(jìn)鏈表里面
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
創(chuàng)建了 一個父子進(jìn)程,父進(jìn)程用于接受信息并打印到屏幕,子進(jìn)程用于輸入并發(fā)送信息
//
// 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{//消息的結(jié)構(gòu)
char type;//消息類型
char name[20];
char text[N];//消息內(nèi)容
}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的當(dāng)前狀態(tài)(非阻塞)
// fcntl(tcp_socket, F_SETFL, flags | O_NONBLOCK); //將當(dāng)前sockfd設(shè)置為非阻塞
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) {// 客戶端不聊天了,準(zhǔn)備退出
msg.type = 'q';
send(client_fd,&msg,sizeof(MSG),0);
exit(1);
}
strcpy(msg.text,buf);
msg.type = 'C';
// 發(fā)送字符串,不發(fā)送'\0'數(shù)據(jù)
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("斷開服務(wù)器!\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服務(wù)器

客戶端Ⅰ

客戶端Ⅱ

到此這篇關(guān)于Linux下Select多路復(fù)用實現(xiàn)簡易聊天室示例的文章就介紹到這了,更多相關(guān)Linux下Select易聊天室內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Qt 使用 canon edsdk 實現(xiàn)實時預(yù)覽的示例代碼
這篇文章主要介紹了Qt 使用 canon edsdk 實現(xiàn)實時預(yù)覽的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
C++實現(xiàn)完整功能的通訊錄管理系統(tǒng)詳解
來了來了,通訊錄管理系統(tǒng)踏著七彩祥云飛來了,結(jié)合前面的結(jié)構(gòu)體知識和分文件編寫方法,我總結(jié)并碼了一個帶菜單的通訊錄管理系統(tǒng),在這篇文章中將會提到C的清空屏幕函數(shù),嵌套結(jié)構(gòu)體具體實現(xiàn),簡單且充實,跟著我的思路,可以很清晰的解決這個項目2022-05-05
VsCode搭建C語言運(yùn)行環(huán)境詳細(xì)過程及終端亂碼問題解決方案
這篇文章主要介紹了VsCode搭建C語言運(yùn)行環(huán)境以及終端亂碼問題解決,在VsCode中搭建C/C++運(yùn)行環(huán)境需要先安裝幾個插件,具體插件文中給大家詳細(xì)介紹,需要的朋友可以參考下2022-12-12
FFmpeg獲取網(wǎng)絡(luò)攝像頭數(shù)據(jù)解碼
這篇文章主要為大家詳細(xì)介紹了FFmpeg獲取網(wǎng)絡(luò)攝像頭數(shù)據(jù)解碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-06-06
C語言使用strcmp()函數(shù)比較兩個字符串的實現(xiàn)
這篇文章主要介紹了C語言使用strcmp()函數(shù)比較兩個字符串的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01

