【C++】Windows 下 TCP接口超详介绍,如何实现一个TCP服务端和客户端

发布于:2025-08-19 ⋅ 阅读:(11) ⋅ 点赞:(0)

目录

核心概念 socket

TCP

TCP服务端

主要逻辑流程

1、初始化Winsock环境

2、创建服务端监听套接字

涉及函数接口

socket函数

3、将创建的监听套接字绑定到指定的ip和接口

涉及函数接口

sockaddr_in结构体

inet_pton 函数

bind函数

4、开始监听套接字

涉及函数接口

listen函数

accept函数

5、数据收发

涉及函数介绍

send函数

recv函数

TCP客户端

主要逻辑流程

1、初始化Winsock环境

2、创建客户端连接套接字

3、连接服务端

connect函数

4、数据发送


        在这篇文章中,会创建TCP服务端和客户端、UDP服务端和客户端,让他们进行交互,在这中间穿插着介绍相关接口,帮助大家更好的理解。

核心概念 socket

        在开始学习前,我们首先要了解socket是什么,socket是一个由操作系统提供的、用于代表一条网络连接的、独一无二的整数标识符。

        他不是连接的本身,而是连接的代号,我们可以通过这个代号,来命令操作系统对真正的连接进行操作(发送、接收、关闭连接)。

        我们在使用所有跟网络通信的函数的时候,比如connect、send、recv、closesocket,把这个数字传给他们,这样操作系统就知道我们是要对哪条连接操作。

TCP

TCP服务端

主要逻辑流程

1、初始化Winsock环境

        因为在windows中,使用C++进行网络编程时,我们实际上时在直接调用windows操作系统提供的Winsock接口,Winsock的功能代码被封装在一个Ws2_32.dll的动态链接库文件中,而因为绝大多数windows程序是不需要网络功能的,windows采取按需服务模式,只有程序表示他需要网络功能的时候,操作系统才会为他加载Ws2_32.dll,并初始化相关的资源,下面的代码就是在通知windows,该程序需要使用到Ws2_32.dll。

头文件

#include <WinSock2.h>

#include <ws2tcpip.h>  
#pragma comment(lib, "Ws2_32.lib")

	WSADATA wsaData;
	int nResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (nResult != 0) {
		std::cerr << "WSAStartup 失败,错误码:" << nResult << std::endl;
		return -1;
	}
  • MAKEWORD(2, 2)这个参数是告诉操作系统,我们希望使用2.2版本的Winsock功能,通过 wsaData 结构体返回它实际能提供的版本信息。

2、创建服务端监听套接字

	m_listenSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (m_listenSocket == INVALID_SOCKET) {
		std::cerr << "创建套接字失败,错误码:" << WSAGetLastError() << std::endl;
		return -2;
	}
涉及函数接口
socket函数

SOCKET WSAAPI socket(

          int af,

          int type,

          int protocol

);

参数介绍

1)af(地址族)

这个参数指定网络通信使用的地址格式。

常用参数:

  • AF_INET:用于IPv4地址族
  • AF_INET6:用于IPv6地址族
  • AF_UNIX:域间套接字专用
  • AF_UNSPEC:未指定

2)type(套接字类型)

这个参数定义套接字的通信语义

常用参数:

  • SOCK_STREAM (最常用):流套接字,用于TCP协议
  • SOCK_DGRAM:数据报套接字,用于UDP协议

3)protocol(协议)

这个参数用来指定在前两个参数确定的情况下,具体使用哪个协议

常用参数:

  • 0:让系统根据af和type自动选择最合适的协议
  • IPPROTO_TCP:显示指定使用TCP协议
  • IPPROTO_UDP:显示指定使用UCP协议

返回值

  • 成功:返回一个 SOCKET 类型的值。这是一个 句柄 ,一个非负整数,是新创建套接字的唯一标识。后续所有网络函数( bind , connect , send 等)都将使用这个句柄作为第一个参数。
  • 失败:在 Windows 上,返回 INVALID_SOCKET (通常是 (SOCKET)(~0) ),在 Linux 上,返回 -1。

3、将创建的监听套接字绑定到指定的ip和接口

将套接字绑定IP和端口,就是服务端告诉操作系统,这个套接字负责所绑定的IP和端口,当客户端往这个IP和端口发送数据时,我们传入监听套接字,通过accept接收即可。

