linux

官方信息技术老师·33 页·深入(追求细节与边界)·0 次浏览·2 天前
Linux网络编程套接字TCP/UDP

Linux Socket 编程

看清 socket 调用内核路径、TCP 状态迁移条件、epoll 触发差异

按 空格/→ 演示下一步

1 / 33 页

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

Linux网络编程套接字TCP/UDP

Linux Socket 编程

看清 socket 调用内核路径、TCP 状态迁移条件、epoll 触发差异

1第 1 页 · Linux Socket 编程

什么是 Socket

上一节我们知道要写程序让两台机器对话。这一节解决第一个问题:进程之间怎么互相找到对方?答案就是网络上的一个「端点」——Socket。

Socket 本质
IP 地址 + 端口号 + 协议,三元组定位网络一端
内核抽象
应用层只拿到文件描述符,协议栈细节藏在内核
协议族可换
同一套 API 可承载 TCP、UDP、Unix 域等
全双工通道
连接建立后两端可同时收发,方向相互独立
墙上电话插座对应 →Socket

插座定位房间,Socket 定位进程;插上话机才能通话

2第 2 页 · 什么是 Socket

Socket 通信模型

自上而下五层:用户进程、Socket 接口、内核协议栈、TCP/IP 网络、对端镜像。数据沿箭头方向穿越。

图解渲染中…
C2应用视角的接口,本质是一个文件描述符C3内核实现 TCP/UDP 状态机与缓冲区
3第 3 页 · Socket 通信模型

UDP 是什么

上一页我们讲了 Socket 通信模型,要真正把字节从一头送到另一头还得选协议。UDP 就是其中一种——它根本不握手,发完就走,不问对方收没收到。

无连接
不握手、不维护状态,发完即走
尽力交付
不保证到达、顺序、不重复
面向报文
保留消息边界,sendto 对应一次 recvfrom
轻量开销
首部固定 8 字节,无状态机、无重传、无拥塞
扔进邮筒的明信片对应 →UDP 数据报

没回执、没签收、丢了不补——UDP 也不确认、不重传、不排序

4第 4 页 · UDP 是什么

UDP vs TCP 核心区别

同为传输层协议,但「建连接」「保可靠」「管流量」的哲学截然相反——混用会埋坑。

TCP
  • 三次握手建连接,四次挥手断开——先 accept() 再通信
  • 可靠:超时重传、序号去重、ACK 确认一应俱全
  • 字节流无边界,read() 一次可能横跨多次 send
  • 滑动窗口限速 + 拥塞算法(CUBIC/BBR)自适应网络
UDP
  • 无连接,sendto() 直接发,不握手不挥手
  • 不可靠:丢包、乱序、重复全交应用层处理
  • 保留消息边界,一次 recvfrom() 对应一次 sendto()
  • 无流量、无拥塞控制,发多快由应用自己决定
可靠传输、带宽公平共享选 TCP;可容忍丢包、低延迟敏感(DNS、实时音视频、游戏)选 UDP。
5第 5 页 · UDP vs TCP 核心区别

UDP 通信时序图

UDP 无连接:两端各自建 socket,sendto 直发、recvfrom 直收,沿箭头看数据走向。

图解渲染中…
CA / SA用户态进程,调 sendto 或 recvfromCK / SK内核 UDP 层,只管收发,不维护连接CK->>SK单箭头直达:无 SYN、无 ACK、无重传数据+源地址recvfrom 一次返回数据与对端 IP:端口
6第 6 页 · UDP 通信时序图

UDP 服务端代码

c

用最简回显服务,把 socket→bind→recvfrom→sendto→close 一行一行跑通。

代码高亮加载中…

高亮行正是服务端 5 个核心系统调用:建端点、绑端口、收数据、回数据、关连接。

7第 7 页 · UDP 服务端代码

UDP 客户端代码

c

四步走完 UDP 客户端:socket→sendto→recvfrom→close

