Network Programming For Microsoft Windows Notes:Introduction to Winsock

      Winsock是一种标准API(Application Programming Interface,应用程序编程接口),主要用于网络中的数据通信,它允许两个或者多个应用程序(或进程)在同一台机器上或通过网络相互通信。

注意

(1)   Winsock是一种网络编程接口,而不是协议。使用Winsock编程接口,应用程序可以通过不同网络协议(如TCP/IP)建立通信。

(2)   Winsock接口从在UNIX平台上实现的BSD Socket中继承了大量的特性。

(3)   Windows环境中,这种接口演变成一种真正独立于协议的接口。

(4)   目前Winsock2个主要版本,Winsock 1Winsock 2。通过前缀WSA可以区分这两个版本中的API函数。若Winsock 2在其规范中更新或增添一个新的API函数,该函数名将带有WSA前缀。比如,建立套接字的Winsock 1函数只是被简单称为socket,而Winsock 2引入该函数的新版本时,则将它命名为WSASocket,该函数可以使用Winsock 2中出现的一些新特性。但请注意:这个命名规则有几个例外,如WSAStartupWSACleanupWSARecvExWSAGetLastError都属于Winsock 1.1规范的函数。

(5)   在使用Winsock开发应用程序之前,必须要了解创建应用程序时,需要包含哪些头文件和库(.lib)

 

1.1    Winsock头文件及库文件

Winsock2个主要版本,Winsock 1Winsock 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_ERRORSOCKET_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位的数值来表示。客户机需要通过TCPUDP和服务器通信时,必须指定服务器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-endianlittle-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_addrhtons函数来创建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类型。有两个函数可以用来创建套接字:socketWSASocket

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个有用的函数:setsockoptgetsockoptioctlsocketWSAIoctl。在简单的Winsock编程中,没有必要特别使用这些函数。

 

1.6    面向连接的通信

(一)    服务器API函数

这里所说的服务器其实是一个进程。它需要等待任意数量的客户机与之建立连接,以便为它们的请求提供服务。服务器必须在一个已知的名称上监听连接。在TCP/IP中,这个名称就是本地接口IP地址,再加上一个端口编号

服务器端,在Winsock中,第一步是用socketWSASocket将给定协议的套接字绑定到它已知的名称(本地IP地址和端口号)上,这个过程通过bind调用来完成。第二步是将套接字设置为监听模式,这一步用listen来完成的。最后一步,若一台客户机试图建立连接,服务器必须通过acceptWSAAccept调用来接受连接。即:

(1)    创建socket

(2)    绑定IP和端口

(3)    监听客户端

(4)    接受客户端

(5)    使用send/recvsendto/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参数指定了被搁置的连接的最大队列长度。因为完全可能同时出现几个服务器的连接请求,所以这个参数非常重要。例如,backlog2。如果有3个客户机同时发出请求,那么前面2个会被放在一个“挂起”队列中,以便应用程序依次为它们提供服务,而第3个连接请求会失败,返回一个WSAECONNREFUSED错误。

注意,一旦服务器接受了一个连接,那个连接请求就会从队列中删去,以便别人可以继续发出请求

 

(3)    接受连接

SOCKET accept(IN SOCKET s, OUT struct sockaddr FAR *addr, IN OUT int FAR *addrlen);

s是被绑定的套接字,它处于监听模式。addr是一个有效的SOCKADDR_IN结构的地址。addrlenSOCKADDR_IN结构的长度。注意,对于属于另一种协议的套接字,应当用与那种协议对应的SOCKADDR结构来替换SOCKADDR_IN

通过对accept函数的调用,可以为被搁置的连接队列中的第1个连接请求提供服务。accept函数返回后,addr结构中会包含发出连接请求的那个客户机的IPv4地址信息,而addrlen则指出addr结构的长度。

此外,accept函数会返回一个新的套接字描述符,它对应于已经被接受的那个客户机连接。该客户机后续的所有操作都应使用这个新的套接字。至于原来那个监听套接字,它仍然用于接收其他客户机连接,而且仍处于监听模式。

