笨鸟coding头像

网络编程基础

1.网络通信概述

网络通信:两台或多台设备,按约定好的规则,把数据从一方送到另一方。

1.为什么要分层

网线物理层,电压,数据分包,怎么处理丢包是网页还是视频等,一次性想清楚太难捕捉和处理。
所以网络按层拆开,每一层只管自己的事,上层用下层提供的服务。
常见两套模型:

模型层数谁在用
OSI 参考模型7 层教学、理解概念
TCP/IP 模型4 层实际互联网、工程里常说
1   应用层       ┐
2   表示层       ├──→ TCP/IP [应用层] (HTTP、MQTT、DNS...)
3   会话层       ┘
4   传输层       ──→ TCP/IP [传输层] (TCP、UDP)
5   网络层       ──→ TCP/IP [网络层] (IP)
6   数据链路层   ┐
7   物理层       ┴──→ TCP/IP [网络接口层] (以太网、Wi-Fi)

在这里插入图片描述


2.各层在什么

物理层
  • 干什么:比特 0/1 怎么在介质上传——电平、光、无线电。
  • 例子:网线里的电压、Wi-Fi 射频、RS485 差分信号(严格说 RS485 常归在「物理/链路」交界,入门可先当「线缆层」理解)。
数据链路层
  • 干什么:同一局域网里,设备之间怎么组帧、怎么寻址(MAC 地址)、简单差错检测。
  • 例子:以太网帧、Wi-Fi 数据链路、交换机按 MAC 转发。
    MAC 地址:网卡出厂带的 48 位地址,形如 AA:BB:CC:DD:EE:FF,局域网内标识「哪块网卡」。
网络层
  • 干什么:跨网段、跨路由器,把包送到「目标 IP」。
  • 核心:IP 地址(如 192.168.1.10)、路由(下一跳往哪走)。
  • 协议:主要是 IPv4 / IPv6。
    IP 只负责「尽量送到」,不保证一定送到、不保证顺序(那是上层的事)。
传输层
  • 干什么:在两台主机的 进程/端口 之间传数据。
  • 两个最重要的协议:
协议特点典型用途
TCP面向连接、可靠、有序、有流量控制网页 HTTP、文件、邮件
UDP无连接、不保证可靠、开销小、可广播视频流、DNS 查询、物联网上报、游戏

端口:一台电脑有很多网络程序,用端口号区分,如 80(HTTP)、443(HTTPS)、1883(MQTT)。
通信常写成:192.168.1.10:8080 → IP 找机器,端口找程序。

应用层
  • 干什么:具体业务——网页、邮件、文件、物联网消息格式。
  • 例子:HTTP/HTTPS、FTP、SMTP、MQTT、CoAP、DNS。
    程序员日常打交道最多的是这一层 + 传输层(调 API 发 TCP/UDP 或 HTTP)。

3.一次通信怎么走

在浏览器输入http://192.168.1.1:

  1. 应用层:浏览器按HTTP组请求报文(GET / …)。
  2. 传输层:用TCP,连接对方80端口,保证数据可靠到达
  3. 网络层:包上写上源IP,目标IP,192.168.1.1,交给路由。
  4. 链路层:封装成以太网帧,目标MAC可能是网关或目标设备。
  5. 物理层:网口/Wi-Fi发比特流。
    对方设备层层拆包,HTTP服务器返回网页,再沿原路回来。
    简化图:
1  [浏览器 HTTP]应用层
2    ↓
3  [TCP 端口 80]传输层
4    ↓
5  [IP 192.168.1.x -> 192.168.1.1]网络层
6    ↓
7  [以太网帧 + MAC]链路层
8    ↓
9  [网线 / Wi-Fi]物理层

4. 几个必懂的概念

IP 地址与子网
  • IPv4:32 位,常写成四段,如 192.168.1.100。
  • 子网掩码:如 255.255.255.0,用来划分「哪些位是网络号、哪些是主机号」。
  • 同一子网:如 192.168.1.0/24 里,192.168.1.2 和 192.168.1.100 可直接二层通信;不同网段要经过网关(路由器)。
网关、DNS
  • 默认网关:本机访问「外网或其他网段」时,先把包发给网关(通常是路由器 192.168.1.1)。
  • DNS:把域名 www.example.com 解析成 IP,否则你只能记 IP 访问。
MAC 与 IP 的分工
  • IP:端到端逻辑地址,路由用。
  • MAC:局域网一跳一跳的硬件地址。
  • 局域网内常通过 ARP:「这个 IP 对应哪个 MAC?」
客户端与服务器
  • 服务器:一直监听某端口,等服务(如 Web 服务器听 80)。
  • 客户端:主动发起连接(浏览器、手机 App、单片机上报)。
    也有 P2P、设备互连,但入门先掌握 C/S 即可。
带宽、延迟、丢包
  • 带宽:单位时间能传多少数据(Mbps)。
  • 延迟(RTT):包往返时间,影响「点一下要等多久」。
  • 丢包:无线、拥塞时会发生;TCP 会重传,UDP 要自己处理或容忍。

5. TCP 与 UDP 再对比(入门重点)

TCP:像打电话
  1. 三次握手建立连接
  2. 双方确认收到(可靠)
  3. 四次挥手断开连接
    适合:不能丢,要按序的数据(网页,下载,多数API)。
    代价:连接开销大,速度慢于UDP一点点。
UDP:像寄明信片
  1. 不先[建立连接],发出去就完事
  2. 可能丢,可能乱序
    适合:实时性要求高,能容忍丢包,或小数据很小很频繁(传感器每秒一条,语音/数据常配合应用层处理)
怎么选(简单原则)
场景倾向
网页、REST API、文件TCP(上常跑 HTTP)
物联网周期上报、局域网发现UDP 或 MQTT(MQTT 底层常用 TCP)
视频直播、语音常 UDP + 专用协议

6. 常见应用层协议

协议作用传输层常用
HTTP/HTTPS网页、REST 接口TCP
MQTT物联网发布/订阅、省流量TCP
CoAP轻量物联网,类似简化 HTTPUDP
DNS域名解析UDP/TCP
DHCP自动分配 IPUDP
FTP文件传输TCP
WebSocket浏览器长连接、实时推送TCP

HTTPS = HTTP + TLS 加密,端口 443。

7.嵌入式相关网络的一点点延伸

做MCU/工控时,经常碰到:

  • 以太网 + lwIP:在单片机里实现 TCP/IP协议栈,再跑HTTP/MQTT客户端。
  • Wi-Fi / 4G 模块:模块内部已做好协议栈,MCU用AT指令 或 串口透传 发数据,相当于模块帮你扛网络层以上。
  • RS485 / CAN:多是 现场总线,不算总线互联网 TCP/IP,但在[设备联网]课程里常和以太网对比——距离近,协议简单,无IP
  • Modbus TCP:把Modbus 跑在以太网TCP上,端口常502。
    入门路径可以是:先懂IP,端口,TCP/UDP——>再学 Socket 编程或MQTT——再在板子上用模块或lwIP实践。