代码高亮加载中…

socket 建通道、sendto 主动发、recvfrom 阻塞收、close 释放,覆盖 UDP 客户端完整生命周期。

8第 8 页 · UDP 客户端代码

UDP 代码执行流程

一次 UDP 交互中,服务端与客户端的关键系统调用按什么顺序触发。

1
服务端创建Socket
socket() 返回文件描述符,内核分配 UDP 控制块,但尚未绑定地址
2
服务端绑定端口
bind() 把 fd 与本地 IP:端口绑定,UDP 层由此决定本机收哪些包
3
服务端阻塞等待
recvfrom() 阻塞等待;UDP 没有 listen/accept,这是与 TCP 的关键差异
4
客户端直接发送
客户端 socket() 后无需 connect,直接 sendto 发出数据报
5
服务端收到请求
recvfrom 返回数据内容,并从 IP/UDP 头里读出客户端地址供回包用
6
反向回包交互
服务端用拿到的地址 sendto 回包,客户端对应 recvfrom 接收,完成一次往返
9第 9 页 · UDP 代码执行流程

关键 API 详解

上页代码跑起来了,但 sendto 那一长串参数是否还有点恍惚?fd、buf、len、flags、addr、alen——每个位置到底在问什么?出错时返回值又该怎么看?这页把它们逐个拆开。

sendto 六参数位
fd=套接字;buf=数据指针;len=有效字节数;flags=选项;addr=对端地址;alen=地址长度
flags 不只 0
可传 MSG_DONTWAIT 单次非阻塞;MSG_NOSIGNAL 仅 TCP;UDP 多数场景置 0
sendto 返回与 errno
成功=已发字节数;可能短写;-1 必看 errno(EAGAIN/EINTR)
addrlen 值-结果参数
传入时表示 addr 缓冲容量;返回时被内核改写为实际地址长度
recvfrom 返回与 MSG
>0 是字节数;=0 UDP 罕见(对端 close 是 TCP 特征);MSG_PEEK 可窥看不取走
寄快递填运单对应 →sendto 六个参数

fd=快递员、buf=包裹、len=重量、flags=加急、addr=收件地址、alen=地址栏长度

10第 10 页 · 关键 API 详解

UDP 调试基础

代码写完了,怎么快速验证它能收发?最轻量的工具是 netcat——一行命令起服务端、一行命令发数据,比再写个测试程序省事得多。

nc -u -l -p
起一个 UDP 服务端在指定端口监听,键盘输入即为收到的数据
nc -u IP 端口
作为 UDP 客户端往目标发数据,Ctrl+D 结束 stdin
UDP 无 FIN
nc 默认一直监听,没有断开信号;要主动 Ctrl+C 或加超时
-w 超时
客户端发完 N 秒无新数据就退出,避免 recvfrom 一直阻塞
strace 抓调用
调试疑难时看 sendto/recvfrom 的实际地址、长度与返回值
对讲机对应 →UDP + netcat

对讲机按下就发、收到就响,没有'建立通话'步骤;nc -u 一样,不握手直接收发

11第 11 页 · UDP 调试基础

Wireshark 抓包分析

bash

用 tshark 命令行工具抓取我们 UDP 服务(端口 9999)的流量,按端口/IP/流过滤。

代码高亮加载中…

tshark 有两层过滤:抓包用 BPF 语法 (-f) 减少磁盘写入;回放用 Wireshark 显示语法 (-Y) 精确定位。follow,udp 可把无连接的 UDP 包重组为流视图。

12第 12 页 · Wireshark 抓包分析

UDP 常见错误与解决

Connection refused/Port unreachable 处理

UDP 常见错误与解决
Connection refused/Port unreachable 处理
13第 13 页 · UDP 常见错误与解决

TCP 是什么

上页说 UDP 像扔信封——发出去就不管了。TCP 不一样:发之前要先「握手」,收到后要「签收」,按序号拼回去,少一封就重发。

