最近做了一个ios项目的服务端数据处理,这个项目中涉及到了实时聊天功能,移动设备的聊天信息要通过服务器进行信息转发,为了保证聊天信息能够及时准备的传达到接收方,在服务器端需要存储在线用户信息,因为项目时间紧,服务端的数据处理接口还是复用的以前的c架构,采用的是TPR模型,即:每个用户的每个请求在服务端都单独起一个线程来处理该用户的请求。当初想到在c中使用链表来存储在线用户信息,但当在线用户量不断增加时,对链表的遍历查询效率都太低,每一次查询都得遍历链表,要是在c中也有STL,这就会极大提高查询效率,在网上搜到了开源的libcstl库,这个库仿C++中STL实现了各种数据结构,经过简单的测试,我在用map<userID, userInfo*>来存储在线用户信息,这样根据userID就可快速找到该用户信息。
2、libcstl使用中遇到的问题
用户要实现实时聊天,终端就必须和服务端建立心跳链接,定时向服务器发送心跳,每个心跳就是一个请求,每个请求在服务端就是一个线程来处理,这样的架构当在线用户大量增加的时候,服务端都把时间花在了创建、销毁线程上,所以我在服务端结合epoll轮询fd增加了线程池,这样服务端都集中处理用户请求了,极大减少了线程创建和销毁的开销。同时为了保证在线用户的心跳正常,在服务端有一个专门的线程定时检测在线用户,当在线用户当前收到心跳的时间距离收到上个心跳的时间超过了两个心跳周期时,将用户从map中踢掉。在服务端我用代码这样实现:
void check_userOnline()
{
int userNum = 0;
time_t currentTime; //记录当前时间
while (1) {
currentTime = getTime_second();
map_iterator_t it;
pthread_mutex_lock(&userMutex);
for (it = map_begin(userMap); !iterator_equal(it, map_end(userMap)); ) {
user_HBData *user = (user_HBData*)pair_second(iterator_get_pointer(it));
if ((currentTime - user->last_heartBeat_time > 30)) {
//在线用户超时,剔除在线用户
SMA_LOG(LOG_DEBUG, "onlineUser is timeout,kick it, userID: %d, socketFd: %d, epollFd: %d, time: %u",
user->userID, user->socketFd, user->epollFd, currentTime);
epl_rmv(user->epollFd, user->socketFd, epl_ev_shot);
shutdown(user->socketFd, SHUT_RDWR);
close(user->socketFd);
map_erase_pos(userMap, it);
it = map_begin(it);
//if (user) free(user); //由于map_erase_pos这个函数调用时,user已被free
} else {
it = iterator_next(it);
}
}
userNum = map_size(userMap);
pthread_mutex_unlock(&userMutex);
SMA_LOG(LOG_DEBUG, "current all online user count: %d", userNum);
sleep_select(CHECK_ONLINE_USERSTATE_TIME_SEC, CHECK_ONLINE_USERSTATE_TIME_USEC);
}
}
在上面这段代码中,采用了每次遍历map,找到满足条件的用户删掉,但是我在初期测试过程中都没发现任何问题,后头我模拟10000个终端向服务端发送心跳链接,当10000个用户全部上线后,服务端假死了,没有崩掉,但就是不做任何事情了,当我top查看cpu使用情况时,发现cpu使用100%了,难怪程序假死了,出现这样的情况一定是程序中出现死循环了,占用掉了全部cpu资源,通过gdb调试发现,就是这个定时检测线程出现了死循环,至于这段代码为什么会出现死循环,跟遍历map的做法有关系,我就改成下面这段代码:
void check_userOnline()
{
int userNum = 0;
time_t currentTime; //记录当前时间
while (1) {
currentTime = getTime_second();
map_iterator_t it;
map_iterator_t current_it;
pthread_mutex_lock(&userMutex);
for (it = map_begin(userMap); !iterator_equal(it, map_end(userMap)); ) {
user_HBData *user = (user_HBData*)pair_second(iterator_get_pointer(it));
if ((currentTime - user->last_heartBeat_time > 30)) {
//在线用户超时,剔除在线用户
SMA_LOG(LOG_DEBUG, "onlineUser is timeout,kick it, userID: %d, socketFd: %d, epollFd: %d, time: %u",
user->userID, user->socketFd, user->epollFd, currentTime);
epl_rmv(user->epollFd, user->socketFd, epl_ev_shot);
shutdown(user->socketFd, SHUT_RDWR);
close(user->socketFd);
current_it = it;
it = iterator_next(it);
map_erase_pos(userMap, current_it);
//if (user) free(user); //由于map_erase_pos这个函数调用时,user已被free
} else {
it = iterator_next(it);
}
}
userNum = map_size(userMap);
pthread_mutex_unlock(&userMutex);
SMA_LOG(LOG_DEBUG, "current all online user count: %d", userNum);
sleep_select(CHECK_ONLINE_USERSTATE_TIME_SEC, CHECK_ONLINE_USERSTATE_TIME_USEC);
}
}
对map的遍历换了一种方式,同样是先遍历map,找到满足条件的数据,先记录当前迭代器位置,再记录 下一个迭代器位置,然后再删除当前迭代器, 这样一来当10000用户全部上线后,程序没有假死了,正常了。
对于libcstl中map 的遍历,特别是在遍历过程中又要删除满足条件的数据,对迭代器的操作一定要谨慎明了,不然会出现崩溃、死循环等情况。同时注意:libcstl中的数据结构都是非线程安全的,同一时间只能保证一个线程访问,否则会引起程序coredump。
附:在调试过程中用到的一些命令,
strace -p processID:能够追踪当前进程所有的文件句柄的操作,对于网络程序,特别当出现fd泄漏时,可以用此命令来追踪泄露的fd为什么没有被关闭
lsof -p proceessID:查看当前进程中使用到的文件句柄情况,可以产看到泄露的fd
gdb:这就不用说了,在linux下得c/c++程序调试利器