hpsocket

HPSocket服务Demo学习

最近偶尔接触到HPSocket框架,并找到官方的demo:

Demo官方社区版本

想要快速的上手使用,方法很简单,分几步就完成。

  1. 找到HPSocket的共享文件库,这个库里面包含了Client和Server端需要用的API和公共文件,将这个库移植到需要实现的工程内部。
  2. 根据工程目录编写makefile,生成对象文件。

附简单Demo代码(转载自git,测试可用):

Demo链接

说明

修改IP和port在helper.h内宏定义

调库使用说明在SocketHelper.h,一些常用套接字也有帮助方法

SocketHelper.h

/*
* Copyright: JessMA Open Source (ldcsaa@gmail.com)
*
* Author	: Bruce Liang
* Website	: http://www.jessma.org
* Project	: https://github.com/ldcsaa
* Blog		: http://www.cnblogs.com/ldcsaa
* Wiki		: http://www.oschina.net/p/hp-socket
* QQ Group	: 75375912, 44636872
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include "HPTypeDef.h"
#include "SocketInterface.h"
#include "common/StringT.h"
#include "common/SysHelper.h"
#include "common/BufferPtr.h"
#include "common/BufferPool.h"
#include "common/RingBuffer.h"
#include "common/FileHelper.h"

#include <netdb.h>
#include <sys/un.h>
#include <sys/socket.h>

using ADDRESS_FAMILY	= sa_family_t;
using IN_ADDR			= in_addr;
using IN6_ADDR			= in6_addr;
using SOCKADDR			= sockaddr;
using SOCKADDR_IN		= sockaddr_in;
using SOCKADDR_IN6		= sockaddr_in6;

/************************************************************************
名称:全局常量
描述:声明组件的公共全局常量
************************************************************************/

/* Socket 缓冲区最小值 */
#define MIN_SOCKET_BUFFER_SIZE					8
/* 小文件最大字节数 */
#define MAX_SMALL_FILE_SIZE						0x3FFFFF
/* 最大连接时长 */
#define MAX_CONNECTION_PERIOD					(MAXINT / 2)
/* 处理接收事件时最大读取次数 */
#define MAX_CONTINUE_READS						30
/* 处理发送事件时最大写入次数 */
#define MAX_CONTINUE_WRITES						50

/* 默认工作队列等待的最大描述符事件数量 */
#define DEFAULT_WORKER_MAX_EVENT_COUNT			CIODispatcher::DEF_WORKER_MAX_EVENTS

/* Server/Agent 默认最大连接数 */
#define DEFAULT_MAX_CONNECTION_COUNT			10000
/* Server/Agent 默认 Socket 缓存对象锁定时间 */
#define DEFAULT_FREE_SOCKETOBJ_LOCK_TIME		(10 * 1000)
/* Server/Agent 默认 Socket 缓存池大小 */
#define DEFAULT_FREE_SOCKETOBJ_POOL				150
/* Server/Agent 默认 Socket 缓存池回收阀值 */
#define DEFAULT_FREE_SOCKETOBJ_HOLD				600
/* Server/Agent 默认内存块缓存池大小 */
#define DEFAULT_FREE_BUFFEROBJ_POOL				300
/* Server/Agent 默认内存块缓存池回收阀值 */
#define DEFAULT_FREE_BUFFEROBJ_HOLD				1200
/* Client 默认内存块缓存池大小 */
#define DEFAULT_CLIENT_FREE_BUFFER_POOL_SIZE	10
/* Client 默认内存块缓存池回收阀值 */
#define DEFAULT_CLIENT_FREE_BUFFER_POOL_HOLD	40
/* IPv4 默认绑定地址 */
#define  DEFAULT_IPV4_BIND_ADDRESS				_T("0.0.0.0")
/* IPv6 默认绑定地址 */
#define  DEFAULT_IPV6_BIND_ADDRESS				_T("::")

/* TCP 默认通信数据缓冲区大小 */
#define DEFAULT_TCP_SOCKET_BUFFER_SIZE			DEFAULT_BUFFER_SIZE
/* TCP 默认心跳包间隔 */
#define DEFALUT_TCP_KEEPALIVE_TIME				(30 * 1000)
/* TCP 默认心跳确认包检测间隔 */
#define DEFALUT_TCP_KEEPALIVE_INTERVAL			(10 * 1000)
/* TCP Server 默认 Listen 队列大小 */
#define DEFAULT_TCP_SERVER_SOCKET_LISTEN_QUEUE	SOMAXCONN

/* UDP 默认数据报文最大长度 */
#define DEFAULT_UDP_MAX_DATAGRAM_SIZE			1472
/* UDP 默认 Receive 预投递数量 */
#define DEFAULT_UDP_POST_RECEIVE_COUNT			DEFAULT_WORKER_MAX_EVENT_COUNT
/* UDP 默认监测包尝试次数 */
#define DEFAULT_UDP_DETECT_ATTEMPTS				3
/* UDP 默认监测包发送间隔 */
#define DEFAULT_UDP_DETECT_INTERVAL				30

/* TCP Pack 包长度位数 */
#define TCP_PACK_LENGTH_BITS					22
/* TCP Pack 包长度掩码 */
#define TCP_PACK_LENGTH_MASK					0x3FFFFF
/* TCP Pack 包最大长度硬限制 */
#define TCP_PACK_MAX_SIZE_LIMIT					0x3FFFFF
/* TCP Pack 包默认最大长度 */
#define TCP_PACK_DEFAULT_MAX_SIZE				0x040000
/* TCP Pack 包头标识值硬限制 */
#define TCP_PACK_HEADER_FLAG_LIMIT				0x0003FF
/* TCP Pack 包头默认标识值 */
#define TCP_PACK_DEFAULT_HEADER_FLAG			0x000000

#define PORT_SEPARATOR_CHAR						':'
#define IPV6_ADDR_BEGIN_CHAR					'['
#define IPV6_ADDR_END_CHAR						']'
#define IPV4_ADDR_SEPARATOR_CHAR				'.'
#define IPV6_ADDR_SEPARATOR_CHAR				':'
#define IPV6_ZONE_INDEX_CHAR					'%'

