/* 群聊服务端程序:
* 使用poll同时管理监听socket和连接socket
* 并且使用牺牲空间换取时间的策略来提高服务器性能
* 注意:此服务器程序没有使用到并发的编程技巧
*/
#define _GNU_SOURCE 1
#include <stdio.h>
#include <libgen.h>
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <poll.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#define USER_LIMIT 5 /* 最多用户量 */
#define BUF_SIZE 64 /* 读缓冲区的大小 */
#define FD_LIMIT 65535 /* FD数量限制 */
/* 客户数据:客户端socket地址、待写到客户端的数据的位置、从客户端读入的数据 */
struct client_data
{
sockaddr_in addr;
char *write_buf;
char buf[BUF_SIZE];
};
int setnonblock(int fd);
int main(int argc, char *argv[])
{
if(argc < 3)
{
printf("usage: %s ip port\n", basename(argv[0]));
return -1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
sockaddr_in addr;
memset(&addr, '\0', sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<unsigned short>(port));
inet_pton(AF_INET, ip, &addr.sin_addr);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert( listenfd >= 0 );
assert( bind(listenfd, reinterpret_cast<sockaddr *>(&addr), sizeof (addr)) != -1 );
assert( listen(listenfd, 10) != -1 );
pollfd fds[USER_LIMIT+1]; /* 最多监听5个连接socket和1个监听socket(这个fd一直存在) */
fds[0].fd = listenfd;
fds[0].events = POLLIN | POLLERR;
fds[0].revents = 0;
for(int i = 1; i <= USER_LIMIT; ++i) /* 先初始化所有的connfd */
{
fds[i].fd = -1;
fds[i].events = 0;
}
/* 用已经建立连接的socket fd来索引相应的客户端数据 */
client_data *users = new client_data[FD_LIMIT];
unsigned long user_count = 0; /* 当前用户数量 */
while (1)
{ /* listenfd一直存在,user_count指明当前用户数,所以实际的fds的大小可以确定 */
int ret = poll(fds, user_count + 1, -1); /* 由于timeout==-1永久阻塞,直至事件发生,所以poll不会返回0 */
if(ret < 0)
{
printf("poll fail and errno is %d\n", errno);
break;
}
/* poll机制中,要遍历所有注册事件的文件描述符 */
for(unsigned long i = 0; i < user_count + 1; ++i)
{
if( (fds[i].fd == listenfd) && (fds[i].revents & POLLIN) )
{
sockaddr_in client_addr;
socklen_t client_addrlen = sizeof (client_addr);
int connfd = accept(listenfd, reinterpret_cast<sockaddr *>(&client_addr), &client_addrlen);
if(connfd < 0)
{
printf("accept fail! errno is %d\n", errno);
continue;
}
if(user_count >= USER_LIMIT) /* 用户过多,则关闭连接 */
{
const char *info = "too many users!\n";
printf("%s",info);
send(connfd, info, strlen(info), 0);
close(connfd);
continue;
}
/* 对于新的可接受的连接,同时修改fds和users数组。 */
user_count++;
users[connfd].addr = client_addr;
setnonblock(connfd); /* 设置connfd非阻塞,便于和IO复用合作,提高程序效率 */
fds[user_count].fd = connfd;
fds[user_count].events = POLLIN | POLLRDHUP | POLLERR;
fds[user_count].revents = 0;
printf("comes a new user, now have %lu users\n", user_count);
}
else if(fds[i].revents & POLLERR)
{
printf("get an error from %d\n",fds[i].fd);
char errors[100];
memset(errors, '\0', 100);
socklen_t length = sizeof (errors);
if( getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length) < 0 )
{
printf("get socket option failed!\n");
}
continue;
}
else if(fds[i].revents & POLLRDHUP)
{
/* 如果客户端关闭连接,则服务器也关闭对应的连接,并将用户总数减1 */
users[fds[i].fd] = users[fds[user_count].fd];
fds[i] = fds[user_count];
i--; /* 必须让i减1,因为通过以上操作已经将最后一个socket fd替换到当前位置i,此时的i还未检查 */
user_count--;
printf("a client left!\n");
}
else if(fds[i].revents & POLLIN)
{
int connfd = fds[i].fd;
memset(users[connfd].buf, '\0', BUF_SIZE);
long ret = recv(connfd, users[connfd].buf, BUF_SIZE-1, 0);
printf("get %lu bytes of client data '%s' from %d\n", ret, users[connfd].buf, connfd);
if(ret < 0)
{
/* 如果读操作出错,就关闭连接 */
if(errno != EAGAIN)
{
close(connfd);
users[fds[i].fd] = users[fds[user_count].fd];
fds[i] = fds[user_count];
i--;
user_count--;
}
}
else if(ret == 0){} /* recv返回0,说明对方已经关闭连接,此时由POLLRDHUP事件处理 */
else
{
/* 如果接收到客户数据,则通知其他socket连接准备写数据 */
for(unsigned long j = 1; j < user_count + 1; ++j)
{
if(fds[j].fd == connfd)
{
continue;
}
fds[j].events &= ~POLLIN; /* 暂时取消其他客户连接的可读事件 */
fds[j].events |= POLLOUT; /* 增加客户连接数据可写事件 */
/* 指明要发给其他客户的数据所在发送缓冲区的位置 */
users[fds[j].fd].write_buf = users[connfd].buf;
}
}
}
else if(fds[i].events & POLLOUT)
{
int connfd = fds[i].fd;
if( !users[connfd].write_buf )
{
continue;
}
long ret = send(connfd, users[connfd].write_buf, strlen(users[connfd].write_buf), 0);
if(ret < 0)
{
printf("send one client's data to another client fail!!!\n");
}
users[connfd].write_buf = nullptr;
/* 写完数据后需要重新注册fds[i]上的可读事件 */
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
delete [] users;
close(listenfd);
return 0;
}
int setnonblock(int fd)
{
int old_opt = fcntl(fd, F_GETFL);
int new_opt = old_opt | O_NONBLOCK;
fcntl(fd, F_GETFL, new_opt);
return old_opt;
}
/* ### 程序解析 ###
* 本程序使用client_data来保存客户的数据,包括客户的socket地址,客户发来的信息,以及用于将数据发出的发送缓冲区。
* 在users数组中,保存了每个客户连接的相关信息,并且采用连接socket文件描述符来进行索引。
*
* 本程序使用的I/O复用技术是poll系统调用,所以在poll成功返回时,不得不遍历整个事件集/描述符集(可以采用poll返回值进行优化),
* 其中包括了就绪事件和非就绪事件,所以,在一定程度上降低了服务器效率。
* 并且,本程序没有使用并发手段,所以服务器整个处理过程都是串行工作,造成服务器效率低下。
*
* 另外,在处理方式上也比较低效:
* 当某个客户连接发来数据,服务器接收了此数据之后,服务器就暂时清除其他所有客户连接(即除了发来数据的客户连接)上的可读事件,
* 然后在这些客户连接上设置可写事件。 当再一次调用poll时,服务器将会处理所有的可写事件,处理完毕之后就清除所有的可写事件,
* 并且重置之前被清除的可读事件。 以此循环,直到所有客户下线。
*/