Socket编程核心API详解
目录
一、创建套接字
1、函数原型
2、参数说明
1. domain(协议族/地址族)
2. type(套接字类型)
3. protocol(协议类型)
3、返回值
4、示例代码
1. 创建 TCP 套接字
2. 创建 UDP 套接字
3. 创建原始套接字(需 root 权限)
5、关键注意事项
6、底层原理
7、常见问题
二、绑定端口号(服务器端)
1、函数原型
2、参数说明
1. sockfd(套接字描述符)
2. addr(套接字地址结构体)
struct sockaddr_in(IPv4)
struct sockaddr_in6(IPv6)
struct sockaddr(通用地址结构)
3. addrlen(地址结构体长度)
3、返回值
4、示例代码
1. 绑定 IPv4 TCP 套接字
2. 绑定 IPv6 UDP 套接字
5、关键注意事项
1. 地址格式转换
2. 特殊地址
3. 端口占用(EADDRINUSE)
4. 权限问题(EACCES)
5. 后续操作
6、常见问题
Q1:bind() 失败时如何调试?
Q2:INADDR_ANY 和 127.0.0.1 的区别?
Q3:为什么 bind() 需要 addrlen?
7、总结
三、监听套接字(TCP服务器)
1、函数原型
2、参数说明
1. sockfd(套接字描述符)
2. backlog(等待连接队列的最大长度)
3、返回值
4、工作流程
5、关键注意事项
1. 监听队列的行为
2. 野蛮绑定(未调用 bind())
3. backlog 的实际限制
4. 监听套接字 vs 连接套接字
6、常见问题
Q1:listen() 和 accept() 的关系?
Q2:如何避免连接队列溢出?
Q3:listen() 后能否继续调用 bind()?
Q4:UDP 需要 listen() 吗?
7、示例代码
8、总结
四、接收连接请求(TCP服务器)
1、函数原型
2、参数说明
1. sockfd(监听套接字描述符)
2. addr(客户端地址结构体指针)
3. addrlen(地址长度指针)
3、返回值
4、工作流程
5、关键注意事项
1. 监听套接字 vs 连接套接字
2. 阻塞与非阻塞模式
3. 多线程/多进程处理
4. 地址结构体的兼容性
5. 错误处理
6、常见问题
Q1:accept() 返回的新套接字与监听套接字的关系?
Q2:如果队列中没有连接,accept() 会怎样?
Q3:如何获取客户端的 IP 和端口?
Q4:accept() 和 connect() 的关系?
Q5:能否多次调用 accept() 处理多个客户端?
7、示例代码(完整服务器)
8、总结
五、建立连接(TCP客户端)
1、函数原型
2、参数说明
1. sockfd(套接字描述符)
2. addr(服务器地址结构体指针)
3. addrlen(地址结构体长度)
3、返回值
4、工作流程
5、关键注意事项
1. 阻塞与非阻塞模式
2. 错误处理
3. 地址结构体的兼容性
4. 连接后的套接字行为
5. 多次调用 connect()
6、常见问题
Q1:connect() 失败后能否重试?
Q2:如何设置连接超时?
Q3:connect() 和 bind() 的关系?
Q4:如何获取连接后的本地地址和端口?
Q5:connect() 对 UDP 的作用?
7、示例代码(完整客户端)
8、总结
六、完整TCP服务器和客户端示例
TCP服务器
TCP客户端
七、总结
Socket编程是网络通信的基础,它提供了一组API用于在不同主机或同一主机的不同进程之间建立网络连接。下面我将详细讲解Socket编程中的常见API:
一、创建套接字
1、函数原型
int socket(int domain, int type, int protocol) 是 Unix/Linux 系统中用于创建套接字(socket)的核心系统调用函数。它是网络通信和进程间通信的基础,用于创建一个新的套接字描述符。以下是对该函数的详细讲解:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

2、参数说明
1. domain(协议族/地址族)
指定套接字使用的通信协议族(即地址格式),常见的值包括:
-
AF_INET:IPv4 互联网协议族(如 TCP/UDP over IPv4)。 -
AF_INET6:IPv6 互联网协议族。 -
AF_UNIX(或AF_LOCAL):本地进程间通信(通过文件系统路径)。 -
AF_PACKET:直接访问底层网络接口(如链路层数据包)。 -
AF_NETLINK:用户空间与内核通信(如路由表操作)。
注意:AF_(Address Family)和 PF_(Protocol Family)在历史上可能混用,但现代代码中通常统一使用 AF_。