#define INVALID_SOCKET							INVALID_FD
#define SOCKET_ERROR							HAS_ERROR
#define WSASetLastError							SetLastError
#define WSAGetLastError							GetLastError
#define InetPton								inet_pton
#define InetNtop								inet_ntop
#define closesocket								close

typedef struct hp_addr
{
	ADDRESS_FAMILY family;

	union
	{
		ULONG_PTR	addr;
		IN_ADDR		addr4;
		IN6_ADDR	addr6;
	};

	static const hp_addr ANY_ADDR4;
	static const hp_addr ANY_ADDR6;

	inline int AddrSize() const
	{
		return AddrSize(family);
	}

	inline static int AddrSize(ADDRESS_FAMILY f)
	{
		if(f == AF_INET)
			return sizeof(IN_ADDR);

		return sizeof(IN6_ADDR);
	}

	inline static const hp_addr& AnyAddr(ADDRESS_FAMILY f)
	{
		if(f == AF_INET)
			return ANY_ADDR4;

		return ANY_ADDR6;
	}

	inline const ULONG_PTR* Addr()	const	{return &addr;}
	inline ULONG_PTR* Addr()				{return &addr;}

	inline BOOL IsIPv4()			const	{return family == AF_INET;}
	inline BOOL IsIPv6()			const	{return family == AF_INET6;}
	inline BOOL IsSpecified()		const	{return IsIPv4() || IsIPv6();}
	inline void ZeroAddr()					{::ZeroMemory(&addr6, sizeof(addr6));}
	inline void Reset()						{::ZeroMemory(this, sizeof(*this));}

	inline hp_addr& Copy(hp_addr& other) const
	{
		if(this != &other)
			memcpy(&other, this, offsetof(hp_addr, addr) + AddrSize());

		return other;
	}

	hp_addr(ADDRESS_FAMILY f = AF_UNSPEC, BOOL bZeroAddr = FALSE)
	{
		family = f;

		if(bZeroAddr) ZeroAddr();
	}

} HP_ADDR, *HP_PADDR;

typedef struct hp_sockaddr
{
	union
	{
		ADDRESS_FAMILY	family;
		SOCKADDR		addr;
		SOCKADDR_IN		addr4;
		SOCKADDR_IN6	addr6;
	};

	inline int AddrSize() const
	{
		return AddrSize(family);
	}

	inline static int AddrSize(ADDRESS_FAMILY f)
	{
		if(f == AF_INET)
			return sizeof(SOCKADDR_IN);

		return sizeof(SOCKADDR_IN6);
	}

	inline static const hp_sockaddr& AnyAddr(ADDRESS_FAMILY f)
	{
		static const hp_sockaddr s_any_addr4(AF_INET, TRUE);
		static const hp_sockaddr s_any_addr6(AF_INET6, TRUE);

		if(f == AF_INET)
			return s_any_addr4;

		return s_any_addr6;
	}

	inline static int AddrMinStrLength(ADDRESS_FAMILY f)
	{
		if(f == AF_INET)
			return 16;

		return 46;
	}

	inline BOOL IsIPv4()			const	{return family == AF_INET;}
	inline BOOL IsIPv6()			const	{return family == AF_INET6;}
	inline BOOL IsSpecified()		const	{return IsIPv4() || IsIPv6();}
	inline USHORT Port()			const	{return ntohs(addr4.sin_port);}
	inline void SetPort(USHORT usPort)		{addr4.sin_port = htons(usPort);}
	inline void* SinAddr()			const	{return IsIPv4() ? (void*)&addr4.sin_addr : (void*)&addr6.sin6_addr;}
	inline void* SinAddr()					{return IsIPv4() ? (void*)&addr4.sin_addr : (void*)&addr6.sin6_addr;}

	inline const SOCKADDR* Addr()	const	{return &addr;}
	inline SOCKADDR* Addr()					{return &addr;}
	inline void ZeroAddr()					{::ZeroMemory(((char*)this) + sizeof(family), sizeof(*this) - sizeof(family));}
	inline void Reset()						{::ZeroMemory(this, sizeof(*this));}

	inline hp_sockaddr& Copy(hp_sockaddr& other) const
	{
		if(this != &other)
			memcpy(&other, this, AddrSize());

		return other;
	}

	size_t Hash() const
	{
		ASSERT(IsSpecified());

		if(IsIPv4())
			return ((addr4.sin_family << 16) | addr4.sin_port) ^ addr4.sin_addr.s_addr;
		else
		{
			ULONG* p = (ULONG*)(((char*)this) + offsetof(SOCKADDR_IN6, sin6_addr));
			return ((addr6.sin6_family << 16) | addr6.sin6_port) ^ addr6.sin6_flowinfo ^ p[0] ^ p[1] ^ p[2] ^ p[3] ^ p[4];
		}
	}

	bool EqualTo(const hp_sockaddr& other) const
	{
		ASSERT(IsSpecified() && other.IsSpecified());

		if(IsIPv4())
			return EqualMemory(this, &other, offsetof(SOCKADDR_IN, sin_zero));
		else
			return EqualMemory(this, &other, sizeof(addr6));
	}

	hp_sockaddr(ADDRESS_FAMILY f = AF_UNSPEC, BOOL bZeroAddr = FALSE)
	{
		family = f;

		if(bZeroAddr) ZeroAddr();
	}

} HP_SOCKADDR, *HP_PSOCKADDR;

/* Server 组件和 Agent 组件内部使用的事件处理结果常量 */

// 连接已关闭
#define HR_CLOSED	0xFF

/* 命令类型 */
enum EnDispCmdType
{
	DISP_CMD_SEND		= 0x01,	// 发送数据
	DISP_CMD_RECEIVE	= 0x02,	// 接收数据
	DISP_CMD_UNPAUSE	= 0x03,	// 恢复接收数据
	DISP_CMD_DISCONNECT	= 0x04	// 断开连接
};

