围绕 Socket API 与 TCP/UDP 通信的核心概念、函数对照、示例代码与常见坑。

核心概念速查

概念说明关键点
Socket网络通信端点文件描述符抽象
TCP面向连接、可靠三次握手、四次挥手
UDP无连接、不可靠快速、轻量
sockaddr通用地址结构IPv4/IPv6 统一接口
字节序大端/小端转换htons() htonl()

TCP 三次握手与四次挥手

三次握手(建立连接):
  Client ──SYN────▶ Server
  Client ◀─SYN+Ack─ Server
  Client ──Ack────▶ Server

四次挥手(关闭连接):
  Client ──FIN────▶ Server
  Client ◀─Ack───── Server
  Client ◀─FIN───── Server
  Client ──Ack────▶ Server

TCP 客户端-服务器模型

服务器: socket() → bind() → listen() → accept() → recv/send → close()
客户端: socket() → connect() → send/recv → close()

核心函数对照表

函数用途参数要点
socket()创建套接字AF_INET, SOCK_STREAM, 0
bind()绑定地址套接字 + sockaddr_in
listen()监听连接backlog 队列长度
accept()接受连接返回新套接字描述符
connect()发起连接服务器地址
send/recv()发送/接收数据缓冲区 + 长度
htons/htonl()主机→网络字节序short/long
ntohs/ntohl()网络→主机字节序short/long

从 Qt 到原生 Socket

Qt 写法原生等价物差异要点
server.listen(QHostAddress::Any, 8080)socket() + bind() + listen() 三步Qt 一行帮你做三件事
newConnection 信号 → nextPendingConnection()accept()Qt 信号通知;原生阻塞死等
socket->connectToHost("ip", port)inet_pton() + connect()IP 解析要自己做
socket->write(bytes)send(fd, buf, len, 0)返回值要检查,可能只发一半
readyRead 信号 → readAll()recv(fd, buf, size, 0)没有"就绪通知",直接阻塞读
errorOccurred / stateChanged检查返回值 + errno错误要手动判断
QUdpSocket::writeDatagram/readDatagramsendto()/recvfrom()多一个对端地址参数
QHostAddress("127.0.0.1")sockaddr_in + inet_pton()字符串 IP 手动转结构体
端口字节序自动处理必须自己 htons()Qt 内部帮你转了

核心差异:Qt 信号槽背后是「事件循环 + 就绪通知」(reactor 模式)。原生编程没有这个循环——要么阻塞死等,要么用线程,要么用 select/epoll 自己搭事件循环。学原生网络的本质,就是把 Qt 隐藏的三件事(fd 管理、字节序、事件分发)亲手做一遍。

示例:简单 TCP 服务器

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    // 1. 创建 socket
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 绑定地址
    sockaddr_in address{};
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;   // 监听所有网卡
    address.sin_port = htons(8080);
    bind(server_fd, (sockaddr*)&address, sizeof(address));

    // 3. 监听
    listen(server_fd, 5);

    // 4. 接受连接(阻塞,直到有客户端连入)
    sockaddr_in client_addr{};
    socklen_t addr_len = sizeof(client_addr);
    int client_fd = accept(server_fd, (sockaddr*)&client_addr, &addr_len);

    // 5. 收发数据
    char buffer[1024] = {0};
    recv(client_fd, buffer, sizeof(buffer), 0);
    std::cout << "收到: " << buffer << std::endl;

    send(client_fd, "Hello!", 6, 0);

    // 6. 关闭
    close(client_fd);
    close(server_fd);
    return 0;
}

常见坑

问题原因解决
Address already in use服务器重启时 TIME_WAITsetsockopt(SO_REUSEADDR)
Connection refused服务器未启动或端口错检查服务器状态、端口
数据截断 / 粘包TCP 是字节流无边界定义协议或按长度读取
阻塞卡死accept/recv 阻塞等待多线程 / IO 多路复用
头文件缺失平台差异<sys/socket.h> <netinet/in.h>

Windows 注意:上面的原生 Socket 示例基于 Linux API。在 Windows 下需 WSAStartup() 初始化、closesocket() 代替 close()、链接 ws2_32.lib,头文件为 <winsock2.h> + <ws2tcpip.h>。函数名和流程基本一致。

顿悟

  • TCP 是字节流,不是消息流:一次 send 不一定被一次 recv 完整收到,数据粘包是网络编程第一坑,需要自己定义消息边界。
  • accept 的返回值是新套接字:监听套接字和通信套接字是两个描述符,一个服务器要并发,就得对每个 accept 出来的新 fd 单独处理(线程/进程/事件驱动)。
  • 网络字节序是规定:多字节整数在网络上统一大端,本地要转。

自测清单

  • 理解 TCP 三次握手 / 四次挥手流程
  • 能写出基本的 TCP 服务器 / 客户端
  • 掌握 sockaddr_in 结构体初始化
  • 理解字节序转换(htons/htonl)的必要性
  • 知道如何处理 TIME_WAIT(SO_REUSEADDR)
  • 理解粘包问题,能想到加长度头或分隔符