zoukankan      html  css  js  c++  java
  • windows网络编程第二版 第一章 winsock简介 读书笔记

    Network Programming for Microsoft Windows, 2nd Edition Chapter 1 Winsock介绍

    1. Winsock是微软做的网络通讯库,以前winsock是1.1版本,现在winsock有了winsock 2.2版本,winsock2版本变动比较大,做了很多工作。Winsock的接口设计在很大程度上参考了UNIX平台上的BSD的socket实现,在 Winsock 2里面接口做了一些变动,目的是做到winsock真正和协议无关,是一个通用的开发平台。

    2. 本章介绍了TCP和UPD的两个最简单的winsock例子,实际上,这样的例子对于一般的网络通讯程序来说已经够用了。这些例子都是block的网络通讯方式,第五章会讲解non-block的winsock编程。

    3. Winsock 1和Winsock 2的函数的命名方式。一般来说,Winsock 2的函数都以WSA开头,比如创建一个socket在winsock 1中就是调用socket函数,在winsock 2中我们就可以使用WSASocket,相比socket,WSASocket提供了更多的特性。Winsock 2兼容Winsock 1的所有函数。上述的命名方式有一些例外,他们是:WSAStartup, WSACleanup, WSARecvEx, and WSAGetLastError,这四个函数在Winsock 1中就定义了。

    4. winsock的头文件和lib文件。这是开发winsock程序必须的了。在目前大部分的windows平台下,winsock 2都是ready的。Windows CE只支持winsock 1。在开发winsock 2的程序的时候,我们需要include winsock2.h,在开发winsock 1的程序的时候,我们需要include winsock.h。还有一个叫做mswsock.h,这里面定义的函数是只有在微软平台上运行的函数,这些函数能提供高性能,在我们书写需要高性能的网 络通讯程序的时候,我们使用这些函数,具体内容在第六章中描述。

    lib文件。winsock 2的程序需要链接ws2_32.lib, winsock 1的程序需要链接wsock32.lib,如果使用mswsock,需要链接mswsock.dll

    5. Winsock初始化。调用WSAStartup可以初始化winsock,也就是程序load winsock的dll文件。如果没有初始化winsock就调用了winsock中的函数,函数会返回 SOCKET_ERROR,SOCKET_ERROR是一个generic的返回值表示winsock操作失败,详细的错误信息可以通过调用 WSAGetLastError来获得,对于上述描述的错误,得到的错误码是WSANOTINITIALISED。WSAStartup函数的原型如下:

    Code: Select all
    int WSAStartup(
        WORD wVersionRequested,
        LPWSADATA lpWSAData
    );


    wVersionRequested 参数用来指定我们要使用的winsock版本。WORD中高字节部分用来指定使用的winsock的minor版本,低字节用来指定使用winsock的 major版本。我们可以使用MAKEWORD(x,y)宏来生成这样一个WORD, x是minor version,y是major version。

    lpWSAData参数是一个struct指针,WSAStartup函数会填写这个struct中的内容,这个struct的定义是:

    Code: Select all
    typedef struct WSAData
    {
        WORD           wVersion;
        WORD           wHighVersion;
        char           szDescription[WSADESCRIPTION_LEN + 1];
        char           szSystemStatus[WSASYS_STATUS_LEN + 1];
        unsigned short iMaxSockets;
        unsigned short iMaxUdpDg;
        char FAR *     lpVendorInfo;
    } WSADATA, * LPWSADATA;


    wVersion -- 使用的winsock版本
    wHighVersion -- 目前平台上可用的winsock的最高版本
    szDescription, szSystemStatus -- 用作特殊用途,一般不常用
    iMaxSockets, iMaxUdpDg -- 不用使用这两个字段。他们定义了最大的并发连接数和最大的udp datagram size。然而,要找出正确的这些值,可能还要参考协议本身中的一些限制。
    lpVendorInfo -- 保留用,用来存放vendor-specific information.不用使用这个字段。

    6. 各种windows平台支持的winsock版本:

    附件1

    可见,大部分windows都支持winsock 2

    7. winsock 1的程序可以在支持winsock 2的windows上良好运行。实际上,在winsock 2的windows上,所有的winsock 1的请求都会被映射到winsock 2的dll中。微软的兼容工作做的还是不错的。如果我们在WSAStartup中申请了一个高于目前平台支持的winsock版本,WSAStartup 会失败,同时WSADATA中的wHighVersion中会存放该平台上支持的winsock的最高版本。

    8. 调用WSACleanup用来结束一个winsock程序。

    int WSACleanup(void);

    这个函数会释放所有资源,关闭所有pending的请求等。实际上,就算我们的程序不调用这个函数,操作系统也会把这些资源回收,但是,一个良好的程序,必须调用这个函数。

    9. 错误处理。前面说过了,大多数的winsock函数失败的时候,都会返回SOCKET_ERROR,使用WSAGetLastError能获得详细的错误 码,这些错误码都定义在winsock2.h, winsock.h中。有些函数例外,他们不返回SOCKET_ERROR而是其他的返回值。下面是一个使用WSAGetLastError函数的例子:

    Code: Select all
    #include <winsock2.h>

    void main(void)
    {
       WSADATA wsaData;

       // Initialize Winsock version 2.2

       if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
       {
          // NOTE: Since Winsock failed to load we cannot use
          // WSAGetLastError to determine the specific error for
          // why it failed. Instead we can rely on the return
          // status of WSAStartup.

          printf("WSAStartup failed with error %d\n", Ret);
          return;
       }

       // Setup Winsock communication code here

       // When your application is finished call WSACleanup
       if (WSACleanup() == SOCKET_ERROR)
       {
          printf("WSACleanup failed with error %d\n", WSAGetLastError());
       }
    }


    10. Addressing a Protocol. 本节只介绍基于IP的协议,参考第三章有很多关于网络协议的东西。本节只介绍在winsock中定义一个IPv4的信息结构。定义这样的结构对于winsock中的bind等这样的函数来说是必须的。

    Code: Select all
    struct sockaddr_in
    {
        short           sin_family;
        u_short         sin_port;
        struct in_addr  sin_addr;
        char            sin_zero[8];
    };


    sockaddr_in其实是针对Internet,也就是TCP/IP的一个结构。在后面我们会看到,任何结构将来都会转换成SOCKADDR这个结构,有点类似SOCKADDR是基类,sockaddr_in这些都是SOCKADDR的派生类一样。

    sin_family -- 必须设成AF_INET,表示我们要使用IP地址family
    sin_port -- 指定端口,不过要考虑网络次序和主机次序的问题,下面会介绍
    sin_addr -- 指定IP地址。其实是指定这个struct中的一个字段。IP地址也是以long的类型存放的,也要处理网络次序和主机次序的问题。微软提供了一个function来让我们把一个IP字符串转换成一个long型的数值。这个函数是:

    Code: Select all
    unsigned long inet_addr(
        const char FAR *cp
    );


    注意,调用了inet_addr函数之后生成的long,就不需要再做主机次序,网络次序的转换了。但是后面会看到,如果没用inet_addr,比如给出的IP地址是INADDR_ANY的话,还是要调用htonl函数的。

    sin_zero -- 没有用,完全是放在这里为了和SOCKADDR的大小兼容

    11. 主机次序和网络次序。这个问题的起源是因为在上述的结构中,我们需要指定一个数字类型的变量,比如port和long型的IP地址。由于数字是多字节的一 块内容,所以就有字节摆放顺序的问题。众所周知,数字类型的变量在不同的计算机(主机)中存放的次序是不一样的,有把高位字节存在前面,低位字节存在后面 的,也有反过来的。对于网络通讯来说,必需要把这种情况统一起来,否则这些关键的信息就会解析错误了。于是就有了所谓的主机次序和网络次序的相互转换了。

    winsock中提供了一组函数用来做这个事情,下面的四个函数用来将主机次序转成网络次序:

    Code: Select all
    u_long htonl(u_long hostlong);

    int WSAHtonl(
        SOCKET s,
        u_long hostlong,
        u_long FAR * lpnetlong
    );

    u_short htons(u_short hostshort);

    int WSAHtons(
        SOCKET s,
        u_short hostshort,
        u_short FAR * lpnetshort
    );


    带 l的是处理4字节的数,带s的是处理2字节的数。不带WSA的函数传入主机次序的数值,返回网络次序的数;带WSA的是把主机次序的数传入 hostlong, hostshort参数,处理后的网络次序的数被填充在lpnetlong, lpnetshort指针指向的变量中。

    以下四个函数从网络次序转成主机次序:

    Code: Select all
    u_long ntohl(u_long netlong);

    int WSANtohl(
        SOCKET s,
        u_long netlong,
        u_long FAR * lphostlong
    );

    u_short ntohs(u_short netshort);

    int WSANtohs(
        SOCKET s,
        u_short netshort,
        u_short FAR * lphostshort
    );


    12. 给出一个例子用来示范怎么创建这么一个sockadd_in的东西:

    Code: Select all
    SOCKADDR_IN InternetAddr;
    INT nPortId = 5150;

    InternetAddr.sin_family = AF_INET;

    // Convert the proposed dotted Internet address 136.149.3.29
    // to a four-byte integer, and assign it to sin_addr

    InternetAddr.sin_addr.s_addr = inet_addr("136.149.3.29");

    // The nPortId variable is stored in host-byte order. Convert
    // nPortId to network-byte order, and assign it to sin_port.

    InternetAddr.sin_port = htons(nPortId);


    第三章会讲述如何在填写IP地址的时候,填写一个域名,以及winsock中包含的一些域名和IP地址互转换的函数。

    13. 创建一个socket。socket是网络通讯程序中必须的一个数据结构,他不同于文件描述符,他有单独的数据类型定义-SOCKET。创建一个 socket可以调用socket方法或WSASocket。这里我们用socket函数来举例,后面会详细介绍这些方法。

    Code: Select all
    SOCKET socket (
        int af,
        int type,
        int protocol
    );


    af -- 定义协议的address family,对于IPv4,当然是AF_INET
    type -- 定义socket类型。对于TCP,填写SOCK_STREAM,对于UDP,填写SOCK_DGRAM
    protocol -- 定义协议。如果我们在af或type中定义了多种类型的话,这里可以定义一个协议的组合。对于TCP,设置此项为IPPROTO_TCP,对于UDP,设置为IPPROTO_UDP


     

    14. 基于TCP的winsock代码编写例子(包含server端和client端程序)。服务器端的程序需要依次调用如下函 数:socket/WSASocket, bind, listen, accept/WSAAccept,注意,本节中没有说bind, listen有对应的WSA的函数

    bind:
    Code: Select all
    int bind(
        SOCKET                     s,
        const struct sockaddr FAR* name,
        int                        namelen
    );


    示例代码:

    Code: Select all
    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);

    bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));


    这里IP地址指定成INADDR_ANY,表示绑定在本机,也就是说,如果本机有多块网卡的话,往任何一块网卡上指定的端口上发送的数据都能被上述程序得到。

    bind 的错误处理。bind失败,返回SOCKET_ERROR,错误码是WSAEADDRINUSE,表示本机有另外一个进程已经使用了我们指定的IP地址和 端口或者是这个IP地址或端口处于TIME_WAIT状态(TIME_WAIT状态在下面会描述);此外,如果我们bind了一次之后又再次往同样的端 口,IP bind,那错误码是WSAEFAULT。

    listen:
    Code: Select all
    int listen(
        SOCKET s,
        int    backlog
    );


    backlog -- 处理并发请求的数量。比如设成2,如果同时有三个并发请求过来,那么前两个会放入排队队列,第三个请求就会被拒绝,同时返回 WSAECONNREFUSED错误。一旦一个请求被accept处理之后,排队队列中就会清除这个请求,新的请求就可以进来了。backlog的最大值 和协议有关系,没有一个确切的方法可以给出一个具体的最大值,如果我们指定的backlog值超过了允许的范围,函数会帮我们把这个值设成最大允许的值。

    listen的错误处理,一般返回WSAEINVAL,表示在listen之前没有调用bind。

    accept:
    Code: Select all
    SOCKET accept(
        SOCKET s,
        struct sockaddr FAR* addr,
        int FAR* addrlen
    );


    accept 中除了socket之外的两个参数是OUT类型的,也就是accept会填写这两个参数。accept会取出排队队列中的第一个request,然后处 理,addr中会存放client的IP地址,端口等信息,addrlen中存放的是addr结构的大小。此外,accept返回一个SOCKET变量, 利用这个socket变量,server端程序就能和client端程序进行send/recv这样的操作了。

    accept的错误处理, 出错时,accept的返回值是INVALID_SOCKET,错误码有WSAEWOULDBLOCK,当我们使用非阻塞的non-block的 listen方法,而当前队列中没有可服务的request的时候,会产生这个错误码。WSAAccept方法是winsock 2中的方法,这个方法比accept增强了一些特性,比如可以给定一个condition,这个condition返回true的时候才accept一个 connection。具体看第10章

    下面给出示例代码,这段代码是核心代码演示,不一定完整,而且这段代码只会accept一次就退出了,正常程序应该有个while循环:

    Code: Select all
    #include <winsock2.h>

    void main(void)
    {
       WSADATA              wsaData;
       SOCKET               ListeningSocket;
       SOCKET               NewConnection;
       SOCKADDR_IN          ServerAddr;
       SOCKADDR_IN          ClientAddr;
       int                  Port = 5150;
       
       // Initialize Winsock version 2.2

       WSAStartup(MAKEWORD(2,2), &wsaData);
       
          // Create a new socket to listen for client connections.

          ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        
          // Set up a SOCKADDR_IN structure that will tell bind that we
          // want to listen for connections on all interfaces using port
          // 5150. Notice how we convert the Port variable from host byte
          // order to network byte order.
        
          ServerAddr.sin_family = AF_INET;
          ServerAddr.sin_port = htons(Port);   
          ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
        
          // Associate the address information with the socket using bind.
        
          bind(ListeningSocket, (SOCKADDR *)&ServerAddr,
          sizeof(ServerAddr));

       // Listen for client connections. We used a backlog of 5, which
       // is normal for many applications.

          listen(ListeningSocket, 5);

       // Accept a new connection when one arrives.

          NewConnection = accept(ListeningSocket, (SOCKADDR *)
                              &ClientAddr,&ClientAddrLen));

       // At this point you can do two things with these sockets. Wait
       // for more connections by calling accept again on ListeningSocket
       // and start sending or receiving data on NewConnection. We will
       // describe how to send and receive data later in the chapter.

       // When you are finished sending and receiving data on the
       // NewConnection socket and are finished accepting new connections
       // on ListeningSocket, you should close the sockets using the
       // closesocket API. We will describe socket closure later in the
       // chapter.

          closesocket(NewConnection);
          closesocket(ListeningSocket);

       // When your application is finished handling the connections,
       // call WSACleanup.

          WSACleanup();
    }
    15. 下面讲述基于Client的程序如何写。相比server端的程序,client端的程序简单很多。在此之前,书中补充了一些TCP/IP中的知识,我觉得在这里摘录下来很有必要。

    16. TCP States.

    每个TCP socket的初始状态都是CLOSED状态。当一个client初始化一个连接时,他向server发送一个SYN的packet,然后将自己 socket的状态设成SYN_SENT;server收到SYN,发送SYN-ACK;client受到SYN-ACK,发送ACK,此时client 的socket的状态变成ESTABLISHED。如果server没有发送SYN-ACK,那么client的socket在timeout之后自动变 回CLOSED状态。

    看server这边。当一个server处于监听状态时,他的socket状态是LISTEN;当收到一个client的SYN后,server 反馈SYN-ACK,同时socket的状态变成SYN_RCVD;最后client反馈ACK,此时server的socket状态变成 ESTABLISHED

    下面来看在关闭连接的时候SOCKET的变化状态。首先一个连接的关闭分成两种-主动关闭和被动关闭。所谓主动关闭就是由自身发起的一个连接关 闭。当主动关闭开始时,我方发送一个FIN给对方,同时socket状态变成FIN_WAIT_1,这个动作一般是我们在调用shutdown或 closesocket的时候产生;对方回馈一个ACK,此时我方的socket状态变成FIN_WAIT_2;如果对方也关闭了连接,那么我方会收到对 方的FIN,此时我方反馈一个ACK,同时我方将socket的状态置成TIME_WAIT。

    TIME_WAIT状态也称为2MSL状态。MSL是Maximum Segment Lifetime的意思,他用来表示一个packet能在网络上的存活时间。当一个程序进入TIME_WAIT状态的时候,他将保持两倍的MSL时间,保 持这个时间是为了保证TCP能发送FIN,ACK这些packet能正确被收发。当2MSL时间到达后,socket将进入CLOSED状态。

    前面我们在讲述bind的时候提到,bind失败有一种可能就是socket处于TIME_WAIT状态,当socket处于TIME_WAIT 状态的时候,我方的IP地址和端口,以及对方的IP地址和端口,都处于不可用的状态,这称为一对socket。很多TCP的实现库是不允许在 TIME_WAIT的时候创建socket的,bind的,微软的winsock在这方面做了一些小的工作,他允许我们使用socket option SO_REUSEADDR来使用一个已经处于TIME_WAIT状态的socket,具体在第七章讨论。

    OK,回到前面的连接关闭上来。主动关闭还有两种可能的情况。当我方发送FIN后,此时对方不返回ACK,而是也返回FIN,此时我方发送一个 ACK给对方,同时将自己socket的状态置成CLOSING状态;然后我方收到对方的ACK,我方将socket的状态置成TIME_WAIT。第三 种主动关闭的情况:我方发送FIN;对方发送FIN-ACK;我方发送ACK,同时将socket状态立即置成TIME_WAIT

    现在讨论被动关闭,被动关闭其实是收到了对方正在关闭连接的notify,此时自己关闭连接的流程:我方收到对方的FIN;我方回馈ACK,同时 将自己socket的状态置成CLOSE_WAIT状态;然后我方发送FIN给对方,同时将自己socket的状态置成LAST_ACK;我方收到对方的 ACK,socket状态变成CLOSED

    上面有关主动关闭和被动关闭的描述请参考下图:

    附件1

    17. 回头来讲client端的winsock程序写法,关键函数只有一个:connect

    Code: Select all
    int connect(
        SOCKET s,
        const struct sockaddr FAR* name,
        int namelen
    );


    name是sockaddr的结构,里面填写服务器的IP地址和端口号,namelen是结构的长度。

    connect的出错:如果服务器上没有启动服务器端的程序,connect设置出错码WSAECONNREFUSED;如果connect设置了出错码WSAETIMEDOUT,那可能是网络不通。

    下面演示了一个客户端程序:

    Code: Select all
    #include <winsock2.h>

    void main(void)
    {
       WSADATA              wsaData;
       SOCKET               s;
       SOCKADDR_IN          ServerAddr;
       int                  Port = 5150;
       
       // Initialize Winsock version 2.2

       WSAStartup(MAKEWORD(2,2), &wsaData);
       
       // Create a new socket to make a client connection.

          s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
       
       // Set up a SOCKADDR_IN structure that will be used to connect
       // to a listening server on port 5150. For demonstration
       // purposes, let's assume our server's IP address is 136.149.3.29.
       // Obviously, you will want to prompt the user for an IP address
       // and fill in this field with the user's data.

          ServerAddr.sin_family = AF_INET;
          ServerAddr.sin_port = htons(Port);   
          ServerAddr.sin_addr.s_addr = inet_addr("136.149.3.29");

       // Make a connection to the server with socket s.

          connect(s, (SOCKADDR *) &ServerAddr, sizeof(ServerAddr));
         
       // At this point you can start sending or receiving data on
       // the socket s. We will describe sending and receiving data
       // later in the chapter.

       // When you are finished sending and receiving data on socket s,
       // you should close the socket using the closesocket API. We will
       // describe socket closure later in the chapter.

          closesocket(s);

       // When your application is finished handling the connection, call
       // WSACleanup.

          WSACleanup();
    }


    18. 数据传输。关键函数是send, WSASend, recv, WSARecv,不要和sendto, recvfrom, WSASendTo, WSARecvFrom混淆,这四个是UDP的数据传输函数,由于UDP实现不需要建立连接,所以每次发送数据都要指定对方的地址和端口,收数据也是一 样,所以他们的函数才有To, From这样的后缀。

    send, WSASend, recv, WSARecv这些函数的出错返回值都是SOCKET_ERROR。最常见的出错码有WSAECONNABORTED, WSAECONNRESET,这两个出错码一般都指示数据超时或对方关闭了连接。还有一个出错码WSAEWOULDBLOCK,在我们使用异步I/O的时 候会发生,第五章会介绍。

    send:
    Code: Select all
    int send(
        SOCKET s,
        const char FAR * buf,
        int len,
        int flags
    );


    buf -- 存放发送数据的buffer
    len -- buffer的大小
    flags -- 设为0,表示没有flag;还可设为MSG_DONTROUTE, MSG_OOB,这些flags可以用或 | 符号合并使用。MSG_DONTROUTE表示发送的数据包不要被路由,当然,这需要底层协议的支持,协议不支持,设置这个flag将被 ignore;MSG_OOB表示发送一个Out Of Band的数据,下面会专门介绍OOB

    正常情况下,send函数返回实际发送的 数据数量(字节数)。否则,返回SOCKET_ERROR,出错码有WSAECO-NNABORTED,表示协议出错或 timeout;WSAECONNRESET,对方关闭了连接或对方程序以外终止;WSAETIMEDOUT,网络不通。

    WSASend:
    Code: Select all
    int WSASend(
        SOCKET s,
        LPWSABUF lpBuffers,
        DWORD dwBufferCount,
        LPDWORD lpNumberOfBytesSent,
        DWORD dwFlags,
        LPWSAOVERLAPPED lpOverlapped,
        LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );


    带WSA的收发函数和不带WSA的都差不多是这样的一个原型。
    lpBuffers -- 指向WSABUF的指针。这个指针可以指向一个WSABUF,也可以指向一批WSABUF,比如一个WSABUF的数组。
    dwBufferCount -- WSABUF的数量。不是字节数哦,是一共有多少个WSABUF
    lpNumberOfBytesSent -- 实际发送的字节数,由WSASend函数填写
    dwFlags -- 和send函数的flag一样
    lpOverlapped, lpCompletionRoutine -- 用于overlapped I/O,第五章介绍

    WSASend正常返回0,否则返回SOCKET_ERROR

    在 这里我们可能会想为什么WSASend中要让我们可以设置发送一批WSABUF呢?这是基于scatter-gather I/O的思想。比如我们在实际程序中,经常会将我们的消息这样定义:前4个字节用于命令字;中间4个字节用于实际数据长度;最后N个字节存放具体数据。这 样就有了三段的数据,有了WSASend之后,我们可以使用三个WSABUF来存放这三块数据,然后用WSASend一起发送出去,而对方使用 WSARecv也可以一并接受下来,这就是scatter-gather I/O的想法,也就是说,虽然我们提供了多个buffer,但是使用这种模式的时候,函数就把这些buffer当成连续的数据来处理。以前没有这个,我们 要手动把三块buffer add成一个,然后发送,收的时候也是收到整个数据,然后一块一块拆下来。

    19. Out of Band Data. 这里介绍一下OOB数据的背景知识。当一个应用在一个流式的socket上(比如TCP)需要发送一个比普通数据重要的数据的时候,就可以使用OOB。从理论上来说,对方可以在一个独立的通道中接受并处理OOB数据。

    在 TCP中,OOB data的实现是通过设置一个urgent 1-bit marker(called URG),和在TCP头增加一个16位长度的指针来实现的。通过这个指针,就可以寻找到OOB数据。我们可以使用函数ioctlsocket,配合 SIOCATMARK option来检查数据中是否包含OOB data,第七章会讨论这个问题。

    OOB数据可以被包含在一个普通的数据包中,也可以单独封装在一个数据包中,Winsock对这两种情况都有API的封装,第七章将讨论这个问题。

    Telnet和Rlogin这样的程序使用了OOB数据。

    20. WSASendDisconnect函数:

    Code: Select all
    int WSASendDisconnect (
        SOCKET s,
        LPWSABUF lpOutboundDisconnectData
    );


    这 个函数能graceful的关闭一个connection,同时发送disconnect data。目前还没有哪个transport provider支持disconnect data,因为关闭了connection,就不能再发送数据了,微软的winsock支持disconnect data,lpOutboundDisconnectData存放的就是我们想发送的disconnect data。graceful的关闭一个connection,其实就类似使用shutdown函数(带上SD_SEND参数),下面会讲到 shutdown函数。

    21. recv, WSARecv:

    Code: Select all
    int recv(
        SOCKET s,
        char FAR* buf,
        int len,
        int flags
    );


    buf, len -- 接受数据的缓冲区和大小
    flags -- 0, 表示无flag;还可设置成MSG_PEEK或MSG_OOB或很多flag的组合。MSG_PEEK表示窥视接受到的数据,也就是说,仅仅查看而已,数 据将不会从系统的网络接受缓冲区中除掉。我们都知道,无论是send,还是recv,被操作的数据都需要从用户缓冲区拷贝到系统缓冲区然后发送,或是从系 统缓冲区拷贝到用户缓冲区(recv的时候)。MSG_PEEK设置了之后,数据将不会从系统缓冲区被移除,这是一个不好的东西,因为这样会减少系统缓冲 区接受数据的能力,从而TCP将不得不减小windows的大小(TCP中的window概念在学网络的时候接触过)来告知对方,我方的接受能力正在减 小。

    题外话:在使用message-based或datagram-based 的协议中(比如UDP),recv函数会产生WSAEMSGSIZE这样的错误,这表示接受缓冲区过小,无法装下所有的数据。但是在TCP中,不会碰到这 样的情况,因为TCP是有连接的通讯方式,一般我们的程序都会循环取数据,直到所有的数据都取完,在没有取完之前,数据会被cache或resend,从 而保证我们接受到所有的数据。

    Code: Select all
    int WSARecv(
        SOCKET s,
        LPWSABUF lpBuffers,
        DWORD dwBufferCount,
        LPDWORD lpNumberOfBytesRecvd,
        LPDWORD lpFlags,
        LPWSAOVERLAPPED lpOverlapped,
        LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );


    和 WSASend一样,我们可以设置多个WSABUF来接受数据。这里需要提的是lpFlags,这里注意到,这个flags是一个指针,不是 WSASend中的一个DWORD,这表示这个flag有可能会被WSARecv函数所修改。为什么要这么做呢?这是因为在WSARecv中,有个 MSG_PARTIAL flag的存在。MSG_PARTIAL这个flag只能用于message-based的协议,而且这个协议还要支持MSG Partial,比如AppleTalk协议。当我们调用在WSARecv的时候,设置了MSG_PARTIAL的话,这表示本次recv的动作要尽可能 快的结束,因为我们只需要接受一部分的数据。如果我们在调用WSARecv的时候,没有设置MSG_PARTIAL,但是在WSARecv返回的时候,我 们检查flag,发现WSARecv设置了MSG_PARTIAL这个flag,这表示本次接受的数据只是整个message中的一部分,我们应该继续接 受数据,直至MSG_PARTIAL flag消息,这就表示整个消息接受完了。

    lpOverlapped, lpCompletionRoutine -- 第五章介绍,用于overlapped I/O

    22. WSARecvDisconnect

    Code: Select all
    int WSARecvDisconnect(
        SOCKET s,
        LPWSABUF lpInboundDisconnectData
    );


    和WSASendDisconnect一样,用来接受disconnect data

    23. 下面来看流式协议(比如TCP)下,send和recv的例子代码。和message, datagram based的协议不同,TCP有连接,有数据传输安全保证,所以我们应该用循环来保证所有的数据都被正确发送和接受。而象UDP这样的协议,在发送和接受 的时候,都必须保证buffer足够大,能装下数据,否则多余的数据就会被截断,再也无法找回。

    send数据的例子:

    Code: Select all
    char sendbuff[2048];
    int  nBytes = 2048,
         nLeft,
         idx;

    // Fill sendbuff with 2048 bytes of data

    // Assume s is a valid, connected stream socket
    nLeft = nBytes;
    idx = 0;

    while (nLeft > 0)
    {
        ret = send(s, &sendbuff[idx], nLeft, 0);
        if (ret == SOCKET_ERROR)
        {
            // Error
        }
        nLeft -= ret;
        idx += ret;
    }


    recv的例子:

    Code: Select all
    char    recvbuff[1024];
    int     ret,
            nLeft,
            idx;

    nLeft = 512;
    idx = 0;

    while (nLeft > 0)
    {
        ret = recv(s, &recvbuff[idx], nLeft, 0);
        if (ret == SOCKET_ERROR)
        {
            // Error
        }
        idx += ret;
        nLeft -= ret;
    }


    24. shutdown, closesocket

    Code: Select all
    int shutdown(
        SOCKET s,
        int how
    );


    shutdown的做法其实就是一种graceful关闭连接的方法。就好比我们在关闭连接的时候要通知对方一下一样(参考TCP states一节)。how这个参数有几种选择:SD_SEND, SD_RECEIVE, SD_BOTH。使用SD_SEND,表示我方程序不再发送数据了;使用SD_RECEIVE,表示我方不再接受数据了。使用SD_SEND调用 shutdown,功能类似WSASendDisconnect函数前半部分的工作(WSASendDisconnect接着还会发送 disconnect数据)。SD_RECEIVE类似WSARecvDisconnect

    shutdown函数只需要用于那些有连接的协议。对于UDP这样的协议,本身没有连接的概念,所以不需要调用shutdown,直接调用closesocket即可:

    int closesocket (SOCKET s);

    closesocket用来释放所有和该socket相关的资源。对于一个已经close的socket而言,调用closesocket,会产 生WSAENOTSOCK错误码。closesocket没有一个双方协商关闭连接的过程,这是shutdown做的,closesocket仅仅是释放 资源而已。不要和WSACleanup函数混淆,这个函数是用来卸载winsock的,和WSAStartup配对使用。

     

    25. 本节描述UDP的编程-Connectionless Communication. UDP简单多了,先来看server端的UDP程序。server端的UDP程序不需要调用listen和accept,直接socket,bind,然 后用recvfrom/WSARecvFrom即可。

    Code: Select all
    int recvfrom(
        SOCKET s,
        char FAR* buf,
        int len,
        int flags,
        struct sockaddr FAR* from,
        int FAR* fromlen
    );


    buf, len -- 接受缓冲区和缓冲区大小
    from, fromlen -- sockaddr结构和这个结构的大小。里面会由recvfrom来填写对方的sockaddr信息。
    flags -- 0, MSG_OOB, MSG_PEEK

    Code: Select all
    int WSARecvFrom(
        SOCKET s,
        LPWSABUF lpBuffers,
        DWORD dwBufferCount,
        LPDWORD lpNumberOfBytesRecvd,
        LPDWORD lpFlags,
        struct sockaddr FAR * lpFrom,
        LPINT lpFromlen,
        LPWSAOVERLAPPED lpOverlapped,
        LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );


    和WSARecv一样,不解释了。lpFlags可能取值0, MSG_OOB, MSG_PEEK, MSG_PARTIAL。含义都和WSARecv一样,本参数我们可以设定,WSARecvFrom函数也会修改这个参数。

    一 个比较好玩的事情:winsock支持在UDP程序中,调用connect,建立一个逻辑意义上的“连接”,其实这个连接是不存在的,调用了 connect之后,我们就可以用recv,WSARecv了,这样做的唯一好处就是如果我们只需要和单一的host通信,可以用这种方法,这样以后收发 数据就不需要填对方的IP地址,端口这些东西了。如果我们要取消connect中指定的host绑定,再调用一次connect,host参数设成 INETADDR_ANY即可。

    完整的receiver的程序:

    Code: Select all
    #include <winsock2.h>

    void main(void)
    {
       WSADATA              wsaData;
       SOCKET               ReceivingSocket;
       SOCKADDR_IN          ReceiverAddr;
       int                  Port = 5150;
       char                 ReceiveBuf[1024];
       int                  BufLength = 1024;
       SOCKADDR_IN          SenderAddr;
       int                  SenderAddrSize = sizeof(SenderAddr);
       
       // Initialize Winsock version 2.2

       WSAStartup(MAKEWORD(2,2), &wsaData);
       
          // Create a new socket to receive datagrams on.

          ReceivingSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

          // Set up a SOCKADDR_IN structure that will tell bind that we
          // want to receive datagrams from all interfaces using port
          // 5150.

          ReceiverAddr.sin_family = AF_INET;
          ReceiverAddr.sin_port = htons(Port);   
          ReceiverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

          // Associate the address information with the socket using bind.

          bind(ReceivingSocket, (SOCKADDR *)&SenderAddr, sizeof(SenderAddr));

       // At this point you can receive datagrams on your bound socket.
       recvfrom(ReceivingSocket, ReceiveBuf, BufLength, 0,
                (SOCKADDR *)&SenderAddr, &SenderAddrSize);

       // When your application is finished receiving datagrams close
       // the socket.

       closesocket(ReceivingSocket);

       // When your application is finished call WSACleanup.

       WSACleanup();
    }


    26. UPD Sender

    Code: Select all
    int sendto(
        SOCKET s,
        const char FAR * buf,
        int len,
        int flags,
        const struct sockaddr FAR * to,
        int tolen
    );

    int WSASendTo(
        SOCKET s,                                               
        LPWSABUF lpBuffers,                                     
        DWORD dwBufferCount,                                   
        LPDWORD lpNumberOfBytesSent,                           
        DWORD dwFlags,                                         
        const struct sockaddr FAR * lpTo,                       
        int iToLen,                                             
        LPWSAOVERLAPPED lpOverlapped,                           
        LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );


    不解释了,send的时候也可以调用connect哦,和receive一样。

    完整的sender程序:

    Code: Select all
    #include <winsock2.h>

    void main(void)
    {
       WSADATA              wsaData;
       SOCKET               SendingSocket;
       SOCKADDR_IN          ReceiverAddr;
       int                  Port = 5150;
       char                 SendBuf[1024];
       int                  BufLength = 1024;

       // Initialize Winsock version 2.2

       WSAStartup(MAKEWORD(2,2), &wsaData);
       
       // Create a new socket to receive datagrams on.

       SendingSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
       
       // Set up a SOCKADDR_IN structure that will identify who we
       // will send datagrams to. For demonstration purposes, let's
       // assume our receiver's IP address is 136.149.3.29 and waits
       // for datagrams on port 5150.

       ReceiverAddr.sin_family = AF_INET;
       ReceiverAddr.sin_port = htons(Port);   
       ReceiverAddr.sin_addr.s_addr = inet_addr("136.149.3.29");

       // Send a datagram to the receiver.

       sendto(SendingSocket, SendBuf, BufLength, 0,
              (SOCKADDR *)&ReceiverAddr, sizeof(RecieverAddr));
           
       // When your application is finished sending datagrams close
       // the socket.

       closesocket(SendingSocket);

       // When your application is finished call WSACleanup.

       WSACleanup();
    }


    27. 注意点。前面提过了,message-based, datagram-based的协议,在发送和接受数据的时候,需要注意buffer的大小。对于支持MSG_PARTIAL的协议,比如 AppleTalk,可以解决buffer不够大的问题,因为数据可以一部分一部分的取。

    另外一个常见问题:在UDP sender程序中,如果我们调用了bind,会有什么效果呢?这里书中解释了一下,调用了bind之后,会在UDP的数据包中将sender的IP地址 和端口设置成我们bind的那些数据,但是实际数据在发送时,winsock还是会根据路由表,把数据从正确的网络端口上发送出去。并不是我们调用了 bind,UDP包就会一定从我们指定的IP地址上发送出去,bind仅仅是修改了sender UDP包中的数据而已。

    联想到TCP编 程中,如果我们在client端程序的connect之前也调用了bind,会有什么效果呢?我估计效果是一样的,具体数据从哪个网卡出去,还是要根据路 由表来的,否则数据就有可能发送不到对方,bind应该是仅仅修改数据包中的source ip address/port而已。

    28. 其他的一些实用的API。

    Code: Select all
    int getpeername(
        SOCKET s,
        struct sockaddr FAR* name,
        int FAR* namelen
    );


    这个函数可以用来取到对方的sockaddr信息。对于TCP,没什么好说的,因为TCP本身是有连接的;对于UDP,如果我们的UDP程序中调用了connect方法,那么该函数返回connect方法中定义的sockaddr信息,否则,该函数什么都得不到。

    Code: Select all
    int getsockname(
        SOCKET s,
        struct sockaddr FAR* name,
        int FAR* namelen
    );


    这个函数的作用和getpeername相反,用来取得本地的sockaddr信息。只有server端的程序调用这个函数才有用(客户端不需要bind)

    Code: Select all
    int WSADuplicateSocket(
        SOCKET s,
        DWORD dwProcessId,
        LPWSAPROTOCOL_INFO lpProtocolInfo
    );


    这个函数用来复制一个socket,这个函数会填充WSAPROTOCOL_INFO结构,这个结构被填充后,可以用来传递给其他的进程,换句话 说,当多个进程需要同时用一个socket的时候,可以用这个函数复制socket,然后传递。对于线程,没有必要用这个函数,线程之间,可以直接传递 socket变量即可。

    要注意的是,进程中取道的socket是一个被复制的socket,对该socket的操作会影响到其他进程,比如,在这个socket上设置的 一些状态变量(通过setsockopt函数),会影响到其他进程中的socket,但是,进程对socket调用closesocket函数,只会影响 到该进程。也就是说,要等所有的进程都调用了closesocket之后,这个socket才会被真正析构。

    29. Windows CE上的winsock编程:

    (1) windows CE只支持winsock 1.1
    (2) windows CE只支持UNICODE,不过对于数据传送来说,无所谓了,因为网络传送都是以字节为操作对象的。
    (3) windows CE中没有console application的概念,所以程序中只能出现winmain函数,不能出现main函数
     

  • 相关阅读:
    [导入]C#播放rm文件[转贴]
    新工具软件发布!名称:剪切板记录器
    黑發4/14
    sql20002 4/19
    頁面按百分比設定失調3/27
    廣告控件:AdRotator 3/23
    19992020小農曆JS 3/31
    ASP.NET1.1編譯錯誤代碼:1073741502 4/10
    SiteMapPath控件 3/24
    MYSQL時間問題4/16
  • 原文地址:https://www.cnblogs.com/super119/p/2011319.html
Copyright © 2011-2022 走看看