面向连接
通信前要像打电话那样先握手建立连接
可靠传输
每段数据都要收到对方的 ACK 确认
有序到达
报文带序号,接收端按序重组
字节流
对应用层是连续字节,没有消息边界
挂号信对应 →TCP 传输

每封都要签收回执、丢了重寄、序号乱了重排

14第 14 页 · TCP 是什么

TCP 三次握手

SYN→SYN+ACK→ACK 的完整时序

TCP 三次握手
SYN→SYN+ACK→ACK 的完整时序
15第 15 页 · TCP 三次握手

TCP 四次挥手

读法:从左到右看消息流动,客户端先发 FIN,服务端分开发 ACK 和 FIN,最后客户端 ACK。

图解渲染中…
FIN seq=u客户端主动关闭,无更多数据ACK ack=u+1服务端确认收到,但可能还在发FIN seq=v服务端数据发完,也准备关闭ACK ack=v+1客户端最后确认,进入 TIME_WAIT
16第 16 页 · TCP 四次挥手

TCP 状态转换图

前两页我们跟完了三次握手、四次挥手——那只走了状态图上的一条主线。完整的连接生命周期里,TCP 一共要穿过 11 个状态,每个状态都对应着一段特定的协议行为。

三类阵营
服务端独有、客户端独有、双方共有——先分清谁在哪个区
服务端主线
LISTEN→SYN_RCVD→ESTABLISHED→CLOSE_WAIT→LAST_ACK
客户端主线
SYN_SENT→ESTABLISHED→FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT
CLOSING 异常态
两端同时发 FIN 时交汇,只在同时关闭场景出现
2MSL 等待
主动关闭方最后停留 2MSL,防旧报文段鬼影干扰下一连接
地铁线路图对应 →TCP 状态转换图

每个站点是一个状态,列车到站(收到报文)就换乘下一站;客户端与服务端是两条独立的线路

17第 17 页 · TCP 状态转换图

TCP 服务端代码

c

最小可运行的 TCP 回显服务端,覆盖 socket→bind→listen→accept→read/write→close 全流程。

代码高亮加载中…

六行对应 TCP 服务端六个阶段:SOCK_STREAM 锁定 TCP 协议;listen 把主动 socket 转为被动;accept 阻塞等三次握手完成;cfd 是新连接的专用描述符。

18第 18 页 · TCP 服务端代码

TCP 客户端代码

c

一个最小可运行的 TCP 客户端,跑通 socket→connect→read/write→close 五步生命周期。

代码高亮加载中…

connect() 是 TCP 客户端特有的入口,三次握手在此完成;read/write 走全双工字节流;close 触发四次挥手。对比 UDP 客户端,多出的就是 connect 这一步。

19第 19 页 · TCP 客户端代码

TCP 连接建立流程

三次握手看似协议层动作,实际由 socket API 在内核里串起来。

1
connect() 发 SYN
客户端调 connect(),内核代发首个 SYN 包
2
内核回 SYN+ACK
listen 队列收下 SYN,服务端内核自动回 SYN+ACK
3
内核回 ACK
客户端内核收到 SYN+ACK,自动回 ACK
4
accept() 唤醒
三次握手完成,accept() 从阻塞中返回新 fd
20第 20 页 · TCP 连接建立流程

listen 与 accept 详解

TCP 服务端 socket() 拿到的是主动套接字,要接客户端必须先 listen() 转成被动监听,再由 accept() 从内核队列里把连接取出来——这是服务端并发的根。

listen()
把主动套接字转为被动监听状态,启动内核握手处理
backlog 参数
提示内核未 accept 的连接最多排队多少(Linux 2.2 后语义变了)
半连接队列
SYN 已收、握手未完成,SYN flood 也打这里
全连接队列
三次握手已完成、静静等 accept() 取走的连接池
accept()
从全连接队列取一个连接,生成专属通信 fd 返回
酒店前台叫号对应 →listen/accept 与双队列

