windows

官方信息技术老师·27 页·深入(追求细节与边界)·0 次浏览·2 天前
Winsock网络编程协议栈异步IO

UDP Socket编程

搞清socket()到IOCP每层API背后的真实行为与边界

按 空格/→ 演示下一步

1 / 27 页

全部页面点击任意一页,跳回舞台从这页播放

Winsock网络编程协议栈异步IO

UDP Socket编程

搞清socket()到IOCP每层API背后的真实行为与边界

1第 1 页 · UDP Socket编程

UDP协议与Socket的关系

上一页你用 socket(AF_INET, SOCK_DGRAM, ...) 收发数据报时,有没有想过:Socket 到底是什么?它和 UDP 协议是同一个东西吗?

Socket 是接口
操作系统提供的编程抽象,对上暴露 API、对下封装 UDP 协议细节
Socket 即端点
一个 Socket 绑定本地 (IP, Port),是数据收发的本地端点
Socket ≠ UDP
Socket 是「门」,UDP 是「路」;多个 Socket 可共用同一 UDP 协议
无连接调用
sendto/recvfrom 直接映射 UDP 的无连接数据报语义
邮筒与邮政系统对应 →Socket 与 UDP

投信到邮筒(sendto),邮政(UDP)送达;对端从邮筒取信(recvfrom)。邮筒是接口

2第 2 页 · UDP协议与Socket的关系

UDP无连接通信模型

从左到右是一条数据报的完整生命周期:客户端 sendto → 无连接直接发送 → 网络独立传输 → 服务端 recvfrom。

图解渲染中…
A3内核不维护连接状态,无握手、无确认B1每个数据报独立加 UDP 头,含目的端口D1接收端无需事先打开连接,匹配端口即派发E1可同时获得数据内容与发送方地址
3第 3 页 · UDP无连接通信模型

UDP Socket地址结构

上一页说 UDP 发数据要指定「发给谁、哪个端口」。落到 C 代码,这个「谁」不是一个字符串,而是一个结构体。它叫什么、每个字段又存什么?这页拆开看。

sin_family
地址族。AF_INET 表示 IPv4,决定后续字段按什么「语法」解读
sin_port
端口号,2 字节无符号整型,存的是网络字节序(大端)
sin_addr
IP 地址。IPv4 下为 4 字节结构,同样要转网络字节序
sin_zero
8 字节填充,让 sockaddr_in 与通用 sockaddr 等长
网络字节序
用 htons/htonl 把主机序转大端,保证不同 CPU 间通信正确
国际快递信封对应 →sockaddr_in 结构体

family=走哪个国家邮政,port=门牌号,addr=街道地址,zero=信封统一尺寸的填料

sin_family(2)+sin_port(2)+sin_addr(4)+sin_zero(8)=16B\sin\_family(2)+\sin\_port(2)+\sin\_addr(4)+\sin\_zero(8)=16B
4第 4 页 · UDP Socket地址结构

UDP客户端代码框架

python

用 Python 演示 UDP 客户端四步:建 socket、发数据、收响应、关 socket。

代码高亮加载中…

SOCK_DGRAM 标识 UDP;sendto 因无连接必须带目标地址;recvfrom 同时返回数据与来源;close 释放 socket。

5第 5 页 · UDP客户端代码框架

UDP服务器端代码框架

c

一段真实可跑的 Windows Winsock UDP 服务器,串起 socket→bind→recvfrom→sendto→close 整条主流程。

代码高亮加载中…

对照流程看:socket 拿句柄→bind 占端口→recvfrom 同时取数据和对方地址→sendto 用同一地址回写→close 收尾,这正是无连接 UDP 服务器的最小骨架。

6第 6 页 · UDP服务器端代码框架

关键函数参数详解

前面我们已经搭好了客户端和服务器端的代码骨架,但骨架只告诉你函数怎么摆位——真正决定程序健壮性的,是 sendto 和 recvfrom 每个参数的含义,以及返回值告诉你的事。

sendto 六参数
socket、缓冲区buf、长度len、标志flags通常为0、目标地址to、地址结构长度tolen
recvfrom 六参数
前四个与sendto对称;from记录数据来源地址,fromlen既是输入也是输出
fromlen 双重身份
调用前必须初始化为sizeof(sockaddr_in),返回时被回写为实际地址长度
返回值 = 字节数
成功返回字节数;失败SOCKET_ERROR;sendto可能少于请求;recvfrom=0为空报文
截断与错误码
缓冲区小于数据报时recvfrom截断丢弃多余字节;失败后用WSAGetLastError查具体原因
填快递单对应 →sendto参数填写

