本节将逐一介绍WinSock的主要特性和组件,套接字、WinSock动态库的使用。
本节必须掌握的知识点:
Windows Socket接口简介
Windows Socket接口的使用
第178练:网络时间校验
24.1.1 Windows Socket接口简介
■以下是WinSock的主要特性和组件:
●套接字(Socket): 套接字是网络通信的基本概念,它代表了一个网络连接的端点。WinSock提供了函数来创建、绑定、连接和关闭套接字,以及发送和接收数据。
●协议支持: WinSock支持多种网络协议,包括TCP/IP、UDP、IPX/SPX等。开发者可以根据需要选择适当的协议。
●网络地址转换: WinSock提供了用于网络地址转换的函数,使开发者可以将人类可读的IP地址和端口号转换为计算机可理解的格式,并在不同的网络字节序(大端序和小端序)之间进行转换。
●异步操作: WinSock支持异步操作,允许开发者在进行网络通信时使用非阻塞操作,以提高效率和响应性。
●多线程支持: WinSock支持在多线程环境中进行网络编程,可以同时处理多个套接字和连接。
●错误处理: WinSock定义了一套错误码和错误处理函数,使开发者能够检测和处理网络操作中的错误情况。
●使用WinSock进行网络编程时,开发者通常需要按照以下步骤进行操作:
1.初始化WinSock库,通过调用WSAStartup函数来启动WinSock。
2.创建套接字,通过调用socket函数创建一个套接字,并指定协议类型、地址族等参数。
3.配置套接字,通过设置套接字选项,如设置超时时间、启用广播等。
4.绑定套接字到本地地址,通过调用bind函数将套接字与本地IP地址和端口号绑定。
5.连接到远程主机,对于客户端应用程序,通过调用connect函数连接到远程服务器。
6.发送和接收数据,使用send和recv函数发送和接收网络数据。
7.关闭套接字,通过调用closesocket函数关闭套接字,释放资源。
8.清理WinSock库,通过调用WSACleanup函数来终止WinSock。
■TCP/IP模型
●TCP/IP的核心协议运行于传输层和Internet层,主要包括TCP、UDP和IP协议,而TCP协议和UDP协议是以IP协议为基础而封装的。这两种协议提供了不同方式的数据通信服务。
●IP协议比喻为道路,则下一层的网络访问层上的协议相当于不同的铺路材料,上面的TCP和UPD协议相当于路上跑的不同类型的车辆,再上层应用层的协议相当于车上的丰富多彩的货物。他们都是以TCP、UDP为载体的。
图24-1 OSI模型、TCP/IP模型的结构和WinSock接口的关系
■ WinSock动态库
●早期的1.1版的WinSock接口最后也是调用2.0版的WS2_32.dll文件的
●使用前须包含头文件#include Winsock2.h和增加导入库Ws2_32.lib
■加载和释放动态链接库
●WSAStartup函数:WSAStartup(wVersionRequested, lpWSAData)
参数 | 含义 |
WORD wVersionRequested | 指定动态库的版本号。如2.0版时0x0002(MAKEWORD(2,0)) |
LPWSADATA lpWSAData | 指向WSADATA结构体,用来返回动态链接库的详细信息。 wVersion:库文件建议应用程序使用的版本 wHighVersion:库文件支持的最高WinSock版本 szDescription:返回库描述字符串,如“WinSock2.0”之类的。 szSystemStatus:系统状态字符串:返回如“Runing”之类的状态 iMaxSockets:同时支持的最大套接字数量 iMaxUpdDg: 2.0版中己废弃的字段 lpVendorInfo:2.0版中己废弃的字段 |
返回值 | 如果装入成功,返回0。否则,返回出错代码: WSASYSNOTREADY:网络子系统未准备好 WSAVERNOTSUPPORTED:不支持指定的版本 WSAEINPROGRESS::另一个阻塞方式的WinSock1.1操作正在进行中 WSAEPROCLIM:WinSock接口己达到所支持的最大任务数 WSAEFAULT:输入参数lpWSAData指定的指针无效 ★该函数出错时直接返回出错代码,因为库还没装入,无法使用WSAGetLastError函数。其他WinSock函数出错时返回SOCKET_ERROR或INVALID_SOCKET,要进一步得到出错代码,须调用WSAGetLastError函数来获取。 |
●释放WinSock:int WSACleanup(void);//返回值成功为0,否则为SOCKET_ERROR
24.1.2 Windows Socket接口的使用
■ IP地址的转换
●IP地址和端口:
1.IP地址(32位):如11000000.10101000.00000001.01100100(192.168.1.100)
2.端口(16位):即端口数量为65536个。
3.TCP协议和UDP协议是两个完全独立的模块,两者的工作互不相干,所以TCP和UDP各自的端口号也相互独立,即一个进程使用TCP协议的某个端口号并不影响另一进程使用UDP协议的同名端口号。但同一协议的同一端口号无法同时被两个进程同时使用。
●常用协议和应用程序使用的默认端口号
协议或应用程序 | TCP端口号 | UDP端口号 |
FTP | 21 | |
Telnet | 23 | |
SMTP | 25 | |
HTTP | 80 | |
POP3 | 110 | |
DNS查询 | 53 | |
TFTP协议 | 69 | |
NetBIOS名字服务 | 137 | |
NetBIOS数据包服务 | 138 | |
SQLServer数据库 | 139、1433 | |
Oracle数据库 | 1521 |
●sockaddr_in结构体:由于TCP和UDP协议必须同时指定IP和端口号。(封装)
字段 | 含义 |
short sin_family | 地址族,指明互联网的地址类型。在WinSock中必须为AF_INET |
unsigned short sin_port | 端口号(使用网络字节顺序),如53端口号,则等于htons(53); |
struct in_addr sin_addr | IP地址(使用网络字节顺序),如 sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); sin_addr.S_un.S_addr=htonl(INADDR_ANY); |
char sin_zero[8]; | 空字节 |
■ 网络字节顺序——大端模式
●小端模式:低位放低地址,高位放高地址。如Intel80x86系列的处理器
●大端模式:低位放高地址,高位放低地址。如RISC芯片、网络字节。如0x12345678,则依次送入端口中号的数据为0x12、0x34、0x56、0x78(从低地址开始发送)。
■ 字节顺序转换函数
函数 | 说明 |
htons、htonl | 将16(或32)位的当前主机字节顺序数据转为网络顺序 |
ntohs、ntohl | 将16(或32)位的网络顺序的数据转为当前主机字节顺序 |
inet_addr | 将字符串转为IP地址,如inet_addr(“127.0.0.1”); |
inet_ntoa | 将IP转为字符串。如inet_ntoa(sa.sin_addr) |
■网络应用程序的一般流程
●TCP协议的特征和TCP程序的工作流程
1.特点:面向连接、可靠的字节流服务
面向连接 | ①两个TCP套接字在开始传输数据之前必须先建立一个连接。(犹如打电话) ②TCP协议不能用于广播 |
字节流服务 | 传输入数据是流方式的,没有边界。如发送方分3次发送100、150、200字节的数据包。对接收来说,无法知道数据如何分割,可以一次接收450字节,也可以分10次接收,每次接收45字节。 |
可靠 | ①采用超时及重传机制保证不丢失数据。每发送一个数据包,会启动一定时器,等待对方确认收到这个包。如果指定时间内没得到确认,会重发这个数据包。 ②如果接收方发现数据包校验有错,TCP协议丢弃这个数据包,并且不发送确认,从而发送方因收不到确认而重发这个数据包。 |
2.数据包在传输的时候会通过多个路由器,不同数据包到达终点的先后顺序可能与发送数据包的先后顺序不同。但这没关系,因为TCP协议首部保存数据包的序号。如有必要,在收到数据时TCP协议会重新排序,并将正确的顺序交给应用程序。
3.接收方收到的数据包有可能重复,原因之一是发送和确认之间有个时差,发送方可能因超时而重发数据,对于这种情况,接收方会丢弃重复的数据。
4.TCP协议还提供流量控制机制,发送方可根据接收方应答时间和速率来调整数据的发送速度。防止速度太快,使接收方出现缓冲区溢出。
图24-3 TCP服务器和客户端模型
【注意】客户端无需绑定IP,只需向服务端发起连接请求。服务端处于监听状态。
●UDP协议的特征和UDP程序的工作流程
1.特点:是一个无连接的,面向消息的,不可靠的传输层协议。
无连接 | ①客户端在发送UDP数据包前不需要先与服务器端进行握手确认。无法确认对方是否在线、也无法确认对方指定的端口是否在监听。属于“发出就不管”的协议 ②同一个UDP套接字可以向任何服务器地址发送数据,而无须创建多个套接字,即可采用广播方式。 |
面向消息 | UDP数据包是有边界保护的。如发送方分三次分别发送100、150、200字节的UDP数据包,接收方必须分三次接收这些数据包。各个数据包之间的数据不会粘连。 |
不可靠 | UDP协议并不对数据的可靠性与有序性等进行控制。 |
2.TCP协议像打电话,而UDP协议像寄信。发信人虽然知道收信人的地址,但他并确定信是否会被收到。如果发了好几封信,在收信人回信之前,发信人也无法确定信件是否安全、无损和有序的到达。
3.UDP协议不对数据进行可靠性保证,因此传输的效率较高。经常用在在线视频的传送。
图24-4 UDP服务器和客户端模型
【注意】客户端不必连接,直接发送数据。服务端也不必进入监听状态。客户端与服务端的唯一区别就是服务端必须首先将套接字绑定到一个固定端口。以便客户端能向约定的端口发送数据。
■套接字
在Windows操作系统中,套接字(Socket)是用于进行网络通信的一种机制,它提供了一种在网络上发送和接收数据的方式。Windows提供了一套名为Winsock的API(应用程序编程接口),用于在Windows平台上进行套接字编程。
Winsock是Windows Sockets的缩写,它是对底层网络协议的封装和抽象,使得开发者可以使用统一的接口进行网络编程,而无需关注底层协议的细节。
●在Windows中使用套接字进行网络编程时,通常遵循以下步骤:
1.初始化Winsock库:使用WSAStartup函数初始化Winsock库,这是进行任何套接字操作之前必须执行的步骤。
2.创建套接字:使用socket函数创建一个套接字,指定协议类型(如TCP或UDP)和其他参数。
3.绑定套接字:如果是服务器端程序,可以使用bind函数将套接字绑定到特定的IP地址和端口号。
4.监听连接请求:如果是服务器端程序,使用listen函数开始监听连接请求。
5.接受连接:使用accept函数接受客户端的连接请求,建立与客户端的连接。
6.连接到远程主机:如果是客户端程序,使用connect函数连接到远程服务器。
7.发送和接收数据:使用send和recv函数发送和接收数据。对于TCP套接字,可以进行可靠的、面向连接的数据传输;对于UDP套接字,可以进行不可靠的、无连接的数据传输。
8.关闭套接字:使用closesocket函数关闭套接字,释放资源。
9.清理Winsock库:使用WSACleanup函数在程序结束时清理Winsock库。
Winsock API提供了一系列函数和数据结构,用于管理套接字和进行网络通信操作。这些函数包括socket、bind、listen、accept、connect、send、recv、closesocket等。通过使用这些函数,开发者可以方便地进行网络编程,并实现各种网络通信需求。
套接字建立用来通信的对象,是“通信的一端”。套接字的种类:流套接字(stream socket)、数据报套接字(datagram socket)、原始套接字(raw socket)、可靠信息分递套接字(rdm socket)、连续小分包套接字(seqpacket socket)。
●套接字的创建和关闭
1.创建套接字:SOCKET socket(af, type, protocol)
参数 | 含义 |
int af | 用来指定套接字使用的地址格式,和sockaddr_in中的sin_family的定义是一样的。唯一可使用的值是AF_INET。 |
int type | 用来指定套接字的类型 SOCK_STREAM——流套接字,使用TCP协议提供有连接和可靠的传输 SOCK_DGRAM——数据报套接字,使用UDP协义提供无连接的不可靠的传输 SOCK_RAW——原始套接字,WinSock接口并不使用某种特定的协议去封装它,而是由程序自行处理数据包,以及协议首部,正因为如此,所以可以使用特殊的功能,如伪造发送者地址等。 |
int protocol | 当type指定为SOCK_RAW时,protocol可指定以下的值 ①IPPROTO_IP、IPPROTO_ICMP、IPPROTO_TCP、IPPROTO_UDP:分别指定使用IP、ICMP、TCP和UDP协议。这时会自动为数据加上IP首部。并且将IP首部中的上层协议字段设置为指定的这些协议的名称。但是使用这个套接字接收数据时,系统却不会将IP首部自动去除,需要自行处理。 ②IPPROTO_RAW:系统将数据包直接送到网络访问层,程序需要自己添加IP首部及其他协议的首部,并用需要自己计算和填充协议首部中的检验和字段。但这个socket只能用来发送数据包而无法接收数据。 |
2.关闭套接字:int closesocket(SOCKET s);
●套接字的工作模式:阻塞模式(创建时默认的工作方式)和非阻塞模式。
■监听、发起连接和接收连接
●TCP客户端——连接到服务器:connect函数
参数 | 含义 |
SOCKET s | TCP套接字的句柄 |
const struct sockaddr FAR *name | 指向一个sockaddr_in结构,用来指定服务器端的地址和端口 |
int namelen | 指定的sockaddr_in结构的长度 |
返回值 | ①阻塞模式下:成功返回0,否则SOCKET_ERROR。要知道详细原因,可调用WSAGetLastError函数。 常见错误: WSAECOONNERREFUSED:服务器没有在指定端口监听。 WSA_ETIMEDOUT:网络不通,或服务器不在线 ②非阻塞模式:均返回SOCKET_ERROR,但并不意味着连接失败,而是指函数返回里连接尚末成功。要调用WSAGetLastError得到出错代码。只是WSAEWOULDBLOCK才表示连接失败。 |
【注意】客户端发起连接时,系统会自动为套接字选择一个空闲的端口,如果一定要用特定的端口连接服务器,可在调用connect前用bind函数来指定端口。
●TCP服务器端——在指定的IP地址和端口监听并接收连接
1.绑定IP和端口:int bind(SOCKET s, const struct sockaddr FAR *name,int namelen );
参数 | 含义 |
SOCKET s | TCP套接字的句柄 |
const struct sockaddr FAR *name | 指向一个sockaddr_in结构,用来指定需要绑定的服务器端的地址和端口。sin_addr字段的设置: INADDR_ARRAY(0):自动在本机的所有IP地址上监听 //如本机有3个网卡,配置3个IP //那么会自动在监听3个地址上监听 指定为内网IP:在指定的那个地址上进行监听 |
int namelen | 指定的sockaddr_in结构的长度 |
返回值 | 绑定成功,返回0。否则返回SOCKET_ERROR。一般是端口是被其他程序占用,出错代码为WSAEADDRINUSE。如果套接字己经绑定过了,返回WSAEFAULT。 |
2.监听:int listen(SOCKET s, int backlog);
参数 | 含义 |
SOCKET s | TCP套接字的句柄 |
int backlog | 监听队列中允许保持的尚未处理的最大连接数量。当套接字监听到客户端连接请求时,还需要调用accept才能建立真正的连接。在调用accept之前,连接请求会被保留在队列 中,如果这时另一个客户端也发起连接的话,这个连接也会被保留在队列里。Backlog指的就是这个队列最大的长度。 |
返回值 | 成功,返回0。这里套接字处于等待连接进入的状态。失败返回SOCKET_ERROR。如果没有bind操作就去listen,这里的出错代码是WSAINVAL。 |
3.接受连接:SOCKET accept(SOCKET s, struct sockaddr FAR *addr,int FAR *addrlen);
参数 | 含义 |
SOCKET s | 监听中的套接字句柄 |
struct sockaddr FAR *addr | addr指向一个缓冲区,函数会在这里返回一个sockaddr_in结构。结构中存放有连接请求方的IP地址和端口(即客户端的IP和端口)。可以通过这个参数,对客户端进行认证,如果检测到IP不合法,则调用closesocket关闭这个新套接字。如果不需要得到对方的地址信息,addr和addrlen都设为NULL。 |
addrlen | 指向一个int型的变量,函数在这里放入返回到上述结构长度。 |
返回值 | 如果成功,函数新建一个TCP套接字,这个新的套接字才是用来和该客户端连接。原来的套接字仍保持着监听状态。当要断开与客户端的连接时,也是要对这个新的套接字调用closesocket。 如果失败,返回INVALID_SOCKET。 |
●典型的accept处理
while(TRUE)
{
SOCKET sc=accept(hListenSocket,NULL,0);
if (sc==INVALID_SOCKET) break;
//在这里创建一个新线程,对新套接字进行通信,以便马上能处理新连接。
//但这里直接对新连接进行数据收发,因为其他客户的连接请求可能没办法及时处理。
//新套接字可以通过lParam参数传递线程函数。
//可以用其他线程中closesocket这个监听套接字,表示不再进行监听。这样accept
//会返回INVALID_SOCKET,这样程序就可以退出循环。(注意关闭的是监听套接字)
}
■数据的收发
TCP一旦连接(对客户端来说是connect返回成功,对服务器端来说是accept返回新套接字)。那么连接双方是对等的,因为TCP连接是一个全双工的连接。任何一方可以在任何时刻向对方发送数据。
●使用TCP套接字收发数据
1.发送数据:int send(SOCKET s, const char FAR *buf,int len, int flags);
参数 | 含义 |
SOCKET s | 指定套接字句柄 |
const char FAR *buf | 指向要发送的数据缓冲区 |
int len | 指定发送的数据长度 |
int flags | 一般默认为0 |
返回值 | 发送失败,返回SOCKET_ERROR。否则返回发送的字节数。 (注意:WinSock会为每个套接字分一个发送缓冲区和接收缓冲区,用send发送数据时,并不马上在网络上传递,而是先发送到“发送缓冲区”,WinSock会在合适的时候将数据发送出去,所以前面的“成功发送”指的是放入“发送缓冲区”而己)。 |
【扩展“发送缓冲区”】设send函数中要求发送为n字节,发送缓冲区的空闲空间m字节。
工作 模式 | 表现 | 备注 |
阻塞模式 | A、如果发送缓冲区足够大(m≥n),数据放入缓冲区,函数马上返回。 B、如果发送缓冲区不够大(m<n),函数会一直等到全部数据放入缓冲区才返回。 | 1、在阻塞模式下,函数在发送完后才返回,但返回值是实际发送的字节数n。 2、非阻塞模式下,函数会立即返回,返回值是函数实际发送的字节,介于(1到n)之间。要利用循环,多次调用send函数。 |
非阻塞模式 | A、如果发送缓冲区足够大(m≥n),数据放入缓冲区,函数马上返回。返回值为为实际发送的字节数n B、如果发送缓冲区不够大(n>m>0)将直接将m字节放入缓冲区,然后返回,这里的返回值为发送的实际字节数m; C、这里缓冲区满(即m=0),函数返回SOCKET_ERROR,再获取出错代码时会返回WSAEWOULDBLOCK。 |
2.接收数据:int recv(SOCKET s, char FAR *buf, int len, int flags);
参数 | 含义 |
SOCKET s | 指定读取的套接字句柄 |
char FAR *buf | 用来返回数据的缓冲区 |
int len | 指定缓冲区的大小 |
int flags | 指定读取时的选项,可以是: MSG_PEEK——返回数据后并不从缓冲区清除数据。 MSG_OOB——发送或接收带外数据(表示重要数据)。如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方 MSG_WAITALL——等待所有数据 |
返回值 | 接收失败,返回SOCKET_ERROR。否则返回实际接收的字节数。 (注意:WinSock会为每个套接字分一个发送缓冲区和接收缓冲区,用send发送数据时,并不马上在网络上传递,而是先发送到“发送缓冲区”,WinSock会在合适的时候将数据发送出去,所以前面的“成功发送”指的是放入“发送缓冲区”而己)。 |
【扩展“接收缓冲区”】设recv函数接收为n字节,当前接收缓冲区有m字节数据。
工作 模式 | 表现 | 备注 |
阻塞 模式 | A、如果接收缓冲区为空(即m=0),函数会等待直至有数据到达为止。 B、如果缓冲区己经有m字节数据,则: 当m≥n时,函数从缓冲区读n个字节并返回; 当m< n时,那么只读取m个字节数据并马返回。 | 1、两种模式下返回成功时(返回值不是SOCKET_ERROR)时: ①函数返回的是实际接收的字节数,这个值在1<到n之间,也就是在不超过n的前提下,有多少返回多少。 ②如果要接收到指定数量的字节(n),即应通过循环接收。 2、如果返回SCOKET_ERROR: ①阻塞模式意味着连接己经因各种情况而断开。 ②非阻塞模式下,要继续调用WSAGetLastError获取出错代码。如果出错代码为WSAEWOULDBLOCK,意味着缓冲区为空。否则表示连接己断开。 |
非阻塞模式 | A、当接收缓冲区中己经有数据的情况,表现与阻塞模式一样,函数马上返回。 B、在接收缓冲区为空(m=0)时,函数不会等待,也是马上返回。但返回值为SOCKET_ERROR,再获取出错代码时会返回WSAEWOULDBLOCK。 |
●使用UDP套接字收发数据:UDP套接字创建后,就可直接向服务器收送数据了。
1.发送UDP数据包:sendto函数——int sendto(SOCKET s, const char FAR *buf, int len, int flags,const struct sockaddr FAR *to, int tolen);
A、参数to:指向一个包含目标地址和端口号的sockaddr_in结构,tolen指定了这个结构体的大小。
B、UDP数据包有最大尺寸SO_MAX_MSG_SIZE的限制。如果数据包小于超过该尺寸,且发送成功,函数会返回实际发送数据的字节数;如果数据包超过该尺寸,函数将返回失败,这时没有任何数据被发送,并且出错代码是WSAEMSGSIZE。
C、阻塞模式与非阻塞模式下的sendto表现不同:
如果没有足够的发送缓冲区:阻塞模式下,函数将等待到缓冲区足够大为止;而非阻塞模式下会马上返回SOCKET_ERROR,这时得到的出错代码为WSAEWOULDBLOCK。
因UDP是面向消息的,数据包不会被割裂发送,所以不管哪种情况下,函数不会只发送部分数据。
D、sendto发送UDP数据包时,如果在调用sendto函数前bind了IP和端口号,则按指定的方式发送。如果没有绑定,系统会在第一次调用sendto时为该socket自动分配一个空闲端口,以后一直使用这个端口来发送。(注意一定要有IP和端口号了才能接收数据!)
2.接收UDP数据包:recvfrom函数——int recvfrom(SOCKET s, char FAR* buf, int len, int flags,struct sockaddr FAR *from, int FAR *fromlen );
A、参数from指定了用来接收发送方地址的sockaddr_in结构体。可以从这个结构体中得到发送方的IP地址和端口。如果需要回复的话,可以根据这个地址进行回复。
B、同一个UDP套接字可以接收任何客户端发送过来的UDP数据包。只要对方指定了正确的IP和端口。该UDP就是专门负责用来接收来自该端口号的UDP数据包的。
C、UDP是面向消息的,当指定缓冲区尺寸小于接收缓冲区中的UDP包的尺寸,那么多余的部分数据会丢失,这里函数返回SOCKET_ERROR,出错代码为WSAMSGSIZE。如果大于缓冲区中UDP包的大小时,会接收该UDP包。但不会将后面到达的数据包内容一并返回。因为UDP包是有边界的,这里函数返回实际接收的数据大小。
D、阻塞模式与非阻塞模式下recv表现不同
阻塞模式下,如果接收缓冲区没有数据到达,函数会等待有数据包到达为止。
非阻塞模式下,函数会马上返回SOCKET_ERROR,出现代码为WSAEWOULDBLOCK。
●select函数及作用——用来检测套接字的各种情况
1.当阻塞时,recv会等待对方发送数据而将本身所在的线程挂起。可以先用select函数检则是否有数据到达,如果有才去recv。如果没有,则线程继续执行下去。
2.可以检测套接字是否有数据到达(即可读)、或套接字是否可写、或异常(如断开连接)。
3.select函数:int select(nfds,lpreadfds,lpwritefds,lpexcept,lptimeout);
参数 | 含义 |
int nfds | 等于0,是为了兼容UNIX Socket而设置的 |
fd_set* lpreadfds | ①fd_set结构体: u_int fd_count; 存放要检测的套接字的数量 SOCKET fd_array[FD_SETSIZE]; //套接字句柄列表 ②lpreadfds:要检测的套接字是否可读(接收缓冲区数据是否有数据) ③lpwritefds:要检测的套接字是否可读(即发送缓冲区是否为空。 ④lpexcept:要检测的套接字是否出错(如连接是否断掉) |
fd_set* lpwritefds | |
fd_set* lpexcept | |
const struct timeval *lptimeout | ①timeval结构体: long tv_sec; //秒数 long tv_usec;//微秒(注意不是毫秒) ②如果lptimeout为NULL表示永远等待下去,直到列表中某个套接字就绪才返回 ③如果lptimeout结构中的时间为0,表示不管有没有套接字就绪马上返回。 ④如果lptimeout结构中的时间不为0,在指定时间内还没有套接字就绪,就超时返回。如果指定时间内有套接字就绪,则马上返回。 |
返回值 | ①因超时而返回时,返回0 ②因出错而返回时,返回SOCKET_ERROR。 ③因某个套按字就绪而返回,返回值是就绪套接字的数量。 |
24.1.3 第178练:网络时间校验
/*------------------------------------------------------------------------
178 WIN32 API 每日一练
第178个例子NETTIME.C:网络时间校验---WinSock API
AdjustWindowRect函数
DialogBoxParam函数
WSAStartup函数
socket函数
WSAAsyncSelect函数
closesocket函数
WSACleanup()函数
SOCKADDR_IN结构
connect()函数
recv函数
ntohl函数
http://tf.nist.gov/tf-cgi/servers.cgi //Internet时间服务器一览表
(c) www.bcdaren.com 编程达人
-----------------------------------------------------------------------*/
#include <windows.h>
#include "resource.h"
#pragma comment(lib,"WS2_32.lib")
#define WM_SOCKET_NOTIFY WM_USER + 100
#define ID_TIMER 1
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL CALLBACK MainDlg(HWND, UINT, WPARAM, LPARAM);
BOOL CALLBACK ServerDlg(HWND, UINT, WPARAM, LPARAM);
void EditPrint(HWND hwndEdit, TCHAR* szFormat, ...);
void ChangeSystemTime(HWND hwndEdit, ULONG ulTime);
void FormatUpdateTime(HWND hwndEdit, SYSTEMTIME* pstOld, SYSTEMTIME* pstNew);
HINSTANCE hInst;
HWND hwndModeless;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("NetTime");
HWND hwnd;
MSG msg;
RECT rect;
WNDCLASS wndclass;
hInst = hInstance;
wndclass.style = 0;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hbrBackground = NULL;
wndclass.hCursor = NULL;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hInstance = hInstance;
wndclass.lpszClassName = szAppName;
wndclass.lpszMenuName = NULL;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, TEXT("Set System Clock from Internet"),
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
WS_BORDER | WS_MINIMIZEBOX,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,NULL,hInstance,NULL);
//创建非模态对话框
hwndModeless = CreateDialog(hInstance, szAppName, hwnd, MainDlg);
//获取对话框的大小
GetWindowRect(hwndModeless, &rect);
//调整rect,增加大到有标题栏和边框,第3个选项表明没有菜单
AdjustWindowRect(&rect, WS_CAPTION | WS_BORDER, FALSE);
//设置主窗口大小,并保留原来的位置
SetWindowPos(hwnd, NULL, 0, 0, rect.right - rect.left,
rect.bottom - rect.top,SWP_NOMOVE);
ShowWindow(hwndModeless, SW_SHOW);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg,NULL,0,0))
{
if (hwndModeless == 0 || !IsDialogMessage(hwndModeless,&msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_SETFOCUS:
SetFocus(hwndModeless);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
BOOL CALLBACK MainDlg(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static char szIPAddr[32] = { "132.163.4.101" };//互联网时间服务器地址
static TCHAR szOKLabel[32];
static HWND hwndButton, hwndEdit;
static SOCKET sock;
static SOCKADDR_IN sa;
WSADATA WSAdata;
int iError, iSize;
unsigned long ulTime;
WORD wEvent, wError;
switch (message)
{
case WM_INITDIALOG:
hwndButton = GetDlgItem(hwnd, IDOK);
hwndEdit = GetDlgItem(hwnd, IDC_TEXTOUT);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_SERVER:
//创建模式对话框
DialogBoxParam(hInst, TEXT("Servers"), hwnd, ServerDlg,
(LPARAM)szIPAddr);
return TRUE;
case IDOK:
//启动进程对Winsock DLL的使用。调用WSAStartup函数并显示WinSock库信息
if (iError = WSAStartup(MAKEWORD(2, 0), &WSAdata))
{
EditPrint(hwndEdit, TEXT("Startup error #%i.\r\n"), iError);
return TRUE;
}
EditPrint(hwndEdit, TEXT("Started up %hs\r\n"),
WSAdata.szDescription); //%hs窄字符格式输出
//创建socket对象
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//返回套接字描述符
if (sock == INVALID_SOCKET)
{
EditPrint(hwndEdit, TEXT("Socket createion error #%i.\r\n"),
WSAGetLastError());
WSACleanup(); //卸载WinSock库
return TRUE;
}
EditPrint(hwndEdit, TEXT("Socket %i created.\r\n"), sock);
//调用设置异步函数,为套接字请求基于Windows消息的网络事件通知
if (SOCKET_ERROR == WSAAsyncSelect(sock, hwnd, WM_SOCKET_NOTIFY,
FD_CONNECT | FD_READ))
{
EditPrint(hwndEdit, TEXT("WSAAsyncSelect error #%i.\r\n"),
WSAGetLastError());
closesocket(sock);
WSACleanup(); //卸载WinSock库
return TRUE;
}
//连接到指定的服务器和端口
sa.sin_family = AF_INET;
sa.sin_addr.S_un.S_addr = inet_addr(szIPAddr);
//IPPORT_TIMESERVER=37,定义在winsock.h文件
sa.sin_port = htons(IPPORT_TIMESERVER);
connect(sock, (SOCKADDR*)&sa, sizeof(SOCKADDR));
//connect函数会立即返回,并返回SOCKET_ERROR。即使成功,也会返回该值,因为该函
//数需要用阻塞方式使用,但这里却使用了非阻塞的方式,只有以下的情况才是真正错误
if (WSAEWOULDBLOCK !=(iError = WSAGetLastError()))
{
EditPrint(hwndEdit, TEXT("Connect error #%i.\r\n"), iError);
closesocket(sock);
WSACleanup();
return TRUE;
}
EditPrint(hwndEdit, TEXT("Connecting to %hs..."), szIPAddr);
//connect的结果将通过WM_SOCKET_NOTIFY(自定义)消息发送给窗口过程。
//设置定时器并改变按钮为Cancel
SetTimer(hwnd, ID_TIMER, 1000, NULL);
GetWindowText(hwndButton, szOKLabel, sizeof(szOKLabel) /
sizeof(TCHAR));
SetWindowText(hwndButton, TEXT("Cancel"));
SetWindowLong(hwnd, GWL_ID, IDCANCEL); //将按钮ID改为取消
return TRUE;
case IDCANCEL:
closesocket(sock);
sock = 0;
WSACleanup();
SetWindowText(hwndButton, szOKLabel);
SetWindowLong(hwndButton, GWL_ID, IDOK);
KillTimer(hwnd, ID_TIMER);
EditPrint(hwndEdit, TEXT("\r\nSocket closed.\r\n"));
return TRUE;
case IDC_CLOSE:
if (sock)
SendMessage(hwnd, WM_COMMAND, IDCANCEL, 0);
DestroyWindow(GetParent(hwnd));//销毁父窗口,本窗口也会自动被销毁
return TRUE;
}
break;
case WM_TIMER:
EditPrint(hwndEdit, TEXT("."));
return TRUE;
case WM_SOCKET_NOTIFY:
wEvent = WSAGETSELECTEVENT(lParam); //LOWORD,该宏返回网络事件
wError = WSAGETSELECTERROR(lParam); //HIWORD,该宏返回错误代码
//处理指定的两个异步事件
switch (wEvent)
{
case FD_CONNECT: //connect函数调用的结果
EditPrint(hwndEdit, TEXT("\r\n"));
if (wError) //连接失败
{
EditPrint(hwndEdit, TEXT("Connect error#%i."), wError);
SendMessage(hwnd, WM_COMMAND, IDCANCEL, 0);
return TRUE;
}
//连接成功
EditPrint(hwndEdit, TEXT("Connected to %hs.\r\n"), szIPAddr);
/*尝试去接收数据。该调用会产生一个WSAEWOULDBLOCK错误和一个FD_READ事件
最后一个为PEEK,表示只是看看,不会将其从输入缓冲队列中删除。该函数可能至少会从服务器获得//部分数据,必须在FD_READ中接收//剩余的数据。*/
recv(sock, (char*)&ulTime, 4, MSG_PEEK);
EditPrint(hwndEdit, TEXT("Waiting to receive..."));
return TRUE;
case FD_READ:
KillTimer(hwnd, ID_TIMER);
EditPrint(hwndEdit, TEXT("\r\n"));
if (wError)
{
EditPrint(hwndEdit, TEXT("FD_READ error#%i."), wError);
SendMessage(hwnd, WM_COMMAND, IDCANCEL, 0);
return TRUE;
}
//读取服务器的时间,ulTime是从1900.1.1 零时以来的秒数,并且是网络字节顺序4个字节//长度;最后一个参数为0,表示接收完后//从接收缓冲区删除队列数据
iSize = recv(sock, (char*)&ulTime, 4, 0);
ulTime = ntohl(ulTime);
EditPrint(hwndEdit, TEXT("Received current time of %u seconds ")
TEXT("since Jan.1 1900.\r\n"),ulTime);
//改变系统时间
ChangeSystemTime(hwndEdit, ulTime);
SendMessage(hwnd, WM_COMMAND, IDCANCEL, 0);
return TRUE;
}
return FALSE;
}
return FALSE;
}
BOOL CALLBACK ServerDlg(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static char* szServer;
static WORD wServer = IDC_SERVER1;
char szLabel[64];
char* pstr;
char* pContext;
switch (message)
{
case WM_INITDIALOG:
szServer = (char*)lParam;
//添加(选中)复选标记,并从组中的所有其他单选按钮中删除(清除)复选标记
CheckRadioButton(hwnd, IDC_SERVER1, IDC_SERVER10, wServer);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_SERVER1:
case IDC_SERVER2:
case IDC_SERVER3:
case IDC_SERVER4:
case IDC_SERVER5:
case IDC_SERVER6:
case IDC_SERVER7:
case IDC_SERVER8:
case IDC_SERVER9:
case IDC_SERVER10:
wServer = LOWORD(wParam);
return TRUE;
case IDOK:
GetDlgItemTextA(hwnd, wServer, szLabel, sizeof(szLabel));
//strok_s分割字符串,将szLabel中的指定的字符用\0替换以达到分割字符串的目的
//如“ntp-nist.ldsbc.net (198.60.73.8) LDSBC, Salt Lake City, Utah”当
//第一次调用strtok_s时,将左括号处的字符替换为\0,分割成2个字符串,返回值
//指定第一串的首字母的位置。第2次调用时(注意,参数转入NULL),将右括号
//替换为\0,返回值指向这里szLabel被分为三个串,返回值指向第2串字符串的首字母位置即IP地址的首字符。
//返回值指向"("之前的字符串,第1次调用转入szLabel
strtok_s(szLabel, "(",&pContext);
//返回值指向")"之前的字符串,即IP地址字符串
pstr = strtok_s(NULL, ")", &pContext); //第2次调用,转入NULL参数
strcpy_s(szServer,lstrlenA(pstr)+1,pstr);
EndDialog(hwnd, TRUE);
return TRUE;
case IDCANCEL:
EndDialog(hwnd, FALSE);
return TRUE;
}
break;
}
return FALSE;
}
void EditPrint(HWND hwndEdit, TCHAR* szFormat, ...)
{
TCHAR szBuffer[1024];
va_list pArtList;
va_start(pArtList, szFormat);
wvsprintf(szBuffer, szFormat, pArtList);
va_end(pArtList);
//wParam-1为取消选择,
SendMessage(hwndEdit, EM_SETSEL, (WPARAM)-1, (LPARAM)-1);
//在编辑框末尾加入文本
SendMessage(hwndEdit, EM_REPLACESEL, FALSE, (LPARAM)szBuffer);
SendMessage(hwndEdit, EM_SCROLLCARET, 0, 0);//将插入光标滚动到可视范围
}
//在Vista、Win7及以上版本的系统,更改系统时间需要管理员身份运行
//才能成功。
void ChangeSystemTime(HWND hwndEdit, ULONG ulTime)
{
FILETIME ftNew;
LARGE_INTEGER li;
SYSTEMTIME stOld, stNew;
GetLocalTime(&stOld);
stNew.wYear = 1900;
stNew.wMonth = 1;
stNew.wDay = 1;
stNew.wHour = 0;
stNew.wMinute = 0;
stNew.wSecond = 0;
stNew.wMilliseconds = 0;
SystemTimeToFileTime(&stNew, &ftNew);
li = *(LARGE_INTEGER*)&ftNew;
//1纳秒等于10亿分之一秒在,而ftNew的单位是100纳秒
li.QuadPart += (LONGLONG)10000000 * ulTime;
ftNew = *(FILETIME*)&li;
FileTimeToSystemTime(&ftNew, &stNew);
if (SetSystemTime(&stNew))
{
GetLocalTime(&stNew);
FormatUpdateTime(hwndEdit, &stOld, &stNew);
}
else
EditPrint(hwndEdit, TEXT("Could Not set new data and time."));
}
//GetDateFormat函数说明:
//作用:用来针对指定的“当地”格式,对一个系统日期进行格式化
//参数:
//Locale long:用来决定格式的地方ID
void FormatUpdateTime(HWND hwndEdit, SYSTEMTIME* pstOld, SYSTEMTIME* pstNew)
{
TCHAR szDataOld[64], szTimeOld[64], szDataNew[64], szTimeNew[64];
//pstOld格式化成“当地”格式和短日期格式并存放在szDataOld缓冲区中
//系统默认格式和短日期(如2015/7/3
GetDateFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | DATE_SHORTDATE,
pstOld,NULL,szDataOld,sizeof(szDataOld));
GetTimeFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE |
TIME_NOTIMEMARKER | TIME_FORCE24HOURFORMAT,
pstOld,NULL,szTimeOld,sizeof(szTimeOld)); //24小时制
//系统默认格式和短日期
GetDateFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | DATE_SHORTDATE,
pstNew, NULL, szDataNew, sizeof(szDataNew));
GetTimeFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE |
TIME_NOTIMEMARKER | TIME_FORCE24HOURFORMAT,//
pstNew, NULL, szTimeNew, sizeof(szTimeNew)); //24小时制
EditPrint(hwndEdit,
TEXT("System data and time successfully changed ")
TEXT("from\r\n\t%s, %s.%03i to\r\n\t%s, %s.%03i."),
szDataOld,szTimeOld,pstOld->wMilliseconds,
szDataNew,szTimeNew,pstNew->wMilliseconds);
}
/******************************************************************************
AdjustWindowRect函数:根据所需的客户端矩形大小,计算所需的窗口矩形大小。然后可以将窗口矩形传递给CreateWindow函数,
以创建其客户区域为所需大小的窗口。若要指定扩展窗口样式,请使用AdjustWindowRectEx函数。
BOOL AdjustWindowRect(
LPRECT lpRect, //指向RECT结构的指针,该结构包含所需客户区的左上角和右下角的坐标。
DWORD dwStyle,//要计算其所需大小的窗口的窗口样式。
BOOL bMenu //指示窗口是否具有菜单。
);
*******************************************************************************
DialogBoxParamA函数:从对话框模板资源创建模式对话框。在显示对话框之前,
该函数将应用程序定义的值作为WM_INITDIALOG消息的lParam参数传递给对话框过程。
应用程序可以使用此值来初始化对话框控件。
INT_PTR DialogBoxParamA(
HINSTANCE hInstance, //包含对话框模板的模块的句柄。
LPCSTR lpTemplateName,//对话框模板。此参数是指向以零结尾的字符串的指针,该字符串指定对话框模板的名称,或者为整数值,该整数值指定对话框模板的资源标识符。
HWND hWndParent, //拥有对话框的窗口的句柄。
DLGPROC lpDialogFunc,//指向对话框过程的指针。
LPARAM dwInitParam //在WM_INITDIALOG消息的lParam参数中传递给对话框的值。
);
返回值
类型:INT_PTR
如果函数成功,则返回值是在用于终止对话框的EndDialog函数的调用中指定的nResult参数的值。
*******************************************************************************
WSAStartup函数:启动进程对Winsock DLL的使用。WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数。
它允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。
应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。
int WSAStartup(
WORD wVersionRequired,//待定
LPWSADATA lpWSAData //指向WSADATA数据结构的指针,该 数据结构将接收Windows套接字实现的详细信息。
);
返回值
如果成功,则 WSAStartup函数将返回零。否则,它将返回下面列出的错误代码。
*******************************************************************************
socket函数:是一种可用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数。
SOCKET WSAAPI socket(
int af, //地址族规范。地址族的可能值在Winsock2.h头文件中定义。当前支持的值为AF_INET或AF_INET6,这是IPv4和IPv6的Internet地址族格式。
int type,//新套接字的类型规范。套接字类型的可能值在Winsock2.h头文件中定义。SOCK_STREAM、SOCK_DGRAM、SOCK_RAW
int protocol//要使用的协议。IPPROTO_TCP/IPPROTO_UDP
);
如果未发生错误,则 套接字返回引用新套接字的描述符。返回值存储在一个SOCKET类型的变 量中,以后调用套接字函数时会用到它。
否则,将返回INVALID_SOCKET的值,并且可以通过调用WSAGetLastError来检索特定的错误代码 。
*******************************************************************************
WSAAsyncSelect函数:为套接字请求基于Windows消息的网络事件通知。
int WSAAsyncSelect(
SOCKET s, //一个描述符,用于标识需要事件通知的套接字。
HWND hWnd,//标识在发生网络事件时将接收消息的窗口的句柄。
u_int wMsg,//网络事件发生时要接收的消息。
long lEvent//一个位掩码,它指定应用程序感兴趣的网络事件的组合。
);
返回值
如果 WSAAsyncSelect函数成功执行,则返回值将为零,前提是应用程序对网络事件集的兴趣声明成功。
否则,将返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误号 。
*******************************************************************************
closesocket函数:关闭一个套接口。
int closesocket(
IN SOCKET s //标识要关闭的套接字的描述符。
);
*******************************************************************************
WSACleanup():终止Winsock2 DLL (Ws2_32.dll) 的使用
返回值
如果操作成功,则返回值为零。否则,将返回值SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误号 。
*******************************************************************************
SOCKADDR_IN结构:取决于所选协议。除sin * _family参数外,sockaddr内容以网络字节顺序表示。
typedef struct sockaddr_in {
short sin_family;//值设为AF_INET,以指定地址族
u_short sin_port;//设 定端口号
struct in_addr sin_addr;//一个联合,它可以让你使用4个字节或2个无符号短整数或一个无符号长 整数来表示互联网地址。
char sin_zero[8];//空字节
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;
*******************************************************************************
connect()函数:建立与指定socket的连接。
int WSAAPI connect(
SOCKET s,//标识未连接套接字的描述符。
const sockaddr *name,//指向应建立连接的sockaddr结构的指针 。
int namelen//name参数所指向的sockaddr结构的长度(以字节为单位)。
);
返回值
如果没有错误发生, connect将返回零。否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码 。
在阻塞套接字上,返回值指示连接尝试成功或失败。
使用非阻塞套接字,无法立即完成连接尝试。在这种情况下, connect将返回SOCKET_ERROR,而 WSAGetLastError将返回 WSAEWOULDBLOCK。
*******************************************************************************
recv函数:从已连接的套接字或绑定的无连接套接字接收数据。
int recv(
SOCKET s,//标识已连接套接字的描述符
char *buf,//指向缓冲区以接收传入数据的指针。
int len,//buf参数指向的缓冲区的长度(以字节为单位)。
int flags//一组影响此功能行为的标志。
);
返回值
如果未发生错误,则 recv返回接收到的字节数,并且buf参数指向的缓冲区将包含接收到的该数据。如果已正常关闭连接,则返回值为零。
否则,将返回SOCKET_ERROR的值,并且可以通过调用WSAGetLastError来检索特定的错误代码 。
*******************************************************************************
ntohl函数:是将一个无符号长整形数从网络字节顺序转换为主机字节顺序, ntohl()返回一个以主机字节顺序表达的数。
u_long ntohl(
u_long netlong//TCP / IP网络字节顺序的32位数字。
);
*/
Resource.h
#define IDC_SERVER1 1001
#define IDC_SERVER2 1002
#define IDC_SERVER3 1003
#define IDC_SERVER4 1004
#define IDC_SERVER5 1005
#define IDC_SERVER6 1006
#define IDC_SERVER7 1007
#define IDC_SERVER8 1008
#define IDC_SERVER9 1009
#define IDC_SERVER10 1010
#define IDC_CLOSE 1011
#define IDC_SERVER 1012
#define IDC_TEXTOUT 1013
// 新对象的下一组默认值
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 103
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1005
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
NETTIME.rc
/
//
// Dialog
//
SERVERS DIALOGEX 0, 0, 271, 176
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "NIST Time Service Servers"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
DEFPUSHBUTTON "OK",IDOK,70,148,50,14
PUSHBUTTON "Cancel",IDCANCEL,143,148,50,14
CONTROL "time-a.timefreq.bldrdoc.gov (132.163.4.101) NIST, Boulder, Colorado",IDC_SERVER1,
"Button",BS_AUTORADIOBUTTON,15,12,241,10
CONTROL "time-b.timefreq.bldrdoc.gov (132.163.4.102) NIST, Boulder, Colorado",IDC_SERVER2,
"Button",BS_AUTORADIOBUTTON,15,25,241,10
CONTROL "time-c.timefreq.bldrdoc.gov (132.163.4.103) NIST, Boulder, Colorado",IDC_SERVER3,
"Button",BS_AUTORADIOBUTTON,15,38,220,10
CONTROL "utcnist.colorado.edu (128.138.140.44) University of Colorado, Boulder",IDC_SERVER4,
"Button",BS_AUTORADIOBUTTON,15,51,243,10
CONTROL "nist1-pa.ustiming.org (206.246.122.250) Hatfield, PA",IDC_SERVER5,
"Button",BS_AUTORADIOBUTTON,15,64,188,10
CONTROL "ntp-nist.ldsbc.net (198.60.73.8) LDSBC, Salt Lake City, Utah",IDC_SERVER6,
"Button",BS_AUTORADIOBUTTON,15,77,209,10
CONTROL "nist1-lv.ustiming.org (64.250.229.100) Las Vegas, Nevada",IDC_SERVER7,
"Button",BS_AUTORADIOBUTTON,15,90,209,10
CONTROL "time-nw.nist.gov (131.107.13.100) Microsoft, Redmond, Washington",IDC_SERVER8,
"Button",BS_AUTORADIOBUTTON,15,103,238,10
CONTROL "nist-time-server.eoni.com (216.228.192.69) La Grande, Oregon",IDC_SERVER9,
"Button",BS_AUTORADIOBUTTON,15,116,215,10
CONTROL "wwv.nist.gov (24.56.178.140) WWV, Fort Collins, Colorado",IDC_SERVER10,
"Button",BS_AUTORADIOBUTTON,15,129,236,10
END
NETTIME DIALOGEX 0, 0, 291, 176
STYLE DS_SETFONT | WS_CHILD
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
DEFPUSHBUTTON "Set Correct Time",IDOK,128,148,74,14
PUSHBUTTON "Close",IDC_CLOSE,215,148,50,14
PUSHBUTTON "Select Server...",IDC_SERVER,18,148,97,14
EDITTEXT IDC_TEXTOUT,16,14,260,126,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL | NOT WS_TABSTOP
END
/
//
// DESIGNINFO
//
#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
"SERVERS", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 264
TOPMARGIN, 7
BOTTOMMARGIN, 169
END
"NETTIME", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 284
TOPMARGIN, 7
BOTTOMMARGIN, 169
END
END
#endif // APSTUDIO_INVOKED
#endif // 中文(简体,中国) 资源
/
运行结果
图24-5 时间网络校验
总结
实例NETTIME.C依据NETTIME.RC文件里的NETTIME模板创建一个非模态对话框。程序重新调整了窗口的尺寸,使得该非模态对话框可以覆盖该程序的 整个客户区。该对话框包括一个只读的编辑区(程序在其中写入文字信息)、一个Select Server 按钮、一个Set Correct Time按钮和一个Close按钮。Close按钮用于终止程序。
MainDlg中的szIPAddr变量用于存储服务器的地址,该变量的默认值为字符"132.163.4.101" 。使用 Select Server 按钮将弹出一个基于NETTIME.RC 文件中 SERVERS 模板的对话框。变量szIPAddr作为最后一个参数被传给DialogBoxParam。Server对话框中列出了 10个我们感兴趣的可提供时间服务的服务器(从NIST的网站上逐字复制下来的)。当用户选定一个服务器之后,ServerDlg将解析按钮的文本来获取IP地址。新的地址存储 在szIPAddr变量中。
在用户按下Set Correct Time按钮时,会发送一个WM_COMMAND消息,其wParam 参数的低位字等于IDOK。大部分对套接字的操作始于MainDlg中IDOK的处理过程。
任何使用Windows套接API的Windows程序调用的第一个函数必须是:
iError = WSAStartup (wVersion, &WSAData);
NETTIME将第一个参数值设为0x0200(表示是2.0版)。函数返回时,WSAData结构中包 含了有关Windows套接字的一些信息,而NETTIME将显示szDescription字符串的内容。 它们只是提供一些版本信息。
下一步,NETTIME如下调用了 socket函数:
sock = socket (AF_INET, SOCK_STREAM, IPPR0T0_TCP);
第一个参数是一个地址族,用来指明互联网地址的类别。第二个参数指明返回的数据是数 据流(stream)形式而不是数据报(datagram)形式。(我们预计数据长度只有4个字节,数据报比较适用于大块的数据。)最后一个参数是协议类型,我们指明为TCP(传输控制协议)。这 是RFC-868中指定的两项协议之一。函数socket的返回值存储在一个SOCKET类型的变 量中,以后调用套接字函数时会用到它。
NETTIME下一步调用WSAAsyncSelect,这是另一个Windows特有的套接字函数。这个函数用于避免一个应用程序因为互联网的反应时间过于缓慢而死等在那里。在 Winsock文档中,一些函数被称为“阻塞式”,这是指它们不能保证将控制权立即返回给程序。使用函数WSAAsyncSelect的目的在于强制将阻塞式的函数转变为非阻塞式的,即在函数执行结束之前,就把控制权返还给调用程序。函数的结果会通过消息的形式传给应用程序。函数WSAAsyncSelect可让应用程序指定消息的数值和接受这一消息的窗口。通常,该函数的语法如下:
WSAAsyncSelect (sock, hwnd, message, iConditionsl;
NETTIME使用程序特定的消息WM_SOCKET_NOTIFY来完成此项任务。WSAAsyncSelect 的最后一个参数用来指定在何种条件下发送这个消息,特别是连接和接收数据 (FD_CONNECT 丨 FD_READ)期间。
NETTIME下一步调用的WinSock函数是connect。该函数需要一个指向套接字地址结 构的指针。对不同的协议,该套接字地址结构是不同的。NETTIME使用以下为TCP/IP协 议设计的套接字地址结构:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中的in_addr是一个联合,它可以让你使用4个字节或2个无符号短整数或一个无符号长 整数来表示互联网地址。
NETTIME把sin_family的值设为AF_INET,以指定地址族。字段sin_port的值用于设 定端口号,在这里我们使用RFC-868指定的时间协议端口号37。但不能简单地把值设为 37。和大多数在互联网上传输的数值一样,这个结构的端口号必须遵从大字节序 (big-endian),即高字节在前面。而英特尔微处理器则遵从小字节序(little-endian)。幸运的是,htons函数(“host-to-network short”——中文含义为把16位的数值从主机的字节序转换为网络字节序)可实现字节翻转,因此NETTIME将结构sockaddr_in的sin_port字段设定为:
htons (IPPORT_TXMESERVER)
WINSOCK2.H 中,常数 IPPORT_TIMESERVER 的定义为 37。NETTIME 使用 inet_addr 函数将存储在szIPAddr字符串中的服务器地址转换成一个无符号的长整数,用它来设置结构 中sin_addr字段的值。
如果一个应用程序在Windows 98中调用connect函数,而当时Windows没有接在互联 网上,则将出现一个拨号连接对话框。此功能称为自动拨号。Windows NT 4.0没有这个功能,所以如果运行的是NT,就必须先连接到互联网,然后再运行NETTIME。
函数connect通常是阻塞式的,因为它可能需要花一段时间才能连接上互联网。然而, 由于NETTIME调用了 WSAAsyncSelect,所以connect函数不会等待连接结束,而是立即返回一个SOCKET_ERROR值。这并不是一个真正的错误,只是表明连接尚未完成。 NETTIME会忽略这个返回值,取而代之的是,NETTIME调用了 WSAGetLastError函数。如果WSAGetLastError返回值为WSAEWOULDBLOCK(即表示该函数通常是阻塞式的,这里却被用于非阻塞的方式),那么就表示一切正常。NETTIME将Set Correct Time按钮改成 Cancel按钮,并启动一个周期为1秒的计时器。WM_TIMER在该程序的窗口里以简单的句点向用户显示程序还在继续运行没有死掉。
在一个连接最终完成时,MainDlg会接收到一条WM_SOCKET_NOTIFY消息——这是NETTIME在调用函数WSAAsyncSelect时指定的消息。该消息的IParam值的低位字等于FD_CONNECT,而高位字则指示可能出现的错误。这时的错误可能表明该程序无法连接到指定的服务器。NETTIME会提供另外9个服务器,可以选择其他的服务器进行尝试。
如果一切顺利,NETTIME将调用recv( “receive” )函数来读取数据:
recv (sock, (char *) fiulTirae, 4, MSG_PEEK);
这表示要接收4个字节,并把它们存储在ulTime变量里。最后一个参数MSG_PEEK表示 只是想“看看”这个数据,而不会将其从输入队列中删除。类似于connect函£ recv函数也会返回一个错误代码,以表明该函数通常情况下会阻塞,但此时却被用于非阻塞的方式。从理论上讲(虽然不太可能该函数可能会至少返回一部分数据。程序必须再次调用recv 函数来获得32位值的其余部分数值。这就是为什么调用recv函数时要使用MSG_PEEK选项的原因。
与connect函数类似,recv函数也产生一条WM_SOCKET_NOTIFY消息,此时的事件代码为FD_READ。处理这一消息时,NETTIME会再次调用recv函数。这一次该函数的最后一个参数值为0,以指示将数据从输入队列中删除。我将简要介绍程序如何处理收到的ulTime值。请注意,NETTIME通过给自己发送一条wParam等于IDCANCEL的 WM_COMMAND消息来终结对该消息的处理。对话框过程收到该消息后,调用closesocket 和WSACleanup函数来做出响应。
NETTIME收到的32位ulTime值是自UTC 1900年1月1日0时以来的秒数,不过最高顺序字节在第一位。该数值必须经ntohl(“network-to-host long”-中文含义为把一个32位的数字从网络字节序转换成主机字节序)函数重新排列字节顺序,以便英特尔微处理器能够处理。下一步,NETTIME调用ChangeSystemTime函数。
ChangeSystemTime首先获得本地的当前时间,也就是位于用户的时区并经过夏令时调 整后的当前系统时间。然后它建立一个SYSTEMTIME结构,其值为1900年I月1日0时。 然后,SystemTimeToFileTime 函数将该 SYSTEMTIME 结构转换为 FILETIME 结构。 FILETIME实际上是由两个32位DWORD构成的一个64位整数,用于表示从1601年1 月1 口 0时起以100纳秒为单位的时间间隔段数。
函数ChangeSystemTime 把该 FILETIME 结构转换成 LARGEJNTEGER。 LARGEJNTEGER是一个联合,它可允许其64位的数值被用作两个32位值成一个基于 _int64数据类型的64位整数。(此数据类型是Microsoft编译器对ANSI C标准的一个扩充。) 转换后的数值表示了从1601年1月1日到1900年1月1日之间的100纳秒时间间隔的数目。在这个数值上再加上从1900年1月1日0时到现在的100纳秒的时间间隔的数目(就是ulTime值的一千万倍)。
由此产生的 FILETIME 值,再通过 FileTimeToSystemTime 函数转换M SYSTEMTIME 结构。因为时间协议返回的是当前的UTC时间,所以NETTIME必须通过调用 SetSystemTime函数来设定时间,因为SetSystemTime也基于UTC的。至于实际显示的时间数值,是程序通过调WGetLocalTime函数获得的更新了的本地时间。原来的本地时间和新的本地时间都传递给函数FormatUpdatedTime,该函数调用GetTimeFormat函数和 GetDateFormat函数来将时间转换为ASCII字符串。
如果程序运行在Windows NT操作系统下,而用户没有设定时间的权限,则调用 SetSystemTime函数可能会失败。如果SetSystemTime失败,NETTIME将显示一条信息,表示新的时间未被设定。