深入理解C/C++套接字编程:从基础到实践(超详细)

深入理解C/C++套接字编程:从基础到实践

前言

网络编程是现代软件开发中不可或缺的技能,而套接字(Socket)编程则是网络通信的基石。无论是在Windows还是Linux平台上,Socket编程的核心思想都是相通的。本文将深入探讨C/C++语言下的Socket编程,从基础概念到实际应用,通过丰富的代码示例帮助读者全面掌握这一重要技术。

套接字编程看似复杂,实则遵循着清晰的模式。无论是TCP还是UDP协议,无论是客户端还是服务器端,其基本流程都有章可循。本文将通过逐步分解的方式,让读者理解网络通信的每一个环节,并能够编写出自己的网络程序。

需要特别说明的是:为保持示例的简洁性和易理解性,本文中的代码未引入多线程机制,所有的例子都是单线程阻塞式的。在实际生产环境中,根据需求可能需要考虑多线程、非阻塞I/O或异步I/O等技术。此外,所有示例都运行在Windows平台,但代码的核心逻辑在Linux平台上同样适用,只需进行少量修改(主要是头文件和库函数的变化)。

调试助手推荐

在学习网络编程时,使用一个网络调试助手可以极大地提高学习效率。通过调试助手,我们可以直观地查看网络数据、模拟各种网络场景,从而更好地理解网络通信的原理。

1. 野人家园的NetAssist网络调试助手

这是一个功能全面的网络调试工具,支持TCP/UDP协议,既可以作为客户端也可以作为服务器,非常适合初学者使用。

下载地址NetAssist网络调试助手-软件工具-野人家园

2. 野火的多功能调试助手

野火调试助手不仅支持网络调试,还支持串口、CAN总线等多种通信方式,界面友好,功能强大。

下载地址野火多功能调试助手上位机 --- 野火产品资料下载中心

3. 本人的网络助手

如果你对Qt感兴趣,也可以尝试我基于Qt开发的网络助手。这个工具完全开源,你可以查看源代码,了解其实现原理。

下载地址Qt网络助手_网络助手发送集成版-CSDN博客

GitHub仓库Network_Assistant: Qt自制网络助手

基础知识

在深入套接字编程之前,我们需要先理解一些基础概念。这些概念是理解网络编程的基石,掌握它们将帮助我们更好地理解后续的代码示例。

大小端(Endianness)

大小端是计算机系统中一个重要的概念,特别是在跨平台网络通信中。不同的处理器架构采用不同的字节序,这导致了数据在内存中的存储顺序不同。

说明

大端序(Big-Endian)

  • 高位字节存储在低地址,低位字节存储在高地址
  • 符合人类的阅读习惯(从左到右 = 从高到低)
  • 以十六进制数0x01234567为例(假设起始地址为0x1000):
内存地址 0x1000 0x1001 0x1002 0x1003
存储内容 01 23 45 67

重要提示:🌐 网络字节序(Network Byte Order)就是大端序!所有TCP/IP协议规定:IP地址、端口号等多字节字段必须以大端序传输。

小端序(Little-Endian)

  • 低位字节存储在低地址,高位字节存储在高地址
  • 绝大多数PC和手机CPU都采用小端序
  • 同样以0x01234567为例:
内存地址 0x1000 0x1001 0x1002 0x1003
存储内容 67 45 23 01

注意:💻 主机字节序(Host Byte Order)在大多数PC上是小端序。

小端序之所以被广泛采用,是因为它在CPU运算中更为方便。对于加法运算,从最低位开始计算更为自然,这与小端序的存储方式一致。

相关函数

在网络编程中,我们需要在主机字节序和网络字节序之间进行转换。以下是一组重要的转换函数:

htons() ------ host to network short(16位)

c 复制代码
uint16_t htons(uint16_t hostshort);
  • 作用:将16位无符号整数从主机字节序转为网络字节序
  • 参数:hostshort ------ 主机字节序的16位值
  • 返回值:网络字节序的16位值

ntohs() ------ network to host short(16位)

c 复制代码
uint16_t ntohs(uint16_t netshort);
  • 作用:将16位无符号整数从网络字节序转为主机字节序
  • 参数:netshort ------ 网络字节序的16位值
  • 返回值:主机字节序的16位值

htonl() ------ host to network long(32位)

c 复制代码
uint32_t htonl(uint32_t hostlong);
  • 作用:将32位无符号整数从主机字节序转为网络字节序
  • 参数:hostlong ------ 主机字节序的32位值
  • 返回值:网络字节序的32位值

ntohl() ------ network to host long(32位)

c 复制代码
uint32_t ntohl(uint32_t netlong);
  • 作用:将32位无符号整数从网络字节序转为主机字节序
  • 参数:netlong ------ 网络字节序的32位值(如从socket接收的协议字段)
  • 返回值:主机字节序的32位值

IP地址简介

IP地址是网络中设备的唯一标识符。了解IP地址的分类和表示方法是网络编程的基础。

IPv4(Internet Protocol version 4)
  • 位数:32位(4字节)

  • 人类可读格式:点分十进制(Dotted Decimal Notation)

  • 将32位地址分为4段,每段8位(1字节),转换为十进制后用点.分隔:

    复制代码
    11000000.10101000.00000001.00001010 → 192.168.1.10
  • 地址空间:约43亿个(2³² ≈ 4.3×10⁹)

  • 特殊地址:

    • 127.0.0.1:本地回环地址(loopback),代表本机
    • 0.0.0.0:表示"任意IP"或"未指定地址",常用于服务器绑定
    • 255.255.255.255:广播地址