2. type(套接字类型)
指定套接字的通信语义,常见的值包括:
-
SOCK_STREAM:面向连接的可靠字节流(如 TCP)。提供双向、有序、无重复的数据传输。 -
SOCK_DGRAM:无连接的数据报服务(如 UDP)。不可靠,但开销小。 -
SOCK_RAW:原始套接字,允许直接操作网络层协议(如自定义 IP 头)。 -
SOCK_SEQPACKET:面向连接的固定长度有序数据报。 -
SOCK_RDM:可靠但无序的数据报(较少用)。

3. protocol(协议类型)
指定具体的协议,通常设为 0(自动选择默认协议)。例如:
-
若
domain=AF_INET且type=SOCK_STREAM,默认协议是IPPROTO_TCP。 -
若
domain=AF_INET且type=SOCK_DGRAM,默认协议是IPPROTO_UDP。
其他常见协议:
-
IPPROTO_TCP:TCP 协议。 -
IPPROTO_UDP:UDP 协议。 -
IPPROTO_ICMP:ICMP 协议(用于ping)。
注意:某些 domain 和 type 组合可能只支持特定协议(如 SOCK_RAW 必须显式指定协议)。

3、返回值
-
成功:返回一个非负整数,表示套接字描述符(类似文件描述符,用于后续操作如
bind()、listen()、connect()等)。 -
失败:返回
-1,并设置errno表示错误原因(如EACCES权限不足、ENOMEM内存不足等)。

4、示例代码
#include <sys/socket.h>
#include <iostream>int main() {// 创建TCP套接字int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);if (tcp_socket == -1) {std::cerr << "创建TCP套接字失败" << std::endl;return -1;}std::cout << "TCP套接字创建成功,描述符: " << tcp_socket << std::endl;// 创建UDP套接字int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);if (udp_socket == -1) {std::cerr << "创建UDP套接字失败" << std::endl;close(tcp_socket);return -1;}std::cout << "UDP套接字创建成功,描述符: " << udp_socket << std::endl;close(tcp_socket);close(udp_socket);return 0;
}
1. 创建 TCP 套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket == -1) {perror("socket creation failed");exit(EXIT_FAILURE);
}
2. 创建 UDP 套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket == -1) {perror("socket creation failed");exit(EXIT_FAILURE);
}
3. 创建原始套接字(需 root 权限)
int raw_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (raw_socket == -1) {perror("raw socket creation failed");exit(EXIT_FAILURE);
}
5、关键注意事项
-
协议兼容性:并非所有
domain和type的组合都有效。例如,AF_INET+SOCK_SEQPACKET可能无效。 -
原始套接字权限:创建
SOCK_RAW套接字通常需要root权限(因可能绕过内核协议栈)。 -
资源释放:使用
close()关闭套接字描述符以释放资源。 -
错误处理:始终检查返回值,并通过
errno诊断错误(如perror()或strerror(errno))。 -
后续操作:创建套接字后,通常需要:
-
服务器端:
bind()→listen()→accept()。 -
客户端:
connect()。
-
6、底层原理
-
套接字是内核提供的通信端点,通过文件描述符抽象。
-
调用
socket()时,内核会分配内核数据结构(如struct socket和struct sock),并返回描述符供用户空间操作。
7、常见问题
Q:protocol=0 的含义?
A:表示根据 domain 和 type 自动选择默认协议(如 TCP 对应 SOCK_STREAM)。
Q:AF_INET 和 PF_INET 的区别?
A:历史上 PF_ 表示协议族,AF_ 表示地址族,但 POSIX 规范中两者应一致,通常混用无害。
Q:如何创建非阻塞套接字?
A:创建后通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置非阻塞标志。
通过理解 socket() 的参数和行为,可以灵活实现各种网络通信场景(如 HTTP 服务器、UDP 广播、自定义协议等)。
二、绑定端口号(服务器端)
1、函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 是 Unix/Linux 网络编程中用于将套接字(socket)绑定到特定地址和端口的系统调用函数。它是服务器端程序初始化套接字的关键步骤之一。以下是对该函数的详细讲解:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2、参数说明

