在Winodows平台上,网络编程的主要接口就是WinSock,目前大多数的Windows平台上的WinSock平台已经升级到2.0版,简称为WinSock2。在WinSock2中扩展了很多很有用的Windows味很浓的SOCKET专用API,为Windows平台用户提供高性能的网络编程支持。这些函数中的大多数已经不再是标准的“Berkeley”套接字模型的API了。使用这些函数的代价就是你不能再将你的网络程序轻松的移植到“尤里平台”(我给Unix +Linux平台的简称)下,反过来因为Windows平台支持标准的“Berkeley”套接字模型,所以你可以将大多数尤里平台下的网络应用移植到Windows平台下。

如果不考虑可移植性(或者所谓的跨平台性),而是着重于应用的性能时,尤其是注重服务器性能时,对于Windows的程序,都鼓励使用WinSock2扩展的一些API,更鼓励使用IOCP模型,因为这个模型是目前Windows平台上比较完美的一个高性能IO编程模型,它不但适用于SOCKET编程,还适用于读写硬盘文件,读写和管理命名管道、邮槽等等。如果再结合Windows线程池,IOCP几乎可以利用当今硬件所有可能的新特性(比如多核,DMA,高速总线等等),本身具有先天的扩展性和可用性。

今天讨论的重点就是SOCKET池。很多VC程序员也许对SOCKET池很陌生,也有些可能很熟悉,那么这里就先讨论下这个概念。

在Windows平台上SOCKET实际上被视作一个内核对象的句柄,很多Windows API在支持传统的HANDLE参数的同时也支持SOCKET,比如有名的CreateIoCompletionPort就支持将SOCKET句柄代替HANDLE参数传入并调用。熟悉Windows内核原理的读者,立刻就会发现,这样的话,我们创建和销毁一个SOCKET句柄,实际就是在系统内部创建了一个内核对象,对于Windows来说这牵扯到从Ring3层到Ring0层的耗时操作,再加上复杂的安全审核机制,实际创建和销毁一个SOCKET内核对象的成本还是蛮高的。尤其对于一些面向连接的SOCKET应用,服务端往往要管理n多个代表客户端通信的SOCKET对象,而且因为客户的变动性,主要面临的大量操作除了一般的收发数据,剩下的就是不断创建和销毁SOCKET句柄,对于一个频繁接入和断开的服务器应用来说,创建和销毁SOCKET的性能代价立刻就会体现出来,典型的例如WEB服务器程序,就是一个需要频繁创建和销毁SOCKET句柄的SOCKET应用。这种情况下我们通常都希望对于断开的SOCKET对象,不是简单的“销毁”了之(很多时候“断开”的含义不一定就等价于“销毁”,可以仔细思考一下),更多时候希望能够重用这个SOCKET对象,这样我们甚至可以事先创建一批SOCKET对象组成一个“池”,在需要的时候“重用”其中的SOCKET对象,不需要的时候将SOCKET对象重新丢入池中即可,这样就省去了频繁创建销毁SOCKET对象的性能损失。在原始的“Berkeley”套接字模型中,想做到这点是没有什么办法的。而幸运的是在Windows平台上,尤其是支持WinSock2的平台上,已经提供了一套完整的API接口用于支持SOCKET池。

对于符合以上要求的SOCKET池,首先需要做到的就是对SOCKET句柄的“回收”,因为创建函数无论在那个平台上都是现成的,而最早能够实现这个功能的WinSock函数就是TransmitFile,如果代替closesocket函数像下面这样调用就可以“回收”一个SOCKET句柄,而不是销毁:(注意“回收”这个功能对于TransmitFile函数来说只是个“副业”。)

TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT | TF_REUSE_SOCKET );

注意上面函数的最后一个参数,使用了标志TF_DISCONNECT和TF_REUSE_SOCKET,第一个值表示断开,第二个值则明确的表示“重用”实际上也就是回收这个SOCKET,经过这个处理的SOCKET句柄,就可以直接再用于connect等操作,但是此时我们会发现,这个回收来的SOCKET似乎没什么用,因为其他套接字函数没法直接利用这个回收来的SOCKET句柄。

