23.1 Windows 套接字

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P1081

        套接字是美国加利福尼亚大学伯克利分校为了给 UNIX 操作系统添加网络通信支持而开发的一个概念。由他们开发的 API 如今被称为 “伯克利套接字接口” (Berkeley socket interface)。

23.1.1  套接字和 TCP/IP

        套接字通常与占互联网主导地位的 TCP/IP 协议一起使用,但并不局限于此。TCP/IP 协议的组成部分之一,IP 协议(网际协议),把数据封装成一个个数据报(Datagram),数据的来源和目的地信息都存放在数据报的报头中。而 TCP(传输控制协议)则提供了一种可以对 IP 数据报进行可靠传输和错误检测的方法。

        在 TCP/IP 协议中,通信端点由 IP 地址和端口号来定义。IP 地址包含 4 个字节,用来确定互联网上的任一台服务器。IP 地址通常显示为“由点分隔的四个十进制数字”,例如“209.86.105.231”。端口号被用来确定服务器提供的特定服务或程序其中的一些端口号已经有标准化的定义,用于提供一些广为人知的服务

        当一个套接字和 TCP/IP 一起使用时,套接字就是 TCP/IP 通信的端点。因此,套接字指定了一个 IP 地址和一个端口号。

23.1.2  网络时间服务

        下面的范例程序将展示如何连接到一台提供时间协议服务的互联网服务器。该范例程序获取当前的确切日期和时间,并使用该信息来设置 PC 的时钟。

        在美国,国家标准和技术研究所 NIST(原国家标准局)负责与世界各地的相应机构一起维护精确的时间。精确的时间通过无线电广播、电话号码、计算机拨号上网以及互联网提供给公众,所有这些资料都保存在http://ww.bldrdoc.gov/timefreq 网站上。(网络域名 “bldrdoc” 指的是 Boulder,Colorado,那是 NIST 的时间与频率部门的所在地。)

        我们感兴趣的是 NIST 提供的网络时间服务,其更详细的信息记录于 http://www.bldrdoc.gov/timefreq/service/nts.htm(译注:该网页已不再有效,可以打开http://tf.nist.gov/tf-cgi/servers.cgi 网页获得提供 NIST 时间服务的服务器列表)。此网页列出了 10 个提供 NIST 时间服务的服务器。例如,第一个叫做 time-a.timefreq.bldrdoc.gov,其 IP 地址是 132.163.125.130(译注:现在该服务器的 IP 地址为 132.163.4.101)。

        (我曾经编写过一个使用非互联网 NIST 的计算机拨号上网服务程序,发表于 PC Magazine,在 Ziff-Davis 网站 http://www.zdnet.com/pcmga/pctech/content/16/20/ut1620.001.htm 上可以找到。该程序对希望学习使用 Windows Telephony API 的读者会有帮助。)

        在互联网上有三种不同的时间服务,每一种服务都由 RFC(Request for Comment,请求注解)描述为互联网标准。日期时间协议(Daytime Protocol)(RFC-867)定义了如何用 ASCII 字符串表示准确的日期和时间。这个 ASCII 字符串的确切格式不是很标准,但它的目的是具有可读性。时间协议(Time Protocol)(RFC-868)提供了一个 32 位数字以表示从 1900 年 1 月 1 日午夜至今的秒数。该时间是 UTC(不考虑字母顺序,它代表 Coordinated Universal Time,世界协调时)时间,与所谓的格林尼治标准时间(Greenwich Mean Time, GMT)即英国格林尼治的时间非常接近。第三个协议叫网络时间协议(Network Time Protocol)(RFC-1305),该协议相当复杂。

        对于我们的初衷,即尝试使用套接字和更新我们的计算机时钟,时间协议已经足够适用了。RFC-868 是一个仅有两页的简短文档,主要描述了对一个想用 TCP 来得到确定时间的程序,所应采用的步骤。

  1.   连接到提供这项服务的服务器的 37 号端口。
  2.   接收 32 位的时间。
  3.   关闭连接。

        到此,所有我们需要知道的编写使用套接字访问这个时间服务的应用程序的相关信息都已经具备。

23.1.3  NETTIME 程序

        Windows 套接字 API,通常被称为 WinSock,是与伯克利套接字 API 相兼容的。UNIX 套接字的代码可以很轻松地移植到 Windows 操作系统。Windows 更提供了一组以 WSA(“WinSock API”)为前缀的函数作为对伯克利套接字的扩充,以提供进一步的支持。与此相关的概述和详细资料都放在了 /Platform SDK/Networking and Distributed Services/Windows Sockets Version 2。(译注://MSDN Library/Win32 and COM Development/Networking/Network Protocols/Windows Sockets 2。)

        图 23-1 的 NETTIME 程序演示了如何使用 WinSock API。(译注:在 Vista 和 Windows 7 中,一般的应用程序都使用非管理员令牌运行。由于该程序需要修改系统时间,而该操作需要管理员权限,所以必须提升该程序以管理员身份运行它,才能成功修改系统时间。)

/*-------------------------------------------------------
   NETTIME.C -- Sets System Clock from Internet Services
		(c) Charles Petzold, 1998
-------------------------------------------------------*/

#include <Windows.h>
#include "resource.h"

#define WM_SOCKET_NOTIFY (WM_USER + 1)
#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 ChangeSystemTime(HWND hwndEdit, ULONG ulTime);
void FormatUpdatedTime(HWND hwndEdit, SYSTEMTIME * pstOld, SYSTEMTIME * pstNew);

void EditPrintf(HWND hwndEdit, TCHAR * szFormat, ...);
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.hInstance = hInstance;
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor = NULL;
	wndclass.hbrBackground = NULL;
	wndclass.lpszMenuName = NULL;
	wndclass.lpszClassName = szAppName;

	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);

		// Create the modeless dialog box to go on top of the window

	hwndModeless = CreateDialog(hInstance, szAppName, hwnd, MainDlg);

		// Size the main parent window to the size of the dialog box.
		// Show both windows.

	GetWindowRect(hwndModeless, &rect);
	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);

	// Normal message loop when a modeless dialog box is used.

	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.135.130" };
	static char	  szIPAddr[32] = { "132.163.4.101" };
	static HWND	  hwndButton, hwndEdit;
	static SOCKET sock;
	static struct sockaddr_in sa;
	static TCHAR  szOKLabel[32];
	int			  iError, iSize;
	unsigned long ulTime;
	WORD		  wEvent, wError;
	WSADATA		  WSAData;

	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:
				// Call "WSAStartup" and display description text

			if (iError = WSAStartup(MAKEWORD(2, 0), &WSAData))
			{
				EditPrintf(hwndEdit, TEXT("Startup error #%i.\r\n"),
							iError);
				return TRUE;
			}
			EditPrintf(hwndEdit, TEXT("Started up %hs\r\n"), WSAData.szDescription);

				// Call 'socket'

			sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

			if (sock == INVALID_SOCKET)
			{
				EditPrintf(hwndEdit, TEXT("Socket creation error #%i.\r\n"), WSAGetLastError());
				WSACleanup();
				return TRUE;
			}
			EditPrintf(hwndEdit, TEXT("Socket %i created.\r\n"), sock);

				// Call "WSAAsyncSelect"

			if (SOCKET_ERROR == WSAAsyncSelect(sock, hwnd, WM_SOCKET_NOTIFY, FD_CONNECT | FD_READ))
			{
				EditPrintf(hwndEdit, TEXT("WSAAsyncSelect error #%i.\r\n"), WSAGetLastError());
				closesocket(sock);
				WSACleanup();
				return TRUE;
			}

				// Call "connect" with IP address and time-server port

			sa.sin_family = AF_INET;
			sa.sin_port = htons(IPPORT_TIMESERVER);
			sa.sin_addr.S_un.S_addr = inet_addr(szIPAddr);
			connect(sock, (SOCKADDR *)&sa, sizeof(sa));

				// "connect" will return SOCKET_ERROR because even if it
				// succeeds, it will require blocking. The following only
				// reports unexpected errors.

			if (WSAEWOULDBLOCK != (iError = WSAGetLastError()))
			{
				EditPrintf(hwndEdit, TEXT("Connect error #%i.\r\n"), iError);
				closesocket(sock);
				WSACleanup();
				return TRUE;
			}
			EditPrintf(hwndEdit, TEXT("Connecting to %hs..."), szIPAddr);

				// The result of the "connect" call will be reported
				// through the WM_SOCKET_NOTIFY message.
				// Set timer and change the button to "Cancel"

            SetTimer (hwnd, ID_TIMER, 1000, NULL) ;
            GetWindowText (hwndButton, szOKLabel, sizeof (szOKLabel) /
                                                    sizeof (TCHAR)) ;
            SetWindowText (hwndButton, TEXT ("Cancel")) ;
            SetWindowLong (hwndButton, GWL_ID, IDCANCEL) ;
            return TRUE ;

		case IDCANCEL:
			closesocket(sock);
			sock = 0;
			WSACleanup();
			SetWindowText(hwndButton, szOKLabel);
			SetWindowLong(hwndButton, GWL_ID, IDOK);
			
			KillTimer(hwnd, ID_TIMER);
			EditPrintf(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;
		}
		return FALSE;

	case WM_TIMER:
		EditPrintf(hwndEdit, TEXT("."));
		return TRUE;

	case WM_SOCKET_NOTIFY:
		wEvent = WSAGETSELECTEVENT(lParam);	// ie, LOWORD
		wError = WSAGETSELECTERROR(lParam); // ie, HIWORD

			// Process two evetns specified in WSAAsyncSelect

		switch (wEvent)
		{
				// This event occurs as a result of the "connect" call
		case FD_CONNECT:
			EditPrintf(hwndEdit, TEXT("\r\n"));

			if (wError)
			{
				EditPrintf(hwndEdit, TEXT("Connect error #%i."), wError);

				SendMessage(hwnd, WM_COMMAND, IDCANCEL, 0);
				return TRUE;
			}
			EditPrintf(hwndEdit, TEXT("Connected to %hs.\r\n"), szIPAddr);

				// Try to receive data. The call will generate an error
				// of WSAEWOULDBLOCK and an event of FD_READ
			recv(sock, (char *)&ulTime, 4, MSG_PEEK);
			EditPrintf(hwndEdit, TEXT("Waiting to receive..."));
			return TRUE;

				// This even occurs when the "recv" call can be made

		case FD_READ:
			KillTimer(hwnd, ID_TIMER);
			EditPrintf(hwndEdit, TEXT("\r\n"));

			if (wError)
			{
				EditPrintf(hwndEdit, TEXT("FD_READ error #%i."), wError);
				SendMessage(hwnd, WM_COMMAND, IDCANCEL, 0);
				return TRUE;
			}
				// Get the time and swap the bytes

			iSize = recv(sock, (char *)&ulTime, 4, 0);
			ulTime = ntohl(ulTime);
			EditPrintf(hwndEdit, TEXT("Received current time of %u seconds ")
								 TEXT("since Jan. 1 1900.\r\n"), ulTime);

				// Change the system time

			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];

	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));
			strtok(szLabel, "(");
			strcpy(szServer, strtok(NULL, ")"));
			EndDialog(hwnd, TRUE);
			return TRUE;

		case IDCANCEL:
			EndDialog(hwnd, FALSE);
			return TRUE;
		}
		break;
	}
	return FALSE;
}

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;
	li.QuadPart += (LONGLONG)10000000 * ulTime;
	ftNew = *(FILETIME *)&li;
	FileTimeToSystemTime(&ftNew, &stNew);

	if (SetSystemTime(&stNew))
	{
		GetLocalTime(&stNew);
		FormatUpdatedTime(hwndEdit, &stOld, &stNew);
	}
	else
		EditPrintf(hwndEdit, TEXT("Could NOT set new date and time."));
}

