首页
论坛
课程
招聘
[原创]网络编程技术学习笔记之I/O模型
2021-11-23 11:39 18884

[原创]网络编程技术学习笔记之I/O模型

2021-11-23 11:39
18884

一.前言

在文章网络编程技术学习笔记之基础模型中,当服务端收到新的客户端的连接的时候都通过开起一个新线程的方式来完成与客户端的连接。但是,开起一个新的线程所耗费的资源是比较大的。因此,需要考虑其他的方法来让服务端可以只用一个线程就完成与所有客户端的通信。

二.I/O复用

实现I/O复用的目的就是想要改变如图用多进程或者多线程来处理客户端连接的服务端。

所以引入了I/O复用模型,它将改变服务端与客户端的连接方式变为如下的单线程的连接方式。

而实现I/O复用的核心是使用select函数,该函数可以监视一组套接字的状态变化,定义如下:

int
WSAAPI
select(
    __in int nfds,
    __inout_opt fd_set FAR * readfds,
    __inout_opt fd_set FAR * writefds,
    __inout_opt fd_set FAR * exceptfds,
    __in_opt const struct timeval FAR * timeout
    );
参数含义
nfds忽略。包含nfds参数只是为了与Berkeley套接字兼容。
readfds将所有关注“是否存在待读取数据”的套接字注册到fd_set型变量中,并传递其地址值
writefds将所有关注“是否可传输无阻塞数据”的套接字注册到fd_set型变量中,并传递其地址值
exceptfds将所有关注“是否发生异常”的套接字注册到fd_set型变量中,并传递其值
timeout调用select函数后,为了防止陷入无线阻塞,传递超时(time-out)信息

返回值:发生错误时返回SOCKET_ERROR,超时返回0。因关注的事件返回时,返回值大于0,该值是发生事件的套接字

由此可知,select函数的作用是可以用来监视多个套接字(文件描述符),监视的事件如下:

  • 是否存在套接字接收数据

  • 无需阻塞传输数据的套接字有哪些

  • 哪些套接字发生了异常

另外,select函数所监视的变量是fd_set结构体变量,该结构体的定义如下:

#ifndef FD_SETSIZE
#define FD_SETSIZE      64
#endif /* FD_SETSIZE */

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;
成员含义
fd_count要监视的套接字的数量
fd_array用来保存套接字的数组

由此可知,要监视的套接字需要加入到该结构体中才可以调用select函数进行监视。对于fd_set结构体的操作,Winsock库中提供了一组宏来完成,

作用
FD_ZERO(fd_set *set)将set变量初始化为0
FD_SET(int fd, fd_set *fdset)将set指向的变量中注册套接字fd的信息,也就是将套接字放入监视范围
FD_CLD(int fd, fd_set *fdset)从参数fdset指向的变量中清除文件描述符fd的信息,也就是将套接字从监视范围里删除
FD_ISSET(int fd, fd_set *fdset)若参数fdset指向的变量中包含文件描述符fd的信息,则返回"真",也就是判断相应的套接字状态是否发生了改变

根据这组宏中的FD_ZERO和FD_SET就可以完成对结构体fd_set变量的初始化,而这个初始化的目的就是将服务器套接字加入到fd_set变量中,这样才可以通过调用select函数来监视套接字的变化。

另外为了防止长时间进入阻塞状态,也需要对select函数的timeout进行初始化,也就是指定超时时间,这个参数是一个timeval结构体变量,该结构体的定义如下:

struct timeval {
        long    tv_sec;         /* seconds */
        long    tv_usec;        /* and microseconds */
};

所以对于select函数的调用就如下图所示:

这里还需要知道的几点是:

  1. 连接服务端和与服务端断开连接的变化都在select函数的第二个参数,也就是readfds中

  2. 监视到readfds中变化的套接字是服务器套接字的时候,说明客户端发起了对服务端的连接,此时应该要把客户端的套接字加入监视范围

  3. 当监视到readfds中变化的是客户端套接字,且接收数据的长度为0的时候,说明发生了与客户端的断开连接,需要将相应的客户端套接字从监视范围内删除,否则就是接收到了数据

