开发者社区> season雅宁> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

windows linux—unix 跨平台通信集成控制系统

简介: 首先,我们可以用到这个开源的开发包: mdk(Micro-Development-Kit)微量级软件开发包,提供几个常用类,主要实现了一个高性能的并发服务器引擎    使用c++开发,是一个跨平台的开发包,支持linux32/linux64/win32/win64的类库  。
+关注继续查看

首先,我们可以用到这个开源的开发包:

mdk(Micro-Development-Kit)微量级软件开发包,提供几个常用类,主要实现了一个高性能的并发服务器引擎 

  使用c++开发,是一个跨平台的开发包,支持linux32/linux64/win32/win64的类库  。

  mdk服务器引擎,提出面向业务的服务器开发模式,根据服务器业务层最关心的3件事,抽象出连接发生(OnConnect),消息到达(OnClose),连接关闭(OnClose)3个接口,让服务器端开发者可以全身心的投入业务逻辑的开发中。 

特点:        提供分布式支持,自身是一个server-client的结合体(即做服务器使用的同时,也可以像client一样去连接别的服务器,组成分布式系统),并且io接口统一到onconnect onmsg onclose中,不必区别对待        事件轮巡使用的是原生epoll iocp实现,确保了对io响应速度的完全掌控        几乎等同于lock free的并发算法,保证并发效率        欢迎大家共同学习使用。

 

http://download.csdn.net/detail/wangyaninglm/8260299

或者是这里:

https://github.com/huoyu820125/Micro-Development-Kit

 

 

 我们写客户端的时候可以用到里面 的socket类

socket.h如下:

// Socket.h: interface for the Socket class.
//
//////////////////////////////////////////////////////////////////////
/***********************************************************************

************************************************************************/

#ifndef MDK_SOCKET_H
#define MDK_SOCKET_H

#include <iostream>

using namespace std;

#ifdef WIN32
#include <windows.h>



#define socklen_t int
#else
#include <errno.h>

#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/types.h>

#define INVALID_SOCKET -1
#define SOCKET_ERROR -1
#define closesocket close
typedef int SOCKET;
#endif


#include <time.h>
#include <assert.h>
#include <string>

namespace mdk
{

class Socket
{
public:
	enum SocketError
	{
		seTimeOut = -2,
		seSocketClose = -1,
		seError = -3,
	};
	enum protocol 
	{
		tcp = SOCK_STREAM,
		udp = SOCK_DGRAM,
	};

	Socket();
	Socket( SOCKET hSocket, protocol nProtocolType );
	virtual ~Socket();
	
public:
	//
	SOCKET GetSocket();
	/*
		
	*/
	bool Init(protocol nProtocolType);
	/**
		
	 */
	void Close();
	//
	bool IsClosed();

	//////////////////////////////////////////////////////////////////////////
	/*
    TCP
		
	*/
	int Send( const void* lpBuf, int nBufLen, int nFlags = 0 );
	/*
		
	*/
	int Receive( void* lpBuf, int nBufLen, bool bCheckDataLength = false, long lSecond = 0, long lMinSecond = 0 );

	//////////////////////////////////////////////////////////////////////////
	/*UDP
	
	*/
	int SendTo( const char *strIP, int nPort, const void* lpBuf, int nBufLen, int nFlags = 0 );
	/*
		
	*/
	int ReceiveFrom( char* lpBuf, int nBufLen, std::string &strFromIP, int &nFromPort, bool bCheckDataLength = false, long lSecond = 0, long lMinSecond = 0 );

	/*
		
	*/
	bool Connect( const char* lpszHostAddress, unsigned short nHostPort );

	/*
		
	*/
	bool StartServer( int nPort );
	/*
	*/
	bool Accept(Socket& rConnectedSocket);
	//
	void GetWanAddress( std::string& strWanIP, int& nWanPort );
	//
	void GetLocalAddress( std::string& strWanIP, int& nWanPort );
	/*
		
	*/
	bool SetSockMode( bool bWait = false );
	/*
		
	*/
	bool SetSockOpt( int nOptionName, const void* lpOptionValue, int nOptionLen, int nLevel = SOL_SOCKET );

