Server - IOCP自用文档及例子

一、IO完成端口概念

1.1 什么是完成端口?
    I/O完成端口提供了一种有效的线程模型,用于在多处理器系统上处理多个异步I / O请求

1.2 完成端口是如何工作的?
    该CreateIoCompletionPort函数创建一个I / O完成端口和同事一个或与该端口更多的文件句柄。
    当对这些文件句柄之一的异步I / O操作完成时,I / O完成数据包将按照先进先出(FIFO)
    的顺序排队到关联的I/O完成端口。这种机制的一种强大用途是将多个文件句柄的同步点组合到单个对象中,
    尽管还有其他有用的应用程序。请注意,虽然数据包以FIFO顺序排队,但它们可能以不同的顺序出队。

    当文件句柄与完成端口关联时,传入的状态块将不会更新,直到将数据包从完成端口中删除为止。
    唯一的例外是原始操作是否同步返回错误。线程(由主线程创建或由主线程本身创建)
    使用GetQueuedCompletionStatus功能是等待将完成数据包排队到I/O完成端口,而不是直接等待异步I/O完成。
    阻塞在I / O完成端口上执行的线程以后进先出(LIFO)顺序释放,并且下一个完成包从该线程的I/O完成端口
    的FIFO队列中拉出。这意味着,当完成包被释放到线程时,系统释放与该端口关联的最后一个(最近)线程,
    并向其传递最旧的I / O完成的完成信息。

    虽然任何数量的线程可以调用GetQueuedCompletionStatus时为指定的I / O完成端口,
    当指定的线程调用GetQueuedCompletionStatus时在第一时间,就成了具有指定I / O完成端口,
    直到三件事情之一发生有关:线程退出,指定不同的I / O完成端口,或关闭I / O完成端口。
    换句话说,一个线程最多可以与一个I / O完成端口关联。

    当完成数据包排队到I / O完成端口时,系统首先检查与该端口关联的线程数。
    如果正在运行的线程数小于并发值(在下一节中讨论),则允许一个等待线程(最新线程)处理完成包。
    当正在运行的线程完成其处理时,通常会再次调用GetQueuedCompletionStatus,
    此时它要么返回下一个完成包,要么等待队列为空。

    线程可以使用PostQueuedCompletionStatus函数将完成包放入I / O完成端口的队列中。
    这样,除了从I / O系统接收I / O完成数据包之外,完成端口还可用于从该进程的其他线程接收通信。
    该PostQueuedCompletionStatus功能允许应用程序来排队了自己的专用完成包的I / O完成端口,
    而无需启动一个异步I / O操作。例如,这对于通知工作线程外部事件很有用。

    I / O完成端口句柄以及与该特定I / O完成端口关联的每个文件句柄都称为对I / O完成端口的引用。
    没有更多引用时,将释放I / O完成端口。
    因此,必须正确关闭所有这些句柄以释放I / O完成端口及其相关的系统资源。满足这些条件后,应
    用程序应通过调用CloseHandle函数来关闭I / O完成端口句柄。


二、基础函数

函数目录:
2.1 CreateIoCompletionPort函数
2.2 GetQueuedCompletionStatus函数
2.3 PostQueuedCompletionStatus函数
2.4 AcceptEx 向IOCP投递接收链接任务

函数正文:
2.1 CreateIoCompletionPort函数

     基本语法:
        HANDLE WINAPI CreateIoCompletionPort(
          _In_     HANDLE    FileHandle,
          _In_opt_ HANDLE    ExistingCompletionPort,
          _In_     ULONG_PTR CompletionKey,
          _In_     DWORD     NumberOfConcurrentThreads
        );
    此函数的两种作用:
    (1)创建输入/输出(I / O)完成端口并将其与指定的文件句柄相关联;
        使用第一个功能时候,前三个参数固定传入INVALID_HANDLE_VALUE、NULL、0、最后一个参数传入IOCP
        允许并发线程数量,值为零则默认为CPU的核心数量
    (2)创建尚未与文件句柄相关联的I / O完成端口,从而允许以后进行关联。
        使用第二个参数时候,第一个参数传入设备句柄(包含文件、socket等)、第二个参数传入IOCP句柄、
        第三个参数传入完成键值,第四个参数传入0,只在第一个功能中有作用。

        注意:将打开句柄的实例与I / O完成端口关联后,
        就不能在ReadFileEx或WriteFileEx函数中使用它,
        因为这些函数具有自己的异步I / O机制。

    函数返回值:
    如果函数成功,则返回值是I/O完成端口的句柄:
    如果ExistingCompletionPort参数为NULL,则返回值为新句柄。
    如果ExistingCompletionPort参数是有效的I / O完成端口句柄,则返回值就是该句柄。
    如果FileHandle参数是有效的句柄,则该文件句柄现在与返回的I / O完成端口关联。
    如果函数失败,则返回值为NULL。若要获取扩展的错误信息,请调用GetLastError函数。

