TCP套接字编程详解:从API到实战细节
一、TCP通信整体流程
TCP是面向连接的可靠传输协议,其通信流程需严格遵循"服务器绑定监听→客户端连接→数据交互→断开连接"的步骤。以下是服务器和客户端的核心流程对比:
角色 | 核心流程(常用API) |
---|---|
服务器 | 创建套接字 socket() → 绑定地址端口 bind() → 监听连接 listen() → 接受连接 accept() → 数据交互 read()/write() → 关闭连接 close() |
客户端 | 创建套接字 socket() → 连接服务器 connect() → 数据交互 read()/write() → 关闭连接 close() |
二、核心API详解(Linux环境)
1. 创建套接字:socket()
功能:创建一个网络套接字(文件描述符),用于后续网络通信。
c
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明:
domain
(协议族) :指定网络协议类型AF_INET
:IPv4协议(最常用);AF_INET6
:IPv6协议;AF_UNIX
:本地套接字(进程间通信)。
type
(套接字类型) :指定传输方式SOCK_STREAM
:流式套接字(对应TCP协议,可靠连接);SOCK_DGRAM
:数据报套接字(对应UDP协议,无连接)。
protocol
(协议) :一般填0
,表示使用type
对应的默认协议(TCP用IPPROTO_TCP
,UDP用IPPROTO_UDP
,但通常省略)。
示例:创建TCP套接字
c
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket创建失败");
exit(1);
}
2. 绑定地址和端口:bind()
功能:将套接字与本地IP地址和端口号绑定(服务器必须绑定,客户端通常由系统自动分配端口)。
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
-
sockfd
:socket()
返回的套接字描述符。 -
addr
:指向地址结构体的指针(需填充IP、端口等信息)。- IPv4使用
struct sockaddr_in
(需强制转换为sockaddr*
):
cstruct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; // 协议族(IPv4) serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地IP(INADDR_ANY) serv_addr.sin_port = htons(8080); // 端口号(需转换为网络字节序)
- IPv4使用
-
addrlen
:地址结构体的大小(sizeof(struct sockaddr_in)
)。
关键细节:
INADDR_ANY
:表示绑定本地所有网络接口的IP(如服务器有多个网卡时,可接收任意网卡的连接)。- 端口号范围:
0~65535
,其中0~1023
是知名端口(如HTTP 80),建议使用1024~65535
避免冲突。
3. 监听连接:listen()
功能:将套接字设置为监听状态,允许接收客户端连接请求。
c
int listen(int sockfd, int backlog);
参数说明:
sockfd
:已绑定的套接字描述符(监听套接字)。backlog
:未完成连接队列的最大长度(处于SYN_RCVD
状态的连接数),超过则新连接被拒绝(Linux通常建议设为5~10
,具体受系统限制)。
注意:
listen()
仅标记套接字为监听状态,不主动接收连接,需配合 accept()
使用。
4. 接受连接:accept()
功能 :从监听队列中取出一个已完成的连接请求,返回一个新的套接字描述符(用于与该客户端通信)。
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
sockfd
:监听套接字描述符(listen()
后的套接字)。addr
:输出参数,存储客户端的IP和端口信息(需提前分配空间)。addrlen
:输入输出参数,传入addr
大小,返回实际存储的地址长度。
核心特性:
- 阻塞性 :若没有客户端连接,
accept()
会一直阻塞等待。 - 新套接字:返回的新描述符是"通信套接字",用于与客户端收发数据;原监听套接字仍继续监听新连接。
5. 客户端连接:connect()
功能:客户端向服务器发起连接请求,完成TCP三次握手。
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd
:客户端创建的套接字描述符。addr
:服务器的地址信息(需填充服务器IP和端口,端口和IP需转换为网络字节序)。addrlen
:服务器地址结构体的大小。
示例:客户端连接服务器
c
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
// 将点分十进制IP转换为网络字节序二进制
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
// 发起连接
if (connect(client_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
perror("连接失败");
exit(1);
}
6. 数据交互:send()/recv()
与 read()/write()
TCP通信中,数据通过套接字的读写缓冲区传输,常用函数如下:
函数 | 原型 | 功能 |
---|---|---|
send() |
ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
发送数据到套接字缓冲区 |
recv() |
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
从套接字缓冲区接收数据 |
write() |
ssize_t write(int fd, const void *buf, size_t count); |
等同于 send(flags=0) |
read() |
ssize_t read(int fd, void *buf, size_t count); |
等同于 recv(flags=0) |
参数说明:
sockfd/fd
:通信套接字描述符(accept()
或connect()
返回的描述符)。buf
:数据缓冲区(发送时存数据,接收时存结果)。len/count
:数据长度(字节)。flags
:传输标志(通常为0
,表示默认行为)。
核心特性:
- 阻塞性:若缓冲区满(发送)或空(接收),函数会阻塞等待。
- 返回值 :成功返回实际传输的字节数;
0
表示连接关闭;-1
表示出错。
7. 关闭连接:close()
功能:关闭套接字,释放资源,触发TCP四次挥手。
c
int close(int fd);
- 服务器需关闭 通信套接字 (与客户端的连接)和 监听套接字(停止接收新连接)。
- 客户端关闭套接字后,会向服务器发送
FIN
包,终止连接。
三、核心基础概念
1. 网络字节序
不同计算机的字节存储顺序(大端/小端)不同,TCP协议规定网络中必须使用 大端字节序(高位字节存低地址),需通过转换函数统一格式:
函数 | 功能 | 适用场景 |
---|---|---|
htons() |
主机字节序 → 网络字节序(16位) | 端口号转换(short ) |
htonl() |
主机字节序 → 网络字节序(32位) | IP地址转换(int ) |
ntohs() |
网络字节序 → 主机字节序(16位) | 解析端口号 |
ntohl() |
网络字节序 → 主机字节序(32位) | 解析IP地址 |
示例:端口和IP转换
c
uint16_t port = 8080;
uint16_t net_port = htons(port); // 主机→网络(端口)
uint32_t ip = inet_addr("192.168.1.1"); // 点分十进制→网络字节序(IPv4)
2. IP地址格式转换
IP地址在代码中需在"点分十进制字符串"和"二进制整数"间转换,常用函数:
函数 | 功能 | 适用协议 |
---|---|---|
inet_addr() |
点分十进制字符串 → 网络字节序整数 | IPv4(过时) |
inet_ntoa() |
网络字节序整数 → 点分十进制字符串 | IPv4(非线程安全) |
inet_pton() |
字符串 → 二进制(支持IPv4/IPv6) | 现代推荐 |
inet_ntop() |
二进制 → 字符串(支持IPv4/IPv6) | 现代推荐 |
现代用法示例:
c
// 字符串→二进制(IPv4)
struct in_addr ip_bin;
inet_pton(AF_INET, "127.0.0.1", &ip_bin);
// 二进制→字符串(IPv4)
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ip_bin, ip_str, INET_ADDRSTRLEN);
printf("IP: %s\n", ip_str); // 输出 "127.0.0.1"
3. 文件描述符与缓冲区
- 服务器的两个套接字描述符 :
- 监听套接字(
listen()
后的fd):仅用于接收新连接,不参与数据交互。 - 通信套接字(
accept()
返回的fd):每个客户端连接对应一个,用于收发数据。
- 监听套接字(
- 缓冲区机制 :每个套接字对应内核中的 读缓冲区 和 写缓冲区 :
- 发送数据:
send()
/write()
将数据写入写缓冲区,内核异步发送到网络。 - 接收数据:内核将网络数据存入读缓冲区,
recv()
/read()
从缓冲区读取。 - 缓冲区满时:
send()
阻塞(直到有空间);缓冲区空时:recv()
阻塞(直到有数据)。
- 发送数据:
四、TCP粘包问题及解决方案
什么是粘包?
TCP是流式传输,数据无边界,若发送方连续发送小数据包,接收方可能将多个数据包合并为一个"大数据块",导致无法区分边界(如发送"hi"和"jack",接收方可能收到"hijack")。
粘包原因
- 发送方:数据太小,内核合并发送(Nagle算法)。
- 接收方:数据到达速度快于处理速度,缓冲区累积多个数据包。
解决方案:固定格式分包
核心思路:先发送数据长度,再发送实际数据,接收方按长度解析。
步骤示例:
-
发送方:
- 将数据长度(
len
)转换为网络字节序,先发送len
(4字节整数)。 - 再发送长度为
len
的实际数据。
c// 发送"hello"(长度5) int len = htonl(5); // 长度转换为网络字节序 send(fd, &len, 4, 0); // 先发送长度 send(fd, "hello", 5, 0); // 再发送数据
- 将数据长度(
-
接收方:
- 先读取4字节获取数据长度
len
(转换为主机字节序)。 - 再读取
len
字节的实际数据。
cint len; recv(fd, &len, 4, 0); // 先读长度 len = ntohl(len); // 转换为主机字节序 char buf[len]; recv(fd, buf, len, 0); // 再读实际数据
- 先读取4字节获取数据长度
五、跨平台差异(Windows环境)
Windows套接字编程与Linux有以下核心区别:
-
头文件和库:
c#include <winsock2.h> // 核心头文件 #pragma comment(lib, "ws2_32.lib") // 链接套接字库
-
初始化和清理:
c// 初始化套接字库 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); // 程序结束时清理 WSACleanup();
-
函数差异:
- 关闭套接字用
closesocket()
而非close()
。 - 错误码获取用
WSAGetLastError()
而非errno
。
- 关闭套接字用
六、高频面试考点总结
-
TCP通信核心流程:
- 服务器:
socket()
创建套接字 →bind()
绑定IP和端口 →listen()
监听连接 →accept()
阻塞等待客户端连接 →read()/write()
数据交互 →close()
关闭连接。 - 客户端:
socket()
创建套接字 →connect()
发起连接 →read()/write()
数据交互 →close()
关闭连接。
- 服务器:
-
INADDR_ANY
的作用 :绑定服务器本地所有网络接口的IP地址(如服务器有多个网卡时),使服务器能接收来自任意网卡的连接请求,无需硬编码具体IP,增强灵活性。
-
listen()
中backlog
的含义 :表示未完成连接队列(处于
SYN_RCVD
状态)的最大长度,超过该值的新连接会被拒绝。实际有效长度可能受系统内核参数(如net.core.somaxconn
)限制。 -
accept()
的特性:- 阻塞性:无新连接时会一直阻塞,直到有客户端连接或被信号中断。
- 返回新套接字:
accept()
返回的是与客户端通信的套接字,原监听套接字仍可继续接收其他连接(实现多客户端通信需多线程/多进程)。
-
网络字节序与转换函数:
- 网络字节序为 大端字节序,主机字节序可能为大端或小端(取决于CPU架构)。
- 必须转换的场景:端口号(
htons()
/ntohs()
)、IP地址(htonl()
/ntohl()
)。 - 现代推荐用
inet_pton()
/inet_ntop()
转换IP地址(支持IPv4/IPv6,线程安全)。
-
TCP粘包问题:
- 原因:TCP是流式传输,无数据边界,多个小数据包可能被合并传输。
- 解决方案:长度前缀法(先发送数据长度,再发送实际数据,接收方按长度解析)。
-
套接字缓冲区机制:
- 每个套接字对应内核中的读缓冲区和写缓冲区,数据通过缓冲区间接传输。
send()
/write()
成功仅表示数据写入写缓冲区,不代表已发送到对端;recv()
/read()
从读缓冲区取数据,缓冲区为空时阻塞。
-
文件描述符的角色:
- 服务器有两类描述符:监听描述符 (负责接收连接)和 通信描述符(每个客户端一个,负责数据交互)。
- 描述符是内核管理套接字的索引,关闭描述符会触发TCP四次挥手释放连接。
总结
TCP套接字编程的核心是理解"连接建立→数据交互→连接关闭"的全流程,掌握 socket()
/bind()
/listen()
/accept()
/connect()
等API的参数和特性,以及网络字节序、粘包处理等细节。实际开发中需注意跨平台差异(如Windows的 WSAStartup()
),并通过多线程/IO复用等技术实现高效的多客户端通信。这些基础是网络编程的核心。