示例代码:

	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	inet_pton(AF_INET, sIp.c_str(), &serverAddr.sin_addr.s_addr);
	serverAddr.sin_port = htons(nPort);
	/*绑定*/
	if (SOCKET_ERROR==bind(m_listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr))) {
		std::cerr<<"绑定失败,错误码:"<<WSAGetLastError()<<std::endl;
		closesocket(m_listenSocket);
		WSACleanup();
		return -3;
	}
涉及函数接口
sockaddr_in结构体
struct sockaddr_in {
    short           sin_family;     // 地址族 (Address Family)
    unsigned short  sin_port;       // 端口号 (Port Number)
    struct in_addr  sin_addr;       // IP 地址 (IP Address)
    char            sin_zero[8];    // 填充位 (Padding)
};

1)sin_family

常用参数值:

  • AF_INET:指定使用IPv4
  • AF_INET6:指定使用IPv6

2)sin_port

  • 端口号是一个16位的数字(0-65535),在使用时必须使用htons()函数转换

3)sin_addr.s_addr

这是一个32位的无符号整数,在使用时通常用inet_addr()函数或者INADDR_ANY宏来赋值

  • INADDR_ANY绑定到本机所有可用的网络接口
struct in_addr {
    unsigned long s_addr; // 存放一个32位的IPv4地址
};
inet_pton 函数

        inet_pton 函数的作用是将一个以 字符串形式表示的、人类可读的IP地址 (例如 "192.168.1.1" 或 "2001:db8::1"),转换成它在网络中传输时使用的 二进制网络字节序格式 。

函数原型

INT WSAAPI inet_pton(
  [in]  INT   Family,
  [in]  PCSTR pszAddrString,  /*PCSTR (在 C++ 中就是 const char* )*/
  [out] PVOID pAddrBuf        /*PVOID (在 C++ 中就是 void* )*/
);

参数介绍

1)Family

地址族 (Address Family) 。这个参数告诉函数,你提供的 IP 地址字符串是 IPv4 格式还是 IPv6 格式。

常用参数:

  • AF_INET : 用于指定pszAddrString是一个 IPv4 地址 (例如: "127.0.0.1")。
  • AF_INET6 : 用于pszAddrString是一个 IPv6 地址 (例如: "::1")。

2)pszAddrString

源字符串 (Source String) 。这是一个指向包含人类可读 IP 地址的字符串的指针。

3)pAddrBuf

目标缓冲区 (Destination Buffer) 。这是一个指向内存缓冲区的指针,函数会将转换后的二进制格式 IP 地址存放在这里

  • 如果 af 是 AF_INET ,这个缓冲区的大小必须至少是 sizeof(struct in_addr) (4 字节)。
  • 如果 af 是 AF_INET6 ,这个缓冲区的大小必须至少是 sizeof(struct in6_addr) (16 字节)。
  • 通常,这个指针会指向一个 sockaddr_in 或 sockaddr_in6 结构体中的地址成员。

返回值

  • 1:转换成功
  • 0:输入字符串无效,这里的字符串也就是要转换成二进制网络字节序的字符串。
  • -1:发生错误
