5.4 TCP套接字编程的相关函数

TCP套接字编程的相关函数由windows socket库(简称winsock库)提供。该库分1.0和2.0两个版本,现在主流是2.0版本。2.0版本的winsock API函数的声明在Winsock2.h中,在Ws2_32.dll中实现。我们编程的时候需要包含头文件Winsock2.h,同时要加入引用库ws2_32.lib。

5.4.1 WSAStartup函数

该函数用于初始化Winsock DLL库,这个库提供了所有Winsock函数,因此WSAStartup必须要在所有Winsock函数调用之前调用。函数声明如下:

    int WSAStartup(WORD  wVersionRequested,  LPWSADATA  lpWSAData);

其中,参数wVersionRequested指明程序请求使用的Winsock规范的版本,高位字节指明副版本,低位字节指明主版本;参数lpWSAData返回请求的Socket的版本信息,是一个指向结构体WSADATA的指针。如果函数成功就返回零,否则返回错误码。

结构体WSADATA保存Windows套接字的相关信息,定义如下:

    typedef struct WSAData {
    WORD wVersion;  // Winsock规范的版本号,即文件Ws2_32.dll的版本号
    WORD wHighVersion;  // Winsock规范的最高版本号
    char szDescription[WSADESCRIPTION_LEN+1];  //套接字的描述信息
    char szSystemStatus[WSASYS_STATUS_LEN+1];  //系统状态或配置信息
    unsigned short iMaxSockets;  //能打开套接字的最大数目
    unsigned short iMaxUdpDg;  //数据报的最大长度,2或以上的版本中该字段忽略
    char FAR* lpVendorInfo;    //套接字的厂商信息,2或以上的版本中该字段忽略
    } WSADATA,  *LPWSADATA;

当一个应用程序调用WSAStartup函数时,操作系统根据请求的Winsock版本来搜索相应的Winsock库,然后绑定找到的Winsock库到该应用程序中。以后应用程序就可以调用所请求的Winsock库中的函数了。比如一个程序要使用2.0版本的Winsock,代码可以这样写:

    WORD  wVersionRequested = MAKEWORD( 2,0 );
    int err = WSAStartup( wVersionRequested, &wsaData );

5.4.2 socket/WSASocket函数

socket函数用来创建一个套接字,声明如下:

    SOCKET  WSAAPI  socket( int af,  int type,  int  protocol);

其中,参数af用于指定套接字所使用的协议簇(地址簇):对于IPv4协议簇,该参数取值为AF_INET(PF_INET);对于IPv6,该参数取值为AF_INET6。当然不仅仅局限于这两种协议簇,我们可以在ws2def.h中看到其他的协议簇定义:

    #define AF_UNSPEC     0       // unspecified
    #define AF_UNIX       1       // local to host (pipes, portals)
    #define AF_INET       2       // internetwork: UDP, TCP, etc.
    #define AF_IMPLINK    3       // arpanet imp addresses
    #define AF_PUP        4       // pup protocols: e.g. BSP
    #define AF_CHAOS      5       // mit CHAOS protocols
    #define AF_NS         6       // XEROX NS protocols
    #define AF_IPX        AF_NS   // IPX protocols: IPX, SPX, etc.
    #define AF_ISO        7       // ISO protocols
    #define AF_OSI        AF_ISO  // OSI is ISO
    #define AF_ECMA       8       // european computer manufacturers
    #define AF_DATAKIT    9       // datakit protocols
    #define AF_CCITT      10      // CCITT protocols, X.25 etc
    #define AF_SNA        11      // IBM SNA
    #define AF_DECnet     12      // DECnet
    #define AF_DLI        13      // Direct data link interface
    #define AF_LAT        14      // LAT
    #define AF_HYLINK     15      // NSC Hyperchannel
    #define AF_APPLETALK  16      // AppleTalk
    #define AF_NETBIOS    17      // NetBios-style addresses
    #define AF_VOICEVIEW  18      // VoiceView
    #define AF_FIREFOX    19      // Protocols from Firefox
    #define AF_UNKNOWN1   20      // Somebody is using this!
    #define AF_BAN        21      // Banyan
    #define AF_ATM        22      // Native ATM Services
    #define AF_INET6      23      // Internetwork Version 6
    #define AF_CLUSTER    24      // Microsoft Wolfpack
    #define AF_12844      25      // IEEE 1284.4 WG AF
    #define AF_IRDA       26      // IrDA
    #define AF_NETDES     28      // Network Designers OSI & gateway

