20231326《密码系统设计》十一周预习报告

20231326《密码系统设计》十一周预习报告

学习内容

《Head First C 嗨翻 C 语言》 第11章

1. 网络程序概述

  • 组成部分: 网络程序通常由服务器客户端两部分组成 。
  • 通信基础: 不同计算机上的程序需要对话 。网络通信使用一种新的数据流——套接字(Socket),用于读写字节 。
  • 协议(Protocol): 客户端与服务器之间展开一段结构化对话,这被称为协议 。
    • 低层协议: 如IP (Internet Protocol,网际协议) 。
    • 高层协议: 如HTTP (Hypertext Transfer Protocol,超文本传输协议) 或自定义协议(如IKKP) 。

2. 服务器编程:BLAB四部曲

服务器在使用套接字与客户端通信前,需要经历四个阶段,缩写为 BLAB绑定 (Bind)监听 (Listen)接受 (Accept)开始 (Begin)

  1. 创建套接字(Socket):
    • 使用 socket(PF_INET, SOCK_STREAM, 0) 系统调用创建互联网套接字描述符(listener_d)。
  2. 绑定端口(Bind):
    • 端口(Port): 类似于电视频道,用于防止不同对话混淆 。常见的端口有网页(80)、Email(25)、聊天(5222)等 。
    • 服务器需要将套接字绑定到特定的端口,通常建议使用 1024号以上的端口
    • 使用 bind() 系统调用将套接字描述符和套接字名(表示“互联网IP地址和端口号”的结构体 struct sockaddr_in)进行绑定 。
  3. 监听(Listen):
    • 使用 listen(listener_d, 10) 系统调用设置客户端连接的等待队列长度(例如最多10个客户端排队等待连接)。
  4. 接受连接(Accept):
    • 使用 accept() 系统调用等待客户端连接 。当有客户端连接时,accept() 会返回一个新的套接字描述符connect_d),服务器将使用此描述符与客户端通信 。

3. 套接字数据传输

  • 套接字特性: 套接字是双向的,不能使用 fprintf()fscanf() 等传统I/O函数进行通信 。
  • 发送数据: 使用 send() 函数向套接字输出数据 。
  • 读取数据: 使用 recv() 函数从套接字读取数据 。
    • recv() 返回读取的字节数,错误返回-1,客户端关闭连接返回0 。
    • recv() 不一定能一次接收到所有字符,可能需要多次调用 。

4. 服务器高级话题与错误处理

  • 错误检查: 必须始终检查所有系统调用的返回值(如 socketbindlistenacceptsend),以处理可能发生的网络错误 。
  • 端口重用: 当服务器关闭后立即重启时,由于绑定端口有延时(约30秒),bind() 调用可能会失败("Address already in use") 。
    • 解决方法: 在调用 bind() 前,使用 setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, ...) 设置套接字的 SO_REUSEADDR 选项,允许重新使用已绑定的端口 。
  • 处理多客户端:
    • 单个进程的服务器在处理一个客户端时,其他客户端会被阻塞 。
    • 解决方案: 使用 fork() 系统调用,在接收到连接后,克隆一个独立的子进程来处理与该客户端的对话 。
    • 分派后,父进程关闭用于对话的副套接字(connect_d),子进程关闭主监听套接字(listener_d)。

5. 客户端编程

客户端的主动权在于它自己,只需两步即可与服务器建立通信:连接远程端口开始通信

  1. 解析地址(getaddrinfo):
    • 为了使用域名(如www.oreilly.com)而非IP地址,客户端需要使用 DNS(域名系统) 将域名转化为IP地址 。
    • 使用 getaddrinfo(host, port, &hints, &res) 函数,给定域名和端口号,获取地址信息结构 addrinfo
  2. 连接远程套接字(Connect):
    • 利用 getaddrinfo 获得的地址信息,首先使用 socket() 创建套接字,然后使用 connect() 系统调用连接远程套接字 。
    • 连接后,同样使用 send()recv() 进行数据读写 。

AI 对学习内容的总结

1. 网络程序概述