8.局域网 vs 广域网

类型范围例子
LAN 局域网家里、办公室、车间192.168.x.x、10.x.x.x
WAN 广域网跨城市、互联网公网 IP、运营商网络

家里路由器:对内做 DHCP 分 192.168.1.x,对外一个公网 IP(NAT),多台设备共享上网。
NAT:内网多台设备共用一个公网 IP 出去,外部要主动连进来较麻烦(要做端口映射或云中转)。

2.TCP协议详解

TCP(Transmission Control Protocol,传输控制协议)工作在 传输层,和 IP 配合使用:
IP负责把包送到目标主机,TCP负责在 两台主机的两个端口之间,提供可靠,有序,按字节流的数据传输。
可以理解为:先建立一条逻辑上的“管道”,数据从一端写进去,另一端按顺序读出来,丢了重传,满了会等一等。
在这里插入图片描述

1.TCP解决什么问题

网络层IP的特点:

  • 不保证一定能送到
  • 不保证按发送顺序到达
  • 不保证不重复
    对网页,文件下载,API接口来说,这不够可信。TCP补上:
能力说明
可靠丢了重传,直到对方确认收到
有序每个字节有编号,接收端按序交给应用
面向连接传数据前先建立连接,传完再断开
流量控制发太快时,接收方会让发送方慢下来
拥塞控制网络堵了时,主动减小发送速度

代价是:比 UDP 慢一些、实现更复杂、占更多内存(要维护连接状态)。

2. TCP 和 UDP 对比

项目TCPUDP
连接要先建立连接无连接,发完即走
可靠性可靠不保证
顺序保证不保证
首部开销一般 20 字节起8 字节
速度相对慢相对快
典型用途HTTP、HTTPS、邮件、MQTT、数据库DNS 查询、视频、语音、简单传感器上报

选型:数据不能丢、要完整顺序 → TCP;能容忍丢包、要实时或极简 → UDP。

3. 端口:TCP 怎么找到“哪个程序”

IP 地址定位 哪台设备,端口号(16 位,0~65535)定位 哪条连接/哪个应用。

  • 知名端口(0~1023):如 80(HTTP)、443(HTTPS)、22(SSH)
  • 注册端口(1024~49151)
  • 动态端口(49152~65535):客户端临时分配用
    一条 TCP 连接由 四元组 唯一确定:
    (源 IP, 源端口, 目标 IP, 目标端口)
    例如:手机 192.168.1.100:52341 访问服务器 93.184.216.34:443。
  • 源 IP:192.168.1.100
  • 源端口:52341
  • 目标 IP:93.184.216.34(服务器的 IP)
  • 目标端口:443(HTTPS 服务的默认端口)

4. TCP 报文段 (Segment) 长什么样

应用层交给TCP的是字节流;TCP切成一块块 段发出去。每段前面有一个TCP首部,后面是 数据。

4.1 首部主要字段(必记)
 1   0                   1                   2                   3
 2   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 3  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 4  |          源端口 (16)          |          目的端口 (16)          |
 5  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 6  |                        序号 Seq (32)                          |
 7  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 8  |                        确认号 Ack (32)                        |
 9  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
10  | 数据偏移 | 保留 | 标志位 |        窗口大小 Window (16)        |
11  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12  |        校验和         |      紧急指针     |    选项 (可选)      |
13  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
14  |                               数据                            |
15  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段作用
源/目的端口标识两端应用
序号 Seq本段数据在字节流中的位置(第几个字节开始)
确认号 Ack期望对方下一个字节的序号(表示此前都已收到)
标志位SYN、ACK、FIN、RST、PSH等,控制连接状态
窗口 Window流量控制:接收方还能收多少字节
校验和(16位)检测首部+数据是否出错
紧急指针(16位)指示报文段中“紧急数据”的结束位置(或范围)

TCP 首部通常 20 字节;带选项时更长。IP 层再包一层 IP 首部,下面是以太网帧等。

4.2 常见标志位
标志含义
SYN建立连接用,同步序号(TCP 规定:SYN 和 FIN 标志位必须各消耗一个序列号)
ACK确认号有效
FIN发送方不再发数据,要关闭连接
RST复位,强制断开或拒绝连接
PSH尽快交给应用层(如 telnet 按键)
URG紧急数据(很少用)

5. 序号与确认:可靠性的核心

序号 (Sequence Number)

TCP 把应用数据看成连续的字节,每个字节一个编号。

  • 建立连接时,双方各选一个 初始序号 ISN(随机,防伪造)。
  • 每发一段数据,序号增加这段的长度。
    例:初始序号 1000,发 100 字节,则本段 Seq=1000,下一段 Seq=1100。
确认号 (Acknowledgement Number)

接收方回 ACK:“我已经按序收到 Ack-1 之前的所有字节,请从 Ack 开始发”。

  • ACK 段可以 不带数据,只带确认号。
  • 累积确认:Ack=n 表示 n 之前的都收到了,减少 ACK 数量。
超时重传

发出去一段时间内没收到 ACK,认为丢失,重传同一段(或重传该序号起的字节,实现细节与算法有关)。
接收方收到重复段会 丢弃但可再发 ACK,帮助对方确认。

乱序与重复
  • 乱序:先到的段可先缓存,等前面段到了再拼成有序流交给应用。
  • 重复:靠序号识别,重复则丢弃

6.三次握手:建立连接

客户端要主动访问服务器,需先完成三次握手。(3-way handshake):
在这里插入图片描述

  • 第1次:客户端发 SYN=1,序号 Seq=x(x为客户端ISN)
  • 第2次:服务器回 SYN=1,ACK=1,Seq=y,Ack=x+1(同意连接,自己的ISN是y)
  • 第3次:客户端发 ACK=1,Ack=y+1,此后双方都认为连接建立。
    为什么是三次。不是两次?
  • 两次无法让服务器确认:客户端确实收到自己的 SYN-ACK(避免历史重复SYN导致服务器单方面认为连接已建立)
  • 三次握手同时 协商双方的初始序号。

7.四次挥手:关闭连接

TCP是全双工:两个方向各有一条字节流,关闭时 每个方向都要单独关,所以通常要 四次挥手:
在这里插入图片描述

