epoll实现高并发聊天室
项目基本介绍:
基于epoll机制,实现多客户在线实时通信。
通过此项目学习了基本的TCP客服、服务程序的基本流程以及epoll的使用。一边查看unix网络编程卷一,对原来的项目做了稍稍的改变
创建了一个IPv4套接字地址
//用户连接的服务器 IP + port
struct sockaddr_in serverAddr;
将服务器的ip地址和端口号填入套接字地址结构
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT); // htons返回网络字节序
// inet_addr已被废弃
// serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
inet_aton(SERVER_IP,(struct in_addr*)&serverAddr.sin_addr.s_addr);
创建TCP套接字,通过指定使用的协议族和套接字类型使用对应的传输协议:
//创建socket套接字,使用TCP传输协议,返回套接字描述符,类似于文件描述符
int listener = socket(PF_INET, SOCK_STREAM, 0);
将套接字地址结构绑定到套接字上
//绑定地址
if( bind(listener, (const struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("bind error");
exit(-1);
}
socket被创建之初,被假设是一个主动套接字,listen将一个未连接的套接字转化为一个被动套接字,第二个参数5,是已排队连接的最大数目
//监听,下面设置的5对于现在的网络可能较小
int ret = listen(listener, 5);
使用epoll_create方法创建事件表,后续的连接都将注册到此事件表中
//在内核中创建事件表
int epfd = epoll_create(EPOLL_SIZE);
if(epfd < 0) { perror("epfd error"); exit(-1);}
通过调用自己的addfd方法,将监听套接字描述符填入epoll_event结构体中,并添加到事件表中
//utility.h
void addfd( int epollfd, int fd, bool enable_et )
{
struct epoll_event ev;
ev.data.fd = fd;
STDIN_FILENO
ev.events = EPOLLIN;
if( enable_et )
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
setnonblocking(fd);
printf("fd added to epoll!\n");
}
//往内核事件表里添加事件
addfd(epfd, listener, true);
参考:epoll详解
通过epoll_wait方法,将就绪事件从事件表中复制到第二个参数events数组中。
如果是监听套接字状态发生变化,使用accept函数,通过监听套接字描述符创建一个已连接套接字描述符,并通过自己的addfd方法将已连接套接字添加到事件表中,同时添加到创建的clients_list中,方便后续进行广播群发消息。并使用send函数给新连接的客户端发送欢迎消息。
如果发送变化的是已连接套接字,说明有客户端发送来的新的消息,那么就需要对所有除发送消息的客户端进行群发广播消息。
//主循环
while(1)
{
//epoll_events_count表示就绪事件的数目
int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if(epoll_events_count < 0) {
perror("epoll failure");
break;
}
printf("epoll_events_count = %d\n", epoll_events_count);
//处理这epoll_events_count个就绪事件
for(int i = 0; i < epoll_events_count; ++i)
{
int sockfd = events[i].data.fd;
//新用户连接
if(sockfd == listener)
{
struct sockaddr_in client_address;
socklen_t client_addrLength = sizeof(struct sockaddr_in);
int clientfd = accept( listener, ( struct sockaddr* )&client_address, &client_addrLength );
printf("client connection from: %s : % d(IP : port), clientfd = %d \n",
inet_ntoa(client_address.sin_addr),
ntohs(client_address.sin_port),
clientfd);
addfd(epfd, clientfd, true); //把这个新的客户端添加到内核事件列表
// 服务端用list保存用户连接
clients_list.push_back(clientfd);
printf("Add new clientfd = %d to epoll\n", clientfd);
printf("Now there are %d clients int the chat room\n", (int)clients_list.size());
// 服务端发送欢迎信息
printf("welcome message\n\n");
char message[BUF_SIZE];
bzero(message, BUF_SIZE);
// sprintf无法检测缓冲区是否溢出,snprintf是更好的选择
// sprintf(message, SERVER_WELCOME, clientfd);
snprintf(message,sizeof(message),SERVER_WELCOME,clientfd);
int ret = send(clientfd, message, BUF_SIZE, 0);
if(ret < 0) { perror("send error"); exit(-1); }
}
//客户端唤醒//处理用户发来的消息,并广播,使其他用户收到信息
else
{
int ret = sendBroadcastmessage(sockfd);
if(ret < 0) { perror("error");exit(-1); }
}
}
}
进行广播消息的时候,要判断recv函数的返回值,如果是0表示被动关闭,接着调用close方法,将客户端进行关闭,并移出客户端列表。如果大于0表示发来了数据,将数据对客户端列表中除发送方的所有客户端进行广播群发。
// utility.h
int sendBroadcastmessage(int clientfd)
{
// buf[BUF_SIZE] receive new chat message
// message[BUF_SIZE] save format message
char buf[BUF_SIZE], message[BUF_SIZE];
bzero(buf, BUF_SIZE);
bzero(message, BUF_SIZE);
// receive message
printf("read from client(clientID = %d)\n", clientfd);
int len = recv(clientfd, buf, BUF_SIZE, 0);
if(len == 0) // len = 0 means the client closed connection
{
close(clientfd);
clients_list.remove(clientfd); //server remove the client
printf("ClientID = %d closed.\n now there are %d client in the char room\n", clientfd, (int)clients_list.size());
}
else //broadcast message
{
if(clients_list.size() == 1) { // this means There is only one int the char room
send(clientfd, CAUTION, strlen(CAUTION), 0);
return len;
}
// format message to broadcast
sprintf(message, SERVER_MESSAGE, clientfd, buf);
list<int>::iterator it;
for(it = clients_list.begin(); it != clients_list.end(); ++it) {
if(*it != clientfd){
if( send(*it, message, BUF_SIZE, 0) < 0 ) {
perror("error");
exit(-1);
}
}
}
}
return len;
}
套接字地址的创建以及和socket套接字的绑定与服务端类似,然后通过使用connect函数阻塞式主动打开进行与服务端的连接。connect前调用bind函数不是必须的,内核会确定源ip,并选择临时端口作为源端口。
// 连接服务端
if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("connect error");
exit(-1);
}
使用管道进行进程间通信,后面要一个进程负责接收用户的输入,一个进程等待服务器的消息
// 创建管道,fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入
// 打算其中fd[0]用于父进程读,fd[1]用于子进程写
int pipe_fd[2];
if(pipe(pipe_fd) < 0) { perror("pipe error"); exit(-1); }
依然还是用epoll机制来监听管道读和套接字描述符sock是否有事件发生
// 创建epoll
int epfd = epoll_create(EPOLL_SIZE);
if(epfd < 0) { perror("epfd error"); exit(-1); }
static struct epoll_event events[2];
//将sock和管道读端描述符都添加到内核事件表中
addfd(epfd, sock, true);
addfd(epfd, pipe_fd[0], true);
使用fork函数,创建一个父进程的副本---------子进程,一次被调用,但返回两次,子进程返回0,而在父进程返回子进程的进程id
子进程负责向管道写数据,关闭子进程的读端
父进程负责接收数据,关闭管道的写端,通过判断是sock状态发送了变化还是管道的状态发送了变化,将数据显示还是将数据发送到服务端。
int pid = fork();
if(pid < 0) {
perror("fork error"); exit(-1);
}
else if(pid == 0) // 子进程
{
//子进程负责写入管道,因此先关闭读端
close(pipe_fd[0]);
printf("Please input 'exit' to exit the chat room\n");
while(isClientwork){
bzero(&message, BUF_SIZE);
fgets(message, BUF_SIZE, stdin);
// 客户输出exit,退出
if(strncasecmp(message, EXIT, strlen(EXIT)) == 0){
isClientwork = 0;
}
// 子进程将信息写入管道
else {
if( write(pipe_fd[1], message, strlen(message) - 1 ) < 0 )
{ perror("fork error"); exit(-1); }
}
}
}
else //pid > 0 父进程
{
//父进程负责读管道数据,因此先关闭写端
close(pipe_fd[1]);
// 主循环(epoll_wait)
while(isClientwork) {
int epoll_events_count = epoll_wait( epfd, events, 2, -1 );
//处理就绪事件
for(int i = 0; i < epoll_events_count ; ++i)
{
bzero(&message, BUF_SIZE);
//服务端发来消息
if(events[i].data.fd == sock)
{
//接受服务端消息
int ret = recv(sock, message, BUF_SIZE, 0);
// ret= 0 服务端关闭
if(ret == 0) {
printf("Server closed connection: %d\n", sock);
close(sock);
isClientwork = 0;
}
else printf("%s\n", message);
}
//子进程写入事件发生,父进程处理并发送服务端
else {
//父进程从管道中读取数据,输入和输出的字节数可能比请求的数量少,
// 内核中用于套接字的缓冲区可能已经到了极限
int ret = read(events[i].data.fd, message, BUF_SIZE);
// ret = 0
if(ret == 0) isClientwork = 0;
else{ // 将信息发送给服务端
send(sock, message, BUF_SIZE, 0);
}
}
}//for
}//while
}
通过学习此项目:学到了客户端与服务端进行通信的基本步骤,以及使用使用管道进行进程间通信,管道进行通信时需要进程有共同的祖先,在这里是学习管道的一个很好的例子。