前言
两周时间里,我们完成了一个简易的聊天室,现在还没有经过大量的测试,所以代码中一定存在着很多隐藏的bug没有被我发现。
两周前的现在,我还对于这个项目没有任何的了解,什么数据库,什么epoll,什么套接字都还一无所知。后来没过几天也就渐渐熟悉了项目中需要用到的新的知识点,也就慢慢熟悉了这个项目。
在项目进行过程中,我也遇到了许多莫名其妙的bug,本文就对我遇到的那些让我印象深刻的bug进行复现与总结。
一、MYSQL resource temporarily unavailable
在一开始使用C语言操作数据库的时候我们难免有些手不顺心的感觉,往往一些简单的mysql语句在我的程序中就会出现问题
在我的错误处理程序my_err
void my_err(const char *str, const int line)
{
fprintf(stderr, "%d : %s : %s", line, str, strerror(errno));
exit(1);
}
下,总是会出现下面这样的错误
Mysql resource temporarily unavailable(Mysql资源暂时不可用)
在stackoverflow上给出的下面这6种原因距离我目前的开发有点过于遥远。
- You’ve run out of memory available to MySQL.
- You’ve run out of file descriptors available to your MySQL user account.
- You’ve run into a livelock or deadlock scenario where your thread is unsatisfiable.
- Your system is running out of PIDs available to fork.
- An upstream proxy or connection manager has run out of resources and ceased servicing requests.
- Your port mapper has exhausted itself after 65,536 connections.
其实我们大部分情况下,这种报错都意味着SQL语法出了问题。我们可以在每次调用mysql_real_query之后、my_err()之前调用mysql_error(&mysql),它可以返回像mysql中那样的语法错误报错信息,方便我们找出语法出错的位置。
具体就像这样:
if(mysql_real_query(&mysql, query_str, strlen(query_str)) != 0)
{
printf("%d:error:%s", __LINE__, mysql_error(&mysql));
my_err("mysql_real_query error", __LINE__);
}
二、客户端输入的信息无法被服务器正确读入
这个问题的出现可能不具有普遍性,每个人的服务器架构不同,这个问题出现的原因也就不同(更大的可能是大家根本不会遇到这个问题)。而我遇到这个问题主要就是服务器的架构问题,我在服务器架构的问题上出过几次严重的错误,因为架构的问题,整个项目被我重构了两次。下面就先讲一讲我的服务器的架构。
下面就是一个最基础的epoll服务器架构
while(1)
{
ret = epoll_wait(epfd, ep, 1024, -1)
for(i=0; i<ret; i++)
{
if(ep[i].data.fd == lfd)
{
clit_addr_len = sizeof(clit_addr);
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
if(cfd == -1)
my_err("accept error", __LINE__);
tep.data.fd = cfd;
tep.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
}
else
{
n = read(ep[i].data.fd, buf, sizeof(buf));
if(n == 0)
{
close(ep[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd, NULL);
}
else if(n > 0)
{
cm.cfd = ep[i].data.fd;
cm.mysql = mysql;
cm.clit_addr = clit_addr;
if(pthread_create(&thid, NULL, serv_new_client, (void *)&cm) == -1)
{
my_err("pthread_create error", __LINE__);
}
}
else
{
my_err("read error", __LINE__);
}
}
}
}
我整个服务器的思路就是,每当epoll_wait()接收到一个套接字的活动后,就为这个套接字创建一个线程,之后这个线程就专门负责处理这个客户端套接字传来的包。
但是这朴素的想法搭配上朴素的算法,并没有使程序可以正常的运行。
这个BUG的出现形式也很具有迷惑性——客户端输入的消息(偶尔)无法被服务器接收到。我们会很自然的去代码中找read()/write()的问题,但是我在接收不到内容的代码附近打了许多断点进行调试,好久也没有找到read()/write()函数到底哪里出了问题。
最后,我想到了epoll所在的主线程。epoll_wait()函数的功能在于检索套接字是否有活动产生并把这个活跃套接字加入ep[i].data.fd中,在上面的服务器设计中,主线程中的read还在不断的接收来自客户端包,而子线程中的read要与主线程的read来竞争这个包。因此就会出现客户端输入的指令有时可以被正确响应,有时却无法被应答。
那么让主线程中的read不干扰到子线程中的read就成了修改bug的一条路。
下面是修改过后的server
while(1)
{
//等待客户端的连接请求
ret = epoll_wait(epfd, ep, MAXEVE, -1);
for(i=0; i<ret; i++)
{
if(ep[i].data.fd == lfd) //lfd满足读事件,有新的客户端发起连接请求
{
clit_addr_len = sizeof(clit_addr);
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
if(cfd == -1)
my_err("accept error", __LINE__);
tep.data.fd = cfd;
tep.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
printf("ip %s is connect\n", inet_ntoa(clit_addr.sin_addr));
strcpy(temp, "Welcome to my_server!\n");
Write(cfd, temp);
strcpy(temp, "输入任意建进入欢迎界面\n");
Write(cfd, temp);
}
else //cfd们满足读事件,有客户端数据写来
{
j = 0;
flag = 0;
while(j<1000)
{
if(clients[j] == ep[i].data.fd)
{
flag = 1;
break;
}
j++;
}
if(flag == 1) //只有当该活跃套接字第一次出现时,才会为其创建线程。
{
continue;
}
//读套接字
n = read(ep[i].data.fd, buf, sizeof(buf));
if(n == 0)
{
for(j=0; j<birth; j++)
{
if(clients[j] == ep[i].data.fd)
{
clients[j] = 0;
}
}
close(ep[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd, NULL);
}
else if(n > 0) //接受到客户端的消息,就转发给子线程serv_new_client()处理
{
//判断数组clients中有没有此用户的套接字
j = 0;
flag = 0;
while(j<1000)
{
if(clients[j] == ep[i].data.fd)
{
flag = 1;
break;
}
j++;
}
printf("flag = %d\n", flag);
if(flag == 0) //flag = 0 证明该客户端还未创建属于它的子线程
{
cm.cfd = ep[i].data.fd;
cm.mysql = mysql;
cm.clit_addr = clit_addr;
clients[birth] = cfd;
birth++;
if(pthread_create(&thid, NULL, serv_new_client, (void *)&cm) == -1)
{
my_err("pthread_create error", __LINE__);
}
}
}
else
{
my_err("read error", __LINE__);
}
}
}
}
三、缓存区清空问题
在进行客户端与服务器端进行数据交互的时候,缓存区起着非常重要的作用。在使用缓存区时,我们要适时的memset()清空缓存区,当我们的一些输入最后没有结尾’\0’时,空的缓存区可以让我们避免乱码的问题;而且可能会出现一些奇异的BUG;不管怎么样,清理缓存区都是一个很好的编程习惯。
在经常要用到write()/read()或send()/recv()的时候,我们可以封装几个函数帮我们完成缓存区的清理。像下面这样
int Read(int fd, void *buf, size_t count, int line)
{
int n;
memset(buf, 0, sizeof(buf)); //清空缓存
n = read(fd, buf, count);
if(n < 0)
{
my_err("read error", line);
}
return n;
}
总结
本文只是自己在编写聊天室的时候遇到的一些奇异bug,MYSQL的错误可能大家遇到的会很多,但是第二个BUG想要出现需要我们的服务器架构思路基本相似,大部分人并不会遇到;这里只是给大家在改不出BUG时提供一些灵感。第三个问题是一个编程习惯的问题,或许在绝大部分时候我们都不会因为缓存区没有清空而出问题,但是养成一个良好的编程习惯可以避免我们在以后会遇到的一些奇怪问题。
关于这个聊天室的代码都存放在我的github,代码还很不成熟,目前还在修补联机测试时出现的BUG。