参数type指定要创建的套接字类型:如果要创建流套接字类型,则取值为SOCK_STREAM;如果要创建数据报套接字类型,则取值为SOCK_DGRAM;如果要创建原始套接字协议,则取值为SOCK_RAW。在Winsock1.1中,仅仅支持SOCK_STREAM和SOCK_DGRAM;到了Winsock2,就支持较多的套接字类型了,包括SOCK_RAW。在ws2def.h中定义了套接字类型的宏定义:

    // Socket types.
    #define SOCK_STREAM     1
    #define SOCK_DGRAM      2
    #define SOCK_RAW         3
    #define SOCK_RDM         4
    #define SOCK_SEQPACKET   5

参数protocol指定应用程序所使用的通信协议,即协议簇参数af所使用的上层(传输层)协议,比如IPPROTO_TCP表示TCP协议,IPPROTO_UDP表示UDP协议。这个参数通常和前面两个参数都有关,如果该参数为0,就表示使用所选套接字类型对应的默认协议,比如如果协议簇是AF_INET,套接字是SOCK_STREAM,那么系统默认使用TCP协议,而SOCK_DGRAM套接字默认使用的协议是UDP。一般而言,给定协议簇和套接字类型,如果只支持一种协议,那么用0没有问题;如果给定协议簇和套接字类型支持多种协议,就要指定协议参数protocol了。这一章我们进行的是TCP编程,因此取IPPROTO_TCP或0即可。如果函数成功返回一个SOCKET类型的描述符,那么该描述符可以用来引用新创建的套接字,如果失败就返回INVALID_SOCKET,可以使用函数WSAGetLastError来获取错误码。

SOCKET的定义如下:

    typedef  UINT_PTR  SOCKET;

UINT_PTR其实是一个无符号整型,定义如下:

    typedef  _W64  unsigned int  UINT_PTR

WSASocket函数是socket的扩展版本,功能更为强大,通常用socket即可。默认情况下,这两个函数创建的套接字都是阻塞(模式)套接字。

5.4.3 bind函数

该函数让本地地址信息关联到一个套接字上,既可以用于连接的(流式)套接字,也可以用于无连接的(数据报)套接字。当新建了一个Socket以后,套接字数据结构中有一个默认的IP地址和默认的端口号。服务程序必须调用bind函数来给其绑定自己的IP地址和一个特定的端口号。客户程序一般不必调用bind函数来为其Socket绑定IP地址和端口号,客户端程序通常会用默认的IP和端口来与服务器程序通信。bind函数声明如下:

    int bind(  SOCKET  s,  const struct  sockaddr *  name,  int  namelen);

其中,参数s标识一个待绑定的套接字描述符;name为指向结构体sockaddr的指针,该结构体包含了IP地址和端口号;namelen确定name的缓冲区长度。如果函数成功就返回零,否则返回SOCKET_ERROR。

结构体sockaddr的定义如下:

    struct sockaddr {
           ushort  sa_family; //协议簇,在socket编程中只能是AF_INET
           char    sa_data[14]; //为套接字存储的目标IP地址和端口信息
    };

这个结构体不是那么直观,所以人们又定义了一个新的结构:

    struct sockaddr_in {
            short   sin_family; //协议簇,在socket编程中只能是AF_INET
            u_short  sin_port; //端口号(使用网络字节顺序)
            struct  in_addr  sin_addr; //IP地址,是一个结构
            char    sin_zero[8];//为了与sockaddr结构保持大小相同而保留的空字节,填充零即可
    };

这两个结构长度是一样的,所以可以相互强制转换。

结构in_addr用来存储一个IP地址,它定义如下:

    typedef struct in_addr {
      union {
        struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
        struct { USHORT s_w1,s_w2; } S_un_w;
        ULONG S_addr; //一般使用这个,它的字节序为网络字节序
      } S_un;
    } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

我们通常习惯用点数的形式表示IP地址,为此系统提供了函数inet_addr,将IP地址从点数格式转换成网络字节格式。比如,已知IP为223.153.23.45,我们把它存储到in_addr中,可以这样写:

    sockaddr_in  in;
    unsigned long  ip = inet_addr("223.153.23.45");
    if(ip!= INADDR_NONE) //如果IP地址不合法,inet_addr将返回INADDR_NONE
    in. sin_addr.S_un.S_addr=ip;

我们对套接字进行绑定时,要注意设置的IP地址是服务器真实存在的地址,不要输错。比如服务器主机的IP地址是192.168.1.2,而我们却设置绑定到了192.168.1.3上,此时bind函数会返回错误:

    //创建一个套接字,用于监听客户端的连接
    SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.3");
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(8000);  //使用端口8000

    int res = bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //绑定
    if (res == SOCKET_ERROR)
    {
         printf("bind failed:%d\n", WSAGetLastError());
         return -1;
    }

