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

智能推荐

【史上最易懂】马尔科夫链-蒙特卡洛方法:基于马尔科夫链的采样方法,从概率分布中随机抽取样本,从而得到分布的近似_马尔科夫链期望怎么求-程序员宅基地

文章浏览阅读1.3k次,点赞40次,收藏19次。虽然你不能直接计算每个房间的人数,但通过马尔科夫链的蒙特卡洛方法,你可以从任意状态(房间)开始采样,并最终收敛到目标分布(人数分布)。然后,根据一个规则(假设转移概率是基于房间的人数,人数较多的房间具有较高的转移概率),你随机选择一个相邻的房间作为下一个状态。比如在巨大城堡,里面有很多房间,找到每个房间里的人数分布情况(每个房间被访问的次数),但是你不能一次进入所有的房间并计数。但是,当你重复这个过程很多次时,你会发现你更有可能停留在人数更多的房间,而在人数较少的房间停留的次数较少。_马尔科夫链期望怎么求

linux以root登陆命令,su命令和sudo命令,以及限制root用户登录-程序员宅基地

文章浏览阅读3.9k次。一、su命令su命令用于切换当前用户身份到其他用户身份,变更时须输入所要变更的用户帐号与密码。命令su的格式为:su [-] username1、后面可以跟 ‘-‘ 也可以不跟,普通用户su不加username时就是切换到root用户,当然root用户同样可以su到普通用户。 ‘-‘ 这个字符的作用是,加上后会初始化当前用户的各种环境变量。下面看下加‘-’和不加‘-’的区别:root用户切换到普通..._限制su root登陆

精通VC与Matlab联合编程(六)_精通vc和matlab联合编程 六-程序员宅基地

文章浏览阅读1.2k次。精通VC与Matlab联合编程(六)作者:邓科下载源代码浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程  Matlab C/C++函数库是Matlab扩展功能重要的组成部分,包含了大量的用C/C++语言重新编写的Matlab函数,主要包括初等数学函数、线形代数函数、矩阵操作函数、数值计算函数_精通vc和matlab联合编程 六

Asp.Net MVC2中扩展ModelMetadata的DescriptionAttribute。-程序员宅基地

文章浏览阅读128次。在MVC2中默认并没有实现DescriptionAttribute(虽然可以找到这个属性,通过阅读MVC源码,发现并没有实现方法),这很不方便,特别是我们使用EditorForModel的时候,我们需要对字段进行简要的介绍,下面来扩展这个属性。新建类 DescriptionMetadataProvider然后重写DataAnnotationsModelMetadataPro..._asp.net mvc 模型description

领域模型架构 eShopOnWeb项目分析 上-程序员宅基地

文章浏览阅读1.3k次。一.概述  本篇继续探讨web应用架构,讲基于DDD风格下最初的领域模型架构,不同于DDD风格下CQRS架构,二者架构主要区别是领域层的变化。 架构的演变是从领域模型到C..._eshoponweb

Springboot中使用kafka_springboot kafka-程序员宅基地

文章浏览阅读2.6w次,点赞23次,收藏85次。首先说明,本人之前没用过zookeeper、kafka等,尚硅谷十几个小时的教程实在没有耐心看,现在我也不知道分区、副本之类的概念。用kafka只是听说他比RabbitMQ快,我也是昨天晚上刚使用,下文中若有讲错的地方或者我的理解与它的本质有偏差的地方请包涵。此文背景的环境是windows,linux流程也差不多。 官网下载kafka,选择Binary downloads Apache Kafka 解压在D盘下或者什么地方,注意不要放在桌面等绝对路径太长的地方 打开conf_springboot kafka

随便推点

VS2008+水晶报表 发布后可能无法打印的解决办法_水晶报表 不能打印-程序员宅基地

文章浏览阅读1k次。编好水晶报表代码,用的是ActiveX模式,在本机运行,第一次运行提示安装ActiveX控件,安装后,一切正常,能正常打印,但发布到网站那边运行,可能是一闪而过,连提示安装ActiveX控件也没有,甚至相关的功能图标都不能正常显示,再点"打印图标"也是没反应解决方法是: 1.先下载"PrintControl.cab" http://support.businessobjects.c_水晶报表 不能打印

一. UC/OS-Ⅱ简介_ucos-程序员宅基地

文章浏览阅读1.3k次。绝大部分UC/OS-II的源码是用移植性很强的ANSI C写的。也就是说某产品可以只使用很少几个UC/OS-II调用,而另一个产品则使用了几乎所有UC/OS-II的功能,这样可以减少产品中的UC/OS-II所需的存储器空间(RAM和ROM)。UC/OS-II是为嵌入式应用而设计的,这就意味着,只要用户有固化手段(C编译、连接、下载和固化), UC/OS-II可以嵌入到用户的产品中成为产品的一部分。1998年uC/OS-II,目前的版本uC/OS -II V2.61,2.72。1.UC/OS-Ⅱ简介。_ucos

python自动化运维要学什么,python自动化运维项目_运维学python该学些什么-程序员宅基地

文章浏览阅读614次,点赞22次,收藏11次。大家好,本文将围绕python自动化运维需要掌握的技能展开说明,python自动化运维从入门到精通是一个很多人都想弄明白的事情,想搞清楚python自动化运维快速入门 pdf需要先了解以下几个事情。这篇文章主要介绍了一个有趣的事情,具有一定借鉴价值,需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获,下面让小编带着大家一起了解一下。_运维学python该学些什么

解决IISASP调用XmlHTTP出现msxml3.dll (0x80070005) 拒绝访问的错误-程序员宅基地

文章浏览阅读524次。2019独角兽企业重金招聘Python工程师标准>>> ..._hotfix for msxml 4.0 service pack 2 - kb832414

python和易语言的脚本哪门更实用?_易语言还是python适合辅助-程序员宅基地

文章浏览阅读546次。python和易语言的脚本哪门更实用?_易语言还是python适合辅助

redis watch使用场景_详解redis中的锁以及使用场景-程序员宅基地

文章浏览阅读134次。详解redis中的锁以及使用场景,指令,事务,分布式,命令,时间详解redis中的锁以及使用场景易采站长站,站长之家为您整理了详解redis中的锁以及使用场景的相关内容。分布式锁什么是分布式锁?分布式锁是控制分布式系统之间同步访问共享资源的一种方式。为什么要使用分布式锁?​ 为了保证共享资源的数据一致性。什么场景下使用分布式锁?​ 数据重要且要保证一致性如何实现分布式锁?主要介绍使用redis来实..._redis setnx watch

推荐文章

热门文章

相关标签