bind函数

      bind 函数的作用是将一个之前用 socket() 函数创建的 套接字(socket) 与一个具体的 本地 IP 地址和端口号 进行**“绑定”**。

     可以把它理解为给你的房子(套接字)装上一个明确的门牌号(IP地址 + 端口号)。当网络上发往这个“门牌号”的数据包到达时,操作系统就知道应该把这个数据包交给你的这个套接字来处理。

     客户端程序通常 不需要 调用 bind ,因为操作系统会自动为客户端的套接字分配一个临时的、未被占用的端口号。

    函数原型

    int WSAAPI bind(
      [in] SOCKET                s,
      [in] const struct sockaddr *name,
      [in] int                   namelen
    );

    参数介绍

    1)s

    套接字描述符 (Socket Descriptor) 。这是一个整数值,它代表了你要进行绑定的那个套接字。

    2)name

    套接字地址结构 (Socket Address Structure) 。这是一个指向特定地址结构体的指针,包含了要绑定的 IP 地址和端口号信息。

    • 这是一个 通用指针类型 。在实际编程中,我们从不直接使用 struct sockaddr 。
    • 对于 IPv4 ,我们创建一个 struct sockaddr_in 结构体,填充好它,然后将它的指针 强制类型转换 为 (const struct sockaddr *) 再传给 bind 函数。

    3)namelen

    地址结构体的大小 。这个参数告诉 bind 函数,你传入的第二个参数 name 指向的那个结构体到底有多大。

    4、开始监听套接字

    	/*第四步:开始监听*/
    	if (listen(m_listenSocket, SOMAXCONN) == SOCKET_ERROR) {
    		std::cerr << "listen 监听失败:" << WSAGetLastError() << std::endl;
    		closesocket(m_listenSocket);
    		WSACleanup();
    		return -1;
    	}
    	while (true) {
    		sockaddr_in clientAddr;
    		int nAddrSize = sizeof(clientAddr);
    		SOCKET clientSocket = accept(m_listenSocket, (SOCKADDR*)&clientAddr, &nAddrSize);
    		if (clientSocket == INVALID_SOCKET) {
    			std::cerr << "accept 失败:" << WSAGetLastError() << std::endl;
    			continue;
    		}
    		std::cout << "主线程:新用户上线,分机号是:" << clientSocket << std::endl;
    
    		/*开启新线程处理*/
    		std::thread clientThread(
    			&TcpServer::handleClient,  // 成员函数指针
    			this,                      // 对象实例
    			clientSocket,              // 第一个参数
    			clientAddr                // 第二个参数
    		);
    		clientThread.detach();
    	}
    涉及函数接口
    listen函数

            listen 函数的作用是将一个已经用 bind 绑定了地址的 套接字(socket) 从一个主动的、可以发起连接(用 connect )的套接字,转换成一个 被动的、专门用于“监听”的套接字 。

    函数原型

    int WSAAPI listen(
      [in] SOCKET s,
      [in] int    backlog
    );

    参数介绍

    1)s

    套接字描述符 (Socket Descriptor),传入我们用于监听的套接字。

    2)backlog

    设置等待队列的最大长度。

    • 当服务器非常繁忙,来不及用 accept 函数处理新的客户端连接请求时,操作系统会为这个监听套接字维护一个队列,用来存放那些已经完成了 TCP 三次握手、正在等待被服务器应用程序接受的连接。
    • backlog 参数就指定了这个队列的最大长度。
    • 如果队列已满,此时再有新的客户端尝试连接,服务器的 TCP/IP 协议栈会 拒绝 这个新的连接请求。客户端的 connect 函数会立刻失败,通常返回 ECONNREFUSED 错误。

    在现代操作系统中,直接使用宏 SOMAXCONN 是 最推荐 的做法。 SOMAXCONN 是一个系统定义的常量,它会自动为 backlog 设置一个合理的、足够大的值。这使得你的代码更具可移植性。

    返回值

    0:表示套接字已经进入监听状态了

    -1/SOCKET_ERROR:失败,失败的原因可能包括:

    • EBADF (POSIX) / WSAENOTSOCK (Windows) : 提供的套接字描述符不是一个有效的套接字。

    • EOPNOTSUPP (POSIX) / WSAEOPNOTSUPP (Windows) : 该套接字类型不支持 listen 操作(例如,你不能在一个 UDP 套接字上调用 listen )。

    • WSAEINVAL (Windows) : 套接字还没有被 bind 绑定,或者已经在监听状态

    accept函数

            accept是一个阻塞函数,如果没有连接请求,会暂停程序执行。

    函数原型

    SOCKET WSAAPI accept(
      [in]      SOCKET   s,
      [out]     sockaddr *addr,
      [in, out] int      *addrlen
    );

    1)SOCKET s

    • 第一个参数传入的是服务端的监听套接字,也就是已经经过 socket() , bind() , listen() 标准三步曲初始化的 监听套接字。

    2)sockaddr *addr

    • 第二个参数是一个输出型参数,通常我们传入一个 sockaddr_in 结构体变量的地址,并将其强制类型转换为 (sockaddr*) ,当接收到客户端的信息时,accept函数会填入这个参数。

    3)int *addrlen 

    • 地址信息的大小,我们需要先告诉 accept 函数,为第二个参数 addr 准备的“登记表”有多大。

    返回值

    • 成功:返回一个全新的SOCKET句柄,所有与这个客户端进行数据交互操作都是用这个新的套接字
    • 失败:返回INVALID_SOCKET,可以通过WSAGetLastError()获取失败原因。

    5、数据收发

    void TcpServer::handleClient(const SOCKET& clientSocket, const sockaddr_in& ClientAddr) {
    
    	/*开始处理*/
    	char recvBuf[MAX_LEN];
    	memset(recvBuf, 0, sizeof(recvBuf));
    	char cIpBuffer[MAX_LEN];
    	inet_ntop(AF_INET, &ClientAddr.sin_addr.s_addr, cIpBuffer, MAX_LEN);
    
    	while (recv(clientSocket, recvBuf, MAX_LEN, 0) > 0) {
    		std::cout << "收到" << cIpBuffer << "发送的" << recvBuf << std::endl;
    		const char* sendBuf = "已收到您发的消息" ;
    		send(clientSocket, sendBuf, strlen(sendBuf),0);
    	}
    }
    涉及函数介绍

    send 和 recv 是用于在 已连接 的、面向流的套接字(如 TCP 套接字)上进行数据传输的两个核心函数。

    • send : 发送 数据到对方。
    • recv : 从对方 接收 数据。
    send函数

    函数原型

    int WSAAPI send(
      [in] SOCKET s,
      [in] const char *buf,
      [in] int len,
      [in] int flags
    );

    1)s

    已连接的套接字描述符 。

    在 服务器端 ,这是 accept 函数返回的那个 新的 套接字描述符,代表与特定客户端的连接。
    在 客户端 ,这是调用 connect 时使用的那个套接字。

    2)buf

     数据缓冲区指针 。一个指向内存中要发送数据的缓冲区的指针。

    3)len

     要发送的数据长度 (单位:字节)。即你想从 buf 中发送多少字节的数据。

    4)flags

    发送标志 。通常情况下,这个参数被设置为 0 。它允许一些特殊的发送行为,但对于常规的 TCP 通信, 0 是最常用和最标准的设置。

    返回值

    • 成功 ( > 0 ) : 返回 实际发送出去的字节数。
    • 失败 (SOCKET_ERROR / -1) : 发送失败。
    recv函数

    函数原型

    int WSAAPI recv(
      [in]  SOCKET s,
      [out] char *buf,
      [in]  int len,
      [in]  int flags
    );

    参数介绍

    1)s 

    已连接的套接字描述符 (同 send )。

    2)buf 

    接收缓冲区指针 。一个指向用于存放接收到数据的缓冲区的指针。你必须保证这个缓冲区有足够的空间。

    3)len

    接收缓冲区的最大长度 (单位:字节)。这个参数告诉 recv 函数,你的缓冲区最多能装多少字节的数据,以防止缓冲区溢出。

    4)flags

    接收标志 。通常情况下,这个参数被设置为 0 。

    返回值

    • 成功 ( > 0 ) : 返回 实际接收到的字节数 。
    • 连接已正常关闭 ( == 0 ) : 返回值为 0 是一个明确的信号,表示 对方已经调用 close 或 shutdown 正常关闭了连接 。
    • 失败 ( < 0 ) : 即 SOCKET_ERROR (Windows) 或 -1 (POSIX)。表示发生了错误。

    TCP客户端

    主要逻辑流程

    1、初始化Winsock环境

    客户端这里与服务端一样,同样需要先初始化Winsock环境

    	WSADATA wsaData;
    	int nResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    	if (nResult != 0) {
    		std::cerr << "WSAStartup 失败,错误码:" << nResult << std::endl;
    		return -1;
    	}

    2、创建客户端连接套接字

        SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);
        if (clientSocket == INVALID_SOCKET) {
            std::cerr << "准备套接字失败,错误码:" << WSAGetLastError()<< std::endl;
            return -2;
        }

    3、连接服务端

    在连接之前,要先准备我们要连接的服务端ip地址信息

        /*准备要连接的服务端地址信息*/
        sockaddr_in serverAddr;
        serverAddr.sin_family = AF_INET;
        inet_pton(AF_INET, sIp.c_str(), &serverAddr.sin_addr.s_addr);
        serverAddr.sin_port = htons(nPort);

    通过connect接口连接服务端

    if (connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
         std::cerr << "连接服务器失败,错误码:" << WSAGetLastError() << std::endl;
         return -3;
    }
    connect函数

    函数原型

    int connect(
      SOCKET s,
      const sockaddr* name,
      int namelen
    );

    参数介绍

    1)SOCKET s

    • 客户端创建的连接套接字

    2)const sockaddr* name

    • 这个是我们创建用来描述要连接的服务端的IP地址信息,具体的参考前面sockaddr_in结构体介绍

    3)int namelen

    • 第二个参数结构体的大小

    返回值

    • 成功:返回0
    • 失败:返回非0

    4、数据发送

    int nSendSize = send(clientSocket, sInput.c_str(), sInput.size(), 0);
    if (nSendSize == SOCKET_ERROR) {
        std::cerr << "send failed with error: " << WSAGetLastError() << std::endl;
        closesocket(clientSocket);
        WSACleanup();
        return -4;
    }

    上面服务端已经介绍了send和recv函数,这里就不再介绍了。


      网站公告

      今日签到

      点亮在社区的每一天
      去签到