	/*
	*/
	static bool SocketInit(void *lpVoid = NULL);
	static void SocketDestory();
	//
	void GetLastErrorMsg( std::string &strError );
	
	//
	//
	static bool InitForIOCP( SOCKET hSocket );

	/*
		
	*/
	void Attach( SOCKET hSocket );
	/*
		
	*/
	SOCKET Detach();

	//
	//
	bool InitWanAddress();
	//
	//
	bool InitLocalAddress();
	
protected:
	/*
		
	*/
	bool TimeOut( long lSecond, long lMinSecond );
	//
	bool WaitData();
	/*
		
	*/
	void GetAddress( const sockaddr_in &sockAddr, std::string &strIP, int &nPort );
	/*
		
	*/
	bool Bind( unsigned short nPort, char *strIP = NULL );
	/*
		
	*/
	bool Listen( int nConnectionBacklog = SOMAXCONN );
		
public:
private:
	SOCKET m_hSocket;//
	bool m_bBlock;//
	bool m_bOpened;//
	sockaddr_in m_sockAddr;
	std::string m_strWanIP;
	int m_nWanPort;
	std::string m_strLocalIP;
	int m_nLocalPort;
};

}//namespace mdk

#endif // MDK_SOCKET_H


 

socket.cpp的实现:

// Socket.cpp: implementation of the Socket class.
//
//////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <iostream>
#include "Socket.h"

#ifdef WIN32
//#include <windows.h>
#pragma comment ( lib, "ws2_32.lib" )
#endif

using namespace std;
namespace mdk
{

Socket::Socket()
{
	m_hSocket = INVALID_SOCKET;
	m_bBlock = true;
	m_bOpened = false;//
}

Socket::Socket( SOCKET hSocket, protocol nProtocolType )
{
	m_hSocket = hSocket;
	m_bBlock = true;
	m_bOpened = false;//
	Init(nProtocolType);
	InitWanAddress();
	InitLocalAddress();
}

Socket::~Socket()
{

}

/*

*/
bool Socket::SocketInit(void *lpVoid)
{
#ifdef WIN32
	// initialize Winsock library
	WSADATA *lpwsaData = (WSADATA *)lpVoid;
	WSADATA wsaData;
	if (lpwsaData == NULL)
		lpwsaData = &wsaData;

	WORD wVersionRequested = MAKEWORD(1, 1);
	__int32 nResult = WSAStartup(wVersionRequested, lpwsaData);
	if (nResult != 0)
		return false;

	if (LOBYTE(lpwsaData->wVersion) != 1 || HIBYTE(lpwsaData->wVersion) != 1)
	{
		WSACleanup();
		return false;
	}
#endif
	return true;
}

void Socket::SocketDestory()
{
#ifdef WIN32
	WSACleanup();
#endif
}


void Socket::GetLastErrorMsg( string &strError )
{
	char strErr[1024];
#ifdef WIN32
	LPSTR lpBuffer;
	DWORD dwErrorCode = WSAGetLastError();

	FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER
		| FORMAT_MESSAGE_FROM_SYSTEM,
		NULL,
		dwErrorCode,
		LANG_NEUTRAL,
		(LPTSTR)&lpBuffer,
		0,
		NULL );
	sprintf( strErr, "Socket Error(%ld):%s", dwErrorCode, lpBuffer );
	strError = strErr;
	LocalFree(lpBuffer);

#else
	sprintf( strErr, "socket errno(%d):%s\n", errno, strerror(errno) );
	strError = strErr;
#endif
}


bool Socket::InitForIOCP( SOCKET hSocket )
{
#ifdef WIN32
	return 0 == setsockopt( hSocket,
		SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
		(char *)&(hSocket), sizeof(hSocket) );
#else
	return true;
#endif
}


//
SOCKET Socket::GetSocket()
{
	return m_hSocket;
}


void Socket::GetWanAddress( string& strWanIP, int& nWanPort )
{
	nWanPort = m_nWanPort;
	strWanIP = m_strWanIP;
	return;
}