我的总结 补充细节
组成部分: 网络程序通常由服务器客户端两部分组成。 互联网底层代码: 互联网中大部分的底层网络代码都是用C语言写的 。
协议(Protocol): 低层协议如IP,高层协议如HTTP或自定义协议(如IKKP)。 低层协议功能: IP(网际协议)用来控制二进制的0和1在互联网中的发送方式 。
高层协议功能: HTTP(超文本传输协议)用来控制浏览器和网络服务器的对话 。

2. 服务器编程:BLAB四部曲

我的总结 补充细节
绑定端口(Bind): 端口号类似于电视频道,通常建议使用 1024号以上的端口 端口号范围: 端口号从0开始一直到65535 。
小端口号限制: 1024号以下的端口通常保留给知名服务,在很多计算机中,只有超级用户或管理员才有资格使用 。
监听(Listen): 使用 listen(listener_d, 10) 设置客户端连接的等待队列长度(例如最多10个客户端排队等待连接)。 队列满时的行为: 如果队列长度设为10,第11个客户端会被告知服务器太忙 。

3. 套接字数据传输

我的总结 补充细节
读取数据(recv): recv() 返回读取的字节数,错误返回-1,客户端关闭连接返回0。recv() 不一定能一次接收到所有字符,可能需要多次调用。 客户端输入格式: 当用户在telnet等客户端输入文本时,字符串通常以\r\n(回车换行符)结尾 。

4. 服务器高级话题与错误处理

我的总结 补充细节
端口重用: 服务器关闭后立即重启时,bind() 调用可能会失败("Address already in use"),需设置 SO_REUSEADDR 选项 延时原因: 绑定端口有延时,在服务器关闭后的接下来约30秒内,操作系统不允许任何程序再绑定它 。
处理多客户端: 使用 fork() 克隆一个独立的子进程来处理与客户端的对话。 进程数量控制: 如果预期有大量客户端连接,需要控制创建子进程的上限

5. 客户端编程

我的总结 补充细节
解析地址(getaddrinfo): 使用 DNS 将域名转化为IP地址,获取地址信息结构 addrinfo addrinfo 结构特性: addrinfo(名字资源)可以在堆上创建 ,大型网站通常有好几个IP地址,该结构可以从中挑选一个 。
连接远程套接字(Connect): 利用 getaddrinfo 获得的地址信息,使用 socket() 创建套接字,然后使用 connect() 连接。 连接后的清理: 连接后,必须使用 freeaddrinfo() 函数清除在堆上创建的地址信息结构(名字资源)。

学习思维导图

mindmap root((网络编程总结)) 概述 服务器与客户端 套接字通信 协议 低层:IP 高层:HTTP、自定义协议 服务器编程(BLAB) Bind:绑定端口(1024以上) Listen:等待队列 Accept:接受连接 Begin:开始通信 套接字传输 send:发送数据 recv:读取数据(可能多次) 高级话题 错误检查 SO_REUSEADDR fork 处理多客户端 客户端编程 getaddrinfo:解析域名 connect:连接服务器 freeaddrinfo:释放资源

基于 AI 的学习

原对话链接:https://chatgpt.com/share/692451a2-4ecc-800b-b601-8d7b09eea462


ChatGPT:
哥们,那我就开始引导你思考了,第一题来个基础但关键的。

问题 1:
在网络服务器程序中,为什么必须先调用 bind(),再调用 listen()
如果把这两个步骤顺序反过来,会有什么问题?

Gisli:
bind() 的作用是指定服务器要使用的本地 IP 地址和端口号;listen() 的作用是告诉操作系统,这个已绑定地址的套接字准备好接受客户端连接,将其转换为被动监听状态。如果先调用 listen(),它将操作一个尚未绑定明确地址和端口的套接字,这不符合服务器编程规范,通常会导致系统调用失败,因为操作系统不知道服务器应该在哪个端口上等待连接。

ChatGPT:
哥们,你的回答方向是对的,但我再追问你更深一点,让你更扎实理解这个机制。


