Windows下网络编程(win32API+VS2022)

一、开发环境

我这里介绍下我用的环境安装过程。 所有版本的VS都可以的。

我当前环境是在Windows下,IDE用的是地表最强IDE VS2022。

下载地址:visualstudio.microsoft.com/zh-hans/dow...

因为我这里只需要用到C++和C语言编程,那么安装的时候可以自己选择需要安装的包。

安装好之后,创建项目。

二、网络编程的基础知识

2.1 什么是网络编程

网络编程是通过使用IP地址和端口号等网络信息,使两台以上的计算机能够相互通信,按照规定的协议交换数据的编程方式。

在网络编程中,程序员使用各种协议和技术,使得不同的设备可以通过网络进行数据交换和信息共享。

要实现网络编程,程序员需要了解并掌握各种网络通信协议,比如TCP/IP协议族,包括TCP、UDP、IP等,这些协议是实现设备间通信的基础。网络编程内部涉及到数据的打包、组装、发送、接收、解析等一系列过程,以实现信息的正确传输。

在TCP/IP协议族中,TCP和UDP是位于IP协议之上的传输层协议。 在OSI模型中,传输层是第四层,负责总体数据传输和数据控制,为会话层等高三层提供可靠的传输服务,为网络层提供可靠的目的地点信息。在TCP/IP协议族中,TCP和UDP正是位于这一层的协议。

这篇文章主要介绍 TCP 和 UDP 协议 以及 使用方法。

2.2 TCP 和 UDP协议介绍

TCP协议

TCP(传输控制协议)是一种面向连接的、可靠的传输层协议。在传输数据之前需要先建立连接,确保数据的顺序和完整性。TCP通过三次握手建立连接,并通过确认、超时和重传机制确保数据的可靠传输。TCP采用流量控制和拥塞控制机制,以避免网络拥塞,确保数据的顺利传输。因为TCP的这些特性,通常被应用于需要高可靠性和顺序性的应用,如网页浏览、电子邮件等。

UDP协议

UDP(用户数据报协议)是一种无连接的、不可靠的传输层协议。与TCP不同,UDP在传输数据之前不需要建立连接,直接将数据打包成数据报并发送出去。因此,UDP没有TCP的那些确认、超时和重传机制,也就不保证数据的可靠传输。UDP也没有TCP的流量控制和拥塞控制机制。因为UDP的简单性和高效性,通常被应用于实时性要求较高,但对数据可靠性要求不高的应用,如语音通话、视频直播等。

2.3 TCP通信的实现过程

要实现TCP通信,两端必须要知道对方的IP和端口号:

(1)IP地址:TCP协议是基于IP协议进行通信的,因此需要知道对方的IP地址,才能建立连接。

(2)端口号:每个TCP连接都有一个唯一的端口号,用于标识进程和应用程序。建立连接时,需要指定本地端口号和远端端口号。

(3)应用层协议:TCP协议只提供数据传输服务,应用程序需要定义自己的应用层协议,用于解析报文和处理数据。例如,HTTP协议就是基于TCP协议的应用层协议。

在正常的TCP通信过程中,第一步需要建立连接,这个过程称为"三次握手"。建立连接时,客户端向服务器发送一个SYN包,表示请求建立连接;服务器接收到SYN包后,向客户端发送一个ACK包,表示确认收到了SYN包;最后客户端再向服务器发送一个ACK包,表示确认收到了服务器的ACK包,此时连接建立成功。建立连接后,数据传输就可以开始了。

三、Windows下的API介绍

微软的官方文档地址:learn.microsoft.com/zh-cn/windo...

3.1 常用的函数介绍

在Windows下进行网络编程,可以使用Winsock API(Windows Sockets API)来实现。Winsock API是Windows平台上的标准网络编程接口,提供了一系列函数和数据结构,用于创建、连接、发送和接收网络数据等操作。

下面是常用的Winsock API接口函数:

(1)WSAStartup:初始化Winsock库,必须在使用其他Winsock函数之前调用。

(2)socket:创建一个套接字,用于网络通信。

(3)bind:将套接字与本地地址(IP地址和端口号)绑定。

(4)listen:开始监听连接请求,将套接字设置为被动模式。

(5)accept:接受客户端的连接请求,创建一个新的套接字用于与客户端通信。

(6)connect:与远程服务器建立连接。

(7)send:发送数据到已连接的套接字。

(8)recv:从已连接的套接字接收数据。

(9)sendto:发送数据到指定的目标地址。

(10)recvfrom:从指定的地址接收数据。

