計算機網絡 c++17+ 基於asio2的protocol6(選擇重傳)udp異步模擬通信樣例 [廢棄,原模板工程實現請看《烤全羊啊嗯》]


TODO\color{red}{TODO}
+++NAK,ACK\color{red}{+++確定NAK, ACK之類的意義是否混亂}
+++ //NAK\color{red}{+++添加隨機~誤碼/丟包/NAK}
++調\color{red}{++調整延時與同步策略,刪除不必要的對象組}
+使spdlog\color{pink}{+使用spdlog生成運行日誌}

config.h

#pragma once

#include <array>
#include <memory>
#include <vector>
#include <random>
#include <cstdint>
#include <timer.h>
#include <type_traits>
#include <sdkddkver.h>
#include <asio2/udp/udp_cast.hpp>
#include <asio2/udp/udp_client.hpp>
#include <asio2/udp/udp_server.hpp>
#include <asio2/udp/udp_session.hpp>

#define CLIENT_MEMBER
#define SERVER_MEMBER

#define TEST(x) printf("********TEST" #x "********\n");

using byte   = uint8_t;
using uint16 = uint16_t;
using uint32 = uint32_t;
using uint64 = uint64_t;

constexpr uint32 MAX_BUFFER_SIZE = 1024;

constexpr uint64 CRC32_SETENCE  = 0x104C11DB7;
constexpr uint32 CRC32_GENERATE = CRC32_SETENCE & 0xFFFF; //0x4C11DB7

// Get memory layout info: /d1 reportSingleClassLayoutXXX

enum class NetworkLayerStatus {
	PHYSICAL_LAYER_READY,
	NETWORK_LAYER_READY,
	FRAME_RECEIVED,
	DATA_TIMEOUT,
	ACK_TIMEOUT
};

//enum class FrameType: byte {
//};

constexpr byte   FRAME_DATA = 0;							//type+seqNum+ack+data+crc32
constexpr byte   FRAME_ACK  = 1;							//type+ack+crc32
constexpr byte   FRAME_NAK  = 2;							//type+nak+crc32

constexpr double PACKAGE_LOSING_RATE	= 1e-5;			//factor
constexpr double ERR_BYTE_RATE			= 1e-6;

constexpr uint32 PACKAGE_SENDING_DELAY  = 270;				//ms

constexpr uint32 MAX_SEQ			= 31;					//2^n-1 窗口序號
constexpr uint32 PKT_LEN			= 256;					//248;	總長度應小於udp各端緩衝區的大小?
constexpr uint32 CACHE_SIZE			= (MAX_SEQ + 1) / 2;
constexpr uint32 ACK_TIMEOUT_LMT	= 1000;					//ms
constexpr uint32 DATA_TIMEOUT_LMT	= 3000;					//ms

#pragma pack(2)
struct Frame {
	byte 						type	= FRAME_DATA;
	byte						ack		= 0;
	byte						seqNum	= 0;
	byte                        _		= 0;				//_定義內存對齊位
	std::array<byte, PKT_LEN>	data	= {};
	uint32						crc32	= 0x0;
};

struct ACKFrame {
	byte 						type  = FRAME_ACK;
	byte                        ack	  = 0;
	uint32						crc32 = 0x0;
};

struct NAKFrame {
	byte 						type  = FRAME_NAK;
	byte                        nak   = 0;
	uint32						crc32 = 0x0;
};
#pragma pack()

timer.h(棄用?)

#pragma once

#include <mutex>
#include <chrono>
#include <thread>
#include <atomic>
#include <memory>
#include <functional>
#include <type_traits>
#include <condition_variable>

class Timer {
public:
	Timer() :_expired(true), _tryToExpire(false) {}

	Timer(const Timer& t) {
		_expired = t._expired.load();
		_tryToExpire = t._tryToExpire.load();
	}

	~Timer() {
		Expire();
		// std::cout << "timer destructed!" << std::endl;
	}

	void StartTimer(int interval, std::function<void()> task) {
		if (_expired == false) {
			// std::cout << "timer is currently running, please expire it first..." << std::endl;
			return;
		}
		_expired = false;
		std::thread([this, interval, task]() {
			while (!_tryToExpire) {
				std::this_thread::sleep_for(std::chrono::milliseconds(interval));
				task();
			}
			// std::cout << "stop task..." << std::endl; {
			std::lock_guard<std::mutex> locker(_mutex);
			_expired = true;
			_expiredcond.notify_one();
		}).detach();
	}

