Winsock网络编程接口

来源:互联网 发布:如何评价列宁 知乎 编辑:程序博客网 时间:2024/04/29 02:11

       Winsock 是一套开放的、支持多种协议的 Windows 下网络编程接口,是 Windows 网络编程上的标准接口。应用程序通过调用 Winsock 的 API 实现相互之间的通信,而 Winsock利用下层的网络通信协议功能和操作系统调用实现实际的通信工作。
       套接字(Sockets)是通信端点的一种抽象,是支持 TCP/IP  协议网络通信的基本操作单元,它提供了一种发送和接收数据的机制。在开发服务器/客户端应用程序时,可以利用Sockets 实现数据结构或数据包的交换,以完成应用程序之间的通信。
       套接字一般有两种类型:流套接字和数据报套接字。
       流套接字提供双向的、有序的、无重复并且无记录边界的数据流服务,它适用于处理大量数据。流套接字是面向连接的,通信双方进行数据交换之前,必须建立一条路径,类似于打电话,首先要双方能连接,才能继续通话。这样既确定了它们之间存在的路由,又保证了双方都是活动的、可彼此响应的。在数据传输过程中,如果连接断开,则应用程序会被通知,此时应用程序可以根据中断原因作相应处理,在实际中,由于其可靠性高,流式 Sockets  得到了广泛应用。但在通信双方之间建立一个通信信道需要很多开支。除此以外,大部分面向连接的协议为保证发送无误,可能会需要执行额外的计算来验证正确性,因此会进一步增加开支。
       数据报套接字支持双向的数据流,但并不保证数据传输的可行性、有序性和无重复性。也就是说,一个从数据报套接字接收信息的进程有可能被发现信息重复,或者和发出时的顺序不同的情况。此外,数据报套接字的一个重要特点是它保留了记录边界。数据报套接字是无连接的,它不保证接收端是否正在侦听,类似于邮政服务:发信人把信装入邮箱即可,至于收信人是否能收到这封信或邮局是否会因为暴风雨未能按时将信件投递到收信人处等,发信人都不得而知。因此,数据报并不十分可靠,需要程序员负责管理数据报的排序和可靠性。应用程序具体可以采用的技术有:通过加流水号方式实现数据包的不丢失传输,通过对数据包校验实现正确传输,当出现传输错误时采用重发技术。当然读者可以采用自己的独特方法来保证数据的稳定可靠传输。数据包的一个优点是:它提供了向多个目标地址发送广播数据包的功能。

 Winsock 编程原理

1.  简单客户机/服务器

       进入 20 世纪 90 年代以后,随着计算机和网络技术的发展,很多数据处理系统都采用开放系统结构的客户机/服务器(Client/Server)网络模型,即客户机向服务器提出请求,服务器对请求做相应的处理并执行被请求的任务,然后将结果返回给客户机。

       客户机/服务器模型工作时要求有一套为客户机和服务器所共识的惯例来保证服务能够被提供(或被接受),这一套惯例包含一套协议,它必须在通信的两端都被实现。根据不
同的实际情况,协议可能是对称的或是非对称的。在对称的协议中,每一方都有可能扮演主从角色,如 Internet 协议中的 Telnet 协议;在非对称协议中,一方不可改变地被认为是主机,而另一方是从机,如 Internet 中的 Http 协议。无论具体的协议是对称的还是非对称的,当服务被提供时必须存在客户进程和服务进程。
       一个服务程序通常在一个众所周知的地址监听客户对服务的请求,也就是说,服务进程一直处于休眠状态,直到一个客户对这个服务提出了连接请求。在这个时刻,服务程序被“惊醒”并且为客户提供服务——对客户的请求作出适当的反应。

 2.Winsock 的启动和终止

       由于 Winsock  的服务是以动态链接库 Winsock  DLL  形式实现的,所以必须先调用WSAStartup 函数对 Winsock  DLL 进行初始化,协商 Winsock 的版本支持,并分配必要的资源。如果在调用 Winsock 函数之前,没有加载 Winsock 库,则会返回 SOCKET_ERROR错误,错误的信息是 WSANOTINITIALIZED。WSAStartup 函数原型为:
 
                          int  WSAStartup(WORD  wVersionRequested,LPWSADATA lpWSAData);
 
       其中,参数 wVersionRequested 指定应用要使用的 Windows Sockets 最高版本,其中高位字节表示辅版本号,低位字节表示主版本号。目前使用最广泛的是 Windows Sockets 1.1版本,最高版本已经是 2.0 版本。一般用宏 MAKEWORD(X,Y)获得 wVersionRequested 的正确值。如 MAKEWORD(2,0)表示使用 Windows Sockets2.0  版本。
      参数 lpWSAData 指向 WSADATA 结构的指针。该结构包含了加载的库版本的有关的信息。
      该函数成功则返回 0,失败则返回如下可能值。 
      (1) WSASYSNOTREADY:表示网络设备没有准备好。
      (2) WSAVERNOTSUPPORTED:Winsock 的版本信息号不支持。
      (3) WSAEINPROGRESS:一个阻塞式的 Winsock1.1 存在于进程中。
      (4) WSAEPROCLIM:已经达到 Winsock 使用量的上限。
      (5) WSAEFAULT:lpWSAData 不是一个有效的指针。
      此外,在应用程序关闭套接字后,还应调用 WSACleanup 函数终止对 Winsock DLL 的使用,并释放资源,以备下一次使用。WSACleanup 函数的原型为:
 
                         int  WSACleanup(void);

      该函数不带任何参数,若调用成功则返回 0,否则返回错误。

