Cpp网络编程Winsock API

发布于:2025-04-03 ⋅ 阅读:(22) ⋅ 点赞:(0)

Cpp网络编程Winsock API

作者:blue

时间:2025.3.31


本文是使用Winsock API进行网络编程的具体实例,最终完成一个C/S架构的TCP通信小demo。文章是对笔者自己学习过程的一个复盘。笔者是从大佬写的这篇文章中学习网络编程的Windows上的C++网络编程保姆级教学-CSDN博客

Q:如何使用Winsock API?

A:答案显而易见,导包,但这里的导包和我们平常直接 #include 还要多加一步。为什么呢?

因为在使用 Windows Sockets API 进行网络编程时,需要使用 ws2_32.lib 库,Windows Sockets API 中的函数(如 socketbindconnectsendrecv 等)的实现都包含在 ws2_32.lib 库中。

#pragma comment(lib, "ws2_32.lib") 是一个预处理器指令,用于在编译时指定链接器需要链接的库文件。

为什么导入 iostream 这种库时不需要类似语句

因为C++ 标准库(如 iostreamvectorstring 等)通常是由编译器自动链接的。编译器在安装时会配置好标准库的路径和链接选项,当你使用标准库中的功能时,编译器会自动处理链接过程,无需手动指定链接库文件。

#include <winSock2.h>
#pragma comment(lib,"WS2_32")

有了Windows Sockets API 我们就可以来编写服务端与客户端程序了。

1.服务端(Server)

1.1初始化网络库

Q:为什么要初始化网络库?

A:因为Windows Sockets 网络功能是通过动态链接库(DLL)来实现的,WSAStartup 函数会对 Windows Sockets 库进行初始化操作,设置内部数据结构、分配必要的资源等。只有完成这些初始化步骤,后续的网络操作函数(如 socketbindconnect 等)才能正常工作。

  • WSADATA wsaData;这行代码的作用是定义一个WSADATA类型的变量wsaDataWSADATA是一个结构体,在 Windows 套接字编程里,该结构体用于存储 Windows 套接字实现的相关信息,像版本号、描述信息等。
  • MAKEWORD(2,2)是一个宏,它的作用是创建一个 16 位的字,这个字的低字节代表主版本号,高字节代表副版本号。这里MAKEWORD(2,2)表示请求使用的 Windows 套接字版本是 2.2。
  • WSAStartup函数会把 Windows 套接字实现的信息填充到wsaData这个结构体里
//1.初始化网络库
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) {//等于0代表初始化成功
    std::cerr << "初始化网络库失败" << std::endl;
    return 1;
}

1.2创建套接字对象

//socket(协议族,套接字类型,协议)
协议族:AF_INET:代表 IPv4 协议族,AF_INET6:表示 IPv6 协议族

套接字类型:
    SOCK_STREAM:代表面向连接的流式套接字,基于 TCP
    SOCK_DGRAM:表示无连接的数据报套接字,基于 UDP

协议:
    通常情况下,如果 type 参数已经明确指定了套接字类型,protocol 可以设置为 0,让系统自动选择合适的协议。
//2.创建套接字对象
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM,0);
//检测是不是有效SOCKET对象
if (listenSocket == INVALID_SOCKET) {
    std::cerr << "SOCKET对象创建失败" << std::endl;
    WSACleanup();
    return 1;
}

1.3设置ip和端口

sockaddr和sockaddr_in(Socket Address Internet)。

用于存储网络地址信息,包括协议族类型、IP地址和端口号,其实就是两个结构体用来设置服务端和客户端的IP地址和端口号的,我们最常使用的是sockaddr_in,操作系统内部使用的则是sockaddr。

引用:原文链接:https://blog.csdn.net/weixin_74027669/article/details/138437305

//3.设置ip和端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;//设置协议簇
serverAddr.sin_port = htons(10086);//端口号
//serverAddr.sin_addr.s_addr = INADDR_ANY;
if (inet_pton(AF_INET, "127.0.0.1", &(serverAddr.sin_addr)) <= 0) {
    std::cerr << "无效地址" << std::endl;
    closesocket(listenSocket);
    WSACleanup();
    return 1;
}