listen=前台开工挂牌限排队;半连接=手续没办完的住客;accept=叫号发专属房卡

21第 21 页 · listen 与 accept 详解

connect 阻塞与超时

connect 的行为与错误处理

connect 阻塞与超时
connect 的行为与错误处理
22第 22 页 · connect 阻塞与超时

多客户端并发处理

fork/线程池实现并发服务器

多客户端并发处理
fork/线程池实现并发服务器
23第 23 页 · 多客户端并发处理

TCP 调试基础工具

TCP 连接建立后,怎么证明它真的「活着」?握手的数据落在哪个 socket 里?现在该请出调试工具——netstat 和 ss,把内核里的连接表「投影」到屏幕上。

netstat
遍历 /proc/net/tcp 读取连接表,老牌工具几乎所有发行版都自带
ss 命令
通过 netlink 直接与内核通信,速度更快、信息更全,是 netstat 的现代替代
四大状态
LISTEN / ESTABLISHED / TIME_WAIT / CLOSE_WAIT 反映连接生命周期的各阶段
实战过滤
ss -tn state established 看活跃连接;ss -lnt 看监听端口
酒店前台登记表对应 →内核 TCP 连接表

LISTEN=前台待客、ESTABLISHED=客人已入住、TIME_WAIT=客人刚走正在结账

24第 24 页 · TCP 调试基础工具

Wireshark TCP 抓包分析

text

真实抓包输出:从 SYN 到数据传输逐包追踪三次握手过程

代码高亮加载中…

对比 Seq/Ack 递增规律:每次 ACK 等于对方 Seq+Len,三次握手后 Seq 从 0 起步,数据包每段递增 75。

25第 25 页 · Wireshark TCP 抓包分析

TCP 连接异常排查

上一节我们用 Wireshark 抓到了完整握手。但线上常常拿不到握手——连接直接失败,错误码有 refused、timeout、reset 三种,背后原因和排查路径完全不同。

Refused
目标端口无进程监听,内核直接回 RST。客户端立即拿到 ECONNREFUSED
Timeout
SYN 发出去后再无任何回应,多半被防火墙 drop 或路由不可达
Reset
连接中收到对端 RST,常见于服务端进程崩溃或 SO_LINGER 强关
排查顺序
先 ss 查监听排除 refused,再抓包看是否收到 RST 定位 reset,最后追网络查 timeout
敲门找人的三种结局对应 →三种 TCP 连接异常

refused=屋里没人(无监听);timeout=敲门没声音(被丢包);reset=开门被赶走(对端主动 RST)

26第 26 页 · TCP 连接异常排查

TCP 半连接与全连接队列

上一页讲到 listen() 创建监听 socket、accept() 取出连接。但三次握手发生在内核里,应用感知不到。内核为此维护着两个隐藏队列——握手就在它们之间流转。

半连接队列
收到 SYN 后、还没收到第三次 ACK 的连接,状态属于 SYN_RCVD
全连接队列
三次握手已完成、等待应用调用 accept() 取走的连接,状态属于 ESTABLISHED
队列流转
SYN 进入半连接;收到第 3 次 ACK 移入全连接;应用 accept() 真正交付
容量与溢出
上限涉及 backlog、somaxconn、tcp_max_syn_backlog;溢出丢 ACK
餐厅等位与点餐对应 →两个连接队列

取号区=半连接;已入座=全连接;服务员过来=accept()

27第 27 页 · TCP 半连接与全连接队列

Socket 选项详解

SO_REUSEADDR/SO_KEEPALIVE 等常用选项

Socket 选项详解
SO_REUSEADDR/SO_KEEPALIVE 等常用选项
28第 28 页 · Socket 选项详解

阻塞 vs 非阻塞 I/O

前面 TCP/UDP 代码里,recv() 没数据就卡住等——这是阻塞 I/O 的默认行为。想让进程「不等」或「边等边干别的」,得切到非阻塞模式。

