項目參考:
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
}
通過學習此項目:學到了客戶端與服務端進行通信的基本步驟,以及使用使用管道進行進程間通信,管道進行通信時需要進程有共同的祖先,在這裏是學習管道的一個很好的例子。