3.错误的检查和控制

       错误检查和控制对于编写成功的 Winsock 应用程序是至关重要的。事实上,对 Winsock API 函数来说,返回错误是很常见的,但是多数情况下,这些错误都是无关紧要的,通信仍可在套接字上进行。尽管返回的值并非一成不变,但不成功的 Winsock 调用返回的最常见的值是 SOCKET_ERROR。SOCKET_ERROR 是值为- 1 的常量。如果错误情况发生了,就可用 WSAGetLastError 函数来获得一段代码,这段代码明确地表明产生错误的原因。该函数的原型为:
 
                                        int  WSAGetLastError(void);
 
       WSAGetLastError 函数返回的错误都是预声明的常量值,根据 Winsock 版本的不同,这些值的声明不在 Winsock1.h 中就会在 Winsock2.h 中。为各种错误代码声明的常量一般都以 WSAE 开头。

4.   Winsock 编程模型

       不论是流套接字还是数据报套接字编程,一般都采用客户机/服务器方式,它们的过程基本类似,下面着重介绍流套接字的编程模型。
(1)  流套接字编程模型
       考虑使用电话进行通信的过程:如果想要使用电话进行通话,首先双方必须安装电话机,并由一方拨号与另一方建立连接,然后可以通过电话听取对方的声音,或者向对方讲话,最后关闭连接。流套接字的过程与打电话的过程非常相似,服务进程和客户进程在通信前必须创建各自的套接字并建立连接,然后才能对相应的套接字进行读、写操作,以实现数据的传输。具体编程步骤如下。
①  服务器进程创建套接字。服务进程总是先于客户进程启动,服务进程首先调用socket 函数创建一个流套接字,socket 函数的原型为:
 
                             SOCKET  socket(int  af,int type,int  protocol);
 
       其中参数 af 指定网络地址类型,一般都取 AF_INET,表示是在 Internet 上的 Socket。参数 type 用于指定套接字类型,当采用流连接方式时用 SOCK_STREAM,用数据报方式时用 SOCK_DGRAM。protocol 用于指定网络协议,一般都为 0,表示用对流套接字采用默认的 TCP 协议,数据报套接字采用默认的 UDP 协议。函数的返回值是 Winsock 定义的一种数据类型 SOCKET,它实际就是个整型数据,在 Socket 创建成功时,代表 Winsock 分配给程序的 Socket 编号,后面调用传输函数时,就可以把它像文件指针一样引用。如果 Socket建立失败,返回值为 INVALID_SOCKET。
②  将本地地址绑定到所创建的套接字上以使在网络上标识该套接字。在成功创建了Socket 之后,就应该选定通信的对象。首先是自己的程序要与网上的哪台计算机通话;其
次,在多任务系统下,该台计算机上可能会有几个程序在工作,必须指出要与哪个程序通信。前者可以通过 Internet 的网络 IP 地址来确定,而后者则由端口号来确定。用端口号来表示同一台计算机上不同的应用程序,端口号可以为 0~65535,不同功能的通信程序使用不同的端口号,如 pop3 协议使用 110 端口,http 协议使用 80 端口等,这样一台计算机上可以有几个程序同时使用一个 IP 地址通信而不互相干扰,IP 地址与端口号的关系好像电话总机号码与分机号码的关系一样。因为一些常用的网络服务往往占据了 1024 以下的端口号,所以编制自己的通信程序时,应指定大于 1024 的端口号。 
      一般该过程是通过函数 bind 来完成的,该函数的原型为:
 
              int  bind(SOCKET  s,struct  sockaddr_in *  name,int  namelen);
 
       其中参数 s 是已经创建好的套接字。name 是指向描述通信对象地址信息的结构体的指针,namelen 是该结构体的长度。结构体 sockaddr_in 的定义如下。
 