完整的代码如下:

服务端:

// Server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <cstdio>
#include <WinSock2.h>
#include <Windows.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 30

VOID ShowError(PCHAR msg);

int main()
{
	WSADATA wsaData;
	SOCKET hServerSocket = INVALID_SOCKET, hClientSocket = INVALID_SOCKET;
	SOCKADDR_IN servAddr, clientAddr;
	int iClientAddrSize = 0, fdNum = 0, i = 0, strLen = 0;
	fd_set reads, cpyReads;
	TIMEVAL timeout;
	CHAR szBuf[BUF_SIZE] = { 0 };

	// 初始化Winsock库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ShowError("WSAStartup");
		goto fail;
	}

	// 创建套接字
	hServerSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (hServerSocket == INVALID_SOCKET)
	{
		ShowError("socket");
		goto exit;
	}

	// 绑定端口
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(1900);
	if (bind(hServerSocket, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ShowError("bind");
		goto exit;
	}

	// 监听端口
	if (listen(hServerSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		ShowError("listen");
		goto exit;
	}

	printf("服务端已开启...\n");

	// 设置文件描述符
	FD_ZERO(&reads);
	// 指定监视范围
	FD_SET(hServerSocket, &reads);

	while (TRUE)
	{
		cpyReads = reads;
		// 设置超时
		timeout.tv_sec = 5;
		timeout.tv_usec = 5000;

		fdNum = select(0, &cpyReads, 0, 0, &timeout);
		if (fdNum == SOCKET_ERROR)
		{
			ShowError("select");
			goto exit;
		}


		// 超时,结束本次循环
		if (fdNum == 0) continue;

		for (i = 0; i < reads.fd_count; i++)
		{
			// 该套接字是否有变化
			if (FD_ISSET(reads.fd_array[i], &cpyReads))
			{
				// 如果改变状态的是hServerSocket说明有新的连接
				if (reads.fd_array[i] == hServerSocket)
				{
					// 建立连接
					iClientAddrSize = sizeof(clientAddr);
					hClientSocket = accept(hServerSocket, 
						                   (SOCKADDR *)&clientAddr, 
						                   &iClientAddrSize);
					if (hClientSocket == INVALID_SOCKET)
					{
						ShowError("accept");
						goto exit;
					}
					printf("新的连接,IP:%s\n", inet_ntoa(clientAddr.sin_addr));
					// 在reads中设置新的套接字,以监听它的状态
					FD_SET(hClientSocket, &reads);
				}
				else
				{
					strLen = recv(reads.fd_array[i], szBuf, BUF_SIZE, 0);
					if (strLen == SOCKET_ERROR)
					{
						ShowError("recv");
						goto exit;
					}
					else if (strLen == 0)
					{
						// 如果接收的数据的长度为0,说明是断开连接
						// 从reads中清除对应的套接字
						FD_CLR(reads.fd_array[i], &reads);
						closesocket(cpyReads.fd_array[i]);
						printf("close client: %d\n", cpyReads.fd_array[i]);
					}
					else
					{
						printf("receive message: %s\n", szBuf);
					}
				}
			}
		}
	}

exit:
	if (hServerSocket != INVALID_SOCKET)
	{
		closesocket(hServerSocket);
		hServerSocket = INVALID_SOCKET;
	}
	WSACleanup();
fail:
	system("pause");
	return 0;
}

VOID ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, WSAGetLastError());
}

客户端:

// Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <cstdio>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 30

VOID ShowError(PCHAR msg);

int main()
{
	WSADATA wsaData;
	SOCKET hSocket = INVALID_SOCKET;
	SOCKADDR_IN servAddr;

	// 初始化Winsock库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ShowError("WSAStartup");
		goto fail;
	}

	// 创建套接字
	hSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (hSocket == INVALID_SOCKET)
	{
		ShowError("socket");
		goto exit;
	}

	// 连接服务端
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	servAddr.sin_port = htons(1900);
	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ShowError("connect");
		goto exit;
	}

	printf("连接成功\n");
	Sleep(5000);

	CHAR szBuf[BUF_SIZE] = { "Hello 1900" };

	if (send(hSocket, szBuf, sizeof(szBuf) - 1, 0) == SOCKET_ERROR)
	{
		ShowError("send");
		goto exit;
	}
	printf("发送数据成功\n");
