C++ Socket精讲:TCP/UDP套接字、客户端服务端实现、字节序、粘包拆包、简单HTTP服务、网络踩坑与面试全解

一、前言:Socket到底是什么

前面我们完成C++语法、STL、模板、内存、多线程并发整套体系。开始进入C++网络编程,把计算机网络理论真正落地写代码。

Socket(套接字)本质是操作系统提供的一组网络系统调用API,应用程序通过socket向内核申请网络资源,内核完成底层TCP/IP协议封装,实现跨主机进程通信。

网络通信本质:不同机器上两个进程之间的数据收发,IP定位主机,端口定位进程。

  • IP地址:找到网络上哪一台机器

  • 端口port:找到机器内部哪一个应用进程,范围 0‑65535,1‑1023为系统保留端口


二、TCP与UDP核心区别

|----------|----------------------|--------------------|
| 特性 | TCP | UDP |
| 连接属性 | 面向连接,三次握手建立连接,四次挥手断开 | 无连接,直接发包,不建立连接 |
| 可靠性 | 可靠传输,重传、确认、丢包重传、顺序保证 | 不可靠,不保证到达,不保证顺序 |
| 流量&拥塞控制 | 具备滑动窗口、拥塞控制 | 无 |
| 报文边界 | 字节流,无报文边界,产生粘包拆包 | 数据报,保留报文边界 |
| 开销 | 头部大,开销高 | 头部8字节,开销极小 |
| 典型场景 | 文件传输、HTTP、数据库访问、聊天 | 直播、语音视频、DNS、游戏实时对战 |

核心记忆:TCP追求可靠;UDP追求速度。


三、网络字节序:大端小端转换

不同CPU架构主机字节序不一样:x86/x86‑64是小端序 ;网络协议统一规定使用大端序

主机发送IP、端口前,必须转为网络大端;收到网络数据,转回主机字节序。

cpp 复制代码
#include <arpa/inet.h>

// 主机短整型 → 网络字节序
uint16_t htons(uint16_t hostshort);
// 主机长整型 → 网络字节序
uint32_t htonl(uint32_t hostlong);

// 网络字节序 → 主机短整型
uint16_t ntohs(uint16_t netshort);
// 网络字节序 → 主机长整型
uint32_t ntohl(uint32_t netlong);

// IP字符串 "127.0.0.1" → 网络二进制
int inet_pton(int af, const char *src, void *dst);
// 网络二进制IP → 点分十进制字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

af填AF_INET代表IPv4。


四、TCP服务端完整代码(Linux)

服务端流程:socket → bind → listen → accept → read/write → close

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

using namespace std;

