zoukankan      html  css  js  c++  java
  • 使用 winsock 实现简单的 Client 和 Server

    本篇文章将介绍如何使用 winsock 来实现 Client 客户端 和 Server 服务器应用程序。由于 Client 和 Server 的具体实现有所不同,所以本文将分成两部分来对 Client 和 Server 的实现进行讲解。

    运行环境

    • Windows 10
    • Visual Studio Community 2017 (version 15.9.17)

    Server 的实现

    Server 的运行步骤如下:

    1. 初始化 Winsock。(Initialize Winsock.)
    2. 创建一个socket。(Create a socket.)
    3. 绑定 socket。(Bind the socket.)
    4. 监听客户端的 socket。(Listen on the socket for a client.)
    5. 接受客户端的连接请求。(Accept a connection from a client.)
    6. 接收和发送数据。(Receive and send data.)
    7. 断开连接。(Disconnect.)

    创建 Winsock 应用

    打开 visual studio,创建一个空应用,并命名为 myserver

    在这里插入图片描述

    然后新建一个空的 c++ 源文件,并命名为 main.cpp

    在这里插入图片描述

    在这里插入图片描述

    然后确认编译环境中的包含目录,库目录和源码路径中包含 Microsoft Windows Software Development Kit (SDK)。由于在安装 Visual Studio的时候,已经安装了 Microsoft Windows SDK,所以这些已经默认包含到了相应的环境中。不需要再自己添加。

    在这里插入图片描述

    在这里插入图片描述

    确认依赖库文件 Ws2_32.lib 文件添加到依赖库中。在代码中可以使用 #pragma comment(lib, "Ws2_32.lib") 来进行链接。

    要想使用相关的 Winsock 库的函数,需要导入相应的头文件。

    Winsock2.h :包含大部分 Winsock 函数、结构体和定义。
    Ws2tcpip.h :包含 WinSock 2 协议特定的 TCP/IP 附件文档中引入的定义,其中包括用于检索 IP 地址的较新函数和结构体。

    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <stdio.h>
    
    #pragma comment(lib, "Ws2_32.lib")
    
    int main() {
      return 0;
    }
    

    1. 初始化 Winsock

    要使用 Winsock 相关的函数,必须先进行初始化。首先创建一个 WSADATA 对象。

    WSADATA wsaData;
    

    调用 WSAStartup() 并进行错误检查。MAKEWORD(2,2) 表示 Winsock 版本为 2.2。

    int iResult;
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        std::cout << "WSAStartup failed:" << iResult << std::endl;
        return 1;
    }
    

    2. 创建一个socket

    初始化后,必须实例化 SOCKET 对象以供服务器使用。

    这里会涉及到 addrinfo 结构体,这里将结构体源码拿出来看一下具体的结构。

    typedef struct addrinfo
    {
        int                 ai_flags;       // AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST
        int                 ai_family;      // PF_xxx
        int                 ai_socktype;    // SOCK_xxx
        int                 ai_protocol;    // 0 or IPPROTO_xxx for IPv4 and IPv6
        size_t              ai_addrlen;     // Length of ai_addr
        char *              ai_canonname;   // Canonical name for nodename
        _Field_size_bytes_(ai_addrlen) struct sockaddr *   ai_addr;        // Binary address
        struct addrinfo *   ai_next;        // Next structure in linked list
    }
    

    ai_familyAF_INET 表示 Ipv4, AF_INET6 表示 Ipv6。
    ai_socktypeSOCK_STREAM 表示 TCP 数据流传输方式, SOCK_DGRAM 表示 UDP 数据包传输方式。
    ai_protocol: IPPROTO_TCP 表示 TCP 协议, IPPROTO_UDP 表示 UDP 协议。

    关于各个参数的其他默认变量可以参考 ADDRINFOA structure

    addrinfo 结构体可以作为 getaddrinfo 函数的参数使用,获取相关的信息。

    #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;
    
    // Resolve the local address and port to be used by the server
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        std::cout << "getaddrinfo failed: " << iResult << std::endl;
        WSACleanup();
        return 1;
    }
    

    然后为服务器创建一个名为 ListenSocketSOCKET 对象,以监听客户端连接。

    SOCKET ListenSocket = INVALID_SOCKET;
    

    然后调用 socket 函数,将返回值赋给 SOCKET 对象并进行错误检查。

    // Create a SOCKET for the server to listen for client connections
    
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET) {
        std::cout << "Error at socket(): " << WSAGetLastError() << std::endl;
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }
    

    3. 绑定 socket

    为了使服务器接受客户端连接,必须将其绑定到系统内的网络地址。sockaddr 结构体保存地址族(address family),IP 地址和端口号的信息。调用 bind 函数,将从 getaddrinfo 函数返回的创建的套接字和 sockaddr 结构体作为参数传递,并进行错误检查。

    iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
        std::cout << "bind failed with error: " << WSAGetLastError() << std::endl;
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    

    调用 bind 函数后,就不再需要由 getaddrinfo 函数返回的地址信息。调用 freeaddrinfo 函数可释放由 getaddrinfo 函数为此地址信息分配的内存。

    freeaddrinfo(result);
    

    4. 监听客户端的 socket

    调用 listen 函数并进行错误检查。

    if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) {
        std::cout << "Listen failed: " << WSAGetLastError() << std::endl;
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    

    5. 接受客户端的连接请求

    创建一个名为 ClientSocket 的临时 SOCKET 对象,以接受来自客户端的连接。通常,服务器应用程序将被设计为监听来自多个客户端的连接。对于高性能服务器,通常使用多个线程来处理多个客户端连接。本实例中并没有考虑多个客户端请求。对于多客户端的解决方法可以参考 Accepting a Connection

    SOCKET ClientSocket;
    
    ClientSocket = INVALID_SOCKET;
    
    // Accept a client socket
    ClientSocket = accept(ListenSocket, NULL, NULL);
    if (ClientSocket == INVALID_SOCKET) {
        std::cout << "accept failed: " << WSAGetLastError() << std::endl;
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }
    

    6. 接收和发送数据

    通过调用 sendrecv 函数可以进行数据的发送和接收。

    #define DEFAULT_BUFLEN 512
    
    char recvbuf[DEFAULT_BUFLEN];
    int iResult, iSendResult;
    int recvbuflen = DEFAULT_BUFLEN;
    
    // Receive until the peer shuts down the connection
    do {
    
        iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0) {
            std::cout << "Byte received: " << iResult << std::endl;
    
            // Echo the buffer back to the sender
            iSendResult = send(ClientSocket, recvbuf, iResult, 0);
            if (iSendResult == SOCKET_ERROR) {
                printf("send failed: %d
    ", WSAGetLastError());
                closesocket(ClientSocket);
                WSACleanup();
                return 1;
            }
            std::cout << "Byte sent: " << iSendResult << std::endl;
        } else if (iResult == 0)
            std::cout << "
    Connection closing..." << std::endl;
        else {
            std::cout << "recv failed: " << WSAGetLastError() << std::endl;
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }
    
    } while (iResult > 0);
    
    1. 断开连接。(Disconnect.)

    服务器完成从客户端接收数据并将数据发送回客户端后,服务器将与客户端断开连接并关闭套接字。断开连接调用 shutdown 函数,关闭套接字调用 closesocket 函数。

    // shutdown the send half of the connection since no more data will be sent
    iResult = shutdown(ClientSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        std::cout << "shutdown failed:" << WSAGetLastError() << std::endl;
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }
    
    // cleanup
    closesocket(ClientSocket);
    WSACleanup();
    
    return 0;
    

    Client 的实现

    Client 的运行步骤如下:

    1. 初始化 Winsock。(Initialize Winsock.)
    2. 创建一个socket。(Create a socket.)
    3. 连接到服务器。(Connect to the server.)
    4. 发送和接收数据。(Send and receive data.)
    5. 断开连接。(Disconnect.)

    要实现 Client 需要重新创建一个应用程序,创建过程和服务器程序的创建过程相同,并且前两步都是相同的,但还是由点点区别,这里将只对不同的地方进行说明。具体的请参考文末的源代码链接。

    初始化 Winsock

    客户端要连接到服务器需要指定服务器的主机地址,因此在主函数中需要传递该参数。所以需要添加以下代码判断。

    if (argc != 2) {
        std::cout << "usage: " << argv[0] << "server-name" << std::endl;
        system("pause");
        return 1;
    }
    

    然后进行初始化

    // 1. Initialize Winsock
    WSADATA wsaData;
    int iResult;
    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        std::cout << "WSAStartup failed: " << iResult << std::endl;
        system("pause");
        return 1;
    }
    

    创建一个socket & 连接到服务器

    创建 socket 和连接与服务器相同,但是由于连接可能失败,因此这里需要使用循环来进行连接 getaddrinfo 返回的不同的结果,直到连接成功。

    // 2. Create a socket
    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;
    
    iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        std::cout << "getaddrinfo failed: " << iResult << std::endl;
        WSACleanup();
        system("pause");
        return 1;
    }
    
    SOCKET ConnectSocket = INVALID_SOCKET;
    
    // Attempt to connect to an address until one succeeds
    for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
    
        /* Create a SOCKET for connecting to server*/
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            std::cout << "Error at socket(): " << WSAGetLastError() << std::endl;
            freeaddrinfo(result);
            WSACleanup();
            system("pause");
            return 1;
        }
    
        // 3. Connect to the server
        iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }
    
    freeaddrinfo(result);
    
    if (ConnectSocket == INVALID_SOCKET) {
        std::cout << "Unable to connect to server!" << std::endl;
        WSACleanup();
        system("pause");
        return 1;
    }
    

    4. 发送和接收数据

    连接建立后,客户端首先是发送数据给客户端。

    int recvbuflen = DEFAULT_BUFLEN;
    
    const char *sendbuf = "Hello, I am client, can you receive my message?";
    char recvbuf[DEFAULT_BUFLEN];
    
    iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iResult == SOCKET_ERROR) {
        std::cout << "send failed: " << WSAGetLastError() << std::endl;
        closesocket(ConnectSocket);
        WSACleanup();
        system("pause");
        return 1;
    }
    
    std::cout << "Byte sent: " << iResult << std::endl;
    std::cout << "String sent:
    	" << sendbuf << std::endl;
    

    当客户端发送完数据之后,将断开发送的连接

    // shutdown the send half of the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        std::cout << "shutdown failed: " << WSAGetLastError() << std::endl;
        closesocket(ConnectSocket);
        WSACleanup();
        system("pause");
        return 1;
    }
    

    虽然断开了,但是客户端还可以接收服务器发来的数据。

    do {
        iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0) {
            std::cout << "
    Bytes received: " << iResult << std::endl;
            std::cout << "String received:
    	" << std::string(recvbuf, 0, iResult) << std::endl;
        }
        else if (iResult == 0)
            std::cout << "
    Connection closed" << std::endl;
        else
        {
            std::cout << "recv failed: " << WSAGetLastError() << std::endl;
        }
    } while (iResult > 0);
    

    5. 断开连接

    当服务器发送完了数据并断开连接后,客户端也需要断开连接,并关闭套接字。

    closesocket(ConnectSocket);
    WSACleanup();
    

    最终代码以及运行效果

    源代码地址:winsock-server-client

    运行结果:

    在这里插入图片描述

    在这里插入图片描述

    参考链接

  • 相关阅读:
    hdu2138(求素数)
    hdu2104
    poj1664(放苹果)
    数塔问题给你有哪些启示?
    汉诺塔问题(1)
    算法的力量(转李开复)
    最长子序列问题之系列一
    forward和redirect的区别
    group by 和having
    java中的多态三要素是什么?
  • 原文地址:https://www.cnblogs.com/busyboxs/p/12245376.html
Copyright © 2011-2022 走看看