Winsock是一种标准API(Application Programming Interface,应用程序编程接口),主要用于网络中的数据通信,它允许两个或者多个应用程序(或进程)在同一台机器上或通过网络相互通信。
注意:
(1) Winsock是一种网络编程接口,而不是协议。使用Winsock编程接口,应用程序可以通过不同网络协议(如TCP/IP)建立通信。
(2) Winsock接口从在UNIX平台上实现的BSD Socket中继承了大量的特性。
(3) 在Windows环境中,这种接口演变成一种真正独立于协议的接口。
(4) 目前Winsock有2个主要版本,Winsock 1和Winsock 2。通过前缀WSA可以区分这两个版本中的API函数。若Winsock 2在其规范中更新或增添一个新的API函数,该函数名将带有WSA前缀。比如,建立套接字的Winsock 1函数只是被简单称为socket,而Winsock 2引入该函数的新版本时,则将它命名为WSASocket,该函数可以使用Winsock 2中出现的一些新特性。但请注意:这个命名规则有几个例外,如WSAStartup、WSACleanup、WSARecvEx及WSAGetLastError都属于Winsock 1.1规范的函数。
(5) 在使用Winsock开发应用程序之前,必须要了解创建应用程序时,需要包含哪些头文件和库(.lib)。
1.1 Winsock头文件及库文件
Winsock有2个主要版本,Winsock 1和Winsock 2。两者都能在除Windows CE之外(Windows CE只支持Winsock 1)的所有Windows平台上运行。
注意:
(1) 如果将winsock2.h文件包含在应用程序中,该程序将使用Winsock2规范。使用winsock2.h的时候,程序还必须链接到WS2_32.LIB库。
(2) 为了和其他旧的Winsock应用程序兼容以及保证Windows CE平台上的程序开发,可以使用winsock.h。使用winsock.h的时候,程序还必须链接到WSOCK32.LIB。
(3) 另外,还有一个头文件mswsock.h,该头文件用于微软专用编程扩展,这些扩展通常用于高效Winsock应用程序的开发。使用mswsock.h的时候,程序还必须链接到MSWSOCK.DLL。
一旦包含了必需的头文件和链接环境,就可以开始编写应用程序代码了。
1.2 Winsock的初始化
每个Winsock应用程序都必须加载合适的Winsock DLL版本。如果调用一个Winsock函数之前没有加载Winsock库,这个函数就会返回一个SOCKET_ERROR,错误的信息是WSANOTINITIALISED。加载Winsock库是通过调用WSAStartup函数实现的。这个函数的定义如下:
int WSAStartup(
WORD wVersionRequired,
LPWSADATA lpWSAData
);
wVersionRequired参数用于指定准备加载的Winsock库的版本。高位字节指定所需Winsock库的次版本,而低位字节则是主版本。可以使用宏MAKEWORD(x,y)(其中,x是高位字节,代表的是Winsock的次版本;y是低位字节,代表的是Winsock的主版本)来方便地获得wVersionRequired的正确值。
lpWSAData参数是指向WSAData结构的指针,WSAStartup用与其加载的库版本有关的信息填充这个结构。
注意:
(1) 要注意版本之间的差别,Winsock 1.x不支持很多高级Winsock特性。
(2) 如果使用的Winsock版本比平台所能支持的版本新,WSAStartup就会失败。
(3) 如果WSAStartup正确返回,WSADATA结构中的wHighVersion就是当前系统中的Winsock库能过支持的最新版本。
在使用Winsock接口编写好应用程序之后,应该调用WSACleanup函数。这个函数能够使Winsock释放所有由Winsock分配的资源,并取消这个应用程序挂起的Winsock调用。WSACleanup函数的定义为:
int WSACleanup(void);
注意:
(1) 因为操作系统将会自动释放资源,所以在退出应用程序时也可以不调用WSACleanup函数。然而如果这样做,我们的应用程序就不再符合Winsock规范了。所以,为了符合规范,应当在每次调用WSAStartup后都应该调用WSACleanup。
1.3 错误检查和处理
要想成功编写Winsock应用程序,检查和处理错误是至关重要的。因为,对Winsock函数来说,返回错误是很常见的。但是,在有些情况下,这些错误是无关紧要的,通信仍可在那个套接字上进行。Winsock调用失败时,最常见的返回值是SOCKET_ERROR(SOCKET_ERROR常量实际上是-1)。
注意:
(1) 如果调用Winsock函数时出现了错误,可以用WSAGetLastError函数来获得一段代码,这段代码专用来说明错误。该函数的定义如下:
int WSAGetLastError(void);
发生错误之后,调用这个函数,返回的是所发生的错误的整数代码。
(2) WSAGetLastError函数返回的这些错误代码都有已经定义的常量值。因Winsock版本不同,这些值的声明可能在winsock.h中,也可能在winsock2.h中。两个头文件的惟一差别是winsock2.h中包含更多针对Winsock 2中引入的一些新的API函数和功能的错误代码。
(3) 为各种错误代码定义的常量(带有#define指令),一般都以WSAE开头。
(4) 相对于WSAGetLastError函数,另一个与此相关的函数是WSASetLastError函数。使用该函数可以手动设置WSAGetLastError获取的错误代码。
下面是一个Winsock应用程序框架:
#include <winsock2.h>
int main()
{
WSADATA wsaData;
// initialize
int ret;
if ((ret=WSAStartup(MAKEWORD(2,2),&wsaData))! = 0)
{
printf("WSAStartup failed with error %d/n",ret);
return -1;
}
// communication codes...
// clear up
if (WSACleanup() == SOCKET_ERROR)
{
printf("WSACleanup failed with error %d/n",WSAGetLastError());
}
return 0;
}
1.4 协议寻址
下面开始讲述如何使用网络协议建立通信。本部分主要介绍使用IPv4协议建立Winsock通信的基本知识。
(一) IPv4寻址
在IPv4中,计算机都分配有一个地址,该地址用一个32位的数值来表示。客户机需要通过TCP或UDP和服务器通信时,必须指定服务器的IP地址和服务端口号。另外,服务器打算监听传入的客户机请求时,也必须指定一个IP地址和一个端口号。在Winsock中,应用程序通过SOCKADDR_IN结构来指定IP地址和服务端口信息。该结构的格式如下:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
注意:
(1) sin_family字段必须设为AF_INET,目的是告知Winsock此时正在使用IP地址簇。
(2) sin_port字段标识服务器服务的TCP/UDP通信端口。因为有些可用端口号是为“已知的”服务保留的,所以应用程序在选择端口时,必须特别小心。
(3) sin_addr字段把IPv4地址作为一个4字节的量存储起来,它是无符号长整数的数据类型。根据这个字段的不同用法,它可以表示一个本地或远程的IP地址。
(4) IP地址一般是用“Internet标准点分表示法”,像a.b.c.d一样指定的。其中,每个字母代表一个字节的数字(用十进制、八进制或十六进制格式表示)。
(5) sin_zero字段,作用是充当填充项,以使sockaddr_in结构和sockaddr结构的长度一样。
(6) inet_addr函数的作用是,把一个点分IP地址(比如“127.0.0.1”)转换成一个32位的无符号长整数。它的定义如下:unsigned long inet_addr(const char FAR *cp); cp字段是一个空终止字符串,用于接受点分表示法的IP地址。注意,这个函数把IP地址当作一个按网络字节顺序排列的32位无符号长整数返回。
(二) 字节排序
不同的计算机处理器可能采用big-endian或little-endian不同的编码方法。比如,在Intel86处理器上,多字节编号用little-endian(主机字节序)形式表示。然而,在“Internet互联网标准”中,指定多字节编号必须使用big-endian(网络字节序)的形式表示。有一些列函数,用于多字节数的转换,把主机字节序转换成网络字节序,或者逆向。
// host to networking
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
// networking to host
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort);
下面是一段代码,利用inet_addr和htons函数来创建sockaddr_in结构,并进行IPv4寻址。
sockaddr_in InternetAddr;
int nPortId=5150;
InternetAddr.sin_family=AF_INET;
// convert "136.149.3.29" into 4 bytes integer, and assign it to sin_addr
InternetAddr.sin_addr.S_addr=inet_addr("136.149.3.29");
// convert the host sequence of nPortId into the networking sequence, and then assign it to sin_port
InternetAddr.sin_port=htons(nPortId);
由于IP地址不便于记忆,因此,大多数人更喜欢使用一个容易记忆且容易掌握的主机名。有一些有用的地址和名称解析函数,使用它们可以将主机名解析为IP地址、服务名称或端口号。
现在有了协议寻址的基础,就可以准备通过创建套接字来建立通信了。
1.5 创建套接字
套接字是传输提供程序的句柄。在Windows中,套接字和文件描述符不是一回事,因而是一个独立的类型,即winsock2.h中的socket类型。有两个函数可以用来创建套接字:socket和WSASocket。
SOCKET socket(int af, int type, int protocol);
注意:
(1) af是协议的地址簇。使用IPv4来描述Winsock,应将这个字段设为AF_INET。
(2) type是协议的套接字类型。如果使用TCP/IP创建套接字,应该将该字段设为SOCK_STREAM;而用UDP/IP时,则应设为SOCK_DGRAM。
(3) protocol用于在给定的地址簇和套接字类型具有多重入口时,对具体的传送作限定。对于TCP,应将该字段设为IPPROTO_TCP;而对于UDP,则设为IPPROTO_UDP。
(4) 为了控制各种套接字选项和套接字行为,Winsock提供了4个有用的函数:setsockopt、getsockopt、ioctlsocket及WSAIoctl。在简单的Winsock编程中,没有必要特别使用这些函数。
1.6 面向连接的通信
(一) 服务器API函数
这里所说的服务器其实是一个进程。它需要等待任意数量的客户机与之建立连接,以便为它们的请求提供服务。服务器必须在一个已知的名称上监听连接。在TCP/IP中,这个名称就是本地接口的IP地址,再加上一个端口编号。
服务器端,在Winsock中,第一步是用socket或WSASocket将给定协议的套接字绑定到它已知的名称(本地IP地址和端口号)上,这个过程通过bind调用来完成。第二步是将套接字设置为监听模式,这一步用listen来完成的。最后一步,若一台客户机试图建立连接,服务器必须通过accept或WSAAccept调用来接受连接。即:
(1) 创建socket
(2) 绑定IP和端口
(3) 监听客户端
(4) 接受客户端
(5) 使用send/recv或sendto/recvfrom相互通信
注意:
(1) 绑定。一个TCP连接上的一段代码:
SOCKET s;
SOCKADDR_IN tcpaddr;
int port=5150;
s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
tcpaddr.sin_family=AF_INET;
tcpaddr.sin_port=htons(port);
tcpaddr.sin_addr.S_addr=htonl(INADDR_ANY); // 套接字被绑定到本地默认的IP地址
bind(s,(SOCKADDR *)&tcpaddr,sizeof(tcpaddr));
(2) 监听。
int listen(SOCKET s, int backlog);
s是被绑定的套接字。backlog参数指定了被搁置的连接的最大队列长度。因为完全可能同时出现几个服务器的连接请求,所以这个参数非常重要。例如,backlog为2。如果有3个客户机同时发出请求,那么前面2个会被放在一个“挂起”队列中,以便应用程序依次为它们提供服务,而第3个连接请求会失败,返回一个WSAECONNREFUSED错误。
注意,一旦服务器接受了一个连接,那个连接请求就会从队列中删去,以便别人可以继续发出请求。
(3) 接受连接。
SOCKET accept(IN SOCKET s, OUT struct sockaddr FAR *addr, IN OUT int FAR *addrlen);
s是被绑定的套接字,它处于监听模式。addr是一个有效的SOCKADDR_IN结构的地址。addrlen是SOCKADDR_IN结构的长度。注意,对于属于另一种协议的套接字,应当用与那种协议对应的SOCKADDR结构来替换SOCKADDR_IN。
通过对accept函数的调用,可以为被搁置的连接队列中的第1个连接请求提供服务。accept函数返回后,addr结构中会包含发出连接请求的那个客户机的IPv4地址信息,而addrlen则指出addr结构的长度。
此外,accept函数会返回一个新的套接字描述符,它对应于已经被接受的那个客户机连接。该客户机后续的所有操作都应使用这个新的套接字。至于原来那个监听套接字,它仍然用于接收其他客户机连接,而且仍处于监听模式。
accept如果出错,将返回INVALID_SOCKET。
(4) 一个简单的TCP/IP服务器。程序中没有对调用实施任何错误检查,这样可以使代码显得清晰易读。
(二) 客户端API函数
客户机的创建要简单得多,建立成功连接所需的步骤也要少得多。创建客户机只需3步操作:
(1) 创建一个套接字。
(2) 建立一个SOCKADDR地址结构,结构名称为准备连接到的服务器名称。
(3) 用connect或WSAConnect初始化客户机与服务器的连接。
注意:
(1) int connect(IN SOCKET s, IN const struct sockaddr FAR *name, IN int namelen);
s是即将在其上面建立连接的那个有效TCP套接字。name是TCP的套接字地址结构(SOCKADDR_IN),表示要连接到的服务器。namelen是name参数的长度。
(5) 一个简单的TCP/IP客户机。程序中没有对调用实施任何错误检查,这样可以使代码显得清晰易读。
(三) 数据传输
收发数据是网络编程的主题。
发送数据:可以使用send或WSASend。
接收数据:可以使用recv或WSARecv。
注意:
(1) 必须牢记一点:所有关系到收发数据的缓冲区都属于简单的char类型,即面向字节的数据。事实上,它可能是一个包含任何原始数据的缓冲区,至于这个原始数据是二进制数据,还是字符型数据,则无关紧要。
关于上述函数的用法和详细内容可以查一下资料,这里就不总结了。
(四) 流协议
面向连接的协议,同时也是流传输协议。
在流协议中,发送者和接受者可以将数据分解成小块数据,或将数据合并成为大块数据。对于流套接字上收发数据所用的函数,需要了解的是:它们不能保证要求进行读取或写入的数据量。
要保证将所有的字节发送出去,可采用下面的代码:
在流套接字上接收数据时,这一原则照样适用,但意义不大。因为流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读取多少数据。
如果应用程序需要通过流协议获取离散消息,则需要做一些额外的工作。如果所有消息的长度都一样,则处理起来比较简单,比如说,需要读取的512个字节的消息:
如果消息长度不同,处理起来就会麻烦一点。因此,有必要利用自己的协议来通知接收端,让它知道即将到来的消息长度是多少。比方说,写入接收端的前4个字节总是整数,大小为即将到来的消息的字节数。这样,接收端每次开始读取时,会先查看前4个字节,把它们转换成一个整数,并查看构成消息的字节数是多少。
(五) 中断连接
一旦完成了套接字连接,就必须将它关掉,并释放关联到那个套接字句柄的所有资源。
注意:
(1) 要真正地释放与一个打开的套接字句柄关联的资源,执行closesocket(正式关闭)调用即可。但是要明白一点,closesocket可能会带来负面影响(和如何调用它有关),即可能会导致数据的丢失。鉴于此,应该在调用closesocket函数之前,利用shutdown(从容关闭)函数从容地终止连接。
(2) 对于无连接的协议而言,shutdown毫无意义。
1.7 无连接通信
和面向连接的协议比较起来,无连接协议的行为有很大不同,因此,收发数据的方法也会有所差别。在IP中,无连接通信是通过UDP/IP协议完成的。
(一) 接收端
对于在一个无连接套接字上接收数据的进程来说,操作过程并不复杂。概括为:
(1) 先用socket或WSASocket创建套接字。
(2) 再把这个套接字和准备接收数据的接口用bind函数绑定在一起。
(3) 使用recvfrom等待接收数据。(注意,和面向连接不同,不必调用listen和accept)
(二) 发送端
要在一个无连接的套接字上发送数据,有两种选择。最简单的一种,便是建立一个套接字,然后调用sendto或WSASendTo。
(三) 释放套接字资源
因为无连接协议没有连接,所以也不会有对连接的正式关闭和从容关闭。在接收端或发送端完成收发数据时,它只需要在套接字句柄上调用closesocket函数,便可释放为套接字分配的所有相关资源。
2010-5-25 wcdj