局域网下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!\n\n";
    printf(
    "%d join chatting!!!\n",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!\n";
    write(clntSock,mess,strlen(mess));
    printf(
    "Atention!only ID %d client in the chatting room!\n",clntSock);
    }

             printf(</span><span style="color: #800000;">"</span><span style="color: #800000;">saved: %s\n</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\n")){
shutdown(clntSock,SHUT_WR);
return;
}
write(clntSock,buf,strlen(buf));
}
}

复制代码

 

<div id="blog_post_info">
0
0
<div class="clear"></div>
<div id="post_next_prev">

<a href="https://www.cnblogs.com/dingxiaoqiang/p/7622127.html" class="p_n_p_prefix">« </a> 上一篇:    <a href="https://www.cnblogs.com/dingxiaoqiang/p/7622127.html" title="发布于 2017-10-02 19:33">为什么需要半关闭</a>
<br>
<a href="https://www.cnblogs.com/dingxiaoqiang/p/7652704.html" class="p_n_p_prefix">» </a> 下一篇:    <a href="https://www.cnblogs.com/dingxiaoqiang/p/7652704.html" title="发布于 2017-10-11 20:34">2017-10-11第二次万革始面经</a>
posted @ 2019-09-05 15:30  xjyxp01  阅读(1047)  评论(0编辑  收藏  举报