1. sockfd(套接字描述符)
-
作用:由
socket()创建的套接字描述符。 -
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
2. addr(套接字地址结构体)
-
作用:指向一个
struct sockaddr类型的指针,用于指定绑定的地址和端口。 -
常见结构体:
-
struct sockaddr_in(用于 IPv4) -
struct sockaddr_in6(用于 IPv6) -
struct sockaddr_un(用于 Unix 域套接字)
-
struct sockaddr_in(IPv4)
struct sockaddr_in {sa_family_t sin_family; // 地址族(AF_INET)in_port_t sin_port; // 16 位端口号(网络字节序)struct in_addr sin_addr; // 32 位 IPv4 地址(网络字节序)char sin_zero[8]; // 未使用(填充为 0)
};struct in_addr {uint32_t s_addr; // 32 位 IPv4 地址(网络字节序)
};
struct sockaddr_in6(IPv6)
struct sockaddr_in6 {sa_family_t sin6_family; // 地址族(AF_INET6)in_port_t sin6_port; // 16 位端口号(网络字节序)uint32_t sin6_flowinfo; // IPv6 流量信息struct in6_addr sin6_addr; // 128 位 IPv6 地址uint32_t sin6_scope_id; // 接口 ID(用于链路本地地址)
};struct in6_addr {unsigned char s6_addr[16]; // 128 位 IPv6 地址
};
struct sockaddr(通用地址结构)
struct sockaddr {sa_family_t sa_family; // 地址族(如 AF_INET、AF_INET6)char sa_data[14]; // 地址数据(具体格式取决于 sa_family)
};
注意:bind() 接受 struct sockaddr*,但实际使用时通常转换为 struct sockaddr_in* 或 struct sockaddr_in6*。
3. addrlen(地址结构体长度)
-
作用:指定
addr指向的结构体的长度(字节数)。 -
计算方式:
-
IPv4:
sizeof(struct sockaddr_in) -
IPv6:
sizeof(struct sockaddr_in6) -
Unix 域套接字:
sizeof(struct sockaddr_un)
-
3、返回值
-
成功:返回
0。 -
失败:返回
-1,并设置errno(如EADDRINUSE地址已占用,EACCES权限不足)。

4、示例代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>int main() {int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {std::cerr << "创建套接字失败" << std::endl;return -1;}// 设置服务器地址结构struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口server_addr.sin_port = htons(8080); // 设置端口号为8080// 绑定套接字到指定端口if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {std::cerr << "绑定端口失败" << std::endl;close(server_fd);return -1;}std::cout << "成功绑定到端口8080" << std::endl;close(server_fd);return 0;
}
1. 绑定 IPv4 TCP 套接字
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 1. 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 设置地址和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080); // 端口 8080(网络字节序)server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地 IP(0.0.0.0)// 3. 绑定套接字if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("Bind successful!\n");close(sockfd);return 0;
}
2. 绑定 IPv6 UDP 套接字
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in6 server_addr;sockfd = socket(AF_INET6, SOCK_DGRAM, 0);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}memset(&server_addr, 0, sizeof(server_addr));server_addr.sin6_family = AF_INET6;server_addr.sin6_port = htons(8080); // 端口 8080server_addr.sin6_addr = in6addr_any; // 监听所有 IPv6 地址(::)if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("Bind successful!\n");close(sockfd);return 0;
}
5、关键注意事项
1. 地址格式转换
将本机的主机字节序(可能是大端或者小端),转化为网络字节序(为大端)!!!(这个要记住!!!)
-
htons():主机字节序 → 网络字节序(用于端口号)。 -
htonl():主机字节序 → 网络字节序(用于 IPv4 地址,如INADDR_ANY)。 -
inet_pton():字符串 IP → 二进制格式(如"192.168.1.1"→in_addr)。
2. 特殊地址
-
INADDR_ANY(IPv4):绑定到所有本地 IP(0.0.0.0)。 -
in6addr_any(IPv6):绑定到所有 IPv6 地址(::)。 -
127.0.0.1:仅允许本地回环访问。
3. 端口占用(EADDRINUSE)
-
如果端口已被占用,
bind()会失败。 -
解决方法:
-
使用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, ...)允许端口重用。 -
确保之前的进程已释放端口(如调用
close())。
-
4. 权限问题(EACCES)
-
在 Linux 上,绑定
1024以下的端口需要root权限。
5. 后续操作
-
TCP 服务器:
bind()→listen()→accept()。 -
UDP 服务器:
bind()→recvfrom()。 -
客户端:通常不需要
bind(),除非需要指定本地端口。
6、常见问题
Q1:bind() 失败时如何调试?
检查 errno:
if (bind(...) == -1) {perror("bind error");printf("Error code: %d\n", errno);
}
常见错误:
-
EADDRINUSE:端口已被占用。 -
EACCES:权限不足。 -
EINVAL:套接字已绑定或未正确初始化。
Q2:INADDR_ANY 和 127.0.0.1 的区别?
-
INADDR_ANY(0.0.0.0):监听所有网络接口(包括外网和内网)。 -
127.0.0.1:仅允许本地回环访问(外部无法连接)。
Q3:为什么 bind() 需要 addrlen?
-
因为
struct sockaddr是通用结构,addrlen告诉内核addr的具体格式(IPv4/IPv6/Unix 域套接字)。
7、总结
-
bind()用于将套接字与特定 IP 和端口绑定。 -
关键步骤:
-
创建套接字(
socket())。 -
填充
struct sockaddr_in或struct sockaddr_in6。 -
调用
bind()。
-
-
错误处理和地址格式转换是常见难点。
通过正确使用 bind(),服务器程序可以监听指定的网络接口和端口,为后续的 listen() 和 accept() 做准备。
三、监听套接字(TCP服务器)
1、函数原型
int listen(int sockfd, int backlog) 是 Unix/Linux 网络编程中用于 将套接字设置为被动监听状态 的系统调用函数,主要用于 TCP 服务器。它是服务器端程序初始化套接字的关键步骤之一,通常在 bind() 之后、accept() 之前调用。以下是对该函数的详细讲解:
#include <sys/socket.h>
int listen(int sockfd, int backlog);

