一、简介
smallchat是redis作者使用c开发的聊天服务器,实现了聊天室的功能。核心代码200多行,使用了IO多路复用技术。是一款适合学习IO多路复用的开源软件。原实现是使用select实现了IO多路复用。本文改造了原代码,改用epoll实现,并使用reacotor模型,使代码更精简,功能更聚合,方便使用和理解。
二、smallchat-server改造
server和client都使用了select,本文仅使用epoll改造server。smallchat-server改造如下:
/* smallchat.c -- Read clients input, send to all the other connected clients.
*
* Copyright (c) 2023, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the project name of nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/epoll.h>
#include "chatlib.h"
/* ============================ Data structures =================================
* The minimal stuff we can afford to have. This example must be simple
* even for people that don't know a lot of C.
* =========================================================================== */
#define MAX_CLIENTS 1000 // This is actually the higher file descriptor.
#define SERVER_PORT 7711
#define MAX_EPOLL_EVENTS 200
/* This structure represents a connected client. There is very little
* info about it: the socket descriptor and the nick name, if set, otherwise
* the first byte of the nickname is set to 0 if not set.
* The client can set its nickname with /nick <nickname> command. */
struct client {
int fd; // Client socket.
char *nick; // Nickname of the client.
};
/* This global structure encapsulates the global state of the chat. */
struct chatState {
int serversock; // Listening server socket.
int numclients; // Number of connected clients right now.
int maxclient; // The greatest 'clients' slot populated.
struct client *clients[MAX_CLIENTS]; // Clients are set in the corresponding
// slot of their socket descriptor.
};
// reactor Data structures
typedef void (*CALLBACK) (int epollfd, int fd);
void recv_callback(int epollfd, int fd);
void accept_callback(int epollfd, int sockfd);
struct sockitem {
int sockfd;
CALLBACK callback;
};
struct chatState *Chat; // Initialized at startup.
/* ====================== Small chat core implementation ========================
* Here the idea is very simple: we accept new connections, read what clients
* write us and fan-out (that is, send-to-all) the message to everybody
* with the exception of the sender. And that is, of course, the most
* simple chat system ever possible.
* =========================================================================== */
/* Create a new client bound to 'fd'. This is called when a new client
* connects. As a side effect updates the global Chat state. */
struct client *createClient(int fd, int epollfd) {
char nick[32]; // Used to create an initial nick for the user.
int nicklen = snprintf(nick,sizeof(nick),"user:%d",fd);
struct client *c = chatMalloc(sizeof(*c));
socketSetNonBlockNoDelay(fd); // Pretend this will not fail.
c->fd = fd;
c->nick = chatMalloc(nicklen+1);
memcpy(c->nick,nick,nicklen);
assert(Chat->clients[c->fd] == NULL); // This should be available.
Chat->clients[c->fd] = c;
/* We need to update the max client set if needed. */
if (c->fd > Chat->maxclient) Chat->maxclient = c->fd;
Chat->numclients++;
struct sockitem *si = (struct sockitem *)malloc(sizeof(struct sockitem));
si->sockfd = fd;
si->callback = recv_callback;
struct epoll_event ep_event;
ep_event.data.ptr = si;
ep_event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ep_event);
return c;
}
/* Free a client, associated resources, and unbind it from the global
* state in Chat. */
void freeClient(struct client *c, int epollfd) {
free(c->nick);
close(c->fd);
Chat->clients[c->fd] = NULL;
Chat->numclients--;
if (Chat->maxclient == c->fd) {
/* Ooops, this was the max client set. Let's find what is
* the new highest slot used. */
int j;
for (j = Chat->maxclient-1; j >= 0; j--) {
if (Chat->clients[j] != NULL) {
Chat->maxclient = j;
break;
}
}
if (j == -1) Chat->maxclient = -1; // We no longer have clients.
}
free(c);
epoll_ctl(epollfd, EPOLL_CTL_DEL, c->fd, NULL);
}
/* Allocate and init the global stuff. */
void initChat(void) {
Chat = chatMalloc(sizeof(*Chat));
memset(Chat,0,sizeof(*Chat));
/* No clients at startup, of course. */
Chat->maxclient = -1;
Chat->numclients = 0;
/* Create our listening socket, bound to the given port. This
* is where our clients will connect. */
Chat->serversock = createTCPServer(SERVER_PORT);
if (Chat->serversock == -1) {
perror("Creating listening socket");
exit(1);
}
}
/* Send the specified string to all connected clients but the one
* having as socket descriptor 'excluded'. If you want to send something
* to every client just set excluded to an impossible socket: -1. */
void sendMsgToAllClientsBut(int excluded, char *s, size_t len) {
for (int j = 0; j <= Chat->maxclient; j++) {
if (Chat->clients[j] == NULL ||
Chat->clients[j]->fd == excluded) continue;
/* Important: we don't do ANY BUFFERING. We just use the kernel
* socket buffers. If the content does not fit, we don't care.
* This is needed in order to keep this program simple. */
write(Chat->clients[j]->fd,s,len);
}
}
void accept_callback(int epollfd, int sockfd)
{
int connfd = acceptClient(sockfd);
struct client *c = createClient(connfd, epollfd);
/* Send a welcome message. */
char *welcome_msg =
"Welcome to Simple Chat! "
"Use /nick <nick> to set your nick.\n";
write(c->fd,welcome_msg,strlen(welcome_msg));
printf("Connected client fd=%d\n", connfd);
}
void recv_callback(int epollfd, int fd)
{
char readbuf[256];
int nread = read(fd,readbuf,sizeof(readbuf)-1);
if (nread <= 0) {
/* Error or short read means that the socket
* was closed. */
printf("Disconnected client fd=%d, nick=%s\n",
fd, Chat->clients[fd]->nick);
freeClient(Chat->clients[fd], epollfd);
} else {
/* The client sent us a message. We need to
* relay this message to all the other clients
* in the chat. */
struct client *c = Chat->clients[fd];
readbuf[nread] = 0;
/* If the user message starts with "/", we
* process it as a client command. So far
* only the /nick <newnick> command is implemented. */
if (readbuf[0] == '/') {
/* Remove any trailing newline. */
char *p;
p = strchr(readbuf,'\r'); if (p) *p = 0;
p = strchr(readbuf,'\n'); if (p) *p = 0;
/* Check for an argument of the command, after
* the space. */
char *arg = strchr(readbuf,' ');
if (arg) {
*arg = 0; /* Terminate command name. */
arg++; /* Argument is 1 byte after the space. */
}
if (!strcmp(readbuf,"/nick") && arg) {
free(c->nick);
int nicklen = strlen(arg);
c->nick = chatMalloc(nicklen+1);
memcpy(c->nick,arg,nicklen+1);
} else {
/* Unsupported command. Send an error. */
char *errmsg = "Unsupported command\n";
write(c->fd,errmsg,strlen(errmsg));
}
} else {
/* Create a message to send everybody (and show
* on the server console) in the form:
* nick> some message. */
char msg[256];
int msglen = snprintf(msg, sizeof(msg),
"%s> %s", c->nick, readbuf);
/* snprintf() return value may be larger than
* sizeof(msg) in case there is no room for the
* whole output. */
if (msglen >= (int)sizeof(msg))
msglen = sizeof(msg)-1;
printf("%s",msg);
/* Send it to all the other clients. */
sendMsgToAllClientsBut(fd,msg,msglen);
}
}
}
/* The main() function implements the main chat logic:
* 1. Accept new clients connections if any.
* 2. Check if any client sent us some new message.
* 3. Send the message to all the other clients. */
int main(void) {
initChat();
int epollfd;
struct epoll_event events[MAX_EPOLL_EVENTS];
if ((epollfd = epoll_create1(0)) == -1) {
perror("epoll_create1 error");
exit(1);
}
struct sockitem *si = (struct sockitem *)malloc(sizeof(struct sockitem));
si->sockfd = Chat->serversock;
si->callback = accept_callback;
struct epoll_event ep_event;
ep_event.data.ptr = si;
ep_event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, Chat->serversock, &ep_event);
int timeout = 1000;
while(1) {
int retval;
retval = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, timeout);
if (retval == -1) {
perror("epoll() error");
close(Chat->serversock);
close(epollfd);
exit(1);
} else if (retval) {
for (int i = 0; i < retval; i++) {
if (events[i].events & EPOLLIN) {
struct sockitem *si = (struct sockitem *)events[i].data.ptr;
si->callback(epollfd, si->sockfd);
}
}
} else {
/* Timeout occurred. We don't do anything right now, but in
* general this section can be used to wakeup periodically
* even if there is no clients activity. */
}
}
return 0;
}