int main()
{
    // 1. 创建socket套接字
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(server_fd < 0)
    {
        perror("socket create failed");
        return -1;
    }

    // 设置端口复用,服务重启不会报address already in use
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 2. bind绑定IP+端口
    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8888);
    // INADDR_ANY监听本机所有网卡
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(server_fd, (sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
    {
        perror("bind failed");
        close(server_fd);
        return -1;
    }

    // 3. listen开启监听,第二个参数为半连接队列长度
    if(listen(server_fd, 5) < 0)
    {
        perror("listen failed");
        close(server_fd);
        return -1;
    }
    cout << "TCP服务端启动,监听端口8888" << endl;

    // 4. accept阻塞等待客户端连接
    sockaddr_in clientAddr{};
    socklen_t clientLen = sizeof(clientAddr);
    int conn_fd = accept(server_fd, (sockaddr*)&clientAddr, &clientLen);
    if(conn_fd < 0)
    {
        perror("accept failed");
        close(server_fd);
        return -1;
    }
    cout << "客户端连接:" << inet_ntoa(clientAddr.sin_addr)
         << ":" << ntohs(clientAddr.sin_port) << endl;

    // 5. read读取客户端数据
    char buf[1024] = {0};
    ssize_t n = read(conn_fd, buf, sizeof(buf)-1);
    if(n > 0)
    {
        cout << "收到客户端:" << buf << endl;
        write(conn_fd, "hello from server", strlen("hello from server"));
    }

    close(conn_fd);
    close(server_fd);
    return 0;
}

注意:上面是单连接版本,一次只能服务一个客户端;生产环境需要多线程/IO多路复用来支持多客户端。


五、TCP客户端完整代码(Linux)

客户端流程:socket → connect → read/write → close

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

using namespace std;

int main()
{
    int cli_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(cli_fd < 0)
    {
        perror("socket");
        return -1;
    }

    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8888);
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

    // connect发起连接
    if(connect(cli_fd, (sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
    {
        perror("connect");
        close(cli_fd);
        return -1;
    }

    write(cli_fd, "hello client msg", strlen("hello client msg"));

    char buf[1024] = {0};
    ssize_t n = read(cli_fd, buf, sizeof(buf)-1);
    if(n > 0)
    {
        cout << "收到服务端:" << buf << endl;
    }

    close(cli_fd);
    return 0;
}

六、UDP套接字通信实战

UDP无连接,不需要listen、accept;使用sendto、recvfrom收发数据报。

UDP服务端

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

using namespace std;

int main()
{
    int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
    sockaddr_in servAddr{};
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(9999);
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(udp_fd, (sockaddr*)&servAddr, sizeof(servAddr));

    char buf[1024] = {0};
    sockaddr_in clientAddr{};
    socklen_t len = sizeof(clientAddr);

    ssize_t n = recvfrom(udp_fd, buf, sizeof(buf)-1, 0,
                         (sockaddr*)&clientAddr, &len);
    cout << "udp recv:" << buf << endl;

    sendto(udp_fd, "udp reply", strlen("udp reply"), 0,
           (sockaddr*)&clientAddr, len);

    close(udp_fd);
    return 0;
}

UDP客户端

cpp 复制代码
int main()
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    sockaddr_in servAddr{};
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &servAddr.sin_addr);

    sendto(fd, "udp hello", strlen("udp hello"), 0,
           (sockaddr*)&servAddr, sizeof(servAddr));

    char buf[1024] = {0};
    sockaddr_in tmp;
    socklen_t tlen = sizeof(tmp);
    recvfrom(fd, buf, sizeof(buf)-1, 0, (sockaddr*)&tmp, &tlen);
    cout << buf << endl;
    close(fd);
    return 0;
}

七、TCP粘包拆包成因与解决方案

7.1 什么是粘包拆包

TCP是字节流协议,没有报文边界。操作系统内核TCP缓冲区会合并、拆分数据。

  • 粘包:多次发送的数据包,接收方一次读到多个逻辑报文,数据粘连在一起

  • 拆包:一个逻辑报文被内核拆成多段,接收方分多次才收完整

注意:UDP不会发生粘包拆包,UDP是数据报,保留报文边界。

7.2 四种工程解决方案

  1. 固定长度报文:每条消息固定字节,不足补0;简单,浪费带宽。

  2. 特殊分隔符:消息末尾加\n、\r\n分隔;注意业务数据不能出现分隔字符。HTTP1.1就是用\r\n做分隔。

  3. 长度字段 + 消息体(工业最常用):头部4字节存消息体长度,后面紧跟消息内容。接收方先读4字节得到长度,再读取对应长度字节。

  4. 上层现成协议:Protobuf、JSON‑RPC等序列化协议自带长度编码,直接使用。

生产网络程序几乎全部采用长度+内容方案解决粘包拆包。


八、实战:极简HTTP服务(基于TCP Socket)

HTTP本质就是基于TCP之上的文本协议。浏览器发起TCP连接,发送HTTP请求报文,服务端回HTTP响应报文。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

using namespace std;

int main()
{
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(server_fd, (sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 3);
    cout << "simple http server listen 8080" << endl;

    int conn = accept(server_fd, nullptr, nullptr);
    char buf[2048] = {0};
    read(conn, buf, sizeof(buf)-1);
    cout << "http request:\n" << buf << endl;

    // http响应
    const char* resp =
"HTTP/1.1 200 OK\r\n"
"Content‑Type:text/plain;charset=utf‑8\r\n"
"Content‑Length:13\r\n"
"\r\n"
"Hello Socket HTTP";

    write(conn, resp, strlen(resp));
    close(conn);
    close(server_fd);
    return 0;
}

编译运行,浏览器访问 127.0.0.1:8080 即可看到返回文本。该版本仅支持单请求,不支持并发。


九、Socket常见坑点与错误处理

  1. read返回0:代表对端正常关闭连接,不是出错,需要关闭本地fd。

  2. read返回‑1:出错;errno为EINTR表示被信号中断,可以重试。

  3. bind失败 Address already in use:上一次程序退出端口处于TIME_WAIT;设置SO_REUSEADDR选项解决。

  4. write/send 不代表对方收到:仅代表拷贝到内核发送缓冲区;内核缓冲区满会阻塞。

  5. 忘记字节序转换:端口IP直接填主机序,导致通信异常。

  6. TCP recv读不到完整报文:不要假设一次read就拿到完整业务消息,必须循环读取,处理粘包拆包。

  7. 文件描述符泄漏:异常分支没有close(fd),不断消耗系统句柄。


十、网络编程面试真题解析

Q1 TCP三次握手四次挥手分别发生在哪些socket函数?

connect触发客户端发起三次握手;listen让服务端内核可以接收连接;accept从内核已完成连接队列取出连接,不参与握手。close触发四次挥手。

Q2 什么是TIME_WAIT,作用是什么?

主动关闭方最后状态,等待2MSLS时间。两个作用:保证被动关闭方收到我方ACK;处理延迟重复报文,防止旧连接数据包干扰新连接。

Q3 TCP粘包拆包原因,怎么解决?

TCP是字节流,无消息边界,内核缓冲区合并拆分数据。方案:定长、分隔符、长度+内容、序列化协议。

Q4 read返回0代表什么?

对端执行close,TCP连接正常关闭。

Q5 UDP为什么不可靠?UDP可以connect吗?

UDP无确认、无重传机制。UDP可以调用connect,只是在本地内核记录对端地址,不会产生网络报文,之后send就不用每次填对端地址。


十一、今日总结

掌握C++ Socket网络编程基础:

✅ Socket本质、IP端口作用

✅ TCP/UDP对比与选型

✅ 网络字节序大小端全套转换API

✅ TCP服务端、客户端完整可运行代码

✅ UDP套接字sendto/recvfrom通信

✅ TCP粘包拆包成因和工业级解决方案

✅ 手写极简HTTP服务,理解HTTP基于TCP

✅ Socket常见错误、坑点、面试高频题

相关推荐
程与留22 分钟前
12_常用控件速查(下):QSpinBox、QSlider、QProgressBar、QCheckBox
c++·qt
A hao33 分钟前
户外LED广告牌需要什么防护等级IP级别?
大数据·网络·图像处理·人工智能·网络协议·tcp/ip·广告
rcms1527026921833 分钟前
Nikon 4S013-684-1 半导体放大器模块
网络
云上工程笔记34 分钟前
ELI5:如何把复杂概念自动画成图,方便写文档、做分享、带新人
运维·服务器·网络·云计算
OPEN-F1 小时前
C++进阶教程:运算符重载与类型转换
java·c++·算法
国科安芯1 小时前
ASC8T245S:把 8 位并行总线稳稳“跨过“电压域的双电源收发器
网络·分布式·单片机·嵌入式硬件·fpga开发·架构
uoKent1 小时前
c++中的extern关键字
开发语言·c++
2401_868534782 小时前
容灾、备份、存储
linux·网络