说来惭愧第二季都快断更一年了,不是不更新是winsocket的IO模型对于新人太难了。以我的编程能力整整研究了两天才把重叠IO搞定
当调用 ReadFile 和 WriteFile 时,如果最后一个参数 lpOverlapped 设置为 NULL,那么线程就阻塞在这里,直到读写完指定的数据后,它们才返回。这样在读写大文件的时候,很多时间都浪费在等待 ReadFile 和 WriteFile 的返回上面。如果 ReadFile 和 WriteFile 是往管道里读写数据,那么有可能阻塞得更久,导致程序性能下降。
为了解决这个问题,windows 引进了重叠 I/O 的概念,它能够同时以多个线程处理多个 I/O
设置了 OVERLAPPED 参数后,ReadFile/WriteFile 的调用会立即返回,这时候你可以去做其他的事 所谓异步,系统会自动替你完成 ReadFile/WriteFile 相关的 I/O 操作。你也可以同时发出几个 ReadFile/WriteFile 的调用 所谓重叠。
需要注意的是,有两个方式可以用来管理重叠 IO 请求的完成情况(就是说接到重叠操作完成的通知):
事件对象通知(event object notification)
完成例程(completion routines) 注意:这里并不是完成端口
重叠 I/O 模型便能适用于安装了 Winsock 2 的所有 Windows 平台
比起阻塞、select、WSAAsyncSelect 以及 WSAEventSelect 等模型,重叠 I/O 模型使应用程序能达到更佳的系统性能。
我们常用的 send, sendto, recv, recvfrom 也都要被 WSASend, WSASendto, WSARecv, WSARecvFrom 替换掉
在正式开始之前,先说一下重叠 I/O 模型的编程步骤:
创建一个套接字,开始在指定的端口上监听连接请求
接受一个客户端进入的连接请求;
为接受的套接字新建一个 WSAOVERLAPPED 结构,并为该结构分配一个事件对象句柄,同时将该事件对象句柄分配给一个事件数组,以便稍后由 WSAWaitForMultipleEvents 函数使用。
在套接字上投递一个异步 WSARecv 请求,指定参数为 WSAOVERLAPPED 结构。
使用步骤 3)的事件数组,调用 WSAWaitForMultipleEvents 函数,并等待与重叠调用关联在一起的事件进入“已传信”状态(换言之,等待那个事件的“触发”)
WSAWaitForMultipleEvents 函数返回后,针对“已传信”状态的事件,调用 WSAResetEvent(重设事件)函数,从而重设事件对象,并对完成的重叠请求进行处理
使用 WSAGetOverlappedResult 函数,判断重叠调用的返回状态是什么
在套接字上投递另一个重叠 WSARecv 请求
重复步骤 5)~8)
重叠结构自然是重叠模型里的核心,其实真正需要操作的函数它是这么定义的
创建套接字的时候,假如使用的是 socket 函数,而非 WSASocket 函数,那么会默认设置 WSA_FLAG_OVERLAPPED 标志。
使用 WSASocket 函数创建套接字则需要 dwFlags 参数设置为 WSA_FLAG_OVERLAPPED
创建服务器的代码,以函数的形式进行封装,代码还是以前课程的代码
type
{服务器对象封装}
TScoketRecord = record
ServerAddr: TSockAddrIn;
Scoket: TSocket;
end;
type
{ 网络异常 }
TWinSocketException = class(Exception)
{ 构造方法 }
constructor Create(const Msg: string);
end;
{初始化网络库}
function InitSocket: Integer;
{初始化}
var
WSAData: TWSAData;
begin
// 定义当前使用网络库版本
if WSAStartup(WINSOCK_VERSION, WSAData) <> 0 then begin
Result := -1;
raise TWinSocketException.Create('网络库初始化失败');
end;
Result := 0;
end;
{启动服务器}
function CreateServerSocket(Address: string; Port: Integer): TScoketRecord;
var
ServerAddr: TSockAddrIn;
var
ScoketRecord: TScoketRecord;
begin
// 创建服务器端对象。在套接字上使用重叠 I/O 模型,必须使用 WSA_FLAG_OVERLAPPED 标志创建套接字
var Server := WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, nil, 0, WSA_FLAG_OVERLAPPED);
// 创建是否成功
if (Server = INVALID_SOCKET) then begin
raise TWinSocketException.Create('服务器创建失败');
Exit;
end;
// 给服务器指定 IP 和端口 ,组装信息
with ServerAddr do begin
sin_family := PF_INET;
// 端口号
sin_port := Port;
// 本机所有有可能的 IP 作为服务器端的 IP
sin_addr.S_addr := inet_addr(PAnsiChar(AnsiString(Address)));
end;
ScoketRecord.ServerAddr := ServerAddr;
if bind(Server, TSockAddr(ServerAddr), SizeOf(ServerAddr)) = SOCKET_ERROR then begin
raise TWinSocketException.Create('端口号被占用');
Exit;
end;
if listen(Server, SOMAXCONN) = SOCKET_ERROR then begin
raise TWinSocketException.Create('监听失败');
Exit;
end;
ScoketRecord.Scoket := Server;
Result := ScoketRecord;
end;
核心代码,主要分为两个部分
无限监听新客户端连接并绑定重叠结构对象(发送或者接收数据)以线程的方式书写
无限监听重叠结构的事件信号,WSAWaitForMultipleEvents 就是遍历是否有信号发生,同时将数据填充到缓冲区(以线程的方式书写)
此处为了便于 IO 完成时,需要读取缓冲区的数据,获取之前连接成功的客户端对象和重叠结构,所以我封装了新的结构体
//在读取数据时需要一一对应,所以封装成结构体比较方便
TOverlappedDomain = record
//事件数组
Events: array[0..WSA_MAXIMUM_WAIT_EVENTS] of TWSAEVENT;
//客户端数组
Clients: array[0..WSA_MAXIMUM_WAIT_EVENTS] of TSocket;
//重叠结构数组
Overlappeds: array[0..WSA_MAXIMUM_WAIT_EVENTS] of TWSAOVERLAPPED;
end;
为了便于测试,当前项目是以控制台应用书写,并在 DelphiXE10.4.2 下测试通过
var
//缓冲区
buf: array[0..1023] of Char;
//记录事件总数
EventTotal: Integer = 0;
var
wBuf: WSABUF;
var
//封装的结构体变量
OverlappedDomain: TOverlappedDomain;
begin
wBuf.len := 1024;
wBuf.buf := @buf;
if (InitSocket() <> 0) then begin
Writeln('初始化失败');
Readln;
Exit;
end;
//无线侦听客户端连接
TThread.CreateAnonymousThread(
procedure
begin
//1) 创建一个套接字,开始在指定的端口上监听连接请求;
var ScoketRecord := CreateServerSocket('127.0.0.1', 10086);
Writeln('服务器启动完成,等待客户端连接');
while true do begin
//2) 接受一个客户端进入的连接请求;
var AddrSize := sizeof(ScoketRecord.ServerAddr);
var ClientSocket := WSAAccept(ScoketRecord.Scoket, @ScoketRecord.ServerAddr, @AddrSize, nil, 0);
//新客户端加入列表
OverlappedDomain.Clients[EventTotal] := ClientSocket;
// 当客户端连接成功时,显示一下客户端的 IP
var CustomWinSocket := TCustomWinSocket.Create(ClientSocket);
Writeln('客户端 IP:' + CustomWinSocket.RemoteAddress);
//3)为接受的套接字新建一个 WSAOVERLAPPED 结构,并为该结构分配一个事件对象句柄
// Events[0] := WSACreateEvent();
//创建事件对象并加入列表
OverlappedDomain.Events[EventTotal] := WSACreateEvent();
//重叠结构绑定事件
OverlappedDomain.Overlappeds[EventTotal].hEvent := OverlappedDomain.Events[EventTotal];
Writeln('事件对象:' + IntToStr(OverlappedDomain.Overlappeds[EventTotal].hEvent));
//4) 在套接字上投递一个异步 WSARecv(接收数据) 请求,指定参数为 WSAOVERLAPPED 结构 ,投递一个的请求
var rec := 0;
var flg := 0;
var nRecv := WSARecv(ClientSocket, @wBuf, 1, Cardinal(rec), Cardinal(flg), @OverlappedDomain.Overlappeds[EventTotal], nil);
//10014—WSAEFAULT 地址无效。传给 Winsock 函数的指针地址无效。若指定的缓冲区太小,也会产生这个错误
writeln('WSARecv 函数:' + IntToStr(WSAGetLastError()) + ',' + IntToStr(nRecv));
EventTotal := EventTotal + 1;
end;
end).Start;
//无线侦听信号
TThread.CreateAnonymousThread(
procedure()
begin
while True do begin
//5) 使用步骤 3)的事件数组,调用 WSAWaitForMultipleEvents 函数,并等待与重叠调用关联在一起的事件进入“已传信”状态
var nRes := WSAWaitForMultipleEvents(EventTotal, @OverlappedDomain.Events, FALSE, WSA_INFINITE, FALSE);
//WSAWaitForMultipleEvents 的返回值,减去预定义的值 WSA_WAIT_EVENT_0,得到具体的引用值(即索引位置)。
if (WSA_WAIT_FAILED = nRes) or (WSA_WAIT_IO_COMPLETION = nRes) then begin
continue;
end;
writeln('WSAWaitForMultipleEvents 函数:' + IntToStr(nRes) + ',' + IntToStr(nRes - WSA_WAIT_EVENT_0));
// 信号重置
WSAResetEvent(OverlappedDomain.Events[nRes - WSA_WAIT_EVENT_0]);
//7) 使用 WSAGetOverlappedResult 函数,判断重叠调用的返回状态是什么
var flag := 0;
var bytes: Cardinal;
WSAGetOverlappedResult(OverlappedDomain.Clients[nRes - WSA_WAIT_EVENT_0], @OverlappedDomain.Overlappeds[nRes - WSA_WAIT_EVENT_0], bytes, True, Cardinal(flag));
//判断接收到的字节数
if (bytes = 0) then begin
closesocket(OverlappedDomain.Clients[nRes - WSA_WAIT_EVENT_0]);
WSACloseEvent(OverlappedDomain.Events[nRes - WSA_WAIT_EVENT_0]);
//数据已经不可读取(客户端发生异常)
EventTotal := EventTotal - 1;
continue;
end;
//读取数据
if (bytes > 0) then
Writeln('客户端序号:' + IntToStr(nRes - WSA_WAIT_EVENT_0) + ',内容:', PChar(wBuf.buf));
//8) 在套接字上投递另一个重叠 WSARecv 请求;
var rec := 0;
var flg := 0;
WSARecv(OverlappedDomain.Clients[nRes - WSA_WAIT_EVENT_0], @wBuf, 1, Cardinal(rec), Cardinal(flg), @OverlappedDomain.Overlappeds[nRes - WSA_WAIT_EVENT_0], nil);
end;
end).Start;
// WSACleanup();
Readln;
end.
需要说一下以下几个函数的区别
accept、WSAAccept 是同步操作,AcceptEx 是异步操作
WSAAccept 函数在 accept 函数基础上添加了条件函数判断是否接受客户端连接
AcceptEx 是异步的,可以同时发出多个 AcceptEx 请求
说一下引用的单元
uses
System.Generics.Collections,
System.Classes,
System.Win.ScktComp,
Windows,
WinSock2,
WinSock,
Sysutils;
客户端的代码就不放了,随便搞一个发送消息的就可以,同时当前只有读(也就是接收),并没有写。
至此基于事件通知方式的重叠 IO 完成,当然限于篇幅当前文章的代码可能有遗漏,可以给我发消息或者等稍后的视频······
在此特别感谢群里的无为,起初我不知道 WSABUF 类型的定义,直到看了他的C++代码
放一张运行效果图吧
推一下另一个公众号,有兴趣的朋友可以去看看