IPv6(Internet Protocol version 6)
  • 位数:128位(16字节)
  • 人类可读格式:冒号分隔的十六进制(Hexadecimal Colon Notation)
  • 将128位地址分为8段,每段16位(2字节),用十六进制表示,段间用冒号:分隔
  • 示例:2001:0db8:85a3:0000:0000:8a2e:0370:7334
  • 可简写为:2001:db8:85a3::8a2e:370:7334(连续的0可用::代替,但只能用一次)
  • 特殊地址示例:
    • ::1:IPv6的本地回环地址
    • :::表示"任意IP"或"未指定地址"
私有IP地址

以下范围内的IP地址是私有IP地址(Private IP),只能在局域网内部使用:

  • 10.0.0.0 -- 10.255.255.255
  • 172.16.0.0 -- 172.31.255.255
  • 192.168.0.0 -- 192.168.255.255

互联网上的其他设备无法直接访问私有IP地址,需要通过NAT(网络地址转换)技术才能与外部通信。

查看本机IP地址(Windows)

可以通过以下方式查看本机IP地址:

  1. 从网络和共享中心查看:直接在Windows设置中查看网络状态
  2. 从命令行查看 :使用ipconfig命令

示例:

cmd 复制代码
Microsoft Windows [Version 10.0.19045.6466]
(c) Microsoft Corporation. All rights reserved.

C:\Users\Cai>ipconfig

Windows IP Configuration

Wireless LAN adapter WLAN:

   Connection-specific DNS Suffix  . :
   Link-local IPv6 Address . . . . . : fe80::50e:99e5:e69c:d797%14
   IPv4 Address. . . . . . . . . . . : 10.53.1.148
   Subnet Mask . . . . . . . . . . . : 255.255.240.0
   Default Gateway . . . . . . . . . : 10.53.0.1

在上面的示例中,我使用的是WiFi连接,IPv4地址是10.53.1.148,这是一个私有IP地址,由路由器分配。当我的设备需要访问外部网络时,数据包会先到达默认网关(路由器),然后由路由器转发到外部网络。

相关函数简介

inet_pton()函数:字符串IP → 二进制格式

c 复制代码
int inet_pton(int af, const char* src, void* dst);
  • 作用:将可读的IP字符串转换为网络协议所需的二进制格式
  • 参数:
    • af(Address Family):地址族,AF_INET(IPv4)或AF_INET6(IPv6)
    • src:指向IP字符串的指针
    • dst:指向目标缓冲区的指针
  • 返回值:
    • 1:成功
    • 0:输入不是有效IP地址
    • -1:af不支持

inet_ntop()函数:二进制格式 → 字符串IP

c 复制代码
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
  • 作用:将网络字节序的二进制IP地址转换可读的IP字符串,并存入用户提供的缓冲区
  • 参数:
    • af(Address Family):地址族
    • src:指向二进制IP地址的指针
    • dst:指向目标字符数组的指针,用于存放转换后的字符串
    • size:dst缓冲区的大小(以字节为单位)
  • 返回值:
    • 成功:返回转换后的字符串地址(即dst指针)
    • 失败:返回nullptr

端口(Port)简介

端口是设备中的逻辑编号,用于区分同一设备上不同的网络应用程序或服务。

  • 位数:16位无符号整数(0-65535)
  • 作用:在一台设备上同时运行多个网络程序时,端口用于确保数据被正确投递给目标程序
  • 常用端口:
    • 0-1023:知名端口(Well-known ports),通常被系统服务占用
    • 1024-49151:注册端口(Registered ports),可供用户程序使用
    • 49152-65535:动态/私有端口(Dynamic/Private ports)

注意:在进行测试时,通常选择5000以上的端口号,避免与系统服务冲突。

套接字(Socket)简介

什么是套接字(Socket)

套接字是操作系统提供的一种通信端点(endpoint)抽象,用于在两个网络程序之间进行数据交换。它屏蔽了底层网络协议(如TCP/IP、UDP/IP)的复杂性,让程序员只需关注"发送"和"接收"数据,而无需关心数据如何穿越路由器、如何分片重组等细节。

可以将套接字理解为"网络通信的插座"------一端插在你的程序里,另一端通过网络连接到对方程序。

相关结构体、函数说明

SOCKET类型本质

c 复制代码
typedef UINT_PTR SOCKET;
  • 本质:是一个无符号整数类型
  • 作用:作为套接字的句柄(handle),用于标识一个网络连接或监听端点

socket()函数:创建套接字

c 复制代码
SOCKET socket(int af, int type, int protocol);
  • 作用:创建一个新的通信端点(套接字)
  • 参数:
    • af(Address Family):地址族,AF_INET(IPv4)或AF_INET6(IPv6)
    • type(Socket Type):通信方式,SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)
    • protocol:协议(通常设为0,表示自动选择)
  • 返回值:
    • 成功:返回一个有效的SOCKET值
    • 失败:返回INVALID_SOCKET

closesocket()函数:关闭套接字

c 复制代码
int closesocket(SOCKET s);
  • 作用:关闭一个套接字,释放相关资源
  • 参数:
    • s:要关闭的套接字
  • 返回值:
    • 0:成功关闭
    • -1SOCKET_ERROR):关闭失败

sockaddr_in结构体(IPv4地址)

