您的位置:首页 > 理论基础 > 计算机网络

windows网络编程(第2版)读书笔记

2011-12-08 18:27 330 查看
在CSDN有电子书(英文)和源码下载。在此首先感谢上传的大侠!

第一章 Introduction to Winsock

一般来说WSA开头的函数是Winsock2新加的,少数例外,以下几个在winsock1就有了

WSAStartup, WSACleanup, WSARecvEx, and WSAGetLastError


头文件和lib文件

WINSOCK2.H WS2_32.LIB // For WinSock 2
MSWSOCK.H WSOCK32.LIB // For WinSock 1
MSWSOCK.H MSWSOCK.DLL // For Microsoft-specific programming extensions normally used for developing high performance winsock applications.


Initializing Winsock

int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
int WSACleanup(void);


WSAStartup函数用来初始化winsock,其中第一个参数是winsock的版本,这个参数决定了你的程序会load winsock的哪个dll。一般用一个宏MAKEWORD(x,y)来指定。

第二个参数其实是函数返回的信息,由该函数填写其中的信息。关于其详细信息,请参考其它资料。

WSACleanup用来取消还没完成的,等待中的Winsock调用并且释放资源。

Error Checking and Handling

出错代码都是int,在以下两个文件中定义:

winsock.h
winsock2.h // Contains defined in winsock.1 and some more is added


以下两个函数用来得到/设置错误代码

int WSAGetLastError (void);
void WSASetLastError(__in  int iError);


下面是应用目前所得到的知识所能写出的代码,随便看看

#include <winsock2.h>
void main(void)
{
WSADATA wsaData;
// Initialize Winsock version 2.2
if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
{
// NOTE: Since Winsock failed to load we cannot use WSAGetLastError to determine the specific error for why it failed. Instead we can rely on the return status of SAStartup.
printf("WSAStartup failed with error %d\n", Ret);
return;
}

// Setup Winsock communication code here

// When your application is finished call WSACleanup
if (WSACleanup() == SOCKET_ERROR)
{
printf("WSACleanup failed with error %d\n", WSAGetLastError());
}
}


协议的地址

通过sockaddr_in指定协议

struct sockaddr_in
{
short           sin_family; // AF_NET for IP address family
u_short         sin_port; // port
struct in_addr  sin_addr; //  four-byte ipv4 address(unsinged long)
char            sin_zero[8]; // only for padding
};


这个函数能把字符串表示的10.3.4.5ip地址变换成unsinged long类型的ip address

unsigned long inet_addr(const char FAR *cp);


Byte Ordering (big endian, little endian)

// h: host byte order. n: network byte order.
// l: long. s: short
/* host to network */
u_long htonl(u_long hostlong); // The return value is the final

int WSAHtonl(
SOCKET s,
u_long hostlong,
u_long FAR * lpnetlong // This is the returned long
);

u_short htons(u_short hostshort);

int WSAHtons(
SOCKET s,
u_short hostshort,
u_short FAR * lpnetshort // This is the returned short
);

/* network to host */
u_long ntohl(u_long netlong);

int WSANtohl(
SOCKET s,
u_long netlong,
u_long FAR * lphostlong
);

u_short ntohs(u_short netshort);

int WSANtohs(
SOCKET s,
u_short netshort,
u_short FAR * lphostshort
);


Creating a socket

这里只给出一种最基本的创建socket的方法:

SOCKET socket (
int af, // AF_INET for ipv4
int type, // SOCK_STREAM for TCP/IP SOCK_DGRAM for UDP/IP
int protocol // IPPROTO_UDP or IPPROTO_TCP
);


Connection-Oriented Communication

Winsock Server: socket()/WSASocket() -> Bind() -> Listen() -> accept()/WSAAccept() -> shutdown() -> clostSocket()

Winsock Client: socket/WSASocket() -> Address resolution -> connect/WSAConnect() -> shutdown() -> clostSocket()

Server:

构造一个socket并绑定到一个地址,注意地址加端口在这里叫做一个“名字”

SOCKET               s;
SOCKADDR_IN          tcpaddr;
int                  port = 5150;

s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

tcpaddr.sin_family = AF_INET;
tcpaddr.sin_port = htons(port);
tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY means the socket is binded to all address of the computer(The server has 2 network card for example)

bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));


绑定到一个地址以后,用Listen函数让这个socket开始监听。参数backlog的意思是,只有这么多连接可以pending在哪里等待处理,更多进来的连接会失败。

int listen(SOCKET s, int    backlog);


Cleint连接过来以后要用accept函数接收请求。注意accept之后,你可以关闭这个socket,用accept返回的那个socket去传送数据,也可以让原来的那个socket继续accept新的连接(毕竟它还是在那里listen)

// Reterns another SOCKET object. To communicate with the client, use the returned socket
SOCKET accept(
SOCKET s,  // The socket
struct sockaddr FAR* addr,  // The output var. Contains the client information
int FAR* addrlen
);


Client:

创建一个Socket(与server一样)

Connect到一个server:

int connect(
SOCKET s,
const struct sockaddr FAR* name, // Server address and port。Note that this is called “name”
int namelen
);


Server 与 Client 都创建好了,可以用Send 和 Recv函数来发送和接收数据,具体的有一下这些。这里就不具体讲API了

int send(
SOCKET s,
const char FAR * buf,
int len,
int flags
);
int WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
int WSASendDisconnect (
SOCKET s,
LPWSABUF lpOutboundDisconnectData
);
int recv(
SOCKET s,
char FAR* buf,
int len,
int flags
);
int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
int WSARecvDisconnect(
SOCKET s,
LPWSABUF lpInboundDisconnectData
);


读写完毕,应该关闭socket,关闭应该用两个函数:

int shutdown(
SOCKET s,
int how // SD_RECEIVE, SD_SEND, SD_BOTH
);
int closesocket (SOCKET s);


Connectionless Communication

Receiver: socket()/WSASocket() -> Bind() -> recvfrom() -> clostSocket()

Sender:socket()/WSASocket() -> SendTo() -> closeSocket()

注意UUP还有一种发送方式,就是不用recvFrom(),而是先调用connect然后调用recv()。这种方式我感觉是为了和TCP保持接口统一而做的。你不一定要使用这种接口。另外,如果用了这种方式,你这个socket就只能从一个peer接受UDP数据了(因为你“connect”到了这个peer)。

UDP的socket也不需要shutdown().

这里不给出具体代码和API了,有了前面写TCP的基础,UDP只有更简单。

一些其它API

/* Get socket connected to remote peer address info. */
int getpeername(
SOCKET s, // The socket for the connection. (The socket represents connection to another machine
struct sockaddr FAR* name,  // return value
int FAR* namelen
);
/* Get local socket address info. */
int getsockname(
SOCKET s,  // The local interface of a given socket on local host
struct sockaddr FAR* name,
int FAR* namelen
);
/* Create a ProtocalInfo info for sharing the socket between processes */
int WSADuplicateSocket(
SOCKET s,
DWORD dwProcessId,  // process ID that intents to use the socket
LPWSAPROTOCOL_INFO lpProtocolInfo // Return value. To be passed to the other process.
);


Windows CE

WinCE用的是Winsock 1.1其他内容略过没看

第二章 Winsock Design

System Architecture

讲的比较简单,看的不是很明白。总之WinSock2.dll调用了什么service provider,每种service provider实现了某种网络协议,并且还有socket什么的用来实现其他外部协议之类的。

Protocol Characteristics

message oriented,Stream oriented:

这里的区别在于一个叫做“preserve message boundary”的概念。所谓“preserve message boundary”就是说你send 64byte,然后马上send 128 byte,那么接收的时候你要用两次recv,分别接收64和128byte的包。即使你接收的时候提供的buffer可能是1024byte的,你仍然每次接收一个包。TCP不是“preserve message boundary”的,所以如果用TCP,接收的时候可能在一个buffer中就一下子接受了64+128byte的数据。

Pseudo Stream

就是说一个message oriented 的接收者,通过某种方式让他可以接收任意大小的数据(也就是说不是每次接收一个包,而是可以接收多个),那么这就叫做Pseudo Stream。干什么用,怎么用,书中没讲。。。

Connection-Oriented and Connectionless

略,就是TCP和UDP

Reliability and Ordering

略,就是TCP和UDP

Graceful Close

是说在connection-roiented protocols里面,要关闭连接什么的,做一些扫尾工作。

Broadcast Data

是connectionless协议的一种传播方式。