这时就要WinSock2的一组专用API上场了。我将它们按传统意义上的服务端和客户端分为两组:

一、         服务端:

SOCKET WSASocket(

__in          int af,

__in          int type,

__in          int protocol,

__in          LPWSAPROTOCOL_INFO lpProtocolInfo,

__in          GROUP g,

__in          DWORD dwFlags

);

BOOL AcceptEx(

__in          SOCKET sListenSocket,

__in          SOCKET sAcceptSocket,

__in          PVOID lpOutputBuffer,

__in          DWORD dwReceiveDataLength,

__in          DWORD dwLocalAddressLength,

__in          DWORD dwRemoteAddressLength,

__out         LPDWORD lpdwBytesReceived,

__in          LPOVERLAPPED lpOverlapped

);

BOOL DisconnectEx(

__in          SOCKET hSocket,

__in          LPOVERLAPPED lpOverlapped,

__in          DWORD dwFlags,

__in          DWORD reserved

);

二、         客户端:

SOCKET WSASocket(

__in          int af,

__in          int type,

__in          int protocol,

__in          LPWSAPROTOCOL_INFO lpProtocolInfo,

__in          GROUP g,

__in          DWORD dwFlags

);

BOOL PASCAL ConnectEx(

__in          SOCKET s,

__in          const struct sockaddr* name,

__in          int namelen,

__in_opt      PVOID lpSendBuffer,

__in          DWORD dwSendDataLength,

__out         LPDWORD lpdwBytesSent,

__in          LPOVERLAPPED lpOverlapped

);

BOOL DisconnectEx(

__in          SOCKET hSocket,

__in          LPOVERLAPPED lpOverlapped,

__in          DWORD dwFlags,

__in          DWORD reserved

);

注意观察这些函数,似乎和传统的“Berkeley”套接字模型中的一些函数“大同小异”,其实仔细观察他们的参数,就已经可以发现一些调用他们的“玄机”了。

首先我们来看AcceptEx函数,与accept函数不同,它需要两个SOCKET句柄作为参数,头一个参数的含义与accept函数的相同,而第二个参数的意思就是accept函数返回的那个代表与客户端通信的SOCKET句柄,在传统的accept内部,实际在返回那个代表客户端的SOCKET时,是在内部调用了一个SOCKET的创建动作,先创建这个SOCKET然后再“accept”让它变成代表客户端连接的SOCKET,而AcceptEx函数就在这里“扩展”(实际上是“阉割”才对)accept函数,省去了内部那个明显的创建SOCKET的动作,而将这个创建动作交给最终的调用者自己来实现。AcceptEx要求调用者创建好那个sAcceptSocket句柄然后传进去,这时我们立刻发现,我们回收的那个SOCKET是不是也可以传入呢?答案是肯定的,我们就是可以利用这个函数传入那个“回收”来的SOCKET句柄,最终实现服务端的SOCKET重用。

这里需要注意的就是,AcceptEx函数必须工作在非阻塞的IOCP模型下,同时即使AcceptEx函数返回了,也不代表客户端连接进来或者连接成功了,我们必须依靠它的“完成通知”才能知道这个事实,这也是AcceptEx函数区别于accept这个阻塞方式函数的最大之处。通常可以利用AcceptEx的非阻塞特性和IOCP模型的优点,一次可以“预先”发出成千上万个AcceptEx调用,“等待”客户端的连接。对于习惯了accept阻塞方式的程序员来说,理解AcceptEx的工作方式还是需要费一些周折的。下面的例子就演示了如何一次调用多个AcceptEx:

//批量创建SOCKET,并调用对应的AcceptEx

for(UINT i = 0; i < 1000; i++)

