zoukankan      html  css  js  c++  java
  • 局域网下C++命令行聊天室简易版

    利用C++在Linux环境下写了一个简单的命令行聊天服务器。主要用到的技术是socket,I/O复用(epoll),非阻塞IO,进程等知识。下面主要叙述其中的关键技术点以及编写过程中遇到的问题。

    0、聊天室的基本功能

    编写了一个简单的聊天室程序,该聊天室程序能够让所有的用户同时在线群聊,它分为服务器和客户端两个部分。

    • 服务器:接收客户端数据,并将该客户端数据发送给其他登录到该服务器上的客户端。
    • 客户端:从标准输入读入数据,并将数据发送给服务器,同时接收服务器发送的数据。

    1、服务器端IO模型

    采用IO复用+非阻塞IO的模型,IO复用采用Linux下的epoll机制。下面介绍epoll具体的函数。

    复制代码
    //实现epoll服务器端需要三个函数。
    //1)epoll_create:创建保持epoll文件描述符的空间,即epoll例程,size只是建议的例程大小。
    #include<sys/epoll.h>
    int epoll_create(int size);//成功时返回epoll文件描述符,失败时返回-1
    /**
    2)epoll_ctl:向空间注册并且注销文件描述符。
    要使用epoll_event结构体:
    struct epoll_event{
        __uint32_t events;
        epoll_data_t data;
    }
        typedef union epoll_data{
             void * ptr;
             int fd;
             __uint32_t u32;
             __uint64_t u64;
        }epoll_data_t;
    这里注意要声明足够大的epoll_event结构体数组后,传递给epoll_eait函数时,发生变化的文件描述符信息被填入该数组。可以直接申明也可以动态分配。
    op有三个宏选项:
    @EPOLL_CTL_ADD:将文件描述符注册到epoll例程。
    @EPOLL_CTL_DEL:从epoll例程中删除文件描述符。
    @EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况。
    events常用的可以保存的常量以及事件类型。
    @EPOLLIN:需要读取数据的情况.
    @EPOLLET:以边缘触发的方式得到事件通知。
    @EPOLLONESHOT:发生一次事件后,相应文件描述符不在接收事件通知,需要再次设置事件才能继续使用。
    

    /
    int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
    //成功时返回0,失败时返回-1
    int epoll_wait(int wpfd,struct epoll_event* events,int maxevents,int timeout);
    //成功时返回发生事件的文件描述符数,失败时返回-1

    复制代码

      首先epoll_create创建一个epoll文件描述符,底层同时创建一个红黑树,和一个就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。ET模式不会检查,只会调用一次。

    1.1 为什么IO复用需要搭配非阻塞IO?(select/epoll返回可读后还用非阻塞是不是没有意义?)

     问题分析:a、输入过程通常分为两个阶段1)等待数据准备好(等待数据从网络中到达,它被复制到内核中的某个缓冲区)。2)从内核向进程复制数据。

    阻塞IO模型和非阻塞IO模型如下:

    Linux下五种I/O模型:

    阻塞式IO,非阻塞式IO,IO复用,信号驱动式IO,异步IO(aio_系列);

    五种IO模型可以划分为两大类:同步IO(导致请求进程阻塞,直到IO操作完成)和异步IO(不导致请求进程阻塞);

    同步IO:阻塞式IO,非阻塞式IO,IO复用,信号驱动式IO;

    异步IO:异步IO;

    阻塞式IO,非阻塞式IO,同步IO,异步IO区别:

    • 调用阻塞式IO会一直阻塞住对应的进程直到操作完成,而非阻塞式IO在内核还准备数据的情况下会立刻返回,不断轮询数据是否准备好,进程不会进入睡眠。
    • 同步IO在IO操作时会阻塞进程,异步IO在IO操作时立即返回,不会阻塞,数据读好后内核通知进程取数据。

    b、文件描述符就绪条件有可读,可写或者出现异常。设置非阻塞的方法有两种一种是使用fcntl函数,另一种是通过socket API创建非阻塞的socket。

    int fd_sock = socket(AF_INET,SOCK_STREAM|SOCK_NONBLOCK,0);

    答:select/epoll返回了可读,并不代表一定能够读取数据,因为在返回可读到调用read函数之间,是有时间间隙的,这段时间内核可能将数据丢失。也有可以多个线程同时监听该套接字,数据也可能被其他线程读取。使用阻塞IO在这种情况下就会一直阻塞进程,而非阻塞IO在没有数据可读的情况下会返回一个错误。

    可以参考知乎这个问题  https://www.zhihu.com/question/37271342

    1.2、epoll的条件触发LT和边缘触发ET区别。

    答:条件触发方式中,只要输出缓冲中有数据就会一直注册该事件(这次没处理该事件,下次调用epoll_wait还会继续通告该事件)。

    边缘触发中输入缓冲收到数据时仅注册一次事件。

    边缘触发中,一旦发生输入相关事件,就应该读取输入缓冲中的全部数据,因此需要验证输入缓冲是否为空。read函数返回-1,变量errno中的值为EAGAIN时,说明没有数据可以读。

    边缘触发方式下,为什么要将套接字变为非阻塞模式呢?以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿,没有数据可读,就会一直阻塞进程,所以一定要采用非阻塞的IO函数。

    边缘触发的优点是:可以分离接收数据和处理数据的时间点。

    1.3、select和epoll的区别

    答:select缺点:

    1)针对所有文件描述符的循环语句;

    2)每次都需要向操作系统传递监视对象信息。

    最耗时间的是第二点向操作系统传递监视对象信息。

    epoll支持ET模式,而select只支持LT模式。select的优点是:

    1)服务器端接入者少的时候适用;

    2)兼容性好。

    1.4、服务器端发生地址分配错误(提前终止服务器端,重启的时候出现bind() error)

    答:原因是先断开的主机需要进过time-wait状态,套接字进过四次挥手最后要发送ACK(A->B),最后B接收到ACK才会正常关闭,如果没有收到,会超时重传。这个时候相应的端口处于正在使用的状态,所以bind()重新分配相同的IP和port就会出错。

    关闭方法:在套接字可选项中更改SO_REUSEADDR状态,将0改为1即可。(客户端是调用connect随机分配IP&port,所以不会出现该错误)

    1.5、多个客户端建立连接后,一个客户端突然断开(意外断电),如何在服务器端知道哪个客户端断开了?

    答:往一个已经关闭的客户端套接字发送信息,系统会发送SIGPIPE信号,这个信号对应的处理机制是终止、关闭。所以在服务器端需要把SIGPIPE设为SIG_IGN。

    但是还需要服务器端移除这个客户端文件描述符,就需要服务器知道哪个客户端挂了,1)服务器端设置socket套接字KEEPALIVE,TCP的长连接,用到心跳机制,就是不断的发送试探包,一定时间没响应就认为断开连接。

    在服务器端使用getsockopt得到每个客户端的连接状态(errno),这样就知道哪个客户端出错了。

    第二种方法是主流方法:第一次写会正常返回,第二次写就会引发SIGPIPE信号,返回值是-1,并将errno设置为EPIPE,perror打印错误为Broken pipe。可以在服务器注册该信号的处理函数。

    2、客户端client

    client采用分割读写的方法进行操作,子进程负责发送数据,父进程负责接收数据。

    分离流的好处:

    1)减低实现难度;

    2)与输入无关的输出操作可以提高速度。

    复制代码
        pid_t pid = fork();
        if(pid == 0){//子进程负责写
            write_routine(clntSock,buff);
        }
        else{//父进程负责读
            read_routine(clntSock,buff);
        }
    复制代码

    3、具体实现代码。

    复制代码
    //utility.h
    #ifndef _UTILITY_
    #define _UTILITY_
    

    include<sys/types.h>

    include<sys/socket.h>

    include<arpa/inet.h>

    include<stdio.h>

    include<stdlib.h>

    include<unistd.h>

    include<errno.h>

    include<string.h>

    include<fcntl.h>

    include<stdlib.h>

    include<sys/epoll.h>

    include<list>

    include<string>

    using namespace std;
    /存储客户端文件描述符/
    list
    <int> clientLists;

    #define MAX_EVENT_NUMBER 1024

    #define BUFF_SIZE 400

    /服务器ip/
    #define SERVERIP "127.0.0.1"

    /端口号(只要在1024~5000都行)/
    #define PORT "6666"

    /epoll例程大小/
    #define EPOLLSIZE 50

    #define EXIT "exit"

    /
    *将文件描述符设置成非阻塞的
    *返回文件描述符旧的状态,以便日后恢复该状态标志
    /
    int setNonBlocking(int fd){
    int oldOption = fcntl(fd,F_GETFL);
    int newOption = oldOption | O_NONBLOCK;
    fcntl(fd,F_SETFL,newOption);
    return oldOption;
    }

    /

    • 将文件描述符fd上的EPOLLIN注册到epollfd指示的内核事件表中

    • 参数enable_et指定是否对fd启用ET模式
      /
      void addfd(int epollfd,int fd,bool enable_et){
      epoll_event
      event;
      event.data.fd = fd;
      event.events = EPOLLIN;//主要读取客服端套接字信息
      if(enable_et){
      event.events |= EPOLLET;
      }
      setNonBlocking(fd);
      epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,
      &event);
      }
      /

    • 服务端向其他客户端发送消息
      /
      void sendBroadCast(struct epoll_event* waitEvents,int eventsNumber,int epollfd,int listenfd){
      int clntSock = 0;
      struct sockaddr_in clntAdr;
      char buff[BUFF_SIZE];
      for(int i = 0;i < eventsNumber;++i){
      if(waitEvents[i].data.fd == listenfd){//未建立连接,先建立连接
      socklen_t clientLength = sizeof(clntAdr);
      clntSock
      = accept(listenfd,(struct sockaddr) &clntAdr,&clientLength);
      addfd(epollfd,clntSock,
      true);
      /
      第一次connect/
      const char
      message = "welcome join chatting! ";
      printf(
      "%d join chatting!!! ",clntSock);
      write(clntSock,message,strlen(message));
      /将新clientID加入链表/
      clientLists.push_back(clntSock);
      /向例程中注册事件/
      addfd(epollfd,clntSock,
      true);
      }
      else{//已经建立连接,需要读取数据,然后发送给其他客户端
      clntSock = waitEvents[i].data.fd;
      bzero(
      &buff,strlen(buff));
      int strLen = sprintf(buff,"te clientID %d saying: ",clntSock);
      strLen
      += read(clntSock,buff + strLen,BUFF_SIZE);
      if(strLen < 0){//客户端读取数据出错
      perror("read");
      close(clntSock);
      exit(
      -1);
      }
      else if(strLen == 0){//已经没数据,需要关闭客户端
      epoll_ctl(epollfd,EPOLL_CTL_DEL,clntSock,NULL);
      clientLists.remove(clntSock);
      close(clntSock);
      }
      else{
      buff[strLen]
      = 0;
      /发送给其他的所有客户端/
      if(clientLists.size() == 1){
      const char *mess = "Atention!only one client in the chatting room! ";
      write(clntSock,mess,strlen(mess));
      printf(
      "Atention!only ID %d client in the chatting room! ",clntSock);
      }

               printf(</span><span style="color: #800000;">"</span><span style="color: #800000;">saved: %s
      </span><span style="color: #800000;">"</span><span style="color: #000000;">,buff);
      
               list</span>&lt;<span style="color: #0000ff;">int</span>&gt;<span style="color: #000000;"> :: iterator iter;
               </span><span style="color: #0000ff;">for</span>(iter = clientLists.begin();iter != clientLists.end();++<span style="color: #000000;">iter){
                   </span><span style="color: #0000ff;">if</span>(*iter ==<span style="color: #000000;"> clntSock){
                       </span><span style="color: #0000ff;">continue</span><span style="color: #000000;">;
                   }                
                   write(</span>*iter,buff,strLen + <span style="color: #800080;">1</span><span style="color: #000000;">);
      
               }
           }
       }
      

      }
      }

    #endif

    复制代码
    复制代码
    //server.cpp
    #include"utility.h"
    

    int main(){
    int err = 0;
    char buff[BUFF_SIZE];
    struct sockaddr_in servAddr;
    bzero(
    &servAddr,sizeof(servAddr));
    servAddr.sin_family
    = AF_INET;
    inet_aton(SERVERIP,
    &servAddr.sin_addr);//将字符串IP地址转化为32位整数型数据
    servAddr.sin_port = htons(atoi(PORT));

    </span><span style="color: #008000;">/*</span><span style="color: #008000;">监听套接字描述符</span><span style="color: #008000;">*/</span>
    <span style="color: #0000ff;">int</span> listenfd = socket(PF_INET,SOCK_STREAM,<span style="color: #800080;">0</span><span style="color: #000000;">);
    </span><span style="color: #0000ff;">if</span>(listenfd == -<span style="color: #800080;">1</span><span style="color: #000000;">){
        perror(</span><span style="color: #800000;">"</span><span style="color: #800000;">listenfd</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        exit(</span><span style="color: #800080;">1</span><span style="color: #000000;">);
    }
    </span><span style="color: #008000;">/*</span><span style="color: #008000;">更改服务器套接字的time_wait状态</span><span style="color: #008000;">*/</span>
    <span style="color: #0000ff;">int</span> option = <span style="color: #800080;">0</span><span style="color: #000000;">;
    socklen_t optlen;
    optlen </span>= <span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(option);
    option </span>= <span style="color: #800080;">1</span><span style="color: #000000;">;
    setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(</span><span style="color: #0000ff;">void</span>*)&amp;<span style="color: #000000;">option,optlen);
    
    </span><span style="color: #008000;">/*</span><span style="color: #008000;">分配IP地址和端口号</span><span style="color: #008000;">*/</span><span style="color: #000000;">
    err </span>= bind(listenfd,(<span style="color: #0000ff;">struct</span> sockaddr*)&amp;servAddr,<span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(servAddr));
    </span><span style="color: #0000ff;">if</span>(err == -<span style="color: #800080;">1</span><span style="color: #000000;">){
        perror(</span><span style="color: #800000;">"</span><span style="color: #800000;">bind</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        exit(</span><span style="color: #800080;">1</span><span style="color: #000000;">);
    }
    </span><span style="color: #008000;">/*</span><span style="color: #008000;">转化为可接受请求转态</span><span style="color: #008000;">*/</span><span style="color: #000000;">
    err </span>= listen(listenfd,<span style="color: #800080;">10</span><span style="color: #000000;">);
    </span><span style="color: #0000ff;">if</span>(err == -<span style="color: #800080;">1</span><span style="color: #000000;">){
        perror(</span><span style="color: #800000;">"</span><span style="color: #800000;">listen</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        exit(</span><span style="color: #800080;">1</span><span style="color: #000000;">);
    }
    
    </span><span style="color: #0000ff;">int</span> epfd =<span style="color: #000000;"> epoll_create(EPOLLSIZE);    
    </span><span style="color: #0000ff;">struct</span> epoll_event waitEvents[MAX_EVENT_NUMBER]; <span style="color: #008000;">//</span><span style="color: #008000;">预留足够大的空间来存储后面发生变化的事件,也可以使用动态分配  </span>
    
    <span style="color: #008000;">/*</span><span style="color: #008000;">注册监听套接字</span><span style="color: #008000;">*/</span><span style="color: #000000;">
    addfd(epfd,listenfd,</span><span style="color: #0000ff;">true</span><span style="color: #000000;">);    
    
    </span><span style="color: #008000;">/*</span><span style="color: #008000;">监测文件描述符的变化</span><span style="color: #008000;">*/</span>
    <span style="color: #0000ff;">int</span> eventsNumber = <span style="color: #800080;">0</span><span style="color: #000000;">;    
    </span><span style="color: #0000ff;">while</span>(<span style="color: #800080;">1</span><span style="color: #000000;">){  
        eventsNumber </span>= epoll_wait(epfd,waitEvents,EPOLLSIZE,-<span style="color: #800080;">1</span>);<span style="color: #008000;">//</span><span style="color: #008000;">一直等待事件的发生,除非出错返回</span>
        <span style="color: #0000ff;">if</span>(eventsNumber == -<span style="color: #800080;">1</span><span style="color: #000000;">){
            perror(</span><span style="color: #800000;">"</span><span style="color: #800000;">eventsNumber</span><span style="color: #800000;">"</span><span style="color: #000000;">);
            exit(</span><span style="color: #800080;">1</span><span style="color: #000000;">);
        }        
        sendBroadCast(waitEvents,eventsNumber,epfd,listenfd);</span><span style="color: #008000;">//</span><span style="color: #008000;">将waitEvents当作平常的数组,数组名就是指针</span>
    

    }
    close(listenfd);
    close(epfd);
    return 0;
    }

    复制代码
    复制代码
    #include"utility.h"
    

    void read_routine(int clntSock,char *buf);
    void write_routine(int clntSock,char *buf);

    int main(){
    int clntSock;
    char buff[BUFF_SIZE];
    clntSock
    = socket(PF_INET,SOCK_STREAM,0);

    </span><span style="color: #0000ff;">if</span>(clntSock == -<span style="color: #800080;">1</span><span style="color: #000000;">){
        perror(</span><span style="color: #800000;">"</span><span style="color: #800000;">clntSock</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        exit(</span><span style="color: #800080;">1</span><span style="color: #000000;">);
    }
    </span><span style="color: #0000ff;">struct</span><span style="color: #000000;"> sockaddr_in servAdr;
    bzero(</span>&amp;servAdr,<span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(servAdr));
    servAdr.sin_family </span>=<span style="color: #000000;"> AF_INET;
    inet_aton(SERVERIP,</span>&amp;servAdr.sin_addr);<span style="color: #008000;">//</span><span style="color: #008000;">将字符串IP地址转化为32位整数型数据</span>
    servAdr.sin_port =<span style="color: #000000;"> htons(atoi(PORT));
    
    </span><span style="color: #0000ff;">int</span> err = connect(clntSock,(<span style="color: #0000ff;">struct</span> sockaddr*)&amp;servAdr,<span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(servAdr));
    </span><span style="color: #0000ff;">if</span>(err == -<span style="color: #800080;">1</span><span style="color: #000000;">){
        perror(</span><span style="color: #800000;">"</span><span style="color: #800000;">connect</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        exit(</span><span style="color: #800080;">1</span><span style="color: #000000;">);
    }
    
    pid_t pid </span>=<span style="color: #000000;"> fork();
    </span><span style="color: #0000ff;">if</span>(pid == <span style="color: #800080;">0</span>){<span style="color: #008000;">//</span><span style="color: #008000;">子进程负责写</span>
    

    write_routine(clntSock,buff);
    }
    else{//父进程负责读
    read_routine(clntSock,buff);
    }
    close(clntSock);
    return 0;
    }

    void read_routine(int clntSock,char *buf){
    while(1){
    int strLen = read(clntSock,buf,BUFF_SIZE);
    if(strLen == 0){
    return;
    }
    buf[strLen]
    = 0;
    printf(
    "%s",buf);
    }
    }

    void write_routine(int clntSock,char *buf){
    while(1){
    fgets(buf,BUFF_SIZE,stdin);
    if(!strcmp(buf,"exit ")){
    shutdown(clntSock,SHUT_WR);
    return;
    }
    write(clntSock,buf,strlen(buf));
    }
    }

    复制代码
  • 相关阅读:
    asp.net mvc 从数据库中读取图片
    给折腾ramdisk的朋友们一点建议
    docker安装Jenkins和构建python容器
    docker笔记
    接口加密方式
    Dockerfile常用命令
    北斗七星小队团队展示
    固件程序设计实验内容1.4
    Myod实验 20181328祝维卿
    电子公文系统团队作业(四):描述设计
  • 原文地址:https://www.cnblogs.com/xjyxp/p/11466372.html
Copyright © 2011-2022 走看看