快递单的发件人、收件人、重量、物品都对应参数,地址写错或漏填长度就寄不到

7第 7 页 · 关键函数参数详解

UDP程序编译运行

从代码到运行:gcc 编译 UDP 程序并本地验证通信。

1
编写源文件
分别创建 server.c 和 client.c 两个源文件
2
gcc 编译
gcc -o server server.c 编译为可执行文件
3
启动服务端
先运行 server 绑定端口,进入接收状态
4
启动客户端
再运行 client 指定服务端 IP 和端口发送
5
观察输出
通过双方 printf 信息确认数据来回正确
8第 8 页 · UDP程序编译运行

UDP常见错误与对策

上一节你们跑通了 UDP 程序,但很可能第一次 bind 就报错,或者客户端发完消息什么回执也没收到——这些都不是逻辑写错,而是 UDP 编程里几个典型的'坑'。

EADDRINUSE
bind()失败,端口已被占用(含TIME_WAIT残留)
EACCES
bind特权端口(<1024)需root权限
EMSGSIZE
sendto包超过UDP载荷上限
ICMP端口不可达
UDP版'连接拒绝',Linux只报告一次
防御对策
SO_REUSEADDR/SO_REUSEPORT + 完整errno检查
打不通的电话对应 →UDP对关闭端口的sendto

首次听到'号码不存在',之后运营商只默默挂断

UDP max payload=65535208=65507 bytes\text{UDP max payload} = 65535 - 20 - 8 = 65507\text{ bytes}
9第 9 页 · UDP常见错误与对策

UDP调试工具使用

UDP问题分两层排查:先netstat看本地socket状态,再抓包看网络层真实流量,从本地逻辑走到物理抓包逐步缩小范围。

图解渲染中…
a2netstat -ano 看UDP端口是否被监听a5Wireshark/tcpdump 抓取真实UDP包a7防火墙常静默丢弃UDP入站包a8注意htonl/ntohl和字符编码
10第 10 页 · UDP调试工具使用

TCP协议与Socket的关系

UDP那页讲过——发出去就不管。TCP反过来:发出去必须确认收到、丢了重发、按序到达。这么复杂的可靠性机制,是怎么让几行代码就能享受到的?靠的就是Socket。

TCP面向连接
通信前必须三次握手建立连接,结束时要四次挥手断开
Socket是抽象层
操作系统把TCP的握手、重传、排序、流控封装为统一的fd操作
应用只见字节流
send()/recv()调用时,TCP在背后兜底可靠性,应用无需操心
全双工字节管道
Socket把TCP视为一条双向管道,两端可同时独立读写
带签收的快递服务对应 →TCP Socket的端到端传输

你只管寄件和签收;中途丢件、错路、重发都由快递系统兜底

11第 11 页 · TCP协议与Socket的关系

TCP面向连接通信模型

TCP通信分三阶段:三次握手建连、数据传输、四次挥手释放,自上而下按时序推进。

图解渲染中…
C客户端,主动发起SYN与FINS服务器,被动响应连接与关闭->>实线箭头:主动发送的报文-->>虚线箭头:应答方向的报文
12第 12 页 · TCP面向连接通信模型

TCP三次握手原理

UDP像寄平信,写完地址就扔出去,不管对方在不在。TCP不一样——它必须先确认双方「收发都通畅」才会传数据,这套协商机制就是三次握手。

第一次 SYN
客户端发 SYN 给服务器,附上自己的初始序号,进入 SYN_SENT 状态等待
第二次 SYN+ACK
服务器把 SYN 与 ACK 合并回送,确认收到客户端序号,并给出自己的初始序号
第三次 ACK
客户端再发 ACK 确认,双方序号各自对齐,连接进入 ESTABLISHED
为何要三次
两次只能确认单向通路,三次才能保证双方收发都通,还能识别并丢弃失效的旧请求包
打电话互报家门对应 →三次握手

一方问「能听到吗?我是A」;另一方答「能,我是B」;一方再说「收到,开始聊」

13第 13 页 · TCP三次握手原理

TCP四次挥手原理

三次握手建立连接是双向确认,那关闭连接也得双向确认——两边各自独立关掉发送方向才算结束。所以挥手需要四次信号,而不是三次。