{//调用1000次

//创建与客户端通讯的SOCKET,注意SOCKET的创建方式

skAccept = ::WSASocket(AF_INET,

SOCK_STREAM,

IPPROTO_TCP,

NULL,

0,

WSA_FLAG_OVERLAPPED);

if (INVALID_SOCKET == skAccept)

{

throw CGRSException((DWORD)WSAGetLastError());

}

//创建一个自定义的OVERLAPPED扩展结构,使用IOCP方式调用

pAcceptOL = new CGRSOverlappedData(GRS_OP_ACCEPT

,this,skAccept,NULL);

pAddrBuf = pAcceptOL->GetAddrBuf();

//4、发出AcceptEx调用

//注意将AcceptEx函数接收连接数据缓冲的大小设定成了0,这将导致此函数立即返回,虽然与

//不设定成0的方式而言,这导致了一个较低下的效率,但是这样提高了安全性,所以这种效率

//牺牲是必须的

if(!AcceptEx(m_skServer,

skAccept,

pAddrBuf->m_pBuf,

0,//将接收缓冲置为0,令AcceptEx直接返回,防止拒绝服务攻击

GRS_ADDRBUF_SIZE,

GRS_ADDRBUF_SIZE,

NULL,

(LPOVERLAPPED)pAcceptOL))

{

int iError = WSAGetLastError();

if( ERROR_IO_PENDING != iError

&& WSAECONNRESET != iError )

{

if(INVALID_SOCKET != skAccept)

{

::closesocket(skAccept);

skAccept = INVALID_SOCKET;

}

if( NULL != pAcceptOL)

{

GRS_ISVALID(pAcceptOL,sizeof(CGRSOverlappedData));

delete pAcceptOL;

pAcceptOL = NULL;

}

}

}

}

以上的例子只是简单的演示了AcceptEx的调用,还没有涉及到真正的“回收重用”这个主题,那么下面的例子就演示了如何重用一个SOCKET句柄:

if(INVALID_SOCKET == skClient)

{

throw CGRSException(_T("SOCKET句柄是无效的!"));

}

OnPreDisconnected(skClient,pUseData,0);

CGRSOverlappedData*pData

= new GRSOverlappedData(GRS_OP_DISCONNECTEX

,this,skClient,pUseData);

//回收而不是关闭后再创建大大提高了服务器的性能

DisconnectEx(skClient,&pData->m_ol,TF_REUSE_SOCKET,0);

......

//在接收到DisconnectEx函数的完成通知之后,我们就可以重用这个SOCKET了

CGRSAddrbuf*pBuf = NULL;

pNewOL = new CGRSOverlappedData(GRS_OP_ACCEPT

,this,skClient,pUseData);

pBuf = pNewOL->GetAddrBuf();

//把这个回收的SOCKET重新丢进连接池

if(!AcceptEx(m_skServer,skClient,pBuf->m_pBuf,

0,//将接收缓冲置为0,令AcceptEx直接返回,防止拒绝服务攻击

GRS_ADDRBUF_SIZE, GRS_ADDRBUF_SIZE,

NULL,(LPOVERLAPPED)pNewOL))

{

int iError = WSAGetLastError();

if( ERROR_IO_PENDING != iError

&& WSAECONNRESET != iError )

{

throw CGRSException((DWORD)iError);

}

}

//注意在这个SOCKET被重新利用后,重新与IOCP绑定一下,该操作会返回一个已设置的错误,这个错误直接被忽略即可

::BindIoCompletionCallback((HANDLE)skClient

,Server_IOCPThread, 0);

至此回收重用SOCKET的工作也就结束了,以上的过程实际理解了IOCP之后就比较好理解了,例子的最后我们使用了BindIoCompletionCallback函数重新将SOCKET丢进了IOCP线程池中,实际还可以再次使用CreateIoCompletionPort函数达到同样的效果,这里列出这一步就是告诉大家,不要忘了再次绑定一下完成端口和SOCKET。

对于客户端来说,可以使用ConnectEx函数来代替connect函数,与AcceptEx函数相同,ConnectEx函数也是以非阻塞的IOCP方式工作的,唯一要注意的就是在WSASocket调用之后,在ConnectEx之前要调用一下bind函数,将SOCKET提前绑定到一个本地地址端口上,当然回收重用之后,就无需再次绑定了,这也是ConnectEx较之connect函数高效的地方之一。