c 复制代码
struct sockaddr_in {
    short          sin_family;   // 地址族,必须为AF_INET
    unsigned short sin_port;     // 端口号(网络字节序!)
    struct in_addr sin_addr;     // IPv4地址(网络字节序!)
    char           sin_zero[8];  // 填充字段,必须置0
};

struct in_addr {
    uint32_t s_addr; // 实际存储IP的4字节(如0x7F000001=127.0.0.1)
};
  • 用途:描述一个IPv4网络地址(IP+端口)
  • 初始化建议:sockaddr_in addr = {}; // 零初始化,确保sin_zero全0

bind()函数:将套接字绑定到本地地址和端口

c 复制代码
int bind(SOCKET s, const struct sockaddr* addr, socklen_t namelen);
  • 作用:将套接字关联到本机的一个IP地址和端口号,使其具备"身份标识"
  • 参数:
    • s:由socket()创建的套接字
    • addr:指向地址结构体的指针(如sockaddr_in
    • namelen:地址结构体的实际大小
  • 返回值:
    • 0:绑定成功
    • -1(或SOCKET_ERROR):失败

注意事项

  1. 服务器必须调用bind()来指定监听的IP和端口
  2. 客户端通常不需要显式bind(),操作系统会在connect()时自动分配一个临时端口和本地IP
  3. 在同一个协议(TCP/UDP)下,任意两个套接字不能同时绑定到完全相同的(IP地址, 端口号)组合

TCP(传输控制协议)

简介

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接、可靠、有序、基于字节流的传输层协议。它是互联网上绝大多数关键应用(如网页浏览、文件传输、电子邮件)的基础。

TCP的主要特点:

  • 面向连接:通信前必须先建立连接
  • 可靠传输:通过确认机制、重传机制保证数据不丢失、不重复
  • 有序传输:数据按发送顺序到达接收方
  • 流量控制:防止发送方发送过快导致接收方缓冲区溢出
  • 拥塞控制:根据网络状况动态调整发送速率

TCP严格区分客户端与服务器:

  • 服务器(Server):被动等待连接请求
  • 客户端(Client):主动发起连接请求
三次握手(Three-Way Handshake)------建立连接

TCP在传输数据前,必须通过三次握手建立可靠连接:

复制代码
客户端                     服务器
  |        SYN=1, seq=x        |
  | -------------------------> |  (1) 客户端请求连接
  |        SYN=1, ACK=1,       |
  |        seq=y, ack=x+1      |
  | <------------------------- |  (2) 服务器确认并回应
  |        ACK=1, ack=y+1      |
  | -------------------------> |  (3) 客户端确认
  |                            |
  |        ← 连接已建立 →        |
四次挥手(Four-Way Wavehand)------关闭连接

由于TCP连接是全双工(双方可同时收发),关闭时需分别关闭两个方向,因此通常需要四次交互:

复制代码
主动关闭方(如客户端)       被动关闭方(服务器)
       |        FIN=1, seq=u        |
       | -------------------------> |  (1) 客户端:我数据发完了
       |        ACK=1, ack=u+1      |
       | <------------------------- |  (2) 服务器:收到,但我还可能有数据发你
       |        FIN=1, seq=v        |
       | <------------------------- |  (3) 服务器:我也发完了
       |        ACK=1, ack=v+1      |
       | -------------------------> |  (4) 客户端:收到,再见

相关函数

connect()函数:发起TCP连接

c 复制代码
int connect(SOCKET s, const struct sockaddr* name, int namelen);
  • 作用:主动向服务器发起TCP三次握手
  • 参数:
    • s:由socket()创建的客户端套接字
    • name:指向服务器地址结构的指针
    • namelen:地址结构大小
  • 返回值:
    • 0:连接成功
    • SOCKET_ERROR(即-1):失败
  • 注意:默认为阻塞模式,会阻塞线程直到连接成功或失败!

listen()函数:将套接字设为监听状态

c 复制代码
int listen(SOCKET s, int backlog);
  • 作用:将一个已绑定的TCP套接字转为被动监听模式
  • 参数:
    • s:已绑定(bind)的TCP套接字
    • backlog:等待连接队列的最大长度
  • 返回值:
    • 0:成功进入监听状态
    • -1(或SOCKET_ERROR):失败
  • 注意 :仅用于服务器端,必须在accept()之前调用!

accept()函数:接受一个客户端连接

c 复制代码
SOCKET accept(SOCKET s, struct sockaddr* addr, socklen_t* addrlen);
  • 作用:从监听套接字的连接请求队列中取出一个已完成三次握手的连接
  • 参数:
    • s:处于监听状态的服务器套接字
    • addr:可选,用于获取客户端地址信息
    • addrlen:传入/传出参数,地址结构体大小
  • 返回值:
    • 成功:返回一个全新的已连接套接字描述符
    • 失败:返回INVALID_SOCKET
  • 注意:默认为阻塞模式!当没有新连接时会阻塞当前线程!

recv()函数:接收TCP数据

c 复制代码
int recv(SOCKET s, char* buf, int len, int flags);
  • 作用:从已连接的TCP套接字接收数据
  • 参数:
    • s:已连接的套接字
    • buf:接收缓冲区
    • len:缓冲区最大长度
    • flags:通常为0
  • 返回值:
    • > 0:实际接收到的字节数
    • 0:对端正常关闭连接(收到FIN)
    • -1SOCKET_ERROR):发生错误
  • 注意:默认为阻塞模式!在没有数据可读时会阻塞当前线程!

send()函数:发送TCP数据