2.2 GetQueuedCompletionStatus函数

    基本语法:
    BOOL GetQueuedCompletionStatus(
      HANDLE       CompletionPort,
      LPDWORD      lpNumberOfBytesTransferred,
      PULONG_PTR   lpCompletionKey,
      LPOVERLAPPED *lpOverlapped,
      DWORD        dwMilliseconds
    );

    参量
    CompletionPort:
    完成端口的句柄。要创建完成端口,请使用 CreateIoCompletionPort函数。

    lpNumberOfBytesTransferred:
    指向变量的指针,该变量接收在完成的I/O操作中传输的字节数。

    lpCompletionKey
    指向变量的指针,该变量接收与I / O操作已完成的文件句柄关联的完成键值。
    完成密钥是在对CreateIoCompletionPort的调用中指定的每个文件的密钥 。

    lpOverlapped:
    指向变量的指针,该变量接收启动完整的I / O操作时指定的OVERLAPPED结构的地址。
    即使您已将该函数传递了与完成端口和有效的OVERLAPPED结构相关联的文件句柄 ,
    应用程序仍可以阻止完成端口通知。这是通过为OVERLAPPED结构的hEvent成员指
    定有效的事件句柄并设置其低位来完成的。设置了低位的有效事件句柄可防止I / O
    完成被排队到完成端口。

    dwMilliseconds:
    呼叫者愿意等待完成数据包出现在完成端口的毫秒数。如果完成包在指定时间内没有出现,则该函数超时,返回FALSE,
    并将* lpOverlapped设置为NULL。如果dwMilliseconds为INFINITE,则该函数将永不超时。
    如果dwMilliseconds为零,并且没有要出队的I / O操作,则该函数将立即超时。
   
   函数返回值:
   如果成功,则返回非零(TRUE),否则返回零(FALSE)。
   要获取扩展的错误信息,请调用 GetLastError。


2.3 PostQueuedCompletionStatus函数、将I / O完成包发送到I / O完成端口。
    
    基本语法:
    BOOL WINAPI PostQueuedCompletionStatus(
      _In_     HANDLE       CompletionPort,
      _In_     DWORD        dwNumberOfBytesTransferred,
      _In_     ULONG_PTR    dwCompletionKey,
      _In_opt_ LPOVERLAPPED lpOverlapped
    );
    
    函数是否成功返回:
    如果函数成功,则返回值为非零。
    如果函数失败,则返回值为零。
    要获取扩展的错误信息,请调用GetLastError。

