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

目录
  • 前言
  • 多路复用的原理
  • 基本概念
    • select
    • fd_set
  • 服务器Code
  • 客户端Code
  • 效果演示
    • 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(&current);
    FD_SET(listen_fd, &current);
    FD_SET(0, &current);
    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, &current);
                    max_fds = new_fd > max_fds ? new_fd : max_fds;
                } else {
                    // 新的连接描述符可读  recv
                    ret = client_handler(i);
                    if (ret <= 0) {
                        // 收尾处理
                        close(i);
                        FD_CLR(i, &current);
                    }
                }
            }
        }
//        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易聊天室内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • linux内核select/poll,epoll实现与区别

    下面文章在这段时间内研究 select/poll/epoll的内核实现的一点心得体会: select,poll,epoll都是多路复用IO的函数,简单说就是在一个线程里,可以同时处理多个文件描述符的读写. select/poll的实现很类似,epoll是从select/poll扩展而来,主要是为了解决select/poll天生的缺陷. epoll在内核版本2.6以上才出现的新的函数,而他们在linux内核中的实现都是十分相似. 这三种函数都需要设备驱动提供poll回调函数,对于套接字而言,他们是

  • linux使用select实现精确定时器详解

    在编写程序时,我们经常会用到定时器.首先看看select函数原型如下: 复制代码 代码如下: int select(int nfds, fd_set *readfds, fd_set *writefds,                  fd_set *exceptfds, struct timeval *timeout); 参数说明:slect的第一个参数nfds为fdset集合中最大描述符值加1,fdset是一个位数组,其大小限制为__FD_SETSIZE(1024),位数组的每一位代表其

  • Linux shell select实现菜单选择

    假设需要在shell下实现一个菜单,估计会使用 循环和read实现交互,在学习中发现一个select可在shell下很方便的实现这种交互菜单 1. select 语法 select var in ... do #your code done 2. 示例 #! /bin/bash echo "what is your favorite color :" select color in "red" "blue" "green" &

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

    目录 前言 多路复用的原理 基本概念 select fd_set 服务器Code 客户端Code 效果演示 select服务器 客户端Ⅰ 客户端Ⅱ 前言 和之前的udp聊天室有异曲同工之处,这次我们客户端send的是一个封装好了的数据包,recv的是一个字符串,服务器recv的是一个数据包,send的是一个字符串,在用户连接的时候发送一个login请求,然后服务器端处理,并广播到其他客户端去 多路复用的原理 基本概念 多路复用指的是:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读

  • vue + socket.io实现一个简易聊天室示例代码

    vue + vuex + elementUi + socket.io实现一个简易的在线聊天室,提高自己在对vue系列在项目中应用的深度.因为学会一个库或者框架容易,但要结合项目使用一个库或框架就不是那么容易了.功能虽然不多,但还是有收获.设计和实现思路较为拙劣,恳请各位道友指正. 可以达到的需求 能查看在线用户列表 能发送和接受消息 使用到的框架和库 socket.io做为实时通讯基础 vuex/vue:客户端Ui层使用 Element-ui:客户端Ui组件 类文件关系图 服务端: 客户端: 服

  • python实现简易聊天室(Linux终端)

    本文实例为大家分享了python实现简易聊天室的具体代码,供大家参考,具体内容如下 群聊聊天室 1.功能:类似qq群聊功能 1.有人进入聊天室需要输入姓名,姓名不能重复 2.有人进入聊天室,其他人会受到通知 xxx进入聊天室 3.一个人发消息,其他人会受到消息 xxx:xxxxxxxx 4.有人退出聊天室,其他人也会收到通知 xxx退出聊天室 5.扩展功能:服务端消息公告,服务端发送消息所有人都能收到 管理员消息:xxxxxxxx 2.确定技术模型 1.服务端和客户端 服务端处理请求,发送管理员

  • php基于websocket搭建简易聊天室实践

    本文实例讲述了php基于websocket搭建简易聊天室实践.分享给大家供大家参考.具体如下: 1.前言 公司游戏里面有个简单的聊天室,了解了之后才知道是node+websocket做的,想想php也来做个简单的聊天室.于是搜集各种资料看文档.找实例自己也写了个简单的聊天室. http连接分为短连接和长连接.短连接一般可以用ajax实现,长连接就是websocket.短连接实现起来比较简单,但是太过于消耗资源.websocket高效不过兼容存在点问题.websocket是html5的资源 2.前

  • Nodejs实现多房间简易聊天室功能

    1.前端界面代码 前端不是重点,够用就行,下面是前端界面,具体代码可到github下载. 2.服务器端搭建 本服务器需要提供两个功能:http服务和websocket服务,由于node的事件驱动机制,可将两种服务搭建在同一个端口下. 1.包描述文件:package.json,这里用到了两个依赖项,mime:确定静态文件mime类型,socket.io:搭建websocket服务,然后使用npm install  安装依赖 { "name": "chat_room",

  • nodejs和react实现即时通讯简易聊天室功能

    npx create-react-app socketio-demo 进入socketio-demo目录 运行eject进行拆包,本项目也可以不拆,这是个人习惯. 注意如果运行eject命令最好在项目初始阶段执行,已经开始编写后不要再使用容易出现bug,新人谨慎使用eject命令 yarn eject 项目拆包后创建服务器文件夹和文件 mkdir server type null>index.js 创建完成后目录如下 编写即时通讯(聊天室)后台 安装nodejs插件 npm i express

  • golang基于websocket实现的简易聊天室程序

    本文实例讲述了golang基于websocket实现的简易聊天室.分享给大家供大家参考,具体如下: 先说点无关的,最近忙于工作没有更新博客,今天休息顺便把golang websocket研究了一下,挺好玩的,写了一个聊天室,分享给大家. websocket包 : code.google.com/p/go.net/websocket 文档 : http://go.pkgdoc.org/code.google.com/p/go.net/websocket 首先安装websocket包 复制代码 代码

  • Java基于socket实现简易聊天室实例

    本文实例讲述了Java基于socket实现简易聊天室的方法.分享给大家供大家参考.具体实现方法如下: chatroomdemo.java package com.socket.demo; import java.io.IOException; import java.net.DatagramSocket; public class ChatRoomDemo { /** * @param args * @throws IOException */ public static void main(S

  • Java Socket实现简易聊天室

    Java-Socket编程实现简易聊天室(TCP),供大家参考,具体内容如下 实现一个服务器接收多个客户端 测试: 首先启动服务器,然后启动三个客户端,输入三个不同的用户名,分别在聊天室发消息 看其他客户端是否能接收到消息 效果如下图: 一号发消息 可以在二号和三号客户端接收到消息 服务器设计 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import ja

  • node+socket实现简易聊天室功能

    本文实例为大家分享了node+socket实现简易聊天室的具体代码,供大家参考,具体内容如下 服务端 const net = require('net') const server = net.createServer() //用户列表 let clients = [] //监听连接 server.on('connection',client=>{ client.on('data',(chunk)=>{ let data = chunk.toString() if(data.match(/lo

随机推荐