Windows编程 网络编程基础
Winsock介绍
Winsock 是一个基本的编程接口,它允许两个或更多程序(或者进程)进行本机通信和网络通信。理解Winsock是一个网络编程接口(而非协议)非常重要。Winsock接口从执行于UNIX平台上的BSD套接字那里继承了大量特性。在Windows环境中,Winsock2真正意义上实现了协议无关性。
Winsock 1和Winsock 2两个不同的版本的API函数。对比一下WSA前缀你就可以发现这一点:若Winsock 2升级或新增了API函数 该函数名也会相应的加上WSA前缀。 比如: Winsock 1用于创建一个新的套接字的函数是“socket”,而Winsock2则命名为“WSASocket”,同时也意味着您可以使用它的一些新特性。当然这种命名规律也不是绝对的,还有一些例外情况如: WSAStartup, WSACleanup, WSARecvEx, and WSAGetLastError 这些函数实际上在Winsock 1.1 标准中就定义了。 在你使用Winsock编程前,你还得知道哪些文件以及库是构造一个项目必须包含和引用的。
Winsock头文件和库
前面曾提到, Winsock目前有两个主要版本—— Winsock 1 以及 Winsock 2。除了Windows CE外,的所有Windows平台都同时兼容二者,而WindowsCE只兼容Winsock 1。在开发一个新的项目时,可以通过包含WINSOCK2.H来使用Winsock 2 API。为了能够和其它Winsock项目兼容,或者你要开发一个 WindowsCE项目,可以包含WINSOCK.H。还有另外一个头文件MSWSOCK.H ,它作为微软特有扩展,经常用于开发高性能Winsock项目。当你编译包含WINSOCK2.H的程序时,你首先要连接WS2_32.LIB库。若使用WINSOCK.H那就得连接WSOCK32.LIB库。若你要用MSWSOCK.H中的扩展API函数,那么你必须连接MSWSOCK.DLL。一旦你包含了必须的头文件并且连接了合适的库,你就可以开始为你的项目编写Winsock代码了。
Winsock初始化
每个Winsock程序都会加载相应的Winsock DLL。假如你没能正确加载Winsock 库,而试图调用Winsock函数,将返回一个错误;该错误可能是WSANOTINITIALISED(WSA没有初始化)。这时你就得通过调用WSAStartup函数来加载Winsock 库。 该函数定义如下: int WSAStartup( WORD wVersionRequested,LPWSADATAlpWSAData);
wVersionRequested参数用于说明你想加载的Winsock版本。高字节指定副版本,低字节指定主版本。我们一般用现成的宏MAKEWORD(x, y)来填充该参数,其中x代表高字节,y代表低字节。 LpWSAData参数是一个指向LPWSADATA 结构体的指针。我们用LPWSADATA结构体来描述Winsock版本相关信息。 LPWSADATA定义如下:
typedef struct WSAData {
WORD wVersion; WORD wHighVersion; char szDescription[WSADESCRIPTION_LEN + 1]; char szSystemStatus[WSASYS_STATUS_LEN + 1]; unsigned short iMaxSockets;unsigned short iMaxUdpDg; char FAR * lpVendorInfo; } WSADATA, * LPWSADATA;
wVersion,用于说明你将使用的Winsock版本。 wHighVersion ,用于说明现存最高Winsock库。切记在上述两个字段中,高字节描述Winsock副版本,低字节描述主版本。
SzDescription和szSystemStatus字段是具体执行的时候才填充的,对于我们来说并不用去设置。
更不要去设置后面的iMaxSockets和iMaxUdpDg字段。这两个字段是为最大并发套接字数以及最大数据报长度而预留的;那,如果你要想知道最大数据报长度怎么办呢?这时你应该通过调用WSAEnumProtocols来实现。最大并发套接字数也并非你想的那么神秘——它物理资源的多少有关。至于最后那个lpVendorInfo字段是为了向前兼容而保留的,一般我们都不予理睬。
除了每种Windows平台支持的Winsock的版本。 实际上,只需记住主版本的不同就行了。 不同平台支持的Winsock版本
Platform Winsock Version Windows 95 1.1 (2.2) Windows 98 2.2
Windows Me 2.2 Windows NT4.0 2.2 Windows 2000 2.2 Windows XP 2.2
Windows CE 1.1
正如你看到的,几乎所有的平台只支持2.2。但是,你也不是必须要使用最新的版本。比如,你要写一个适用于大多数Windows平台的程序,就得使用Winsock 1.1。这个程序就会很好的运行在NT 4.0平台上,因为所有的Winsock 1.1调用都将映射到Winsock 2 DLL 来执行。同样的如果将来推出的平台支持更高级的Winsock库,你目前的程序仍然可以很好的在新平台上跑起来——至少理论上如此。但在某些情况下,Winsock的堆栈操作并不完全依照规范所说明的那样执行。而结果是,许多程序员在写程序时,是依照目标平台的规范来进行开发的。
大多数情况下,写一个新的程序时,你最好加载目前最新的版本。我们假设若支持Winsock 3的新平台发布了,而你在NT4.0下进行开发。那么,你的Winsock2.2程序会在新平台上按预期那样运行。但如果你试图使用比你目前平台所支持更高的Winsock版本,WSAStartup 将会失败。回忆一下前面讲的, WSADATA 的wHighVersion参数就是用来说明你目前开发平台所支持的最高Winsock版本的。
当你的程序使用Winsock接口完成了通信,就应该调用WSACleanup函数来释放相关资源和取消任何被挂起的Winsock调用。WSACleanup定义如下:
int WSACleanup(void);
若你的程序因为其它原因而终止,而没来得及调用WSACleanup也不会有什么危害,因为操作系统会自动释放相关资源;当然你要做的是在每一个WSAStartup建立的通信使用完毕后调用WSACleanup来释放资源。
错误检测及处理
一开始就讲错误检测和处理,是因为它对于写出一个成功的Winsock项目来说非常重要。事实上Winsock函数报错是很常见的事情;当然,一些错误并不会被自动发现,而通信调用仍然占用socket。大多数非成功的Winsock调用会抛出“SOCKET_ERROR”错误,当然这也不是绝对的。当涉及到具体的API调用时,我们会指出返回错误代码。“SOCKET_ERROR”实际上是一个代表-1的常量。 使用函数WSAGetLastError可以得到由Winwock函数抛出的错误信息。WSAGetLastError函数定义如下: int WSAGetLastError (void);
在一个错误出现时调用该函数会返回一个int型的代码来标志一个特定的错误。这些错误代码是在WINSOCK.H或WINSOCK2.H中预先约定好的常量。这两个库中的错误代码唯一不同的是WINSOCK2为新的API增加了一些新错误代码,而已有的那些并没有变。此类常量定义了各种错误代码 (用#号指明)通常以WSAE开头。当然,你也可以自己在程序中定义符合自己习惯的错误代码常量。
下面这段程序描述了如何利用现有知识构造一个Winsock程序的骨架:
#include <winsock2.h> void main(void)
{
WSADATA wsaData; // 初始化 Winsock 2.2if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0) { // 注意:若Winsock加载失败我们就没法继续使用 printf("WSAStartup failed with error %d\n", Ret); //WSAGetLastError可以用于检测加载失败的具体原因 return; } //这里放置Winsock通信的具体代码 // 当你的程序调用完Winsock后,要调用WSACleanup()来释放资源 if (WSACleanup() == SOCKET_ERROR) { printf("WSACleanup failed with error %d\n",WSAGetLastError()); }}
好,接下来我们就要开始讨论如何使用网络协议建立通信。
协议地址
为简单起见,接下来只讨论如何使用基本的Winsock调用来实现通过IP协议进行基本的网络通信。之所以选择IP协议,是因为今天大多Winsock程序都使用该协议,IP协议是Internet上最流行的协议。我们之前提到过,Winsock是一种协议无关的接口,这里IP协议的讨论只限于IPV4。
将会给出一个通过IPV4建立Winsock链接并通信。IP协议受到绝大部分计算机操作系统的支持,不仅可以用做局域网通信,更能用于广域网的通信(如Inernet)。在设计之初,IP协议就是基于非链接的,并且不保证数据的正确传送。另外两个高层协议——传输控制协议(Transmission Control Protocol,TCP)以及用户数据报协议(User Datagram Protocol,UDP)——分别是用于基于链接和非链接的数据通信协议。这两个协议我们稍后再讲。TCP协议和UDP协议都是基于IP协议进行同信的,所以我们通常把他们合起来称呼为TCP/IP协议和UDP/IP 。要在Winsock中使用IPV4,你就必须理解IPV4的编址方案。 IPv4地址
在IPv4中,网络上的计算机地址被定义为32位长。当一个客户端想要与服务端通过TCP或UDP进行同信时,必须指定服务端的IP地址和服务端口号。同时,当服务端要监听来自客户的请求,它就必须预先制定指定端口号。SOCKADDR_IN 结构体定义如下:
struct sockaddr_in {
short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };
sin_family字段必须设置为AF_INET,以告诉Winsock我们使用的是IP协议簇。
sin_port字段定义了TCP或UDP通信用于唯一标志一个服务的端口号。程序可能会经常小心的选择一个端口,因为也许某一个端口正被未知程序所占用。
SOCKADDR_IN结构体中的sin_addr字段用于存放一个4字节的IPV4地址。根据使用情况的不同,有两种不同的地址格式,分别为本地地字节序格式和网络字节序格式。IP地址通常以“a.b.c.d”的形式表示,每一个字母都代表了一个数字(十进制、八进制或十六进制)用于表示一个字节的数据。按照从左到右的顺序四个字节表示了一个完整长整型的地址(32位)
sin_zero字段只用于把SOCKADDR_IN结构体填充成和SOCKADDR结构体相同的大小。
有一个很有用的函数名叫inet_addr能够将点分的IP地址字符串转化为32位无符号长整形数。该函数定义如下:
unsigned long inet_addr(const char FAR *cp );
Cp字段是一个以空结束的字符串,用于接受点分形式的IP地址。要注意,这个函数返回的IP地址是一个网络字节序的32位无符号长整形数据。
字节序
不同的计算机系统(处理器)处理字节的顺序是不同的,分为big-endian字节序和little-endian字节序两种形式。这两者的区别在于little-endian是高地址字节表示高位,低地址字节表示低位,读的时候从高地址字节读向低地址字节;而big-endian正好相反,即低地址字节表示高位,高地址字节表示低位,读的时候从低地址字节读向高地址字节。在Winsock编程中,我们把litle-endian字节序称为“本地字节序”,把big-endian字节序又称为“网络字节序”。
下面四个函数可以进行网络字节序到本地字节序的转化:
u_long htonl(u_long hostlong); int WSAHtonl( SOCKET s,u_long hostlong,u_long FAR * lpnetlong); u_short htons(u_short hostshort); int WSAHtons(SOCKET s,u_short hostshort,u_short FAR * lpnetshort);
htonl函数和WSAHtonl函数中的参数hostlong是一个本地字节序的四字节数字。htonl函数返回相应的网络字节序,而WSAHtonl函数返回有多少个本地字节转化为网络字节,并且填充指向转化后字节序的指针。而下面两个函数操作的就不是四字节,而是两字节了,功能和上述两个函数类似。
相应的,下面四个函数何以进行网络字节序到网络本地序的转化:
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);
不用多说了吧,只不过是反过来了而已,参数意义都很类似的。
我们紧接着就要给出一个给IPV4编址,并且使用iet_addr和htons函数将其填入SOCKADDR_IN结构的例子:
SOCKADDR_IN InternetAddr; INT nPortId = 5150; InternetAddr.sin_family = AF_INET; // 转化IP 127.0.0.1为长整型数 InternetAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 变量nPortId以本地字节序存储 // 把nPortId转化为网络字节序 InternetAddr.sin_port = htons(nPortId);
这时你可能会说:这样以来IP地址就不好记啦!毕竟我们经常使用点分法表示IP地址,这下换成长整型表示很不习惯。那有没有一个函数可以帮我们进行“翻译”呢?当然!不过先不告诉你,等到以后再讨论这类问题(这类问题还包括如何将一个主机名转化为IP地址和端口号)。
好啦,现在你已经对编址(如IPV4)有了一个基本印象,接下来就可以了解一下怎样创建一个sorekt。那就先到这儿吧!
参考书籍《Windows网络编程》第二版
posted on 2013-11-14 18:43 ′ Visitors 阅读(676) 评论(0) 编辑 收藏 举报