	void Expire() {
		if (_expired) {
			return;
		}
		if (_tryToExpire) {
			// std::cout << "timer is trying to expire, please wait..." << std::endl;
			return;
		}
		_tryToExpire = true;
		{
			std::unique_lock<std::mutex> locker(_mutex);
			_expiredcond.wait(locker, [this] {
				return _expired == true;
			});
			if (_expired == true) {
				// std::cout << "timer expired!" << std::endl;
				_tryToExpire = false;
			}
		}
	}

	template<typename Call, class... Args>
	void SyncWait(int after, Call&& f, Args&&... args) {
		//using retType = typename std::invoke_result<Call, Args....>::type;
		auto task = std::bind(
			std::forward<Call>(f),
			std::forward<Args>(args)...
		);
		std::this_thread::sleep_for(std::chrono::milliseconds(after));
		task();
	}

	template<typename Call, class... Args>
	void AsyncWait(int after, Call&& f, Args&&... args) {
		if (_asyncRunning)
			throw std::runtime_error(
				"forbidden to start a new async event now"
		);
		_asyncRunning = true;
		//if(_asyncId._Id)
		/*using retType =
			typename std::invoke_result<Call, Args...>::type;

		std::function<retType(Args...)> */
		auto task = std::bind(
			std::forward<Call>(f),
			std::forward<Args>(args)...
		);
		std::thread([after, task, this] {
			//_asyncRunning = true;
			std::chrono::milliseconds limit(after);
			std::chrono::milliseconds interval(50);
			auto start = std::chrono::steady_clock::now();
			while (
				_asyncRunning &&
				std::chrono::steady_clock::now()-start < limit
			) std::this_thread::sleep_for(interval);
			if (_asyncRunning) {
				task();
			}
			_asyncRunning = false;
		}).detach();

		/*_asyncThrdInfo._Hnd = ((_Thrd_t*)(_asyncThrd))->_Hnd;
		_asyncThrdInfo._Id = ((_Thrd_t*)(_asyncThrd))->_Id;

		_asyncThrd->detach();*/
	}

	void StopAsyncWait() {
		_asyncRunning = false;
		/*TerminateThread(_asyncThrdInfo._Hnd, 0);
		delete _asyncThrd;
		_asyncThrd = nullptr;*/
	}

private:
	std::mutex				_mutex;
	std::atomic<bool>		_expired;
	std::thread::id			_asyncId;
	std::atomic<bool>		_tryToExpire;
	std::condition_variable _expiredcond;
	
	std::atomic_bool		_asyncRunning = false;
	/*std::thread*			_asyncThrd = nullptr;
	_Thrd_t					_asyncThrdInfo { 0,0 };*/
};

main.cpp

#include "ProtocolApp/ProtocolApp.h"

int main(int argc, char* argv[]) {
	ProtocolApp app("1919");
	app.startServer().detach();
	app.startClient().detach();
	while (true) {
		std::this_thread::sleep_for(
			std::chrono::seconds(1)
		);
	}
	return 0;
}

ProtocolApp.h

#pragma once

#include <config.h>

// 目前只有一對一廣播模式

class ProtocolApp {
public:
	using strView = std::string_view;
	explicit ProtocolApp(const strView& port):
		_port(port), _running(true),

		_client(new asio2::udp_client),
		_server(new asio2::udp_server),

		_servCache(new std::array<Frame, CACHE_SIZE>),
		_sChecked(new std::array<bool, CACHE_SIZE>),

		_clntCache(new std::array<Frame, CACHE_SIZE>),
		_cChecked(new std::array<bool, CACHE_SIZE>)
	{
		std::thread(&ProtocolApp::InitServer, this).detach();
		std::thread(&ProtocolApp::InitClient, this).detach();
	}

	virtual ~ProtocolApp() {
		_server->stop();
		_client->stop();

		delete _server;
		delete _client;

		delete _servCache;		_servCache = nullptr;
		delete _sChecked;		_sChecked = nullptr;

		delete _clntCache;		_clntCache = nullptr;
		delete _cChecked;		_cChecked = nullptr;
	}

