使用 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\n", WSAGetLastError());
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }
        std::cout << "Byte sent: " << iSendResult << std::endl;
    } else if (iResult == 0)
        std::cout << "\nConnection 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:\n\t" << 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 << "\nBytes received: " << iResult << std::endl;
        std::cout << "String received:\n\t" << std::string(recvbuf, 0, iResult) << std::endl;
    }
    else if (iResult == 0)
        std::cout << "\nConnection closed" << std::endl;
    else
    {
        std::cout << "recv failed: " << WSAGetLastError() << std::endl;
    }
} while (iResult > 0);

5. 断开连接

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

closesocket(ConnectSocket);
WSACleanup();

最终代码以及运行效果

源代码地址:winsock-server-client

运行结果:

在这里插入图片描述

在这里插入图片描述

参考链接

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C++中,可以使用套接字(socket)来实现进程之间的通信。一个进程既可以作为服务器(server),也可以作为客户端(client),并且在同一进程中同时运行。 以下是一个简单的示例,展示了如何在同一进程中实现服务器和客户端: ```c++ #include <iostream> #include <string> #include <thread> #include <mutex> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") std::mutex mtx; void server() { // 创建套接字 SOCKET serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (serverSock == INVALID_SOCKET) { std::cerr << "创建套接字失败!" << std::endl; return; } // 绑定套接字 sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(12345); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(serverSock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { std::cerr << "绑定套接字失败!" << std::endl; closesocket(serverSock); return; } // 监听套接字 if (listen(serverSock, SOMAXCONN) == SOCKET_ERROR) { std::cerr << "监听套接字失败!" << std::endl; closesocket(serverSock); return; } // 等待客户端连接 std::cout << "等待客户端连接..." << std::endl; sockaddr_in clientAddr; int clientAddrLen = sizeof(clientAddr); SOCKET clientSock = accept(serverSock, (sockaddr*)&clientAddr, &clientAddrLen); if (clientSock == INVALID_SOCKET) { std::cerr << "接受连接失败!" << std::endl; closesocket(serverSock); return; } // 接收客户端消息 char recvBuf[1024] = { 0 }; while (true) { int ret = recv(clientSock, recvBuf, sizeof(recvBuf), 0); if (ret <= 0) { std::cerr << "客户端已断开连接!" << std::endl; break; } // 处理客户端消息 std::string msg = recvBuf; mtx.lock(); std::cout << "客户端说:" << msg << std::endl; mtx.unlock(); } // 关闭套接字 closesocket(clientSock); closesocket(serverSock); } void client() { // 创建套接字 SOCKET clientSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSock == INVALID_SOCKET) { std::cerr << "创建套接字失败!" << std::endl; return; } // 连接服务器 sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(12345); serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(clientSock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { std::cerr << "连接服务器失败!" << std::endl; closesocket(clientSock); return; } // 发送消息给服务器 std::string msg; while (true) { std::getline(std::cin, msg); if (msg.empty()) { break; } int ret = send(clientSock, msg.c_str(), msg.length(), 0); if (ret <= 0) { std::cerr << "发送消息失败!" << std::endl; break; } } // 关闭套接字 closesocket(clientSock); } int main() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { std::cerr << "初始化套接字库失败!" << std::endl; return -1; } // 在不同的线程中分别运行服务器和客户端 std::thread serverThread(server); std::thread clientThread(client); serverThread.join(); clientThread.join(); WSACleanup(); return 0; } ``` 在这个示例中,主函数中启动了两个线程,一个运行服务器,一个运行客户端。服务器和客户端都使用相同的套接字库,但是服务器使用 `socket()` 创建套接字,绑定地址和端口,监听连接请求,接受客户端连接,接收客户端消息,处理客户端消息,并且关闭套接字。客户端使用 `socket()` 创建套接字,连接服务器,发送消息给服务器,并且关闭套接字。在服务器中,使用互斥锁保护共享的输出流,避免输出混乱。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值