一、前言: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 四种工程解决方案
-
固定长度报文:每条消息固定字节,不足补0;简单,浪费带宽。
-
特殊分隔符:消息末尾加\n、\r\n分隔;注意业务数据不能出现分隔字符。HTTP1.1就是用\r\n做分隔。
-
长度字段 + 消息体(工业最常用):头部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常见坑点与错误处理
-
read返回0:代表对端正常关闭连接,不是出错,需要关闭本地fd。
-
read返回‑1:出错;errno为EINTR表示被信号中断,可以重试。
-
bind失败 Address already in use:上一次程序退出端口处于TIME_WAIT;设置SO_REUSEADDR选项解决。
-
write/send 不代表对方收到:仅代表拷贝到内核发送缓冲区;内核缓冲区满会阻塞。
-
忘记字节序转换:端口IP直接填主机序,导致通信异常。
-
TCP recv读不到完整报文:不要假设一次read就拿到完整业务消息,必须循环读取,处理粘包拆包。
-
文件描述符泄漏:异常分支没有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常见错误、坑点、面试高频题