accept如果出错,将返回INVALID_SOCKET

 

(4)    一个简单的TCP/IP服务器。程序中没有对调用实施任何错误检查,这样可以使代码显得清晰易读。

   

(二)    客户端API函数

客户机的创建要简单得多,建立成功连接所需的步骤也要少得多。创建客户机只需3步操作:

(1)   创建一个套接字。

(2)   建立一个SOCKADDR地址结构,结构名称为准备连接到的服务器名称。

(3)   connectWSAConnect初始化客户机与服务器的连接。

注意

(1)   int connect(IN SOCKET s, IN const struct sockaddr FAR *name, IN int namelen);

s是即将在其上面建立连接的那个有效TCP套接字。nameTCP的套接字地址结构(SOCKADDR_IN),表示要连接到的服务器。namelenname参数的长度。

 

(5)    一个简单的TCP/IP客户机。程序中没有对调用实施任何错误检查,这样可以使代码显得清晰易读。

  

(三)    数据传输

收发数据是网络编程的主题。

发送数据:可以使用sendWSASend

接收数据:可以使用recvWSARecv

注意

(1) 必须牢记一点:所有关系到收发数据的缓冲区都属于简单的char类型,即面向字节的数据。事实上,它可能是一个包含任何原始数据的缓冲区,至于这个原始数据是二进制数据,还是字符型数据,则无关紧要。

关于上述函数的用法和详细内容可以查一下资料,这里就不总结了。

 

(四)    流协议

面向连接的协议,同时也是流传输协议。

在流协议中,发送者和接受者可以将数据分解成小块数据,或将数据合并成为大块数据。对于流套接字上收发数据所用的函数,需要了解的是:它们不能保证要求进行读取或写入的数据量

要保证将所有的字节发送出去,可采用下面的代码:

在流套接字上接收数据时,这一原则照样适用,但意义不大。因为流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读取多少数据。

如果应用程序需要通过流协议获取离散消息,则需要做一些额外的工作。如果所有消息的长度都一样,则处理起来比较简单,比如说,需要读取的512个字节的消息:

 

      如果消息长度不同,处理起来就会麻烦一点。因此,有必要利用自己的协议来通知接收端,让它知道即将到来的消息长度是多少。比方说,写入接收端的前4个字节总是整数,大小为即将到来的消息的字节数。这样,接收端每次开始读取时,会先查看前4个字节,把它们转换成一个整数,并查看构成消息的字节数是多少。

 

(五)    中断连接

一旦完成了套接字连接,就必须将它关掉,并释放关联到那个套接字句柄的所有资源。

注意

(1)    要真正地释放与一个打开的套接字句柄关联的资源,执行closesocket(正式关闭)调用即可。但是要明白一点,closesocket可能会带来负面影响(和如何调用它有关),即可能会导致数据的丢失。鉴于此,应该在调用closesocket函数之前,利用shutdown(从容关闭)函数从容地终止连接。

(2)    对于无连接的协议而言,shutdown毫无意义。

 

1.7   无连接通信

和面向连接的协议比较起来,无连接协议的行为有很大不同,因此,收发数据的方法也会有所差别。在IP中,无连接通信是通过UDP/IP协议完成的。

(一)    接收端

对于在一个无连接套接字上接收数据的进程来说,操作过程并不复杂。概括为:

(1)    先用socketWSASocket创建套接字。

(2)    再把这个套接字和准备接收数据的接口用bind函数绑定在一起。

(3)    使用recvfrom等待接收数据。(注意,和面向连接不同,不必调用listenaccept

 

(二)    发送端

要在一个无连接的套接字上发送数据,有两种选择。最简单的一种,便是建立一个套接字,然后调用sendtoWSASendTo

 

(三)    释放套接字资源

因为无连接协议没有连接,所以也不会有对连接的正式关闭从容关闭。在接收端或发送端完成收发数据时,它只需要在套接字句柄上调用closesocket函数,便可释放为套接字分配的所有相关资源。

 

2010-5-25   wcdj

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值