第1次:主动关闭方发 FIN:“我这边没有数据要发了”
第2次:对方 ACK:“知道了。”此时对方仍可以发数据(半关闭)
第3次:对方也发 FIN:“我这边也不发了”
第4次:主动关闭方 ACK,然后进入 TIME_WAIT (通常等待2MSL,几十秒到几分钟),防止最后的ACK丢失导致对方重发FIN时无法正确响应。
TIME_WAIT原因:

  • 原因一:防止最后的 ACK 丢失(为了保证对方能正常关闭)
    假设主动方发的第4次 ACK 在网络中丢了。对方没收到确认,就会触发超时重传,再次发送第3次的 FIN。
    如果主动方发完 ACK 就立刻关闭跑路了,对方重传的 FIN 过来时,主动方已经不存在了,就会回一个 RST(强制复位)错误。
    所以,主动方进入 TIME_WAIT 等一会儿,如果对方重传了 FIN,主动方还能再回一个 ACK。这就保证了“接完对方可能重发的 FIN 并回复”。
  • 原因二:防止“历史幽灵包”污染新连接
    等待 2MSL 的时间,足以让本次连接产生的所有数据包在网络中彻底“死掉”。这样下次再用同样的端口建立新连接时,绝对不会收到上一次连接的残渣。

为什么是四次?
因为TCP允许一端先关发送,另一端继续发(半关闭),FIN 和 ACK往往分开发,所以比握手多一次。
同时关闭:双方都发FIN,可能合并成类似三次的包,逻辑仍是两边都关闭。


8.连接状态(看懂netstat)

TCP 用状态机管理连接,常见状态:

状态含义
CLOSED无连接
LISTEN服务器监听端口,等连接
SYN_SENT客户端已发 SYN,等回复
SYN_RCVD服务器收到 SYN,已回 SYN+ACK
ESTABLISHED连接已建立,可传数据
FIN_WAIT_1 / FIN_WAIT_2主动关闭,等对方 FIN 或 ACK
CLOSE_WAIT收到对方 FIN,应用还未 close
LAST_ACK被动关闭方发完 FIN,等最后 ACK
TIME_WAIT主动关闭方发完最后 ACK,等待一段时间
CLOSED完全结束

开发常见问题:

  • 服务器大量 CLOSE_WAIT:应用收到连接关闭后 没有调用 close,要查代码是否漏关 socket。
  • 主动关闭方大量 TIME_WAIT:短连接太多,可考虑复用连接、调参或让服务器主动关(视场景而定)。

9. 流量控制(滑动窗口)

接收方缓冲区有限,不能无限制收。TCP 首部 窗口字段 告诉对方:我还能接收多少字节。

  • 发送方维护 发送窗口:未确认且未超窗口的数据才能发。
  • 接收方处理慢 → 窗口变小 → 发送方减速。
  • 窗口为 0 时,发送方暂停;接收方后来用 窗口更新 通知可以再发。

这叫 滑动窗口,既保证可靠,又尽量把链路跑满。
零窗口与死锁:窗口长期为 0 时,用 持续计时器 定期发探测段,避免永远等不到窗口更新。

10. 拥塞控制(简要)

流量控制解决的是“接收方吃不下”;拥塞控制解决的是“网络中间堵了”。
发送方维护 拥塞窗口 cwnd,实际发送量 ≈ min(拥塞窗口, 对方通告窗口)。
经典阶段(了解概念即可,现代实现会更复杂):

  1. 慢启动:cwnd 从小指数增长,试探网络容量。
  2. 拥塞避免:快到阈值后线性增长。
  3. 快重传 / 快恢复:收到 3 个重复 ACK,认为丢包,快速重传并调整 cwnd,不必总是超时后才重传。
    你看到下载速度时快时慢,partly 就是拥塞控制在调节。

11. TCP 对应用层呈现什么

应用调用 socket API 时,看到的是 无消息边界的字节流:

  • 发 100 字节 + 200 字节,对方可能一次 read 收到 300 字节,也可能分多次收到。
  • 若需要“一条一条消息”,要在应用层自己加长度头、分隔符或用专门协议(如 HTTP Content-Length 固定头)。
    粘包(一次接完分不清哪个是哪个) / 拆包(包较长几次接):TCP 本身不保留“包”的边界,这是初学者最常踩的坑。

12. 常见应用与端口

应用端口说明
HTTP80网页(明文)
HTTPS443HTTP + TLS 加密
SSH22远程登录
FTP 控制21文件(数据通道另说)
SMTP25发邮件
MQTT1883物联网,基于 TCP
MySQL3306数据库

浏览器访问 https://example.com 底层就是:DNS 查 IP → TCP 连 443 → TLS 握手 → HTTP 请求/响应

13. 编程时你要会的几件事

服务器典型流程

socket() —> bind(端口)—>listen()—> accept() 得到新连接—>read()/write()与客户端通信—>close()

  • socket()
    • 作用:创建套接字(买电话机)。
    • 状态:CLOSED(无连接)。
  • bind(端口)
    • 作用:绑定IP和端口。
    • 状态:CLOSED。
  • listen()
    • 作用:开启监听,等待客户端连接。
    • 状态:LISTEN(服务器监听端口,等连接)。
  • accept()
    • 作用:从已完成握手的队列中取出一个连接,返回一个新的Socket用于通信。(底层三次握手已由内核自动完成)。
    • 状态:原Socket保持LISTEN;新Socket进入ESTABLISHED(连接已建立,可传数据)。
  • read()/write()
    • 作用:读写(收发)字节流数据。
    • 状态:ESTABLISHED。
  • close()
    • 作用:关闭连接。
    • 状态:若主动关闭,进入 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED。(若是收到对方FIN后被动关闭,会先进入CLOSE_WAIT,调用close后进入LAST_ACK)。
客户端典型流程

socket() —> connect(IP,端口)—>read()/write()—>close()

  • socket()
    • 作用:创建套接字。
    • 状态:CLOSED。
  • connect(IP,端口)
    • 作用:发起连接(底层直接触发三次握手)。
    • 状态:发送SYN后进入 SYN_SENT;握手成功后进入 ESTABLISHED。
  • read()/write()
    • 作用:读写数据。
    • 状态:ESTABLISHED。
  • close()
    • 作用:关闭连接(通常由客户端主动发起四次挥手)。
    • 状态:FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT(等待2MSL) -> CLOSED。
注意点
  • 每个 accept 的连接要单独 close,否则 CLOSE_WAIT 堆积。
  • 短连接 会产生大量 TIME_WAIT,高并发时要考虑连接池、长连接。
  • 阻塞 / 非阻塞 / 多路复用 (select、poll、epoll) 决定单线程能扛多少连接。
  • 嵌入式用 lwIP 时,同样有 TCP 状态机,RAM 要留给每连接的发送/接收缓冲。

14. TCP 与 IP 的关系 (串起来)

应用数据
→ TCP 加首部 (端口、序号、ACK、窗口…) 变成 TCP 段
→ IP 加首部 (源/目标 IP) 变成 IP 包
→ 以太网加 MAC 首部变成帧
→ 物理线发送
对端逐层剥开,TCP 按序号拼回字节流,交给应用。

15. 小结

要点一句话
定位传输层,端口到端口,可靠字节流
连接三次握手建连,四次挥手断连
可靠序号 + 累积 ACK + 超时重传
流控接收窗口,滑动窗口
拥塞拥塞窗口,慢启动、快重传等
应用不保留消息边界,要自己处理粘包