阻塞 I/O
未就绪时进程休眠,内核在数据到达后唤醒并返回
非阻塞 I/O
调用立即返回;未就绪时返回 -1 并设 errno=EAGAIN
切换方式
fcntl(fd, F_SETFL, O_NONBLOCK) 动态切换两种模式
返回值约定
>0 字节数;==0 对端关闭;<0 看 errno 区分错误
正交于同步异步
阻塞/非阻塞描述调用行为,同步/异步描述通知机制,两套维度
餐厅取号等位对应 →阻塞 vs 非阻塞 I/O

阻塞=站在取餐口不走;非阻塞=拿号坐回去,被叫号时再取

29第 29 页 · 阻塞 vs 非阻塞 I/O

Socket 错误处理

上一节区分了阻塞/非阻塞模式,但 recv/send 不管哪种模式都可能因信号或网络异常返回 -1。errno 就是系统告诉你「为什么失败」的唯一线索。

EINTR
慢系统调用被信号处理器打断;可能数据未到即返回 -1;可设 SA_RESTART 自动重试,或手动 while 循环
EAGAIN
非阻塞 I/O 下缓冲空(读)或满(写)时设置;EAGAIN 与 EWOULDBLOCK 在 Linux 上值相同
EPIPE
对端关闭后本端再写;默认先触发 SIGPIPE 杀进程,write 才返回 -1;须 MSG_NOSIGNAL 或 signal 屏蔽
ECONNRESET
对端发 RST 而非 FIN;本端 read 返回 -1、errno 置此值;区别于 FIN 关闭时 read 正常返回 0
餐厅服务员送餐对应 →Socket 错误码

EINTR=厨师接电话回来继续;EAGAIN=菜没好请等;EPIPE=客人走还上菜;RST=客人撤单走人

30第 30 页 · Socket 错误处理

Socket 核心知识自测

点击作答

对 UDP socket 调用 connect() 后,下列行为描述正确的是?

31第 31 页 · Socket 核心知识自测

Socket 编程要点总结

  • TCP 暴露无消息边界的字节流,UDP 保留独立数据报边界。
  • TCP 以连接、确认、排序、流量与拥塞控制换取可靠交付。
  • UDP 默认不保证送达、顺序、时限,也不承诺只收一次。
  • 完整性优先选 TCP;可容忍丢包且追求低时延选 UDP。
  • 需要可靠 UDP,应补齐重传、排序、拥塞控制和应用层校验。
延伸主题:QUIC 与可靠 UDPepoll 高并发网络模型超时、重传与协议调优
32第 32 页 · Socket 编程要点总结

课后思考

先合上屏幕自己想一分钟,再点开参考答案对思路。三道题分别对应回顾、应用、边界三个层次,看看你最擅长哪一层。

1为什么 TCP 建立连接需要三次握手,而 UDP 不需要?两次握手会带来什么问题?

参考答案TCP 需保证双方收发能力正常且序号同步,两次握手只能确认一方的发送和对端的接收,无法防止已失效的旧请求突然重传到服务端造成错连。UDP 无连接概念,自然不需要。

2如果让你设计一个视频会议系统,你会选 UDP 还是 TCP?为什么?UDP 缺失的可靠传输你打算怎么补?

参考答案选 UDP——视频会议容忍少量丢包但不能容忍延迟。需要自己处理:丢包重传(仅限关键帧)、乱序重排、抖动缓冲、流量控制。TCP 的重传和拥塞控制反而会放大延迟。

3服务端 listen 后如果长时间不调用 accept,已完成三次握手的连接会去哪?最终会怎样?

参考答案完成握手的连接进入内核的全连接队列(accept 队列)。队列满后,新连接请求被丢弃或返回 RST。半连接队列同理有 backlog 限制。客户端 connect 会超时或收到 RST。

33第 33 页 · 课后思考