四次挥手步骤
A发FIN→B回ACK→B发FIN→A回ACK
TIME_WAIT 状态
主动关闭方发完最后ACK后进入的等待状态
2MSL 等待时长
Linux默认MSL=30秒,TIME_WAIT持续约60秒
双重设计目的
保证最后ACK可达 + 清除网络中残留的旧报文
SO_REUSEADDR
socket选项,允许立即复用处于TIME_WAIT的端口
挂电话双向道别对应 →TCP四次挥手

一方说『不说了』,对方回『好』;等对方也说完再回『再见』,才彻底挂断

14第 14 页 · TCP四次挥手原理

TCP Socket地址结构

三次握手解决的是「连接怎么建」,但客户端得先知道连哪台机器、哪个端口,服务器也得指明自己挂在哪个地址上。把这些信息封装起来的,就是 sockaddr_in。

sin_family
地址族字段,AF_INET 表示走 IPv4 协议栈
sin_port
端口号,必须用 htons() 转网络字节序再赋值
sin_addr
IP 地址结构,内部 s_addr 成员存 32 位地址
初始化四步曲
memset 清零 → 设 family → htons(port) → 设 addr
通用 sockaddr
系统 API 收 sockaddr*,调用时把 sockaddr_in* 强转过去
快递信封对应 →sockaddr_in

family=国家编码、port=门牌号、addr=街道地址,都得填对才能送达

addr.sin_port=htons(PORT)\text{addr.sin\_port}=\text{htons}(\text{PORT})
15第 15 页 · TCP Socket地址结构

TCP客户端代码框架

c

从初始化到关闭,TCP客户端的六个标准步骤一目了然。

代码高亮加载中…

WSAStartup→socket→connect→send/recv→closesocket,对应TCP面向连接的完整生命周期。

16第 16 页 · TCP客户端代码框架

TCP服务器端代码框架

c

一段最小可跑的C代码,串起socket→bind→listen→accept→recv/send→close六步流程。

代码高亮加载中…

六行高亮严格对应大纲六步流程。进阶要点:socket()返回的是监听fd,accept()返回的是连接fd;二者各有独立生命周期,必须分别close,漏一个就是经典资源泄漏。

17第 17 页 · TCP服务器端代码框架

listen与accept的机制

前台接待最清楚这过程:酒店开门营业(listen),门口摆几张等候椅(backlog),客人到了就由前台带一位进房间(accept),前台转身继续迎接下一位。

listen()双重作用
把socket从CLOSED转成LISTENING状态,并在内核建一条等待accept的连接队列
backlog是队列上限
已完成三次握手但还没被accept取走的连接数;超过就丢弃或重传SYN(OS有差异)
三次握手在底层完成
客户端connect发SYN时,内核自动回SYN+ACK并等ACK;握手成功后连接才入队
accept()默认阻塞
队列空时调用线程挂起;有连接就出队一个,并返回新的已连接socket
原监听socket不受影响
accept返回的新socket只服务那一个客户端;原socket继续监听后续连接
酒店前台对应 →listen+accept

listen=开门+摆等候椅,accept=带一位进房间,原前台继续接下一位

18第 18 页 · listen与accept的机制

TCP字节序转换

上页代码里,sin_port 和 sin_addr.s_addr 都用了 htons、htonl。端口和 IP 是整数,为什么不能直接赋值?这两个函数做了什么?

字节序分歧
0x12345678 在小端机存为 78 56 34 12,大端机存为 12 34 56 78
网络字节序
TCP/IP 协议统一规定为大端,不论发送方 CPU 是什么
主机字节序
本机 CPU 的原生顺序;Windows 上 x86/x64 都是小端
四个转换函数
htons/ntohs 处理端口(16位),htonl/ntohl 处理 IP(32位)
使用场景
端口用 htons,IP 用 htonl 或 inet_addr 直接转换
寄快递的装箱方式对应 →字节序转换

寄件人按自己习惯装箱,快递公司规定统一标准打包,收件人再按自己习惯拆开

19第 19 页 · TCP字节序转换

TCP程序编译运行

多客户端场景下,TCP程序的编译与测试需要按顺序配合:服务端先就位,再让多个客户端连上去验证并发。

1
编译两端程序
分别编译server和client,生成可执行文件
2
启动服务端监听
服务端先跑起来,进入listen状态等待连接
3
并发启动多个客户端
用脚本或手动开启多个客户端同时连服务器
4
观察服务端并发处理
验证服务端能否同时正确处理多个客户端请求
5
用工具查看连接状态
用netstat/ss查看ESTABLISHED状态的连接数
6
关闭并清理资源
客户端逐个断开,服务端关闭监听并优雅退出
20第 20 页 · TCP程序编译运行