void FormatUpdatedTime(HWND hwndEdit, SYSTEMTIME * pstOld, SYSTEMTIME * pstNew)
{
	TCHAR szDateOld[64], szTimeOld[64], szDateNew[64], szTimeNew[64];

	GetDateFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | DATE_SHORTDATE,
		pstOld, NULL, szDateOld, sizeof(szDateOld));

	GetTimeFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | TIME_NOTIMEMARKER | TIME_FORCE24HOURFORMAT,
		pstOld, NULL, szTimeOld, sizeof(szTimeOld));

	GetDateFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | DATE_SHORTDATE,
		pstNew, NULL, szDateNew, sizeof(szDateNew));

	GetTimeFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | TIME_NOTIMEMARKER | TIME_FORCE24HOURFORMAT,
		pstNew, NULL, szTimeNew, sizeof(szTimeNew));

	EditPrintf(hwndEdit, TEXT("System date and time successfully changed ")
				TEXT("from\r\n\t%s, %s.%03i to\r\n\t%s, %s.%03i."),
				szDateOld, szTimeOld, pstOld->wMilliseconds,
				szDateNew, szTimeNew, pstNew->wMilliseconds);
}

void EditPrintf(HWND hwndEdit, TCHAR * szFormat, ...)
{
	TCHAR szBuffer[1024];

	va_list pArgList;

	va_start(pArgList, szFormat);
	wvsprintf(szBuffer, szFormat, pArgList);
	va_end(pArgList);
	SendMessage(hwndEdit, EM_SETSEL, (WPARAM)-1, (LPARAM)-1);
	SendMessage(hwndEdit, EM_REPLACESEL, FALSE, (LPARAM)szBuffer);
	SendMessage(hwndEdit, EM_SCROLLCARET, 0, 0);
}
NETTIME.RC (excerpts)

// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"

/
//
// Dialog
//

SERVERS DIALOG DISCARDABLE  20, 20, 274, 202
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "NIST Time Service Servers"
FONT 8, "MS Sans Serif"
BEGIN
    DEFPUSHBUTTON   "OK", IDOK, 73, 181, 50, 14
    PUSHBUTTON      "Cancel", IDCANCEL, 150, 181, 50, 14
    CONTROL         "time-a.timefreq.bldrdoc.gov (132.163.135.130) NIST, Boulder, Colorado",
    IDC_SERVER1, "Button", BS_AUTORADIOBUTTON, 9, 7, 256, 16
    CONTROL         "time-b.timefreq.bldrdoc.gov (132.163.135.131) NIST, Boulder, Colorado",
    IDC_SERVER2, "Button", BS_AUTORADIOBUTTON, 9, 24, 256, 16
    CONTROL         "time-c.timefreq.bldrdoc.gov (132.163.135.132) Boulder, Colorado, ",
    IDC_SERVER3, "Button", BS_AUTORADIOBUTTON, 9, 41, 256, 16
    CONTROL         "utcnist.colorado.edu (128.138.140.44) University of Colorado, Boulder",
    IDC_SERVER4, "Button", BS_AUTORADIOBUTTON, 9, 58, 256, 16
    CONTROL         "time.nist.gov (192.43.244.18) NCAR, Boulder, Colorado",
    IDC_SERVER5, "Button", BS_AUTORADIOBUTTON, 9, 75, 256, 16
    CONTROL         "time-a.nist.gov (129.6.16.35) NIST, Gaithersburg, Maryland",
    IDC_SERVER6, "Button", BS_AUTORADIOBUTTON, 9, 92, 256, 16
    CONTROL         "time-b.nist.gov (129.6.16.36) NIST, Gaithersburg, Maryland",
    IDC_SERVER7, "Button", BS_AUTORADIOBUTTON, 9, 109, 256, 16
    CONTROL         "time-nw.nist.gov (131.107.1.10) Microsoft, Redmond, Washington",
    IDC_SERVER8, "Button", BS_AUTORADIOBUTTON, 9, 126, 256, 16
    CONTROL         "utcnist.reston.mci.net (204.70.131.13) MCI, Reston, Virginia",
    IDC_SERVER9, "Button", BS_AUTORADIOBUTTON, 9, 143, 256, 16
    CONTROL         "nist1.data.com (209.0.72.7) Datum, San Jose, California",
    IDC_SERVER10, "Button", BS_AUTORADIOBUTTON, 9, 160, 256, 16