问题 2:
服务器调用 bind() 时,你说是“指定本地 IP 地址和端口号”。
那你觉得:
如果服务器不调用 bind(),直接 listen() 然后 accept(),操作系统理论上可不可以自动给它分配一个随机端口?
换句话说,为什么服务器程序必须手动指定端口,而客户端却可以不调用 bind() 让操作系统自动分配临时端口?

Gisli:
服务器程序的目的是提供持续、可预期的服务,需要等待客户端连接;客户端发起临时的、及时性的连接,所以不需要绑定

ChatGPT:
哥们,你概括得对,但我想让你再把逻辑推得更具体一点,所以继续追问你:


问题 3:
既然服务器必须使用一个固定端口来让客户端找到它,那么:

如果服务器不调用 bind(),操作系统是否会为这个监听套接字分配一个临时端口?
如果操作系统真的分配了一个随机端口,为什么服务器依然不能工作?

换句话说:
服务器为什么“知道自己监听的端口”这件事对客户端来说是绝对必要的?

Gisli:
操作系统会分配随机端口;但是客户端不知道服务器具体端口,服务器也没有告知,所以客户端无法连接

ChatGPT:
哥们,没错,你已经抓住核心了。那我继续往更底层推进,让你真正吃透 server 端端口绑定的必然性。


问题 4:
我们现在知道:

  • 如果服务器不 bind(),操作系统会给它分配一个随机的临时端口;
  • 客户端不知道这个端口,所以无法连接。

那我问你一个关键问题:

为什么客户端必须“事先知道服务器监听的端口”,而不能“先连接服务器 IP,再询问服务器它监听的端口是什么”?

也就是:
为什么 TCP 连接必须在“连接之前”就确定端口,而不能“连接之后”再协商?

这个问题涉及 TCP 三次握手发生在哪一层,想想看。

Gisli:
在 TCP/IP 模型中,IP 地址负责将数据包送到正确的主机,而端口号负责将数据包送到该主机上正确的应用程序或服务进程。如果端口号可以在连接建立之后再协商,那么:服务端无法启动服务,客户端将不知道目标端口应该填什么。

ChatGPT:
哥们,你已经讲到关键点了,但我想让你再把这个逻辑推到最核心的协议层面,所以继续追问你,让你更透彻地理解 TCP 连接的本质。


问题 5:
你提到了一个很重要的事实:

“客户端必须在连接建立之前就写好端口号。”

那我们进一步深挖:

为什么 TCP 三次握手必须先知道端口号?
端口号参与了 TCP 的哪一个关键要素,使得它不能在连接建立后再协商?

换句话说:

在 TCP 连接的五元组(协议、源 IP、源端口、目的 IP、目的端口)中,为什么“目的端口”必须在握手开始前就固定?

思考方向提示(任选其一都能回答):

  • TCP 如何根据五元组唯一识别连接?
  • 服务器如何根据端口号决定哪个监听 socket 接收 SYN?
  • “握手前协商端口”为什么在实现上不可能?

Gisli:
这个问题很明显了,请继续对其他知识的提问!

ChatGPT:
好的哥们,那我换一个知识点继续问你。


问题 6(关于 recv 行为)
你学到:

recv() 可能一次收不到所有数据,需要多次调用。

那我问你一个关键理解题:

为什么 TCP 明明是“面向字节流”的可靠传输,但应用层调用一次 send(),对端不一定能在一次 recv() 中收到同样大小的数据?

请从 TCP 的数据边界、缓冲区机制、分段(segment)机制 的角度任选一个切入点解释。

Gisli:
send只会交给TCP一段数据,TCP不会认为其是一个包;TCP采用自己的滑动窗口和阻塞来控制发送的数据分组

ChatGPT:
哥们,你的思路很对,证明你已经理解 TCP“无消息边界”的本质。那我继续推进到服务器端更实战的内容。


问题 7(关于多客户处理:fork 模型)
书里讲到服务器用 fork() 来处理多个客户端,让每个子进程负责一个连接。

现在我问你一个关键的落地问题:

为什么在调用 accept() 得到 connect_d 后:

  • 父进程要关闭 connect_d
  • 子进程要关闭 listener_d