2、参数说明

1. sockfd(套接字描述符)
作用:由 socket() 创建并已绑定(bind())的套接字描述符。
要求:
-
必须是 面向连接的套接字(如
SOCK_STREAM,即 TCP)。 -
必须已经调用
bind()绑定了本地地址和端口(除非是野蛮绑定,见下文)。
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(sockfd, backlog); // 开始监听
2. backlog(等待连接队列的最大长度)
作用:指定 未完成连接队列(SYN_RCVD 状态) 和 已完成连接队列(ESTABLISHED 状态) 的总和上限。
详细解释:
-
未完成连接队列:客户端已发送
SYN,但服务器尚未完成三次握手(SYN_RCVD状态)。 -
已完成连接队列:三次握手已完成,等待
accept()取走(ESTABLISHED状态)。
历史与现代实现:
-
传统实现:
backlog直接限制队列长度。 -
Linux 2.2+:
backlog是 已完成连接队列 的上限,未完成连接队列由内核参数net.ipv4.tcp_max_syn_backlog控制。 -
默认值:通常为
128(Linux)或SOMAXCONN(系统最大值,可通过cat /proc/sys/net/core/somaxconn查看)。
设置建议:
-
高并发服务器可设为
SOMAXCONN(如listen(sockfd, SOMAXCONN))。 -
过小会导致连接被拒绝(
ECONNREFUSED),过大会占用更多内存。
3、返回值
-
成功:返回
0。 -
失败:返回
-1,并设置errno(如EBADF无效套接字,ENOTSOCK非套接字描述符)。

4、工作流程
-
创建套接字(
socket())。 -
绑定地址和端口(
bind())。 -
设置为监听状态(
listen())。 -
接受客户端连接(
accept())。
// 典型 TCP 服务器监听流程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr = {.sin_family = AF_INET,.sin_port = htons(8080),.sin_addr.s_addr = INADDR_ANY
};
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(sockfd, SOMAXCONN); // 开始监听
5、关键注意事项
1. 监听队列的行为
三次握手与队列:
-
客户端
connect()触发三次握手。 -
服务器在收到
SYN后,将连接放入 未完成队列(SYN_RCVD)。 -
完成三次握手后,连接移至 已完成队列(
ESTABLISHED),等待accept()。
队列溢出:
-
如果两个队列的总和超过
backlog,新连接会被 静默丢弃(客户端收到ECONNREFUSED)。 -
可通过
netstat -ntp或ss -ltp查看监听队列状态。
2. 野蛮绑定(未调用 bind())
如果未调用 bind(),listen() 会触发 隐式绑定:
-
内核自动分配一个临时端口(通常从
32768开始)。 -
适用于临时服务器或测试场景。
3. backlog 的实际限制
Linux 调整:
-
最终队列长度取
min(backlog, somaxconn)。 -
可通过
/proc/sys/net/core/somaxconn修改系统上限:echo 1024 > /proc/sys/net/core/somaxconn
其他系统:macOS/BSD 的 backlog 可能限制为 5(需通过 sysctl 调整)。
4. 监听套接字 vs 连接套接字
-
监听套接字(
sockfd):仅用于接受新连接(accept())。 -
连接套接字(
accept()返回):用于与客户端通信。
6、常见问题
Q1:listen() 和 accept() 的关系?
-
listen():将套接字设为被动监听,初始化连接队列。 -
accept():从已完成队列中取出一个连接,返回新的套接字用于通信。
Q2:如何避免连接队列溢出?
-
增大
backlog(如SOMAXCONN)。 -
调整
net.ipv4.tcp_max_syn_backlog(未完成队列上限)。 -
优化
accept()速度,避免长时间阻塞。
Q3:listen() 后能否继续调用 bind()?
-
不能!
bind()必须在listen()前调用。
Q4:UDP 需要 listen() 吗?
-
不需要!
listen()仅用于面向连接的套接字(如 TCP)。UDP 是无连接的,直接使用recvfrom()/sendto()。
7、示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 1. 创建 TCP 套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 绑定地址和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}// 3. 开始监听if (listen(sockfd, SOMAXCONN) == -1) {perror("listen failed");close(sockfd);exit(EXIT_FAILURE);}printf("Server listening on port 8080...\n");// 4. 接受连接(此处省略 accept() 循环)close(sockfd);return 0;
}
8、总结
-
listen()的作用是将套接字标记为 被动监听,并初始化连接队列。 -
backlog参数控制等待队列的长度,影响服务器并发处理能力。 -
典型流程:
socket()→bind()→listen()→accept()。 -
监听套接字仅用于接受连接,实际通信需通过
accept()返回的新套接字。
通过合理配置 listen(),可以优化服务器的连接处理能力,避免队列溢出导致的连接丢失问题。
四、接收连接请求(TCP服务器)
1、函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) 是 Unix/Linux 网络编程中用于 从监听套接字的已完成连接队列中提取一个客户端连接 的系统调用函数,主要用于 TCP 服务器。它是服务器端处理客户端连接的关键步骤,通常在 listen() 之后调用。以下是对该函数的详细讲解:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