与AcceptEx函数类似,也可以一次发出成千上万个ConnectEx函数的调用,可以连接到不同的服务器,也可以连接到相同的服务器,连接到不同的服务器时,只需提供不同的sockaddr即可。

通过上面的例子和讲解,大家应该对SOCKET池概念以及实际的应用有个大概的了解了,当然核心仍然是理解了IOCP模型,否则还是寸步难行。

在上面的例子中,回收SOCKET句柄主要使用了DisconnectEx函数,而不是之前介绍的TransmitFile函数,为什么呢?因为TransmitFile函数在一些情况下会造成死锁,无法正常回收SOCKET,毕竟不是专业的回收重用SOCKET函数,我就遇到过好几次死锁,最后偶然的发现了DisconnectEx函数这个专用的回收函数,调用之后发现比TransmitFile专业多了,而且不管怎样都不会死锁。

最后需要补充的就是这几个函数的调用方式,不能像传统的SOCKET API那样直接调用它们,而需要使用一种间接的方式来调用,尤其是AcceptEx和DisconnectEx函数,下面给出了一个例子类,用于演示如何动态载入这些函数并调用之:

class CGRSMsSockFun

{

public:

CGRSMsSockFun(SOCKET skTemp = INVALID_SOCKET)

{

if( INVALID_SOCKET != skTemp )

{

LoadAllFun(skTemp);

}

}

public:

virtual ~CGRSMsSockFun(void)

{

}

protected:

BOOL LoadWSAFun(SOCKET& skTemp,GUID&funGuid,void*&pFun)

{

DWORD dwBytes = 0;

BOOL bRet = TRUE;

pFun = NULL;

BOOL bCreateSocket = FALSE;

try

{

if(INVALID_SOCKET == skTemp)

{

skTemp = ::WSASocket(AF_INET,SOCK_STREAM,

IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);

bCreateSocket = (skTemp != INVALID_SOCKET);

}

if(INVALID_SOCKET == skTemp)

{

throw CGRSException((DWORD)WSAGetLastError());

}

if(SOCKET_ERROR == ::WSAIoctl(skTemp,

SIO_GET_EXTENSION_FUNCTION_POINTER,

&funGuid,sizeof(funGuid),

&pFun,sizeof(pFun),&dwBytes,NULL,

NULL))

{

pFun = NULL;

throw CGRSException((DWORD)WSAGetLastError());

}

}

catch(CGRSException& e)

{

if(bCreateSocket)

{

::closesocket(skTemp);

}

}

return NULL != pFun;

}

protected:

LPFN_ACCEPTEX m_pfnAcceptEx;

LPFN_CONNECTEX m_pfnConnectEx;

LPFN_DISCONNECTEX m_pfnDisconnectEx;

LPFN_GETACCEPTEXSOCKADDRS m_pfnGetAcceptExSockaddrs;

LPFN_TRANSMITFILE m_pfnTransmitfile;

LPFN_TRANSMITPACKETS m_pfnTransmitPackets;

LPFN_WSARECVMSG m_pfnWSARecvMsg;

protected:

BOOL LoadAcceptExFun(SOCKET &skTemp)

{

GUID GuidAcceptEx = WSAID_ACCEPTEX;

return LoadWSAFun(skTemp,GuidAcceptEx

,(void*&)m_pfnAcceptEx);

}

BOOL LoadConnectExFun(SOCKET &skTemp)

{

GUID GuidAcceptEx = WSAID_CONNECTEX;

return LoadWSAFun(skTemp,GuidAcceptEx

,(void*&)m_pfnConnectEx);

}

BOOL LoadDisconnectExFun(SOCKET&skTemp)

{

GUID GuidDisconnectEx = WSAID_DISCONNECTEX;

return LoadWSAFun(skTemp,GuidDisconnectEx

,(void*&)m_pfnDisconnectEx);

}

BOOL LoadGetAcceptExSockaddrsFun(SOCKET &skTemp)

{

GUID GuidGetAcceptExSockaddrs

= WSAID_GETACCEPTEXSOCKADDRS;

return LoadWSAFun(skTemp,GuidGetAcceptExSockaddrs

,(void*&)m_pfnGetAcceptExSockaddrs);

}

BOOL LoadTransmitFileFun(SOCKET&skTemp)

{

GUID GuidTransmitFile = WSAID_TRANSMITFILE;

return LoadWSAFun(skTemp,GuidTransmitFile

,(void*&)m_pfnTransmitfile);

}

BOOL LoadTransmitPacketsFun(SOCKET&skTemp)

{

GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;

return LoadWSAFun(skTemp,GuidTransmitPackets

,(void*&)m_pfnTransmitPackets);

}

BOOL LoadWSARecvMsgFun(SOCKET&skTemp)

{

GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;

return LoadWSAFun(skTemp,GuidTransmitPackets

,(void*&)m_pfnWSARecvMsg);

}

public:

BOOL LoadAllFun(SOCKET skTemp)

{//注意这个地方的调用顺序,是根据服务器的需要,并结合了表达式副作用

//而特意安排的调用顺序

return (LoadAcceptExFun(skTemp) &&

LoadGetAcceptExSockaddrsFun(skTemp) &&

LoadTransmitFileFun(skTemp) &&

LoadTransmitPacketsFun(skTemp) &&

LoadDisconnectExFun(skTemp) &&

LoadConnectExFun(skTemp) &&

LoadWSARecvMsgFun(skTemp));

}

public:

GRS_FORCEINLINE BOOL AcceptEx (

SOCKET sListenSocket,

SOCKET sAcceptSocket,

PVOID lpOutputBuffer,

DWORD dwReceiveDataLength,

DWORD dwLocalAddressLength,

DWORD dwRemoteAddressLength,

LPDWORD lpdwBytesReceived,

LPOVERLAPPED lpOverlapped

)

{

GRS_ASSERT(NULL != m_pfnAcceptEx);

return m_pfnAcceptEx(sListenSocket,

sAcceptSocket,lpOutputBuffer,

dwReceiveDataLength,dwLocalAddressLength,

dwRemoteAddressLength,lpdwBytesReceived,

lpOverlapped);

}

GRS_FORCEINLINE BOOL ConnectEx(

SOCKET s,const struct sockaddr FAR *name,

int namelen,PVOID lpSendBuffer,

DWORD dwSendDataLength,LPDWORD lpdwBytesSent,

LPOVERLAPPED lpOverlapped

)

{

GRS_ASSERT(NULL != m_pfnConnectEx);

return m_pfnConnectEx(

s,name,namelen,lpSendBuffer,

dwSendDataLength,lpdwBytesSent,

lpOverlapped

);

}

GRS_FORCEINLINE BOOL DisconnectEx(

SOCKET s,LPOVERLAPPED lpOverlapped,

DWORD  dwFlags,DWORD  dwReserved

)

{

GRS_ASSERT(NULL != m_pfnDisconnectEx);

return m_pfnDisconnectEx(s,

lpOverlapped,dwFlags,dwReserved);

}

GRS_FORCEINLINE VOID GetAcceptExSockaddrs (

PVOID lpOutputBuffer,

DWORD dwReceiveDataLength,

DWORD dwLocalAddressLength,

DWORD dwRemoteAddressLength,

sockaddr **LocalSockaddr,

LPINT LocalSockaddrLength,

sockaddr **RemoteSockaddr,

LPINT RemoteSockaddrLength

)

{

GRS_ASSERT(NULL != m_pfnGetAcceptExSockaddrs);

return m_pfnGetAcceptExSockaddrs(

lpOutputBuffer,dwReceiveDataLength,

dwLocalAddressLength,dwRemoteAddressLength,

LocalSockaddr,LocalSockaddrLength,

RemoteSockaddr,RemoteSockaddrLength

);

}

GRS_FORCEINLINE BOOL TransmitFile(

SOCKET hSocket,HANDLE hFile,

DWORD nNumberOfBytesToWrite,

DWORD nNumberOfBytesPerSend,

LPOVERLAPPED lpOverlapped,

LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,

DWORD dwReserved

)

{

GRS_ASSERT(NULL != m_pfnTransmitfile);

return m_pfnTransmitfile(

hSocket,hFile,nNumberOfBytesToWrite,

nNumberOfBytesPerSend,lpOverlapped,

lpTransmitBuffers,dwReserved

);

}

GRS_FORCEINLINE BOOL TransmitPackets(

SOCKET hSocket,

LPTRANSMIT_PACKETS_ELEMENT lpPacketArray,

DWORD nElementCount,DWORD nSendSize,

LPOVERLAPPED lpOverlapped,DWORD dwFlags

)

{

GRS_ASSERT(NULL != m_pfnTransmitPackets);

return m_pfnTransmitPackets(

hSocket,lpPacketArray,nElementCount,

nSendSize,lpOverlapped,dwFlags

);

}

GRS_FORCEINLINE INT WSARecvMsg(

SOCKET s,LPWSAMSG lpMsg,

LPDWORD lpdwNumberOfBytesRecvd,

LPWSAOVERLAPPED lpOverlapped,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

)

{

GRS_ASSERT(NULL != m_pfnWSARecvMsg);

return m_pfnWSARecvMsg(

s,lpMsg,lpdwNumberOfBytesRecvd,

lpOverlapped,lpCompletionRoutine

);

}

/*WSAID_ACCEPTEX

WSAID_CONNECTEX

WSAID_DISCONNECTEX

WSAID_GETACCEPTEXSOCKADDRS

WSAID_TRANSMITFILE

WSAID_TRANSMITPACKETS

WSAID_WSARECVMSG

WSAID_WSASENDMSG */

};