	static uint32
		GetCRC32Tail(void* data, size_t length);
	static uint32
		GenerateRandomFrameData(void* data, size_t length);
	inline static bool		
		CheckCRC32Tail(void* data, size_t length, uint32 crcTail);
	static void  //aborted
		buildFrame(std::array<byte, sizeof(Frame)>& data, Frame& srcFrame);
	
	static void 
		UpperLayerCall(const size_t index, const size_t frameCnt);

	void 
		InitServer();
	void 
		InitClient();
	void
		sendDataFrame(const size_t index);

	std::thread
		startServer();
	std::thread
		startClient();

private:
	strView								_port;
	bool								_running;

private CLIENT_MEMBER:
	asio2::udp_server*					_server;
	std::mutex							_servNetMutex;

	std::array<Frame, CACHE_SIZE>*		_servCache;
	std::array<bool,  CACHE_SIZE>*		_sChecked;

	uint32								_sBufIndex	= 0;
	uint32								_sIndexL	= 0;
	uint32								_sIndexR	= 0;
	unsigned long long					_sTotalRecv = 0;
	unsigned long long					_sTotalSend = 0;


private SERVER_MEMBER:
	asio2::udp_client*					_client;
	std::mutex							_clntNetMutex;

	std::array<Frame, CACHE_SIZE>*		_clntCache;
	std::array<bool,  CACHE_SIZE>*		_cChecked;

	uint32								_cBufIndex	= 0;
	uint32								_cIndexL	= 0;
	uint32								_cIndexR	= 0;
	unsigned long long					_cTotalRecv = 0;
	unsigned long long					_cTotalSend = 0;
};

protocolApp.cpp

#include "ProtocolApp.h"

#define _servCache		(*_servCache)
#define _sChecked		(*_sChecked)
#define _sFrameTimer	(*_sFrameTimer)

#define _clntCache		(*_clntCache)
#define _cChecked		(*_cChecked)
#define _cFrameTimer	(*_cFrameTimer)

template<typename A, typename B, typename C>
bool isBetween(A&& left, B&& b, C&& right) {
	return
		((left <= b) && (b <= right)) ||
		((left <= b) && (right < left)) ||
		((b <= right) && (right < left));
}