2.4 AcceptEx 向IOCP投递接收链接任务
    基本语法:
    BOOL AcceptEx(
      SOCKET       sListenSocket,            //监听socket
      SOCKET       sAcceptSocket,            //链接进来的socket
      PVOID        lpOutputBuffer,           
      DWORD        dwReceiveDataLength,
      DWORD        dwLocalAddressLength,
      DWORD        dwRemoteAddressLength,
      LPDWORD      lpdwBytesReceived,
      LPOVERLAPPED lpOverlapped
    );

    lpOutputBuffer:(发送缓存,指向缓存区的指针)
    指向缓冲区的指针,该缓冲区接收在新连接上发送的第一数据块,服务器的本地地址和客户端的远程地址
    接收数据从偏移量零开始写入缓冲区的第一部分,而地址写入缓冲区的后半部分。必须指定此参数。

    dwReceiveDataLength(接收缓存的长度)
    lpOutputBuffer中将用于缓冲区开头的实际接收数据的字节数。
    此大小不应包括服务器的本地地址的大小,也不应包括客户端的远程地址的大小;
    它们被附加到输出缓冲区。如果dwReceiveDataLength为零,则接受连接将不会导致接收操作。
    相反, AcceptEx在连接到达后立即完成,而无需等待任何数据。

    dwLocalAddressLength
    为本地地址信息保留的字节数。该值必须比使用的传输协议的最大地址长度至少多16个字节。

    dwRemoteAddressLength
    为远程地址信息保留的字节数。
    该值必须比使用的传输协议的最大地址长度至少多16个字节。不能为零。

    lpdwBytesReceived
    指向接收到的字节数的DWORD的指针。
    仅当操作同步完成时才设置此参数。
    如果返回ERROR_IO_PENDING并在以后完成,则永远不会设置此DWORD,
    并且您必须获取从完成通知机制读取的字节数。

    lpOverlapped
    用于处理请求的 OVERLAPPED结构。必须指定该参数。它不能为NULL。

    返回值:错误返回值false


2.5 WSARecv函数 WSARecv功能从连接的插座或结合的连接的套接字接收数据。
    基本语法:
    int WSAAPI WSARecv(
      SOCKET                             s,
      LPWSABUF                           lpBuffers,
      DWORD                              dwBufferCount,
      LPDWORD                            lpNumberOfBytesRecvd,
      LPDWORD                            lpFlags,
      LPWSAOVERLAPPED                    lpOverlapped,
      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );

    s
    标识已连接套接字的描述符。

    lpBuffers
    指向WSABUF结构数组的指针 。每个 WSABUF结构都包含一个指向缓冲区的指针以及缓冲区的长度(以字节为单位)。

    dwBufferCount
    lpBuffers数组中WSABUF结构的数量 。

    lpNumberOfBytesRecvd
    如果接收操作立即完成,则指向此调用接收的数据数的指针(以字节为单位)。

    使用NULL这个参数,如果lpOverlapped的参数不是NULL,以避免潜在错误的结果。
    此参数可以是NULL只有当lpOverlapped的参数不是NULL。

    lpFlags
    指向用于修改WSARecv函数调用行为的标志的指针 。

    lpOverlapped
    指向WSAOVERLAPPED结构的指针 (对于非重叠套接字将被忽略)。

    lpCompletionRoutine
    指向完成例程的指针,该例程在接收操作完成时被调用(非重叠套接字忽略)。

    返回值
    如果没有错误发生并且接收操作立即完成,则 WSARecv返回零。在这种情况下,
    一旦调用线程处于警报状态,就已经计划完成例程的调用。
    否则,将返回SOCKET_ERROR的值,并且可以通过调用WSAGetLastError来检索特定的错误代码 。
    错误代码 WSA_IO_PENDING表示重叠操作已成功启动,并且稍后将指示完成。
    任何其他错误代码表示重叠操作未成功启动,并且不会出现完成指示。