这个类的使用非常简单,只需要声明一个类的对象,然后调用其成员AcceptEx、DisconnectEx函数等即可,参数与这些函数的MSDN声明方式完全相同,除了本文中介绍的这些函数外,这个类还包含了很多其他的Winsock2函数,那么都应该按照这个类中演示的这样来动态载入后再行调用,如果无法载入通常说明你的环境中没有Winsock2函数库,或者是你初始化的不是2.0版的Winsock环境。

这个类是本人完整类库的一部分,如要使用需要自行修改一些地方,如果不知如何修改或遇到什么问题,可以直接跟帖说明,我会不定期回答大家的问题,这个类可以免费使用、分发、修改,可以用于任何商业目的,但是对于使用后引起的任何问题,本人概不负责,有问题请跟帖。关于AcceptEx以及其他一些函数,包括本文中没有介绍到得函数,我会在后续的一些专题文章中进行详细深入的介绍,敬请期待。如果你有什么疑问,或者想要了解什么也请跟帖说明,我会在后面的文章中尽量说明。

WinSock2编程之打造完整的SOCKET池相关推荐

  1. Python编程:打造太空入侵者游戏(含完整源码)

    Python编程:打造太空入侵者游戏(含完整源码) 太空入侵者这款游戏经典得让人不能忘怀,而Python语言的简单易学.快速开发的特性,使其成为实现该游戏的最好选择.在本篇文章中,我们将为读者呈现如何 ...

  2. 看完之后保证你对socket编程步骤胸有成竹。 C++ Socket网络编程基础详解(TCP)

    C++ Socket网络编程基础详解(TCP版) ​    网络编程,就是编写程序使得两台计算机交换数据,其实从本质上来讲,网络编程最终所实现的功能,和我们文件的输入输出很相似,只是文件输入输出的对象 ...

  3. WinSock Socket 池

    之前在WinSock2.0 API 中说到,像DisConnectEx 函数这样,它具有回收SOCKET的功能,而像AcceptEx这样的函数,它不会自己在内部创建新的SOCKET,需要外部传入SOC ...

  4. linux多网卡网络编程,Linux网络编程之Socket初探

    Socket由来 Socket 的英文原意就是"孔"或"插座",现在,作为 BSD UNIX 的进程通讯机制,取其后一种意义.一起看下网络编程里说的socket ...

  5. 网络编程套接字(Socket编程)

    Socket:Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元. Socket API 分类: 1.UDP数据报套接字编程: DatagramSock ...

  6. Linux网络编程 | Socket编程(一):Socket的介绍、UDPSocket的封装、UDP服务器/客户端的实现

    目录 套接字编程 Sockaddr结构 字节序 地址转换 常用套接字接口 UDP的通信流程 UDPSocket的封装 UDP服务器 UDP客户端 套接字编程 所谓套接字(Socket),就是对网络中不 ...

  7. 3Dmax场景小房子—打造完整还原游戏场景

    3Dmax场景小房子-打造完整还原游戏场景 3Dmax场景小房子-打造完整还原游戏场景 由于上传限制,本教程分集播放,若网站播放不完整,可以加裙领取完整录播和新手学习资料. 3Dmax场景小房子-打造 ...

  8. 刷脸支付会员积分卡券打造完整商业闭环

    刷脸支付成为新的支付趋势的原因:缓解对外部媒介的过渡依赖:移动支付的过程需要手机,而很多消费者会遇到手机没电或者忘记携带手机的情况,而刷脸支付不需要手机,仅通过人脸识别就可以完成付款.随着支付宝.微信 ...

  9. SAP ABAP 对话框编程教程:中的模块池-09

    SAP ABAP 对话框编程教程:中的模块池-09 SAP-ABAP 支持两种类型的程序 – 报告程序和对话程序. 如果您的 ABAP 程序需要用户输入 ,则使用对话框编程. 在本教程中,您将学习: ...

