网络与通信学习笔记
几种I/O模型的总结:
一.选择(select)模型
描述:主要使用select函数来管理I/O,设计源于UNIX系统.
关键函数:select()
1.
int select( int nfds, //忽略,仅是为了与Berkeley套接字兼容 fd_set FAR *readfds, //指向一个套接字集合,用来检查其可"读"性 fd_set FAR *writefds, //指向一个套接字集合,用来检查其可"写"性 fd_set FAR *exceptfds, //指向一个套接字集合,用来检查"错误" const struct timeval FAR *timeout //指定此函数等待的最长时间,如果为NULL,则最长时间为无限大 );
调用成功,返回发生网络事件的所有套接字数量的总和.如果超过了时间限制,返回0,失败则返回SOCKET_ERROR.
此函数可以确定一个或者多个套接字的状态.如果套接字上没有网络事件发生,则进入等待状态,以便执行同步IO.
typedef struct fd_set { u_int fd_count; // how many are SET? 下面数组的大小 SOCKET fd_array[FD_SETSIZE]; // an array of SOCKETs 套接字句柄数组 } fd_set;
操作fd_set套接字的宏:
FD_ZERO(*set): 初始化set为空集合.集合在使用前总应该清空
FD_CLR(s,*set):从set移除套接字s
FD_ISSET(s,*set):检查s是不是set的成员,如果是返回TRUE
FD_SET(s,*set):添加套接字到集合
当select返回时,它通过移除没有未决IO操作的套接字句柄修改每个fd_set集合.
如要测试套接字s是否可读时,必须将它添加到readfds集合,然后等待select函数返回.当select调用完成后再确定s是否仍然还在readds集合中,如果还在,就说明s可读了.3 个参数中的任意两个都可以是NULL(至少有一个不是NULL),任何不是NULL的集合必须至少包含一个套接字句柄
select最后一个参数说明:
struct timeval { long tv_sec; // seconds 等待多少秒 long tv_usec; // and microseconds 等待多少毫秒 };
二.WSAAsyncSelect模型
描述:WSAAsyncSelect模型允许应用程序以Windows消息的形式接收网络事件通知.这个模型是为了适应Windows胡消息驱动环境而设置的,现在很多对性能要求不高的网络应用程序都采用了WSAAsyncSelect模型,MFC中的CSocket类也使用了它.它的缺点是:单个Windows函数处理上千个客户请求时,服务器势必会受到影响.
关键函数:WSAAsyncSelect(),WindowProc
1.
int WSAAsyncSelect( SOCKET s, //套接字句柄 HWND hWnd, //指定一个窗口句柄.套接字的通知消息将被发送到与其对应的窗口过程中 unsigned int wMsg, //网络事件到来时接收到的消息ID.可以在WM_USER以上的数值中任选一个用作ID long lEvent //指定哪些通知码需要发送 );
WSAAsyncSelect自动把套接字设为非阻塞模式,并且为套接字绑定一个窗口句柄,当有网络事件发生时,便向这个窗口发送消息.
最后一个参数lEvent指定了要发送的通知码,可以是如下取值的组合:
FD_READ:套接字接收到对方发送过来的数据包,表明这时可以去读套接字了.
FD_WRITE:数据缓冲区满后再次变空时(短时间内发送数据过多,便会造成数据缓冲区变满),Winsock接口通过该通知码通知应用程序,表示可以继续发送数据了.
FD_ACCEPT:监听中的套接字检测到有连接进
FD_CONNECT:如果用套接字连接对方的主机,当连接动作完成以后会接收到这个通知码
FD_CLOSE:检测到套接字对应的连接关闭.
例如:
::WSAAsyncSelect(sListen,hWnd,WM_SOCKET,FD_ACCEPT | FD_CLOSE); //WM_SOCKET为自定义消息
关于窗口处理函数的说明:
wParam参数指定了发生网络事件的套接字句柄,lParam参数的低字位指定了发生的网络事件(可以用WSAGETSELECTEVENT宏取出),高字位包含了任何可能出现的错误代码(可以用WSAGETSELECTERROR宏取出)
先检查错误代码是否为0(不为0说明有错误 ),不为0再检查通知码.
三.WSAEventSelect模型
描述:
和WSAAsyncSelect类似,也接收FD_XXX类型的消息,不过WSAEventSelect并不是依靠Windows消息驱动机制,而是由事件对象句柄通知.
使用这个模型的基本思路的为感兴趣的一组网络事件(FD_ACCEPT,FD_READ等)创建一个事件对象(创建事件对象的函数为WSACreateEvent),再调用WSAEventSelect函数将网络事件对象关联起来.
当网络事件发生时,winsock使相应的事件对象受信,之后调用WSAEnumNetworkEvents函数便可以获取到底发生了什么网络事件.
关键函数: WSACreateEvent(),WSAEventSelect(),WSAWaitForMultipleEvents()
1.创建事件对象:
WSAEVENT WSACreateEvent (void);//返回一个手工重置的事件对象句柄
2.将指定的一组网络事件与事件对象关联在一起
int WSAEventSelect( SOCKET s, //套接字句柄 WSAEVENT hEventObject, //事件对象句柄 long lNetworkEvents //感兴趣的FD_XXX网络事件的组合 );
3.网络事件与事件对象关联后,应用程序便可以使用WSAWaitForMultipleEvents函数在一个或多个事件对象上等待了,当所等待的事件对象受信,或者指定的时间过去时,此函数返回.
DWORD WSAWaitForMultipleEvents( DWORD cEvents, //指定下面lphEvents所指的数组中事件对象句柄个数 const WSAEVENT FAR *lphEvents, //指向一个事件 对象句柄数组 BOOL fWaitAll, //指定是否等待所有事件对象都变成受信状态 DWORD dwTimeout, //指定要等待的时间,WSA_INFINITE为无穷大 BOOL fAlertable //在使用WSAEventSelect模型时可以忽略,应设为FALSE );
WSAWaitForMultipleEvents最多支持WSA_MAXIMUM_WAIT_EVENTS个对象(默认为64),也就是说这个I/O模型在一个线程中同一时间差最多能支持64个套接字,如果需要使用这个模型管理更多套接字,就需要创建额外的工作线程.
WSAWaitForMultipleEvents返回值:
1.WSA_WAIT_TIMEOUT:超时
2.WSA_WAIT_FAILED:函数调用失败
3.返回值-WSA_WAIT_EVENT_0: (如果fWaitAll参数设为FALSE) 第一个受信的事件的索引(index).
有可能同时有几个事件对象受信,返回值-WSA_WAIT_EVENT_0仅仅返回最前面那个.如果要保证受信的每一个事件对象都得到处理,
需要对第一个受信及后面的事件对象再次调用WSAWaitForMultipleEvents来确定其状态.
3.一旦事件对象受信,那么找到与之对应的套接字句柄,然后调用WSAEnumNetwordEvents函数即可查看发生了什么网络事件.
int WSAEnumNetworkEvents( SOCKET s, //套接字句柄 WSAEVENT hEventObject, //对应的事件对象句柄.如果提供了此参数,本函数会重置这个事件对象的状态 LPWSANETWORKEVENTS lpNetworkEvents //指向一个WSANETWORKEVENTS结构 );
typedef struct _WSANETWORKEVENTS { long lNetworkEvents; //指定已发生的网络事件(如FD-ACCEPT,FC_READ等) int iErrorCode[FD_MAX_EVENTS]; //与lNetworkEvents相关的出错代码 } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
iErrorCode参数是个数组,数组的每个成员对应着一个网络事件的出错代码.可以用FD_READ_BIT,FD_WRITE_BIT等标示来检查FD_READ,FD_WRITE等事件发生时的出错代码.如:
int nIndex = ::WSAWaitForMultipleEvents(nEventTotal,eventArray,FALSE,WSA_INFINITE,FALSE); nIndex = nIndex - WSA_WAIT_EVENT_0; for (int i=nIndex;i<nEventTotal;i++) { nIndex = ::WSAWaitForMultipleEvents(1,&eventArray[i],TRUE,0,FALSE); if (nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT) { continue; } else { WSANETWORKEVENTS netEvent; //第二个参数用于标识需要复位的相应事件对象,本函数为重置这个事件对象的状态 //第三个参数用来取得套接字上发生的网络事件和相关的出错代码 ::WSAEnumNetworkEvents(sockArray[i],eventArray[i],&netEvent); if(netEvent.lNetworkEvents & FD_ACCEPT) { //检查是否有错误 if (netEvent.iErrorCode[FD_ACCEPT_BIT] == 0 ) { ...... SOCKET sockNew = ::accept(sockArray[i],(LPSOCKADDR)&addrAccept,&nSize); WSAEVENT eventNew = ::WSACreateEvent(); //将客户端的连接加入进来 ::WSAEventSelect(sockNew,eventNew,FD_CLOSE | FD_READ | FD_WRITE); ...... } } //读数据 else if(netEvent.lNetworkEvents & FD_READ) { //检查是否有错误 if(netEvent.iErrorCode[FD_READ_BIT] == 0) { ...... int nRecv = ::recv(sockArray[i],szText,strlen(szText),0); ...... } } else { ...... } } }
四.重叠(Overlapped)I/O模型:
描述:
提供了更好的系统性能.
设计思想:
1.应用程序使用重叠结构一次投递一个或者多个异步I/O请求(即所谓的重叠I/O)
2.提交的I/O请求完成之后,与之关联的重叠数据结构中的事件对象受信.
3.应用程序使用WSAGetOverlappedResut函数获取重叠操作结果.
为了使用重叠I/O模型,必须调用特定的重叠I/O函数创建套接字,在套接字上传输数据.
关键函数:WSASocket(),WSACreateEvent(),WSASend(),WSARecv,AcceptEx(),WSAGetOverlappedResult()
1.创建套接字
SOCKET WSASocket( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, //指定下层服务提供者,可以是NULL GROUP g, //保留 DWORD dwFlags //指定套接字属性.要使用重叠I/O模型,必须指定WSA_FLAG_OVERLAPPED ); //例: SOCKET sListen = ::WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
2.传输数据
I/O操作函数都接收一个WSAOVERLAPPED结构类型的参数,这些函数被调用之后一般会立即返回,它们依靠应用程序传递的WSAOVERLAPPED结构管理I/O请求的完成.应用程序有两种方法可以接收到重叠I/O的请求操作完成的通知:
1.在与WSAOVERLAPPED结构关联的事件对象上等待,I/O操作完成后,此事件对象受信,这是最常用方法.
2.使用lpCompletionRoutine指向的完成例程.完成例程是一个自定义函数,I/O操作完成后,Winsock便去调用它.这种方法很少使用,将lpCompletionRoutine 设为NULL即可.
int WSASend( SOCKET s, //套接字句柄 LPWSABUF lpBuffers, //WSABUF结构的数组.每个WSABUF结构包含一个缓冲区指针和对应缓冲区的长度 DWORD dwBufferCount, //第二个参数数组的数量 LPDWORD lpNumberOfBytesSent, //如果I/O操作立即完成的话,此参数取得实际传输数据的字节数 DWORD dwFlags, //标志 LPWSAOVERLAPPED lpOverlapped, //于此I/O操作关联的WSAOVERLAPPED结构 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //指定一个完成例程 );
int WSARecv( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
3.接收连接
BOOL AcceptEx( SOCKET sListenSocket, //监听套接字句柄 SOCKET sAcceptSocket, //指定一个未使用的套接字,在这个套接字上接受新的连接 PVOID lpOutputBuffer, //指定一个缓冲区,用来取得在新连接上接收到第一块数据,服务器的本地地址和客户端地址 DWORD dwReceiveDataLength, //lpOutputBuffer所指缓冲区的大小,需减去下面两个参数的长度BUFF_SIZE- (sizeof(sockaddr_in) + 16)*2).如果为0,只要客户端和服务器一建立连接就返回,而不用等待第一块数据 DWORD dwLocalAddressLength, //缓冲区中,为本地地址预留的长度.必须必最大地址长度多16(sizeof(sockaddr_in) + 16) DWORD dwRemoteAddressLength, //缓冲区中,为远程地址预留的长度.必须必最大地址长度多16(sizeof(sockaddr_in) + 16) LPDWORD lpdwBytesReceived, //用来取得接收到数据的长度 LPOVERLAPPED lpOverlapped //指定用来处理本请求的OVERLAPPED结构,不能为NULL );
AcceptEx如果投递请求成功完成,则执行了如下3个操作:
1.接收了新的连接
2.新连接的本地地址和远程地址都会返回
3.接收到了远程主机发来的第一块数据(第四个参数dwReceiveDataLength需不为0,否则连接一建立就返回,而不用等待第一块数据)
AcceptEx需要调用者提供两个套接字,一个指定了在哪个套接字上监听(sListenSocket参数),另一个指定了再哪个套接字上接受连接(sAcceptSocket参数. 使用前用WSASocket创建).
如果提供了接受缓冲区,AcceptEx投递的重叠操作直到接受到连接并且读到数据之后才会返回.
AcceptEx函数(Microsoft扩展函数都是这样)是从Mswsock.lib库中导出的.为了能直接调用它,而不用连接到Mswsock.lib库,需要使用WSAIoctl函数将AcceptEx函数加载到内存.WSAIoctl是ioctlsocket函数的扩展,它可以用重叠I/O.
GUID guidAcceptEx = WSAID_ACCEPTEX; DWORD dwBytes; WSAIoctl(pListen->s, SIO_GET_EXTENSION_FUNCTION_POINTER, &guidAcceptEx, sizeof(guidAcceptEx), &pListen->lpFnAcceptEx, sizeof(pListen->lpFnAcceptEx), &dwBytes, NULL, NULL);
WSAOVERLAPPED结构:
typedef struct _OVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED;
前三个参数由系统内部调用.
hEvent允许应用程序为这个操作关联一个事件对象句柄.重叠I/O的事件通知方法需要将Windows事件对象关联到WSAOVERLAPPED结构.
当使用WSAOVERLAPPED结构进行I/O调用时,如调用WSASend和WSARecv,这些函数立即返回.
通常情况下,调用会失败,返回值为SOCKET_ERROR,可使用WSAGetLastError函数获得出错状态.
如果出错状态为WSA_IO_PENDING,表示I/O操作正在进行.
在以后的一个时间,需要通过关联到WSAOVERLAPPED的事件对象上等待以确定什么时候一个重叠I/O请求完成.
WSAOVERLAPPED在重叠I/O请求的初始化和随后的完成之间提供了交流媒介.
当重叠请求最终完成以后,与之关联的事件对象受信,等待函数返回,应用程序可以使用WSAGetOverlappedResult函数取得重叠操作的结果.
BOOL WSAGetOverlappedResult( SOCKET s, //套接字句柄 LPWSAOVERLAPPED lpOverlapped, //重叠操作启动时指定的WSAOVERLAPPED LPDWORD lpcbTransfer, //用来取得实际传输字节的数量 BOOL fWait, //指定是否要等待未决的重叠操作 LPDWORD lpdwFlags //用于取得完成状态 );
当客户端断开和服务器的连接时,服务器的WSAWaitForMultipleEvents会返回,并且WSAGetOverlappedResult会返回为FALSE,由此可判断客户端关闭.
四.IOCP与可伸缩网络程序
IOCP(I/O completion port,I/O完成端口)是伸缩性最好的一种I/O模型.广泛应用于各种高性能服务器,如Apache等.
I/O完成端口是应用程序使用线程池处理异步I/O请求的一种机制.处理多个并发异步I/O请求时,使用I/O完成端口比在I/O请求时创建线程更快更有效.
完成端口实际上一个Windows I/O结构,它可以接收多种对象的句柄,如文件对象,套接字对象等.
I/O完成端口基本流程:
1.应用程序发出异步I/O请求(不会马上响应)
2.异步I/O请求完成,设备驱动将把这些工作项目安排到完成端口
3.在完成端口上等待的线程池处理这些完成I/O
创建完成端口对象:
HANDLE CreateIoCompletionPort ( HANDLE FileHandle, // handle to file HANDLE ExistingCompletionPort, // handle to I/O completion port ULONG_PTR CompletionKey, // completion key DWORD NumberOfConcurrentThreads // number of threads to execute concurrently );
此函数有两个不同功能:
1.创建一个完成端口对象.
2.将一个或者多个文件句柄(网络编程的话就是套接字句柄)关联到I/O完成端口对象.
最初创建完成端口对象时,NumberOfConcurrentThreads定义了允许在完成端口上同时执行的线程的数量.为0表示系统允许的线程数量与处理器一样多.(注:这个线程应该是由系统调用?)
成功使用CreateIoCompletionPort 创建完成端口对象之后,便可以向这个对象关联套接字句柄了(关联套接字还是用CreateIoCompletionPort 函数),参考上面说的此函数的两个功能.
在关联套接字之前,需要先创建一个或者多个工作线程(称为I/O服务线程.当用来执行并处理投递到完成端口上的I/O请求).
工作线程数量的问题:如果工作线程会遇到阻塞,那就应该创建被CreateIoCompletionPort 指定的数量还要多得的线程.
CreateIoCompletionPort 的第三个参数CompletionKey通常用来描述与套接字相关的信息,所以称它为句柄唯一数据(per-handle).
向完成端口关联套接字句柄后,便可以通过在套接字上投递重叠发送或接收请求处理I/O了.
在这些I/O操作完成时,I/O系统会向完成端口对象发送一个完成通知封包.I/O完成端口以先进先出的方式为这些封包排队.
应用程序使用GetQueuedCompletionStatus函数取得有事件发生的套接字的信息
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, // handle to completion port LPDWORD lpNumberOfBytes, // bytes transferred PULONG_PTR lpCompletionKey, // file completion key LPOVERLAPPED *lpOverlapped, // buffer DWORD dwMilliseconds // optional timeout value );
通过lpNumberOfBytes参数得到传输的字节数量,通过lpCompletionKey参数得到与套接字关联的句柄唯一(per-handle)数据(套接字第一次与完成端口关联时传递给CreateIoCompletionPort的CompletionKey参数)
通过lpOverlapped得到投递I/O请求时使用的重叠对象地址,进一步得到I/O唯一(per-I/O)数据.
lpOverlapped指向一个OVERLAPPED结构,结构的后面便是我们成为per-I/O的数据,可以使工作线程处理完成封包时想要知道的任何信息.
Windows Socket2定义了一些扩展函数,这些扩展函数从MSWSOCK.DLL导入,不建议直接链接这个DLL,这会将程序绑定在MicrosoftWinSock提供者上,应该使用WSAIoctl函数动态加载他们.
VOID GetAcceptExSockaddrs( PVOID lpOutputBuffer, //指向传递给AcceptEx函数接收客户第一块数据的缓冲区 DWORD dwReceiveDataLength, //lpOutputBuffer的大小,必须和传递给AcceptEx函数的一致 DWORD dwLocalAddressLength, //为本地地址预留的空间大小,必须和传递给AcceptEx函数的一致 DWORD dwRemoteAddressLength, //为远程地址预留的控件大小,必须和传递给AcceptEx函数的一致 LPSOCKADDR *LocalSockaddr, //用来返回连接的本地地址 LPINT LocalSockaddrLength, //用来返回本地地址的长度 LPSOCKADDR *RemoteSockaddr, //用来返回远程地址 LPINT RemoteSockaddrLength //用来返回远程地址的长度 );
GetAcceptExSockaddrs是专门为AcceptEx函数准备的,它将AcceptEx接收的第一块数据中的本地和远程机器的地址返回给客户.
BOOL TransmitFile( SOCKET hSocket, // 一个套接字句柄,将在这个套接字上传输文件数据 HANDLE hFile, //已打开的文件句柄,将传输这个文件.如为NULL,lpTransmitBuffers将会被传输 DWORD nNumberOfBytesToWrite, //要传输的字节数.如为0,就传输整个文件 DWORD nNumberOfBytesPerSend, //每次发送的数据块大小,如为0,就选择默认的大小 LPOVERLAPPED lpOverlapped, //如套接字是以重叠方式创建的,指定这个参数可以进行异步I/O.默认情况下,套接字是以重叠方式创建的. LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers, //指定在文件数据发送之前和之后要发送的数据.TRANSMIT_FILE_BUFFERS可设置这两种数据. DWORD dwFlags //标志 );
lpOverlapped结构是可选的,如果省略了此结构,文件传输将会从当前文件指针位置开始,否则,OVERLAPPED结构中的偏移量(Offset,OffsetHigh)将指定操作从哪儿开始.
dwFlags:可选,它将影响文件操作的行为,可使用的标志有如下6个(可同时指定多个):
TF_DISCONNECT: 所有的文件数据排队准备传输后,开启一个传输级别的断开.
TF_REUSE_SOCKET:准备重新使用这个套接字句柄.当TransmitFile请求完成时,这个套接字可以作为AcceptEx中的客户端套接字使用.仅当同时指定了TF_DISCONNECT,这个标志才有效
TF_USE_DEFAULT_WORKER:指示文件传输使用系统默认的线程,这对传输大文件有用
TF_USE_SYSTEM_THREAD:这个选项页指示操作使用系统线程来处理
TF_USE_KERNEL_APC:指示应该使用内核异步程式调用(Asynchronous Procedure Call,APC),而不使用工作线程来处理TransmitFile请求.注意,内核APC仅当应用程序处于等待状态时才会被调用
TF_WRITE_BEHIND:指示TransmitFile请求应该立即返回,即便是数据还没有被远端确认.这个标志不应该和TF_DISCONNECT或者TF_REUSE_SOCKET标志一起使用.
如果TF_DISCONNECT和TF_DISCONNECT同时被指定,文件和(或)缓冲区数据要被传输,一旦操作完成,套接字断开.套接字可以作为客户端套接字在AcceptEx中使用,或者作为连接套接字再ConnectEx函数中使用.
这样做好处很大,可以节省套接字创建的开销(创建的开销十分昂贵).
可以令hFile和lpTransmitBuffers都为NULL,且TF_DISCONNECT和TF_DISCONNECT被指定,此时调用不会发送任何数据,但是允许套接字再AcceptEx中被重用.
BOOL PACAL TransmitPackets ( SOCKET hSocket, //已建立连接的套接字,并不需要是面向连接的套接字 LPTRANSMIT_PACKETS_ELEMENT lpPacketArray, //封包元素素组,描述要要传输的数据 DWORD nElementCount, //lpPacketArray数组中元素的数量 DWORD nSendSize, //发送操作每次发送的数据大小 LPOVERLAPPED lpOverlapped, //同TransmitFile一样,可选的重叠结构 DWORD dwFlags //同TransmitFile一样.但是名称以TP开头 );
TransmitPackets既可以发送文件,也可以发送内存缓冲区中的数据
lpPacketArray的类型是一个TRANSMIT_PACKETS_ELEMENT数组,描述如下:
struct _TRANSMIT_PACKETS_ELEMENT { ULONG dwElFlags; //缓冲类型.或者是内存或者文件,取值为下面的定义的值 #define TP_ELEMENT_MEMORY 1 #define TP_ELEMENT_FILE 2 #define TP_ELEMENT_EOP 4 ULONG cLength; //指定从文件的内存缓冲区中要传输多少字节.如果元素包含文件之后,cLength为0表示传输整个文件 union { struct { LARGE_INTEGER nFileOffset; HANDLE hFile; }; PVOID pBuffer; }; } TRANSMIT_PACKETS_ELEMENT;
dwElFlags用来描述封包数组元素包含的缓冲区类型.TP_ELEMENT_EOP可以和另外两个标志中的一个按位或,指示在发送操作中这个元素不应该和后面的元素混合起来.嵌在结构的联合包含一个内存缓冲区的指针或者一个打开文件的句柄,以及文件的偏移值. 可以再多个元素中使用同一个文件句柄,这种情况下,偏移值可以指定从哪里开始传输.如果偏移值为-1表示从文件当前文件指针位置开始传输.