Linux(程序设计):56---poll复用技术实现在线群聊程序

一、程序介绍

  • 我们本文使用poll I/O复用来实现一个聊天室程序,能让所有的用户在线群聊
  • 服务端有一个功能:
    • 接收客户端数据,并把客户端数据发送给每一个登录到该服务器上的客户端(数据发送者除外)
  • 客户端有两个功能:
    • 1.从标准输入终端读入数据,并将用户数据发送至服务器
    • 2.往标准输出终端打印服务器发送给自己的数据

二、客户端程序设计

//poll_cli.cpp

#define _GNU_SOURCE 1

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <libgen.h>
#include <errno.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>

#define BUFFER_SIZE 1024 //接受数据的缓冲区


int setnonblocking(int fd);

int main(int argc,char* argv[])
{
    //检测是否输入要连接的服务端IP和端口
    if(argc!=3){
        printf("usage:./%s [server ip] [server port]\n",basename(argv[0]));
        exit(EXIT_FAILURE);
    }

    int cli_fd,ser_port;
    const char *ser_ip;
    struct sockaddr_in ser_address;

    //创建套接字
    if((cli_fd=socket(AF_INET,SOCK_STREAM,0))==-1){
        perror("socket");
        exit(EXIT_FAILURE);
    }

    //服务端IP和端口
    ser_ip=argv[1];
    ser_port=atoi(argv[2]);

    //初始化服务端地址
    bzero(&ser_address,sizeof(ser_address));
    ser_address.sin_family=AF_INET;
    ser_address.sin_port=htons(ser_port);
    if(inet_pton(AF_INET,ser_ip,&ser_address.sin_addr.s_addr)==-1){
        perror("inet_pton");
        close(cli_fd);
        exit(EXIT_FAILURE);
    }

    //连接服务端
    if(connect(cli_fd,(struct sockaddr*)&ser_address,sizeof(ser_address))==-1){
        perror("connect");
        exit(EXIT_FAILURE);
    }

    //setnonblocking(cli_fd);

    int poll_ret_val;
    int pipe_fd[2];
    char recv_buff[BUFFER_SIZE];
    struct pollfd fd_array[2];
    bzero(fd_array,sizeof(fd_array));
    bzero(pipe_fd,sizeof(pipe_fd));

    //因为下面需要使用splice函数,创建用到的管道
    if(pipe(pipe_fd)==-1){
        perror("pipe");
        close(cli_fd);
        exit(EXIT_FAILURE);
    }

    //标准输入设置为可以判断:有数据可读
    fd_array[0].fd=0;
    fd_array[0].events=POLLIN;
    fd_array[0].revents=0;
    //连接的套接字设置为可以判断:服务端发送数据过来可读与TCP被对方(服务端)断开连接
    fd_array[1].fd=cli_fd;
    fd_array[1].events=POLLIN|POLLRDHUP;
    fd_array[1].revents=0;

    //开始poll轮询事件表
    while(1)
    {
        //参数3:-1永远阻塞等待
        poll_ret_val=poll(fd_array,2,-1);

        //poll出错,终止程序
        if(poll_ret_val==-1){
            perror("poll");
            close(cli_fd);
            exit(EXIT_FAILURE);
        }

        /*服务端关闭连接
        一定要放在POLLIN前处理,否则客户端将永远接收不到服务端断开的消息,因为
        服务端断开也可以当做POLLIN事件处理
        */
        if(fd_array[1].revents&POLLRDHUP){
            printf("server close\n");
            break;;
        }

        //接收到服务端的数据,打印到标准输出
        else if(fd_array[1].revents&POLLIN){
            int recv_ret_value;
            bzero(recv_buff,sizeof(recv_buff));
            //接收数据
            recv_ret_value=recv(fd_array[1].fd,recv_buff,sizeof(recv_buff)-1,0);
            if(recv_ret_value<0){
                if(errno!=EAGAIN){
                    perror("recv");
                    close(cli_fd);
                    exit(EXIT_FAILURE);
                }
            }
            printf("recv:%s\n",recv_buff);
        }

        //标准输入输入数据,发送给服务端
        if(fd_array[0].revents&POLLIN){
            //从标准输入发送到管道的写端(pipe_fd[1]为写端,数据会发送到管道的读端pipe_fd[0])
            if(splice(0,NULL,pipe_fd[1],NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE)==-1){
                perror("splice");
                close(cli_fd);
                exit(EXIT_FAILURE);
            }
            //从管道读端(pipe_fd[0])读到数据,发送给服务端
            if(splice(pipe_fd[0],NULL,cli_fd,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE)==-1){
                perror("splice");
                close(cli_fd);
                exit(EXIT_FAILURE);
            }
        }
    }
    
    close(cli_fd);
    exit(EXIT_SUCCESS);
}

