使用 NetLib 库进行网络套接字编程
Palm OS 操作系统提供了一个名为 NetLib 的库,可以方便地访问网络套接字。该库使得为 Palm OS 开发网络应用程序与为类 Unix 系统编写此类程序非常相似(同样简单)。
本文由 Dariusz Paduch 撰写,最初发表于 2005 年 6 月的《软件开发者杂志》。您可以在 SDJ 网站上找到更多文章。
图 1. Progress Manager 正在工作
引言
在接下来的文章中,我将演示如何为 Palm OS 编写一个应用程序,该应用程序利用系统库 NetLib 提供的网络套接字。使用此处描述的方法,可以创建一个功能上类似于 Linux 的 wget 的应用程序,使用 HTTP 协议从互联网下载选定的文件。我将假设读者熟悉 Palm OS 平台编程的基础知识。
Palm OS 下的套接字编程与类 Unix 系统下的编程几乎没有区别。因此,任何以前在 Unix 下进行过套接字编程的人,在 Palm OS 的情况下应用他们的解决方案应该不会遇到任何问题。
初始化 NetLib
为了利用系统库提供的优势,首先必须确定其引用号。使用该库的任何函数都需要此引用号。在函数之间切换时,应小心不要丢失引用号,因为否则程序将无法使用该库的任何函数。我们通过调用 SysLibFind()
来获取引用号,并将指向我们变量 (AppNetRefnum
) 的指针作为其参数。当然,要查找的库必须存在于系统中,并且必须已经加载;尽管对于 NetLib 来说这通常不是问题,但最好还是做好防护以应对 SysLibFind()
返回的错误。
获得库的引用号后,就可以使用 NetLibOpen()
函数打开并初始化它;当然,我们必须将引用号作为其参数之一传递。该函数将尝试启动所有已配置的网络接口,因此如果没有可用的接口,它将返回错误。如果 NetLib 已经打开,函数返回的错误将是 netErrAlreadyOpen
– 这个错误可以安全地忽略,因为它仅仅告知我们打开库不是必需的。这一点值得记住,以免由于此错误而中止整个操作。
在开始使用套接字之前,还应该做的一件事是测试网络连接。为此,应该使用 NetLibConnectionRefresh()
函数,该函数会检查并(如果我们将其第二个参数设置为 true
)打开所有连接。
完成所有必需的套接字操作后,应该使用 NetLibClose()
函数关闭 NetLib 以清理系统;这将防止程序在以后出现 netErrAlreadyOpen
之类的错误。
寻址
Palm OS 下套接字的地址存储在相应的结构中,就像在 Unix 下一样。由于在我们的例子中使用了 Internet 套接字的地址,我们可以使用 NetSocketAddrINType
结构,其中存储三个值。地址族决定了地址的语法。我们将连接到 Internet,因此将使用 IP 地址(我们将此值写为 netSocketAddrINET
)。剩余的是端口号和远程主机的 IP 地址。如果用户提供的是主机名而不是 IP 地址,则需要解析它;幸运的是,NetLibGetHostByName()
可以帮助我们。为了使用它,我们需要另一个结构来存储主机信息——NetHostInfoBufType
。然后 NetLibGetHostByName()
函数的参数将是包含主机名的字符串和一个指向输出结构的指针。函数执行后,NetHostInfoBufType
将包含一个名为 address
的数组,其中存储了有关该主机的所有 IP 地址。地址数组的大小取决于 netDNSMaxAddresses
常量,因此它可以包含多个 IP 地址。我们只对一个感兴趣,所以我们只引用 address[0]
下存储的值。这样获得主机的 IP 地址后,就可以将其写入前面提到的 NetSocketAddrINType
地址结构中。
然后我们将使用此结构来将我们的套接字连接到远程主机。
列表 1. NetLib 库的初始化
Err err, errcode; UInt8 allup; UInt16 AppNetRefnum = 0; // Look for a system library err = SysLibFind("Net.lib", &AppNetRefnum); if (err || !AppNetRefnum) return err; // Open NetLib NetLibOpen(AppNetRefnum, &errcode); if (errcode && errcode != netErrAlreadyOpen) return errcode; // Refresh network connections NetLibConnectionRefresh(AppNetRefnum, true, &allup, &errcode); if (errcode) { NetLibClose(AppNetRefnum, true); return errcode; } (...) NetLibClose(AppNetRefnum, true);
套接字 (Sockets)
现在是时候实际打开网络套接字了。为了做到这一点,我们需要一些信息。首先,我们需要域——我们传递给 NetSocketAddrINType
作为地址族的那个值(netSocketAddrINET
)。接下来,我们将定义套接字类型,即连接是使用数据报还是流;在我们的例子中,我们必须指定 netSocketTypeStream
。我们还必须根据套接字类型选择合适的协议;对于流套接字,正确的协议是 TCP (netSocketProtoIPTCP
)。我们将所有这些信息作为参数传递给 NetLibSocketOpen()
函数,该函数反过来返回一个套接字描述符,我们将将其存储在 NetSocketRef
类型的变量中。在对该套接字的所有操作中,我们将使用此已打开套接字的描述符。
我们已经有了一个打开的网络套接字和一个地址结构,因此是时候建立与远程主机的连接了(除非使用数据报套接字,在这种情况下,无需建立连接,在对套接字进行所有发送或接收操作时,只需提供相应主机的地址数据)。要将套接字连接到远程主机,我们使用 NetLibSocketConnect()
函数,其参数是套接字描述符和指向地址结构的指针。现在我们终于可以使用套接字描述符与远程主机进行通信了。
完成通信后,我们调用 NetLibSocketClose()
函数关闭网络套接字。
与远程主机通信
连接建立后,与服务器的实际对话只是发送查询和接收回复的问题。我们使用两个函数来发送和接收数据——NetLibSend()
和 NetLibReceive()
。它们都接受的参数是之前获得的套接字描述符、一个作为待发送数据缓冲区或用于接收数据的字符字符串,以及一个定义缓冲区长度的值。这两个函数都返回传输的字节数——这是一个非常有用的功能,用于验证通信的准确性。当函数返回零时,表示服务器已关闭套接字,意味着所有数据都已发送。
对于数据报套接字,我们还必须提供指向地址结构的指针及其大小。
在应用程序的这一点上,是时候开始考虑与服务器通信的方式,即协议了。我们必须知道要发送给服务器什么以及期望得到什么样的答复。以 wget 为例,使用的协议是 HTTP,因此我们也需要发送和接收 HTTP 头部。HTTP 服务器的响应由头部和数据组成。头部可以告诉我们传输的文件名和大小,为我们提供应该写入文件的字节数以及文件名等信息。头部还可以包含适当的错误消息,这在我们应用程序的错误处理函数中应该予以考虑。
列表 2. 寻址和与远程主机通信
char host[] = "www.palmtop.pl"; char buffer[40]; // A buffer for sent/received data int bytes = 1; NetSocketRef sock; NetSocketAddrINType addr; NetHostInfoBufType hostinfo; NetLibGetHostByName( AppNetRefnum, host, &hostinfo, -1, &errcode); if (errcode) return errcode; addr.family = netSocketAddrINET; addr.port = 80; addr.addr = hostinfo.address[0]; // Open a network socket sock = NetLibSocketOpen( AppNetRefnum, netSocketAddrINET, netSocketTypeStream, netSocketProtoIPTCP, -1, &errcode); // Connect the socket to the specifi ed host and port NetLibSocketConnect(AppNetRefnum, sock, &addr, sizeof(addr), -1, &errcode); if (errcode) { NetLibSocketClose (AppNetRefnum, sock, -1, &err); return errcode; } // Transmit the contents of the buffer through // the socket to the host NetLibSend(AppNetRefnum, sock, buffer, StrLen(buffer), 0, 0, 0, -1, &errcode); // Retrieve data from the socket to the buffer while(bytes > 0) bytes = NetLibReceive(AppNetRefnum, sock, buffer, 40, 0, 0, 0, -1, &errcode); // Close the network socket NetLibSocketClose(AppNetRefnum, sock, -1, &errcode)
与用户通信
在数据传输进行期间,让用户了解数据流的进度将很有帮助。为此,Palm OS 的创建者创建了一个名为 Progress Manager 的特殊界面。
使用 Progress Manager,我们可以显示和控制进度窗口的内容。窗口中显示的内容由程序员定义的函数 PrgCallbackFunc
控制。可以声明多种不同的进度状态,例如 Connecting
、Retrieving
或 Disconnecting
。在连接到远程主机之前,我们会打开进度窗口,调用 PrgStartDialog
函数,并为其提供窗口标题和回调函数的指针;所有后续对窗口内容的进一步操作都应使用 PrgUpdateDialog
函数进行。此函数可以切换不同的进度状态、向回调函数提供附加文本输出或显示错误消息。另一个值得一提的是 PrgUserCancel
宏,它会检查用户是否在操作进行过程中取消了该操作。
下一步?
我们的程序还有很多可以改进的地方,为程序员提供了极好的机会。首先,应该记住加强错误检查。每个函数都可以返回大量的错误,在显示消息时应该考虑到其中的一些。所有这些错误的详细描述可以在 Palm OS API 文档中找到。此外,大多数操作套接字的函数都具有超时属性,值得留意并根据我们的需求进行适当调整,以避免用户不得不永远等待服务器响应。