template<typename T>
bool isPerfectTure(T&& data) {
	for (auto i = 0; i < data.size(); i++) {
		if (!data[i])
			return false;
	}
	return true;
}
void ProtocolApp::InitServer() {
	_sBufIndex	= 0;
	_sIndexL	= 0;
	_sIndexR	= CACHE_SIZE-1;
	// typeof session_ptr: std::shared_ptr<asio2::udp_session>&
	// 可使用連續.bind鏈,但會影響vs代碼結構化導致操作麻煩
	_server->bind_recv([this](auto& session_ptr, strView s)
	{
		/*static */std::random_device r;								// 同下
		/*static */std::default_random_engine gen(r());					// 覺得不夠隨機,取消這兩個static
		static std::uniform_real_distribution<double> zero_one_distr(
			0, 1
		);
		static std::chrono::milliseconds SENDING_DELAY(PACKAGE_SENDING_DELAY);

		if (
			zero_one_distr(gen) > PACKAGE_LOSING_RATE
		)
		//TODO:
		try {

			if (s.size() == sizeof(Frame)) {
				Frame temp;
				memcpy(&temp, s.data(), s.size());

				uint16 errCnt = 0;
				for (int i = 0; i < sizeof(Frame); i++) {
					if (zero_one_distr(gen) < ERR_BYTE_RATE) {
						errCnt++;
					}
				}
				for (uint16 i = 0; i < errCnt; i++) {
					byte* pErrByte =
						reinterpret_cast<byte*>(&temp) + gen() % sizeof(ACKFrame);
					*pErrByte = ~(*pErrByte);
				}

				if (isBetween(_sIndexL, temp.ack, _sIndexR)) {
					if (temp.type == FRAME_DATA && CheckCRC32Tail(temp.data.data(), PKT_LEN, temp.crc32)) {
						//TEST(1);
						//std::this_thread::sleep_for(std::chrono::seconds(1));
						//TEST(2);
						uint32 sub = ((temp.ack + MAX_SEQ + 1) - _sIndexL) % (MAX_SEQ + 1);
						memcpy(&_servCache[(_sBufIndex + sub) % CACHE_SIZE], &temp, sizeof(Frame));
						printf("COPY FRAME %d INTO _servCache[%d]\n", temp.ack, (_sBufIndex + sub) % CACHE_SIZE);
						_sChecked[(_sBufIndex + sub) % CACHE_SIZE] = true;

						uint32 count = CACHE_SIZE;
						for (uint32 i = _sBufIndex; i < _sChecked.size() + _sBufIndex; i++) {
							if (!_sChecked[i % CACHE_SIZE]) {
								count = static_cast<byte>(i - _sBufIndex);
								break;
							}
							else {
								_sChecked[i % CACHE_SIZE] = false;
							}
						}

						//emit signal for upper layer start handling
						UpperLayerCall(_sBufIndex, count); 

						{
							ACKFrame ack;
							ack.ack = temp.ack;
							ack.crc32 = GetCRC32Tail(&ack, sizeof(ACKFrame));
							ack.type = FRAME_ACK;
							// emulate data sending delay
							std::this_thread::sleep_for(SENDING_DELAY);
							_server->send(
								asio::buffer(&ack, sizeof(ACKFrame))
							);
							printf("SEND ACK FRAME %d FINISHED\n", ack.ack);
						}
						//std::thread(
						//	[this](byte ack_) {
						//		
						//	}, temp.ack
						//).detach();
						

						if (count != 0) {	/*&& count != CACHE_SIZE*/
							_sIndexL = (_sIndexL + count) % (MAX_SEQ + 1);
							_sIndexR = (_sIndexR + count) % (MAX_SEQ + 1);
							_sBufIndex = (_sBufIndex + count) % CACHE_SIZE;
						}

						printf("---------------SERVER FORWARD STEP COUNT: %d---------------\n", count);
					}
					// crc check error, send NAK Frame
					else {
						NAKFrame nak;
						nak.nak = temp.ack;
						nak.crc32 = GetCRC32Tail(&nak, sizeof(NAKFrame));
						nak.type = FRAME_NAK;
						// emulate data sending delay
						std::this_thread::sleep_for(SENDING_DELAY);
						_server->send(
							asio::buffer(&nak, sizeof(NAKFrame))
						);
						printf("SEND NAK FRAME %d FINISHED\n", nak.nak);
					}
				}
			}
		}
		catch (std::exception& err) {
			printf("\n\n%s\n\n", err.what());
		}
		else {
			printf("————————DATA PACKAGE SEGMENTATION LOST————————\n");
		}
	});

	_server->bind_connect([](auto& session_ptr)
	{
		printf("client connected: %s %u %s %u\n",
			session_ptr->remote_address().c_str(),
			session_ptr->remote_port(),
			session_ptr->local_address().c_str(),
			session_ptr->local_port()
		);
	
	});

	_server->bind_disconnect([](auto& session_ptr)
	{
		printf("client disconnected: %s %u %s\n",
			session_ptr->remote_address().c_str(),
			session_ptr->remote_port(),
			asio2::last_error_msg().c_str()
		);
	});

	_server->bind_handshake([](auto& session_ptr, asio::error_code ec)
	{
		printf("client handshaked: %d %s\n",
			asio2::last_error_val(),
			asio2::last_error_msg().c_str()
		);

	});
	
	_server->bind_start([this](asio::error_code ec)
	{
		memset(_sChecked.data(), false, CACHE_SIZE);
		memset(_servCache.data(), 0, CACHE_SIZE * sizeof(Frame));
		_sIndexL = 0;
		_sIndexR = CACHE_SIZE-1;

		if (asio2::get_last_error())
			printf("start udp server failed: %d %s\n",
				asio2::last_error_val(),
				asio2::last_error_msg().c_str()
			);
		else
			printf("start udp server succeed: %s %u\n",
				_server->listen_address().c_str(),
				_server->listen_port()
			);
	});

}