/* 关闭连接标识 */
enum EnSocketCloseFlag
{
	SCF_NONE	= 0,	// 不触发事件
	SCF_CLOSE	= 1,	// 触发 正常关闭 OnClose 事件
	SCF_ERROR	= 2		// 触发 异常关闭 OnClose 事件
};

/* 数据缓冲节点 */
typedef TItem			TBufferObj;
/* 数据缓冲区对象池 */
typedef CItemPool		CBufferObjPool;
/* 数据缓冲区链表模板 */
typedef TItemListExV	TBufferObjList;

/* 线程 ID - 接收缓冲区哈希表 */
typedef unordered_map<THR_ID, CBufferPtr*>	TReceiveBufferMap;
/* 线程 ID - 接收缓冲区哈希表迭代器 */
typedef TReceiveBufferMap::iterator			TReceiveBufferMapI;
/* 线程 ID - 接收缓冲区哈希表 const 迭代器 */
typedef TReceiveBufferMap::const_iterator	TReceiveBufferMapCI;

/* Socket 缓冲区基础结构 */
struct TSocketObjBase
{
	CONNID		connID;
	HP_SOCKADDR	remoteAddr;
	PVOID		extra;
	PVOID		reserved;
	PVOID		reserved2;
	BOOL		valid;
	DWORD		activeTime;

	union
	{
		DWORD	freeTime;
		DWORD	connTime;
	};

	volatile BOOL		paused;

	TBufferObjList		sndBuff;

	CReentrantCriSec	csSend;

	TSocketObjBase(CBufferObjPool& bfPool)
	: sndBuff(bfPool)
	{

	}

	static BOOL IsExist(TSocketObjBase* pSocketObj)
		{return pSocketObj != nullptr;}

	static BOOL IsValid(TSocketObjBase* pSocketObj)
		{return pSocketObj != nullptr && pSocketObj->valid;}

	static void Invalid(TSocketObjBase* pSocketObj)
		{ASSERT(IsExist(pSocketObj)); pSocketObj->valid = FALSE;}

	static void Release(TSocketObjBase* pSocketObj)
	{
		ASSERT(IsExist(pSocketObj));

		pSocketObj->freeTime = ::TimeGetTime();
		pSocketObj->sndBuff.Release();
	}

	int Pending()		{return sndBuff.Length();}
	BOOL IsPending()	{return Pending() > 0;}
	BOOL IsPaused()		{return paused;}

	void Reset(CONNID dwConnID)
	{
		connID		= dwConnID;
		valid		= TRUE;
		paused		= FALSE;
		extra		= nullptr;
		reserved	= nullptr;
		reserved2	= nullptr;
	}
};

/* 数据缓冲区结构 */
struct TSocketObj : public TSocketObjBase
{
	using __super = TSocketObjBase;

	SOCKET				socket;
	CReentrantSpinGuard	csIo;

	TSocketObj(CBufferObjPool& bfPool)
	: __super(bfPool)
	{

	}

	static BOOL InvalidSocketObj(TSocketObj* pSocketObj)
	{
		BOOL bDone = FALSE;

		if(TSocketObjBase::IsValid(pSocketObj))
		{
			CReentrantSpinLock	 locallock(pSocketObj->csIo);
			CReentrantCriSecLock locallock2(pSocketObj->csSend);

			if(TSocketObjBase::IsValid(pSocketObj))
			{
				TSocketObjBase::Invalid(pSocketObj);
				bDone = TRUE;
			}
		}

		return bDone;
	}

	void Reset(CONNID dwConnID, SOCKET soClient)
	{
		__super::Reset(dwConnID);
		
		socket = soClient;
	}
};

/* Agent 数据缓冲区结构 */
struct TAgentSocketObj : public TSocketObj
{
	using __super = TSocketObj;

	CStringA host;
	BOOL connected;
	
	TAgentSocketObj(CBufferObjPool& bfPool)
	: __super(bfPool)
	{

	}

	void Reset(CONNID dwConnID, SOCKET soClient)
	{
		__super::Reset(dwConnID, soClient);

		host.Empty();

		connected = FALSE;
	}

	BOOL GetRemoteHost(LPCSTR* lpszHost, USHORT* pusPort = nullptr)
	{
		*lpszHost = host;

		if(pusPort)
			*pusPort = remoteAddr.Port();

		return (!host.IsEmpty());
	}

	BOOL HasConnected()
	{
		return connected;
	}

	VOID SetConnected(BOOL bConnected = TRUE)
	{
		connected = bConnected;
	}
};

/* UDP 数据缓冲区结构 */
struct TUdpSocketObj : public TSocketObjBase
{
	using __super		= TSocketObjBase;
	using CRecvQueue	= CCASQueue<TItem>;

	CRecvQueue		recvQueue;
	volatile DWORD	detectFails;
	CSimpleRWLock	lcIo;

	CBufferObjPool&	itPool;

	TUdpSocketObj(CBufferObjPool& bfPool)
	: __super(bfPool), itPool(bfPool)
	{

	}

	~TUdpSocketObj()
	{
		ClearRecvQueue();
	}

	BOOL HasRecvData() {return !recvQueue.IsEmpty();}

	static BOOL InvalidSocketObj(TUdpSocketObj* pSocketObj)
	{
		BOOL bDone = FALSE;

		if(TSocketObjBase::IsValid(pSocketObj))
		{
			CWriteLock			 locallock(pSocketObj->lcIo);
			CReentrantCriSecLock locallock2(pSocketObj->csSend);

			if(TSocketObjBase::IsValid(pSocketObj))
			{
				TSocketObjBase::Invalid(pSocketObj);
				bDone = TRUE;
			}
		}

		return bDone;
	}

	void Reset(CONNID dwConnID)
	{
		__super::Reset(dwConnID);

		detectFails = 0;
	}

	static void Release(TUdpSocketObj* pSocketObj)
	{
		__super::Release(pSocketObj);
		pSocketObj->ClearRecvQueue();
	}