END

NETTIME DIALOG DISCARDABLE  0, 0, 270, 150
STYLE WS_CHILD
FONT 8, "MS Sans Serif"
BEGIN
    DEFPUSHBUTTON   "Set Correct Time", IDOK, 95, 129, 80, 14
    PUSHBUTTON      "Close", IDC_CLOSE, 183, 129, 80, 14
    PUSHBUTTON      "Select Server...", IDC_SERVER, 7, 129, 80, 14
    EDITTEXT        IDC_TEXTOUT, 7, 7, 253, 110, ES_MULTILINE | ES_AUTOVSCROLL |
    ES_READONLY | WS_VSCROLL | NOT WS_TABSTOP
END
RESOURCE.H (excerpts)

// Microsoft Visual C++ generated include file.
// Used by NetTime.rc
#define IDC_TEXTOUT                     101
#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_SERVER                      1011
#define IDC_CLOSE                       1012

        从程序结构上看,NETTIME 程序依据 NETTIME.RC 文件里的 NETTIME 模板创建了一个非模态对话框。程序重新调整了窗口的尺寸,使得该非模态对话框可以覆盖该程序的整个客户区。该对话框包括一个只读的编辑区(程序在其中写入文字信息)、一个 Select Server 按钮、一个 Set Correct Time 按钮和一个 Close 按钮。Close 按钮用于终止程序。

        MainDlg 中的 szIPAddr 变量用于存储服务器的地址,该变量的默认值为字符串 “132.162.135.130”。使用 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 将显示 szDesctiption 字符串的内容。它们只是提供一些版本信息。

        下一步,NETTIME 如下调用了 socket 函数:

sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
第一个参数是一个地址族,用来指明互联网地址的类别。第二个参数指明返回的数据是数据流(stream)形式而不是数据报(datagram)形式。(我们预计数据长度只有 4 个字节, 数据报比较适用于大块的数据。)最后一个参数是协议类型,我们指明为 TCP(传输控制协议)。这是 RFC-868 中指定的两项协议之一。函数 socket 的返回值存储在一个 SOCKET 类型的变量中,以后调用套接字函数时会用到它。

        NETTIME 下一步调用 WSAAsyncSelect,这是另一个 Windows 特有的套接字函数。这个函数一用于避免一个应用程序因为互联网的反应时间过于缓慢而死等在那里。在 Windows 文档中,一些函数被称为“阻塞式”,这是指它们不能保证将控制权立即返回给程序。使用函数 WSAAsyncSelect 的目的在于强制将阻塞式的函数转变为非阻塞式的,即在函数执行结束之前,就把控制权返还给调用程序。函数的结果会通过消息的形式传给应用程序。函数 WSAAsyncSelect 可让应用程序指定消息的数值和接受这一消息的窗口。通常,该函数的语法如下:

WSAAsyncSelect(sock, hwnd, message, iConditions);
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_TIMESERVER);
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 返回值为 WSAEWOLUDBLOCK(即表示该函数通常是阻塞式的,这里却被用于非阻塞的方式),那么就表示一切正常。NETTIME 将 Set Correct Time 按钮改成 Cancel 按钮,并启动一个周期为 1 秒的计时器。WM_TIMER 在该程序的窗口里以简单的句点向用户显示程序还在继续运行没有死掉。

        在一个连接最终完成时,MainDlg 会接收到一条 WM_SOCKET_NOTIFY 消息——这是 NETTIME 在调用函数 WSAAsyncSelect 时指定的消息。该消息的 lParam 值的低位字等于 FD_CONNECT,而高位字则指示可能出现的错误,这时的错误可能表明该程序无法连接到指定的服务器。NETTIME 会提供另外 9 个服务器,可以选择其他的服务器进行尝试。

        如果一切顺利,NETTIME 将调用 recv(“receive”)函数来读取数据:

recv (sock, (char *) &ulTime, 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 年 1 月 1 日 0 时。然后,SystemTimeToFileTime 函数将该 SYSTEMTIME 结构转换为 FILETIME 结构。FILETIME 实际上是由两个 32 位 DWORD 构成的一个 64 位整数,用于表示从 1601 年 1 月 1 日 0 时其以 100 纳秒为单位的时间间隔段数

        函数 ChangeSystemTime  把该 FILETIME 结构转换成 LARGE_INTEGER。LARGE_INTEGER 是一个联合,它可允许其 64 位的数值被用作两个 32 位值或一个基于 __int64 数据类型的 64 位整数。(此数据类型是 Microsoft 编译器对 ANSI C 标准的一个扩充。)转换后的数值表示了从 1601 年 1 月 1 日到 1900 年 1 月 1 日之间的 100 纳秒时间间隔的数目。在这个数值上再加上从 1900 年 1 月 1 日 0 时到现在的 100 纳秒的时间间隔的数目(就是 ulTime 值的一千万倍)。

        由此产生的 FILETIME 值,再通过 FileTimeToSystemTime 函数转换回 SYSTEMTIME 结构。因为时间协议返回的是当前的 UTC 时间,所以 NETTIME 必须通过调用 SetSystemTime 函数来设定时间,因为 SetSystemTime 也是基于 UTC 的。至于实际显示的时间数值,是程序通过调用 GetLocalTime 函数获得的更新了的本地时间。原来的本地时间和新的本地时间都传递给函数 FormatUpdatedTime,该函数调用 GetTimeFormat 函数和 GetDateFormat 函数来讲时间转换为 ASCII 字符串。

        如果程序运行在 Windows NT 操作系统下,而用户没有设定时间的权限,则调用 SetSystemTime 函数可能会失败。如果 SetSystemTime 失败,NETTIME 将显示一条信息,表示新的时间未被设定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值