struct  sockaddr_in{
     short              sin_family;
     unsigned  short    sin_port;
     struct   in_addr   sin_addr;
     char               sin_zero[8];
     };
 
       其中,sin_family 是指一套地址族,通常被设为 AF_INET;sin_port 是指端口号;sin_addr是指 IP 地址;sin_zero[8]主要是使该结构的大小和 SOCKADDR 大小相同(SOCKADDR 结构由一个无符号 short 型和一个长度为 14 的 char 型数组构成,这个结构一共是 16 个字节),在 sockaddr_in 中添加这个长度为 8 的数组,使 sockaddr_in 的长度也为 16(2+2+4+8),这样做的目的是使地址操作更方便。该函数如果调用失败,会返回 SOCKET_ERROR。对 bind来说,最常见的错误是 WSAEADDRINUSE。如果使用的是 TCP/IP,那么该错误表示另一个进程已经同本地 IP  接口和端口号绑定到了一起,或者那个 IP  接口和端口号处于TIME_WAIT 状态。假如对一个已经绑定的套接字调用 bind,便会返回 WSAEFFAULT错误。
③  将套接字置入监听模式并准备接受连接请求。bind  函数的作用只是将一个套接字和一个指定的地址关联在一起,让一个套接字等候进入连接的 API 函数是 listen,其原型为:
 
                        int listen(SOCKET  s,int  backlog);
 
       其中参数 s 标识一个已绑定但未连接套接字的描述字。backlog 参数用于指定正在等待连接的最大队列长度,这个参数非常重要,因为完全可能同时出现几个服务器连接请求。例如,假定 backlog 参数为 2,如果 3 个客户机同时发出请求,那么头两个会被放在一个等待队列中,以便应用程序依次为它们提供服务,而第 3 个连接请求(队列已满)会造成一个WSAECONNREFUSED 错误,一旦服务器接受了一个连接,那个连接请求就会被从队列中删除,以便别人可继续发出请求,backlog 参数本身是由基层的协议提供者决定的,如果出现非法值,那么会用与之最接近的一个合法值来取代。 
       如果无错误发生,listen 函数返回 0,若失败则返回 SOCKET_ERROR 错误,最常见的错误是 WSAFINVAL,该错误通常表示套接字在 listen 前没有调用 bind。

       进入监听状态之后,通过调用 accept 函数使套接字作好接受客户连接的准备。Accept( )函数的原型为:
 
                      SOCKET   accept(SOCKET   s,struct  sockaddr *addr,int  *addrlen);


       其中参数 s 是处于监听模式的套接字描述字。第 2 个参数是一个有效的 SOCKADDR _IN 结构的地址,而 addrlen 是 SOCKADDR_IN 结构的长度。这样,服务器便可为等待连接队列中的第一个连接请求提供服务了。accept 函数返回,addr( )参数变量中会包含发出连接请求的那个客户机的 IP 地址信息,而 addrlen 参数则指出该结构的长度,并返回一个新的套接字描述字,它对应于已经接受的那个客户机连接。对于该客户机后续的所有操作,都应使用这个新套接字,至于原来的那个监听套接字,它仍然用于接受其他客户机连接,而且仍处于监听模式。如果无连接请求,服务进程将被阻塞。
④  客户进程调用 socket 函数创建客户端套接字。
⑤  客户向服务进程发出连接请求。通过调用 connect(  )函数可以建立一个到服务进程的连接。其中 s 是刚建立的套接字描述字,name 与 namelen 的含义和使用方法与 bind( )相同,用来指定通信对象。如果连接失败,该函数会返回 SOCKET_ERROR。如果欲连接的计算机没有侦听指定端口的这一进程,connect 调用就会失败,并发生错误WSAECONNREF USED。另一个常见的错误是 WSAETIMEOUT,表示连接超时。
⑥  当连接请求到来后,被阻塞服务进程的 accept( )函数如③中所述即生成一个新的套接字与客户套接字建立连接,并向客户返回接收信号。
⑦  一旦客户机的套接字接收到来自服务器的信号,则表示客户机与服务器已实现连接,即可以进行数据传输了。senD. recv 函数是进行数据收发的函数。它们的函数原型是:
 
                       int  send(SOCKET  s,char  *buf,int  len,int flags);
                       int  recv(SOCKET  s,char  *buf,int  len,int flags);
 
       s 是已建立连接的套接字的描述字。buf 和 len 是发送或接收的数据包及其长度,参数flags 一般取 0。recv(  )函数实际上是读取 send(  )函数发过来的一个数据包。当读到的数据字节少于规定接收的数目时,就把数据全部接收,并返回实际收到的字节数;当读到的数据多于规定值时,在流方式下剩余的数据由下个 recv(  )读出。这两个函数在出错时都返回SOCKET_ERROR。
