TCP 编程是网络通信的核心,其 API 围绕面向连接的特性设计,涵盖服务端和客户端的交互流程。以下是基于 C 语言的 TCP 编程核心 API 及使用流程的详细解析:
核心 API 概览
函数 | 角色 | 描述 |
---|---|---|
socket() |
通用 | 创建套接字,指定协议族(IPv4/IPv6)和类型(SOCK_STREAM )。 |
bind() |
服务端 | 将套接字绑定到特定 IP 地址和端口。 |
listen() |
服务端 | 将套接字设为监听模式,等待客户端连接请求。 |
accept() |
服务端 | 接受客户端连接,返回用于通信的新套接字。 |
connect() |
客户端 | 客户端主动连接服务端。 |
send() /write() |
通用 | 发送数据(TCP 保证数据顺序,可能拆包/粘包)。 |
recv() /read() |
通用 | 接收数据(需处理部分读取和缓冲区管理)。 |
close() |
通用 | 关闭套接字,终止连接。 |
shutdown() |
通用 | 优雅关闭连接(可选关闭读/写方向)。 |
1、socket函数
函数原型与头文件
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数详解
domain
(协议族/地址族)
定义通信使用的协议族,常见值包括:
值 | 描述 |
---|---|
AF_INET |
IPv4 协议(最常用) |
AF_INET6 |
IPv6 协议 |
AF_UNIX /AF_LOCAL |
本地进程间通信(UNIX 域套接字) |
AF_PACKET |
底层数据包接口(如原始以太网帧捕获) |
如果是IPV6编程,要使用struct sockddr_in6结构体(man 7 IPV6),通常使用struct sockaddr_storage来编程。
type
(套接字类型)
指定数据传输的语义,常用类型:
值 | 描述 |
---|---|
SOCK_STREAM |
面向连接的流式套接字(TCP,可靠传输) |
SOCK_DGRAM |
无连接的数据报套接字(UDP,尽最大努力交付) |
SOCK_RAW |
原始套接字(直接访问 IP/ICMP 等协议) |
protocol
(具体协议)
通常设为 0
,表示根据 domain
和 type
自动选择默认协议。例如:
SOCK_STREAM
默认使用IPPROTO_TCP
SOCK_DGRAM
默认使用IPPROTO_UDP
若需显式指定协议,可用:
值 | 描述 |
---|---|
IPPROTO_TCP |
强制使用 TCP 协议 |
IPPROTO_UDP |
强制使用 UDP 协议 |
IPPROTO_ICMP |
用于原始套接字的 ICMP 协议 |
返回值
- 成功:返回一个非负整数(套接字文件描述符),后续操作(如
bind
,connect
)均基于此描述符。 - 失败:返回
-1
,并设置errno
表示错误原因(如EACCES
,EAFNOSUPPORT
)。
典型使用场景
创建 TCP 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket() failed");
exit(EXIT_FAILURE);
}
- 用于 HTTP、FTP 等需要可靠传输的应用。
创建 UDP 套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket() failed");
exit(EXIT_FAILURE);
}
- 用于 DNS 查询、实时音视频传输等场景。
创建原始套接字(需 root 权限)
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd == -1) {
perror("socket() failed");
exit(EXIT_FAILURE);
}
- 可手动构造 IP 或 ICMP 头部,用于网络探测工具(如
ping
)。
2、bind函数
bind()
函数是网络编程中用于将套接字(socket)与特定的IP地址和端口绑定的关键步骤,常用于服务端设置监听地址。以下是 bind()
的详细解析,包含函数原型、参数解释、使用示例及常见问题:
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
参数 | 类型 | 描述 |
---|---|---|
sockfd |
int |
已创建的套接字描述符(由socket() 返回) |
addr |
struct sockaddr* |
指向保存绑定地址信息的结构体指针(需根据协议族填充对应的结构类型) |
addrlen |
socklen_t |
地址结构体的长度(字节数) |
地址结构体
1. IPv4 地址结构 (struct sockaddr_in)
struct sockaddr_in {
sa_family_t sin_family; // 地址族(如 AF_INET)
in_port_t sin_port; // 端口号(需用 `htons()` 转换字节序)
struct in_addr sin_addr; // IPv4 地址(需用 `inet_pton()` 或 `htonl(INADDR_ANY)`)
unsigned char sin_zero[8]; // 填充字段(一般置零)
};
struct in_addr {
uint32_t s_addr; // 32位 IPv4 地址(网络字节序)
};
2. IPv6 地址结构 (struct sockaddr_in6)
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族(AF_INET6)
in_port_t sin6_port; // 端口号(网络字节序)
uint32_t sin6_flowinfo; // IPv6 流信息(通常为0)
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 接口范围标识符(用于本地链路地址)
};
struct in6_addr {
unsigned char s6_addr[16]; // 128位 IPv6 地址
};
3. 通用地址结构 (struct sockaddr)
struct sockaddr {
sa_family_t sa_family; // 地址族(AF_xxx)
char sa_data[14];// 具体地址数据(由子结构展开填充)
};
使用场景与示例
1. 服务端绑定 IP 和端口
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) { perror("socket"); exit(1); }
// 允许地址重用(避免服务端重启时 bind 失败)
int optval = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET; // IPv4
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地 IP
addr.sin_port = htons(8080); // 绑定端口 8080
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind");
close(server_fd);
exit(1);
}
// 后续可调用 listen() 启动监听
2. 客户端绑定特定源地址
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in client_addr = {0};
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(12345); // 指定客户端端口
inet_pton(AF_INET, "192.168.1.100", &client_addr.sin_addr); // 指定源 IP
bind(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr));
connect(client_fd, server_addr, sizeof(server_addr));
3、listen函数
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数详解
参数 | 类型 | 描述 |
---|---|---|
sockfd |
int |
已通过 bind() 绑定地址的套接字描述符(必须是 SOCK_STREAM 类型)。 |
backlog |
int |
已建立连接(完成三次握手)的队列最大长度,决定同时等待 accept() 处理的连接数。 |
核心作用
转换套接字状态:
- 将套接字从主动模式(默认)转为被动模式,使其能够接收客户端的连接请求。
- 未调用
listen()
的套接字无法调用accept()
。
管理连接队列:
- 内核为监听套接字维护两个队列(具体实现可能因操作系统而异):
- 未完成队列(SYN_RCVD 状态):客户端已发送 SYN,但未完成三次握手。
- 已完成队列(ESTABLISHED 状态):已完成三次握手,等待
accept()
取出。
backlog
参数通常指已完成队列的最大长度(Linux 中默认上限由/proc/sys/net/core/somaxconn
定义)。
- 内核为监听套接字维护两个队列(具体实现可能因操作系统而异):
使用场景与示例
1. 服务端启动监听
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// ...绑定地址(bind())...
// 设置监听队列长度为 128
if (listen(server_fd, 128) == -1) {
perror("listen() failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 循环接受客户端连接
while (1) {
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen);
// ...处理 client_fd...
}
2. backlog 的合理取值
经验值:通常设为 SOMAXCONN(系统定义的最大值,如 Linux 默认 4096)。
调整方法(Linux):
# 临时修改 somaxconn
echo 4096 > /proc/sys/net/core/somaxconn
# 永久修改(需编辑 /etc/sysctl.conf)
net.core.somaxconn = 4096
- 注意:实际允许的连接数受系统资源和并发模型(如多线程、epoll)影响。
4、accept函数
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数详解
参数 |
类型 |
描述 |
---|---|---|
|
|
处于监听状态的套接字描述符(由 |
|
|
输出参数,用于存储客户端地址信息(如 IP 和端口)。若不需要可设为 |
|
|
输入输出参数:传入 |
返回值
成功:返回一个新的已连接套接字描述符(非负整数),专门用于与客户端通信。
失败:返回
-1
,并设置errno
(如EINTR
、ECONNABORTED
)。
核心作用
提取连接:从监听套接字的已完成连接队列(已完成三次握手)中取出一个客户端连接。
生成新套接字:返回的已连接套接字与客户端一一对应,原监听套接字继续接受其他连接。
获取客户端地址:通过
addr
参数获取客户端的 IP 地址和端口(可选)。
5、connect函数
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数详解
参数 | 类型 | 描述 |
---|---|---|
sockfd |
int |
客户端套接字描述符(由 socket() 创建)。 |
addr |
struct sockaddr* |
指向服务端地址结构体的指针(如 sockaddr_in )。 |
addrlen |
socklen_t |
地址结构体的长度(单位:字节)。 |
返回值
- 成功:返回
0
,套接字进入已连接状态(TCP)或设置默认地址(UDP)。 - 失败:返回
-1
,并设置errno
表示错误原因。