	void ClearRecvQueue()
	{
		TItem* pItem = nullptr;

		while(recvQueue.PopFront(&pItem))
			itPool.PutFreeItem(pItem);

		VERIFY(recvQueue.IsEmpty());
	}
};

/* 有效 TSocketObj 缓存 */
typedef CRingCache2<TSocketObj, CONNID, true>		TSocketObjPtrPool;
/* 失效 TSocketObj 缓存 */
typedef CRingPool<TSocketObj>						TSocketObjPtrList;
/* 失效 TSocketObj 垃圾回收结构链表 */
typedef CCASQueue<TSocketObj>						TSocketObjPtrQueue;

/* 有效 TSocketObj 缓存 */
typedef CRingCache2<TAgentSocketObj, CONNID, true>	TAgentSocketObjPtrPool;
/* 失效 TSocketObj 缓存 */
typedef CRingPool<TAgentSocketObj>					TAgentSocketObjPtrList;
/* 失效 TSocketObj 垃圾回收结构链表 */
typedef CCASQueue<TAgentSocketObj>					TAgentSocketObjPtrQueue;

/* 有效 TUdpSocketObj 缓存 */
typedef CRingCache2<TUdpSocketObj, CONNID, true>	TUdpSocketObjPtrPool;
/* 失效 TUdpSocketObj 缓存 */
typedef CRingPool<TUdpSocketObj>					TUdpSocketObjPtrList;
/* 失效 TUdpSocketObj 垃圾回收结构链表 */
typedef CCASQueue<TUdpSocketObj>					TUdpSocketObjPtrQueue;

/* HP_SOCKADDR 比较器 */
struct hp_sockaddr_func
{
	struct hash
	{
		size_t operator() (const HP_SOCKADDR* pA) const
		{
			return pA->Hash();
		}
	};

	struct equal_to
	{
		bool operator () (const HP_SOCKADDR* pA, const HP_SOCKADDR* pB) const
		{
			return pA->EqualTo(*pB);
		}
	};

};

/* 地址-连接 ID 哈希表 */
typedef unordered_map<const HP_SOCKADDR*, CONNID, hp_sockaddr_func::hash, hp_sockaddr_func::equal_to>
										TSockAddrMap;
/* 地址-连接 ID 哈希表迭代器 */
typedef TSockAddrMap::iterator			TSockAddrMapI;
/* 地址-连接 ID 哈希表 const 迭代器 */
typedef TSockAddrMap::const_iterator	TSockAddrMapCI;

/* IClient 组件关闭上下文 */
struct TClientCloseContext
{
	BOOL bFireOnClose;
	EnSocketOperation enOperation;
	int iErrorCode;

	TClientCloseContext(BOOL bFire = TRUE, EnSocketOperation enOp = SO_CLOSE, int iCode = SE_OK)
	{
		Reset(bFire, enOp, iCode);
	}

	void Reset(BOOL bFire = TRUE, EnSocketOperation enOp = SO_CLOSE, int iCode = SE_OK)
	{
		bFireOnClose = bFire;
		enOperation	 = enOp;
		iErrorCode	 = iCode;
	}

};

/*****************************************************************************************************/
/******************************************** 公共帮助方法 ********************************************/
/*****************************************************************************************************/

/* 获取错误描述文本 */
LPCTSTR GetSocketErrorDesc(EnSocketError enCode);
/* 确定地址簇 */
ADDRESS_FAMILY DetermineAddrFamily(LPCTSTR lpszAddress);
/* 地址字符串地址转换为 HP_ADDR */
BOOL GetInAddr(LPCTSTR lpszAddress, HP_ADDR& addr);
/* 地址字符串地址转换为 HP_SOCKADDR */
BOOL GetSockAddr(LPCTSTR lpszAddress, USHORT usPort, HP_SOCKADDR& addr);
/* 检查字符串是否符合 IP 地址格式 */
BOOL IsIPAddress(LPCTSTR lpszAddress, EnIPAddrType* penType = nullptr);
/* 通过主机名获取 IP 地址 */
BOOL GetIPAddress(LPCTSTR lpszHost, LPTSTR lpszIP, int& iIPLenth, EnIPAddrType& enType);
/* 通过主机名获取 HP_SOCKADDR */
BOOL GetSockAddrByHostName(LPCTSTR lpszHost, USHORT usPort, HP_SOCKADDR& addr);
/* 通过主机名获取 HP_SOCKADDR */
BOOL GetSockAddrByHostNameDirectly(LPCTSTR lpszHost, USHORT usPort, HP_SOCKADDR &addr);
/* 枚举主机 IP 地址 */
BOOL EnumHostIPAddresses(LPCTSTR lpszHost, EnIPAddrType enType, LPTIPAddr** lpppIPAddr, int& iIPAddrCount);
/* 填充 LPTIPAddr* */
BOOL RetrieveSockAddrIPAddresses(const vector<HP_PSOCKADDR>& vt, LPTIPAddr** lpppIPAddr, int& iIPAddrCount);
/* 释放 LPTIPAddr* */
BOOL FreeHostIPAddresses(LPTIPAddr* lppIPAddr);
/* 把 HP_SOCKADDR 结构转换为地址字符串 */
BOOL sockaddr_IN_2_A(const HP_SOCKADDR& addr, ADDRESS_FAMILY& usFamily, LPTSTR lpszAddress, int& iAddressLen, USHORT& usPort);
/* 把地址字符串转换为 HP_SOCKADDR 结构 */
BOOL sockaddr_A_2_IN(LPCTSTR lpszAddress, USHORT usPort, HP_SOCKADDR& addr);
/* 获取 Socket 的本地或远程地址信息 */
BOOL GetSocketAddress(SOCKET socket, LPTSTR lpszAddress, int& iAddressLen, USHORT& usPort, BOOL bLocal = TRUE);
/* 获取 Socket 的本地地址信息 */
BOOL GetSocketLocalAddress(SOCKET socket, LPTSTR lpszAddress, int& iAddressLen, USHORT& usPort);
/* 获取 Socket 的远程地址信息 */
BOOL GetSocketRemoteAddress(SOCKET socket, LPTSTR lpszAddress, int& iAddressLen, USHORT& usPort);

