前言
本章专门讲解编写成功的 Winsock 用程序的基木方法。
Winsock 是一种标准 API(Application Programming Interface,应用程序编程接口
),要用于网络中的数据通信,它允许两个或者多个应开程序(或进程)在同一台机器上或通过网络相互通信。
有点我们必须明白:
- Winsock 是一种
网络编程接口
,而不是协议
。
使用 Winsock 编程接口,应用程序可通过通网络协议如 TCP/IP(TransmissionControl Protocol/Internet Protocol,传输控制协议/网际协议
) 或 IPX(Intermet Packet Exchange, Internet 数据包交换
)协议建立通信。
Winsock 接口从在UNX 平台上实现的 BSD Socket(套接字中继承了大量的特性。在 Windows 坏境中,这种接口演变成一种真正独立于协议的接口,新发布的 Winsock2版本更是如此。
本章将讨论 从网络上的一台机器到另一台机器建立通信的基本知识,以及如何收发数据。
为了便于大家理解接受连接、建立连接和收发数据所需的 Winsock 调用,本章给出了多个示例。
由于本章的目的是学习这些基本的 Winsock 调用,因而所举的示例均采用了直接阻塞的 Winsock 调用。
第5章将讲述 Winsock 支持的非阻塞调用及其他各种 I/O 方法,其中包含示例代码。
除此以外,本章还将介绍各种 API函数的:
- Winsock 1版本
- Winsock 2版本。
通过前 WSA
可以区分该函数的两种版本,若 Winsock 2 在其规范中更新或增添了一个新的 API 函数,该函数名将带有WSA前缀
。比如,建立套接字的 Winsock1函数只是被简单称为socket
。而Winsock2引入该函数的新版本时,则将它命名为 WSASocket
,该函数可以使用 Winsock2中出现的一些新特性。
但请注意这个命名规则有几个例外,如WSAStartup
、WSACleanup
、WSARecvEx
及WSAGetLastError
都属于Winsock 1.1规范的函数
在使用 Winsock 开发应用程序前,必须了解创建应用程序时学要哪些文件和库。
1.1 WinSock头文件及库文件
如前所述,Winsock 有两个主要版本,
- Winsock1
- Winsock2
两者都能在除 Windows CE之外(Windows CE 只支持 Winsock 1)的所有 Windows 平台上运行。
开发新的应用程序时,把WINSOCK2.H 文件包含在应用程序中,该程序将使用 Winsock2 规范。为了和其他旧的 Winsock 应用程序兼容以及保证 Windows CE 平台上的程序开发,可以使用 WINSOCKH。
另外,还有个头文件MSWSOCKH,该头文件用于微软专用编程扩展,这些扩展通常用于高效 Winsock 应用程序的开发第6章中将对此加以描述在编译采用广WINSOCK2.H 的应用程序时,须链接 WS2_32.LIB 库。
使用 WINSOCKH比如在Windows CE 中)时须使用 WSOCK32LIB。如果从 MSWSOCKH 中使用扩展 API,还必须链接MSWSOCKDLL。一旦包含了必需的头文件和链接环境,就以开始编写应用程序代码了,这时需要初始化WinSock。
1.2 WinSock的初始化
每个Wimsock 应用都必须加载合适的 Winsock DLL版本。如调用·个Winsock 所数之前没有加载 Winsock 库,这个函数就会返回一个 SOCKET ERROR,错误信息是 WSANOTINITIALISED加载 Winsock 库是通过调用 WSAStartup 函数实现的。这个函数的定义如下:
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpwsAData
);
wVersionRequested
参数用于指定准备加载的 Winsock 库的版本。高位字节指定所需 Winsock 库的次版本,而低位字节则是主版本。可以使用宏 MAKEWORD(x,yX其中,x是高位字节,y 是低位字节)来方便地获得 wVersionRequested 的正确值。
IpWSAData
参数是指向 LPWSADATA 结构的指针WSAStartup 用与其加载的库版本有关的信息填充这个结构:
typedef struct WSAData { WORD wVersion WORD wHighVersion char szDescription[WSADESCRIPTION LEN +1]: char szSystemstatus[WSASYS STATUS LEN +1]; unsigned short iMaxSockets; uns igned short iMaxUdpDq; char FAR* lpVendorInfo; WSADATA,ALPWSADATA; }
WSAStartup
将第一个字段 wVersion
设置为将要使用的 Winsock 版本。
wHighVersion
参数包含了现有 Winsock 库的最高版本。
记住,这两个字段中,高位字节代表的是 Winsock 次版本,而低位字节代衣的则是 Winsock 主版本,szDescription
和szSystemStatus
这两个宁段由特定的Winsock
来实现和设置,它们并没有实际作用。
不要使用下面这两个字段:
- iMaxSockets
- iMaxUdpDg
它们分别表示可以时打开的取大套接字数量,以及数报的最大长度。
通过 WSAEnumProtocols(请参阅第 2章查询协议信息,才能知道数据报的最人长度:可以同时打开的最大套接字数量个是固定的,它的值在很人程度上取决于可用的物理资源。最后的 pVendorlnfo是 个保留子段,用下存储实现 Winsock的特定供应商的信息,所有的 Windows 平台都没有使用这个学段。
1.1 列出了微软各种 Windows 平台文持的 Winsock 版本。必须注意这两个版本之间的差别Winsock l.x 不支持描述的很多高级Winsock 特性
注意,即使某个平台支持 Winsock 2,也可以不使用这个 Winsock 的最新版本。也就是说,如果打算编写大多数平台均能支持的应用程序,应根据 Winsock 1.1 规范来编写。为所有的 Winsock 1.1调用都通过 Winsock 2 DLL 映射,所以所编程序可以在Windows NT 4.0 平台上确无误地运行同时,如果市面上出现了您所用平台可以使用的 Winsock 库更新版本,那么您很可能要进行版本升级。内为这些新版本中仙含了对错误的修止,所以从理论上来说,旧代码完全可以正常运行。
在有些情况下,Wimsock 堆栈的行为和规范中的定义有所不同,这样,许多程序员是根据特定!标平台的行为来编程,前不是根据规范来编写程序。
但是,在很多情况下,编写新应用程序时,程序员都想加载已发布的 Winsock 库的最新版本。当然,如果 Winsock 3 发布了,加载 2.2 版本的应用程序仍可一如既社地运行。如果用户要求的 Winsock版本比平台所能支持的版本新,WSAStartup 就会失败。
如果WSAStartup 正确返回,WSADATA
结构中的wHighVersion
就是当前系统中的 Windsock 库能够支持的最新版本。
在使用Winsock 接口编写好应用程序之后,应该调用WSACleanup
函数这个函数能够使 Winsock释放所有由 Winsock分配的资源,并取消这个应用程序挂起的 Winsock 调用。
WSACleanup
数的定义为:
int WSACleanup(void);
因为操作系统将会自动释放资源,所以退出应用程序时也可以不调用 WSACleanup 函数:
然而如果这样做,您的应用程序就不再符合 Winsock 规范了。另外,每次调用 WSAStartup 后都应该调用WSACleanup。
1.3 错误检查和处理
要想成功编写 Winsock 应用程序,检查和处理错误是全关重要的,所以这里首先对此进行介绍事实上,对 Winsock 函数来说,返回错误是很常见的,但是,在有些情况下,这些错误是无关紧要的通信仍可在那个套接子上进行。Winsock 调用失败时最常见的返回值是 SOCKET ERROR。本书在详细介绍各个API调用时,都将省出和各种错误对应的返回值。
SOCKET ERROR 常量实际上是-1。
如果调用 Winsock 函数时出现了错误,可以用 WSAGetLastError 函数来获得一段代码,这段代码专用来说明错误。该函数的定义如下:
int WSAGetLastError(void);
发生错误之后调用这个函数,返回的是所发生的错误的整数代码。
WSAGetLastError
函数返回的这些错误代码都有已经预先定义的常量值;
因 Winsock 版本的不同,这些值的声明可能在WINSOCK1.H中,也可能在 WINSOCK2 中。
两个头文件的惟一差别是:
- WINSOCK2H 中包含更多针对 Winsock2中引入的一些新API函数和功能的错误代码。
- 为各种错误代码定义的常量(带有#dfinc指令)一般都以 WSAE 开头。
相时于 WSAGetLastError
函数,另一个与此关的函数是WSASetLastError
,使用该函数可以手动设置WSAGetLastError
获取的错误代码。
下述程序演示了如何基于上述内容构建一个Winsock 应用程序框架:
#includecwinsock2.h> void main(void) { WSADATA wsaDatd; //初始化winsock版本2.2 if ((Ret = WSAStartup(MAKEWORD(2,2),EwsaData))!= 0) //注意:因为winsock没有加载,所以我们不能使用WSAGetLastError来确定导致故障的特定错//误。但我们将根据WSAStartup的返回状态进行判断 prinTf("WSAStartup failed with error ed n",Ret);return; //当应用程序结束调用WSACleanup之片,设置Winsock通信代码 if (wSACleanup() == SOCKET ERROR) printf("wSACleanup falled with error d n",WSAGetLastError()); }
下面开始叙述如何使用网络协议建立通信。
1.4 协议寻址
随着Internet 的不断普及,IP 协议随处可得,当今大多数 Winsock 应用程序开发都使用IP,因此为了简便和避免重复,本章其余部分将只描述怎样使用IP(Internet Protocol,网际协议)协议创建基本的Winsock 调用来建立通信。前面已提到,Winsock 是一种独立于协议的接口,第4章将讲述使用其他协议(如 IPX)的具体内容。另外,本章对 IP 的讨论仅限于简单描述 IPV4(IP 第4版)第3章将全面而详细地讲述所有IP版本,包括IPv4 和IPV6(IP 第6版)
本章的其余部分将介绍使用 IPy4 协议建立 Winsock 通信的基本知识。IP 在大多数计算机操作系统中都能得到支持,并可在多数局域网(LAN)上使用,如办公室中的小型网络,也可在广域网(WAN)中使用,例如 Intemmet。从设计角度看,IP 是种无连接协议,它不能确保数据传输的成功。两个高级协议-TCP(Transmission Control Protocol,输控制协议)和 UDP(User Datagram Protocol,用户数据报协议)用通过 IP 进行面向连接和无连接的数据通信,这方面的内容将在以后讲述。TCP 和UDP 都使用IP 进行数据传输,通常被称作 TCP/IP 利UDP/IP。如果需要在 Winsock 中使用IPy4,需要知道怎样为IPv4 寻址。
在IPv4 中,计算机都分配有一个地址,该地址用一个 32 位的数值来表示。客户机需要通过 TCP或UDP 和服务器通信时,必须指定服务器的 IP 地址和服务端号。另外,服务器打算监听代入的客户机请求时,也必须指定一个IP地址和一个端口号。
在Winsock 中,应用程序通过SOCKADDR IN结构来指定 IP 地址和服务端口信息,该结构的格式如下:
struct sockaddr_in{ short sin family; u_short sin port; sin addr struct in addr; charsin_zero[8]; }
sin family
字段必须设为AF_INET
,以告知 Winsock 此时正在使用IP 地址族。
用标识服务器服务的 TCP 或 UDP 通信端口 sin port 字定义。因为有些可用端口号是为“过知的"服务保留的,如 FTP(文件传输协议,File Transfer Protocol) 和 HTTP(Hypertext Transfer Protocol.超文本传输协议),所以应用程序在选择端口时,必须特别小心。第 2 章中有更多关于选择端的详细内容。
SOCKADDRIN结构的sin addr 宁段把IP4 地址作为一个4字节的量存储起来,它是无符号长整数的数据类型。
根据这个字段的不同用法,它还可表示一个本地或远程 IP 地址。IP 地址一般是用“Internet 标准点分表示法”像 a.b.c.d 一样指定的,其中每个字母代表一个宁节的数字(用十进制、八进制或十六进制格式表示),从无到右分配了一个无符号长整数的 4个字节。后-·个字段 si zero只充当填充项,以使SOCKADDR IN 结构和 SOCKADDR 结构的长度一样。
inet_addr
是一个很实用的支持函数,它可把一个点分IP地址转换成一个32 位的无符号长整数
它的定义如下:
unsigned long inet_addr (const char FAR *cp);
cp 字段是一个空终止字符串,用于接受点分表示法的 P 地址。注意,这个函数把IP 地址当作-个按网络字节顺序排列的 32 位无符号长整数返回(网络字节顺序在下面的“字节排序”小节中有简费说明)。
1.4.1 字节排序
不同的计算机处理器采用 big-endian 和 little-endian 形式进行编号,具体采用哪种表示方法,由各自的设计决定。比如,Intel86 处理器上,多字节编号用 little-endian 形式来表示:
- 字节的排序是从最无意义的字节到最有意义的字节。
在计算机中把 IP 地址和端口号指定成多字节数时,这个数就按 主机字节(host-byte)顺序
来表示。
但是,如果在网络上指定P 地址和端口号,“Intemet 联网标准”指定多字节值必须用 big-endian 形式来表示(从最有意义的宁节到最无意义的字节),般称之为 网络字节(network-byte)顺序
有一系列函数可用于多字节数的转换,把后者从主机字节顺序转换成网络字节顺序,或进行反方向的转换。
下面 4 个API函数使将一个数从机宁节顺序转换成网络字节顺序:
u_long htonl(u long hostlorg); int WSAHtonl( SOCKET s, u_long hostlong, u_long FAR * lpnetlong ); u_short htons( short hostshort); int WSAHtons( SOCKET s, u_short hostshort, u_short FAR * lpnetshort )
htonl
和 WSAHtonl
的 hostlong
参数是按主机子节顺序排序的一个 4字节数。
htonl函数
返回的数是网络字节顺序,而 WSAHtonl 函数通过 pnetlong 参数返的数也按网络节顺序排列。
htons 和WSAHtons 的 hostshort 参数是按机字节顺序排列的一个2节数。
htons 函数把这个数当作按网络字节顺序排列的一个2字节数值返回,而WSAHtons 函数则通过lpnetshort 参数返这个数。
下面这 4 个函数是前面4 个函数的逆向数,它们把网络字节顺序转换成主机字节顺序。
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 )
现在,演示··下如何利用上面描述的 inet_addr 和 htons 函数来创建 SOCKADDR IN 结构,并进行IPv4 寻址。
SOCKADDR_IN InternetAddr; INT nPortId = 5150; InternetAddr.sin family = AF_INET; //将准备使用的点分Internet 地址 136,149.329 转换为4字节整数,并把它分配给 sin addx InternetAddr.sin addr.s addr = inet_addr("136,149,3,29"); //nPoxtId 变量按存储主机宁节顺序排列。将 nPortId 转换为网络字节顺序,并分配给 sin port InternetAddr.sin port = htons(nPortid);
IP 地址不便于记忆,因此,大多数人更喜欢使用一个容易记忆且容易掌握的主机名。
第3 章将叙述一些有用的地址利名称解析两数,使用它们可以将主机名(如 www.somewebsite.com)解析为IP地址服务名称(如 FTP)或端口号。
这些函数有 getaddrinfo、getameinfo、gethostbyaddr、gethostbyname、gethostname、getprotobyname、getprotobynumber、getservbyname 以及 getservbyport 等。
同时还有这些函数的异步版本,如: WSAAsyncGetHostByAddr 、 WSAAsyncGetHostByNameWSAAsyncGetProtoByNameWSAAsyncGetProtoByNumber、WSAAsyncGetServByNameWSAAsyncGetServByPort 等。
现在有了协议寻址的基础,诸如 IPv4,就可以准备通过创建套接字来建立通信了。
1.5 创建套接字
熟悉 Winsock 的人应该知道,API 是建立在套接字概念基础上的。套接字是传输提供程序的句柄,在 Windows 中,套接字和文件描述符不是一回事,因而是一个独立的类型,即 WINSOCK2.H 中的SOCKET类型。
有两个函数可以用来创建套接字:
- socket
- WSASocket。
随后的3 章将详细讲述如何创建每种可用协议的套接字。
为简便起见,对套接字将仅作简要叙述:
SOCKET socket ( int af, int type, int protocol )
第1个参数 af 是协议的地址族。由于本仅使用 IP4 来描述 Winsock,因此应将这个字段设为AF_INET
。
第 2个参数 type 是协议的套接字类型。如果使用 TCP/P 创建接字,应将该字段设为SOCK_STREAM,而用 UDP/IP 时则应设为 SOCK DGRAM。
第3 个参数是 protocol,用于在给定地址族和套接字类型具有多重入口时,对具体的传送作限定。对于 TCP,应将该字段设为 IPPROTO_TCP
而对于UDP则设为IPPROTO_UDP
第2章将详细讲述如何创建所有协议的接字,包括 WSASocketAPI在内。
为控制各种套接字选项和套接字行为,Winsock 提供了4个有用的函数:
- setsockopt
- getsockopt
- ioctisocket
- WSAloctl
在简单的 Wisock 编程中,没有必要特别使用这些函数。第7章节,将描述这些函数及其所有可用选项。
在成功地创建了套接字之后,就可以开始在套接字上建立通信,并为收发数据做好准备。
在 Winsock 中有两种基本的通信技术:
- 面向连接的通信
- 无连接的通信