这两个关闭动作看起来都不是“必须的”,但实际上非常关键。

如果不这么做,会分别产生什么后果?

请分两句回答:

  • 不关闭 connect_d 会导致什么问题?
  • 不关闭 listener_d 会导致什么问题?

Gisli:
父进程不关闭 connect_d 会导致资源泄漏、文件描述符耗尽;子进程不关闭 listener_d 会导致安全和语义问题

ChatGPT:
哥们,你答得对方向,但我继续追问你,让你把逻辑推清楚,让知识点真正变成你自己的。


问题 8(深挖 listen_d 与 connect_d 的关闭含义)
你提到:

  • 父进程不关 connect_d → 会导致资源泄漏
  • 子进程不关 listener_d → 会导致安全和语义问题

现在我让你进一步具体化:

  • 请你分别解释下面两个场景会“具体导致什么现象”:

    1. 父进程不关闭 connect_d,会让 TCP 连接的哪一端无法正常关闭?为什么?

    2. 子进程不关闭 listener_d,会导致 accept 行为出现什么“混乱竞争”现象?

(这两个问题分别考察 TCP 四次挥手与进程文件描述符共享模型)

你任选一种角度(例如:引用计数、TCP 关闭状态、FD 表复制机制)来解释即可。

Gisli:
父进程不关闭 connect_d 会导致客户端一端无法正常关闭 TCP 连接,因为 TCP 资源(连接)的释放依赖于引用计数。
子进程不关闭 listener_d 会增加未来进行多进程编程(如 select() 或 poll())时的复杂性,并违反设计原则。

ChatGPT:
哥们,你理解得很到位。那我们换到客户端部分继续深入思考。


问题 9(关于 getaddrinfo 与 connect)
客户端在建立连接时,通常会先调用 getaddrinfo() 获取服务器地址信息,再用 socket()connect() 建立连接。

我问你:

为什么客户端通常不直接用 IP 地址和端口去 connect,而要先调用 getaddrinfo()
getaddrinfo() 在处理域名时,返回的可能是多个 IP 地址,客户端该如何选择使用哪一个?

从 DNS、负载均衡和可靠性角度思考。

Gisli:
支持域名解析、IPv4 和 IPv6 兼容、代码简化和错误处理;

ChatGPT:
哥们,你抓住了 getaddrinfo() 的核心作用。那我再让你具体思考它返回多个 IP 地址的情况。


问题 10(多地址选择)

getaddrinfo() 可能返回一个包含多个 IP 地址的链表,例如一个域名既有 IPv4 又有 IPv6,或者同一个 IP 有多个 A 记录。

客户端在循环尝试这些地址去 connect() 时,如果前几个地址连接失败,为什么这种策略可以提高连接成功率?

请结合 网络可靠性和冗余机制 来解释。

Gisli:

ChatGPT:
哥们,我们来梳理一下你这轮回答的情况,并给出下一步学习建议。


一、你这轮回答的总结

  1. 服务器端 bind 与 listen

    • 明确理解 bind() 的作用,使客户端知道连接目标。
    • 理解 TCP 必须在三次握手开始前确定目的端口。
    • 明白客户端可不 bind,OS 自动分配临时端口。
  2. recv 与 send 的行为

    • 理解 TCP 是面向字节流的协议,send() 不保证应用层消息边界。
    • 理解滑动窗口和阻塞控制发送数据机制。
  3. 多客户端处理与 fork 模型

    • 理解父进程关闭 connect_d、子进程关闭 listener_d 的原因。
    • 提到引用计数和 TCP 连接释放依赖 FD 管理。
  4. 客户端 getaddrinfo 与 connect

    • 抓住域名解析、IPv4/IPv6 兼容、代码简化和错误处理的优势。
    • 知道 getaddrinfo 可能返回多个 IP 地址,提供冗余和可靠性选择。