2、参数说明

1. sockfd(监听套接字描述符)
作用:由 socket() 创建、bind() 绑定并调用 listen() 的 监听套接字。
特点:
-
该套接字处于被动监听状态(
LISTEN)。 -
仅用于接受新连接,不用于实际数据通信。
示例:
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listen_sock, SOMAXCONN);
int client_sock = accept(listen_sock, &client_addr, &addrlen); // 提取连接
2. addr(客户端地址结构体指针)
作用:用于 返回客户端的地址信息(如 IP 和端口)。
类型:通常是 struct sockaddr_in(IPv4)或 struct sockaddr_in6(IPv6),但需强制转换为 struct sockaddr*。
参数传递:
-
调用前:
addr指向一个足够大的缓冲区(如struct sockaddr_storage)。 -
调用后:内核填充客户端地址信息。
-
若不需要地址信息,可设为
NULL。
示例:
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
int client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);
3. addrlen(地址长度指针)
作用:
-
输入:告诉内核
addr缓冲区的长度。 -
输出:返回实际写入的地址长度。
要求:
-
调用前必须初始化(如
socklen_t addrlen = sizeof(struct sockaddr_in))。 -
若
addr为NULL,addrlen也应为NULL。
示例:
socklen_t addrlen = sizeof(client_addr); // 初始化
accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);
printf("Client port: %d\n", ntohs(client_addr.sin_port)); // 打印客户端端口
3、返回值
-
成功:返回一个新的 已连接套接字描述符(
client_sock),用于与客户端通信。 -
失败:返回
-1,并设置errno(如EWOULDBLOCK非阻塞模式下无连接,ECONNABORTED连接中止)。

