C 语言网络编程 — Socket 通信

补一下网络编程吧,好久每更新了,,
过两天慢慢更新C学习结束项目--远程终端管理
其实想写个微信来着,但是时间不太够,就做的比较粗糙了

目录

  1. 网络基础概念
  2. 协议分层模型
  3. 套接字 (Socket)
  4. UDP 编程
  5. TCP 编程
  6. TCP 粘包问题与解决方案
  7. 附录:核心 API 速查表

一、网络基础概念

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 上需:

  1. 包含 <winsock2.h> 替代 <sys/socket.h>
  2. 调用 WSAStartup() 初始化
  3. close() 改为 closesocket()
  4. 链接 -lws2_32

总结

知识点 要点
Socket 网络编程的标准 API,本质是一个文件描述符
UDP 无连接、不可靠、有边界;用 sendto/recvfrom
TCP 面向连接、可靠、字节流;用 bind→listen→accept (服务端) / connect (客户端)
粘包 TCP 字节流特性的必然产物;解决核心是自定消息边界
字节序 htons/htonl 转网络序,ntohs/ntohl 转回来,千万别忘!
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