把三次握手、序号/ACK、四次挥手、窗口 四块吃透,再看 Wireshark 抓包(过滤 tcp ),对照 Seq/Ack 标志位,会比纯背概念快很多。

3.TCP Socket编程

1. Socket 是什么

Socket (套接字) 是应用层和传输层之间的接口,在 Windows 上通常指 Winsock(Windows Sockets)提供的个“句柄”。
可以这样理解:
你的程序 ↔ socket API ↔ 系统 TCP 协议栈 ↔ 网卡

  • 服务器:在本机某个端口上 listen ,等别人连进来,每连上一个客户 accept 得到一个新的 socket 专门和该客户通信。
  • 客户端:主动 connect 到对方的 IP + 端口。
    一条 TCP 连接 = 本地的 (IP, 端口) 和远端的 (IP, 端口) 四元组唯一确定。在这里插入图片描述

2.Windows上的Socket:Winsock

在Linux上,Socket返回的是一个文件描述符(int);在Windows上,Winsock返回的是 SOCKET 类型 (本质 UINT_PTR,可理解为一个“网络句柄”)。
使用前必须先初始化 Winsock,用完后再清理。

 1 #include <winsock2.h>
 2 #include <ws2tcpip.h>  // getaddrinfo 等
 3 #pragma comment(lib, "ws2_32.lib")  // MSVC 自动链接; MinGW 需在编译参数里加 -lws2_32
 4 WSADATA wsa;
 5 if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
 6     printf("WSAStartup failed: %d\n", WSAGetLastError());
 7     return 1;
 8 }
 9 // ... 所有 socket 操作 ...
10 WSACleanup();

要点:

概念说明
WSAStartup加载 ws2_32.dll,协商 Winsock 版本
WSACleanup与 WSAStartup 成对调用
SOCKETWindows 专用类型,不要和 Linux 的 int fd 混用
INVALID_SOCKET创建失败时的返回值(不是 -1)
closesocket()关闭 socket(不是 close())
WSAGetLastError()获取最后一次 Winsock 错误码

3. 地址结构: sockaddr_in

IPv4 通信最常用 sockaddr_in (“in” = internet):

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;              // IPv4
addr.sin_port   = htons(8080);          // 端口:必须网络字节序
addr.sin_addr.s_addr = INADDR_ANY;      // 监听本机所有网卡;客户端填对方 IP

几个容易踩坑的点:

  1. 端口要用 htons()(host to network short)。大端/小端不同机器上端口字节序必须统一。
  2. INADDR_ANY:服务器"监听所有本地 IP";客户端要填目标 IP。
  3. 字符串 IP(如 "127.0.0.1")用 inet_pton() 转换,比老式 inet_addr() 更安全:
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    bind / connect 的参数类型是通用的 struct sockaddr *,所以要强转:
bind(sock, (struct sockaddr*)&addr, sizeof(addr));

4. Winsock 初始化 API

4.1 WSAStartup

加载 Winsock 库,任何 socket 操作之前必须先调用。

int WSAStartup(
    WORD      wVersionRequested,  // 请求的版本,通常 MAKEWORD(2, 2)
    LPWSADATA lpWSAData           // 输出:实际协商到的版本信息
);
返回值含义
0成功
非 0失败,可配合 WSAGetLastError() 查原因
WSADATA wsa;
int ret = WSAStartup(MAKEWORD(2, 2), &wsa);
if (ret != 0) {
    printf("WSAStartup failed: %d\n", ret);
    return 1;
}
4.2 WSACleanup

与 WSAStartup 成对,进程结束前调用,释放 Winsock 资源。
int WSACleanup(void);

返回值含义
0成功
SOCKET_ERROR失败
4.3 WSAGetLastError

获取最近一次 Winsock 调用的错误码(类似 Linux 的 errno)。

int WSAGetLastError(void);

常见错误码(后面排错会反复见到):

错误码宏名常见原因
10048WSAEADDRINUSE端口已被占用
10061WSAECONNREFUSED对方没监听 / 防火墙拦截
10054WSAECONNRESET对端异常断开
10060WSAETIMEDOUT连接超时
10035WSAEWOULDBLOCK非阻塞模式下暂时无数据
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
    printf("bind failed: %d\n", WSAGetLastError());
}

5.Socket生命周期API

5.1 socket — 创建套接字
SOCKET socket(
    int af,        // 地址族: AF_INET(IPv4) / AF_INET6(IPv6)
    int type,      // 类型: SOCK_STREAM(TCP) / SOCK_DGRAM(UDP)
    int protocol   // 协议: TCP 填 IPPROTO_TCP,可填 0 让系统自动选
);
返回值含义
有效 SOCKET成功
INVALID_SOCKET失败,用 WSAGetLastError() 查原因
// 创建一个 IPv4 + TCP 的 socket
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
    printf("socket failed: %d\n", WSAGetLastError());
}
5.2 bind — 绑定本机地址 (服务器必用)

把 socket 和本机的 (IP, 端口) 绑在一起。
只有服务器需要 bind;客户端一般由系统在 connect 时自动分配本地端口。

int bind(
    SOCKET s,
    const struct sockaddr *name,  // 指向 sockaddr_in 等地址结构
    int namelen                   // 结构体大小
);
返回值含义
0成功
SOCKET_ERROR失败
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family      = AF_INET;
addr.sin_port        = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;  // 0.0.0.0,监听所有网卡
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
5.3 listen — 进入监听状态 (服务器必用)

声明此 socket 用于接受连接,内核开始维护“已完成三次握手的连接队列”。

int listen(
    SOCKET s,
    int backlog  // 等待 accept 的连接队列最大长度
);
返回值含义
0成功
SOCKET_ERROR失败
listen(sock, SOMAXCONN);  // SOMAXCONN 让系统选一个合理上限
// 或
listen(sock, 5);

backlog 不是“最多 5 个客户端在线”,而是已完成握手、还没被 accept 取走的连接最多排队几个。

5.4 accept — 接受一个客户端连接 (服务器)

从 listen 的队列里取出一个已建立的 TCP 连接,返回新的 socket,专门和该客户端通信。

SOCKET accept(
    SOCKET s,
    struct sockaddr *addr,  // 输出:客户端地址,可传 NULL
    int *addrlen            // 输入/输出:地址结构长度
);
返回值含义
新的 SOCKET成功,用于和该客户端 send/recv
INVALID_SOCKET失败
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
SOCKET client_sock = accept(listen_sock,
                            (struct sockaddr*)&client_addr,
                            &len);
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
printf("Client: %s:%d\n", ip, ntohs(client_addr.sin_port));

要点:

  • listen_sock:只负责接客,一般不拿收发业务数据。
  • client_sock:和某个客户端的专用通道,有几个客户端就可能 accept 出几个。
5.5 connect — 主动连接服务器 (客户端)