4、工作流程
-
监听套接字(
sockfd)通过listen()进入被动模式。 -
客户端连接:客户端调用
connect(),触发三次握手。 -
连接完成:握手完成后,连接进入服务器的 已完成队列(
ESTABLISHED状态)。 -
提取连接:
accept()从队列中取出一个连接,返回一个新的套接字(client_sock)用于通信。 -
数据通信:通过
client_sock使用read()/write()或send()/recv()交换数据。
// 典型 TCP 服务器 accept 流程
int listen_sock = socket(...);
bind(listen_sock, ...);
listen(listen_sock, SOMAXCONN);while (1) {struct sockaddr_in client_addr;socklen_t addrlen = sizeof(client_addr);int client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);if (client_sock == -1) {perror("accept failed");continue;}printf("New client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 处理客户端请求(如读写数据)char buf[1024];read(client_sock, buf, sizeof(buf));write(client_sock, "Hello", 5);close(client_sock); // 关闭连接套接字
}
close(listen_sock); // 关闭监听套接字
5、关键注意事项
1. 监听套接字 vs 连接套接字
监听套接字(sockfd):
-
用于接受新连接(
accept())。 -
一个服务器通常只有一个监听套接字。
连接套接字(client_sock):
-
由
accept()返回,用于与单个客户端通信。 -
每个客户端连接对应一个独立的套接字。
2. 阻塞与非阻塞模式
默认阻塞模式:accept() 会阻塞,直到队列中有已完成连接。
非阻塞模式:
-
通过
fcntl(sockfd, F_SETFL, O_NONBLOCK)设置。 -
若队列为空,
accept()立即返回-1,errno为EWOULDBLOCK或EAGAIN。 -
通常与
select()/poll()/epoll()配合使用。
3. 多线程/多进程处理
每个 accept() 返回的 client_sock 可独立处理:
-
多进程:
fork()后子进程处理连接,父进程继续监听。 -
多线程:创建新线程处理
client_sock。 -
I/O 多路复用:用
epoll监听多个client_sock。
4. 地址结构体的兼容性
addr 参数是 struct sockaddr* 类型,但实际使用时需转换为具体类型(如 sockaddr_in):
struct sockaddr_in client_addr;
accept(sockfd, (struct sockaddr*)&client_addr, &addrlen);
使用 sockaddr_storage 可兼容 IPv4 和 IPv6:
struct sockaddr_storage client_addr;
accept(sockfd, (struct sockaddr*)&client_addr, &addrlen);
5. 错误处理
常见错误:
-
EWOULDBLOCK/EAGAIN:非阻塞模式下无连接。 -
ECONNABORTED:连接在队列中时被中止(如客户端超时)。 -
EINTR:被信号中断,可重启调用。
6、常见问题
Q1:accept() 返回的新套接字与监听套接字的关系?
-
完全独立:新套接字仅用于当前客户端通信,监听套接字继续监听其他连接。
Q2:如果队列中没有连接,accept() 会怎样?
-
阻塞模式:一直等待,直到有连接。
-
非阻塞模式:立即返回
-1,errno为EWOULDBLOCK。
Q3:如何获取客户端的 IP 和端口?
-
通过
addr和addrlen参数返回客户端地址信息:struct sockaddr_in *addr_in = (struct sockaddr_in*)addr; printf("Client IP: %s, Port: %d\n", inet_ntoa(addr_in->sin_addr), ntohs(addr_in->sin_port));
Q4:accept() 和 connect() 的关系?
-
accept()是服务器端提取连接,connect()是客户端发起连接。两者共同完成 TCP 三次握手。
Q5:能否多次调用 accept() 处理多个客户端?
-
可以!每次调用
accept()从队列中取出一个连接,直到队列为空。
7、示例代码(完整服务器)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int listen_sock, client_sock;struct sockaddr_in server_addr, client_addr;socklen_t addrlen = sizeof(client_addr);// 1. 创建监听套接字listen_sock = socket(AF_INET, SOCK_STREAM, 0);if (listen_sock == -1) {perror("socket failed");exit(EXIT_FAILURE);}// 2. 绑定地址和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(listen_sock);exit(EXIT_FAILURE);}// 3. 开始监听if (listen(listen_sock, SOMAXCONN) == -1) {perror("listen failed");close(listen_sock);exit(EXIT_FAILURE);}printf("Server listening on port 8080...\n");// 4. 循环接受客户端连接while (1) {client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);if (client_sock == -1) {perror("accept failed");continue;}// 打印客户端信息char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);printf("New client: %s:%d\n", client_ip, ntohs(client_addr.sin_port));// 5. 处理客户端请求(示例:回显服务)char buf[1024];ssize_t bytes_read;while ((bytes_read = read(client_sock, buf, sizeof(buf))) > 0) {write(client_sock, buf, bytes_read); // 回显数据}close(client_sock); // 关闭客户端连接printf("Client disconnected.\n");}close(listen_sock); // 关闭监听套接字(实际不会执行到这里)return 0;
}
8、总结
-
accept()的作用是从监听套接字的已完成队列中提取一个客户端连接,返回新的套接字用于通信。 -
参数
addr和addrlen用于获取客户端地址信息。 -
监听套接字和连接套接字分离,支持多客户端并发处理。
-
阻塞/非阻塞模式、错误处理和多线程/I/O 多路复用是实际开发中的关键点。
通过合理使用 accept(),可以构建高效的 TCP 服务器,处理多个客户端连接。
五、建立连接(TCP客户端)
1、函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 是 Unix/Linux 网络编程中用于 主动发起 TCP 连接 的系统调用函数,主要用于 TCP 客户端。它尝试与指定的服务器地址建立连接,完成 TCP 三次握手。以下是详细讲解:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2、参数说明

