一个关于 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 记录的主要内容,如果未能解决你的问题,请参考以下文章

从忽视再到复现,我是如何一步步发现SQLite Bug

一个奇怪的bug

多线程bug学习记录

关于 insufficient memory case 4 的解决记录

记录一个前端bug的解决过程

软件评测