serverAddr.sin_addr.s_addr = INADDR_ANY;

  • 功能INADDR_ANY 是一个预定义的常量,其值通常表示为 0.0.0.0。当你将 serverAddr.sin_addr.s_addr 设置为 INADDR_ANY 时,意味着服务器套接字会监听服务器上所有可用的网络接口。无论服务器有多少块网卡,每个网卡对应不同的 IP 地址,使用 INADDR_ANY 能让服务端程序接受来自任意网络接口的客户端连接请求。

inet_pton(AF_INET, "127.0.0.1", &(serverAddr.sin_addr))

  • 功能inet_pton 函数的作用是将点分十进制的 IP 地址字符串(如 "127.0.0.1")转换为适合存储在 in_addr 结构体中的二进制形式。这里将 "127.0.0.1" 转换后赋值给 serverAddr.sin_addr,意味着服务器套接字只会监听本地回环地址(127.0.0.1)对应的网络接口。本地回环地址通常用于在同一台计算机上进行网络通信测试,程序可以通过该地址与自身进行通信。

1.4将套接字对象与ip和端口绑定

int bind(SOCKET s, const struct sockaddr *name, int namelen);
//bind(套接字描述符,sockaddr结构体指针,结构体大小)
//4.将套接字对象与ip端口进行绑定
if (bind(listenSocket, reinterpret_cast<sockaddr*>(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR) {
    std::cerr << "套接字对象与ip端口绑定失败" << std::endl;
    closesocket(listenSocket);
    WSACleanup();
    return 1;
}

reinterpret_cast 是 C++ 中的一种强制类型转换运算符,它用于将一种类型的指针或引用转换为另一种不相关的类型。在这个表达式中:

  • &serverAddr 是获取 serverAddr 变量的地址,serverAddrsockaddr_in 类型的变量,用于存储 IPv4 地址和端口信息。
  • sockaddr* 是目标类型,即 sockaddr 结构体的指针类型。
  • reinterpret_cast<sockaddr*>(&serverAddr) 的作用是将 serverAddr 的地址从 sockaddr_in* 类型强制转换为 sockaddr* 类型。

1.5设置套接字为监听状态

int listen(SOCKET s, int backlog);
//listen(套接字描述符,等待连接的最大数量)
//5.设置套接字为监听状态
if (listen(listenSocket, 5) == SOCKET_ERROR) {
    std::cerr << "套接字设置监听状态失败" << std::endl;
    closesocket(listenSocket);
    WSACleanup();
    return 1;
}

1.6等待客户端连接

SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
//accept(套接字描述符,sockaddr结构体指针,结构体大小)

addr

  • 这是一个指向 sockaddr 结构体的指针,用于存储发起连接请求的客户端的地址信息,包括客户端的 IP 地址和端口号。不同的地址族(如 IPv4、IPv6)对应的具体结构体不同,常见的是 sockaddr_in(用于 IPv4)和 sockaddr_in6(用于 IPv6)。当 accept 函数成功接受一个客户端连接时,会将客户端的地址信息填充到这个结构体中。
  • addrnullptr 时,accept 函数不会尝试填充客户端的地址信息,也就不会使用 addrlen 参数。
//6.等待客户端连接
std::cout << "等待客户端连接" << std::endl;
SOCKET clientSocket = accept(listenSocket,nullptr,nullptr);
if (clientSocket == INVALID_SOCKET) {
    std::cerr << "获取到的客户端SOCKET对象无效" << std::endl;
    closesocket(listenSocket);
    WSACleanup();
    return 1;
}
std::cout << "连接成功" << std::endl;

1.7收发数据

//7.收发数据
while (true) {

    //接收
    char recvBuffer[1024]="";
    if (recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0) == SOCKET_ERROR) {
        std::cerr << "接收数据失败" << std::endl;
        closesocket(clientSocket);
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    std::cerr << "客户端发来信息:" << recvBuffer << std::endl;

    //发送
    std::string message = "hello client";
    if (send(clientSocket, message.c_str(), message.size(),0) == SOCKET_ERROR) {
        std::cerr << "向客户端发送信息失败" << std::endl;
        closesocket(clientSocket);
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
}

recv

int recv(
  SOCKET s,
  char   *buf,
  int    len,
  int    flags
);
  • s:参数为 clientSocket,它是一个已建立连接的套接字描述符。对于 TCP 连接而言,这个套接字是通过 accept 函数返回的用于和客户端进行通信的新套接字。服务器借助这个套接字接收客户端发送的数据。
  • buf:参数为 recvBuffer,这是一个长度为 1024 的字符数组,用来存储接收到的数据。在调用 recv 函数之前,将其初始化为空字符串 ""
  • len:参数为 sizeof(recvBuffer),也就是 recvBuffer 数组的大小,这里是 1024 字节。这表明 recv 函数最多能接收 1024 字节的数据到 recvBuffer 中。
  • flags:参数为 0,表示采用默认的接收方式。

send

int send(
  SOCKET s,
  const char *buf,
  int len,
  int flags
);
  • s:参数为 clientSocket,同样是用于和客户端通信的已建立连接的套接字描述符。服务器通过这个套接字向客户端发送数据。
  • buf:参数为 message.c_str()message 是一个 std::string 类型的对象,存储着要发送的消息 "hello client"c_str() 方法会返回一个指向以空字符结尾的 C 风格字符串的指针,该指针指向存储消息内容的内存地址。
  • len:参数为 message.size(),表示要发送的消息的长度,也就是 "hello client" 这个字符串的长度。
  • flags:参数为 0,表示采用默认的发送方式。

1.8关闭连接

WSACleanup()会释放Winsock库所占用的资源,并通知系统不再需要Winsock动态连接库函数。

//8.关闭连接
closesocket(clientSocket);
closesocket(listenSocket);
WSACleanup();

2.客户端(Client)

2.1初始化网络库

//1.初始化网络库
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
    std::cerr << "初始化网络库失败" << std::endl;
    return 1;
}

2.2创建套接字对象

//2.创建SOCKET对象
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);;
//检测是不是有效SOCKET对象
if (clientSocket == INVALID_SOCKET) {
    std::cerr << "SOCKET对象创建失败" << std::endl;
    WSACleanup();
    return 1;
}

2.3设置要连接的服务端的IP地址和端口号

注意是服务器的

//3.设置要连接的服务端的IP地址和端口号
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(10086);
if (inet_pton(AF_INET, "127.0.0.1", &(serverAddr.sin_addr)) <= 0) {
    std::cerr << "无效地址" << std::endl;
    closesocket(clientSocket);
    WSACleanup();
    return 1;
}

2.4连接服务端

//4.连接服务端
if (connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
    std::cerr << "连接服务器失败" << std::endl;
    closesocket(clientSocket);
    WSACleanup();
    return 1;
}
int connect(
  SOCKET         s,
  const sockaddr *name,
  int            namelen
);

参数解释

  • s:这是一个客户端套接字描述符,也就是通过 socket 函数创建的套接字。此套接字会被用于与服务器建立连接并进行后续的数据通信。在你给出的代码里,clientSocket 就是这个客户端套接字。
  • name:这是一个指向 sockaddr 结构体的指针,该结构体存储着服务器的地址信息,包含服务器的 IP 地址和端口号。由于 sockaddr 是通用的地址结构体,在实际使用时,通常会使用更具体的地址结构体(如 sockaddr_in 用于 IPv4 地址)来设置地址信息,然后再将其指针强制转换为 sockaddr* 类型。在你的代码中,(sockaddr*)&serverAddr 就是将 serverAddr(通常是 sockaddr_in 类型)的地址强制转换为 sockaddr* 类型,serverAddr 里存储着服务器的地址和端口信息。
  • namelen:这是 name 所指向的 sockaddr 结构体的长度,通常使用 sizeof(serverAddr) 来获取。这个参数告知 connect 函数地址结构体的大小。

返回值

  • connect 函数调用成功,会返回 0,表示客户端已成功与服务器建立连接。
  • 若调用失败,会返回 SOCKET_ERROR

2.5收发数据

//5.收发数据
while(true) {
    std::string message;
    std::cout << "输入你想发送的信息(输入quit退出):";
    std::getline(std::cin, message);
    if (message == "quit") break;

    if (send(clientSocket, message.c_str(), message.size(), 0) == SOCKET_ERROR) {
        std::cerr << "向服务端发送信息失败" << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }

    char recvBuffer[1024]="";
    if (recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0) == SOCKET_ERROR) {
        std::cerr << "接收数据失败" << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }
    std::cout << "服务端发来消息:" << recvBuffer << std::endl;
}

2.6关闭连接

//6.关闭连接
closesocket(clientSocket);
WSACleanup();
return 0;

网站公告

今日签到

点亮在社区的每一天
去签到