2.6 WSASend函数 :WSASend函数发送上所连接的插座的数据。
    基本语法:
    int WSAAPI WSASend(
      SOCKET                             s,
      LPWSABUF                           lpBuffers,
      DWORD                              dwBufferCount,
      LPDWORD                            lpNumberOfBytesSent,
      DWORD                              dwFlags,
      LPWSAOVERLAPPED                    lpOverlapped,
      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );

    参量
    s
    标识已连接套接字的描述符。

    lpBuffers
    指向WSABUF结构数组的指针 。每个 WSABUF结构都包含一个指向缓冲区的指针以及缓冲区的长度(以字节为单位)。
    对于Winsock应用程序,一旦 调用WSASend函数,系统将拥有这些缓冲区,并且应用程序可能无法访问它们。
    该数组必须在发送操作期间保持有效。

    dwBufferCount
    lpBuffers数组中WSABUF结构的数量 。

    lpNumberOfBytesSent
    如果I / O操作立即完成,则指向此调用发送的数字的指针(以字节为单位)。
    使用NULL这个参数,如果lpOverlapped的参数不是NULL,以避免潜在错误的结果。
    此参数可以是NULL只有当lpOverlapped的参数不是NULL。

    dwFlags
    用于修改WSASend函数调用行为的标志 。有关更多信息,请参见“备注”部分中的“ 使用dwFlags ”。

    lpOverlapped
    指向WSAOVERLAPPED结构的指针 。对于非重叠套接字,将忽略此参数。

    lpCompletionRoutine
    发送操作完成时调用的指向完成例程的指针。对于非重叠套接字,将忽略此参数。

    返回值
    如果没有错误发生并且发送操作立即完成,则 WSASend返回零。
    在这种情况下,一旦调用线程处于警报状态,就已经计划完成例程的调用
    否则,将返回SOCKET_ERROR的值,并且可以通过调用WSAGetLastError来检索特定的错误代码 。
    错误代码 WSA_IO_PENDING表示重叠操作已成功启动,并且稍后将指示完成。
    任何其他错误代码表示重叠操作未成功启动,并且不会出现完成指示。