(11)closesocket:关闭套接字。

(12)getaddrinfo:根据主机名和服务名获取地址信息。

(13)gethostbyname:根据主机名获取主机的IP地址。

(14)gethostname:获取本地主机名。

3.2 函数参数介绍

下面是常用的几个Winsock API函数及其函数原型和参数含义的介绍:

(1)WSAStartup

arduino 复制代码
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
  • wVersionRequested:请求的Winsock版本号。
  • lpWSAData:指向WSADATA结构的指针,用于接收初始化结果和相关信息。

(2)socket

arduino 复制代码
SOCKET socket(int af, int type, int protocol);
  • af:地址族(Address Family),如AF_INET表示IPv4。
  • type:套接字类型,如SOCK_STREAM表示面向连接的TCP套接字。
  • protocol:指定协议。通常为0,表示根据type自动选择合适的协议。

(3)bind

arduino 复制代码
int bind(SOCKET s, const struct sockaddr* name, int namelen);
  • s:要绑定的套接字。
  • name:指向sockaddr结构的指针,包含要绑定的本地地址信息。
  • namelenname结构的长度。

(4)listen

arduino 复制代码
int listen(SOCKET s, int backlog);
  • s:要监听的套接字。
  • backlog:等待连接队列的最大长度。

(5)accept

arduino 复制代码
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
  • s:监听套接字。
  • addr:用于存储客户端地址信息的sockaddr结构。
  • addrlenaddr结构的长度。

(6)connect

arduino 复制代码
int connect(SOCKET s, const struct sockaddr* name, int namelen);
  • s:要连接的套接字。
  • name:指向目标地址信息的sockaddr结构指针。
  • namelenname结构的长度。

(7)send

arduino 复制代码
int send(SOCKET s, const char* buf, int len, int flags);
  • s:要发送数据的套接字。
  • buf:要发送的数据缓冲区。
  • len:要发送的数据长度。
  • flags:额外选项,如MSG_DONTROUTE等。

(8)recv

arduino 复制代码
int recv(SOCKET s, char* buf, int len, int flags);
  • s:要接收数据的套接字。
  • buf:用于存储接收数据的缓冲区。
  • len:要接收的数据长度。
  • flags:额外选项。

(9)sendto

arduino 复制代码
int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);
  • s:要发送数据的套接字。
  • buf:要发送的数据缓冲区。
  • len:要发送的数据长度。
  • flags:额外选项。
  • to:指向目标地址信息的sockaddr结构指针。
  • tolento结构的长度。

(10)recvfrom

csharp 复制代码
int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);
  • s:要接收数据的套接字。
  • buf:用于存储接收数据的缓冲区。
  • len:要接收的数据长度。
  • flags:额外选项。
  • from:用于存储发送方地址信息的sockaddr结构指针。
  • fromlenfrom结构的长度。

(11)closesocket

arduino 复制代码
int closesocket(SOCKET s);
  • s:要关闭的套接字。

(12)getaddrinfo

arduino 复制代码
int getaddrinfo(const char* nodename, const char* servname, const struct addrinfo* hints, struct addrinfo** res);
  • nodename:目标主机名或IP地址。
  • servname:服务名或端口号。
  • hints:指向addrinfo结构的指针,提供关于地址查找的提示。
  • res:指向addrinfo结构链表的指针,用于接收查找结果。

(13)gethostbyname

c 复制代码
struct hostent* gethostbyname(const char* name);
  • name:要查询的主机名。

(14)gethostname

arduino 复制代码
int gethostname(char* name, int namelen);
  • name:用于接收主机名的缓冲区。
  • namelenname缓冲区的长度。

四、基本示例代码

4.1 创建TCP服务器

下面代码实现一个简单的TCP服务器。

实现的功能:初始化Winsock、创建套接字、绑定到本地地址和指定端口、监听连接请求、接受客户端连接、发送和接收数据,最后关闭套接字和清理Winsock资源。

cpp 复制代码
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib") // 链接到ws2_32库

