一个关于 recv 的可复现奇怪 bug 记录
Posted 看,未来
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个关于 recv 的可复现奇怪 bug 记录相关的知识,希望对你有一定的参考价值。
demo
其实不止一个 bug,昨天就写了篇小短文,但是那个 bug 复现了几次之后就无法复现了,所以也就不提了,提了也没用,复现不了说给谁信呢?
server.cc
没有头文件,毕竟是陪衬,后面要专门写一个reactor模型做网络层。
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include "service.hpp"
using namespace std;
int main()
{
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("192.168.190.129"); //具体的IP地址
serv_addr.sin_port = htons(8887); //端口
bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
//进入监听状态,等待用户发起请求
listen(serv_sock, 20);
//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
cout << "Acceptting···" << endl;
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
while (1)
{
Service::instance()->check_service(clnt_sock);
}
return 0;
}
service.hpp
业务层的头文件,和本文无关的我先抹去了。
#ifndef SERVICE_H_
#define SERVICE_H_
#include "json.hpp"
#include <map>
#include <unordered_map>
#include <functional>
#include <mutex>
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#define DEBUG 1
using json = nlohmann::json;
using namespace std;
using namespace placeholders;
const int click_time = 2; //点击间隔时间
enum EnMsgType
{
LOGIN_TYPE = 2, //正常登录
REG_TYPE, //正常注册
REGS_TYPE, //多人注册
UPCOURSE_TYPE, //发布课程
UPSCORE_TYPE, //发布成绩
CHOOSECOURSE_TYPE, //选择课程
CANCELCOURSE_TYPE, //撤销选课
SEARCHSCORE_TYPE, //成绩查询
};
//处理消息的事件回调方法类型
using MsgHandler = std::function<void(int fd,char* msg)>;
class Service
{
public:
//单例模式
static Service* instance(); //为什么要做成单例?你去看看它数据域就知道了。
//1、数据域大
//2、数据域应全局共享
//诊断业务:
void check_service(int fd);
private:
Service();
//如果这个类对象需要析构,那说明服务器关了,所以这个对象交给操作系统打理了
//网络层只需要将数据包直接转入业务层,不需要去拆包
void Login(int fd,char *msg);
//获取消息对应的处理器
MsgHandler getHandle(int msgid);
private:
//存储消息id和对应的处理方法,用map就够了
std::map<int,MsgHandler> _msgHanderMap;
//存储服务用户时间戳
std::unordered_map<int,time_t> _userTimeMap;
//存储服务用户令牌环
std::unordered_map<int,long> _userTokenMap;
//定义互斥锁
std::mutex _connMutex;
};
#endif
service.cc
bug就出在这里面,主要是 recv 之后就不正常,创建好的 char* 对象会接收到大于指定大小的内容,但是 recv 的返回值却是指定大小。
奇怪之处不止在这里,第一个 buf 使用new分配空间并无不妥,在于第二个 buff,使用 new 申请空间,则会在第三次接收数据时出现脏数据,稳稳的,测了十几次,就是第三个数据包接收出问题(每个数据包内容都一样)。
将 char* 转为 char[lenth] 之后恢复正常。
诡异的不止于此,当第二个 buff 恢复正常之后,我想是不是堆区太乱了啊?于是就想把第一个 buf 也换成 char[8],但是又出现了脏数据的问题,这回更快,第一个数据包就出现了脏数据,无语得很。我又想,不会是内存串了吧?于是我打印出地址,二者之间差了80个字节,有什么串不串的,而且我还 memset 了,依旧无济于事。
所以,这个 bug 是解决了吗?我觉得没有,虽然能跑起来,但是我不知道为什么会这样,那就是没有解决。
#include "service.hpp"
Service *Service::instance()
{
static Service _service;
return &_service;
}
//获取消息对应的处理器
MsgHandler Service::getHandle(int msgid)
{
auto it = _msgHanderMap.find(msgid);
if (it == _msgHanderMap.end())
{
return [=](int fd, char *msg)
{
cout << "magid:" << msgid << "can not find handle!!!" << endl; //这里应该有日志模块
};
}
else
{
return _msgHanderMap[msgid];
}
}
/*
1、检查业务是否在本服务器被处理,这一点有待考证,为什么一定要把一台服务和一个客户端绑死呢?
客户端上线的时候绑定了一台服务器,下线的时候就应该从那台服务器中解绑定,下次再上线的时候重新绑定一台服务器即可。
所以这里直接进入第二步,检查令牌环。
2、检查令牌环 //登录之后才有令牌环,所以这个应该在具体业务里面做,令牌环应该以具体账号+密码的形式组成,如果不放心,还可以加上时间戳
3、检查时间戳 //每个连接在服务器上都保留有一个时间戳,防止过于频繁的访问,设置为全局变量(往后可以设定为配置文件形式),初步设定 1 s
4、检查数字签名 //这个也可以在解包之前做
5、调度任务管理器
*/
void Service::check_service(int fd)
{
//接收包头
char* buf = new char[8];
//char buf[8] = {}; //为什么这里用这个就会出现内存垃圾?是两块内存被复用了吗?
cout<<&buf<<endl;
//memset(buf,0,8);
int n = recv(fd, buf, 8, 0);
if (n == -1 || n == 0)
{
//客户端退出
_connMutex.lock();
_userTimeMap.erase(fd); //如果时间戳为 1,就是拉黑了,给它清空了它一会儿又来
close(fd);
_connMutex.unlock();
return;
}
// #if DEBUG
// cout << n << endl;
// cout << "buf:" << buf << endl;
// #endif
//拆解包头
int num = atoi(buf);
int a = num / 10000; //前四个为 X + 包体长度
int b = num % 10000; //后四个为数字签名
int lenth = a % 1000; //获取包体长度
int bid = a / 1000 + b / 1000; //获取业务id
b %= 1000;
///cout << lenth << endl; //这里是正常长度
//char* buff = new char[lenth];
char buff[lenth] = {};
cout<<&buff<<endl;
// char* buff = new char[lenth];
//memset(buf,0,lenth);
//先把缓冲区数据拿走,别占位置
n = recv(fd, buff, lenth, 0); //为什么走完这一步lenth就发生了突变(这个bug已经无法复现,最初的解决方法是将lenth等一众会突变的数据放到全局变量区去)
if (n < 0)
{
cout << "recv errno!" << endl; //这里应该写入日志,日志模块这不是还没开发嘛
exit(-1);
}
cout << strlen(buff) << endl; //这里也已经不正常了
cout << n << endl; //n是正常长度
cout << buff << endl; //buff已经不正常了
//时间戳处理:
time_t t;
time(&t);
auto it = _userTimeMap.find(fd);
if (it == _userTimeMap.end()) //未有此用户
{
if (bid != 2)
{ //如果不是登录业务
cout << "Time Flag Do Not Find!!!" << endl;
//此处应有日志
return;
}
else
{
_connMutex.lock();
_userTimeMap.insert({fd,t}); //如果时间戳为 1,就是拉黑了,给它清空了它一会儿又来
_connMutex.unlock();
}
}
else
{
if (it->second == 1) //如果是登录,这里是没有it的
{ //被拉黑了
cout << "Bad Login!!!" << endl;
//此处应有日志
return;
}
if (t - it->second < click_time)
{
cout << "frequent fd:" << fd << endl;
//此处应有日志
//清理连接(如果是用户连接,过不了客户端那边的)
_connMutex.lock();
_userTimeMap[fd] = 1; //如果时间戳为 1,就是拉黑了,给它清空了它一会儿又来
close(fd);
_connMutex.unlock();
return;
}
}
//sign验证
int count = 0;
for (int i = 0; i < lenth; i++)
{
count += i * buff[i];
}
count %= 1000;
if (b != count)
{
cout << "业务包被篡改,业务号:" << bid << endl;
cout<<count<<endl;
cout<<lenth<<endl;
//此处可以考虑发个包回去给客户端
//此处还要写入日志
//或者直接丢弃这个包
return;
}
//通过msgid获取业务回调,进行网络模块和任务模块之间的解耦合
auto msgHandler = Service::instance()->getHandle(bid);
msgHandler(fd, buff);
}
//用户登录
void Service::Login(int fd, char *msg)
{
json js = json::parse(msg);
//检查账号密码是否正确
if (js["id"] == "12345678" && js["pwd"] == "123456")
{
//获取时间戳
time_t t;
time(&t); //直接用时间戳当令牌环,机智如我
string res = to_string(t);
//校验码设计(四位数字)
int count = 0;
int len = res.size();
for (int i = 0; i < len; i++)
{
count += i * res[i]; //如果这样的话就不支持中文了(本来也没要在数据包里面放中文嘛)
}
count %= 1000;
count += 9000;
res = to_string(len + 2000) + to_string(count) + res;
// int lenth = strlen(str.c_str()); //sizeof response 老是算成16,sizeof string也有问题
//cout << res << endl;
send(fd, res.c_str(), len + 8, 0); //直接发串儿,就不打包了
}
else
{
char *res = new char[8];
sprintf(res, "%d%d", 9000, 2000); //不用包体,直接一个头过去就好
send(fd, res, 8, 0);
}
}
//注册消息以及对应的回调操作
Service::Service()
{
//这里对函数指针取地址别忘了
_msgHanderMap.insert({LOGIN_TYPE, std::bind(&Service::Login, this, _1, _2)});
_msgHanderMap.insert({REG_TYPE, std::bind(&Service::Register, this, _1, _2)});
_msgHanderMap.insert({UPCOURSE_TYPE, std::bind(&Service::UpCourse, this, _1, _2)});
_msgHanderMap.insert({CHOOSECOURSE_TYPE, std::bind(&Service::ChooseCourse, this, _1, _2)});
_msgHanderMap.insert({CANCELCOURSE_TYPE, std::bind(&Service::CancelCourse, this, _1, _2)});
_msgHanderMap.insert({SEARCHSCORE_TYPE, std::bind(&Service::CancelCourse, this, _1, _2)});
}
客户端代码
Python写的
import time
from socket import *
HOST = '192.168.190.129' # or 'localhost'
PORT = 8887
BUFSIZ = 1024
ADDR = (HOST, PORT)
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
while True:
data1 = "10321568{\\"id\\":\\"12345678\\",\\"pwd\\":\\"123456\\"}"
tcpCliSock.send(data1.encode())
print(data1)
data2 = tcpCliSock.recv(8).decode('utf-8')
print(data2)
num = int(data2)
num1 = int(num/10000)
num2 = num1%1000
data3 = tcpCliSock.recv(num2).decode('utf-8')
print(data2)
time.sleep(2)
tcpCliSock.close()
以上是关于一个关于 recv 的可复现奇怪 bug 记录的主要内容,如果未能解决你的问题,请参考以下文章