天天看點

一個關于 recv 的可複現奇怪 bug 記錄

文章目錄

    • demo
      • server.cc
      • service.hpp
      • service.cc
      • 用戶端代碼

其實不止一個 bug,昨天就寫了篇小短文,但是那個 bug 複現了幾次之後就無法複現了,是以也就不提了,提了也沒用,複現不了說給誰信呢?

沒有頭檔案,畢竟是陪襯,後面要專門寫一個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;
}
           

業務層的頭檔案,和本文無關的我先抹去了。

#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
           

bug就出在這裡面,主要是 recv 之後就不正常,建立好的 char* 對象會接收到大于指定大小的内容,但是 recv 的傳回值卻是指定大小。

奇怪之處不止在這裡,第一個 buf 使用new配置設定空間并無不妥,在于第二個 buff,使用 new 申請空間,則會在第三次接收資料時出現髒資料,穩穩的,測了十幾次,就是第三個資料包接收出問題(每個資料包内容都一樣)。

将 char* 轉為 char[lenth] 之後恢複正常。

#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)});
}
           

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()