马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
十、基于I/O模子的网络开发
10.10 重叠I/O 模子
10.10.1 根本概念
在 Winsock 中,重叠 I/O(Overlapped I/O) 模子能到达更佳的体系性能,高于 select 模 型、异步选择和变乱选择3种。重叠模子的根本计划原理便是让应用步伐利用一个重叠的数据 结 构(WSAOVERLAPPED), 一次投递一个或多个 Winsock I/O 哀求。针对这些提交的哀求, 在它们完成之后,我们的应用步伐会收到关照,于是我们就可以对数据举行处置惩罚了。
重叠I/O 这个概念来自文件I/O 利用。在Win32 文件I/O 利用中,当调用ReadFile 和 WriteFile时,假如末了一个参数lpOverlapped设置为NULL, 那么线程就壅闭在这里,直到读 写完指定的数据后才返回。如许在读写大文件的时间,许多时间都浪费在等待 ReadFile 和WriteFile的返回上面。假如ReadFile 和 WriteFile 是往管道里读写数据,那么有大概壅闭得更 久,导致步伐性能降落。
为了办理这个题目,Windows 引进了重叠I/O的概念,它可以或许同时以多个线程处置惩罚多个I/O。 着实你自己开多个线程也可以处置惩罚多个I/O, 但是体系内部对I/O 的处置惩罚在性能上有很大的优 化。重叠I/O 是 Windows 下实现异步I/O 常用的方式。
Windows 为险些全部范例的文件提供这个工具:磁盘文件、通讯端口、定名管道和套接 字。通常,利用ReadFile 和 WriteFile 就可以很好地实行重叠I/O。
重叠模子的焦点是一个重叠数据布局。若想以重叠方式利用文件,必须用 FILE_FLAG_OVERLAPPED 标记打开它,比方:- HANDLE hFile = CreateFile(lpFileName, GENERIC READ I GENERIC WRITE,
- FILE_SHARE_READ I FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
复制代码 假如没有指定该标记,那么针对这个文件(句柄)而言,重叠I/O 是不可用的。假如设置 了该标记,当调用ReadFile 和 WriteFile 利用这个文件(句柄)时,必须为末了一个参数提供 OVERLAPPED 布局:- // WINBASE.H
- typedef struct _OVERLAPPED
- {
- ULONG_PTR Internal;
- ULONG_PTR InternalHigh;
- union
- {
- struct
- {
- DWORD Offset;
- DWORD OffsetHigh;
- } DUMMYSTRUCTNAME;
- PVOID Pointer;
- } DUMMYUNIONNAME;
- HANDLE hEvent;
- } OVERLAPPED, *LPOVERLAPPED;
复制代码
- Internal:I/O 哀求的状态代码。发出哀求时,体系将此成员设置为状态“挂起”,以 指示利用尚未启动。哀求完成后,体系将此成员设置为已完成哀求的状态代码。该字 段由体系内部利用。
- InternalHigh:被传输数据的长度。
- DUMMYUNIONNAME.DUMMYSTRUCTNAME.Offset: 启动输入输出哀求的文件位 置的低阶部门,由用户指定。只有在支持偏移(也称为文件指针机制)概念的查找设 备(如文件)上实行I/O 哀求时此成员才为非零;否则,此成员必须为零。
- DUMMYUNIONNAME.DUMMYSTRUCTNAME.OffsetHigh: 启动输入输出哀求的文 件位置的高阶部门,由用户指定。只有在支持偏移(也称为文件指针机制)概念的查 找装备(如文件)上实行I/O 哀求时此成员才为非零;否则,此成员必须为零。
- DUMMYUNIONNAME.Pointer: 保存供体系利用。初始化为零后不要利用。
- hEvent: 当利用完成时,由体系设置为信号状态的变乱句柄。在将此布局通报给任何重叠函数之前,用户必须利用CreateEvent函数将此成员初始化为零或有效的变乱句 柄。然后可以利用此变乱同步装备的同时I/O 哀求。函数(如ReadFile和 WriteFile) 在开始I/O利用之前将此句柄设置为非署名状态。利用完成后,句柄将设置为信号状 态。诸如GetOverlappedResult 和同步等待函数等函数将主动重置变乱重置为非信号 状态。因此,应该利用手动重置变乱。假如利用主动重置变乱,等待利用完成,然后 使 用bWait 参数设置为TRUE 调 用GetOverlappedResult, 则应用步伐可以制止相应。
- 在函数调用中利用该布局之前,应始终将该布局的任何未利用成员初始化为零;否则,函 数大概会失败并返回ERROR INVALID PARAMETER 。Offset 和 OffsetHigh 成员一起表现64 位文件位置。它是从文件或雷同文件的装备开始的字节偏移量,由用户指定,体系不会修改这 些值。调用进程必须在将重叠布局通报给利用偏移量的函数(如ReadFile 、WriteFile 或其他相 关函数)之前设置此成员。
- 因 为I/O 异步发生,就不能确定利用是否按次序完成。因此,这里没有当前位置的概念。 对于文件的利用,总是规定该偏移量。在数据流下(如COM 端口或socket), 没有探求正确 偏移量的方法,以是在这些环境中体系忽略偏移量。这4个字段不应由应用步伐直接举行处置惩罚 或利用,OVERLAPPED 布局的末了一个参数是可选的变乱句柄。稍后会提到怎样利用这个参 数来设定变乱关照完成 I/O 。如今,假定该句柄是NULL 。设 置 了OVERLAPPED 参数后, ReadFile/WriteFile 的调用会立刻返回,这时你可以去做其他的事(所谓异步),体系会主动替 你完成ReadFile/WriteFile 相干的I/O 利用。你也可以同时发出几个ReadFile/WriteFile 的调用 (所谓重叠)。当体系完成I/O利用时,会将OVERLAPPED.hEvent 置信(置有信号状态), 我们可以通过调用WaitForSingleObject/WaitForMultipleObjects来等待这个I/O 完成关照,在得 到关照信号后,就可以调用GetOverlappedResult来查询I/O 利用的结果,并举行相干处置惩罚。由 此可以看出,OVERLAPPED 布局在一个重叠I/O 哀求的初始化及厥后续的完成之间提供了一 种沟通或通讯机制。
- 以Win32 重 叠I/O 机制为根本,自Winsock2发布开始,重叠I/O 便已集成到新的Winsock 函数中,比如WSARecv/WSASend。如许一来,重叠I/O 模子便能实用于安装了Winsock 2的 所 有Windows 平台。可以一次投递一个或多个Winsock I/O哀求。针对那些提交的哀求,在它 们完成之后,应用步伐可为它们提供服务(对I/O 的数据举行处置惩罚)。
- 比起壅闭、select 、WSAAsyncSelect 以 及WSAEventSelect 等 模 型 ,Winsock 的 重 叠I/O (Overlapped I/O) 模子使应用步伐能到达更佳的体系性能。由于它和这4种模子差异的是, 利用重叠模子的应用步伐关照缓冲区收发体系直接利用数据。也就是说,假如应用步伐投递了 一个10KB巨细的缓冲区来吸收数据,且数据已经到达套接字,那么该数据将直接被复制到投递的缓冲区。而其他4种模子中,数据到达先复制到套接字自己的吸收缓冲区中,此时应用步伐会被体系关照有数据可读。当应用步伐调用吸收函数之后,数据才从套接字自己的缓冲区复 制到应用步伐的缓冲区,如许就多了一次从套接字缓冲区到应用步伐缓冲区的复制,性能差异 就在于此。
10.10.2 创建重叠I/O模子下的套接字
要想在一个套接字上利用重叠 I/O 模子来处置惩罚网络数据通讯,起首必须利用 WSA_FLAG_OVERLAPPED标记来创建一个套接字:- sOCKET s = WSASocket(AF_INET, SOCK_STEAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
复制代码 创建套接字的时间,假如利用的是socket 函数,而非WSASocket 函数,那么会默认设置 WSA_FLAG_OVERLAPPED 标记。乐成创建好了一个套接字,将其与一个本地接口绑定到一 起后,便可开始举行这个套接字上的重叠I/O 利用。
为了要利用重叠布局,我们常用的 send、 recv 等收发数据的函数也都要被WSASend 、WSARecv 更换掉了。方法是调用以下Winsock 2函数,同时为它们指定一个WSAOVERLAPPED 布局参数(可选): WSASend、WSASendTo、
WSARecv、WSARecvFrom、WSAloctl、AcceptEx、TransmitFile。 若 随 WSAOVERLAPPED
布局一起调用这些函数,则函数会立刻返回,无论套接字是否设为锁定模式。它们依赖于 WSAOVERLAPPED 布局来返回一个I/O 哀求利用的结果。
在Windows NT和Windows 2000中,重叠I/O 模子也答应应用步伐以一种重叠方式实现对套接字毗连的处置惩罚。详细的做法是在监听套接字上调用AcceptEx 函数。AcceptEx 是一个特 殊的Winsock 1.1扩展函数,该函数最初的计划宗旨是在Windows NT与 Windows 2000利用 体系上利用Win32 的重叠I/O 机制。究竟上,它也实用于Winsock 2中的重叠I/O 。AcceptEx 的声明如下:- BOOL AcceptEx(
- SOCKET SListenSocket,
- SOCKET SAcceptSocket,
- PVOID SAcceptSocket,
- DWORD dwReceiveDataLength,
- DWORD dwLocalAddressLength,
- DWORD dwRemoteAddressLength,
- LPOVERLAPPED lpOverlapped
- )
复制代码
- sListenSocket: 标识已用listen 函数调用过的套接字的形貌符。服务器步伐在此套接 字上等待毗连。
- sAcceptSocket: 一种形貌符,用于标识继承传入毗连的套接字。
- lpOutputBuffer:指向缓冲区的指针。该缓冲区是一个特殊的缓冲区,由于它要负责3 种数据的吸收:服务器的本地地点,客户机的远程地点,以及在新建毗连上发送的第 一个数据块。
- dwReceiveDataLength:以字节为单位,指定了在lpOutputBuffer 缓冲区中,保存多大 的空间来吸收数据。假如将这个参数设为0,那么在继承毗连的过程中不会再一起接 收任何数据。
- dwLocalAddressLength: 为本地地点信息保存的字节数。此值必须至少比正在利用的 传输协议的最大地点长度多16字节。举个例子,假定正在利用的是TCP/IP 协议,那么这里的巨细应设为“SOCKADDR IN 布局的长度+16字节”。
- dwRemoteAddressLength: 为远程地点信息保存的字节数。此值必须至少比正在利用 的传输协议的最大地点长度多16字节,不能为零。
- lpdwBytesReceived:用于返回吸收到的实际数据量,以字节为单位。只有在利用以同 步方式完成的条件下才会设置这个参数。假如 AcceptEx 函数返回 ERROR_IO_PENDING, 那么这个参数永世都不会设置,我们必须利用完成变乱关照 机制来获知实际读取的字节量。
- lpOverlapped: 它对应的是一个OVERLAPPED 布局,答应AcceptEx 以一种异步方式 工作。如我们早先所述,只有在一个重叠I/O应用中该函数才必要利用变乱对象关照 机制(hEvent 字段),这是由于此时没有一个完成例程参数可供利用。
假如函数乐成,就返回 TRUE 。 假如函数失败,就返回 FALSE, 此时可以调用 WSAGetLastError 函数返回错误码。若WSAGetLastError 返 回ERROR_IO_PENDING, 则利用 已乐成启动,而且仍在举行中。若错误码为WSAECONNRESET, 则表现有一个传入毗连,但 随后在继承呼唤之前被远程对等端制止。
10.10.3 获取重叠I/O利用完成结果
异步I/O 哀求挂起后,终极要知道I/O利用是否完成。 一个重叠I/O 哀求终极完成后,应 用步伐要负责取回重叠I/O 利用的结果。对于读,直到I/O 完成,吸收缓冲区才有效。对于写, 要知道写是否乐成。有几种方法可以做到这一点,最直接的方法是调用 WSAGetOverlappedResult, 其函数原型如下:- BOOL WSAAPI WSAGetOverlappedResult(
- SOCKET s,
- LPWSAOVERLAPPED lpOverlapped,
- LPDWORD lpcbTransfer,
- BOOL fWait,
- LPDWORD lpdwFlags
- );
复制代码
- s: 套接字句柄。
- lpOverlapped: 关联的WSAOVERLAPPED 布局,在调用CreateFile 、WSASocket 或 AcceptEx 时指定。
- lpcbTransfer: 指向字节计数指针,负责吸收一次重叠发送或吸收利用实际传输的字 节 数 。
- fWait: 确定下令是否等待的标记,用于决定函数是否应该等待一次重叠利用完成。 若将该参数设为TRUE, 则直到利用完成函数才返回;若设为FALSE, 而且利用仍 然处于未完成状态,那么WSAGetOverlappedResult 函数会返回FALSE 值。
- lpdwFlags: 指向32位变量的指针,该变量将吸收一个或多个增补完成状态的标记。 假如重叠利用是通过WSARecv 或 WSARecvFrom 启动的,那么此参数将包罗lpFlags
- 参数的结果值。此参数不能是空指针。
假如函数乐成,那么返回值为TRUE 。这意味偏重叠利用已乐成完成,而且lpcbTransfer 指向的值已更新。
假如函数回FALSE, 就意味偏重叠利用尚未完成,大概重叠利用已完成但有错误,大概 由 于WSAGetOverlappedResult 的一个或多个参数中的错误而无法确定重叠利用的完成状态。 失败时,lpcbTransfer 指向的值将不会更新。利用WSAGetLastError 可以确定失败的缘故原由。
下面先容两种常用重叠I/O 完成关照的方法。
10.10.4 基于变乱关照(有64个socket 的限定)
套接字重叠I/O 的变乱关照方法要求变乱对象与WSAOVERLAPPED 布局关联在一起。 当 I/O 利用完成后,该变乱对象从未触发状态变为触发状态。在应用步伐中先调用 WSAWaitForMultipleEvents 函数等待该变乱的发生。得到该变乱对象对应的 WSAOVERLAPPED 布局后可以根据 Internal 和 InternalHigh 字段(也可以调用 WSAGetOverlappedResult 函 数 ) 判 断I/O 完成的环境。
详细步调如下:
- (1)创建具有WSAOVERLAPPED 标记的套接字。假如调用socket()函数,那么默认创 建具有 WSAOVERLAPPED 标记的套接字。假如调用WSASocket 函数,就必要明白指定 WSAOVERLAPPED 标记。
- (2)为套接字界说WSAOVERLAPPED 布局,并清零。
- (3)调用WSACreateEvent 函数创建变乱对象,并将该变乱句柄分配给WSAOVERLAPPED 布局的hEvent 字段。
- (4)调用吸收大概发送函数。
- (5)调用WSAWaitForMultipleEvents 函数等待与重叠I/O 关联的变乱变为已触发状态。
- (6)WSAWaitForMultipleEvents 返回后,调用WSAResetEvent 函数,将该变乱对象规复 为未触发态。
- (7) 调 用WSAGetOverlappedResult 函数判定重叠I/O 的完成状态。
下面的实例演示了利用socket 重 叠I/O 模子开发服务步伐的步调。该步伐涉及两个线程: 吸收线程用于继承客户端毗连哀求,初始化重叠I/O 利用;服务线程用于重叠I/O 处置惩罚。
【例10.5】利用变乱关照实现重叠I/O模子- #define _WINSOCK_DEPRECATED_NO_WARNINGS
- #include <winsock2.h>
- #include <Windows.h>
- #include <iostream>
- #pragma comment(lib,"Ws2_32.lib")
- using std::cout;
- using std::cin;
- using std::endl;
- using std::ends;
- void WSAEventServerSocket()
- {
- SOCKET server = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- if (server == INVALID_SOCKET) {
- cout << "创建SOCKET失败!,错误代码:" << WSAGetLastError() << endl;
- return;
- }
- int error = 0;
- sockaddr_in addr_in;
- addr_in.sin_family = AF_INET;
- addr_in.sin_port = htons(6000);
- addr_in.sin_addr.s_addr = INADDR_ANY;
- error = ::bind(server, (sockaddr*)&addr_in, sizeof(sockaddr_in));
- if (error == SOCKET_ERROR) {
- cout << "绑定端口失败!,错误代码:" << WSAGetLastError() << endl;
- return;
- }
- listen(server, 5);
- if (error == SOCKET_ERROR) {
- cout << "监听失败!,错误代码:" << WSAGetLastError() << endl;
- return;
- }
- cout << "成功监听端口 :" << ntohs(addr_in.sin_port) << endl;
- WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 事件对象数组
- SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]; // 事件对象数组对应的SOCKET句柄
- int nEvent = 0; // 事件对象数组的数量
- WSAEVENT event0 = ::WSACreateEvent();
- ::WSAEventSelect(server, event0, FD_ACCEPT | FD_CLOSE);
- eventArray[nEvent] = event0;
- sockArray[nEvent] = server;
- nEvent++;
- while (true) {
- int nIndex = ::WSAWaitForMultipleEvents(nEvent, eventArray, false, WSA_INFINITE, false);
- if (nIndex == WSA_WAIT_IO_COMPLETION || nIndex == WSA_WAIT_TIMEOUT) {
- cout << "等待时发生错误!错误代码:" << WSAGetLastError() << endl;
- break;
- }
- nIndex = nIndex - WSA_WAIT_EVENT_0;
- WSANETWORKEVENTS event;
- SOCKET sock = sockArray[nIndex];
- ::WSAEnumNetworkEvents(sock, eventArray[nIndex], &event);
- if (event.lNetworkEvents & FD_ACCEPT) {
- if (event.iErrorCode[FD_ACCEPT_BIT] == 0) {
- if (nEvent >= WSA_MAXIMUM_WAIT_EVENTS) {
- cout << "事件对象太多,拒绝连接" << endl;
- continue;
- }
- sockaddr_in addr;
- int len = sizeof(sockaddr_in);
- SOCKET client = ::accept(sock, (sockaddr*)&addr, &len);
- if (client != INVALID_SOCKET) {
- cout << "接受了一个客户端连接 " << inet_ntoa(addr.sin_addr) << ":" << ntohs(addr.sin_port) << endl;
- WSAEVENT eventNew = ::WSACreateEvent();
- ::WSAEventSelect(client, eventNew, FD_READ | FD_CLOSE | FD_WRITE);
- eventArray[nEvent] = eventNew;
- sockArray[nEvent] = client;
- nEvent++;
- }
- }
- }
- else if (event.lNetworkEvents & FD_READ) {
- if (event.iErrorCode[FD_READ_BIT] == 0) {
- char buf[2500];
- ZeroMemory(buf, 2500);
- int nRecv = ::recv(sock, buf, 2500, 0);
- if (nRecv > 0) {
- cout << "收到一个消息 :" << buf << endl;
- char strSend[] = "hi,client,I am server, I recvived your message.";
- ::send(sock, strSend, strlen(strSend), 0);
- }
- }
- }
- else if (event.lNetworkEvents & FD_CLOSE) {
- ::WSACloseEvent(eventArray[nIndex]);
- ::closesocket(sockArray[nIndex]);
- cout << "一个客户端连接已经断开了连接" << endl;
- for (int j = nIndex; j < nEvent - 1; j++) {
- eventArray[j] = eventArray[j + 1];
- sockArray[j] = sockArray[j + 1];
- }
- nEvent--;
- }
- else if (event.lNetworkEvents & FD_WRITE) {
- cout << "一个客户端连接允许写入数据" << endl;
- }
- } // end while
- ::closesocket(server);
- }
- int main(){
- WSADATA wsaData;
- int error;
- WORD wVersionRequested;
- wVersionRequested = WINSOCK_VERSION;
- error = WSAStartup(wVersionRequested, &wsaData);
- if (error != 0) {
- WSACleanup();
- return 0;
- }
- WSAEventServerSocket();
- WSACleanup();
- return 0;
- }
复制代码 这个模子与其他模子差异的是它利用Winsock2 提供的异步I/O 函 数WSARecv。 在调用 WSARecv 时,指定一个WSAOVERLAPPED 布局,这个调用不是壅闭的,也就是说,它会立 刻返回。一旦有数据到达,被指定的WSAOVERLAPPED 布局中的hEvent被 Signaled。在 取 得吸收的数据后,把数据原封不动地打印出来,大概同道们也可以调用一下 send 函数,把数 据发送回客户端(客户端那里有吸收线程),然后重新激活一个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: 假如吸收利用立刻完成,就指向吸收数据的字节数指针。
假如 lpOverlapped 参数不是空值,就对此参数利用空值,以制止潜伏的错误结果。只有 lpOverlapped参数不为空时,此参数才气为空。
- lpFlags: 指向用于修改WSARecv 函数调用活动的标记的指针。
- lpOverlapped: 指 向 W SAOVERLAPPED 布局的指针(对于非重叠的套接字忽略)。
- lpCompletionRoutine: 当吸收利用完成时调用的完成例程的指针(对于非重叠的套接 字忽略)。
假如没有发生错误,而且吸收利用立刻完成,则函数返回零。在这种环境下, 一旦调用线 程处于可警报状态,就已经操持调用完成例程。否则,将返回SOCKET_ERROR, 此时可以通 过调用WSAGetLastError 来检索特定的错误代码。错误代码WSA _IO_PENDING 表现重叠操 作已乐成启动,稍后将指示完成。任何其他错误代码都表现重叠利用未乐成启动,不会出现完 成指示 。
10.10.5 基于完成例程
10.10.5.1 根本概念
假如你想要利用重叠I/O 机制带来的高性能模子,又懊恼于基于变乱关照的重叠模子要收到64个等待变乱的限定,尚有点畏惧完成端口稍显复杂的初始化过程,那么“完成例程”无 疑是最好的选择!由于完成例程摆脱了变乱关照的限定,可以连入恣意数目客户端而不消另开 线程,也就是说只用很简单的一些代码就可以利用Windows 内部的I/O 机制来得到网络服务 器的高性能。
在基于变乱关照的重叠I/O 模子中,在你投递了一个哀求(比如WSARecv) 以后,体系 在完成以后是用变瞎搅关照你的,而在完成例程中,体系在网络利用完成以后会主动调用你提 供的回调函数,区别仅此而已。接纳完成例程的服务器端,通讯流程如图10-12所示。
从图10-12中可以看到,服务器端存在一个显着的异步过程,也就是说我们把客户端连入的 SOCKET 与一个重叠布局绑定之后,便可以将通讯过程全权交给体系内部去帮我们调治处置惩罚,我 们在主线程中可以边做其他的变乱边等待体系完成的关照。这就是完成例程高性能的缘故原由所在。
假如还没有看明白,我们打个普通易懂的比方:完成例程的处置惩罚过程,也就像我们告诉系 统,“我想要在网络上吸收网络数据,你去帮我办一下”(投递WSARecv 利用),“不外我 并不知道网络数据何时到达,总之在吸收到网络数据之后,你就直接调用我给你的这个函数(比 如 CompletionProess),把它们生存到内存大概体现到界面中等,全权交给你处置惩罚了”。于是 乎,体系在吸收到网络数据之后, 一方面会给我们一个关照,另一方面体系也会主动调用我们 事先准备好的回调函数,就不必要我们自己费心了。
10.10.5.2 完成例程的优点
基于完成例程的重叠I/O 模子相对于变乱选择I/O 模子的精良之处在于,重叠I/O 模子完 全办理了recv 的壅闭题目。在从前的模子当中,recv 只是吸收数据到来的关照,之后仍然要己去内核复制数据到用户空间中,如今recv 过程全部交由利用体系完成,减去了数据等待 与将数据从内核复制到步伐缓冲区的时间,而且其间不占用步伐自身的时间片。
基于完成例程的重叠I/O 异步模子如图10-13所示。
变乱选择网络模子的数据复制如图10-14所示。
别的,完成例程相比基于变乱相应的重叠I/O 模子的精良之处在于,完成例程并没有64 个变乱的上限,而是利用体系调用完成例程(也就是一个由利用体系调用的回调函数)对吸收 到的数据举行处置惩罚。固然都是基于重叠I/O,但是由于前两种模子都必要自己来管理任务的分 派,以是性能上没有区别。
10.10.5.3 完成例程的关键函数
完成例程方式和前面的变乱关照方式最大的差异之处在于,我们必要提供一个回调函数供 体系收到网络数据后主动调用,回调函数的参数界说应该依照如下函数原型:- void CALLBACK_CompletionRoutineFunc(DWORD dwError,
- DWORD cbTransferred,
- LPWSAOVERLAPPED lpOverlapped,
- DWORD dwFlags);
复制代码
- 此中,参数dwError 标记咱们投递的重叠利用,比如WSARecv, 完成的状态是什么;
- 参 数 cbTransferred 指明白在重叠利用期间,实际传输的字节量是多大;
- 参数lpOverlapped参数 指明通报到最初的I/O 调用内的一个重叠布局;
- 参数dwFlags 返回利用竣事时大概用的标记(一 般没用)。
留意:函数名字任意起,但是参数范例不能错。尚有一点必要重点提一下,由于我们必要 给体系提供一个如上面界说的那样的回调函数,以便体系在完成了网络利用后主动调用,这里 就必要提一下究竟是怎样把这个函数与体系内部绑定的。比如,在WSARecv 函数中是如许绑 定 的 :- int WSARecv(
- SOCKET s,
- LPWSABUF lpBuffers, DWORD dwBufferCount,
- LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags,
- LPWSAOVERLAPPED lpOverlapped,
- LPWSAOVERLAPPED COMPLETION ROUTINE lpCompletionRoutine);
复制代码 这个函数前面着实先容过,由于比力告急,我们再次简单地报告一下这个函数。
- 参数 s 投递这个利用的套接字;
- 参数IpBuffers表现吸收缓冲区,与recv函数差异,这里必要一个由 WSABUF布局构成的数组;
- 参数dwBufferCount 表现数组中WSABUF 布局的数目,设置为1 即可;
- 参数lpNumberOfBytesRecvd 表现当吸收利用完成时,会返回函数调用所吸收到的字节 数 ;
- lpOverlapped 表现“绑定”的重叠布局;
- 末了一个参数lpCompletionRoutine 就是完成例程 函数的指针。
我们的回调函数 CompletionRoutineFunc和 WSARecv 利用关联起来了,体系一 完成吸收数据,就回调我们的函数,之后我们就可以在内里处置惩罚数据了。
假如以为有些抽象,我们可以看一段代码:- SOCKET s;
- WSABUF DataBuf; // 定义WSABUF结构的缓冲区
- #define DATA BUFSIZE 4096 // 初始化一下DataBuf
- char buffer[DATA BUFSIZE];
- ZeroMemory(buffer, DATA BUFSIZE);
- DataBuf.len = DATA BUFSIZE;
- DataBuf.buf = buffer;
- DWORD dwBufferCount = 1, dwRecvBytes = 0, Flags = 0;
- // 建立需要的重叠结构,每个连入的sOCKET上的每一个重叠操作都得绑定一个
- WSAOVERLAPPED Acceptoverlapped; // 如果要处理多个操作,这里当然需要一个WSAOVERLAPPED 数组
- ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
- // 做了这么多工作,终于可以使用WSARecv 来把我们的完成例程函数绑定上了
- // 当然,假设我们的_CompletionRoutine 函数已经定义好了
- WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes, &Flags, &AcceptOverlapped, _CompletionRoutine);
复制代码 其他参数我们可以先不消细看,重点关注末了一个。
10.10.5.4 完成例程的实现步调
理论知识方面必要知道的就是这么多。下面我们共同代码, 一步步地教学怎样亲手实现一 个基于完成例程的重叠I/O 模子。
详细步调如下:
- 第一步,创建一个套接字,开始在指定的端口上监听毗连哀求。
- 第一步很简单,和其他的SOCKET 初始化并无多大区别。必要留意的是,为了突出重点, 笔者去掉了错误处置惩罚,详细开发时可不能如许,只管这里堕落的概率比力小:
- WSADATA wsaData;
- WSAStartup(MAKEWORD(2, 2), &wsaData); // 创建TCP 套接字
- ListenSocket = socket(AF INET, SOCK STREAM, IPPROTO TCP);
- SOCKADDR_IN ServerAddr; // 分配端口及协议簇并绑定 ServerAddr.sin family=AF INET;
- ServerAddr.sin addr.S un.S addr = htonl(INADDR ANY); // 在8888端口监听,端口号可以随意更改,但最好不要少于1024
- ServerAddr.sin port = htons(8888);
- bind(ListenSocket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字
- listen(ListenSocket, 5); // 开始监听
复制代码 第二步,继承一个入站的毗连哀求。调用accept 函数即可:- AcceptSocket = accept(ListenSocket, NULL, NULL);
复制代码 假如想要得到连入客户端的信息,则accept 的后两个参数不要用NULL, 而是如许:- SOCKADDR IN ClientAddr; // 定义一个客户端的地址结构作为参数
- int addr length = sizeof(ClientAddr);
- AcceptSocket = accept(ListenSocket, (SOCKADDR *)&ClientAddr, &addr length);
- // 于是乎,我们就可以轻松得知连入客户端的信息了
- LPCTSTR lpIP = inet ntoa(ClientAddr.sin addr); // 连入客户端的 IP
- UINT nPort = ClientAddr.sin port; // 连入客户端的Port
复制代码
- 第三步,准备好我们的重叠布局。有新的套接字连入以后,新建一个WSAOVERLAPPED 重叠布局(固然也可以提前创建 好),准备绑定到我们的重叠利用上去。这里也可以看到和基于变乱的显着区别,就是不消再 为WSAOVERLAPPED 布局绑定一个hEvent 了 。这里只界说一个,实际上是每一个SOCKET 的每一个利用都必要绑定一个重叠布局,所 以在实际利用面对多个客户端的时间要界说为数组,详见示例代码:
- WSAOVERLAPPED AcceptOverlapped;
- ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 置 零
复制代码
- 第四步,开始在套接字上投递WSARecv 哀求。这一步必要将第三步准备的WSAOVERLAPPED 布局和我们界说的完成例程函数为参数。 各个变量都已经初始化完成以后,我们就可以开始举行详细的Socket 通讯函数调用了,然后 让体系内部的重叠布局来替我们管理I/O 哀求,我们只用等待网络通讯完成后调用回调函数就 可 以 了 。这个步调的重点是绑定一个Overlapped 变量和一个完成例程函数:
- // 将WSAOVERLAPPED 结构指定为一个参数,在套接字上投递一个异步WSARecv() 请求 // 并提供下面的作为完成例程的_CompletionRoutine 回调函数(函数名字)
- if (WSARecv(
- AcceptSocket, &DataBuf, 1,
- &dwRecvBytes, &Flags,
- &AcceptOverlapped,
- _CompletionRoutine) == SOCKET_ERR OR) // 注意我们传入的回调函数指针
- {
- if (WSAGetLastError() != WSA_IO_PENDING)
- {
- ReleaseSocket(nSockIndex);
- continue;
- }
- }
复制代码
- 第五步,调用WSAWaitForMultipleEvents 函数大概SleepEx 函数等待重叠利用返回的结 果。我们在前面提到过,投递完WSARecv 利用,并绑定了Overlapped 布局和完成例程函数之 后,根本就完事大吉了。比及体系自己去完成网络通讯,并在吸收到数据的时间,会主动调用 我们的完成例程函数。
我们在主线程中必要做的变乱只有做别的变乱,而且等待体系完成了完成例程调用后的返 回结果。就是说在WSARecv 调用发起完毕之后,我们不得不在背面紧跟上一些等待完成结果 的代码。有两种办法可以实现:
- (1)和上一节重叠I/O 中讲到的一样,我们可以利用WSAWaitForMultipleEvent 来等待 重叠利用的变乱关照,演示如下:
- /*因为WSAWaitForMultipleEvents()API 要求在一个或多个事件对象上等待,但是这个事件
- 数组已经不是和SOCKET 相关联的了,因此不得不创建一个伪事件对象*/
- WSAEVENT EventArray[1];
- EventArray[0] = WSACreateEvent(); // 建立一个事件
- // 然后等待重叠请求完成就可以了。注意保存返回值,这个很重要
- DWORD dwIndex = WSAWaitForMultipleEvents(1, EventArray, FALSE, WSA_INFINITE, TRUE);
复制代码 WSAWaitForMultipleEvents参数的寄义上一节中已经先容过了。调用这个函数以后,线程 就会置于一个警觉的等待状态。留意,fAlertable 参数肯定要设置为TRUE。
- (2)可以直接利用SleepEx 函数来完成等待,结果是一样的。SleepEx 函数调用起来就简 单得多了,它的函数原型界说是如许的:
- DWORD SleepEx(
- DWORD dwMilliseconds, BOOL bAlertable);
复制代码 此中,参数dwMilliseconds 为等待的超时时间,假如设置为INFINITE 就会不停等待下去; 参数bAlertable 表现是否置于警觉状态,假如为FALSE, 则肯定要等待超时时间完毕之后才 会返回。假如指定的时间隔断已逾期,则函数返回值为零。假如函数由于一个或多个I/O 完成 回调函数而返回,则返回值为WAIT_IO_COMPLETION, 只有当bAlertable为 TRUE, 而且调 用SleepEx函数的线程与调用扩展I/O函数的线程雷同时才会发生这种环境。
这里我们盼望重叠利用一完成绩能返回,以是肯定要设置为TRUE。调用这个函数的时间, 同样留意用一个DWORD 范例变量来生存它的返回值,背面会派上用场。
- 第六步,通逾等待函数的返回值取得重叠利用的完成结果。这是我们最关心的变乱,费了那么大劲投递的这个重叠利用究竟是什么结果呢?就是通过 上一步中我们调用的等待函数的DWORD 范例的返回值。正常环境下,在利用完成之后,应 该是返回 WAIT_IO_COMPLETION, 假如返回的是WAIT_TIMEOUT, 则表现等待设置的超时时间到了,但是重叠利用仍然没有完成,应该通过循环再继续等待。假如是其他返回值,就 坏事了,阐明网络通讯出现了其他非常,步伐就可以报错退出了。
判定返回值的代码大抵如下:- // 返 回WAIT IO COMPLETION 表示一个重叠请求完成例程代码的结束,继续为更多的完成例程服务
- if (dwIndex == WAIT IO COMPLETION)
- {
- TRACE(" 重叠操作完成...\n");
- }
- else if (dwIndex == WAIT TIMEOUT)
- {
- TRACE(" 超时了,继续调用等待函数\n");
- }
- else
- {
- TRACE(" 出错了.…\n");
- }
复制代码 利用完成了之后,就阐明我们上一个利用已经乐成了。乐成了之后做什么?固然是继续投 递下一个重叠利用了。继续上面的循环。
- 第七步,继续回到第四步,在套接字上继续投递WSARecv 哀求,重复第4~7步。
- 第八步,处置惩罚吸收到的数据。
忙活了这么久,客户端传来的数据到底在那里吸收啊?怎么一点都没有提到呢?这个题目 提得好,我们写了这么多代码图什么呢?着实想要读取客户端的数据很简单,由于我们在 WSARecv 调用的时间通报了一个WSABUF 变量,用于生存网络数据,而在我们写的完成例 程回调函数内里就可以取到客户端传送来的网络数据了。体系在调用我们完成例程函数的时间 网络利用已经完成了,WSABUF 内里已经有我们必要的数据了,只是通过完成例程来举行后 期的处置惩罚。详细可以参考示例代码。此中,DataBuf.buf就是一个char*字符串指针。
下面我们来看两个例子,分别利用SleepEx 和 WSAWaitForMultipleEvents 函数。
【例10.6】基于完成例程的重叠I/O例 子(SleepEx 版 )- # define _WINSOCK_DEPRECATED_NO_WARNINGS
- #include <WINSOCK2.H>
- #include <stdio.h>
- #define PORT 6000
- #define MSGSIZE 1024
- #pragma comment(lib, "ws2_32.lib")
- typedef struct
- {
- WSAOVERLAPPED overlap;
- WSABUF Buffer;
- char szMessage[MSGSIZE];
- DWORD NumberOfBytesRecvd;
- DWORD Flags;
- SOCKET sClient;
- }PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
- DWORD WINAPI WorkerThread(LPVOID);
- void CALLBACK CompletionROUTINE(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
- SOCKET g_sNewClientConnection;
- BOOL g_bNewConnectionArrived = FALSE;
- int main()
- {
- WSADATA wsaData;
- SOCKET sListen;
- SOCKADDR_IN local, client;
- DWORD dwThreadId;
- int iaddrSize = sizeof(SOCKADDR_IN);
- // Initialize Windows Socket library
- WSAStartup(0x0202, &wsaData);
- // Create listening socket
- sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- // Bind
- local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
- local.sin_family = AF_INET;
- local.sin_port = htons(PORT);
- bind(sListen, (struct sockaddr*)&local, sizeof(SOCKADDR_IN));
- // Listen
- listen(sListen, 3);
- puts("服务器已经启动。。。\n");
- // Create worker thread
- CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
- while (TRUE)
- {
- // Accept a connection
- g_sNewClientConnection = accept(sListen, (struct sockaddr*)&client, &iaddrSize);
- g_bNewConnectionArrived = TRUE;
- printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
- }
- }
- DWORD WINAPI WorkerThread(LPVOID lpParam)
- {
- LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
- while (TRUE)
- {
- if (g_bNewConnectionArrived)
- {
- // Launch an asynchronous operation for new arrived connection
- lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(
- GetProcessHeap(),
- HEAP_ZERO_MEMORY,
- sizeof(PER_IO_OPERATION_DATA));
- lpPerIOData->Buffer.len = MSGSIZE;
- lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
- lpPerIOData->sClient = g_sNewClientConnection;
- WSARecv(lpPerIOData->sClient,
- &lpPerIOData->Buffer,
- 1,
- &lpPerIOData->NumberOfBytesRecvd,
- &lpPerIOData->Flags,
- &lpPerIOData->overlap,
- CompletionROUTINE);
- g_bNewConnectionArrived = FALSE;
- }
- SleepEx(1000, TRUE);
- }
- return 0;
- }
- void CALLBACK CompletionROUTINE(DWORD dwError,
- DWORD cbTransferred,
- LPWSAOVERLAPPED lpOverlapped,
- DWORD dwFlags)
- {
- LPPER_IO_OPERATION_DATA lpPerIOData = (LPPER_IO_OPERATION_DATA)lpOverlapped;
- if (dwError != 0 || cbTransferred == 0)
- {
- // Connection was closed by client
- closesocket(lpPerIOData->sClient);
- HeapFree(GetProcessHeap(), 0, lpPerIOData);
- }
- else
- {
- lpPerIOData->szMessage[cbTransferred] = '\0';
- puts(lpPerIOData->szMessage);//打印下收到的客户端信息
- send(lpPerIOData->sClient, lpPerIOData->szMessage, cbTransferred, 0);//再重新发回给客户端
- // Launch another asynchronous operation
- memset(&lpPerIOData->overlap, 0, sizeof(WSAOVERLAPPED));
- lpPerIOData->Buffer.len = MSGSIZE;
- lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
- WSARecv(lpPerIOData->sClient,
- &lpPerIOData->Buffer,
- 1,
- &lpPerIOData->NumberOfBytesRecvd,
- &lpPerIOData->Flags,
- &lpPerIOData->overlap,
- CompletionROUTINE);
- }
- }
复制代码 用完成例程来实现重叠I/O 比用变乱关照简单得多。在这个模子中,主线程只用不绝地接 受毗连即可;辅助线程判定有没有新的客户端毗连被创建,假如有,就为谁人客户端套接字激 活一个异步的WSARecv 利用,然后调用SleepEx使线程处于一种可告诫的等待状态,以使得 I/O完成后CompletionROUTINE 可以被内核调用。假如辅助线程不调用SleepEx, 则内核在完 成一次I/O 利用后,无法调用完成例程(由于完成例程的运行应该和当初激活WSARecv 异步 利用的代码在同一个线程之内)。
完成例程内的实当代码比力简单,它取出吸收到的数据,然后将数据原封不动地发送给客 户端,末了重新激活另一个WSARecv 异步利用。留意,在这里用到了“尾随数据”。我们在 调用 WSARecv 的时间,参数 lpOverlapped 实际上指向一个比它大得多的布局 PER_IO_OPERATION_DATA, 这个布局除了WSAOVERLAPPED 以外,还被我们附加了缓 冲区的布局信息,别的还包罗客户端套接字等告急的信息。如许,在完成例程中通过参数 lpOverlapped 拿到的不但仅是WSAOVERLAPPED 布局,尚有后边尾随的包罗客户端套接字和 吸收数据缓冲区等告急信息。如许的C 语言本事在背面先容完成端口的时间还会利用到。
【例10.7】基于完成例程的重叠I/O例 子(WSAWaitForMultipleEvents 版 )- #include <WinSock2.h>
- #include <process.h>
- #include <stdio.h>
- #pragma comment(lib,"ws2_32.lib")
- #define PORT 6000
- #define MAXBUF 128
- //自定义一个存放socket信息的结构体,用于完成例程中对OVERLAPPED的转换
- typedef struct _SOCKET_INFORMATION {
- OVERLAPPED Overlapped; //这个字段一定要放在第一个,否则转换的时候,数据的赋值会出错
- SOCKET Socket; //后面的字段顺序可打乱并且不限制字段数,也就是说你还可以多定义几个字段
- CHAR Buffer[MAXBUF];
- WSABUF wsaBuf;
- } SOCKET_INFORMATION, *LPSOCKET_INFORMATION;
- SOCKET g_sClient; //不断新加进来的client
- //打开服务器
- BOOL OpenServer(SOCKET* sServer)
- {
- BOOL bRet = FALSE;
- WSADATA wsaData = { 0 };
- SOCKADDR_IN addrServer = { 0 };
- addrServer.sin_family = AF_INET;
- addrServer.sin_port = htons(PORT);
- addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
- do
- {
- if (!WSAStartup(MAKEWORD(2, 2), &wsaData))
- {
- if (LOBYTE(wsaData.wVersion) == 2 || HIBYTE(wsaData.wVersion) == 2)
- {
- //在套接字上使用重叠I/O模型,必须使用WSA_FLAG_OVERLAPPED标志创建套接字
- //g_sServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
- *sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- if (*sServer != INVALID_SOCKET)
- {
- if (SOCKET_ERROR != bind(*sServer, (SOCKADDR*)&addrServer, sizeof(addrServer)))
- {
- if (SOCKET_ERROR != listen(*sServer, SOMAXCONN))
- {
- bRet = TRUE;
- break;
- }
- closesocket(*sServer);
- }
- closesocket(*sServer);
- }
- }
- }
- } while (FALSE);
- return bRet;
- }
- //完成例程
- void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
- {
- DWORD dwRecvBytes;
- DWORD dwFlag;
- //强制转换为我们自定义的结构,这里就解释了为什么第一个字段要是OVERLAPPED
- //因为转换后首地址肯定会相同,读取的数据一定会是Overlapped的数据
- //所以要先把Overlapped的数据保存下来,接下来内存中的数据再由系统分配到各个字段中
- LPSOCKET_INFORMATION pSi = (LPSOCKET_INFORMATION)Overlapped;
- if (dwError != 0) //错误显示
- printf("I/O operation failed with error %d\n", dwError);
- if (dwBytesTransferred == 0)
- printf("Closing socket %d\n\n", pSi->Socket);
- if (dwError != 0 || dwBytesTransferred == 0) //错误处理
- {
- closesocket(pSi->Socket);
- GlobalFree(pSi);
- return;
- }
- //如果已经发送完成了,接着投递下一个WSARecv
- printf("Recv%d:%s\n", pSi->Socket, pSi->wsaBuf.buf);
- dwFlag = 0;
- ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
- pSi->wsaBuf.len = MAXBUF;
- pSi->wsaBuf.buf = pSi->Buffer;
- if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlag, &(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
- {
- if (WSAGetLastError() != WSA_IO_PENDING)
- {
- printf("WSARecv() failed with error %d\n", WSAGetLastError());
- return;
- }
- }
- }
- //把client和完成例程绑定起来
- unsigned int __stdcall ThreadBind(void* lparam)
- {
- DWORD dwFlags;
- LPSOCKET_INFORMATION pSi;
- DWORD dwIndex;
- DWORD dwRecvBytes;
- WSAEVENT eventArry[1];
- eventArry[0] = (WSAEVENT)lparam;
- while (1)
- {
- //等待一个完成例程返回
- while (TRUE)
- {
- dwIndex = WSAWaitForMultipleEvents(1, eventArry, FALSE, WSA_INFINITE, TRUE);
- if (dwIndex == WSA_WAIT_FAILED)
- {
- printf("WSAWaitForMultipleEvents() failed with error %d\n", WSAGetLastError());
- return FALSE;
- }
- if (dwIndex != WAIT_IO_COMPLETION)
- break;
- }
- //重设事件
- WSAResetEvent(eventArry[0]);
- //为SOCKET_INFORMATION分配一个全局内存空间,相当于全局变量了
- //这里为什么要分配全局的呢?因为我们要在完成例程中引用socket的数据
- if ((pSi = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
- {
- printf("GlobalAlloc() failed with error %d\n", GetLastError());
- return 1;
- }
- //填充各个字段
- pSi->Socket = g_sClient;
- ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
- pSi->wsaBuf.len = MAXBUF;
- pSi->wsaBuf.buf = pSi->Buffer;
- dwFlags = 0;
- //投递一个WSARecv
- if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlags,
- &(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
- {
- if (WSAGetLastError() != WSA_IO_PENDING)
- {
- printf("WSARecv() failed with error %d\n", WSAGetLastError());
- return 1;
- }
- }
- printf("Socket %d got connected...\n", g_sClient);
- }
- return 0;
- }
- //接受client请求线程
- unsigned int __stdcall ThreadAccept(void* lparam)
- {
- SOCKET sServer = *(SOCKET*)lparam;
- WSAEVENT event = WSACreateEvent();
- _beginthreadex(NULL, 0, ThreadBind, event, 0, NULL);
- while (TRUE)
- {
- g_sClient = accept(sServer, NULL, NULL);
- if (g_sClient != INVALID_SOCKET)
- WSASetEvent(event);
- }
- return 0;
- }
- int main(int argc, char **argv)
- {
- SOCKET sServer = INVALID_SOCKET;
- puts("服务器已经启动...\n");
- if (OpenServer(&sServer))
- _beginthreadex(NULL, 0, ThreadAccept, &sServer, 0, NULL);
- Sleep(10000000);
- return 0;
- }
复制代码 参考册本《Visual C++2017网络编程实战》
|