⑧  关闭套接字。一旦任务完成,就必须关闭连接,以释放套接字占用的所有资源。通常调用 closesocket 函数即可达到目的,但 closesocket 可能会导致数据的丢失,因此应该在调用该函数之前,先调用 shutdown 函数从容地中断连接,即发送端通知接收端“不再发送数据”或接收端通知发送端“不再接收数据”。
       shutdown( )函数的原型为:
 
                        int  shutdown(SOCKET  s,int  how);
 
       其中,how  参数用于描述禁止哪些操作,它可取的值有:SD_RECEIVE、SD_SEND或 SD_BOTH。如果是 SD_RECEIVE,就表示不允许再调用接收函数,这对底部的协议层没有影响;如果选择 SD_SEND,表示不允许再调用发送函数;如果指定 SD_BOTH,则表示取消连接两端的收发操作。如果没有错误发生,则返回 0,否则返回 SOCKET_ERROR。

      shutdown( )函数并不关闭套接字,且套接字所占用的资源将被一起保持到closesocket( )函数调用。closesocket( )函数的原型为:
 
                     int  closesocket(SOCKET  s); 
 
      其中,参数 s  是要关闭的套接字描述字,再利用套接字执行调用就会失败,并出现WSAE_OTSOCK 错误。

  图 10.3 列出了流套接字编程的时序流程图。

                   服务器

              ▌ socket()   ▌

                      ↓ 

              ▌   bind()     ▌

                      ↓

              ▌ listen()     ▌

                      ↓                                                                                                                       客户端

              ▌  accept()  ▌                                                                                                  ▌    socket()    ▌

                      ↓                                                                                                                           ↓

            阻塞,等待客户数据                                                                                                   ↓

                      ↓                                                                                                                           ↓

                      ↓                                  ←←←←←←←建立连接←←←←←←←      ▌    connect()  ▌           

                      ↓                                                                                                                            ↓

              ▌    recv()      ▌                  ←←←←←←←请求数据←←←←←←←      ▌      send()     ▌

                      ↓                                                                                                                            ↓

              ▌    send()     ▌                 →→→→→→→应答数据→→→→→→→     ▌       recv()      ▌

                      ↓                                                                                                                            ↓

              ▋closesocket()▋                                                                                           ▋closesocket()  ▋

 (2)  数据报套接字编程模型
        数据报套接字是无连接的,它的编程过程比流套接字要简单一些。 对于服务器端,先用 socket(  )函数建立套接字,再通过 bind(  )函数进行绑定,但不需要调用 listen( )和 accept( )函数,只需等待接收数据。由于它是无连接的,因此它可以接收网络上任何一台机器所发的数据包。常用的接收数据函数是 recvfrom(  ),发送函数是sendto( ),它们的原型为: 
           int  recvfrom(SOCKET  s,char  *buf,int  len,int  flags,struct  sockaddr *from,int  *fromlen);
           int  sendto(SOCKET  s,char  *buf,int len,int flags,struct sockaddr_into,  int  *tolen);

        其中 recvfrom( )函数前 4 个参数和 recv( )函数一样,而参数 from 是一个 SOCKADDR结构指针,fromlen 参数是带有指向地址结构长度的指针。当它返回数据时,SOCKADDR结构内便填入发送数据端的地址。Sendto(  )函数的参数除了 buf 是指向发送数据缓冲,len是指发送数据长度,sockaddr_into 是指接收数据端的地址外,其他与 recvfrom 相似。

 用流套接字进行通信的简单例子

        本节是使用流套接字进行简单的网络通信编程的实例。它主要建立一个服务器程序和一个客户端程序,在建立连接后,由客户端向服务器发出消息“来自服务器”,服务器在收到消息后显示,并向客户端发送消息“来自服务器”,客户端在接收后显示。
