1.简单的服务端和客户端(仅作为内容补充)
文章目录
1.简单的服务端和客户端(仅作为内容补充)
一、什么是 Socket?
Socket(套接字)是操作系统提供的网络通信接口。它把复杂的 TCP/IP 协议栈封装成一组简单的函数调用,让程序员可以像读写文件一样进行网络通信。
生活类比:
| Socket 操作 | 电话类比 |
|---|---|
socket() |
买一部电话 |
bind() |
给电话分配一个号码 |
listen() |
开机,等待来电 |
accept() |
接听电话 |
connect() |
拨打电话 |
read() / write() |
通话 |
close() |
挂断电话 |
二、TCP 通信流程
服务器端 客户端
──────── ────────
socket() socket()
│ │
bind() │
│ │
listen() │
│ │
accept() ←─────── 三次握手 ──────→ connect()
│ │
read()/write() ←──── 数据传输 ────→ read()/write()
│ │
close() close()
服务器必须先启动并监听,客户端才能连接成功。整个通信过程是全双工的——双方可以同时收发数据。
三、核心 API 一览
| 函数 | 原型 | 作用 | 调用方 |
|---|---|---|---|
socket() |
int socket(int domain, int type, int protocol) |
创建套接字 | 双方 |
bind() |
int bind(int fd, const sockaddr *addr, socklen_t len) |
绑定 IP + 端口 | 服务器 |
listen() |
int listen(int fd, int backlog) |
开始监听 | 服务器 |
accept() |
int accept(int fd, sockaddr *addr, socklen_t *len) |
接受连接 | 服务器 |
connect() |
int connect(int fd, const sockaddr *addr, socklen_t len) |
发起连接 | 客户端 |
read() |
ssize_t read(int fd, void *buf, size_t count) |
接收数据 | 双方 |
write() |
ssize_t write(int fd, const void *buf, size_t count) |
发送数据 | 双方 |
close() |
int close(int fd) |
关闭连接 | 双方 |
观察上表你会发现:bind()、connect()、accept() 的地址参数都是指针(sockaddr *),而不是直接传结构体的值。为什么?下一节专门解释。
四、为什么传指针而不是传值?
这是理解 Socket API 设计的关键。几乎所有涉及地址的函数都要求传入 struct sockaddr * 指针,这背后有四个层面的原因。
4.1 哪些函数传了地址指针?
| 函数 | 地址参数 | 方向 | 说明 |
|---|---|---|---|
bind() |
const struct sockaddr *addr |
入参(只读) | 你告诉内核"绑定到这个地址" |
connect() |
const struct sockaddr *addr |
入参(只读) | 你告诉内核"连接到这个地址" |
accept() |
struct sockaddr *addr |
出参(写入) | 内核把客户端地址写回给你 |
getsockname() |
struct sockaddr *addr |
出参(写入) | 内核把本机地址写回给你 |
getpeername() |
struct sockaddr *addr |
出参(写入) | 内核把对端地址写回给你 |
sendto() |
const struct sockaddr *dest_addr |
入参(只读) | UDP 指定目标地址 |
recvfrom() |
struct sockaddr *src_addr |
出参(写入) | UDP 获取发送方地址 |
规律:带
const的是输入型(你传给内核),不带const的是输出型(内核写回给你)。
4.2 原因一:值传递无法"回写"数据
这是最根本的原因。C 语言的函数参数是单向拷贝——函数内部拿到的是副本,修改副本不会影响调用者的原始变量。
// ❌ 假设 accept() 用值传递
sockaddr_in client_addr;
accept(fd, client_addr, &len);
// client_addr 被拷贝了一份传入函数
// 内核修改的是副本,你的 client_addr 永远是空的!
// 你根本拿不到客户端的 IP 和端口。
// ✅ 实际设计:传指针
sockaddr_in client_addr;
accept(fd, (sockaddr *)&client_addr, &len);
// 内核拿到的是 client_addr 的内存地址
// 可以直接往这块内存里写入客户端信息
// 函数返回后,你的 client_addr 就有数据了。
对于 accept()、recvfrom() 这类需要内核向用户空间回写数据的函数,值传递在语言层面就不可行。你必须给内核一个"可以写入的内存地址"。
4.3 原因二:结构体太大,值传递代价高
struct sockaddr_in → 16 字节
struct sockaddr_in6 → 28 字节
struct sockaddr_storage → 128 字节
// 值传递:每次调用都在栈上拷贝整个结构体
bind(fd, addr, sizeof(addr)); // 拷贝 16~128 字节
// 指针传递:只拷贝一个地址(64位系统上 8 字节)
bind(fd, (sockaddr *)&addr, sizeof(addr)); // 只拷贝 8 字节
在高频调用场景下(如 UDP 服务器每秒处理数万个包),反复拷贝大结构体会带来不必要的性能开销。指针传递是固定 O(1) 的成本。
4.4 原因三:统一接口,兼容多种地址族
Socket API 设计于 1980 年代,需要同时支持多种协议,而它们的地址结构体大小完全不同:
struct sockaddr_in { ... }; // IPv4,16 字节
struct sockaddr_in6 { ... }; // IPv6,28 字节
struct sockaddr_un { ... }; // Unix Domain Socket,110 字节
如果用值传递,编译器必须知道确切类型才能确定拷贝多少字节,那就得为每种地址族定义不同的函数。而使用 struct sockaddr * 指针 + socklen_t len 的组合,一个函数签名就能处理所有地址类型:
// 同一个 bind() 处理所有协议
bind(fd, (struct sockaddr *)&ipv4_addr, sizeof(ipv4_addr)); // IPv4
bind(fd, (struct sockaddr *)&ipv6_addr, sizeof(ipv6_addr)); // IPv6
bind(fd, (struct sockaddr *)&unix_addr, sizeof(unix_addr)); // Unix Socket
这就是经典的 “通用指针多态” 模式——用 void* 风格的指针 + 长度参数实现类型无关的接口。
4.5 原因四:系统调用的天然需求
Socket 函数本质上是系统调用,会触发用户态到内核态的切换。内核通过指针来:
- 读取用户空间的数据(
bind/connect)→ 调用copy_from_user() - 写入结果到用户空间(
accept/recvfrom)→ 调用copy_to_user()
这套机制天然基于内存地址(指针),而非值拷贝。
4.6 对比总结
| 传递方式 | 能否回写数据 | 拷贝开销 | 多协议兼容 | 系统调用适配 |
|---|---|---|---|---|
| 值传递 | ❌ 不可能 | 高(拷贝整个结构体) | ❌ 需要不同签名 | ❌ 不匹配 |
| 指针传递 | ✅ 可以 | 低(仅 8 字节) | ✅ 统一签名 | ✅ 天然匹配 |
4.7 编程注意事项
// ⚠️ 错误1:忘记取地址
sockaddr_in addr{};
bind(fd, addr, sizeof(addr)); // ❌ 编译报错
bind(fd, (sockaddr *)&addr, sizeof(addr)); // ✅ 正确
// ⚠️ 错误2:输出型参数未初始化长度
sockaddr_in client_addr{};
socklen_t len = sizeof(client_addr); // ✅ 必须提前设置
accept(fd, (sockaddr *)&client_addr, &len);
// ⚠️ 错误3:混淆输入/输出方向
// bind/connect 的地址参数带 const → 内核只读,不会修改你的结构体
// accept/recvfrom 的地址参数不带 const → 内核会写入数据
一句话记忆:输入传指针是为了让内核"看到"你的数据,输出传指针是为了让内核"写到"你的内存。
五、地址结构体详解
5.1 struct sockaddr_in(IPv4 专用)
struct sockaddr_in {
sa_family_t sin_family; // 地址族:AF_INET
in_port_t sin_port; // 端口号(必须是网络字节序)
struct in_addr sin_addr; // IPv4 地址(必须是网络字节序)
char sin_zero[8]; // 填充字节,保持与 sockaddr 等长
};
5.2 struct in_addr
struct in_addr {
uint32_t s_addr; // 32位 IPv4 地址(网络字节序)
};
5.3 struct sockaddr(通用地址)
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址数据(具体含义由 sa_family 决定)
};
这就是为什么
bind()、connect()、accept()的参数类型是struct sockaddr *——它是所有地址结构体的"基类"。使用时需要将具体的sockaddr_in *强制转换为sockaddr *。
六、字节序与地址转换函数
6.1 为什么需要字节序转换?
不同 CPU 存储多字节整数的方式不同:
- 小端序(x86/x64):低字节在低地址
- 大端序(网络标准):高字节在低地址
网络通信统一使用大端序(网络字节序),所以本机数据发送到网络前必须转换。
以整数 0x12345678 为例:
主机字节序(小端,x86): 内存中 → [0x78] [0x56] [0x34] [0x12]
网络字节序(大端): 网络中 → [0x12] [0x34] [0x56] [0x78]
6.2 转换函数速查表
| 函数 | 全称 | 方向 | 用途 |
|---|---|---|---|
htons() |
host to network short | 主机 → 网络 | 端口号(16位) |
htonl() |
host to network long | 主机 → 网络 | IPv4 地址(32位) |
ntohs() |
network to host short | 网络 → 主机 | 端口号(16位) |
ntohl() |
network to host long | 网络 → 主机 | IPv4 地址(32位) |
inet_pton() |
presentation to network | 字符串 → 二进制 | 点分十进制 → 网络字节序 |
inet_ntop() |
network to presentation | 二进制 → 字符串 | 网络字节序 → 点分十进制 |
记忆口诀:
h= host,n= network,s= short(16位),l= long(32位),p= presentation(字符串表示),ton= to network
七、两种地址赋值写法详解(重点)
设置 IP 地址时,你会在不同代码中看到两种完全不同的写法。这不是"两种风格",而是新旧两代 API 的更替。
7.1 旧写法:inet_addr() + 直接赋值
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
工作原理:
inet_addr("127.0.0.1")将点分十进制字符串转为uint32_t(已是网络字节序)- 返回值直接赋给
s_addr字段 - 操作对象是结构体的一个字段(
uint32_t)
致命缺陷:
// inet_addr 失败时返回 INADDR_NONE,即 0xFFFFFFFF
// 而 255.255.255.255 的合法地址恰好也是 0xFFFFFFFF
in_addr_t a = inet_addr("255.255.255.255"); // 返回 0xFFFFFFFF(合法地址)
in_addr_t b = inet_addr("999.999.999.999"); // 返回 0xFFFFFFFF(转换失败)
// 两者返回值相同,你无法区分哪个是成功、哪个是失败!
7.2 新写法:inet_pton()(推荐)
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
工作原理:
inet_pton将字符串转换后,直接写入struct in_addr结构体- 第三个参数是
void *,指向目标结构体 - 操作对象是整个结构体
优势——返回值语义明确:
int ret = inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
// ret == 1 → 转换成功
// ret == 0 → 字符串格式不合法(如 "999.999.999.999")
// ret == -1 → 系统错误(如 af 参数不支持)
7.3 为什么传入的参数不一样?
| 写法 | 传入参数 | 参数类型 | 原因 |
|---|---|---|---|
| 旧 | s_addr |
uint32_t(字段) |
inet_addr 返回一个整数,直接赋值给字段 |
| 新 | &addr.sin_addr |
struct in_addr *(结构体指针) |
inet_pton 是通用函数,需要知道往哪种结构体写 |
为什么 inet_pton 要传结构体指针而不是字段地址?
因为 inet_pton 同时支持 IPv4 和 IPv6:
// IPv4:写入 struct in_addr(4字节)
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
// IPv6:写入 struct in6_addr(16字节)
inet_pton(AF_INET6, "::1", &addr6.sin6_addr);
如果只传 uint32_t *,就无法处理 16 字节的 IPv6 地址。所以第三个参数设计为 void *,由第一个参数 af 决定写入多大的数据。
7.4 在 Linux 上两者等价吗?
struct in_addr {
uint32_t s_addr; // 唯一的成员
};
由于 struct in_addr 只有一个成员,所以 &addr.sin_addr 和 &addr.sin_addr.s_addr 的内存地址完全相同。在 Linux 上效果一样,但传 &addr.sin_addr 是语义正确的写法,具有跨平台可移植性。
7.5 绑定地址的两种写法
// ========== 监听所有网卡(0.0.0.0)==========
// 旧写法:INADDR_ANY = 0,htonl(0) 仍然是 0
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 新写法(推荐):更直观
inet_pton(AF_INET, "0.0.0.0", &addr.sin_addr);
// ========== 绑定指定 IP ==========
// 旧写法
addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// 新写法(推荐)
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
7.6 完整对比
| 对比项 | 旧写法 inet_addr |
新写法 inet_pton |
|---|---|---|
| 操作对象 | s_addr 字段(uint32_t) |
struct in_addr 结构体 |
| 赋值方式 | 函数返回值直接赋值 | 函数内部写入指针指向的内存 |
| 错误处理 | 无法区分 255.255.255.255 和错误 |
返回值明确区分三种情况 |
| IPv6 支持 | ❌ 不支持 | ✅ 支持 |
| 标准 | BSD 扩展 | POSIX 标准 |
| 推荐程度 | 仅用于读懂旧代码 | 新代码必须用这个 |
八、服务器端完整代码
#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
using namespace std;
int main() {
// ================================================================
// 第1步:创建 TCP 套接字
// ================================================================
// AF_INET → IPv4 协议族
// SOCK_STREAM → 面向连接的流式套接字(TCP)
// 0 → 自动选择协议(TCP 对应 IPPROTO_TCP)
// 返回值:文件描述符 fd,失败返回 -1
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
cerr << "socket() failed" << endl;
return -1;
}
// ================================================================
// 第2步:设置端口复用(SO_REUSEADDR)
// ================================================================
// 作用:服务器重启时,端口可能处于 TIME_WAIT 状态(约60秒)
// 不加此选项,bind() 会报 "Address already in use"
// 注意:必须在 bind() 之前调用
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
cerr << "setsockopt() failed" << endl;
close(fd);
return -1;
}
// ================================================================
// 第3步:绑定地址和端口
// ================================================================
sockaddr_in addr{}; // 值初始化,所有字段置 0
addr.sin_family = AF_INET; // 地址族:IPv4
addr.sin_port = htons(8888); // 端口 8888,转网络字节序
// ---------- 绑定 IP 的两种写法 ----------
// 【旧写法】监听所有网卡(0.0.0.0)
// INADDR_ANY = 0,htonl(0) 仍然是 0
// 含义:接受来自任何网卡的连接
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 【新写法】等价写法(推荐)
// inet_pton(AF_INET, "0.0.0.0", &addr.sin_addr);
// 【旧写法】绑定指定 IP
// addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// 【新写法】绑定指定 IP(推荐)
// inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
// bind() 将套接字与地址绑定
// 第二个参数需要强转为 sockaddr*(通用地址类型)
if (bind(fd, (sockaddr *)&addr, sizeof(addr)) == -1) {
cerr << "bind() failed (port 8888 may be in use)" << endl;
close(fd);
return -1;
}
// ================================================================
// 第4步:开始监听
// ================================================================
// 第二个参数 5 → 等待队列最大长度(半连接 + 全连接)
// 当多个客户端同时连接时,超出的连接会被拒绝
if (listen(fd, 5) == -1) {
cerr << "listen() failed" << endl;
close(fd);
return -1;
}
cout << "Server listening on port 8888..." << endl;
// ================================================================
// 第5步:接受客户端连接
// ================================================================
// accept() 是阻塞调用:
// - 没有客户端连接时,程序停在这里等待
// - 有客户端连接时,返回一个新的文件描述符 cfd
//
// 重要:fd 和 cfd 是两个不同的文件描述符
// fd → 监听套接字,继续等待其他客户端
// cfd → 通信套接字,专门和当前客户端收发数据
sockaddr_in client_addr{};
socklen_t len = sizeof(client_addr);
int cfd = accept(fd, (sockaddr *)&client_addr, &len);
if (cfd == -1) {
cerr << "accept() failed" << endl;
close(fd);
return -1;
}
// ================================================================
// 第6步:输出客户端信息
// ================================================================
// inet_ntop:将二进制地址(网络字节序)转为点分十进制字符串
// 参数:
// AF_INET → IPv4
// &client_addr.sin_addr → 源地址(struct in_addr 指针)
// ip_str → 目标缓冲区
// sizeof(ip_str) → 缓冲区大小
char ip_str[INET_ADDRSTRLEN]; // INET_ADDRSTRLEN = 16
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str));
// ntohs:将网络字节序的端口号转为主机字节序以便打印
cout << "Client connected from " << ip_str
<< ":" << ntohs(client_addr.sin_port) << endl;
// ================================================================
// 第7步:读取客户端消息
// ================================================================
// read() 返回值:
// n > 0 → 成功读取 n 个字节
// n == 0 → 对端关闭了连接(收到 FIN)
// n == -1 → 出错
char buf[64] = {};
ssize_t n = read(cfd, buf, sizeof(buf) - 1); // 留 1 字节给 '\0'
if (n == -1) {
cerr << "read() failed" << endl;
} else if (n == 0) {
cout << "Client closed connection" << endl;
} else {
buf[n] = '\0'; // 手动添加字符串终止符
cout << "Received: " << buf << endl;
}
// ================================================================
// 第8步:发送回复消息
// ================================================================
const char *msg = "Hello from server";
ssize_t sent = write(cfd, msg, strlen(msg));
if (sent == -1) {
cerr << "write() failed" << endl;
} else {
cout << "Sent: " << msg << endl;
}
// ================================================================
// 第9步:关闭连接
// ================================================================
close(cfd); // 先关闭与客户端的通信套接字
close(fd); // 再关闭监听套接字
return 0;
}
九、客户端完整代码
#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
using namespace std;
int main() {
// ================================================================
// 第1步:创建 TCP 套接字
// ================================================================
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
cerr << "socket() failed" << endl;
return -1;
}
// ================================================================
// 第2步:设置服务器地址
// ================================================================
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 服务器端口,转网络字节序
// ---------- 设置目标 IP 的两种写法 ----------
// 【旧写法】inet_addr() 返回 uint32_t(已是网络字节序),直接赋值
// 缺陷:无法区分 "255.255.255.255" 和转换失败
// addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 【新写法】inet_pton() 将结果写入 struct in_addr(推荐)
// 返回值:1=成功,0=格式错误,-1=系统错误
if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) {
cerr << "Invalid address" << endl;
close(fd);
return -1;
}
// ================================================================
// 第3步:发起连接
// ================================================================
// connect() 是阻塞调用:
// - 成功:完成三次握手,返回 0
// - 失败:返回 -1(如服务器未启动、端口不对等)
if (connect(fd, (sockaddr *)&addr, sizeof(addr)) == -1) {
cerr << "connect() failed (server may not be running)" << endl;
close(fd);
return -1;
}
cout << "Connected to server" << endl;
// ================================================================
// 第4步:发送消息
// ================================================================
const char *msg = "Hello from client";
ssize_t sent = write(fd, msg, strlen(msg));
if (sent == -1) {
cerr << "write() failed" << endl;
close(fd);
return -1;
}
cout << "Sent: " << msg << endl;
// ================================================================
// 第5步:接收服务器回复
// ================================================================
char buf[64] = {};
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n == -1) {
cerr << "read() failed" << endl;
} else if (n == 0) {
cout << "Server closed connection" << endl;
} else {
buf[n] = '\0';
cout << "Received: " << buf << endl;
}
// ================================================================
// 第6步:关闭连接
// ================================================================
close(fd);
return 0;
}
十、编译与运行
# 编译
g++ -o server server.cpp
g++ -o client client.cpp
# 终端1:启动服务器
./server
# 终端2:启动客户端
./client
运行结果:
# 服务器终端
Server listening on port 8888...
Client connected from 127.0.0.1:54321
Received: Hello from client
Sent: Hello from server
# 客户端终端
Connected to server
Sent: Hello from client
Received: Hello from server
十一、两种写法完整对比(终极总结)
11.1 设置 IP 地址
// ==================== 旧写法 ====================
// 函数:inet_addr()
// 返回:uint32_t(网络字节序),失败返回 INADDR_NONE (0xFFFFFFFF)
// 赋值:直接赋给 s_addr 字段
addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// ==================== 新写法 ====================
// 函数:inet_pton()
// 返回:1=成功,0=格式错误,-1=系统错误
// 赋值:函数内部写入 struct in_addr 结构体
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);
11.2 监听所有网卡
// ==================== 旧写法 ====================
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// ==================== 新写法 ====================
inet_pton(AF_INET, "0.0.0.0", &addr.sin_addr);
11.3 打印 IP 地址
// ==================== 旧写法 ====================
// inet_ntoa() 返回静态缓冲区指针,线程不安全,不可重入
char *ip = inet_ntoa(client_addr.sin_addr);
cout << ip << endl;
// ==================== 新写法(推荐) ====================
// inet_ntop() 写入用户提供的缓冲区,线程安全
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str));
cout << ip_str << endl;
11.4 对比表格
| 对比项 | 旧写法 | 新写法 |
|---|---|---|
| 字符串→二进制 | inet_addr() |
inet_pton() |
| 二进制→字符串 | inet_ntoa() |
inet_ntop() |
| 操作对象 | s_addr 字段 |
struct in_addr 结构体 |
| 错误处理 | 有歧义(255.255.255.255) | 返回值明确 |
| IPv6 支持 | ❌ | ✅ |
| 线程安全 | inet_ntoa 不安全 |
inet_ntop 安全 |
| 标准 | BSD 扩展 | POSIX 标准 |
| 推荐 | 仅用于读懂旧代码 | 新代码必须使用 |
十二、关键注意事项
12.1 字节序:什么时候需要转,什么时候不需要?
// ✅ 端口号:必须用 htons()
addr.sin_port = htons(8888);
// ✅ 手动设置 IP(旧写法):必须用 htonl()
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// ✅ inet_addr / inet_pton 返回的已经是网络字节序,不需要再转!
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ✅ 已是网络序
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); // ✅ 已是网络序
// ❌ 重复转换会导致地址错误!
addr.sin_addr.s_addr = htonl(inet_addr("127.0.0.1")); // ❌ 转了两次!
12.2 read() 返回值必须检查
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0'; // 手动加终止符
// 处理数据...
} else if (n == 0) {
// 对端关闭连接,应该 close(fd) 并退出
} else {
// 出错,检查 errno
}
12.3 accept() 的两个 fd 不要混淆
int fd = socket(...); // 监听套接字:只负责等待新连接
int cfd = accept(fd, ...); // 通信套接字:负责和客户端收发数据
// fd 继续 listen,cfd 用来 read/write
// 服务器要支持多客户端,就需要对每个 cfd 创建线程/进程
12.4 SO_REUSEADDR 必须在 bind() 之前
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 先设置
bind(fd, ...); // 后绑定
// 顺序反了无效!
十三、完整调用流程图
服务器端:
socket() ──→ setsockopt() ──→ bind() ──→ listen() ──→ accept()
│
┌───────┘
▼
read() / write()
│
▼
close(cfd)
close(fd)
客户端:
socket() ──→ connect() ──→ write() ──→ read() ──→ close(fd)
十四、后续学习方向
| 方向 | 内容 |
|---|---|
| 并发服务器 | fork() 多进程 / pthread 多线程 |
| IO 多路复用 | select() → poll() → epoll() |
| 非阻塞 IO | fcntl() 设置 O_NONBLOCK |
| UDP 通信 | SOCK_DGRAM + sendto() / recvfrom() |
| 高级特性 | TCP 粘包处理、心跳机制、优雅关闭 |
一句话总结:Socket 编程的核心就是 7 个函数(
socket、bind、listen、accept、connect、read/write、close),地址设置认准inet_pton+&addr.sin_addr这一种写法就够了。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)