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 中的函数(如 socket
、bind
、connect
、send
、recv
等)的实现都包含在 ws2_32.lib
库中。
#pragma comment(lib, "ws2_32.lib")
是一个预处理器指令,用于在编译时指定链接器需要链接的库文件。
为什么导入 iostream
这种库时不需要类似语句
因为C++ 标准库(如 iostream
、vector
、string
等)通常是由编译器自动链接的。编译器在安装时会配置好标准库的路径和链接选项,当你使用标准库中的功能时,编译器会自动处理链接过程,无需手动指定链接库文件。
#include <winSock2.h>
#pragma comment(lib,"WS2_32")
有了Windows Sockets API 我们就可以来编写服务端与客户端程序了。
1.服务端(Server)
1.1初始化网络库
Q:为什么要初始化网络库?
A:因为Windows Sockets 网络功能是通过动态链接库(DLL)来实现的,WSAStartup
函数会对 Windows Sockets 库进行初始化操作,设置内部数据结构、分配必要的资源等。只有完成这些初始化步骤,后续的网络操作函数(如 socket
、bind
、connect
等)才能正常工作。
WSADATA wsaData;
这行代码的作用是定义一个WSADATA
类型的变量wsaData
。WSADATA
是一个结构体,在 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
变量的地址,serverAddr
是sockaddr_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
函数成功接受一个客户端连接时,会将客户端的地址信息填充到这个结构体中。 - 当
addr
为nullptr
时,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;