利用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操作时立即返回,不会阻塞,数据读好后内核通知进程取数据。
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><<span style="color: #0000ff;">int</span>><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>*)&<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*)&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>&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>&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*)&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));
}
}
LeetCode第[11]题(Java):Container With Most Water (数组容器盛水)——Medium
LeetCode第[4]题(Java):Median of Two Sorted Arrays (俩已排序数组求中位数)——HARD
LeetCode第[1]题(Java):Two Sum (俩数和为目标数的下标)——EASY
FILTER:progid:DXImageTransform.Microsoft.Gradient (转)
青蛙的烦恼(dp好题)
凸多边形的三角剖分(dp好题)
火车安排问题(dp好题)
快餐问题(dp好题)
给出一个长度为n的数列,请对于每一个数,输出他右边第一个比他大的数。n<=100000.