c 复制代码
int send(SOCKET s, const char* buf, int len, int flags);
  • 作用:通过已连接的TCP套接字向对端发送数据
  • 参数:
    • s:已连接的套接字
    • buf:指向要发送的数据缓冲区
    • len:要发送的字节数
    • flags:通常为0
  • 返回值:
    • > 0:成功发送的字节数
    • -1SOCKET_ERROR):发送失败

TCP客户端程序示例

例1-只能接收数据的TCP客户端

这是一个简单的TCP客户端,它只能接收服务器发送的数据,而不能向服务器发送数据。

cpp 复制代码
// 只接收服务器数据的TCP客户端(Windows版)

#include <iostream>
#include <string>
using namespace std;

#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main(void)
{
    cout << "只能接收服务器数据的TCP客户端" << endl;
    
    // 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
    WSADATA wsaData;
    int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (wsaResult != 0)
    {
        cerr << "WSAStartup failed: " << wsaResult << endl;
        return 1;
    }

    // 创建通信的套接字
    cout << "创建套接字" << endl;
    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket == INVALID_SOCKET)
    {
        cerr << "套接字创建失败" << endl;
        cerr << "错误码 " << WSAGetLastError() << endl;
        WSACleanup();
        return 1;
    }

    // 由用户输入目标的IP地址和端口
    cout << "\n请输入服务器IP地址" << endl;
    char targetIP[128] = { 0 };
    cin >> targetIP;
    cout << "请输入目标端口" << endl;
    uint16_t targetPort = UINT16_MAX;
    cin >> targetPort;

    // 要连接的服务器IP端口    
    sockaddr_in targetAddr = { 0 };
    targetAddr.sin_family = AF_INET;  // 设置地址簇
    targetAddr.sin_port = htons(targetPort);  // 设置目标的端口
    inet_pton(AF_INET, targetIP, &targetAddr.sin_addr.s_addr);  // 将字符串IP转为二进制格式

    // 发起TCP连接,主动向服务器发起TCP三次握手
    cout << "\n\n发起TCP连接" << endl;
    // 默认为阻塞模式,调用后线程被挂起!!!直到连接成功或者失败
    int result = connect(clientSocket, (sockaddr*)&targetAddr, sizeof(targetAddr));
    if (result == SOCKET_ERROR)
    {
        cerr << "连接失败 " << endl;
        cerr << "错误码 " << WSAGetLastError() << endl;
        return 1;
    }

    cout << "\nTCP连接成功开始接收数据\n\n" << endl;
    // 接收数据
    while (1)
    {
        char receiveBuffer[1024] = { 0 };  // 缓冲区
        // 接收数据,默认没有数据可读时阻塞当前线程!!!!
        int bytesReceived = recv(clientSocket, receiveBuffer, sizeof(receiveBuffer), 0);
        if (bytesReceived > 0)
        {
            cout << "接收到了 " << bytesReceived << " 字节数据:" << endl;
            cout << receiveBuffer << endl;
        }
        else if (bytesReceived == 0)
        {
            cout << "服务器断开了连接" << endl;
            break;
        }
        else
        {
            cerr << "出现错误" << endl;
            cerr << "错误码 " << WSAGetLastError() << endl;
            break;
        }
    }
        
    closesocket(clientSocket);  // 关闭描述符
    WSACleanup();  // Windows下需要清理
    cout << "\n\n客户端程序停止运行" << endl;
    
    return 0;
}

使用说明

  1. 编译并运行该程序
  2. 输入服务器的IP地址(如127.0.0.1)和端口号(如8080)
  3. 程序会尝试连接到指定的服务器
  4. 连接成功后,程序会等待接收服务器发送的数据
  5. 当服务器断开连接时,程序会退出

注意事项

  • 这是一个阻塞式的客户端,在等待连接和接收数据时会阻塞当前线程
  • 程序没有错误恢复机制,一旦出错就会退出
  • 只能接收数据,不能发送数据
例2-只能发送数据的TCP客户端

这个TCP客户端只能向服务器发送数据,而不能接收服务器发送的数据。

cpp 复制代码
// 只能向服务器发送数据的TCP客户端(Windows版)

#include <iostream>
#include <string>
using namespace std;

#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main(void)
{
    cout << "只能向服务器发送数据的TCP客户端" << endl;

    // 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
    WSADATA wsaData;
    int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (wsaResult != 0)
    {
        cerr << "WSAStartup failed: " << wsaResult << endl;
        return 1;
    }

    // 创建通信的套接字
    cout << "创建套接字" << endl;
    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket == INVALID_SOCKET)
    {
        cerr << "套接字创建失败 错误码 " << WSAGetLastError() << endl;
        WSACleanup();
        return 1;
    }

    // 由用户输入目标的IP地址和端口
    cout << "\n请输入目标服务器IP地址" << endl;
    char targetIP[128] = { 0 };
    cin >> targetIP;
    cout << "请输入目标端口" << endl;
    uint16_t targetPort = UINT16_MAX;
    cin >> targetPort;

    // 连接服务器IP端口    
    sockaddr_in targetAddr = { 0 };
    targetAddr.sin_family = AF_INET;  // 设置地址簇
    targetAddr.sin_port = htons(targetPort);  // 设置目标的端口
    inet_pton(AF_INET, targetIP, &targetAddr.sin_addr.s_addr);  // 将字符串IP转为二进制格式

    // 发起TCP连接,主动向服务器发起TCP三次握手
    // 默认为阻塞模式,调用后线程被挂起!!!直到连接成功或者失败
    cout << "\n\n发起TCP连接" << endl;
    int result = connect(clientSocket, (sockaddr*)&targetAddr, sizeof(targetAddr));
    if (result == SOCKET_ERROR)
    {
        cerr << "连接失败 错误码 " << WSAGetLastError() << endl;
        return 1;
    }
    cout << "TCP连接成功" << endl;

    cout << "\n可以发送数据了  输入 __QUIT__ 停止发送\n" << endl;
    // 发送数据
    while (1)
    {
        cout << "\n请输入字符串" << endl;
        string userString;
        cin >> userString;
        if (userString.empty()) continue;
        if (userString == "__QUIT__")
        {
            break;
        }        

        // 发送数据
        int sendResult = send(clientSocket, userString.c_str(), userString.size(), 0);
        if (sendResult == SOCKET_ERROR)
        {
            cout << "发送失败 错误码: " << WSAGetLastError() << endl;
            continue;
        }
        else
        {
            cout << "成功发送 " << sendResult << "字节数据" << endl;
        }
    }

    closesocket(clientSocket);  // 关闭描述符
    WSACleanup();  // Windows下需要清理
    cout << "\n\n客户端程序停止运行" << endl;
    return 0;
}