Multicast Data

在IP协议里,多播是广播的一种变形。要参与多播的机器要加入一个多播组,第九章,会详细介绍。

Quality of Service

第十章讲QOS

Partial Messages
这个概念是针对message oriented protocol的。就是说协议提供了一种把目前接收到的不完全的message提交给reader。如果没有partial message的支持,只有当收到一个完整的message的时候才会吧message

整个提交给reader。

Routing Considerations

windows操作系统所支持的不能route的协议只有NetBEUI。如果需要route而协议又不支持,那么数据会被直接丢掉。

Winsock Category

int WSAEnumProtocols (
LPINT lpiProtocols,
LPWSAPROTOCOL_INFO lpProtocolBuffer,
LPDWORD lpdwBufferLength
);
typedef struct _WSAPROTOCOL_INFO {
DWORD             dwServiceFlags1;
DWORD             dwServiceFlags2;
DWORD             dwServiceFlags3;
DWORD             dwServiceFlags4;
DWORD             dwProviderFlags;
GUID              ProviderId;
DWORD             dwCatalogEntryId;
WSAPROTOCOLCHAIN  ProtocolChain;
int               iVersion;
int               iAddressFamily;
int               iMaxSockAddr;
int               iMinSockAddr;
int               iSocketType;
int               iProtocol;
int               iProtocolMaxOffset;
int               iNetworkByteOrder;
int               iSecurityScheme;
DWORD             dwMessageSize;
DWORD             dwProviderReserved;
TCHAR             szProtocol[WSAPROTOCOL_LEN + 1];
} WSAPROTOCOL_INFO, FAR * LPWSAPROTOCOL_INFO;


方法是调用WSAEnumProtocols两次。第一次后两个参数传递0,那么调用失败但是最后的参数告诉我们所需要的buffer的大小,第二次再传递正确的buffer。
另外值得一提的是当我们构造socket的时候,所传递的参数实际上是到Enum出来的数组里去做匹配,如果匹配到了,就是用所得到的协议。

第三章 IP协议

IPv4

ipv4是美国ARPA在60年代发明的。

关于这里讲到的ipv4的知识点,简单罗列如下:

Addressing; Addressing Classes (A B C D E); unicast; Multicast; broadcast; ARP, ICMP, IGMP, port

IPv6



Address and Name Resolution(地址和名字解析)

有一些函数同时支持ipv4和ipv6

int getaddrinfo(
const char FAR *nodename, // host name or literal address
const char FAR *servname, // port number or service name(ftp, telnet)
const struct addrinfo FAR *hints, // One or more options
struct addrinfo FAR *FAR *res // returned results
);


使用该函数解析主机名+端口号

SOCKET        s;
struct addrinfo    hints, *result;
int        rc;

memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
rc = getaddrinfo("foobar", "5001", &hints, &result);
if (rc != 0) { /*unable to resolve the name*/}
s = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (s == INVALID_SOCKET) {/*socket API failed*/}
rc = connect(s, result->ai_addr, result->ai_addrlen);
if (rc == SOCKET_ERROR) {/*connect API failed*/}
freeaddrinfo(result);


Note that the above code called freeaddrinfo(result) to free the memory because result is dynamically allocated by the API.

-未完待续

Writing IP Version–Independent Programs



第四章 Other Supported Protocols



第五章 Winsock I/O Methods

socket mode

决定winsock函数的行为。socket有两种mode:blocking 和 non-blocking。

I/O model

是应用程序管理一个或多个socket上I/O的方法。I/O model有六种:blocking, select, WSAAsyncSelect, WSAEventSelect, overlapped I/O, completion

port. 不同的windows版本支持特定的I/O model。具体去查资料

socket的blocking mode

当recv的时候,如果没有数据recv会使当前线程挂起在哪里。

blocking server的描述:

// 主函数里首先根据【某种】API获得本机的所有地址(网络编程里应该叫名字才对)
// 给每个地址(名字)都创建一个socket并且将地址与socket绑定。这个创建的socket叫做ListenSocket
// 为每个Socket都创建一个线程,这个线程叫做ServerListenThread,每个Socket作为线程的参数传进去。这样ServerListenThread就能操作主线程创建的Socket
// 主线程这时候把任务都交给那些ListenThread了,因此主线程调用WaitForMultipleObjects(那些ListenThreads)从而挂起,直到所有ListenThread都结束之后,做一些清理工作,然后结束应用程序。由于WaitForMultipleObjects设置了超时,所以在超时返回的时候我们可以打印一些统计信息,然后继续等待。
// 至此,主线程描述完了,剩下的工作都在ServerListenThread里。
// ListenThread有两种工作模式。一种是UDP,一种是TCP。
// 如果是UDP,那么ListenThread它会创建一个新的线程,叫做ReceiveThread。同时ServerListenThread会调用SendThread函数,从而变身为Send线程(listen和send线程是同一线程)。
// 如果是TCP,ListenThread会listen然后accep(阻塞)在那里。当有新的连接进来,那么就用accept返回的那个socket创建一个send线程和一个recv线程。
// 至此为止,无论是TCP还是UDP,我们都有了是用同一个socket的Send线程和Recv线程。由于Send线程和Receive线程要共同操作socket和数据,因此他们共享一个叫做ConnectionObject的数据结构(这个结构是自己定义的,不是windows API)。这个结构里包含了需要共享的socket handle,用于同步的semaphore,critical section等。这个共享的数据结构是在创建线程的时候作为参数传递进去的。
// Send 和 Recv线程做的事情就比较简单了。Send等待一个Semaphore, 得到以后就从共享的bufferlist里取出一个buffer,发出去。Recv做的事情就是从网络接收数据,接收以后就release semaphore。这里无论是Send还是Recv操作都是阻塞的,但是两个线程合作,我们可以及时的接收并处理数据,CPU并没有闲置。这是一个典型的生产者消费者模型


blocking client比较简单,主线程创建两个线程,分别是send和recv。两者共享socket,在其上发送和接收。主线程等待两个线程结束后做清理工作,应用程序结束。
socket的non-blocking mode

non-blocking mode 的socket是这样创建的:

SOCKET        s;
unsigned long ul = 1;
int           nRet;

s = socket(AF_INET, SOCK_STREAM, 0);
nRet = ioctlsocket(s, FIONBIO, (unsigned long *) &ul);
if (nRet == SOCKET_ERROR)
{
// Failed to put the socket into non-blocking mode
}


non-blocking mode主要用于asynced Socket IO model。下面会逐一介绍

Socket I/O models

1. blocking

上面描述的blocking server就是blocking方法的实现。中心思想就是两个或多个线程,各自调用阻塞函数,通过线程间的锁机制通讯。缺点是socket不能多,你可以看到该程序给每个socket都加了两个线程。线程太多,开销会很大。

2. The Select Model

这个model叫做select的原因是该model的中心点就是用select函数来操作IO。

Select model是winsock 1.1里面从Berkeley socket实现中抄过来。

/*fd_set is a group of sockets*/
int select(
int nfds, // ignored,只是为了和berkeley socket app兼容
fd_set FAR * readfds, // for checking reading
fd_set FAR * writefds, // for checking writing
fd_set FAR * exceptfds, // for checking out of band data
const struct timeval FAR * timeout
);


一个最简单的用select来读一个socket是这样写的

SOCKET  s;
fd_set  fdread; // 把要等待读操作的socket放在这个set里。当select返回的时候,如果你的socket还在这里,说明你可以读了
int     ret;
// 假设这里你初始化了socket