bool Socket::InitWanAddress()
{
	assert( INVALID_SOCKET != m_hSocket );

	sockaddr_in sockAddr;
	memset(&sockAddr, 0, sizeof(sockAddr));
	socklen_t nSockAddrLen = sizeof(sockAddr);
	if ( SOCKET_ERROR == getpeername( m_hSocket,
		(sockaddr*)&sockAddr, &nSockAddrLen ) ) return false;
	m_nWanPort = ntohs(sockAddr.sin_port);
	m_strWanIP = inet_ntoa(sockAddr.sin_addr);

	return true;
}


void Socket::GetLocalAddress( string& strWanIP, int& nWanPort )
{
	nWanPort = m_nLocalPort;
	strWanIP = m_strLocalIP;
	return;
}

bool Socket::InitLocalAddress()
{
	sockaddr_in sockAddr;
	memset(&sockAddr, 0, sizeof(sockAddr));
	socklen_t nSockAddrLen = sizeof(sockAddr);
	if ( SOCKET_ERROR == getsockname( m_hSocket,
		(sockaddr*)&sockAddr, &nSockAddrLen )) return false;
	m_nLocalPort = ntohs(sockAddr.sin_port);
	m_strLocalIP = inet_ntoa(sockAddr.sin_addr);

	return true;
}

/*

*/
bool Socket::Init(protocol nProtocolType)
{
	if ( m_bOpened ) return true;
	if ( m_hSocket == INVALID_SOCKET )
	{
		m_hSocket = socket( PF_INET, nProtocolType, 0 );
		if ( m_hSocket == INVALID_SOCKET ) return false;
	}
	m_bOpened = true;

	return m_bOpened;
}

/*

*/
bool Socket::Connect( const char *lpszHostAddress, unsigned short nHostPort)
{
	assert( NULL != lpszHostAddress );

	sockaddr_in sockAddr;
	memset(&sockAddr,0,sizeof(sockAddr));
	sockAddr.sin_family = AF_INET;
	sockAddr.sin_addr.s_addr = inet_addr(lpszHostAddress);
	sockAddr.sin_port = htons( nHostPort );

	if ( SOCKET_ERROR != connect(m_hSocket, (sockaddr*)&sockAddr, sizeof(sockAddr)) )
	{
		InitWanAddress();
		InitLocalAddress();
		return true;
	}

	return false;
}

//
bool Socket::StartServer( int nPort )
{
	if ( !this->Bind( nPort ) ) return false;
	return this->Listen();
}

//
bool Socket::IsClosed()
{
	return !m_bOpened;
}
/*

*/
void Socket::Close()
{
	if ( INVALID_SOCKET == m_hSocket ) return;
	if ( m_bOpened )
	{
		closesocket(m_hSocket);
		m_bOpened = false;
	}
	m_hSocket = INVALID_SOCKET;
	return;
}

/*

*/
void Socket::Attach(SOCKET hSocket)
{
	m_hSocket = hSocket;
	m_bBlock = true;
	m_bOpened = true;//
	InitWanAddress();
	InitLocalAddress();
}

/*

*/
SOCKET Socket::Detach()
{
	SOCKET hSocket = m_hSocket;
	m_hSocket = INVALID_SOCKET;
	m_bBlock = true;
	m_bOpened = false;//
	return hSocket;
}

/*

*/
int Socket::Receive( void* lpBuf, int nBufLen, bool bCheckDataLength, long lSecond, long lMinSecond )
{
	if ( TimeOut( lSecond, lMinSecond ) ) return seTimeOut;//³¬Ê±
	int nResult;
	int nFlag = 0;
	if ( bCheckDataLength ) nFlag = MSG_PEEK;
	nResult = recv(m_hSocket, (char*)lpBuf, nBufLen, nFlag);
	if ( 0 == nResult ) return seSocketClose;//
	if ( SOCKET_ERROR != nResult ) return nResult;//ú

	//socket
#ifdef WIN32
		int nError = GetLastError();
		if ( WSAEWOULDBLOCK == nError ) return 0;//
		return seError;
#else
		if ( EAGAIN == errno ) return 0;//
		return seError;
#endif
}