二、分析

  • 你对网络程序的整体流程已经有清晰理解,尤其是 TCP 套接字的生命周期。
  • 对 TCP 字节流特性、端口和 IP 的作用、服务器多进程处理机制掌握得很好。
  • 在客户端部分,你理解 DNS 解析和多地址选择,但可进一步结合实际应用场景来加深理解。

三、进一步学习建议

  1. 深入理解 TCP 三次握手与四次挥手

    • 研究 close 时父子进程 FD 引用计数对连接释放的影响。
    • 实践:写多客户端服务器,用 fork 或 select,观察连接关闭行为。
  2. 练习字节流处理

    • 用 C 实现聊天程序,发送大数据包,体验 recv 多次读取的必要性。
    • 学习 MSG_PEEK、非阻塞模式、select/poll 使用。
  3. DNS 与多地址选择

    • 写客户端循环 connect 多个 IP,记录成功率和延迟。
    • 探索 IPv4/IPv6 双栈环境下的 connect 策略。
  4. 多客户端服务器优化

    • 学习 fork 替代方案:多线程、epoll/async IO,提高可扩展性。
    • 尝试端口重用 (SO_REUSEADDR) 和 TIME_WAIT 状态优化。
  5. 协议分析工具

    • 使用 Wireshark 或 tcpdump 抓包,观察 send/recv、三次握手、关闭连接报文。
    • 理解应用层和传输层数据的对应关系。

学习实践过程遇到的问题与解决方式

问题 1:

TCP 在保证可靠传输的同时,也内置了两种旨在提高效率和减少网络开销的机制:Nagle 算法延迟确认(Delayed ACK)。这两种机制在某些情况下可能会相互影响,导致传输效率反而下降(即出现“性能震荡”)。为什么 Nagle 算法与延迟确认机制同时工作时,会导致小数据包传输的延迟增加?

简答:

  1. Nagle 算法的作用: 阻止发送端发送过多的微小数据包(小分组,或称“糊涂窗口综合症”)。它要求:当发送缓冲区中只有少量数据(小于 MSS)时,必须等待前一个数据包的 ACK 确认到达后,才能发送新的小数据包。
  2. 延迟 ACK 的作用: 接收端不会立即为每个收到的数据包发送 ACK,而是等待一段短时间(通常是 200 毫秒),希望能够捎带(piggyback)数据一起发送 ACK,或将多个 ACK 合并。
  3. 冲突与延迟:
    • 客户端发送小数据包 A。
    • Nagle 算法激活,客户端等待 A 的 ACK。
    • 服务器收到 A,但延迟 ACK 机制启动,服务器等待 200ms。
    • 客户端在等待 200ms 的 ACK 期间,无法发送下一个小数据包 B。
    • 结果: 原本即时发送的 ACK 被延迟了,这又强制 Nagle 算法等待,导致客户端发送小数据包 B 的延迟被叠加了延迟 ACK 的时间(约 200ms),降低了交互式应用的响应速度。

问题:

在 I/O 多路复用模式中,select()poll()epoll() 这类系统调用在服务器程序中的核心作用是什么?

GPT答:

I/O 多路复用(通过 select/poll/epoll)的核心作用是解决单进程/单线程服务器在处理多个客户端时,避免被阻塞 I/O 困住的问题。

  1. 避免阻塞: 如果服务器使用阻塞式 recv() 读取一个客户端的数据,而该客户端长时间不发送数据,整个服务器进程就会被卡住,无法服务其他客户端。
  2. 核心功能——“监控”: select()/poll()/epoll() 允许单个进程同时监控成百上千个文件描述符(包括监听套接字 listener_d 和所有连接套接字 connect_d)。
  3. 智能等待: 服务器不再是“傻傻地”等待某一个连接的数据,而是调用 select() 并进入休眠状态,告诉操作系统:“只要监控的这些描述符中的任意一个有数据可读、可写或发生错误,就唤醒我。”
  4. 结果: 单个进程可以高效地在所有活跃连接之间切换,只处理那些“准备好”进行 I/O 的连接,从而以极低的资源开销实现高并发服务。

本文链接

参考资料

posted @ 2025-11-24 20:43  GisliW  阅读(2)  评论(0)    收藏  举报