// Manage I/O on the socket
while(TRUE) {
// Always clear the read set before calling select()
FD_ZERO(&fdread);
// Add socket s to the read set
FD_SET(s, &fdread);
if ((ret = select(0, &fdread, NULL, NULL, NULL)) == SOCKET_ERROR){// Error condition}
if (ret > 0) {
if (FD_ISSET(s, &fdread)) {// A read event has occurred on socket s}
}
}


select函数也是阻塞的,但是用select的好处是你不必为每一个socket都开一个线程,select函数帮助你挑出能进行IO操作的socket。

select函数也有缺点:1. 有socket数量上限。默认是64个,你可以自己改宏FD_SETSIZE但是系统的实现上总有一个最大值,可能是1024但是也不能保证。2. 当你socket很多的时候,每次调用select等待新的IO操作的时候你都要把你的socket填到某个fd_set中。这本身也有点慢。

随书源码中针对select的例子是non-blocking里的nbserver.cpp。这个源码其实很简单,只有一个线程。通过将所有socket,包括tcp listen socket accept得到的socket都加入一个数组,用select去找出能读或写的socket进行读写。但是这个例子有一个问题就是我搜遍源文件也没找到把socket设置成non-blocking的语句!我认为这是源码里的一个错误(如果我错了,请指出,我非常感谢!)。

3. The WSAAsyncSelect Model

这个model的实现原理是:利用windows的message机制来通知network event。

MFC CSocket就用这个model.

#define WM_SOCKET WM_USER + 1
#include <winsock2.h>
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE  PrevInstance, LPSTR lpCmdLine, int nCmdShow){
WSADATA wsd;
SOCKET Listen;
SOCKADDR_IN InternetAddr;
HWND Window;

// Create a window and assign the ServerWinProc below to it
Window = CreateWindow();
// Start Winsock and create a socket get addr and bind ...

// Set up window message notification on the new socket using the WM_SOCKET define above. Listen is the socket
WSAAsyncSelect(Listen, Window, WM_SOCKET,  FD_ACCEPT │ FD_CLOSE);
listen(Listen, 5);
// Translate and dispatch window messages until the application terminates
while (1) {// ...}
}

BOOL CALLBACK ServerWinProc(HWND hDlg,UINT wMsg,WPARAM wParam, LPARAM lParam) {
SOCKET Accept;
switch(wMsg) {
case WM_PAINT: // Process window paint messages
break;
case WM_SOCKET:
// Determine whether an error occurred on the socket by using the WSAGETSELECTERROR() macro
if (WSAGETSELECTERROR(lParam)) {
// Display the error and close the socket
closesocket( (SOCKET) wParam);
break;
}
// Determine what event occurred on the socket
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
Accept = accept(wParam, NULL, NULL);
// Prepare accepted socket for read, write, and close notification
WSAAsyncSelect(Accept, hDlg, WM_SOCKET, FD_READ │ FD_WRITE │ FD_CLOSE); break;
case FD_READ:
// Receive data from the socket in wParam
break;
case FD_WRITE:
// The socket in wParam is ready for sending data
break;
case FD_CLOSE:
closesocket( (SOCKET)wParam); break;
}
break;
}
return TRUE;
}


上面这个例子只是一个框架,要真看这个model的话还是去看看源码里的程序,要考虑的周到一些。另外值得一提的是,里面还通过windows的PostMessage函数来读完socket上的数据。
WSAAsyncSelect的好处是它不像select model一样有socket数量限制。缺点是为了用这个model,你必须要有一个window(或者dialog)。并且将所有socket的处理都放在windproc函数里,有点不太协调。

4. The WSAEventSelect Model

这个model和WSAAsyncSelect model很类似,甚至event的定义都是一样的。只是接收event的不再是windows窗口的消息处理函数,而是一个event。

创建和绑定event的代码如下:

WSAEVENT WSACreateEvent(void);
int WSAEventSelect(
SOCKET s,
WSAEVENT hEventObject, // 就是WSACreateEvent返回的EVENT
long lNetworkEvents // Events 和WSAAsyncSelect的一样
);


和socket绑定也很简单

int WSAEventSelect(
SOCKET s,
WSAEVENT hEventObject,
long lNetworkEvents
);


WSAEventSelect有两种状态和两种mode。所谓的状态就是signaled 和 non-signaled。两种mode就是手动reset和自动reset模式。

如果是手动reset(默认就是手动)模式,那么你接收完了event以后要用函数reset之。程序结束要调用一个函数释放event资源。

BOOL WSAResetEvent(WSAEVENT hEvent);
BOOL WSACloseEvent(WSAEVENT hEvent);


用这个函数来等待event变成signaled(类似于等待一个windows 同步对象)

DWORD WSAWaitForMultipleEvents(
DWORD cEvents, // event数量
const WSAEVENT FAR * lphEvents, // event指针的数组
BOOL fWaitAll,
DWORD dwTimeout, // 设置为0则直接返回,用来poll状态
BOOL fAlertable // 在WSAEventSelect里应该设置为false
);


一个线程中WSAEventSelect只支持最多64个event,如果要更多,就要create更多线程。

另外如果设置fWaitAll为false,那么处于数组lphEvents后面的event可能会“饥饿”。解决办法是如果WSAWaitForMultipleEvents返回后,不仅仅处理返回值代表的那个event,还要看看其它event是否也被signal了(即可用)。怎么看其它event是否signal了呢?答案是用所返回的event后面的每个event再分别调用WSAWaitForMultipleEvents一次,并且设置dwTimeout为0来poll这些event的状态。

得到被signaled的那个event:

Index = WSAWaitForMultipleEvents(...);
MyEvent = EventArray[Index - WSA_WAIT_EVENT_0];


得到激活的原因(FD_READ还是FD_WRITE...)

int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject, // 这个会被reset。不想reset请传一个NULL。
LPWSANETWORKEVENTS lpNetworkEvents // 包含所有激活的event type 类型 和 error code
);