向指定 (IP, 端口) 发起 TCP 三次握手,成功返回时连接已建立。

int connect(
    SOCKET s,
    const struct sockaddr *name,
    int namelen
);
返回值含义
0成功
SOCKET_ERROR失败
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port   = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &server.sin_addr);
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) == SOCKET_ERROR) {
    printf("connect failed: %d\n", WSAGetLastError());
}
5.6 closesocket — 关闭并释放 socket
int closesocket(SOCKET s);
返回值含义
0成功
SOCKET_ERROR失败
Windows 用 closesocket(),不是 Linux 的 close()。
closesocket(client_sock);
closesocket(listen_sock);
5.7 shutdown — 半关闭连接

只关闭连接的某一方向,比直接 closesocket 更“礼貌”。

int shutdown(
    SOCKET s,
    int    how   // SD_RECEIVE / SD_SEND / SD_BOTH
);
how含义
SD_RECEIVE不再接收 (对端再发会收到 RST 等)
SD_SEND不再发送 (会发 FIN,通知对端“我发完了”)
SD_BOTH双向都关
shutdown(sock, SD_SEND);  // 告诉对方:我的数据发完了
// 还可以继续 recv 读对方剩余数据
closesocket(sock);

6. 数据收发 API

6.1 send — 发送数据 (TCP)
int send(
    SOCKET s,
    const char *buf,
    int    len,
    int    flags   // 通常填 0
);
返回值含义
> 0本次实际发送的字节数 (可能小于 len)
0一般不会在 TCP 上返回 0
SOCKET_ERROR失败
const char *msg = "Hello";
int n = send(sock, msg, (int)strlen(msg), 0);
if (n == SOCKET_ERROR) {
    printf("send failed: %d\n", WSAGetLastError());
}
6.2 recv — 接收数据 (TCP)
int recv(
    SOCKET s,
    char *buf,
    int    len,
    int    flags   // 通常填 0
);
返回值含义
> 0本次实际收到的字节数
0对端已正常关闭连接 (收到 FIN)
SOCKET_ERROR失败
char buf[1024];
int n = recv(sock, buf, sizeof(buf), 0);
if (n > 0) {
    // 收到 n 字节,TCP 不保证一次收齐一条“消息”
} else if (n == 0) {
    printf("Peer closed connection\n");

以下是图片中提取的文字和代码内容,已按原图结构排版:

6.3 send / recv 的 flags (了解即可)

宏作用
0默认阻塞收发
MSG_PEEK只读取缓冲区数据,不从队列移除
MSG_DONTROUTE不查路由表 (很少用)

入门阶段 flags 一律填 0 即可。

7. 地址与字节序 API

7.1 字节序转换

网络传输统一用大端 (网络字节序),本机可能是小端,所以端口/IP 要转换。

u_short htons(u_short hostshort);  // host to network short (端口)
u_short ntohs(u_short netshort);   // network to host short
u_long htonl(u_long hostlong);     // host to network long (IPv4 地址)
u_long ntohl(u_long netlong);      // network to host long
addr.sin_port = htons(8080);       // 端口: 主机 -> 网络
printf("%u\n", ntohs(addr.sin_port)); // 打印时: 网络 -> 主机
7.2 IP 字符串 <-> 二进制

inet_pton (推荐,字符串 -> 二进制)

int inet_pton(
    int af,             // AF_INET 或 AF_INET6
    const char *src,    // 如 "192.168.1.1"
    void *dst           // 输出到 sin_addr 等
);
返回值含义
1转换成功
0字符串格式无效
-1出错
struct sockaddr_in addr;
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

inet_ntop (推荐,二进制 -> 字符串)

const char *inet_ntop(
    int af,
    const void *src,
    char *dst,
    socklen_t size
);
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str));

老式 API (了解,不推荐新项目用)

unsigned long inet_addr(const char *cp); // "127.0.0.1" -> sin_addr.s_addr
char *inet_ntoa(struct in_addr in);      // 静态缓冲区,非线程安全

特殊地址常量

INADDR_ANY;      // 0.0.0.0,绑定所有本地网卡
INADDR_LOOPBACK; // 127.0.0.1

8. 地址结构体 (和 API 配套理解)

8.1 sockaddr_in (IPv4 最常用)
struct sockaddr_in {
    short          sin_family; // AF_INET
    u_short        sin_port;   // 端口,需 htons
    struct in_addr sin_addr;   // IPv4 地址
    char           sin_zero[8];// 填充,填 0
};

调用 bind / connect / accept 时要转成通用类型:

(struct sockaddr*)&addr
8.2 通用 sockaddr
struct sockaddr {
    u_short sa_family; // 地址族
    char    sa_data[14];// 地址数据
};

Winsock 很多函数参数类型是 struct sockaddr *,实际传入的往往是 sockaddr_in,所以需要强制转换。

9. 常用扩展 API

9.1 setsockopt — 设置 socket 选项

开发中最常用:地址重用,避免服务器重启后 WSAEADDRINUSE。

int setsockopt(
    SOCKET s,
    int    level,       // SOL_SOCKET
    int    optname,     // SO_REUSEADDR 等
    const char *optval,
    int    optlen
);
BOOL reuse = TRUE;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
           (const char*)&reuse, sizeof(reuse));
常用选项作用
SO_REUSEADDR允许重用本地地址 (开发服务器几乎必开)
SO_RCVTIMEO接收超时
SO_SNDTIMEO发送超时
9.2 getsockname / getpeername — 查本端/对端地址

连接建立后,若想知道“实际连的是哪”:

int getsockname(SOCKET s, struct sockaddr *name, int *namelen); // 本端
int getpeername(SOCKET s, struct sockaddr *name, int *namelen); // 对端
struct sockaddr_in local, peer;
int llen = sizeof(local), plen = sizeof(peer);
getsockname(sock, (struct sockaddr*)&local, &llen);
getpeername(sock, (struct sockaddr*)&peer, &plen);
9.3 getaddrinfo / freeaddrinfo — 现代地址解析 (推荐)

比手写 sockaddr_in 更通用,支持域名、IPv4/IPv6。

int getaddrinfo(
    const char *nodename,     // 主机名或 IP,如 "www.example.com"
    const char *servname,     // 端口字符串,如 "8080"
    const struct addrinfo *hints,
    struct addrinfo **result
);
void freeaddrinfo(struct addrinfo *ai);

struct addrinfo hints, *res = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo("127.0.0.1", "8080", &hints, &res) == 0) {
    connect(sock, res->ai_addr, (int)res->ai_addrlen);
    freeaddrinfo(res);
}

10. API 与角色对照表

API服务器客户端一句话
WSAStartup✓✓初始化 Winsock
socket✓✓创建套接字
bind✓—绑定本机端口
listen✓—开始监听
accept✓—接受连接,得新 socket
connect—✓连接服务器
send/recv✓✓收发数据
shutdown✓✓半关闭
closesocket✓✓释放句柄
WSACleanup✓✓清理 Winsock