exit:
	if (hSocket != INVALID_SOCKET)
	{
		closesocket(hSocket);
		hSocket = INVALID_SOCKET;
	}
	WSACleanup();
fail:
	system("pause");
	return 0;
}

VOID ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, WSAGetLastError());
}

运行结果如下,可以看到此时只用了一个线程就完成了与多个客户端的连接和通信。

三.重叠I/O

重叠I/O的模型如下所示:

由图可知,重叠I/O是同一线程内部向多个目标传输(或从多个目标接收)数据引起I/O重叠现象。为了完成这项任务,调用的I/O函数应立即返回,只有这样才能发送后续的数据,为了完成这项功能,调用的I/O函数应以非阻塞模型工作。

首先要使用WSASocket函数来创建适用于重叠I/O套接字

SOCKET WSASocket(
  __in  int af,
  __in  int type,
  __in  int protocol,
  __in  LPWSAPROTOCOL_INFO lpProtocolInfo,
  __in  GROUP g,
  __in  DWORD dwFlags);
参数含义
af协议族信息
type套接字数据传输方式
protocol2个套接字之间使用的协议信息
lpProtocolInfo包含创建的套接字信息的WSAPROTOCOL_INFO结构体变量地址值,不需要时传递为NULL
g为扩展函数而预约的参数,可以使用0
dwFlags套接字属性,当为WSA_FLAG_OVERLAPPED的时候会创建出重叠I/O

在重叠I/O中,使用WSASend函数来发送数据,该函数定义如下:

