APUE中關於I/O複用沒有epoll的講解,Linux高性能服務器中的講解比較中肯,寫個聊天室練習一下。
基於C/S模型的高併發聊天室項目,在linux環境下開發,主要功能包括用戶上線通知、用戶離線通知和用戶羣聊等基本功能。
基於Socket套接字和epoll機制開發了系統的客戶端和服務器端;服務端爲了提高併發量採用epoll監視網絡接口;客戶端採用epoll方式分別監視網絡socket接口和pipe的讀端,進行網絡socket接口和本地STDIN和STDOUT的交互。
關於LT與ET:
服務器預設LT模式,可以切換ET模式。
整體架構如下:
客戶機:
建立pipe,然後進行fork,父子進程之間通信用的是pipe,
子進程直接和標準輸入交互,關閉pipe的讀端fd0;當stdin讀入的buffer和“EXIT”比較,然後在決定是否關閉,將buffer write到pipe的寫端,寫入pipe_fd[1]
父進程的epoll句柄監控pipe的讀pipe_fd[0]和socket的套接字的寫
如果檢測到socket套接字則recv消息到buffer,然後從stdin輸出給用戶
如果檢測到pipe的寫入,則讀入buffer,在send到服務器
服務器:TCP
監聽socket套接字
如果是listen則進行accept得到confd然後將confd加入到保持鏈接的鏈表中,並調用broadcast函數進行廣播,廣播給所有鏈接保持鏈表中的在線用戶,發送welcome消息。
Broadcast函數:
Recv(cilentfd)若返回0則表示此客戶端鏈接斷開,從鏈表中刪除此鏈接描述符,並循環告知所有在線用戶此此用戶下線。
否則檢查鏈表的長度,若長度爲1,則表示只有一個用戶,告知用戶聊天室只有一個,
然後將此用戶的消息循環send到鏈表中的在線用戶。
功能測試:
Epoll_helper.h
#ifndef EPOLL_HELPER_H_INCLUDED
#define EPOLL_HELPER_H_INCLUDED
#include <iostream>
#include <list>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
// clients_list save all the clients's socket
list<int> clients_list;
/********************** macro defintion **************************/
// server ip
#define SERVER_IP "127.0.0.1"
// server port
#define SERVER_PORT 8888
//epoll size
#define EPOLL_SIZE 5000
//message buffer size
#define BUF_SIZE 0xFFFF
#define SERVER_WELCOME "Welcome you join to the chat room! Your chat ID is: Client #%d"
#define SERVER_MESSAGE "ClientID %d say >> %s"
// exit
#define EXIT "EXIT"
#define CAUTION "There is only one int the char room!"
/********************** some function **************************/
/**
* @param sockfd: socket descriptor
* @return 0
**/
int setnonblocking(int sockfd)
{
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);
return 0;
}
/**
epollfd: epoll handle
socket descriptor
enable_et : enable_et = true, epoll use ET; otherwise LT
**/
void addfd( int epollfd, int fd, bool enable_et )
{
struct epoll_event ev;
ev.data.fd = fd;
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\n");
}
/**
* @param clientfd: socket descriptor
* @return : len
**/
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;
}
#endif // UTILITY_H_INCLUDED
TCP_epoll_client.cpp
#include "Epoll_helper.h"
int main(int argc, char *argv[])
{
//用戶連接的服務器 IP + port
struct sockaddr_in serverAddr;
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
// 創建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("sock error");
exit(-1);
}
// 連接服務端
if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("connect error");
exit(-1);
}
// 創建管道,其中fd[0]用於父進程讀,fd[1]用於子進程寫
int pipe_fd[2];
if(pipe(pipe_fd) < 0)
{
perror("pipe error");
exit(-1);
}
// 創建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);
// 表示客戶端是否正常工作
bool isClientwork = true;
// 聊天信息緩衝區
char message[BUF_SIZE];
// Fork
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
}
if(pid){
//關閉父進程和sock
close(pipe_fd[0]);
close(sock);
}else{
//關閉子進程
close(pipe_fd[1]);
}
return 0;
}
TCP_epoll_server.cpp
#include "Epoll_helper.h"
int main(int argc, char *argv[])
{
//服務器IP + port
struct sockaddr_in serverAddr;
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
//創建監聽socket
int listener = socket(AF_INET, SOCK_STREAM, 0);
if(listener < 0)
{
perror("listener");
exit(-1);
}
printf("listen socket created \n");
//綁定地址
if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("bind error");
exit(-1);
}
//監聽
int ret = listen(listener, 5);
if(ret < 0)
{
perror("listen error");
exit(-1);
}
printf("Start to listen: %s\n", SERVER_IP);
//在內核中創建事件表
int epfd = epoll_create(EPOLL_SIZE);
if(epfd < 0)
{
perror("epfd error");
exit(-1);
}
printf("epoll created, epollfd = %d\n", epfd);
static struct epoll_event events[EPOLL_SIZE];
//往內核事件表裏添加事件
addfd(epfd, listener, true);
//主循環
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");
char message[BUF_SIZE];
bzero(message, BUF_SIZE);
sprintf(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); }
}
}
}
close(listener); //關閉socket
close(epfd); //關閉內核 不在監控這些註冊事件是否發生
return 0;
}
參考:Linux 高性能服務器編程
https://www.cnblogs.com/-Lei/archive/2012/09/12/2681475.html