Windows Embedded Compact 7网络编程概述(下)
11.1.1 Select I/O模型
在Windows CE中,Select模型是唯一被支持的I/O模型。Select I/O模型就是利用select函数对I/O进行管理。
函数select的功能在于获取一个或多个套接字的状态,以及在必要的时候执行同步I/O操作进行等待。它的原型如下:
int select(
int nfds,
fd_set FAR* readfds,
fd_set FAR* writefds,
fd_set FAR* exceptfds,
const struct timeval FAR* timeout
);
参数nfds被忽略,只是为了保持与Berkeley的套接字规范相兼容。
参数readfds指向用于检查可读性的一系列套接字。
参数writefds指向用于检查可写性的一系列套接字。
参数exceptfds指向用于检查错误的一系列套接字。
参数timeout设置select函数能够最多等待的I/O操作时间。在阻塞操作时,该参数被设置为NULL,表示必须有操作发生才停止等待。
该函数返回结构体fd_set结构体中处于准备好状态的套接字句柄的数目。如果超时,函数select返回0;否则,在发生错误的时候返回SOCKET_ERROR。函数WSAGetLastError能返回的错误码以及相应的描述如下:如表11-14
错误码 |
描述 |
WSANOTINITIALISED |
必须在成功调用WSAStartup函数之后,才能调用此函数 |
WSAENETDOWN |
网络子系统出错或者相关的服务提供者出现故障 |
WSAEINPROGRESS |
阻塞性的Winsock函数正在被调用,或是服务提供者正在处理回调函数 |
WSAENOTSOCK |
指定的套接字描述符不是合法的套接字 |
WSAEFAULT |
参数argp不是合法的用户地址空间的地址 |
WSAEINVAL |
参数不被支持或是不合法 |
WSAEINTR |
套接字被关闭 |
表11-14错误码以及对应描述
该函数用于获取一个或多个套接字的状态。对于每个套接字来说,函数能够分别获取读、写以及发生错误的状态。待查询状态的套接字用结构体fd_set来表示。统一个fd_set结构体中的所有套接字必须和同一个服务提供者相关联。函数返回的时候,结构体fd_set的值反映了满足指定条件的套接字。而函数返回值代表了满足指定条件的套接字的数目。
如果套接字处于监听的状态且有连接请求到达,那么fd_set中相应的位被设置为可读,而accept函数不被阻塞而直接完成。对于其他状态的套接字来说,可读性表示队列中存在数据,而接收函数,如recv和recvfrom不用被阻塞。
如果套接字在处理非阻塞的connect函数调用,套接字只有在创建连接成功完成的时候才被设置为可写。如果套接字不是处理connect函数调用,可写行表示发送函数,如send和sendto,能够保证成功执行。
在readfds、writefds和exceptfds这三个参数中,最多只能有两个参数被同时设置为空;而另外的非空参数中至少包含一个套接字的句柄。
readfds集合包括符合下述任一条件的套接字:
1) 如果listen函数已经被调用,而且一个连接正在被建立,那么accept函数将成功执行。
2) 有供读取的数据;如果SO_OOBINLINE属性被设置,这里的数据包括带外数据。
3) 连接被关闭、重置或是中断。
writefds集合包括符合下述任一条件的套接字:
1) 如果正在处理一个非阻塞的connect函数调用,那么连接会成功。
2) 数据能够被发送。
exceptfds集合包括符合下述任一条件的套接字:
1) 如果正在处理一个非阻塞的connect函数调用,连接尝试会失败。
2) 在SO_OOBINLINE属性被禁止的情形下,有带外数据可以被读取。
Winsock动态链接库还提供了相应的fd_set结构体的函数,便于程序对fd_set的控制。主要的fd_set结构体操作函数如下:
1) FD_CLR(s, *set):从集合set中删除套接字s。
2) FD_ISSET(s, *set):检查套接字s是否是集合set的一员。如果是,返回非零值;否则,返回0。
3) FD_SET(s, *set):往集合set中添加套接字s。
4) FD_ZERO(*set):初始化集合set为空集合。
11.1 Ping编程
11.2.1 Ping编程概述
在Windows环境中,Ping命令是常见的网络命令。它的主要目的是探测目标机器是否可达,以及检测两者间的网络状态;而它的实现主要是通过向目标机器发送一个ICMP(Internet Control Message Protocol)格式的包来完成的。
ICMP协议是一个在网络主机间执行流控制、错误信息、路由或是其他数据的网络协议。它主要用于网络Ping(或是Packet Internet Groper)中。
在RFC 792的网络协议规范中,ICMP定位于维护状态的协议,且作为IP协议的一部分,工作在ISO模型的网络层中。ICMP的消息都封装成IP数据包的格式,因而可以在网络中进行路由传输。在Windows CE中,ICMP的用途主要有:
1) 创建和维护路由表;
2) 路由发现;
3) 错误诊断;
4) 路由选择;
5) 流量控制。
要编写Ping的应用程序,离不开三个Winsock API函数的支持:IcmpCreateFile, IcmpSendEcho和IcmpSendEcho。
一、函数IcmpCreateFile介绍
函数IcmpCreateFile的功能在于创建一个用于发送ICMP请iude句柄,原型如下:
HANDLE WINAPI IcmpCreateFile (VOID);
函数执行成功时,返回有效的ICMP句柄;否则,返回INVALID_HANDLE_VALUE。
二、函数IcmpSendEcho介绍
函数IcmpSendEcho的功能在于发送一个Ping数据包,即ICMP协议包,并接受一个或多个应答。它的原型如下:
DWORD WINAPI IcmpSendEcho(
HANDLE IcmpHandle,
IPAddr DestinationAddress,
LPVOID RequestData,
WORD RequestSize,
PIP_OPTION_INFORMATION RequestOptions,
LPVOID ReplyBuffer,
DWORD ReplySize,
DWORD Timeout );
参数IcmpHandle指定了ICMP的句柄,这个句柄是由函数IcmpCreateFile打开的。
参数DestinationAddress指定了目标主机的IP地址。
参数RequestData指定了要发送数据包的缓存。
参数RequestSize指定了要发送数据包的长度,及参数RequestData中数据的长度。
参数RequestOptions指定了请求IP头的方式,可以为空。另外,还支持的方式有时间戳(IP_OPT_TS)和记录路由(IP_OPT_RR)。
参数ReplyBuffer表示应答数据的缓存。在函数返回的时候,这个缓存会保存一个或多个ICMP_ECHO_REPLY结构体,以及相应的选项和数据。
参数ReplySize表示收到的应答数据的大小。
参数Timeout表示指定等待应答的最大时间,单位是毫秒。
函数执行成功的时候,返回收到的应答的数目;否则,返回0。
三、函数IcmpCloseHandle介绍
函数IcmpCloseHandle的功能在于关闭ICMP句柄,原型如下:
BOOL WINAPI IcmpCloseHandle(
HANDLE IcmpHandle );
参数IcmpHandle是已经被函数IcmpCreateFile打开的ICMP句柄。
函数执行成功,返回TRUE;否则,返回FALSE。
如果在程序中需要探测目标主机是否可达,程序的执行流程如下:
1. 调用IcmpCreateFile函数创建一个新的ICMP句柄。
2. 循环调用IcmpSendEcho函数发送ICMP数据包并接收应答。在网络不可达,或是连接超时,函数会返回错误。
3. 调用IcmpCloseHandle函数关闭已经创建的ICMP句柄。
11.2.2 Ping编程示例
11.2 RAS拨号编程
RAS,即远程访问服务(Remote Access Service),主要连接远程主机和本地计算机。它允许用户将远程节点的计算机连接到一个本地计算机网络。如果建立了连接,就可以像访问局域网中的计算机一样,即使计算机实际连接的是一个远程网络。
在Windows CE 的 RAS 架构中, RAS 直接与 PPP 协议( Point-to-Point 协议)通信,创建连接到远程访问的通路。 RAS 利用 PPP 协议将需要进行传输的数据封装成 IP 数据包,并通过点对点的链路将数据发送出去。当 PPP 协议接收到从 TCP/IP 协议上发送过来的 IP 请求数据包时,它将包发送到 AsyncMAC 微端口。之后, AsyncMAC 将数据包组装成异步帧,并调用 Win32 的串行 API 将包转发到 TAPI 设备。这样,就完成了数据包的传输。整个数据包的处理和控制流程如图 11.2 所示。
下面将介绍Windows CE为RAS架构提供的一些常用的API。
11.3.1 建立拨号连接
在Winsock为RAS架构提供的服务中,函数RasDial用于建立拨号连接。函数RasDial的功能在于在RAS客户端和RAS服务器端,即本地主机和远程主机间建立一个RAS连接。这个连接中传输的数据包括了相互间的反馈信息,以及用户的认证信息。函数RasDial的原型如下:
DWORD RasDial(
LPRASDIALEXTENSIONS dialExtensions,
LPTSTR phoneBookPath,
LPRASDIALPARAMS rasDialParam,
DWORD NotifierType,
LPVOID notifier,
LPHRASCONN pRasConn
);
参数dialExtensions将被忽略,应该被设置为空。在Windows CE中,它的默认值为RASDIALEXTENSIONS。
参数phoneBookPath的值也应该被设置为空。在拨号连接中,拨号名都存储在注册表的电话本表项中,而不是电话本文件中。
参数rasDialParam是指向结构体RASDIALPARAMS的指针,指定了拨号和用户身份验证参数。结构体RASDIALPARAMS的定义如下:
typedef struct _RASDIALPARAMS {
DWORD dwSize;
TCHAR szEntryName[ RAS_MaxEntryName + 1 ];
TCHAR szPhoneNumber[ RAS_MaxPhoneNumber + 1 ];
TCHAR szCallbackNumber[ RAS_MaxCallbackNumber + 1 ];
TCHAR szUserName[ UNLEN + 1 ];
TCHAR szPassword[ PWLEN + 1 ];
TCHAR szDomain[ DNLEN + 1 ];
} RASDIALPARAMS;
结构体RASDIALPARAMS的属性如下:
1) dwSize域指定了结构体的大小,单位为字节数。
2) szEntryName域指定了建立拨号连接的名称,不能为空。
3) szPhoneNumber域被忽略,应该设置为空。
4) szCallbackNumber域被忽略,应该设置为空。
5) szUserName域指定了连接用户的用户名。它是RAS服务器进行身份验证的用户名,不能为空。
6) szPassword域指定了连接用户的密码。它是RAS服务器进行身份验证的用户名的密码,不能为空。
7) szDomain域指定了连接用户所在的域。
参数NotifierType指定了参数notifier的属性。如果参数notifier为空,那么参数NotifierType被忽略;否则,参数NotifierTye的值为0xFFFFFFFF。
参数notifier指向了接收建立连接过程消息的窗口句柄。如果参数notifier不为空,那么RasDial将会每一个RasDial建立连接的事件发送一个消息。此时,RasDial执行异步调用。这表示RasDial在进行连接的同时,函数调用会立即返回。如果参数notifier为空,RasDial执行同步调用。这表示知道RasDial连接完成后,不论成功或失败,才会返回。
参数pRasConn指定RasDial函数建立的拨号连接句柄。
下面的程序展示如何使用RasDial函数来进行异步调用。
BOOL MakeRasDial (HWND hDlgWnd)
{
BOOL bPassword;
TCHAR szBuffer[100];
if (bUseCurrent)
{
// Get the last configuration parameters used for this connection.
// If the password was saved, then the logon dialog box will not be
// displayed.
if (RasGetEntryDialParams (NULL, &RasDialParams, &bPassword) != 0)
{
MessageBox (hDlgWnd,
TEXT("Could not get parameter details"),
szTitle,
MB_OK);
return FALSE;
}
}
else
{
// Display the Authentication dialog box.
DialogBox (hInst, MAKEINTRESOURCE(IDD_AUTHDLG), hDlgWnd,
AuthDlgProc);
// Set hRasConn to NULL before attempting to connect.
hRasConn = NULL;
// Initialize the structure.
memset (&RasDialParams, 0, sizeof (RASDIALPARAMS));
// Configure the RASDIALPARAMS structure.
RasDialParams.dwSize = sizeof (RASDIALPARAMS);
RasDialParams.szPhoneNumber[0] = TEXT('\0');
RasDialParams.szCallbackNumber[0] = TEXT('\0');
wcscpy (RasDialParams.szEntryName, szRasEntryName);
wcscpy (RasDialParams.szUserName, szUserName); //This is optional
wcscpy (RasDialParams.szPassword, szPassword); //This is optional
wcscpy (RasDialParams.szDomain, szDomain); //This is optional
}
// Try to establish RAS connection.
if (RasDial (NULL, // Extension not supported
NULL, // Phone book is in registry
&RasDialParams, // RAS configuration for connection
0xFFFFFFFF, // Notifier type is a window handle
hDlgWnd, // Window receives notification message
&hRasConn) != 0) // Connection handle
{
MessageBox (hDlgWnd,
TEXT("Could not connect using RAS"),
szTitle,
MB_OK);
return FALSE;
}
wsprintf (szBuffer, TEXT("Dialing %s..."), szRasEntryName);
// Set the Dialing dialog box window name to szBuffer.
SetWindowText (hDlgWnd, szBuffer);
return TRUE;
}
11.3.2 关闭拨号连接
函数RasHangUp是Winsock中用于关闭拨号连接的函数,原型如下:
DWORD RasHangUp(
HRASCONN Session
);
参数Session是待关闭的连接的句柄。它是函数RasDial或函数RasEnumConnections返回的一个句柄。
函数执行成功的时候,返回值为0;否则,返回非零值。
在拨号连接的过程中,如果连接关闭,连接端口需要花很长时间来重新设置这个连接,因此,应该一直等到端口连接完全关闭为止。要判断连接是否完全关闭,可以调用函数RasGetConnectStatus来判断。
函数RasGetConnectStatus的功能在于获取当前RAS的状态信息,原型如下:
DWORD RasGetConnectStatus(
HRASCONN rasconn,
LPRASCONNSTATUS lprasconnstatus
);
参数rasconn是函数RasDial或RasEnumConnections返回的句柄。
参数lprasconnstatus是一个指向结构体RASCONNSTATUS的指针,用来获取当前的连接状态。结构体RASCONNSTATUS的定义如下:
typedef struct _RASCONNSTATUS {
DWORD dwSize;
RASCONNSTATE rasconnstate;
DWORD dwError;
TCHAR szDeviceType[ RAS_MaxDeviceType + 1 ];
TCHAR szDeviceName[ RAS_MaxDeviceName + 1 ];
} RASCONNSTATUS;
结构体RASCONNSTATUS的属性如下:
1) dwSize域指定了结构体的大小,单位为字节数。
2) rasconnstate域指定了当前RasDail连接的状态。它可选的值是RASCS_Connected和RASCS_Disconnected,分别表示建立连接成功或是失败。
3) dwError域如果不为空,则表示失败的原因。它的可选值是:ERROR_NOT_ENOUGH_MEMORY和ERROR_INVALID_HANDLE。
4) szDeviceType域表示连接所用的设备类型,不能为空。
5) szDeviceName域表示当前的设备名,不能为空。
11.3.3 列举已建立的活动连接
函数RasEnumConnections的功能在于列举当前所有活动的拨号连接,该函数的原型如下:
DWORD RasEnumConnections(
LPRASCONN lprasconn,
LPDWORD lpcb,
LPDWORD lpcConnections
);
参数lprasconn指向结构体RASCONN的结构数组,每个数组项代表一个RAS连接。结构体RASCONN的定义如下:
typedef struct _RASCONN {
DWORD dwSize;
HRASCONN hrasconn;
TCHAR szEntryName[ RAS_MaxEntryName + 1 ];
} RASCONN;
结构体RASCONNSTATUS的属性如下:
1) dwSize域指定了结构体的大小,单位为字节数。
2) hrasconn域代表拨号连接句柄。
3) szEntryName域代表拨号连接时的名字,不能为空。
参数lpcb表示参数lprasonn的数据大小。
参数lpcConnections表示活动连接的数目。
函数RasEnumConnections的返回值为0,表示执行成功;否则,表示失败。
下面的程序展示如何关闭当前所有活动的拨号连接。
DWORD CloseRasConnections ()
{
int index; // An integer index
TCHAR szError[100]; // Buffer for error codes
DWORD dwError, // Error code from a function call
dwRasConnSize, // Size of RasConn in bytes
dwNumConnections; // Number of connections found
RASCONN RasConn[20]; // Buffer for connection state data
// Assume the maximum number of entries is
// 20.
// Assume no more than 20 connections.
RasConn[0].dwSize = sizeof (RASCONN);
dwRasConnSize = 20 * sizeof (RASCONN);
// Find all connections.
if (dwError = RasEnumConnections (RasConn, &dwRasConnSize,
&dwNumConnections))
{
wsprintf (szError, TEXT("RasEnumConnections Error: %ld"), dwError);
return dwError;
}
// If there are no connections, return zero.
if (!dwNumConnections)
{
wsprintf (szError, TEXT("No open RAS connections"));
return 0;
}
// Terminate all of the remote access connections.
for (index = 0; index < (int)dwNumConnections; ++index)
{
if (dwError = RasHangUp (RasConn[index].hrasconn))
{
wsprintf (szError, TEXT("RasHangUp Error: %ld"), dwError);
return dwError;
}
}
return 0;
}
11.3 UDP编程概述
UDP(User Datagram Protocol),即用户数据报协议,提供无连接的、不可靠的传输服务。“无连接”意味着在相互交换数据之前,通信主机间没有建立连接链路。UDP的这种“无连接”数据传输服务无法保障数据的可靠传输。UDP既不保证数据报被正确发送出去,也不会发送确认信息。另外,UDP协议也不能保证数据报的有序性。UDP经常用于“一对多”的通信,既能够向若干个目标发送数据,也能接收发自若干个源的数据。
由于UDP数据报不保证的可靠性,应用程序必须采取机制来维护UDP传送数据的可靠性。虽然UDP不保证顺序性、可靠性和无重复性等限制,UDP协议仍用于很多场景。例如,Winsock库的IP组播技术就是利用UDP数据报来实现的。而且,UDP的传输效率高和延迟小。Microsoft的网络利用UDP处理网络登录、浏览以及域名解析。
在编程方面,UDP实现相对简单。UDP服务器不需要监听或是接收客户端的连接,而UDP客户端也不用连接到服务器。UDP服务器端和客户端的编程流程如图11.3所示。
UDP通信中服务器端的代码的执行流程如下:
1) 调用socket函数创建一个数据报套接字。其中,参数address format的值为AF_INET,而参数type的值为SOCK_DGRAM。
2) 调用bind函数。其中,参数address使用SOCKADDR_IN结构体。
3) 调用函数sendto和recvfrom与客户端进行数据的交换。
4) 调用closesocket函数关闭连接。此时,函数shutdown对于UDP套接字来说无效。
UDP通信中客户端的代码的执行流程如下:
1) 调用socket函数创建一个数据报套接字。
2) 调用函数sendto和recvfrom与服务器端进行数据的交换。
11.4 TCP编程概述
TCP(Transport Control Protocol),即传输控制协议,提供了无差错无重复且顺序的数据传输。TCP的套接字也被称为流式套接字。与UDP通信不同,应用程序在利用TCP进行通信之前,客户端和服务器端会建立一个虚拟连接,创建一个虚拟的数据传输链路。当这个连接成功建立之后,客户端和服务器端就可以把数据当作一个双向字节流进行交换。
在编程方面,TCP编程相对于UDP编程来说要复杂的多。TCP服务器端和客户端的编程流程如图11.4所示。
TCP通信中服务器端的代码的执行流程如下:
1) 调用socket函数创建一个流式套接字。其中,参数address format的值为AF_INET,而参数type的值为SOCK_STREAM。
2) 调用bind函数。其中,参数address使用SOCKADDR_IN结构体。
3) 调用listen函数监听客户端发送的连接请求。
4) 如果监听到有客户端发出连接请求,调用accept函数建立与客户端的连接;否则,一直在3)处循环等待。
5) 调用send和recv函数与客户端进行数据的交换。
6) 调用closesocket函数关闭连接。为了保证TCP连接上的数据不会丢失,可以先调用shutdown函数关闭所有的连接。
TCP通信中客户端的代码的执行流程如下:
1) 调用socket函数创建一个流式套接字。其中,参数address format的值为AF_INET,而参数type的值为SOCK_STREAM。
2) 调用connect函数向服务器发起连接请求。其中,参数address使用SOCKADDR_IN结构体。
3) 调用send和recv函数与客户端进行数据的交换。
4)
调用 closesocket 函数关闭连接。为了保证 TCP 连接上的数据不会丢失,可以先调用 shutdown 函数关闭所有的连接。
11.5 小结
本章主要介绍了Windows Embedded Compact 7中网络编程的基础,以及常见的TCP、UDP等编程的概述。首先,介绍了Windows CE中网络编程的基础,也就是套接字。对如何在Windows CE环境中使用套接字,以及常用的套接字的API作了讲解。其次,介绍了Windows CE的网络编程中最常见的四种编程方式:Ping编程、RAS拨号编程、UDP编程以及TCP编程。