TCP常见错误与对策

程序终于跑通——可这只是开始。真实网络里 connect、bind、recv 时不时崩出一行让人查半天的错误。这一页把高频错误码和它们的来历摆到桌面。

ECONNREFUSED
目标端口无进程监听;内核立即回 RST,秒级失败
EADDRINUSE
bind 时端口被占;常因 TIME_WAIT 残留或他进程占用
ETIMEDOUT
SYN 重传耗尽;默认约 75 秒;防火墙或路由黑洞
ECONNRESET
对端发 RST 撕毁连接;常见于进程崩溃或主动关闭
EPIPE
向已关闭对端写数据;触发 SIGPIPE,默认杀进程
各种吃闭门羹对应 →TCP 错误家族

没人应门、被赶走、敲太久没人回——对应不同错误码的不同故事

21第 21 页 · TCP常见错误与对策

TCP调试工具使用

从抓包到分析,五步走完TCP流量调试。箭头即数据流向,方框即工具动作。

图解渲染中…
a2BPF语法过滤只保留TCP段a3tcpdump -w 输出pcap格式文件a4Wireshark图形化解析pcap文件a5/a6/a7对应TCP连接生命周期三阶段
22第 22 页 · TCP调试工具使用

UDP vs TCP核心特性对比

UDP和TCP常被混用,但本质是两种相反的传输哲学——速度vs可靠。

UDP 用户数据报
  • 无连接:发包前不需要握手
  • 不可靠:不保证送达、顺序、去重
  • 头部8字节,开销极小
  • 适用实时:丢一帧不致命
TCP 传输控制
  • 面向连接:必须三次握手
  • 可靠传输:重传、排序、去重
  • 头部≥20字节,控制字段多
  • 适用准确:错一字节就出错
可靠传输选TCP(文件/HTTP),实时传输选UDP(直播/DNS)
23第 23 页 · UDP vs TCP核心特性对比

协议选择决策树

从起点向下按四道判断条件走,Yes 分支汇向 TCP,No 继续下沉直至落到 UDP。

图解渲染中…
b1可靠传输=不能丢包、不能出错,TCP 通过重传与 ACK 保证b3低延迟场景如视频会议、游戏,UDP 握手开销更小b4广播/组播只有 UDP 支持,TCP 仅做点对点b5MTU 通常 1500 字节,超出后 UDP 易被分片丢包,TCP 字节流更稳
24第 24 页 · 协议选择决策树

UDP与TCP核心理解

点击作答

TCP 客户端连续两次 send() 各发 100 字节,服务端两次 recv() 接收,最可能发生什么?

25第 25 页 · UDP与TCP核心理解

Socket编程知识总结

  • UDP 的"简陋"是设计取舍,应用层补足可靠性
  • TCP 是字节流不是消息流,必须自己分包
  • listen 建队列,accept 才完成握手
  • 网络字节序大端,跨平台必须显式转换
  • 协议选择是实时性、可靠性与复杂度的权衡
延伸主题:I/O 多路复用:select 与 epoll非阻塞 socket 与异步 IO 模型应用层协议:分包、粘包、心跳
26第 26 页 · Socket编程知识总结

课后思考

先自己想 30 秒,再点开参考答案——这是思考路径,不是标准答案。

1为什么 UDP 服务器不需要 listen 和 accept,而 TCP 必须有这两个调用?

参考答案UDP 是无连接的——每次 sendto 都是一次独立投递,不需要"挂牌迎客"。TCP 必须先建立专属通道(三次握手 + accept),才能在这条通道上可靠收发。

2实时直播宁可丢几个包也不能卡顿,为什么这种场景 UDP 比 TCP 更合适?TCP 的重传机制反而帮了倒忙吗?

参考答案TCP 的重传保证"字节不丢",但代价是延迟累积:一个包丢就阻塞后续。UDP 把丢包和乱序交给你处理,对实时画面而言,丢一帧远好过卡三秒等重传。

3如果 TCP 三次握手中服务器发的 SYN+ACK 丢失了,客户端会一直等下去吗?操作系统是怎么处理的?

参考答案不会。客户端发 SYN 后启动重传计时器(约 1 秒),收不到第二个 SYN+ACK 就重发 SYN,重试若干次后放弃报错。SYN flood 攻击正是利用这一点耗尽服务器资源。

27第 27 页 · 课后思考