前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个关于 recv 的可复现奇怪 bug 记录

一个关于 recv 的可复现奇怪 bug 记录

作者头像
看、未来
发布2021-10-25 13:35:45
5450
发布2021-10-25 13:35:45
举报

文章目录

demo

其实不止一个 bug,昨天就写了篇小短文,但是那个 bug 复现了几次之后就无法复现了,所以也就不提了,提了也没用,复现不了说给谁信呢?

server.cc

没有头文件,毕竟是陪衬,后面要专门写一个reactor模型做网络层。

代码语言:javascript
复制
#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

业务层的头文件,和本文无关的我先抹去了。

代码语言:javascript
复制
#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 是解决了吗?我觉得没有,虽然能跑起来,但是我不知道为什么会这样,那就是没有解决。

代码语言:javascript
复制
#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写的

代码语言:javascript
复制
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()

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-10-20 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • demo
    • server.cc
      • service.hpp
        • service.cc
          • 客户端代码
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档