4. TCP客户端 服务器代码编写

1. 什么是服务器 (Server)

服务器是网络里被动等待连接、提供服务的一方。
可以把它理解成一家商店:

  • 商店有个固定地址 (IP + 端口),比如 192.168.1.10:8080
  • 商店开门营业 (listen),等顾客上门
  • 每来一个顾客,店员单独接待 (accept 得到一个专门对话的 socket)
  • 顾客问什么,店员答什么 (recv / send)
    在程序里,服务器通常做这些事:
  1. 在本机某个端口上监听
  2. 接受客户端的连接请求
  3. 和某个客户端收发数据
  4. 结束后关闭连接
    典型场景:Web服务器,游戏服务器,工控上位机,你的Echo测试程序
    在这里插入图片描述

2. 什么是客户端 (Client)

客户端是网络里主动发起连接、请求服务的一方。
继续用商店比喻:

  • 顾客知道商店的地址(知道服务器的 IP + 端口)
  • 顾客主动走过去敲门(connect)
  • 门开了之后,顾客和店员对话(send / recv)
  • 聊完离开(closesocket)
    在程序里,客户端通常做这些事:
  1. 知道服务器的 IP 和端口
  2. 主动连接服务器
  3. 收发数据
  4. 关闭连接
    典型场景:浏览器、手机 App、你的测试客户端、嵌入式设备上报数据。
    在这里插入图片描述

3. 客户端 vs 服务器:一张表看懂

对比项服务器 (Server)客户端 (Client)
角色提供服务请求服务
主动性被动等待主动连接
是否 bind必须,绑定固定端口一般不需要,系统自动分配本地端口
是否 listen必须不需要
建立连接accept() 接受connect() 发起
要知道什么自己监听哪个端口对方的 IP + 端口
数量关系1 个服务器可服务多个客户端多个客户端可连同一服务器

记住一句话:

服务器 = 开店等人来;客户端 = 知道地址主动上门。

4.一条TCP连接的完整流程

4.1逻辑流程(从开始到结束)

在这里插入图片描述

4.2TCP三次握手(建立连接时系统自动完成)

客户端调用 connect()、服务器调用 accept() 时,操作系统在底层完成三次握手:

客户端                        服务器
  |                             |
  | ——— SYN (我想连接) ————————> |  第 1 次
  |                             |
  | <—— SYN+ACK (好的,我也准备好了) ——|  第 2 次
  |                             |
  | ——— ACK (收到,连接建立) —————> |  第 3 次
  |                             |
  |       连接进入 ESTABLISHED      |

应用层不需要自己发 SYN,只要调用 connect / accept 即可。

4.3 服务器上的两个 socket (非常重要)

很多初学者这里会混,服务器上其实有两种 socket:

listen_sock —— 监听 socket,绑定 8080 端口,只负责“接客”
                  |
                accept()
                  |
client_sock —— 连接 socket,专门和某一个客户端收发数据
  • listen_sock:一直存在,负责监听 8080
  • client_sock:每 accept 一个客户端,就新建一个
    类比:前台负责接待(listen_sock),每来一个客人就分配一个专属客服(client_sock)。
4.4 四元组:一条连接如何唯一确定

一条 TCP 连接由四个信息唯一确定:
(源 IP, 源端口, 目标 IP, 目标端口)
例如客户端 192.168.1.5:54321 连服务器 192.168.1.10:8080:
客户端视角:192.168.1.5:54321 -> 192.168.1.10:8080
服务器视角:192.168.1.10:8080 <- 192.168.1.5:54321
所以:

  • 服务器只需监听一个端口(如 8080)
  • 多个客户端连进来时,靠不同的源 IP/源端口区分

5. 服务器代码编写 (逐步讲解)

5.1 第一步:初始化 Winsock
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
    printf("WSAStartup failed\n");
    return 1;
}
5.2 第二步:创建监听 socket
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listen_sock == INVALID_SOCKET) {
    printf("socket failed: %d\n", WSAGetLastError());
    WSACleanup();
    return 1;
}
5.3 第三步:设置端口重用 (推荐)

服务器刚关闭立刻重启时,端口可能还处于 TIME_WAIT,不加这句会 bind 失败。

BOOL reuse = TRUE;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR,
           (const char*)&reuse, sizeof(reuse));
5.4 第四步:bind 绑定本机地址
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);        // 监听 8080 端口
addr.sin_addr.s_addr = INADDR_ANY;  // 0.0.0.0,所有网卡
if (bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
    printf("bind failed: %d\n", WSAGetLastError());
    closesocket(listen_sock);
    WSACleanup();
    return 1;
}
5.5 第五步:listen 开始监听
if (listen(listen_sock, 5) == SOCKET_ERROR) {
    printf("listen failed: %d\n", WSAGetLastError());
    closesocket(listen_sock);
    WSACleanup();
    return 1;
}
printf("Server listening on port 8080...\n");
5.6 第六步:accept 等待客户端
struct sockaddr_in client_addr;
int client_len = sizeof(client_addr);
SOCKET client_sock = accept(listen_sock,
                            (struct sockaddr*)&client_addr,
                            &client_len);
if (client_sock == INVALID_SOCKET) {
    printf("accept failed: %d\n", WSAGetLastError());
    closesocket(listen_sock);
    WSACleanup();
    return 1;
}
5.7 第七步:recv / send 收发数据
char buf[1024];
int n = recv(client_sock, buf, sizeof(buf) - 1, 0);
if (n > 0) {
    buf[n] = '\0';
    printf("Received: %s\n", buf);
    send(client_sock, buf, n, 0); // Echo 原样发回
} else if (n == 0) {
    printf("Client disconnected\n");
}
5.8 第八步:关闭 socket,清理资源
closesocket(client_sock); // 关闭和该客户端的连接
closesocket(listen_sock); // 关闭监听
WSACleanup();

6. 客户端代码编写 (逐步讲解)

客户端流程比服务器短,没有 bind / listen / accept。

6.1 初始化 Winsock
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
    printf("WSAStartup failed\n");
    return 1;
}
6.2 创建 socket
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
    printf("socket failed: %d\n", WSAGetLastError());
    WSACleanup();
    return 1;
}
6.3 填写服务器地址,connect 连接
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port   = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);  // 本机测试用回环地址
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) == SOCKET_ERROR) {
    printf("connect failed: %d\n", WSAGetLastError());
    closesocket(sock);
    WSACleanup();
    return 1;
}
printf("Connected to server\n");

connect() 返回成功时,三次握手已完成,可以开始 send / recv。

6.4 发送数据,接收回复
const char *msg = "Hello, Server!";
send(sock, msg, (int)strlen(msg), 0);
char buf[1024];
int n = recv(sock, buf, sizeof(buf) - 1, 0);
if (n > 0) {
    buf[n] = '\0';
    printf("Server replied: %s\n", buf);
}
6.5 关闭连接
closesocket(sock);
WSACleanup();