/* 64 位网络字节序转主机字节序 */
ULONGLONG NToH64(ULONGLONG value);
/* 64 位主机字节序转网络字节序 */
ULONGLONG HToN64(ULONGLONG value);

HRESULT ReadSmallFile(LPCTSTR lpszFileName, CFile& file, CFileMapping& fmap, DWORD dwMaxFileSize = MAX_SMALL_FILE_SIZE);
HRESULT MakeSmallFilePackage(LPCTSTR lpszFileName, CFile& file, CFileMapping& fmap, WSABUF szBuf[3], const LPWSABUF pHead = nullptr, const LPWSABUF pTail = nullptr);

/************************************************************************
名称:setsockopt() 帮助方法
描述:简化常用的 setsockopt() 调用
************************************************************************/

int SSO_SetSocketOption		(SOCKET sock, int level, int name, LPVOID val, int len);
int SSO_GetSocketOption		(SOCKET sock, int level, int name, LPVOID val, int* len);
int SSO_IoctlSocket			(SOCKET sock, long cmd, PVOID arg);

int SSO_NoBlock				(SOCKET sock, BOOL bNoBlock = TRUE);
int SSO_NoDelay				(SOCKET sock, BOOL bNoDelay = TRUE);
int SSO_DontLinger			(SOCKET sock, BOOL bDont = TRUE);
int SSO_Linger				(SOCKET sock, int l_onoff, int l_linger);
int SSO_KeepAlive			(SOCKET sock, BOOL bKeepAlive = TRUE);
int SSO_KeepAliveVals		(SOCKET sock, BOOL bOnOff, DWORD dwIdle, DWORD dwInterval, DWORD dwCount = 5);
int SSO_ReuseAddress		(SOCKET sock, BOOL bReuse = TRUE);
int SSO_RecvBuffSize		(SOCKET sock, int size);
int SSO_SendBuffSize		(SOCKET sock, int size);
int SSO_RecvTimeout			(SOCKET sock, int sec, int microsec = 0);
int SSO_SendTimeout			(SOCKET sock, int sec, int microsec = 0);
int SSO_GetError			(SOCKET sock);

/* 生成 Connection ID */
CONNID GenerateConnectionID();
/* 关闭 Socket */
int ManualCloseSocket(SOCKET sock, int iShutdownFlag = 0xFF, BOOL bGraceful = TRUE, BOOL bReuseAddress = FALSE);

helper.h

#pragma once

#include "../hpsocket/HPTypeDef.h"
#include "../hpsocket/common/GeneralHelper.h"

#include <cstdio>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <sys/socket.h>

#define EVT_ON_SEND				_T("OnSend")
#define EVT_ON_RECEIVE			_T("OnReceive")
#define EVT_ON_CLOSE			_T("OnClose")
#define EVT_ON_ERROR			_T("OnError")
#define EVT_ON_PREPARE_CONNECT	_T("OnPrepareConnect")
#define EVT_ON_PREPARE_LISTEN	_T("OnPrepareListen")
#define EVT_ON_ACCEPT			_T("OnAccept")
#define EVT_ON_CONNECT			_T("OnConnect")
#define EVT_ON_HAND_SHAKE		_T("OnHandShake")
#define EVT_ON_SHUTDOWN			_T("OnShutdown")
#define EVT_ON_END_TEST			_T("END TEST")
#define EVT_ON_STAT_TEST		_T("STAT TEST")

#define EVT_ON_MESSAGE_BEGIN	_T("OnMessageBegin")
#define EVT_ON_REQUEST_LINE		_T("OnRequestLine")
#define EVT_ON_STATUS_LINE		_T("OnStatusLine")
#define EVT_ON_HEADER			_T("OnHeader")
#define EVT_ON_HEADERS_COMPLETE	_T("OnHeadersComplete")
#define EVT_ON_BODY				_T("OnBody")
#define EVT_ON_CHUNK_HEADER		_T("OnChunkHeader")
#define EVT_ON_CHUNK_COMPLETE	_T("OnChunkComplete")
#define EVT_ON_MESSAGE_COMPLETE	_T("OnMessageComplete")
#define EVT_ON_UPGRADE			_T("OnUpgrade")
#define EVT_ON_PARSE_ERROR		_T("OnParseError")

#define EVT_ON_WS_MSG_HEADER	_T("OnWSMessageHeader")
#define EVT_ON_WS_MSG_BODY		_T("OnWSMessageBody")
#define EVT_ON_WS_MSG_COMPLETE	_T("OnWSMessageComplete")

#define EVT_ON_UNCOMPRESS_BODY		_T("Uncompress Body")
#define EVT_ON_UNCOMPRESS_BODY_FAIL	_T("Uncompress Body Fail")

#define IPV4_LOOPBACK_ADDRESS	_T("127.0.0.1")
#define IPV6_LOOPBACK_ADDRESS	_T("::1")
#define IPV4_ANY_ADDRESS		_T("0.0.0.0")
#define IPV6_ANY_ADDRESS		_T("::")
#define DEF_BROAD_CAST_ADDRESS	_T("233.0.0.1")
#define DEF_TCP_UDP_PORT		5555
#define DEF_HTTP_PORT			8080
#define DEF_HTTPS_PORT			8443

#define TCP_KEEPALIVE_TIME		(30 * 1000)
#define UDP_DETECT_ATTEMPTS		3

enum EnAppState
{
	ST_STARTING, ST_STARTED, ST_CONNECTING, ST_CONNECTED, ST_STOPPING, ST_STOPPED
};

struct app_arg
{
	static char OPTIONS[];

	// -a
	CString remote_addr;
	// -b
	CString bind_addr;
	// -p
	USHORT port;
	// -j
	CString reject_addr;
	// -n
	bool async;
	// -t
	DWORD thread_count;
	// -e
	DWORD test_times;
	// -i
	DWORD test_interval;
	// -c
	DWORD conn_count;
	// -l
	DWORD data_length;
	// -s
	EnSendPolicy send_policy;
	// -m
	DWORD max_conn;
	// -q
	bool keep_alive;

