? ? ? ?近期接到一款新的硬件设备需要实现PC端与安卓终端实现COM口通信,以往都是厂家封装了通信模块或者采用Windows的SCARD读卡器设备,调用SCardConnectA、SCardTransmit等方法来实现通信。
? ? ? 这一次比较惨,需要写个比较原始一点的。串口的连接和读写参考了如下地址:C++ 串口通讯(含所有源代码)_vivisl的博客-CSDN博客_串口通信代码
? ? ? ?本人在创建和数据读写上做了些许优化,采用新增写线程、读线程、和令牌回令机制来保证一应一答方便上层操作。秉着程序员天下为公,一切开源的念想,特将源码贴出望能给需要帮助之人提供些许的帮助。
-- -- -- -- --?
头文件?DataTransmit.h
#ifndef __DATATRANSMIT__H__
#define __DATATRANSMIT__H__
#include<windows.h>
#include<deque>
#include<map>
/*
此文件实现com口端口通信操作
作者:王邵华
时间:2021/9/17
*/
//int reqid = (fd & 0xFF )<< 24 | (uid & 0x00FFFFFF)
//口令
typedef struct _SShibboleth
{
int buffsize;
const unsigned char *buff;
_SShibboleth(const unsigned char *b,int s){buff=b;buffsize=s;}
_SShibboleth(){}
}SShibboleth,*pSShibboleth;
//回令
typedef struct _SBackcross
{
long long timestamp;
int buffsize;
unsigned char* buff;
_SBackcross(unsigned char *b,int s,long long t){buff=b;buffsize=s;timestamp=t;}
_SBackcross(){}
~_SBackcross()
{
if(buff)
{
delete buff;
buff = nullptr;
}
}
}SBackcross,*pSBackcross;
typedef struct _SComBuffer
{
_SShibboleth shibboleth;
_SBackcross backcross;
int shibbolethRet;
int backcrossRet;
}SComBuffer,*pSComBuffer;
class CCOM
{
public:
CCOM(int com,UINT baud = 9600, char parity = 'N', UINT databits = 8, UINT stopsbits = 1);
virtual ~CCOM();
HANDLE GetComHandle();
/*
WriteBuffer: 从PC端写入buff的buffsize个数据到终端
params:
in:
const unsigned char *buff:写入数据指针,上传分配空间
int buffsize:写入数据的长度
out:
int &reqid:此次请求的唯一ID,用于后续操作的令牌
return:
int:成功写入的长度,写入失败返回负数
*/
int WriteBuffer(const unsigned char *buff, int buffsize, int &reqid);
/*
ReadBufferLen: 获取终端回应该令牌的数据长度
params:
in:
const int reqid:WriteBuffer调用时得到的令牌ID
out:
return:
int:回令的长度,失败返回-1
*/
int ReadBufferLen(const int reqid);
/*
ReadBuffer: 从终端读取该令牌的回令数据
params:
in:
const int reqid:WriteBuffer调用时得到的令牌ID
out:
unsigned char *buff:写入数据指针,上传分配空间
int buffsize:写入数据的长度
return:
int:成功写入的长度,写入失败返回负数
备注:
回令只能被读取一次,自动失效,超时未回令也会自动失效
*/
int ReadBuffer(const int reqid, unsigned char *buff, int buffsize);
/*
SetTimeout: 设置超时时间
params:
in:
int time:超时时间,单位毫秒.时间不合法[20-300000]20毫秒-5分钟按默认6秒
out:
return:
bool:成功true,失败false
备注:
回令只能被读取一次,自动失效,超时未回令也会自动失效
*/
bool SetTimeout(int time){
time = time * 1000;
if(time>=20 && time <= 300000)timeout = time;
else
timeout = 6000;
return true;
}
private:
CCOM(const CCOM&){};
CCOM(const CCOM&&){};
CCOM& operator=(const CCOM&){};
CCOM& operator=(const CCOM&&){};
static unsigned int __stdcall ListenReadThread(void* pParam);
static unsigned int __stdcall ListenWriteThread(void* pParam);
bool WriteComData(int reqid, const unsigned char* pData, unsigned int length);
bool ReadComData(int reqid, unsigned char* pData, unsigned int length);
void deleteReqidFront();
unsigned int GetBytesInCOM();
bool openPort(UINT portNo);
bool InitPort(UINT portNo, const LPDCB& plDCB);
/** 初始化串口函数
*
* @param: UINT portNo 串口编号,默认值为1,即COM1,注意,尽量不要大于9
* @param: UINT baud 波特率,默认为9600
* @param: char parity 是否进行奇偶校验,'Y'表示需要奇偶校验,'N'表示不需要奇偶校验
* @param: UINT databits 数据位的个数,默认值为8个数据位
* @param: UINT stopsbits 停止位使用格式,默认值为1
* @param: DWORD dwCommEvents 默认为EV_RXCHAR,即只要收发任意一个字符,则产生一个事件
* @return: bool 初始化是否成功
* @note: 在使用其他本类提供的函数前,请先调用本函数进行串口的初始化
* /n本函数提供了一些常用的串口参数设置,若需要自行设置详细的DCB参数,可使用重载函数
* /n本串口类析构时会自动关闭串口,无需额外执行关闭串口
* @see:
*/
bool InitPort(UINT portNo = 1, UINT baud = CBR_9600, char parity = 'N', UINT databits = 8, UINT stopsbits = 1, DWORD dwCommEvents = EV_RXCHAR);
private:
HANDLE m_hComm;
int m_comPort;
int m_reqid;
/** 线程句柄 */
volatile HANDLE m_hListenReadThread;
volatile HANDLE m_hListenWriteThread;
unsigned int m_ListenReadThreadId;
unsigned int m_ListenWriteThreadId;
volatile HANDLE hEventListenReadThread; //开启读数据事件
volatile HANDLE hEventListenWriteThread; //开启写数据事件
volatile HANDLE hEventExit;
char szEventListenReadThread[128], szEventListenWriteThread[128], szEventExit[128];
bool isRun;
int timeout;
/** 同步互斥,临界区保护 */
CRITICAL_SECTION m_csSync; //!< 互斥操作串口
std::deque<int> m_Reqid;
std::map<int,SComBuffer> m_SComBuffer;
};
class CComHelper
{
public:
HANDLE CreatePort(UINT portNo = 1, UINT baud = CBR_9600, char parity = 'N', UINT databits = 8, UINT stopsbits = 1);
bool RemovePort(HANDLE h);
int Write(HANDLE h,const unsigned char *buff, int buffsize, int &reqid);
int ReadLen(HANDLE h, const int reqid);
int Read(HANDLE h, const int reqid, unsigned char *buff, int buffsize);
bool SetTimeout(HANDLE h,int time);
static CComHelper* getInstance();
virtual ~CComHelper()
{
m_mapPortCom.erase(m_mapPortCom.begin(),m_mapPortCom.end());
}
private:
CComHelper(){};
CComHelper(const CComHelper&){};
CComHelper(const CComHelper&&){};
CComHelper& operator=(const CComHelper&){};
CComHelper& operator=(const CComHelper&&){};
std::map<UINT,CCOM*> m_mapPortCom;
std::map<HANDLE,UINT> m_mapHandlePort;
/*
HANDLE h = CreatePort(3);
unsigned char writebuff[5] = {0xAA,0AA,0XAA,0X99,0X96};
int reqid = 0;
int len = 0;
int ret = 0;
if(0 < Write(h, writebuff,5,reqid))
{
if(0 < (len = ReadBufferLen(h,reqid)))
{
unsigned char *readbuff = new unsigned char[len];
if(0 < Read(h,reqid,readbuff,len))
{
// do your things
//...
}
if(readbuff){delete[] readbuff; readbuff=nullptr;}
}
}
if(INVALID_HANDLE_VALUE != h)
{
RemovePort(h);
}
*/
};
#endif
源文件?DataTransmit.cpp
#include "DataTransmit.h"
#include <stdio.h>
#include <process.h>
#define ERROR_DEFAULT -1
#define ERROR_NOEXECUTE -9999
#define ERROR_INVALID_HANDLE_VALUE -9988
#define ERROR_READORWRITE -9977
#define ERROR_UNCONNECTION -9966
#define ERROR_PARAMS -2
#define MAX_TIMEOUT 6000
CCOM::CCOM(int com, UINT baud, char parity, UINT databits, UINT stopsbits)
{
m_comPort = com;
m_reqid = 0;
m_hComm = INVALID_HANDLE_VALUE;
m_hListenReadThread = INVALID_HANDLE_VALUE;
m_hListenWriteThread = INVALID_HANDLE_VALUE;
hEventListenReadThread = INVALID_HANDLE_VALUE;
hEventListenWriteThread = INVALID_HANDLE_VALUE;
//开启端口通信句柄
if(InitPort(com,baud,parity,databits,stopsbits))
{
//创建读写线程
m_hListenReadThread = (HANDLE)_beginthreadex(NULL, 0, ListenReadThread, this, 0, &m_ListenReadThreadId);
m_hListenWriteThread = (HANDLE)_beginthreadex(NULL, 0, ListenWriteThread, this, 0, &m_ListenWriteThreadId);
sprintf_s(szEventListenReadThread, "EventListenReadThread_COM%d_%d",com,m_hComm);
sprintf_s(szEventListenWriteThread, "EventListenWriteThread_COM%d_%d",com,m_hComm);
sprintf_s(szEventExit, "szEventExit_COM%d_%d",com,m_hComm);
hEventListenReadThread = ::CreateEventA(NULL,true,false,szEventListenReadThread);
hEventListenWriteThread = ::CreateEventA(NULL,true,false,szEventListenWriteThread);
hEventExit = ::CreateEventA(NULL,true,false,szEventExit);
}
::InitializeCriticalSection(&m_csSync);
timeout = MAX_TIMEOUT;
}
bool CCOM::InitPort(UINT portNo, const LPDCB& plDCB)
{
/** 打开指定串口,该函数内部已经有临界区保护,上面请不要加保护 */
if (!openPort(portNo))
{
return false;
}
/** 配置串口参数 */
if (!SetCommState(m_hComm, plDCB))
{
return false;
}
/** 清空串口缓冲区 */
PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT);
return true;
}
bool CCOM::openPort(UINT portNo)
{
/** 把串口的编号转换为设备名 */
char szPort[50];
sprintf_s(szPort, "\\\\.\\COM%d", portNo);
/** 打开指定的串口 */
m_hComm = CreateFileA(szPort, /** 设备名,COM1,COM2等 */
GENERIC_READ | GENERIC_WRITE, /** 访问模式,可同时读写 */
0, /** 共享模式,0表示不共享 */
NULL, /** 安全性设置,一般使用NULL */
OPEN_EXISTING, /** 该参数表示设备必须存在,否则创建失败 */
0,
0);
/** 如果打开失败,释放资源并返回 */
if (m_hComm == INVALID_HANDLE_VALUE)
{
return false;
}
return true;
}
bool CCOM::InitPort(UINT portNo /*= 1*/, UINT baud /*= CBR_9600*/, char parity /*= 'N'*/,
UINT databits /*= 8*/, UINT stopsbits /*= 1*/, DWORD dwCommEvents /*= EV_RXCHAR*/)
{
/** 临时变量,将制定参数转化为字符串形式,以构造DCB结构 */
char szDCBparam[50];
sprintf_s(szDCBparam, "baud=%d parity=%c data=%d stop=%d", baud, parity, databits, stopsbits);
/** 打开指定串口,该函数内部已经有临界区保护,上面请不要加保护 */
if (!openPort(portNo))
{
return false;
}
/** 是否有错误发生 */
BOOL bIsSuccess = TRUE;
/** 在此可以设置输入输出的缓冲区大小,如果不设置,则系统会设置默认值.
* 自己设置缓冲区大小时,要注意设置稍大一些,避免缓冲区溢出
*/
/*if (bIsSuccess )
{
bIsSuccess = SetupComm(m_hComm,10,10);
}*/
/** 设置串口的超时时间,均设为0,表示不使用超时限制 */
COMMTIMEOUTS CommTimeouts;
CommTimeouts.ReadIntervalTimeout = 0;
CommTimeouts.ReadTotalTimeoutMultiplier = 0;
CommTimeouts.ReadTotalTimeoutConstant = 0;
CommTimeouts.WriteTotalTimeoutMultiplier = 0;
CommTimeouts.WriteTotalTimeoutConstant = 0;
if (bIsSuccess)
{
bIsSuccess = SetCommTimeouts(m_hComm, &CommTimeouts);
}
DCB dcb;
if (bIsSuccess)
{
/** 获取当前串口配置参数,并且构造串口DCB参数 */
bIsSuccess = GetCommState(m_hComm, &dcb) && BuildCommDCB(szDCBparam, &dcb);
/** 开启RTS flow控制 */
dcb.fRtsControl = RTS_CONTROL_ENABLE;
}
if (bIsSuccess)
{
/** 使用DCB参数配置串口状态 */
bIsSuccess = SetCommState(m_hComm, &dcb);
}
/** 清空串口缓冲区 */
PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT);
return bIsSuccess == TRUE;
}
#define CLOSEHANDLE(x) \
if((x) != INVALID_HANDLE_VALUE)\
{\
::CloseHandle((x));\
(x) = INVALID_HANDLE_VALUE;\
}
CCOM::~CCOM()
{
m_SComBuffer.erase(m_SComBuffer.begin(),m_SComBuffer.end());
::SetEvent(hEventExit);
::Sleep(200);
::DeleteCriticalSection(&m_csSync);
CLOSEHANDLE(m_hComm)
CLOSEHANDLE(m_hListenReadThread)
CLOSEHANDLE(m_hListenWriteThread)
CLOSEHANDLE(hEventListenReadThread)
CLOSEHANDLE(hEventListenWriteThread)
CLOSEHANDLE(hEventExit)
}
HANDLE CCOM::GetComHandle()
{
return m_hComm;
}
unsigned int __stdcall CCOM::ListenReadThread(void* pParam)
{
CCOM *pSerialPort = reinterpret_cast<CCOM*>(pParam);
HANDLE handles[2] = {pSerialPort->hEventListenReadThread,pSerialPort->hEventExit};
while(true)
{
int waitobject = ::WaitForMultipleObjects(2,handles,false,INFINITE); //
int errorno = ::GetLastError();
switch (waitobject)
{
case WAIT_OBJECT_0: //读数据
{
auto reqid = pSerialPort->m_Reqid.front();
pSerialPort->deleteReqidFront();
auto &itr = pSerialPort->m_SComBuffer.find(reqid);
if(itr == pSerialPort->m_SComBuffer.end()) break;
long start = ::GetTickCount();
long end = 0;
unsigned int size = 0;
while(size <= 0)
{
size = pSerialPort->GetBytesInCOM();
end = ::GetTickCount();
if(end - start > pSerialPort->timeout) break; //超时未应答
}
if(size <= 0)
{
pSerialPort->m_SComBuffer[reqid].backcrossRet = ERROR_UNCONNECTION;
break;
}
pSerialPort->m_SComBuffer[reqid].backcross.buff = new unsigned char[size];
memset(pSerialPort->m_SComBuffer[reqid].backcross.buff,0x00,size);
pSerialPort->m_SComBuffer[reqid].backcross.buffsize = size;
pSerialPort->m_SComBuffer[reqid].backcross.timestamp = ::GetTickCount();
pSerialPort->ReadComData(reqid,pSerialPort->m_SComBuffer[reqid].backcross.buff,pSerialPort->m_SComBuffer[reqid].backcross.buffsize);
break;
}
case WAIT_OBJECT_0 + 1: //结束
{
return 0;
break;
}
default:
break;
}
::ResetEvent(pSerialPort->hEventListenReadThread);
}
return 0;
}
unsigned int __stdcall CCOM::ListenWriteThread(void* pParam)
{
CCOM *pSerialPort = reinterpret_cast<CCOM*>(pParam);
HANDLE handles[2] = {pSerialPort->hEventListenWriteThread,pSerialPort->hEventExit};
while(true)
{
int waitobject = ::WaitForMultipleObjects(2,handles,false,INFINITE); //
int errorno = ::GetLastError();
switch (waitobject)
{
case WAIT_OBJECT_0: //写数据
{
auto reqid = pSerialPort->m_Reqid.front();
//
auto itr = pSerialPort->m_SComBuffer.find(reqid);
if(itr != pSerialPort->m_SComBuffer.end())
{
if(pSerialPort->WriteComData(reqid, itr->second.shibboleth.buff,itr->second.shibboleth.buffsize))
{
::SetEvent(pSerialPort->hEventListenReadThread); //触发令牌获取回令
}
}
//
break;
}
case WAIT_OBJECT_0 + 1: //结束
{
return 0;
break;
}
default:
break;
}
::ResetEvent(pSerialPort->hEventListenWriteThread);
}
return 0;
}
int CCOM::WriteBuffer(const unsigned char *buff, int buffsize, int &reqid)
{
//m_Shibboleth
if(nullptr == buff || 0 >= buffsize)
return ERROR_PARAMS;
::EnterCriticalSection(&m_csSync);
reqid = (m_comPort & 0xFF )<< 24 | (++m_reqid & 0x00FFFFFF);
m_Reqid.push_back(reqid);
SComBuffer combuffer;
combuffer.shibboleth.buff = buff;
combuffer.shibboleth.buffsize = buffsize;
combuffer.shibbolethRet = ERROR_NOEXECUTE;
combuffer.backcrossRet = ERROR_NOEXECUTE;
combuffer.backcross.buff = nullptr;
combuffer.backcross.buffsize = 0;
combuffer.backcross.timestamp = 0;
m_SComBuffer.insert(std::make_pair<int,SComBuffer>(reqid,combuffer));
::LeaveCriticalSection(&m_csSync);
::SetEvent(hEventListenWriteThread);//触发写
int ret = ERROR_DEFAULT;
auto itr = m_SComBuffer.find(reqid);
if(itr != m_SComBuffer.end())
{
long start = ::GetTickCount();
long end = 0;
while(true){ //等待发送完成通知
ret = m_SComBuffer[reqid].shibbolethRet;
if(ERROR_NOEXECUTE != ret) return ret; //未知值
::Sleep(200);
end = ::GetTickCount();
if(end - start > timeout) break; //6秒超时
}
}
return ret;
}
bool CCOM::WriteComData(int reqid, const unsigned char* pData, unsigned int length)
{
bool bResult = TRUE;
DWORD BytesToSend = 0;
auto &itr = m_SComBuffer.find(reqid);
if(itr == m_SComBuffer.end())
{
return false;
}
if (m_hComm == INVALID_HANDLE_VALUE)
{
itr->second.shibbolethRet = ERROR_INVALID_HANDLE_VALUE;
return false;
}
printf("object[%X] reqid[%X] WRITE\n",this,reqid);
for(int i=0;i<length;++i)
printf("%02X",pData[i]);
printf("\n");
/** 向缓冲区写入指定量的数据 */
bResult = (bool)WriteFile(m_hComm, pData, length, &BytesToSend, NULL);
if (!bResult)
{
DWORD dwError = GetLastError();
/** 清空串口缓冲区 */
PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_RXABORT);
m_SComBuffer[reqid].shibbolethRet = ERROR_READORWRITE;
return false;
}
m_SComBuffer[reqid].shibbolethRet = BytesToSend;
return true;
}
void CCOM::deleteReqidFront()
{
::EnterCriticalSection(&m_csSync);
if(!m_Reqid.empty())
m_Reqid.pop_front();
::LeaveCriticalSection(&m_csSync);
}
unsigned int CCOM::GetBytesInCOM()
{
DWORD dwError = 0; /** 错误码 */
COMSTAT comstat; /** COMSTAT结构体,记录通信设备的状态信息 */
memset(&comstat, 0, sizeof(COMSTAT));
UINT BytesInQue = 0;
/** 在调用ReadFile和WriteFile之前,通过本函数清除以前遗留的错误标志 */
if (ClearCommError(m_hComm, &dwError, &comstat))
{
BytesInQue = comstat.cbInQue; /** 获取在输入缓冲区中的字节数 */
}
return BytesInQue;
}
bool CCOM::ReadComData(int reqid, unsigned char* pData, unsigned int length)
{
bool bResult = true;
DWORD BytesRead = 0;
auto itr = m_SComBuffer.find(reqid);
if(itr == m_SComBuffer.end())
{
return false;
}
if (m_hComm == INVALID_HANDLE_VALUE)
{
m_SComBuffer[reqid].backcrossRet = ERROR_INVALID_HANDLE_VALUE;
return false;
}
/** 从缓冲区读取一个字节的数据 */
bResult = (bool)ReadFile(m_hComm, pData, length, &BytesRead, NULL);
printf("object[%X] reqid[%X] READ\n",this,reqid);
for(int i=0;i<BytesRead;++i)
printf("%02X",pData[i]);
printf("\n");
if ((!bResult))
{
/** 获取错误码,可以根据该错误码查出错误原因 */
DWORD dwError = GetLastError();
/** 清空串口缓冲区 */
PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_RXABORT);
m_SComBuffer[reqid].backcrossRet = ERROR_READORWRITE;
return false;
}
/** 离开临界区 */
m_SComBuffer[reqid].backcrossRet = BytesRead;
return true;
}
int CCOM::ReadBufferLen(const int reqid)
{
auto itr = m_SComBuffer.find(reqid);
if(itr == m_SComBuffer.end())
{
return ERROR_PARAMS;
}
int ret = ERROR_DEFAULT;
long start = ::GetTickCount();
long end = 0;
while(true){ //等待发送完成通知
ret = m_SComBuffer[reqid].backcrossRet;
if(ERROR_NOEXECUTE != ret) return ret; //未知值
::Sleep(200);
end = ::GetTickCount();
if(end - start > timeout) break; //6秒超时
}
return ret;
}
int CCOM::ReadBuffer(const int reqid, unsigned char *buff, int buffsize)
{
auto itr = m_SComBuffer.find(reqid);
if(itr == m_SComBuffer.end())
{
return ERROR_PARAMS;
}
if(buffsize < itr->second.backcross.buffsize)
return ERROR_PARAMS;
int ret = ERROR_DEFAULT;
memcpy(buff,itr->second.backcross.buff,itr->second.backcross.buffsize);
ret = itr->second.backcross.buffsize;
m_SComBuffer.erase(itr);//口令 回令 销毁
return ret;
}
CComHelper* CComHelper::getInstance()
{
static CComHelper comHelper;
return &comHelper;
}
HANDLE CComHelper::CreatePort(UINT portNo, UINT baud, char parity, UINT databits, UINT stopsbits)
{
if(0 < m_mapPortCom.count(portNo))
return m_mapPortCom[portNo]->GetComHandle();
CCOM* com = new CCOM(portNo,baud,parity,databits,stopsbits);
m_mapPortCom[portNo] = com;
m_mapHandlePort[com->GetComHandle()] = portNo;
return com->GetComHandle();
}
bool CComHelper::RemovePort(HANDLE h)
{
if(0 >= m_mapHandlePort.count(h))
return false;
auto itr = m_mapPortCom.find(m_mapHandlePort[h]);
if(m_mapPortCom.end() != itr)
m_mapPortCom.erase(itr);
return true;
}
int CComHelper::Write(HANDLE h,const unsigned char *buff, int buffsize, int &reqid)
{
if(0 >= m_mapHandlePort.count(h))
return -1;
auto itr = m_mapPortCom.find(m_mapHandlePort[h]);
if(m_mapPortCom.end() != itr)
return itr->second->WriteBuffer(buff,buffsize,reqid);
return -1;
}
int CComHelper::ReadLen(HANDLE h,const int reqid)
{
if(0 >= m_mapHandlePort.count(h))
return -1;
auto itr = m_mapPortCom.find(m_mapHandlePort[h]);
if(m_mapPortCom.end() != itr)
return itr->second->ReadBufferLen(reqid);
return -1;
}
int CComHelper::Read(HANDLE h,const int reqid, unsigned char *buff, int buffsize)
{
if(0 >= m_mapHandlePort.count(h))
return -1;
auto itr = m_mapPortCom.find(m_mapHandlePort[h]);
if(m_mapPortCom.end() != itr)
return itr->second->ReadBuffer(reqid,buff,buffsize);
return -1;
}
bool CComHelper::SetTimeout(HANDLE h,int time)
{
if(0 >= m_mapHandlePort.count(h))
return false;
auto itr = m_mapPortCom.find(m_mapHandlePort[h]);
if(m_mapPortCom.end() != itr)
return itr->second->SetTimeout(time);
return false;
}
使用案例:
获取端口数据长度和获取端口数据2个接口,内部实现了超时机制,上传只需阻塞式调用。
?啥也不说了,一切都在代码里。如果有用记得一键三连。笔芯!!!
|