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;
- }