第7章Winsock基础
本章专门讲解编写成功网络应用程序时所需的基本知识和A P I调用。
7.1 Winsock的初始化
每个Wi n s o c k应用都必须加载Winsock DLL的相应版本。如果调用Wi n s o c k之前,没有加载
Wi n s o c k库,这个函数就会返回一个S O C K E T _ E R R O R,错误信息是W S A N O T I N I T I A L I S E D。
加载Wi n s o c k库是通过调用W S A S t a r t u p函数实现的。这个函数的定义如下:
int WSAStartup( WORD wVersionRequested,
LPWSADATA lpWSAData
);
w Ve r s i o n R e q u e s t e d参数用于指定准备加载的Wi n s o c k库的版本。高位字节指定所需要的
Wi n s o c k库的副版本,而低位字节则是主版本。然后,可用宏M A K E W O R D ( X , Y )(其中,x是
高位字节, y是低位字节)方便地获得w Ve r s i o n R e q u e s t e d的正确值。
l p W S A D a t a参数是指向L P W S A D ATA结构的指针, W S A S t a r t u p用其加载的库版本有关的
信息填在这个结构中:
typedef struct WSAData
{
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char saSystemStatus[WSASYS_STATUS_LEN+1];
unsighd short iMaxSockets;
unsighd short iMaxUdpDg;
char FAR * lpVendorInfo;
}WSADATA,FAR * LPWSADATA;
W S A S t a r t u p把第一个字段w Ve r s i o n设成打算使用的Wi n s o c k版本。w H i g h Ve r s i o n参数容
纳的是现有的Wi n s o c k库的最高版本。记住,这两个字段中,高位字节代表的是Wi n s o c k副版
本,而低位字段代表的则是Wi n s o c k主版本。s z D e s c r i p t i o n和s z S y s t e m S t a t u s这两个字段由特定
的Wi n s o c k 实施方案设定,事实上没有用。不要使用下面这两个字段: i M a x S o c k e t s和
i M a x U d p D g,它们是假定的同时最多可打开多少套接字和数据报的最大长度。然而,要知道
数据报的最大长度应该通过W S A E n u m P r o t o c o l s来查询协议信息。同时最多打开套接字的数目
不是固定的,很大程度上和可用物理内存的多少有关。最后, l p Ve n d o r I n f o字段是为Wi n s o c k
实施方案有关的指定厂商信息预留的。任何一个Wi n 3 2平台上都没有使用这个字段。
7.2 错误检查和控制
对编写成功的Wi n s o c k应用程序而言,错误检查和控制是至关重要的,因此,我们打算先
为大家介绍错误检查和控制。事实上,对Wi n s o c k函数来说,返回错误是非常常见的。。但是,
多数情况下,这些错误都是无关紧要的,通信仍可在套接字上进行。尽管其返回的值并非一
成不变,但不成功的Wi n s o c k调用返回的最常见的值是S O C K E T _ E R R O R。
实际上, S O C K E T _ E R R O R常量是- 1。
如果调用一个Wi n s o c k函数,错误情况发生了,就可用W S A G e t L a s t E r r o r函数来获得一段代码,
这段代码明确地表明发生的状况。该函数的定义如下:
int WSAGetLastError(void);
发生错误之后调用这个函数,就会返回所发生的特定错误的完整代码。W S A G e t L a s t E r r o r
函数返回的这些错误都已预定义常量值,为各种错误代码定义的常量
(带有#定义指令)一般都以W S A E开头。
7.3 面向连接的协议
对服务器监听的连接来说,它必须在一个已知的名字上。在T C P / I P中,这个名字就是
本地接口的I P地址,加上一个端口编号。每种协议都有一套不同的定址方案,所以有一种不
同的命名方法。在Wi n s o c k中,第一步是将指定协议的套接字绑定到它已知的名字上。这个过
程是通过A P I调用b i n d来完成的。下一步是将套接字置为监听模式。这时,用A P I函数l i s t e n来
完成的。最后,若一个客户机试图建立连接,服务器必须通过a c c e p t或W S A A c c e p t调用来接
受连接。
1. bind
一旦为某种特定协议创建了套接字,就必须将套接字绑定到一个已知地址。b i n d函数可将
指定的套接字同一个已知地址绑定到一起。该函数声明如下;
int bind(
SOCKET s,
const sturct sockaddr FAR * name,
int namelen
);
其中,第一个参数s代表我们希望在上面等待客户机连接的那个套接字。第二个参数的类
型是struct sockaddr,它的作用很简单,就是一个普通的缓冲区。针对自己打算使用的那个协
议,必须把该参数实际地填充一个地址缓冲区,并在调用b i n d时将其造型为一个s t r u c t
s o c k a d d r。
举个例子来说,下列代码阐述了在一个T C P连接上,如何来做到这一点:
SOCKET s;
struct 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);
bind(s,(SOCKADDR *)&tcpaddr,sizeof(tcpaddr));
一旦出错, b i n d 就会返回S O C K E T _ E R R O R 。对b i n d 来说,最常见的错误是
W S A E A D D R I N U S E。如使用的是T C P / I P,那么W S A E A D D R I N U S E就表示另一个进程已经同
本地I P接口和端口号绑定到了一起,或者那个I P接口和端口号处于T I M E _ WA I T状态。假如你
针对一个套接字调用b i n d,但那个套接字已经绑定,便会返回W S A E F FA U LT错误。
2. listen
我们接下来要做的是将套接字置入监听模式。b i n d函数的作用只是将一个套接字和一个
指定的地址关联在一起。指示一个套接字等候进入连接的A P I函数则是l i s t e n,其定义如下:
int listen(
SOCKET s,
int backlog
);
第一个参数同样是限定套接字。b a c k l o g参数指定了正在等待连接的最大队列长度。这个
参数非常重要,因为完全可能同时出现几个服务器连接请求。例如,假定b a c k l o g参数为2。如
果三个客户机同时发出请求,那么头两个会被放在一个“待决”(等待处理)队列中,以便应
用程序依次为它们提供服务。而第三个连接会造成一个W S A E C O N N R E F U S E D错误。注意,
一旦服务器接受了一个连接,那个连接请求就会从队列中删去,以便别人可继续发出请求。
b a c k l o g参数其实本身就存在着限制,这个限制是由基层的协议提供者决定的。如果出现非法
值,那么会用与之最接近的一个合法值来取代。除此以外,对于如何知道实际的b a c k l o g值,
其实并不存在一种标准手段。
与l i s t e n对应的错误是非常直观的。到目前为止,最常见的错误是W S A E I N VA L。该错误
通常意味着,你忘记在l i s t e n之前调用b i n d。否则,与b i n d调用相反,使用l i s t e n时可能收到
W S A E A D D R I N U S E。这个错误通常是在进行b i n d调用时发生的。
3. accept和W S A A c c e p t
现在,我们已做好了接受客户连接的准备。这是通过a c c e p t或W S A A c c e p t函数来完成的。
a c c e p t格式如下:
SOCKET accept(
SOCKET s,
struct sockaddr FAR * addr,
int FAR * addrlen
);
其中,参数s是一个限定套接字,它处在监听模式。第二个参数应该是一个有效的
S O C K A D D R _ I N结构的地址,而a d d r l e n应该是S O C K A D D R _ I N结构的长度。对于属于另一种
协议的套接字,应当用与那种协议对应的S O C K A D D R结构来替换S O C K A D D R _ I N。通过对
a c c p e t函数的调用,可为待决连接队列中的第一个连接请求提供服务。a c c e p t函数返回后,
a d d r结构中会包含发出连接请求的那个客户机的I P地址信息,而a d d r l e n参数则指出结构的长
度。此外,a c c e p t会返回一个新的套接字描述符,它对应于已经接受的那个客户机连接。对于
该客户机后续的所有操作,都应使用这个新套接字。至于原来那个监听套接字,它仍然用于
接受其他客户机连接,而且仍处于监听模式。
Winsock 2引入了一个名为W S A A c c e p t的函数。它能根据一个条件函数的返回值,选择性
地接受一个连接。这个新函数的定义如下:
SOCKET WSAAccept(
SOCKET s,
struct sockaddr FAR * addr,
LPINT addlen,
LPCONDITIONPROC lpfnCondition,
DWORD dwCallbackData
);
其中,头三个参数与a c c e p t的Winsock 1版本是相同的。l p f n C o n d i t i o n参数是指向一个函数
的指针,那个函数是根据客户请求来调用的。该函数决定是否接受客户的连接请求,定义如
下:
int CALLBACK ConditionFunc(
LPWSABUF lpCallerId,
LPWSABUF lpCallerData,
LPQOS lpSQOS,
LPQOS lpGQOS,
LPWSABUF lpCalleeId,
LPWSABUF lpCalleeData,
GROUP FAR * g,
DWORD dwCallbackData
);
l p C a l l e r I d是一个值参数,其中包含连接实体的地址。W S A B U F结构是许多Winsock 2函数
常用的。对它的声明如下:
typedef struct __WSABUF{
u_long len;
char FAR * buf;
} WSABUF,FAR * LPWSABUF;
对l p C a l l e r I d来说,b u f指针指向的是一个地址结构。该结构针对的是建立连接的那种特定
通信协议。为正确返回信息,只须将b u f指针建立为恰当的S O C K A D D R类型。在T C P / I P的情
况下,在S O C K A D D R _ I N结构中,当然应该包含建立连接的那个客户机的I P地址。在连接请
求期间,大多数网络协议都能提供对呼叫者I D信息的支持。
l p C a l l e r D a t a参数中包含了随连接请求一道,由客户机发出的任何连接数据。若其中未指
定呼叫者数据,那么该参数就默认为N U L L。要注意的是,对大多数网络协议(如T C P)来说,
它们并不提供对连接数据的支持。至于一种协议到底是支持连接数据,还是支持断开数据,
可用W S A E n u m P r o t o c o l s函数对Wi n s o c k目录中相应的条目进行查询,从而得出正确的结论。
l p S Q O S和l p G Q O S参数对客户机请求的任何一个服务质量( Q O S)参数进行指定,两个
参数都引用了一个Q O S结构,该结构中包含的信息是关于收发数据所需要的带宽。如果客户
机没有要求Q O S,这些参数都将是N U L L。这两个参数的不同之处在于l p S Q O S指定的是一个
独立的连接,而l p G Q O S则用于套接字组。在Winsock 1或2中没有实施或支持套接字组
l p C l a l l e e I d属于另一种W S A B U F结构,这一结构中包含已与客户机需要与之连接的本地地
址。该结构的b u f字段同样指向其相应地址家族的一个S O C K A D D R对象。对正在一个多主机
的机器上运行的服务器来说,这种信息非常有用。记住,如果服务器和I N A D D R _ A N Y地址绑
定在一起,任何一个网络接口都可为连接请求提供服务。随后,该参数会返回实际建立连接
的那个接口。
l p C l a l l e e D a t a参数是l p C a l l e r D a t a的补充。l p C a l l e e D a t a参数指向一个W S A B U F结构,服务
器可利用这个结构把数据当作连接请求进程的一部分,返回客户机。如果服务提供者支持这
一选项, l e n字段就会指出作为这个连接请求一部分,服务器最多可向客户机返回多少字节。
这种情况下,服务器会根据这一数量,将尽可能多的字节复制到W S A B U F结构的b u f部分,同
时用l e n字段指出实际传输了多少个字节。如果服务器不想返回任何连接数据,那么,在返回
之前,条件接受函数应将l e n字段设为0。假如提供者不支持连接数据, l e n字段就会为0。大多
数协议同样都不支持在接受连接之前进行数据交换。事实上, Wi n 3 2平台当前支持的所有协议
都不支持这一特性。
服务器将传递给条件函数的参数处理完之后,必须指出到底是接受、拒绝还是延后客户机的连接请求。如果服务器打算接受连接,那么条件函数就应返回C F _ A C C E P T。如果拒绝,
函数就应返回C F _ R E J E C T。如果出于某种原因,现在还不能做出决定,就应返回C F _ D E F E R。
若服务器准备对这个连接请求进行处理,就应调用W S A A c c c e p t。要注意的是,条件函数在与
W S A A c c e p t函数相同的进程内运行,而且会立即返回。另外还要注意的是,对于当前的
Wi n 3 2平台支持的协议来说,条件接受函数并不意味着客户机的连接请求必须在从该函数返回
一个值之后才会得到满足。大多数情况下,最基层的网络堆栈在条件接受函数调用的那一刻,
就已经接受了连接。如果返回C F _ R E J E C T值,基层堆栈就会将连接简单地关闭了事。
如发生错误,就会返回I N VA L I D _ S O C K E T。最常见的错误是W S A E W O U L D B L O C K。如
果监听套接字处于异步状态或非暂停模式,同时没有要接受的连接时,就会产生此类的错误。
若条件函数返回C F _ D E F E R,W S A A c c e p t就会返回W S AT RY _ A G A I N错误。如果条件函数返
回C F _ R E J E C T,W S A A c c e p t错误就是W S A E C O N N R E F U S E D。