三、重叠结构 
包含用于异步(或重叠)输入和输出(I / O)的信息。
    基础结构:
    typedef struct _OVERLAPPED {
      ULONG_PTR Internal;
      ULONG_PTR InternalHigh;
      union {
        struct {
          DWORD Offset;
          DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
      } DUMMYUNIONNAME;
      HANDLE    hEvent;
    } OVERLAPPED, *LPOVERLAPPED;


四、预加载Accept动态库
将Accept动态库添加至内存中进行调用

使用WSAIoctl将AcceptEx函数加载到内存中,WSAIoctl函数是ioctlsocket()的扩展
可以使用重叠I/O的函数。函数的第3到第6个参数是输入和输出缓冲区
我们将指针传递给AcceptEx函数。这是使用这样我们就可以直接调用AcceptEx函数了 参考Mswsock。lib库。
  客户端进行连接优化
  LPFN_ACCEPTEX lpfnAcceptEx = NULL;
  GUID GuidAcceptEx = WSAID_ACCEPTEX;
  
  iResult = WSAIoctl(ListenSocket, SIO_GET_EXTENSION_FUNCTION_POINTER,
             &GuidAcceptEx, sizeof (GuidAcceptEx), 
             &lpfnAcceptEx, sizeof (lpfnAcceptEx), 
             &dwBytes, NULL, NULL);
    if (iResult == SOCKET_ERROR) {
        wprintf(L"WSAIoctl failed with error: %u\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

五、示例内容

msdn上提供更为详细简洁的内容,此仅仅作为个人的参考

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS


#include <Windows.h>
#include <WinSock2.h>
#include <mswsock.h>/*AccptEx需要头文件*/
#pragma comment(lib,"ws2_32.lib")
//#pragma comment(lib,"mswsock.lib")
#include<stdio.h>

LPFN_ACCEPTEX lpfnAcceptEx = NULL;

void loadAccetEx(SOCKET listenSocket)
{
	GUID GuidAcceptEx = WSAID_ACCEPTEX;
	DWORD dwBytes=0;
	int iResult = WSAIoctl(listenSocket, SIO_GET_EXTENSION_FUNCTION_POINTER,
		&GuidAcceptEx, sizeof(GuidAcceptEx),
		&lpfnAcceptEx, sizeof(lpfnAcceptEx),
		&dwBytes, NULL, NULL);
	if (iResult == SOCKET_ERROR) {
		printf("WSAIoctl failed with error: %u\n", WSAGetLastError());
		return;
	}
}


#define  DATA_BUFFER_SIZE  1024
#define  CLIENT_NUMS 10

enum IO_DATA_TYPE{IODT_ACCPECT=10,IODT_RECV, IODT_SEND};

struct IO_DATA_BASE
{
	//重叠结构
	OVERLAPPED overlapped;
	//socket
	SOCKET sockfd;
	//数据缓冲区
	char buf[DATA_BUFFER_SIZE];
	//实际缓存数据长度
	int length;
	//
	IO_DATA_TYPE iotype;
};



bool postAccept(SOCKET serverSock, IO_DATA_BASE* pIO_Data)
{
	pIO_Data->iotype = IODT_ACCPECT;
	pIO_Data->sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	


	/*微软msdn
	acceptEx示例 地址:https://docs.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-acceptex#requirements
	*/

	/*投递一个接收请求*/
	bool acceptRetBool = lpfnAcceptEx(serverSock, pIO_Data->sockfd, pIO_Data->buf, 0, sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, NULL, &pIO_Data->overlapped);

	if (acceptRetBool == false)
	{
		int ret = GetLastError();
		if (ret != ERROR_IO_PENDING)
		{
			printf("error,AcceptEx  failed with error code<%d>\n", GetLastError());
			return false;

		}
	}
	else
	{
		printf("success,AcceptEx success...\n");
		return true;
	}
	return false;
}

//投递接收数据链接
bool postRecv(IO_DATA_BASE* pIO_Data)
{
	pIO_Data->iotype = IODT_RECV;
	
	WSABUF wsBuff = { };
	wsBuff.buf = pIO_Data->buf;
	wsBuff.len = DATA_BUFFER_SIZE;
	DWORD flags = 0;

	int ret = WSARecv(pIO_Data->sockfd,&wsBuff,1,NULL, &flags, &pIO_Data->overlapped,NULL);

	/*
	  如果没有错误发生并且接收操作立即完成,则 WSARecv返回零。
	否则,将返回SOCKET_ERROR的值,并且可以通过调用WSAGetLastError来检索特定的错误代码 。
	错误代码 WSA_IO_PENDING表示重叠操作已成功启动,并且稍后将指示完成。
	任何其他错误代码表示重叠操作未成功启动,并且不会出现完成指示。
	*/
	if (ret == 0)
		return true;

	if (ret == SOCKET_ERROR)
		if (WSA_IO_PENDING == WSAGetLastError())
			return true;

	return false;

}
//投送发送数据
bool postSend(IO_DATA_BASE* pIO_Data)
{
	pIO_Data->iotype = IODT_SEND;

	WSABUF wsBuff = { };
	wsBuff.buf = pIO_Data->buf;
	wsBuff.len = pIO_Data->length;
	DWORD flags = 0;

	int ret = WSASend(pIO_Data->sockfd, &wsBuff, 1, NULL, flags, &pIO_Data->overlapped, NULL);

	/*
	  如果没有错误发生并且接收操作立即完成,则 WSARecv返回零。
	否则,将返回SOCKET_ERROR的值,并且可以通过调用WSAGetLastError来检索特定的错误代码 。
	错误代码 WSA_IO_PENDING表示重叠操作已成功启动,并且稍后将指示完成。
	任何其他错误代码表示重叠操作未成功启动,并且不会出现完成指示。
	*/
	if (ret == 0)
		return true;

	if (ret == SOCKET_ERROR)
		if (WSA_IO_PENDING == WSAGetLastError())
			return true;

	return false;

}

// -- 用socket api建立简易的tcp服务端
// -- IOCP sever基础流程
int main(int argc, char** argv)
{
	//启动Windows socket 2.x环境
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);

	//建立socket 会默认支持完成端口 WSA_FLAG_OVERLAPPED
	/*
	SOCKET sockServer = WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,
		WSA_FLAG_OVERLAPPED);
	*/

	SOCKET 	sockServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	//设置对外端口及ip信息
	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);
	_sin.sin_addr.S_un.S_addr = INADDR_ANY;

	//绑定端口
	if (SOCKET_ERROR == bind(sockServer, (sockaddr*)&_sin, sizeof(_sin)))
		printf("error, bind port failed...\n");
	else
		printf("success,bind port success...\n");


	//监听端口
	if (SOCKET_ERROR == listen(sockServer, 64))
		printf("error,listen port failed....\n");
	else
		printf("success,listen port success....\n");


	/* ------------IOCP Begin---------------------- */

	//4 创建完成端口IOCP
	// MSDN man
	HANDLE createIoCompleteHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
	/*创建完成端口失败返回值空*/
	if (createIoCompleteHandle == NULL)
		printf("error,CreateIoCompletionPort create port failed....\n");
	else
		printf("success,CreateIoCompletionPort create port success....\n");

	//关联完成端口 ULONG_PTR 及 DWORD 在32位的指针是4字节 在64位下的指针是8字节的
	//如果第三个参数使用DOWRD强转将导致指针在64位下的操作系统不兼容
	HANDLE bindIoComleteHandle = CreateIoCompletionPort((HANDLE)sockServer, createIoCompleteHandle, (ULONG_PTR)sockServer, 0);
	/*
	socket绑定完成端口 失败时返回为NULL 成功时候返回为传入的完成端口值
	也就是说 createIoCompleteHandle==bindIoComleteHandle
	*/
	if (bindIoComleteHandle == NULL)
		printf("error,CreateIoCompletionPort bind port failed....\n");
	else
		printf("success,CreateIoCompletionPort bind port success....\n");

	//加载函数acceptEx
	loadAccetEx(sockServer);



	//向IOCP投递接收链接任务 什么叫做池:预先创建的一些将会使用的内存
	//要想节约性能 提前创建一些对象 如线程池 对象池 内存池 提升效率
	IO_DATA_BASE ioData[CLIENT_NUMS] = { };

	for (int n = 0; n < CLIENT_NUMS; n++)
	{
		postAccept(sockServer, &ioData[n]);
	}
	
	

	int msgSendCount = 0;
	int msgRecvCount = 0;
	/*循环检测iocp中的io状态*/

	while (true)
	{
		DWORD bytesTrans = 0;
		SOCKET sock = INVALID_SOCKET;
		LPOVERLAPPED lpOverlAppend = NULL;

		IO_DATA_BASE* pIoData;

		bool getQueuedRet = GetQueuedCompletionStatus(createIoCompleteHandle,&bytesTrans, (PULONG_PTR)&sock, (LPOVERLAPPED*)&pIoData, 1000/*超时时间*/);
		if (getQueuedRet == false)
		{
			int retGetLastError = GetLastError();
			if (WAIT_TIMEOUT == retGetLastError)
			{
				continue;
			}
			else
			{
				printf("error,GetQueuedCompletionStatus error with error code <%d>...\n", retGetLastError);
			}
		}

		switch (pIoData->iotype)
		{
		case IODT_ACCPECT:
		{
			printf("GetQueuedCompletionStatus pIoData->iotype=IODT_ACCPECT new client join<socket=%d>....\n", pIoData->sockfd);
			HANDLE ret = CreateIoCompletionPort((HANDLE)pIoData->sockfd, createIoCompleteHandle, (ULONG_PTR)pIoData->sockfd, 0);

			if (!ret)
			{
				printf("error,bind IoCompletionPort failed....\n");
				closesocket(pIoData->sockfd);
				continue;
			}
			for(int i=0;i<10;i++)
				postRecv(pIoData);
		}
		break;
		case IODT_RECV:
			if (bytesTrans <= 0)
			{
				printf("error,recv msg failed client disconnect....\n");
				closesocket(pIoData->sockfd);
				continue;
			}
			pIoData->length = bytesTrans;
			printf("recv client<socket=%d> msglegth<%d> msgCount<%d>\n", pIoData->sockfd, bytesTrans,++msgSendCount);
			//postRecv(pIoData);
			postSend(pIoData);
			break;
		case IODT_SEND:
			if (bytesTrans <= 0)
			{
				printf("error,send msg failed client disconnect....\n");
				closesocket(pIoData->sockfd);
				continue;
			}
			printf("send client<socket=%d> msglegth<%d> msgCount<%d>\n", pIoData->sockfd, bytesTrans, ++msgRecvCount);
			break;
		default:
			printf("error,msg is not be defied....\n");
			break;
		}
	}

	/*清理环境*/
	//关闭服务端socket
	closesocket(sockServer);
	CloseHandle(createIoCompleteHandle);

	//清除windows socket 2.x环境
	WSACleanup();


	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫忘输赢

莫忘输赢 - 收钱袋

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值