7. 完整可编译代码

7.1 服务器完整代码 tcp_server.c
#define _WIN32_WINNT 0x0601
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define SERVER_PORT 8080
#define BUF_SIZE   1024

int main(void)
{
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        printf("[Server] WSAStartup failed\n");
        return 1;
    }

    /* 1. 创建监听 socket */
    SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listen_sock == INVALID_SOCKET) {
        printf("[Server] listen failed: %d\n", WSAGetLastError());
        closesocket(listen_sock);
        WSACleanup();
        return 1;
    }

    printf("[Server] Listening on port %d ...\n", SERVER_PORT);

    /* 5. accept: 阻塞等待客户端连接 */
    struct sockaddr_in client_addr;
    int client_len = sizeof(client_addr);
    SOCKET client_sock = accept(listen_sock,
                                (struct sockaddr*)&client_addr,
                                &client_len);
    if (client_sock == INVALID_SOCKET) {
        printf("[Server] accept failed: %d\n", WSAGetLastError());
        closesocket(listen_sock);
        WSACleanup();
        return 1;
    }

    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
    printf("[Server] Client connected: %s:%d\n",
           client_ip, ntohs(client_addr.sin_port));

    /* 6. 收发数据: 收到什么就 Echo 回去 */
    char buf[BUF_SIZE];
    int n = recv(client_sock, buf, BUF_SIZE - 1, 0);
    if (n > 0) {
        buf[n] = '\0';
        printf("[Server] Received: %s\n", buf);
        send(client_sock, buf, n, 0);
        printf("[Server] Echo sent\n");
    } else if (n == 0) {
        printf("[Server] Client closed connection\n");
    } else {
        printf("[Server] recv failed: %d\n", WSAGetLastError());
    }

    /* 7. 清理 */
    closesocket(client_sock);
    closesocket(listen_sock);
    WSACleanup();
    printf("[Server] Exit\n");
    return 0;
}
7.2 客户端完整代码 tcp_client.c
#define _WIN32_WINNT 0x0601
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define SERVER_PORT 8080
#define SERVER_IP   "127.0.0.1"
#define BUF_SIZE    1024

int main(void)
{
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        printf("[Client] WSAStartup failed\n");
        return 1;
    }

    /* 1. 创建 socket */
    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == INVALID_SOCKET) {
        printf("[Client] socket failed: %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    /* 2. 填写服务器地址 */
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port   = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server.sin_addr);

    /* 3. connect: 连接服务器 (内部完成三次握手) */
    if (connect(sock, (struct sockaddr*)&server, sizeof(server)) == SOCKET_ERROR) {
        printf("[Client] connect failed: %d\n", WSAGetLastError());
        closesocket(sock);
        WSACleanup();
        return 1;
    }
    printf("[Client] Connected to %s:%d\n", SERVER_IP, SERVER_PORT);

    /* 4. 发送消息 */
    const char *msg = "Hello, TCP Server!";
    if (send(sock, msg, (int)strlen(msg), 0) == SOCKET_ERROR) {
        printf("[Client] send failed: %d\n", WSAGetLastError());
        closesocket(sock);
        WSACleanup();
        return 1;
    }
    printf("[Client] Sent: %s\n", msg);

    /* 5. 接收服务器回复 */
    char buf[BUF_SIZE];
    int n = recv(sock, buf, BUF_SIZE - 1, 0);
    if (n > 0) {
        buf[n] = '\0';
        printf("[Client] Received: %s\n", buf);
    } else if (n == 0) {
        printf("[Client] Server closed connection\n");
    } else {
        printf("[Client] recv failed: %d\n", WSAGetLastError());
    }

    /* 6. 清理 */
    closesocket(sock);
    WSACleanup();
    printf("[Client] Exit\n");
    return 0;
}

5.UDP协议详解

一、UDP是什么

UDP(User Datagram Protocol,用户数据报协议)是 TCP/IP 协议栈中传输层的一个核心协议,与 TCP 并列,位于 IP 层之上、应用层之下。
UDP 的设计哲学非常明确:尽可能快地交付数据报,不保证可靠性、不保证顺序、不建立连接。
可以把 TCP 理解为「打电话」——先拨号建立连接,确认对方在听,再说内容,漏了会重说;UDP 则是「寄明信片」——写好就扔出去,不管对方有没有收到,也不保证按顺序到达。
![[Pasted image 20261107210410.png]]

一、UDP在协议栈中的位置

![[Pasted image 20261107210434.png]]

应用层数据交给 UDP 后,UDP 加上自己的头部,再交给 IP 层封装成 IP 数据包发送到网络。接收端 IP 层剥掉 IP 头后,将 UDP 数据报交给对应端口的应用程序处理。

三、UDP数据报格式

UDP头部固定8字节,结构简洁。
![[Pasted image 20261107210756.png]]

字段长度含义
源端口16 bit发送方端口号(0~65535),标识本端应用
目的端口16 bit接收方端口号,标识目标应用
长度16 bitUDP 头部 + 数据的总字节数(最小 8)
校验和16 bit可选的差错检测,IPv4 下可为 0(表示不校验)

与 TCP 头部对比:TCP 头部至少 20 字节,还包含序列号、确认号、窗口、标志位等复杂字段;UDP 仅 8 字节,开销极低,适合对实时性敏感的场景。

四、UDP 的核心特性

4.1 无连接 (Connectionless)

UDP 通信前不需要三次握手,发送方知道目标 IP 和端口后即可直接 sendto(),接收方通过 bind() 绑定端口后 recvfrom() 即可收包。
没有连接建立、维护、释放的过程,延迟低,适合高频小包场景。

4.2 不可靠 (Unreliable)

UDP 不提供以下保证:

  • 数据一定送达
  • 数据按发送顺序到达
  • 数据不重复
  • 数据内容不出错(校验和可检测错误,但 UDP 本身不重传)
    丢包、乱序、重复均由应用层自行处理,或干脆接受丢失。
4.3 面向数据报 (Datagram-Oriented)

UDP 以数据报为最小传输单位。发送 100 字节,接收端收到的就是 100 字节的一个完整报文,不会出现 TCP 那种「粘包 / 半包」问题。
但这也意味着:单个 UDP 数据报不能超过底层 MTU 限制(以太网通常 1500 字节,扣除 IP 头 20 字节和 UDP 头 8 字节,有效载荷约 1472 字节)。更大的数据需在应用层分包。

4.4 支持一对一、一对多、多对一、多对多

UDP 天然支持:

  • 单播 (Unicast) :一对一
  • 广播 (Broadcast) :一对子网内所有主机
  • 组播 (Multicast) :一对订阅了特定组播地址的主机组
    TCP 只支持点对点连接,这是 UDP 在音视频、设备发现等场景的重要优势。