int WSASend(
  __in   SOCKET s,
  __in   LPWSABUF lpBuffers,
  __in   DWORD dwBufferCount,
  __out  LPDWORD lpNumberOfBytesSent,
  __in   DWORD dwFlags,
  __in   LPWSAOVERLAPPED lpOverlapped,
  __in   LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
参数含义
s套接字句柄,传递具有重叠I/O属性的套接字句柄时,以重叠I/O模型输出
lpBuffersWSABUF结构体变量数组的地址,WSABUF中存有待传输数据
dwBufferCountlpBuffers数组的长度
lpNumberOfBytesSent用于保存实际发送字节数的变量地址
dwFlags用于更改数据传输特性,如传递MSG_OOB时发送OOB模式的数据
lpOverlappedWSAOVERLAPPED结构体变量的地址值,使用事件对象,用于确认完成数据传输
lpCompletionRoutine传入Completion Routine函数的入口地址值,可以通过该函数确认是否完成数据传输

其中WSABUF结构体定义如下:

typedef struct _WSABUF {
    ULONG len;     /* the length of the buffer */
    __field_bcount(len) CHAR FAR *buf; /* the pointer to the buffer */
} WSABUF, FAR * LPWSABUF;
成员含义
len待传输数据长度
buf缓冲区地址值

该参数保存的就是需要传输的数据信息,而WSAOVERLAPPED结构体的定义如下:

typedef struct _OVERLAPPED {
    ULONG_PTR Internal;
    ULONG_PTR InternalHigh;
    union {
        struct {
            DWORD Offset;
            DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
    } DUMMYUNIONNAME;
    HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;

这里最关键的是hEvent句柄,该句柄用来保证数据的完整发送。

为了进行重叠I/O,WSASend函数的lpOverlapped参数中应该传递有效的结构体变量地址值,而不是NULL。如果传递的是NULL,那么WSASend函数的第一个参数中的句柄所指的套接字将以阻塞模式工作

WSASend函数调用以后,如果返回值为SOCKET_ERROR并且WSAGetLastError的值为WSA_IO_PENDING就说明此时数据并没有发送完。而要等待数据发送完几句要用到OVERLAPPED结构体的hEvent,通过调用WSAWaitFirMultipleEvent函数并将其事件传入就可以等待发送数据完成,该函数定义如下

DWORD WSAWaitForMultipleEvents(
  __in  DWORD cEvents,
  __in  const WSAEVENT* lphEvents,
  __in  BOOL fWaitAll,
  __in  DWORD dwTimeout,
  __in  BOOL fAlertable);
参数含义
cEventslphEvents指向的数组中的事件对象句柄数。事件对象句柄的最大数量是WSA_MAXIMUM_WAIT_EVENTS事件。必须指定一个或多个事件。
lpEvents指向事件对象句柄数组的指针。数组可以包含不同类型对象的句柄。如果fWaitAll参数设置为TRUE,则它可能不包含同一句柄的多个副本。如果其中一个句柄在等待仍处于挂起状态时关闭,则WSAWaitForMultipleEvents的行为未定义。句柄必须具有同步访问权限。有关更多信息,请参阅标准访问权限。
fWaitAll指定等待类型的值。如果为TRUE,则当lphEvents数组中所有对象的状态都被通知时,函数返回。如果为FALSE,则当任何事件对象发出信号时,函数返回。在后一种情况下,返回值减去WSA_WAIT_EVENT_0表示其状态导致函数返回的事件对象的索引。如果在调用过程中有多个事件对象发出信号,则这是信号事件对象的数组索引,其索引值在所有信号事件对象中最小。
dwTimeout超时间隔,以毫秒为单位。如果超时时间间隔过期,即使不满足fWaitAll参数指定的条件,WSAWaitForMultipleEvents也会返回。如果dwTimeout参数为零,WSAWaitForMultipleEvents将测试指定事件对象的状态并立即返回。如果dwTimeout是WSA_INFINITE,那么WSAWaitForMultipleEvents将永远等待;也就是说,超时间隔永远不会过期
fAlertable指定线程是否处于可警报的等待状态,以便系统可以执行I/O完成例程。如果为TRUE,线程将处于可变等待状态,并且当系统执行I/O完成例程时,WSAWaitForMultipleEvents可以返回。在这种情况下,将返回WSA_WAIT_IO_COMPLETION,并且尚未通知正在等待的事件。应用程序必须再次调用WSAWaitForMultipleEvents函数。如果为FALSE,则线程不会处于可变等待状态,并且不会执行I/O完成例程

当该函数成功返回,此时代表数据发送完毕,可以使用WSAGetOverlappedResult函数来获取状态,该函数定义如下:

BOOL WSAAPI WSAGetOverlappedResult(
  __in   SOCKET s,
  __in   LPWSAOVERLAPPED lpOverlapped,
  __out  LPDWORD lpcbTransfer,
  __in   BOOL fWait,
  __out  LPDWORD lpdwFlags);
参数含义
s进行重叠I/O的套接字
lpOverlapped进行重叠I/O时传递的WSAOVERLAPPED结构体变量的地址
lpcbTransfer用于保存实际传输的字节数的变量地址值
fWait如果调用该函数时仍在进行I/O,fWait为TRUE时等待I/O完成,fWait为FALSE时将返回FALSE并跳出函数
lpdwFlags调用WSARecv函数时,用于获取附加信息(例如OOB消息)。如果不需要,可以传递NULL

在重叠I/O模型中,接收数据使用WSARecv函数,该函数定义如下:

int WSARecv(
  __in     SOCKET s,
  __inout  LPWSABUF lpBuffers,
  __in     DWORD dwBufferCount,
  __out    LPDWORD lpNumberOfBytesRecvd,
  __inout  LPDWORD lpFlags,
  __in     LPWSAOVERLAPPED lpOverlapped,
  __in     LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
参数含义
s
赋予重叠I/O属性的套接字句柄
lpBuffers用于保存接收数据的WSABUF结构体数组地址值
dwBufferCount第二个参数传递的数组长度
lpNumberOfBytesRecvd保存接收数据大小信息的变量地址值
lpFlags用于设置或读取传输特性信息
lpOverlappedWSAOVERLAPPED结构体变量地址值
lpCompletionRoutineCompletion Routine函数地址

参数含义和WSASend是一样的所以不重复,到这里就可以实现基于事件的重叠I/O模型,具体代码如下:

服务端:

// Server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <cstdio>
#include <WinSock2.h>
#include <Windows.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 30

VOID ShowError(PCHAR msg);

int main()
{
	WSADATA wsaData;
	SOCKET hServerSocket = INVALID_SOCKET, hClientSocket = INVALID_SOCKET;
	SOCKADDR_IN servAddr, clientAddr;
	int iClientAddrSize = 0;
	
	WSABUF dataBuf;
	WSAEVENT evObj;
	WSAOVERLAPPED overlapped;
	CHAR szBuf[BUF_SIZE] = { 0 };
	DWORD recvBytes = 0, flags = 0;

	// 初始化Winsock库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ShowError("WSAStartup");
		goto fail;
	}
	
	// 创建套接字
	hServerSocket = WSASocket(PF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (hServerSocket == INVALID_SOCKET)
	{
		ShowError("socket");
		goto exit;
	}

	// 绑定端口
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(1900);
	if (bind(hServerSocket, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ShowError("bind");
		goto exit;
	}

	// 监听端口
	if (listen(hServerSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		ShowError("listen");
		goto exit;
	}

	printf("服务端已开启...\n");

	iClientAddrSize = sizeof(clientAddr);
	hClientSocket = accept(hServerSocket, (SOCKADDR *)&clientAddr, &iClientAddrSize);
	printf("新的连接 IP:%s\n", inet_ntoa(clientAddr.sin_addr));

	// 创建等待事件
	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = evObj;
	// 初始化数据接收
	dataBuf.len = BUF_SIZE;
	dataBuf.buf = szBuf;

	if (WSARecv(hClientSocket, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
	{
		// 说明此时正在接收数据
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			// 等待数据接收完毕
			WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
			// 获取接收状态
			WSAGetOverlappedResult(hClientSocket, &overlapped, &recvBytes, FALSE, NULL);
		}
		else
		{
			ShowError("WSARecv");
		}
	}

	printf("receive message: %s\n", szBuf);
	WSACloseEvent(evObj);

exit:
	if (hServerSocket != INVALID_SOCKET)
	{
		closesocket(hServerSocket);
		hServerSocket = INVALID_SOCKET;
	}
	if (hClientSocket != INVALID_SOCKET)
	{
		closesocket(hClientSocket);
		hClientSocket = INVALID_SOCKET;
	}
	WSACleanup();
fail:
	system("pause");
	return 0;
}

VOID ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, WSAGetLastError());
}

客户端:

// Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <cstdio>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 30

VOID ShowError(PCHAR msg);

int main()
{
	WSADATA wsaData;
	SOCKET hSocket = INVALID_SOCKET;
	SOCKADDR_IN servAddr;

	CHAR szBuf[BUF_SIZE] = { "Hello 1900" };
	WSAEVENT evObj;
	WSAOVERLAPPED overlapped;
	WSABUF dataBuf;
	DWORD sendBytes = 0;

	// 初始化Winsock库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ShowError("WSAStartup");
		goto fail;
	}

	// 创建套接字
	hSocket = WSASocket(PF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (hSocket == INVALID_SOCKET)
	{
		ShowError("socket");
		goto exit;
	}

	// 连接服务端
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	servAddr.sin_port = htons(1900);
	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ShowError("connect");
		goto exit;
	}

	printf("连接成功\n");

	// 创建等待事件
	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = evObj;
	// 初始化发送数据信息
	dataBuf.buf = szBuf;
	dataBuf.len = BUF_SIZE;

	if (WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL))
	{
		// 说明此时正在发送数据
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			// 等到数据发送完毕
			WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
			WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);
		}
		else ShowError("WSASend");
	}
exit:
	if (hSocket != INVALID_SOCKET)
	{
		closesocket(hSocket);
		hSocket = INVALID_SOCKET;
	}
	WSACleanup();
fail:
	system("pause");
	return 0;
}

VOID ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, WSAGetLastError());
}

