第3章 套接字编程简介
Socket基础
一、导读
3.1 本章学习目标
第3章是从理论走向实践的关键转折点。前两章建立了网络协议的知识框架,本章则将这些知识转化为可运行的代码。你将系统学习套接字编程的核心API——这些API是所有网络程序的基础,无论你编写的是Web服务器、聊天应用还是分布式系统,都离不开它们。
本章的核心学习目标包括:
- 掌握socket()函数的参数含义:地址族、套接字类型和协议如何选择?返回值是什么?
- 理解bind()的作用和使用场景:什么时候需要bind?服务器和客户端的bind有何不同?
- 深入理解listen()的机制:监听队列的工作原理?backlog参数的实际含义?
- 掌握accept()的阻塞行为和返回值:accept返回的新描述符与监听描述符的区别?
- 理解connect()的完整流程:TCP三次握手在connect调用中的具体过程?
- 能够独立编写TCP客户/服务器的基本框架:将各个API串联成完整的通信流程。
3.2 为什么本章如此重要
套接字API是网络编程的"Hello World"——看似简单,实则蕴含着大量容易忽视的细节。Stevens在本书中花了大量篇幅讨论这些API的边界情况和错误处理,因为网络程序中80%的bug都源于对这些基础API的误用。
常见的错误包括:
- 不理解
accept()返回新描述符的意义,导致在子进程中忘记关闭监听描述符。 - 不理解
listen()的backlog含义,导致高并发时连接被拒绝。 - 不理解
connect()可能返回EINTR(被信号中断),导致连接建立失败。 - 不理解
bind()中使用INADDR_ANY和特定IP地址的区别。
本章将逐一澄清这些误区,帮助你写出正确的网络代码。
3.3 核心问题引导
在阅读本章时,请带着以下问题思考:
socket() 返回的描述符和 open() 返回的描述符有什么共同点?它们可以互换使用吗?
为什么TCP服务器需要两个不同的描述符(listenfd和connfd),而客户端只需要一个?
listen() 的backlog参数是128,是否意味着最多只能有128个客户同时连接?
accept() 返回后,如果用新的描述符发送数据,监听描述符是否还能接受新的连接?
如果客户端 connect() 一个没有进程监听的端口,会发生什么?错误是如何传递的?
为什么 bind() 端口号小于1024需要root权限?
二、核心概念详解
3.1 socket()函数:创建通信端点
3.1.1 函数原型
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 返回值:成功返回非负描述符,出错返回-13.1.2 参数详解
domain(地址族):
| 常量 | 值 | 说明 |
|---|---|---|
| AF_INET | 2 | IPv4 Internet协议 |
| AF_INET6 | 10 | IPv6 Internet协议 |
| AF_UNIX | 1 | UNIX内部协议(本机进程间通信) |
| AF_UNSPEC | 0 | 未指定 |
type(套接字类型):
| 常量 | 值 | 说明 |
|---|---|---|
| SOCK_STREAM | 1 | 字节流套接字(TCP) |
| SOCK_DGRAM | 2 | 数据报套接字(UDP) |
| SOCK_RAW | 3 | 原始套接字 |
| SOCK_SEQPACKET | 5 | 有序分组套接字 |
可以在type上使用OR操作添加标志:
SOCK_CLOEXEC:设置close-on-exec标志,fork后子进程不会继承该描述符SOCK_NONBLOCK:设置非阻塞标志
protocol(协议):
通常为0,表示让系统根据domain和type自动选择合适的协议。例如 AF_INET + SOCK_STREAM 自动选择TCP,AF_INET + SOCK_DGRAM 自动选择UDP。
3.1.3 返回值与错误处理
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket error");
exit(1);
}常见错误:
EAFNOSUPPORT:不支持指定的地址族EINVAL:不支持指定的套接字类型EMFILE:进程的文件描述符已达上限ENFILE:系统的文件描述符已达上限
文件描述符的本质:socket() 返回的是一个文件描述符——一个非负整数,在进程的文件描述符表中索引一个内核对象。对于套接字描述符,内核创建的是一个 struct socket 对象,它包含了协议信息、缓冲区、状态等。
// 文件描述符的分配规则
// 进程的文件描述符表从0开始,分配最小的可用描述符
// 0 = stdin, 1 = stdout, 2 = stderr
// 所以socket()通常返回3或更大的值
// 验证:
int fd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket fd = %d\n", fd); // 通常输出 3
// 如果先关闭stdin
close(0);
int fd2 = socket(AF_INET, SOCK_STREAM, 0);
printf("socket fd = %d\n", fd2); // 输出 0!3.1.4 常见用法示例
// TCP套接字(IPv4)
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
// UDP套接字(IPv4)
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
// TCP套接字(IPv6)
int tcp6_sock = socket(AF_INET6, SOCK_STREAM, 0);
// 带CLOEXEC标志的TCP套接字(防止fork后子进程继承)
int safe_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
// 原始套接字(需要root权限)
int raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);3.2 bind()函数:绑定本地地址
3.2.1 函数原型
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 返回值:成功返回0,出错返回-13.2.2 参数详解
bind() 将一个本地地址(IP地址+端口号)与套接字描述符关联。
- sockfd:
socket()返回的描述符 - addr:指向协议特定地址结构的指针。对于IPv4,是
struct sockaddr_in * - addrlen:地址结构的长度
// 绑定到特定端口(所有接口)
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 - 所有接口
addr.sin_port = htons(8080); // 端口8080
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));3.2.3 INADDR_ANY vs 特定IP
// 方式1:绑定到INADDR_ANY(推荐用于服务器)
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 服务器将接收来自所有网络接口的连接
// 适用于多网卡或多IP的服务器
// 方式2:绑定到特定IP
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
// 服务器只接收来自192.168.1.100这个接口的连接
// 适用于需要限制访问来源的场景INADDR_ANY的含义:当服务器绑定到 INADDR_ANY 时,它告诉内核"如果有数据报到达本机的任何IP地址且端口匹配,都交付给这个套接字"。当服务器回复时,内核自动选择数据报到达时的那个IP地址作为源地址。
3.2.4 端口号的分配
// 方式1:指定端口号
addr.sin_port = htons(8080); // 绑定到8080端口
// 方式2:让系统自动分配(端口号设为0)
addr.sin_port = htons(0);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
// bind后可以用getsockname获取系统分配的端口号
struct sockaddr_in assigned_addr;
socklen_t len = sizeof(assigned_addr);
getsockname(sockfd, (struct sockaddr *)&assigned_addr, &len);
printf("Assigned port: %d\n", ntohs(assigned_addr.sin_port));端口号小于1024的限制:在大多数UNIX系统中,绑定到0-1023范围的端口号需要超级用户权限。这些是"知名端口",保留给标准服务(如80=HTTP,22=SSH,443=HTTPS)。普通用户只能绑定1024-65535范围的端口。
3.2.5 常见错误
// 错误1:忘记设置sin_family
struct sockaddr_in addr;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "0.0.0.0", &addr.sin_addr);
// 忘记 addr.sin_family = AF_INET;
// 可能导致bind失败或行为异常
// 错误2:地址结构未清零
struct sockaddr_in addr; // 未初始化,sin_zero包含垃圾数据
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// sin_zero中的垃圾数据可能导致bind失败
// 正确做法:memset(&addr, 0, sizeof(addr));
// 错误3:端口号未转换字节序
addr.sin_port = 8080; // 错误!应该用htons(8080)
// 在小端序机器上,8080 (0x1F90) 被解释为 0x901F = 368953.3 listen()函数:开始监听
3.3.1 函数原型
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 返回值:成功返回0,出错返回-13.3.2 两个关键动作
listen() 对套接字做了两件事:
将套接字从主动模式转为被动模式:默认情况下,socket() 创建的套接字是主动的(active),可以用来 connect()。调用 listen() 后,套接字变为被动的(passive),只能用来 accept() 传入的连接。
创建监听队列:内核为该套接字维护两个队列:
- 未完成连接队列(incomplete connection queue):已收到SYN,等待三次握手完成。状态为SYN_RCVD。
- 已完成连接队列(completed connection queue):三次握手已完成,等待 accept() 取出。状态为ESTABLISHED。
SYN到达 → [未完成队列] → 收到ACK → [已完成队列] → accept()取出3.3.3 backlog参数的含义
backlog 参数提示系统已完成连接队列的最大长度。但实际行为因系统而异:
- 传统实现:
backlog是已完成队列和未完成队列的总长度。 - 现代Linux:
backlog是已完成队列的长度。未完成队列的长度由/proc/sys/net/ipv4/tcp_max_syn_backlog控制。
// 设置backlog
listen(sockfd, 128); // 已完成连接队列最大128
// 当已完成队列满时:
// - TCP忽略新的SYN(不回复SYN+ACK)
// - 或者回复SYN+ACK后丢弃(取决于实现)
// - 客户端的connect可能超时backlog的选择:
- 太小:高并发时连接被拒绝
- 太大:浪费内核内存,且可能被SYN flood攻击利用
- 推荐值:128或256,根据实际负载调整
// 查看系统对backlog的实际限制
// cat /proc/sys/net/core/somaxconn
// 默认值通常是128或4096
// listen()的backlog不能超过somaxconn
// 调整somaxconn(需要root)
// sysctl -w net.core.somaxconn=40963.3.4 listen只适用于TCP
listen() 只对面向连接的协议(如TCP)有意义。UDP是无连接的,不存在"监听连接"的概念。对UDP套接字调用 listen() 会返回 EOPNOTSUPP 错误。
3.4 accept()函数:接受连接
3.4.1 函数原型
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 返回值:成功返回非负已连接描述符,出错返回-13.4.2 关键行为
accept() 从已完成连接队列中取出第一个连接,返回一个新的文件描述符,代表与该客户的连接。
返回值的重要性:
- listenfd(监听描述符):保持不变,继续用于监听新的连接。
- connfd(已连接描述符):新返回的描述符,代表与特定客户的连接。用于
read/write数据传输。
// accept的基本用法
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd < 0) {
perror("accept error");
// 处理错误...
}
// 获取客户信息
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cliaddr.sin_addr, ip, sizeof(ip));
printf("Client connected: %s:%d\n", ip, ntohs(cliaddr.sin_port));
// 使用connfd进行数据传输
char buf[1024];
ssize_t n = read(connfd, buf, sizeof(buf));
write(connfd, buf, n);
// 处理完毕后关闭connfd
close(connfd);
// listenfd仍然有效,可以继续accept3.4.3 阻塞行为
默认情况下,accept() 是阻塞的——如果没有已完成的连接在队列中等待,accept() 会一直等待,直到有连接到达。
// accept被信号中断的情况
// 如果进程在accept阻塞期间收到信号,accept返回-1,errno设为EINTR
int connfd;
while (1) {
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd < 0) {
if (errno == EINTR) {
// 被信号中断,重新accept
continue;
}
perror("accept error");
break;
}
// 处理连接...
}3.4.4 addr参数的使用
// 方式1:获取客户地址
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
// cliaddr现在包含客户的IP和端口
// 方式2:不关心客户地址
int connfd = accept(listenfd, NULL, NULL);
// 不需要客户信息时,可以传NULL
// 方式3:addr和addrlen的初始化
// addrlen是值-结果参数:
// - 调用前:告诉accept缓冲区大小
// - 返回后:告诉accept实际写入的地址长度
socklen_t clilen = sizeof(cliaddr); // 必须初始化!
accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
// clilen现在可能是 sizeof(struct sockaddr_in) 或更小常见错误:忘记初始化 addrlen。如果 clilen 未初始化(包含随机值),accept() 可能写入超出缓冲区的数据,导致栈溢出。
3.5 connect()函数:发起连接
3.5.1 函数原型
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 返回值:成功返回0,出错返回-13.5.2 TCP三次握手过程
connect() 触发TCP三次握手。具体过程:
客户端发送SYN(connect() 返回前)
服务器回复SYN+ACK
客户端收到SYN+ACK后发送ACK(connect() 返回)
connect() 在三次握手完成之前不会返回(阻塞模式)。
// 客户端基本流程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("connect error");
exit(1);
}
// 连接已建立,可以开始数据传输3.5.3 自动绑定
如果客户端在 connect() 之前没有调用 bind(),内核会自动为套接字选择一个临时端口(ephemeral port)和源IP地址。
// 客户端通常不需要bind
// 内核在connect时自动选择:
// - 源IP:根据路由表选择到达目的地的最佳接口
// - 源端口:从临时端口范围中选择一个未使用的端口
// Linux临时端口范围:
// cat /proc/sys/net/ipv4/ip_local_port_range
// 32768 60999
// 如果需要知道自动分配的地址:
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
struct sockaddr_in localaddr;
socklen_t len = sizeof(localaddr);
getsockname(sockfd, (struct sockaddr *)&localaddr, &len);
printf("Local: %s:%d\n",
inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));3.5.4 connect的错误处理
// 常见connect错误
// 1. ECONNREFUSED - 服务器未运行或端口未监听
// 服务器主机存在,但没有进程监听指定端口
// TCP收到ICMP Port Unreachable后返回此错误
if (connect(sockfd, ...) < 0) {
if (errno == ECONNREFUSED) {
fprintf(stderr, "Connection refused - server not running?\n");
}
}
// 2. ETIMEDOUT - 连接超时
// 服务器不可达(防火墙丢弃SYN,无响应)
// 默认超时时间很长(75秒到几分钟)
if (errno == ETIMEDOUT) {
fprintf(stderr, "Connection timed out\n");
}
// 3. ENETUNREACH / EHOSTUNREACH - 网络/主机不可达
// 路由表中没有到达目的网络的路由
if (errno == ENETUNREACH || errno == EHOSTUNREACH) {
fprintf(stderr, "No route to host\n");
}
// 4. EINTR - 被信号中断
// connect被信号中断,可以重试
if (errno == EINTR) {
// 重试connect(注意:可能需要重新创建套接字)
}3.5.5 connect与非阻塞
// 设置非阻塞connect
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
int ret = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (ret < 0) {
if (errno == EINPROGRESS) {
// 连接正在进行中(三次握手尚未完成)
// 使用select/poll/epoll等待连接完成
fd_set wfds;
FD_ZERO(&wfds);
FD_SET(sockfd, &wfds);
struct timeval tv = {5, 0}; // 5秒超时
if (select(sockfd + 1, NULL, &wfds, NULL, &tv) > 0) {
int error;
socklen_t len = sizeof(error);
getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
if (error == 0) {
printf("Connected!\n");
} else {
printf("Connect failed: %s\n", strerror(error));
}
} else {
printf("Connect timeout\n");
close(sockfd);
}
} else {
perror("connect error");
}
}3.6 TCP客户/服务器的完整流程
3.6.1 TCP服务器流程
socket() → bind() → listen() → accept() → read/write → close()
↑ │
└────── 循环 ←──────────┘// 完整的TCP服务器框架
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define MAXLINE 4096
#define LISTENQ 128
int main(int argc, char **argv) {
int listenfd, connfd;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen;
char buf[MAXLINE];
ssize_t n;
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
// 1. 创建TCP套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket error");
exit(1);
}
// 可选:设置SO_REUSEADDR,允许快速重启
int optval = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
// 2. 绑定地址和端口
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(atoi(argv[1]));
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind error");
exit(1);
}
// 3. 开始监听
if (listen(listenfd, LISTENQ) < 0) {
perror("listen error");
exit(1);
}
printf("Server listening on port %s\n", argv[1]);
// 4. 循环接受连接
for (;;) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd < 0) {
if (errno == EINTR)
continue; // 被信号中断,重试
perror("accept error");
continue;
}
// 打印客户信息
char cli_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
printf("Connection from %s:%d\n", cli_ip, ntohs(cliaddr.sin_port));
// 5. 处理客户请求(回显)
while ((n = read(connfd, buf, MAXLINE)) > 0) {
write(connfd, buf, n);
}
if (n < 0) {
perror("read error");
}
// 6. 关闭连接
close(connfd);
printf("Connection closed\n");
}
close(listenfd);
return 0;
}3.6.2 TCP客户端流程
socket() → connect() → read/write → close()// 完整的TCP客户端框架
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define MAXLINE 4096
int main(int argc, char **argv) {
int sockfd, n;
struct sockaddr_in servaddr;
char sendline[MAXLINE], recvline[MAXLINE + 1];
if (argc != 3) {
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(1);
}
// 1. 创建TCP套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket error");
exit(1);
}
// 2. 设置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
fprintf(stderr, "inet_pton error for %s\n", argv[1]);
exit(1);
}
// 3. 连接到服务器
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("connect error");
exit(1);
}
printf("Connected to %s:%s\n", argv[1], argv[2]);
// 4. 交互循环
while (fgets(sendline, MAXLINE, stdin) != NULL) {
n = strlen(sendline);
if (write(sockfd, sendline, n) != n) {
perror("write error");
exit(1);
}
n = read(sockfd, recvline, MAXLINE);
if (n < 0) {
perror("read error");
exit(1);
} else if (n == 0) {
printf("Server closed connection\n");
break;
}
recvline[n] = '\0';
fputs(recvline, stdout);
}
// 5. 关闭连接
close(sockfd);
return 0;
}3.6.3 描述符的生命周期
理解描述符的生命周期对于编写正确的网络程序至关重要:
服务器端:
socket() → listenfd (生命周期:整个服务器运行期间)
accept() → connfd (生命周期:单个客户连接期间)
close(connfd) → 关闭单个连接
close(listenfd) → 关闭服务器(通常在服务器退出时)
客户端:
socket() → sockfd (生命周期:整个客户端运行期间)
close(sockfd) → 关闭连接并退出关键区别:
listenfd是"工厂"——不断生产连接connfd是"产品"——每个连接一个,用完即弃- 多个
connfd可以同时存在(并发服务器) listenfd在整个服务器生命周期内只有一个
三、实战案例
3.1 时间获取服务器
// 一个简单的 daytime 服务器
// 客户端连接后获取当前时间,然后服务器关闭连接
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int listenfd, connfd;
struct sockaddr_in servaddr;
char buf[64];
time_t ticks;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(1300); // 使用非标准端口
int optval = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 128);
printf("Daytime server listening on port 1300\n");
for (;;) {
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
char cli_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
printf("Connection from %s:%d\n", cli_ip, ntohs(cliaddr.sin_port));
ticks = time(NULL);
snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
write(connfd, buf, strlen(buf));
close(connfd);
}
return 0;
}测试:
# 终端1:启动服务器
./daytime_server
# 终端2:使用nc连接
nc localhost 1300
# 输出: Tue Oct 6 15:30:45 20263.2 获取套接字信息的工具函数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
void print_sock_info(int sockfd, const char *label) {
struct sockaddr_in local, remote;
socklen_t local_len = sizeof(local);
socklen_t remote_len = sizeof(remote);
printf("=== %s (fd=%d) ===\n", label, sockfd);
if (getsockname(sockfd, (struct sockaddr *)&local, &local_len) == 0) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &local.sin_addr, ip, sizeof(ip));
printf(" Local: %s:%d\n", ip, ntohs(local.sin_port));
}
if (getpeername(sockfd, (struct sockaddr *)&remote, &remote_len) == 0) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &remote.sin_addr, ip, sizeof(ip));
printf(" Remote: %s:%d\n", ip, ntohs(remote.sin_port));
} else {
printf(" Remote: (not connected)\n");
}
}四、本章小结
本章系统介绍了套接字编程的核心API,核心要点回顾:
socket():创建通信端点,返回文件描述符。参数决定地址族(IPv4/IPv6)、套接字类型(TCP/UDP)和协议。
bind():将套接字绑定到本地地址。服务器必须bind,客户端通常不需要。INADDR_ANY 绑定所有接口。端口号需要 htons() 转换。
listen():将套接字转为被动模式,创建监听队列。backlog控制已完成连接队列的大小。仅适用于TCP。
accept():从监听队列取出连接,返回新的已连接描述符。listenfd保持不变继续监听。阻塞等待直到有连接到达。
connect():触发TCP三次握手。自动绑定源地址(如果未bind)。常见错误包括ECONNREFUSED、ETIMEDOUT、EINTR。
完整流程:服务器 socket→bind→listen→accept→read/write→close,客户端 socket→connect→read/write→close。
五、思考与练习
思考题
如果服务器在 accept() 返回后、处理请求前崩溃了,客户端会收到什么错误?为什么?
为什么 accept() 要返回新的描述符而不是复用 listenfd?如果复用会有什么问题?
一个服务器可以 bind() 到同一个端口但不同的IP地址吗?如果可以,数据如何路由?
listen() 的backlog设为128,但 somaxconn 系统参数是64,实际生效的值是多少?
如果客户端 connect() 一个防火墙后面的服务器,防火墙丢弃所有SYN包,客户端多久会收到超时错误?
编程练习
多接口服务器:编写一个服务器,分别绑定到 127.0.0.1 和 0.0.0.0 的同一端口,观察行为。解释为什么一个成功另一个失败(或都成功/都失败)。
连接信息显示器:修改echo服务器,在每个客户连接时打印详细的连接信息(本地地址/端口、远程地址/端口、使用的文件描述符编号)。
带超时的connect:实现一个非阻塞connect,设置5秒超时。如果超时则打印错误并退出,而不是等待系统默认的长时间超时。