4.5 无流量控制与拥塞控制

UDP 发送速率完全由应用决定,不会因为对端缓冲区满而自动降速。这在实时场景是优点(不被阻塞),在网络拥塞时也可能加剧丢包。
![[Pasted image 20261107212752.png]]

五、UDP vs TCP 对比

特性UDPTCP
连接无连接面向连接(三次握手)
可靠性不保证送达保证送达、有序、不重复
头部开销8 字节20~60 字节
传输单位数据报(有边界)字节流(无边界,需处理粘包)
速度快,延迟低相对慢,有握手和确认开销
流量控制无有(滑动窗口)
拥塞控制无有
广播/组播支持不支持
适用场景实时、高频、可容忍丢包文件传输、网页、可靠消息

选型原则:

  • 数据 不能丢、必须完整 → TCP(或应用层在 UDP 上自建可靠机制)
  • 数据 丢了可以接受、但要快 → UDP
  • 需要 广播 / 组播发现设备 → UDP
  • 数据报 有明确边界、小包高频 → UDP

六、UDP 的典型应用场景

6.1 DNS 域名解析

DNS 查询通常使用 UDP 端口 53。一次查询、一次响应,数据量小,偶发丢包可超时重发,无需维持长连接。

6.2 DHCP 动态地址分配

DHCP 基于 UDP(客户端 68 端口,服务端 67 端口),利用 UDP 广播在局域网内发现 DHCP 服务器。

6.3 实时音视频 (RTP/RTSP)

视频通话、直播推流对延迟极度敏感。丢一两帧画面可以容忍,但等 TCP 重传造成的卡顿不可接受,因此 RTP 通常跑在 UDP 上。

6.4 物联网与工业控制
  • 设备发现:局域网广播 UDP 包,设备应答
  • 传感器高频上报:温湿度、GPS 定位等周期性数据,丢一两个点不影响整体趋势
  • Modbus UDP:Modbus 协议除 RTU/TCP 外还有 Modbus UDP 变体,用于以太网工控场景
  • OTA 固件分包传输:大文件分块经 UDP 发送,应用层做序号、ACK 和重传
6.5 嵌入式 / MCU 场景

资源受限的 MCU 上,UDP 栈比 TCP 栈占用更少 RAM 和 Flash,且无连接状态机,适合:

  • 心跳包上报
  • 远程命令下发(配合应用层 ACK)
  • 日志批量上报
6.6 游戏与 QUIC

传统在线游戏状态同步大量使用 UDP;更新的 QUIC 协议(HTTP/3 底层)也在 UDP 之上实现了可靠传输,兼顾低延迟与可靠性。


七、UDP 编程模型 (以 Berkeley Socket 为例)

7.1 服务端
int sock = socket(AF_INET, SOCK_DGRAM, 0);  // SOCK_DGRAM = UDP
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port   = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
while (1) {
    char buf[1024];
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    int n = recvfrom(sock, buf, sizeof(buf), 0,
                     (struct sockaddr *)&client, &len);  /* 等客户端发送数据,并获取客户端地址 */
    // 处理 buf[0..n-1]
    sendto(sock, reply, reply_len, 0,
           (struct sockaddr *)&client, len);
}
7.2 客户端
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port   = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &server.sin_addr);/* 解析服务端IP */
sendto(sock, data, data_len, 0,
       (struct sockaddr *)&server, sizeof(server));

关键 API:

函数作用
socket(AF_INET, SOCK_DGRAM, 0)创建 UDP 套接字
bind()绑定本地 IP/端口 (服务端必须,客户端可选)
sendto()发送数据报到指定地址
recvfrom()接收数据报,同时获取发送方地址
connect()可选,绑定默认目标地址,之后可用 send() / recv()

注意:UDP 的 connect() 不建立 TCP 式连接,只是让内核记住默认目标,并只接收来自该地址的包。

6. UDP Socket编程

UDP 服务端一般负责监听某个端口,等待客户端发送数据。
基本流程:
![[Pasted image 20261107213515.png]]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_PORT 8080
#define BUFFER_SIZE 1024

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t client_addr_len;
    char buffer[BUFFER_SIZE];

    // 1. 创建 UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket failed");
        return -1;
    }

    // 2. 设置服务端地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;               // IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY;       // 监听本机所有网卡
    server_addr.sin_port = htons(SERVER_PORT);      // 端口号,转为网络字节序

    // 3. 绑定 IP 和端口
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("bind failed");
        close(sockfd);
        return -1;
    }

    printf("UDP server started, listening on port %d...\n", SERVER_PORT);

    // 4. 循环接收客户端数据
    while (1)
    {
        memset(buffer, 0, BUFFER_SIZE);
        client_addr_len = sizeof(client_addr);

        int recv_len = recvfrom(sockfd,
                                buffer,
                                BUFFER_SIZE - 1,
                                0,
                                (struct sockaddr *)&client_addr,
                                &client_addr_len);

        if (recv_len < 0)
        {
            perror("recvfrom failed");
            continue;
        }

        buffer[recv_len] = '\0';

        printf("Received from %s:%d -> %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);

        // 5. 回复客户端
        const char *reply = "Hello, UDP client. Message received.";

        sendto(sockfd,
               reply,
               strlen(reply),
               0,
               (struct sockaddr *)&client_addr,
               client_addr_len);
    }

    // 6. 关闭 socket
    close(sockfd);

    return 0;
}

UDP 客户端一般主动向服务端发送数据。
基本流程:
![[Pasted image 20261107213603.png]]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024

int main(void)
{
    int sockfd;
    struct sockaddr_in server_addr;
    socklen_t server_addr_len;
    char buffer[BUFFER_SIZE];

    // 1. 创建 UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket failed");
        return -1;
    }

    // 2. 设置服务端地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);

    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0)
    {
        perror("inet_pton failed");
        close(sockfd);
        return -1;
    }

    server_addr_len = sizeof(server_addr);

    // 3. 发送数据到服务端
    const char *message = "Hello, UDP server.";

    int send_len = sendto(sockfd,
                          message,
                          strlen(message),
                          0,
                          (struct sockaddr *)&server_addr,
                          server_addr_len);

    if (send_len < 0)
    {
        perror("sendto failed");
        close(sockfd);
        return -1;
    }

    printf("Message sent to server: %s\n", message);

    // 4. 接收服务端回复
    memset(buffer, 0, BUFFER_SIZE);

    int recv_len = recvfrom(sockfd,
                            buffer,
                            BUFFER_SIZE - 1,
                            0,
                            (struct sockaddr *)&server_addr,
                            &server_addr_len);

    if (recv_len < 0)
    {
        perror("recvfrom failed");
        close(sockfd);
        return -1;
    }

    buffer[recv_len] = '\0';

    printf("Received from server: %s\n", buffer);

    // 5. 关闭 socket
    close(sockfd);

    return 0;
}

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/NBlby/article/details/170246982

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--