围绕 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/readDatagram | sendto()/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_WAIT | setsockopt(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) - 理解粘包问题,能想到加长度头或分隔符