【Windows】 C++实现 Socket 通讯
一:头文件与套接字实例
(1)Windows 系统下所需头文件 : #include<WinSock2.h >
(2)我们使用 SOCKET 来作为套接字的实例:通过查看源码得知其是一个无符号 64 位的整形,如下:
cpp
typedef unsigned __int64 UINT_PTR, *PUINT_PTR;
typedef UINT_PTR SOCKET;
二:常用方法
(1)WSAStartup :
WSAStartup 函数通过进程启动对 Winsock DLL 的使用,即打开网络库/启动网络库,启动了这个库,这个库里的函数/功能才能使用。
cpp
int WSAAPI WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
参数:
WORD wVersionRequested:
cpp
typedef unsigned short WORD;//无符号短整型;
表示调用者可以使用的 Windows 套接字规范的最高版本。 高位字节指定次要版本号; 低位字节指定主要版本号。
可以使用 MAKEWORD 函数来创建该参数:
MAKEWORD(lowbyte, highbyte):低位字节和高位字节参数指定主要和次要版本号
LPWSADATA lpWSAData:
表示指向 WSADATA 数据结构的指针,该数据结构将接收Windows套接字实现的详细信息。定义时需定义一个 WSADATA 结构体对象,并将其指针传递给该参数
返回值:
成功返回零,否则为错误
【注意 】
①WSAStartup 函数必须是应用程序或 DLL 调用的第一个 Windows 套接字函数 。 它允许应用程序或 DLL 指定所需的 Windows 套接字版本,并检索特定 Windows 套接字实现的详细信息。 应用程序或 DLL 只能在成功调用 WSAStartup 后才能使用更多 Windows 套接字函数。
②当应用程序或 DLL 调用 WSAStartup 函数时,Winsock DLL 检查在 wVersionRequested 参数中传递的应用程序请求的 Windows 套接字规范的版本。 如果应用程序请求的版本等于或高于 Winsock DLL 支持的最低版本,则调用成功,并且 Winsock DLL 将在 lpWSAData 参数指向的 WSADATA 结构中返回详细信息。 WSADATA 结构的 wHighVersion 成员指示 Winsock DLL 支持的 Windows 套接字规范的最高版本。 WSADATA 结构的 wVersion 成员指示 Winsock DLL 希望调用方使用的 Windows 套接字规范的版本。
③如果调用方无法接受 WSADATA 结构的 wVersion 成员,则应用程序或 DLL 应调用 WSACleanup 来释放 Winsock DLL 资源,并且无法初始化 Winsock 应用程序。 为了支持此应用程序或 DLL,需要搜索要安装在平台上的 Winsock DLL 的更新版本。
当前是 2024/9/25日,Windows 套接字规范的当前版本为版本 2.2。 当前的 Winsock DLL 【Ws2_32.dll】支持请求以下任一版本的 Windows 套接字规范的应用程序:1.0;1.1;2.0;2.1;2.2,具体对应的版本可以去查询。
④这里需要注意的是最开始必须要引入到 Ws2_32.dll 库,所以需要调用 #pragma comment(lib,"ws2_32.lib") 方式来调用到 Ws2_32.dll 库,在 WSAStartup 初始化的时候分配对应资源,然后在结束的时候使用 WSAClearup 来释放掉;
cpp
#pragma comment(lib, "XXX.lib")//是visual studio中使用的,
#pragma comment ( lib,"wpcap.lib" )
//是导入1个库文件,以使程序可以调用相应的动态链接库。
示例代码:
cpp
#pragma comment(lib, "ws2_32.lib")//添加动态连接库的调用
//--------------------------------
WORD wVersionRequested = MAKEWORD(2, 2);//指定支持的 windows套接字规范的版本
WSADATA wsadata;
int init_res = WSAStartup(wVersionRequested, &wsadata);
if (init_res != 0)
{
std::cout << "WSA Start Failed" << std::endl;
return -1;
}
else {
std::cout << "WSA Start success" << std::endl;
}
(2)WSACleanup:
WSACleanup 函数终止使用 Winsock 2 DLL (Ws2_32.dll) ,即释放 WSAStartup 初始化时调用 Ws2_32.dll 库所分配的资源,一般在程序结束时使用。
示例代码:
cpp
WSACleanup();//释放动态链接库 ws2_32.dll 初始化时多分配的空间
(3)socket:
socket 函数创建绑定到特定传输服务提供程序的套接字
cpp
SOCKET WSAAPI socket(
[in] int af,
[in] int type,
[in] int protocol
);
参数:
af: 指定地址族:
AF_UNSPEC 0 地址系列未指定
AF_INET 2 IPv4
AF_INET6 23 IPv6
...
type: 指定Socket类型:
SOCK_STREAM 1 一种套接字类型,它通过 OOB 数据传输机制提供排序的、可靠的双向、基于连接的字节流。 此套接字类型对 Internet 地址系列 (AF_INET 或AF_INET6) 使用传输控制协议 (TCP) 。
SOCK_DGRAM 2 支持数据报的套接字类型,这些数据报是固定 (通常较小) 最大长度的无连接、不可靠的缓冲区。 此套接字类型对 Internet 地址系列 (AF_INET 或AF_INET6) 使用用户数据报协议 (UDP) 。
SOCK_RAW 3 一种套接字类型,它提供允许应用程序操作下一层协议标头的原始套接字。 若要操作 IPv4 标头,必须在套接字上设置 IP_HDRINCL 套接字选项。 若要操作 IPv6 标头,必须在套接字上设置 IPV6_HDRINCL 套接字选项。
SOCK_RDM 4 提供可靠消息数据报的套接字类型。 此类型的一个示例是 Windows 中的实用常规多播 (PGM) 多播协议实现,通常称为 可靠多播编程。
SOCK_SEQPACKET 5 提供基于数据报的伪流数据包的套接字类型。
注意:在 Windows 套接字 1.1 中,只有 SOCK_DGRAM 和 SOCK_STREAM 这两种套接字类型,而在 套接字 2.0,才添加了其他套接字类型。
protocol: 是与特定的地址家族相关的协议,如果指定为0,那么系统就会根据地址格式和套接类别,自动选择一个合适的协议
IPPROTO_ICMP 1 Internet 控制消息协议 (ICMP)
IPPROTO_IGMP 2 Internet 组管理协议 (IGMP)
BTHPROTO_RFCOMM 3 蓝牙射频通信 (蓝牙 RFCOMM) 协议。 当 af 参数AF_BTH且类型参数SOCK_STREAM时,这是一个可能的值。
IPPROTO_TCP 6 传输控制协议 (TCP) 。 当 af 参数AF_INET或AF_INET6且类型参数 SOCK_STREAM时,这是一个可能的值。
IPPROTO_UDP 17 用户数据报协议 (UDP) 。 当 af 参数AF_INET或AF_INET6且类型参数SOCK_DGRAM时,这是一个可能的值。
IPPROTO_ICMPV6 58 Internet 控制消息协议版本 6 (ICMPv6)
IPPROTO_RM 113 可靠多播的 PGM 协议
返回值:
如果等于 INVALID_SOCKET,则表示创建不成功
示例代码:
cpp
SOCKET SocketServer;//声明一个 Socket 套接字对象
SocketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SocketServer != INVALID_SOCKET)//判断是否创建成功
{
}
(3)bind:
bind 函数将本地地址与套接字相关联。
cpp
int WSAAPI bind(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
参数:
s: 标识未绑定套接字的描述符,定义的套接字。
name: 指向要绑定给 s 的协议地址,即 IP 端口号等,创建一个 sockaddr_in 结构体对象,在这里指定地址族、IP 还有端口号等,然后强转成 const sockaddr * 赋值给该参数,其定义如下:
cpp
struct sockaddr_in addr;
addr.sin_family = AF_INET;//一般与使用 socket() 函数创建 socket 对象时使用的地址族一样,如下:
addr.sin_port = htons(13111);//设置端口号
inet_pton(AF_INET, "127.0.0.1", &(addr.sin_addr));
其中涉及函数解析:
cpp
u_short WSAAPI htons(
[in] u_short hostshort
);
htons 函数将主机u_short转换为 TCP/IP 网络字节顺序 (大端) 。
参数:[in] hostshort 主机字节顺序为 16 位数字。
返回值:htons 函数以 TCP/IP 网络字节顺序返回值。
作用:
htons 函数采用主机字节顺序为 16 位数字,并返回 TCP/IP 网络 (AF_INET或AF_INET6地址系列) 中使用的 16 位数字。
htons 函数可用于将主机字节顺序中的 IP 端口号转换为网络字节顺序的 IP 端口号。
htons 函数不要求 Winsock DLL 之前已通过对 WSAStartup 函数的成功调用加载。
inet_pton()、InetPton()之类的新函数,用于IP地址在"点分十进制"和"二进制整数"之间转换,并且能够处理ipv4和ipv6。
namelen : name 参数指向的值的长度(以字节为单位),即对应的是地址的长度。
返回值 :
如果未发生错误, 绑定 将返回零。 否则,它将返回SOCKET_ERROR
示例代码:
cpp
struct sockaddr_in addr;
addr.sin_family = AF_INET;//一般与使用 socket() 函数创建 socket 对象时使用的地址族一样
addr.sin_port = htons(13111);//设置端口号
inet_pton(AF_INET, "127.0.0.1", &(addr.sin_addr));
int addr_len = sizeof(addr);//地址信息的长度
int res = bind(SocketServer, (sockaddr*)&addr, addr_len);
if (res == 0)//返回 0 则代表绑定成功
{
}
(4)listen:
listen 函数 (winsock2.h) 让套接字进入侦听状态
cpp
int WSAAPI listen(
[in] SOCKET s,
[in] int backlog
);
参数:
s 需要进入监听状态的套接字
backlog 请求队列的最大长度
侦听状态 :是指当没有客户端请求时,套接字处于"睡眠"状态,只有当接收到客户端请求时,套接字才会被"唤醒"来响应请求。
请求队列 :当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
缓冲区的长度: (能存放多少个客户端请求)可通过 backlog 参数指定,如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,一般来说这个长度会比较大。
【注意】
当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。
listen() 只是让套接字处于侦听状态,并没有接收请求。接收请求需要使用 accept() 函数。
示例代码:
cpp
listen(SocketServer, max_listen);
(5)accept:
accept 函数 (winsock2.h) 当套接字处于侦听状态,可用 accept 函数接受客户端的请求
cpp
SOCKET WSAAPI accept(
[in] SOCKET s,
[out] sockaddr *addr,
[in, out] int *addrlen
);
参数:
s 一个描述符,使用已使用 listen 函数置于侦听状态的套接字。 与客户端连接实际上是使用 accept 返回的套接字建立的。
addr 指向接收连接实体地址的缓冲区的可选指针,该地址称为通信层。 addr 参数的确切格式由创建 sockaddr 结构中的套接字时建立的地址系列确定。
addrlen 指向包含 addr 参数指向的结构长度的整数的可选指针,即 addr 的长度。
返回值:
如果未发生错误, 则 accept 将返回 类型为 SOCKET 的值,该值是新套接字的描述符。 此返回值是建立实际连接的套接字的句柄。否则,将返回 值 INVALID_SOCKET ,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
【注意 】
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号(addr 属于 out,只需要声明以下,然后当接收到客户端时,会将客户端的IP地址和端口号存到里面)
当没有客户端连接过来时,accept 函数会处于堵塞状态,所以可以使用多线程去处理
示例代码:
cpp
sockaddr_in addr_client;
int addr_client_len = sizeof(addr_client);
SOCKET socketClient = accept(SocketServer, (sockaddr*)&addr_client, &addr_client_len);
(6)send:
send 函数 (winsock2.h) send 函数在连接的套接字上发送数据。
cpp
int WSAAPI send(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags
);
参数:
[in] s 发送端套接字描述符。
[in] buf 指向包含要传输的数据的缓冲区的指针,即需要发送的数据的缓冲区。
[in] len buf参数指向的缓冲区中数据的长度(以字节为单位),需要发送数据的字节长度。
[in] flags 一组指定调用方式的标志。 此参数是使用以下任一值的按位 OR 运算符构造的,一般为 0。
返回值:
如果未发生错误, send 将返回发送的总字节数,该字节数可能小于 在 len 参数中请求发送的字节数。 否则,将返回值 SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
示例代码:
cpp
char buf_send[1024];
send(soc, buf_send, sizeof(buf_send), 0);
(7)recv:
recv 函数 (winsock2.h)
cpp
int WSAAPI recv(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags
);
参数
[in] s 标识连接的套接字的描述符。即接收端套接字描述符
[out] *buf 指向用于接收传入数据的缓冲区的指针,即指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
[in] len buf 参数指向的缓冲区的长度,即buf的长度
[in] flags 影响此函数行为的一组标志,一般置为0
返回值:
如果未发生错误, recv 将返回收到的字节数, buf 参数指向的缓冲区将包含接收的此数据。 如果连接已正常关闭,则返回值为零。
【注意】默认情况下,如果 recv 没有接收到数据,它会处于堵塞状态,知道接收到下一个数据
示例代码:
cpp
char buf_rec[1024];
int res = recv(soc, buf_rec, sizeof(buf_rec), 0);
if (res > 0)
{
}
else {
}
(8)connect:
connect 函数 (winsock2.h) connect 函数与指定的服务端套接字建立连接。
cpp
int WSAAPI connect(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
参数:
s 标识未连接的套接字的描述符。需要连接 socket 服务端的客户端
name 指向应建立连接的 sockaddr 结构的指针,需要被连接的 socket 服务端的信息
namelen name 参数指向的 sockaddr 结构的长度(以字节为单位)。需要被连接的 socket 服务端的信息的大小
返回值:
如果未发生错误, 返回零。 否则返回SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
示例代码:
cpp
sockaddr_in server_addr;
server_addr.sin_port = htons(13111);
server_addr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &(server_addr.sin_addr));
int res = connect(SocketClient, (sockaddr*)&server_addr, sizeof(server_addr));
if (res == 0)//代表连接成功
{
}
(9)closesocket:
closesocket 函数 (winsock2.h) closesocket 函数关闭现有套接字。
cpp
int WSAAPI closesocket(
[in] SOCKET s
);
参数:
s 标识要关闭的套接字的描述符。
返回值:
如果未发生错误, 则 closesocket 返回零。 否则,将返回 值 SOCKET_ERROR ,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
示例代码:
cpp
closesocket(SocketServer);
三:服务端整体代码展示
cpp
#include<WinSock2.h>
#include<iostream>
#include <WS2tcpip.h>
#include<thread>
#include<mutex>
#pragma comment(lib,"ws2_32.lib") //链接库文件
std::mutex mux;
std::unique_lock<std::mutex> lock(mux);
bool is_used = true;
/// <summary>
/// 开启线程进行通讯
/// </summary>
/// <param name="soc"></param>
void ServerCom(SOCKET& soc) {
char buf_rec[1024];
char buf_send[1024];
std::string msg;
while (is_used) {
int res = recv(soc, buf_rec, sizeof(buf_rec), 0);
if (res > 0)
{
std::cout << buf_rec << std::endl;
memset(buf_send, 0, sizeof(buf_send));
//赋值
std::cin >> msg;
memcpy(buf_send, msg.c_str(), sizeof(buf_send));
send(soc, buf_send, sizeof(buf_send), 0);
}
else {
std::cout << "receive fail" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}
//创建 socket 对象
int main(int argc, char* argv[]) {
//① 初始化动态链接库 ws2_32.dll,分配资源
WORD wVersionRequested = MAKEWORD(2, 2);//指定支持的 windows套接字规范的版本
WSADATA wsadata;
int init_res = WSAStartup(wVersionRequested, &wsadata);
if (init_res != 0)
{
std::cout << "WSA Start Failed" << std::endl;
return -1;
}
else {
std::cout << "WSA Start success" << std::endl;
}
//②创建 socket 服务端
SOCKET SocketServer;//声明一个 Socket 套接字对象
constexpr int max_listen = 20;//设置最大连接数
SOCKET socket_client[max_listen];//创建一个客户端组
int index = 0;//当前为第几个连接的客户端
std::thread server_th[max_listen];
SocketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SocketServer != INVALID_SOCKET)//判断是否创建成功
{
//③绑定对应地址信息
//设置地址端口号机器地址族
struct sockaddr_in addr;
addr.sin_family = AF_INET;//一般与使用 socket() 函数创建 socket 对象时使用的地址族一样
addr.sin_port = htons(13111);//设置端口号
inet_pton(AF_INET, "127.0.0.1", &(addr.sin_addr));
int addr_len = sizeof(addr);//地址信息的长度
int res = bind(SocketServer, (sockaddr*)&addr, addr_len);
if (res == 0)//返回 0 则代表绑定成功
{
std::cout << "bind success" << std::endl;
//④将套接字设置成被动监听状态,并且为其指定可访问的服务端个数
listen(SocketServer, max_listen);
while (true) {
//创建一个线程
sockaddr_in addr_client;
int addr_client_len = sizeof(addr_client);
//⑤ 使用 accept 等待连接客户端, addr_client 获取客户端 ip,端口
//这里可以优化下,使用多线程,在实际情况下避免堵塞
SOCKET socketClient = accept(SocketServer, (sockaddr*)&addr_client, &addr_client_len);
if (index < max_listen && socketClient != INVALID_SOCKET)//判断接收是否正确,而且没超出最大个数
{
//发送消息回去,通知一下已经连接成功
socket_client[index] = std::move(socketClient);//没有问题,且能连接成功将其传给数组
char buf[] = "service: connect success";
int send_len = sizeof(buf);
send(socket_client[index], buf, send_len, 0);
std::cout << "accept" << std::endl;
//开启一个线程
server_th[index] = std::thread(ServerCom, std::ref(socket_client[index]));
index++;
}
}
}
else {
std::cout << "bind Failed" << std::endl;
}
}
else {
std::cout << "socket create Failed" << std::endl;
}
lock.lock();
is_used = false;//关闭线程使用
lock.unlock();
for (size_t i = 0; i < index; i++)
{
if (server_th[i].joinable())
{
server_th[i].join();
}
}
//当不在使用时,关闭 socket 服务端和释放动态链接库 ws2-32.dll 分配的资源
closesocket(SocketServer);
WSACleanup();//释放动态链接库 ws2_32.dll 初始化时多分配的空间
return 0;
}
三:客户端整体代码展示
cpp
#include<WinSock2.h>
#include<WS2tcpip.h>
#include<iostream>
#include<thread>
#pragma comment(lib, "ws2_32.lib")//添加动态连接库的调用
int main(int argc, char* argv[]) {
//使用 WSAStartuo 来调用 ws2_32.dll 初始化环境,设定所需要的版本号
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
//创建客户端的 Socket
SOCKET SocketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//连接服务端,设置服务端信息
if (SocketClient != INVALID_SOCKET)
{
//连接服务端
sockaddr_in server_addr;
server_addr.sin_port = htons(13111);
server_addr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &(server_addr.sin_addr));
int res = connect(SocketClient, (sockaddr*)&server_addr, sizeof(server_addr));
if (res == 0)//代表连接成功
{
char buf[1024];
char buf_send[1024];
std::string msg;
//开线程接收数据
while (true) {
int n = recv(SocketClient, buf, sizeof(buf), 0);
if (n > 0)
{
std::cout << buf << std::endl;
memset(buf_send, 0, sizeof(buf_send));
std::cin >> msg;
memcpy(buf_send, msg.c_str(), sizeof(buf_send));
send(SocketClient, buf_send, sizeof(buf_send), 0);
}else
{
std::cout << "info len:" << n << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
//判断下是否是服务端死掉
//如果errno == EINTR 则说明 recv 函数是由于程序接收到信号后返回的,socket 连接还是正常的,不应 close 掉 socket 连接。
if (errno != EINTR)
{
closesocket(SocketClient);
//释放调用 ws2_32 所占用的内存空间
WSACleanup();
return -1;
}
}
}
}
}
closesocket(SocketClient);
//释放调用 ws2_32 所占用的内存空间
WSACleanup();
return 0;
}