typedef struct _WSANETWORKEVENTS
{
long lNetworkEvents; // FD_READ,FD_WRITE等的“或”结果
int  iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;


得到激活的event类型和可能的错误

// Process FD_READ notification
if (NetworkEvents.lNetworkEvents & FD_READ)
{
if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0)
{
printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]);
}
}


WSAEventSelect的优点是简单且不像WSAAsyncSelect一样依赖于一个windows窗口。缺点是一个线程只支持64个event object。要支持更多则需要维护一个thread pool。

关于WSAEventSelect的源码,我只是大概浏览了一下,详细的就不写了。但是值得一提的是源码里根本没有用WSAWaitForMultipleEvents,而是直接用了WaitForMultipleObjects。我上网查了一下,两者是等价的,只是ws2_32.dll 暴露出来的是用WSA开头的名字。当然既然写的是网络的程序,最好还是按照网络的规范来比较好。

5. The Overlapped Model

这个模型比前面的四个效率都高。windows上有些设备使用ReadFile和WriteFile函数。这些设备上有一种overlapped I/O 机制, 这种机制是用来在前述的设备上执行I/O操作的。(翻译起来怎么这么累。看英文!The model's overall design is based on the Windows overlapped I/O mechanisms available for performing I/O operations on devices using the ReadFile and WriteFile functions.)

要用overlapped model,需要:

  a) 创建一个带overlapped flag的socket并把这个socket bind到本地网络接口

  b) 在调用某些winsock函数的时候,传递一个WSAOVERLAPPED结构。这些函数有WSASend, WSASendTo, WSARev, WSARecvFrom, WSAIoctrl, WSARecvMsg, AcceptEx, ConnectEx, TransmitFile, TransmitPackets, DisconnectEx, WSANSPIoctl。加了WSAOVERLAPPED参数以后,这些函数调用会直接返回,不管你设置它为blocking还是non-blocking。

  c) 通过两种方式之一来管理overlapped request的完成(b中不是说这些函数会直接返回嘛)。这两种方式是:等待event object notification和使用completion routines。b中所列的12个函数中前六个都能接受一个WSAOVERLAPPED_COMPLETION_ROUTINE作为参数。加了这个参数那么当overlapped request完成以后,系统会调用这个routine。

现在看看等待event的完成通知方式

event就存在于WSAOVERLAPPED结构里面

typedef struct WSAOVERLAPPED
{
DWORD    Internal;
DWORD    InternalHigh;
DWORD    Offset;
DWORD    OffsetHigh;
WSAEVENT hEvent; // 在这里
} WSAOVERLAPPED, FAR * LPWSAOVERLAPPED;


用WSAWaitForMultipleEvents函数来等待这个event,你就知道什么时候这个overlapped requets已经完成。

还有一个函数用来取得overlapped reqeust的结果

BOOL WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,  // 就是那个带event的 WSAOVERLAPPED
LPDWORD lpcbTransfer,  // 传输的数据大小
BOOL fWait,  // fWait为true并且overlapped request没完成,那么本函数就会等待
LPDWORD lpdwFlags
);


现在看看Completion Routine的完成通知方式

如书中所述,用这种方式分如下步骤:

创建一个socket,绑定之

accept得到一个进来的socket

为2中进来的socket创建一个WSAOVERLAPPED 结构

调用WSARecv函数,把3中创建的结构和一个completion routine传进去