可以看到成功完成通信

WSARecv和WSASend函数的最后一个参数指定的Completion Routine函数验证I/O完成情况,注册该函数具体含义就是I/O完成时调用注册过的函数进行事后处理。

该函数定义如下:

typedef
void
(CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
    IN DWORD dwError,
    IN DWORD cbTransferred,
    IN LPWSAOVERLAPPED lpOverlapped,
    IN DWORD dwFlags
    );

当dwError不等于0的时候,cbTransferred保存了接收或者发送数据的长度,要通过Completion函数来完成数据的接收还需要在调用WSAWaitForMultipleEvents函数的时候,第三个参数指定为FALSE,第五个参数指定为TRUE,此时函数返回值为WAIT_IO_COMPLETION的时候,则说明数据接收完毕,具体代码如下:

服务端代码:

// Server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <cstdio>
#include <WinSock2.h>
#include <Windows.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 30

VOID ShowError(PCHAR msg);
void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags);


CHAR szBuf[BUF_SIZE] = { 0 };
DWORD recvBytes = 0;

int main()
{
	WSADATA wsaData;
	SOCKET hServerSocket = INVALID_SOCKET, hClientSocket = INVALID_SOCKET;
	SOCKADDR_IN servAddr, clientAddr;
	int iClientAddrSize = 0;
	
	WSABUF dataBuf;
	WSAEVENT evObj;
	WSAOVERLAPPED overlapped;
	DWORD flags = 0;


	// 初始化Winsock库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ShowError("WSAStartup");
		goto fail;
	}
	
	// 创建套接字
	hServerSocket = WSASocket(PF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (hServerSocket == INVALID_SOCKET)
	{
		ShowError("socket");
		goto exit;
	}

	// 绑定端口
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(1900);
	if (bind(hServerSocket, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ShowError("bind");
		goto exit;
	}

	// 监听端口
	if (listen(hServerSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		ShowError("listen");
		goto exit;
	}

	printf("服务端已开启...\n");

	iClientAddrSize = sizeof(clientAddr);
	hClientSocket = accept(hServerSocket, (SOCKADDR *)&clientAddr, &iClientAddrSize);
	printf("新的连接 IP:%s\n", inet_ntoa(clientAddr.sin_addr));

	// 创建等待事件
	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = evObj;
	// 初始化数据接收
	dataBuf.len = BUF_SIZE;
	dataBuf.buf = szBuf;

	if (WSARecv(hClientSocket, &dataBuf, 1, &recvBytes, &flags, &overlapped, CompRoutine) == SOCKET_ERROR)
	{
		// 说明此时正在接收数据
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			// 等待数据接收完毕
			if (WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE) != WAIT_IO_COMPLETION)
			{
				ShowError("WSAWaitForMultipleEvents");
			}
		}
		else
		{
			ShowError("WSARecv");
		}
	}

exit:
	if (hServerSocket != INVALID_SOCKET)
	{
		closesocket(hServerSocket);
		hServerSocket = INVALID_SOCKET;
	}
	if (hClientSocket != INVALID_SOCKET)
	{
		closesocket(hClientSocket);
		hClientSocket = INVALID_SOCKET;
	}
	if (evObj)
	{
		WSACloseEvent(evObj);
	}
	WSACleanup();
fail:
	system("pause");
	return 0;
}

