
























线程编号(如 Thread 2、Thread 3 )和线程的 LWP(Light - Weight Process,轻量级进程,类似系统给线程分配的 ID )编号 87747、87748 并无严格对应顺序。前者可能是 gdb 等调试工具按某种内部规则(比如创建顺序等 )编排,后者是系统层面分配的标识符,两者来源和用途不同,所以会出现看似 “倒着” 的情况
避免死锁问题的发生
四个必要条件:互斥、持有并等待、不可剥夺、环路等待
破坏一个就行,最常见的并且可行的就是使用资源有序分配法,来破坏环路等待条件。(这又一个勘误)
这里就让线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。他这几个printf太眼花了
跟小学生似得,换作我直接打印#¥@!这些
他代码就不帖了,思路就是AB都是先获取mutex_A后获取mutex_B
关于锁 —— 悲观锁、乐观锁
铺垫:
关于锁有很多,上面提到自旋锁,
如果选择了错误的锁,高并发的场景下,选对了合适的锁,则会大大提高系统的性能,否则性能会降低
为了选择合适的锁,我们需要清楚知道:
加锁的成本开销有多大
分析业务场景中访问的共享资源的方式
考虑并发访问共享资源时的冲突概率
接下来说下:「互斥锁、自旋锁、读写锁、乐观锁、悲观锁」
先说下,最底层的是「互斥锁和自旋锁」,很多高级的锁都是基于它们实现的,你可以认为它们是各种锁的地基
已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的
互斥锁加锁失败后,线程会释放 CPU ,给其他线程
他是一种独占锁,只要线程 A 没释放手中锁,线程 B 加锁就失败,于是释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞(由os内核实现的,加锁失败,内核会将线程置为睡眠状态,等锁释放后,内核会在合适的时机唤醒线程)

所以,互斥锁加锁失败时,从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本 —— 两次线程上下文切换的成本
当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行
接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行
线程的上下文切换的是什么?
当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据
上下切换的耗时大概在几十纳秒到几微秒之间,如果锁住的代码执行时间比较短(几十纳秒几微秒),那可能上下文切换的时间 比 锁住的代码执行时间还要长
所以,如果能确定被锁住的代码执行时间很短,就不该用互斥锁,而要用自旋锁
自旋锁加锁失败后,线程会忙等待,直到它拿到锁
加锁步骤:查看锁的状态,如果锁是空闲的,则将锁设置为当前线程持有
具体是通过 CPU 提供的 CAS 函数,在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些
这个CAS函数,就是把 加锁步骤 搞成一条硬件级指令,形成原子指令
比如,设锁为变量 lock,整数 0 表示锁是空闲未锁状态,整数 pid 表示线程 ID,那么
CAS(lock, 0, pid) 就表示自旋锁的加锁操作
CAS(lock, pid, 0)则表示解锁操作
具体解释下这里的 CAS 参数含义被计算机科学家们定义的很鸡巴傻逼智障不好懂反人类
CAS参数:
lock 是共享变量(通常是内存地址),初始值为 0,表示锁未被任何线程持有
第二个参数 0 是预期值:线程在尝试加锁时,期望锁当前的值是 0(即未被锁定)
第三个参数 pid 是新值:如果锁当前的值确实是 0,则将其更新为当前线程的 ID(pid),表示锁已被当前线程持有
对于CAS(lock, 0, pid),假设初始状态:lock = 0(全局变量,代表锁状态,0 = 空闲),当前线程想加锁,执行 CAS(lock, 0, pid) 时:
读取当前值:线程先读取 lock 的值,得到 current_value = 0即lock
CAS 操作:系统原子执行以下判断和更新
检查 lock 的实际值是否仍等于 期望值 0(即 lock == 0)
若是:将 lock 更新为 新值 pid(表示当前线程持有锁),返回成功(加锁成功)
若否:说明其他线程已修改过 lock(值≠0),不更新 lock,返回失败(加锁失败,需重试)
对于 CAS(lock, pid, 0) 中:
lock 是共享锁变量(内存地址),当前值为持有锁的线程 ID(即 pid)。
期望值 pid:线程在解锁前,期望锁仍被自己持有(值等于自己的 pid)
只有锁的持有者(lock 值等于当前线程 pid)能成功释放锁将其重置为 0
若其他线程已获取锁(值变为其他线程的 pid),当前线程的 CAS 操作会失败,避免误释放他人持有的锁
新值 0:如果锁确实还被自己持有(值等于 pid),则将其重置为 0(表示释放锁)
很反人类的一点就是感觉参数怎么不固定,一会第二个参数是线程ID,一会第三个参数是线程ID
参数类型始终固定:三个参数分别是 指针、整数、整数。
参数含义随场景变化:
加锁时,新值是线程 ID(将锁从 0 改为 pid)。
解锁时,期望值是线程 ID(验证锁仍被自己持有,再改为 0)。
注:单核 CPU 需要抢占式调度器,即不断通过时钟中断一个线程,运行其他线程,否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU
自旋锁开销少,多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式。如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源, “被锁住的代码” 指的是加锁保护下的、临界区内实际执行的业务代码。所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系
总结:
自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同,当加锁失败时,
互斥锁用「线程切换」来应对
自旋锁则用「忙等待」来应对
所有其他高级锁,都会选择 互斥、自旋 其中一种来实现,比如读写锁既可以选择互斥锁实现,也可以基于自旋锁实现
豆包表格:
|
类别 |
互斥锁 |
自旋锁 |
|---|---|---|
|
是否需内核参与 |
需要 获取失败时线程睡眠及唤醒需内核调度 |
不需要 通过用户态原子操作和自循环实现 |
|
对 CPU 资源占用情况 |
获取失败时线程睡眠,不占用 CPU ; 但线程上下文切换有开销 |
等待锁时持续自循环,占用 CPU 资源 |
读写锁:
写锁没被线程持时,多个线程可以并发持有读锁(并发持有读锁指多个线程可同时获取读锁,但不仅仅是并发,核心数够也可以说是并行,只是并发的说法更宽泛),但写锁被持有后,读线程的获取读锁 和 其他写线程获取写锁 的操作,也会被阻塞
写:独占锁
读:共享锁
所以,读写锁在读多写少的场景,能发挥出优势
根据实现的不同,读写锁可以分为两种:
「读优先锁」:

只要是读线程,后来的也可以阻塞先来的写线程,读都完事了,写线程才能拿到锁
「写优先锁」:

当读线程先持有了读锁,接着写来了会阻塞,但后面的读也会阻塞,到时候锁释放了,写线程会先拿到锁
这种读写都会饥饿
公平读写锁:搞个队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁
它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁
相反,乐观锁工作方式:(这里小林说的不对,我做了修改)
先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃按原方式提交,改用对比合并重试啥的。这玩意也叫无锁编程
比如多人同时编辑文档(这里小林解释的不够透彻,我追问豆包明白的):
如果用悲观锁,那么只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档了,体验不好
乐观锁可以同时打开文档进行编辑,首先说明我们日常用的是采用了WebSocket技术,实时推送过,A、B同时编辑,B修改A会同步看到,这是底层是乐观锁的功能增强,即文档系统实时同步机制,显实层的优化,但底层数据更新验证逻辑是:
共享文档有版本号,比如 A 先打开文档编辑,这个操作的底层逻辑是从浏览器下载文档,然后记录当前版本号(初始创建是 1.0 版。每次有人成功修改提交,版本号递增)
然后 B 打开立马修改,比如 B 8:00 提交修改,服务器验证 B 提交的版本号 2.0 和当前版本号(假设没人中途修改,也是 2.0 )一致,B 修改成功,版本号升为 2.1
然后由于 A 先打开,但编辑很慢, 9:00 提交,带的版本号是 2.0,但服务器当前版本是 2.1,说明 A 编辑期间有他人修改,A 提交失败,需重新处理冲突后再提交
这时候乐观锁的作用是进行合并重试啥的
冲突概率非常低的话,可以用,因为重试的成本高
至此锁了解了,但回顾说下,之前说的CAS其实是乐观锁!(这里小林解释的我总要让豆包翻译下,不知道是我理解能力太差,还是小林表达能力真的很垃圾)
CAS本身是乐观锁,基于 “先修改,再验证冲突” 的思路
比如CAS(lock, 0, pid) 体现的是乐观锁思想,假定对共享资源操作大概率不会冲突,先尝试修改,再验证。CAS 操作时,先读取共享变量(lock )值,将其与期望值(这里是 0 )比较,若相等则更新为新值(pid )。若在比较 - 更新期间,lock 被其他线程修改,导致当前线程预期值和实际值不等,更新失败,线程需重试 。整个过程没有提前加锁,符合乐观锁 “先操作后验证冲突” 特征
所以总结起来就是:
尝试获取锁:线程用 CAS 操作尝试修改表示锁状态的变量(如将表示锁空闲的 0 改为自己的标识 pid ) ,期望此时没有其他线程持有锁。这一步有乐观成分,因为没有提前加锁,直接尝试修改。
竞争处理:若 CAS 操作失败,说明锁被其他线程持有,这与悲观锁预先认为会有竞争相符。此时,自旋锁会让线程进入忙等待(如通过 while 循环不断重试 CAS 操作 ),或短暂睡眠后再尝试,直到 CAS 操作成功获取到锁。
持有锁访问资源:当线程通过 CAS 成功修改锁状态变量后,就持有了锁,可以安全访问共享资源。访问结束后,再通过合适操作(如再次使用 CAS 将锁状态改回初始空闲值)释放锁
我的理解(经过豆包肯定)
所以拿锁再访问是实现了悲观锁,但拿锁的过程本质是乐观锁
所以其实自旋锁是基于乐观锁的锁,的实现了悲观锁
5.6节略过重复内容
5.7节
线程崩溃了,进程也会崩溃吗
之前说过,C++里线程崩溃,进程也会崩溃。Java却不会(勘误)
如果线程是因为非法访问内存引起的崩溃,那么进程肯定会崩溃,因为在进程中,各个线程的地址空间是共享的,既然是共享,那么某个线程对地址的非法访问就会导致内存的不确定性,进而可能会影响到其他线程,这种操作是危险的,操作系统会认为这很可能导致一系列严重的后果,于是干脆让整个进程崩溃
比如同一进程内线程共享地址空间,一个线程非法访问内存(如写入未授权区域)会破坏内存中的数据或程序结构。其他线程后续正常访问内存时,会因这些被破坏的内容(如数据错乱、关键结构损坏)无法正确执行

线程共享代码段,数据段,地址空间,文件非法访问内存有以下几种情况
1.、针对只读内存写入数据

2、访问了进程没有权限访问的地址空间(比如内核空间)

在计算机内存地址空间的使用上,通常是从低位地址开始使用,地址高位部分一般属于内核空间
从十六进制角度看,0xC0000fff 中,C 在十六进制里表示十进制的 12 ,是比较大的数值。在常见的 32 位操作系统内存布局中,一般将低地址部分(例如 0x00000000 - 0xbfffffff 左右 )划分给用户空间
所以32位虚拟地址空间中,p 指向的是内核空间,不具有写入权限
3、访问了不存在的内存

以上三种统一会报 Segment Fault 错误(即段错误),这些都会导致进程崩溃
进程是如何崩溃的-信号机制简介
发个信号进程就崩溃的底层逻辑:
(勘误1这里总共就四个,没第五步)
(勘误2这里妈逼的调用kill系统调用无法发送SIGSEGV)
CPU 执行正常的进程指令
调用 kill 系统调用向进程发送信号(假设为 11,即 SIGSEGV,一般非法访问内存报的都是这个错误)
进程收到操作系统发的信号,CPU 暂停当前程序运行,并将控制权转交给操作系统
操作系统根据情况执行相应的信号处理程序(函数),一般执行完信号处理程序逻辑后会让进程退出
如果注册了自己的函数,就会执行自己的函数也可以使用 sigsetjmp,siglongjmp 这两个函数来恢复进程的执行

解释小林的错误:
kill -l是系统支持的所有信号
kill 命令本质是向进程发送信号
但kill 命令并不能发送kill -l里的全部,比如的 SIGSEGV 也在 kill -l 里,但kill命令无法直接发送 SIGSEGV命令
SIGSEGV 通常由操作系统在进程执行无效内存引用、发生段错误时自动生成
还有这里的sigHandler函数解释的容易误解!
这就是我一直不喜欢的比喻!解释东西尽可能严谨!为了节目娱乐氛围效果而搞的很歧义,那需要娱乐的那群傻逼就根本不是学这玩意的料!反而给真正想学的人引起不必要的误会
这个“垂死挣扎”到底是延迟了一会即printf, 之后必然会退出崩溃还是咋的?没有exit还会不会退出了 ?退出究竟是因为访问非法而崩溃还是因为exit ?啥都没说清楚,比喻个JB
具体严谨说法是:
当程序执行到非法访问内存触发 SIGSEGV 信号时,会调用注册的信号处理函数 sigHandler
在函数里,printf 执行完打印信息后,若没有 exit(sig) 语句,程序不会因该函数调用而直接终止
如果没有exit(sig)执行完信号处理函数后,程序会返回到触发信号的指令处尝试继续执行。但由于触发 SIGSEGV 是非法内存访问操作,即便继续执行也可能再次触发错误信号,最终还是可能崩溃退出
sigHandler 里的 printf 是打印捕获信号信息
没有 exit(sig) ,触发 SIGSEGV 后,没 exit(sig) ,执行完 sigHandler 会返回继续执行,但因非法内存访问问题,大概率还会出错崩溃,只是不会因 sigHandler 调用就直接退出
这才是解释,直接来一个垂死挣扎,谁能理解
也可以忽略,用SIG_IGN
kill -9 命令例外,不管进程是否定义了信号处理函数,都会马上被干掉
如何让正在运行的 Java 工程的优雅停机?
JVM 是作为 Java 程序运行的虚拟机,其底层基于操作系统实现信号处理机制
当 JVM 启动时,会通过本地方法向操作系统注册自定义信号处理函数,用于捕获和处理特定信号(如 SIGTERM ),这些自定义函数是 JVM 内部实现的一部分
这里自定义函数指的是:对默认的一些信号又封装了更安全的自定义函数,然后直接用,不需要程序员再定义啥的
比如先对SIGTERM拦截避免直接触发系统默认行为;触发 Java 层面的优雅关闭流程(如执行开发者编写的shutdown hooks、释放 JVM 内部资源);最后再让进程退出
不需要程序员定义,JVM 内部已实现并注册(JVM 是基于 C/C++ 等底层语言写的)
这样当发送 kill pid 命令(默认会传 15 也就是 SIGTERM)后,JVM 就可以在信号处理函数中执行一些资源清理之后再调用 exit 退出
这种场景 kill -9 的话会一下把进程干掉,资源就来不及清除,内存泄漏
为什么线程崩溃不会导致 JVM 进程崩溃
这个先略过吧(强迫症挣扎好久~~~~(>_<)~~~~)
总是无止境的钻研、看
豆包追问链接同上
调度算法是CPU调度算法,因为进程由CPU控制,CPU空闲的时候,os在内存中选择就绪状态进程,分配给CPU
啥时候调度?当进程从:
运行 转到 等待「非抢占式调度:当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件而被阻塞」
运行 转到 就绪「抢占式调度:进程正在运行的时,可以被打断,使其把 CPU 让给其他进程」(这里是时间片用完)
等待 转到 就绪「抢占式调度:进程正在运行的时,可以被打断,使其把 CPU 让给其他进程」(需要等待的事件完成后如果优先级比较高,一旦转到就绪态,就会以优先级来调度,立马强占正在运行的进程)
运行 转到 终止「非抢占式调度:当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件而被阻塞」
抢占式强占原则:时间片、优先权、短作业优先
自己的疑惑:
此文搜“啃完尹圣雨TCPIP网络编程很多都会了”那个图,我的疑问,“就绪到运行,不CPU调度吗”
“就绪到运行” 本身不是触发 CPU 调度的条件,而是 CPU 调度的 结果。以下是具体分析:
CPU 调度的本质:从就绪队列中选择一个进程,将 CPU 资源分配给它。例如,多个进程在就绪队列等待,CPU 调度器决定选某个进程运行,该进程就从就绪态转为运行态。这是调度行为的 “结果”,而非触发调度的 “时机”。
触发 CPU 调度的常见场景(如原文所列):
当进程从 运行→等待(如等待用户输入、I/O 操作),CPU 空闲,需调度其他就绪进程。
当进程从 运行→就绪(如时间片用完,或被更高优先级进程抢占),需重新从就绪队列选进程运行。
当进程从 等待→就绪(如 I/O 完成),就绪队列内容变化,可能需重新评估调度谁运行。
当进程从 运行→终止,CPU 空闲,需调度其他就绪进程。
“就绪→运行” 的特殊性:
就绪态进程已在就绪队列等待调度,它转为运行态,是因为之前已被调度器选中(调度行为已发生)。就像排队时,调度是 “选谁上前”,而 “就绪→运行” 是 “被选中者上前”,后者是前者的结果,而非触发前者的条件
调度算法影响的是等待时间(进程在就绪队列中等待调度的时间总和),而不能影响进程真在使用 CPU 的时间和 I/O 时间
总共六种调度算法:
我操才发现这学过了,之前有,不再说了
此文搜“01 先来先服务调度算法”总共六种
说完了进程调度算法,开始说页面置换算法
先说个引子:缺页异常(缺页中断)
当 CPU 访问的页面不在物理内存时,便会产生一个缺页中断,请求操作系统将所缺页调入物理内存
诸多中断,这个缺页中断有啥区别?
一般中断:打印任务,CPU 快速传数据到打印机缓冲区后继续做其他事 ,打印机打完缓冲区内容发中断请求 ,CPU 响应处理(再传数据 ),处理完成后,回到原任务下一条指令执行 。
缺页中断:程序访问大数组 ,访问元素时对应页面不在物理内存 ,指令执行期间触发缺页中断 ,系统调页面入内存 ,调完回该指令开头重执行 。
所以:
缺页中断在指令执行「期间」产生和处理中断信号,而一般中断在一条指令执行「完成」后检查和处理中断信号
缺页中断返回到该指令的开始重新执行「该指令」,而一般中断返回回到该指令的「下一个指令」执行
缺页中断的流程:

在 CPU 里访问一条 Load M 指令,然后 CPU 会去找 M 所对应的页表项
如果该页表项的状态位是「有效的」,那 CPU 就可以直接去访问物理内存了,如果状态位是「无效的」,则 CPU 则会发送缺页中断请求
操作系统收到了缺页中断,则会执行缺页中断处理函数,先会查找该页面在磁盘中的页面的位置
找到磁盘中对应的页面后,需要把该页面换入到物理内存中,但是在换入前,需要在物理内存中找空闲页,如果找到空闲页,就把页面换入到物理内存中
页面从磁盘换入到物理内存完成后,则把页表项中的状态位修改为「有效的」
最后,CPU 重新执行导致缺页异常的指令
第 4 步是能在物理内存找到空闲页的情况,那如果找不到,就说明此时内存已满了,这时候,就需要「页面置换算法」选择一个物理页
如果该物理页有被修改过(脏页),则把它换出到磁盘,然后把该被置换出去的页表项的状态改成「无效的」,最后把正在访问的页面装入到这个物理页中
但这句话小林说的,我觉得很有歧义,问题很大!!啥叫换出?磁盘里本身不就有吗?覆盖吗?既然叫换,那啥回来?最主要的是,脏页换出,那非脏页呢???
说东西得叨重点啊,说到根上!
自己重新问豆包总结的如下:
非脏页不存在写回,这里比如还是从A位置来的,直接删除物理内存里的
页表项字段:

状态位:用于表示该页是否有效,即是否在物理内存中,供程序访问时参考
访问字段:用于记录该页在一段时间被访问的次数,供页面置换算法选择出页面时参考
修改位:表示该页在调入内存后是否有被修改过,由于内存中的每一页都在磁盘上保留一份副本,没修改过置换该页时就不需要将该页写回到磁盘上,以减少系统的开销。如果已经被修改,则将该页重写到磁盘上,以保证磁盘中所保留的始终是最新的副本
硬盘地址:用于指出该页在硬盘上的地址,通常是物理块号,供调入该页时使用
虚拟内存的管理整个流程

所以,页面置换算法的功能是,当出现缺页异常,需调入新页面而内存已满时,选择被置置换的物理页面,即选择一个物理页面换出到磁盘,然后把需要访问的页面换入到物理页
那其算法目标则是,尽可能减少页面的换入换出的次数
五大置换算法:
最佳页面置换算法(OPT)也叫最优页面置换算法

先捋顺下:
分页机制:是虚拟内存和物理内存被等分成固定大小的块,虚拟内存的块叫 “页/页面” ,物理内存的叫 “页帧” 。CPU 中的内存管理单元 MMU 里有页表,负责页和页帧的映射管理 。
页表:存储虚拟地址到物理地址映射关系的关键数据结构
然后,这里有个很傻逼的知识点,不提前说下会导致根本看不懂图
初始阶段:有 3 个空闲物理页,依次请求 7、0、1 号页面时,因内存空闲,直接将对应页面调入,这 3 次操作算缺页异常(空闲页换入 ) 。
页面置换阶段:请求 2 号页面时,内存已满,需用最优页面置换算法。该算法基于未来页面访问情况选择换出页面,内存中有 7、0、1 号页面。观察后续请求序列,0 号页面会被再次访问(下一个请求就是 0 ),而 1 号和 7 号页面在后续序列中均不再被访问,为了演示,即先杀猪还是先杀驴都没事都一样,就随便说了个7 号页面,所以将 7 号页面换出,2号换入
不这么说一句,就会纠结为啥是7号,是不是有什么自己不知道的知识点
总结:
这里缺页共发生了 7 次:
4 次(用的最优页面置换 )但这个是属于事后诸葛,无法未卜先知,实际系统中无法实现
小林说的是“无法预知每个页面在「下一次」访问前的等待时间”研究半天没搞懂,最后问豆包,给出的解答更易于理解“操作系统无法提前知晓每个页面未来何时会被再次访问”
所以,最佳页面置换算法,是为了衡量使用的算法的效率,越接近,则越高效
先进先出置换算法(FIFO)

看图太JB麻烦了了
不用看了,节省时间就是7、0、1满了,再来就先把7搞走
10次缺页,页面置换7次,性能太垃圾了
实现倒是简单,只需按顺序维护队列,新元素入队、旧元素出队操作时间复杂度低
这里的是只看进出,比如:7、0、1、2,0,
这里 2 来了在,取代7,
现在是0、1、2,即再取代就按照这个顺序
然后又范访问0,这个0不影响置换规则
再访问3,就取代0了
最近最久未使用的置换算法(LRU)
选择最长时间没有被访问的页面进行置换
假设已经很久没使用的页面,未来较长时间仍然不会被使用
最优置换算法是通过未来,这个是通过历史

这里访问7、0、1,
再访问2就取代7,然后取代顺序变成了0、1、2
再访问0的时候,上一个算法是0不管,而这个算法取代顺序就变成了1、2、0
接下来访问3,取代的就是1
缺页9次,置换6次
看似很好,但如果用队列实现,需要每次都遍历,复杂度O(n),找到再调整到队尾很耗时
所以把每个页面编号弄成一个链表,查找页面在链表中的位置、将其移动到表头 等操作,可即便如此,频繁执行这些操作也有较大开销,占用较多 CPU 时间和系统资源,实际应用较少用
以上这些都是重点阐述算法咋选虚拟页进行置换,侧重虚拟内存层面概念,对物理内存提及较少
接下来的时钟不同
时钟页面置换算法(Lock)(“既”勘误,小林这个狗东西用屁眼写东西吗?不带脑子的吗?老子这么多博客不会有一个错别字)
既能优化次数,也能方便实现

小林这个图傻逼玩意,没头没脑的,他自己懂吗?就出来讲。
我追问豆包真的异常痛苦,详见豆包链接搜“咋就开板放了这么多?”
首先,之前3个物理地址是为了简化,这里5个物理(由物理页号 0、4、5、6、7 体现),是为了展示更复杂的内存状态下时钟算法的工作过程,帮助理解算法在更丰富内存布局下如何选择淘汰页面
其次说步骤,
物理内存为空,无任何虚拟页。
物理内存有空闲,将虚拟页 7 装入物理页号 0,访问位设为 1。
内存状态:
虚拟页号 | 物理页号 | 访问位
7 | 0 | 1
访问虚拟页 0
物理内存仍有空闲,将虚拟页 0 装入物理页号 4,访问位设为 1。
内存状态:
虚拟页号 | 物理页号 | 访问位
7 | 0 | 1
0 | 4 | 1
第一次访问虚拟页 1
虚拟页号 | 物理页号 | 访问位
7 | 0 | 1
0 | 4 | 1
1 | 5 | 1
第二次访问虚拟页 1
此时系统可能采用了多级页表或页面共享机制,为同一虚拟页号 1 分配了新的物理页号 6(例如,不同进程共享同一代码页,但数据页不同)。
将虚拟页 1 的新实例装入物理页号 6,访问位设为 1。
内存状态:
虚拟页号 | 物理页号 | 访问位
7 | 0 | 1
0 | 4 | 1
1 | 5 | 1
1 | 6 | 1
访问虚拟页 3
物理内存仍有空闲,将虚拟页 3 装入物理页号 7,访问位设为 1。
内存状态:
虚拟页号 | 物理页号 | 访问位
7 | 0 | 1
0 | 4 | 1
1 | 5 | 1
1 | 6 | 1
3 | 7 | 1
傻逼小林图都写错了,不是淘汰物理页号6,用阳寿写博客?
假设此时进程访问虚拟页 2(不在内存中),物理内存已满,启动时钟算法,顺时针扫描顺序为:0 → 6 → 4 → 7 → 5(环形)。
第一轮扫描:
物理页号 0(虚拟页 7):访问位 1 → 置为 0,继续顺时针到 6。
物理页号 6(虚拟页 1):访问位 1 → 置为 0,继续顺时针到 4。
物理页号 4(虚拟页 0):访问位 1 → 置为 0,继续顺时针到 7。
物理页号 7(虚拟页 3):访问位 1 → 置为 0,继续顺时针到 5。
物理页号 5(虚拟页 1):访问位 1 → 置为 0,回到起点 0,完成第一轮扫描(所有页面访问位均置 0)。
第二轮扫描:
从起点 0 开始,继续顺时针扫描,寻找第一个访问位为 0 的页面(此时所有页面访问位均为 0,因此遇到的第一个页面即触发淘汰):
物理页号 0(虚拟页 7):访问位为 0 → 满足淘汰条件,操作系统将此页面淘汰,腾出物理页号 0 的空间
将虚拟页 2 调入物理页号 0,访问位设为 1
最不常用置换算法(LFU)
当发生缺页中断时,选择「访问次数」最少,即最不常用的那个页面,并将其淘汰
对每个页面设置一个「访问计数器」,每当一个页面被访问时,该页面的访问计数器就累加 1
在发生缺页中断时,淘汰计数器值最小的那个页面
但要增加一个计数器来实现,这个硬件成本高,另外如果要对这个计数器查找哪个页面访问次数最小,查找链表本身,如果链表长度很大,是非常耗时的,效率不高
还有,这个算法只考虑了频率问题,没考虑时间的问题,比如有些页面在过去时间里访问的频率很高,但是现在已经没有访问了,而当前频繁访问的页面由于没有这些页面访问的次数高,在发生缺页中断时,就会可能会误伤当前刚开始频繁访问,但访问次数还不高的页面
解决的办法:
定期减少访问的次数,比如当发生时间中断时,把过去时间访问的页面的访问次数除以 2,也就说,随着时间的流失,以前的高访问次数的页面会慢慢减少,相当于加大了被置换的概率
磁盘调度算法
傻逼小林说“盘片中的每一层分为多个磁道”很容易误解
红色方框是盘片,最右侧单独的是一个盘片的结构,盘片的每个同心圆圈即环形区域,叫磁道,最右侧的图有4个磁道
操作系统不直接用物理磁道,核心原因是物理磁道地址复杂且不便于管理
物理磁道需通过 “柱面号 + 磁头号 + 扇区号” 定位,操作繁琐。
操作系统将磁盘统一视为连续逻辑块(如用 1、2、3… 编号),屏蔽物理细节,简化存储管理和数据读写
扇区就是一个磁道的, 比如分隔成n份,就n个扇区,每个扇区512字节
多个具有相同编号的磁道形成一个圆柱,称之为磁盘的柱面,如图里中间的样子

磁盘的调度算法目的是提高磁盘访问性能,即优化磁盘的访问请求顺序
最耗时的就是寻道时间
假设有下面一个请求序列,每个数字代表磁道的位置:
98,183,37,122,14,124,65,67
初始磁头当前的位置是在第 53 磁道
用这个例子讲解五大磁盘调度算法
先来先服务算法

总共移动了 640 个磁道的距离,性能差,因为寻道时间长
简称傻逼凑数算法
最短寻道时间优先算法
优先选择从当前磁头位置所需寻道时间最短的请求
例子中的就变为了:65,67,37,14,98,122,124,183

总共距离是236磁道,但会饥饿,如果后续请求都是小于183的,那183就永远不会被响应,磁头在一小块区域来回移动
依旧是傻逼无脑算法
扫描算法(电梯算法)
磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向,这就是扫描(Scan)算法
假设先朝磁道号减少的方向移动,例子就成了:37,14,0,65,67,98,122,124,183

直到到达最左端( 0 磁道)后,才开始反向移动,响应右边的请求
中间部分的磁道会比较占便宜,中间部分相比其他部分响应的频率会比较多,也就是说每个磁道的响应频率存在差异
追问豆包的具体解释:
实际应用中,I/O 请求在磁盘上分布不均匀。中间区域往往请求更多,磁头在中间区域往复经过时,能及时响应这些频繁的请求。而外端(或内端)到达一次后,要等磁头再次绕一圈回来才会再次处理,等待时间更长
细节懂了。这里再说下磁盘磁道咋跟之前的串联,I/O 操作的本质是程序访问磁盘数据时,需通过I/O 请求触发磁头移动(寻道)、定位扇区(旋转延迟)等操作,完成数据的读取或写入
这就是比如缺页中断干的事,磁盘到物理内存
循环扫描算法(Circular Scan, C-SCAN )
为了解决频率差异,这个算法规定:
只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且返回中途不处理任何请求,该算法的特点,就是磁道只响应一个方向上的请求
假设先朝磁道增加的方向移动
例子变成了:65,67,98,122,124,183,199,0,14,37
固定扫描方向 + 快速回退实现 “平均化”,不折回
更tm离去,匪夷所思感觉这些思路都是猪狗脑子的傻逼想出来的吗
LOOK 与 C-LOOK 算法
磁头在移动到「最远的请求」位置,然后立即反向移动
针对 SCAN 算法的优化则叫 LOOK 算法,不需要移动到磁盘的最始端或最末端,反向移动的途中会响应请求:

针 C-SCAN 算法的优化则叫 C-LOOK,在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,不需移到最始/最末,反向移动的途中不会响应请求:

豆包追问链接同上
妈逼的垃圾AdGuard乱屏蔽,把图都搞没了
前言:
文件系统就是,OS 中负责管理持久数据的子系统,即负责把用户的文件存到磁盘硬件中,计算机断电数据不丢
文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理
Linux:一切皆文件
九大模块之一:文件系统组成
目录项(dentry / directory entry)与索引节点(inode)二者是独立的数据结构
目录项:是内核维护的内存数据结构,不存放于磁盘
它记录文件的名字,同时包含一个指向对应 inode 的指针
其作用是建立文件名字与文件元信息(存储于 inode)的关联,方便通过文件名快速定位文件的 inode
索引节点(inode):存储于磁盘,记录文件的元信息(如文件大小、访问权限、数据在磁盘的位置等),是文件的唯一标识
为何目录项记录文件名字及对应索引节点的指针?
文件系统需要通过文件名来管理文件,但真正标识文件内容及属性的是
inode目录项记录文件名,可让用户通过熟悉的名字操作文件。记录指向
inode的指针,则能将文件名与文件的实际元信息及数据存储位置关联起来这种设计使得多个目录项(不同文件名)可以指向同一个
inode(如硬链接),实现同一文件内容通过不同名字访问,增强了文件管理的灵活性,同时保证文件元信息(由inode维护)的一致性。(这个比文中的硬链接描述容易理解多了)
目录也是文件,也是用索引节点唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录或文件
区分:
目录是个文件,持久化存储在磁盘
目录项是内核一个数据结构,缓存在内存
查询目录频繁从磁盘读,效率低,所以内核把已读过的 目录 用 目录项 这个数据结构缓存在内存,下次直接从内存读 目录项 就可以,大大提高了文件系统的效率(目录项这个数据结构不只是表示目录,也是可以表示文件的)
一个扇区512B,提高效率每次多读,规定多个扇区组成逻辑块,每次读的最小单位四回逻辑块(数据块),Linux里逻辑块大小4KB,即8个扇区
索引节点、目录项、文件数据的关系:

索引节点是存储在硬盘上的数据,那么为了加速文件的访问,通常会把索引节点加载到内存中
磁盘进行格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区
超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。只有当文件系统挂载时才进入内存(挂载指的是:让电脑能找到并使用存储设备(如 U 盘、硬盘)里的文件。好比把 U 盘插到电脑,电脑识别它,让你能打开里面的文件夹,这个 “让电脑识别并能用” 的过程就是挂载。在一些系统(如 Linux)里,还需指定一个目录作为 “连接点”,访问该目录就等于访问存储设备内容)
索引节点区,用来存储索引节点;只有当文件被访问时进入内存
数据块区,用来存储文件或目录数据;
至此,文件、目录、目录项我有点懵,追问豆包后理解了:
目录项是磁盘那些目录加载到内存上的,但需要更精准地区分 存储形式 和 运行时形态:
- 本质:目录是一个 磁盘文件(和普通文件一样,有自己的
inode和数据块)。- 内容:
目录的数据块中存储着一系列 条目,每个条目包含:
- 文件名(如
file1)- 对应文件的
inode编号(通过编号关联到文件元信息)- 其他元数据(如文件名长度等)
- 这些条目是目录项信息在磁盘上的持久化形式,但 不叫目录项(磁盘上的格式由文件系统定义,如 EXT4 的
ext4_dir_entry_2结构)。举例:
磁盘上的目录/home/user中可能有这样的条目:[文件名: "file1", inode编号: 123] [文件名: "dir", inode编号: 456]2. 内存中的目录项(运行时数据结构)
- 本质:目录项(
dentry)是 内核在内存中维护的数据结构,用于快速查找和管理文件。- 来源:
当系统需要访问磁盘上的目录文件时(例如打开目录、读取文件列表),会将磁盘目录文件中的条目 解析并加载到内存,转化为 目录项(dentry)对象。
- 每个磁盘目录条目中的 文件名 和 inode 编号 会被提取出来,生成一个对应的内存目录项。
- 目录项会缓存文件名与
inode的映射关系,加速文件访问(如通过文件名找inode)。举例:
内存中加载/home/user目录后,会生成两个目录项:dentry1: { 文件名: "file1", inode: inode_123 } dentry2: { 文件名: "dir", inode: inode_456 }
- 信息对应:
磁盘目录文件中的条目(文件名 + inode 编号)是内存目录项的 数据源头,两者一一对应。- 用途差异:
- 磁盘条目:用于 持久化存储目录结构(关机后不丢失)。
- 内存目录项:用于 实时文件系统操作(如查找文件、建立硬链接),关机后消失。
- 当创建硬链接(如
ln file1 file1_hard)时:
- 磁盘上会在另一个目录(如当前目录)中新增一个条目:
[文件名: "file1_hard", inode编号: 123](和原文件file1共享同一个inode)。- 内存中会为新文件名生成一个目录项
dentry3: { 文件名: "file1_hard", inode: inode_123 },与原目录项dentry1指向同一个inode。- 关键:多个磁盘条目(不同目录中的文件名)对应多个内存目录项,但它们的
inode编号相同,因此指向同一个文件实体。
- 磁盘目录文件 是目录项信息的 “硬盘存档”,内存目录项 是这些信息的 “运行时副本”。
- 两者靠 文件名 + inode 编号 建立映射,共同实现文件系统的层级管
inode 是什么
- inode 是一个编号(如 12345),每个编号对应一组固定的文件属性(如大小、权限、创建时间)和数据块位置(文件内容存放在磁盘的哪些位置)。
- 每个文件必须有且仅有一个 inode 编号(类似身份证号),但一个 inode 可以被多个文件名引用(硬链接)。
目录的作用
- 目录是一张表格,表格里每行记录两列信息:
文件名 inode 编号 file1 12345 file2 67890 - 当你创建文件时,系统会:
- 分配一个 inode 编号(如 12345),并记录文件属性和数据块位置。
- 在当前目录的表格中新增一行,写入文件名(如
file1)和对应的inode 编号(12345)。如何通过文件名找到文件内容
- 当你执行
ls命令查看目录时,系统读取目录表格并显示所有文件名。- 当你执行
cat file1时:
- 系统先在目录表格中找到
file1对应的 inode 编号(12345)。- 根据编号找到对应的 inode 数据(属性和数据块位置)。
- 根据数据块位置读取磁盘上的文件内容。
硬链接的本质
- 创建硬链接(如
ln file1 link1)时,系统做了两件事:
- 在目录表格中新增一行:
[link1, 12345](inode 编号与file1相同)。- 增加 inode 的引用计数(记录有多少个文件名指向这个 inode)。
- 此时
file1和link1是两个不同的文件名,但共享同一个 inode,因此修改其中一个文件会影响另一个(因为数据块相同)。
- 目录项是内存中的临时记录,作用是缓存目录表格中的文件名和 inode 编号映射,让系统快速找到文件。
- 类比:如果把磁盘上的目录表格(文件名 + inode 编号)看作 “硬盘上的纸质名单”,那么目录项就是内存中缓存的电子名单,方便系统快速查询。
当你执行
ls /etc或访问某个目录时:
系统先读取磁盘上的目录文件(如
/etc的目录文件),里面是文件名 + inode 编号的表格(持久化存储)。系统将表格中的每一行(如
file1:12345)加载到内存,生成对应的目录项对象(dentry)。
每个目录项包含:
文件名(如
file1)对应的 inode 编号(如
12345)指向 inode 对象的指针(内存中真正的 inode 数据结构)
- 当你访问文件
file1时:
- 系统先在内存的目录项缓存中查找是否有
file1的记录。- 如果有,直接通过目录项获取对应的 inode 指针,找到 inode 数据(属性 + 数据块位置)。
- 如果没有,再去磁盘读取目录文件,生成目录项并缓存。
- 磁盘目录(纸质名单) ↔ 内存目录项(电子名单) ↔ inode(文件身份证)
文件名 "file1"(目录项缓存中) ↓ ├─ 对应磁盘目录中的条目:[file1, 12345] └─ 通过 inode 编号 12345 找到 inode 对象 ↳ inode 对象记录:文件大小、权限、数据块位置等
- 创建硬链接
ln file1 link1时:
- 磁盘上的某个目录(如当前目录)新增一行:
[link1, 12345](和file1共享 inode 12345)。- 内存中生成一个新的目录项
link1,其 inode 编号指向 12345。- 此时内存中有两个目录项(
file1和link1),但它们的 inode 指针相同,因此指向同一个文件实体。
- 磁盘上没有目录项,只有目录文件(存储文件名 + inode 编号的表格)。
- 目录项只存在于内存,是系统为了加速文件访问而创建的临时缓存。
- 目录项的核心作用:建立 “文件名 → inode” 的快速映射,避免每次访问文件都去磁盘读取目录表格。
我的思考经过豆包确定:
不那么严谨但很助于理解的说就是,他俩本质不同:目录是磁盘上的持久存储的文件实体,目录项是内存中的缓存结构,(内核动态创建的内存对象)
但我理解为其实“就是一个东西”,只不过一个硬盘上的,它找的慢,一个内核里的,它找得快,这个找得快的内核上的,来源就是由于经常用所以从磁盘调入的,但其实严谨说是内核根据需要主动生成的缓存对象
咋感觉跟缺页中断差不多呢?
一个是基石,一个是数据
缺页中断是内存管理的 “基石机制”,目录项是文件系统中缓存的 “数据结构”
都是按需加载,缓存加速,但
维度 缺页中断(内存管理) 目录项加载(文件系统) 处理对象 内存页(虚拟地址空间的最小单位) 目录项(文件名 + inode 编号的映射关系) 触发条件 访问虚拟地址时发现页表项未关联物理页 访问文件名时发现 dentry 缓存中无记录 数据来源 磁盘上的交换分区(swap)或可执行文件 磁盘上的目录文件(存储文件名 + inode 表) 内核组件 内存管理模块(MMU + 页表管理) VFS(虚拟文件系统)+ dentry 缓存机制 生命周期 内存页长期存在(直到被置换或释放) 目录项可能被动态删除(如文件关闭后) 目的 实现虚拟内存与物理内存的映射 建立 “文件名→inode” 的快速查找通道
九大模块之二:虚拟文件系统
文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)
VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可

根据存储位置的不同,可以把Linux支持的文件系统分为三类:
磁盘的文件系统:它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统
内存的文件系统:这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据
网络的文件系统:用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录
九大模块之三:文件的使用


我们咋使用文件?
先用 open 系统调用打开文件,open 的参数中包含文件的路径名和文件名
使用 write 写数据,其中 write 使用 open 所返回的文件描述符,并不使用文件名作为参数
使用完文件后,要用 close 系统调用关闭文件,避免资源的泄露
打开一个文件后,os会为每个进程维护一个打开文件表,里面每一项代表文件描述符

操作系统在打开文件表中维护着打开文件的状态和信息:(自己重新组织描述了下,小林说的太鸡巴der了)
文件指针:系统跟踪上次读写位置作为当前文件位置指针,这指针对打开文件的某个进程来说是唯一的,从当前位置继续
文件打开计数器:文件关闭时,操作系统必须重用其打开文件表条目,否则表内空间不够用。因为多个进程可能打开同一个文件,所以系统在删除打开文件条目之前,必须等待最后一个进程关闭文件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭文件,删除该条目
文件磁盘位置:为了修改文件后可以放回去;
访问权限:操作系统能允许或拒绝之后的 I/O 请求
操作系统的视角是如何把文件数据和磁盘块对应起来,所以,用户和操作系统对文件的读写操作是有差异的,用户习惯以 字节 的方式读写文件,而 OS 以 数据块 来读写文件,那屏蔽掉这种差异的工作就是文件系统了
当用户进程从文件读取 1 个字节大小的数据时,文件系统则需获取字节所在的数据块,再返回数据块对应的用户进程所需的数据部分
当用户进程把 1 个字节大小的数据写进文件时,文件系统则找到需要写入数据的数据块的位置,然后修改数据块中对应的部分,最后再把数据块写回磁盘
九大模块之四:文件的存储
文件数据存在磁盘的存储方式,有以下两种:
连续空间存放方式
非连续空间存放方式:「链表方式」和「索引方式」
重点分析他们的存储效率和读写性能:
1、连续空间存放方式
连续物理空间,读写效率高,一次磁盘寻道就可读出整个文件,但前提必须知道一个文件的大小,文件头需指定「起始块的位置」和「长度」

缺点:有「磁盘空间碎片」和「文件长度不易扩展」的缺陷。

想挪动把空闲的放一起,腾出更多空间,就比较耗时
A文件想要扩大,也要挪动,很耗时
2、非连续空间存放方式
「链表方式」:
离散,不连续,可以消除磁盘碎片,可提高磁盘空间的利用率,同时文件的长度可以动态扩展
实现方式不同,可以分为「隐式链表」和「显式链接」:
「隐式链表」:文件头要包含「第一块」和「最后一块」的位置,并且每个数据块里面留出一个指针空间,用来存放下一个数据块的位置

缺点:
无法直接访问数据块,只能通过指针顺序访问文件
数据块指针消耗了一定的存储空间
稳定性差,系统在运行过程中由于软件或者硬件错误导致链表中的指针丢失或损坏,会导致文件数据的丢失
解决:取出每个磁盘块的指针,整个磁盘仅设置这一张表,把它放在内存的一个表中,每个表项中存放链接指针,指向下一个数据块号,就可以解决上述隐式链表的不足,即
「显式链接」:

磁盘块是磁盘存储数据的基本单元,
0、1、2、3 等是 磁盘块编号,10、11、7 等是 指向下一个磁盘块的链接编号
文件 A 依次使用了磁盘块 4、7、2、10、12
文件 B 依次使用了磁盘块 6、3、11、14
内存中的这样一个表格称为文件分配表(File Allocation Table,FAT)
提高了检索速度,大大减少了访问磁盘的次数,但不利于大磁盘
如果 200GB 磁盘,和 1KB 大小的块,这张表需要 2亿 项,每一项对应2 亿个磁盘块中的一个块,每项如果需要 4 个字节,那这张表要占用 800MB 内存,详情计算如下:

显然 FAT 对于大磁盘不合适,引出索引
「索引方式」:
文件头包含指向「索引数据块」的指针,先通过文件头找到索引数据块,再依据其中的指针直接定位目标数据块,无需顺序遍历,支持随机访问
跟隐式的区别:隐式指向下一个数据块,索引是每个文件都有独立的索引,文件头指向该索引块,索引块里有指针,直接访问不同的数据块,不必有衔接啥的
跟显式的区别:显式集中放在内存一张表里,索引是每个文件都有独立的索引数据块
文件头是针对整个文件的属性信息(如文件类型、大小等),并非每个数据块都有
(感觉我理解里确实差点意思,小林这里写的挺清晰了,还是得问豆包,不然刚接触懵懵的)
创建文件时,索引块的所有指针都设为空。当首次写入第 i 块时,先从空闲空间中取得一个块,再将其地址写到索引块的第 i 个条目

优点:
文件的创建、增大、缩小很方便;
不会有碎片的问题;
支持顺序读写和随机读写;
缺点:
由于索引数据也是存放在磁盘块的,如果文件很小,明明只需一块就可以存放的下,但还是需要额外分配一块来存放索引数据,所以缺陷之一就是存储索引带来的开销
思考:
如果文件很大,大到一个索引数据块放不下索引信息,就用组合的方式
方案一:链表 + 索引的组合,这种组合称为「链式索引块」
它的实现方式是在索引数据块留出一个存下一个索引数据块的指针,于是当一个索引数据块的索引信息用完了,就可以通过指针找下一个索引数据块的信息。那这种方式也会出现前面提到的链表方式的问题,万一某个指针损坏了,后面的数据也就会无法读取了

方案二:索引 + 索引的方式,这种组合称为「多级索引块」
实现方式是通过一个索引块来存放多个索引数据块,一层套一层索引,像极了俄罗斯套娃是吧
我的思考:
为啥指针会丢,而索引不会?
解答:
链式索引中,索引块通过单一线性指针(指向下一索引块)连接,若某个指针损坏,后续索引块及数据就无法读取;
多级索引是分层嵌套的索引结构,不依赖这种单一线性指针链,某部分索引块问题不影响整体结构的查找,所以链式索引(链表 + 索引组合)因依赖易损的单指针
感觉有点牵强,算了不纠结了,无脑过吧
总结:

Unix 文件的实现方式
早期 Unix 文件系统是组合了这三种文件存放方式的优点

根据文件的大小,存放的方式会有所变化:
如果存放文件所需的数据块小于 10 块,则采用直接查找的方式;
如果存放文件所需的数据块超过 10 块,则采用一级间接索引方式;
如果前面两种方式都不够存放大文件,则采用二级间接索引方式
如果二级间接索引也不够存放大文件,这采用三级间接索引方式;
那么,文件头(Inode)就需要包含 13 个指针:
10 个指向数据块的指针;
第 11 个指向索引块的指针;
第 12 个指向二级索引块的指
第 13 个指向三级索引块的指针;
Linux Ext 2/3 文件系统里就是这个方案,虽然解决大文件的存储,但是对于大文件的访问,需要大量的查询,效率比较低
九大模块之五:空闲空间管理
文件的存储是针对已经被占用的数据块组织和管理,接下来的问题是,如果我要保存一个数据块,我应该放在硬盘上的哪个位置呢?难道需要将所有的块扫描一遍,找个空的地方随便放吗?
太低效率了
所以针对磁盘的空闲空间也是要引入管理的机制,接下来介绍几种常见的方法:
空闲表法
为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数
这个方式是连续分配的,如下图:

解释:
当请求分配磁盘空间时:系统依次扫描空闲表里的内容,直到找到一个合适的空闲区域为止
当用户撤销一个文件时:系统回收文件空间。这时,也需顺序扫描空闲表:
若释放空间与某空闲区相邻,便合并更新对应条目;
若不相邻,就找一个空白或合适条目,比如新开个序号5,然后将释放空间的首块号与块数填入,即把这个要回收的块号写到里面
这种方法仅当有少量的空闲区时才有较好的效果,不然空闲多,表太大,查询效率低
空闲链表法
每一个空闲块里有一个指针指向下一个空闲块,这样也能很方便的找到空闲块并管理起来

解释:
在主存中保存一个指针,令它指向第一个空闲块
当创建文件需要一块或几块时,就从链头上依次取下一块或几块。反之,当回收空间时,把这些空闲块依次接到链头上
点评:
其特点是简单,但不能随机访问,工作效率低
因为每当在链上增加或移动空闲块时需要做很多 I/O 操作,同时数据块的指针消耗了一定的存储空间
空闲表法和空闲链表法都不适合用于大型文件系统,因为这会使空闲表或空闲链表太大
位图法
利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应
当值为 0 时,表示对应的盘块空闲,值为 1 时,表示对应的盘块已分配

Linux 文件系统就采用了位图的方式来管理空闲空间,不仅用于数据空闲块的管理,还用于 inode 空闲块的管理,因为 inode 也是存储在磁盘的,自然也要有对其管理
仔细想想也有很多问题,就比如,这玩意起码得遍历吧? 遍历就耗时,算了,无脑过吧
九大模块之六:文件系统的结构
前面提到 Linux 是用位图的方式管理空闲空间:
用户在创建一个新文件时,Linux 内核会通过 inode 的位图找到空闲可用的 inode,并进行分配
要存储数据时,会通过块的位图找到空闲的块,并分配
但思考个事:
数据块的位图是放在磁盘块里的,假设是放在一个块里(放多个块会分散管理),一个块 4K,每位表示一个数据块,共可以表示 4 * 1024 * 8 = 2^15 个空闲块,由于 1 个数据块是 4K 大小,共可以表示 4 * 1024 * 8 = 2^15 个空闲块,由于 1 个数据块是 4K 大小
计算详情如下:

如果采用「一个块的位图 + 一系列的块」,外加「一个块的 inode 的位图 + 一系列的 inode 的结构」能表示的最大空间也就 128M,这太少了
在 Linux 文件系统,把这个结构称为一个块组,那么有 N 多的块组,就能够表示 N 大的文件
如下是 Linux Ext2 整个文件系统的结构和块组的内容,文件系统都由大量块组组成

超级块,包含的是文件系统的重要信息,比如 inode 总个数、块总个数、每个块组的 inode 个数、每个块组的块个数等等。
块组描述符,包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息」
数据位图和 inode 位图, 用于表示对应的数据块或 inode 是空闲的,还是被使用中
inode 列表,包含了块组中所有的 inode,inode 用于保存文件系统中与各个文件和目录相关的所有元数据
数据块,包含文件的有用数据
这里每个块都有重复信息,比如 超级块、块组描述符表,这两个都是全局信息,而且非常的重要,这么做是有两个原因:
如果系统崩溃破坏了超级块或块组描述符,有关文件系统结构和内容的所有信息都会丢失。那有冗余副本就可以恢复
通过使文件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提高文件系统的性能。意思是说如果丢了,可以附近直接获取,无需跨磁盘远距离查找这些全局信息
Ext2 的后续版本采用了稀疏技术。该做法是,超级块和块组描述符表不再存储到文件系统的每个块组中,而是只写入块组 0、块组 1 和其他 ID 可以表示为 3、 5、7 的幂的块组中
比如:
3^1、3^2、3^3、... ...
5^1、5^2、5^3、... ...
7^1、7^2、7^3、... ...
九大模块之七:目录的存储
我们知道了一个普通文件是如何存储的,但还有一个特殊的文件,经常用到的目录
普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息
目录文件的块中,最简单的保存格式就是列表,即一项一项地将目录下的文件信息(如文件名、文件 inode、文件类型等)列在表里
列表中每一项就代表该目录下的文件的文件名和对应的 inode,通过这个 inode,就可以找到真正的文件

第一项是「.」,表示当前目录,第二项是「..」,表示上一级目录,接下来就就是一项一项的文件名和 inode
如果一个目录有超级多的文件,我们要想在这个目录下找文件,按照列表一项一项的找,效率就不高
引入保存格式:哈希表
对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面
妈逼的这里问豆包追问好久,贼鸡巴抽象
假设我们有 5 个抽屉(在哈希表中称为 “桶”),编号为 0 - 4,放入10个文件,10 个文件名为 file1、file2、file3、…、file10:
当要查找 file6 时,计算其哈希值(6 % 5 = 1),系统会直接定位到 1 号抽屉去找,而非想列表一样从 file1 开始逐个检查。通过 “分桶” 大幅缩小了查找范围
Linux 系统的 ext 文件系统就是采用了哈希表,来保存目录的内容,这种方法的优点是查找非常迅速,插入和删除也较简单,不过需要一些预备措施来避免哈希冲突
目录查询是通过在磁盘上反复搜索完成,需要不断地进行 I/O 操作,开销较大。所以,为了减少 I/O 操作,把当前使用的文件目录缓存在内存,以后要使用该文件时只只要在内存中操作,从而降低了磁盘操作次数,提高了文件系统的访问速度
九大模块之八:软连接和硬链接
有时候我们希望给某个文件取个别名,Linux 中可以通过硬链接(Hard Link) 和软链接(Symbolic Link)来实现
1、硬链接:

硬链接的本质:硬链接是让多个目录项中的 “索引节点(inode)” 指向同一个文件,即指向同一个 inode。图中,/home/xiaolin 目录下的 file、/home/jay 目录下的 hardlink1、/tmp 目录下的 hardlink2 均指向 inode 100,表明它们通过硬链接关联到同一个文件
inode 的局限性:每个文件系统都有独立的 inode 数据结构和列表,inode 不能跨越文件系统,因此硬链接无法用于跨文件系统的场景
删除机制:由于多个目录项指向同一个 inode,只有当文件的所有硬链接(如 file、hardlink1、hardlink2)以及源文件被全部删除后,系统才会彻底删除该文件(即释放 inode 100 及其关联的数据块)
图里一个文件的数据若较多,一个数据块无法容纳时,就需要多个数据块来存储。
我的思考:
我之前刷题那个链接咋回事?完全不是一回事!
硬链接:是文件系统层面的概念
编译链接(编程中的链接):是程序开发编译阶段的操作,指将源代码编译生成的目标文件(.o 等)与库文件(静态库 .a 或动态库 .so 等)进行组合,生成可执行程序的过程,与文件系统中文件的多名称引用机制毫无关联
二、软链接
软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径
实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已

九大模块之九:文件I/O
文件的读写方式不同,I/O分类也非常多,
缓冲与非缓冲 I/O
文件操作的标准库是可以实现数据的缓存,那么根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O:
缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件
非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存
这里所说的「缓冲」特指标准库内部实现的缓冲
很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来,这样做的目的是,减少系统调用的次数,毕竟系统调用是有 CPU 上下文切换的开销的
直接与非直接 I/O
磁盘 I/O 非常慢,Linux 内核为了减少磁盘 I/O 次数,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是「页缓存」,只有当缓存满足某些条件的时候(比如缓存量过大、用户主动释放),才发起磁盘 I/O 的请求
(符号勘误)
根据「是否利用操作系统的缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O:
直接 I/O:
不会发生内核缓存和用户程序之间数据复制,而是直接经过文件系统(是操作系统中管理存储设备文件数据的软件层,比如read访问接口比如/组织文件结构)访问磁盘
流程:
你(用户程序)直接和磁盘 “面对面”,数据不经过内核缓存中转站,直接从磁盘读取或写入磁盘。
读数据:直接从磁盘读数据到你的程序内存
写数据:直接把你的程序数据写入磁盘,不经过内核缓存
非直接 I/O:
流程:
你(用户程序)要读写文件时,数据不会直接和磁盘 “打交道”,而是先经过一个 内核缓存中转站(页缓存)
读数据:磁盘先把数据 “搬” 到内核缓存,再由内核缓存 “传给” 你
写数据:用户程序先把数据 “交给” 内核缓存,内核缓存过一会儿(比如攒够一批数据、时间到了或内存紧张时)再统一写入磁盘
使用文件操作类的系统调用函数时,指定了 O_DIRECT 标志,则表示使用直接 I/O。如果没有设置过,默认非直接I/O
如果用了非直接 I/O 进行写数据操作,内核啥时候把缓存数据写入到磁盘?
在调用 write 的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上
用户主动调用 sync,内核缓存会刷到磁盘上
当内存十分紧张,无法再分配页面时
内核缓存的数据的缓存时间超过某个时间
阻塞与非阻塞 I/O VS 同步与异步 I/O
阻塞 I/O:当用户程序执行 read ,线程会被阻塞,直到内核数据准备好,并把数据从内核缓冲区,拷贝到 应用程序的缓冲区中,当拷贝过程完成,read 才会返回
注意:阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程

非阻塞I/O: read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果

TCPIP网络编程书里说过,不去强迫症翻看了~~~~(>_<)~~~~
这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
举个例子:
访问管道或 socket 时,如果设置了 O_NONBLOCK 标志,那么就表示使用的是非阻塞 I/O 的方式访问,不设置默认是阻塞 I/O
应用程序每次轮询内核的 I/O 是否准备好(循环 + 条件检查)期间啥也做不了,引入I/O多路复用(如 select、poll),它是通过 I/O 事件分发,当内核数据准备好时,再以事件通知应用程序进行操作
这里他只是说了一句,但我之前还啥也不会的时候,问豆包的时候记得说epoll挺重点的(唉,还是得看啊!!~~~~(>_<)~~~~)
不对,他后面讲了
这种做法提高了 CPU 利用率,当调用了 I/O 多路复用接口,如果没有事件发生,那么当前线程就会发生阻塞,这时 CPU 会切到其他线程执行任务,等内核发现有事件到来,会唤醒阻塞在 I/O 多路复用接口的线程,然后用户可以进行后续的事件处理
整个流程要比阻塞 IO 要复杂,似乎也更浪费性能。但 I/O 多路复用接口最大的优势在于,用户可以在一个线程内同时处理多个 socket 的 IO 请求
用户可以注册多个 socket,然后不断地调用 I/O 多路复用接口读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的
而同步阻塞模型(阻塞I/O)中,必须通过多线程的方式才能达到这个目的
啥JB玩意啊,看到一头雾水,~~~~(>_<)~~~~

妈逼的学的东西跟准备面试真的南辕北辙 学东西总想弄懂,现在那么多大厂员工搞培训,狗逼玩意,乌烟瘴气的简历注水,速成 学东西重在多看重复,理解,而如果为了面试,一直的方法就是错的,因为学的时候自己无法解释说出来,就禁止浪费时间去看,真他妈傻逼~~~~(>_<)~~~~ 对我来说 学东西没有一知半解 只有学没学完 水哥王昱珩:找不到、不会错 垃圾TinyMCE编辑器的home还总多个空格 无论微信还是其他只要这网页上开东西,一按esc立马编辑框都消失退出 这个编辑器是用阳寿阳寿的吗? TinyMCE妈逼的右边的滚动条还总没,整个右边都显示不全 TinyMCE5保存的时候,啥也不能懂,只要点击就立马跳到做末尾,整个网页还跟着缩放一下在,哎,心酸 妈逼的每次用项目符号,编号列表就是三个点来分每行那个,行太密集,所有都得挨个改段落,全选只要选到一行已经是大间距段落的,就默认是大间距段落,实际选中的90%妈逼的都是小间距,哎标题也是
View Code
先问下豆包吧:(给我干精神分裂了,强迫总结这些异常痛苦)
线程(顾客):拿着订单排队的人,每个订单对应一个任务(如 IO 请求)
CPU(服务员):负责 “执行订单逻辑” 的人,同一时间只能处理一个订单(线程),但可快速切换订单(时间片轮转)
内核 / 驱动(后厨):真正处理数据读取 / 写入的 “厨房”,不占用 CPU 资源,负责从磁盘 / 网络取数据(做菜)
线程状态:
运行态:服务员正在处理该订单(线程占用 CPU)
阻塞态:订单需要等后厨做菜(数据未就绪),服务员暂时放下该订单,去处理其他订单
就绪态:订单已准备好(数据就绪),等待服务员处理
服务员(CPU)处理顾客 A 的订单(线程 1),订单需要读取数据(让后厨做菜):
顾客 A 告诉服务员:“我要读数据,请后厨处理”(调用read)。
服务员向后厨下单,后厨说:“菜没做好,等通知”(数据未就绪)。
关键操作:
服务员将顾客 A 的订单放入 “后厨等待队列”(线程进入阻塞态,内核记录等待事件)
服务员立即回到订单队列,从 “就绪订单” 中选下一个顾客 B(线程 2)处理(CPU 调度其他线程)
核心结论:
阻塞 IO 中,线程本身不占用 CPU(服务员没被顾客 A “卡住”),而是主动让出 CPU,让操作系统调度其他线程
CPU(服务员)从未 “等待”,而是一直在处理就绪队列中的其他订单(线程)
顾客 A 的订单设置为 “非阻塞”:
服务员每处理完一个订单(如顾客 B),就立刻回头问顾客 A:“后厨菜好了吗?”(调用非阻塞recv)。
若菜没好(返回EWOULDBLOCK),服务员又立刻去问顾客 C、D…… 循环 10 个顾客(10 个 socket)。
问题本质:
服务员(CPU)在无数据时,主动把时间片浪费在 “无效查询” 上(每次查询都需 CPU 执行指令)。
虽然每个查询很快,但 10 个顾客循环查,CPU 被大量无效指令占用(类似服务员在 10 个顾客间来回跑腿问 “好了吗”,无法干正事)。
四、IO 多路复用的精髓:服务员委托 “前台小妹”(内核)统一管理等待订单,自己只处理就绪的场景:引入 “前台小妹”(内核提供的select/epoll机制):
服务员把 10 个顾客的订单全交给前台小妹,告诉她:“任何一个菜做好了,立刻叫我”(注册事件监听)。
服务员当前没有就绪订单可处理时:
不是 “阻塞” 或 “等待”,而是主动告诉操作系统:我现在没事做,可以把我从 CPU 运行队列中移除(线程进入睡眠态,不参与 CPU 调度)。
此时 CPU(服务员)去处理其他程序的订单(如系统任务、其他进程的线程),完全不占用资源。
当某顾客的菜做好(socket 数据就绪),前台小妹唤醒服务员,服务员只处理这一个订单(避免遍历 10 个顾客)。
与阻塞 IO 的本质区别:
阻塞 IO 是 “单个订单阻塞服务员,但服务员会去处理其他订单”;
IO 多路复用是 “批量订单委托给前台,服务员在无任务时彻底休息(不占 CPU)”。
线程阻塞时,该线程的 CPU 时间片被释放,操作系统从 “就绪线程队列” 中选取其他线程运行(类似服务员处理完一个订单,按队列顺序取下一个)。
CPU(服务员)从未 “等待数据”,而是一直在执行 “就绪线程” 的指令(处理其他订单)。
| 模型 | 线程状态(顾客订单) | CPU 行为(服务员动作) | CPU 利用率(无数据时) |
|---|---|---|---|
| 阻塞 IO | 订单 A 进入 “后厨等待队列”(阻塞态)。 | 服务员立刻处理订单 B(就绪态线程),订单 A 不占 CPU。 | 100%(CPU 在处理其他线程)。 |
| 非阻塞 IO | 订单 A 处于 “轮询查询态”(非阻塞,循环调用)。 | 服务员每处理完一个订单,就回头问订单 A~J “好了吗”(无效 CPU 指令)。 | 100%(全耗在无效查询)。 |
| IO 多路复用 | 订单 A~J 注册到 “前台监听”,线程进入睡眠态。 | 无就绪订单时,服务员被操作系统标记为 “空闲”,CPU 去处理其他程序(如浏览器、后台服务)。 | 0%(线程不参与 CPU 调度)。 |
深入:
核心区别:
阻塞 IO 和 IO 多路复用的 “阻塞” 都是线程状态(睡眠 / 挂起),不占用 CPU;
非阻塞 IO 的 “非阻塞” 是线程主动忙等,浪费 CPU;
所有模型CPU 永远在执行 “就绪态线程” 的指令,阻塞的线程根本不在 CPU 的 “运行队列” 里

系统、CPU、时间片、阻塞睡眠挂起这些词一团乱麻,谁和谁关联我都不知道 但现在我说下我的理解: 比如线程叫haha,线程的CPU利用自己分到的时间片去干活对吧?然后阻塞IO是,比如10个活,总共1~10编号,处理1的时,1没准备好,此时线程haha的CPU时间片释放,处理1的这个haha线程被挂起睡眠,不占用CPU,此时CPU干嘛?是去处理下一个就绪的2对吧?可是不是说haha线程被挂起了吗?咋还能处理?是又开了一个?我们重点不是解决什么实际问题!因为会了自然就解决了!重点就是怎么能会!这里就说10个活,只开一个是不是1号阻塞导致这个haha线程挂起睡眠了,CPU分给这个线程的时间片也就释放了 非阻塞是不是说,线程比如叫haha,先处理1,没准备好,这时候,线程不睡眠挂起,而是处理2,然后回头询问1? 这个询问和处理2,都是CPU一直在干活做事?CPU指导线程做事吗?这里头的最基础的逻辑关系是这样吧? 然后IO多路复用,线程haha睡眠,CPU做其他类型的事,不处理这10个东西了,然后比如2号好了,CPU就唤醒haha,来做2 ?
View Code 原汁原味的,我在豆包上的提问
工人处理工单 1:发现没原料(数据未就绪)→ 放下工单 1,将其放入 “等待原料区”(线程进入阻塞态)。
工人立即转向工单 2:继续处理其他就绪工单(执行其他线程),全程无休息(CPU 利用率 100%)。
工单 1 原料到货后:调度员(内核)将工单 1 重新放入 “待处理区”,等待工人下次轮到它时处理。
阻塞的是工单(线程),不是工人(CPU)。工人在工单 1 等待时,立即去处理其他工单,不会 “卡在” 工单 1 上
与非阻塞 IO 的本质区别:
阻塞 IO:工人发现工单 1 没原料,直接放下,不检查后续工单,专注处理其他就绪工单(如工单 2)。
非阻塞 IO:工人发现工单 1 没原料,不放下,而是继续检查工单 2~10,导致 CPU 空转(忙等)
操作系统的线程调度队列:
就绪队列:存放 “数据已就绪、等待 CPU 执行” 的线程(如工单 2~10 中已准备好原料的工单)。
阻塞队列:存放 “数据未就绪、等待内核通知” 的线程(如工单 1)。
阻塞 IO 的流程:
步骤 1:CPU 处理线程 1(工单 1),调用read发现数据未就绪 → 内核将线程 1 从就绪队列移动到阻塞队列(无需 CPU 参与)。
步骤 2:CPU 直接从就绪队列中取下一个线程(如线程 2),该线程已被内核标记为 “数据就绪”(工单 2 原料已到),无需 CPU 再次检查。
核心逻辑:就绪队列中的线程一定是数据就绪的,因为内核在数据就绪时(如磁盘读取完成),会主动将线程从阻塞队列移到就绪队列,并触发调度。
非阻塞 IO 的流程:
线程 1 调用非阻塞recv,数据未就绪 → 返回EWOULDBLOCK → 线程 1 仍在就绪队列中(未被移到阻塞队列)
假设你开了一家餐厅,有 10 个厨师(线程)在厨房(就绪队列)里待命,负责处理订单(IO 操作)
阻塞 IO(传统模式):
厨师接到订单后,发现食材未准备好(数据未就绪),会主动去休息室(阻塞队列)睡觉,直到食材备好(内核通知)才被叫醒(唤醒到就绪队列)。
优点:厨师不占用厨房(CPU),省电(资源利用率高)。
缺点:若多个订单都需等待食材,厨师全在休息室睡觉,厨房空转(无法处理其他订单)。
非阻塞 IO(叛逆模式):
厨师接到订单后,发现食材未准备好,不睡觉!直接告诉老板(返回EAGAIN),继续留在厨房(就绪队列):
正确操作:厨师转身去切菜、擦桌子(执行其他逻辑),等老板通知(IO 多路复用,如epoll)食材备好后再处理订单。
recv),什么都不做,浪费厨房空间(CPU 资源)。内核设计决定:
非阻塞 IO 的系统调用(如recv)不会主动让线程进入阻塞队列,只是告诉线程 “数据未就绪”,线程自己决定下一步做什么(继续干活 or 傻等)。
阻塞 IO:内核强制线程去休息室(阻塞队列)→ 不占 CPU。
非阻塞 IO:内核不管线程,线程留在厨房(就绪队列)→ 可被 CPU 调度执行。
“就绪队列中的未就绪线程” 是啥?
这里的 “未就绪” 不是线程状态,而是IO 操作的状态(数据未就绪)。
线程本身始终处于就绪状态(在厨房),只是它要处理的 IO 任务没准备好。
误区:误以为 “线程未就绪”,其实是IO 任务未就绪,线程状态还是就绪(随时能被 CPU 调度)。
1. 内核层面(线程状态)
阻塞队列:线程因等待事件(如
IO 数据)被内核挂起,不参与 CPU 调度。2. 应用层面(你的代码逻辑)
非阻塞 IO 的正确用法:
// 用epoll让内核替你监控,线程去“休息”(阻塞队列)
epoll_wait(epfd, events, max_events, -1); // 线程主动进入阻塞队列
// 内核通知数据就绪后,线程被唤醒(回到就绪队列),再调用recv
核心:线程通过epoll_wait主动进入阻塞队列(去休息室),等内核通知数据就绪后,再被唤醒处理 IO(高效,不忙等)。
非阻塞 IO 的错误用法(忙等):
// 错误!无意义的循环检查(忙等)
while (recv(fd, buffer, sizeof(buffer), 0) == EAGAIN) {
// 线程持续占用CPU,反复执行无效检查
// 相当于厨师在厨房傻站着反复问“食材好了吗?”
}
后果:线程一直留在就绪队列(厨房),CPU 被迫执行循环代码,浪费资源(厨师不干活,占着厨房)
这种错误是程序员造成的啊?但为啥要提及这个啊!!就比如for,你程序员误写成fr这也能拿来当例子讲吗?
当你把 socket 设为非阻塞时,内核规定:
recv数据未就绪时,立刻返回错误码(如EAGAIN),
但:不将线程从就绪队列移到阻塞队列(线程不会被内核挂起)。
这是内核的设计逻辑,与程序员怎么写代码无关。
关键:此时线程的状态是 就绪(可被 CPU 调度),但 IO 操作未就绪(数据没到)。
内核只负责返回状态,线程接下来做什么,由程序员的代码决定:
类比:内核说 “菜没好”,程序员却让厨师站在原地反复问 “菜好了吗”,浪费厨房空间(CPU)。
核心逻辑:程序员利用内核提供的 多路复用机制,让线程在数据未就绪时 主动阻塞(进入阻塞队列),避免忙等。
类比:内核说 “菜没好”,程序员让厨师去擦桌子(执行其他任务)或去休息室(阻塞队列),等内核喊 “菜好了” 再回来。
误区根源:
很多人误以为 “非阻塞 IO = 循环调用 recv”,甚至认为 “非阻塞 IO 就是浪费 CPU”。
但实际上:非阻塞 IO 本身不浪费 CPU,浪费 CPU 的是程序员写的忙等循环。
举个极端例子:
就像你学for循环时,老师不会特意讲fr是错的,
但会讲 “for(;;)死循环会导致程序卡死”——
不是语法错误,而是逻辑错误,属于 “正确语法的错误用法”。
非阻塞 IO 的忙等同理:语法正确,但逻辑错误,是必须澄清的常见坑
所以理解之后,知道,非阻塞模式下,线程不会自动进入阻塞队列,但接下来这句话就是错的:CPU 需主动循环调用recv检查线程 1~10,因为就绪队列中可能存在 “未就绪线程”
解释:CPU 不主动循环调用任何函数,而是 线程自身执行忙等逻辑while,即程序员代码用的也就是应用层设计缺陷
这里说程序员的错误,while那个就是没优化的非阻塞IO。然后epoll_wait的是优化的多路复用
“不检查咋知道就没就绪?!”
阻塞 IO 中:
内核替 CPU 检查!数据未就绪时,内核直接将线程移到阻塞队列,CPU 无需关心。
CPU 从就绪队列取线程时,默认该线程已就绪(否则内核不会将其放入就绪队列)。
类比:工厂调度员(内核)提前检查工单原料,原料到了才将工单放入 “待处理区”(就绪队列),工人(CPU)只需按顺序处理即可,无需自己检查。
非阻塞 IO 中:
没有调度员提前检查!所有工单都在 “待处理区”(就绪队列),但可能没原料。
工人(CPU)必须自己逐个检查工单 1~10 的原料,导致忙等(如工单 1 没原料,工单 2 可能也没,但 CPU 仍需检查)。
| 模型 | 谁负责检查数据就绪? | CPU 是否参与就绪检查? | 就绪队列中是否有未就绪线程? |
|---|---|---|---|
| 阻塞 IO | 内核(调度员)在数据就绪时自动移动线程 | 否(CPU 只处理就绪队列中的线程) | 否(就绪队列全是已就绪线程) |
| 非阻塞 IO | 线程主动调用非阻塞函数检查 | 是(CPU 执行recv/send检查) |
是(线程未就绪时仍在就绪队列) |
这就是阻塞 IO 与非阻塞 IO 在 “是否需要检查就绪” 上的根本区别 —— 前者由内核自动处理,后者由 CPU 暴力轮询
再继续
异步IO调度员和阻塞IO那个内核调度员是一个?
| 概念 | 类比角色 | 所属 “部门” | 核心职责 | 是否涉及 CPU 时间片 |
|---|---|---|---|---|
| 内核线程调度器 | 工厂车间主任 | CPU 资源管理部门 | 决定哪个工人(CPU 核心)该处理哪张工单(线程) | 是(分配时间片) |
| IO 多路复用调度员 | 原料仓库调度员(如 select) | IO 事件监听部门 | 帮工人盯着多张工单的原料到货状态 | 否(仅监听 IO 状态) |
场景:工人处理工单 1 时遇到原料阻塞
工人(CPU)正在处理工单 1(线程 haha):
发现工单 1 原料未就绪(如 socket 数据未到达)→ 线程 haha 进入阻塞状态(标记为TASK_UNINTERRUPTIBLE)。
车间主任(内核调度器)介入:
判定线程 haha 无法继续工作 → 回收其 CPU 时间片,将工人调度到工单 2(就绪的线程 hehe)→ 这是 CPU 资源的切换。
关键点:
阻塞的是线程(工单处理暂停),工人(CPU)并未空闲,而是去处理其他就绪工单 → CPU 时间片被释放给其他线程。
车间主任的职责是线程在 CPU 上的执行权分配,与 IO 是否就绪无关,仅关注线程是否处于 “可运行” 状态。
场景:工人让调度员同时盯 10 张工单的原料状态
工人(CPU)提交工单 1-10 给仓库调度员:
调用select/epoll → 仓库调度员(内核 IO 模块)开始监控所有工单的原料通道(如文件描述符)。
工人此时的状态:
主动阻塞自己(坐在调度员旁边等待叫号)→ 不占用 CPU 时间片(车间主任会将工人分配给其他线程,除非当前线程是唯一就绪的)。
或不阻塞(非阻塞模式下继续处理其他逻辑,但需轮询调度员结果,类似 “边干活边问调度员有没有到货”)。
原料到货时:
仓库调度员发现工单 2 原料就绪 → 通知工人 → 工人主动调用read处理工单 2(自己去仓库取货)。
关键点:
仓库调度员不涉及 CPU 时间片分配,仅负责IO 事件的状态监听(类似快递分拣员通知取件)。
工人是否阻塞取决于调用方式:
select默认阻塞 → 工人暂停工作,等待通知(车间主任可能调度其他工人干活)。
非阻塞 + 轮询 → 工人持续询问调度员(忙等,浪费 CPU)。
| 维度 | 内核线程调度员(车间主任) | IO 多路复用调度员(仓库调度员) |
|---|---|---|
| 管理对象 | 线程(工单处理任务) | IO 事件(工单的原料状态) |
| 触发条件 | 线程进入阻塞 / 就绪状态 | IO 事件就绪(如 socket 可读) |
| 对 CPU 的影响 | 决定 CPU 时间片分配(工人切换工单) | 不直接影响 CPU,仅通知 IO 状态 |
| 典型场景 | 阻塞 IO 时切换到其他线程 | 同时监控多个 IO,避免线程频繁阻塞 / 唤醒 |
| 类比动作 | “工单 1 停工,工人去做工单 2” | “工单 2 原料到了,通知工人来取” |
误区 1:认为两者都是 “调度员”,所以职责相同
纠正:前者是CPU 资源的 “任务分配者”,后者是IO 状态的 “消息通知者”。
类比:车间主任决定 “谁该干活”,仓库调度员决定 “哪些活可以干了”。
误区 2:阻塞 IO 和 IO 多路复用中的 “阻塞” 是同一概念
纠正:
阻塞 IO 中的阻塞:线程因 IO 未就绪被内核调度器暂停,CPU 去处理其他线程(车间主任强制换工单)。
IO 多路复用中的阻塞:线程主动等待仓库调度员通知(工人自己坐在调度员旁边等叫号,可能占用或不占用 CPU)。
误区 3:认为 IO 多路复用能减少 CPU 切换
部分正确:
当多个 IO 事件可批量处理时,减少线程因单个 IO 阻塞导致的 CPU 切换(如 10 张工单中只有 2 张就绪,工人无需为每个工单单独切换)。
但本质上仍是同步模型,工人仍需主动处理 IO(自己取货),而异步 IO(外卖员模式)可完全释放工人。
先看是否涉及 CPU 时间片:
涉及 → 内核线程调度器(车间主任),处理线程在 CPU 上的运行状态(如阻塞 IO 导致线程切换)。
不涉及 → IO 多路复用调度员(仓库调度员),处理 IO 事件是否就绪(如 select 监听 socket)。
再看阻塞的主体:
线程被内核强制阻塞(如read阻塞)→ 车间主任调度其他线程。
线程主动等待事件(如select阻塞)→ 仓库调度员通知后,线程恢复运行(可能需车间主任重新分配时间片)。
通过 “部门分工” 理解:CPU 资源管理和 IO 事件监听是内核的两个独立模块,前者决定 “谁在用 CPU”,后者决定 “哪些 IO 可以处理”,两者协同工作但职责完全不同
我的疑问:
IO多路复用不是对非阻塞不停轮询的优化吗?咋IO多路复用也要轮询呢??? 多路复用是不是把非阻塞这个CPU的任务不停轮询,换用select/epoll这些不占用CPU的来做了
一、先回答你的核心质疑:IO 多路复用与非阻塞 IO 的轮询关系
用户空间轮询:在非阻塞 IO 模型中,
线程(用户空间的 “工人”)需要主动循环调用read/write等系统调用,不断询问内核 “数据准备好了吗?”。
举例:线程处理 10 个工单(文件描述符 FD1~FD10),每次对 FD1 调用read发现没数据,立刻调用 FD2 的read,依此类推,全程占用 CPU 不断轮询,直到某个 FD 返回数据。
问题:CPU 被无效轮询浪费,且线程需管理所有 FD 的状态,复杂度高。
内核代劳轮询:IO 多路复用(如select/epoll)的核心是让内核帮忙监控多个 FD 的状态,用户空间的线程只需调用一次系统调用(如epoll_wait),阻塞等待内核通知哪些 FD 就绪。
对比非阻塞 IO:
非阻塞 IO:用户线程在用户空间主动轮询所有 FD(CPU 忙等)。
IO 多路复用:用户线程被动等待内核通知,内核在内部通过某种机制(如select的遍历检查、epoll的事件驱动)管理 FD 状态,用户空间不再轮询,仅在收到内核通知后处理就绪的 FD。
关于 “轮询” 的误解:
select/poll内核实现确实需要遍历检查所有注册的 FD(类似轮询),但这是内核空间的操作,用户空间的线程无需参与,减少了用户态的 CPU 浪费。
epoll通过事件驱动机制(内核维护就绪队列),无需遍历所有 FD,彻底避免了轮询开销,是更高效的多路复用实现。
结论:
IO 多路复用是对 “非阻塞 IO 用户空间轮询” 的优化,将轮询逻辑从用户空间转移到内核空间,并通过epoll等机制进一步消除内核轮询。
你之前的理解 “换用 select/epoll 这些不占用 CPU 的来做” 正确,但需注意:select的内核实现仍有遍历开销,而epoll没有,这是多路复用内部的优化细节。
阻塞 IO vs 非阻塞 IO:
阻塞 IO 中,线程被内核挂起(睡眠),CPU 释放给其他线程(如工单 1 阻塞,CPU 去处理工单 2 的线程)。
非阻塞 IO 中,线程不挂起,疯狂轮询,CPU 被该线程持续占用(工人不休息,一直问每个工单)。
IO 多路复用如何优化非阻塞 IO:
非阻塞 IO 需要线程自己管理所有 FD 的轮询,而 IO 多路复用通过内核统一管理 FD 状态,线程只需等待内核通知,用一个线程处理多个 FD,减少线程数量和上下文切换。
类比:
非阻塞 IO:多个工人各自轮询自己的工单,CPU 忙不过来。
IO 多路复用:一个工人配一个调度员(内核),调度员帮工人盯着所有工单,哪个好了喊工人去处理,工人无需自己轮询。
异步 IO 与多路复用的本质区别:
IO 多路复用仍是同步模型:线程调用epoll_wait后阻塞,等待内核告知 “哪些 FD 可读”,然后主动读取数据。
异步 IO 是真正的异步模型:线程调用aio_read后直接返回,内核负责读取数据并回调线程(无需线程主动读)
调度员的角色:内核在不同模型中的作用
所有 IO 模型的调度员都是内核,但职责不同:
阻塞 IO:内核发现 FD 未就绪时,将线程放入等待队列(挂起),CPU 切换到其他线程;FD 就绪后,内核唤醒线程。
非阻塞 IO:内核收到read请求后,立即返回未就绪状态,不挂起线程,线程需自行决定下一步(继续轮询或做其他事)。
IO 多路复用:内核通过select/epoll集中管理多个 FD 的就绪状态,线程调用epoll_wait时,内核阻塞线程,直到至少一个 FD 就绪,然后通知线程。
异步 IO:内核收到异步请求后,独立完成数据读取,数据到达后通过信号或回调通知线程。
区分关键:
阻塞 / 非阻塞描述的是线程调用系统调用时是否立即返回(是否阻塞线程)。
多路复用描述的是用一个线程管理多个 FD 的就绪通知(内核帮忙调度 FD)。
异步 IO 描述的是内核完全接管 IO 操作(包括数据拷贝),线程无需参与。
阻塞 IO:工人等工单 1 原料→被调度员(内核)挂起→CPU 去忙其他工人。
非阻塞 IO:工人不停问工单 1~10→全程占用 CPU(忙等)。
IO 多路复用(epoll):工人让调度员(内核)盯着工单 1~10→自己睡觉→调度员喊 “工单 2 好了”→工人去处理(内核替工人轮询,用户空间无轮询)。
异步 IO:工人下单 “工单好了喊我”→去干别的→调度员(内核)自己搬原料→搬完喊工人(全程无等待 / 轮询)。
核心对比:
| 模型 | 线程是否阻塞 | 轮询位置 | 效率瓶颈 |
|---|---|---|---|
| 阻塞 IO | 是 | 无 | 线程数量限制 |
| 非阻塞 IO | 否 | 用户空间 | CPU 轮询浪费 |
| IO 多路复用 | 是(等待时) | 内核空间(select 有,epoll 无) | 内核 FD 管理效率(epoll 最优) |
| 异步 IO | 否 | 无 | 系统支持程度(如 Linux 的 io_uring) |
忙等是不停询问的意思
我的疑问:
如果是内核的话不是跟阻塞IO的那个一样了吗! 阻塞IO不就是内核通知的吗!
阻塞 IO 和 IO 多路复用的内核作用不同:
多路复用:内核批量监控多个 FD,主动告知线程哪些就绪(类似批量通知)
说异步IO
| 维度 | IO 多路复用(同步 IO 模型) | 异步 IO(异步 IO 模型) |
|---|---|---|
| 核心定义 | 应用程序主动轮询内核,获取多个 FD 的就绪状态,自己处理 IO 操作。 | 应用程序告知内核目标操作(如读 / 写),内核独立完成全流程(数据读取 + 复制到用户空间),完成后通知应用。 |
| 同步 / 异步划分 | 同步:应用程序需主动参与 IO 操作的每一步(查询就绪→读取数据)。 | 异步:应用程序仅发起请求,无需参与 IO 过程,内核完成后通过回调 / 信号通知结果。 |
| 模型 | 阻塞 IO | 非阻塞 IO + 轮询 | IO 多路复用(同步) | 异步 IO |
|---|---|---|---|---|
| 内核通知内容 | “数据已到,来取” | “数据未到”/“数据已到” | “哪些 FD 可以操作了” | “操作已完成,结果在 X” |
| 应用程序职责 | 阻塞等待后取数据 | 循环查询 + 取数据 | 查询就绪列表 + 取数据 | 发起请求 + 处理结果 |
| 典型系统调用 | read |
read+ 循环 |
epoll_wait+read |
aio_read+ 回调函数 |
| 适用场景 | 单连接低并发 | 低效,几乎不用 | 多连接高并发(如 Nginx) | 高 IO 密集型(如文件异步读写) |
误区 1:认为多路复用是 “异步”
错!多路复用仍属于同步 IO 模型,因为应用程序必须主动根据内核的就绪通知执行 IO 操作(类似 “你问快递员‘我的包裹到了吗’,快递员说‘到了’,你再自己去取”)。
异步 IO 是 “快递员直接把包裹放到你家,然后发短信告诉你”。
误区 2:认为 “非阻塞 IO + 多路复用 = 异步”
错!非阻塞 IO 只是让read调用不阻塞,但应用程序仍需轮询检查 FD 状态(浪费 CPU),而多路复用通过内核批量查询优化了轮询效率,但本质仍是同步操作(需主动处理 IO)。
异步 IO 的核心是内核替你完成 IO 操作,无需任何轮询或主动调用read
核心差异:IO 操作的 “体力活” 由谁完成 ——多路复用中你自己干(同步),异步 IO 中内核干(异步)。
| 场景 | 非阻塞 IO 的用户空间轮询 | IO 多路复用的内核空间监控 |
|---|---|---|
| 执行者 | 应用程序线程(用户空间) | 内核(内核空间) |
| 实现方式 | 线程循环调用read检查每个 FD 状态(如:while (read(fd) == EAGAIN)) |
内核通过数据结构(如 epoll 的红黑树)管理 FD,仅在状态变化时记录事件 |
| CPU 占用 | 持续消耗 CPU(线程未阻塞,循环执行指令) | 不消耗应用程序 CPU(内核在内核空间异步处理,应用线程调用epoll_wait时阻塞并释放 CPU) |
| 典型错误认知 | 认为 “多路复用的内核监控 = 用户轮询” | 内核监控是操作系统底层优化的被动监听,非主动循环查询 |
select/poll 的 “轮询” 本质
内核维护 FD 列表,调用select时遍历所有 FD 检查状态(类似线性扫描)。
这是内核空间的轮询,但相比用户空间轮询有两大优势:
✅ 一次系统调用批量处理(用户空间需多次read);
✅ 内核态轮询效率更高(直接访问内核数据结构,无需用户态 - 内核态频繁切换)。
epoll 的 “事件驱动” 机制(无轮询)
内核使用红黑树管理 FD,事件就绪时通过链表主动通知(类似 “信号触发”)。
调用epoll_wait时,内核仅返回就绪事件列表,无需遍历所有 FD(时间复杂度 O (1) vs select 的 O (n))。
结论:epoll没有轮询行为,而是基于操作系统的异步事件通知机制。
| 操作阶段 | IO 多路复用(同步) | 异步 IO(异步) |
|---|---|---|
| 1. 监控阶段 | 内核被动监控 FD 状态(等数据就绪) | 内核主动执行 IO 操作(读 / 写数据) |
| 2. 数据处理阶段 | 应用程序主动调用read读取数据 |
内核自动将数据复制到用户空间 |
| 关键判断标准 | IO 操作是否由应用程序主动触发 | IO 操作是否由内核自动完成 |
多路复用的 “同步性” 体现在:即使内核帮你监控了 FD,读取数据的read调用仍需应用程序主动执行(类似 “你让快递员帮你盯着快递到没到,快递到了后你还是得自己去取”)。
异步 IO 的 “异步性” 体现在:内核不仅盯着快递,还直接把快递放到你家(数据从内核缓冲区→用户缓冲区完成后才通知你)。
多路复用的内核监控 ≠ 用户空间轮询
(前者是内核底层优化的状态管理,后者是用户线程的无效循环)
epoll 等机制 ≠ 轮询
(select 是轮询,epoll 是事件驱动,无轮询行为)
内核替你监控 FD ≠ 替你做 IO 操作
(多路复用只解决 “何时可以做 IO”,不解决 “如何做 IO”,后者仍是应用程序的责任)
非阻塞 IO + 用户轮询:
你每 1 分钟打一次电话问快递员 “我的快递到了吗”(每次调用read,CPU 忙等)。
IO 多路复用(select):
你给快递员留了个地址,说 “快递到了就记下来”,然后每小时打电话问快递员 “哪些快递到了”(内核轮询记录状态,你批量处理)。
IO 多路复用(epoll):
快递员装了个智能门铃,快递到了就按铃通知你(内核事件驱动,无需轮询)。
异步 IO:
你雇了个保姆,告诉她 “快递到了就帮我取回来放在客厅”,保姆取完快递后告诉你 “快递已放客厅”(内核完成完整 IO 流程,你直接用数据)。
最终结论:
IO 多路复用的核心优化是将 FD 状态监控从用户空间转移到内核空间,通过内核的高效数据结构(如 epoll 的红黑树 + 事件队列)避免用户线程的无效轮询,但数据读取仍需应用程序主动执行,因此属于同步 IO 模型。“内核轮询” 仅适用于 select/poll 的实现细节,epoll 等高级机制已无轮询行为,且无论哪种实现,内核的工作都是 “通知何时能做 IO”,而非 “替你做 IO”,这是与异步 IO 的本质区别
哎,这豆包答的我看的直想吐~~~~(>_<)~~~~
妈逼的我不知道是不是又学深了~~~~(>_<)~~~~
之前啃TCPIP网络编程只是“自欺欺人+强迫症+很贬义词的执着”强行看懂了书上写的,但依旧不是很懂,甚至一度以为非阻塞IO才是牛逼的。亦或是书不怎么好?
现在连环追问豆包才发现,原来阻塞IO才是好的,非阻塞是很垃圾的哎,自学真的学东西真的心酸血泪

第一:找不到方向,陷入死胡同,钻牛角尖,仅对我这种钻研细心好研究的
那些应付应试面试的那群傻逼垃圾狗不算
学的很多不考,不是面试问的,哎,冷门算法,啃书
第二:真的无奈,找不到对的资料,很多夹杂这错的,还得花费大力气辨别
第三:乌烟瘴气的网络环境就业面试,真的烂粪坑、无数次心灰意冷绝望,但这点我的经历绝境可以自己克服,给自己打气。自制力我很强
第四:赚钱钓鱼的课导致自己很难速成,只能自己啃书,不屑看视频
View Code
妈逼的我可以写书了
继续回到小林coding网站看
如下图是使用select I/O 多路复用过程
read 获取数据的过程(数据从内核态拷贝到用户态的过程),也是一个同步的过程,需要等待

由于read调用时,内核将数据从内核空间拷贝到应用程序空间,过程都是需要等待的,也就是说这个过程是同步的,所以无论阻塞/非阻塞的IO,还是非阻塞的IO多路复用都是同步调用。如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间
异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待
发起 aio_read 之后,就立即返回,内核自动将数据从内核空间拷贝到应用程序空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作


I/O核心本质:数据在内存与外部设备之间的流动(输入:外部→内存;输出:内存→外部)。
I/O 是分为两个过程的:
数据准备的过程
数据从内核空间拷贝到用户进程缓冲区的过程
|
IO 模型 |
阶段 1(数据从外部设备到内核) |
阶段 2(内核→用户) |
线程是否阻塞等待 |
典型系统调用 |
|---|---|---|---|---|
|
阻塞 IO |
阻塞等待 |
阻塞等待 |
是(全程) |
|
|
非阻塞 IO |
立即返回( |
立即返回(需轮询) |
否(忙等) |
|
|
多路复用 |
内核通知就绪 |
阻塞调用 |
仅在 |
|
|
异步 IO |
内核自动完成 |
内核自动完成 |
否(全程不阻塞) |
|
同步 IO(阻塞 / 非阻塞 / 多路复用):
阶段 2 必须由应用程序主动调用read触发,且线程需等待拷贝完成。
异步 IO:内核完成全部 IO 流程(阶段 1+2),应用程序仅接收结果通知。
解读下多路复用:
内核通知就绪:通过epoll_wait等,内核是负责阶段 1 的就绪告知,通知应用(如 socket)数据已到内核缓冲区(阶段 1 完成)
read处理拷贝:应用需要执行代码里的read,此时阻塞等待数据从内核空间→用户空间(阶段 2),直到拷贝完成。
小林的例子:

这个例子中多路复用 “挨个问” 的设计,对应 select 机制的缺陷,已经很少用,只是入门对比(而非所有多路复用):
监控阶段:内核记录所有待监控的 FD(窗口),调用 select 时 阻塞等待(阿姨通知)。
就绪通知:内核仅返回 “有 FD 就绪”(知道有窗口菜好了,但不明确具体是哪个)。
遍历检查:应用程序 必须遍历所有监控的 FD(挨个窗口看),才能找到真正就绪的 FD(5 号窗口)。
直接通知就绪 FD:内核通过事件队列 明确告知具体就绪的 FD(阿姨直接说 “5 号窗口好了”),无需遍历。
O (1) 效率:避免 select 的 O (n) 遍历开销,是多路复用的主流实现(如 Nginx 使用 epoll)
7.2节太JB底层了吧,先放弃。我的天,小林可真tm能研究,这些远比算法枯燥多了吧。感觉纯粹是为了秀技能,徒增压力
先跳过
豆包链接同上
前言:
最慢的磁盘,所以针对优化磁盘的技术有:零拷贝、直接IO异步IO,如此就可以提高系统的吞吐量,即单位时间内数据传输总量,提升 IO 效率,可增大吞吐量
六大模块之一:为什么要有DMA
这里在学计算机网络的时候追问过豆包
没有 DMA 技术前,CPU亲自搬运,I/O 过程如下:
CPU 发出对应的指令给磁盘控制器,然后返回
磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断
CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 无法执行其他任务

说下I/O,C 盘(磁盘)属于外部设备。读取数据时,先从磁盘(外部设备)到内核缓冲区,再拷贝到用户内存
内核 是 硬件访问 的 唯一入口(用户进程无权直接操作硬件,数据必须先由内核从硬件读取到内核缓冲区
用户进程的内存 与 内核内存物理隔离,需通过 拷贝操作 将数据从内核空间传递到用户空间,供应用程序用
例子:读文件时,磁盘数据先到内核的 “页缓存”(内核缓冲区),再拷贝到用户进程的内存(如char buf[1024]),用户才能通过buf访问数据
这就是内存安全机制(隔离用户与内核内存)和 OS 的分层设计(内核负责硬件交互,用户空间负责应用逻辑)
引入DMA(直接内存访问), I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务:

调用 read ,进程阻塞
OS 收到后将 I/O 请求发送 DMA,让 CPU 干别的
DMA 进一步将 I/O 发送给磁盘
磁盘收到 I/O 后,把数据从磁盘读到 磁盘控制器的缓冲区,磁盘控制器的缓冲区 满了后,向 DMA 发送中断信号,告知自己满了
DMA 知道后,将 磁盘控制器的缓冲区 数据拷贝到内核缓冲区,不占用 CPU,CPU干别的
CPU 收到 DMA 信号后,将数据从内核拷贝到用户空间,系统调用的 read 返回
CPU 只是控制从哪传输到哪,传啥
早期 DMA 在主板,如今 I/O 设备多,数据传输需求不同,每个 I/O 都有自己的 DMA 控制器
唉,又问了豆包,链接搜“29岁,测试转行”,说“先入职 中厂(如米哈游、B 站、网易),选择 “高并发服务端”“实时通信” 等核心业务线,积累 1 年以上分布式系统经验(如参与微服务架构设计、流量调度优化),再以 “资深开发” 身份冲击大厂,薪资可跳涨至 25-35W / 年”、“(如腾讯 ieg 某游戏后台团队曾明确表示 “欢迎有测试背景的开发,提升代码质量”)”
微信搜“没资格再找所有,人
只能自己弄”
六大模块之二:传输的文件传输有多糟糕
服务端提供文件传输功能,方法是将磁盘上的文件读取出来,通过网络协议栈发送给客户端
传统 IO 工作方式:数据的读写是从用户空间到内核空间来回复制,而内核空间的数据是通过 OS 的 IO 接口从磁盘读写
一般需要两个系统调用
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
这两行发生的底层逻辑如下图:

期间共 4 次用户态 与 内核态的上下文切换,因为两次系统调用read & write
而每次又都要先从用户态切换到内核态,等内核完成任务,再从内核切换用户态
read:切换 1 次(用户→内核)、切换 2 次(内核→用户)。
write:切换 3 次(用户→内核)、切换 4 次(内核→用户)。
一次耗时几十纳秒到几微秒,但高并发场景,累计放大,影响性能
除了切换,还有 4 次 数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的
第一次拷贝,通过 DMA:磁盘 → 内核
把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的
第二次拷贝,通过 CPU:内核 → 用户
把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的
第三次拷贝,通过 CPU:用户 → socket
把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的
第四次拷贝,通过 DMA:socket → 网卡
把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的
冗余的上文切换和数据拷贝,影响系统性能
提高文件传输的性能,就得减少「用户态与内核态的上下文切换」和「内存拷贝」的次数
六大模块之三:如何优化文件传输性能
如何减少「用户态与内核态的上下文切换」的次数?
读取磁盘数据的时候,之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或网卡,内核的权限最高,这些操作设备的过程都需要交由操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。
而一次系统调用必然会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行
所以,要想减少上下文切换到次数,就要减少系统调用的次数。
如何减少「数据拷贝」的次数?
在前面我们知道了,传统的文件传输方式会历经 4 次数据拷贝,而且这里面,「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里」,这个过程是没有必要的
因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的
六大模块之四:如何实现零拷贝
即如何减少「上下文切换」和「数据拷贝」的次数
方法一、mmap + write
read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read()
buf = mmap(file, len);
write(sockfd, buf, len);
mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作
通过 DMA:磁盘 → 内核
应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
通过 CPU:内核 → socket
应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
通过 DMA:socket → 网卡
最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的

通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程,
但还不够,仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,且仍然需要 4 次上下文切换,因为系统调用还是 2 次
方法二、sendfile
Linux 内核版本 2.1 中,提供专门发文件的系统调用函数 sendfile()
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
//out_fd, 目的端文件描述符
//int in_fd, 源端文件描述符
//off_t *offset, 源端偏移量
//size_t count);复制数据长度
可以替代read() 和 write() 减少一次系统调用 ,即减少 2 次上下文切换的开销
直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,如此一来,只有 2 次上下文切换,和 3 次数据拷贝

但这不是真正的零拷贝,如果网卡支持 SG-DMA (可惜我的不支持)
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
且uname -rLinux 内核高于 2.4,则 可以进一步减少 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程:
第一步,通过 DMA 磁盘数据→ 内核缓冲区
第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝
这就是真正的零拷贝,没内存层拷贝也就没 CPU 搬运数据,全程通过 DMA
只需要 2 次数据拷贝 & 2 次上下文切换,且 2 次的拷贝都不用 CPU,性能翻倍

使用零拷贝技术的项目
不就是用了个函数而已吗,底层多牛逼是底层的事,跟程序员有啥关系?这有啥牛逼的
Kafka 这个开源项目,就利用了「零拷贝」技术
transferTo方法最后就是用的sendfile()系统调用,实际缩短 65% 时间,提高吞吐量
Nginx也默认开启零拷贝,但Nginx运行在自己笔记本上,所以我笔记本不支持,则Nginx的功能会受限,即有sendfile也白扯

设置为 on 表示,使用零拷贝技术来传输文件:sendfile ,这样只需要 2 次上下文切换,和 2 次数据拷贝
设置为 off 表示,使用传统的文件传输技术:read + write,这时就需要 4 次上下文切换,和 4 次数据拷贝
这玩意都是底层的,实际也不会用到,我其实真的不知道之前做银行外包测试的时候,那些外包开发,跟大厂的开发差啥,这些我现在学会了,但感觉知不知道好像都无所谓吧╮(╯▽╰)╭
六大模块之五:PageCache作用
内核缓冲区就是磁盘高速缓存(PageCache)
磁盘→DMA→内核缓冲区(Pagecache)→CPU→用户缓冲区→CPU→socket 缓冲区→DMA→网卡
磁盘→DMA→内核缓冲区(Pagecache)→DMA(借助 SG - DMA 等,若网卡支持 )→网卡
PageCache咋实现零拷贝的?
读写磁盘比读写内存慢,所以要通过 DMA 把磁盘数据搬运到内存,但内存空间小,只能拷贝小一部分
哪些拷贝到磁盘?
依据局部性原理,缓存最近访问的数据,空间不足的时候淘汰最久未被访问的缓存
还有,读数据需要找到数据所在的位置,即磁头旋转到数据所在扇区,再顺序读取数据,但旋转磁头耗时,为了降低影响,PageCache 就使用预读功能
比如,假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰(因空间不足等被移除缓存 )出 PageCache 前,进程读取到它了,收益就非常大
所以,PageCache优点:
缓存最近被访问的数据;
预读功能;
但 GB 级别大文件的时候,PageCache不起作用,白白浪费 DMA 多做一次数据拷贝,具体解释这个“白白浪费”:
零拷贝依赖 PageCache:
磁盘→DMA→PageCache(第一次拷贝,但大文件导致缓存失效)→DMA→网卡(第二次拷贝)
问题:第一次 DMA 拷贝到 PageCache 的数据很快被淘汰,无法被网卡复用,等于白做(但不是完全白做,如果是完全白做,那后面第二次拷贝就需要从磁盘来获取,但实际趋同的网卡无法从磁盘直接到网卡,继续往下看)
理想的 “不依赖 PageCache”(需特殊硬件支持):
磁盘→直接 DMA→网卡(仅一次拷贝,跳过 PageCache)。
注:普通网卡不支持,仅特殊硬件(如 NVMe 直接 I/O)可行
大文件场景下,零拷贝因 PageCache 失效,多了一次无效的 DMA 拷贝(磁盘→PageCache),导致比理想情况(无此步骤)更慢,但普通网卡只能走 PageCache,没得选
“白做” 的含义:
不是完全失败,而是低效。数据确实被 DMA 到了 PageCache,但因 PageCache 空间不足,很快被后续数据覆盖(淘汰)
网卡仍能读取:当网卡需要数据时,它会从 PageCache 读。若数据已被淘汰,则需重新从磁盘读取(触发新的 DMA)
文件大小:1GB(1000MB)
PageCache 容量:500MB
传输过程:
第一次 DMA:磁盘→PageCache(0-500MB)
网卡读取:从 PageCache 读 0-500MB(第一次 DMA 的数据被使用)
第二次 DMA:磁盘→PageCache(500-1000MB)
PageCache 淘汰:因空间不足,0-500MB 被淘汰,PageCache 只剩 500-1000MB
网卡需要 0-500MB 的数据:发现已被淘汰,触发新的 DMA(磁盘→PageCache,重新读 0-500MB)
大文件传输慢的原因:
第一次 DMA的数据虽被使用,但因 PageCache 容量不足,后续需反复重新读取相同数据(触发多次 DMA)。
相比小文件(PageCache 能完整缓存),大文件的缓存命中率极低,导致零拷贝的 “减少 CPU 拷贝” 优势被频繁的磁盘 DMA抵消。
大文件导致的问题:
长时间被大文件占据,其他「热点」的小文件可能就无法充分使用到 PageCache,于是这样磁盘读写的性能就会下降
PageCache 中的大文件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次
零拷贝与 PageCache 绑定,所大文件传输不应该使用 PageCache 也就是不用零拷贝
六大模块之六:大文件传输用啥实现
read阻塞,等待磁盘数据的返回
调用read阻塞着,内核向磁盘发起 IO 请求,磁盘寻址,准备好数据向内核发起 IO 中断
内核收到中断,将数据从磁盘控制器缓冲区拷贝到 PageCache
内核把 PageCache 数据拷贝到用户缓冲区,read返回

对于阻塞问题,可以用异步 IO

内核向磁盘发起读请求,但是可以不等待数据就位就可以返回,于是进程此时可以处理其他任务
当内核将磁盘中的数据拷贝到进程缓冲区后,进程将接收到内核的通知,再去处理数据
异步 IO 不涉及 PageCache,即绕开 PageCache,也叫直接 IO。异步 IO 只支持直接 IO
用 PageCache 叫缓存 IO
所以,大文件用 异步 IO + 直接 IO 代替零拷贝技术
异步 IO 指的是,不等待数据就绪
直接 IO 指的是,绕开缓存。直接从磁盘 → 网卡。(可同步 / 异步:直接 IO 可阻塞(同步)或不阻塞(异步,内核通知))
还有疑惑,追问豆包的解释:
直接 IO 的同步 / 异步:
同步直接 IO:read 加 O_DIRECT(绕 PageCache,阻塞,等数据到进程)。
异步直接 IO:用 AIO(非阻塞,内核通知数据就绪,进程可忙其他)。
(直接 IO 是数据路径,同步 / 异步是处理方式,可组合)
磁盘→网卡的实际路径:
必须过内核(无法直接磁盘→网卡),但:
直接 IO:磁盘→内核缓冲区(绕 PageCache,无缓存,适合大文件,减少缓存挤占)。
零拷贝:内核缓冲区(PageCache)→网卡(DMA,减少 CPU 拷贝,适合小文件,利用缓存)。
read的默认行为:
默认缓存 IO(经 PageCache,同步,阻塞),但可通过 O_DIRECT 改为直接 IO(绕缓存,同步,阻塞)。
异步 IO 需用 AIO 等 API,与直接 IO 可共存,如 AIO + O_DIRECT
直接 IO(绕缓存):适合大文件(不占缓存,减少无效 DMA),可同步(read+O_DIRECT)或异步(AIO+O_DIRECT)。
零拷贝(经缓存):适合小文件(利用缓存,减少 CPU 拷贝),依赖 PageCache,同步 / 异步均可。
磁盘→网卡必过内核,但通过不同 IO 策略(直接 IO / 零拷贝)优化路径,提升性能(大文件用直接 IO,小文件用零拷贝,异步提升并发)。
(无需纠结特殊情况,记住场景匹配:大文件→直接 IO(绕缓存,防挤占),小文件→零拷贝(用缓存,少 CPU),异步让进程不阻塞,多任务并行
注意:绕 PageCache ≠ 无内核缓冲
PageCache:缓存文件内容(可多次复用,如小文件重复读时快,大文件易占满它)。
内核临时缓冲(直接 IO 用):只临时处理当前 IO 数据(不长期存文件内容,绕开 PageCache 的文件缓存功能,大文件传输时不占 PageCache 空间)。
(核心:PageCache 存文件内容(可复用),直接 IO 的内核缓冲不存文件内容(仅临时过一下,不占缓存))
还是很朦胧:
PageCache 能分多次缓存,但大文件分多次缓存后总内存占用仍 = 文件大小(如 10GB 文件分 1 万次读,内存会存满 10GB 数据)。
直接 IO 不分次缓存,每次读几 MB 数据用完即丢,内存始终只占几 MB(不存文件内容)。
核心矛盾:PageCache 存的是文件副本(占内存),直接 IO 只走临时通道(不占内存)。大文件若用 PageCache,内存会被 “撑爆”,小文件缓存被挤走
懂了,妈逼的这么简单点事,之前说的这么复杂
直接 IO 应用场景:
应用自身有独立的缓存机制(如 MySQL 的 InnoDB 缓冲池,缓存数据库数据页),无需依赖内核的 PageCache 再次缓存。在 MySQL 数据库中,可以通过参数设置开启直接 I/O,默认是不开启;
大文件的时候,由于大文件难以命中 PageCache 缓存(访问频率低),而且会占满 PageCache 导导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用直接 I/O
直接 IO 绕过了 PageCache,无法享受内核俩优点:
I/O 合并:
内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 PageCache 中,最后「合并」成一个更大的 I/O 请求再发给磁盘,这样做是为了减少磁盘的寻址操作
仅 PageCache(缓存 IO)会缓存请求并合并,直接 IO(绕缓存)无此合并,因请求不进 PageCache。
预读:
仅 PageCache 会预读数据到缓存,直接 IO 不使用 PageCache,故无预读优化(数据不进缓存,自然没有预读内容
大文件的时候,使用「异步 I/O + 直接 I/O」了,就可以无阻塞、高并发地读取文件
同步read无论开不开零拷贝,都要阻塞等待数据返回
所以要根据文件大小使用不同方式:
传输大文件的时候,使用「异步 I/O + 直接 I/O」;
传输小文件的时候,零拷贝
Nginx里
#include
location /video/ {
sendfile on;
aio on;
directio 1024m;
}
当文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」
总结:
早期 I/O 操作,内存与磁盘的数据传输的工作都是由 CPU 完成的,而此时 CPU 不能执执行其他任务,会特别浪费 CPU 资源
于是,为了解决这一问题,DMA 技术就出现了,每个 I/O 设备都有自己的 DMA 控制器,通过这个 DMA 控制器,CPU 只需要告诉 DMA 控制器,传输啥从哪来到哪去,就可以做其他事,后续交给DMA
Kafka(消息队列系统,暂存转发消息,让不同程序间传递数据) 和 Nginx 都有实现零拷贝技术,这将大大提高文件传输的性能
零拷贝基于 PageCache,为了解决机械硬盘寻址慢问题,协助 IO 调度算法实现 IO 合并 & 预读,这也是顺序读比随机读性能好的原因
零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送
当传输大文件时,不能使用零拷贝,因为可能由于 PageCache 被大文件占据,而导导致「热点」小文件无法利用到 PageCache,并且大文件的缓存命中率不高,这时就需要使用「异步 IO + 直接 IO 」的方式
在 Nginx 里,可以通过配置,设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝
I/O 多路复用:select/poll/epoll(9.2这一章不知道他看的啥书,反正在我啃完 尹圣雨 TCPIP网络编程后,感觉小林coding讲的很业余)(我重新回顾 结合 TCPIP网络编程写吧)
五大模块之一:最基本的 Socket 模型
服务端:
调用socket() 函数,创建网络协议为 IPv4,以及传输协议为 TCP 的 socket,
调用 bind() 函数,给这个 Socket 绑定一个 IP 地址 和 端口
绑定端口:
笔记本插USB的插口是物理端口,有具体实物,但和这里的网络端口完全不同,这里的网络端口是虚拟编号,用于区分不同程序(80 给网页服务)
当内核收到 TCP 报文,通过 TCP 头里面的端口号,来找到我们的应用程序,然后把数据传递给我们
绑定 IP 地址:
一台机器是可以有多个网卡的,每个网卡都有对应的 IP 地址,当绑定一个网卡时,内核收到该网卡上的包,才会发给我们
IP 选网卡(走哪条路),端口选程序(进哪个屋),两者结合让数据 准确到达目标程序的指定网卡
调用 listen() 函数进行监听,判定服务器中一个网络程序有没有启动,可以通过 netstat 命令查看对应的端口号是否被监听,进入监听状态后
调用 accept() 函数,来从内核 阻塞 获取客户端的连接
客户端:
客户端在创建好 Socket 后,调用 connect() 函数指明服务端 IP 地址 & 端口号,然后三次握手
TCP连接的时候,服务端内核为每个 socket 维护的两个队列:
一个是「还没完全建立」连接的队列,称为 TCP 半连接队列,这个队列都是没有完成三次握手的连接,此时服务端处于 syn_rcvd 的状态
一个是「已经建立」连接的队列,称为 TCP 全连接队列,这个队列都是完成了三次握手的连接,此时服务端处于 established 状态
TCP 全连接队列不为空后,服务端的 accept() 函数,就会从内核中的 TCP 全连接队列里拿出一个已经完成连接的 Socket 返回应用程序,后续数据传输都用这个 socket
注意:一个监听 socket、一个已连接 socket
建立连接后传输数据,read() & write()

内核里的数据结构
每个进程都有一个数据结构 task_struct,该结构体里有一个成员指针 指向 文件描述符数组,这个数组列出进程打开的所有文件的 文件描述符,数组下标是文件描述符,内容是指针,指向内核打开的文件列表,内核可以通过文件描述符找到对应打开的文件
每个文件都有一个 inode,Socket 文件的 inode 指向了内核中的 Socket 结构,在这个结构体里有两个队列,分别是发送队列和接收队列,这个两个队列里面保存的是一个个 struct sk_buff,用链表的组织形式串起来
sk_buff 可以表示各个层的数据包,
在应用层的数据包叫 data
在 TCP 层叫 segment
在 IP 层叫 packet
在数据链路层叫 frame
协议栈采用的是分层结构,上层向下层传递数据时需要增加包头,下层向上层数据时又需要去掉包头,如果每一层都用一个结构体,那在层之间传递数据的时候,就要发生多次拷贝,这将大大降低 CPU 效率
所以全部数据包只用一个 sk_buff 结构体描述,具体是调整 sk_buff中的 data 指针
当要发送报文时,创建 sk_buff 结构体,数据缓存区的头部预留足够的空间,用来填充各层首部,在经过各下层协议时,通过减少 skb->data 的值来增加协议首部
当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加 skb->data 的值,来逐步剥离协议首部

这个 TCP socket 调用流程,一对一,后面会阻塞
五大模块之二:如何服务更多的用户
TCP 连接由四元组唯一确认:本机IP、 本机端口、 对端IP、对端端口
服务器本地 IP 和 端口 固定,所以最大 TCP 连接数 = 客户端 IP 数×客户端端口数
IPv4 下:IP最多 2^32、端口最多 2^32,服务端单机最大 TCP 连接数 2^48
但实际:
TCP 连接在内核里有对应的数据结构 struct sock,即占内存
Linux 下单个进程打开的文件描述符有限制,默认1024,可以通过 ulimit 改
经典的 C10K 问题:
如果服务器的内存只有 2 GB,网卡是千兆的,能支持并发 1 万个客户端同时连接请求吗?
单机处理指的是单台服务器独立处理
科普:
支持 1000Mbps(兆比特每秒) 传输的网卡,是设备连网的 “高速通道”,理论上每秒最多传 125MB(1000÷8,字节与比特换算) 数据(净荷,不含协议等开销)。
125MB/s 由来:
单位转换:网络速度用 比特(bit),存储用 字节(Byte),1Byte=8bit。
故 1000Mbps(比特) ÷ 8 = 125MB/s(字节)
但实际会受如下限制无法达到千兆
硬件性能不足:比如老旧 PCI 总线,
协议包装:TCP/IP协议需要包装数据占带宽,
系统:系统设备CPU内存处理慢无法喂饱网卡
环境:服务器限速,多设备抢带宽
思考:
内存(2GB):1 万连接若每连接占 200KB,刚好 “卡满”(无余量,突发或复杂业务易超内存)
千兆网卡:125MB/s 带宽,每连接 100Kbit/s 时刚跑满(大文件传输或协议开销易超带宽)
但要真正实现还需要服务器的网络 I/O 模型,效率低的模型,会加重系统开销
五大模块之三:多进程模型
多进程是最传统的方法
服务器的主进程负责监听客户的连接,一旦与客户端连接完成,accept() 函数就会返回一个「已连接 Socket」,这时就通过 fork() 函数创建一个子进程,实际上就把父进程所有相关的东西都复制一份,包括文件描述符、内存地址空间、程序计数器、执行的代码等
子进程可以直接使用「已连接 Socket 」和客户端通信,不需要关心「监听 Socket」,只需要关心「已连接 Socket」;父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心「已连接 Socket」,只需要关心「监听 Socket」
子进程退出时,要用wait() & waitpid()回收否则僵尸
(这里我早就在之前说过了)
这多进程应对1w个客户端肯定不行,每一个进程都占用系统资源,进程间上下文切换(涉及用户空间资源:虚拟内存、栈、全局变量 ,还涉及内核空间资源:内核堆栈、寄存器)的“包袱”是很重的,性能会大打折扣
五大模块之四:多线程模型
多线程共享进程的部分资源,比如文件描述符列表、进程空间、代码、全局数据、堆、共享库
这些不需要上下文切换,只需要切换线程私有数据、寄存器等不共享的数据
TCP三次握手后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,达到并发处理
如果每来一个连接就创建一个线程,线程运行完后,需要 OS 频繁销毁,所以引入线程池避免线程的频繁创建和销毁
线程池(错别字勘误):
提前创建若干个线程,有新连接建立时,将这个已连接的 Socket 放入到一个队列里,然后线程池里的线程负责从队列中取出「已连接 Socket 」进行处理
这个队列是全局的,每个线程都会操作,为了避免多线程竞争,线程在操作这个队列前加锁

没懂,追问豆包理解了:
线程:提前创建好,放入线程池
队列:存放的是任务(包含 socket 文件描述符),不是线程
流程:
客户端连接 → 主线程 accept 得到 socket → 封装成任务放入队列
线程池中的空闲线程,从队列取已连接的socket → 处理 socket 读写
对于 C10K 问题,新到来一个 TCP 连接,就要分配线程,一台机器要维护 1w 个连接,相当于 1w 个线程。OS 扛不住
妈逼的我感觉自己刷那些算法题,啃TCPIP网络编程尹圣雨,真他妈亏死了艹
就应该无脑傻逼应试面向找工作学习
太痛苦了
这些各种东西无非就是调用现成的各种函数,呵呵
~~~~(>_<)~~~~
五大模块之五:I/O 多路复用
一个连接一个线程会导致 资源爆炸(线程数多,内存、调度扛不住)
引入多路复用:用 1 个进程(或少量进程) 管理 所有 socket
处理每个请求 1ms,1s 内可以处理上千请求,多个请求复用了一个进程
这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用
select/poll/epoll 内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件
具体是,在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可
我一个没干过开发的这块啃完 TCPIP网络编程尹圣雨 都感觉比小林掌握的深,唉我真傻逼,做了那么多无用功,乌烟瘴气的职场环境都吃不饱饭
select工作流程:
将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合 拷贝 到内核,让内核 遍历 文件描述符集合来检查是否有网络事件产生,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合 拷贝 回用户态里,然后用户态还要再 遍历找到可读或可写的 Socket,才能再对其处理
需要 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还要 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中
Bitmap(位图):用 每一位(bit) 表示一个文件描述符(FD)是否被关注(如是否有数据),像 FD=3 对应第 3 位,位为 1 表示关注。
固定长度:select 的 Bitmap 大小 固定(由 FD_SETSIZE 决定,默认 1024 位,即 128 字节),只能存 0~1023 的 FD(共 1024 个),空间不随实际 FD 数量变(哪怕只用 1 个 FD,也占 128 字节)。
FD_SETSIZE:内核规定的 Bitmap 最大长度,直接限制 select 能处理的 FD 数量(超 1024 就不行),这是 select 的 硬伤(对比 poll/epoll 的动态结构,可突破此限制)
总结:select 的 Bitmap 是 固定位数的位集合(存 FD 状态),长度由FD_SETSIZE定死(默认 1024 位),导致 FD 处理上限低,而 poll/epoll 用动态结构(如链表),没这限制
poll:
不用 BitsMap 存关注的文件描述符,用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制
跟 select 没太大差别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合
epoll:
先用 epoll_create创建一个 epoll对象epfd,再通过 epoll_ctl将需要监视的 socket添加到 epfd中,调用 epoll_wait等待数据

epoll 通过俩方面,解决了 select/poll 的问题
epoll_ctl() 函数,一次就可以加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),减少了内核和用户空间大量的数据拷贝和内存分配。而 select/poll 内核里没有这种,所以 select/poll 每次操作时都传入整个 socket 集合给内核epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率 
即使监听的 Socket 数量越多的时候,效率不会大幅度降低,上限就是:为系统定义的进程打开的最大文件描述符个数。epoll 解决 C10K 的利器
epoll_wait 返回时,对于就绪的事件,epoll 没有使用共享内存的方式,用户态和内核态没有都指向就绪链表,epoll 内核源码里,没有共享内存,epoll_wait 底层内核代码调用了 _put_user函数,即将数据从内核拷贝到用户空间
边缘触发和水平触发(这里感觉讲的比 尹圣雨 TCPIP网络编程书 讲的号)
epoll 支持
当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完
只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了
被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取
所以没必要一次就执行尽可能多的读写
比如,短信通知你取快递,一次没取完:
水平触发是没取完会不停的短信通知你
边缘触发IO事件只通知一次,避免错失会循环读,没数据就会阻塞,程序就会卡那。所以,边缘一般和非阻塞IO搭配,一直 IO 操作,直到read 和 write返回错误,即EAGAIN 或 EWOULDBLOCK(表示没数据或缓冲区满,稍后再试)
由于减少 epoll_wait 的系统调用(存在上下文切换)次数,边缘触发效率更高
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发:
边缘触发虽高效,但 编程易出错(需一次性处理完数据,否则漏通知),且 对场景要求高(如文件 IO 不适用),所以默认用水平触发(更稳、易兼容),边缘作为可选优化(给熟练者用)
Linux手册关于 select说明里说,IO多路复用最好搭配非阻塞IO一起用:
select() 可能会将一个 socket 文件描述符报告为 "准备读取",而后续的读取块却没有。例如,当数据已经到达,但经检查后发现有错误的校验而被丢弃时(错别字勘误),文件描述符被错误地报告为就绪。因此,在不应该阻塞的 socket 上使用
O_NONBLOCK即非阻塞,可能更安全。即,多路复用 API 返回的事件并不一定可读写的,使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞
总结:
最基础的 TCP 的 Socket 编程,它是阻塞 I/O 模型,基本上只能一对一通信,为了服务更多的客户端,我们需要改进网络 I/O 模型:
多进程/多线程 → IO 多路复用(可以只在一个进程里处理多个文件的IO,Linux下提供三种多路复用API:select、poll、epoll)
豆包链接同
不知道为啥重新回顾尹圣雨TCPIP网络编程这本书,其实发现写的真的挺JB抽象挺JB恶心的。
开始下一节之前,先强迫症的回顾下TCPIP网络编程书的这些内容吧。其实之前啃书的时候,只是自欺欺人掩耳盗铃的的搞懂了细节,根本没串联,唉~~~~(>_<)~~~~
下定决心二刷 尹圣雨TCPIP网络编程 这本书
作为面试的一个项目,小林coding网站里的经验贴《机械上岸腾讯》的提到说,面试官注重看你“学到了什么”,那我就重新啃书,弄个逐步优化,而不是上来就各种高大上的玩意
妈逼的 vim 真的好恶心,我如果弄个cpp1234那种,也太der了吧,万一让我临时改个代码,vim快捷键我都费劲~~~~(>_<)~~~~,难道要运行 1.cpp、2.cpp、3.cpp 去给面试官演示吗?
百度查豆包问,都没找到啥太好用的IDE,知乎还说很多人就是用 vim,那我直接 vim 来运行(租的腾讯云服务器里的/root/cpp_projects_2文件夹)搭配自己习惯的codeblock来看代码 这么学习,但成果展示的时候用之前无意间发现的(现在按钮好像没有了,但功能还在)豆包生成代码后有一个按钮“生成网页”功能,就用那个做逐步学习的优化版本展示。主要是方便对比书上讲解的不同代码。这个实在是基于 vim 不好演示 + 不想让啃的书白费,无路可走逼出来的
这样还方便对比
之前博客提到 github.xyz 网站可以看变化过程,但感觉没法定住去看
叶问x光学武术、小林给人培训班的诊断测试学都有什么、机械上岸学学习方法和面试官喜好、饭馆学管理、之前傻逼一样啃最底层废弃的winAPI真的头大、W钰H说不用看win、前端直接用gpt生成
md突然好奇自己博客园的主页风格咋弄了的,忘记了,感觉自己都不如之前,之前照着别人的参考博客就可以弄,现在怕改坏了,就没试验,估计是博客园提供的风格样式中的一个
豆包必须说明:“从此以后所有代码都用通过 JavaScript 控制点击版本标签时动态显示,因此需要点击触发展开来给出!!,将所有代码示例都通过点击版本标签的方式动态显示”,如果“直接嵌套在页面结构中,代码随页面加载直接显示在回答区”占用篇幅,且有长度限制生成到一半就停了!但总是不按要求来,总是生成一半就结束了,太头疼了!!
妈逼的豆包生成代码超过800行就经常自动停止,这html还得自己断断续续拼接
只能上传文件然后问想增加xx功能该在哪添加什么内容了
搜“创造很多项目”、“写书”
为了学C++写书,为了搞项目学啃书顺手且逼于无奈发明个顶级项目,为了学unix发明了linux
自己从0搞,比github扒好多了,知道哪里是生成的(html),知道其实本身代码就那么一点(书上的),但自己以为很low~~~~(>_<)~~~~,哪里有问题知道咋改,可以上手
但用网上的项目之前找过,编程指北的打不开了,github我根本不会用,估计弄到本地咋搞都不知道,费劲巴拉不知道该研究哪里,都混在一起,万一运行不了又tm无从下手,都不知道html里哪些是该研究的C++服务器代码~~~~(>_<)~~~~
本来是因为强迫症,想学完小林的epoll/select然后自己啃书回顾这块,然后想从头捋顺一遍,因为很多都忘记了,但vim不方便,结合之前W钰H说可以生成,结合之前无意间看到的豆包代码下有个按钮“生成网页”,突发奇想直接弄成一个优化过渡多好,然后学C++,然后搞http多线程服务器项目。但这么一看,妈逼的不用搞项目了,这个捋顺完就很牛逼艹。其实当初啃完这本书就很强了,可以直接看网络+os面试了,但那时候W钰H问我啥也回答不上来~~~~(>_<)~~~~
就跟之前说的,搜“别人的”,别人的代码不敢调、不敢改一样,不应看别人的代码,有思路自己想,不然受干扰
比如这里我逻辑想的就是这么写,但代码记得这是那样的,但其实前面不一样,到这只能这么写,所以先看别人代码,写的不爽
毕设课设就想弄个马踏棋盘+搜索的所有算法结合的故事算法项目
可是感觉是不是只有epoll不够啊,看网上B站视频教的都有很多更高级的功能,IDE都是可视化的好多行代码,我一个本地的html都不知道是不是太简单了,不知道需不需要用数据库~~~~(>_<)~~~~。
怎么部署到腾讯云服务器也不晓得,找B站好多连运行结果都没有,难道到时候要给面试官展示命令行黑框吗?~~~~(>_<)~~~~,到底该做到啥程度啊,B站好多卖课的弄的天花乱坠的
又找了别人的发现有auto的转换,感觉自己啃的书还没废弃,我以为都有现成封装好的,发现是浏览器,一问豆包说书上的无法弄浏览器127.0.0.1那么访问,书的epoll多线程服务器只是是TCP的,没应用层,方便于理解epoll的,加个应用层http就好了POST方法啥的,就几句话,之前学的长连接啥的就用上了,发现项目从0上手起手弄好简单
我这个优化版本是不是太小儿科太der了
我是不是该搞个图形界面化的IDE啊~~~~(>_<)~~~~找不到可以问的人~~~~(>_<)~~~~好痛苦~~~~(>_<)~~~~
他妈想问问王YH,再帮帮我吧。我现在实在是项目搞的不知道怎么搞,不知道该搞到什么程度。或者公众号的鱼皮、吴师兄、帅地、小林他们 算了 他们都是商人 没有人能帮我 最难最无助的时候 起手用算法找感觉,去建立学习信心,进入这个学习的状态,建立自信,太牛逼了。然后,啃TCPip网络编程,这基本上就已经是很多人的实战项目了,然后啃小林的各种东西。 但是vim不方便,而且给面试官这么展示,我操也太麻烦了。再加上我不能每次都就是去对比啊,比如说五六个版本,你这直接为什么cat A/B/C.CPP那么着看不同的代码,我操太不方便了,那给我搞成一个网站呢?然后用这个正好还做了个服务器,就那么弄了。但是,其实我这个属于是被逼无奈,但可能对于他们来说挺新颖。但是我确实,没有其他办法了,他们可能也没有我的本事,完全另辟蹊径人,都是自己想的招儿。但其实按照中规中矩的模式,我还真不会,你看他们IDE个图形化啥的,我的命令行太他妈费劲了 然后他们可能有很多的优化,我想着通过这玩意儿弄成一个网站学习的时候,因为也好对比,他们可能就是在自己的电脑上搞一个对应的服务器,就直接代码能跑起来就完事了。但是我感觉我这跑起来没啥玩意儿呢。太小儿科了呀,我只能搞一个网站,到时候看看他们都怎么整吧。大家都怎么整的 我不愿意再去加各种群、各种网站,跟他们唠嗑儿,论坛看这些鸡巴玩意儿,贴吧什么,我就自己单搞 而且,每个人都整一个腾讯云服务器吗?难道每个人都装Linux?我操,不能吧,反正不知道,我就按照这么整吧。我他妈感觉我这学的有点儿der儿啊。我这没有别的招儿了,我想知道他们是怎么搞的。我感觉他们好像没有这么费劲,就是好像弄偏了,感觉 而且还有啊,这玩意儿他妈逼比Java简单,你就这玩意儿就几行代码运行,我不知道是不是得需要各种包儿什么的我操之前毕设的时候儿,我研究了好久啊,然后那个how2j,我觉得java挺鸡巴费劲的,就是各种包儿各种包儿在一起,各种项目包儿。嗯,这玩意儿好像也不用啥包吧,直接客户端服务端就OK了,我他妈不会还得整个IDE然后导各种这个包吧,一个目录里面好几个代码包儿,我操,那他妈可就恐怖了~~~~(>_<)~~~~ 感觉要死掉了 感觉随时都要猝死View Code
我这个优化版本目咋感觉是展示苦劳呢?
这个socket用vim改个东西就得保存退出g++一下,然后服务端运行+客户端运行,我靠这也太麻烦了吧~~~~(>_<)~~~~,豆包追问链接里说“现在能跑通就继续,边做边学工具,优先提升代码逻辑(HTTP + 多线程),工具效率问题后期补”
每次看代码都cat,特费劲,vim都没有滚轮滑动上下屏幕
开始用VS code + makefile代替
妈逼的VS code root用户还不让用,linux咋这么多逼事呢!之前vim一个复制就卡了我一天艹
妈逼的用
cat /etc/passwd | cut -d: -f1出现一堆,我哪知道用哪个用户啊Linux搞各种东西配置,真的异常痛苦,每次搞新的软件都tm要一天白搭
搞了个
gerjcs普通用户,密码139的
接下来每天回顾:算法/网络/OS,学C++/做HTTP多线程服务器项目
socket函数
int socket(协议族,套接字数据传输类型信息,协议信息)
PF_INET 是 IPv4 协议族
PF_INET6 是 IPv6 协议族
同一个协议族还有不同套接字类型,
SOCK_STREAM 面向连接的,读写都有由字节数组构成的缓冲,按序,没边界,数据不会消失,写n次 & 读m次都是不固定的
SOCK_DGRAM 面向消息的,快速传输,不可靠,不按序,写n次,读也必须n次,有数据边界
协议族确定了,套接字传输确定了,第三个参数也可能有多种
比如:
socket(PF_INET, SOCK_STREAM, 0) 是默认 ipv4 跟面向连接的组合就是 TCP
而如果不用默认 socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
就是不同协议(TCP vs SCTP)
打开文件的函数(具体后面说,搜“插一句 open 函数原型”)
| 函数 | 成功返回值 | 失败返回值 | 头文件 |
| open(文件名的字符串地址,打开模式) | 文件描述符 | -1 | <sys/types.h>、<sys/stat.h>、<fcntl.h> |
| close(文件描述符) | 0 | -1 | unistd.h |
| ssize_t write(文件描述符,准备传输的数据的地址,要传的字节数) | 写入的字节数 | -1 | unistd.h |
| ssize_t read (文件描述符,准备保存接收数据的地址,要接收数据的最大的字节数) | 接收的字节数但遇到末尾返回0 | -1 | unistd.h |
PS:
size_t 是标准库 typedef 定义的 unsigned,范围更大。而多加个s代表 signed
如果用size_t,从16位到32位直接改声明的size_t 就行
// 在32位系统上可能的定义
typedef unsigned int size_t;
// 在64位系统上可能的定义
typedef unsigned long long size_t;
程序员无需手动改,系统自动识别的
书 P5 代码(纯单个连接)
运行时,客户端代码接入服务端的IP,服务端用的是本地计算机的IP地址,如果在不同计算机运行,就采用服务端所在计算机的IP
服务端:socket创建套接字,bind分配IP &端口号,listen将套接字转为可接收状态,accept 阻塞受理请求,有请求后返回
客户端:创建套接字后调用connect则成为客户端套接字,并向服务端发起请求
个人思考:
listen(5) 的作用listen 的第二个参数(这里是 5)表示未处理的连接请求队列的最大长度,而非服务器能够处理的最大客户端数量。当有客户端尝试连接时,如果服务器暂时无法处理(例如正忙于其他操作),这些连接会被放入队列等待。
代码逻辑限制accept() 只被调用了一次,所以只能连接一次
代码细节:接下来所有代码的记录都写到网页里了,后续放到 git 或者 github 里
关于基础知识:
sizeof:计算对象占用的内存大小(含 \0),是编译时确定的常量
strlen:计算字符串的实际长度(不含 \0),运行时计算
\0 是空字符(ASCII 码为 0),是字符串的结束标志,不是空格,不可见且无法直接printf输出关于缓冲区溢出风险:
message是缓冲区大小30字节,read返回实际读的字节,无论是否-1,str_len 都是13,接收到到的就是 Hello World!\0
假设 message 数组大小为 100 字节(sizeof(message) == 100):
不减 1:read(sock, message, 100)
若服务器发送 100 字节数据,read 会将 100 字节全部写入 message,此时数组末尾没有空间存放 \0。若后续用
printf("%s", message) 输出,会因找不到 \0 而继续读取内存,导致段错误或输出乱码。
减 1:read(sock, message, 99)read 最多读取 99 字节,剩余 1 字节用于手动添加 \0,避免溢出。
str_len = read(sock, message, sizeof(message)-1); // 预留1字节
if (str_len > 0) {
message[str_len] = '\0'; // 手动添加结束符
}
好累,豆包超过800行总断,只能自己改html真的累死我了
再也不搞html了,字符串再也不弄成高亮了
豆包生的html bug好多
豆包的client.c无法复制,server.c就可以,豆包半天没解决,Deepseek定位到问题了,改了就好了。且Deepseek还可以主动生成完整代码,还有继续生成按钮,妈逼的豆包就没有继续生成,AdGuard也没拦截。白改大半天了

TinyMCE5选中超链接加粗变色总是失效,把其他的也一起选中搞就好了
艹好像是自动备份的恢复,今天28号,不断找历史版本发现大概23号有一个view code干空了,展不开了
先保存现在版本,记住时间,然后历史版本恢复,不断保存,刷新页面看是否恢复,发现是23号没的,复制下,然后再恢复刚才记住时间的最新版本,复制过去
其实 f12 是有的,很强
View Code
我之前豆包最初生成的:

1 <!DOCTYPE html> 2 <html lang="zh-CN"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>C Socket 编程示例</title> 7 <script src="https://cdn.tailwindcss.com"></script> 8 <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> 9 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script> 10 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> 11 12 <script> 13 tailwind.config = { 14 theme: { 15 extend: { 16 colors: { 17 primary: '#165DFF', 18 secondary: '#00B42A', 19 danger: '#F53F3F', 20 warning: '#FF7D00', 21 dark: '#1D2129', 22 light: '#F2F3F5', 23 code: '#1E1E1E', 24 }, 25 fontFamily: { 26 inter: ['Inter', 'sans-serif'], 27 mono: ['Consolas', 'Monaco', 'monospace'], 28 }, 29 } 30 } 31 } 32 </script> 33 34 <style type="text/tailwindcss"> 35 @layer utilities { 36 .content-auto { 37 content-visibility: auto; 38 } 39 .scrollbar-hide { 40 -ms-overflow-style: none; 41 scrollbar-width: none; 42 } 43 .scrollbar-hide::-webkit-scrollbar { 44 display: none; 45 } 46 .text-shadow { 47 text-shadow: 0 2px 4px rgba(0,0,0,0.1); 48 } 49 .bg-gradient { 50 background: linear-gradient(135deg, #165DFF 0%, #00B42A 100%); 51 } 52 .version-tab { 53 @apply px-4 py-2 rounded-lg font-medium transition-all duration-200; 54 } 55 .version-tab.active { 56 @apply bg-primary text-white; 57 } 58 .version-tab:not(.active) { 59 @apply bg-gray-100 text-gray-600 hover:bg-gray-200; 60 } 61 } 62 </style> 63 </head> 64 <body class="font-inter bg-light text-dark min-h-screen flex flex-col"> 65 <!-- 导航栏 --> 66 <header class="bg-white shadow-md fixed w-full z-50 transition-all duration-300" id="navbar"> 67 <div class="container mx-auto px-4 py-3 flex justify-between items-center"> 68 <div class="flex items-center space-x-2"> 69 <i class="fa fa-code text-primary text-2xl"></i> 70 <h1 class="text-xl font-bold text-primary">C Socket 编程示例</h1> 71 </div> 72 <nav class="hidden md:flex space-x-8"> 73 <a href="#overview" class="text-dark hover:text-primary transition-colors duration-200 font-medium">概述</a> 74 <a href="#versions" class="text-dark hover:text-primary transition-colors duration-200 font-medium">版本演进</a> 75 <a href="#demo" class="text-dark hover:text-primary transition-colors duration-200 font-medium">运行演示</a> 76 <a href="#comparison" class="text-dark hover:text-primary transition-colors duration-200 font-medium">性能对比</a> 77 </nav> 78 <button class="md:hidden text-dark text-xl" id="menu-toggle"> 79 <i class="fa fa-bars"></i> 80 </button> 81 </div> 82 <!-- 移动端菜单 --> 83 <div class="md:hidden hidden bg-white absolute w-full shadow-lg" id="mobile-menu"> 84 <div class="container mx-auto px-4 py-2 flex flex-col space-y-3"> 85 <a href="#overview" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">概述</a> 86 <a href="#versions" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">版本演进</a> 87 <a href="#demo" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">运行演示</a> 88 <a href="#comparison" class="text-dark hover:text-primary transition-colors duration-200 py-2">性能对比</a> 89 </div> 90 </div> 91 </header> 92 93 <!-- 英雄区 --> 94 <section class="pt-24 pb-12 bg-gradient-to-br from-primary/5 to-secondary/5"> 95 <div class="container mx-auto px-4"> 96 <div class="max-w-4xl mx-auto text-center"> 97 <h2 class="text-[clamp(2rem,5vw,3.5rem)] font-bold text-dark mb-6 leading-tight"> 98 C Socket 编程示例 99 </h2> 100 <p class="text-lg text-gray-600 mb-8 max-w-2xl mx-auto"> 101 从基础单线程到高性能 epoll,探索 Socket 编程的演进之路 102 </p> 103 <div class="flex flex-wrap justify-center gap-4"> 104 <a href="#versions" class="bg-primary hover:bg-primary/90 text-white px-6 py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-1"> 105 <i class="fa fa-code mr-2"></i>查看版本演进 106 </a> 107 <a href="#demo" class="bg-white hover:bg-gray-50 text-primary border border-primary px-6 py-3 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-1"> 108 <i class="fa fa-play-circle mr-2"></i>观看运行演示 109 </a> 110 </div> 111 </div> 112 </div> 113 </section> 114 115 <!-- 概述部分 --> 116 <section id="overview" class="py-16 bg-white"> 117 <div class="container mx-auto px-4"> 118 <div class="max-w-6xl mx-auto"> 119 <div class="flex items-center mb-8"> 120 <div class="w-2 h-8 bg-primary rounded-full mr-3"></div> 121 <h2 class="text-2xl md:text-3xl font-bold text-dark">Socket 编程演进概述</h2> 122 </div> 123 124 <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12"> 125 <div> 126 <p class="text-gray-700 mb-6"> 127 Socket 编程是网络通信的基础,从简单的单线程服务器到高性能的异步 IO 模型,经历了多个阶段的演进。本示例展示了从基础版本逐步升级到使用 epoll 的完整过程。 128 </p> 129 <div class="bg-light rounded-xl p-6 shadow-md"> 130 <h3 class="text-lg font-semibold mb-4 text-primary">5个版本演进路线</h3> 131 <ol class="space-y-3 text-gray-700"> 132 <li><span class="font-bold">V1:基础 TCP 单连接</span> - 单线程处理单个客户端,阻塞式 IO</li> 133 <li><span class="font-bold">V2:多进程并发</span> - 使用 fork() 创建子进程处理多个客户端</li> 134 <li><span class="font-bold">V3:多线程优化</span> - 使用 pthread 创建线程处理并发</li> 135 <li><span class="font-bold">V4:IO 复用 (select/poll)</span> - 使用 select/poll 实现单线程处理多连接</li> 136 <li><span class="font-bold">V5:epoll 高性能模型</span> - 使用 epoll 实现高效的异步 IO</li> 137 </ol> 138 </div> 139 </div> 140 <div class="bg-white rounded-xl overflow-hidden shadow-xl"> 141 <img src="https://picsum.photos/seed/socket/800/600" alt="Socket 编程演进" class="w-full h-auto"> 142 <div class="p-6"> 143 <h3 class="text-lg font-semibold mb-2">Socket 模型性能对比</h3> 144 <p class="text-gray-600">随着版本演进,服务器处理并发连接的能力显著提升</p> 145 </div> 146 </div> 147 </div> 148 149 <div class="grid grid-cols-1 md:grid-cols-5 gap-4"> 150 <div class="bg-primary/5 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-primary/20"> 151 <div class="text-3xl font-bold text-primary mb-2">V1</div> 152 <p class="text-gray-700 text-sm">基础单连接</p> 153 </div> 154 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 155 <div class="text-3xl font-bold text-gray-600 mb-2">V2</div> 156 <p class="text-gray-700 text-sm">多进程</p> 157 </div> 158 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 159 <div class="text-3xl font-bold text-gray-600 mb-2">V3</div> 160 <p class="text-gray-700 text-sm">多线程</p> 161 </div> 162 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 163 <div class="text-3xl font-bold text-gray-600 mb-2">V4</div> 164 <p class="text-gray-700 text-sm">select/poll</p> 165 </div> 166 <div class="bg-secondary/5 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-secondary/20"> 167 <div class="text-3xl font-bold text-secondary mb-2">V5</div> 168 <p class="text-gray-700 text-sm">epoll</p> 169 </div> 170 </div> 171 </div> 172 </div> 173 </section> 174 175 <!-- 版本演进部分 --> 176 <section id="versions" class="py-16 bg-gray-50"> 177 <div class="container mx-auto px-4"> 178 <div class="max-w-6xl mx-auto"> 179 <div class="flex items-center mb-8"> 180 <div class="w-2 h-8 bg-primary rounded-full mr-3"></div> 181 <h2 class="text-2xl md:text-3xl font-bold text-dark">版本演进</h2> 182 </div> 183 184 <!-- 版本切换标签 --> 185 <div class="flex flex-wrap justify-center gap-3 mb-12"> 186 <button class="version-tab active" data-version="1">V1:基础 TCP 单连接</button> 187 <button class="version-tab" data-version="2">V2:多进程并发</button> 188 <button class="version-tab" data-version="3">V3:多线程优化</button> 189 <button class="version-tab" data-version="4">V4:IO 复用 (select/poll)</button> 190 <button class="version-tab" data-version="5">V5:epoll 高性能模型</button> 191 </div> 192 193 <!-- 版本内容容器 --> 194 <div class="version-content" id="version-1"> 195 <!-- 新增思考板块 --> 196 <!-- 新增思考板块 --> 197 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 198 <div class="bg-light rounded-xl p-6 shadow-md"> 199 <h3 class="text-lg font-semibold mb-4 flex items-center"> 200 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 201 《我自己的思考》 202 </h3> 203 <div class="border-b border-gray-300 pb-4 mb-4"> 204 <h4 class="text-base font-semibold mb-2">listen(5) 的作用</h4> 205 <p class="text-sm text-gray-700 leading-relaxed"> 206 listen 的第二个参数(5)表示未处理的连接请求队列的最大长度,而非服务器能处理的最大客户端数。<br> 207 当客户端连接时,若服务器正忙,连接会被放入队列等待(最多5个) 208 </p> 209 </div> 210 <div> 211 <h4 class="text-base font-semibold mb-2">代码逻辑限制</h4> 212 <p class="text-sm text-gray-700 leading-relaxed"> 213 代码中 accept() 仅调用一次,处理完一个客户端后立即关闭连接并退出程序,<br> 214 因此即使队列中有等待连接,也无法处理后续客户端。<br> 215 改进方法:需要将 accept() 放入循环中持续接收新连接<br> 216 accept() 是阻塞调用,若无连接会一直等待<br> 217 客户端连接上后服务端就触发 write,客户端的 read() 同样阻塞,直到接收到数据<br> 218 然后客户端读 helloworld 219 </p> 220 </div> 221 </div> 222 <div class="bg-light rounded-xl p-6 shadow-md"> 223 <h3 class="text-lg font-semibold mb-4 flex items-center"> 224 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 225 《代码细节》 226 </h3> 227 <div class="border-b border-gray-300 pb-4 mb-4"> 228 <h4 class="text-base font-semibold mb-2">关于基础知识:</h4> 229 <p class="text-sm text-gray-700 leading-relaxed"> 230 对于字符串 231 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">"Hello World!"</code>: 232 <br> 233 234 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">sizeof</code> 235 236 :计算对象占用的内存大小(含 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>),是编译时确定的常量<br> 237 238 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">strlen</code> 239 :计算字符串的实际长度(不含 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>),运行时计算<br><br> 240 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code> 241 是空字符(ASCII 码为 0),是字符串的结束标志,不是空格,不可见且无法直接 printf 输出<br><br> 242 </p> 243 </div> 244 <div> 245 <h4 class="text-base font-semibold mb-2">关于缓冲区溢出风险:</h4> 246 <p class="text-sm text-gray-700 leading-relaxed"> 247 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">message</code> 是缓冲区大小 30 字节,read 返回实际读的字节,无论是否 -1,<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">strlen</code> 都是 13,接收到的就是<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">Hello World!\0</code><br><br> 248 假设 message 数组大小为 100 字节 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">(sizeof(message) == 100)</code>:<br><br> 249 不减 1:<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">read(sock, message, 100)</code><br> 250 若服务器发送 100 字节数据,read 会将 100 字节全部写入 message,此时数组末尾没有空间存放<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>。若后续用 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">printf("%s", message)</code> 输出,会因找不到 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code> 而继续读取内存,导致段错误或输出乱码<br><br> 251 减 1:<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">read(sock, message, 99)</code><br> 252 read 最多读取 99 字节,剩余 1 字节用于手动添加 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code> ,避免溢出<br><br> 253 然后还要添加上 \0,防止末本身尾没有 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>,代码如下: <br> 254 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm"> message[str_len] = '\0';</code <br> 255 </p> 256 </div> 257 </div> 258 </div> 259 <br> 260 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 261 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 262 <div class="bg-gray-900 py-2 px-4 flex justify-between items-center"> 263 <div class="flex items-center"> 264 <div class="flex space-x-2 mr-4"> 265 <div class="w-3 h-3 rounded-full bg-danger"></div> 266 <div class="w-3 h-3 rounded-full bg-warning"></div> 267 <div class="w-3 h-3 rounded-full bg-secondary"></div> 268 </div> 269 <div class="text-gray-400 text-sm font-mono">server.c</div> 270 </div> 271 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-server-v1"> 272 <i class="fa fa-copy mr-1"></i>复制 273 </button> 274 </div> 275 <div class="p-4 overflow-x-auto scrollbar-hide"> 276 <pre class="text-gray-300 font-mono text-sm leading-relaxed"><code>#include <stdio.h> 277 #include <stdlib.h> 278 #include <string.h> 279 #include <unistd.h> 280 #include <arpa/inet.h> 281 #include <sys/socket.h> 282 283 void error_handling(const char *message); 284 285 int main(int argc, char *argv[]) 286 { 287 int serv_sock; 288 int clnt_sock; 289 struct sockaddr_in serv_addr; 290 struct sockaddr_in clnt_addr; 291 socklen_t clnt_addr_size; 292 char message[]="Hello World!"; 293 294 if(argc!=2) 295 { 296 printf("Usage : %s <port>\n", argv[0]); 297 exit(1); 298 } 299 300 serv_sock=socket(PF_INET, SOCK_STREAM, 0); 301 if(serv_sock == -1) 302 error_handling("socket() error"); 303 304 memset(&serv_addr, 0, sizeof(serv_addr)); 305 serv_addr.sin_family=AF_INET; 306 serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); 307 serv_addr.sin_port=htons(atoi(argv[1])); 308 309 if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1) 310 error_handling("bind() error"); 311 312 if(listen(serv_sock, 5)==-1) 313 error_handling("listen() error"); 314 315 clnt_addr_size=sizeof(clnt_addr); 316 clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); 317 if(clnt_sock==-1) 318 error_handling("accept() error"); 319 320 write(clnt_sock, message, sizeof(message)); 321 close(clnt_sock); 322 close(serv_sock); 323 return 0; 324 } 325 326 void error_handling(const char *message) 327 { 328 fputs(message, stderr); 329 fputc('\n', stderr); 330 exit(1); 331 }</code></pre> 332 </div> 333 </div> 334 <div class="bg-white rounded-xl p-6 shadow-xl"> 335 <h3 class="text-xl font-semibold mb-4 text-primary">版本特点</h3> 336 <div class="space-y-4"> 337 <div class="flex items-start"> 338 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 339 <i class="fa fa-check text-primary"></i> 340 </div> 341 <div> 342 <h4 class="font-semibold">单线程处理</h4> 343 <p class="text-gray-600">服务器在单个线程中运行,一次只能处理一个客户端连接</p> 344 </div> 345 </div> 346 <div class="flex items-start"> 347 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 348 <i class="fa fa-check text-primary"></i> 349 </div> 350 <div> 351 <h4 class="font-semibold">阻塞式 IO</h4> 352 <p class="text-gray-600">accept() 和 read() 操作会阻塞线程,直到有新连接或数据到达</p> 353 </div> 354 </div> 355 <div class="flex items-start"> 356 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 357 <i class="fa fa-times text-danger"></i> 358 </div> 359 <div> 360 <h4 class="font-semibold">并发能力</h4> 361 <p class="text-gray-600">无法同时处理多个客户端,后续连接需等待当前连接关闭</p> 362 </div> 363 </div> 364 <div class="flex items-start"> 365 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 366 <i class="fa fa-lightbulb-o text-warning"></i> 367 </div> 368 <div> 369 <h4 class="font-semibold">适用场景</h4> 370 <p class="text-gray-600">简单测试、学习 Socket 基础,不适用于生产环境</p> 371 </div> 372 </div> 373 </div> 374 375 <div class="mt-8 pt-6 border-t border-gray-100"> 376 <h4 class="font-semibold mb-3">版本升级方向</h4> 377 <p class="text-gray-600 mb-4"> 378 单线程阻塞模型无法满足高并发需求,下一版本将引入多进程模型处理并发连接 379 </p> 380 <a href="#" class="text-primary hover:text-secondary transition-colors duration-200 inline-flex items-center"> 381 查看 V2:多进程并发 <i class="fa fa-arrow-right ml-2"></i> 382 </a> 383 </div> 384 </div> 385 </div> 386 387 <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-8"> 388 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 389 <div class="bg-gray-900 py-2 px-4 flex justify-between items-center"> 390 <div class="flex items-center"> 391 <div class="flex space-x-2 mr-4"> 392 <div class="w-3 h-3 rounded-full bg-danger"></div> 393 <div class="w-3 h-3 rounded-full bg-warning"></div> 394 <div class="w-3 h-3 rounded-full bg-secondary"></div> 395 </div> 396 <div class="text-gray-400 text-sm font-mono">client.c</div> 397 </div> 398 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-client-v1"> 399 <i class="fa fa-copy mr-1"></i>复制 400 </button> 401 402 </div> 403 <div class="p-4 overflow-x-auto scrollbar-hide"> 404 <pre class="text-gray-300 font-mono text-sm leading-relaxed"><code>#include <stdio.h> 405 #include <stdlib.h> 406 #include <string.h> 407 #include <unistd.h> 408 #include <arpa/inet.h> 409 #include <sys/socket.h> 410 411 void error_handling(const char *message); 412 413 int main(int argc, char* argv[]) 414 { 415 int sock; 416 struct sockaddr_in serv_addr; 417 char message[30]; 418 int str_len; 419 420 if(argc!=3) 421 { 422 printf("Usage : %s <IP> <port>\n", argv[0]); 423 exit(1); 424 } 425 426 sock=socket(PF_INET, SOCK_STREAM, 0); 427 if(sock == -1) 428 error_handling("socket() error"); 429 430 memset(&serv_addr, 0, sizeof(serv_addr)); 431 serv_addr.sin_family=AF_INET; 432 serv_addr.sin_addr.s_addr=inet_addr(argv[1]); 433 serv_addr.sin_port=htons(atoi(argv[2])); 434 435 if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 436 error_handling("connect() error!"); 437 438 str_len=read(sock, message, sizeof(message)-1); 439 if(str_len==-1) 440 error_handling("read() error!"); 441 442 printf("Message from server : %s \n", message); 443 close(sock); 444 return 0; 445 } 446 447 void error_handling(const char *message) 448 { 449 fputs(message, stderr); 450 fputc('\n', stderr); 451 exit(1); 452 }</code></pre> 453 </div> 454 </div> 455 <div class="bg-white rounded-xl p-6 shadow-xl"> 456 <h3 class="text-xl font-semibold mb-4 text-primary">客户端代码解析</h3> 457 <div class="space-y-4"> 458 <div class="flex items-start"> 459 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 460 <i class="fa fa-info-circle text-primary"></i> 461 </div> 462 <div> 463 <h4 class="font-semibold">基本功能</h4> 464 <p class="text-gray-600">创建套接字,连接到服务器,接收服务器消息并显示</p> 465 </div> 466 </div> 467 <div class="flex items-start"> 468 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 469 <i class="fa fa-info-circle text-primary"></i> 470 </div> 471 <div> 472 <h4 class="font-semibold">阻塞式 IO</h4> 473 <p class="text-gray-600">connect() 和 read() 操作会阻塞,直到连接成功或数据到达</p> 474 </div> 475 </div> 476 <div class="flex items-start"> 477 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 478 <i class="fa fa-info-circle text-primary"></i> 479 </div> 480 <div> 481 <h4 class="font-semibold">单次通信</h4> 482 <p class="text-gray-600">客户端接收服务器消息后立即关闭连接,不支持持续通信</p> 483 </div> 484 </div> 485 </div> 486 487 <div class="mt-8 pt-6 border-t border-gray-100"> 488 <h4 class="font-semibold mb-3">编译和运行</h4> 489 <pre class="bg-light p-4 rounded-lg text-sm font-mono overflow-x-auto scrollbar-hide"> 490 # 编译客户端 491 gcc client.c -o client 492 493 # 运行客户端(连接到本地8080端口) 494 ./client 127.0.0.1 8080</pre> 495 </div> 496 </div> 497 </div> 498 </div> 499 500 <!-- 其他版本内容(默认隐藏) --> 501 <div class="version-content hidden" id="version-2"> 502 <!-- V2 版本内容 --> 503 </div> 504 <div class="version-content hidden" id="version-3"> 505 <!-- V3 版本内容 --> 506 </div> 507 <div class="version-content hidden" id="version-4"> 508 <!-- V4 版本内容 --> 509 </div> 510 <div class="version-content hidden" id="version-5"> 511 <!-- V5 版本内容 --> 512 </div> 513 </div> 514 </div> 515 </section> 516 517 <!-- 运行演示部分 --> 518 <section id="demo" class="py-16 bg-white"> 519 <div class="container mx-auto px-4"> 520 <div class="max-w-6xl mx-auto"> 521 <div class="flex items-center mb-8"> 522 <div class="w-2 h-8 bg-warning rounded-full mr-3"></div> 523 <h2 class="text-2xl md:text-3xl font-bold text-dark">运行演示</h2> 524 </div> 525 526 <div class="bg-dark rounded-xl overflow-hidden shadow-xl mb-8"> 527 <div class="bg-gray-800 py-2 px-4 flex justify-between items-center"> 528 <div class="flex space-x-2"> 529 <div class="w-3 h-3 rounded-full bg-danger"></div> 530 <div class="w-3 h-3 rounded-full bg-warning"></div> 531 <div class="w-3 h-3 rounded-full bg-secondary"></div> 532 </div> 533 <div class="text-gray-300 text-sm font-mono">版本对比演示</div> 534 <div class="flex space-x-2"> 535 <select id="demo-version-select" class="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 focus:outline-none"> 536 <option value="1">V1:基础 TCP 单连接</option> 537 <option value="2">V2:多进程并发</option> 538 <option value="3">V3:多线程优化</option> 539 <option value="4">V4:IO 复用 (select/poll)</option> 540 <option value="5">V5:epoll 高性能模型</option> 541 </select> 542 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="play-demo"> 543 <i class="fa fa-play mr-1"></i>运行 544 </button> 545 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="reset-demo"> 546 <i class="fa fa-refresh mr-1"></i>重置 547 </button> 548 </div> 549 </div> 550 <div class="p-4 h-80 overflow-y-auto scrollbar-hide bg-dark text-gray-300 font-mono text-sm leading-relaxed" id="demo-output"> 551 <p class="text-gray-400">选择版本并点击"运行"按钮开始演示...</p> 552 </div> 553 </div> 554 555 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 556 <div class="bg-light rounded-xl p-6 shadow-md"> 557 <h3 class="text-lg font-semibold mb-4 flex items-center"> 558 <i class="fa fa-terminal text-warning mr-2"></i> 559 编译和运行命令 560 </h3> 561 <pre class="bg-code text-gray-300 p-4 rounded-lg text-sm overflow-x-auto scrollbar-hide font-mono"> 562 # 编译各版本服务器代码 563 gcc server_v1.c -o server_v1 564 gcc server_v2.c -o server_v2 565 gcc server_v3.c -o server_v3 -lpthread 566 gcc server_v4.c -o server_v4 567 gcc server_v5.c -o server_v5 568 569 # 运行服务器(监听端口8080) 570 ./server_vX 8080 # X 为版本号 571 572 # 运行客户端(连接到本地8080端口) 573 ./client 127.0.0.1 8080 574 </pre> 575 </div> 576 <div class="bg-light rounded-xl p-6 shadow-md"> 577 <h3 class="text-lg font-semibold mb-4 flex items-center"> 578 <i class="fa fa-exchange text-warning mr-2"></i> 579 通信流程 580 </h3> 581 <ol class="list-decimal pl-5 space-y-2 text-gray-700"> 582 <li>服务器创建套接字并绑定到指定端口</li> 583 <li>服务器开始监听客户端连接</li> 584 <li>客户端创建套接字并连接到服务器</li> 585 <li>服务器接受客户端连接请求</li> 586 <li>服务器与客户端进行数据交互</li> 587 <li>客户端关闭连接</li> 588 <li>服务器关闭客户端连接</li> 589 </ol> 590 </div> 591 </div> 592 </div> 593 </div> 594 </section> 595 596 <!-- 性能对比部分 --> 597 <section id="comparison" class="py-16 bg-gray-50"> 598 <div class="container mx-auto px-4"> 599 <div class="max-w-6xl mx-auto"> 600 <div class="flex items-center mb-8"> 601 <div class="w-2 h-8 bg-secondary rounded-full mr-3"></div> 602 <h2 class="text-2xl md:text-3xl font-bold text-dark">性能对比</h2> 603 </div> 604 605 <div class="bg-white rounded-xl p-6 shadow-xl mb-8"> 606 <h3 class="text-xl font-semibold mb-6 text-center">各版本性能指标对比</h3> 607 <div class="flex justify-center"> 608 <div class="w-full max-w-4xl"> 609 <canvas id="performanceChart" height="300"></canvas> 610 </div> 611 </div> 612 </div> 613 614 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 615 <div class="bg-white rounded-xl p-6 shadow-md"> 616 <h3 class="text-lg font-semibold mb-4">关键性能指标</h3> 617 <div class="space-y-4"> 618 <div> 619 <div class="flex justify-between mb-1"> 620 <span class="text-sm font-medium">最大并发连接数</span> 621 <span class="text-sm font-bold text-primary">V5 > V4 > V3 > V2 > V1</span> 622 </div> 623 <div class="w-full h-2 bg-gray-200 rounded-full"> 624 <div class="h-full bg-primary rounded-full" style="width: 90%"></div> 625 </div> 626 </div> 627 <div> 628 <div class="flex justify-between mb-1"> 629 <span class="text-sm font-medium">CPU 利用率</span> 630 <span class="text-sm font-bold text-secondary">V5 < V4 < V3 < V2 < V1</span> 631 </div> 632 <div class="w-full h-2 bg-gray-200 rounded-full"> 633 <div class="h-full bg-secondary rounded-full" style="width: 20%"></div> 634 </div> 635 </div> 636 <div> 637 <div class="flex justify-between mb-1"> 638 <span class="text-sm font-medium">内存占用</span> 639 <span class="text-sm font-bold text-warning">V5 < V4 < V3 > V2 > V1</span> 640 </div> 641 <div class="w-full h-2 bg-gray-200 rounded-full"> 642 <div class="h-full bg-warning rounded-full" style="width: 30%"></div> 643 </div> 644 </div> 645 <div> 646 <div class="flex justify-between mb-1"> 647 <span class="text-sm font-medium">响应延迟</span> 648 <span class="text-sm font-bold text-danger">V5 < V4 < V3 < V2 < V1</span> 649 </div> 650 <div class="w-full h-2 bg-gray-200 rounded-full"> 651 <div class="h-full bg-danger rounded-full" style="width: 10%"></div> 652 </div> 653 </div> 654 </div> 655 </div> 656 <div class="bg-white rounded-xl p-6 shadow-md"> 657 <h3 class="text-lg font-semibold mb-4">版本选择建议</h3> 658 <ul class="space-y-3 text-gray-700"> 659 <li class="flex items-start"> 660 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V1</span> 661 <div> 662 <p><span class="font-bold">适用场景:</span>学习基础 Socket 编程,简单测试</p> 663 <p class="text-sm text-gray-500">优点:代码简单;缺点:无法处理并发</p> 664 </div> 665 </li> 666 <li class="flex items-start"> 667 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V2</span> 668 <div> 669 <p><span class="font-bold">适用场景:</span>中等并发,稳定性要求高</p> 670 <p class="text-sm text-gray-500">优点:进程隔离安全;缺点:创建进程开销大</p> 671 </div> 672 </li> 673 <li class="flex items-start"> 674 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V3</span> 675 <div> 676 <p><span class="font-bold">适用场景:</span>高并发,计算密集型</p> 677 <p class="text-sm text-gray-500">优点:线程开销小;缺点:需要处理线程安全</p> 678 </div> 679 </li> 680 <li class="flex items-start"> 681 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V4</span> 682 <div> 683 <p><span class="font-bold">适用场景:</span>大量连接但活跃连接少</p> 684 <p class="text-sm text-gray-500">优点:单线程处理多连接;缺点:轮询开销大</p> 685 </div> 686 </li> 687 <li class="flex items-start"> 688 <span class="bg-green-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3 text-green-700">V5</span> 689 <div> 690 <p><span class="font-bold">适用场景:</span>高性能服务器,海量并发</p> 691 <p class="text-sm text-gray-500">优点:事件驱动,零拷贝;缺点:代码复杂度高</p> 692 </div> 693 </li> 694 </ul> 695 </div> 696 </div> 697 </div> 698 </div> 699 </section> 700 701 <!-- 页脚 --> 702 <footer class="bg-dark text-white py-12"> 703 <div class="container mx-auto px-4"> 704 <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> 705 <div> 706 <div class="flex items-center space-x-2 mb-4"> 707 <i class="fa fa-code text-primary text-2xl"></i> 708 <h2 class="text-xl font-bold">C Socket 编程示例</h2> 709 </div> 710 <p class="text-gray-400 mb-4"> 711 通过简洁的代码示例,了解如何在C语言中使用Socket实现网络通信 712 </p> 713 <div class="flex space-x-4"> 714 <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200"> 715 <i class="fa fa-github text-xl"></i> 716 </a> 717 <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200"> 718 <i class="fa fa-twitter text-xl"></i> 719 </a> 720 <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200"> 721 <i class="fa fa-linkedin text-xl"></i> 722 </a> 723 </div> 724 </div> 725 <div> 726 <h3 class="text-lg font-semibold mb-4">快速导航</h3> 727 <ul class="space-y-2"> 728 <li><a href="#overview" class="text-gray-400 hover:text-white transition-colors duration-200">概述</a></li> 729 <li><a href="#versions" class="text-gray-400 hover:text-white transition-colors duration-200">版本演进</a></li> 730 <li><a href="#demo" class="text-gray-400 hover:text-white transition-colors duration-200">运行演示</a></li> 731 <li><a href="#comparison" class="text-gray-400 hover:text-white transition-colors duration-200">性能对比</a></li> 732 </ul> 733 </div> 734 <div> 735 <h3 class="text-lg font-semibold mb-4">相关资源</h3> 736 <ul class="space-y-2"> 737 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">C语言教程</a></li> 738 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">Socket编程文档</a></li> 739 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">网络编程指南</a></li> 740 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">更多示例</a></li> 741 </ul> 742 </div> 743 </div> 744 <div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-500"> 745 <p>© 2023 C Socket 编程示例 | 保留所有权利</p> 746 </div> 747 </div> 748 </footer> 749 750 <!-- 脚本 --> 751 <script> 752 // 导航栏滚动效果 753 window.addEventListener('scroll', function() { 754 const navbar = document.getElementById('navbar'); 755 if (window.scrollY > 50) { 756 navbar.classList.add('py-2', 'bg-white/95', 'backdrop-blur-sm'); 757 navbar.classList.remove('py-3', 'bg-white'); 758 } else { 759 navbar.classList.add('py-3', 'bg-white'); 760 navbar.classList.remove('py-2', 'bg-white/95', 'backdrop-blur-sm'); 761 } 762 }); 763 764 // 移动端菜单切换 765 document.getElementById('menu-toggle').addEventListener('click', function() { 766 const mobileMenu = document.getElementById('mobile-menu'); 767 mobileMenu.classList.toggle('hidden'); 768 }); 769 770 // 版本切换功能 771 const versionTabs = document.querySelectorAll('.version-tab'); 772 const versionContents = document.querySelectorAll('.version-content'); 773 774 versionTabs.forEach(tab => { 775 tab.addEventListener('click', () => { 776 // 移除所有激活状态 777 versionTabs.forEach(t => t.classList.remove('active')); 778 versionContents.forEach(c => c.classList.add('hidden')); 779 780 // 设置当前激活状态 781 tab.classList.add('active'); 782 const version = tab.getAttribute('data-version'); 783 document.getElementById(`version-${version}`).classList.remove('hidden'); 784 785 // 平滑滚动到版本内容 786 document.getElementById(`version-${version}`).scrollIntoView({ behavior: 'smooth' }); 787 }); 788 }); 789 790 // 复制代码功能 791 document.getElementById('copy-server-v1').addEventListener('click', function() { 792 const code = document.querySelector('#version-1 pre code').textContent; 793 navigator.clipboard.writeText(code).then(() => { 794 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 795 setTimeout(() => { 796 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 797 }, 2000); 798 }); 799 }); 800 801 document.getElementById('copy-client-v1').addEventListener('click', function() { 802 const code = document.querySelector('#version-1 .grid-cols-1 md\\:grid-cols-2:nth-child(2) pre code').textContent; 803 navigator.clipboard.writeText(code).then(() => { 804 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 805 setTimeout(() => { 806 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 807 }, 2000); 808 }); 809 810 811 }); 812 813 814 // 演示运行功能 815 const demoOutput = document.getElementById('demo-output'); 816 const demoVersionSelect = document.getElementById('demo-version-select'); 817 818 document.getElementById('play-demo').addEventListener('click', function() { 819 const version = demoVersionSelect.value; 820 demoOutput.innerHTML = `<p class="text-gray-400">正在运行 V${version} 服务器演示...</p>`; 821 822 // 根据选择的版本显示不同的演示内容 823 const steps = getDemoSteps(version); 824 825 let i = 0; 826 const interval = setInterval(() => { 827 demoOutput.innerHTML += steps[i] + '<br>'; 828 demoOutput.scrollTop = demoOutput.scrollHeight; 829 i++; 830 if (i >= steps.length) { 831 clearInterval(interval); 832 } 833 }, 1000); 834 }); 835 836 document.getElementById('reset-demo').addEventListener('click', function() { 837 demoOutput.innerHTML = '<p class="text-gray-400">选择版本并点击"运行"按钮开始演示...</p>'; 838 }); 839 840 // 获取不同版本的演示步骤 841 function getDemoSteps(version) { 842 const baseSteps = [ 843 '<p class="text-yellow-300">编译服务器代码...</p>', 844 '<p class="text-gray-400">服务器代码编译完成</p>', 845 '<p class="text-yellow-300">启动服务器...</p>', 846 ]; 847 848 const versionSpecificSteps = { 849 1: [ 850 '<p class="text-gray-400">V1 服务器已启动,等待客户端连接...</p>', 851 '<p class="text-yellow-300">客户端1连接成功</p>', 852 '<p class="text-gray-400">服务器处理客户端1请求</p>', 853 '<p class="text-yellow-300">客户端2尝试连接...</p>', 854 '<p class="text-red-300">客户端2连接被拒绝:服务器正忙</p>', 855 '<p class="text-yellow-300">客户端1断开连接</p>', 856 '<p class="text-yellow-300">客户端2连接成功</p>', 857 '<p class="text-gray-400">服务器处理客户端2请求</p>', 858 '<p class="text-green-400">演示完成:V1 单线程模型无法同时处理多客户端</p>' 859 ], 860 5: [ 861 '<p class="text-gray-400">V5 服务器已启动,使用 epoll 监听连接...</p>', 862 '<p class="text-yellow-300">客户端1连接成功</p>', 863 '<p class="text-yellow-300">客户端2连接成功</p>', 864 '<p class="text-yellow-300">客户端3连接成功</p>', 865 '<p class="text-yellow-300">客户端4连接成功</p>', 866 '<p class="text-yellow-300">客户端5连接成功</p>', 867 '<p class="text-gray-400">服务器同时处理 5 个客户端连接...</p>', 868 '<p class="text-yellow-300">客户端1断开连接</p>', 869 '<p class="text-yellow-300">客户端6连接成功</p>', 870 '<p class="text-green-400">演示完成:V5 epoll 模型高效处理多并发连接</p>' 871 ] 872 // 其他版本步骤可在此扩展 873 }; 874 875 return [...baseSteps, ...(versionSpecificSteps[version] || [])]; 876 } 877 878 // 性能对比图表 879 window.addEventListener('load', function() { 880 const ctx = document.getElementById('performanceChart').getContext('2d'); 881 882 const performanceChart = new Chart(ctx, { 883 type: 'radar', 884 data: { 885 labels: ['并发连接数', 'CPU利用率', '内存占用', '响应延迟', '代码复杂度'], 886 datasets: [ 887 { 888 label: 'V1:基础单连接', 889 data: [10, 85, 20, 90, 10], 890 backgroundColor: 'rgba(22, 93, 255, 0.2)', 891 borderColor: 'rgba(22, 93, 255, 1)', 892 pointBackgroundColor: 'rgba(22, 93, 255, 1)', 893 pointBorderColor: '#fff', 894 pointHoverBackgroundColor: '#fff', 895 pointHoverBorderColor: 'rgba(22, 93, 255, 1)' 896 }, 897 { 898 label: 'V5:epoll高性能', 899 data: [90, 20, 30, 10, 80], 900 backgroundColor: 'rgba(0, 180, 42, 0.2)', 901 borderColor: 'rgba(0, 180, 42, 1)', 902 pointBackgroundColor: 'rgba(0, 180, 42, 1)', 903 pointBorderColor: '#fff', 904 pointHoverBackgroundColor: '#fff', 905 pointHoverBorderColor: 'rgba(0, 180, 42, 1)' 906 } 907 ] 908 }, 909 options: { 910 scales: { 911 r: { 912 angleLines: { 913 display: true 914 }, 915 suggestedMin: 0, 916 suggestedMax: 100 917 } 918 } 919 } 920 }); 921 }); 922 </script> 923 </body> 924 </html>
View Code
仅有V1的版本:

1 <!DOCTYPE html> 2 <html lang="zh-CN"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>C Socket 编程示例</title> 7 <script src="https://cdn.tailwindcss.com"></script> 8 <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> 9 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script> 10 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> 11 12 <!-- 引入Prism.js代码高亮库 --> 13 <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/themes/prism-tomorrow.min.css" rel="stylesheet"> 14 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/prism.min.js"></script> 15 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/components/prism-c.min.js"></script> 16 17 <script> 18 tailwind.config = { 19 theme: { 20 extend: { 21 colors: { 22 primary: '#165DFF', 23 secondary: '#00B42A', 24 danger: '#F53F3F', 25 warning: '#FF7D00', 26 dark: '#1D2129', 27 light: '#F2F3F5', 28 code: '#1E1E1E', 29 }, 30 fontFamily: { 31 inter: ['Inter', 'sans-serif'], 32 mono: ['Consolas', 'Monaco', 'monospace'], 33 }, 34 } 35 } 36 } 37 </script> 38 39 <style type="text/tailwindcss"> 40 @layer utilities { 41 .content-auto { 42 content-visibility: auto; 43 } 44 .scrollbar-hide { 45 -ms-overflow-style: none; 46 scrollbar-width: none; 47 } 48 .scrollbar-hide::-webkit-scrollbar { 49 display: none; 50 } 51 .text-shadow { 52 text-shadow: 0 2px 4px rgba(0,0,0,0.1); 53 } 54 .bg-gradient { 55 background: linear-gradient(135deg, #165DFF 0%, #00B42A 100%); 56 } 57 .version-tab { 58 @apply px-4 py-2 rounded-lg font-medium transition-all duration-200; 59 } 60 .version-tab.active { 61 @apply bg-primary text-white; 62 } 63 .version-tab:not(.active) { 64 @apply bg-gray-100 text-gray-600 hover:bg-gray-200; 65 } 66 /* 调整Prism.js样式以匹配网站设计 */ 67 .code-block pre[class*="language-"] { 68 @apply bg-code text-gray-300 font-mono text-sm leading-relaxed m-0 p-0; 69 border-radius: 0; 70 box-shadow: none; 71 } 72 .code-header { 73 @apply bg-gray-900 py-2 px-4 flex justify-between items-center; 74 } 75 .token.comment, .token.prolog, .token.doctype, .token.cdata { 76 @apply text-gray-500; 77 } 78 .token.keyword { 79 @apply text-purple-400; 80 } 81 .token.function { 82 @apply text-blue-400; 83 } 84 .token.string, .token.attr-value { 85 @apply text-green-400; 86 } 87 .token.operator { 88 @apply text-yellow-400; 89 } 90 .token.number { 91 @apply text-red-400; 92 } 93 } 94 </style> 95 </head> 96 <body class="font-inter bg-light text-dark min-h-screen flex flex-col"> 97 <!-- 导航栏 --> 98 <header class="bg-white shadow-md fixed w-full z-50 transition-all duration-300" id="navbar"> 99 <div class="container mx-auto px-4 py-3 flex justify-between items-center"> 100 <div class="flex items-center space-x-2"> 101 <i class="fa fa-code text-primary text-2xl"></i> 102 <h1 class="text-xl font-bold text-primary">C Socket 编程示例</h1> 103 </div> 104 <nav class="hidden md:flex space-x-8"> 105 <a href="#overview" class="text-dark hover:text-primary transition-colors duration-200 font-medium">概述</a> 106 <a href="#versions" class="text-dark hover:text-primary transition-colors duration-200 font-medium">版本演进</a> 107 <a href="#demo" class="text-dark hover:text-primary transition-colors duration-200 font-medium">运行演示</a> 108 <a href="#comparison" class="text-dark hover:text-primary transition-colors duration-200 font-medium">性能对比</a> 109 </nav> 110 <button class="md:hidden text-dark text-xl" id="menu-toggle"> 111 <i class="fa fa-bars"></i> 112 </button> 113 </div> 114 <!-- 移动端菜单 --> 115 <div class="md:hidden hidden bg-white absolute w-full shadow-lg" id="mobile-menu"> 116 <div class="container mx-auto px-4 py-2 flex flex-col space-y-3"> 117 <a href="#overview" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">概述</a> 118 <a href="#versions" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">版本演进</a> 119 <a href="#demo" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">运行演示</a> 120 <a href="#comparison" class="text-dark hover:text-primary transition-colors duration-200 py-2">性能对比</a> 121 </div> 122 </div> 123 </header> 124 125 <!-- 英雄区 --> 126 <section class="pt-24 pb-12 bg-gradient-to-br from-primary/5 to-secondary/5"> 127 <div class="container mx-auto px-4"> 128 <div class="max-w-4xl mx-auto text-center"> 129 <h2 class="text-[clamp(2rem,5vw,3.5rem)] font-bold text-dark mb-6 leading-tight"> 130 C Socket 编程示例 131 </h2> 132 <p class="text-lg text-gray-600 mb-8 max-w-2xl mx-auto"> 133 从基础单线程到高性能 epoll,探索 Socket 编程的演进之路 134 </p> 135 <div class="flex flex-wrap justify-center gap-4"> 136 <a href="#versions" class="bg-primary hover:bg-primary/90 text-white px-6 py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-1"> 137 <i class="fa fa-code mr-2"></i>查看版本演进 138 </a> 139 <a href="#demo" class="bg-white hover:bg-gray-50 text-primary border border-primary px-6 py-3 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-1"> 140 <i class="fa fa-play-circle mr-2"></i>观看运行演示 141 </a> 142 </div> 143 </div> 144 </div> 145 </section> 146 147 <!-- 概述部分 --> 148 <section id="overview" class="py-16 bg-white"> 149 <div class="container mx-auto px-4"> 150 <div class="max-w-6xl mx-auto"> 151 <div class="flex items-center mb-8"> 152 <div class="w-2 h-8 bg-primary rounded-full mr-3"></div> 153 <h2 class="text-2xl md:text-3xl font-bold text-dark">Socket 编程演进概述</h2> 154 </div> 155 156 <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12"> 157 <div> 158 <p class="text-gray-700 mb-6"> 159 Socket 编程是网络通信的基础,从简单的单线程服务器到高性能的异步 IO 模型,经历了多个阶段的演进。本示例展示了从基础版本逐步升级到使用 epoll 的完整过程。 160 </p> 161 <div class="bg-light rounded-xl p-6 shadow-md"> 162 <h3 class="text-lg font-semibold mb-4 text-primary">5个版本演进路线</h3> 163 <ol class="space-y-3 text-gray-700"> 164 <li><span class="font-bold">V1:基础 TCP 单连接</span> - 单线程处理单个客户端,阻塞式 IO</li> 165 <li><span class="font-bold">V2:多进程并发</span> - 使用 fork() 创建子进程处理多个客户端</li> 166 <li><span class="font-bold">V3:多线程优化</span> - 使用 pthread 创建线程处理并发</li> 167 <li><span class="font-bold">V4:IO 复用 (select/poll)</span> - 使用 select/poll 实现单线程处理多连接</li> 168 <li><span class="font-bold">V5:epoll 高性能模型</span> - 使用 epoll 实现高效的异步 IO</li> 169 </ol> 170 </div> 171 </div> 172 <div class="bg-white rounded-xl overflow-hidden shadow-xl"> 173 <img src="https://picsum.photos/seed/socket/800/600" alt="Socket 编程演进" class="w-full h-auto"> 174 <div class="p-6"> 175 <h3 class="text-lg font-semibold mb-2">Socket 模型性能对比</h3> 176 <p class="text-gray-600">随着版本演进,服务器处理并发连接的能力显著提升</p> 177 </div> 178 </div> 179 </div> 180 181 <div class="grid grid-cols-1 md:grid-cols-5 gap-4"> 182 <div class="bg-primary/5 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-primary/20"> 183 <div class="text-3xl font-bold text-primary mb-2">V1</div> 184 <p class="text-gray-700 text-sm">基础单连接</p> 185 </div> 186 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 187 <div class="text-3xl font-bold text-gray-600 mb-2">V2</div> 188 <p class="text-gray-700 text-sm">多进程</p> 189 </div> 190 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 191 <div class="text-3xl font-bold text-gray-600 mb-2">V3</div> 192 <p class="text-gray-700 text-sm">多线程</p> 193 </div> 194 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 195 <div class="text-3xl font-bold text-gray-600 mb-2">V4</div> 196 <p class="text-gray-700 text-sm">select/poll</p> 197 </div> 198 <div class="bg-secondary/5 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-secondary/20"> 199 <div class="text-3xl font-bold text-secondary mb-2">V5</div> 200 <p class="text-gray-700 text-sm">epoll</p> 201 </div> 202 </div> 203 </div> 204 </div> 205 </section> 206 207 <!-- 版本演进部分 --> 208 <section id="versions" class="py-16 bg-gray-50"> 209 <div class="container mx-auto px-4"> 210 <div class="max-w-6xl mx-auto"> 211 <div class="flex items-center mb-8"> 212 <div class="w-2 h-8 bg-primary rounded-full mr-3"></div> 213 <h2 class="text-2xl md:text-3xl font-bold text-dark">版本演进</h2> 214 </div> 215 216 <!-- 版本切换标签 --> 217 <div class="flex flex-wrap justify-center gap-3 mb-12"> 218 <button class="version-tab active" data-version="1">V1:基础 TCP 单连接</button> 219 <button class="version-tab" data-version="2">V2:多进程并发</button> 220 <button class="version-tab" data-version="3">V3:多线程优化</button> 221 <button class="version-tab" data-version="4">V4:IO 复用 (select/poll)</button> 222 <button class="version-tab" data-version="5">V5:epoll 高性能模型</button> 223 </div> 224 225 <!-- 版本内容容器 --> 226 <div class="version-content" id="version-1"> 227 <!-- 新增思考板块 --> 228 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 229 <div class="bg-light rounded-xl p-6 shadow-md"> 230 <h3 class="text-lg font-semibold mb-4 flex items-center"> 231 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 232 《我自己的思考》 233 </h3> 234 <div class="border-b border-gray-300 pb-4 mb-4"> 235 <h4 class="text-base font-semibold mb-2">listen(5) 的作用</h4> 236 <p class="text-sm text-gray-700 leading-relaxed"> 237 listen 的第二个参数(5)表示未处理的连接请求队列的最大长度,而非服务器能处理的最大客户端数。<br> 238 当客户端连接时,若服务器正忙,连接会被放入队列等待(最多5个) 239 </p> 240 </div> 241 <div> 242 <h4 class="text-base font-semibold mb-2">代码逻辑限制</h4> 243 <p class="text-sm text-gray-700 leading-relaxed"> 244 代码中 accept() 仅调用一次,处理完一个客户端后立即关闭连接并退出程序,<br> 245 因此即使队列中有等待连接,也无法处理后续客户端。<br> 246 改进方法:需要将 accept() 放入循环中持续接收新连接<br> 247 accept() 是阻塞调用,若无连接会一直等待<br> 248 客户端连接上后服务端就触发 write,客户端的 read() 同样阻塞,直到接收到数据<br> 249 然后客户端读 helloworld 250 </p> 251 </div> 252 </div> 253 <div class="bg-light rounded-xl p-6 shadow-md"> 254 <h3 class="text-lg font-semibold mb-4 flex items-center"> 255 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 256 《代码细节》 257 </h3> 258 <div class="border-b border-gray-300 pb-4 mb-4"> 259 <h4 class="text-base font-semibold mb-2">关于基础知识:</h4> 260 <p class="text-sm text-gray-700 leading-relaxed"> 261 对于字符串 262 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">"Hello World!"</code>: 263 <br> 264 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">sizeof</code>:计算对象占用的内存大小(含 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>),是编译时确定的常量<br> 265 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">strlen</code>:计算字符串的实际长度(不含 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>),运行时计算<br><br> 266 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>是空字符(ASCII 码为 0),是字符串的结束标志,不是空格,不可见且无法直接 printf 输出<br><br> 267 </p> 268 </div> 269 <div> 270 <h4 class="text-base font-semibold mb-2">关于缓冲区溢出风险:</h4> 271 <p class="text-sm text-gray-700 leading-relaxed"> 272 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">message</code> 是缓冲区大小 30 字节,read 返回实际读的字节,无论是否 -1,<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">strlen</code> 都是 13,接收到的就是<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">Hello World!\0</code><br><br> 273 假设 message 数组大小为 100 字节 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">(sizeof(message) == 100)</code>:<br><br> 274 不减 1:<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">read(sock, message, 100)</code><br> 275 若服务器发送 100 字节数据,read 会将 100 字节全部写入 message,此时数组末尾没有空间存放<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>。若后续用 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">printf("%s", message)</code> 输出,会因找不到 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code> 而继续读取内存,导致段错误或输出乱码<br><br> 276 减 1:<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">read(sock, message, 99)</code><br> 277 read 最多读取 99 字节,剩余 1 字节用于手动添加 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code> ,避免溢出<br><br> 278 然后还要添加上 \0,防止末本身尾没有 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>,代码如下: <br> 279 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm"> message[str_len] = '\0';</code> <br> 280 </p> 281 </div> 282 </div> 283 </div> 284 <br> 285 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 286 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 287 <div class="code-header"> 288 <div class="flex items-center"> 289 <div class="flex space-x-2 mr-4"> 290 <div class="w-3 h-3 rounded-full bg-danger"></div> 291 <div class="w-3 h-3 rounded-full bg-warning"></div> 292 <div class="w-3 h-3 rounded-full bg-secondary"></div> 293 </div> 294 <div class="text-gray-400 text-sm font-mono">server.c</div> 295 </div> 296 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-server-v1"> 297 <i class="fa fa-copy mr-1"></i>复制 298 </button> 299 </div> 300 <div class="code-block"> 301 <pre><code class="language-c">#include <stdio.h> 302 #include <stdlib.h> 303 #include <string.h> 304 #include <unistd.h> 305 #include <arpa/inet.h> 306 #include <sys/socket.h> 307 308 void error_handling(const char *message); 309 310 int main(int argc, char *argv[]) 311 { 312 int serv_sock; 313 int clnt_sock; 314 struct sockaddr_in serv_addr; 315 struct sockaddr_in clnt_addr; 316 socklen_t clnt_addr_size; 317 char message[]="Hello World!"; 318 319 if(argc!=2) 320 { 321 printf("Usage : %s <port>\n", argv[0]); 322 exit(1); 323 } 324 325 serv_sock=socket(PF_INET, SOCK_STREAM, 0); 326 if(serv_sock == -1) 327 error_handling("socket() error"); 328 329 memset(&serv_addr, 0, sizeof(serv_addr)); 330 serv_addr.sin_family=AF_INET; 331 serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); 332 serv_addr.sin_port=htons(atoi(argv[1])); 333 334 if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1) 335 error_handling("bind() error"); 336 337 if(listen(serv_sock, 5)==-1) 338 error_handling("listen() error"); 339 340 clnt_addr_size=sizeof(clnt_addr); 341 clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); 342 if(clnt_sock==-1) 343 error_handling("accept() error"); 344 345 write(clnt_sock, message, sizeof(message)); 346 close(clnt_sock); 347 close(serv_sock); 348 return 0; 349 } 350 351 void error_handling(const char *message) 352 { 353 fputs(message, stderr); 354 fputc('\n', stderr); 355 exit(1); 356 }</code></pre> 357 </div> 358 </div> 359 <div class="bg-white rounded-xl p-6 shadow-xl"> 360 <h3 class="text-xl font-semibold mb-4 text-primary">版本特点</h3> 361 <div class="space-y-4"> 362 <div class="flex items-start"> 363 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 364 <i class="fa fa-check text-primary"></i> 365 </div> 366 <div> 367 <h4 class="font-semibold">单线程处理</h4> 368 <p class="text-gray-600">服务器在单个线程中运行,一次只能处理一个客户端连接</p> 369 </div> 370 </div> 371 <div class="flex items-start"> 372 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 373 <i class="fa fa-check text-primary"></i> 374 </div> 375 <div> 376 <h4 class="font-semibold">阻塞式 IO</h4> 377 <p class="text-gray-600">accept() 和 read() 操作会阻塞线程,直到有新连接或数据到达</p> 378 </div> 379 </div> 380 <div class="flex items-start"> 381 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 382 <i class="fa fa-times text-danger"></i> 383 </div> 384 <div> 385 <h4 class="font-semibold">并发能力</h4> 386 <p class="text-gray-600">无法同时处理多个客户端,后续连接需等待当前连接关闭</p> 387 </div> 388 </div> 389 <div class="flex items-start"> 390 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 391 <i class="fa fa-lightbulb-o text-warning"></i> 392 </div> 393 <div> 394 <h4 class="font-semibold">适用场景</h4> 395 <p class="text-gray-600">简单测试、学习 Socket 基础,不适用于生产环境</p> 396 </div> 397 </div> 398 </div> 399 400 <div class="mt-8 pt-6 border-t border-gray-100"> 401 <h4 class="font-semibold mb-3">版本升级方向</h4> 402 <p class="text-gray-600 mb-4"> 403 单线程阻塞模型无法满足高并发需求,下一版本将引入多进程模型处理并发连接 404 </p> 405 <a href="#" class="text-primary hover:text-secondary transition-colors duration-200 inline-flex items-center"> 406 查看 V2:多进程并发 <i class="fa fa-arrow-right ml-2"></i> 407 </a> 408 </div> 409 </div> 410 </div> 411 412 <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-8"> 413 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 414 <div class="code-header"> 415 <div class="flex items-center"> 416 <div class="flex space-x-2 mr-4"> 417 <div class="w-3 h-3 rounded-full bg-danger"></div> 418 <div class="w-3 h-3 rounded-full bg-warning"></div> 419 <div class="w-3 h-3 rounded-full bg-secondary"></div> 420 </div> 421 <div class="text-gray-400 text-sm font-mono">client.c</div> 422 </div> 423 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-client-v1"> 424 <i class="fa fa-copy mr-1"></i>复制 425 </button> 426 </div> 427 <div class="code-block"> 428 <pre><code class="language-c">#include <stdio.h> 429 #include <stdlib.h> 430 #include <string.h> 431 #include <unistd.h> 432 #include <arpa/inet.h> 433 #include <sys/socket.h> 434 435 void error_handling(const char *message); 436 437 int main(int argc, char* argv[]) 438 { 439 int sock; 440 struct sockaddr_in serv_addr; 441 char message[30]; 442 int str_len; 443 444 if(argc!=3) 445 { 446 printf("Usage : %s <IP> <port>\n", argv[0]); 447 exit(1); 448 } 449 450 sock=socket(PF_INET, SOCK_STREAM, 0); 451 if(sock == -1) 452 error_handling("socket() error"); 453 454 memset(&serv_addr, 0, sizeof(serv_addr)); 455 serv_addr.sin_family=AF_INET; 456 serv_addr.sin_addr.s_addr=inet_addr(argv[1]); 457 serv_addr.sin_port=htons(atoi(argv[2])); 458 459 if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 460 error_handling("connect() error!"); 461 462 str_len=read(sock, message, sizeof(message)-1); 463 if(str_len==-1) 464 error_handling("read() error!"); 465 466 printf("Message from server : %s \n", message); 467 close(sock); 468 return 0; 469 } 470 471 void error_handling(const char *message) 472 { 473 fputs(message, stderr); 474 fputc('\n', stderr); 475 exit(1); 476 }</code></pre> 477 </div> 478 </div> 479 <div class="bg-white rounded-xl p-6 shadow-xl"> 480 <h3 class="text-xl font-semibold mb-4 text-primary">客户端代码解析</h3> 481 <div class="space-y-4"> 482 <div class="flex items-start"> 483 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 484 <i class="fa fa-info-circle text-primary"></i> 485 </div> 486 <div> 487 <h4 class="font-semibold">基本功能</h4> 488 <p class="text-gray-600">创建套接字,连接到服务器,接收服务器消息并显示</p> 489 </div> 490 </div> 491 <div class="flex items-start"> 492 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 493 <i class="fa fa-info-circle text-primary"></i> 494 </div> 495 <div> 496 <h4 class="font-semibold">阻塞式 IO</h4> 497 <p class="text-gray-600">connect() 和 read() 操作会阻塞,直到连接成功或数据到达</p> 498 </div> 499 </div> 500 <div class="flex items-start"> 501 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 502 <i class="fa fa-info-circle text-primary"></i> 503 </div> 504 <div> 505 <h4 class="font-semibold">单次通信</h4> 506 <p class="text-gray-600">客户端接收服务器消息后立即关闭连接,不支持持续通信</p> 507 </div> 508 </div> 509 </div> 510 511 <div class="mt-8 pt-6 border-t border-gray-100"> 512 <h4 class="font-semibold mb-3">编译和运行</h4> 513 <pre class="bg-light p-4 rounded-lg text-sm font-mono overflow-x-auto scrollbar-hide"> 514 # 编译客户端 515 gcc client.c -o client 516 517 # 运行客户端(连接到本地8080端口) 518 ./client 127.0.0.1 8080</pre> 519 </div> 520 </div> 521 </div> 522 </div> 523 524 <!-- 其他版本内容(默认隐藏) --> 525 <div class="version-content hidden" id="version-2"> 526 <!-- V2 版本内容 --> 527 </div> 528 <div class="version-content hidden" id="version-3"> 529 <!-- V3 版本内容 --> 530 </div> 531 <div class="version-content hidden" id="version-4"> 532 <!-- V4 版本内容 --> 533 </div> 534 <div class="version-content hidden" id="version-5"> 535 <!-- V5 版本内容 --> 536 </div> 537 </div> 538 </div> 539 </section> 540 541 <!-- 运行演示部分 --> 542 <section id="demo" class="py-16 bg-white"> 543 <div class="container mx-auto px-4"> 544 <div class="max-w-6xl mx-auto"> 545 <div class="flex items-center mb-8"> 546 <div class="w-2 h-8 bg-warning rounded-full mr-3"></div> 547 <h2 class="text-2xl md:text-3xl font-bold text-dark">运行演示</h2> 548 </div> 549 550 <div class="bg-dark rounded-xl overflow-hidden shadow-xl mb-8"> 551 <div class="bg-gray-800 py-2 px-4 flex justify-between items-center"> 552 <div class="flex space-x-2"> 553 <div class="w-3 h-3 rounded-full bg-danger"></div> 554 <div class="w-3 h-3 rounded-full bg-warning"></div> 555 <div class="w-3 h-3 rounded-full bg-secondary"></div> 556 </div> 557 <div class="text-gray-300 text-sm font-mono">版本对比演示</div> 558 <div class="flex space-x-2"> 559 <select id="demo-version-select" class="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 focus:outline-none"> 560 <option value="1">V1:基础 TCP 单连接</option> 561 <option value="2">V2:多进程并发</option> 562 <option value="3">V3:多线程优化</option> 563 <option value="4">V4:IO 复用 (select/poll)</option> 564 <option value="5">V5:epoll 高性能模型</option> 565 </select> 566 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="play-demo"> 567 <i class="fa fa-play mr-1"></i>运行 568 </button> 569 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="reset-demo"> 570 <i class="fa fa-refresh mr-1"></i>重置 571 </button> 572 </div> 573 </div> 574 <div class="p-4 h-80 overflow-y-auto scrollbar-hide bg-dark text-gray-300 font-mono text-sm leading-relaxed" id="demo-output"> 575 <p class="text-gray-400">选择版本并点击"运行"按钮开始演示...</p> 576 </div> 577 </div> 578 579 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 580 <div class="bg-light rounded-xl p-6 shadow-md"> 581 <h3 class="text-lg font-semibold mb-4 flex items-center"> 582 <i class="fa fa-terminal text-warning mr-2"></i> 583 编译和运行命令 584 </h3> 585 <pre class="bg-code text-gray-300 p-4 rounded-lg text-sm overflow-x-auto scrollbar-hide font-mono"> 586 # 编译各版本服务器代码 587 gcc server_v1.c -o server_v1 588 gcc server_v2.c -o server_v2 589 gcc server_v3.c -o server_v3 -lpthread 590 gcc server_v4.c -o server_v4 591 gcc server_v5.c -o server_v5 592 593 # 运行服务器(监听端口8080) 594 ./server_vX 8080 # X 为版本号 595 596 # 运行客户端(连接到本地8080端口) 597 ./client 127.0.0.1 8080 598 </pre> 599 </div> 600 <div class="bg-light rounded-xl p-6 shadow-md"> 601 <h3 class="text-lg font-semibold mb-4 flex items-center"> 602 <i class="fa fa-exchange text-warning mr-2"></i> 603 通信流程 604 </h3> 605 <ol class="list-decimal pl-5 space-y-2 text-gray-700"> 606 <li>服务器创建套接字并绑定到指定端口</li> 607 <li>服务器开始监听客户端连接</li> 608 <li>客户端创建套接字并连接到服务器</li> 609 <li>服务器接受客户端连接请求</li> 610 <li>服务器与客户端进行数据交互</li> 611 <li>客户端关闭连接</li> 612 <li>服务器关闭客户端连接</li> 613 </ol> 614 </div> 615 </div> 616 </div> 617 </div> 618 </section> 619 620 <!-- 性能对比部分 --> 621 <section id="comparison" class="py-16 bg-gray-50"> 622 <div class="container mx-auto px-4"> 623 <div class="max-w-6xl mx-auto"> 624 <div class="flex items-center mb-8"> 625 <div class="w-2 h-8 bg-secondary rounded-full mr-3"></div> 626 <h2 class="text-2xl md:text-3xl font-bold text-dark">性能对比</h2> 627 </div> 628 629 <div class="bg-white rounded-xl p-6 shadow-xl mb-8"> 630 <h3 class="text-xl font-semibold mb-6 text-center">各版本性能指标对比</h3> 631 <div class="flex justify-center"> 632 <div class=" 633 634 <div class="bg-white rounded-xl p-6 shadow-xl mb-8 dark:bg-darkcard"> 635 <h3 class="text-xl font-semibold mb-6 text-center dark:text-white">各版本性能指标对比</h3> 636 <div class="flex justify-center"> 637 <div class="w-full max-w-4xl"> 638 <canvas id="performanceChart" height="300"></canvas> 639 </div> 640 </div> 641 </div> 642 643 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 644 <div class="bg-white rounded-xl p-6 shadow-md dark:bg-darkcard"> 645 <h3 class="text-lg font-semibold mb-4 dark:text-white">关键性能指标</h3> 646 <div class="space-y-4"> 647 <div> 648 <div class="flex justify-between mb-1"> 649 <span class="text-sm font-medium dark:text-gray-300">最大并发连接数</span> 650 <span class="text-sm font-bold text-primary dark:text-primary">V5 > V4 > V3 > V2 > V1</span> 651 </div> 652 <div class="w-full h-2 bg-gray-200 rounded-full dark:bg-gray-700"> 653 <div class="h-full bg-primary rounded-full" style="width: 90%"></div> 654 </div> 655 </div> 656 <div> 657 <div class="flex justify-between mb-1"> 658 <span class="text-sm font-medium dark:text-gray-300">CPU 利用率</span> 659 <span class="text-sm font-bold text-secondary dark:text-secondary">V5 < V4 < V3 < V2 < V1</span> 660 </div> 661 <div class="w-full h-2 bg-gray-200 rounded-full dark:bg-gray-700"> 662 <div class="h-full bg-secondary rounded-full" style="width: 20%"></div> 663 </div> 664 </div> 665 <div> 666 <div class="flex justify-between mb-1"> 667 <span class="text-sm font-medium dark:text-gray-300">内存占用</span> 668 <span class="text-sm font-bold text-warning">V5 < V4 < V3 > V2 > V1</span> 669 </div> 670 <div class="w-full h-2 bg-gray-200 rounded-full dark:bg-gray-700"> 671 <div class="h-full bg-warning rounded-full" style="width: 30%"></div> 672 </div> 673 </div> 674 <div> 675 <div class="flex justify-between mb-1"> 676 <span class="text-sm font-medium dark:text-gray-300">响应延迟</span> 677 <span class="text-sm font-bold text-danger">V5 < V4 < V3 < V2 < V1</span> 678 </div> 679 <div class="w-full h-2 bg-gray-200 rounded-full dark:bg-gray-700"> 680 <div class="h-full bg-danger rounded-full" style="width: 10%"></div> 681 </div> 682 </div> 683 </div> 684 </div> 685 <div class="bg-white rounded-xl p-6 shadow-md dark:bg-darkcard"> 686 <h3 class="text-lg font-semibold mb-4 dark:text-white">版本选择建议</h3> 687 <ul class="space-y-3 text-gray-700 dark:text-gray-400"> 688 <li class="flex items-start"> 689 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3 dark:bg-gray-700">V1</span> 690 <div> 691 <p><span class="font-bold">适用场景:</span>学习基础 Socket 编程,简单测试</p> 692 <p class="text-sm text-gray-500 dark:text-gray-400">优点:代码简单;缺点:无法处理并发</p> 693 </div> 694 </li> 695 <li class="flex items-start"> 696 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3 dark:bg-gray-700">V2</span> 697 <div> 698 <p><span class="font-bold">适用场景:</span>中等并发,稳定性要求高</p> 699 <p class="text-sm text-gray-500 dark:text-gray-400">优点:进程隔离安全;缺点:创建进程开销大</p> 700 </div> 701 </li> 702 <li class="flex items-start"> 703 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3 dark:bg-gray-700">V3</span> 704 <div> 705 <p><span class="font-bold">适用场景:</span>高并发,计算密集型</p> 706 <p class="text-sm text-gray-500 dark:text-gray-400">优点:线程开销小;缺点:需要处理线程安全</p> 707 </div> 708 </li> 709 <li class="flex items-start"> 710 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3 dark:bg-gray-700">V4</span> 711 <div> 712 <p><span class="font-bold">适用场景:</span>大量连接但活跃连接少</p> 713 <p class="text-sm text-gray-500 dark:text-gray-400">优点:单线程处理多连接;缺点:轮询开销大</p> 714 </div> 715 </li> 716 <li class="flex items-start"> 717 <span class="bg-green-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3 text-green-700 dark:bg-green-900/30 dark:text-green-300">V5</span> 718 <div> 719 <p><span class="font-bold">适用场景:</span>高性能服务器,海量并发</p> 720 <p class="text-sm text-gray-500 dark:text-gray-400">优点:事件驱动,零拷贝;缺点:代码复杂度高</p> 721 </div> 722 </li> 723 </ul> 724 </div> 725 </div> 726 </div> 727 </div> 728 </section> 729 730 <!-- 页脚 --> 731 <footer class="bg-dark text-white py-12"> 732 <div class="container mx-auto px-4"> 733 <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> 734 <div> 735 <div class="flex items-center space-x-2 mb-4"> 736 <i class="fa fa-code text-primary text-2xl"></i> 737 <h2 class="text-xl font-bold">C Socket 编程示例</h2> 738 </div> 739 <p class="text-gray-400 mb-4"> 740 通过简洁的代码示例,了解如何在C语言中使用Socket实现网络通信 741 </p> 742 <div class="flex space-x-4"> 743 <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200"> 744 <i class="fa fa-github text-xl"></i> 745 </a> 746 <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200"> 747 <i class="fa fa-twitter text-xl"></i> 748 </a> 749 <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200"> 750 <i class="fa fa-linkedin text-xl"></i> 751 </a> 752 </div> 753 </div> 754 <div> 755 <h3 class="text-lg font-semibold mb-4">快速导航</h3> 756 <ul class="space-y-2"> 757 <li><a href="#overview" class="text-gray-400 hover:text-white transition-colors duration-200">概述</a></li> 758 <li><a href="#versions" class="text-gray-400 hover:text-white transition-colors duration-200">版本演进</a></li> 759 <li><a href="#demo" class="text-gray-400 hover:text-white transition-colors duration-200">运行演示</a></li> 760 <li><a href="#comparison" class="text-gray-400 hover:text-white transition-colors duration-200">性能对比</a></li> 761 </ul> 762 </div> 763 <div> 764 <h3 class="text-lg font-semibold mb-4">相关资源</h3> 765 <ul class="space-y-2"> 766 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">C语言教程</a></li> 767 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">Socket编程文档</a></li> 768 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">网络编程指南</a></li> 769 <li><a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">更多示例</a></li> 770 </ul> 771 </div> 772 </div> 773 <div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-500"> 774 <p>© 2023 C Socket 编程示例 | 保留所有权利</p> 775 </div> 776 </div> 777 </footer> 778 779 <!-- 脚本 --> 780 <script> 781 // 导航栏滚动效果 782 window.addEventListener('scroll', function() { 783 const navbar = document.getElementById('navbar'); 784 if (window.scrollY > 50) { 785 navbar.classList.add('py-2', 'bg-white/95', 'backdrop-blur-sm', 'dark:bg-gray-900/95'); 786 navbar.classList.remove('py-3', 'bg-white', 'dark:bg-gray-900'); 787 } else { 788 navbar.classList.add('py-3', 'bg-white', 'dark:bg-gray-900'); 789 navbar.classList.remove('py-2', 'bg-white/95', 'backdrop-blur-sm', 'dark:bg-gray-900/95'); 790 } 791 }); 792 793 // 移动端菜单切换 794 document.getElementById('menu-toggle').addEventListener('click', function() { 795 const mobileMenu = document.getElementById('mobile-menu'); 796 mobileMenu.classList.toggle('hidden'); 797 }); 798 799 // 版本切换功能 800 const versionTabs = document.querySelectorAll('.version-tab'); 801 const versionContents = document.querySelectorAll('.version-content'); 802 803 versionTabs.forEach(tab => { 804 tab.addEventListener('click', () => { 805 // 移除所有激活状态 806 versionTabs.forEach(t => t.classList.remove('active')); 807 versionContents.forEach(c => c.classList.add('hidden')); 808 809 // 设置当前激活状态 810 tab.classList.add('active'); 811 const version = tab.getAttribute('data-version'); 812 document.getElementById(`version-${version}`).classList.remove('hidden'); 813 814 // 平滑滚动到版本内容 815 document.getElementById(`version-${version}`).scrollIntoView({ behavior: 'smooth' }); 816 }); 817 }); 818 819 // 复制代码功能 820 document.getElementById('copy-server-v1').addEventListener('click', function() { 821 const code = document.getElementById('server-code-v1').textContent; 822 navigator.clipboard.writeText(code).then(() => { 823 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 824 setTimeout(() => { 825 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 826 }, 2000); 827 }); 828 }); 829 830 // 修复客户端复制功能 831 document.getElementById('copy-client-v1').addEventListener('click', function() { 832 const code = document.getElementById('client-code-v1').textContent; 833 navigator.clipboard.writeText(code).then(() => { 834 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 835 setTimeout(() => { 836 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 837 }, 2000); 838 }); 839 }); 840 841 // 演示运行功能 842 const demoOutput = document.getElementById('demo-output'); 843 const demoVersionSelect = document.getElementById('demo-version-select'); 844 845 document.getElementById('play-demo').addEventListener('click', function() { 846 const version = demoVersionSelect.value; 847 demoOutput.innerHTML = `<p class="text-gray-400">正在运行 V${version} 服务器演示...</p>`; 848 849 // 根据选择的版本显示不同的演示内容 850 const steps = getDemoSteps(version); 851 852 let i = 0; 853 const interval = setInterval(() => { 854 demoOutput.innerHTML += steps[i] + '<br>'; 855 demoOutput.scrollTop = demoOutput.scrollHeight; 856 i++; 857 if (i >= steps.length) { 858 clearInterval(interval); 859 } 860 }, 1000); 861 }); 862 863 document.getElementById('reset-demo').addEventListener('click', function() { 864 demoOutput.innerHTML = '<p class="text-gray-400">选择版本并点击"运行"按钮开始演示...</p>'; 865 }); 866 867 // 获取不同版本的演示步骤 868 function getDemoSteps(version) { 869 const baseSteps = [ 870 '<p class="text-yellow-300">编译服务器代码...</p>', 871 '<p class="text-gray-400">服务器代码编译完成</p>', 872 '<p class="text-yellow-300">启动服务器...</p>', 873 ]; 874 875 const versionSpecificSteps = { 876 1: [ 877 '<p class="text-gray-400">V1 服务器已启动,等待客户端连接...</p>', 878 '<p class="text-yellow-300">客户端1连接成功</p>', 879 '<p class="text-gray-400">服务器处理客户端1请求</p>', 880 '<p class="text-yellow-300">客户端2尝试连接...</p>', 881 '<p class="text-red-300">客户端2连接被拒绝:服务器正忙</p>', 882 '<p class="text-yellow-300">客户端1断开连接</p>', 883 '<p class="text-yellow-300">客户端2连接成功</p>', 884 '<p class="text-gray-400">服务器处理客户端2请求</p>', 885 '<p class="text-green-400">演示完成:V1 单线程模型无法同时处理多客户端</p>' 886 ], 887 5: [ 888 '<p class="text-gray-400">V5 服务器已启动,使用 epoll 监听连接...</p>', 889 '<p class="text-yellow-300">客户端1连接成功</p>', 890 '<p class="text-yellow-300">客户端2连接成功</p>', 891 '<p class="text-yellow-300">客户端3连接成功</p>', 892 '<p class="text-yellow-300">客户端4连接成功</p>', 893 '<p class="text-yellow-300">客户端5连接成功</p>', 894 '<p class="text-gray-400">服务器同时处理 5 个客户端连接...</p>', 895 '<p class="text-yellow-300">客户端1断开连接</p>', 896 '<p class="text-yellow-300">客户端6连接成功</p>', 897 '<p class="text-green-400">演示完成:V5 epoll 模型高效处理多并发连接</p>' 898 ] 899 // 其他版本步骤可在此扩展 900 }; 901 902 return [...baseSteps, ...(versionSpecificSteps[version] || [])]; 903 } 904 905 // 性能对比图表 906 window.addEventListener('load', function() { 907 const ctx = document.getElementById('performanceChart').getContext('2d'); 908 909 const performanceChart = new Chart(ctx, { 910 type: 'radar', 911 data: { 912 labels: ['并发连接数', 'CPU利用率', '内存占用', '响应延迟', '代码复杂度'], 913 datasets: [ 914 { 915 label: 'V1:基础单连接', 916 data: [10, 85, 20, 90, 10], 917 backgroundColor: 'rgba(22, 93, 255, 0.2)', 918 borderColor: 'rgba(22, 93, 255, 1)', 919 pointBackgroundColor: 'rgba(22, 93, 255, 1)', 920 pointBorderColor: '#fff', 921 pointHoverBackgroundColor: '#fff', 922 pointHoverBorderColor: 'rgba(22, 93, 255, 1)' 923 }, 924 { 925 label: 'V5:epoll高性能', 926 data: [90, 20, 30, 10, 80], 927 backgroundColor: 'rgba(0, 180, 42, 0.2)', 928 borderColor: 'rgba(0, 180, 42, 1)', 929 pointBackgroundColor: 'rgba(0, 180, 42, 1)', 930 pointBorderColor: '#fff', 931 pointHoverBackgroundColor: '#fff', 932 pointHoverBorderColor: 'rgba(0, 180, 42, 1)' 933 } 934 ] 935 }, 936 options: { 937 scales: { 938 r: { 939 angleLines: { 940 display: true 941 }, 942 suggestedMin: 0, 943 suggestedMax: 100, 944 grid: { 945 color: 'rgba(255, 255, 255, 0.1)' 946 }, 947 pointLabels: { 948 color: '#94a3b8' 949 }, 950 ticks: { 951 backdropColor: 'transparent', 952 color: '#94a3b8' 953 } 954 } 955 }, 956 plugins: { 957 legend: { 958 labels: { 959 color: '#94a3b8' 960 } 961 } 962 } 963 } 964 }); 965 }); 966 967 // 深色模式切换 968 const darkModeToggle = document.getElementById('dark-mode-toggle'); 969 const darkModeToggleMobile = document.getElementById('dark-mode-toggle-mobile'); 970 971 function toggleDarkMode() { 972 document.documentElement.classList.toggle('dark'); 973 localStorage.setItem('darkMode', document.documentElement.classList.contains('dark') ? 'enabled' : 'disabled'); 974 } 975 976 darkModeToggle.addEventListener('click', toggleDarkMode); 977 darkModeToggleMobile.addEventListener('click', toggleDarkMode); 978 979 // 检查本地存储中的深色模式设置 980 if (localStorage.getItem('darkMode') === 'enabled') { 981 document.documentElement.classList.add('dark'); 982 } 983 </script> 984 </body> 985 </html>
View Code
要我大半条命了艹
V1+V2的版本:

1 <!DOCTYPE html> 2 <html lang="zh-CN"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>C Socket 编程示例 - 新增V2版本</title> 7 <script src="https://cdn.tailwindcss.com"></script> 8 <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> 9 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script> 10 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> 11 12 <!-- 引入Prism.js代码高亮库 --> 13 <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/themes/prism-tomorrow.min.css" rel="stylesheet"> 14 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/prism.min.js"></script> 15 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.27.0/components/prism-c.min.js"></script> 16 17 <script> 18 tailwind.config = { 19 theme: { 20 extend: { 21 colors: { 22 primary: '#165DFF', 23 secondary: '#00B42A', 24 danger: '#F53F3F', 25 warning: '#FF7D00', 26 dark: '#1D2129', 27 light: '#F2F3F5', 28 code: '#1E1E1E', 29 }, 30 fontFamily: { 31 inter: ['Inter', 'sans-serif'], 32 mono: ['Consolas', 'Monaco', 'monospace'], 33 }, 34 } 35 } 36 } 37 </script> 38 39 <style type="text/tailwindcss"> 40 @layer utilities { 41 .content-auto { 42 content-visibility: auto; 43 } 44 .scrollbar-hide { 45 -ms-overflow-style: none; 46 scrollbar-width: none; 47 } 48 .scrollbar-hide::-webkit-scrollbar { 49 display: none; 50 } 51 .text-shadow { 52 text-shadow: 0 2px 4px rgba(0,0,0,0.1); 53 } 54 .bg-gradient { 55 background: linear-gradient(135deg, #165DFF 0%, #00B42A 100%); 56 } 57 .version-tab { 58 @apply px-4 py-2 rounded-lg font-medium transition-all duration-200; 59 } 60 .version-tab.active { 61 @apply bg-primary text-white; 62 } 63 .version-tab:not(.active) { 64 @apply bg-gray-100 text-gray-600 hover:bg-gray-200; 65 } 66 /* 调整Prism.js样式以匹配网站设计 */ 67 .code-block pre[class*="language-"] { 68 @apply bg-code text-gray-300 font-mono text-sm leading-relaxed m-0 p-0; 69 border-radius: 0; 70 box-shadow: none; 71 } 72 .code-header { 73 @apply bg-gray-900 py-2 px-4 flex justify-between items-center; 74 } 75 .token.comment, .token.prolog, .token.doctype, .token.cdata { 76 @apply text-gray-500; 77 } 78 .token.keyword { 79 @apply text-purple-400; 80 } 81 .token.function { 82 @apply text-blue-400; 83 } 84 .token.string, .token.attr-value { 85 @apply text-green-400; 86 } 87 .token.operator { 88 @apply text-yellow-400; 89 } 90 .token.number { 91 @apply text-red-400; 92 } 93 .highlight-box { 94 @apply bg-blue-50 border-l-4 border-blue-500 p-4 rounded; 95 } 96 .highlight-box-warning { 97 @apply bg-amber-50 border-l-4 border-amber-500 p-4 rounded; 98 } 99 .highlight-box-danger { 100 @apply bg-red-50 border-l-4 border-red-500 p-4 rounded; 101 } 102 } 103 </style> 104 </head> 105 <body class="font-inter bg-light text-dark min-h-screen flex flex-col"> 106 <!-- 导航栏 --> 107 <header class="bg-white shadow-md fixed w-full z-50 transition-all duration-300" id="navbar"> 108 <div class="container mx-auto px-1 py-1 flex justify-between items-center"> 109 <div class="flex items-center space-x-2"> 110 <i class="fa fa-code text-primary text-2xl"></i> 111 <h1 class="text-xl font-bold text-primary">C Socket 编程示例</h1> 112 </div> 113 <nav class="hidden md:flex space-x-8"> 114 <a href="#overview" class="text-dark hover:text-primary transition-colors duration-200 font-medium">概述</a> 115 <a href="#versions" class="text-dark hover:text-primary transition-colors duration-200 font-medium">版本演进</a> 116 <a href="#demo" class="text-dark hover:text-primary transition-colors duration-200 font-medium">运行演示</a> 117 <a href="#comparison" class="text-dark hover:text-primary transition-colors duration-200 font-medium">性能对比</a> 118 </nav> 119 <button class="md:hidden text-dark text-xl" id="menu-toggle"> 120 <i class="fa fa-bars"></i> 121 </button> 122 </div> 123 <!-- 移动端菜单 --> 124 <div class="md:hidden hidden bg-white absolute w-full shadow-lg" id="mobile-menu"> 125 <div class="container mx-auto px-4 py-2 flex flex-col space-y-3"> 126 <a href="#overview" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">概述</a> 127 <a href="#versions" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">版本演进</a> 128 <a href="#demo" class="text-dark hover:text-primary transition-colors duration-200 py-2 border-b border-gray-100">运行演示</a> 129 <a href="#comparison" class="text-dark hover:text-primary transition-colors duration-200 py-2">性能对比</a> 130 </div> 131 </div> 132 </header> 133 134 <!-- 英雄区 --> 135 <section class="pt-24 pb-12 bg-gradient-to-br from-primary/5 to-secondary/5"> 136 <div class="container mx-auto px-4"> 137 <div class="max-w-4xl mx-auto text-center"> 138 <h2 class="text-[clamp(2rem,5vw,3.5rem)] font-bold text-dark mb-6 leading-tight"> 139 C Socket 编程示例 140 </h2> 141 <p class="text-lg text-gray-600 mb-8 max-w-2xl mx-auto"> 142 从基础单线程到高性能 epoll,探索 Socket 编程的演进之路 143 </p> 144 <div class="flex flex-wrap justify-center gap-4"> 145 <a href="#versions" class="bg-primary hover:bg-primary/90 text-white px-6 py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-1"> 146 <i class="fa fa-code mr-2"></i>查看版本演进 147 </a> 148 <a href="#demo" class="bg-white hover:bg-gray-50 text-primary border border-primary px-6 py-3 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-1"> 149 <i class="fa fa-play-circle mr-2"></i>观看运行演示 150 </a> 151 </div> 152 </div> 153 </div> 154 </section> 155 156 <!-- 概述部分 --> 157 <section id="overview" class="py-16 bg-white"> 158 <div class="container mx-auto px-4"> 159 <div class="max-w-6xl mx-auto"> 160 <div class="flex items-center mb-8"> 161 <div class="w-2 h-8 bg-primary rounded-full mr-3"></div> 162 <h2 class="text-2xl md:text-3xl font-bold text-dark">Socket 编程演进概述</h2> 163 </div> 164 165 <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12"> 166 <div> 167 <p class="text-gray-700 mb-6"> 168 Socket 编程是网络通信的基础,从简单的单线程服务器到高性能的异步 IO 模型,经历了多个阶段的演进。本示例展示了从基础版本逐步升级到使用 epoll 的完整过程。 169 </p> 170 <div class="bg-light rounded-xl p-6 shadow-md"> 171 <h3 class="text-lg font-semibold mb-4 text-primary">6个版本演进路线</h3> 172 <ol class="space-y-3 text-gray-700"> 173 <li><span class="font-bold">V1:基础 TCP 单连接</span> - 单线程处理单个客户端,阻塞式 IO</li> 174 <li><span class="font-bold">V2:TCP 缓冲验证</span> - 验证TCP流式传输特性,分次读取数据</li> 175 <li><span class="font-bold">V3:多进程并发</span> - 使用 fork() 创建子进程处理多个客户端</li> 176 <li><span class="font-bold">V4:多线程优化</span> - 使用 pthread 创建线程处理并发</li> 177 <li><span class="font-bold">V5:IO 复用 (select/poll)</span> - 使用 select/poll 实现单线程处理多连接</li> 178 <li><span class="font-bold">V6:epoll 高性能模型</span> - 使用 epoll 实现高效的异步 IO</li> 179 </ol> 180 </div> 181 </div> 182 <div class="bg-white rounded-xl overflow-hidden shadow-xl"> 183 <img src="https://picsum.photos/seed/socket/800/600" alt="Socket 编程演进" class="w-full h-auto"> 184 <div class="p-6"> 185 <h3 class="text-lg font-semibold mb-2">Socket 模型性能对比</h3> 186 <p class="text-gray-600">随着版本演进,服务器处理并发连接的能力显著提升</p> 187 </div> 188 </div> 189 </div> 190 191 <div class="grid grid-cols-1 md:grid-cols-6 gap-4"> 192 <div class="bg-primary/5 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-primary/20"> 193 <div class="text-3xl font-bold text-primary mb-2">V1</div> 194 <p class="text-gray-700 text-sm">基础单连接</p> 195 </div> 196 <div class="bg-blue-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-blue-200"> 197 <div class="text-3xl font-bold text-blue-600 mb-2">V2</div> 198 <p class="text-gray-700 text-sm">TCP验证</p> 199 </div> 200 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 201 <div class="text-3xl font-bold text-gray-600 mb-2">V3</div> 202 <p class="text-gray-700 text-sm">多进程</p> 203 </div> 204 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 205 <div class="text-3xl font-bold text-gray-600 mb-2">V4</div> 206 <p class="text-gray-700 text-sm">多线程</p> 207 </div> 208 <div class="bg-gray-50 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-gray-200"> 209 <div class="text-3xl font-bold text-gray-600 mb-2">V5</div> 210 <p class="text-gray-700 text-sm">select/poll</p> 211 </div> 212 <div class="bg-secondary/5 rounded-xl p-4 text-center hover:shadow-md transition-shadow duration-300 border border-secondary/20"> 213 <div class="text-3xl font-bold text-secondary mb-2">V6</div> 214 <p class="text-gray-700 text-sm">epoll</p> 215 </div> 216 </div> 217 </div> 218 </div> 219 </section> 220 221 <!-- 版本演进部分 --> 222 <section id="versions" class="py-16 bg-gray-50"> 223 <div class="container mx-auto px-4"> 224 <div class="max-w-6xl mx-auto"> 225 <div class="flex items-center mb-8"> 226 <div class="w-2 h-8 bg-primary rounded-full mr-3"></div> 227 <h2 class="text-2xl md:text-3xl font-bold text-dark">版本演进</h2> 228 </div> 229 230 <!-- 版本切换标签 --> 231 <div class="flex flex-wrap justify-center gap-3 mb-12"> 232 <button class="version-tab active" data-version="1">V1:基础 TCP 单连接</button> 233 <button class="version-tab" data-version="2">V2:TCP 缓冲验证</button> 234 <button class="version-tab" data-version="3">V3:多进程并发</button> 235 <button class="version-tab" data-version="4">V4:多线程优化</button> 236 <button class="version-tab" data-version="5">V5:IO 复用 (select/poll)</button> 237 <button class="version-tab" data-version="6">V6:epoll 高性能模型</button> 238 </div> 239 240 <!-- V1版本内容 --> 241 <div class="version-content" id="version-1"> 242 <!-- 新增思考板块 --> 243 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 244 <div class="bg-light rounded-xl p-6 shadow-md"> 245 <h3 class="text-lg font-semibold mb-4 flex items-center"> 246 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 247 《我自己的思考》 248 </h3> 249 <div class="border-b border-gray-300 pb-4 mb-4"> 250 <h4 class="text-base font-semibold mb-2">listen(5) 的作用</h4> 251 <p class="text-sm text-gray-700 leading-relaxed"> 252 listen 的第二个参数(5)表示未处理的连接请求队列的最大长度,而非服务器能处理的最大客户端数。<br> 253 当客户端连接时,若服务器正忙,连接会被放入队列等待(最多5个) 254 </p> 255 </div> 256 <div> 257 <h4 class="text-base font-semibold mb-2">代码逻辑限制</h4> 258 <p class="text-sm text-gray-700 leading-relaxed"> 259 代码中 accept() 仅调用一次,处理完一个客户端后立即关闭连接并退出程序,<br> 260 因此即使队列中有等待连接,也无法处理后续客户端。<br> 261 改进方法:需要将 accept() 放入循环中持续接收新连接<br> 262 accept() 是阻塞调用,若无连接会一直等待<br> 263 客户端连接上后服务端就触发 write,客户端的 read() 同样阻塞,直到接收到数据<br> 264 然后客户端读 helloworld 265 </p> 266 </div> 267 </div> 268 <div class="bg-light rounded-xl p-6 shadow-md"> 269 <h3 class="text-lg font-semibold mb-4 flex items-center"> 270 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 271 《代码细节》 272 </h3> 273 <div class="border-b border-gray-300 pb-4 mb-4"> 274 <h4 class="text-base font-semibold mb-2">关于基础知识:</h4> 275 <p class="text-sm text-gray-700 leading-relaxed"> 276 对于字符串 277 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">"Hello World!"</code>: 278 <br> 279 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">sizeof</code>:计算对象占用的内存大小(含 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>),是编译时确定的常量<br> 280 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">strlen</code>:计算字符串的实际长度(不含 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>),运行时计算<br><br> 281 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>是空字符(ASCII 码为 0),是字符串的结束标志,不是空格,不可见且无法直接 printf 输出<br><br> 282 </p> 283 </div> 284 <div> 285 <h4 class="text-base font-semibold mb-2">关于缓冲区溢出风险:</h4> 286 <p class="text-sm text-gray-700 leading-relaxed"> 287 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">message</code> 是缓冲区大小 30 字节,read 返回实际读的字节,无论是否 -1,<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">strlen</code> 都是 13,接收到的就是<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">Hello World!\0</code><br><br> 288 假设 message 数组大小为 100 字节 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">(sizeof(message) == 100)</code>:<br><br> 289 不减 1:<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">read(sock, message, 100)</code><br> 290 若服务器发送 100 字节数据,read 会将 100 字节全部写入 message,此时数组末尾没有空间存放<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>。若后续用 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">printf("%s", message)</code> 输出,会因找不到 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code> 而继续读取内存,导致段错误或输出乱码<br><br> 291 减 1:<code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">read(sock, message, 99)</code><br> 292 read 最多读取 99 字节,剩余 1 字节用于手动添加 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code> ,避免溢出<br><br> 293 然后还要添加上 \0,防止末本身尾没有 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm">\0</code>,代码如下: <br> 294 <code class="bg-amber-100 text-red-700 px-1.5 py-0.5 rounded text-xs font-bold border border-amber-200 shadow-sm"> message[str_len] = '\0';</code> <br> 295 </p> 296 </div> 297 </div> 298 </div> 299 <br> 300 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 301 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 302 <div class="code-header"> 303 <div class="flex items-center"> 304 <div class="flex space-x-2 mr-4"> 305 <div class="w-3 h-3 rounded-full bg-danger"></div> 306 <div class="w-3 h-3 rounded-full bg-warning"></div> 307 <div class="w-3 h-3 rounded-full bg-secondary"></div> 308 </div> 309 <div class="text-gray-400 text-sm font-mono">server.c</div> 310 </div> 311 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-server-v1"> 312 <i class="fa fa-copy mr-1"></i>复制 313 </button> 314 </div> 315 <div class="code-block"> 316 <pre><code class="language-c">#include <stdio.h> 317 #include <stdlib.h> 318 #include <string.h> 319 #include <unistd.h> 320 #include <arpa/inet.h> 321 #include <sys/socket.h> 322 323 void error_handling(const char *message); 324 325 int main(int argc, char *argv[]) 326 { 327 int serv_sock; 328 int clnt_sock; 329 struct sockaddr_in serv_addr; 330 struct sockaddr_in clnt_addr; 331 socklen_t clnt_addr_size; 332 char message[]="Hello World!"; 333 334 if(argc!=2) 335 { 336 printf("Usage : %s <port>\n", argv[0]); 337 exit(1); 338 } 339 340 serv_sock=socket(PF_INET, SOCK_STREAM, 0); 341 if(serv_sock == -1) 342 error_handling("socket() error"); 343 344 memset(&serv_addr, 0, sizeof(serv_addr)); 345 serv_addr.sin_family=AF_INET; 346 serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); 347 serv_addr.sin_port=htons(atoi(argv[1])); 348 349 if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1) 350 error_handling("bind() error"); 351 352 if(listen(serv_sock, 5)==-1) 353 error_handling("listen() error"); 354 355 clnt_addr_size=sizeof(clnt_addr); 356 clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); 357 if(clnt_sock==-1) 358 error_handling("accept() error"); 359 360 write(clnt_sock, message, sizeof(message)); 361 close(clnt_sock); 362 close(serv_sock); 363 return 0; 364 } 365 366 void error_handling(const char *message) 367 { 368 fputs(message, stderr); 369 fputc('\n', stderr); 370 exit(1); 371 }</code></pre> 372 </div> 373 </div> 374 <div class="bg-white rounded-xl p-6 shadow-xl"> 375 <h3 class="text-xl font-semibold mb-4 text-primary">版本特点</h3> 376 <div class="space-y-4"> 377 <div class="flex items-start"> 378 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 379 <i class="fa fa-check text-primary"></i> 380 </div> 381 <div> 382 <h4 class="font-semibold">单线程处理</h4> 383 <p class="text-gray-600">服务器在单个线程中运行,一次只能处理一个客户端连接</p> 384 </div> 385 </div> 386 <div class="flex items-start"> 387 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 388 <i class="fa fa-check text-primary"></i> 389 </div> 390 <div> 391 <h4 class="font-semibold">阻塞式 IO</h4> 392 <p class="text-gray-600">accept() 和 read() 操作会阻塞线程,直到有新连接或数据到达</p> 393 </div> 394 </div> 395 <div class="flex items-start"> 396 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 397 <i class="fa fa-times text-danger"></i> 398 </div> 399 <div> 400 <h4 class="font-semibold">并发能力</h4> 401 <p class="text-gray-600">无法同时处理多个客户端,后续连接需等待当前连接关闭</p> 402 </div> 403 </div> 404 <div class="flex items-start"> 405 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 406 <i class="fa fa-lightbulb-o text-warning"></i> 407 </div> 408 <div> 409 <h4 class="font-semibold">适用场景</h4> 410 <p class="text-gray-600">简单测试、学习 Socket 基础,不适用于生产环境</p> 411 </div> 412 </div> 413 </div> 414 415 <div class="mt-8 pt-6 border-t border-gray-100"> 416 <h4 class="font-semibold mb-3">版本升级方向</h4> 417 <p class="text-gray-600 mb-4"> 418 单线程阻塞模型无法满足高并发需求,下一版本将引入多进程模型处理并发连接 419 </p> 420 <a href="#" class="text-primary hover:text-secondary transition-colors duration-200 inline-flex items-center"> 421 查看 V2:多进程并发 <i class="fa fa-arrow-right ml-2"></i> 422 </a> 423 </div> 424 </div> 425 </div> 426 427 <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-8"> 428 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 429 <div class="code-header"> 430 <div class="flex items-center"> 431 <div class="flex space-x-2 mr-4"> 432 <div class="w-3 h-3 rounded-full bg-danger"></div> 433 <div class="w-3 h-3 rounded-full bg-warning"></div> 434 <div class="w-3 h-3 rounded-full bg-secondary"></div> 435 </div> 436 <div class="text-gray-400 text-sm font-mono">client.c</div> 437 </div> 438 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-client-v1"> 439 <i class="fa fa-copy mr-1"></i>复制 440 </button> 441 </div> 442 <div class="code-block"> 443 <pre><code class="language-c">#include <stdio.h> 444 #include <stdlib.h> 445 #include <string.h> 446 #include <unistd.h> 447 #include <arpa/inet.h> 448 #include <sys/socket.h> 449 450 void error_handling(const char *message); 451 452 int main(int argc, char* argv[]) 453 { 454 int sock; 455 struct sockaddr_in serv_addr; 456 char message[30]; 457 int str_len; 458 459 if(argc!=3) 460 { 461 printf("Usage : %s <IP> <port>\n", argv[0]); 462 exit(1); 463 } 464 465 sock=socket(PF_INET, SOCK_STREAM, 0); 466 if(sock == -1) 467 error_handling("socket() error"); 468 469 memset(&serv_addr, 0, sizeof(serv_addr)); 470 serv_addr.sin_family=AF_INET; 471 serv_addr.sin_addr.s_addr=inet_addr(argv[1]); 472 serv_addr.sin_port=htons(atoi(argv[2])); 473 474 if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 475 error_handling("connect() error!"); 476 477 str_len=read(sock, message, sizeof(message)-1); 478 if(str_len==-1) 479 error_handling("read() error!"); 480 481 printf("Message from server : %s \n", message); 482 close(sock); 483 return 0; 484 } 485 486 void error_handling(const char *message) 487 { 488 fputs(message, stderr); 489 fputc('\n', stderr); 490 exit(1); 491 }</code></pre> 492 </div> 493 </div> 494 <div class="bg-white rounded-xl p-6 shadow-xl"> 495 <h3 class="text-xl font-semibold mb-4 text-primary">客户端代码解析</h3> 496 <div class="space-y-4"> 497 <div class="flex items-start"> 498 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 499 <i class="fa fa-info-circle text-primary"></i> 500 </div> 501 <div> 502 <h4 class="font-semibold">基本功能</h4> 503 <p class="text-gray-600">创建套接字,连接到服务器,接收服务器消息并显示</p> 504 </div> 505 </div> 506 <div class="flex items-start"> 507 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 508 <i class="fa fa-info-circle text-primary"></i> 509 </div> 510 <div> 511 <h4 class="font-semibold">阻塞式 IO</h4> 512 <p class="text-gray-600">connect() 和 read() 操作会阻塞,直到连接成功或数据到达</p> 513 </div> 514 </div> 515 <div class="flex items-start"> 516 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 517 <i class="fa fa-info-circle text-primary"></i> 518 </div> 519 <div> 520 <h4 class="font-semibold">单次通信</h4> 521 <p class="text-gray-600">客户端接收服务器消息后立即关闭连接,不支持持续通信</p> 522 </div> 523 </div> 524 </div> 525 526 <div class="mt-8 pt-6 border-t border-gray-100"> 527 <h4 class="font-semibold mb-3">编译和运行</h4> 528 <pre class="bg-light p-4 rounded-lg text-sm font-mono overflow-x-auto scrollbar-hide"> 529 # 编译客户端 530 gcc client.c -o client 531 532 # 运行客户端(连接到本地8080端口) 533 ./client 127.0.0.1 8080</pre> 534 </div> 535 </div> 536 </div> 537 </div> 538 539 <!-- 新增V2版本内容 --> 540 <div class="version-content hidden" id="version-2"> 541 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 542 <div class="bg-light rounded-xl p-6 shadow-md"> 543 <h3 class="text-lg font-semibold mb-4 flex items-center"> 544 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 545 《我的思考》 546 </h3> 547 <div class="highlight-box-warning mb-4"> 548 <h4 class="text-base font-semibold mb-2">TCP无边界验证不够好</h4> 549 <p class="text-sm text-gray-700 leading-relaxed"> 550 这个版本是缓冲一次输出,我觉得可以分次写更好,感觉可以证明有边界? 551 由于write发送纯二进制数据,printf("%s")依赖\0作为字符串结束标志, 552 所以不用担心前4个字符出现没\0的问题,所以试图修改代码 553 </p> 554 </div> 555 <div class="highlight-box mb-4"> 556 <h4 class="text-base font-semibold mb-2">修改为分次发</h4> 557 <p class="text-sm text-gray-700 leading-relaxed"> 558 服务端代码可以改为:<br> 559 <code class="bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded text-xs font-bold border border-blue-200 shadow-sm">write(clnt_sock, message, 4);</code><br> 560 <code class="bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded text-xs font-bold border border-blue-200 shadow-sm">sleep(1);</code><br> 561 <code class="bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded text-xs font-bold border border-blue-200 shadow-sm">write(clnt_sock, message+4, 8);</code> 562 <br><br> 563 client.cpp运行结果可以证明TCP是两次传输1次读取<br> 564 </p> 565 </div> 566 <div class="highlight-box-danger"> 567 <h4 class="text-base font-semibold mb-2">进一步思考</h4> 568 <p class="text-sm text-gray-700 leading-relaxed"> 569 这是强制合并到缓冲的,如果读完就清空缓冲立马输出,TCP不就有边界了吗?<br> 570 尹圣雨《TCPIP网络编程》书里的代码并没体现 TCP 弊端,因为要输出成 hell oworld 才好!打算改进<br> 571 尽管TCP没法提前知道发送方次数,<br> 572 但可以用 write(STDOUT_FILENO, buf, len) 将buf中获取到的长度为 len 的数据都直接立马输出到标准输出(这函数等同于printf,但不依赖\0)<br> 573 <br>但豆包说,若服务端分两次发送Hello和World,且网络延迟足够高,客户端可能输出:<br> 574 <span class="font-bold">HelloWorld</span>(一次read合并输出,常见于局域网)<br> 575 或<br> 576 <span class="font-bold">Hello</span>(第一次read输出前5字节)<br> 577 <span class="font-bold">World</span>(第二次read输出后5字节,需发送方延迟足够)<br><br> 578 所以说就算立马输出,在非局域网,有延迟,也会有先后到达<br> 579 </p> 580 </div> 581 </div> 582 <div class="bg-light rounded-xl p-6 shadow-md"> 583 <h3 class="text-lg font-semibold mb-4 flex items-center"> 584 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 585 《TCP缓冲分析》 586 </h3> 587 <div class="border-b border-gray-300 pb-4 mb-4"> 588 <h4 class="text-base font-semibold mb-2">缓冲机制</h4> 589 <p class="text-sm text-gray-700 leading-relaxed"> 590 TCP协议有缓冲机制,数据可能被合并或拆分传输,取决于网络状况和缓冲状态。 591 </p> 592 </div> 593 <div class="border-b border-gray-300 pb-4 mb-4"> 594 <h4 class="text-base font-semibold mb-2">逐字节读取</h4> 595 <p class="text-sm text-gray-700 leading-relaxed"> 596 客户端采用逐字节读取方式,可以验证TCP缓冲的特性。 597 </p> 598 </div> 599 <div> 600 <h4 class="text-base font-semibold mb-2">实际应用</h4> 601 <p class="text-sm text-gray-700 leading-relaxed"> 602 在实际应用中,需要正确处理缓冲数据,不能假设数据会按照发送时的分段到达。 <br> <br> <br> 603 </p> 604 </div> 605 606 <h3 class="text-lg font-semibold mb-4 flex items-center"> 607 <i class="fa fa-lightbulb-o text-warning mr-2"></i> 608 《自己记录的代码细节》 609 </h3> 610 <div class="border-b border-gray-300 pb-4 mb-4"> 611 <h4 class="text-base font-semibold mb-2">read和write</h4> 612 <p class="text-sm text-gray-700 leading-relaxed"> 613 write的就是从第二个参数首地址一直写第三个参数的字符数<br> 614 这个代码里read_len始终为1,读到末尾就返回0跳出while<br> 615 read在读到末尾的\0的是也返回1<br> 616 </p> 617 </div> 618 <div class="border-b border-gray-300 pb-4 mb-4"> 619 <h4 class="text-base font-semibold mb-2">指针</h4> 620 <p class="text-sm text-gray-700 leading-relaxed"> 621 write和read函数调用次数不同,每次读一个字节<br> 622 message 是数组名,在函数参数中会隐式转换为指向数组首元素的指针(即 &message[0])<br> 623 &message[idx++] 通过 & 显式获取数组中第 idx 个元素的地址。<br> 624 </p> 625 </div> 626 </div> 627 </div> 628 629 630 631 <br> 632 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 633 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 634 <div class="code-header"> 635 <div class="flex items-center"> 636 <div class="flex space-x-2 mr-4"> 637 <div class="w-3 h-3 rounded-full bg-danger"></div> 638 <div class="w-3 h-3 rounded-full bg-warning"></div> 639 <div class="w-3 h-3 rounded-full bg-secondary"></div> 640 </div> 641 <div class="text-gray-400 text-sm font-mono">server.c</div> 642 </div> 643 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-server-v2"> 644 <i class="fa fa-copy mr-1"></i>复制 645 </button> 646 </div> 647 <div class="code-block"> 648 <pre><code class="language-c">// 同V1版本服务端代码</code></pre> 649 </div> 650 </div> 651 <div class="bg-white rounded-xl p-6 shadow-xl"> 652 <h3 class="text-xl font-semibold mb-4 text-primary">版本特点</h3> 653 <div class="space-y-4"> 654 <div class="flex items-start"> 655 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 656 <i class="fa fa-check text-primary"></i> 657 </div> 658 <div> 659 <h4 class="font-semibold">TCP缓冲验证</h4> 660 <p class="text-gray-600">展示TCP协议的缓冲特性,数据可能被合并或拆分传输</p> 661 </div> 662 </div> 663 <div class="flex items-start"> 664 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 665 <i class="fa fa-check text-primary"></i> 666 </div> 667 <div> 668 <h4 class="font-semibold">逐字节读取</h4> 669 <p class="text-gray-600">客户端采用逐字节读取方式,验证缓冲特性</p> 670 </div> 671 </div> 672 <div class="flex items-start"> 673 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 674 <i class="fa fa-times text-danger"></i> 675 </div> 676 <div> 677 <h4 class="font-semibold">并发能力</h4> 678 <p class="text-gray-600">仍然只能处理单个客户端连接</p> 679 </div> 680 </div> 681 </div> 682 683 <div class="mt-8 pt-6 border-t border-gray-100"> 684 <h4 class="font-semibold mb-3">版本意义</h4> 685 <p class="text-gray-600 mb-4"> 686 此版本帮助理解TCP协议的缓冲特性,展示为什么应用层需要正确处理数据边界。 687 </p> 688 <a href="#" class="text-primary hover:text-secondary transition-colors duration-200 inline-flex items-center"> 689 查看 V3:多进程并发 <i class="fa fa-arrow-right ml-2"></i> 690 </a> 691 </div> 692 </div> 693 </div> 694 695 <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-8"> 696 <div class="bg-code rounded-xl overflow-hidden shadow-xl"> 697 <div class="code-header"> 698 <div class="flex items-center"> 699 <div class="flex space-x-2 mr-4"> 700 <div class="w-3 h-3 rounded-full bg-danger"></div> 701 <div class="w-3 h-3 rounded-full bg-warning"></div> 702 <div class="w-3 h-3 rounded-full bg-secondary"></div> 703 </div> 704 <div class="text-gray-400 text-sm font-mono">client.c</div> 705 </div> 706 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="copy-client-v2"> 707 <i class="fa fa-copy mr-1"></i>复制 708 </button> 709 </div> 710 <div class="code-block"> 711 <pre><code class="language-c"> 712 <!-- 713 #include <stdio.h> 714 #include <stdlib.h> 715 #include <string.h> 716 #include <unistd.h> 717 #include <arpa/inet.h> 718 #include <sys/socket.h> 719 720 void error_handling(const char *message); 721 722 int main(int argc, char* argv[]) 723 { 724 int sock; 725 struct sockaddr_in serv_addr; 726 char message[30]; 727 int str_len = 0; 728 int idx = 0, read_len = 0; 729 730 if(argc != 3) 731 { 732 printf("Usage : %s <IP> <port>\n", argv[0]); 733 exit(1); 734 } 735 736 sock = socket(PF_INET, SOCK_STREAM, 0); 737 if(sock == -1) 738 error_handling("socket() error"); 739 740 memset(&serv_addr, 0, sizeof(serv_addr)); 741 serv_addr.sin_family = AF_INET; 742 serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 743 serv_addr.sin_port = htons(atoi(argv[2])); 744 745 if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) 746 error_handling("connect() error!"); 747 --> 748 749 int idx=0,read_len=0; //main函数里加一句这个,其余一样 750 751 752 // 逐字节读取数据 仅这里不同,其余跟V1一样 753 while((read_len = read(sock, &message[idx++], 1)) 754 { 755 if(read_len == -1) 756 error_handling("read() error!"); 757 str_len += read_len; 758 } 759 760 message[str_len] = '\0';//以防万一没\0,但针对这个代码没意义,发送的\0已经在message[12]了 761 printf("read all count:%d \n", str_len); // 输出 13 762 <!-- 763 printf("Message from server: %s \n", message); 764 close(sock); 765 return 0; 766 } 767 768 void error_handling(const char *message) 769 { 770 fputs(message, stderr); 771 fputc('\n', stderr); 772 exit(1); 773 } --> 774 </code></pre> 775 </div> 776 </div> 777 <div class="bg-white rounded-xl p-6 shadow-xl"> 778 <h3 class="text-xl font-semibold mb-4 text-primary">客户端代码解析</h3> 779 <div class="space-y-4"> 780 <div class="flex items-start"> 781 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 782 <i class="fa fa-info-circle text-primary"></i> 783 </div> 784 <div> 785 <h4 class="font-semibold">逐字节读取</h4> 786 <p class="text-gray-600">使用循环每次读取一个字节,验证TCP缓冲特性</p> 787 </div> 788 </div> 789 <div class="flex items-start"> 790 <div class="bg-primary/10 p-2 rounded-lg mr-3"> 791 <i class="fa fa-info-circle text-primary"></i> 792 </div> 793 <div> 794 <h4 class="font-semibold">缓冲处理</h4> 795 <p class="text-gray-600">正确处理缓冲数据,不假设数据会按发送顺序到达</p> 796 </div> 797 </div> 798 </div> 799 800 <div class="mt-8 pt-6 border-t border-gray-100"> 801 <h4 class="font-semibold mb-3">运行结果示例</h4> 802 <pre class="bg-light p-4 rounded-lg text-sm font-mono overflow-x-auto scrollbar-hide"> 803 Message from server: Hello World! 804 read all count:13</pre> 805 </div> 806 </div> 807 </div> 808 </div> 809 810 <!-- 其他版本内容(原V2变为V3,V3变为V4,依此类推) --> 811 <div class="version-content hidden" id="version-3"> 812 <!-- 原V2内容,现在变为V3 --> 813 </div> 814 <div class="version-content hidden" id="version-4"> 815 <!-- 原V3内容,现在变为V4 --> 816 </div> 817 <div class="version-content hidden" id="version-5"> 818 <!-- 原V4内容,现在变为V5 --> 819 </div> 820 <div class="version-content hidden" id="version-6"> 821 <!-- 原V5内容,现在变为V6 --> 822 </div> 823 </div> 824 </div> 825 </section> 826 827 <!-- 运行演示部分 --> 828 <section id="demo" class="py-16 bg-white"> 829 <div class="container mx-auto px-4"> 830 <div class="max-w-6xl mx-auto"> 831 <div class="flex items-center mb-8"> 832 <div class="w-2 h-8 bg-warning rounded-full mr-3"></div> 833 <h2 class="text-2xl md:text-3xl font-bold text-dark">运行演示</h2> 834 </div> 835 836 <div class="bg-dark rounded-xl overflow-hidden shadow-xl mb-8"> 837 <div class="bg-gray-800 py-2 px-4 flex justify-between items-center"> 838 <div class="flex space-x-2"> 839 <div class="w-3 h-3 rounded-full bg-danger"></div> 840 <div class="w-3 h-3 rounded-full bg-warning"></div> 841 <div class="w-3 h-3 rounded-full bg-secondary"></div> 842 </div> 843 <div class="text-gray-300 text-sm font-mono">版本对比演示</div> 844 <div class="flex space-x-2"> 845 <select id="demo-version-select" class="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 focus:outline-none"> 846 <option value="1">V1:基础 TCP 单连接</option> 847 <option value="2">V2:TCP 缓冲验证</option> 848 <option value="3">V3:多进程并发</option> 849 <option value="4">V4:多线程优化</option> 850 <option value="5">V5:IO 复用 (select/poll)</option> 851 <option value="6">V6:epoll 高性能模型</option> 852 </select> 853 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="play-demo"> 854 <i class="fa fa-play mr-1"></i>运行 855 </button> 856 <button class="text-gray-400 hover:text-white transition-colors duration-200 text-sm" id="reset-demo"> 857 <i class="fa fa-refresh mr-1"></i>重置 858 </button> 859 </div> 860 </div> 861 <div class="p-4 h-80 overflow-y-auto scrollbar-hide bg-dark text-gray-300 font-mono text-sm leading-relaxed" id="demo-output"> 862 <p class="text-gray-400">选择版本并点击"运行"按钮开始演示...</p> 863 </div> 864 </div> 865 866 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 867 <div class="bg-light rounded-xl p-6 shadow-md"> 868 <h3 class="text-lg font-semibold mb-4 flex items-center"> 869 <i class="fa fa-terminal text-warning mr-2"></i> 870 编译和运行命令 871 </h3> 872 <pre class="bg-code text-gray-300 p-4 rounded-lg text-sm overflow-x-auto scrollbar-hide font-mono"> 873 # 编译各版本服务器代码 874 gcc server_v1.c -o server_v1 875 gcc server_v2.c -o server_v2 876 gcc server_v3.c -o server_v3 877 gcc server_v4.c -o server_v4 -lpthread 878 gcc server_v5.c -o server_v5 879 gcc server_v6.c -o server_v6 880 881 # 运行服务器(监听端口8080) 882 ./server_vX 8080 # X 为版本号 883 884 # 运行客户端(连接到本地8080端口) 885 ./client 127.0.0.1 8080 886 </pre> 887 </div> 888 <div class="bg-light rounded-xl p-6 shadow-md"> 889 <h3 class="text-lg font-semibold mb-4 flex items-center"> 890 <i class="fa fa-exchange text-warning mr-2"></i> 891 通信流程 892 </h3> 893 <ol class="list-decimal pl-5 space-y-2 text-gray-700"> 894 <li>服务器创建套接字并绑定到指定端口</li> 895 <li>服务器开始监听客户端连接</li> 896 <li>客户端创建套接字并连接到服务器</li> 897 <li>服务器接受客户端连接请求</li> 898 <li>服务器与客户端进行数据交互</li> 899 <li>客户端关闭连接</li> 900 <li>服务器关闭客户端连接</li> 901 </ol> 902 </div> 903 </div> 904 </div> 905 </div> 906 </section> 907 908 <!-- 性能对比部分 --> 909 <section id="comparison" class="py-16 bg-gray-50"> 910 <div class="container mx-auto px-4"> 911 <div class="max-w-6xl mx-auto"> 912 <div class="flex items-center mb-8"> 913 <div class="w-2 h-8 bg-secondary rounded-full mr-3"></div> 914 <h2 class="text-2xl md:text-3xl font-bold text-dark">性能对比</h2> 915 </div> 916 917 <div class="bg-white rounded-xl p-6 shadow-xl mb-8"> 918 <h3 class="text-xl font-semibold mb-6 text-center">各版本性能指标对比</h3> 919 <div class="flex justify-center"> 920 <div class="w-full max-w-4xl"> 921 <canvas id="performanceChart" height="300"></canvas> 922 </div> 923 </div> 924 </div> 925 926 <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> 927 <div class="bg-white rounded-xl p-6 shadow-md"> 928 <h3 class="text-lg font-semibold mb-4">关键性能指标</h3> 929 <div class="space-y-4"> 930 <div> 931 <div class="flex justify-between mb-1"> 932 <span class="text-sm font-medium">最大并发连接数</span> 933 <span class="text-sm font-bold text-primary">V6 > V5 > V4 > V3 > V2 > V1</span> 934 </div> 935 <div class="w-full h-2 bg-gray-200 rounded-full"> 936 <div class="h-full bg-primary rounded-full" style="width: 90%"></div> 937 </div> 938 </div> 939 <div> 940 <div class="flex justify-between mb-1"> 941 <span class="text-sm font-medium">CPU 利用率</span> 942 <span class="text-sm font-bold text-secondary">V6 < V5 < V4 < V3 < V2 < V1</span> 943 </div> 944 <div class="w-full h-2 bg-gray-200 rounded-full"> 945 <div class="h-full bg-secondary rounded-full" style="width: 20%"></div> 946 </div> 947 </div> 948 <div> 949 <div class="flex justify-between mb-1"> 950 <span class="text-sm font-medium">内存占用</span> 951 <span class="text-sm font-bold text-warning">V6 < V5 < V4 > V3 > V2 > V1</span> 952 </div> 953 <div class="w-full h-2 bg-gray-200 rounded-full"> 954 <div class="h-full bg-warning rounded-full" style="width: 30%"></div> 955 </div> 956 </div> 957 <div> 958 <div class="flex justify-between mb-1"> 959 <span class="text-sm font-medium">响应延迟</span> 960 <span class="text-sm font-bold text-danger">V6 < V5 < V4 < V3 < V2 < V1</span> 961 </div> 962 <div class="w-full h-2 bg-gray-200 rounded-full"> 963 <div class="h-full bg-danger rounded-full" style="width: 10%"></div> 964 </div> 965 </div> 966 </div> 967 </div> 968 <div class="bg-white rounded-xl p-6 shadow-md"> 969 <h3 class="text-lg font-semibold mb-4">版本选择建议</h3> 970 <ul class="space-y-3 text-gray-700"> 971 <li class="flex items-start"> 972 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V1</span> 973 <div> 974 <p><span class="font-bold">适用场景:</span>学习基础 Socket 编程,简单测试</p> 975 <p class="text-sm text-gray-500">优点:代码简单;缺点:无法处理并发</p> 976 </div> 977 </li> 978 <li class="flex items-start"> 979 <span class="bg-blue-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3 text-blue-700">V2</span> 980 <div> 981 <p><span class="font-bold">适用场景:</span>TCP特性验证,学习缓冲机制</p> 982 <p class="text-sm text-gray-500">优点:验证TCP流式特性;缺点:无法处理并发</p> 983 </div> 984 </li> 985 <li class="flex items-start"> 986 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V3</span> 987 <div> 988 <p><span class="font-bold">适用场景:</span>中等并发,稳定性要求高</p> 989 <p class="text-sm text-gray-500">优点:进程隔离安全;缺点:创建进程开销大</p> 990 </div> 991 </li> 992 <li class="flex items-start"> 993 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V4</span> 994 <div> 995 <p><span class="font-bold">适用场景:</span>高并发,计算密集型</p> 996 <p class="text-sm text-gray-500">优点:线程开销小;缺点:需要处理线程安全</p> 997 </div> 998 </li> 999 <li class="flex items-start"> 1000 <span class="bg-gray-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium mr-3">V5</span> 1001 <div> 1002 <p><span class="font-bold">适用场景:</span>大量连接但活跃连接少</p> 1003 <p class="text-sm text-gray-500">优点:单线程处理多连接;缺点:轮询开销大</p> 1004 </div> 1005 </li> 1006 <li class="flex items-start"> 1007 <span class="bg-green-100 rounded-full w-6 h-6 flex items-center justify-center text-sm font-medium text-green-700">V6</span> 1008 <div> 1009 <p><span class="font-bold">适用场景:</span>高性能服务器,海量并发</p> 1010 <p class="text-sm text-gray-500">优点:事件驱动,零拷贝;缺点:代码复杂度高</p> 1011 </div> 1012 </li> 1013 </ul> 1014 </div> 1015 </div> 1016 </div> 1017 </div> 1018 </section> 1019 1020 <!-- 页脚 --> 1021 <footer class="bg-dark text-white py-12"> 1022 <div class="container mx-auto px-4 text-center"> 1023 <p>C Socket 编程示例</p> 1024 </div> 1025 </footer> 1026 1027 <!-- 脚本 --> 1028 <script> 1029 // 导航栏滚动效果 1030 window.addEventListener('scroll', function() { 1031 const navbar = document.getElementById('navbar'); 1032 if (window.scrollY > 50) { 1033 navbar.classList.add('py-2'); 1034 navbar.classList.remove('py-3'); 1035 } else { 1036 navbar.classList.add('py-3'); 1037 navbar.classList.remove('py-2'); 1038 } 1039 }); 1040 1041 // 移动端菜单切换 1042 document.getElementById('menu-toggle').addEventListener('click', function() { 1043 const mobileMenu = document.getElementById('mobile-menu'); 1044 mobileMenu.classList.toggle('hidden'); 1045 }); 1046 1047 // 版本切换功能 1048 const versionTabs = document.querySelectorAll('.version-tab'); 1049 const versionContents = document.querySelectorAll('.version-content'); 1050 1051 versionTabs.forEach(tab => { 1052 tab.addEventListener('click', () => { 1053 // 移除所有激活状态 1054 versionTabs.forEach(t => t.classList.remove('active')); 1055 versionContents.forEach(c => c.classList.add('hidden')); 1056 1057 // 设置当前激活状态 1058 tab.classList.add('active'); 1059 const version = tab.getAttribute('data-version'); 1060 document.getElementById(`version-${version}`).classList.remove('hidden'); 1061 1062 // 平滑滚动到版本内容 1063 document.getElementById(`version-${version}`).scrollIntoView({ behavior: 'smooth' }); 1064 }); 1065 }); 1066 1067 // 复制代码功能 1068 document.getElementById('copy-server-v1').addEventListener('click', function() { 1069 const code = document.querySelector('#version-1 pre code').textContent; 1070 navigator.clipboard.writeText(code).then(() => { 1071 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 1072 setTimeout(() => { 1073 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 1074 }, 2000); 1075 }); 1076 }); 1077 1078 // 修复客户端复制功能 1079 document.getElementById('copy-client-v1').addEventListener('click', function() { 1080 const code = document.querySelector('#version-2 pre code').textContent; 1081 navigator.clipboard.writeText(code).then(() => { 1082 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 1083 setTimeout(() => { 1084 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 1085 }, 2000); 1086 }); 1087 }); 1088 1089 1090 1091 document.getElementById('copy-server-v2').addEventListener('click', function() { 1092 const serverCode = `// 同V1版本服务端代码`; 1093 1094 navigator.clipboard.writeText(serverCode).then(() => { 1095 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 1096 setTimeout(() => { 1097 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 1098 }, 2000); 1099 }); 1100 }); 1101 1102 document.getElementById('copy-client-v2').addEventListener('click', function() { 1103 const clientCode = ` 1104 int idx=0,read_len=0; //main函数里加一句这个,其余一样 1105 1106 // 逐字节读取数据 仅这里不同,其余跟V1一样 1107 while((read_len = read(sock, &message[idx++], 1)) 1108 { 1109 if(read_len == -1) 1110 error_handling("read() error!"); 1111 str_len += read_len; 1112 } 1113 1114 message[str_len] = '\0';//以防万一没\0,但针对这个代码没意义,发送的\0已经在message[12]了 1115 printf("read all count:%d \n", str_len); // 输出 13 1116 `; 1117 1118 navigator.clipboard.writeText(clientCode).then(() => { 1119 this.innerHTML = '<i class="fa fa-check mr-1"></i>已复制'; 1120 setTimeout(() => { 1121 this.innerHTML = '<i class="fa fa-copy mr-1"></i>复制'; 1122 }, 2000); 1123 }); 1124 }); 1125 1126 // 演示运行功能 1127 const demoOutput = document.getElementById('demo-output'); 1128 const demoVersionSelect = document.getElementById('demo-version-select'); 1129 1130 document.getElementById('play-demo').addEventListener('click', function() { 1131 const version = demoVersionSelect.value; 1132 demoOutput.innerHTML = `<p class="text-gray-400">正在运行 V${version} 服务器演示...</p>`; 1133 1134 // 根据选择的版本显示不同的演示内容 1135 const steps = getDemoSteps(version); 1136 1137 let i = 0; 1138 const interval = setInterval(() => { 1139 demoOutput.innerHTML += steps[i] + '<br>'; 1140 demoOutput.scrollTop = demoOutput.scrollHeight; 1141 i++; 1142 if (i >= steps.length) { 1143 clearInterval(interval); 1144 } 1145 }, 1000); 1146 }); 1147 1148 document.getElementById('reset-demo').addEventListener('click', function() { 1149 demoOutput.innerHTML = '<p class="text-gray-400">选择版本并点击"运行"按钮开始演示...</p>'; 1150 }); 1151 1152 // 获取不同版本的演示步骤 1153 function getDemoSteps(version) { 1154 const baseSteps = [ 1155 '<p class="text-yellow-300">编译服务器代码...</p>', 1156 '<p class="text-gray-400">服务器代码编译完成</p>', 1157 '<p class="text-yellow-300">启动服务器...</p>', 1158 ]; 1159 1160 const versionSpecificSteps = { 1161 1: [ 1162 '<p class="text-gray-400">V1 服务器已启动,等待客户端连接...</p>', 1163 '<p class="text-yellow-300">客户端1连接成功</p>', 1164 '<p class="text-gray-400">服务器处理客户端1请求</p>', 1165 '<p class="text-yellow-300">客户端2尝试连接...</p>', 1166 '<p class="text-red-300">客户端2连接被拒绝:服务器正忙</p>', 1167 '<p class="text-yellow-300">客户端1断开连接</p>', 1168 '<p class="text-yellow-300">客户端2连接成功</p>', 1169 '<p class="text-gray-400">服务器处理客户端2请求</p>', 1170 '<p class="text-green-400">演示完成:V1 单线程模型无法同时处理多客户端</p>' 1171 ], 1172 2: [ 1173 '<p class="text-gray-400">V2 服务器已启动,等待客户端连接...</p>', 1174 '<p class="text-yellow-300">客户端连接成功</p>', 1175 '<p class="text-gray-400">服务器发送第一部分数据: "Hell"</p>', 1176 '<p class="text-gray-400">等待1秒...</p>', 1177 '<p class="text-gray-400">服务器发送第二部分数据: "o World!"</p>', 1178 '<p class="text-yellow-300">客户端开始逐字节读取数据...</p>', 1179 '<p class="text-gray-400">客户端输出: H</p>', 1180 '<p class="text-gray-400">客户端输出: e</p>', 1181 '<p class="text-gray-400">客户端输出: l</p>', 1182 '<p class="text-gray-400">客户端输出: l</p>', 1183 '<p class="text-gray-400">客户端输出: o</p>', 1184 '<p class="text-gray-400">客户端输出: </p>', 1185 '<p class="text-gray-400">客户端输出: W</p>', 1186 '<p class="text-gray-400">客户端输出: o</p>', 1187 '<p class="text-gray-400">客户端输出: r</p>', 1188 '<p class="text-gray-400">客户端输出: l</p>', 1189 '<p class="text-gray-400">客户端输出: d</p>', 1190 '<p class="text-gray-400">客户端输出: !</p>', 1191 '<p class="text-green-400">演示完成:V2 展示了TCP缓冲特性</p>' 1192 ], 1193 6: [ 1194 '<p class="text-gray-400">V6 服务器已启动,使用 epoll 监听连接...</p>', 1195 '<p class="text-yellow-300">客户端1连接成功</p>', 1196 '<p class="text-yellow-300">客户端2连接成功</p>', 1197 '<p class="text-yellow-300">客户端3连接成功</p>', 1198 '<p class="text-yellow-300">客户端4连接成功</p>', 1199 '<p class="text-yellow-300">客户端5连接成功</p>', 1200 '<p class="text-gray-400">服务器同时处理 5 个客户端连接...</p>', 1201 '<p class="text-yellow-300">客户端1断开连接</p>', 1202 '<p class="text-yellow-300">客户端6连接成功</p>', 1203 '<p class="text-green-400">演示完成:V6 epoll 模型高效处理多并发连接</p>' 1204 ] 1205 }; 1206 1207 return [...baseSteps, ...(versionSpecificSteps[version] || [])]; 1208 } 1209 1210 // 性能对比图表 1211 window.addEventListener('load', function() { 1212 const ctx = document.getElementById('performanceChart').getContext('2d'); 1213 1214 const performanceChart = new Chart(ctx, { 1215 type: 'radar', 1216 data: { 1217 labels: ['并发连接数', 'CPU利用率', '内存占用', '响应延迟', '代码复杂度'], 1218 datasets: [ 1219 { 1220 label: 'V1:基础单连接', 1221 data: [10, 85, 20, 90, 10], 1222 backgroundColor: 'rgba(22, 93, 255, 0.2)', 1223 borderColor: 'rgba(22, 93, 255, 1)', 1224 pointBackgroundColor: 'rgba(22, 93, 255, 1)', 1225 pointBorderColor: '#fff', 1226 pointHoverBackgroundColor: '#fff', 1227 pointHoverBorderColor: 'rgba(22, 93, 255, 1)' 1228 }, 1229 { 1230 label: 'V2:TCP缓冲验证', 1231 data: [10, 80, 22, 85, 15], 1232 backgroundColor: 'rgba(59, 130, 246, 0.2)', 1233 borderColor: 'rgba(59, 130, 246, 1)', 1234 pointBackgroundColor: 'rgba(59, 130, 246, 1)', 1235 pointBorderColor: '#fff', 1236 pointHoverBackgroundColor: '#fff', 1237 pointHoverBorderColor: 'rgba(59, 130, 246, 1)' 1238 }, 1239 { 1240 label: 'V6:epoll高性能', 1241 data: [90, 20, 30, 10, 80], 1242 backgroundColor: 'rgba(0, 180, 42, 0.2)', 1243 borderColor: 'rgba(0, 180, 42, 1)', 1244 pointBackgroundColor: 'rgba(0, 180, 42, 1)', 1245 pointBorderColor: '#fff', 1246 pointHoverBackgroundColor: '#fff', 1247 pointHoverBorderColor: 'rgba(0, 180, 42, 1)' 1248 } 1249 ] 1250 }, 1251 options: { 1252 scales: { 1253 r: { 1254 angleLines: { 1255 display: true 1256 }, 1257 suggestedMin: 0, 1258 suggestedMax: 100, 1259 grid: { 1260 color: 'rgba(0, 0, 0, 0.1)' 1261 }, 1262 pointLabels: { 1263 color: '#4b5563' 1264 }, 1265 ticks: { 1266 backdropColor: 'transparent', 1267 color: '#4b5563' 1268 } 1269 } 1270 }, 1271 plugins: { 1272 legend: { 1273 labels: { 1274 color: '#4b5563' 1275 } 1276 } 1277 } 1278 } 1279 }); 1280 }); 1281 </script> 1282 </body> 1283 </html>
View Code
deepseek真牛逼,但经常服务器忙,太牛逼了连代码彩色高亮都可以弄。问小白也牛逼
从豆包风格生成的风格开始,但一半就停了,用这个风格让Deepseek重新搞了一个,再加V2的时候就崩溃了,又转问小白(没法分享,名字叫“版本演进”),完整代码只是在V2做了新增,其他略了,整体代码又有bug不知道咋改,很僵硬,回家凌晨用的人少,Deepseek服务器不崩,直接生成完V1+V2完整代码
书 P31 代码(验证 基本的 TCP 原理)
改进尹圣雨TCPIP网络编程书里的代码,并没体现 TCP 弊端,因为要输出成 hell oworld 才好
这些版本的bind error()最后说吧
chrome闪退180次
用个Deepseek真费劲,只有半夜人少
妈的一个Deepseek生成html的标签页,页面内存容量干出来5.7G。chrome/editplus接连不断的闪退
心路历程用网页的形式记录了下来
这是真的踏破铁鞋无觅处,终于知道怎么让面试官看到我的内容了
前端是个好东西
乌烟瘴气的职场环境我这么辛辛苦苦做的估计会被认为抄的
说家人有病毕业后3年照顾家人,也没人信,公众号都说空窗就说照顾家人,唉逼的真正是那样的任却无路可走
bind函数
通过传结构体 struct sockaddr_in 接收地址信息
包涵成员变量:
sin_family:协议族对应的地址族,IPv4对应地址族AF_INET
sin_port :端口号以为网络字节保存
结构体 sin_addr:保存32位IP地址信息,网络字节序保存(成员 sin_addr 是 struct in_addr 结构体类型,in_addr类型里的成员是 s_addr 当作32位整数)
sin_zero:没用,为了和 sockaddr_in 结构体大小保持一致
函数形式:int bind(要绑的套接字,指向 sockaddr 结构体的指针,第二个参数的结构体的长度)
第二个参数期望得到 sockaddr 类型的结构体变量地址值,但保存麻烦,所以用 sockaddr_in,同理IPv6也有自己的特殊结构体,都是为了和 sockaddr 一致,最后强制转化就行
bind(要分配地址 IP & 端口 信息的套接字,存有地址信息的结构体变量地址值,第二个结构体变量的长度)
哈哈开心,真的是书越读越懂
成功返回 0 ,失败返回 -1
accept(服务器套接字,保存发起连接请求的客户端的地址信息的变量地址值,调用函数后向传来的地址变量参参数填充客户端地址信息,上一个参数的结构体长度,但注意,是长度的地址,调用完成后,该参数即被填入客户端地址长度)
这段屁话第一次啃书啃的时候云里雾里,第二个回顾忘记写了,是写到后面快结尾了,多播那,才回头写的,越来越懂啦,真的是书读百遍其义自见
成功返回创建的套接字,失败 -1
connect( 客户端套接字,保存目标服务器端地址信息的变量地址值,第二个参数的地址变量的长度)
要么服务端接收连接成功,返回 0,
要么异常中断,即失败,返回 -1


字节序
大端序:高 位字节存 低 位地址
小端序:高 位字节存 高 位地址

CPU保存方式叫计算机主机字节序
十六进制0x中,大端 0x1234 发送,小端 收到 0x1234 会解析为 0x3412
固定统一转成网络字节序,即大端序再传
函数htons:h主机host,n代表网络network,s是short,l是long
htons:把 short 从主机字节序 → 网络字节序
妈的我之前研究win API 、规则还问为啥这么规定、还问 fopen、fgetc、fputc、fgets、fputs,write,read,recv,send,这些为何用这个不用另一个、TCPIP网络编程尹圣雨书的每页每句话每行代码事无巨细研究的明明白白,甚至写了书的勘误给书找代码逻辑bug、之前刷算法题,自己想出来A掉后,查其他解法一题多解,给网上的题解博客无法AC的调代码到AC
豆包说 htons不高频,大厂注重协议栈逻辑,我研究的太细节了
如果 host_port = 0x1234,那小端CPU输出 0x3412,大端输出 0x1234
然后 htons 后,小端CPU输出 0x1234,大端CPU输出 0x1234
除了向 sockaddr_in 结构体填充外,都不需要管字节序的事
IP在底层网络传输是32位二进制整数(IPv4)大端存储传输,人类用直观的字符串表示,叫点分十进制字符串
192.168.1.1是点分十进制字符串,其对应的 32 位整数为:3232235777
192 → 11000000168 → 101010001 → 000000011 → 00000001拼接后二进制为:11000000 10101000 00000001 00000001,转为十进制即为 3232235777
inet_addr(),参数是 192.168.1.1 这种字符串,该函数返回“点分十进制的字符组转为 32位大端序的”值,用十六进制 0x 表示
但注意小端CPU输出的时候会按照小端内存的小端序,即 1.2.3.4 网络序是0x 01 02 03 04,inet_addr后打印数来是,十六进制输出一般会截断:0x4030201
PS:
1字节最大255,inet_addr 转换失败返回 INADDR_NONE 可判断无效 IP.
现在大多数都是基于x86小端CPU,小端效率高,因为取 0x1234 低字节 34 时,直接读低内存就行
inet_addr 在网络编程里需要把转换后的 IP 手动插入到 sockaddr_in 结构体类型的 变量的 sin_addr 成员变量的 s_addr 中(插到最深处),而 inet_aton 则自动填入
inet_aton(第一个参数依旧是字符串,第二个参数是要保存的地址,直接到 sin_addr 就行)
成功返回1,失败返回0
现在习惯性学一半问豆包“这个是目前大厂面试最主流的用法吗? 低于80%则为不主流! ”
32位二进制 IP → 点分字符串:
inet_ntoa(),成功返回转换的字符串地址值,失败返回 -1
需要注意调用成功需要立即存,因为内存里函数申请的内存来存,未向程序员要求申请内存空间,且该函数静态分配,第二次再调用会发覆盖掉第一次调用返回的

0x1020304 实际上是 0x01020304,省略的前导 0
输出:
1.2.3.4
1.1.1.1
1.2.3.4
0x01020304 小端存是 0x04030201,无论主机如何存,网络协议规定 IPv4 地址必须按大端序解释
inet_ntoa 会自动按大端序解析,将其转换为 0x01020304,然后转成 1.2.3.4
服务端的网络地址信息初始化:
atoi 字符串 → 十进制整数
struct sockaddr_in addr;
char *serv_ip = "211.217.168.13";//INADDR_ANY会自动获取IP
char *serv_port = "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(serv_ip);
addr.sin_port = htons(atoi(serv_port));
//如果用 INADDR_ANY
//就是 addr.sin_addr.s_addr = htonl(INADDR_ANY);
//inet_addr(serv_ip) 用于将点分十进制字符串 IP(如 "192.168.1.1")转换为网络字节序的 32 位整数;
//htonl(INADDR_ANY) 用于将本地字节序的特殊常量 INADDR_ANY(值为 0.0.0.0)转换为网络字节序,表示监听所有可用 IP
至此这段代码就懂了
其实就是客户端服务端的初始化一样,只不过客户端是根据填服务端的 IP + 端口,主动发起连接来初始化绑定, 服务端是根据自动分配的来绑定
还有很容易误导人的是,客户端的结构体名字也用的是serv_addr
书上 P74 的 服务端 / 客户端 通信代码做些解释,除此之外都理解了
客户端 close 触发 FIN 包 或者 数据读到末尾:服务端的 read 返回 0
思考书 P74 的代码简单说流程:
对于客户端循环 write → 服务端循环 read 后原样返回 → 客户端循环 read
服务端:
while((str_len = read(clnt_sock, message, BUF_SIZE))!= 0)
write(clnt_sock, message, str_len);
客户端:
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len] = 0;
由于 TCP 没数据边界,当数据大或者不在同一个计算机上就会有问题:
多次输入的数据,可能一次性到达服务端,一次返回
或者数据太长一次数据,分了2次,返回来2个字符串
既然无法预知传输数据大小,就要在应用层做控制定义 ,做个修改,在应用层通过记录字节数,把 TCP 弄成 手动控制有边界的 传输的方式,但太低级了,因为实际无法预知传输的数据大小:
客户端 write 的时候记录字节数,写了多少就期待返回多少:P84
str_len=write(sock, message, strlen(message));
recv_len=0;
while(recv_len<str_len)
{
recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
if(recv_cnt==-1)
error_handling("read() error!");
recv_len+=recv_cnt;
}
message[recv_len]=0;
哈哈感觉很完美,到时候一起写到网页里
这就是逐步定义规则,即应用层协议,但是规定客户端的
注意,TCP依然是无边界,这里是通过应用层加的控制,应用层一开始我以为只有 HTTP、FTP、SMTP 啥的高大上的,但其实在应用程序里用 write 和 read 等函数处理数据收发,这些操作处于应用层。只要是应用程序中对数据传输进行的控制逻辑(像代码里记录字节数)都属于应用层范畴
再来个,规定服务端的,根据=-*/判断做啥运算,简易计算器,书 P87 太简单了:
题目要求:通过1 字节整数传递操作数个数(opnd_cnt),因为操作数个数通常较小(如 0~255),用 1 字节足够表示,节省网络带宽
opmsg 数组的内存结构被设计为:
[0] [1~4] [5~8] [9~12] [13]
+---------+---------+---------+-------+-------+
| 操作数 | 操作数1 | 操作数2 | 操作数3 | 运算符|
| 个数(1B)| (4B) | (4B) | (4B) | (1B) |
+---------+---------+---------+----------+-------+
客户端代码的读取:
scanf("%d",&opnd_cnd);
opmsg[0] = char(opnd_cnd);//读操作数个数
for(操作数个数)
scanf("%d",(int*)&opmsg[i*OPSZ + 1]);//读具体操作数
注意输入运算符之前要吃回车
fgetc(stdin);
服务端代码:
int recv_len = 0;
int recv_cnt = 0;
while ((opnd_cnt * OPSZ + 1) > recv_len) {
recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
if (recv_cnt == -1) {
error_handling("read() error");
}
recv_len += recv_cnt; // 累加已接收的字节数
}
write 的第二个参数是指向要写入的指针,所有的 write 第二个参数都要指向 char 类型,int 要强制转
因为 write 按字节写入,也是网络字节序要求
基于 UDP
视频音频用 UDP
socket( PF_INET, SOCK_DGRAM, 0);
TCP 慢速两个原因:
就需要1个套接字,每次传数据都需要地址信息
成功返回传输字节数,失败返回 -1
成功返回接收的字节数,失败返回 -1。recvfrom 默认阻塞
其实也发现了,UDP 可靠在于有告知传输字节,无连接,数据因报文形式,封装报文时候确定数据长度。之前就用这个来验证的 TCP 低级可靠
而 TCP 是字节流
我的思考:
封装的时候查字节数 TCP 就不吗? 而且既然封装检查字节数,感觉应该更慢啊
豆包:
UDP 封装 / 解析有额外开销,且头部标记边界,但 TCP 各种可靠机制导致更慢
频繁链接用 UDP
函数细节:
sizeof(message) 是数组的固定大小,编译时确定。strlen(message) 是动态计算的字符串长度,运行时根据 '\0' 位置确定UDP 和 TCP:
sendto发完整报文,strlen取实际数据长度,接收方按此长度收(报文有边界)TCP:若用 write(message, strlen(message)),数据会流入 TCP 缓冲区,接收方需自行解析边界
发送方代码:
while(1){
fgets(message, sizeof(message), stdin);
if(!strcmp(message,"q\n") ||!strcmp(message,"Q\n"))
break;
sendto(sock, message, strlen(message), 0,
(struct sockaddr*)&serv_adr, sizeof(serv_adr));
adr_sz=sizeof(from_adr);
str_len=recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr*)&from_adr, &adr_sz);
message[str_len]=0;
printf("Message from server: %s", message);
}
接收方代码:
while(1){
clnt_adr_sz = sizeof(clnt_adr);
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
调用 sendto 前,sendto 的目标要开始运行
UDP 没服务端和客户端,但发数据的叫发送端,接收的叫接收端,只要 bind 函数所在的叫接收端, 不存在连接请求和受理,只要bind 上了就可以有任意 UDP 往这里发送
UDP 整体代码逻辑步骤是:(并不绝对,详见后面)
接收方:创建套接字 → 网络地址初始化和 TCP 服务端一样 → bind 和 TCP 一样 → recvfrom / sendto
发送方:socket创建套接字 → 网络地址初始化和 TCP 客户端一样 → sendto / recvfrom
地址分配
TCP:
服务端通过 bind
UDP:
但没 bind 的话,首次调用 sendto 时,没 IP & 端口 就自动分配,一直保留到程序结束
验证 UDP 有边界
host1接收:
for(i=0; i<3; i++)
{
sleep(5); // delay 5 sec.
adr_sz=sizeof(your_adr);
str_len=recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr*)&your_adr, &adr_sz);
printf("Message %d: %s \n", i+1, message);
}
host2发送:
sendto(sock, msg1, sizeof(msg1), 0,
(struct sockaddr*)&your_adr, sizeof(your_adr));
sendto(sock, msg2, sizeof(msg2), 0,
(struct sockaddr*)&your_adr, sizeof(your_adr));
sendto(sock, msg3, sizeof(msg3), 0,
(struct sockaddr*)&your_adr, sizeof(your_adr));
验证分了3次 sendto 字符串,就会分 3次 接收
TCP 就会1次调用全读4
继续深入说注册 IP & 端口 的事:
TCP 是通过 connect 和 bind 注册,然后连接,然后通信
UDP 不提前注册,直接每次 sendto 的时候注册(正式因为每次 sendto ,所以其实sendto隐含分三步:注册、传输、删除注册信息以便于下次跟其他人传输通信)
如果同一个注册信息多次发数据,引入 connect (加位置跟 TCP 一样),但并不是连接,只是注册信息可以一直用,(比如给 192.168.1.1 主机的 82 端口多次发数据)
指定好后就可以用 write、read了
bind 并非只在接收方:发送方也可以调用 bind(非必需),用于固定自己的 IP + 端口(否则系统会自动分配随机端口)。
connect 可以在发送方或接收方使用(但和 TCP 的 connect 含义不同):
connect:指定目标 IP + 端口后,后续 sendto 可简化为 send(无需每次指定目标地址),且只能向该目标发送数据。connect:可限制该套接字仅接收特定 IP + 端口的数据(类似过滤功能)至此懂了上面说的“UDP 整体代码逻辑步骤是:(并不绝对,详见后面)”
用 shutdown 优雅的断开连接:
TCP 建立连接后,进入可交换数据的状态叫“单向流状态”,close 意味着无法读写,A 发送完就 close,这时候,B 想让 A 接收的就收不到了
close:
减少文件描述符引用计数,计数为 0 时真正关闭连接,释放内核资源
在单线程 / 单进程场景中足够用,但在多线程 / 多进程共享套接字时需注意引用计数问题(如子进程继承父进程的文件描述符,需手动关闭)
shutdown:
允许一方先关闭写通道(发送 FIN),但保留读通道,用于 “先发完数据再关闭连接” 的场景(如 HTTP 长连接)
不管引用计数,直接触发 TCP 四次挥手
函数原型:
int shutdown(文件描述符,断开方式);
成功返回 0 ,失败返回 -1
断开方式:
SHUT_RD:断输入流,即使缓冲区收到也会抹去
SHUT_WR:断输出流,缓冲区有数据会传完再断
SHUT_RDWR:同时断输入输出流,相当于两次shutdown
为何需要半关闭:
比如连接后,服务端给客户端发送数据,发完客户端返回个“Thank you”
不知道何时传送完,不能无休止发 "Thank you",于是就用 EOF 标志作为表示文件末尾,那咋传递?就用半关闭,传完依旧可以接收“Thank you”
小知识:
read / write 系统调用涉及到内核切换,涉及到套接字就要用这个
fread / fopen 是标准库,读文件用这个
fread 和 read 核心差别就是 fread 有缓冲区,相同数据可以减少系统调用次数
读1个字节100次,fread 是第一次用内部封装的 read,后续 99 次都缓冲区读,read 则 100 次系统调用:
fread(buffer, size, count, stream):从文件流stream读取count个大小为size的元素到buffer,返回实际成功读取的元素数量,第一个参数要强制转成 void*read(fd, buffer, count):从文件描述符fd对应的文件读取最多count字节到buffer,返回实际读取的字节数(-1 表示错误)// read:每次从内核缓冲区读100字节到buf,下次读需重新触发系统调用
char buf[100];
read(fd, buf, 100); // 第1次读,触发系统调用
read(fd, buf, 100); // 第2次读,再次触发系统调用
// fread:首次读填充用户态缓冲区,后续读直接从缓冲区取
char buf[100];
fread(buf, 1, 100, fp); // 第1次读,触发系统调用并填充8KB缓冲区
fread(buf, 1, 100, fp); // 第2次读,直接从用户态缓冲区取数据(无系统调用)
关于文件读取很巧妙的代码:
while(1)
{
read_cnt=fread((void*)buf, 1, BUF_SIZE, fp);
if(read_cnt<BUF_SIZE)
{
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
当 fread 返回的实际读取字节数 read_cnt 小于 BUF_SIZE ,意味着已经读到文件末尾 ,没有足够数据填充 BUF_SIZE 大小的缓冲区了
FILE *fopen(要打开的文件名, 打开模式);
NULLfopen 的打开模式: "r"、"w"、"a"、"rb"
"r" 只读打开文本文件,文件必须存在
"w" 只写打开文本文件,若存在则清空,不存在则创建
"a" 追加打开文本文件,不存在则创建,写操作追加到末尾
"rb" 只读打开二进制文件,文件必须存在(b表示二进制模式)
"r+" 读写,文件必须存在
"w+"读写,创建新文件,原有内容清空
这俩差别是,"w+"全部清空,"r+"是保留原有内容,写几个就从头开始覆盖几个,如果是追加需要通过 fseek 将文件指针移动到末尾
所以说除了追加,模式打开的初始读写位置都在文件开头
正常打开的指的是 txt 文本,二进制指的是音频视频啥的
r+、w+ 第一次读的时候一刷没搞懂,真的是书读百遍其义自见
size_t fwrite( 要写入的 void* 型指针, 一个数据项大小单位是字节, 数据项个数, 写入目标);成功返回写入数据项个数,失败返回小于个数,需要 ferror() 检查错误
开心O(∩_∩)O~~,感觉好通透~
P133 第八章的 gethostbuname 豆包说已经废弃,现在用 getaddrinfo,之前还啃了许久,妈逼的,小林coding还用这个呢艹
妈逼的学 getaddrinfo() 和 struct addrinfo 发现,之前一直学的手动配置 sockaddr_in(即 serv_addr)这玩意也是边缘化废弃的,艹
唉不断调整吧
再一次问豆包“想去大厂Linux C++服务端开发”
时常学着学着问豆包“大厂考试面试频率高吗?低于80%为不高”
简单看下吧
www.baidu.com 是域名,比如他的 IP 是1.2.3.4,那 IP 是会经常变化的,用域名编写代码,然后用转化函数,比用 IP 编写好
一个 IP 可以绑定多个域名
也可以多服务器负载均衡,多个 IP 同一个域名
开始学习豆包给出的 getaddrinfo():域名 转 套接字地址结构
int getaddrinfo(const char *node, // 主机名(如 "www.baidu.com")或 IP 地址
const char *service, // 服务名(如 "http")或端口号(如 "80")
const struct addrinfo *hints, // 输入参数:指定协议族、套接字类型等
struct addrinfo **res); // 输出参数:返回地址信息链表
成功返回 0,失败返回错误码(需用 gai_strerror() 转换为字符串)
重点成员: hints:用于指定期望的地址类型和套接字参数,需先清零再赋值:
struct addrinfo hints;
memset(&hints, 0, sizeof(hints)); // 必须清零!
hints.ai_family = AF_UNSPEC; // IPv4 或 IPv6(自动选择)
hints.ai_socktype = SOCK_STREAM; // TCP 套接字
hints.ai_flags = AI_PASSIVE; // 用于服务器绑定(监听本地所有地址)

重点成员:返回值 addrinfo 链表解析
好以上是云里雾里一头雾水的简单科普,开始讲解:
在编写网络程序时,我们需要完成两个核心步骤:
- 客户端:知道服务器的IP地址和端口号,然后连接它。
服务器:绑定一个本地地址和端口号,然后等待客户端连接。
在没有
getaddrinfo()之前,我们需要手动处理这些复杂的细节:
IPv4/IPv6兼容:需要写两套代码处理不同的地址结构(
struct sockaddr_in和struct sockaddr_in6)字节序转换:手动调用
htons()/htonl()转换端口号和IP地址。域名解析:先用
gethostbyname()解析域名,再手动组装地址结构。它把上述所有复杂操作封装成一个函数,你只需要告诉它:
- 你想要连接的服务器域名(或IP)和端口号(或服务名)
你期望的地址类型(IPv4/IPv6、TCP/UDP)
然后它会返回一个已经完全配置好的地址链表,你直接用这个链表创建套接字就可以了。
这是理解
getaddrinfo()的关键,它的定义如下:struct addrinfo { int ai_flags; // 配置标志(如 AI_PASSIVE) int ai_family; // 地址族(AF_INET、AF_INET6 或 AF_UNSPEC) int ai_socktype; // 套接字类型(SOCK_STREAM 或 SOCK_DGRAM) int ai_protocol; // 协议(通常为 0,表示自动选择) socklen_t ai_addrlen; // 地址长度 struct sockaddr *ai_addr; // 指向具体地址结构的指针 char *ai_canonname; // 规范主机名(如 "www.baidu.com" 的真实域名) struct addrinfo *ai_next; // 指向下一个地址的指针(链表结构) };int getaddrinfo( const char *node, // 主机名(如 "www.baidu.com")或 IP 地址(如 "192.168.1.1") const char *service, // 服务名(如 "http")或端口号(如 "80") const struct addrinfo *hints, // 输入参数:指定你期望的地址类型 struct addrinfo **res // 输出参数:返回配置好的地址链表 );3. 输入参数
hints的配置你需要先创建一个
struct addrinfo对象,设置其中的部分字段,告诉getaddrinfo()你想要的地址类型:struct addrinfo hints; memset(&hints, 0, sizeof(hints)); // 必须清零! hints.ai_family = AF_UNSPEC; // 不指定地址族,自动选择 IPv4 或 IPv6 hints.ai_socktype = SOCK_STREAM; // TCP 套接字 hints.ai_flags = AI_PASSIVE; // 用于服务器绑定(监听本地所有地址)
getaddrinfo()会返回一个链表,每个节点包含一个可能的地址。你需要遍历这个链表,尝试使用每个地址:struct addrinfo *res, *p; int status = getaddrinfo("www.baidu.com", "http", &hints, &res); if (status != 0) { fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status)); return 1; } // 遍历链表 for (p = res; p != NULL; p = p->ai_next) { // 创建套接字 int sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockfd == -1) { continue; // 创建失败,尝试下一个地址 } // 客户端:连接服务器 if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); // 连接失败,关闭套接字 continue; } // 连接成功,跳出循环 break; } // 释放链表内存 freeaddrinfo(res);三、客户端 vs 服务器端的使用差异
// 客户端:连接 www.baidu.com 的 HTTP 服务 struct addrinfo hints, *res, *p; int sockfd; // 配置 hints memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; // IPv4 或 IPv6 hints.ai_socktype = SOCK_STREAM; // TCP // 获取地址链表 int status = getaddrinfo("www.baidu.com", "http", &hints, &res); if (status != 0) { fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status)); return 1; } // 遍历链表,尝试连接 for (p = res; p != NULL; p = p->ai_next) { sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockfd == -1) { continue; } if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); continue; } break; // 连接成功,跳出循环 } freeaddrinfo(res); // 释放内存 if (p == NULL) { fprintf(stderr, "failed to connect\n"); return 2; } // 现在可以使用 sockfd 进行通信
fprintf( 写入目标, 格式化字符串比如%d);成功返回写入字符数,失败返回负数结合ferror()和errno判断
printf固定输出到stdout,而fprintf可指定输出流:标准错误stderr、文件// 服务器端:监听本地 8080 端口 struct addrinfo hints, *res, *p; int sockfd; // 配置 hints memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; // IPv4 或 IPv6 hints.ai_socktype = SOCK_STREAM; // TCP hints.ai_flags = AI_PASSIVE; // 关键参数:用于服务器绑定 // 获取地址链表(注意:第一个参数为 NULL,表示监听本地所有地址) int status = getaddrinfo(NULL, "8080", &hints, &res); if (status != 0) { fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status)); return 1; } // 遍历链表,尝试绑定 for (p = res; p != NULL; p = p->ai_next) { sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockfd == -1) { continue; } // 设置套接字选项(可选) int yes = 1; if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) { perror("setsockopt"); close(sockfd); continue; } if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); continue; } if (listen(sockfd, 5) == -1) { close(sockfd); continue; } break; // 绑定成功,跳出循环 } freeaddrinfo(res); // 释放内存 if (p == NULL) { fprintf(stderr, "failed to bind\n"); return 2; } // 现在可以使用 sockfd 接受客户端连接
同一域名可能对应多个IP地址(负载均衡)。
可能同时支持IPv4和IPv6。
不同的地址配置(如TCP和UDP)。
- 用于服务器端绑定
当
node参数为NULL时,自动填充本地地址(如0.0.0.0或::)
- 使用
gai_strerror(status)将错误码转换为可读字符串。常见错误:
EAI_NONAME(域名不存在)、EAI_AGAIN(DNS查询失败)。
“
getaddrinfo()相比传统方法有什么优势?”
答:自动处理IPv4/IPv6兼容、服务名解析、线程安全,避免手动配置复杂的地址结构。
“
struct addrinfo的ai_flags有哪些常用值?”
答:
AI_PASSIVE(服务器绑定)、AI_CANONNAME(获取规范主机名)、AI_NUMERICHOST(禁止域名解析)。“如何确保你的服务器同时支持IPv4和IPv6?”
答:使用
getaddrinfo(),设置hints.ai_family = AF_UNSPEC和AI_PASSIVE标志。“为什么需要遍历
getaddrinfo()返回的链表?”
答:同一域名可能对应多个IP地址,需要尝试所有地址直到成功。
getaddrinfo()的核心价值在于:用一个函数封装了复杂的地址解析和配置过程,让你无需关心底层是IPv4还是IPv6,也不用手动处理字节序转换。只要按照“配置hints → 调用函数 → 遍历链表 → 使用地址”的流程七、个人思考 + 追问
struct addrinfo *res, *p; // res 和 p 都是指向 struct addrinfo 的指针 getaddrinfo("www.baidu.com", "http", &hints, &res); // 重点!
res:是一个输出参数,由getaddrinfo()函数内部动态分配自动填充。函数会根据域名解析结果,创建一个或多个struct addrinfo节点,并将链表头指针赋值给res。
p:是遍历链表时的游标指针,初始化为res(即链表头)。res 在内存中物理形态:
res ───> [struct addrinfo 节点1] ───> [struct addrinfo 节点2] ───> NULL ├── ai_flags: ... ├── ai_flags: ... ├── ai_family: AF_INET ├── ai_family: AF_INET6 ├── ai_socktype: SOCK_STREAM ├── ai_socktype: SOCK_STREAM ├── ... ├── ... └── ai_next ────────────────┘每个节点都是一个
struct addrinfo,其中:
ai_addr:指向具体的地址结构(如struct sockaddr_in)。
ai_next:指向下一个节点的指针,最后一个节点的ai_next为NULL遍历的时候:
for (p = res; p != NULL; p = p->ai_next) { // 使用 p->ai_addr 创建套接字 }
初始化:
p = res,让p指向链表头。终止条件:
p != NULL,当p走到链表末尾(即p->ai_next为NULL)时,循环结束。迭代步骤:
p = p->ai_next,让p指向下一个节点细节:
“为什么要遍历链表?只尝试第一个地址不行吗?”
答:不行。可能存在多种情况:
第一个地址是 IPv6,但客户端仅支持 IPv4。
域名解析返回多个 IP,部分 IP 可能不可达。
服务器同时监听 TCP 和 UDP,但客户端只需要 TCP。
“
getaddrinfo()返回的内存由谁管理?”
答:由
getaddrinfo()动态分配,必须调用freeaddrinfo(res)释放,否则会内存泄漏。“如何从
ai_addr获取具体的 IP 地址字符串?”
- 答:需要根据
ai_family类型转换:if (p->ai_family == AF_INET) { // IPv4 struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr; printf("IPv4: %s\n", inet_ntoa(ipv4->sin_addr)); } else { // IPv6 struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; char ip[INET6_ADDRSTRLEN]; inet_ntop(AF_INET6, &ipv6->sin6_addr, ip, INET6_ADDRSTRLEN); printf("IPv6: %s\n", ip); }八、再次深入思考
函数 诞生背景 主要用途 局限性 serv_addr早期 IPv4 时代 手动配置 IPv4 地址(如 struct sockaddr_in)仅支持 IPv4,需手动处理字节序转换,不支持域名解析 gethostbyname早期 DNS 支持需求 将域名转换为 IPv4 地址(返回 struct hostent)仅支持 IPv4,线程不安全,不支持服务名解析(如 "http" → 80) getaddrinfoIPv6 普及后的新标准 一站式解决域名解析、地址配置、协议选择(返回 struct addrinfo链表)无明显局限性,完全兼容 IPv4/IPv6,线程安全
serv_addr(如struct sockaddr_in)是为 IPv4 设计的,处理 IPv6 需要改用struct sockaddr_in6,代码需写两套。
getaddrinfo():通过ai_family=AF_UNSPEC自动选择 IPv4/IPv6,一套代码同时支持双协议。
serv_addr:若要连接域名(如 "www.baidu.com"),需先手动调用gethostbyname()解析,再手动填充sin_addr。
getaddrinfo():直接传入域名和服务名(如 "http"),自动完成解析和地址配置。
gethostbyname():依赖全局变量h_errno,多线程环境下会冲突。
getaddrinfo():所有状态通过返回值传递,线程安全。
getaddrinfo():返回标准化错误码(如EAI_NONAME),可通过gai_strerror()转换为可读字符串;支持更多配置选项(如AI_PASSIVE、AI_CANONNAME)。// 手动配置 IPv4 地址 struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; // 硬编码为 IPv4 // 若使用域名,需先手动解析 struct hostent *host = gethostbyname("www.baidu.com"); if (host == NULL) { perror("gethostbyname"); exit(1); } for (int i = 0; host->h_addr_list[i] != NULL; i++) { memcpy(&serv_addr.sin_addr, host->h_addr_list[i], host->h_length); // 创建套接字并尝试连接... } //memcpy(&serv_addr.sin_addr, host->h_addr_list[0], host->h_length); // 手动设置端口(需字节序转换) serv_addr.sin_port = htons(80); // 创建套接字并连接 int sockfd = socket(AF_INET, SOCK_STREAM, 0); connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));struct addrinfo hints, *res, *p; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; // 自动选择 IPv4 或 IPv6 hints.ai_socktype = SOCK_STREAM; // TCP // 一站式解析域名和服务名 getaddrinfo("www.baidu.com", "http", &hints, &res); // 遍历链表尝试所有地址 for (p = res; p != NULL; p = p->ai_next) { int sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); connect(sockfd, p->ai_addr, p->ai_addrlen); } freeaddrinfo(res); // 释放内存四、总结:为什么现代推荐用
getaddrinfo()?
代码简洁性:一行
getaddrinfo()替代了gethostbyname()+ 手动配置serv_addr的繁琐流程。跨协议兼容性:自动处理 IPv4/IPv6,无需为不同协议族写多套代码。
安全性:线程安全,避免全局变量冲突。
未来扩展性:支持更多高级特性(如
AI_PASSIVE用于服务器绑定)
getaddrinfo()自动处理字节序转换,无需手动调用htons()。原因:
服务名解析:
传入"http"或"80",函数内部自动解析为正确的端口号(已完成字节序转换)。地址结构封装:
返回的ai_addr结构体已完全配置好,直接用于connect()/bind(),无需额外处理对比:
- 传统方法:
serv_addr.sin_port = htons(80);现代方法:
getaddrinfo(..., "80", ...);→ 自动处理字节序这也是
getaddrinfo()简化网络编程的重要体现!我之前书里是直接手动输入 IP,实际可能不知道 IP
最后解释下 memcpy:
这行代码是在手动配置 IPv4 地址时的关键操作,核心作用是:
将域名解析结果(host->h_addr_list[0])中的二进制 IP 地址,复制到serv_addr.sin_addr字段中
host->h_addr_list[0]:
gethostbyname()返回的第一个 IP 地址(二进制形式,网络字节序)。serv_addr.sin_addr:
struct sockaddr_in中存储 IP 地址的字段(类型为in_addr)。memcpy作用:
- 直接内存复制,避免类型转换问题。
- 为什么不用赋值?
in_addr是结构体,需用memcpy复制整个内容
memcpy负责搬运二进制数据。而
strcpy是用于操作字符串(以\0结尾的字符数组),而这里的host->h_addr_list[0]和serv_addr.sin_addr存储的是二进制 IP 地址(非字符串),两者数据类型和用途完全不同。
函数 操作对象 数据类型 长度处理 strcpy字符串(文本) char[](带\0)按 \0截断,易越界memcpy二进制数据 任意类型内存块 按指定长度 n精确复制
IP 地址是二进制数据:
例如 IPv4 地址是 4 字节二进制数(如
192.168.1.1的二进制是0xC0A80101),不是以\0结尾的字符串。
host->h_length指定长度:
gethostbyname()返回的h_length是地址长度(IPv4 为 4 字节,IPv6 为 16 字节),memcpy按此长度精确复制,避免错误。避免
strcpy的隐患:
若误用
strcpy,会因二进制数据中可能包含\0而提前截断,导致 IP 地址不完整(如192.0.0.1的二进制中可能有0x00,被strcpy误认为字符串结束)
strcpy像 “按句子空格断句”,memcpy像 “按字节数搬运文件”。搬二进制文件(如图片)时,必须用memcpy按字节精确复制,不能用strcpy按文本逻辑处理
memcpy函数原型为 :memcpy(第一个参数:标标内存的指针,用于存储复制内容 ,需提前分配好至少
n字节空间,第二个参数:源内存的指针,指向要复制的数据源, 第三个参数:要被复制的字节数 )从源
src所指内存地址起始位置开始,拷贝n个字节到目标dest所指内存地址起始位置 。返回指向dest的指针头文件为
string.h
第九章可选项设置
更改缓冲区的跳过
说下和 Time-wait 有关的 SO_REUSEADDR :
之前回声服务器的逻辑是:
输入 Q 调用 close 就是向服务端发送 FIN,然后四次挥手
CTRL+C 由于 os 关闭文件套接字,产生 SIGINT 信号,也会发送 FIN,然后四次握手
只有先 FIN 的才会 Time-wait ,客户端先 FIN 没事(因为每次运行都会任意指定端口),服务端先 FIN 后同一个端口号重新运行服务端就会 bind() error,要等3min
客户端 IP 必须是本机网卡 IP
第一次客户端:192.168.1.100:8887 → 服务器:127.0.0.1:9190
下次: 192.168.1.101:8888 → 服务器:127.0.0.1:9190
Time - wait:是 TCP 连接关闭过程中,主动关闭连接的一方在完成四次挥手最后一步(发送完 ACK )后进入的一个状态 。处于该状态的连接会持续一段时间(通常是 2 倍报文最大生存时间 2MSL ),以确保可靠终止连接、防止旧数据包干扰新连接 。
TIME_WAIT 问题:是指因大量连接处于 Time - wait 状态引发的一系列问题 ,比如占用端口资源,导致端口耗尽,影响服务器性能 ,以及消耗系统资源等
别跟 keep-alive & keepalive 那两个搞混
豆包进一步解释 Time-wait:
在 TCP 的 TIME_WAIT 状态下,存在重传机制,但需要从两个角度理解:
一、TIME_WAIT 的核心作用
确保最后一个 ACK 可靠到达
主动关闭方发送最后一个 ACK 后进入 TIME_WAIT,持续时间为2 倍最大段寿命(2MSL)。
若这个 ACK 丢失,被动关闭方会重发 FIN,TIME_WAIT 状态的主动方需重传 ACK以完成关闭。
防止旧连接的延迟数据包干扰新连接
TIME_WAIT 期间,相同四元组(源 IP、源端口、目标 IP、目标端口)的新连接无法建立,避免旧数据包被误认为属于新连接。
- 若被动关闭方未收到 ACK,会重发 FIN(触发 TCP 的超时重传机制)
TIME_WAIT 状态的主动方收到 FIN 后,重传 ACK 并重置 TIME_WAIT 计时器(重新计时 2MSL)
TIME_WAIT 状态下,连接已关闭,不会主动发送任何数据(包括之前未确认的数据)
重传仅针对被动收到的 FIN 包,目的是完成四次挥手
“TIME_WAIT 状态的作用是什么?”
答:确保最后一个 ACK 可靠到达;防止旧连接的延迟数据包干扰新连接
“TIME_WAIT 期间会重传数据吗?”
答:不会主动重传数据,但会被动重传 ACK 以响应对方重发的 FIN
“如何减少 TIME_WAIT 连接的影响?”
答:设置
SO_REUSEADDR选项允许端口复用;调整系统参数(如tcp_max_tw_buckets);优先让客户端主动关闭连接TIME_WAIT 状态下的 “重传” 特指对被动收到的 FIN 包回复 ACK,而非重传之前的数据。这是 TCP 确保连接可靠关闭的关键机制
FIN 标志位:是 TCP 报文头部中的一个标志位 ,取值为 1 时,表示 “我要关闭连接” ,用于通知对方本端不再有数据要发送,准备关闭连接
FIN 包:是指 TCP 报文中将 FIN 标志位设置为 1 的数据包 ,是携带了关闭连接请求信息的实际数据包 ,用来具体传达关闭连接的请求 。 简单说,FIN 标志位是个状态标识,FIN 包是带有该标识的实际通信数据单元
确保 ACK 到达:在 TCP 四次挥手过程中,主动关闭方发送最后一个 ACK 报文后进入 TIME_WAIT 状态。网络传输可能出现丢包,如果这个 ACK 报文丢失,被动关闭方由于没收到确认,会根据超时重传机制,在一定时间后重发 FIN 报文
MSL 是报文在网络中能够存活的最长时间。一个 MSL 时间足够报文从发送端到达接收端 。所以等待 2 个 MSL,第一个 MSL 用来等主动方发出的 ACK 到达被动方(即使 ACK 没丢 ),第二个 MSL 用来等被动方重发的 FIN 到达主动方(若 ACK 丢了 ),这样主动方就能收到重传的 FIN 并再次回复 ACK,保证连接可靠关闭
避免旧报文干扰:TCP 连接关闭后,可能很快又建立相同四元组(源 IP、源端口、目的 IP、目的端口 )的新连接。旧连接中可能存在一些延迟的数据包还在网络中传输,等待 2 个 MSL 能让这些旧报文在网络中自然过期被丢弃。因为一个 MSL 能保证报文从一端传输到另一端,两个 MSL 就能保证旧连接的所有报文都不会再在网络中传输,不会被新连接误认为是有效报文而接收,防止干扰新连接的数据传输
理论上,延长等待时间到 3 个 MSL 甚至更久,确实能进一步降低旧报文干扰新连接以及 ACK 丢失导致连接无法正常关闭的风险 。但也存在弊端:
- 资源占用:主动关闭方在 TIME_WAIT 状态会占用端口等资源。等待时间过长,会导致端口资源长时间无法被复用,降低系统资源利用率。例如在高并发场景下,大量连接频繁建立和关闭,若都等待 3 个 MSL,会有很多端口长时间被占用,影响新连接的建立 。
- 性能影响:增加连接关闭的时间成本,降低了系统整体的响应性能和吞吐量。比如对于一些对实时性要求较高的应用,过长的等待时间会造成明显延迟 。
综合考虑可靠性和资源利用、性能等因素,TCP 协议规定等待 2 个 MSL ,这是在保证连接可靠关闭和避免资源过度占用之间的一个平衡选择
为啥不等更久?
资源占用:处于 TIME_WAIT 状态的连接会占用端口等系统资源。等待过久,会使端口长时间无法被复用,降低资源利用率,尤其在高并发场景下,会严重影响新连接建立
性能损耗:延长连接关闭时间,增加了时间成本,降低系统整体响应性能与吞吐量,无法满足实时性要求高的应用场景
平衡机制:2 个 MSL 已能在保证连接可靠关闭、避免旧报文干扰新连接方面达到较好效果,是可靠性与资源利用、性能间的合理平衡
如果没有 Time-wait,主动方立马关闭,那如果自己最后发的 ACK 丢了,被动方重传 FIN ,则永远无法收到主动方的重传 ACK 回应。有 Time-wait 则会重传最后的 ACK ,被动方正常终止
这是书上的解释,但我二刷的时候有了新的疑问,我问豆包:
如果被动方重传的一直丢,不也一样永远无法收到重传的回应ACK吗 如果没 Time-wait 那被动方就不会关闭了? 咋感觉TIme-wait没必要呢
豆包解释:
Time-Wait 的核心作用:
不是为了 “确保被动方一定能收到 ACK”,而是确保旧连接的所有报文段从网络中消失(2MSL 时长),避免干扰后续同名连接(如 IP + 端口相同的新连接)。
即使被动方重传的 FIN 一直丢失,主动方在 Time-Wait 超时后仍会关闭,被动方因自身 RTO 超时也会最终关闭(不会 “永远不关闭”)
无 Time-Wait 的风险:
若主动方立即关闭,若 ACK 丢失,被动方会因 RTO 重传 FIN,但此时主动方已关闭端口,重传的 FIN 会被视为新连接的 SYN(若新连接恰好使用相同端口),导致数据错乱。
Time-Wait 通过 “等待旧报文过期” 规避了这一风险,而非依赖 “ACK 是否成功到达”。
为何 Time-Wait 是必要的:
TCP 需要保证连接唯一性(避免新旧连接报文混淆),2MSL 是实现这一目标的最小可靠时长。
被动方的最终关闭由其自身超时机制(RTO)保证,Time-Wait 的存在是为了主动方的 “兜底” 和全局可靠性,而非解决单次重传丢失的问题
结论:
Time-Wait 的核心价值在于防止旧连接报文干扰新连接,而非确保每次重传 FIN 都能被响应。即使重传 FIN 丢失,双方超时机制仍会强制关闭连接,而 Time-Wait 通过时间隔离避免了更严重的协议混乱所以没Time-wait的话,被动方不断重传FIN,那啥时候关闭?
被动方会继续按退避后的 RTO 重传(如 Linux 默认最多重传 5 次),达到最大重传次数后强制关闭
太开门了艹
但紧急事故想重启就必须等待,如图,假如 A 发送的丢了,B 会认为自己发的 FIN 没被对方收到,重传,那此时 A 又会 重启 Time-wait 计时器,四次握手不得不延长 Time-wait 过程。
所以引入地址再分配,
重新可以绑定相同地址

int option;
socklen_t optlen;
optlen=sizeof(option);
option=TRUE;//默认为假, 即无法将 Time-wait 状态下的套接字端口号 重新分配给新套接字
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);
系统底层有 typedef 声明:
typedef unsigned long socklen_t; // 64位系统
typedef unsigned int socklen_t; // 32位系统
getsockopt( 第一个参数:套接字,第二个参数:可选项的协议层, 第三个参数:可选项名, 第四个参数:保存查看结果的缓冲地址, 第五个参数 socklen_t 指针型:向第四个参数传递的缓冲大小即可接收多大的,函数调用后被填充为实际写入的大小到指针指向的变量中)
成功返回 0,失败返回 -1
setsockopt( 第一个参数:套接字,第二个参数:可选项的协议层, 第三个参数:可选项名, 第四个参数:保存要更改的缓冲地址, 第五个参数 socklen_t 型:向第四个参数的缓冲大小)
书里妈逼的跟个狗逼一样艹,管最后一个参数叫返回,其实是 填充
妈逼的书里有错误,勘误,P144 的 12 行,最后一个参数应该是 socklen_t 类型 ,P146写的的对的
Nagle 算法:应用于TCP层
收到 ACK 后再发送下一数据
TCP默认开启,最大限度利用缓冲,N 的 ACK 没到,剩下的 agle 进入缓冲,然后一起发。不用 Nagle 就会对网络流量有负面影响。头部信息很大,传多次的话网络传输效率就低

Nagle 是合并小包,大文件通常数据以较大的 MSS 分段发送,开 ACK 就会额外的延迟,所以追求低延迟要关。但如果网络宽带有限或者拥塞,Nagle依旧要开
P151说的“使用与否在网络流量上差别不大,使用 Nagle 传输就更慢”说的真JB抽象,汉语水平跟他们外国人维族人似得
其实说人话就是“开 Nagle 也不会有太大的网络波动,即代表网络好,用 Nagle 反而慢”
关 Nagle 可能会拥塞性能下降,因为 小数据包泛滥 & ACK 延迟交互
所以要权衡
禁用 Nagle:TCP_NODELAY 改为 1
勘误:书这里的 setsockopt 函数最后一个参数也应该是 socklen_t 类型,不管了,懒得改了
//禁用
int opt_val=1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));
//查看
int opt_val;
socklen_t opt_len;
opt_len=sizeof(opt_val);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len);
//直接使用 sizeof(类型) 看似可行,是因为在多数情况下,sizeof 的返回值类型能隐式转换为 socklen_t 。但从严格意义和代码规范角度,应先将 sizeof 的结果赋值给一个 socklen_t 类型变量,再传递该变量,这样可避免潜在类型问题和增强代码可读性,例如:
socklen_t optlen = sizeof(类型);
setsockopt(sock, level, optname, optval, optlen);
第10章这里之前已经二刷过了,这里贴个面向我生成前端展示的知识提炼总结:
基于 多任务进程 的 并发服务器模型
末尾符 '\0' 的 ASCII 为0
基于之前的有控制的 TCP 客户端代码

1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 8 #define BUF_SIZE 1024 9 void error_handling(const char *message); 10 11 int main(int argc, char *argv[]) 12 { 13 int sock; 14 char message[BUF_SIZE]; 15 int str_len, recv_len, recv_cnt; 16 17 struct sockaddr_in serv_adr; 18 if(argc!=3) { 19 printf("Usage : %s <IP> <port>\n", argv[0]); 20 exit(1); 21 } 22 23 sock=socket(PF_INET, SOCK_STREAM, 0); 24 if(sock == -1) 25 error_handling("socket() error"); 26 27 memset(&serv_adr, 0, sizeof(serv_adr)); 28 serv_adr.sin_family=AF_INET; 29 serv_adr.sin_addr.s_addr=inet_addr(argv[1]); 30 serv_adr.sin_port=htons(atoi(argv[2])); 31 32 if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1) 33 error_handling("connect() error!"); 34 else 35 puts("Connected..........."); 36 37 38 //这里为了引出 分割IO 做铺垫 39 while(1) 40 { 41 fputs("Input message(Q to quit): ", stdout); 42 fgets(message, BUF_SIZE, stdin); 43 44 if(!strcmp(message,"q\n") ||!strcmp(message,"Q\n")) 45 break; 46 47 str_len=write(sock, message, strlen(message)); 48 49 recv_len=0; 50 while(recv_len<str_len) 51 { 52 recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1); 53 if(recv_cnt==-1) 54 error_handling("read() error!"); 55 recv_len+=recv_cnt; 56 } 57 message[recv_len]=0; 58 printf("Message from server: %s", message); 59 } 60 close(sock); 61 return 0; 62 } 63 64 void error_handling(const char *message) 65 { 66 fputs(message, stderr); 67 fputc('\n', stderr); 68 exit(1); 69 }
View Code
P174 多进程 服务端 代码:(下面的分隔 IO 也是这个服务端代码)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(const char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
//防止僵尸
act.sa_handler=read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
state=sigaction(SIGCHLD, &act, 0);
//套接字初始化
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
while(1)
{
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock==-1)
continue;
else
puts("new client connected...");
pid=fork();
if(pid==-1)
{
close(clnt_sock);
continue;
}
if(pid==0) /*子进程运行区域*/
{
close(serv_sock);
while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid=waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
简简单单,但注意连接没断开,有新连接来了照样可以连
继续深入:
fork 父进程 将 套接字文件描述符 复制给 子进程,
注意:这是复制文件描述符,没复制套接字,套接字是 OS 拥有的
不同协议的套接字可以使用同一个端口,但如果理解为 fork 是复制套接字,那多个套接字对应同一个端口就不太合理了
所以 fork 后,2个文件描述符指向同一个套接字,即一个套接字中存在2个文件描述符,只有2个文件描述符都终止才销毁套接字,所以代码里关闭了无关的套接字文件描述符

插一个知识点: fgets(第一个参数:存入str,第二个参数:最多读取 n - 1 个字符,留空间给 \0,第三个参数:从指定stream)
遇到 \n 或者 EOF 停止,成功返回 str ,失败 或者 EOF 返回 NULL
返回值为
NULL时:
先通过
feof(stream)判断是否到达文件末尾(EOF),若是则返回值为非零;若
ferror(stream)返回非零,表示发生了错误。示例代码:
char buffer[100]; if (fgets(buffer, sizeof(buffer), stream) == NULL) { if (feof(stream)) { printf("已到达文件末尾(EOF)\n"); } else if (ferror(stream)) { perror("读取错误"); } }
- 原理:
EOF 是流的状态标志(由系统设置),而非字符;
错误标志(
ferror)和feof需手动重置,调用clearerr(stream)会同时重置,否则后续操作会受之前文件结束状态影响总结:通过
feof()和ferror()可明确区分 EOF 和错误
再接着深入说下分隔 IO
真的好难受,我有很多疑问,想不通书上讲的分割 IO(后来也懂了,此文搜“里面的while(1) 那就是引出分割 IO”,其实就是不用无限等待,等来了返回的,才能键盘传输下一批数据,但IO分割就是可以先输入,不用无条件等返回) ,小林coding也没有,究竟要不要学啊,唉,吃了太多亏,学了太多没用的东西,~~~~(>_<)~~~~
我问豆包发现书里只是简化教学,经不住我的追问,问豆包这个大厂面试含金量咋样,决定学下这个代码,并且联想到了小林coding里讲的那个 HTTP2 并发传输,搜 刷网络时候写的博客 “客户端发起的流”
后来发现不一样,此文搜“但现在学了这么多,有了更深的理解”
~~~~(>_<)~~~~还要跟耗子住多久,还要跟建筑工地的人一起吃便宜饭多久~~~~(>_<)~~~~何时能有出头之日啊~~~~(>_<)~~~~职场十二分的真诚换来被组长领导们联合起来做事阴险背后捅刀子给我搞走,离开了银行外包测试,下定决心开始学开发,何时能有份工作啊~~~~(>_<)~~~~每天都吃不饱饭,感觉真的要死了~~~~(>_<)~~~~
又问豆包说 Boost 是基于 分割 IO 封装的网络库
但应该
这块先放,等再刷的时候,实际写最终 终极版本代码的时候再捋顺
回头来了继续说 分割IO ,上面的服务端代码,再往上,是客户端代码(折叠了),里面的while(1) 那就是引出分割 IO,因为
write 和 read 是串行的,必须等 write 发送完数据,再 read 接收响应。分割IO可以在边写的时候,边读,只是服务端没写回完,依旧阻塞等待阻塞风险:如果 read 被阻塞(如服务器未及时响应),客户端无法发送新数据,必须无限制的阻塞 read服务端返回,才能进行下一次的输入。
我的疑问是:
不分割 IO:比如我写 haha 等着写回 haha, 但此时有小拥塞,导致稍微慢了点,我就一直等 然后等到后再写abc 再等返回
分割 IO:写haha,这时候没写回就可以直接写haha?那写回的haha不就跟abc乱了吗?
豆包解答:
确实,书里为了简化教学,这里仔细想确实有问题
所以引入
在每个请求中添加唯一 ID,服务器在响应中携带相同 ID:
客户端 → 服务器:[ID=1, Data=haha] 客户端 → 服务器:[ID=2, Data=abc] 服务器 → 客户端:[ID=1, Data=haha] // 响应第一个请求 服务器 → 客户端:[ID=2, Data=abc] // 响应第二个请求客户端通过 ID 匹配请求和响应,即使响应顺序与发送顺序不一致,也能正确关联
分割 I/O 的优势主要体现在异步通信场景,例如:
- 实时游戏:客户端连续发送操作指令(如移动、攻击),同时接收服务器广播的游戏状态
长连接服务:客户端发送心跳包、订阅消息,同时接收服务器推送的实时数据
与 HTTP/2 对比:
提升并发能力:HTTP/2 引入 “流(stream)” 的概念,能在一个连接上并发处理多个请求和响应,就像分割 I/O 程序通过多进程 / 多线程让读写操作并行,都致力于提升数据传输和处理的并发性能。
避免阻塞:HTTP/2 解决了 HTTP/1.1 的队头阻塞问题,不同流的请求和响应互不干扰;分割 I/O 程序通过将读写分离到不同进程 / 线程,避免了读操作阻塞时影响写操作
实现层面:HTTP/2 是在应用层协议层面进行优化,基于特定的协议规范(如二进制分帧层等 )来实现多路复用;而分割 I/O 程序是在应用程序代码层面,通过多进程、多线程等编程手段来实现读写操作的分离和并发。
适用范围:HTTP/2 主要用于 Web 相关的网络通信场景,规范浏览器与服务器之间的交互;分割 I/O 程序的思路更通用,适用于各种基于套接字编程的网络应用场景,不限于 Web 应用
然后我又思考,认为可能出现粘包问题,即:[1] ab [2] haha [1] c,豆包回答:
通过固定长度协议或者用\n做末尾区分
解决完感觉这玩意量挺大, 先搁置吧,自由发挥的终极代码再具体研究,继续回到书上那个漏洞百出的 分隔IO 代码:
服务端还是上面的,客户端改成:P180
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(const char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc!= 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
pid=fork();
if(pid==0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while(1)
{
int str_len=read(sock, buf, BUF_SIZE);//这里最好写成 BUF_SIZE - 1
if(str_len==0)
return;
buf[str_len]=0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while(1)
{
fgets(buf, BUF_SIZE, stdin);
if(!strcmp(buf,"q\n") ||!strcmp(buf,"Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
说一些一刷的时候没注意的细节:
abc输入中),接收进程可随时收到服务器对之前请求(如haha)的响应并显示,导致输入过程中 “蹦出” 上次响应因为数据分多次返回,buf[str_len]=0 是给 printf 用的,确保 printf 能正确识别字符串结束位置,没 \0 结束符就会 printf 错误
char message[] = "Hello World!";
write(clnt_sock, message, sizeof(message)); // sizeof(message) = 13(包含\0)
如果这样就必须手动加 \0
char message[] = "Hello World!";
write(clnt_sock, message, strlen(message)); // strlen(message) = 12(不包含\0)
再次给书里 P181 代码做勘误,做优化,即上面的客户端代码中 int str_len=read(sock, buf, BUF_SIZE);应该写成 BUF_SIZE - 1,若服务端发送的数据长度恰好为 BUF_SIZE(30 字节),客户端 read 会将 30 字节填满 buf,此时再执行 buf[str_len] = 0;(即 buf[30] = 0;)会导致数组越界
char buf[10];
int str_len = read(sock, buf, sizeof(buf) - 1);
buf[str_len] = '\0'; // 手动添加字符串结束符
解读 close 和 shutdown
close / shutdown 都会传递 EOF 信号,read 会返回 0,但 EOF 是应用层概念,表示数据传输结束 ;FIN 包是 TCP 层控制报文 ,用于断开连接 。应用层检测到 EOF 后 ,底层 TCP 协议会发送 FIN 包来关闭连接
子进程 读写功能 都有,但代码里仅执行 写 操作, write 写完就shutdown,关闭子的写,还剩子的读,然后close,本质关闭的是 读,子文件描述符关了
父进程 读写功能 都有,但代码里仅执行 读 操作,然后父收到服务器返回的时候,父读完也 close,这时候本质关闭的是 读写
妈逼的代码最后不是有个 close 吗?父子各个减少1次,不就关了吗?还 shutdown 干啥?
仅靠
close无法及时告知服务器EOF如果客户端的子进程 return 了,然后调用 close了,由于父进程还持有套接字描述符,此时连接不会立即关闭,
文件描述符没关闭,服务端也就没法收到 EOF,也就不知道你写完了,必须全关闭才行
所以,在 TCP 协议层面,服务器不会收到代表数据发送结束的
FIN包(对应应用层EOF概念 )因为
close只是减少文件描述符的引用计数,只有当引用计数为 0 时才真正关闭连接并发送FIN包。但父进程还在读取数据,不会马上close,所以服务器无法及时得知子进程不再发送数据
shutdown的必要性:
shutdown(sock, SHUT_WR)则不同,它不依赖引用计数,直接切断子进程到服务器的写通道,向服务器发送FIN包,让服务器及时知道客户端(子进程)不再发送数据了
艹,把这书串联起来真他妈费劲好头大
但发现啃过的东西,再次捋顺确实快不少
书 P189,把管道的进程间通信应用到网络代码中
客户端用的是上面的,搜“客户端改成:P180”,服务端改成如下,进程通信跟服务器没关系,但有助于理解 OS:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 100
void error_handling(const char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
int fds[2];
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler=read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
state=sigaction(SIGCHLD, &act, 0);
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
int optval = 1;
//if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
// error_handling("setsockopt() error");
//}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
pipe(fds);
pid=fork();
if(pid==0) {
FILE *fp=fopen("echomsg.txt", "wt");
char msgbuf[BUF_SIZE];
int i, len;
for(i=0; i<10; i++) {
len=read(fds[0], msgbuf, BUF_SIZE);
fwrite((void*)msgbuf, 1, len, fp);
}
printf("M\n");
fclose(fp);
return 0;
}
while(1) {
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock==-1)
continue;
else
puts("new client connected...");
pid=fork();
if(pid==0) {
close(serv_sock);
while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0) {
write(clnt_sock, buf, str_len);
write(fds[1], buf, str_len);
}
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid=waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
fgets会自动添加\0:fgets(message, BUF_SIZE, stdin)会读取最多BUF_SIZE-1个字符,并在末尾自动添加\0。但若用户输入超过BUF_SIZE-1,则仅存储前BUF_SIZE-1个字符,剩余内容会留在输入缓冲区。服务端返回的数据是否有
\0取决于服务端如何发送数据。若服务端使用
write(sock, message, strlen(message)),则不会发送\0(strlen不包含\0),客户端需手动添加;若使用write(sock, message, sizeof(message))且message是字符串数组,则会发送\0。上文中“基于之前的有控制的 TCP 客户端代码”里:
和
两段代码逻辑等价:
- 第一段代码在循环外添加
\0,前提是已确保完整接收所有数据(recv_len == str_len)第二段代码在每次读取后立即添加
\0,边读边输出这里就是基于之前的服务端代码
首先主父进程在39行创建了个子进程叫做子A,都有serv_sock,所以通过最后的close(serv_sock)是主父和子A关闭文件描述符主父进程在while(1)里:
又创建一个子B进程,往客户端写,再往管道写67、74行很好的关闭了子B的描述符
书上的 79、81行也很好的关闭了主父的描述符
子A就:
就是循环10次从管道读,然后写入文件
关于10次我理解是,如果客户端写入 1 次的数据,如果因服务端网络波动分 2 次读,对于该
for循环来说是算 2 次读取,(或者 服务端代码的 BUF_SIZE 是 比如 4,那你输入客户端输入“abc\n”,fgets得到的是自动加结束符“abc\n\0”总共5个字节,然后write/read的时候原样收发,如果服务端设置的是最大 4,那就要分2次)即分段了,就那分成的段来说,只要读到10个段就不再往文件里写了,就变成只是正常的写回客户端了 ,但若达到 10 次后,write(fds[1])仍会执行,但此时文件已关闭(fp == NULL),会导致fwrite返回错误(但代码未检查该错误)如果此时多个进程同时运行,也就是同时写,那可能进程A写5个,进程B写5个,文件里就是AB组合的字符,但多进程同时写入文件存在竞态条件,若多个子进程 A 同时写入同一文件,会导致内容交错(如进程 A 写 5 个字符,进程 B 写 5 个字符)。需通过文件锁或原子操作保证写入原子性(如使用
O_APPEND标志)4. 在 C 语言中,以下情况字符串数组会自动加
\0:
- 字符串字面量初始化数组:用字符串字面量初始化字符数组时,编译器会自动在末尾添加
\0。比如char str[] = "hello";,这里str数组实际存储为{'h', 'e', 'l', 'l', 'o', '\0'},数组长度是 6 。
fgets函数读取:fgets从标准输入或文件读取字符串时,会自动在读取到的字符串末尾添加\0。它最多读取n - 1个字符(n是指定的缓冲区大小),然后添加\0总结:
会自动添加
'\0':当使用
scanf("%s", str)读取字符串时,scanf会在读取的字符末尾自动添加'\0',形成合法的 C 字符串。例如:char str[6]; scanf("%s", str); // 输入 "hello",str 变为 `{'h','e','l','l','o','\0'}`但存在缓冲区溢出风险:
安全写法:使用
scanf不检查输入长度是否超过数组大小(str的容量需至少为 输入长度 + 1 以容纳'\0')。若输入过长(如str大小为 5,输入 6 个字符),会导致缓冲区溢出(未定义行为)%ns限制读取长度(n为数组大小减 1),例如scanf("%5s", str)(数组大小至少为 6,预留'\0'位置)char str[10]; // 容量10,可存9个字符 + '\0' scanf("%9s", str); // 限制读取9个字符,自动添加 '\0',无溢出风险
read:是底层 I/O 函数,设计初衷是高效传输原始数据(如二进制文件、网络数据),而非专门处理字符串,传啥是啥,不会自动加 \0,\0 是C语言的约定
函数 自动添加 '\0'边界检查 备注 scanf("%s")是 否 需手动限制长度(如 %ns)read()否 否 读取原始数据,需手动处理字符串 gets()是(已废弃,靠,我记得我刷题还用来着) 否 读取到换行符,溢出风险极高 fgets()是 是 安全读取(限制长度,包含换行符)
这是多进程并发服务器的延伸 —— IO 复用模型
每个进程独立空间不利于交换数据(只能用 IPC 进程间通信:管道、消息队列、共享内存、信号量、套接字)
select 就是具有代表性的复用服务端:
可以将多个文件描述符集中到一起监视
监视项 叫 事件,发生 监视项 对应情况 叫 发生事件
步骤一:设置文件描述符 → 指定监视范围 → 设置超时
步骤二:调用 select 函数
步骤三:查看调用结果
~~~~(>_<)~~~~又一次问豆包“我29了,只有银行测试外包工作经验”
linlinsong:怒了,妈逼的我刷通了算法,又啃了TCPIP网络编程,这么不堪吗?依旧只能是个初级?
fd_set 结构体,里面有 fd0、fd1 等状态位的整数数组,fd_set 有各种宏来操作

FD_SET 宏本质是对 fd_set 结构体中表示文件描述符状态的位进行操作。fd_set 内部类似用一个位数组来记录文件描述符状态,每个位对应一个文件描述符编号。比如文件描述符 fd 编号为 5 ,就对应位数组下标为 5 的位
FD_SET(int fd, fd_set *fdset) 中,fd 是要监控的文件描述符 ,fdset 是指向 fd_set 结构体的指针 。“注册文件 fd 的信息” 就是把 fdset 结构体中对应 fd 编号的位设置为 1 ,表示开始监控这个文件描述符 ,告诉 select 函数后续要留意该文件描述符是
FD_ISSET(int fd, fd_set *fdset)就是 fd_set 指向的变量中包含 fd 的信息,则返回“真”
select(
第一个参数:监视对象文件描述符数量、
第二个参数:将所有关注“是否存在待读数据”的文件描述符注册到 fd_set 变量,并传递地址值、
第三个参数:将所有关注“是否可传输无阻塞数据”的文件描述符注册到 fd_set 变量,并传递地址值、
第四个参数:将所有关注“是否发生异常”的文件描述符注册到 fd_set 变量,并传递地址值、
第五个参数:调用 select 函数后,传递超时信息 )
函数返回值:
监视范围 :
每次新建文件描述符,第一个参数都会 + 1,且从 0 开始,所以直接将最大文件描述符的值加1再传递就行
超时时间:
函数最后一个参数是结构体,tv_sec 是秒,tv_usec 是微秒,NULL 不设置
查看返回结果:
调用 select 前都 set 初始化 0 ,然后监视的弄成 1,即监视 fd 1、2、3
select 后没变的就弄成了 0 ,有变化的依旧是 1

为了理解,先整合所有知识点的 书 P201 select 代码:压压惊,开开胃
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char *argv[]) {
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads);
FD_SET(0, &reads); // 0 is standard input(console)
// timeout.tv_sec = 5; // 不可以放这
// timeout.tv_usec = 0;
while (1) {
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
result = select(1, &temps, 0, 0, &timeout);
if (result == -1) {
puts("select() error!");
break;//表示发生了错误 ,比如系统资源不足、文件描述符错误等 。这种情况下程序继续执行也无法得到正确结果,还可能引发更多问题,所以通过 break 跳出循环,及时终止程序避免错误扩大
}
else if (result == 0) {
puts("Time-out!");
}
else {
if (FD_ISSET(0, &temps)) {
str_len = read(0, buf, BUF_SIZE);//为了严谨应该BUF_SIZE-1
buf[str_len] = 0;
printf("message from console: %s", buf);
}
}
}
}
解释:
关于超时:
必须放 while 里,假设 5s 超时,2s 有了反应,那 timeout 结构体成员变量就会返回剩余的时间,即 3s ,再次进入 while 循环调用 select 的时候,不重新设置就会变成 3s ,无法按照预期等待
关于
fd_set reads:哪怕叫
fd_set abc,只要传入第二个参数,也是监视 读 事件关于检查:
就是先复制给 temps 保留,等监听到有事件发生后,就去 temps 里找哪个变化了,然后再次监听的时候,就依旧要拷贝给temps
因为调用 select 后,除了发生变化的文件描述符对应位外,剩下所有位将初始化 0,因此用这种拷贝记住初始值(这玩意在刷题里不就是简单不能再简单的东西了吗)
细节:
不需要监视时间,就传入 0 或者 NULL
优化书里代码,为了严谨,应该是 BUF_SIZE - 1
必须有 buf[]=0,因为输入“hello\n”只会原样的 read ,不会自动加 \0
read是底层 I/O 函数,设计初衷是高效传输原始数据(如二进制文件、网络数据),而非专门处理字符串。
- 5s 持续监听重置 5s,再继续
豆包修改html前端代码真垃圾,用 Deepseek 改成了,V1 的 client.c 点复制成了V2版本的服务端那个内容
开始实操,书 P203 、IO 复用 服务端代码 :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handling(const char *buf);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while (1)
{
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if (fd_num == 0)
continue;
for (i = 0; i < fd_max + 1; i++)
{
if (FD_ISSET(i, &cpy_reads))
{
if (i == serv_sock) // connection request!
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if (fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else // read message!
{
str_len = read(i, buf, BUF_SIZE);
if (str_len == 0) // close request!
{
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else
{
write(i, buf, str_len); // echo!
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(const char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
解释:
- 服务器 监听 客户端 连接请求,当有客户端发起连接,服务器套接字就会收到相关信息 ,这类似从套接字 “读取” 到有连接请求到来
select(fd_max + 1, &cpy_reads, 0, 0, &timeout)会监听文件描述符0、1、2、3:
0:标准输入1:标准输出2:标准错误3:serv_sock(监听套接字)select 返回 ≥ 1 才是有事件发生,先验证服务端,避免积压导致客户端连接超时或失败,已连接客户端的数据处理可稍后进行(不会影响连接建立),否则:
- 客户端处于 SYN_RECV 状态时间过长(TCP 握手延迟)
客户端连接超时或报错
服务器半连接队列(backlog 队列)满,拒绝新连接
- 关于半链接:
- 半连接队列(SYN 队列):
客户端发送 SYN 请求建立连接,服务器收到后回复 SYN+ACK,此时连接处于半连接状态(等待客户端确认 ACK),该状态会被存入半连接队列(backlog 队列的一部分)。
- 全连接队列(ACCEPT 队列):
客户端确认 ACK 后,连接进入全连接状态,等待服务器调用
accept()取出连接并处理,此时连接存入全连接队列。队列满的后果:
若服务器未及时调用
accept()处理全连接队列中的连接,全连接队列会被占满。半连接队列也有固定大小,若同时有大量新连接涌入,半连接队列满后,服务器会直接丢弃新的 SYN 请求(或回复 RST 复位报文),客户端会收到连接被拒绝(
ECONNREFUSED)的错误
服务端套接字有变化,受理请求后,在fd_set 变量 reads 中注册与客户端连接的套接字文件描述符
FD_SET(clnt_sock, &reads);- 更新 fd_max 为当前最大文件描述符确保监听范围活跃套接字
if (fd_max < clnt_sock) fd_max = clnt_sock;- 然后 else,由于只监听了读事件,所以除了客户端的连接请求,就剩下读客户端写的数据了:
read >0:客户端发送数据read ==0:客户端正常关闭(FIN 包)read ==-1:异常(需检查 errno,如 ECONNRESET)echo 是回显的意思
str_len = read(i, buf, BUF_SIZE);从客户端套接字 i 读取数据到 buf然后
write(i, buf, str_len);从 buf 发送到 i搭配的客户端是,此文搜“基于之前的有控制的 TCP 客户端代码”
其实这里我再次啰嗦下,加深理解:
mkdir 新建
mv old new
rm -f 强制删除文件
rm -d 强制删除目录
nano Ctrl+O保存,Ctrl+X退出
sudo fuser -k 端口号
回到上级目录:cd..
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。