最新文章

  1. VALSE 2020来了!60位计算机视觉、模式识别领域的优秀青年学者齐聚一堂
  2. 数据结构乐智教学百度云_网易云课堂 - 我的职业课堂
  3. matplotlib.pyplot中add_subplot方法参数111的含义
  4. 22-高级特性之内建方法(3)
  5. Spring-AOP 切点/切面类型和创建切面
  6. 谷歌的网页排序算法(PageRank Algorithm)
  7. 【NLP】Transformer及其变种
  8. Tomcat6 Spring3 问题:严重: Error configuring application listener of class org.springframework.web.conte
  9. 在google play开放平台上closed texting如何删除_“爷青回”!如何抢先体验《英雄联盟》手游?这份攻略送给你...
  10. XTU 1252 Defense Tower
  11. [2010-8-22]
  12. Microsoft Endpoint Protection for Windows Azure客户技术预览版可供免费下载
  13. 21世纪八大新兴技术领域对美国战略威慑的影响
  14. Ubuntu18.04 上 安装微信(Deepin-Wechat)
  15. LXDE vs XFCE:哪个是更好的轻量级桌面环境?
  16. docker 镜像压缩 docker-squash
  17. python3 excel 图表导出图片_使用python代码将excel中的图表导出为图片
  18. Android万能遥控菜单选择添加,将小米米家万能遥控器添加到Home Assistant
  19. java:获取后缀为doc、docx、xls、xlsx、ppt、pptx、pdf、xml的文件中的文本
  20. 饱受非议的上市手段,不被认可的量子初创公司是如何登陆纳斯达克的?

热门文章

  1. 常用的函数式接口_Supplier接口
  2. Shiro结合redis的统一会话管理:自定义会话管理器
  3. ES6新特性之解构表达式
  4. 数据库-优化-从慢查询日志中分析索引使用情况及pt-find
  5. Servlet_快速入门
  6. 字典-字典的统计、合并、清空操作
  7. Spring Session官方介绍及spring框架学习方法
  8. python log日志_python脚本攻略之log日志
  9. 在 Java 中,为什么需要创建内部类对象之前需要先创建外部类对象
  10. 【SpringBoot零基础案例03】【IEDA 2021.1】SpringBoot框架核心配置文件application.properties的使用