1. sockfd(套接字描述符)
作用:由 socket() 创建的 主动套接字,用于发起连接。
特点:
-
必须是
SOCK_STREAM(TCP)或SOCK_DGRAM(UDP,但 UDP 通常不调用connect())。 -
调用
connect()后,套接字变为 已连接状态(TCP 可直接用send()/recv())。
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP 套接字
2. addr(服务器地址结构体指针)
作用:指定要连接的 服务器地址和端口。
类型:通常是 struct sockaddr_in(IPv4)或 struct sockaddr_in6(IPv6),但需强制转换为 const struct sockaddr*。
关键字段(以 sockaddr_in 为例):
-
sin_family:地址族(如AF_INET)。 -
sin_port:服务器端口(需用htons()转换)。 -
sin_addr:服务器 IP 地址(如inet_addr("192.168.1.1")或INADDR_ANY)。
示例:
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 服务器端口 8080
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 服务器 IP
3. addrlen(地址结构体长度)
作用:指定 addr 指向的结构体的长度。
要求:
-
必须正确设置(如
sizeof(struct sockaddr_in))。 -
若
addr为NULL,addrlen也应为0(但无实际意义)。
示例:
socklen_t addrlen = sizeof(server_addr);
connect(sockfd, (struct sockaddr*)&server_addr, addrlen);
3、返回值
-
成功:返回
0,表示连接建立成功。 -
失败:返回
-1,并设置errno(如ECONNREFUSED连接被拒绝,ETIMEDOUT超时)。