void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	if (dwError != 0)	ShowError("Computine");
	else
	{
		recvBytes = szRecvBytes;
		printf("receive messge:%s\n", szBuf);
	}
}

VOID ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, WSAGetLastError());
}

客户端代码:

// Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <cstdio>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#define BUF_SIZE 30

VOID ShowError(PCHAR msg);
void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags);

int main()
{
	WSADATA wsaData;
	SOCKET hSocket = INVALID_SOCKET;
	SOCKADDR_IN servAddr;

	CHAR szBuf[BUF_SIZE] = { "Hello 1900" };
	WSAEVENT evObj;
	WSAOVERLAPPED overlapped;
	WSABUF dataBuf;
	DWORD sendBytes = 0;

	// 初始化Winsock库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ShowError("WSAStartup");
		goto fail;
	}

	// 创建套接字
	hSocket = WSASocket(PF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (hSocket == INVALID_SOCKET)
	{
		ShowError("socket");
		goto exit;
	}

	// 连接服务端
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	servAddr.sin_port = htons(1900);
	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ShowError("connect");
		goto exit;
	}

	printf("连接成功\n");

	// 创建等待事件
	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	// 初始化发送数据信息
	dataBuf.buf = szBuf;
	dataBuf.len = BUF_SIZE;

	if (WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, CompRoutine))
	{
		// 说明此时正在发送数据
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			// 等到数据发送完毕
			if (WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE) != WAIT_IO_COMPLETION)
			{
				ShowError("WSAWaitForMultipleEvents");
			}
		}
		else ShowError("WSASend");
	}
