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));
    }
    }

    复制代码
  • 相关阅读:
    2. Add Two Numbers
    1. Two Sum
    22. Generate Parentheses (backTracking)
    21. Merge Two Sorted Lists
    20. Valid Parentheses (Stack)
    19. Remove Nth Node From End of List
    18. 4Sum (通用算法 nSum)
    17. Letter Combinations of a Phone Number (backtracking)
    LeetCode SQL: Combine Two Tables
    LeetCode SQL:Employees Earning More Than Their Managers
  • 原文地址:https://www.cnblogs.com/xjyxp/p/11466372.html
Copyright © 2011-2022 走看看