/*

*/
int Socket::Send( const void* lpBuf, int nBufLen, int nFlags )
{
	int nSendSize = send(m_hSocket, (char*)lpBuf, nBufLen, nFlags);
	if ( 0 > nSendSize )
	{
#ifdef WIN32
		int nError = GetLastError();
		return -1;
#else
		return -1;
#endif
	}
	if ( nSendSize <= nBufLen ) return nSendSize;
	return -1;
}

/*

*/
bool Socket::Bind( unsigned short nPort, char *strIP )
{
	memset(&m_sockAddr,0,sizeof(m_sockAddr));
	m_sockAddr.sin_family = AF_INET;
	if ( NULL == strIP ) m_sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	else
	{
		unsigned long lResult = inet_addr( strIP );
		if ( lResult == INADDR_NONE ) return false;
		m_sockAddr.sin_addr.s_addr = lResult;
	}
	m_sockAddr.sin_port = htons((unsigned short)nPort);

	return (SOCKET_ERROR != bind(m_hSocket, (sockaddr*)&m_sockAddr, sizeof(m_sockAddr)));
}

/*

*/
bool Socket::Listen( int nConnectionBacklog )
{
	return (SOCKET_ERROR != listen(m_hSocket, nConnectionBacklog));
}

/*

*/
bool Socket::Accept(Socket& rConnectedSocket)
{
	assert( INVALID_SOCKET == rConnectedSocket.m_hSocket );
	socklen_t sockAddLen = 0;
	rConnectedSocket.m_hSocket = accept(m_hSocket, NULL, &sockAddLen);
	if ( INVALID_SOCKET == rConnectedSocket.m_hSocket )
	{
#ifdef WIN32
		if ( WSAEWOULDBLOCK == GetLastError() ) return true;//
#else
		if ( EAGAIN == errno ) return true;//
#endif
		return false;//socket
	}
	rConnectedSocket.m_bOpened = true;
	rConnectedSocket.InitWanAddress();
	rConnectedSocket.InitLocalAddress();
	return true;
}

/*

*/
bool Socket::SetSockOpt(
						   int nOptionName,
						   const void* lpOptionValue,
						   int nOptionLen,
						   int nLevel)
{
	return ( SOCKET_ERROR != setsockopt(
		m_hSocket,
		nLevel,
		nOptionName,
		(char *)lpOptionValue,
		nOptionLen));
}

/*

*/
bool Socket::TimeOut( long lSecond, long lMinSecond )
{
	if ( lSecond <= 0 && lMinSecond <= 0 ) return false;
	//
	timeval outtime;//
	outtime.tv_sec = lSecond;
	outtime.tv_usec =lMinSecond;
	int nSelectRet;
#ifdef WIN32
	FD_SET readfds = { 1, m_hSocket };
	nSelectRet=::select( 0, &readfds, NULL, NULL, &outtime ); //
#else
	fd_set readfds;
	FD_ZERO(&readfds);
	FD_SET(m_hSocket, &readfds);
	nSelectRet=::select(m_hSocket+1, &readfds, NULL, NULL, &outtime); //
#endif

	if ( SOCKET_ERROR == nSelectRet )
	{
		return true;
	}
	if ( 0 == nSelectRet ) //
	{
		return true;
	}

	return false;
}

//
bool Socket::WaitData()
{
	int nSelectRet;
#ifdef WIN32
	FD_SET readfds = { 1, m_hSocket };
	nSelectRet=::select( 0, &readfds, NULL, NULL, NULL ); //
#else
	fd_set readfds;
	FD_ZERO(&readfds);
	FD_SET(m_hSocket, &readfds);
	nSelectRet=::select(m_hSocket+1, &readfds, NULL, NULL, NULL); //
#endif
	if ( SOCKET_ERROR == nSelectRet )
	{
		return false;
	}
	if ( 0 == nSelectRet ) //
	{
		return false;
	}
	return true;
}

