文章目錄
-
- 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()