int main()
{
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化Winsock
    if (result != 0)
    {
        std::cout << "初始化Winsock失败 " << result << std::endl;
        return 1;
    }

    SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建套接字
    if (listenSocket == INVALID_SOCKET)
    {
        std::cout << "创建套接字失败: " << WSAGetLastError() << std::endl;
        WSACleanup();
        return 1;
    }

    sockaddr_in service;
    service.sin_family = AF_INET;
    service.sin_addr.s_addr = INADDR_ANY;
    service.sin_port = htons(12345);

    result = bind(listenSocket, (SOCKADDR*)&service, sizeof(service)); // 将套接字绑定到本地地址和指定端口
    if (result == SOCKET_ERROR)
    {
        std::cout << "端口绑定失败: " << WSAGetLastError() << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    result = listen(listenSocket, SOMAXCONN); // 监听连接请求
    if (result == SOCKET_ERROR)
    {
        std::cout << "监听连接请求失败: " << WSAGetLastError() << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    std::cout << "等待客户端连接:" << std::endl;

    SOCKET clientSocket = accept(listenSocket, NULL, NULL); // 接受客户端连接
    if (clientSocket == INVALID_SOCKET)
    {
        std::cout << "accept执行失败: " << WSAGetLastError() << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    std::cout << "客户端已连接..." << std::endl;

    char sendBuffer[1024] = "Hello, client!";
    result = send(clientSocket, sendBuffer, sizeof(sendBuffer), 0); // 发送数据给客户端
    if (result == SOCKET_ERROR)
    {
        std::cout << "发送消息执行错误: " << WSAGetLastError() << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }

    char recvBuffer[1024];
    result = recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0); // 接收来自客户端的数据
    if (result == SOCKET_ERROR)
    {
        std::cout << "接收消息执行错误: " << WSAGetLastError() << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return 1;
    }

    std::cout << "收到来着客户端发送的消息: " << recvBuffer << std::endl;

    closesocket(clientSocket); // 关闭客户端套接字
    closesocket(listenSocket); // 关闭监听套接字
    WSACleanup(); // 清理Winsock资源

    return 0;
}

运行效果:

4.2 创建TCP客户端

下面代码实现一个TCP客户端,连接到指定的服务器并完成通信。

cpp 复制代码
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库

int main()
{
    WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值
    if (result != 0)
    {
        std::cout << "WSAStartup failed: " << result << std::endl; //输出错误信息并退出程序
        return 1;
    }

    SOCKET connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值
    if (connectSocket == INVALID_SOCKET)
    {
        std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序
        WSACleanup(); //清除Winsock库
        return 1;
    }

    sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息
    service.sin_family = AF_INET; //指定地址族为IPv4
    inet_pton(AF_INET, "127.0.0.1", &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中
    service.sin_port = htons(12345); //将端口号从主机字节序转换为网络字节序,并存储在结构体中

    result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值
    if (result == SOCKET_ERROR)
    {
        std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序
        closesocket(connectSocket); //关闭套接字
        WSACleanup(); //清除Winsock库
        return 1;
    }

    std::cout << "Connected to server." << std::endl; //连接成功,输出消息

    char sendBuffer[1024] = "Hello, server!"; //创建发送缓冲区,存储待发送的数据
    result = send(connectSocket, sendBuffer, sizeof(sendBuffer), 0); //向服务器发送数据,检查返回值
    if (result == SOCKET_ERROR)
    {
        std::cout << "send failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序
        closesocket(connectSocket); //关闭套接字
        WSACleanup(); //清除Winsock库
        return 1;
    }

    char recvBuffer[1024]; //创建接收缓冲区,用于存储从服务器接收到的数据
    result = recv(connectSocket, recvBuffer, sizeof(recvBuffer), 0); //从服务器接收数据,检查返回值
    if (result == SOCKET_ERROR)
    {
        std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序
        closesocket(connectSocket); //关闭套接字
        WSACleanup(); //清除Winsock库
        return 1;
    }

    std::cout << "Received message from server: " << recvBuffer << std::endl; //输出从服务器收到的数据

    closesocket(connectSocket); //关闭套接字
    WSACleanup(); //清除Winsock库

    return 0;
}

运行效果:

4.3 TCP客户端循环接收消息

cpp 复制代码
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库

int main()
{
    WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值
    if (result != 0)
    {
        std::cout << "WSAStartup failed: " << result << std::endl; //输出错误信息并退出程序
        return 1;
    }

    SOCKET connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值
    if (connectSocket == INVALID_SOCKET)
    {
        std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序
        WSACleanup(); //清除Winsock库
        return 1;
    }

    sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息
    service.sin_family = AF_INET; //指定地址族为IPv4
    inet_pton(AF_INET, "127.0.0.1", &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中
    service.sin_port = htons(12345); //将端口号从主机字节序转换为网络字节序,并存储在结构体中

    result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值
    if (result == SOCKET_ERROR)
    {
        std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序
        closesocket(connectSocket); //关闭套接字
        WSACleanup(); //清除Winsock库
        return 1;
    }

    std::cout << "Connected to server." << std::endl; //连接成功,输出消息

    char recvBuffer[1024]; //创建接收缓冲区,用于存储从服务器接收到的数据

    while (true)
    {
        result = recv(connectSocket, recvBuffer, sizeof(recvBuffer), 0); //从服务器接收数据,检查返回值
        if (result == SOCKET_ERROR)
        {
            std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出循环
            break;
        }
        else if (result > 0) //判断是否有数据接收到
        {
            std::cout << "Received message from server: " << recvBuffer << std::endl; //输出从服务器收到的数据
        }
        else //连接断开
        {
            std::cout << "Server disconnected." << std::endl;
            break;
        }
    }

    closesocket(connectSocket); //关闭套接字
    WSACleanup(); //清除Winsock库

    return 0;
}

4.4 TCP服务器并发处理客户端请求

下面示例代码中,使用了std::vector<std::thread>来存储线程对象,在每个客户端连接时创建一个新线程来处理该连接。使用多线程可以让服务器同时处理多个客户端连接,提高并发性能。

cpp 复制代码
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <thread>
#include <vector>

#pragma comment(lib, "ws2_32.lib")

// 处理客户端连接的函数
void HandleClient(SOCKET clientSocket)
{
    char recvBuffer[1024];
    int result;

    while (true)
    {
        result = recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
        if (result == SOCKET_ERROR)
        {
            std::cout << "recv failed with error: " << WSAGetLastError() << std::endl;
            break;
        }
        else if (result > 0)
        {
            std::cout << "Received message from client: " << recvBuffer << std::endl;
        }
        else
        {
            std::cout << "Client disconnected." << std::endl;
            break;
        }
    }

    closesocket(clientSocket);
}

int main()
{
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0)
    {
        std::cout << "WSAStartup failed: " << result << std::endl;
        return 1;
    }

    SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSocket == INVALID_SOCKET)
    {
        std::cout << "socket failed with error: " << WSAGetLastError() << std::endl;
        WSACleanup();
        return 1;
    }

    sockaddr_in service;
    service.sin_family = AF_INET;
    service.sin_addr.s_addr = INADDR_ANY;
    service.sin_port = htons(12345);

    result = bind(listenSocket, (SOCKADDR*)&service, sizeof(service));
    if (result == SOCKET_ERROR)
    {
        std::cout << "bind failed with error: " << WSAGetLastError() << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    result = listen(listenSocket, SOMAXCONN);
    if (result == SOCKET_ERROR)
    {
        std::cout << "listen failed with error: " << WSAGetLastError() << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    std::cout << "Server is listening for incoming connections." << std::endl;

    std::vector<std::thread> threads; // 存储线程对象

    while (true)
    {
        SOCKET clientSocket = accept(listenSocket, NULL, NULL);
        if (clientSocket == INVALID_SOCKET)
        {
            std::cout << "accept failed with error: " << WSAGetLastError() << std::endl;
            continue;
        }

        std::cout << "Client connected." << std::endl;

        // 创建一个新线程来处理客户端连接
        std::thread thread(HandleClient, clientSocket);

        // 存储线程对象
        threads.push_back(std::move(thread));
    }

    // 等待所有线程执行完毕
    for (auto& thread : threads)
    {
        thread.join();
    }

    closesocket(listenSocket);
    WSACleanup();

    return 0;
}

运行效果:

相关推荐
间彧2 分钟前
Nginx + Keepalived 实现高可用集群(Linux下)
后端
间彧3 分钟前
在Kubernetes中如何部署高可用的Nginx Ingress Controller?
后端
间彧6 分钟前
Ribbon负载均衡器和Nginx负载均衡器有什么区别
后端
间彧14 分钟前
Nacos详解与项目实战
后端
间彧15 分钟前
nginx、网关Gateway、Nacos、多个服务实例之间的数据链路详解
后端
间彧17 分钟前
Nacos与Eureka在性能上有哪些具体差异?
后端
间彧18 分钟前
详解Nacos健康状态监测机制
后端
间彧20 分钟前
如何利用Nacos实现配置的灰度发布?
后端
毕业设计制作和分享32 分钟前
springboot159基于springboot框架开发的景区民宿预约系统的设计与实现
java·spring boot·后端
计算机学长felix2 小时前
基于SpringBoot的“中学信息技术课程教学网站”的设计与实现(源码+数据库+文档+PPT)_2025-10-17
数据库·spring boot·后端