• Winsock 2 入门指南


    Winsock 2 入门指南 (翻译自MSDN+CBuilder2010文档)
    Translated by Xana Hopper
    Blog: http://blog.xanahopper.com
    Email: xanahopper@21cn.com
    一下是Windows Sockets编程的入门指南,提供帮助理解最基础的Winsock函数和数据结构,以及它们是如何一起运作。
    零、关于服务器和客户端
    有两种截然不同网络应用程序:服务器(Servers)和客户端(Clients)。服务器和客户端有着不同的行为,因此建立它们的过程也是
    不一样的,下面是建立一个TCP/IP服务器和客户端的一般模型。
    服务器
    1. 初始化Winsock
    2. 建立一个套接字(Socket)
    3. 绑定Socket
    4. 在Socket上为客户端监听
    5. 接受(Accept)一个来自客户端的连接
    6. 接收和发送数据
    7. 断开连接
    客户端
    1. 初始化Winsock
    2. 建立一个Socket
    3. 连接到服务器
    4. 发送和接收数据
    5. 断开连接
    注意:对于二者来说有几步是一样的,这几步实现起来几乎完全一样,指南中这几步会具体的根据所建立程序的类型说明的。
    一、建立一个基础的 Winsock程序
    为建立一个基础的 Winsock程序
    1. 建立一个空的工程
    2. 添加一个空的C++源文件
    3. 确保构建环境对微软SDK的Lib、Include和Src的文件夹正确引用
    4. 确保构建环境的连接器依赖项包含了WS2_32.lib,使用Winsock的程序必须连接此文件
    5. 开始进行Winsock编程,通过包含Winsock2.h来使用Winsock的API。(Winsock2.h头文件已经包含了大部分Winsock函数、
    数据结构和定义,Ws2tcpip.h头文件包含了在Winsock2协议兼容文档中为TCP/IP用于检索IP地址的新函数和数据结构。Xana:
    括号中内容为MSDN独有,C++ Builder 2010 Document中未给与说明,不知是CB更新及时库文件做得好还是什么其他原因)
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <stdio.h>
    int main() {
      return 0;
    }
    (Xana:以下还是MSDN独有,看来MS的独家历史问题不少啊)
    注意
    如果需要使用IP Helper APIs的话需要引用Iphlpapi.h头文件,当Iphlpapi.h被引用的时候,引用Winsock2.h的#include引用行应
    置于Iphlpapi.h行之上。Winsock2.h头文件已经内在的引用了 Windows.h的核心元素,所以通常在 Winsock程序中不包含 Windows.h文件。如果需要包含
    Windows.h头文件,应定义#define WIN32_LEAN_AND_MEAN宏。因为一些历史原因,Windows.h包含有1.1版的Winsock.h头文件,所
    以Winsock.h和Winsock2.h会因为定义而冲突。WIN32_LEAN_AND_MEAN宏阻止了Winsock.h被Windows.h包含进来。下面是例子:
    #ifndef WIN32_LEAN_AND_MEAN
    #define WIN32_LEAN_AND_MEAN
    #endif
    #include <windows.h>
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <iphlpapi.h>
    #include <stdio.h>
    int main() {
      return 0;
    }
    二、初始化 Winsock
    所有的进程(程序和 DLLs)在使用Winsock函数之前必须用一些函数来使用 Windows Sockets DLL,这也需要弄清楚Winsock在系
    统上是被支持的。(CB版:所有的Winsock程序必须初始化来确保Windows socket被系统支持。)
    初始化Winsock
    1. 建立一个WSADATA对象,叫做wsaData
    WSADATA wsaData;
    2. 调用WSAStartup,并返回一个整数值,使用它来查错
    int iResult;
    //初始化Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf(“WSAStartup 失败:%d ”, iResult);
        return 1;
    }
    WSAStartup函数被调用来使用WS2_32.lib
    WSADATA结构体包含着有关 Winsock的实现信息,参数MAKEWORD(2,2)向系统声明使用2.2版本的Winsock,从而使得高版本的
    Winsock可以使用。
    三、建立一个 Socket
    MSDN和C++Builder的文档在这里就分道扬镳了,虽然内容基本一样,但结构不同。以下以MSDN为主。
    (一)客户端版
    初始化之后,一个SOCKET对象必须被客户端实例化
    建立一个 Socket
    1. 声明一个addrinfo对象,它包含着一个 sockaddr结构,并且初始化这些值。在这个程序中,互联网地址族(Internet address
    family)没有明确的指出返回IPv6还是IPv4。程序请求Socket的类型是一个TCP协议 流套接字(stream socket)。
    #define DEFAULT_PORT "27015"
    struct addrinfo *result = NULL,
                    *ptr = NULL,
                    hints;
    ZeroMemory( &hints, sizeof(hints) );hints.ai_family = AF_UNSPEC;  //未指明返回类型
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    2. 调用getaddrinfo函数获取从命令行传输来的服务器名字的IP地址,getaddrinfo函数返回一个整数用来查错
    //分析服务器地址和端口
    iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo 失败: %d ", iResult);
        WSACleanup();
        return 1;
    }
    3. 建立一个叫ConnectSocket的SOCKET对象
    SOCKET ConnectSocket = INVALID_SOCKET;
    4. 调用socket函数,将返回值赋予ConnectSocket。在本程序中,使用通过调用getaddrinfo得到的hints参数中的第一个IP地址,
    hints中包含了地址族、套接字类型和指定的协议类型。在本例中 TCP 流套接字被指定为 SOCK_STREAM,协议类型被指定为
    IPPROTO_TCP。地址族是未指定的(AF_UNSPEC),所以服务器的返回值可能是IPv4或IPv6。
    如果客户端想只使用IPv6或IPv4,hints参数中的地址族应设置为AF_INET6或AF_INET。
    //试图用调用getaddrinfo返回的第一个地址连接
    ptr = result;
    //建立一个SOCKET来连接服务器
    ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
    5. 检查错误确保这个socket是有效的
    if (ConnectSocket == INVALID_SOCKET) {
        printf(“调用socket时发生错误:%d ”, WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }
    socket函数的参数可以根据不同的需要而改变。
    检错是一个成功的网络程序的关键,如果socket函数调用失败,它会返回INVALID_SOCKET,所以上面的if语句被用来抓住在建立
    套接字时任何可能发生的错误。WSAGetLastError返回最后发生的错误的代码。
    注意 大量的检错取决于你的程序
    WSACleanup用于停止WS2_32.DLL的使用
    (二)服务器版
    初始化后,套接字必须被服务器实例化
    为服务器建立一个 socket
    1.  getaddrinfo函数用来确定sockaddr结构中的值:
    ? AF_INET表示IPv4地址族
    ? SOCK_STREAM表示一个流套接字(stream socket)
    ? IPPROTO_TCP表示使用TCP协议
    ? AI_PASSIVE 标志指出调用者将要在 bind 函数中使用返回的套接字地址结构体。当 AI_PASSIVE 标志被设置,而
    getaddrinfo函数的参数nodename是NULL指针的时候,如果是IPv4地址,套接字的IP地址部分将被设置成INADDR_ANY,
    而IPv6将会是IN6ADDR_ANY_INIT。
    ? 27015是服务器将要和客户端连接的端口
    #define DEFAULT_PORT "27015"
    struct addrinfo *result = NULL,
                  *ptr = NULL,
                  Hints;ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;
    //分析在服务器已使用的本地地址和端口
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
    printf(“getaddrinfo调用失败:%d ”, iResult);
    WSAClearnup();
    return 1;
    }
    2. 为服务器端建立一个名为ListenSocket的SOCKET对象用来监听客户端的连接
    SOCKET ListenSocket = INVALID_SOCKET;
    3. 调用socket函数并将返回值赋予ListenSocket。对于这个服务器程序使用第一个由getaddrinfo返回IP地址,其中参数hints包
    含了地址族、连接类型和协议类型。在本例中一个IPv4的TCP流套接字被建立,类型是SOCK_STREAM,协议是IPPROTO_TCP。
    所以ListenSocket返回IPv4地址。
    如果服务器程序想要使用IPv6监听,参数hints中地址族应该设为AF_INET6,如果服务器想要同时监听IPv4和IPv6,就必须建
    立两个socket,一个监听IPv4,一个监听IPv6。两个套接字必须被程序独立分开处理。
    Windows Vista以及其后版本提供了双重方法来建立一个单独的IPv6套接字来同时监听Ipv4和IPv6。有关此特性的更多信息,参
    看MSDN上“Dual-Stack Sockets”。
    //在服务器建立一个socket用来监听客户端连接
    LisitenSocket = socket( result->ai_family, result->ai_socktype, result->ai_protocol);
    4. 检错确保此套接字是有效的
    if (ListenSocket == INVLID_SOCKET) {
    printf(“Error at socket(): %ld ”, WSAGetLastError());
    freeaddrinfo(result);
    WSACleanup();
    return 1;
    }
    四、数据交换前的行为
    (一)客户端版:连接 Socket
    为了客户端能够通过网络交流,它必须连接到一个服务器。
    连接到 Socket
    调用connect函数,将已建立的socket和一个sockaddr结构体作为参数,然后检错。
    //连接到服务器
    iResult = connect( ConnectSocket, ptr->ai_addr, (int)pter->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
    closesocket(ConnectSocket);
    ConnectSocket = INVALID_SOCKET;
    }
    //如果连接失败,将会尝试getaddrinfo返回的下一个地址
    //但在这个简单的例子中,我们只是释放由getaddrinfo返回的资源并且打印错误信息
    freeaddrinfo(result);
    if (ConnectSocket == INVALID_SOCKET) {
    printf(“无法连接到服务器! ”);WSACleanup();
    return 1;
    }
    函数getaddrinfo用来确定socketaddr结构中的值,在本例中由 getaddrinfo返回的第一个IP地址被用来表述传递到 connect函数的
    sockaddr结构体,如果connect调用第一个IP地址失败,尝试由getaddrinfo返回的链表的下一个addrinfo结构体。
    在sockaddr结构体中描述的信息包括:
    ? 客户端尝试连接的服务器的IP地址
    ? 客户端将要连接的服务器端口,像上面的27015就是客户端调用getaddrinfo函数时的端口
    (二)服务器端版:绑定 Socket、监听端口、接受连接
    为了服务器能够接受来自客户端的连接,它一定要有一个网络地址。下面的代码展示了如何绑定一个已经由IP地址和端口创建的套接
    字,客户端通过IP地址和端口来连接到主机网络。
    绑定一个 Socket
    调用bind函数,将已经建立的socket和从getaddrinfo函数返回的sockaddr作为参数传入,然后检错。
    //设置TCP监听套接字
    iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
    printf(“绑定失败:%d ”, WSAGetLastError());
    freeaddrinfo(result);
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
    }
    监听一个 Socket
    在绑定了Socket之后,服务器一定要为来到的客户端请求监听IP地址和端口。
    调用listen函数,将建立好的ListenSocket和backlog的值——表示待定的需要接受的连接队列的最大数,作为参数传入。在本例中,
    参数backlog被设为SOMAXCONN。这个特别的常数告诉Winsock提供给这个socket允许的最大待连接队列数。然后检错。
    if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) {
    printf(“监听错误:%d ”, WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
    }
    接受连接
    一旦套接字开始监听连接,程序就必须处理套接字上的连接请求
    接受套接字的连接
    1. 为接受来自客户端的连接建立一个临时的SOCKET对象叫做ClientSocket
    SOCKET ClientSocket;
    2. 通常服务器程序会通过listen函数建立一个持续的循环来检测连接请求。如果发生了连接请求,调用accept函数处理这个连接。
    注意在这个基础例子中,代码只接受一个连接。
    ClientSocket = INVALID_SOCKET;
    //接受一个客户端套接字
    ClientSocket = accept(ListenSocket, NULL, NULL);
    if ( ClientSocket == INVALID_SOCKET) {
    printf(“接受连接失败:%d ”, WSAGetLastError());
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
    }
    3. 当客户端连接被接受之后,服务器通常将接受的连接转移到另一个工作的线程或I/O工作端口,并继续接受其他连接。在这个基础例子中,服务器将会直接进行下一步。
    五、数据交换
    (一)客户端版:发送和接收数据
    下面的代码展示了一旦连接完成,客户端如何使用send和recv函数。
    #define DEFAULT_BUFF 512
    char *sendbuf = “this is a test”;
    char recvbuf[DEFAULT_BUFF];
    int recvbuflen = DEFAULT_BUFF;
    int iResult;
    //发送数据
    iResult = send ( ConnectSocket, sendbuff, (int)strlen(sendbuff) );
    if ( iResult == SOCKET_ERROR ) {
        printf(“Send error: %d ”, WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }
    printf(“已经发送:%ld ”, iResult);
    //一旦没有更多数据需要发送,关闭发送连接,客户端仍然可以用ClientSocket来接收数据
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf(“关闭发送连接失败:%d ” WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }
    //接受数据,知道服务器关闭连接
    do {
    iResult = recv(ConnectSocket, recvbuff, recvbuflen, 0);
        if ( iResult > 0 )
            printf(“接收到字节:%d ”, iResult);
        else if ( iResult == 0 )
            printf(“连接关闭 ”);
        else
            printf(“recv失败:%d ”, WSAGetLastError());
    } while ( iResult > 0 );
    函数send和recv的返回值已发送或已接受的字节数目值,或不同的错误。两个函数都有相同的参数:活动的socket,一个char类型
    的缓冲区,发送或接受的字节长度,或任何可以使用的标志。
    (Xana:如果你还不知道数据接收回来放在那里或者要发送的数据放在那里……那么你确实不适合看这个指南)
  • 相关阅读:
    $prufer$序列
    倍增
    二分
    英语词汇速查表
    ACM模拟赛
    Trie树
    关于军训的模拟赛-R2
    树上差分
    列队
    斜率优化dp
  • 原文地址:https://www.cnblogs.com/byfei/p/14104433.html
Copyright © 2020-2023  润新知