	// -o
	EnCastMode cast_mode;
	// -r
	bool reuse_addr;
	// -u
	bool ip_loop;
	// -z
	int ttl;

	// -x port1[,port2]
	USHORT http_port1;
	// -x port1[,port2]
	USHORT http_port2;
	// -y
	bool http_secure;
	// -d
	CString http_method;
	// -f
	CString http_req_path;
	// -k
	bool http_use_cookie;
	// -w
	bool http_with_listener;

public:
	void ParseArgs(int argc, char* const argv[]);
	void ShowPFMTestArgs(BOOL bAgent);
	static void PrintUsage();
	static void PrintVersion();
public:
	app_arg();
	~app_arg();
};

extern app_arg g_app_arg;

class CAppSignalHandler
{
public:

	CAppSignalHandler(const vector<int>&& signals, BOOL bIgnoreSigInt = TRUE)
	: m_bIgnoreSigInt(bIgnoreSigInt)
	{
		sigset_t ss;
		sigemptyset(&ss);

		for(size_t i = 0; i < signals.size(); i++)
			sigaddset(&ss, signals[i]);

		sh.Setup(this, &CAppSignalHandler::handle_sig, &ss);
	}

	int WaitForExit()
	{
		int rs = evt.Wait();
		
		PRINTLN(" bye ~ bye ~");

		return rs;
	}

private:

	void handle_sig(const siginfo_t* pSigInfo)
	{
		if(pSigInfo->si_signo == SIGINT)
			m_bIgnoreSigInt || evt.Set();
	}

private:
	CSignalHandler<CAppSignalHandler> sh;
	CPipeEvent evt;

	BOOL m_bIgnoreSigInt;
};

class CTermAttrInitializer
{
public:
	CTermAttrInitializer(tcflag_t l_mask = 0)
	{
		tcgetattr(STDIN_FILENO, &old_attr);

		termios new_attr = old_attr;

		new_attr.c_lflag	&= ~l_mask;
		new_attr.c_cc[VERASE]= 0x08;
		new_attr.c_cc[VTIME] = 0;
		new_attr.c_cc[VMIN]  = 1;

		tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_attr);
	}

	~CTermAttrInitializer() {tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_attr);}

private:
	termios old_attr;
};

class CCommandParser
{
public:
	using CMD_FUNC = void (*)(CCommandParser*);

	enum EnAppType {AT_SERVER, AT_AGENT, AT_CLIENT};
	enum EnCmdType {CT_START = 0, CT_STOP, CT_STATUS, CT_CONNECT, CT_SEND, CT_PAUSE, CT_KICK, CT_KICK_L, CT_KICK_S, CT_STAT, CT_MAX};

private:

	struct TCmdNameFunc
	{
		LPCTSTR name;
		CMD_FUNC func;
	};

public:
	BOOL Run();
	void PrintStatus(EnServiceState enStatus);

public:
	CCommandParser(EnAppType enAppType, CMD_FUNC fnCmds[CT_MAX]);
	~CCommandParser() {}

private:
	BOOL WaitForExit();
	void WorkerProc(PVOID pv);
	void Parse(LPTSTR lpszLine, SSIZE_T nSize);
	void ParseCmdArgs(EnCmdType type, LPTSTR lpszArg);
	void Reset();

	void PrintUsage();
	CString GetCmdUsage(EnCmdType type);

public:
	BOOL	m_bFlag;
	CONNID	m_dwConnID;
	CString	m_strMessage;
	CString	m_strRemoteAddr;
	USHORT	m_usRemotePort;
	DWORD	m_dwSeconds;

private:
	EnAppType	 m_enAppType;
	TCmdNameFunc m_szCmdNameFuncs[CT_MAX];

	CThread<CCommandParser, VOID, VOID> m_thWorker;
};

struct server_statistics_info
{
	volatile LONG m_lClientCount;
	volatile LONGLONG m_llTotalReceived;
	volatile LONGLONG m_llTotalSent;

	CCriSec m_cs;

	void Reset(BOOL bResetClientCount = TRUE);
	void CheckClientCount();
	void CheckStatistics();
	void AddTotalRecv(int iLength);
	void AddTotalSend(int iLength);

};

struct client_statistics_info
{
	volatile LONGLONG m_llTotalReceived;
	volatile LONGLONG m_llTotalSent;
	LONGLONG m_llExpectReceived;
	DWORD m_dwBeginTickCount;
	DWORD m_dwTimeconsuming;

	void Reset();
	void StartTest();
	void CheckStatistics();
	void AddTotalRecv(int iLength);
	void AddTotalSend(int iLength);
};

struct info_msg
{
	LPCTSTR name;
	CONNID connID;
	LPCTSTR evt;
	int contentLength;
	LPCTSTR content;

	static info_msg* Construct(CONNID dwConnID, LPCTSTR lpszEvent, int iContentLength = 0, LPCTSTR lpszContent = nullptr, LPCTSTR lpszName = nullptr);
	static void Destruct(info_msg* pMsg);

private:
	info_msg(CONNID dwConnID, LPCTSTR lpszEvent, int iContentLength = 0, LPCTSTR lpszContent = nullptr, LPCTSTR lpszName = nullptr);
	~info_msg();
};

struct TPkgHeader 
{
	DWORD seq;
	int body_len;
};

struct TPkgBody 
{
	char name[30];
	short age;
	char desc[1];
};

struct TPkgInfo
{
	bool is_header;
	int length;

	TPkgInfo(bool header = true, int len = sizeof(TPkgHeader)) : is_header(header), length(len) {}
	void Reset() {is_header = true, length = sizeof(TPkgHeader);}
	~TPkgInfo() {}
};

inline TPkgInfo* ConstructPkgInfo()
{
	return new TPkgInfo(true, sizeof(TPkgHeader));
}

inline void DestructPkgInfo(TPkgInfo* pInfo)
{
	delete pInfo;
}