/*

*/
bool Socket::SetSockMode( bool bWait )
{
#ifdef WIN32
	m_bBlock = bWait;
	unsigned long ul = 1;
	if ( m_bBlock ) ul = 0;
	else ul = 1;
	int ret = ioctlsocket( m_hSocket, FIONBIO, (unsigned long*)&ul );
	if ( ret == SOCKET_ERROR )
	{
		return false;
	}
#else
	m_bBlock = bWait;
	int flags = fcntl( m_hSocket, F_GETFL, 0 ); //
	if ( !m_bBlock )
		fcntl( m_hSocket, F_SETFL, flags|O_NONBLOCK );//
	else
		fcntl( m_hSocket, F_SETFL, flags&(~O_NONBLOCK&0xffffffff) );//
#endif
	return true;
}

/*

*/
int Socket::SendTo( const char *strIP, int nPort, const void* lpBuf, int nBufLen, int nFlags )
{
	sockaddr_in sockAddr;
	memset(&sockAddr,0,sizeof(sockAddr));
	sockAddr.sin_family = AF_INET;
	sockAddr.sin_port = htons(nPort);
	sockAddr.sin_addr.s_addr = inet_addr(strIP);

	int ret = sendto( m_hSocket, (const char*)lpBuf, nBufLen, nFlags,
		(sockaddr*)&sockAddr, sizeof(sockaddr));
	if (ret < 0) ret = -1;
	return ret;
}

/*
*/
int Socket::ReceiveFrom( char* lpBuf, int nBufLen, string &strFromIP, int &nFromPort, bool bCheckDataLength, long lSecond, long lMinSecond )
{
	strFromIP = "";
	nFromPort = -1;
	if ( 0 >= nBufLen ) return 0;
	sockaddr_in sockAddr;
	socklen_t nAddrLen = sizeof(sockaddr);
	/* waiting for receive data */
	int nResult;
	int nFlag = 0;
	while ( true )
	{
		if ( TimeOut( lSecond, lMinSecond ) ) return seTimeOut;
		if ( bCheckDataLength )nFlag = MSG_PEEK;
		nResult = recvfrom(m_hSocket, lpBuf, nBufLen, nFlag, (sockaddr*)&sockAddr, &nAddrLen);
		if ( nAddrLen > 0 ) GetAddress(sockAddr, strFromIP, nFromPort);
		if ( SOCKET_ERROR == nResult ) //
		{
#ifndef WIN32
			if ( EAGAIN == errno ) return 0;//
			return seError;
#else
			int nError = GetLastError();
			if ( 0 == nError )//
			{
				if ( MSG_PEEK == nFlag )//
				{
					recvfrom(m_hSocket, lpBuf, nBufLen, 0, (sockaddr*)&sockAddr, &nAddrLen);
				}
				continue;
			}
			if ( WSAEWOULDBLOCK == nError ) return 0;//
			return seError;
#endif
		}
		break;
	}
	return nResult;
}

void Socket::GetAddress( const sockaddr_in &sockAddr, string &strIP, int &nPort )
{
	nPort = ntohs(sockAddr.sin_port);
	strIP = inet_ntoa(sockAddr.sin_addr);
}

}//namespace mdk


 

 定义一个客户端的经常用的头文件:

 

//
//  mac.h
//  mac_client
//
//  Created by mac mac on 13-5-8.
//  Copyright (c) 2013年 __MyCompanyName__. All rights reserved.
//

#ifndef mac_client_mac_h
#define mac_client_mac_h
////////////////////////////////////////////////////////////////////////////////////////
/*
1.注意中文标点,编译器不容易察觉
2.server 和client端的宏定义大小要统一
*/
/////////////////////////////////////////////////////////////////////////////////////////
#define MAX_SIZE 1024
#define MAX_NAME_LENGTH 64
#define MAX_PATH 260            //保持和windows端定义的一样
#define MAX_PATH_MAC 256       //苹果的最大长度
#define FALSE 0
#define TRUE 1

#define MAX_SEND_BUFFER 1024*4
#define MAX_RECV_BUFFER 1024*4

/////////////////////////////////////////////////////////////////////////////////////////



typedef struct Command //自定义命令结构体
{
    int order;//命令序号
    long datasize;
    void * hwnd;//窗口句柄
    
}Command;
 

