C 语言实现 Windows 下 Socket 编程-程序员宅基地

技术标签: c语言  windows  嵌入式开发  开发语言  

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

代码设计

这里使用微软官方给出的示例代码进行讲解,分为服务器端和客户端两种,步骤如下:

服务器:

  1. 初始化 Winsock。

  2. 创建套接字。

  3. 绑定套接字。

  4. 在套接字上监听客户端。

  5. 接受来自客户端的连接。

  6. 接收和发送数据。

  7. 断开连接

客户端

  1. 初始化 Winsock。

  2. 创建套接字。

  3. 连接到该服务器。

  4. 发送和接收数据。

  5. 断开连接

很明显, 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;
}

参考资料

  1. C++Socket套接字编程使用winsock2.h

  2. Windows网络编程socket,服务器和客户端代码

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_59161987/article/details/128478541

智能推荐

神经网络初探-程序员宅基地

文章浏览阅读117次。神经网络初探——读《深度学习的数学》总结​ 学习是一个系统通过某种过程或者方式提升自身的某个或某些性能的过程,它本身包含的是一种自动化和可控化的含义。那么如何能让不具备智能的机器去学习呢?在模拟大脑神经元的工作原理后,看来我们已经找到了通往机器学习的一种方法,它叫做神经网络。神经网络是一种看似不可理解的复杂学习方法,这里包含着许多数学的知识。在阅读《深度学习的数学》一书之后,我想,我对神经网..._神经网络初探

音频分离Spleeter的安装_2stems.tar.gz-程序员宅基地

文章浏览阅读3.5k次,点赞5次,收藏13次。音频分离Spleeter的安装1.环境依赖及建立(需要已安装anaconda)1.0 项目源地址(github地址)1.1 创建虚拟环境1.2 激活虚拟环境1.3 conda 安装spleeter1.4 下载一个示例音乐1.5 将该音乐分离为两部分1.5.1 报错:No module named numba.decorators1.5.2 解决方案:1.6 下载分类模型1.6.1报错ValueError:Can't load save_path when it is None.1.6.2 解决方案:1.6._2stems.tar.gz

让你的软件飞起来:RGB转为YUV-程序员宅基地

文章浏览阅读64次。朋友曾经给我推荐了一个有关代码优化的pdf文档《让你的软件飞起来》,看完之后,感受颇深。为了推广其,同时也为了自己加深印象,故将其总结为word文档。下面就是其的详细内容总结,希望能于己于人都有所帮助。速度取决于算法同样的事情,方法不一样,效果也不一样。比如,汽车引擎,可以让你的速度超越马车,却无法超越音速;涡轮引擎,可以轻松超越音障,却无法飞出地球;如果有火箭发动机,就可以到达火..._bao.yuv

PX4装机教程(五)无人船(车)_在px4固体中如何设置差速船-程序员宅基地

文章浏览阅读2.5k次,点赞3次,收藏33次。文章目录前言一、载具设置二、电机接线三、PWM输出设置四、航点设置前言一个人可以走的更快,一群人才能走的更远,交流学习加qq:2096723956更多保姆级PX4+ROS学习视频:https://b23.tv/ZeUDKqy分享知识,传递正能量,如有疏漏或不当之处,恳请指出.PX4固件版本:1.10.0硬件:淘宝竞速船或者打窝船实验录屏https://www.bilibili.com/video/BV1wA411c7p3?spm_id_from=333.999.0.0一、载具设置单电机_在px4固体中如何设置差速船

一键批量查询快递单号,一键批量查询,共享备份物流,快递物流尽在掌控_批量快递查询-程序员宅基地

