基于对网络的一点兴趣,突然之间想总结以下windows网络相关的一些内容。
通常的网络io模型有四种
1 同步阻塞(blocking io)
2 同步非阻塞(non-blocking io)默认创建的socket都是阻塞的,非阻塞io要求socket被设置为nonblock,
3 io多路复用(io multiplexing),又称为异步阻塞io,经典的reactor模式。
4 异步io(asynchronous io)经典的proactor模式,异步非阻塞io。
以前只知道四种模式,却不明其意,今天在网上看到了一些帖子,学习了一下。
同步和异步的概念描述的是用户线程和内核的交互方式:同步是指用户线程发起io请求后需要等待或轮询内核io操作完成后才能继续执行。异步是指用户线程发起io请求后仍继续执行,当内核io操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核io的操作方式:阻塞是指io操作需要彻底完成后才返回到用户空间;而非阻塞是指io操作被调用后立即返回给用户一个状态值,无需等到io操作彻底完成。
io发生时涉及的对象和步骤。对于一个网络io来说,涉及到两个系统对象,一个是调用方的进程或线程,另一个就是系统内核,步骤如下:
1 等待数据准备
2 将数据从内核拷贝到进程中。
下面依据不同的模式来讲解以下
1 同步阻塞io
linux中默认所有的socket都是blocking,一个典型的读操作流程大致如下:应用程序发起recvfrom通过系统调用调用到内核,此时应用程序会被阻塞,内核等待数据的到来,数据到来之后拷贝到应用程序提供的用户空间而后返回。应用程序接触阻塞状态,继续运行。
大部分的socket接口都是阻塞型的,所谓阻塞型接口是指系统调用不反悔调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错才返回。
实际上,除非特别指定,几乎所有的io接口都是阻塞型的,当发起io操作时,线程将被阻塞,无法执行任何运算或响应任何网络请求。一个简单的改进方案时在服务器端使用多线程。其目的是让每个连接都拥有独立的线程,这样任何一个连接的阻塞都不会影响其它的连接。int accept(int s,struct sockaddr *addr,socklen_t *addrlen);输入参数s是监听套接字,如果有请求,则将该请求加入请求队列。调用accept接口正是从socket s 的请求队列抽取第一个连接信息,创建被一个与s同类的socket并返回句柄。然而如果成百上千个连接请求,无论多线程还是多进程都会严重占用资源,降低响应效率。此时一种新的替代方案,使用线程池或者连接池。这两种方案在一定程度上降低了系统开销, 究其原因,池始终有上限,当请求数据大大超过时,其效果也会收到影响。此时,非阻塞可以尝试来解决这个问题。
2 同步非阻塞io
当用户进程发出read操作时,如果内核数据没有准备好,则不会阻塞用户进程,而是立刻返回一个error。用户进程收到error之后,指导没有准备好,可以再次发送read,一旦内核中数据准备好了,并且又再次收到用户进程的操作,则立刻将数据拷贝到内存并返回。fcntl(fd,F_SETFL,O_NONBLOCK);
在非阻塞状态下,recv()接口在被调用后立即返回,返回值代表了不同的含义。
recv返回值>0,表示接收数据完毕,返回值就是接受到的字节数
recv返回0 ,表示连接已经正常断开
recv返回-1,且errno等于EAGAIN,表示recv操作还没执行完成。
recv返回-1,且errno不等于EAGAIN,表示操作遇到系统错误errno。
此时服务器线程通过循环调用recv接口,可以在单个线程内实现对所有连接的数据接受操作,但是循环调用recv将大幅度推高CPU占用。实际情况中select 多路复用模式,可以一次检测多个连接是否活跃。
3 io多路复用
select单个进程能够监视的文件描述符的数量存在限制,并且需要复制大量的句柄数据结构,需要遍历整个列表才可以发现哪些句柄发生事件,触发方式为水平触发,如果没有完成对一个已经就绪的文件描述符进行io操作,那么之后每次select调用还是会将这些文件描述符通知进程。
poll模型解决了文件描述的的限制,但是依然有后面三个问题。
在多路复用模型中,对于每一个socket,一般都设置成non-blocking,但是整个yoghurt的process其实是一直被苏泽的,只不过进程是被select阻塞,而不是被socket io阻塞。
4 异步io
用户进程发起read之后,立刻做其他事。另一方面,从内核的角度,收到一个异步读之后,立刻返回,不会对应用进程产生任何block。然后内核会等待数据准备完成,然后将数据拷贝到用户内存,当一切完成后,内核会给用户进程发送一个信号,告知read操作完成。
真正的异步io更需要操作系统支持。在io多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据,处理数据。而在异步io模型中,当用户县城收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区中,内核在io完成后通知用户线程直接使用即可。
说完简单的网络模型,下面来说以下windows下overlapped io.
重叠模型 overlapped io 也是一种io模型,可以运行在任何支持winsock2的windows 平台,而不像完成端口只支持NT系统。
比起阻塞/select/WSAAsyncSelect以及WSAEventSelect等模型,重叠io模型使应用程序能达到更号的系统性能。使用重叠模型的应用程序通知缓冲区手法系统直接使用数据,也就是说,如果应用程序投递了一个10K大小的缓冲区来接受数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。而这四种模型中,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区。
重叠模型是让应用程序使用重叠数据结构WSAOVERLAPPED,一次投递一个或多个winsock io请求。针对这些提交的请求,在他们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。
有两个方法可以用来管理重叠io请求的完成情况
1 事件对象通知
2 完成例程,并不是完成端口哦。
既然是基于事件通知,就要求将windows事件对象与WSAOVERLAPPED结构关联在一起,通俗一点讲,就是,需要将send/sendto/recv/recvfrom 替换为WSASend/WSASendto/WSARecv/WSARecvFrom,此时他们的参数中都有一个Overlapped参数,可以假设把我们的WSARecv操作绑定到这个重叠结构上,提交一个请求,其它的事情就交给重叠结构去操心,而其中重叠结构又要与windows的事件对象绑定在一起,这样我们调用完WSARecv就可以坐享其成了。等到重叠操作完成之后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要数据了。
WSAOVERLAPPED结构
typedef struct _WSAOVERLAPPED{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
}WSAOVERLAPPED,*LPWSAOVERLAPPED;
需要把WSARecv等操作投递到一个重叠结构上,而我们又需要一个与重叠结构绑定在一起的事件对象来通知我们操作的完成,需要把事件对象绑定到重叠结构上。
WSAEVENT event;
WSAOVERLAPPED AcceptOverlapped;
event = WSACreateEvent();
ZeroMemory(&AcceptOverlapped,sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = event;
在重叠模型中,接收数据WSARecv要比recv复杂的多
int WSARecv(SOCKET s,LPWSABUF lpBuffers,DWORD dwBufferCount,LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,LPWSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpComletionRoutine);
因为需要事件来通知我们重叠操作的完成,所以自然需要一个等待事件的函数与之配套
DWORD WSAWaitForMultipleEvents(
DWORD cEvents;
const WSAEvents *lphEvents;
BOOL fWaitAll;
DWORD dwTimeout;
BOOL fAlertable
);
返回值如下:
WSA_WAIT_TIMEOUT:需要继续等待
WSA_WAIT_FAILED:出现了错误,请检查cEvents 和 lphEvents两个参数是否有效
WSA_WAIT_EVENT_0 :如果事件数组中有某一个事件被传信,则会返回这个事件的索引值,索引值减去预定义值才是事件在事件数组中的位置。
BOOL WSAGetOverlappedResult(
SOCKET s;
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer, 本次重叠操作实际接收的字节数(如返回0,则表示通信对方已经关闭连接)
BOOL fWait,
LPDWORD lpdwFlags
);
下面可以看一下实现重叠模型的步骤
1 定义变量
#define DATA_BUFSIZE 4096 // 接收缓冲区大小
SOCKET ListenSocket, // 监听套接字
AcceptSocket; // 与客户端通信的套接字
WSAOVERLAPPED AcceptOverlapped; // 重叠结构一个
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 用来通知重叠操作完成的事件句柄数组
WSABUF DataBuf[DATA_BUFSIZE] ;
DWORD dwEventTotal = 0, // 程序中事件的总数
dwRecvBytes = 0, // 接收到的字符长度
Flags = 0; // WSARecv的参数
2 创建一个监听套接字
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2),&wsaData);
ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); //创建TCP套接字
SOCKADDR_IN ServerAddr; //分配端口及协议族并绑定
ServerAddr.sin_family=AF_INET;
ServerAddr.sin_addr.S_un.S_addr =htonl(INADDR_ANY);
ServerAddr.sin_port=htons(11111);
bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字
listen(ListenSocket, 5);
3 接收一个入站请求
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;
4 建立并初始化重叠结构
// 创建一个事件
// dwEventTotal可以暂时先作为Event数组的索引
EventArray[dwEventTotal] = WSACreateEvent();
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 置零
AcceptOverlapped.hEvent = EventArray[dwEventTotal]; // 关联事件
char buffer[DATA_BUFSIZE];
ZeroMemory(buffer, DATA_BUFSIZE);
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer; // 初始化一个WSABUF结构
dwEventTotal ++; // 总数加一
5 投递WSARecv请求
if(WSARecv(AcceptSocket ,&DataBuf,1,&dwRecvBytes,&Flags,
& AcceptOverlapped, NULL) == SOCKET_ERROR)
{
// 返回WSA_IO_PENDING是正常情况,表示IO操作正在进行,不能立即完成
// 如果不是WSA_IO_PENDING错误,就大事不好了~~~~~~!!!
if(WSAGetLastError() != WSA_IO_PENDING)
{
// 那就只能关闭大吉了
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwEventTotal]);
}
}
6 用WSAWaitForMultipleEvents函数等待重叠操作返回的结果
DWORD dwIndex;
// 等候重叠I/O调用结束
// 因为我们把事件和Overlapped绑定在一起,重叠操作完成后我们会接到事件通知
dwIndex = WSAWaitForMultipleEvents(dwEventTotal,
EventArray ,FALSE ,WSA_INFINITE,FALSE);
// 注意这里返回的Index并非是事件在数组里的Index,而是需要减去WSA_WAIT_EVENT_0
dwIndex = dwIndex – WSA_WAIT_EVENT_0;
7 使用WSAResetEvent函数重设当前这个用完的事件对象
WSAResetEvent(EventArray[dwIndex]);
8 使用WSAGetOverlappedResult函数取得重叠调用的返回状态
DWORD dwBytesTransferred;
WSAGetOverlappedResult( AcceptSocket, AcceptOverlapped ,
&dwBytesTransferred, FALSE, &Flags);
// 先检查通信对方是否已经关闭连接
// 如果==0则表示连接已经,则关闭套接字
if(dwBytesTransferred == 0)
{
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwIndex]); // 关闭事件
return;
}
9 接收数据,并且继续6-9的操作,直到通讯完成。
下面分享完整的代码
#include <winsock2.h>
#include <stdio.h>
#define PORT 6000
#define MSGSIZE 1024
#define MAXIMUM_WAIT_OBJECTS 10
#pragma comment (lib, "Ws2_32.lib")
BOOL WinSockInit();
void Cleanup(int);
DWORD WINAPI WorkerThread(LPVOID);
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
}MY_WSAOVERLAPPED, *LPMY_WSAOVERLAPPED;
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];
WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];
LPMY_WSAOVERLAPPED g_pPerIODataArr[MAXIMUM_WAIT_OBJECTS];
int main()
{
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
DWORD dwThreadId;
int iaddrSize = sizeof(SOCKADDR_IN);
// 初始化环境
WinSockInit();
// 创建监听socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 绑定
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(sListen, 3);
// 创建工作者线程
CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
while (TRUE)
{
// 接受连接
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("\nAccepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
g_CliSocketArr[g_iTotalConn] = sClient;
// 分配一个单io操作数据结构
g_pPerIODataArr[g_iTotalConn] =
(LPMY_WSAOVERLAPPED)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(MY_WSAOVERLAPPED));
//初始化单io结构
g_pPerIODataArr[g_iTotalConn]->Buffer.len = MSGSIZE;
g_pPerIODataArr[g_iTotalConn]->Buffer.buf = g_pPerIODataArr[g_iTotalConn]->szMessage;
g_CliEventArr[g_iTotalConn] = g_pPerIODataArr[g_iTotalConn]->overlap.hEvent = WSACreateEvent();
// 开始一个异步操作
int ret= WSARecv(
g_CliSocketArr[g_iTotalConn],
&g_pPerIODataArr[g_iTotalConn]->Buffer,
1,
&g_pPerIODataArr[g_iTotalConn]->NumberOfBytesRecvd,
&g_pPerIODataArr[g_iTotalConn]->Flags,
&g_pPerIODataArr[g_iTotalConn]->overlap,
NULL);
g_iTotalConn++;
}
closesocket(sListen);
WSACleanup();
return 0;
}
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int ret, index;
DWORD cbTransferred;
while (TRUE)
{
//判断是否有信号
ret = WSAWaitForMultipleEvents(g_iTotalConn, g_CliEventArr, FALSE, 1000, FALSE);
if (ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT)
continue;
index = ret - WSA_WAIT_EVENT_0;
//手动设置为无信号
WSAResetEvent(g_CliEventArr[index]);
//判断该重叠调用到底是成功,还是失败
WSAGetOverlappedResult(
g_CliSocketArr[index],
&g_pPerIODataArr[index]->overlap,
&cbTransferred,
TRUE,
&g_pPerIODataArr[g_iTotalConn]->Flags);
//若调用失败
if (cbTransferred == 0)
Cleanup(index);//关闭客户端连接
else
{
//将数据保存到szMessage里边
g_pPerIODataArr[index]->szMessage[cbTransferred] = '\0';
printf("\nrecv: %s" , g_pPerIODataArr[index]->szMessage );
//这里直接就转发回去了
send(g_CliSocketArr[index], g_pPerIODataArr[index]->szMessage,cbTransferred, 0);
// 进行另一个异步操作
WSARecv(g_CliSocketArr[index],
&g_pPerIODataArr[index]->Buffer,
1,
&g_pPerIODataArr[index]->NumberOfBytesRecvd,
&g_pPerIODataArr[index]->Flags,
&g_pPerIODataArr[index]->overlap,
NULL);
}
}
return 0;
}
void Cleanup(int index)
{
closesocket(g_CliSocketArr[index]);
WSACloseEvent(g_CliEventArr[index]);
HeapFree(GetProcessHeap(), 0, g_pPerIODataArr[index]);
if (index < g_iTotalConn - 1)
{
g_CliSocketArr[index] = g_CliSocketArr[g_iTotalConn - 1];
g_CliEventArr[index] = g_CliEventArr[g_iTotalConn - 1];
g_pPerIODataArr[index] = g_pPerIODataArr[g_iTotalConn - 1];
}
g_pPerIODataArr[--g_iTotalConn] = NULL;
}
BOOL WinSockInit()
{
WSADATA data = {0};
if(WSAStartup(MAKEWORD(2, 2), &data))
return FALSE;
if ( LOBYTE(data.wVersion) !=2 || HIBYTE(data.wVersion) != 2 ){
WSACleanup();
return FALSE;
}
return TRUE;
}