C 语言实现 Windows 下 Socket 编程
Windows 上实现 C 语言网络编程
编译准备
网络编程,对于 Windows 和 Linux ,不同系统需要引入不同的头文件,这里我们是在 Windows 中进行网络编程,这里我们采用引入 Winsock2.h 头文件
我们引入了相关的头文件,并不能够直接通过编译器进行编译我们的 socket 编程的相关程序,需要我们在代码中引入 ws2_32.lib 开发环境,才能够保证代码正常执行。
引入相关环境,只是保证了我们的程序可以正常运行,但是我们在编译运行时,还是会产生各种各样的报错,所以在引入了相关环境之后,我们还需要在程序编译时引入相关的命令,才能够完全编译并执行。
引入环境代码如下(在头文件引用下,加入如下代码):
#pragma comment(lib,"ws2_32.lib")
添加编译条件流程:
如果我们使用的时 DevC++ ,我们需要添加如下编译指令:
需要添加的指令如下:
-lwsock32 -lWs2_32
注意:这里每两条指令之间都要有空格,否则讲不被识别
如果我们使用的时 vscode 等编译器,我们可以直接在终端中,通过 gcc 命令进行编译运行相关程序,指令代码如下:
gcc -g main.c -o main -lwsock32 -lWs2_32
代码设计
这里使用微软官方给出的示例代码进行讲解,分为服务器端和客户端两种,步骤如下:
服务器:
-
初始化 Winsock。
-
创建套接字。
-
绑定套接字。
-
在套接字上监听客户端。
-
接受来自客户端的连接。
-
接收和发送数据。
-
断开连接
客户端
-
初始化 Winsock。
-
创建套接字。
-
连接到该服务器。
-
发送和接收数据。
-
断开连接
很明显, 1, 2, 还有 断开连接 步骤完全相同
程序运⾏事项:
启动客户端应⽤程序之前应启动服务器应⽤程序
客户端尝试连接到 TCP 端⼝27015上的服务器。 客户端连接后,客户端会将数据发送到服务器,并接收从服务器发送回的任何数据。 然后,客户端会关闭套接字并退出
下面我们将联系代码分别分析服务端与客户端如何实现
默认数据设置
在进入主函数之前,无论是服务端还是客户端,我们需要设置一些默认数据以保证我们的程序能够正常编译运行
#include <winsock2.h> //传输通信
#include <ws2tcpip.h> //用于检索ip地址的新函数和结构
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")//引入ws2_32.lib库,不然编译报错
#undef UNICODE
#define WIN32_LEAN_AND_MEAN
#define DEFAULT_IP "127.0.0.1"// 服务器为本机
#define DEFAULT_PORT "27015" //默认端口
#define DEFAULT_BUFLEN 512 //字符缓冲区长度
环境检测
在开始编程之前,我们需要使用简单的程序对我们所需要的编程环境进行简单的检测,我们需要按照上述说明添加好我们的编译命令,这里推荐使用 Dev-c++ 或者 Visual Studio 这两款编译器
环境检测代码如下:
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
int main() {
printf("Hello World");
return 0;
}
如果上述代码可以正常运行,即可说明我们具备了网络编程所需环境
服务器端
1. 初始化
#pragma region 1. 初始化
WSADATA wsaData; // 定义一个结构体成员,存放的是 Windows Socket 初始化信息
//Winsock进行初始化
//调用 WSAStartup 函数以启动使用 WS2 _32.dll
int iResult; // 函数返回数据
//WSAStartup的 MAKEWORD (2,2) 参数发出对系统上 Winsock 版本2.2 的请求,并将传递的版本设置为调用方可以使用的最版本的 Windows 套接字支持
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); // 启动命令,如果返回为 0 ,说明成功启动
if (iResult != 0) { // 返回不为 0 启动失败
printf("初始化Winsock出错: %d\n", iResult);
return 1;
}
#pragma endregion 1. 初始化结束
2. 服务器端创建套接字
首先为服务器创建套接字, 这样接下来的客户端就可以连接调试
#pragma region 2. 为服务器创建套接字
struct addrinfo* result = NULL, * ptr = NULL, hints;
ZeroMemory(&hints, sizeof(hints)); // 将内存块的内容初始化为零
hints.ai_family = AF_INET; //AF _INET 用于指定 IPv4 地址族
hints.ai_socktype = SOCK_STREAM; // SOCK _STREAM 用于指定流套接字
hints.ai_protocol = IPPROTO_TCP; // IPPROTO _TCP 用于指定 tcp 协议
hints.ai_flags = AI_PASSIVE; // 指定 getaddrinfo 函数中使用的选项的标志。AI_PASSIVE表示:套接字地址将在调用 bindfunction 时使用
// 从本机中获取 ip 地址等信息为了 sockcet 使用
//getaddrinfo 函数提供从 ANSI 主机名到地址的独立于协议的转换。
//参数1:该字符串包含一个主机(节点)名称或一个数字主机地址字符串。
//参数2:服务名或端口号。
// 参数3:指向 addrinfo 结构的指针,该结构提供有关调用方支持的套接字类型的提示。
//参数4:指向一个或多个包含主机响应信息的 addrinfo 结构链表的指针。
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("解析地址/端⼝失败: %d\n", iResult);
WSACleanup();
return 1;
}
// 创建socket对象,使服务器侦听客户端连接
SOCKET ListenSocket = INVALID_SOCKET;
// socket 函数创建绑定到特定
//为服务器创建一个SOCKET来监听客户端连接
//socket函数创建绑定到特定传输服务提供者的套接字。
//参数1:地址族规范
//参数2:新套接字的类型规范
//参数3:使用的协议
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ListenSocket == INVALID_SOCKET) { //检查是否有错误,以确保套接字为有效的套接字
printf("套接字错误: %ld\n", WSAGetLastError());
freeaddrinfo(result); //调用 freeaddrinfo 函数以释放由 getaddrinfo 函数为此地址信息分配的内存。
WSACleanup(); //终止 WS2 _ 32 DLL 的使用
return 1;
}
#pragma endregion 2. 创建套接字结束
3. 绑定套接字
若要使服务器接受客户端连接,它必须绑定到服务器的网络地址
#pragma region 3. 绑定套接字
//要使服务器接受客户端连接,必须将其绑定到系统中的网络地址。
//Sockaddr结构保存有关地址族、IP 地址和端口号的信息。
//bind函数将本地地址与套接字关联起来。设置TCP监听套接字
//参数1:标识未绑定套接字的描述符。
//2:一个指向本地地址sockaddr结构的指针,用于分配给绑定的套接字。这里面有Sockaddr结构
//3:所指向值的长度(以字节为单位)
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("设置TCP监听套接字失败: %d\n", WSAGetLastError());
freeaddrinfo(result); // 调用 bind 函数后,不再需要地址信息 释放
closesocket(ListenSocket); // 关闭一个已存在的套接字
WSACleanup();
return 1;
}
#pragma endregion 3. 绑定套接字结束
4. 在套接字上监听客户端
将套接字绑定到系统上的 IP 地址和端口之后,服务器必须在该 IP 地址和端口上侦听传入的连接请求
#pragma region 4. 在套接字上监听客户端(监听套接字)
//将套接字绑定到系统的ip地址和端口后,服务器必须在IP地址和端口上监听传入的连接请求
//listen函数将套接字置于侦听传入连接的状态。
//参数1:标识已绑定的未连接套接字的描述符。
//2:挂起连接队列的最大长度。如果设置为SOMAXCONN,负责套接字的底层服务提供者将把待办事项设置为最大合理值
if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
// SOMAXCONN定义了此套接字允许最大连接
printf("监听传入失败: %ld\n", WSAGetLastError());
closesocket(ListenSocket); // 关闭一个已连接的套接字
WSACleanup();
return 1;
}
#pragma endregion 4. 在套接字上监听客户端(监听套接字)结束
注意: window10第一次调试这一步骤会让用户给予防火墙权限
5. 接受来自客户端的连接。
当套接字侦听连接后,程序必须处理该套接字上的连接请求
#pragma region 5.接受来自客户端的连接(Windows 插槽 2)
//当套接字监听连接后,程序必须处理套接字上的连接请求
//创建临时套接字对象,以接受来自客户端的连接
SOCKET ClientSocket;
//通常,服务器应用程序将被设计为侦听来自多个客户端的连接。 对于高性能服务器,通常使用多个线程来处理多个客户端连接。 这个示例比较简单,不用多线程
ClientSocket = INVALID_SOCKET; //INVALID_SOCKET定义代表遮套接字无效
//accept函数允许套接字上的传入连接尝试
//参数1:一个描述符,用来标识一个套接字,该套接字使用listen函数处于侦听状态。连接实际上是用accept返回的套接字建立的。
//2:一种可选的指向缓冲区的指针,用于接收通信层所知的连接实体的地址。addr参数的确切格式是由当socket来自so时建立的地址族决定的
//3:一个可选的指针,指向一个整数,该整数包含addr参数所指向的结构的长度。
ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket == INVALID_SOCKET) {
printf("传入连接失败: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
/*注意:当客户端连接被接受后,服务器应用程序通常会将接受的客户端套接字传递 (ClientSocket 变量) 到工作线程或 i/o 完成端口,并继续接受其他连接。
这个示例没有,可以查看Microsoft Windows 软件开发工具包 (SDK) 附带的 高级 Winsock 示例 中介绍了其中部分编程技术的示例。
链接:https://docs.microsoft.com/zh-cn/windows/win32/winsock/getting-started-with-winsock*/
#pragma endregion 5.接受来自客户端的连接(Windows 插槽 2)结束
注意:运行这一步时, 控制台似乎没有显示任何东西, 其实 是accept 将逻辑流程卡住 等待 客户端连接, 如下图所示
accept 将逻辑流程卡住
6. 在服务器上接收和发送数据
服务器接收的数据来自客户端, 发送也是向客户端发送数据, 故而需要等下面的客户端socket编写完毕才能进行最终的功能测试.
#pragma region 6. 在服务器上接收和发送数据
char recvbuf[DEFAULT_BUFLEN]; //字符缓冲区数组
int iSendResult;
int recvbuflen = DEFAULT_BUFLEN; //缓冲值
do {
//recv函数从已连接的套接字或已绑定的无连接套接字接收数据。
//参数1:套接字描述符
//参数2:一个指向缓冲区的指针,用来接收传入的数据。
//参数3:参数buf所指向的缓冲区的长度,以字节为单位。
//参数4:一组影响此函数行为的标志
iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("接收的字节数: %d\n", iResult);
//将缓冲区回传给发送方
//发送一个初始缓冲区
//send函数参数1:标识已连接套接字的描述符。
//参数2:指向包含要传送的数据的缓冲区的指针。这里为了简单将客户端发送过来的消息再发送给客户端
//参数3:参数buf所指向的缓冲区中数据的长度(以字节为单位)。strlen获取字符串长度
//参数4:指定调用方式的一组标志。
iSendResult = send(ClientSocket, recvbuf, iResult, 0);
if (iSendResult == SOCKET_ERROR) {
printf("发送失败: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
printf("字节发送: %d\n", iSendResult);
}
else if (iResult == 0)
printf("连接关闭...\n");
else {
printf("接受失败: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} while (iResult > 0);
#pragma endregion 6. 在服务器上接收和发送数据结束
注意:这一步相当于完成了服务器的书写,但为了保险, 还是要关闭连接
7. 断开连接
#pragma region 7. 断开服务器连接
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("关闭失败: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
/*第二种关闭方法
使用 Windows 套接字 DLL 完成客户端应用程序时,将调用 WSACleanup 函数来释放资源。
closesocket(ClientSocket);
WSACleanup();*/
#pragma endregion 7. 断开服务器连接结束
注意:这里没有写控制台输入判断 进行关闭服务器 而是等客户端传输完数据后自动执行关闭逻辑
完整服务端代码
点击查看完整服务端代码
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")//引入ws2_32.lib库,不然编译报错
#undef UNICODE
#define WIN32_LEAN_AND_MEAN
#define DEFAULT_PORT "27015" //默认端口
#define DEFAULT_BUFLEN 512 // 字符缓冲区长度
int main() {
printf("启动服务器!\n");
#pragma region 1. 初始化
WSADATA wsaData; // 定义一个结构体成员,存放的是 Windows Socket 初始化信息
//Winsock进行初始化
//调用 WSAStartup 函数以启动使用 WS2 _32.dll
int iResult; // 函数返回数据
//WSAStartup的 MAKEWORD (2,2) 参数发出对系统上 Winsock 版本2.2 的请求,并将传递的版本设置为调用方可以使用的最版本的 Windows 套接字支持
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); // 启动命令,如果返回为 0 ,说明成功启动
if (iResult != 0) { // 返回不为 0 启动失败
printf("初始化Winsock出错: %d\n", iResult);
return 1;
}
#pragma endregion 1. 初始化结束
#pragma region 2. 为服务器创建套接字
#define DEFAULT_PORT "9501" // 服务器监听的端口
struct addrinfo* result = NULL, * ptr = NULL, hints;
ZeroMemory(&hints, sizeof(hints)); // 将内存块的内容初始化为零
hints.ai_family = AF_INET; //AF _INET 用于指定 IPv4 地址族
hints.ai_socktype = SOCK_STREAM; // SOCK _STREAM 用于指定流套接字
hints.ai_protocol = IPPROTO_TCP; // IPPROTO _TCP 用于指定 tcp 协议
hints.ai_flags = AI_PASSIVE; // 指定 getaddrinfo 函数中使用的选项的标志。AI_PASSIVE表示:套接字地址将在调用 bindfunction 时使用
// 从本机中获取 ip 地址等信息为了 sockcet 使用
//getaddrinfo 函数提供从 ANSI 主机名到地址的独立于协议的转换。
//参数1:该字符串包含一个主机(节点)名称或一个数字主机地址字符串。
//参数2:服务名或端口号。
// 参数3:指向 addrinfo 结构的指针,该结构提供有关调用方支持的套接字类型的提示。
//参数4:指向一个或多个包含主机响应信息的 addrinfo 结构链表的指针。
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("解析地址/端口失败: %d\n", iResult);
WSACleanup();
return 1;
}
// 创建socket对象,使服务器侦听客户端连接
SOCKET ListenSocket = INVALID_SOCKET;
// socket 函数创建绑定到特定
//为服务器创建一个SOCKET来监听客户端连接
//socket函数创建绑定到特定传输服务提供者的套接字。
//参数1:地址族规范
//参数2:新套接字的类型规范
//参数3:使用的协议
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ListenSocket == INVALID_SOCKET) { //检查是否有错误,以确保套接字为有效的套接字
printf("套接字错误: %ld\n", WSAGetLastError());
freeaddrinfo(result); //调用 freeaddrinfo 函数以释放由 getaddrinfo 函数为此地址信息分配的内存。
WSACleanup(); //终止 WS2 _ 32 DLL 的使用
return 1;
}
#pragma endregion 2. 创建套接字结束
#pragma region 3. 绑定套接字
//要使服务器接受客户端连接,必须将其绑定到系统中的网络地址。
//Sockaddr结构保存有关地址族、IP 地址和端口号的信息。
//bind函数将本地地址与套接字关联起来。设置TCP监听套接字
//参数1:标识未绑定套接字的描述符。
//2:一个指向本地地址sockaddr结构的指针,用于分配给绑定的套接字。这里面有Sockaddr结构
//3:所指向值的长度(以字节为单位)
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("设置TCP监听套接字失败: %d\n", WSAGetLastError());
freeaddrinfo(result); // 调用 bind 函数后,不再需要地址信息 释放
closesocket(ListenSocket); // 关闭一个已存在的套接字
WSACleanup();
return 1;
}
#pragma endregion 3. 绑定套接字结束
#pragma region 4. 在套接字上监听客户端(监听套接字)
//将套接字绑定到系统的ip地址和端口后,服务器必须在IP地址和端口上监听传入的连接请求
//listen函数将套接字置于侦听传入连接的状态。
//参数1:标识已绑定的未连接套接字的描述符。
//2:挂起连接队列的最大长度。如果设置为SOMAXCONN,负责套接字的底层服务提供者将把待办事项设置为最大合理值
if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
// SOMAXCONN定义了此套接字允许最大连接
printf("监听传入失败: %ld\n", WSAGetLastError());
closesocket(ListenSocket); // 关闭一个已连接的套接字
WSACleanup();
return 1;
}
#pragma endregion 4. 在套接字上监听客户端(监听套接字)结束
#pragma region 5.接受来自客户端的连接(Windows 插槽 2)
//当套接字监听连接后,程序必须处理套接字上的连接请求
//创建临时套接字对象,以接受来自客户端的连接
SOCKET ClientSocket;
//通常,服务器应用程序将被设计为侦听来自多个客户端的连接。 对于高性能服务器,通常使用多个线程来处理多个客户端连接。 这个示例比较简单,不用多线程
ClientSocket = INVALID_SOCKET; //INVALID_SOCKET定义代表遮套接字无效
//accept函数允许套接字上的传入连接尝试
//参数1:一个描述符,用来标识一个套接字,该套接字使用listen函数处于侦听状态。连接实际上是用accept返回的套接字建立的。
//2:一种可选的指向缓冲区的指针,用于接收通信层所知的连接实体的地址。addr参数的确切格式是由当socket来自so时建立的地址族决定的
//3:一个可选的指针,指向一个整数,该整数包含addr参数所指向的结构的长度。
ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket == INVALID_SOCKET) {
printf("传入连接失败: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
/*注意:当客户端连接被接受后,服务器应用程序通常会将接受的客户端套接字传递 (ClientSocket 变量) 到工作线程或 i/o 完成端口,并继续接受其他连接。
这个示例没有,可以查看Microsoft Windows 软件开发工具包 (SDK) 附带的 高级 Winsock 示例 中介绍了其中部分编程技术的示例。
链接:https://docs.microsoft.com/zh-cn/windows/win32/winsock/getting-started-with-winsock*/
#pragma endregion 5.接受来自客户端的连接(Windows 插槽 2)结束
#pragma region 6. 在服务器上接收和发送数据
char recvbuf[DEFAULT_BUFLEN]; //字符缓冲区数组
int iSendResult;
int recvbuflen = DEFAULT_BUFLEN; //缓冲值
do {
//recv函数从已连接的套接字或已绑定的无连接套接字接收数据。
//参数1:套接字描述符
//参数2:一个指向缓冲区的指针,用来接收传入的数据。
//参数3:参数buf所指向的缓冲区的长度,以字节为单位。
//参数4:一组影响此函数行为的标志
iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("接收的字节数: %d\n", iResult);
//将缓冲区回传给发送方
//发送一个初始缓冲区
//send函数参数1:标识已连接套接字的描述符。
//参数2:指向包含要传送的数据的缓冲区的指针。这里为了简单将客户端发送过来的消息再发送给客户端
//参数3:参数buf所指向的缓冲区中数据的长度(以字节为单位)。strlen获取字符串长度
//参数4:指定调用方式的一组标志。
iSendResult = send(ClientSocket, recvbuf, iResult, 0);
if (iSendResult == SOCKET_ERROR) {
printf("发送失败: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
printf("字节发送: %d\n", iSendResult);
}
else if (iResult == 0)
printf("连接关闭...\n");
else {
printf("接受失败: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} while (iResult > 0);
#pragma endregion 6. 在服务器上接收和发送数据结束
#pragma region 7. 断开服务器连接
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("关闭失败: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
/*第二种关闭方法
使用 Windows 套接字 DLL 完成客户端应用程序时,将调用 WSACleanup 函数来释放资源。
closesocket(ClientSocket);
WSACleanup();*/
#pragma endregion 7. 断开服务器连接结束
return 0;
}
在文章末尾会给出代码下载连接
客户端
1. 初始化
这一步和服务端相同
#pragma region 1. 初始化
//WSADATA结构包含有关Windows Sockets实现的信息。
WSADATA wsaData;
int iResult; //结果
//Winsock进行初始化
//调用 WSAStartup 函数以启动使用 WS2 _32.dll
//WSAStartup的 MAKEWORD (2,2) 参数发出对系统上 Winsock 版本2.2 的请求,并将传递的版本设置为调用方可以使用的最新版本的 Windows 套接字支持
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup 失败: %d\n", iResult);
return 1;
}
#pragma endregion 1. 初始化结束
2. 客户端创建套接字
需要重新用vs创建一个新项目当做客户端
#pragma region 2. 为客户端创建套接字
//初始化之后实例套接字对象供客户端使用
//创建套接字
struct addrinfo* result = NULL, * ptr = NULL, hints;
// ZeroMemory 函数,将内存块的内容初始化为零
ZeroMemory(&hints, sizeof(hints));
//addrinfo在getaddrinfo()调用中使用的结构
hints.ai_family = AF_INET; //AF _INET 用于指定 IPv4 地址族
hints.ai_socktype = SOCK_STREAM;// SOCK _STREAM 用于指定流套接字
hints.ai_protocol = IPPROTO_TCP;// IPPROTO _TCP 用于指定 tcp 协议
hints.ai_flags = AI_PASSIVE;
// 从本机中获取ip地址等信息为了sockcet 使用
//解析服务器地址和端口
//getaddrinfo函数提供从ANSI主机名到地址的独立于协议的转换。
//参数1:该字符串包含一个主机(节点)名称或一个数字主机地址字符串。
//参数2:服务名或端口号。
// 参数3:指向addrinfo结构的指针,该结构提供有关调用方支持的套接字类型的提示。
//参数4:指向一个或多个包含主机响应信息的addrinfo结构链表的指针。
iResult = getaddrinfo(DEFAULT_IP, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo 失败: %d\n", iResult);
WSACleanup();
return 1;
}
SOCKET ConnectSocket = INVALID_SOCKET;//创建套接字对象
//尝试连接到返回的第一个地址。
ConnectSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
//检查是否存在错误,以确保套接字为有效套接字。
if (ConnectSocket == INVALID_SOCKET) {
//WSAGetLastError返回与上次发生的错误相关联的错误号。
printf("套接字错误: %ld\n", WSAGetLastError());
//调用 freeaddrinfo 函数以释放由 getaddrinfo 函数为此地址信息分配的内存
freeaddrinfo(result);
WSACleanup();//用于终止 WS2 _ 32 DLL 的使用。
return 1;
}
#pragma endregion 2. 为客户端创建套接字结束
3. 客户端连接到该服务器
#pragma region 3. 连接到套接字
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
//调用getaddrinfo
//尝试连接到一个地址,直到一个成功
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
//检查是否存在错误,以确保套接字为有效套接字。
if (ConnectSocket == INVALID_SOCKET) {
//WSAGetLastError返回与上次发生的错误相关联的错误号。
printf("socket failed with error: %ld\n", WSAGetLastError());
//调用 freeaddrinfo 函数以释放由 getaddrinfo 函数为此地址信息分配的内存
freeaddrinfo(result);
WSACleanup();//用于终止 WS2 _ 32 DLL 的使用。
return 1;
}
//调用 connect 函数,将创建的套接字和 sockaddr 结构作为参数传递。
//connect函数建立到指定套接字的连接。
//参数1:标识未连接套接字的描述符。
//参数2:一个指向要建立连接的sockaddr结构的指针。
//参数3:参数所指向的sockaddr结构的长度,以字节为单位
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);//关闭一个已存在的套接字。
ConnectSocket = INVALID_SOCKET;
continue;
}
break;
}
//应该尝试getaddrinfo返回的下一个地址,如果连接调用失败。但对于这个简单的例子,我们只是释放资源。由getaddrinfo返回并打印一个错误消息
freeaddrinfo(result);//释放由 getaddrinfo 函数为此地址信息分配的内存
if (ConnectSocket == INVALID_SOCKET) {
printf("法连接到服务器!!\n");
WSACleanup();
return 1;
}
#pragma endregion 3. 连接到套接字结束
4. 客户端发送和接收数据
#pragma region 4.在客户端上发送和接收数据
//下面的代码演示建立连接后客户端使用的发送和接收功能。
int recvbuflen = DEFAULT_BUFLEN; //缓冲区
const char* sendbuf = "Hello World";
char recvbuf[DEFAULT_BUFLEN];
//发送一个初始缓冲区
//send函数参数1:标识已连接套接字的描述符。
//参数2:指向包含要传送的数据的缓冲区的指针。
//参数3:参数buf所指向的缓冲区中数据的长度(以字节为单位)。strlen获取字符串长度
//参数4:指定调用方式的一组标志。
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("发送失败: %d\n", WSAGetLastError());
closesocket(ConnectSocket); //关闭套接字
WSACleanup();
return 1;
}
printf("字节发送: %ld\n", iResult);
//关闭正在发送的连接,因为不再发送数据
//客户端仍然可以使用ConnectSocket来接收数据
//shutdown禁止套接字上的发送或接收功能。
//参数1:套接字描述符
//参数2:关闭类型描述符。1代表关闭发送操作
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("关闭失败: %d\n", WSAGetLastError());
closesocket(ConnectSocket); //关闭套接字
WSACleanup();
return 1;
}
//接收数据,直到服务器关闭连接
do {
//recv函数从已连接的套接字或已绑定的⽆连接套接字接收数据。
//参数1:套接字描述符
//参数2:⼀个指向缓冲区的指针,⽤来接收传⼊的数据。
//参数3:参数buf所指向的缓冲区的长度,以字节为单位。
//参数4:⼀组影响此函数⾏为的标志
iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
if (iResult > 0)
printf("接收的字节数: %d\n", iResult);
else if (iResult == 0)
printf("连接关闭\n");
else
printf("连接失败!!: %d\n", WSAGetLastError());
} while (iResult > 0);
#pragma endregion 4.在客户端上发送和接收数据结束
5. 客户端断开连接
#pragma region 5. 断开连接
//两种方法断开客户端连接
// 这里和服务器断开连接写在最后不同, 客户端断开连接写在 发送后 和 接收前
// shutdown(ConnectSocket, SD_SEND) SD_SEND表示socket的发送数据端虽然关闭(为了服务器释放客户端连接资源), 但是仍然能接收服务端的数据
//shutdown禁止套接字上的发送或接收功能。
//参数1:套接字描述符
//参数2:关闭类型描述符。1代表关闭发送操作
//注意:这时客户端应用程序仍可以在套接字上接收数据。
//iResult = shutdown(ClientSocket, SD_SEND);
//if (iResult == SOCKET_ERROR) {
// printf("shutdown failed: %d\n", WSAGetLastError());
// closesocket(ClientSocket);
// WSACleanup();
// return 1;
//}
closesocket(ConnectSocket);
WSACleanup();
#pragma region 5. 断开连接结束
完整客户端代码
点击查看完整客户端代码
#include <winsock2.h> //传输通信
#include <ws2tcpip.h> //用于检索ip地址的新函数和结构
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")//引入ws2_32.lib库,不然编译报错
#undef UNICODE
#define WIN32_LEAN_AND_MEAN
#define DEFAULT_BUFLEN 512 //字符缓冲区长度
#define DEFAULT_IP "127.0.0.1"// 服务器为本机
#define DEFAULT_PORT "27015" // 服务器监听的端口
int main() {
printf("启动客户端\n");
#pragma region 1. 初始化
//WSADATA结构包含有关Windows Sockets实现的信息。
WSADATA wsaData;
int iResult; //结果
//Winsock进行初始化
//调用 WSAStartup 函数以启动使用 WS2 _32.dll
//WSAStartup的 MAKEWORD (2,2) 参数发出对系统上 Winsock 版本2.2 的请求,并将传递的版本设置为调用方可以使用的最新版本的 Windows 套接字支持
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup 失败: %d\n", iResult);
return 1;
}
#pragma endregion 1. 初始化结束
#pragma region 2. 为客户端创建套接字
//初始化之后实例套接字对象供客户端使用
//创建套接字
struct addrinfo* result = NULL, * ptr = NULL, hints;
// ZeroMemory 函数,将内存块的内容初始化为零
ZeroMemory(&hints, sizeof(hints));
//addrinfo在getaddrinfo()调用中使用的结构
hints.ai_family = AF_INET; //AF _INET 用于指定 IPv4 地址族
hints.ai_socktype = SOCK_STREAM;// SOCK _STREAM 用于指定流套接字
hints.ai_protocol = IPPROTO_TCP;// IPPROTO _TCP 用于指定 tcp 协议
hints.ai_flags = AI_PASSIVE;
// 从本机中获取ip地址等信息为了sockcet 使用
//解析服务器地址和端口
//getaddrinfo函数提供从ANSI主机名到地址的独立于协议的转换。
//参数1:该字符串包含一个主机(节点)名称或一个数字主机地址字符串。
//参数2:服务名或端口号。
// 参数3:指向addrinfo结构的指针,该结构提供有关调用方支持的套接字类型的提示。
//参数4:指向一个或多个包含主机响应信息的addrinfo结构链表的指针。
iResult = getaddrinfo(DEFAULT_IP, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo 失败: %d\n", iResult);
WSACleanup();
return 1;
}
SOCKET ConnectSocket = INVALID_SOCKET;//创建套接字对象
//尝试连接到返回的第一个地址。
ConnectSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
//检查是否存在错误,以确保套接字为有效套接字。
if (ConnectSocket == INVALID_SOCKET) {
//WSAGetLastError返回与上次发生的错误相关联的错误号。
printf("套接字错误: %ld\n", WSAGetLastError());
//调用 freeaddrinfo 函数以释放由 getaddrinfo 函数为此地址信息分配的内存
freeaddrinfo(result);
WSACleanup();//用于终止 WS2 _ 32 DLL 的使用。
return 1;
}
#pragma endregion 2. 为客户端创建套接字结束
#pragma region 3. 连接到套接字
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
//调用getaddrinfo
//尝试连接到一个地址,直到一个成功
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
//检查是否存在错误,以确保套接字为有效套接字。
if (ConnectSocket == INVALID_SOCKET) {
//WSAGetLastError返回与上次发生的错误相关联的错误号。
printf("socket failed with error: %ld\n", WSAGetLastError());
//调用 freeaddrinfo 函数以释放由 getaddrinfo 函数为此地址信息分配的内存
freeaddrinfo(result);
WSACleanup();//用于终止 WS2 _ 32 DLL 的使用。
return 1;
}
//调用 connect 函数,将创建的套接字和 sockaddr 结构作为参数传递。
//connect函数建立到指定套接字的连接。
//参数1:标识未连接套接字的描述符。
//参数2:一个指向要建立连接的sockaddr结构的指针。
//参数3:参数所指向的sockaddr结构的长度,以字节为单位
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);//关闭一个已存在的套接字。
ConnectSocket = INVALID_SOCKET;
continue;
}
break;
}
//应该尝试getaddrinfo返回的下一个地址,如果连接调用失败。但对于这个简单的例子,我们只是释放资源。由getaddrinfo返回并打印一个错误消息
freeaddrinfo(result);//释放由 getaddrinfo 函数为此地址信息分配的内存
if (ConnectSocket == INVALID_SOCKET) {
printf("法连接到服务器!!\n");
WSACleanup();
return 1;
}
#pragma endregion 3. 连接到套接字结束
#pragma region 4.在客户端上发送和接收数据
//下面的代码演示建立连接后客户端使用的发送和接收功能。
int recvbuflen = DEFAULT_BUFLEN; //缓冲区
const char* sendbuf = "Hello World";
char recvbuf[DEFAULT_BUFLEN];
//发送一个初始缓冲区
//send函数参数1:标识已连接套接字的描述符。
//参数2:指向包含要传送的数据的缓冲区的指针。
//参数3:参数buf所指向的缓冲区中数据的长度(以字节为单位)。strlen获取字符串长度
//参数4:指定调用方式的一组标志。
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("发送失败: %d\n", WSAGetLastError());
closesocket(ConnectSocket); //关闭套接字
WSACleanup();
return 1;
}
printf("字节发送: %ld\n", iResult);
//关闭正在发送的连接,因为不再发送数据
//客户端仍然可以使用ConnectSocket来接收数据
//shutdown禁止套接字上的发送或接收功能。
//参数1:套接字描述符
//参数2:关闭类型描述符。1代表关闭发送操作
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("关闭失败: %d\n", WSAGetLastError());
closesocket(ConnectSocket); //关闭套接字
WSACleanup();
return 1;
}
//接收数据,直到服务器关闭连接
do {
//recv函数从已连接的套接字或已绑定的⽆连接套接字接收数据。
//参数1:套接字描述符
//参数2:⼀个指向缓冲区的指针,⽤来接收传⼊的数据。
//参数3:参数buf所指向的缓冲区的长度,以字节为单位。
//参数4:⼀组影响此函数⾏为的标志
iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
if (iResult > 0)
printf("接收的字节数: %d\n", iResult);
else if (iResult == 0)
printf("连接关闭\n");
else
printf("连接失败!!: %d\n", WSAGetLastError());
} while (iResult > 0);
#pragma endregion 4.在客户端上发送和接收数据结束
#pragma region 5. 断开连接
//两种方法断开客户端连接
// 这里和服务器断开连接写在最后不同, 客户端断开连接写在 发送后 和 接收前
// shutdown(ConnectSocket, SD_SEND) SD_SEND表示socket的发送数据端虽然关闭(为了服务器释放客户端连接资源), 但是仍然能接收服务端的数据
//shutdown禁止套接字上的发送或接收功能。
//参数1:套接字描述符
//参数2:关闭类型描述符。1代表关闭发送操作
//注意:这时客户端应用程序仍可以在套接字上接收数据。
//iResult = shutdown(ClientSocket, SD_SEND);
//if (iResult == SOCKET_ERROR) {
// printf("shutdown failed: %d\n", WSAGetLastError());
// closesocket(ClientSocket);
// WSACleanup();
// return 1;
//}
closesocket(ConnectSocket);
WSACleanup();
#pragma region 5. 断开连接结束
return 0;
}
所有评论(0)