exit:
	if (hSocket != INVALID_SOCKET)
	{
		closesocket(hSocket);
		hSocket = INVALID_SOCKET;
	}
	WSACleanup();
fail:
	system("pause");
	return 0;
}

void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	if (dwError != 0)	ShowError("Computine");
	else
	{
		printf("成功发送数据\n");
	}
}

VOID ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, WSAGetLastError());
}

可以看到程序成功完成通信


【看雪培训】目录重大更新!《安卓高级研修班》2022年春季班开始招生!

最后于 2021-11-30 09:50 被1900编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (9)
雪    币: 172
活跃值: 活跃值 (76)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_柏双 活跃值 2021-11-23 13:42
2
0
ICOP模型可以说说不大哥
雪    币: 16426
活跃值: 活跃值 (15025)
能力值: ( LV15,RANK:700 )
在线值:
发帖
回帖
粉丝
1900 活跃值 4 2021-11-23 15:11
3
0
wx_柏双 ICOP模型可以说说不大哥
啊--。搞安全的学到这里就可以了吧。。
雪    币: 3317
活跃值: 活跃值 (1425)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
大鲤鱼 活跃值 2021-11-24 11:09
4
0
讲下收发文件呀,我目前只会MFC的单线程,收发文件会卡界面……
雪    币: 16426
活跃值: 活跃值 (15025)
能力值: ( LV15,RANK:700 )
在线值:
发帖
回帖
粉丝
1900 活跃值 4 2021-11-24 11:28
5
0
大鲤鱼 讲下收发文件呀,我目前只会MFC的单线程,收发文件会卡界面……[em_34]
卡界面是因为你没有开单独线程来解决吧
雪    币: 3317
活跃值: 活跃值 (1425)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
大鲤鱼 活跃值 2021-11-24 11:32
6
0
我是用MFC的,它本身是单线程的……
雪    币: 172
活跃值: 活跃值 (76)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_柏双 活跃值 2021-11-24 13:37
7
0
1900 啊--。搞安全的学到这里就可以了吧。。
说说可以不大哥,还是很有用的
雪    币: 10924
活跃值: 活跃值 (5817)
能力值: ( LV12,RANK:302 )
在线值:
发帖
回帖
粉丝
一半人生 活跃值 5 2021-11-24 14:51
8
0
linux 2.6 kerne以后引入了epoll,win就用iocp,主流两大后端框架,当然也有分布式grpc之类的框架,高并发性能也不错
雪    币: 16426
活跃值: 活跃值 (15025)
能力值: ( LV15,RANK:700 )
在线值:
发帖
回帖
粉丝
1900 活跃值 4 2021-11-24 15:18
9
0
一半人生 linux 2.6 kerne以后引入了epoll,win就用iocp,主流两大后端框架,当然也有分布式grpc之类的框架,高并发性能也不错
哥你开个贴讲讲吧
雪    币: 11
活跃值: 活跃值 (625)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
yy大雄 活跃值 2021-11-30 10:08
10
0
此贴发现,大多数搞安全的都不关注开发
游客
登录 | 注册 方可回帖
返回