这几行代码会打印:bind failed:10049。通过查询错误码10049得知,10049所代表的含义是“Cannot assign requested address.”,意思就是不能分配所要求的地址,即IP地址无效。因此碰到这个错误码,大家应该多多注意是否把IP地址写错了。同样,类似的代码在Linux下也是会报错误的(但错误码不同),如下所示:

    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = inet_addr("192.168.1.3");
    server_address.sin_port = htons(8000);
    if (bind(sfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0)
    {
    printf("bind failed:%d\n", errno);
         return -1;
    }

这段代码在Linux下输出“bind failed:99”,错误码errno是99,虽然和Windows下的错误码不同,但代表的含义也是“Cannot assign requested address.”。总而言之,大家设置IP地址时要仔细小心。

能否不具体设置IP地址,让系统去选一个可用的IP地址呢?答案是肯定的,这也算是对粗心之人的一种帮助吧,见下面这一行:

    addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

我们用“htonl(INADDR_ANY);”替换了“inet_addr("192.168.1.3");”,其中htonl是把主机字节序转为网络字节序,在网络上传输整型数据通常要转换为网络字节序。宏INADDR_ANY告诉系统选取一个任意可用的IP地址。

5.4.4 listen函数

该函数用于服务器端的流套接字,让流套接字处于监听状态,监听客户端发来的建立连接的请求。该函数声明如下:

    int listen( SOCKET s,  int backlog);

其中,参数s是一个流套接字描述符,处于监听状态的流套接字s将维护一个客户连接请求队列;backlog表示连接请求队列所能容纳的客户连接请求的最大数量,或者说队列的最大长度。如果函数成功就返回零,否则返回SOCKET_ERROR。

举个例子,如果backlog设置了5,当有6个客户端发来连接请求时,那么前5个客户端连接会放在请求队列中,第6个客户端会收到错误。

5.4.5 accept/ WSAAccept函数

accept函数用于服务程序从处于监听状态的流套接字的客户连接请求队列中取出排在最前的一个客户端请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符,以后就用新创建的套接字与客户套接字相互传输数据。该函数声明如下:

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

其中,参数s为处于监听状态的流套接字描述符;addr返回新创建的套接字的地址结构;addrlen指向结构sockaddr的长度,表示新创建的套接字地址结构的长度。如果函数成功就返回一个新的套接字的描述符,该套接字将与客户端套接字进行数据传输;如果失败就返回INVALID_SOCKET。

下面的代码演示了accept的使用:

       struct  sockaddr_in   NewSocketAddr;
       int addrlen;
       addrlen=sizeof(NewSocketAddr);
       SOCKET  NewServerSocket=accept(ListenSocket, (struct sockaddr *)&
    NewSocketAddr, &addrlen);

WSAAccept函数是accept的扩展版本。

5.4.6 connect/WSAConnect函数

connect函数在套接字上建立一个连接。它用在客户端,客户端程序使用connect函数请求与服务器的监听套接字请求建立连接。该函数声明如下:

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

其中,s为还未连接的套接字描述符;name是对方套接字的地址信息;namelen是name所指缓冲区的大小。如果函数成功就返回零,否则返回SOCKET_ERROR。

对于一个阻塞套接字,该函数的返回值表示连接是否成功,但如果连接不上通常要等较长时间才能返回,此时可以把套接字设为非阻塞方式,然后设置连接超时时间。对于非阻塞套接字,由于连接请求不会马上成功,因此函数会返回SOCKET_ERROR,但这并不意味着连接失败,此时用函数WSAGetLastError返回错误码将是WSAEWOULDBLOCK,如果后续连接成功了,就将获得错误码WSAEISCONN。

函数WSAConnect为connect的扩展版本。

5.4.7 send/ WSASend函数

send函数用于在已建立连接的socket上发送数据,无论是客户端还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。但在该函数内部,它只是把参数buf中的数据发送到套接字的发送缓冲区中,此时数据并不一定马上成功地被传到连接的另一端,发送数据到接收端是底层协议完成的。该函数只是把数据发送(或称复制)到套接字的发送缓冲区后就返回了。该函数声明如下:

    int send( SOCKET s,  const char* buf,  int len,  int flags);

其中,参数s为发送端套接字的描述符;buf存放应用程序要发送数据的缓冲区;len表示buf所指缓冲区的大小;flags一般设零。如果函数复制数据成功,就返回实际复制的字节数,如果函数在复制数据时出现错误,那么send就返回SOCKET_ERROR。

如果底层协议在后续的数据发送过程中出现网络错误,那么下一个socket函数就会返回SOCKET_ERROR(这是因为每一个除send外的socket函数在执行的最开始总要先等待套接字发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该socket函数就返回 SOCKET_ERROR)。

函数WSASend是send的扩展函数。

5.4.8 recv/ WSARecv函数

recv函数从连接的套接字或无连接的套接字上接收数据,该函数声明如下:

    int recv(  SOCKET  s,  char*  buf,  int  len,  int  flags);

其中,参数s为已连接或已绑定(针对无连接)的套接字的描述符;buf指向一个缓冲区,该缓冲区用来存放从套接字的接收缓冲区中复制的数据;len为buf所指缓冲区的大小;flags一般设零。如果函数成功,就返回收到数据的字节数;如果连接被优雅地关闭了,那么函数返回零;如果发生错误,就返回SOCKET_ERROR。

函数WSARecv是recv的扩展版本。

5.4.9 closesocket函数

该函数用于关闭一个套接字。声明如下:

    int closesocket(SOCKET s);

其中,s为要关闭的套接字的描述符。如果函数成功就返回零,否则返回SOCKET_ERROR。

5.4.10 inet_addr函数

该函数用于将一个点分的字符串形式表示的IP转换成无符号长整型。函数声明如下:

    unsigned long inet_addr(  const char* cp);

其中,参数cp指向一个点分的IP地址的字符串。如果函数成功就返回无符号长整型表示的IP地址,如果函数失败就返回INADDR_NONE。

下面的代码演示了函数inet_addr的使用:

    sockaddr_in  in;
    unsigned long  dwip = inet_addr("223.153.23.45");
    //如果inet_addr 失败,比如IP地址不合法,inet_addr将返回INADDR_NONE
    if(dwip!= INADDR_NONE)
    in. sin_addr.S_un.S_addr=ip;

也可以写成“in. sin_addr.s_addr= dwip;”,因为:

    #define  s_addr  S_un.S_addr

5.4.11 inet_ntoa函数

该函数用于将一个in_addr结构类型的IP地址转换成点分的字符串形式表示的IP地址,函数声明如下:

    char *  inet_ntoa( struct  in_addr  in);

其中,参数in是in_addr结构类型的IP地址。如果函数成功就返回点分的字符串形式表示的IP地址,否则返回NULL。

5.4.12 htonl函数

该函数将一个u_long类型的主机字节序转为网络字节序(大端)。函数声明如下:

    u_long  htonl(  u_long  hostlong);

其中,参数hostlong是要转为网络字节序的数据。函数返回网络字节序的hostlong。

5.4.13 htons函数

该函数将一个u_short类型的主机字节序转为网络字节序(大端)。函数声明如下:

    u_short htons(  u_short  hostshort);

其中,参数hostshort是要转为网络字节序的数据。函数返回网络字节序的hostshort。

5.4.14 WSAAsyncSelect函数

该函数把某个套接字的网络事件关联到窗口,以便从窗口上接收该网络事件的消息通知。这个函数用于实现非阻塞套接字的异步选择模型,允许应用程序以Windows消息的方式接收网络事件通知。该函数调用后会自动把套接字设为非阻塞模式,并且为套接字绑定一个窗口句柄,当有网络事件发生时,便向这个窗口发送消息。函数声明如下:

       int WSAAsyncSelect(  SOCKET  s, HWND  hWnd,  unsigned int  wMsg,  long
lEvent);

其中,参数s为网络事件通知所需的套接字描述符;hWnd为当网络事件发生时,用于接收消息的窗口句柄;wMsg为网络事件发生时所接收到的消息;lEvent为应用程序感兴趣的一个或多个网络事件的比特组合码(或称位掩码)。如果函数成功就返回零,否则返回SOCKET_ERROR。

常见的套接字网络事件位掩码值如表5-2所示。

表5-2 常见的套接字网络事件位掩码值

要注意的是FD_WRITE,不是说发送数据时就会触发该事件,只是在连接刚刚建立,或者发送缓冲区原先不够容纳所要发送的数据而现在空间够了,才触发该事件。

此外,可以通过消息wMsg的消息参数lParam来判断错误码和获取事件码。在Winsock2.h中有这样的定义:

    #define WSAGETSELECTERROR(lParam)  HIWORD(lParam)
    #define WSAGETSELECTEVENT(lParam)  LOWORD(lParam)

其中,通过WSAGETSELECTERROR(lParam)可以判断是否发生错误,并且此时不能用WSAGetLastError来获取错误码,要用HIWORD(lParam)来获取错误码,错误码定义在Winsock2.h中;LOWORD(lParam)里存放了事件码,比如FD_READ、FD_WRITE等。

另一个消息参数wParam存放发生错误或事件的那个套接字。

5.4.15 WSACleanup函数

无论是客户端还是服务器端,当程序完成Winsock库的使用后,都要调用WSACleanup函数来解除与Winsock库的绑定并且释放Winsock库所占用的系统资源。该函数声明如下:

    int  WSACleanup ();

如果函数成功就返回零,否则返回SOCKET_ERROR。

TCP套接字编程可以分为阻塞套接字编程和非阻塞套接字编程。两种使用方式不同。