调用WSAWaitForMultipleEvents,并且在参数里把fAlertable设置为TRUE。那么当overlapped request完成以后,completion routine就会执行,WSAWaitForMultipleEvents会返回WSA_IO_COMPLETION,然后按照需求在调用其它的WSARecv函数,但是注意在主函数里要有一个while一直在调用WSAWaitForMultipleEvents等待其他overlapped request的完成。

检查WSAWaitForMultipleEvents是不是返回了WSA_IO_COMPLETION

重复5到6

overlapped mode性能比较高,这是因为:

overlapped model中, 如果调用recv的时候你传进去一个10k的buffer,那么当数据到达这个socket的时候,这些数据是直接copy到你传进去的那个10k的buffer中的. 而在前面的模型中, 数据先到达socket, copy到socket自己所带的那个buffer里,然后通知应用程序你可以读了,应用程序在读的时候是从socket的自身的buffer在copy到你通过recv传递的那个10k的buffer中. 少了一次copy,速度自然快.

用overlapped event方式的缺点是: 一个线程最多有64个event. overlapped routine的方法缺点在于你编程的时候必须很小心:发出request的线程必须挂起在alertable状态.并且为了保证负载较重的时候completion routine也不能做太多计算操作,否则会影响性能(因为这些routine都是在一个线程里的.如果你一个routine就占了很长的CPU,别的routine就别想得到执行机会了).

6. The completion port model

完成端口模型最快,允许成百上千个socket同时运行. 但是请注意这个模型只存在在NT, 2000, XP上.
完成端口模型的关键在于一个windows内核对象: "Windows Completion Port Object". 这个内核对象拥有一定数量的线程来处理overlapped IO request.

Completion Port(以后都指代这个内核对象)不仅可以用来处理socket,它还有其他用途,对于其他用途,这里没做说明.

创建一个Completion Port:

/* The function can be used to:
1. Create a Completion Port
2. Associate a handle with a completion port */
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
DWORD CompletionKey,
DWORD NumberOfConcurrentThreads
);


关于NumberOfConcurrentThread有点特别说明. 这个参数指明所创建的completio port上所能同时运行的worker thread的数量. 但是在你的程序里,你可以创建多于这个数量的worker thread. 为什么呢? 因为你可以让你的worker thread拥有等待操作. 当某一个worker thread block的时候,你当然希望有其他worker thread替代它在completion port上执行. NumberOfConcurrentThread设置为0表示使用系统中处理器的数量作为其值.

从completion port取得输出

每个worker线程应该等待在completion port上. 当有socket上的IO可以执行的时候, worker线程的等待会结束,并处理可用的IO. 等待completion port应该用这个函数:

BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,  // 等在这个completion port上
LPDWORD lpNumberOfBytesTransferred,  // 输出
PULONG_PTR lpCompletionKey,  // 输出
LPOVERLAPPED * lpOverlapped,  // 输出
DWORD dwMilliseconds // 可以设置一个超时
);


由于完成端口模型比较的复杂,我不惜篇幅在这里贴上这段较长的程序:

HANDLE CompletionPort;
WSADATA wsd;
SYSTEM_INFO SystemInfo;
SOCKADDR_IN InternetAddr;
SOCKET Listen;
int i;

typedef struct _PER_HANDLE_DATA
{
SOCKET          Socket;
SOCKADDR_STORAGE    ClientAddr;
// Other information useful to be associated with the handle
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;

typedef struct
{
OVERLAPPED Overlapped;
char       Buffer[DATA_BUFSIZE];
int        BufferLen;
int        OperationType;
} PER_IO_DATA;

// Load Winsock
StartWinsock(MAKEWORD(2,2), &wsd);

// Step 1: Create an I/O completion port
CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

// Step 2: Determine how many processors are on the system
GetSystemInfo(&SystemInfo);

// Step 3: Create worker threads based on the number of rocessors on the system. For this program, we create one for each processor.
for(i = 0; i < SystemInfo.dwNumberOfProcessors; i++)
{
HANDLE ThreadHandle;
// Create a server worker thread, and pass the completion port to the thread.

ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort, 0, NULL;
// Close the thread handle.
CloseHandle(ThreadHandle);
}

// Step 4: Create a listening socket
Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

InternetAddr.sin_family = AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sin_port = htons(5150);
bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr));

listen(Listen, 5);