int setnonblocking(int fd)
{
    int old_options,new_options;
    if((old_options=fcntl(fd,F_GETFL))==-1){
        perror("fcntl");
        exit(EXIT_FAILURE);
    }

    new_options=old_options|O_NONBLOCK;
    if(fcntl(fd,F_SETFL,new_options)==-1){
        perror("fcntl");
        exit(EXIT_FAILURE);
    }

    return old_options;
}

三、服务端程序设计

//poll_ser.cpp
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>

#define LISTEN_NUM     5   //服务端监听(listen)客户端的最大数量值
#define CLI_LIMIT_NUM  5   //允许处理的客户端数量
#define BUFFER_SIZE    1024//读缓冲区大小

//客户端结构体,存放:客户端地址、待写到客户端数据的位置、从客户端读入的数据
struct client_data
{
    struct sockaddr_in address;
    char* write_buf;
    char recv_buf[BUFFER_SIZE];
};


int setnonblocking(int fd);

int main(int argc,char* argv[])
{
    if(argc!=3){
        printf("usage:./%s [server ip] [server port]\n",basename(argv[0]));
        exit(EXIT_FAILURE);
    }

    int ser_fd,port;
    const char* ip;

    //创建socket
    if((ser_fd=socket(AF_INET,SOCK_STREAM,0))==-1){
        perror("socket");
        exit(EXIT_FAILURE);
    }

    //服务端IP、端口
    ip=argv[1];
    port=atoi(argv[2]);

    //初始化服务端地址
    struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));
    ser_addr.sin_family=AF_INET;
    ser_addr.sin_port=htons(port);
    if(inet_pton(AF_INET,ip,&ser_addr.sin_addr.s_addr)==-1){
        perror("inet_pton");
        close(ser_fd);
        exit(EXIT_FAILURE);
    }

    //绑定服务端地址
    if(bind(ser_fd,(struct sockaddr*)&ser_addr,sizeof(ser_addr))==-1){
        perror("bind");
        close(ser_fd);
        exit(EXIT_FAILURE);
    }

    //监听
    if(listen(ser_fd,LISTEN_NUM)==-1){
        perror("listen");
        close(ser_fd);
        exit(EXIT_FAILURE);
    }

    //当前客户端的数量
    int client_counter,poll_ret_value;
    client_counter=0;
    //存放客户端信息的数组
    struct client_data *client_array=new struct client_data[CLI_LIMIT_NUM];

    //poll可以处理的事件数组,为客户端最大数量+1(标准输入)
    struct pollfd fdarray[CLI_LIMIT_NUM+1];
    //初始化poll事件数组
    for(int i=1;i<=CLI_LIMIT_NUM;++i){
        fdarray[i].fd=-1;
        fdarray[i].events=0;
    }
    /*
    设置服务端监听socket可以处理:
        1.当有客户端连接时,监听socket会变为可读(EPOLLIN)
        2.处理服务端socket的错误(EPOLLERR)
    */
    fdarray[0].fd=ser_fd;
    fdarray[0].events=POLLIN|POLLERR;
    fdarray[0].revents=0;

    //开始poll轮询事件表
    while(1)
    {
        /*参数2:有客户端连接时,client_counter就会增加,
                所以我们只需要轮询client_counter+1即可
          参数3:-1永远阻塞等待
        */
        poll_ret_value=poll(fdarray,client_counter+1,-1);

        //poll出错,退出轮询,完成一些程序清理工作
        if(poll_ret_value==-1){
            perror("listen");
            break;
        }

        //轮询标准输入+所有的客户端
        for(int i=0;i<client_counter+1;++i)
        {
            //如果是服务端socket并且有客户端连接,处理新连接的客户端(客户端连接触发EPOLLIN事件)
            if((fdarray[i].fd==ser_fd)&&(fdarray[i].revents==POLLIN)){
                int cli_fd;
                char cli_ip[24];
                struct sockaddr_in cli_addr;
                bzero(&cli_addr,sizeof(cli_addr));
                socklen_t cli_addr_len=sizeof(cli_addr);
                //等待接收新连接的客户端
                if((cli_fd=accept(ser_fd,(struct sockaddr*)&cli_addr,&cli_addr_len))==-1){
                    perror("accept");
                    continue;
                }

                //如果要处理的客户端数量已经达到上限,则关闭新连接的客户端
                if(client_counter>=CLI_LIMIT_NUM){
                    //将信息告诉标准输出与连接的客户端,并关闭新连接的客户端
                    const char* info="too many client";
                    printf("accept:%s\n",info);
                    if(send(cli_fd,info,strlen(info),0)==-1){
                        perror("send");
                        continue;
                    }
                    close(cli_fd);
                }

                //如果要处理的客户端数量还没达到最大数量,那么接收新客户端,并做一些处理工作
                client_counter++;
                client_array[client_counter].address=cli_addr;//放入到客户端数组
                client_array[client_counter].write_buf=NULL;
                //client_array[client_counter].recv_buf=NULL;

                setnonblocking(cli_fd);//设置为非阻塞
                
                //将新客户端放入到事件表中
                fdarray[client_counter].fd=cli_fd;
                fdarray[client_counter].events=POLLIN|POLLOUT|POLLRDHUP|POLLERR;
                fdarray[client_counter].revents=0;

                bzero(cli_ip,sizeof(cli_ip));
                inet_ntop(AF_INET,&cli_addr.sin_addr.s_addr,cli_ip,sizeof(cli_ip));
                printf("comes a new client,ip:%s,port:%d\n",cli_ip,ntohs(cli_addr.sin_port));
            }

            //如果某个套接字出现错误,获取错误码
            else if(fdarray[i].revents&POLLERR){
                char error_buf[50];
                bzero(error_buf,sizeof(error_buf));
                socklen_t error_buf_len=sizeof(error_buf);

                //获取错误码
                if(getsockopt(fdarray[i].fd,SOL_SOCKET,SO_ERROR,error_buf,&error_buf_len)==-1){
                    perror("getsockopt");
                    continue;
                }
                printf("get an error from socket %d,error:%s\n",fdarray[i].fd,error_buf);
            }

            /*如果有个客户端断开连接,关闭客户端并做处理工作
            同客户端程序,也要放在POLLIN事件前处理
            */
            else if(fdarray[i].revents&POLLRDHUP){
                //将客户端数组的最后一个客户端移动到当前要删除的客户端位置处保存
                client_array[i]=client_array[client_counter];
                close(fdarray[i].fd); //关闭客户端套接字
                fdarray[i]=fdarray[client_counter]; //将最后一个客户端的事件表存放到当前要删除的客户端位置处保存
                i--; //for循环次数减1
                client_counter--;
                printf("a client close\n");
            }

            //如果客户端有数据可读
            else if(fdarray[i].revents&POLLIN){
                //获取客户端的套接字,并从套接字读取数据
                int recv_ret_value;
                //int conn_fd=fdarray[].fd;
                bzero(client_array[i].recv_buf,BUFFER_SIZE);
                recv_ret_value=recv(fdarray[i].fd,client_array[i].recv_buf,BUFFER_SIZE-1,0);

                //如果读取失败,说明出错
                if(recv_ret_value<0){
                    if(errno!=EAGAIN){//错误编码不是EAGAIN,则关闭客户端套接字
                        close(fdarray[i].fd); //关闭套接字
                        //交换客户端数组
                        client_array[i]=client_array[client_counter];
                        //交换POLL事件数组对象
                        fdarray[i].fd=fdarray[client_counter].fd;
                        i--;
                        client_counter--;
                    }
                }
                else if(recv_ret_value==0){  //无数据可读 
                }
                else{
                    //将数据发送给其他客户端
                    for(int j=1;j<=client_counter;j++){
                        if(fdarray[j].fd==fdarray[i].fd)
                            continue;
                        
                        /*发送之后注册fdarray[i]上的事件为可写事件
                        fdarray[i].events|=~POLLIN;
                        fdarray[i].events|=POLLOUT;*/
                        client_array[j].write_buf=client_array[i].recv_buf;
                    }
                }
            }

            //客户端套接字可以写
            else if(fdarray[i].revents&POLLOUT){
                int send_ret_value;
                //int conn_fd=fdarray[i].fd;
                //如果客户端写缓冲区为空
                if(!client_array[i].write_buf)
                    continue;
                //发送数据
                if(send(fdarray[i].fd,client_array[i].write_buf,strlen(client_array[i].write_buf),0)==-1){
                    if(errno!=EAGAIN){//错误编码不是EAGAIN,则关闭客户端套接字
                        close(fdarray[i].fd); //关闭套接字
                        //交换客户端数组
                        client_array[i]=client_array[client_counter];
                        //交换POLL事件数组对象
                        fdarray[i].fd=fdarray[client_counter].fd;
                        i--;
                        client_counter--;
                    }
                }
                //清空写缓冲区
                client_array[i].write_buf=NULL;
                /*发送之后重新注册fdarray[i]上的可读事件
                fdarray[i].events|=~POLLOUT;
                fdarray[i].events|=POLLIN;*/
            }
        }
    }

    delete[] client_array;
    client_array=NULL;
    close(ser_fd);

    exit(EXIT_SUCCESS);
}

int setnonblocking(int fd)
{
    int old_options,new_options;
    if((old_options=fcntl(fd,F_GETFL))==-1){
        perror("fcntl");
        exit(EXIT_FAILURE);
    }

    new_options=old_options|O_NONBLOCK;
    if(fcntl(fd,F_SETFL,new_options)==-1){
        perror("fcntl");
        exit(EXIT_FAILURE);
    }

    return old_options;
}

四、演示效果

  • 开启服务端程序

  • 然后开启两个客户端去连接服务端

  • 可以看到服务端接收到了客户端的连接,并打印了客户端的信息

  • 此时两个客户端向服务端发送数据,对方都可以接收到

  • 当我们在客户端按下crtl+c终止程序时,服务端可以接受到客户端退出的消息

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值