热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

大家好,本篇文章主要讲的是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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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服务器

在这里插入图片描述

客户端Ⅰ

在这里插入图片描述

客户端Ⅱ

在这里插入图片描述

前言

和之前的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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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服务器

在这里插入图片描述

客户端Ⅰ

在这里插入图片描述

客户端Ⅱ

在这里插入图片描述


推荐阅读
author-avatar
mobiledu2502863117
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有