while(TRUE)
{
PER_HANDLE_DATA *PerHandleData=NULL;
SOCKADDR_IN saRemote;
SOCKET Accept;
int RemoteLen;
// Step 5: Accept connections and assign to the completion port
RemoteLen = sizeof(saRemote);
Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote, &RemoteLen);

// Step 6: Create per-handle data information structure to associate with the socket
PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));

printf("Socket number %d connected\n", Accept);
PerHandleData->Socket = Accept;
memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);

// Step 7: Associate the accepted socket with the completion port
CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData, 0);

// Step 8: Start processing I/O on the accepted socket. Post one or more WSASend() or WSARecv() calls on the socket using overlapped I/O.
WSARecv(...);
}

DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort = (HANDLE) CompletionPortID;
DWORD BytesTransferred;
LPOVERLAPPED Overlapped;
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_DATA PerIoData;
DWORD SendBytes, RecvBytes;
DWORD Flags;

while(TRUE)
{
// Wait for I/O to complete on any socket associated with the completion port
ret = GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,(LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE);

// First check to see if an error has occurred on the socket; if so, close the socket and clean up the per-handle data and per-I/O operation data associated with the socket
if (BytesTransferred == 0 && (PerIoData->OperationType == RECV_POSTED ││ PerIoData->OperationType == SEND_POSTED))
{
closesocket(PerHandleData->Socket);

GlobalFree(PerHandleData);
GlobalFree(PerIoData);
continue;
}

// Service the completed I/O request. You can
// determine which I/O request has just
// completed by looking at the OperationType
// field contained in the per-I/O operation data.
if (PerIoData->OperationType == RECV_POSTED)
{
// Do something with the received data in PerIoData->Buffer
}

// Post another WSASend or WSARecv operation.
// As an example, we will post another WSARecv()
// I/O operation.

Flags = 0;

// Set up the per-I/O operation data for the next overlapped call
ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;
PerIoData->OperationType = RECV_POSTED;

WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags, &(PerIoData->Overlapped), NULL);
}
}


需要说明的几点:

PerHandleData在这里指的是一个socket所拥有的数据构成的结构. 在绑定completion port和socket的时候, 这个结构作为CompletionKey传递给CreateIOCompletionPort. 当我们用GetQueuedCompletionStatus的时候这个结构又作为一个输出, 返回给worker thread.这样worker thread就通过这个结构的值来标识所服务的socket.

PerIOData中的overlapped指针在调用recv之类的函数通过overlapped指针传递给了completion port. 当completion port通过激活GetQueuedCompletionStatus通知woker thread工作的时候, 这个overlqapped指针又作为返回参数传递给了worker thread.

在这个例子里PerIOData中第一个变量就是overlapped结构, 因此从GetQueuedCompletionStatus里的输出的overlaped结构就是PerIOData的指针. 实际应用中,如果PerIOData中的overlapped结构不是第一个元素,那么可以通过这个windows定义的宏取得PerIOData指针.

#define CONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(&((type *)0)->field)))


回收资源

close所有socket,这样所有overlapped操作都会结束

通过给completion port发送特殊的status数据,让每个worker thread自己结束. 发送自定义数据的方法是用一个和GetQueuedCompletionStatus函数相对应的函数:

BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
);


推荐的IO model

client: overlapped IO或者WSAEventSelect,如果是基于windows的程序,那么用WSAAsyncSelect也可以

Server: overlapped IO.如果socket很多,那么用IOCP

第六章 Scalable Winsock Appliations

APIs and scalabiliy

能谈得上可扩展性的就是iocp, 其他模型一般都是为了从windows3.1或者unix移植什么的考虑而存在的. WSAEventSelect之类的都受到windows最多等待64个对象的限制,也没什么可扩展性.

第七章 Socket Options and loctls



第八章 Registration and Name Resolution



第九章 Multicasting



第十章 Generic Quality of Service



第十一章 Raw Sockets



第十二章 The Winsock Service Provider Interface



第十三章 .NET Sockets Programming Using C#



第十四章 The Microsoft Visual Basic Winsock Control



第十五章 Remote Access Service



第十六章 IP Helper Functions



第十七章 NetBIOS



第十八章 The Redirector



第十九章 Mailslots



第二十章 Named Pipes



第二十一章 【附录】Winsock Error Codes



第二十二章【附录】NetBIOS Command Reference

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