文章浏览阅读370次。每天都有大量的快递单号需要查询,如果一个个手动查询,不仅费时费力,还容易出错。为了解决这个问题,我们教您如何批量查询快递单号,并将快递物流信息进行备份并共享,实现高效管理。弹出一个对话框,文件名和保存类型不变,直接点“保存”,会提示备份成功,那么这个数据库就备份在电脑上了,也可以用第三方工具发送到其他电脑上。第四步,查询速度很快,我们就可以在主页面看到该批单号的运件信息了,比如:发出时间,状态,最后更新的物流时间,等等。第二步,在弹出来的文件框里,将需要查询的德邦快递单号都一一导入,并点击保存。_批量快递查询

敏捷开发(scrum)简介-程序员宅基地

文章浏览阅读7.7k次,点赞6次,收藏61次。敏捷开发(scrum)是一种软件开发的流程,强调快速反应、快速迭代、价值驱动。Scrum的英文意思是橄榄球运动的一个专业术语,表示“争球”的动作;运用该流程,你就能看到你团队高效的工作。一、四大价值观(特点)敏捷开发的特点就是下面4句话:「个体与交互」胜过「过程与工具」「可以工作的软件」胜过「面面俱到的文挡」「客户协作」胜过「合同谈判」「响应变化」胜过「遵循计划」说明:(1)敏捷开发(scrum)适用于竞争激烈,快速变化的市场。 敏捷的客户协作观念,快速迭代能帮助团队以最小成本,最快速_敏捷开发

随便推点

WiFi介绍_wifi dfs-程序员宅基地

文章浏览阅读2.2k次,点赞3次,收藏13次。802.11 Wi-Fi_wifi dfs

RK3568-sata接口_rk3568 sata-程序员宅基地

文章浏览阅读249次。pcie接口sata接口pcie总线pcie总线pcie控制器sata控制器nvme设备sata设备nvme协议ahci协议m-key接口b-key接口_rk3568 sata

java实现循环队列,解决普通队列假溢出问题_但是要利用循环队列的时候,处理假溢出,要使q.front=0的时候,为什么q.rear要加-程序员宅基地

文章浏览阅读1.3k次。循环队列可以很好的解决假溢出问题,不同于普通队列,在循环队列中,需将rear与front初始值都设置为0,rear指针指向队列中最后一个元素的下一个位置,也正因如此,数组是否存满的判定条件也应做出改变,在普通队列中,front==maxSize-1,即可认为数组已满,但是在循环队列中,由于在存放完数组最后一个有效位置后可以继续像数组中的0号位置进行存储,所以判断满的条件也会发生改变,—(rear+1)%maxSize = front.可以举个例子方便理解,假设一maxSize=5的数组,那么其有0 1_但是要利用循环队列的时候,处理假溢出,要使q.front=0的时候,为什么q.rear要加

linux CentOS 7下载步骤_centos7下载-程序员宅基地

文章浏览阅读4.8k次。linux CentOS 7下载步骤_centos7下载

Qt 22 布局管理器1 - QLayout,QBoxLayout,布局管理器的相互嵌套_qt layout可以嵌套layout吗-程序员宅基地

文章浏览阅读464次。布局管理器提供相关的类对界面组件进行布局管理能够自动排布窗口中的界面组件窗口变化后自动更新界面组件的大小QLayoutQLayout 是Qt 中布局管理器的抽象基类通过继承QLayout实现了功能各异且互补的布局管理器Qt中可以根据需要自定义布局管理器布局管理器不是界面部件,而是界面部件的定位策略QBoxLayout 布局管理器以水平或者垂直的方式管理界面组件水平:QHBoxLayout 水平布局管理器垂直:QVBoxLayout 垂直布局管理器sizePolicy:QSize_qt layout可以嵌套layout吗

error MSB6006 rc exe 已退出,代码为 5_vs2010报警 error msb6006: “rc.exe”已退出,代码为 5。-程序员宅基地

文章浏览阅读2.6k次。error MSB6006 rc exe 已退出,代码为 5_vs2010报警 error msb6006: “rc.exe”已退出,代码为 5。

推荐文章

热门文章

相关标签