template<class T, typename = enable_if_t<!is_void<decay_t<T>>::value>>
inline TPkgInfo* CreatePkgInfo(T* pSender, CONNID dwConnID)
{
	TPkgInfo* pInfo = ConstructPkgInfo();
	pSender->SetConnectionExtra(dwConnID, pInfo);

	return pInfo;
}

template<class T, typename = enable_if_t<!is_void<decay_t<T>>::value>>
inline TPkgInfo* FindPkgInfo(T* pSender, CONNID dwConnID)
{
	PVOID pInfo = nullptr;
	pSender->GetConnectionExtra(dwConnID, &pInfo);

	return (TPkgInfo*)pInfo;
}

template<class T, typename = enable_if_t<!is_void<decay_t<T>>::value>>
inline void RemovePkgInfo(T* pSender, CONNID dwConnID)
{
	TPkgInfo* pInfo = FindPkgInfo(pSender, dwConnID);
	ASSERT(pInfo != nullptr);

	DestructPkgInfo(pInfo);
}

CBufferPtr* GeneratePkgBuffer(DWORD seq, LPCTSTR lpszName, short age, LPCTSTR lpszDesc);
CBufferPtr* GeneratePkgBuffer(const TPkgHeader& header, const TPkgBody& body);

sa_family_t GuessAddrFamily(LPCTSTR lpszAddress);
LPCTSTR GetLoopbackAddress(LPCTSTR lpszLikeAddress);
LPCTSTR GetAnyAddress(LPCTSTR lpszLikeAddress);

