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,服务器和客户端代码

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