4、工作流程
-
创建套接字:客户端调用
socket()创建套接字。 -
设置服务器地址:填充
sockaddr_in结构体(IP + 端口)。 -
发起连接:调用
connect(),触发 TCP 三次握手:-
客户端发送
SYN包到服务器。 -
服务器响应
SYN-ACK。 -
客户端回复
ACK,连接建立。
-
-
连接结果:
-
成功:套接字变为 已连接状态,可直接通信。
-
失败:返回错误,需处理(如重试或退出)。
-
// 典型 TCP 客户端 connect 流程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {perror("socket failed");exit(EXIT_FAILURE);
}struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect failed");close(sockfd);exit(EXIT_FAILURE);
}printf("Connected to server!\n");
// 现在可以通过 sockfd 发送/接收数据
write(sockfd, "Hello", 5);
close(sockfd);
5、关键注意事项
1. 阻塞与非阻塞模式
默认阻塞模式:
-
connect()会阻塞,直到连接成功或失败。 -
若服务器不可达,可能长时间阻塞(受系统超时设置影响)。
非阻塞模式:
-
通过
fcntl(sockfd, F_SETFL, O_NONBLOCK)设置。 -
调用后立即返回
-1,errno为EINPROGRESS,表示连接正在进行。 -
需用
select()/poll()/epoll()监听套接字可写事件,再调用getsockopt()检查连接状态。
2. 错误处理
常见错误:
-
ECONNREFUSED:服务器未监听指定端口。 -
ETIMEDOUT:连接超时(如服务器无响应)。 -
ENETUNREACH:网络不可达(如路由错误)。 -
EINPROGRESS(非阻塞模式下连接进行中)。
示例:
if (connect(sockfd, (struct sockaddr*)&server_addr, addrlen) == -1) {if (errno == ECONNREFUSED) {printf("Server refused connection.\n");} else if (errno == ETIMEDOUT) {printf("Connection timed out.\n");}close(sockfd);exit(EXIT_FAILURE);
}
3. 地址结构体的兼容性
addr 参数是 const struct sockaddr* 类型,但实际使用时需转换为具体类型(如 sockaddr_in):
struct sockaddr_in server_addr;
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
使用 sockaddr_storage 可兼容 IPv4 和 IPv6:
struct sockaddr_storage server_addr;
// 填充 server_addr...
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
4. 连接后的套接字行为
TCP 套接字:
-
连接建立后,可直接用
send()/recv()或write()/read()通信。 -
无需每次通信都指定服务器地址(与 UDP 不同)。
UDP 套接字:通常不调用 connect(),但调用后可通过 send()/recv() 简化操作(仅接收指定地址的数据)。
5. 多次调用 connect()
未连接的套接字:首次调用 connect() 发起连接。
已连接的套接字:
-
再次调用
connect()会返回EISCONN错误(已连接)。 -
若需重新连接,需先
close()套接字或调用connect()到新地址(部分系统支持)。
6、常见问题
Q1:connect() 失败后能否重试?
可以!但需注意:
-
立即重试可能失败(如服务器未释放
TIME_WAIT状态)。 -
建议延迟后重试(如指数退避算法)。
Q2:如何设置连接超时?
-
阻塞模式:使用
alarm()或setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO/SO_SNDTIMEO)设置超时。 -
非阻塞模式:结合
select()实现超时控制。
Q3:connect() 和 bind() 的关系?
-
通常不需要
bind():客户端通常由内核自动分配临时端口。 -
需要
bind()的情况:-
必须使用特定本地端口(如 FTP 主动模式)。
-
多网卡环境下需指定源 IP。
-
Q4:如何获取连接后的本地地址和端口?
-
调用
getsockname():struct sockaddr_in local_addr; socklen_t local_len = sizeof(local_addr); getsockname(sockfd, (struct sockaddr*)&local_addr, &local_len); printf("Local port: %d\n", ntohs(local_addr.sin_port));
Q5:connect() 对 UDP 的作用?
-
UDP 是无连接的,但调用
connect()后:-
只能向该地址发送数据(
send()无需指定目标地址)。 -
仅接收来自该地址的数据(过滤其他源)。
-
7、示例代码(完整客户端)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 1. 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket failed");exit(EXIT_FAILURE);}// 2. 设置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);if (inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr) <= 0) {perror("invalid address");close(sockfd);exit(EXIT_FAILURE);}// 3. 发起连接if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect failed");close(sockfd);exit(EXIT_FAILURE);}printf("Connected to server!\n");// 4. 发送数据const char *msg = "Hello from client!";write(sockfd, msg, strlen(msg));// 5. 接收响应(可选)char buf[1024];ssize_t bytes_read = read(sockfd, buf, sizeof(buf));if (bytes_read > 0) {buf[bytes_read] = '\0';printf("Server reply: %s\n", buf);}close(sockfd);return 0;
}
8、总结
-
connect()是 TCP 客户端发起连接的核心函数,完成三次握手。 -
参数
addr和addrlen用于指定服务器地址。 -
阻塞/非阻塞模式、错误处理和重试机制是实际开发中的关键点。
-
连接成功后,套接字可直接用于数据通信(TCP)或简化 UDP 操作。
通过合理使用 connect(),可以构建可靠的 TCP 客户端程序。
六、完整TCP服务器和客户端示例
TCP服务器
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>#define PORT 8080
#define BUFFER_SIZE 1024int main() {// 创建套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");return -1;}// 设置服务器地址struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定套接字if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(server_fd);return -1;}// 监听if (listen(server_fd, 5) == -1) {perror("listen");close(server_fd);return -1;}std::cout << "服务器正在监听端口 " << PORT << "..." << std::endl;// 接受连接struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");close(server_fd);return -1;}// 打印客户端信息char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);std::cout << "客户端已连接: " << client_ip << ":" << ntohs(client_addr.sin_port) << std::endl;// 接收和发送数据char buffer[BUFFER_SIZE];ssize_t bytes_received;while ((bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) {std::cout << "收到消息: " << std::string(buffer, bytes_received) << std::endl;// 回显消息if (send(client_fd, buffer, bytes_received, 0) == -1) {perror("send");break;}}if (bytes_received == -1) {perror("recv");}close(client_fd);close(server_fd);return 0;
}
TCP客户端
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
#include <string>#define PORT 8080
#define BUFFER_SIZE 1024int main() {// 创建套接字int client_fd = socket(AF_INET, SOCK_STREAM, 0);if (client_fd == -1) {perror("socket");return -1;}// 设置服务器地址struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {perror("inet_pton");close(client_fd);return -1;}// 连接到服务器if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect");close(client_fd);return -1;}std::cout << "已连接到服务器 127.0.0.1:" << PORT << std::endl;std::cout << "输入消息(输入'exit'退出):" << std::endl;// 发送和接收数据std::string message;char buffer[BUFFER_SIZE];while (true) {std::getline(std::cin, message);if (message == "exit") {break;}// 发送消息if (send(client_fd, message.c_str(), message.size(), 0) == -1) {perror("send");break;}// 接收回显ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);if (bytes_received <= 0) {if (bytes_received == -1) {perror("recv");}break;}std::cout << "服务器回显: " << std::string(buffer, bytes_received) << std::endl;}close(client_fd);return 0;
}
七、总结
-
socket(): 创建套接字,指定协议族和类型
-
bind(): 将套接字绑定到特定地址和端口(服务器端)
-
listen(): 开始监听连接请求(TCP服务器)
-
accept(): 接受客户端连接请求(TCP服务器)
-
connect(): 主动连接到服务器(TCP客户端)
对于UDP协议,不需要建立连接,因此没有listen()和accept()步骤,而是直接使用sendto()和recvfrom()进行数据收发。
这些API构成了网络编程的基础,理解它们的工作原理和相互关系对于开发网络应用程序至关重要。