1.服务器程序的实现
该程序使用阻塞模式套接字实现,其步骤为如下。
(1)  建立一个基于对话框的 MFC AppWizard 工程。
(2)  在文件 StdAfx.h 中的#endif  前面一行加入如下两行代码以包含 Winsock 相关头文件及连接相应的库文件。  

#include  <winsock.h> #pragma  comment(lib,"wsock32") 

 (3) 在对话框类的 OnInitDialog(  ) 函数中初始化 Winsock ,将下面代码加入到Cdialog::OnInitDialog( )下面。  

WSADATA  wsaData; WORD version=MAKEWORD(2,0);               // 设定 winsock 版本为 2.0 int ret=WSAStartup(version,&wsaData);    // 初始化 Socket if(ret!=0) TRACE("initialize error.!"); 

(4)  为 OK 按钮映射成员函数 OnOK( ),将本服务器程序的主要通信工作填加到该函数中,其代码如下。 
 

void CSocketAPIServerDlg::OnOK()  {   // TODO: Add extra validation here    SOCKET  m_hSocket;    m_hSocket=socket(AF_INET,SOCK_STREAM,0);  // 创建套接字         // 设置绑定地址    sockaddr_in m_addr;   m_addr.sin_family=AF_INET;   m_addr.sin_port=htons(5050);   m_addr.sin_addr.S_un.S_addr=INADDR_ANY; int error=0;   // 绑定套接字到本机    int ret;   ret=bind(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));   if(ret==SOCKET_ERROR)   {       TRACE("Bind Error:%d\n",(error=WSAGetLastError()));       return;   }   // 开始一个侦听过程,等待客户的连接    ret=listen(m_hSocket,2);  // 最多支持客户连接数为 2     if(ret==SOCKET_ERROR)   {       TRACE("Listen Error:%d\n",(error=WSAGetLastError()));       return;   }   // 该函数阻塞,等待客户的连接    SOCKET s=accept(m_hSocket,NULL,NULL);   if(ret==SOCKET_ERROR)   {       TRACE("Accept Error:%d\n",(error=WSAGetLastError()));       return;   }   // 一旦有用户连接,就等待用户发来的请求信息,该函数也阻塞    char buff[256];   ret=recv(s,buff,256,0);   if(ret==0||ret==SOCKET_ERROR)          {       TRACE("recv Error:%d\n",(error=WSAGetLastError()));       return;   }   buff[ret]='\0';     AfxMessageBox(buff);   // 向客户发送消息    ret=send(s," 来自服务器 ",10,0);   if(ret==10)       AfxMessageBox(" 服务器向客户机发送成功 ");   else     {       TRACE("Send Error:%d\n",(error=WSAGetLastError()));       return;   }   CDialog::OnOK();    // 此行代码为函数中原有代码  } 

2.  客户端程序的实现
        该程序与服务器程序一样,必须做前 3 步的准备工作,接下来为 OK 按钮映射成员函数 OnOK( ),为其编写代码如下。

 

void CSocketAPIClientDlg::OnOK() {   SOCKET m_hSocket;   m_hSocket=socket(AF_INET,SOCK_STREAM,0);   ASSERT(m_hSocket!=NULL);   sockaddr_in m_addr;   m_addr.sin_family=AF_INET;   // 改变端口号的数据格式,此端口号要与服务程序的端口口号一样    m_addr.sin_port=htons(5050);         m_addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); // 使用本机 IP 地址    int ret=0;   int error=0;   // 连接服务器    ret=connect(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));   if(ret==SOCKET_ERROR)   {   // 连接失败        TRACE("Connect Error:%d\n",(error=WSAGetLastError()));       if(error==10061)    // 该错误码表示服务器没有正常工作            AfxMessageBox(_T(" 请确认服务器确实已打开并工作在同样的端口 "));           return;   }    // 向服务器发送数据    ret=send(m_hSocket," 来自客户机 ",10,0);   if(ret==10)       AfxMessageBox(" 客户端向服务器发送信息成功 ");   else   {       TRACE("Send data error:%d\n",WSAGetLastError());       return;   }   char buff[256];   // 从服务器端接收数据    ret=recv(m_hSocket,buff,256,0);   if(ret==0)   {       TRACE("Recv data error:%d\n",WSAGetLastError());       return;   }   buff[ret]='\0';   AfxMessageBox(buff);   CDialog::OnOK(); } 

 

 

 

 

 

 

 

 

 

 

 

原创粉丝点击