Day13-
C 语言网络编程 — Socket 通信
补一下网络编程吧,好久每更新了,,
过两天慢慢更新C学习结束项目--远程终端管理
其实想写个微信来着,但是时间不太够,就做的比较粗糙了
目录
一、网络基础概念
1.1 什么是网络
网络是指通过计算机、服务器、路由器等设备互相连接起来的系统。
它允许不同设备之间进行数据和信息的交换和共享。按覆盖范围可分为:
| 类型 | 范围 | 示例 |
|---|---|---|
| 局域网 (LAN) | 局部区域 | 家庭/办公室 WiFi |
| 广域网 (WAN) | 跨城市/国家 | 运营商骨干网 |
| 互联网 (Internet) | 全球互联 | 万维网、云服务 |
1.2 进程间通信 (IPC)
不同主机上的进程要进行数据交换和共享,需要跨越网络。在 C 语言中,套接字 (Socket) 是实现跨网络 IPC 的标准接口。
┌──────────┐ ┌──────────┐
│ 计算机 A │ │ 计算机 B │
│ │ │ │
│ ┌──────┐ │ 网络 │ ┌──────┐ │
│ │ 套接字├─┼─────────┼─┤套接字 │ │
│ └──────┘ │ │ └──────┘ │
│ ↑ 数据 │ │ ↑ 数据 │
│ ↓ 数据 │ │ ↓ 数据 │
└──────────┘ └──────────┘
二、协议分层模型
2.1 TCP/IP 协议栈(四层)
这是实际互联网使用的分层模型:
| 层次 | 名称 | 核心职责 | 代表协议 |
|---|---|---|---|
| 4 | 应用层 | 为应用程序提供接口 | HTTP, FTP, SMTP, DNS |
| 3 | 传输层 | 端到端可靠传输 | TCP, UDP |
| 2 | 网络层 | IP 寻址 & 路由选择 | IP, ICMP |
| 1 | 链路层 | 物理传输 & MAC 地址 | Ethernet, Wi-Fi |
⚡ 关键理解:每一层只关心自己的事情。应用层不需要知道底层是光纤还是 Wi-Fi;传输层不需要知道对方 IP 怎么到达——这就是分层的威力。
2.2 OSI 参考模型(七层)
学术标准,共 7 层(从上往下):
┌───────────┐ 7. 应用层 → 用户直接使用的程序
├───────────┤ 6. 表示层 → 数据格式转换、加密解密
├───────────┤ 5. 会话层 → 管理"对话"的建立与断开
├───────────┤ 4. 传输层 → 端到端传输(TCP/UDP)
├───────────┤ 3. 网络层 → 路由选择、IP寻址
├───────────┤ 2. 数据链路层 → 帧封装、MAC地址
└───────────┘ 1. 物理层 → 比特流传输(光/电信号)
OSI 与 TCP/IP 的对应关系:OSI 的 5~7 层合并为 TCP/IP 的「应用层」,OSI 的 1~2 层合并为 TCP/IP 的「链路层」。
2.3 对 Socket 编程的意义
编写 Socket 程序时,你主要操作的是传输层(选 TCP 还是 UDP)和应用层(定义自己的消息格式):
// 你写的代码在这里:
// socket() → 选择 AF_INET (IPv4) + SOCK_STREAM (TCP) / SOCK_DGRAM (UDP)
// bind() → 绑定端口(告诉操作系统"我在这个端口监听")
// send/recv → 传输层数据收发
// connect() → 建立连接(TCP 特有)
// 底层的事情(IP 路由、以太网帧、物理传输)→ 操作系统内核帮你搞定
三、套接字 (Socket)
3.1 定义
套接字 (Socket) 是计算机网络编程中用于网络通信的一种编程接口(API)。它是操作系统提供的抽象,让程序员无需关心底层网络细节就能进行通信。
可以把 Socket 想象成两台机器之间的虚拟电话线:
- 一端绑定一个 IP + 端口
- 两端的 Socket 连接后就可以互相发送数据
3.2 编程流程总览
UDP 流程(无连接)
接收端 (Server) 发送端 (Client)
┌──────────────┐ ┌──────────────┐
│ socket() │ │ socket() │
│ bind() │ │ │
│ recvfrom() ◄─── 数据报 ────────► │ sendto() │
│ close() │ │ close() │
└──────────────┘ └──────────────┘
TCP 流程(面向连接)
服务器 (Server) 客户端 (Client)
┌──────────────┐ ┌──────────────┐
│ socket() │ │ socket() │
│ bind() │ │ │
│ listen() │ │ connect() ─────────►
│ accept() ◄────────────────────── │ │
│ recv() ◄──── 数据流 ───────────► │ send() │
│ send() ───── 数据流 ───────────► │ recv() │
│ close() │ │ close() │
└──────────────┘ └──────────────┘
3.3 核心函数速查
| 函数 | 用途 | UDP 使用 | TCP 使用 |
|---|---|---|---|
socket() |
创建套接字 | ✅ | ✅ |
bind() |
绑定地址+端口 | ✅(接收方) | ✅(服务端) |
sendto() |
发送数据(指定目标地址) | ✅ | ❌ |
recvfrom() |
接收数据(获取发送者地址) | ✅ | ❌ |
listen() |
监听连接请求 | ❌ | ✅ |
accept() |
接受新连接 | ❌ | ✅ |
connect() |
连接到服务器 | ❌ | ✅(客户端) |
send() |
发送数据(已连接) | ❌ | ✅ |
recv() |
接收数据(已连接) | ❌ | ✅ |
close() |
关闭套接字 | ✅ | ✅ |
四、UDP 编程
4.1 协议特点
UDP (User Datagram Protocol) — 用户数据报协议
| 特性 | 说明 |
|---|---|
| 无连接 | 不需要建立连接,直接发 |
| 不可靠 | 不保证送达、不保证顺序、不重传 |
| 面向数据报 | 每个数据包独立,有边界 |
| 开销小 | 头部仅 8 字节,速度快 |
| 一对多 | 支持广播、组播 |
| 适用场景 | 视频/音频直播、DNS 查询、游戏状态同步 |
💡 类比:UDP 就像寄明信片——写好地址就扔进邮筒了,不知道对方是否收到,也不保证先后顺序,但速度快、成本低。
4.2 UDP 发送端代码
文件名:
udp_sender.c
功能: 向指定 IP:Port 发送 “hello”
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
// ===== 第一步:创建套接字 =====
// AF_INET : IPv4 协议族
// SOCK_DGRAM : 数据报套接字(即 UDP)
// IPPROTO_UDP: 显式指定 UDP 协议(也可填 0 让系统自动选)
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd < 0){
perror("socket error");
return 1;
}
// ===== 第二步:设置目标地址 =====
struct sockaddr_in addr;
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(8888); // 目标端口 8888(htons: 主机字节序→网络字节序)
addr.sin_addr.s_addr = inet_addr("192.168.221.17"); // 目标 IP
// ===== 第三步:发送数据 =====
// sendto(): UDP 专用发送函数,每次都要指定目标地址
int bytes = sendto(sockfd, "hello", 6, 0,
(struct sockaddr *)&addr,
sizeof(addr));
printf("sendto %s:%d %d bytes\n",
inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), bytes);
// ===== 第四步:关闭套接字 =====
close(sockfd);
return 0;
}
4.3 UDP 接收端代码
文件名:
udp_receiver.c
功能: 在指定 IP:Port 上等待接收数据,并打印来源信息
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
// ===== 第一步:创建套接字 =====
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // 创建 UDP 套接字
if(sockfd < 0){
perror("socket error");
return 1;
}
// ===== 第二步:绑定本地地址(关键!)=====
// UDP 接收端必须先 bind,告诉操作系统"我要在这个端口收数据"
struct sockaddr_in addr;
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(8888); // 本地监听端口
addr.sin_addr.s_addr = inet_addr("192.168.221.13"); // 本地 IP(或用 INADDR_ANY 绑定所有网卡)
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0){
perror("bind error");
close(sockfd);
return 1;
}
// ===== 第三步:准备存储发送方信息的容器 =====
struct sockaddr_in r_addr = {}; // 用于保存发送者的地址
socklen_t addr_len = sizeof(r_addr); // 必须初始化为 sizeof!
char msg[128] = {}; // 接收缓冲区
// ===== 第四步:接收数据 =====
// recvfrom(): UDP 专用接收函数,同时返回数据和发送者地址
int bytes = recvfrom(sockfd, msg, 127, 0,
(struct sockaddr *)&r_addr, &addr_len);
// ===== 第五步:输出结果 =====
printf("recv %d bytes msg:\"%s\" from %s:%d\n",
bytes, msg,
inet_ntoa(r_addr.sin_addr), ntohs(r_addr.sin_port));
// ===== 第六步:关闭 =====
close(sockfd);
return 0;
}
4.4 代码逐行详解(PPT 未提及的关键细节)
socket() 参数详解
int socket(int domain, int type, int protocol);
| 参数 | 常用值 | 含义 |
|---|---|---|
domain |
AF_INET |
IPv4 地址族 (AF_INET6 = IPv6) |
type |
SOCK_STREAM |
TCP(流式)SOCK_DGRAM |
protocol |
0 |
自动根据 type 选择IPPROTO_TCP(6)IPPROTO_UDP(17) |
| 返回值 | >=0 成功(文件描述符)<0 失败(设 errno) |
🔑 为什么返回的是 fd?
在 Linux 中,一切皆文件。Socket 返回的是一个文件描述符 (fd),你可以像读写文件一样用read()/write()来收发数据(虽然推荐用recv/send)。
字节序问题(htons / inet_addr)
这是一个 PPT 完全没有提但极其重要的概念:
addr.sin_port = htons(8888); // 主机序 → 网络序(大端)
addr.sin_addr.s_addr = inet_addr("..."); // 点分十进制 → 32 位网络序整数
| 函数 | 全称 | 作用 | 示例 |
|---|---|---|---|
htons() |
Host TO Network Short | 16 位短整型 主机序→网络序 | htons(8888) |
htonl() |
Host TO Network Long | 32 位长整型 主机序→网络序 | htonl(INADDR_ANY) |
ntohs() |
Network TO Host Short | 16 位 网络序→主机序 | 解析 port 时使用 |
inet_addr() |
Internet Address | “192.168.1.1” → 32位整数 | 填入 sin_addr.s_addr |
inet_ntoa() |
Internet Address to ASCII | 32位整数 → “192.168.1.1” | 打印 IP 时使用 |
⚠️ 常见错误:忘记调用
htons()或直接赋值端口号,会导致绑定到错误的端口!现代 x86 CPU 是小端序,而网络协议规定大端序。
bind() 为什么接收端必须调用?
- 发送端可以不 bind:操作系统会自动分配一个临时端口(ephemeral port,通常 32768–60999)
- 接收端必须 bind:如果不 bind,操作系统不知道该把发给哪个端口的数据交给你
recvfrom() vs recv()
recvfrom() (UDP) |
recv() (TCP) |
|
|---|---|---|
| 需要提前知道对方? | ❌ 不需要 | ❌ 不需要(已连接) |
| 能获取发送者地址? | ✅ 能 | ❌ 不能 |
| 保证收到完整消息? | ✅ 一次 recvfrom = 一个完整 UDP 报文 | ❌ 可能只收到部分(TCP 流特性) |
| 阻塞行为 | 无数据时阻塞 | 无数据时阻塞 |
五、TCP 编程
5.1 协议特点
TCP (Transmission Control Protocol) — 传输控制协议
| 特性 | 说明 |
|---|---|
| 面向连接 | 通信前必须三次握手建立连接 |
| 可靠传输 | 确认机制 + 重传 + 序号保证有序 |
| 字节流 | 无消息边界,数据是连续的字节流 |
| 流量控制 | 滑动窗口防止发送过快淹没接收方 |
| 拥塞控制 | 慢启动、拥塞避免、快重传、快恢复 |
| 头部开销 | 最少 20 字节(选项可变) |
| 适用场景 | Web(HTTP)、邮件(SMTP)、文件传输(FTP)、SSH |
💡 类比:TCP 就像打电话——先拨号(握手),确认对方在线后才能说话,每句话都有回应(确认),说完挂断(四次挥手)。但打电话时你说的话是一股脑传过去的(字节流),不是一句一句分开的。
5.2 TCP 服务器代码
文件名:
tcp_server.c
功能: 监听 8888 端口,接受客户端连接并持续接收数据直到客户端断开
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
// ===== 第一步:创建套接字 =====
// SOCK_STREAM : 流式套接字(即 TCP)
// IPPROTO_TCP: 显式指定 TCP 协议
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0){
perror("socket error");
return 1;
}
// ===== 第二步:绑定地址 =====
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("192.168.221.13");
if(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
perror("bind error");
close(sockfd);
return 1;
}
// ===== 第三步:监听连接请求(TCP 特有)=====
// backlog=20: 操作系统维护的已完成三次握手但尚未 accept 的队列长度
if(listen(sockfd, 20) < 0){
perror("listen error");
close(sockfd);
return 1;
}
// ===== 第四步:接受客户端连接(阻塞等待)=====
struct sockaddr_in r_addr = {};
socklen_t addr_len = sizeof(r_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&r_addr, &addr_len);
if(client_fd < 0){
perror("accpet error"); // 注意:原课件拼写有误,应为 accept
close(sockfd);
return 1;
}else{
// 打印已连接的客户端信息
printf("client %s:%d connected .\n",
inet_ntoa(r_addr.sin_addr),
ntohs(r_addr.sin_port));
}
// ===== 第五步:持续接收客户端数据 =====
char buf[128] = {};
while(1){
// recv(): 从已连接的 client_fd 读取数据(注意:用的是 client_fd 不是 sockfd!)
int bytes = recv(client_fd, buf, 127, 0);
if(bytes <= 0){ // bytes==0: 对方正常关闭; bytes<0: 出错
printf("client %s:%d disconnected .\n",
inet_ntoa(r_addr.sin_addr),
ntohs(r_addr.sin_port));
break; // 断开则退出循环
}
else{
printf("recv %d bytes \"%s\" by client %s:%d\n",
bytes, buf,
inet_ntoa(r_addr.sin_addr),
ntohs(r_addr.sin_port));
}
}
// ===== 第六步:清理资源 =====
close(client_fd); // 先关客户端连接
close(sockfd); // 再关监听套接字
return 0;
}
5.3 TCP 客户端代码
文件名:
tcp_client.c
功能: 连接到服务器,发送一条消息后退出
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
// ===== 第一步:创建套接字 =====
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0){
perror("socket error");
return 1;
}
// ===== 第二步:设置服务器地址并连接 =====
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("192.168.221.17");
// connect(): 触发 TCP 三次握手(SYN → SYN-ACK → ACK)
if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
perror("connect error");
close(sockfd);
return 1;
}
// ===== 第三步:发送数据 =====
// send(): 通过已连接的 sockfd 发送数据
int bytes = send(sockfd, "hello server", 13, 0);
printf("send %d bytes msg to server\n", bytes);
// ===== 第四步:关闭 =====
close(sockfd); // 触发 TCP 四次挥手
return 0;
}
5.4 代码逐行详解
listen(backlog) 的真正含义
int listen(int sockfd, int backlog);
backlog 不是"最多能同时连接多少个客户端",而是:
内核为每个 listening socket 维护两个队列:
- SYN 队列(半连接):收到 SYN 但还没完成三次握手的连接
- Accept 队列(全连接):已完成三次握手、等待
accept()取走的连接
backlog大致对应 Accept 队列的长度。Linux 上实际值受/proc/sys/net/core/somaxconn限制。
accept() 返回的是什么?
int client_fd = accept(sockfd, ...);
sockfd: 监听套接字 — 只负责"接电话"client_fd: 已连接套接字 — 和某个具体客户的"通话线路"- 两者是不同的 fd!后续的数据收发必须用
client_fd
sockfd vs client_fd 的关系图
┌─────────────┐
│ Client │
└──────┬──────┘
│ TCP连接
▼
┌────────────────────────┐
│ Server │
│ │
│ sockfd (监听套接字) │ ← 只做 bind+listen+accept
│ │ │
│ ├─→ client_fd_1 ────┼── 与客户A通信 (recv/send)
│ ├─→ client_fd_2 ────┼── 与客户B通信
│ ├─→ client_fd_3 ────┼── 与客户C通信
│ │
└────────────────────────┘
📌 重要:上面的服务器代码只能同时服务一个客户端!要服务多个客户需要用
fork()多进程、pthread_create()多线程、或epoll()I/O 多路复用。(这些内容超出了原课件范围,但在实际开发中必不可少。)
TCP 的"流"特性意味着什么?
发送方 send("AB") → send("CD") → send("EF")
接收方可能读到:
✅ "AB" "CD" "EF" ← 刚好分开(碰巧)
⚠️ "ABCDEF" ← 三次合并成一个(粘包!)
⚠️ "A" "BCDE" "F" ← 被拆散
这就是粘包问题的根源——TCP 是字节流,没有消息边界。下面详细讨论。
六、TCP 粘包问题与解决方案
6.1 问题复现
以下代码会触发粘包:连续快速发送 5 次 "hello server"(13 字节),接收方可能一次性收到一大坨数据。
文件名:
tcp_sticky_problem.c(问题演示)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0){
perror("socket error");
return 1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("192.168.221.13");
if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
perror("connect error");
close(sockfd);
return 1;
}
// 连续快速发送 5 条消息 —— 这就是粘包的触发条件
for(int i=0; i<5; i++){
int bytes = send(sockfd, "hello server", 13, 0);
printf("send %d bytes msg to server\n", bytes);
}
close(sockfd);
return 0;
}
现象:接收方的 recv() 可能一次收到 "hello serverhello serverhello server..." (65 字节),而不是 5 次各收到 13 字节。
6.2 为什么会粘包?
发送方: [MSG1][MSG2][MSG3][MSG4][MSG5]
↓ 快速连续写入内核缓冲区
内核发送缓冲区: [ MSG1MSG2MSG3MSG4MSG5 ] ← 合成了一个大的 TCP 段
↓ 网络传输
接收方: recv() 一次读出全部 → 粘包!
本质原因:TCP 是字节流协议,没有消息边界。send() 的次数 ≠ recv() 的次数。
6.3 四种解决方案(附示例代码思路)
方案一:固定长度包(Fixed Length)
约定每个消息固定 N 字节,不足则补齐,接收方按固定长度读取。
// 发送端
char msg[64] = {}; // 固定 64 字节
strncpy(msg, "hello", 64); // 不足的部分自动补 '\0'
send(fd, msg, 64, 0);
// 接收端
char buf[64] = {};
while(need_read > 0){
int n = recv(fd, buf + offset, 64 - offset, 0);
offset += n;
}
// 每次精确读取 64 字节为一个完整消息
优点:实现最简单
缺点:浪费带宽(短消息也要填充到固定长度)
方案二:长度前缀 / 头部字段(Length Prefix)
在每个消息前面加上一个固定长度的头部,记录消息体的长度。这是业界最常用的方案。
// === 自定义协议格式 ===
// [4字节: 消息体长度(uint32_t)] [N字节: 消息体内容]
// --- 发送端 ---
void send_msg(int fd, const char *data, int len){
uint32_t net_len = htonl(len); // 长度转为网络字节序
send(fd, &net_len, 4, 0); // 先发 4 字节长度头
send(fd, data, len, 0); // 再发消息体
}
// --- 接收端 ---
int read_n(int fd, char *buf, int n){
int total = 0;
while(total < n){
int r = recv(fd, buf + total, n - total, 0);
if(r <= 0) return r;
total += r;
}
return total;
}
// 使用:
uint32_t msg_len;
read_n(fd, (char*)&msg_len, 4); // 先读 4 字节长度
msg_len = ntohl(msg_len); // 转回主机序
char *msg_buf = malloc(msg_len);
read_n(fd, msg_buf, msg_len); // 再读消息体
优点:灵活高效,无浪费,工业界首选(HTTP/2、gRPC、Redis 都用类似方案)
缺点:需要自己处理半包情况(read_n 循环读取)
方案三:分隔符(Delimiter)
用特殊字符作为消息之间的分隔符(如 \r\n)。
// 发送端
send(fd, "hello\r\n", 7, 0);
send(fd, "world\r\n", 7, 0);
// 接收端:按 \r\n 切分
// 类似于 HTTP 协议的做法
优点:人类可读性好(HTTP 就用的这个)
缺点:消息体本身不能包含分隔符(需转义);效率略低(需要扫描查找分隔符)
方案四:应用层自定义协议
设计完整的协议头 + 协议体结构:
#pragma pack(push, 1)
typedef struct {
uint16_t magic; // 魔数(校验包的合法性):0xABCD
uint8_t version; // 协议版本
uint8_t type; // 消息类型:1=登录 2=聊天 3=心跳...
uint32_t body_len; // 消息体长度
uint32_t seq_id; // 序列号(用于去重/排序)
// 之后紧跟 body_len 字节的 payload
} MsgHeader;
#pragma pack(pop)
适用场景:大型项目、RPC 框架、即时通讯等
6.4 方案对比总结
| 方案 | 实现难度 | 效率 | 适用场景 |
|---|---|---|---|
| 固定长度 | ⭐ | 低 | 简单命令协议 |
| 长度前缀 | ⭐⭐ | 高 | 通用首选(推荐) |
| 分隔符 | ⭐⭐ | 中 | 文本协议(HTTP/SMTP) |
| 自定义协议 | ⭐⭐⭐⭐ | 高 | 大型项目/RPC |
七、附录:核心 API 速查表
7.1 头文件一览
#include <sys/socket.h> // socket, bind, listen, accept, connect, send, recv, sendto, recvfrom
#include <netinet/in.h> // struct sockaddr_in, IPPROTO_TCP, IPPROTO_UDP
#include <arpa/inet.h> // inet_addr, inet_ntoa, htons, ntohs, htonl, ntohl
#include <unistd.h> // close
7.2 地址结构 struct sockaddr_in
struct sockaddr_in {
sa_family_t sin_family; // 地址族: AF_INET (IPv4)
in_port_t sin_port; // 端口号 (必须 htons!)
struct in_addr sin_addr; // IP 地址 (32位)
// sin_zero[8] // 填充字节(通常不用管)
};
struct in_addr {
uint32_t s_addr; // 32位 IPv4 地址(网络字节序!)
};
7.3 常见 errno 错误码
| 错误 | 含义 | 常见原因 |
|---|---|---|
EADDRINUSE |
地址已被占用 | 端口被其他程序占用,或处于 TIME_WAIT |
ECONNREFUSED |
连接被拒绝 | 目标端口没有程序在监听 |
ETIMEDOUT |
连接超时 | 网络不通或防火墙拦截 |
EAGAIN/EWOULDBLOCK |
非模式下无数据 | 设置了非阻塞 I/O |
EPIPE |
写入已关闭的连接 | 对方已经 close 了你还 send |
7.4 INADDR 常量
INADDR_ANY // 0.0.0.0 绑定所有网卡(服务器常用)
INADDR_LOOPBACK // 127.0.0.1 仅本机访问
INADDR_BROADCAST // 255.255.255.255 广播地址
INADDR_NONE // 255.255.255.255 无效地址
7.5 编译与运行
# 编译(Linux/macOS)
gcc udp_receiver.c -o udp_recv
gcc udp_sender.c -o udp_send
gcc tcp_server.c -o tcp_srv
gcc tcp_client.c -o tcp_cli
# 运行(先启动接收端/服务器,再启动发送端/客户端)
./udp_recv # 终端1: 接收端(先启动)
./udp_send # 终端2: 发送端
./tcp_srv # 终端1: 服务器(先启动)
./tcp_cli # 终端2: 客户端
⚠️ Windows 用户注意:以上代码基于 POSIX Socket API(Linux/macOS)。Windows 上需:
- 包含
<winsock2.h>替代<sys/socket.h>- 调用
WSAStartup()初始化close()改为closesocket()- 链接
-lws2_32
总结
| 知识点 | 要点 |
|---|---|
| Socket | 网络编程的标准 API,本质是一个文件描述符 |
| UDP | 无连接、不可靠、有边界;用 sendto/recvfrom |
| TCP | 面向连接、可靠、字节流;用 bind→listen→accept (服务端) / connect (客户端) |
| 粘包 | TCP 字节流特性的必然产物;解决核心是自定消息边界 |
| 字节序 | htons/htonl 转网络序,ntohs/ntohl 转回来,千万别忘! |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)