typedef struct Server_Address //服务器地址
{
    
    char strIP[3][MAX_PATH];//服务器ip
    unsigned int uPort[3] ; //服务器端口
    char remark[MAX_NAME_LENGTH];
}Server_Address;


typedef struct _SYS_INFO //到时候得增加备注
{
    Command         cmd;
    char            remark[MAX_NAME_LENGTH];            //备注
    char            computer_name[MAX_NAME_LENGTH];     //
    char            user_name[MAX_NAME_LENGTH];         //
    char            sys_version[MAX_NAME_LENGTH];       //
    char            cpu_type[MAX_NAME_LENGTH];          //
    char            host_ip_address[MAX_NAME_LENGTH];   //内网ip
    char            ip_addresss[MAX_NAME_LENGTH];       //外网ip
    char            uuid[MAX_NAME_LENGTH];             //被控端的唯一标识
    
    unsigned int    cpu_num;                            //
    unsigned int    mem_total;                          //
    int             host_id;                            //
}SYS_INFO;




enum 
{
    COMMAND_BIGIN = 20000,     //命令开始
    TOKEN_ONLINE,              //上线命令
    COMMAND_BREAK,             //断开链接
    COMMAND_UNINSTALL,         //卸载 
    COMMAND_MODIFY_REMARK,     //修改备注
///////////////////////////////////////////////////////////////////////////
    COMMAND_MANAGER_FILE,           //打开文件管理窗口
    COMMAND_GET_DIRECTORY,          //获取控制端主机根目录下所有文件信息 
    COMMAND_GET_REQUEST_DIRECTORY,  //获取双击请求目录中所有文件信息
    COMMAND_SEARCH_FILE,            //文件搜索,还没做
    COMMAND_WRONG_DIRECTORY,        //目录为空或者不可读
    COMMAND_DELETE_FILE,            //删除文件
    COMMAND_FILE_CLOSE,             //关闭当前文件管理功能的链接
//////////////////////////////////////////////////////////////////////////
    
    COMMAND_MANAGER_CMD,    //打开cmd管理窗口
    TOKEN_CMD_NEXT,         //等待下一个命令
    COMMAND_SHELL,          //控制端请求的shell命令
    COMMAND_SHELL_CLOSE,    //关闭shell
////////////////////////////////////////////////////////////////////////// 
    COMMAND_MANAGER_DOWNLOAD,   //打开文件下载功能
    TOKEN_DOWNLOAD_SETUP,   //控制端发送文件下载连接和uuid
    COMMAND_GET_FILEDATA,   //请求文件数据
    TOKEN_SENT_FILEDATA,    //发送文件数据
    TOKEN_CANT_GET_DATA,    //给控制端发送消息文件不可读
////////////////////////////////////////////////////////////////////////
    COMMAND_MANAGER_UPLOAD,     //开启文件上传
    TOKEN_UPLOAD_SETUP,         //接受文件上传连接
    COMMAND_SENT_UPLOAD_DATA,   //发送上传文件数据
    TOKEN_UPLOAD_DATA,          //请求上传文件数据
    TOKEN_UPLOAD_COMPLETE,      //文件上传完成
    COMMAND_RUN_PROGARM,        //上传运行的文件
    
    COMMAND_UPLOAD_CLOSE,      //关闭上传进程和连接
    
    
 };



#endif


 

客户端centos下面使用正常,开发环境codeblocks

 客户端下载:

http://download.csdn.net/detail/wangyaninglm/8326249

 

服务器windows 7,xp都没有问题,开发环境visual studio 2010

服务端下载:

http://download.csdn.net/detail/wangyaninglm/8326321

 

 整体界面:

命令行效果:

文件管理效果:

 

未完待续

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
linux和windows互传文件/用户配置文件和密码配置文件/用户组管理/用户管理
2.27linux和windows互传文件 3.1 用户配置文件和密码配置文件 3.2 用户组管理 3.3 用户管理 linux和windows互传文件 显示日期date [root@centos_1 ~]# date 2017年 11月 21日 星期二 08:38:...
1148 0
+关注
season雅宁
大数据生态圈,计算机视觉,机器学习,高端技术的爱好者,话不多说,上代码!!!
287
文章
1
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载