void ProtocolApp::InitClient() {

	_client->connect_timeout(std::chrono::seconds(3));
	_client->local_endpoint(asio::ip::udp::v4(), 810);	//本地轉發EP?

	_client->bind_connect([&](asio::error_code ec)
	{
		if (asio2::get_last_error())
			printf("connect failed: %d %s\n",
				asio2::last_error_val(),
				asio2::last_error_msg().c_str()
			);
		else
			printf("connect success: %s %u\n",
				_client->local_address().c_str(),
				_client->local_port()
			);
	
	});
	
	_client->bind_disconnect([](asio::error_code ec)
	{
		printf("disconnected: %d %s\n",
			asio2::last_error_val(),
			asio2::last_error_msg().c_str()
		);
	
	});
	
	_client->bind_recv([this](strView s)
	{
/*		if (
			ACK也要丟的話...
		) */{
			ACKFrame ack;
			switch (s.size()) {
				case sizeof(Frame) :
					break;
				case sizeof(ACKFrame) :
					memcpy(&ack, s.data(), sizeof(ACKFrame));
					if (ack.type == FRAME_ACK) {
						std::lock_guard<std::mutex> lock(_clntNetMutex);
						{
							if (isBetween(_cIndexL, ack.ack, _cIndexR)) {
								size_t arrIndex =
									(ack.ack + MAX_SEQ + 1 + _cBufIndex - _cIndexL) %
									(CACHE_SIZE);
								_cChecked[arrIndex] = true;
								printf("\t\t\t\t\tGet frame %d confirm\n", ack.ack);
							}
							else {
								printf("\t\t\t\t\tDrop frame %d confirm\n", ack.ack);
							}
						}
					}
					else if (ack.type == FRAME_NAK) {
						std::lock_guard<std::mutex> lock(_clntNetMutex);
						{
							if (isBetween(_cIndexL, ack.ack, _cIndexR)) {
								size_t arrIndex =
									(ack.ack + MAX_SEQ + 1 + _cBufIndex - _cIndexL) %
									(CACHE_SIZE);
								sendDataFrame(arrIndex);
								printf("\t\t\t\t\tResend data frame %d\n", ack.ack);
							}
							else {
								printf("\t\t\t\t\tDrop nak request %d\n", ack.ack);
							}
						}
					}
					//_cFrameTimer[ack.ack].StopAsyncWait();
					break;
				default:
					break;
			}
		}
		//asio::write(client.get_socket(), asio::buffer(s));
	});
}

std::thread ProtocolApp::startClient() {

	static const byte TEST_SEQ_NUM = 19;

	return std::thread([this] {
		if (!_client->start("127.0.0.1", _port))
			printf("start failure: %d %s\n", asio2::last_error_val(), asio2::last_error_msg().c_str());

		std::random_device r;
		std::default_random_engine gen(r());
		std::uniform_int_distribution<int> distribution(20, 50);

		memset(_cChecked.data(), false, _cChecked.size() * sizeof(bool));
		memset(_clntCache.data(), 0, _cChecked.size() * sizeof(Frame));

		_cIndexL	= 0;
		_cIndexR	= CACHE_SIZE-1;
		_cBufIndex	= 0;

		while (_running) {

			static std::chrono::milliseconds CHECK_INTERVAL(50);
			static std::chrono::milliseconds FRAME_SEND_DELAY(270);
			static std::chrono::milliseconds TIMEOUT_LIMIT(DATA_TIMEOUT_LMT);

			for (byte i = 0; i < CACHE_SIZE; i++) {
				byte index = (_cBufIndex + i) % CACHE_SIZE;
				if (!_cChecked[index]) {
					Frame& frameBuf = _clntCache[index];
					frameBuf.crc32 =
						GenerateRandomFrameData(
							frameBuf.data.data(), PKT_LEN
						);
					frameBuf.type = FRAME_DATA;
					frameBuf.seqNum = TEST_SEQ_NUM;
					frameBuf.ack = (_cIndexL + i) % (MAX_SEQ + 1);
					//  async
					//	std::thread(&ProtocolApp::sendDataFrame, this, index).detach();
					//
					sendDataFrame(index);
				}
			}

			// check and delay
			{
				auto startTime = std::chrono::high_resolution_clock::now();
				while (
					!isPerfectTure(_cChecked) &&
					std::chrono::high_resolution_clock::now() - startTime < TIMEOUT_LIMIT
				) {
					std::this_thread::sleep_for(CHECK_INTERVAL);
				}

				std::lock_guard<std::mutex> lock(_clntNetMutex);
				uint32 count = CACHE_SIZE;
				for (byte i = 0; i < CACHE_SIZE; i++) {
					if (!_cChecked[
						(i + _cBufIndex) % CACHE_SIZE]
					) {
						count = i;
						break;
					}
					else {
						_cChecked[
							(i + _cBufIndex) % CACHE_SIZE
						] = false;
					}
				}
				_cIndexL = (_cIndexL + count) % (MAX_SEQ + 1);
				_cIndexR = (_cIndexR + count) % (MAX_SEQ + 1);
				_cBufIndex = (_cBufIndex + count) % CACHE_SIZE;

				printf("---------------CLIENT FORWARD STEP COUNT: %d---------------\n", count);
			}
		}
		
	});
}