void LogServerStart(LPCTSTR lpszAddress, USHORT port, LPCTSTR lpszName = nullptr);
void LogServerStartFail(DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogServerStop(LPCTSTR lpszName = nullptr);
void LogServerStopFail(DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogAgentStart(LPCTSTR lpszAddress, BOOL bAsync, LPCTSTR lpszName = nullptr);
void LogAgentStarting(LPCTSTR lpszAddress, BOOL bAsync, LPCTSTR lpszName = nullptr);
void LogAgentStartFail(DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogAgentStopping(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogAgentStop(LPCTSTR lpszName = nullptr);
void LogAgentStopFail(DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogAgentSendFail(int iSequence, int iSocketIndex, DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogClientStart(LPCTSTR lpszAddress, USHORT port, LPCTSTR lpszName = nullptr);
void LogClientStarting(LPCTSTR lpszAddress, USHORT port, LPCTSTR lpszName = nullptr);
void LogClientStartFail(DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogClientStopping(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogClientStop(LPCTSTR lpszName = nullptr);
void LogClientStopFail(DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogClientSendFail(int iSequence, int iSocketIndex, DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogSend(CONNID dwConnID, LPCTSTR lpszContent, LPCTSTR lpszName = nullptr);
void LogSendFail(CONNID dwConnID, DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogDisconnect(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogDisconnectFail(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogDisconnect2(CONNID dwConnID, BOOL bForce, LPCTSTR lpszName = nullptr);
void LogDisconnectFail2(CONNID dwConnID, BOOL bForce, LPCTSTR lpszName = nullptr);
void LogPause(CONNID dwConnID, BOOL bPause, LPCTSTR lpszName = nullptr);
void LogPauseFail(CONNID dwConnID, BOOL bPause, LPCTSTR lpszName = nullptr);
void LogConnect(LPCTSTR lpszAddress, USHORT usPort, LPCTSTR lpszName = nullptr);
void LogConnectFail(DWORD code, LPCTSTR lpszDesc, LPCTSTR lpszName = nullptr);
void LogRelease(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogReleaseFail(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogDetect(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogDetectFail(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogOnConnect(CONNID dwConnID, const CString& strAddress, USHORT usPort, LPCTSTR lpszName = nullptr);
void LogOnConnect2(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogOnConnect3(CONNID dwConnID, const CString& strAddress, USHORT usPort, LPCTSTR lpszName = nullptr);
void LogOnHandShake2(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void LogOnClose(CONNID dwConnID, LPCTSTR lpszName = nullptr);

void PostOnSend(CONNID dwConnID, const BYTE* pData, int iLength, LPCTSTR lpszName = nullptr);
void PostOnReceive(CONNID dwConnID, const BYTE* pData, int iLength, LPCTSTR lpszName = nullptr);
void PostOnReceiveCast(CONNID dwConnID, LPCTSTR lpszAddress, USHORT usPort, const BYTE* pData, int iLength, LPCTSTR lpszName = nullptr);
void PostOnClose(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnError(CONNID dwConnID, int enOperation, int iErrorCode, LPCTSTR lpszName = nullptr);
void PostOnAccept(CONNID dwConnID, LPCTSTR lpszAddress, USHORT usPort, BOOL bPass, LPCTSTR lpszName = nullptr);
void PostOnAccept2(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnHandShake(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnPrepareListen(LPCTSTR lpszAddress, USHORT usPort, LPCTSTR lpszName = nullptr);
void PostOnPrepareConnect(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnConnect(CONNID dwConnID, LPCTSTR lpszAddress, USHORT usPort, LPCTSTR lpszName = nullptr);
void PostOnConnect2(CONNID dwConnID, LPCTSTR lpszAddress, USHORT usPort, LPCTSTR lpszName = nullptr);
void PostOnConnect3(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnShutdown(LPCTSTR lpszName = nullptr);
void PostServerStatics(const LONGLONG& llTotalSent, const LONGLONG& llTotalReceived, LPCTSTR lpszName = nullptr);
void PostServerTemporaryStatics(const LONGLONG& llTotalSent, const LONGLONG& llTotalReceived, LPCTSTR lpszName = nullptr);
void PostTimeConsuming(DWORD dwTickCount, LPCTSTR lpszName = nullptr);

void PostOnMessageBegin(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnRequestLine(CONNID dwConnID, LPCSTR lpszMethod, USHORT usUrlFieldSet, LPCSTR lpszUrl, LPCTSTR lpszName = nullptr);
void PostOnStatusLine(CONNID dwConnID, USHORT usStatusCode, LPCSTR lpszDesc, LPCTSTR lpszName = nullptr);
void PostOnHeader(CONNID dwConnID, LPCSTR lpszHeaderName, LPCSTR lpszHeaderValue, LPCTSTR lpszName = nullptr);
void PostOnHeadersComplete(CONNID dwConnID, LPCSTR lpszSummary, LPCTSTR lpszName = nullptr);
void PostOnBody(CONNID dwConnID, const BYTE* pData, int iLength, LPCTSTR lpszName = nullptr);
void PostOnChunkHeader(CONNID dwConnID, int iLength, LPCTSTR lpszName = nullptr);
void PostOnChunkComplete(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnMessageComplete(CONNID dwConnID, LPCTSTR lpszName = nullptr);
void PostOnUpgrade(CONNID dwConnID, EnHttpUpgradeType enUpgradeType, LPCTSTR lpszName = nullptr);
void PostOnParseError(CONNID dwConnID, int iErrorCode, LPCSTR lpszErrorDesc, LPCTSTR lpszName = nullptr);

void PostOnWSMessageHeader(CONNID dwConnID, BOOL bFinal, BYTE iReserved, BYTE iOperationCode, const BYTE lpszMask[4], ULONGLONG ullBodyLen, LPCTSTR lpszName = nullptr);
void PostOnWSMessageBody(CONNID dwConnID, const BYTE* pData, int iLength, LPCTSTR lpszName = nullptr);
void PostOnWSMessageComplete(CONNID dwConnID, LPCTSTR lpszName = nullptr);

void PostUncompressBody(CONNID dwConnID, int iLength, LPCTSTR lpszName = nullptr);
void PostUncompressBodyFail(CONNID dwConnID, int iResult, LPCTSTR lpszName = nullptr);

void PostInfoMsg(info_msg* msg);
void LogInfoMsg(info_msg* pInfoMsg);
void LogMsg(const CString& msg);

extern LPCTSTR g_lpszDefaultCookieFile;

LPCTSTR GetDefaultCookieFile();

#ifdef _SSL_SUPPORT

extern int g_c_iVerifyMode;
extern BOOL g_c_bNeedClientVerification;
extern LPCTSTR g_c_lpszCAPemCertFileOrPath;
extern LPCTSTR g_c_lpszPemCertFile;
extern LPCTSTR g_c_lpszPemKeyFile;
extern LPCTSTR g_c_lpszKeyPasswod;

extern int g_s_iVerifyMode;
extern BOOL g_s_bNeedClientVerification;
extern LPCTSTR g_s_lpszCAPemCertFileOrPath;
extern LPCTSTR g_s_lpszPemCertFile;
extern LPCTSTR g_s_lpszPemKeyFile;
extern LPCTSTR g_s_lpszKeyPasswod;

extern int g_c_iVerifyMode2;
extern BOOL g_c_bNeedClientVerification2;
extern LPCTSTR g_c_lpszCAPemCertFileOrPath2;
extern LPCTSTR g_c_lpszPemCertFile2;
extern LPCTSTR g_c_lpszPemKeyFile2;
extern LPCTSTR g_c_lpszKeyPasswod2;

extern int g_s_iVerifyMode2;
extern BOOL g_s_bNeedClientVerification2;
extern LPCTSTR g_s_lpszCAPemCertFileOrPath2;
extern LPCTSTR g_s_lpszPemCertFile2;
extern LPCTSTR g_s_lpszPemKeyFile2;
extern LPCTSTR g_s_lpszKeyPasswod2;

#endif

#ifdef _HTTP_SUPPORT

#include "../../src/common/crypto/crypto.h"

#define HTTP_NAME					_T("http")
#define HTTPS_NAME					_T("https")

#define CRLF						"\r\n"
#define NV_SEPARATOR_CHAR			'='
#define HEADER_SEPARATOR			": "
#define COOKIE_TOKENIZE				"; "
#define STR_HTTP_1_0				"HTTP/1.0"
#define STR_HTTP_1_1				"HTTP/1.1"
#define HOST_HEADER					"Host"
#define COOKIE_HEADER				"Cookie"
#define SET_COOKIE_HEADER			"Set-Cookie"
#define CONTENT_TYPE_HEADER			"Content-Type"
#define CONTENT_LENGTH_HEADER		"Content-Length"
#define TRANSFER_ENCODING_HEADER	"Transfer-Encoding"
#define UPGRADE_HEADER				"Upgrade"
#define WEB_SOCKET_HEADER_VALUE		"WebSocket"

#define HTTP_METHOD_POST			"POST"
#define HTTP_METHOD_PUT				"PUT"
#define HTTP_METHOD_PATCH			"PATCH"
#define HTTP_METHOD_GET				"GET"
#define HTTP_METHOD_DELETE			"DELETE"
#define HTTP_METHOD_HEAD			"HEAD"
#define HTTP_METHOD_TRACE			"TRACE"
#define HTTP_METHOD_OPTIONS			"OPTIONS"
#define HTTP_METHOD_CONNECT			"CONNECT"

#define HTTP_WEB_SOCKET_SEC_SALT	"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

struct THttpHeader
{
	CStringA strName;
	CStringA strValue;

	THttpHeader(LPCTSTR lpszName, LPCTSTR lpszValue) : strName(lpszName), strValue(lpszValue) {}

	struct hash
	{
		size_t operator() (const CStringA& str) const
		{
			return hash_value(str);
		}
	};

	struct equal_to
	{
		bool operator () (const CStringA& strA, const CStringA& strB) const
		{
			return strA == strB;
		}
	};

};

typedef unordered_multimap<CStringA, CStringA,
		THttpHeader::hash, THttpHeader::equal_to>	THttpHeaderMap;
typedef THttpHeaderMap::const_iterator				THttpHeaderMapCI;
typedef THttpHeaderMap::iterator					THttpHeaderMapI;

CStringA& HttpVersionToString(EnHttpVersion enVersion, CStringA& strResult);
CStringA& MakeSecWebSocketAccept(LPCSTR lpszKey, CStringA& strAccept);

#endif

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值