利用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("saved: %s\n",buff);list<int> :: iterator iter;for(iter = clientLists.begin();iter != clientLists.end();++iter){if(*iter == clntSock){continue;}                write(*iter,buff,strLen + 1);}}}}
}#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));/*监听套接字描述符*/int listenfd = socket(PF_INET,SOCK_STREAM,0);if(listenfd == -1){perror("listenfd");exit(1);}/*更改服务器套接字的time_wait状态*/int option = 0;socklen_t optlen;optlen = sizeof(option);option = 1;setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,optlen);/*分配IP地址和端口号*/err = bind(listenfd,(struct sockaddr*)&servAddr,sizeof(servAddr));if(err == -1){perror("bind");exit(1);}/*转化为可接受请求转态*/err = listen(listenfd,10);if(err == -1){perror("listen");exit(1);}int epfd = epoll_create(EPOLLSIZE);    struct epoll_event waitEvents[MAX_EVENT_NUMBER]; //预留足够大的空间来存储后面发生变化的事件,也可以使用动态分配  /*注册监听套接字*/addfd(epfd,listenfd,true);    /*监测文件描述符的变化*/int eventsNumber = 0;    while(1){  eventsNumber = epoll_wait(epfd,waitEvents,EPOLLSIZE,-1);//一直等待事件的发生,除非出错返回if(eventsNumber == -1){perror("eventsNumber");exit(1);}        sendBroadCast(waitEvents,eventsNumber,epfd,listenfd);//将waitEvents当作平常的数组,数组名就是指针
    }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);if(clntSock == -1){perror("clntSock");exit(1);}struct sockaddr_in servAdr;bzero(&servAdr,sizeof(servAdr));servAdr.sin_family = AF_INET;inet_aton(SERVERIP,&servAdr.sin_addr);//将字符串IP地址转化为32位整数型数据servAdr.sin_port = htons(atoi(PORT));int err = connect(clntSock,(struct sockaddr*)&servAdr,sizeof(servAdr));if(err == -1){perror("connect");exit(1);}pid_t pid = fork();if(pid == 0){//子进程负责写
        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));}
}

分类: TCPIP网络编程
好文要顶 已关注 收藏该文

zqlucky
关注 - 1
粉丝 - 26

我在关注他 取消关注

0
0

« 上一篇: 为什么需要半关闭
» 下一篇: 2017-10-11第二次万革始面经

转载于:https://www.cnblogs.com/xjyxp/p/11466372.html

局域网下C++命令行聊天室简易版相关推荐

  1. 使用Chatkit构建Node.js命令行聊天应用程序

    by Hugo 雨果 使用Chatkit构建Node.js命令行聊天应用程序 (Build a Node.js command-line chat application with Chatkit) ...

  2. 同一局域网下,一台电脑连接另一台电脑的虚拟机(从属机(window)连接主机(window)虚拟机(Linux)的连接流程)

    同一局域网下,一台电脑连接另一台电脑的虚拟机(从属机(window)连接主机(window)虚拟机(Linux)的连接流程) 一. 通过NET网络模式连接 查看主机本身ip: win+r 输入cmd, ...

  3. 局域网下seafiles网盘的使用

    局域网下seafiles网盘的使用 路由器设置 IP与MAC绑定 重要的事情说三遍!重要的事情说3遍!去查看服务器ip挺麻烦的. docker-compose部署seafile 该教程来自B站科技区大 ...

  4. 同一局域网下,手机能连上wifi,电脑连不上

    问题描述 同一局域网下,手机能连上wifi,电脑连不上 解决办法 步骤一:如图所示 步骤二:如图所示 步骤三:关机重启

  5. 桥接模式使手机和电脑,虚拟机在同一个局域网下

    在这一次问题中我发了差不多3天才找到问题(不是都在找),开始是我的项目中上传一个图片到linux的服务器中,然后我的手机端通过linux服务的地址找到图片,显示小头像,但是开始我的头像一直是空白,开始 ...

  6. JavaSE项目之聊天室swing版

    引子: 当前,互联网 体系结构的参考模型主要有两种,一种是OSI参考模型,另一种是TCP/IP参考模型. 一.OSI参考模型,即开放式通信系统互联参考模型(OSI/RM,Open Systems In ...

  7. 不在一个局域网下,如何设置可以被远程登录的服务器[ubuntu]?【ssh登录】【不使用软件】

    不知道在看见这篇博文之前,你是否已经兜兜转转了好久,并且已经实现了局域网下的远程登录,但是两个电脑不在一个网络下,怎么都连不上. 那是因为,你没有进行内网穿透.简单说明一下内网穿透,就是,把的电脑的I ...

  8. ping——判断两个设备是否在同一个局域网下

    有时需要判断两个设备(有线/无线)是否在同一个局域网下面怎么操作呢? 一.ping简介 二.windows下 三.Linux下 四.注意事项 一.ping简介 ping 程序是对两台主机之间连通性进行 ...

  9. 简单聊天室(java版)

    这是本人从其他地方学习到的关于聊天室的一个模本,我从中截取了一部分关于客户端和服务端通信的Socket的内容.希望对大家对socket有个了解,我写的这些代码可以实现两人或多人在多台电脑上实现简单的对 ...

最新文章

  1. NetworkManagementService介绍
  2. android 顺序执行任务
  3. AMD猛攻数据中心市场,拿下15年来最高份额,英特尔DCG收入下滑20%
  4. Java 多态中成员变量与成员属性的特点【复习】
  5. pytorch中torch.manual_seed()的理解
  6. 利用Python进行数据分析(1) 简单介绍
  7. linux c++ 输出到终端,如何将彩色文本输出到Linux终端?
  8. IOS 杂笔-14(被人遗忘的owner)
  9. BAT中一行太长,如何折行
  10. 第156天:canvas(三)
  11. 行业json数据以及elementui级联格式
  12. 查看本地计算机ip命令,查看你本机的IP信息的命令ipconfig详解【图】
  13. CentOS 7.x 内核kernel版本升级实操
  14. 麒麟V10 kylin v10服务器版yum软件源官方源亲测可用
  15. 中山c 语言培训中心,中山英语口语培训中心
  16. A智慧城市,新型信息化城市形态
  17. 英语语法---名词性从句详解
  18. 畅想物联网未来 | 百度云天工智能物联网沙龙圆满落幕
  19. 银河麒麟识别不了U盘
  20. 【关于为什么要刷力扣的思考】记第二次周赛AK

热门文章

  1. 消息摘要算法与密码加密
  2. 详细教程:vivado2019.2 vitis2019.2下,zynq7000系列FPGA固化PL程序到外挂flash和SD卡
  3. 性能分析之两个性能瓶颈分析案例
  4. hadoop无法访问70050(9870)
  5. PPT怎么转Word,多种方式教你快速转换
  6. 程序员vs瓦工 那么到底谁牛逼?
  7. 室外靶场设计方案,移动射击和固定场地有区别
  8. 如何准备计算机二级考试题库,如何准备全国计算机二级考试office?
  9. 【Y9000P】联想Y9000P 笔记本typec 失效解决
  10. 【软件工程】学习笔记(二)