// TODO: 增加depth防止永久丟包導致的棧溢出
void ProtocolApp::sendDataFrame(const size_t index) {
	// bind返回的玩意怎麼變這樣了
	static std::chrono::milliseconds SENDING_DELAY(PACKAGE_SENDING_DELAY);
	std::this_thread::sleep_for(SENDING_DELAY);

	_client->send(
		asio::buffer(&(_clntCache[index]), sizeof(Frame)),
		[this, index] {
			printf(
				"Send frame %d finished\n",
				_clntCache[index].ack
			);
			/*_cFrameTimer[index].AsyncWait(
				DATA_TIMEOUT_LMT,
				[this](const int index) {
					sendDataFrame(index);
				},
				index
			);*/
		}
	);
}

std::thread ProtocolApp::startServer() {
	return std::thread([this] {
		_server->start("127.0.0.1", _port);
		while (_running)
			std::this_thread::sleep_for(std::chrono::seconds(1));
	});
}

void ProtocolApp::buildFrame(
	std::array<byte, sizeof(Frame)>& data,
	Frame& srcFrame
) {
	data[0] = srcFrame.type;
	data[1] = srcFrame.ack;
	data[2] = srcFrame.seqNum;
	data[3] = 0;
	memcpy(
	(void*)((uintptr_t)data.data() + 4),
		srcFrame.data.data(), static_cast<size_t>(PKT_LEN)
	);
	data[sizeof(Frame) - 1] = srcFrame.crc32;
}


void ProtocolApp::UpperLayerCall(const size_t index, const size_t frameCnt) {
	// (異步copy,同步直接取出)緩衝區中的(Cnt個)Frame並進行處理
	/*printf(
		"GET DATA FRAME! [type]: %d [ack]: %d "
		"[seqNum]: %d [crc32]: 0x%08x\n\n",
		temp->type, temp->ack, temp->seqNum, temp->crc32);*/
}


uint32 ProtocolApp::GetCRC32Tail(void* data, size_t length) {

	uint32 crc = ~0;
	size_t dataSize = PKT_LEN;

	const byte* current = (byte*)data;

	while (dataSize-- != 0) {
		byte s = byte(crc) ^ *current++;

		uint32 low = (s ^ (s << 6)) & 0xFF;
		uint32 tmp = (low * ((1 << 23) + (1 << 14) + (1 << 2)));
		crc = (crc >> 8) ^
			(low * ((1 << 24) + (1 << 16) + (1 << 8))) ^
			(tmp) ^
			(tmp >> 1) ^
			(low * ((1 << 20) + (1 << 12))) ^
			(low << 19) ^
			(low << 17) ^
			(low >> 2);
	}
	return ~crc;
}

uint32 ProtocolApp::GenerateRandomFrameData(
	void*					data,
	size_t					length
) {
	uint32 crc = ~0;
	size_t dataSize = PKT_LEN;
	byte* current = (byte*)data;

	std::random_device r;
	std::default_random_engine gen(r());
	std::uniform_int_distribution<int> distribution(0, 255);

	while (dataSize-- != 0) {

		*current = distribution(gen);
		byte s = byte(crc) ^ *current++;

		uint32 low = (s ^ (s << 6)) & 0xFF;
		uint32 tmp = (low * ((1 << 23) + (1 << 14) + (1 << 2)));
		crc = (crc >> 8) ^
			(low * ((1 << 24) + (1 << 16) + (1 << 8))) ^
			(tmp) ^
			(tmp >> 1) ^
			(low * ((1 << 20) + (1 << 12))) ^
			(low << 19) ^
			(low << 17) ^
			(low >> 2);
	}

	return ~crc;
}

inline bool ProtocolApp::CheckCRC32Tail(
	void*						data,
	size_t						length,
	uint32						crcTail
) {
	return GetCRC32Tail(data, length) == crcTail;
}

#undef _servCache
#undef _sChecked
#undef _sFrameTimer

#undef _clntCache
#undef _cChecked
#undef _cFrameTimer
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章