使用说明

  1. 编译并运行该程序
  2. 输入服务器的IP地址和端口号
  3. 程序会尝试连接到指定的服务器
  4. 连接成功后,可以输入要发送的字符串
  5. 输入__QUIT__可以退出程序

注意事项

  • 这个程序只能发送数据,不能接收服务器返回的数据
  • 发送的数据是字符串格式
  • 每次发送后不会等待服务器的响应

TCP服务器程序示例

例1-只能接收数据的TCP服务器

这是一个简单的TCP服务器,它只能接收一个客户端连接,并且只能接收客户端发送的数据,不能向客户端发送数据。

cpp 复制代码
// 只能接收一个客户端连接且只能接收客户端数据的TCP服务器(Windows版)
#include <iostream>
#include <string>
using namespace std;

#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main(void)
{
    cout << "只能接收一个客户端连接且只能接收客户端数据的TCP服务器\n" << endl;

    // 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
    WSADATA wsaData;
    int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (wsaResult != 0)
    {
        cerr << "WSAStartup failed: " << wsaResult << endl;
        return 1;
    }

    // 创建监听的套接字,专门负责监听有没有客户端请求连接
    SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSocket == INVALID_SOCKET)
    {
        cerr << "监听套接字创建失败" << endl;
        cerr << "错误码 " << WSAGetLastError() << endl;
        return 1;
    }

    // 由用户输入本机的IP地址和要监听的端口
    // 0.0.0.0表示本机任意的IP地址
    cout << "\n请输入要绑定的本机IP地址" << endl;
    char localIP[128] = { 0 };
    cin >> localIP;
    cout << "请输入要监听的端口" << endl;
    uint16_t port = UINT16_MAX;
    cin >> port;

    // 设置服务器地址结构
    sockaddr_in serverAddr = { 0 };
    serverAddr.sin_family = AF_INET;  // 设置地址簇
    serverAddr.sin_port = htons(port);  // 设置要监听的端口
    // 将字符串IP转为二进制格式
    inet_pton(AF_INET, localIP, &serverAddr.sin_addr.s_addr);

    // 绑定
    cout << "\n\n进行绑定" << endl;
    if (bind(listenSocket, (sockaddr*)(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR)
    {
        cerr << "绑定失败,错误码: " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    cout << "绑定成功\n" << endl;

    // 设置监听
    cout << "设置监听" << endl;
    if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
    {
        cerr << "监听设置失败 错误码 " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    cout << "监听设置成功,等待客户端连接\n" << endl;

    sockaddr_in clientAddr = { 0 };
    int clientAddr_Len = sizeof(clientAddr);
    // 接受客户端的连接,默认阻塞线程等待客户端!!!
    SOCKET clientSocket = accept(listenSocket, (sockaddr*)(&clientAddr), &clientAddr_Len);
    if (clientSocket == INVALID_SOCKET)
    {
        cerr << "接受连接失败 错误码 " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    else
    {
        char clientIP[INET_ADDRSTRLEN] = { 0 };
        inet_ntop(AF_INET, &clientAddr, clientIP, sizeof(clientIP));
        uint16_t clientPort = ntohs(clientAddr.sin_port);
        cout << "接受客户端的连接" << endl;
        cout << "客户端IP " << clientIP << endl;
        cout << "客户端端口 " << clientPort << endl;
    }

    cout << "\nTCP连接成功开始接收数据\n\n" << endl;
    while (1)
    {
        char receiveBuffer[1024] = { 0 };  // 缓冲区
        // 接收数据,默认没有数据可读时阻塞当前线程!!!!
        int bytesReceived = recv(clientSocket, receiveBuffer, sizeof(receiveBuffer), 0);
        if (bytesReceived > 0)
        {
            cout << "接收到了 " << bytesReceived << " 字节数据:" << endl;
            cout << receiveBuffer << endl;
        }
        else if (bytesReceived == 0)
        {
            cout << "客户端断开了连接" << endl;
            closesocket(clientSocket);  // 关闭客户端套接字
            break;
        }
        else
        {
            cerr << "出现错误" << endl;
            cerr << "错误码 " << WSAGetLastError() << endl;
            break;
        }
    }
    
    closesocket(listenSocket);  // 关闭监听套接字
    WSACleanup();  // Windows下需要清理
    cout << "\n\n服务器程序停止运行" << endl;
    return 0;
}

使用说明

  1. 编译并运行该程序
  2. 输入要绑定的本地IP地址(如127.0.0.1或0.0.0.0)和端口号(如8080)
  3. 程序会开始监听指定端口的连接请求
  4. 当有客户端连接时,程序会接受连接并显示客户端信息
  5. 连接建立后,程序会等待接收客户端发送的数据
  6. 当客户端断开连接时,程序会退出

注意事项

  • 这个服务器只能处理一个客户端连接
  • 服务器是阻塞式的,在等待连接和接收数据时会阻塞当前线程
  • 服务器只能接收数据,不能向客户端发送数据
例2-只能发送数据的TCP服务器

这个TCP服务器只能接收一个客户端连接,并且只能向客户端发送数据,不能接收客户端发送的数据。

cpp 复制代码
// 只能接收一个客户端连接且只能向客户端发送数据的TCP服务器(Windows版)
#include <iostream>
#include <string>
using namespace std;

#include <WinSock2.h> // Windows下使用套接字需要包含
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main(void)
{
    cout << "只能接收一个客户端连接且只能向客户端发送数据的TCP服务器\n" << endl;

    // 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源
    WSADATA wsaData;
    int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (wsaResult != 0)
    {
        cerr << "WSAStartup failed: " << wsaResult << endl;
        return 1;
    }

    // 创建监听的套接字,专门负责监听有没有客户端连接
    SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSocket == INVALID_SOCKET)
    {
        cerr << "监听套接字创建失败" << endl;
        cerr << "错误码 " << WSAGetLastError() << endl;
        return 1;
    }

    // 由用户输入本机的IP地址和要监听的端口
    // 0.0.0.0表示本机任意的IP地址
    cout << "\n请输入要绑定的本机IP地址" << endl;
    char localIP[128] = { 0 };
    cin >> localIP;
    cout << "请输入要监听的端口" << endl;
    uint16_t port = UINT16_MAX;
    cin >> port;

    // 设置服务器地址结构
    sockaddr_in serverAddr = { 0 };
    serverAddr.sin_family = AF_INET;  // 设置地址簇
    serverAddr.sin_port = htons(port);  // 设置要监听的端口
    // 将字符串IP转为二进制格式
    inet_pton(AF_INET, localIP, &serverAddr.sin_addr.s_addr);

    // 绑定
    cout << "\n\n进行绑定" << endl;
    if (bind(listenSocket, (sockaddr*)(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR)
    {
        cerr << "绑定失败,错误码: " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    cout << "绑定成功\n" << endl;

    // 设置监听
    cout << "设置监听" << endl;
    if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
    {
        cerr << "监听设置失败 错误码 " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    cout << "开始监听\n" << endl;

    sockaddr_in clientAddr = { 0 };
    int clientAddr_Len = sizeof(clientAddr);
    // 接受客户端的连接,默认阻塞线程等待客户端!!!
    cout << "等待客户端连接" << endl;
    SOCKET clientSocket = accept(listenSocket, nullptr, nullptr);
    if (clientSocket == INVALID_SOCKET)
    {
        cerr << "接受连接失败 错误码 " << WSAGetLastError() << endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    else
    {
        char clientIP[INET_ADDRSTRLEN] = { 0 };
        inet_ntop(AF_INET, &clientAddr, clientIP, sizeof(clientIP));
        uint16_t clientPort = ntohs(clientAddr.sin_port);
        cout << "接受客户端的连接" << endl;
        cout << "客户端IP " << clientIP << endl;
        cout << "客户端端口 " << clientPort << endl;
    }

    cout << "\nTCP连接成功开始发送数据" << endl;
    cout << "输入 __QUIT__ 停止发送\n" << endl;
    while (1)
    {
        cout << "\n请输入字符串" << endl;

        string userString;
        cin >> userString;
        if (userString.empty()) continue;

        if (userString == "__QUIT__")
        {
            break;
        }

        // 发送数据
        int sendResult = send(clientSocket, userString.c_str(), userString.size(), 0);
        if (sendResult == SOCKET_ERROR)
        {
            cout << "发送失败 错误码: " << WSAGetLastError() << endl;
            continue;
        }
        else
        {
            cout << "成功发送 " << sendResult << "字节" << endl;
        }
    }

    closesocket(clientSocket);  // 关闭客户端套接字
    closesocket(listenSocket);  // 关闭监听套接字
    WSACleanup();  // Windows下需要清理
    cout << "\n\n服务器程序停止运行" << endl;
    return 0;
}

使用说明

  1. 编译并运行该程序
  2. 输入要绑定的本地IP地址和端口号
  3. 程序会开始监听指定端口的连接请求
  4. 当有客户端连接时,程序会接受连接
  5. 连接建立后,可以输入要发送给客户端的字符串
  6. 输入__QUIT__可以停止发送并退出程序

注意事项

  • 这个服务器只能处理一个客户端连接
  • 服务器只能发送数据,不能接收客户端发送的数据
  • 发送的数据是字符串格式

UDP(用户数据报协议)

简介

UDP(User Datagram Protocol,用户数据报协议)是一种无连接、不可靠、基于消息的传输层协议。它不保证数据一定到达、也不保证顺序,但简单、高效、低延迟。

UDP的主要特点:

  • 无连接:通信前不需要建立连接
  • 不可靠:不保证数据到达,不保证顺序
  • 高效:协议头开销小,传输效率高
  • 基于数据报:每次发送一个完整、独立的数据包

与TCP不同,UDP是对等(peer-to-peer)的,没有客户端/服务器之分。程序的角色由应用逻辑定义,而非协议强制。

相关函数

recvfrom()函数:接收一个UDP数据报

c 复制代码
int recvfrom(SOCKET s, char* buf, int len, int flags,
             struct sockaddr* from, int* fromlen);
  • 作用:从套接字接收一个UDP数据报,并获取发送方的地址信息
  • 参数:
    • s:已创建并绑定的套接字
    • buf:用于存放接收到的数据的缓冲区
    • len:缓冲区的最大容量
    • flags:接收标志(通常设为0)
    • from:指向sockaddr结构体的指针,用于输出发送方的地址
    • fromlen:输入/输出参数,地址结构体大小
  • 返回值:
    • 成功:返回接收到的字节数
    • 失败:返回SOCKET_ERROR
  • 注意 :默认情况下recvfrom()会阻塞当前线程,直到有数据报到达

sendto()函数:发送一个UDP数据报

c 复制代码
int sendto(SOCKET s, const char* buf, int len, int flags,
           const struct sockaddr* to, int tolen);
  • 作用:向指定目标地址发送一个UDP数据报
  • 参数:
    • s:已创建的套接字描述符
    • buf:指向要发送数据的缓冲区
    • len:要发送的字节数
    • flags:发送标志(通常设为0)
    • to:指向目标地址结构体的指针
    • tolen:地址结构体的大小
  • 返回值:
    • 成功:返回实际发送的字节数
    • 失败:返回SOCKET_ERROR
  • 注意 :在UDP中,没有"建立连接"的行为,每次调用sendto()都需指定目标地址

UDP程序示例

例1-只能发送数据的UDP程序

这个UDP程序只能发送数据,不能接收数据。每次发送数据时都需要指定目标地址。

cpp 复制代码
// 只能发送数据的UDP程序
#include <iostream>
#include <string>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
    cout << "只能发送数据的UDP程序\n" << endl;    

    // 初始化Winsock
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        cerr << "WSAStartup失败!\n";
        return 1;
    }

    // 创建UDP套接字
    SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock == INVALID_SOCKET)
    {
        cerr << "创建套接字失败 错误码 " << WSAGetLastError();
        WSACleanup();
        return 1;
    }
    cout << "UDP套接字创建成功\n" << endl;    

    cout << "可以开始发送了\n" << endl;
    while (true)
    {
        string targetIP;  // 本次目标IP
        uint16_t targetPort;  // 本次目标端口

        cout << "请输入本次目标IP: " << endl;
        cin >> targetIP;
        cout << "请输入本次目标端口: " << endl;
        cin >> targetPort;

        // 设置本次目标地址
        sockaddr_in destAddr{};
        destAddr.sin_family = AF_INET;
        destAddr.sin_port = htons(targetPort);
        if (inet_pton(AF_INET, targetIP.c_str(), &destAddr.sin_addr) <= 0) {
            cerr << "无效的IP地址\n";
            continue;
        }

        cout << "请输入本次要发送的字符串 输入__QUIT__退出" << endl;
        string message;
        cin >> message;
        if (message == "__QUIT__") break;

        // 发送数据(UDP)
        int sent = sendto(sock,
            message.c_str(), (int)message.size(),
            0,
            (sockaddr*)&destAddr, sizeof(destAddr));

        if (sent == SOCKET_ERROR)
        {
            cerr << "发送失败,错误码: " << WSAGetLastError() << endl;
        }
        else
        {
            cout << "成功发送 " << sent << " 字节数据\n" << endl;
        }
    }

    closesocket(sock);
    WSACleanup();
    cout << "\n客户端退出。\n";
    return 0;
}

使用说明

  1. 编译并运行该程序
  2. 每次发送数据前,需要输入目标IP地址和端口号
  3. 然后输入要发送的字符串
  4. 输入__QUIT__可以退出程序

注意事项

  • 这个程序没有绑定本地端口,操作系统会自动分配一个临时端口
  • 每次发送都可以指定不同的目标地址
  • 程序不能接收数据,只能发送数据
例2-只能接收数据的UDP程序

这个UDP程序只能接收数据,不能发送数据。它需要绑定到一个本地端口来接收数据。

cpp 复制代码
// 只能接收数据的UDP程序
#include <iostream>
#include <string>
using namespace std;

#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
    cout << "只能接收数据的UDP程序\n" << endl;

    // 初始化Winsock
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        cerr << "WSAStartup失败!\n";
        return 1;
    }

    // 创建UDP套接字
    SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock == INVALID_SOCKET)
    {
        cerr << "创建套接字失败 错误码 " << WSAGetLastError();
        WSACleanup();
        return 1;
    }
    cout << "UDP套接字创建成功\n" << endl;

    // 由用户输入本机的IP地址和要监听的端口
    // 0.0.0.0表示本机任意的IP地址
    string localIP;  // 本地IP
    uint16_t localPort;  // 本地端口
    cout << "请输入要监听的IP: " << endl;
    cin >> localIP;
    cout << "请输入要监听的端口: " << endl;
    cin >> localPort;
    // 需要使用的结构体
    sockaddr_in localAddr{};
    localAddr.sin_family = AF_INET;
    localAddr.sin_port = htons(localPort);
    // 将字符串IP转为二进制格式
    inet_pton(AF_INET, localIP.c_str(), &(localAddr.sin_addr.s_addr));

    // 绑定本地IP端口
    if (bind(sock, (sockaddr*)(&localAddr), sizeof(localAddr)) == SOCKET_ERROR)
    {
        cerr << "绑定失败 错误码 " << WSAGetLastError() << endl;
        closesocket(sock);
        WSACleanup();
        return 1;
    }
    cout << "绑定成功\n" << endl;

    cout << "开始接收数据" << endl;
    while (true)
    {
        char receiveBuffer[1024] = { 0 };
        sockaddr_in senderAddr;
        int sockaddr_Len = sizeof(senderAddr);
        // 接收UDP数据包
        // 默认阻塞等待!!!
        int bytesReceived = recvfrom(sock,
            receiveBuffer, sizeof(receiveBuffer),
            0,
            (sockaddr*)(&senderAddr), &sockaddr_Len);

        if (bytesReceived == SOCKET_ERROR)
        {
            cerr << "出现错误 错误码 " << WSAGetLastError();
            continue;
        }
        else
        {
            cout << "收到 " << bytesReceived << " 字节数据" << endl;
            cout << receiveBuffer << endl;
            char senderIP[INET_ADDRSTRLEN] = {};
            inet_ntop(AF_INET, &senderAddr.sin_addr, senderIP, sizeof(senderIP));
            uint16_t senderPort = ntohs(senderAddr.sin_port);
            cout << "发送方IP " << senderIP << endl;
            cout << "发送方端口 " << senderPort << endl;
            cout << endl;
        }
    }

    closesocket(sock);
    WSACleanup();
    cout << "\n客户端退出。\n";
    return 0;
}

使用说明

  1. 编译并运行该程序
  2. 输入要绑定的本地IP地址和端口号
  3. 程序会开始监听指定端口的数据
  4. 当有数据到达时,程序会显示数据内容和发送方信息

注意事项

  • 这个程序只能接收数据,不能发送数据
  • 程序需要绑定到一个本地端口才能接收数据
  • 程序是阻塞式的,在等待数据时会阻塞当前线程

后记

本文主要聚焦于套接字编程的基本使用,通过简单的示例展示了TCP和UDP通信的基本原理。这些示例都是单线程阻塞式的,功能相对简单,但它们是理解网络编程的基础。

需要特别指出的是,无论Windows还是Linux,Socket都可以设置为非阻塞模式!通过配合"轮询查询"或者更高效的"事件驱动"机制(如Windows的I/O完成端口或Linux的epoll),单线程也可以实现复杂的网络应用,如一个TCP服务器同时处理多个客户端连接。

在Windows下,设置套接字为非阻塞模式的简单方法如下:

c 复制代码
// 控制套接字I/O行为
int ioctlsocket(SOCKET s, long cmd, u_long* argp);

另外,需要提醒的是:本教程讲解的Winsock(Windows)以及BSD Socket(Linux/macOS)是操作系统提供的底层网络接口,使用相对繁琐。截至目前(C++23),C++标准库仍未包含网络编程支持。因此,建议这些底层API只用于入门学习。

若需开发实际项目,建议使用成熟的跨平台网络库,如:

  • Qt Network:Qt框架的网络模块,简洁易用
  • Boost.Asio:功能强大的跨平台网络库
  • Poco::Net:全面的C++网络库

这些库以简洁的接口封装了底层细节,大幅提升了开发效率与可靠性。

网络编程是一个深奥而有趣的领域,掌握了基础之后,可以进一步学习多线程编程、异步I/O、网络协议设计等高级主题。希望本文能为你的网络编程学习之旅提供一个良好的起点。

参考资料

  1. 爱编程的大丙

  2. Beej's Guide to Network Programming

    • 经典网络编程指南,有中文翻译版
  3. UNIX网络编程 卷1:套接字联网API

    • W. Richard Stevens的经典著作,网络编程的圣经
  4. TCP/IP详解 卷1:协议

    • 深入理解TCP/IP协议栈的必备书籍

版权声明:本文中的代码示例可以自由使用、修改和分发,但请保留原作者信息。文中提到的第三方工具和资源,版权归各自所有者所有。

更新日志

  • 2024年1月:初版发布
  • 2024年3月:添加UDP示例,修正部分错误

作者 :Cai
联系方式:可通过CSDN博客留言联系


希望这篇超过10000字的详细教程能够帮助你全面理解C/C++套接字编程。如果有任何问题或建议,欢迎留言讨论!

相关推荐
无事好时节2 小时前
Linux 进程通信:信号与共享内存详解
linux·网络·网络协议
lly2024062 小时前
Julia 的复数和有理数
开发语言
春日见2 小时前
如何提升手眼标定精度?
linux·运维·开发语言·数码相机·matlab
weixin_462446232 小时前
使用 Ubuntu 构建 code-server Docker 镜像的完整指南
linux·ubuntu·docker
风中小白菜2 小时前
计算机网络的基本概念 (IP 地址、 MAC 地址、 TCP/UDP、单播/组播广播)
tcp/ip·计算机网络·udp
Tipriest_2 小时前
Python 常用特殊变量与关键字详解
linux·python·关键字·特殊变量
古城小栈2 小时前
Java 响应式编程:Spring WebFlux+Reactor 实战
java·开发语言·spring
攻心的子乐2 小时前
sentinel使用指南 限流/熔断 微服务 ruoyi-cloud使用了
java·开发语言
点云SLAM2 小时前
C++ 偏特化详解
开发语言·c++·c++模板·c++17·c++高级应用·c++偏特化·大型项目
wregjru2 小时前
【C++】2.3 二叉搜索树的实现(附代码)
开发语言·前端·javascript