From 5625eea472e114621f40e32d3de8ddef16b350be Mon Sep 17 00:00:00 2001 From: riba2534 Date: Sun, 28 Jun 2026 12:47:46 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=85=A8=E9=9D=A2=E6=A0=A1=E5=AF=B9?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E7=AB=A0=E8=8A=82=E6=96=87=E6=A1=A3=E4=B8=8E?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过多智能体工作流对 19 章笔记(README.md)与 96 个 .c 示例代码做深度 审查与对抗性验证,修复 317 处确认问题,涵盖: 技术正确性: - 修复缓冲区溢出:echo_mpserv.c / echo_storeserv.c 等的 read(buf, BUFSIZ) 改为 BUF_SIZE(buf 仅 30 字节,BUFSIZ 远大于此) - 修复 open() 缺少 mode 参数:low_open.c / fd_seri.c / desto.c 等 O_CREAT 调用补 0644(原导致 low_read 链路失败) - 修复 feof 循环 off-by-one:news_sender.c / echo_stdserv.c 改用 fgets 返回值判断 - 修复线程竞态:chat_server.c / webserv_linux.c 的 &clnt_sock 栈地址 传子线程改为 malloc 分配 + free - 修复索引混淆:char_EPLTserv.c 错用 clnt_sock 查找改为 ep_events[i].data.fd - 修复格式化符:thread4.c 的 sizeof 用 %d 改为 %zu - 修正习题答案:ch01 fd 序号、ch13 MSG_OOB 加粗项、ch09 Nagle 等 文档规范: - 统一术语:IPv4/IPv6、接收(receive)/连接(connection) - 修正错别字:occured→occurred、cooffee→coffee、Usgae→Usage、 eerror→error、proess→process 等 - 修复病句、补全习题答案解释 - GitHub 绝对 URL 改为相对路径,统一项目引用规范 - 同步根 README.md(前言 + 19 章合并) 另:重命名 ch10/remove_zomebie.c → remove_zombie.c(修正拼写) 所有 .c 文件经 gcc 编译验证通过(ch17 epoll 文件因 macOS 无 sys/epoll.h 跳过,已人工复核)。 --- README.md | 607 +++++++++++---------- ch01/README.md | 30 +- ch01/fd_seri.c | 2 +- ch01/low_open.c | 2 +- ch01/low_read.c | 4 +- ch02/README.md | 22 +- ch03/README.md | 36 +- ch03/inet_addr.c | 4 +- ch04/README.md | 28 +- ch04/echo_client.c | 2 + ch05/My_op_client.c | 1 + ch05/My_op_server.c | 7 +- ch05/README.md | 16 +- ch05/homework/tcp_client_kehou6.c | 4 +- ch05/homework/tcp_server_kehou6.c | 3 +- ch06/README.md | 20 +- ch06/bound_host1.c | 2 + ch06/bound_host2.c | 1 - ch06/homework/uchar_client.c | 4 +- ch06/uecho_client.c | 2 +- ch06/uecho_con_client.c | 4 +- ch06/uecho_server.c | 2 +- ch07/README.md | 24 +- ch07/file_client.c | 2 +- ch08/README.md | 11 +- ch08/gethostbyname.c | 4 +- ch09/README.md | 44 +- ch09/set_buf.c | 1 - ch10/README.md | 40 +- ch10/echo_mpclient.c | 6 +- ch10/echo_mpserv.c | 4 +- ch10/{remove_zomebie.c => remove_zombie.c} | 0 ch10/sigaction.c | 3 +- ch10/signal.c | 2 +- ch10/test_server.c | 4 +- ch10/wait.c | 2 +- ch10/waitpid.c | 2 +- ch10/zombie.c | 2 +- ch11/README.md | 20 +- ch11/echo_storeserv.c | 6 +- ch11/homework/kehou4.c | 2 +- ch12/README.md | 15 +- ch12/echo_selectserv.c | 10 +- ch12/select.c | 5 + ch13/README.md | 40 +- ch13/oob_recv.c | 2 + ch13/peek_recv.c | 2 + ch14/README.md | 39 +- ch14/news_receiver.c | 4 +- ch14/news_receiver_brd.c | 2 +- ch14/news_sender.c | 3 +- ch14/news_sender_brd.c | 5 +- ch15/README.md | 20 +- ch15/desto.c | 2 +- ch15/echo_client.c | 3 +- ch15/echo_stdserv.c | 6 +- ch15/stdcpy.c | 6 +- ch15/todes.c | 2 +- ch16/README.md | 32 +- ch17/README.md | 36 +- ch17/echo_EDGEserv.c | 2 +- ch17/echo_EPETserv.c | 4 +- ch17/echo_EPLTserv.c | 2 +- ch17/echo_epollserv.c | 2 +- ch17/homework/char_EPETserv.c | 11 +- ch17/homework/char_EPLTserv.c | 9 +- ch18/README.md | 26 +- ch18/chat_clnt.c | 3 +- ch18/chat_server.c | 13 +- ch18/homework/echo_client.c | 1 + ch18/homework/echo_threadserv.c | 3 + ch18/thread1.c | 1 - ch18/thread2.c | 3 +- ch18/thread4.c | 2 +- ch24/README.md | 16 +- ch24/webserv_linux.c | 22 +- 76 files changed, 707 insertions(+), 629 deletions(-) rename ch10/{remove_zomebie.c => remove_zombie.c} (100%) diff --git a/README.md b/README.md index ee6dfa4..1b0d525 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,15 @@ 笔记的 PDF 版本可以在本项目 [releases](https://github.com/riba2534/TCP-IP-NetworkNote/releases) 中找到及下载。 + + ## 第一章:理解网络编程和套接字 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到,直接点链接可能进不去。 ### 1.1 理解网络编程和套接字 -#### 1.1.1构建打电话套接字 +#### 1.1.1 构建打电话套接字 以电话机打电话的方式来理解套接字。 @@ -48,7 +50,7 @@ int socket(int domain, int type, int protocol); ```c #include -int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); +int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); //成功时返回0,失败时返回-1 ``` @@ -56,7 +58,7 @@ int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); **调用 listen 函数(连接电话线)时进行的对话**: -> 问:已架设完电话机后是否只需链接电话线? +> 问:已架设完电话机后是否只需连接电话线? > > 答:对,只需要连接就能接听电话。 @@ -78,11 +80,11 @@ int listen(int sockfd, int backlog); ```c #include -int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //成功时返回文件描述符,失败时返回-1 ``` -网络编程中和接受连接请求的套接字创建过程可整理如下: +网络编程中接受连接请求的套接字创建过程可整理如下: 1. 第一步:调用 socket 函数创建套接字。 2. 第二步:调用 bind 函数分配IP地址和端口号。 @@ -95,7 +97,7 @@ int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); 服务器端(server)是能够受理连接请求的程序。下面构建服务端以验证之前提到的函数调用过程,该服务器端收到连接请求后向请求者返回`Hello World!`答复。除各种函数的调用顺序外,我们还未涉及任何实际编程。因此,阅读代码时请重点关注套接字相关的函数调用过程,不必理解全过程。 -服务器端代码请参见:[hello_server.c](ch01/hello_server.c) +服务器端代码请参见:[hello_server.c](hello_server.c) **客户端**: @@ -104,7 +106,7 @@ int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); 1. 调用 socket 函数 和 connect 函数 2. 与服务端共同运行以收发字符串数据 -客户端代码请参见:[hello_client.c](ch01/hello_client.c) +客户端代码请参见:[hello_client.c](hello_client.c) **编译**: @@ -122,11 +124,11 @@ gcc hello_client.c -o hclient ./hclient 127.0.0.1 9190 ``` -运行的时候,首先在 9190 端口启动服务,然后 hserver 就会一直等待客户端进行连接,当客户端连接位于本地的 IP 为 127.0.0.1 的地址的 9190 端口时,客户端就会收到服务端的回应,输出`Hello World!` +运行的时候,首先在 9190 端口启动服务,然后 hserver 就会一直等待客户端进行连接,当客户端连接到本地地址 127.0.0.1 的 9190 端口时,客户端就会收到服务端的回应,输出`Hello World!` ### 1.2 基于 Linux 的文件操作 -讨论套接字的过程中突然谈及文件也许有些奇怪。但是对于 Linux 而言,socket 操作与文件操作没有区别,因而有必要详细了解文件。在 Linux 世界里,socket 也被认为是文件的一种,因此在网络数据传输过程中自然可以使用 I/O 的相关函数。Windows 与 Linux 不同,是要区分 socket 和文件的。因此在 Windows 中需要调用特殊的数据传输相关函数。 +讨论套接字的过程中突然谈及文件也许有些奇怪。但是对于 Linux 而言,socket 操作与文件操作没有区别,因而有必要详细了解文件。在 Linux 世界里,socket 也被认为是文件的一种,因此在网络数据传输过程中自然可以使用 I/O 的相关函数。Windows 与 Linux 不同,是要区分套接字和文件的。因此在 Windows 中需要调用特殊的数据传输相关函数。 #### 1.2.1 底层访问和文件描述符 @@ -142,7 +144,7 @@ gcc hello_client.c -o hclient 文件描述符也被称为「文件句柄」,但是「句柄」主要是 Windows 中的术语。因此,在本书中如果涉及 Windows 平台将使用「句柄」,如果是 Linux 将使用「描述符」。 -#### 1.2.2 打开文件: +#### 1.2.2 打开文件 ```c #include @@ -197,7 +199,7 @@ nbytes : 要传输数据的字节数 创建新文件并保存数据: -代码见:[low_open.c](ch01/low_open.c) +代码见:[low_open.c](low_open.c) 编译运行: @@ -225,7 +227,7 @@ nbytes : 要接收数据的最大字节数 下面示例通过 read() 函数读取 data.txt 中保存的数据。 -代码见:[low_read.c](ch01/low_read.c) +代码见:[low_read.c](low_read.c) 编译运行: @@ -245,9 +247,9 @@ file data: Let's go! #### 1.2.6 文件描述符与套接字 -下面将同时创建文件和套接字,并用整数型态比较返回的文件描述符的值. +下面将同时创建文件和套接字,并用整数类型比较返回的文件描述符的值。 -代码见:[fd_seri.c](ch01/fd_seri.c) +代码见:[fd_seri.c](fd_seri.c) **编译运行**: @@ -260,8 +262,8 @@ gcc fd_seri.c -o fds ``` file descriptor 1: 3 -file descriptor 2: 15 -file descriptor 3: 16 +file descriptor 2: 4 +file descriptor 3: 5 ``` ### 1.3 基于 Windows 平台的实现 @@ -278,17 +280,17 @@ file descriptor 3: 16 1. 套接字在网络编程中的作用是什么?为何称它为套接字? - > 答:操作系统会提供「套接字」(socket)的部件,套接字是网络数据传输用的软件设备。因此,「网络编程」也叫「套接字编程」。「套接字」就是用来连接网络的工具。 + > 答:操作系统会提供「套接字」(socket)的部件,套接字是网络数据传输用的软件设备,因此「网络编程」也叫「套接字编程」。「套接字」是 socket 的中文译名,socket 原意为「插座、插口」,在网络编程中引申为数据通信的连接端点,即用来连接网络的工具。 2. 在服务器端创建套接字以后,会依次调用 listen 函数和 accept 函数。请比较二者作用。 - > 答:调用 listen 函数将套接字转换成可受连接状态(监听),调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止。 + > 答:调用 listen 函数将套接字转换成可接受连接状态(监听),调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止。 3. Linux 中,对套接字数据进行 I/O 时可以直接使用文件 I/O 相关函数;而在 Windows 中则不可以。原因为何? - > 答:在 Linux 中,套接字(socket)被视为文件的一种,遵循「一切皆文件」的设计哲学。因此可以使用标准的文件 I/O 函数(如 `read`、`write`、`close`)对套接字进行操作。而在 Windows 中,套接字和文件是区分开的,套接字操作需要使用专门的 Winsock 函数(如 `send`、`recv`、`closesocket`),不能使用文件 I/O 函数(如 `ReadFile`、`WriteFile`)直接操作套接字。 + > 答:在 Linux 中,套接字(socket)被视为文件的一种,遵循「一切皆文件」的设计哲学。因此可以使用标准的文件 I/O 函数(如 `read`、`write`、`close`)对套接字进行操作。而在 Windows 中,套接字和文件是区分开的,套接字操作需要使用专门的 Winsock 函数(如 `send`、`recv`、`closesocket`),通常不使用文件 I/O 函数(如 `ReadFile`、`WriteFile`)直接操作套接字。 -4. 创建套接字后一般会给他分配地址,为什么?为了完成地址分配需要调用哪个函数? +4. 创建套接字后一般会给它分配地址,为什么?为了完成地址分配需要调用哪个函数? > 答:套接字被创建之后,只有为其分配了IP地址和端口号后,客户端才能够通过IP地址及端口号与服务器端建立连接,需要调用 bind 函数来完成地址分配。 @@ -400,9 +402,10 @@ file descriptor 3: 16 > gcc file_copy_stdio.c -o fcopy_stdio > ./fcopy_stdio source.txt dest.txt > ``` + ## 第二章 套接字类型与协议设置 -本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到,直接点连接可能进不去。 +本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到,直接点链接可能进不去。 本章仅需了解创建套接字时调用的 socket 函数。 @@ -436,7 +439,7 @@ protocol: 计算机间通信中使用的协议信息 | PF_PACKET | 底层套接字的协议族 | | PF_IPX | IPX Novell 协议族 | -本书着重讲 PF_INET 对应的 IPv4 互联网协议族。其他协议并不常用,或并未普及。**另外,套接字中采用的最终的协议信息是通过 socket 函数的第三个参数传递的。在指定的协议族范围内通过第一个参数决定第三个参数。** +本书着重讲 PF_INET 对应的 IPv4 互联网协议族。其他协议并不常用,或并未普及。**另外,套接字中采用的最终的协议信息是通过 socket 函数的第三个参数传递的。在指定的协议族(第一个参数)和套接字类型(第二个参数)范围内,即可确定第三个参数(具体协议)。** #### 2.1.3 套接字类型(Type) @@ -454,9 +457,9 @@ protocol: 计算机间通信中使用的协议信息 这种情形适用于之前说过的 write 和 read 函数 -> 传输数据的计算机通过调用3次 write 函数传递了 100 字节的数据,但是接受数据的计算机仅仅通过调用 1 次 read 函数调用就接受了全部 100 个字节。 +> 传输数据的计算机通过调用3次 write 函数传递了 100 字节的数据,但是接收数据的计算机仅仅通过调用 1 次 read 函数调用就接收了全部 100 个字节。 -收发数据的套接字内部有缓冲(buffer),简言之就是字节数组。只要不超过数组容量,那么数据填满缓冲后过 1 次 read 函数的调用就可以读取全部,也有可能调用多次来完成读取。 +收发数据的套接字内部有缓冲(buffer),简言之就是字节数组。只要不超过数组容量,那么数据填满缓冲后通过 1 次 read 函数的调用就可以读取全部,也有可能调用多次来完成读取。 **套接字缓冲已满是否意味着数据丢失?** @@ -464,7 +467,7 @@ protocol: 计算机间通信中使用的协议信息 套接字联机必须一一对应。面向连接的套接字可总结为: -**可靠地、按序传递的、基于字节的面向连接的数据传输方式的套接字。** +**可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字。** #### 2.1.5 面向消息的套接字(SOCK_DGRAM) @@ -475,13 +478,13 @@ protocol: 计算机间通信中使用的协议信息 - 传输的数据有边界 - 限制每次传输数据的大小 -面向消息的套接字比面向连接的套接字更具有传输速度,但可能丢失。特点可总结为: +面向消息的套接字比面向连接的套接字传输速度更快,但数据可能丢失。特点可总结为: -**不可靠的、不按序传递的、以数据的高速传输为目的套接字。** +**不可靠的、不按序传递的、以数据的高速传输为目的的套接字。** #### 2.1.6 协议的最终选择 -socket 函数的第三个参数决定最终采用的协议。前面已经通过前两个参数传递了协议族信息和套接字数据传输方式,这些信息还不够吗?为什么要传输第三个参数呢? +socket 函数的第三个参数决定最终采用的协议。前面已经通过前两个参数传递了协议族信息和套接字数据传输方式,这些信息还不够吗?为什么要传递第三个参数呢? > 可以应对同一协议族中存在的多个数据传输方式相同的协议,所以数据传输方式相同,但是协议不同,需要用第三个参数指定具体的协议信息。 @@ -501,8 +504,8 @@ int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); 需要对第一章的代码做出修改,修改好的代码如下: -- [tcp_client.c](ch02/tcp_client.c) -- [tcp_server.c](ch02/tcp_server.c) +- [tcp_client.c](tcp_client.c) +- [tcp_server.c](tcp_server.c) 编译: @@ -633,7 +636,7 @@ gcc tcp_server_win.c -o hserver -lws2_32 > 答:①传输过程中数据不会消失②按序传输数据③传输的数据不存在数据边界(Boundary) -3. 下面那些是面向消息的套接字的特性? +3. 下面哪些是面向消息的套接字的特性? - **传输数据可能丢失** - 没有数据边界(Boundary) @@ -653,6 +656,7 @@ gcc tcp_server_win.c -o hserver -lws2_32 > 1. **数据可能分多次到达**:发送方调用一次 write 发送 100 字节,接收方可能需要多次 read 才能读完,或者一次 read 就能读完多次 write 的数据。 > 2. **需要定义应用层协议**:由于没有边界,必须在应用层定义数据边界(如固定长度、分隔符、长度前缀等方式),否则无法正确解析数据。 > 3. **缓冲区管理**:虽然 TCP 内部有流量控制机制保证不丢数据,但应用层仍应及时读取数据,避免接收缓冲区占用过多内存。 + ## 第 3 章 地址族与数据序列 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -661,7 +665,7 @@ gcc tcp_server_win.c -o hserver -lws2_32 ### 3.1 分配给套接字的 IP 地址与端口号 -IP 是 Internet Protocol(网络协议)的简写,是为收发网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为了区分程序中创建的套接字而分配给套接字的序号。 +IP 是 Internet Protocol(网际协议)的简写,是为收发网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为了区分程序中创建的套接字而分配给套接字的序号。 #### 3.1.1 网络地址(Internet Address) @@ -702,7 +706,7 @@ IPv4 标准的 4 字节 IP 地址分为网络地址和主机(指计算机) IP 地址用于区分计算机,只要有 IP 地址就能向目标主机传输数据,但是只有这些还不够,我们需要把信息传输给具体的应用程序。 -所以计算机一般有 NIC(网络接口卡)数据传输设备。通过 NIC 接收的数据内有端口号,操作系统参考端口号把信息传给相应的应用程序。 +所以计算机一般配有 NIC(网络接口卡)这一数据传输设备。通过 NIC 接收的数据内有端口号,操作系统参考端口号把信息传给相应的应用程序。 端口号由 16 位构成,可分配的端口号范围是 0~65535。但是 0~1023 是知名端口,一般分配给特定的应用程序,所以应当分配给此范围之外的值。 @@ -758,7 +762,7 @@ struct in_addr - 成员 sin_family -每种协议适用的地址族不同,比如,IPv4 使用 4 字节的地址族,IPv6 使用 16 字节的地址族。 +每种协议适用的地址族不同,比如,IPv4 使用 4 字节的地址,IPv6 使用 16 字节的地址。 > 地址族 @@ -780,7 +784,7 @@ AF_LOCAL 只是为了说明具有多种地址族而添加的。 - 成员 sin_zero - 无特殊含义。只是为结构体 sockaddr_in 结构体变量地址值将以如下方式传递给 bind 函数。 + 无特殊含义。它的存在是为了让 sockaddr_in 结构体的大小与 sockaddr 结构体保持一致,以便 sockaddr_in 结构体变量的地址值能以如下方式传递给 bind 函数。 在之前的代码中 @@ -799,7 +803,7 @@ AF_LOCAL 只是为了说明具有多种地址族而添加的。 }; ``` - 此结构体 sa_data 保存的地址信息中需要包含 IP 地址和端口号,剩余部分应该填充 0,但是这样对于包含地址的信息非常麻烦,所以出现了 sockaddr_in 结构体,然后强制转换成 sockaddr 类型,则生成符合 bind 条件的参数。 + 此结构体 sa_data 保存的地址信息中需要包含 IP 地址和端口号,剩余部分应该填充 0,但是这样手动填充地址信息非常麻烦,所以出现了 sockaddr_in 结构体,然后强制转换成 sockaddr 类型,则生成符合 bind 条件的参数。 ### 3.3 网络字节序与地址变换 @@ -817,7 +821,7 @@ AF_LOCAL 只是为了说明具有多种地址族而添加的。 00000001 00000000 00000000 00000000 ``` -两种一种是顺序保存,一种是倒序保存。 +一种是顺序保存,一种是倒序保存。 #### 3.3.1 字节序(Order)与网络字节序 @@ -833,7 +837,7 @@ CPU 保存数据的方式有两种,这意味着 CPU 解析数据的方式也 ![zijiexu.png](images/5c3aca956c8e9.png) -因为这种原因,所以在通过网络传输数据时必须约定统一的方式,这种约定被称为网络字节序(Network Byte Order),非常简单,统一为大端序。即,先把数据数组转化成大端序格式再进行网络传输。 +因为这种原因,所以在通过网络传输数据时必须约定统一的方式,这种约定被称为网络字节序(Network Byte Order),非常简单,统一为大端序。即,先把数据转换成大端序格式再进行网络传输。 #### 3.3.2 字节序转换 @@ -855,7 +859,7 @@ unsigned long ntohl(unsigned long); 下面的代码是示例,说明以上函数调用过程: -[endian_conv.c](ch03/endian_conv.c) +[endian_conv.c](endian_conv.c) ```c #include @@ -895,7 +899,7 @@ Host ordered address: 0x12345678 Network ordered address: 0x78563412 ``` -这是在小端 CPU 的运行结果。大部分人会得到相同的结果,因为 Intel 和 AMD 的 CPU 都是小端序为标准。 +这是在小端 CPU 上的运行结果。大部分人会得到相同的结果,因为 Intel 和 AMD 的 CPU 都以小端序为标准。 ### 3.4 网络地址的初始化与分配 @@ -911,7 +915,7 @@ in_addr_t inet_addr(const char *string); 具体示例: -[inet_addr.c](ch03/inet_addr.c) +[inet_addr.c](inet_addr.c) ```c #include @@ -923,13 +927,13 @@ int main(int argc, char *argv[]) unsigned long conv_addr = inet_addr(addr1); if (conv_addr == INADDR_NONE) - printf("Error occured! \n"); + printf("Error occurred! \n"); else printf("Network ordered integer addr: %#lx \n", conv_addr); conv_addr = inet_addr(addr2); if (conv_addr == INADDR_NONE) - printf("Error occured! \n"); + printf("Error occurred! \n"); else printf("Network ordered integer addr: %#lx \n", conv_addr); return 0; @@ -947,7 +951,7 @@ gcc inet_addr.c -o addr ``` Network ordered integer addr: 0x4030201 -Error occured! +Error occurred! ``` 1 个字节能表示的最大整数是 255,所以代码中 addr2 是错误的 IP 地址。从运行结果看,inet_addr 不仅可以转换地址,还可以检测有效性。 @@ -966,7 +970,7 @@ addr: 保存转换结果的 in_addr 结构体变量的地址值 函数调用示例: -[inet_aton.c](ch03/inet_aton.c) +[inet_aton.c](inet_aton.c) ```c #include @@ -1021,7 +1025,7 @@ char *inet_ntoa(struct in_addr adr); 示例: -[inet_ntoa.c](ch03/inet_ntoa.c) +[inet_ntoa.c](inet_ntoa.c) ```c #include @@ -1069,12 +1073,12 @@ Dotted-Decimal notation3: 1.2.3.4 ```c struct sockaddr_in addr; -char *serv_ip = "211.217.168.13"; //声明IP地址族 +char *serv_ip = "211.217.168.13"; //声明IP地址字符串 char *serv_port = "9190"; //声明端口号字符串 memset(&addr, 0, sizeof(addr)); //结构体变量 addr 的所有成员初始化为0 -addr.sin_family = AF_INET; //制定地址族 +addr.sin_family = AF_INET; //指定地址族 addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化 -addr.sin_port = htons(atoi(serv_port)); //基于字符串的IP地址端口号初始化 +addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化 ``` ### 3.5 基于 Windows 的实现 @@ -1154,6 +1158,7 @@ addr.sin_port = htons(atoi(serv_port)); //基于字符串的IP地址端口号 10. **怎样表示回送地址?其含义是什么?如果向回送地址处传输数据将会发生什么情况?** 答:回送地址表示为 127.0.0.1(IPv4)或 ::1(IPv6),指的是计算机自身的 IP 地址,也称为本地环回地址。无论什么程序,一旦使用回送地址发送数据,数据不会真正发送到网络上,而是在本机协议栈内循环,协议软件立即将数据返回给本机的接收端,不进行任何网络传输。回送地址常用于本地服务测试和网络程序调试。 + ## 第 4 章 基于 TCP 的服务端/客户端(1) 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -1172,7 +1177,7 @@ TCP/IP 协议栈共分为 4 层,可以理解为数据收发分成了 4 个层 #### 4.1.2 链路层 -链路层是物理链接领域标准化的结果,也是最基本的领域,专门定义LAN、WAN、MAN等网络标准。若两台主机通过网络进行数据交换,则需要物理连接,链路层就负责这些标准。 +链路层是物理连接领域标准化的结果,也是最基本的领域,专门定义LAN、WAN、MAN等网络标准。若两台主机通过网络进行数据交换,则需要物理连接,链路层就负责这些标准。 #### 4.1.3 IP 层 @@ -1184,11 +1189,11 @@ IP 是面向消息的、不可靠的协议。每次传输数据时会帮我们 IP 层解决数据传输中的路径选择问题,只需照此路径传输数据即可。TCP 和 UDP 层以 IP 层提供的路径信息为基础完成实际的数据传输,故该层又称为传输层。UDP 比 TCP 简单,现在我们只解释 TCP 。 TCP 可以保证数据的可靠传输,但是它发送数据时以 IP 层为基础(这也是协议栈层次化的原因)。 -IP 层只关注一个数据包(数据传输基本单位)的传输过程。因此,即使传输多个数据包,每个数据包也是由 IP 层实际传输的,也就是说传输顺序及传输本身是不可靠的。若只利用IP层传输数据,则可能导致后传输的数据包B比先传输的数据包A提早到达。另外,传输的数据包A、B、C中可能只收到A和C,甚至收到的C可能已经损毁 。反之,若添加 TCP 协议则按照如下对话方式进行数据交换。 +IP 层只关注一个数据包(数据传输基本单位)的传输过程。因此,即使传输多个数据包,每个数据包也是由 IP 层实际传输的,也就是说传输顺序及传输本身是不可靠的。若只利用IP层传输数据,则可能导致后传输的数据包B比先传输的数据包A提早到达。另外,传输的数据包A、B、C中可能只收到A和C,甚至收到的C可能已经损毁。反之,若添加 TCP 协议则按照如下对话方式进行数据交换。 > 主机A:正确接收第二个数据包 > -> 主机B:恩,知道了 +> 主机B:嗯,知道了 > > 主机A:正确收到第三个数据包 > @@ -1204,7 +1209,7 @@ IP 层只关注一个数据包(数据传输基本单位)的传输过程。 ### 4.2 实现基于 TCP 的服务器/客户端 -#### 4.2.1 TCP 服务端的默认函数的调用程序 +#### 4.2.1 TCP 服务端的默认函数调用顺序 ![](images/5c3c2782a7810.png) @@ -1242,13 +1247,13 @@ accept 函数受理连接请求队列中待处理的客户端连接请求。函 #### 4.2.4 回顾 Hello World 服务端 -- 代码:[hello_server.c](ch04/hello_server.c) +- 代码:[hello_server.c](hello_server.c) 重新整理一下代码的思路 1. 服务端实现过程中首先要创建套接字,此时的套接字并非是真正的服务端套接字 2. 为了完成套接字地址的分配,初始化结构体变量并调用 bind 函数。 -3. 调用 listen 函数进入等待连接请求状态。连接请求状态队列的长度设置为5.此时的套接字才是服务端套接字。 +3. 调用 listen 函数进入等待连接请求状态。连接请求等待队列的长度设置为 5。此时的套接字才是服务端套接字。 4. 调用 accept 函数从队头取 1 个连接请求与客户端建立连接,并返回创建的套接字文件描述符。另外,调用 accept 函数时若等待队列为空,则 accept 函数不会返回,直到队列中出现新的客户端连接。 5. 调用 write 函数向客户端传送数据,调用 close 关闭连接 @@ -1274,13 +1279,13 @@ addrlen: 第二个结构体参数 servaddr 变量的字节长度 - 服务端接受连接请求 - 发生断网等异常状况而中断连接请求 -注意:**接受连接**不代表服务端调用 accept 函数,其实只是服务器端把连接请求信息记录到等待队列。因此 connect 函数返回后并不应该立即进行数据交换。 +注意:**接受连接**不代表服务端调用了 accept 函数,其实只是服务器端把连接请求信息记录到等待队列。connect 函数返回时 TCP 三次握手已完成、连接已建立,客户端可以发送数据(数据会暂存在内核缓冲区,待服务端调用 accept 后读取),但服务端应用层可能尚未调用 accept 处理该连接。 -客户端在调用 connect 函数时自动分配主机的 IP,随机分配端口。无需调用显式的 bind 函数进行分配。 +客户端在调用 connect 函数时自动分配主机的 IP,随机分配端口。无需显式调用 bind 函数进行分配。 #### 4.2.6 回顾 Hello World 客户端 -- 代码:[hello_client.c](ch04/hello_client.c) +- 代码:[hello_client.c](hello_client.c) 重新理解这个程序: @@ -1288,7 +1293,7 @@ addrlen: 第二个结构体参数 servaddr 变量的字节长度 2. 结构体变量 serv_addr 中初始化IP和端口信息。初始化值为目标服务器端套接字的IP和端口信息。 3. 调用 connect 函数向服务端发起连接请求 4. 完成连接后,接收服务端传输的数据 -5. 接收数据后调用 close 函数关闭套接字,结束与服务器端的连接。(对套接字调用close函数,对应于向建立连接的对应套接字发送EOF。即,如果客户端的套接字调用了close函数,服务端read时候会返回0。) +5. 接收数据后调用 close 函数关闭套接字,结束与服务器端的连接。(对套接字调用 close 函数,相当于向建立连接的对端套接字发送 EOF。即,如果客户端的套接字调用了 close 函数,服务端 read 时会返回 0。) #### 4.2.7 基于 TCP 的服务端/客户端函数调用关系 @@ -1323,8 +1328,8 @@ addrlen: 第二个结构体参数 servaddr 变量的字节长度 以下是服务端与客户端的代码: -- [echo_server.c](ch04/echo_server.c) -- [echo_client.c](ch04/echo_client.c) +- [echo_server.c](echo_server.c) +- [echo_client.c](echo_client.c) 编译: @@ -1390,7 +1395,7 @@ Windows 平台下的 Socket 编程(Winsock)与 Linux 平台基本类似, 2. **请说出 TCP/IP 协议栈中链路层和IP层的作用,并给出二者关系** - 答:链路层是物理链接领域标准化的结果,专门定义网络标准。若两台主机通过网络进行数据交换,则首先要做到的就是进行物理链接。IP层:为了在复杂的网络中传输数据,首先需要考虑路径的选择。关系:链路层负责进行一系列物理连接,而IP层负责选择正确可行的物理路径。 + 答:链路层是物理连接领域标准化的结果,专门定义网络标准。若两台主机通过网络进行数据交换,则首先要做到的就是进行物理连接。IP层:为了在复杂的网络中传输数据,首先需要考虑路径的选择。关系:链路层负责进行一系列物理连接,而IP层负责选择正确可行的物理路径。 3. **为何需要把 TCP/IP 协议栈分成 4 层(或7层)?开放式回答。** @@ -1398,7 +1403,7 @@ Windows 平台下的 Socket 编程(Winsock)与 Linux 平台基本类似, 4. **客户端调用 connect 函数向服务器端发送请求。服务器端调用哪个函数后,客户端可以调用 connect 函数?** - 答:服务端调用 listen 函数后,客户端可以调用 connect 函数。因为,服务端调用 listen 函数后,服务端套接字才有能力接受请求连接的信号。 + 答:服务端调用 listen 函数后,客户端可以调用 connect 函数。因为服务端调用 listen 函数后,服务端套接字才具备接收连接请求的能力。 5. **什么时候创建连接请求等待队列?它有何种作用?与 accept 有什么关系?** @@ -1407,6 +1412,7 @@ Windows 平台下的 Socket 编程(Winsock)与 Linux 平台基本类似, 6. **客户端中为何不需要调用 bind 函数分配地址?如果不调用 bind 函数,那何时、如何向套接字分配IP地址和端口号?** 答:客户端通常不需要显式调用 `bind` 函数分配地址。如果不调用 `bind` 函数,在调用 `connect` 函数时,操作系统会自动为客户端套接字分配 IP 地址(使用本机网络接口的 IP)和端口号(从临时端口范围中随机选择一个未使用的端口)。 + ## 第 5 章 基于 TCP 的服务端/客户端(2) 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -1431,7 +1437,7 @@ write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE - 1); ``` -二者都在循环调用 read 和 write 函数。实际上之前的回声客户端将 100% 接收字节传输的数据,只不过接收数据时的单位有些问题。扩展客户端代码回顾范围,下面是客户端的代码: +二者都在循环调用 read 和 write 函数。实际上之前的回声客户端会接收服务器传输的全部数据,只不过接收数据时的单位有些问题。扩展客户端代码回顾范围,下面是客户端的代码: ```c while (1) @@ -1455,9 +1461,9 @@ while (1) 这个问题其实很容易解决,因为可以提前接收数据的大小。若之前传输了 20 字节长的字符串,则在接收时循环调用 read 函数读取 20 个字节即可。既然有了解决办法,那么代码如下: -- [echo_client2.c](ch05/echo_client2.c) +- [echo_client2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/echo_client2.c) -这样修改为了接收所有传输数据而循环调用 read 函数。测试及运行结果可参考第四章。 +这样修改后,客户端为接收所有传输数据而循环调用 read 函数。测试及运行结果可参考第四章。 #### 5.1.3 如果问题不在于回声客户端:定义应用层协议 @@ -1466,7 +1472,7 @@ while (1) 现在写一个小程序来体验应用层协议的定义过程。要求: 1. 服务器从客户端获得多个数组和运算符信息。 -2. 服务器接收到数字后对齐进行加减乘运算,然后把结果传回客户端。 +2. 服务器接收到数字后对其进行加减乘运算,然后把结果传回客户端。 例: @@ -1478,8 +1484,8 @@ while (1) 我自己的实现: -- [My_op_server.c](ch05/My_op_server.c) -- [My_op_client.c](ch05/My_op_client.c) +- [My_op_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/My_op_server.c) +- [My_op_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/My_op_client.c) 编译: @@ -1496,8 +1502,8 @@ gcc My_op_server.c -o myserver 书上的实现: -- [op_client.c](ch05/op_client.c) -- [op_server.c](ch05/op_server.c) +- [op_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/op_client.c) +- [op_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/op_server.c) 阅读代码要注意一下,`int*`与`char`之间的转换。TCP 中不存在数据边界。 @@ -1523,7 +1529,7 @@ gcc op_server.c -o opserver #### 5.2.1 TCP 套接字中的 I/O 缓冲 -TCP 套接字的数据收发无边界。服务器即使调用 1 次 write 函数传输 40 字节的数据,客户端也有可能通过 4 次 read 函数调用每次读取 10 字节。但此处也有一些疑问,服务器一次性传输了 40 字节,而客户端竟然可以缓慢的分批接收。客户端接收 10 字节后,剩下的 30 字节在何处等候呢? +TCP 套接字的数据收发无边界。服务器即使调用 1 次 write 函数传输 40 字节的数据,客户端也有可能通过 4 次 read 函数调用每次读取 10 字节。但此处也有一些疑问,服务器一次性传输了 40 字节,而客户端竟然可以缓慢地分批接收。客户端接收 10 字节后,剩下的 30 字节在何处等候呢? 实际上,write 函数调用后并非立即传输数据,read 函数调用后也并非马上接收数据。如图所示,write 函数调用瞬间,数据将移至输出缓冲;read 函数调用瞬间,从输入缓冲读取数据。 @@ -1549,7 +1555,7 @@ I/O 缓冲特性可以整理如下: 数据收发也是如此,因此 TCP 中不会因为缓冲溢出而丢失数据。 -**write 函数在数据传输完成时返回。** +**write 函数在数据成功移入输出缓冲时返回(并非等到对端接收完毕)。** #### 5.2.2 TCP 内部工作原理 1:与对方套接字的连接 @@ -1589,11 +1595,11 @@ TCP 在实际通信中也会经过三次对话过程,因此,该过程又被 > 刚才传输的 SEQ 为 1000 的数据包接收无误,现在请传递 SEQ 为 1001 的数据包。 -对于主机 A 首次传输的数据包的确认消息(ACK 1001)和为主机 B 传输数据做准备的同步消息(SEQ 2000)捆绑发送。因此,此种类消息又称为 SYN+ACK。 +对于主机 A 首次传输的数据包的确认消息(ACK 1001)和为主机 B 传输数据做准备的同步消息(SEQ 2000)捆绑发送。因此,此类消息又称为 SYN+ACK。 收发数据前向数据包分配序号,并向对方通报此序号,这都是为了防止数据丢失做的准备。通过向数据包分配序号并确认,可以在数据包丢失时马上查看并重传丢失的数据包。因此 TCP 可以保证可靠的数据传输。 -通过这三个过程,这样主机 A 和主机 B 就确认了彼此已经准备就绪。 +通过这三个过程,主机 A 和主机 B 就确认了彼此已经准备就绪。 #### 5.2.3 TCP 内部工作原理 2:与对方主机的数据交换 @@ -1626,7 +1632,7 @@ TCP 套接字的结束过程也非常优雅。如果对方还有数据需要传 ![](images/5c3ed7503c18c.png) -图中数据包内的 FIN 表示断开连接。也就是说,双方各发送 1 次 FIN 消息后断开连接。此过程经历 4 个阶段,因此又称四次握手(Four-way handshaking)。SEQ 和 ACK 的含义与之前讲解的内容一致,省略。图中,主机 A 传递了两次 ACK 5001,也许这里会有困惑。其实,第二次 FIN 数据包中的 ACK 5001 只是因为接收了 ACK 消息后未接收到的数据重传的。 +图中数据包内的 FIN 表示断开连接。也就是说,双方各发送 1 次 FIN 消息后断开连接。此过程经历 4 个阶段,因此又称四次握手(Four-way handshaking)。SEQ 和 ACK 的含义与之前讲解的内容一致,省略。图中,主机 A 传递了两次 ACK 5001,也许这里会有困惑。其实,图中第二次出现的 ACK 5001 是重传确认:若主机 B 未收到主机 A 第一次发出的 ACK,B 会重传 FIN,主机 A 收到后再次发送 ACK 5001 予以确认。 ### 5.3 基于 Windows 的实现 @@ -1653,6 +1659,7 @@ TCP 套接字的结束过程也非常优雅。如果对方还有数据需要传 4. **对方主机的输入缓冲剩余 50 字节空间时,若本主机通过 write 函数请求传输 70 字节,请问 TCP 如何处理这种情况?** 答:TCP 中有滑动窗口控制协议,所以传输的时候会保证传输的字节数小于等于对方能接收的字节数。在这种情况下,TCP 只会发送 50 字节的数据(或者更少),剩余的 20 字节会保留在发送方的输出缓冲中,等待对方腾出更多空间后再发送。write 函数可能会阻塞等待,或者返回实际发送的字节数(部分写入)。 + ## 第 6 章 基于 UDP 的服务端/客户端 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -1679,7 +1686,7 @@ TCP 与 UDP 的区别很大一部分来源于流控制。也就是说 TCP 的生 #### 6.1.3 UDP 的高效使用 -UDP 也具有一定的可靠性。对于通过网络实时传递的视频或者音频时情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点: +UDP 也具有一定的可靠性。当通过网络实时传递视频或音频时,情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点: - 收发数据前后进行的连接设置及清除过程。 - 收发过程中为保证可靠性而添加的流控制。 @@ -1719,7 +1726,7 @@ addrlen: 传递给参数 to 的地址值结构体变量长度 */ ``` -上述函数与之前的 TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收 UDP 数据的函数。UDP 数据的发送并不固定,因此该函数定义为可接受发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息。 +上述函数与之前的 TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收 UDP 数据的函数。UDP 数据的发送并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息。 ```c #include @@ -1744,15 +1751,15 @@ addrlen: 保存参数 from 的结构体变量长度的变量地址值。 代码: -- [uecho_client.c](ch06/uecho_client.c) -- [uecho_server.c](ch06/uecho_server.c) +- [uecho_client.c](uecho_client.c) +- [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) 编译运行: ```shell gcc uecho_client.c -o uclient gcc uecho_server.c -o userver -./server 9190 +./userver 9190 ./uclient 127.0.0.1 9190 ``` @@ -1774,12 +1781,12 @@ UDP 程序中,调用 sendto 函数传输数据前应该完成对套接字的 #### 6.3.1 存在数据边界的 UDP 套接字 -前面说得 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」 +前面说的 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」 相反,UDP 是具有数据边界的协议,传输中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数和输出函数的调用次数应该完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。通过一个例子来进行验证: -- [bound_host1.c](ch06/bound_host1.c) -- [bound_host2.c](ch06/bound_host2.c) +- [bound_host1.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host1.c) +- [bound_host2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host2.c) 编译运行: @@ -1800,13 +1807,13 @@ host1 是服务端,host2 是客户端,host2 一次性把数据发给服务 #### 6.3.2 已连接(connect)UDP 套接字与未连接(unconnected)UDP 套接字 -TCP 套接字中需注册待传传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段: +TCP 套接字中需注册待传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段: - 第 1 阶段:向 UDP 套接字注册目标 IP 和端口号 - 第 2 阶段:传输数据 - 第 3 阶段:删除 UDP 套接字中注册的目标地址信息。 -每次调用 sendto 函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接 connected 套接字。显然,UDP 套接字默认属于未连接套接字。当一台主机向另一台主机传输很多信息时,上述的三个阶段中,第一个阶段和第三个阶段占整个通信过程中近三分之一的时间,缩短这部分的时间将会大大提高整体性能。 +每次调用 sendto 函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为已连接(connected)套接字。显然,UDP 套接字默认属于未连接套接字。当一台主机向另一台主机传输很多信息时,上述的三个阶段中,第一个阶段和第三个阶段占整个通信过程中近三分之一的时间,缩短这部分的时间将会大大提高整体性能。 #### 6.3.3 创建已连接 UDP 套接字 @@ -1825,9 +1832,9 @@ connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 之后就与 TCP 套接字一致,每次调用 sendto 函数时只需传递信息数据。因为已经指定了收发对象,所以不仅可以使用 sendto、recvfrom 函数,还可以使用 write、read 函数进行通信。 -下面的例子把之前的 [uecho_client.c](ch06/uecho_client.c) 程序改成了基于已连接 UDP 的套接字的程序,因此可以结合 [uecho_server.c](ch06/uecho_server.c) 程序运行。代码如下: +下面的例子把之前的 [uecho_client.c](uecho_client.c) 程序改成了基于已连接 UDP 的套接字的程序,因此可以结合 [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) 程序运行。代码如下: -- [uecho_con_client.c](ch06/uecho_con_client.c) +- [uecho_con_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_con_client.c) 编译运行过程与上面一样,故省略。 @@ -1843,7 +1850,7 @@ connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 1. **UDP 为什么比 TCP 快?为什么 TCP 传输可靠而 UDP 传输不可靠?** - 答:为了提供可靠的数据传输服务,TCP 在不可靠的IP层进行流控制,而 UDP 缺少这种流控制。所以 UDP 是不可靠的传输方式。 + 答:UDP 比 TCP 快主要有两点原因:一是 UDP 不需要收发数据前后的连接建立与拆除过程;二是 UDP 不进行为保证可靠性而添加的流控制。而 TCP 传输可靠,是因为它在不可靠的 IP 层之上进行了流控制等可靠性保障;UDP 缺少这些保障,所以是不可靠的传输方式。 2. **下面不属于 UDP 特点的是?** @@ -1855,7 +1862,7 @@ connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 4. **UDP 套接字和 TCP 套接字可以共存。若需要,可以同时在同一主机进行 TCP 和 UDP 数据传输。** 5. 针对 UDP 函数也可以调用 connect 函数,此时 UDP 套接字跟 TCP 套接字相同,也需要经过 3 次握手阶段。 - 答:第2句和第5句不属于 UDP 的特点(即这两句话是错误的)。第2句错误是因为 UDP 只需一个套接字就可以向多个目标传输数据;第5句错误是因为 UDP 调用 connect 函数只是注册目标地址信息,不会进行 TCP 那样的三次握手过程。 + 答:第2句、第3句和第5句不属于 UDP 的特点(即这三句话是错误的)。第2句错误是因为 UDP 只需一个套接字就可以向多个目标传输数据;第3句错误是因为 TCP 和 UDP 的端口空间相互独立,UDP 套接字可以使用与 TCP 相同的端口号;第5句错误是因为 UDP 调用 connect 函数只是注册目标地址信息,不会进行 TCP 那样的三次握手过程。 3. **UDP 数据报向对方主机的 UDP 套接字传递过程中,IP 和 UDP 分别负责哪些部分?** @@ -1872,11 +1879,12 @@ connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 6. **TCP 客户端必须调用 connect 函数,而 UDP 可以选择性调用。请问,在 UDP 中调用 connect 函数有哪些好处?** 答:要与同一个主机进行长时间通信时,将 UDP 套接字变成已连接套接字会提高效率。因为三个阶段中,第一个阶段和第三个阶段占用了一大部分时间,调用 connect 函数可以节省这些时间。 -## 第 7 章 优雅的断开套接字的连接 + +## 第 7 章 优雅地断开套接字的连接 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 -本章讨论如何优雅的断开套接字的连接,之前用的方法不够优雅是因为,我们是调用 close 函数或 closesocket 函数单方面断开连接的。 +本章讨论如何优雅地断开套接字的连接,之前用的方法不够优雅是因为,我们是调用 close 函数或 closesocket 函数单方面断开连接的。 ### 7.1 基于 TCP 的半关闭 @@ -1888,9 +1896,9 @@ Linux 的 close 函数和 Windows 的 closesocket 函数意味着完全断开连 ![](images/5c412a8baa2d8.png) -图中描述的是 2 台主机正在进行双向通信,主机 A 发送完最后的数据后,调用 close 函数断开了最后的连接,之后主机 A 无法再接受主机 B 传输的数据。实际上,是完全无法调用与接受数据相关的函数。最终,由主机 B 传输的、主机 A 必须要接受的数据也销毁了。 +图中描述的是 2 台主机正在进行双向通信,主机 A 发送完最后的数据后,调用 close 函数断开了最后的连接,之后主机 A 无法再接收主机 B 传输的数据。实际上,是完全无法调用与接收数据相关的函数。最终,由主机 B 传输的、主机 A 必须要接收的数据也销毁了。 -为了解决这类问题,「只关闭一部分数据交换中使用的流」的方法应运而生。断开一部分连接是指,可以传输数据但是无法接收,或可以接受数据但无法传输。顾名思义就是只关闭流的一半。 +为了解决这类问题,「只关闭一部分数据交换中使用的流」的方法应运而生。断开一部分连接是指,可以传输数据但是无法接收,或可以接收数据但无法传输。顾名思义就是只关闭流的一半。 #### 7.1.2 套接字和流(Stream) @@ -1911,7 +1919,7 @@ shutdown 用来关闭其中一个流: int shutdown(int sock, int howto); /* 成功时返回 0 ,失败时返回 -1 -sock: 需要断开套接字文件描述符 +sock: 需要断开的套接字文件描述符 howto: 传递断开方式信息 */ ``` @@ -1922,7 +1930,7 @@ howto: 传递断开方式信息 - `SHUT_WR` : 断开输出流 - `SHUT_RDWR` : 同时断开 I/O 流 -若向 shutdown 的第二个参数传递`SHUT_RD`,则断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用相关函数。如果向 shutdown 的第二个参数传递`SHUT_WR`,则中断输出流,也就无法传输数据。若如果输出缓冲中还有未传输的数据,则将传递给目标主机。最后,若传递关键字`SHUT_RDWR`,则同时中断 I/O 流。这相当于分 2 次调用 shutdown ,其中一次以`SHUT_RD`为参数,另一次以`SHUT_WR`为参数。 +若向 shutdown 的第二个参数传递`SHUT_RD`,则断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用相关函数。如果向 shutdown 的第二个参数传递`SHUT_WR`,则中断输出流,也就无法传输数据。如果输出缓冲中还有未传输的数据,则将传递给目标主机。最后,若传递关键字`SHUT_RDWR`,则同时中断 I/O 流。这相当于分 2 次调用 shutdown ,其中一次以`SHUT_RD`为参数,另一次以`SHUT_WR`为参数。 #### 7.1.4 为何要半关闭 @@ -1930,15 +1938,15 @@ howto: 传递断开方式信息 > 一旦客户端连接到服务器,服务器将约定的文件传输给客户端,客户端收到后发送字符串「Thank you」给服务器端。 -此处「Thank you」的传递是多余的,这只是用来模拟客户端断开连接前还有数据要传输的情况。此时程序的编写难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止的调用输入函数,因为这有可能导致程序**阻塞**。 +此处「Thank you」的传递是多余的,这只是用来模拟客户端断开连接前还有数据要传输的情况。此时程序的编写难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止地调用输入函数,因为这有可能导致程序**阻塞**。 > 是否可以让服务器和客户端约定一个代表文件尾的字符? -这种方式也有问题,因为这意味这文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接受 EOF ,这样可以避免与文件内容冲突。那么问题来了,服务端如何传递 EOF ? +这种方式也有问题,因为这意味着文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接收 EOF ,这样可以避免与文件内容冲突。那么问题来了,服务端如何传递 EOF ? > 断开输出流时向主机传输 EOF。 -当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接受对方传输的数据。换言之,若调用 close 函数关闭流,就无法接受客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。 +当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接收对方传输的数据。换言之,若调用 close 函数关闭流,就无法接收客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。 #### 7.1.5 基于半关闭的文件传输程序 @@ -1948,8 +1956,8 @@ howto: 传递断开方式信息 下面的代码为编程简便,省略了大量错误处理代码。 -- [file_client.c](ch07/file_client.c) -- [file_server.c](ch07/file_server.c) +- [file_client.c](file_client.c) +- [file_server.c](file_server.c) 编译运行: @@ -1964,7 +1972,7 @@ gcc file_server.c -o fserver ![](images/5c4140bc8db2f.png) -客户端接受完成后,服务器会接收到来自客户端的感谢信息。 +客户端接收完成后,服务器会接收到来自客户端的感谢信息。 ### 7.2 基于 Windows 的实现 @@ -2009,6 +2017,7 @@ Windows 下的 how 参数取值与 Linux 略有不同: 针对输出流执行半关闭(即调用 `shutdown(sock, SHUT_WR)`)的主机处于:可以接收数据,但无法发送数据的状态。 半关闭会导致对方主机的接收函数返回 EOF(文件结束符),对方主机由此得知数据已传输完毕。 + ## 第 8 章 域名及网络地址 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -2033,7 +2042,7 @@ DNS 是对IP地址和域名进行相互转换的系统,其核心是 DNS 服务 #### 8.2.1 程序中有必要使用域名吗? -一句话,需要,因为IP地址可能经常改变,而且也不容易记忆,通过域名可以随时更改解析,达到更换IP的目的 +一句话,需要,因为IP地址可能经常改变,而且也不容易记忆,通过域名可以随时更改解析,达到更换IP的目的。 #### 8.2.2 利用域名获取IP地址 @@ -2064,7 +2073,7 @@ struct hostent - h_name:该变量中存有官方域名(Official domain name)。官方域名代表某一主页,但实际上,一些著名公司的域名并没有用官方域名注册。 - h_aliases:可以通过多个域名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可以指定其他域名。这些信息可以通过 h_aliases 获得。 -- h_addrtype:gethostbyname 函数不仅支持 IPv4 还支持 IPv6。因此可以通过此变量获取保存在 h_addr_list 的IP地址族信息。若是 IPv4,则此变量中存有 AF_INET。 +- h_addrtype:该成员用于获取保存在 h_addr_list 中的地址族信息。若是 IPv4,则此变量中存有 AF_INET;若是 IPv6,则存有 AF_INET6。需要注意,gethostbyname 函数本身只返回 IPv4(AF_INET)地址,若需解析 IPv6 地址应改用 getaddrinfo 函数。 - h_length:保存IP地址长度。若是 IPv4 地址,因为是 4 个字节,则保存 4;IPv6 时,因为是 16 个字节,故保存 16。 - h_addr_list:这个是最重要的成员。通过此变量以整数形式保存域名相对应的IP地址。另外,用户比较多的网站有可能分配多个IP地址给同一个域名,利用多个服务器做负载均衡。此时可以通过此变量获取IP地址信息。 @@ -2074,7 +2083,7 @@ struct hostent 下面的代码通过一个例子来演示 gethostbyname 的应用,并说明 hostent 结构体变量特性。 -- [gethostbyname.c](ch08/gethostbyname.c) +- [gethostbyname.c](gethostbyname.c) 编译运行: @@ -2089,11 +2098,10 @@ gcc gethostbyname.c -o hostname 如图所示,显示出了对百度的域名解析 -可以看出,百度有一个域名解析是 CNAME 解析的,指向了`shifen.com`,关于百度具体的解析过程。 +可以看出,百度的一个域名解析使用了 CNAME 记录,指向了 `shifen.com`。关于百度具体的解析过程,可参考下方资料。 > 这一部分牵扯到了很多关于DNS解析的过程,还有 Linux 下关于域名解析的一些命令,我找了一部分资料,可以点下面的链接查看比较详细的: > -> - [关于百度DNS的解析过程](http://zhan.renren.com/starshen?gid=3602888498023142484&checked=true) > - [DNS解析的过程是什么,求详细的?](https://www.zhihu.com/question/23042131/answer/66571369) > - [Linux DNS 查询剖析](https://zhuanlan.zhihu.com/p/45535596) > - [Linux DNS查询命令](http://www.live-in.org/archives/1938.html) @@ -2128,7 +2136,7 @@ family: 传递地址族信息,IPv4 是 AF_INET,IPv6 是 AF_INET6。 下面的代码演示使用方法: -- [gethostbyaddr.c](ch08/gethostbyaddr.c) +- [gethostbyaddr.c](gethostbyaddr.c) 编译运行: @@ -2183,6 +2191,7 @@ gcc gethostbyaddr.c -o hostaddr 5. **建立连接**:浏览器使用获得的 IP 地址与目标服务器建立 TCP 连接,发送 HTTP 请求,获取并渲染网页内容。 可以参考知乎回答:[在浏览器地址栏输入一个 URL 后回车,背后会进行哪些技术步骤?](https://www.zhihu.com/question/34873227/answer/518086565) + ## 第 9 章 套接字的多种可选项 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -2193,7 +2202,7 @@ gcc gethostbyaddr.c -o hostaddr #### 9.1.1 套接字多种可选项 -我们之前写得程序都是创建好套接字之后直接使用的,此时通过默认的套接字特性进行数据通信,这里列出了一些套接字可选项。 +我们之前写的程序都是创建好套接字之后直接使用的,此时通过默认的套接字特性进行数据通信,这里列出了一些套接字可选项。 | 协议层 | 选项名 | 读取 | 设置 | | :----: | :----: |:--: | :--: | @@ -2217,7 +2226,7 @@ gcc gethostbyaddr.c -o hostaddr 从表中可以看出,套接字可选项是分层的。 -- IPPROTO_IP 可选项是IP协议相关事项 +- IPPROTO_IP 层可选项是 IP 协议相关事项 - IPPROTO_TCP 层可选项是 TCP 协议的相关事项 @@ -2257,9 +2266,9 @@ optlen: 向第四个参数传递的缓冲大小值(选项值的长度)。 */ ``` -下面的代码可以看出 getsockopt 的使用方法。下面示例用协议层为 SOL_SOCKET 、名为 SO_TYPE 的可选项查看套接字类型(TCP 和 UDP )。 +从下面的代码可以看出 getsockopt 的使用方法。下面示例用协议层为 SOL_SOCKET 、名为 SO_TYPE 的可选项查看套接字类型(TCP 和 UDP )。 -- [sock_type.c](ch09/sock_type.c) +- [sock_type.c](sock_type.c) 编译运行: @@ -2279,7 +2288,7 @@ Socket type two: 2 首先创建了一个 TCP 套接字和一个 UDP 套接字。然后通过调用 getsockopt 函数来获得当前套接字的状态。 -用于验证套接类型的 SO_TYPE 是只读可选项,因为**套接字类型只能在创建时决定,以后不能再更改**。 +用于验证套接字类型的 SO_TYPE 是只读可选项,因为**套接字类型只能在创建时决定,以后不能再更改**。 #### 9.1.3 `SO_SNDBUF` & `SO_RCVBUF` @@ -2287,7 +2296,7 @@ Socket type two: 2 SO_RCVBUF 是输入缓冲大小相关可选项,SO_SNDBUF 是输出缓冲大小相关可选项。用这 2 个可选项既可以读取当前 I/O 大小,也可以进行更改。通过下列示例读取创建套接字时默认的 I/O 缓冲大小。 -- [get_buf.c](ch09/get_buf.c) +- [get_buf.c](get_buf.c) 编译运行: @@ -2307,7 +2316,7 @@ Output buffer size: 16384 下面的代码演示了,通过程序设置 I/O 缓冲区的大小 -- [set_buf.c](ch09/set_buf.c) +- [set_buf.c](set_buf.c) 编译运行: @@ -2323,7 +2332,7 @@ Input buffer size: 6144 Output buffer size: 6144 ``` -输出结果和我们预想的不是很相同,缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行。 +输出结果和我们预想的不完全相同,缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行。 ### 9.2 `SO_REUSEADDR` @@ -2331,15 +2340,15 @@ Output buffer size: 6144 在学习 SO_REUSEADDR 可选项之前,应该好好理解 Time-wait 状态。看以下代码的示例: -- [reuseadr_eserver.c](ch09/reuseadr_eserver.c) +- [reuseadr_eserver.c](reuseadr_eserver.c) -这是一个回声服务器的服务端代码,可以配合第四章的 [echo_client.c](ch04/echo_client.c) 使用,在这个代码中,客户端通知服务器终止程序。在客户端控制台输入 Q 可以结束程序,向服务器发送 FIN 消息并经过四次握手过程。当然,输入 CTRL+C 也会向服务器传递 FIN 信息。强制终止程序时,由操作系统关闭文件套接字,此过程相当于调用 close 函数,也会向服务器发送 FIN 消息。 +这是一个回声服务器的服务端代码,可以配合第四章的 [echo_client.c](../ch04/echo_client.c) 使用,在这个代码中,客户端通知服务器终止程序。在客户端控制台输入 Q 可以结束程序,向服务器发送 FIN 消息并经过四次握手过程。当然,输入 CTRL+C 也会向服务器传递 FIN 信息。强制终止程序时,由操作系统关闭套接字,此过程相当于调用 close 函数,也会向服务器发送 FIN 消息。 -这样看不到是什么特殊现象,考虑以下情况: +这样并不会出现什么特殊现象,考虑以下情况: > 服务器端和客户端都已经建立连接的状态下,向服务器控制台输入 CTRL+C ,强制关闭服务端 -如果用这种方式终止程序,如果用同一端口号再次运行服务端,就会输出「bind() error」消息,并且无法再次运行。但是在这种情况下,再过大约 3 分钟就可以重新运行服务端。 +如果用这种方式终止程序,再用同一端口号再次运行服务端,就会输出「bind() error」消息,并且无法再次运行。但是在这种情况下,再过大约 3 分钟就可以重新运行服务端。 #### 9.2.2 `Time-wait` 状态 @@ -2347,11 +2356,11 @@ Output buffer size: 6144 ![](images/5c42db182cade.png) -假设图中主机 A 是服务器,因为是主机 A 向 B 发送 FIN 消息,故可想象成服务器端在控制台中输入 CTRL+C 。但是问题是,套接字经过四次握手后并没有立即消除,而是要经过一段时间的 Time-wait 状态。当然,只有先断开连接的(先发送 FIN 消息的)主机才经过 Time-wait 状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在 Time-wait 过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind 函数调用过程中会发生错误。 +假设图中主机 A 是服务器,因为是主机 A 向 B 发送 FIN 消息,故可想象成服务器端在控制台中输入 CTRL+C 。但是问题是,套接字经过四次握手后并没有立即销毁,而是要经过一段时间的 Time-wait 状态。当然,只有先断开连接的(先发送 FIN 消息的)主机才经过 Time-wait 状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在 Time-wait 过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind 函数调用过程中会发生错误。 **实际上,不论是服务端还是客户端,都要经过一段时间的 Time-wait 过程。先断开连接的套接字必然会经过 Time-wait 过程,但是由于客户端套接字的端口是任意指定的,所以无需过多关注 Time-wait 状态。** -那到底为什么会有 Time-wait 状态呢,在图中假设,主机 A 向主机 B 传输 ACK 消息(SEQ 5001 , ACK 7502 )后立刻消除套接字。但是最后这条 ACK 消息在传递过程中丢失,没有传递主机 B ,这时主机 B 就会试图重传。但是此时主机 A 已经是完全终止状态,因此主机 B 永远无法收到从主机 A 最后传来的 ACK 消息。基于这些问题的考虑,所以要设计 Time-wait 状态。 +那到底为什么会有 Time-wait 状态呢?在图中假设,主机 A 向主机 B 传输 ACK 消息(SEQ 5001 , ACK 7502 )后立刻销毁套接字。但是最后这条 ACK 消息在传递过程中丢失,没有传递主机 B ,这时主机 B 就会试图重传。但是此时主机 A 已经是完全终止状态,因此主机 B 永远无法收到从主机 A 最后传来的 ACK 消息。基于这些问题的考虑,所以要设计 Time-wait 状态。 #### 9.2.3 地址再分配 @@ -2359,9 +2368,9 @@ Time-wait 状态看似重要,但是不一定讨人喜欢。如果系统发生 ![](images/5c42dec2ba42b.png) -从图上可以看出,在主机 A 四次握手的过程中,如果最后的数据丢失,则主机 B 会认为主机 A 未能收到自己发送的 FIN 信息,因此重传。这时,收到的 FIN 消息的主机 A 将重启 Time-wait 计时器。因此,如果网络状况不理想, Time-wait 将持续。 +从图上可以看出,在主机 A 四次握手的过程中,如果最后的数据丢失,则主机 B 会认为主机 A 未能收到自己发送的 FIN 信息,因此重传。这时,收到重传 FIN 消息的主机 A 将重启 Time-wait 计时器。因此,如果网络状况不理想, Time-wait 将持续。 -解决方案就是在套接字的可选项中更改 SO_REUSEADDR 的状态。适当调整该参数,可将 Time-wait 状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR 的默认值为 0.这就意味着无法分配 Time-wait 状态下的套接字端口号。因此需要将这个值改成 1 。具体作法已在示例 [reuseadr_eserver.c](ch09/reuseadr_eserver.c) 给出,只需要把注释掉的东西解除注释即可。 +解决方案就是在套接字的可选项中更改 SO_REUSEADDR 的状态。适当调整该参数,可将 Time-wait 状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR 的默认值为 0,这就意味着无法分配 Time-wait 状态下的套接字端口号。因此需要将这个值改成 1 。具体作法已在示例 [reuseadr_eserver.c](reuseadr_eserver.c) 给出,只需要取消相关代码的注释即可。 ```c optlen = sizeof(option); @@ -2375,7 +2384,7 @@ setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen); #### 9.3.1 `Nagle` 算法 -为了防止因数据包过多而发生网络过载,`Nagle` 算法诞生了。它应用于 TCP 层。它是否使用会导致如图所示的差异: +为了防止因数据包过多而发生网络过载,`Nagle` 算法诞生了。它应用于 TCP 层。是否使用它会导致如图所示的差异: ![](images/5c42e12abc5b8.png) @@ -2383,7 +2392,7 @@ setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen); **只有接收到前一数据的 ACK 消息, `Nagle` 算法才发送下一数据。** -TCP 套接字默认使用 `Nagle` 算法交换数据,因此最大限度的进行缓冲,直到收到 ACK 。左图也就是说一共传递 4 个数据包以传输一个字符串。从右图可以看出,发送数据包一共使用了 10 个数据包。由此可知,不使用 `Nagle` 算法将对网络流量产生负面影响。即使只传输一个字节的数据,其头信息都可能是几十个字节。因此,为了提高网络传输效率,必须使用 `Nagle` 算法。 +TCP 套接字默认使用 `Nagle` 算法交换数据,因此最大限度地缓冲,直到收到 ACK 。左图也就是说一共传递 4 个数据包以传输一个字符串。从右图可以看出,发送数据包一共使用了 10 个数据包。由此可知,不使用 `Nagle` 算法将对网络流量产生负面影响。即使只传输一个字节的数据,其头信息都可能是几十个字节。因此,为了提高网络传输效率,通常应使用 `Nagle` 算法。 `Nagle` 算法并不是什么情况下都适用,网络流量未受太大影响时,不使用 `Nagle` 算法要比使用它时传输速度快。最典型的就是「传输大文件数据」。将文件数据传入输出缓冲不会花太多时间,因此,不使用 `Nagle` 算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而在无需等待 ACK 的前提下连续传输,因此可以大大提高传输速度。 @@ -2405,7 +2414,7 @@ opt_len = sizeof(opt_val); getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len); ``` -如果正在使用`Nagle` 算法,那么 opt_val 值为 0,如果禁用则为 1. +如果正在使用`Nagle` 算法,那么 opt_val 值为 0,如果禁用则为 1。 关于这个算法,可以参考这个回答:[TCP连接中启用和禁用TCP_NODELAY有什么影响?](https://www.zhihu.com/question/42308970/answer/246334766) @@ -2419,16 +2428,17 @@ getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len); 1. **下列关于 Time-wait 状态的说法错误的是?** - 答:错误的说法是第 1、3、4 项。正确的说法是第 2 项(加粗显示)。 - 1. ~~Time-wait 状态只在服务器的套接字中发生~~(错误:客户端先断开连接时也会进入 Time-wait 状态) 2. **断开连接的四次握手过程中,先传输 FIN 消息的套接字将进入 Time-wait 状态。**(正确) 3. ~~Time-wait 状态与断开连接的过程无关,而与请求连接过程中 SYN 消息的传输顺序有关~~(错误:Time-wait 状态与断开连接的四次握手过程直接相关) 4. ~~Time-wait 状态通常并非必要,应尽可能通过更改套接字可选项来防止其发生~~(错误:Time-wait 状态对于保证 TCP 连接可靠关闭是必要的,但在某些紧急重启场景下可通过 SO_REUSEADDR 重用端口) + 答:错误的说法是第 1、3、4 项。正确的说法是第 2 项(加粗显示)。 + 2. **TCP_NODELAY 可选项与 Nagle 算法有关,可通过它禁用 Nagle 算法。请问何时应考虑禁用 Nagle 算法?结合收发数据的特性给出说明。** - 答:当网络流量未受太大影响时,不使用 Nagle 算法要比使用它时传输速度快,比如说在传输大文件时。 + 答:当网络流量未受太大影响、且需要连续传输大量数据(如传输大文件)时,应考虑禁用 Nagle 算法。此时数据能快速填满输出缓冲,无需等待前一数据的 ACK 即可连续发送数据包,既不会明显增加数据包数量,又能避免等待 ACK 带来的延迟,从而提高传输速度。 + ## 第 10 章 多进程服务器端 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -2455,7 +2465,7 @@ getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len); #### 10.1.3 进程 ID -在介绍进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的,所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程 ID」,其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。接下来观察在 Linux 中运行的进程。 +在介绍进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的,所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程 ID」,其值为大于 1 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。接下来观察在 Linux 中运行的进程。 ```shell ps au @@ -2484,9 +2494,9 @@ fork 函数将创建调用的进程副本。也就是说,并非根据完全不 ![](images/5c43da5412b90.png) -从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11,将局部变量 lval 的值增加到 25,因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回值区分父子进程。父进程的 lval 的值增加 1,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 1 也不会影响到父进程的 gval。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子: +从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11,将局部变量 lval 的值增加到 25,因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回值区分父子进程。父进程的 lval 的值减少 2,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 2 也不会影响到父进程的 gval。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子: -- [fork.c](ch10/fork.c) +- [fork.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/fork.c) ```c #include @@ -2531,7 +2541,7 @@ gcc fork.c -o fork 进程的工作完成后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作「僵尸进程」,这也是给系统带来负担的原因之一。 -> 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源 +> 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出,子进程被 init 接管,子进程退出后 init 会回收其占用的相关资源。 **维基百科**: @@ -2560,7 +2570,7 @@ gcc fork.c -o fork 如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)的时候,操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。也就是说,父母要负责收回自己生的孩子。接下来的示例是创建僵尸进程: -- [zombie.c](ch10/zombie.c) +- [zombie.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/zombie.c) ```c #include @@ -2600,7 +2610,7 @@ gcc zombie.c -o zombie ![](images/5c4439a751b11.png) -通过 `ps au` 命令可以看出,子进程仍然存在,并没有被销毁,僵尸进程在这里显示为 `Z+`.30秒后,红框里面的两个进程会同时被销毁。 +通过 `ps au` 命令可以看出,子进程仍然存在,并没有被销毁,僵尸进程在这里显示为 `Z+`。30 秒后,红框里面的两个进程会同时被销毁。 > 利用 `./zombie &`可以使程序在后台运行,不用打开新的命令行窗口。 @@ -2619,7 +2629,7 @@ pid_t wait(int *statloc); 调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数返回值,main 函数的 return 返回值)将保存到该函数的参数所指的内存空间。但函数参数指向的单元中还包含其他信息,因此需要用下列宏进行分离: - WIFEXITED 子进程正常终止时返回「真」 -- WEXITSTATUS 返回子进程时的返回值 +- WEXITSTATUS 返回子进程正常终止时的返回值 也就是说,向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码: @@ -2633,7 +2643,7 @@ if (WIFEXITED(status)) 根据以上内容,有如下示例: -- [wait.c](ch10/wait.c) +- [wait.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/wait.c) ```c #include @@ -2661,7 +2671,7 @@ int main(int argc, char *argv[]) else { printf("Child PID: %d \n", pid); - wait(&status); //之间终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁 + wait(&status); //之前终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁 if (WIFEXITED(status)) //通过 WIFEXITED 来验证子进程是否正常终止。如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值 printf("Child send one: %d \n", WEXITSTATUS(status)); @@ -2701,13 +2711,13 @@ pid_t waitpid(pid_t pid, int *statloc, int options); 成功时返回终止的子进程ID 或 0 ,失败时返回 -1 pid: 等待终止的目标子进程的ID,若传 -1,则与 wait 函数相同,可以等待任意子进程终止 statloc: 与 wait 函数的 statloc 参数具有相同含义 -options: 传递头文件 sys/wait.h 声明的常量 WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。 +options: 传递头文件 sys/wait.h 声明的常量 WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。 */ ``` 以下是 waitpid 的使用示例: -- [waitpid.c](ch10/waitpid.c) +- [waitpid.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/waitpid.c) ```c #include @@ -2758,9 +2768,9 @@ gcc waitpid.c -o waitpid #### 10.3.1 向操作系统求助 -子进程终止的识别主题是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序 +子进程终止的识别主体是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序。 -为了实现上述的功能,引入信号处理机制(Signal Handing)。此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外,为了响应该消息,执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。 +为了实现上述的功能,引入信号处理机制(Signal Handling)。此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外,为了响应该消息,执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。 #### 10.3.2 信号与 signal 函数 @@ -2819,9 +2829,9 @@ unsigned int alarm(unsigned int seconds); // 返回0或以秒为单位的距 SIGALRM 信号发生所剩时间 ``` -如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0,则之前对 SIGALRM 信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用 signal 函数)终止进程,不做任何处理。 +如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0,则之前对 SIGALRM 信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则 SIGALRM 信号按默认处理方式终止进程。 -- [signal.c](ch10/signal.c) +- [signal.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/signal.c) ```c #include @@ -2843,7 +2853,7 @@ int main(int argc, char *argv[]) int i; signal(SIGALRM, timeout); //注册信号及相应处理器 signal(SIGINT, keycontrol); - alarm(2); //预约 2 秒候发生 SIGALRM 信号 + alarm(2); //预约 2 秒后发生 SIGALRM 信号 for (i = 0; i < 3; i++) { @@ -2911,7 +2921,7 @@ struct sigaction 下面的示例是关于 sigaction 函数的使用方法。 -- [sigaction.c](ch10/sigaction.c) +- [sigaction.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/sigaction.c) ```c #include @@ -2970,7 +2980,7 @@ Time out! 下面利用子进程终止时产生 SIGCHLD 信号这一点,来用信号处理来消灭僵尸进程。看以下代码: -- [remove_zomebie.c](ch10/remove_zomebie.c) +- [remove_zombie.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/remove_zombie.c) ```c #include @@ -3034,7 +3044,7 @@ int main(int argc, char *argv[]) 编译运行: ```shell -gcc remove_zomebie.c -o zombie +gcc remove_zombie.c -o zombie ./zombie ``` @@ -3058,9 +3068,9 @@ Child send: 24 wait ``` -请仔细观察结果,结果中的每一个空行代表间隔了 5 秒。程序先创建了两个子进程,子进程在 10 秒之后会返回值。第一个 wait 由于子进程在执行,所以直接被唤醒,然后这两个子进程正在睡眠 10 秒,所以 5 秒之后第二个 wait 开始执行,又过了 5 秒,两个子进程同时被唤醒。所以剩下的 wait 也被唤醒。 +请仔细观察结果,结果中的每一个空行代表间隔了 5 秒(父进程每轮循环 sleep 5 秒)。程序先创建了两个子进程,二者各自睡眠 10 秒后终止。父进程进入循环,每轮打印一次 wait 并 sleep 5 秒:前两轮期间子进程仍在睡眠;约第 10 秒时子进程终止,操作系统向父进程发送 SIGCHLD 信号,中断父进程的 sleep,转而执行信号处理函数,在其中通过 waitpid 回收子进程并获取其返回值,随后父进程继续后续轮次的循环。 -所以在本程序的过程中,当子进程终止时候,会向系统发送一个信号,然后调用我们提前写好的处理函数,在处理函数中使用 waitpid 来处理僵尸进程,获取子进程返回值。 +所以在本程序的过程中,当子进程终止时,操作系统会向父进程发送 SIGCHLD 信号,进而调用我们提前注册的处理函数,在处理函数中使用 waitpid 来回收僵尸进程,获取子进程返回值。 ### 10.4 基于多任务的并发服务器 @@ -3073,20 +3083,20 @@ wait 从图中可以看出,每当有客户端请求时(连接请求),回声服务器都创建子进程以提供服务。如果请求的客户端有 5 个,则将创建 5 个子进程来提供服务,为了完成这些任务,需要经过如下过程: - 第一阶段:回声服务器端(父进程)通过调用 accept 函数受理连接请求 -- 第二阶段:此时获取的套接字文件描述符创建并传递给子进程 -- 第三阶段:进程利用传递来的文件描述符提供服务 +- 第二阶段:通过 fork 创建子进程,并将此时获取的套接字文件描述符传递给子进程 +- 第三阶段:子进程利用传递来的文件描述符提供服务 #### 10.4.2 实现并发服务器 -下面是基于多进程实现的并发的回声服务器的服务端,可以结合第四章的 [echo_client.c](ch04/echo_client.c) 回声客户端来运行。 +下面是基于多进程实现的并发的回声服务器的服务端,可以结合第四章的 [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/echo_client.c) 回声客户端来运行。 -- [echo_mpserv.c](ch10/echo_mpserv.c) +- [echo_mpserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpserv.c) 编译运行: ```shell gcc echo_mpserv.c -o eserver -./eserver +./eserver 9190 ``` 结果: @@ -3097,7 +3107,7 @@ gcc echo_mpserv.c -o eserver 示例中给出了通过 fork 函数复制文件描述符的过程。父进程将 2 个套接字(一个是服务端套接字另一个是客户端套接字)文件描述符复制给了子进程。 -调用 fork 函数时赋值父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。 +调用 fork 函数时复制父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。 ![](images/kP7Rjx.png) @@ -3117,7 +3127,7 @@ gcc echo_mpserv.c -o eserver ![](images/kPbhkD.png) -从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进程传输。 +从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进行传输。 分割 I/O 程序的另外一个好处是,可以提高频繁交换数据的程序性能,如下图所示: @@ -3131,7 +3141,7 @@ gcc echo_mpserv.c -o eserver 下面是回声客户端的 I/O 分割的代码实现: -- [echo_mpclient.c](ch10/echo_mpclient.c) +- [echo_mpclient.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpclient.c) 可以配合刚才的并发服务器进行执行。 @@ -3172,7 +3182,7 @@ gcc echo_mpclient.c -o eclient 3. **创建子进程时复制父进程所有内容,此时复制对象也包含套接字文件描述符。编写程序验证复制的文件描述符整数值是否与原文件描述符数值相同。** - 答:代码为多进程服务器修改而来,代码:[test_server.c](ch10/test_server.c) + 答:代码为多进程服务器修改而来,代码:[test_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/test_server.c) 运行截图: @@ -3192,11 +3202,12 @@ gcc echo_mpclient.c -o eclient - 在父进程中调用 wait() 或 waitpid() 函数来回收子进程的退出状态 - 使用信号处理机制:注册 SIGCHLD 信号处理函数,当子进程终止时,操作系统会向父进程发送 SIGCHLD 信号,在信号处理函数中调用 waitpid() 来回收僵尸进程 - 如果父进程不需要关心子进程的退出状态,可以显式忽略 SIGCHLD 信号(通过 signal(SIGCHLD, SIG_IGN)),这样子进程终止后会被系统自动回收 + ## 第 11 章 进程间通信 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 -进程间通信,意味着两个不同的进程中可以交换数据 +进程间通信,意味着两个不同的进程之间可以交换数据。 ### 11.1 进程间通信的基本概念 @@ -3218,9 +3229,9 @@ filedes[1]: 通过管道传输数据时使用的文件描述符,即管道入 */ ``` -父进程调用函数时将创建管道,同时获取对应于出入口的文件描述符,此时父进程可以读写同一管道。但父进程的目的是与子进程进行数据交换,因此需要将入口或出口中的 1 个文件描述符传递给子进程。下面的例子是关于该函数的使用方法: +父进程调用函数时将创建管道,同时获取对应于出入口的文件描述符,此时父进程可以读写同一管道。但父进程的目的是与子进程进行数据交换,因此需要通过 fork 让子进程继承这两个文件描述符,父子进程各自使用其中一端进行通信(实际编程中还应关闭各自不需要的一端)。下面的例子是关于该函数的使用方法: -- [pipe1.c](ch11/pipe1.c) +- [pipe1.c](pipe1.c) ```c #include @@ -3274,7 +3285,7 @@ Who are you? 下面是双向通信的示例: -- [pipe2.c](ch11/pipe2.c) +- [pipe2.c](pipe2.c) ```c #include @@ -3323,7 +3334,7 @@ Parent proc output: Who are you? Child proc output: Thank you for your message ``` -运行结果是正确的,但是如果注释掉代码中子进程里的 `sleep(2);`(第18行),就会出现问题,导致一直等待下去。因为数据进入管道后变成了无主数据。也就是通过 read 函数先读取数据的进程将得到数据,即使该进程将数据传到了管道。因为,注释掉 `sleep(2);` 会产生问题。子进程可能读回自己向管道发送的数据。结果父进程调用 read 函数后,无限期等待数据进入管道。 +运行结果是正确的,但是如果注释掉代码中子进程里的 `sleep(2);`(第18行),就会出现问题,导致一直等待下去。因为数据进入管道后变成了无主数据。也就是通过 read 函数先读取数据的进程将得到数据,即使该进程将数据传到了管道。此时子进程可能在父进程读取前就把自己写入的数据读回,导致父进程的 read 调用无限期阻塞,等待永远不会到来的数据。 当一个管道不满足需求时,就需要创建两个管道,各自负责不同的数据流动,过程如下图所示: @@ -3331,7 +3342,7 @@ Child proc output: Thank you for your message 下面采用上述模型改进 `pipe2.c` 。 -- [pipe3.c](ch11/pipe3.c) +- [pipe3.c](pipe3.c) ```c #include @@ -3370,13 +3381,13 @@ int main(int argc, char *argv[]) #### 11.2.1 保存消息的回声服务器 -下面对第 10 章的 [echo_mpserv.c](ch10/echo_mpserv.c) 进行改进,添加一个功能: +下面对第 10 章的 [echo_mpserv.c](../ch10/echo_mpserv.c) 进行改进,添加一个功能: > 将回声客户端传输的字符串按序保存到文件中 实现该任务将创建一个新进程,从向客户端提供服务的进程读取字符串信息,下面是代码: -- [echo_storeserv.c](ch11/echo_storeserv.c) +- [echo_storeserv.c](echo_storeserv.c) 编译运行: @@ -3385,13 +3396,13 @@ gcc echo_storeserv.c -o serv ./serv 9190 ``` -此服务端配合第 10 章的客户端 [echo_mpclient.c](ch10/echo_mpclient.c) 使用,运行结果如下图: +此服务端配合第 10 章的客户端 [echo_mpclient.c](../ch10/echo_mpclient.c) 使用,运行结果如下图: ![](images/kFUCct.png) ![](images/kFUAHS.png) -从图上可以看出,服务端已经生成了文件,把客户端的消息保存了下来,只保存了10次消息。 +从图上可以看出,服务端已经生成了文件,把客户端的消息保存了下来,只保存了 10 条消息(代码中固定循环读取 10 次)。 ### 11.3 习题 @@ -3418,6 +3429,7 @@ gcc echo_storeserv.c -o serv 3. **管道允许 2 个进程间的双向通信。双向通信中需要注意哪些内容?** 答:向管道传输数据时,先调用 read 函数的进程会把数据取走。换言之,数据进入管道后会变成无主数据,任何连接到该管道的进程都可以读取。因此,在使用单个管道进行双向通信时,一个进程可能会读取到自己写入的数据,导致另一个进程无限等待。为了避免这个问题,通常需要创建两个管道,各自负责不同方向的数据流动。 + ## 第 12 章 I/O 复用 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -3459,7 +3471,7 @@ I/O 复用技术可以解决这个问题。 > 「好像不能同时说话?」 -实际上,因为是在进行对话,所以很少发生同时说话的情况。也就是说,上述系统采用的是**「时分复用」**技术。因为说话人声频率不同,即使在同时说话也能进行一定程度上的区分(杂音也随之增多)。因此,也可以说是「频分复用技术」。 +实际上,因为是在进行对话,所以很少发生同时说话的情况。也就是说,上述系统采用的是**「时分复用」**技术。因为说话人的声音频率不同,即使在同时说话也能进行一定程度上的区分(杂音也随之增多)。因此,也可以说是「频分复用技术」。 #### 12.1.3 复用技术在服务器端的应用 @@ -3487,7 +3499,7 @@ select 函数是最具代表性的实现复用服务器的方法。在 Windows > 术语:「事件」。当发生监视项对应情况时,称「发生了事件」。 -select 函数的使用方法与一般函数的区别并不大,更准确的说,他很难使用。但是为了实现 I/O 复用服务器端,我们应该掌握 select 函数,并运用于套接字编程当中。认为「select 函数是 I/O 复用的全部内容」也并不为过。select 函数的调用过程如下图所示: +select 函数的使用方法与一般函数的区别并不大,更准确地说,它很难使用。但是为了实现 I/O 复用服务器端,我们应该掌握 select 函数,并运用于套接字编程当中。认为「select 函数是 I/O 复用的全部内容」也并不为过。select 函数的调用过程如下图所示: ![](images/kAtdRs.png) @@ -3536,7 +3548,7 @@ timeout: 调用 select 函数后,为防止陷入无限阻塞的状态,传递 1. 文件描述符的监视(检查)范围是? 2. 如何设定 select 函数的超时时间? -第一,文件描述符的监视范围和 select 的第一个参数有关。实际上,select 函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在 fd_set 变量中的文件描述符数。但每次新建文件描述符时,其值就会增加 1 ,故只需将最大的文件描述符值加 1 再传递给 select 函数即可。加 1 是因为文件描述符的值是从 0 开始的。 +第一,文件描述符的监视范围和 select 的第一个参数有关。实际上,select 函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在 fd_set 变量中的文件描述符数。但文件描述符的值通常随创建而增大(已关闭的描述符可能被系统复用),故只需将最大的文件描述符值加 1 再传递给 select 函数即可。加 1 是因为文件描述符的值是从 0 开始的。 第二,select 函数的超时时间与 select 函数的最后一个参数有关,其中 timeval 结构体定义如下: @@ -3562,7 +3574,7 @@ select 返回正整数时,怎样获知哪些文件描述符发生了变化? 下面是一个 select 函数的例子: -- [select.c](ch12/select.c) +- [select.c](select.c) 编译运行: @@ -3581,7 +3593,7 @@ gcc select.c -o select 下面通过 select 函数实现 I/O 复用服务器端。下面是基于 I/O 复用的回声服务器端。 -- [echo_selectserv.c](ch12/echo_selectserv.c) +- [echo_selectserv.c](echo_selectserv.c) 编译运行: @@ -3616,6 +3628,11 @@ gcc echo_selectserv.c -o selserv 3. **复用服务器端需要 select 函数。下列关于 select 函数使用方法的描述错误的是?** + ① 调用 select 函数前需要集中待监视的文件描述符。 + ② 调用 select 函数后无需再次注册监视对象。 + ③ 复用服务器端无法同时向多个客户端提供服务。 + ④ 基于 select 的复用服务器只需要 1 个进程,可减少创建多进程带来的负担。 + 答:错误的描述是 2 和 3。说明如下: - 描述 1 正确:调用 select 函数前需要集中 I/O 监视对象的文件描述符。 @@ -3626,6 +3643,7 @@ gcc echo_selectserv.c -o selserv 4. **select 函数的观察对象中应包含服务端套接字(监听套接字),那么应将其包含到哪一类监听对象集合?请说明原因**。 答:应该包含到「是否存在待读取数据」(即 readset)集合中。原因是:服务端套接字(监听套接字)的作用是监听客户端的连接请求,当有新的客户端发起连接时,监听套接字变为「可读」状态,此时 accept 函数不会阻塞,可以成功完成连接。因此需要监视监听套接字是否可读,以得知是否有新的连接请求到来。 + ## 第 13 章 多种 I/O 函数 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -3641,7 +3659,7 @@ gcc echo_selectserv.c -o selserv ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags); /* 成功时返回发送的字节数,失败时返回 -1 -sockfd: 表示与数据传输对象的连接的套接字和文件描述符 +sockfd: 表示与数据传输对象连接的套接字文件描述符 buf: 保存待传输数据的缓冲地址值 nbytes: 待传输字节数 flags: 传输数据时指定的可选项信息 @@ -3678,8 +3696,8 @@ send & recv 函数的可选项意义: MSG_OOB 可选项用于创建特殊发送方法和通道以发送紧急消息。下面为 MSG_OOB 的示例代码: -- [oob_recv.c](ch13/oob_recv.c) -- [oob_send.c](ch13/oob_send.c) +- [oob_recv.c](oob_recv.c) +- [oob_send.c](oob_send.c) 编译运行: @@ -3704,7 +3722,7 @@ fcntl(recv_sock, F_SETOWN, getpid()); 的意思是: -> 文件描述符 recv_sock 指向的套接字引发的 SIGURG 信号处理进程变为 getpid 函数返回值用作 ID 进程. +> 将文件描述符 recv_sock 指向的套接字引发 SIGURG 信号时的处理进程,设置为以 getpid 函数返回值作为 ID 的进程。 上述描述中的「处理 SIGURG 信号」指的是「调用 SIGURG 信号处理函数」。但是之前讲过,多个进程可以拥有 1 个套接字的文件描述符。例如,通过调用 fork 函数创建子进程并同时复制文件描述符。此时如果发生 SIGURG 信号,应该调用哪个进程的信号处理函数呢?可以肯定的是,不会调用所有进程的信号处理函数。因此,处理 SIGURG 信号时必须指定处理信号所用的进程,而 getpid 返回的是调用此函数的进程 ID 。上述调用语句指当前为处理 SIGURG 信号的主体。 @@ -3712,7 +3730,7 @@ fcntl(recv_sock, F_SETOWN, getpid()); > 通过 MSG_OOB 可选项传递数据时只返回 1 个字节,而且也不快 -的确,通过 MSG_OOB 并不会加快传输速度,而通过信号处理函数 urg_handler 也只能读取一个字节。剩余数据只能通过未设置 MSG_OOB 可选项的普通输入函数读取。因为 TCP 不存在真正意义上的「外带数据」。实际上,MSG_OOB 中的 OOB 指的是 Out-of-band ,而「外带数据」的含义是: +的确,通过 MSG_OOB 并不会加快传输速度,而通过信号处理函数 urg_handler 也只能读取一个字节。剩余数据只能通过未设置 MSG_OOB 可选项的普通输入函数读取。因为 TCP 不存在真正意义上的「带外数据」。实际上,MSG_OOB 中的 OOB 指的是 Out-of-band ,而「带外数据」的含义是: > 通过完全不同的通信路径传输的数据 @@ -3730,7 +3748,7 @@ MSG_OOB 的真正意义在于督促数据接收对象尽快处理数据。这是 send(sock, "890", strlen("890"), MSG_OOB); ``` -图上是调用这个函数的缓冲状态。如果缓冲最左端的位置视作偏移量 0 。字符 0 保存于偏移量 2 的位置。另外,字符 0 右侧偏移量为 3 的位置存有紧急指针(Urgent Pointer)。紧急指针指向紧急消息的下一个位置(偏移量加一),同时向对方主机传递以下信息: +图上是调用这个函数的缓冲状态。如果将缓冲最左端的位置视作偏移量 0,字符 '0' 保存于偏移量 2 的位置。另外,字符 '0' 右侧偏移量为 3 的位置即为紧急指针(Urgent Pointer)所指向的位置。紧急指针指向紧急消息的下一个位置(偏移量加一),同时向对方主机传递以下信息: > 紧急指针指向的偏移量为 3 之前的部分就是紧急消息。 @@ -3745,14 +3763,14 @@ TCP 数据包实际包含更多信息。TCP 头部包含如下两种信息: 指定 MSG_OOB 选项的数据包本身就是紧急数据包,并通过紧急指针表示紧急消息所在的位置。 -紧急消息的意义在于督促消息处理,而非紧急传输形式受限的信息。 +紧急消息的意义在于督促接收方尽快处理数据,而非指紧急传输的信息本身在形式上受限。 #### 13.1.4 检查输入缓冲 同时设置 MSG_PEEK 选项和 MSG_DONTWAIT 选项,以验证输入缓冲是否存在接收的数据。设置 MSG_PEEK 选项并调用 recv 函数时,即使读取了输入缓冲的数据也不会删除。因此,该选项通常与 MSG_DONTWAIT 配合,用于以非阻塞方式验证待读数据存在与否。下面的示例是二者的含义: -- [peek_recv.c](ch13/peek_recv.c) -- [peek_send.c](ch13/peek_send.c) +- [peek_recv.c](peek_recv.c) +- [peek_send.c](peek_send.c) 编译运行: @@ -3777,14 +3795,14 @@ readv & writev 函数的功能可概括如下: > 对数据进行整合传输及发送的函数 -也就是说,通过 writev 函数可以将分散保存在多个缓冲中的数据一并发送,通过 readv 函数可以由多个缓冲分别接收。因此,适用这 2 个函数可以减少 I/O 函数的调用次数。下面先介绍 writev 函数。 +也就是说,通过 writev 函数可以将分散保存在多个缓冲中的数据一并发送,通过 readv 函数可以由多个缓冲分别接收。因此,使用这 2 个函数可以减少 I/O 函数的调用次数。下面先介绍 writev 函数。 ```c #include ssize_t writev(int filedes, const struct iovec *iov, int iovcnt); /* 成功时返回发送的字节数,失败时返回 -1 -filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 read 一样向其传递文件或标准输出描述符. +filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 read 一样向其传递文件或标准输出描述符。 iov: iovec 结构体数组的地址值,结构体 iovec 中包含待发送数据的位置和大小信息 iovcnt: 向第二个参数传递数组长度 */ @@ -3804,11 +3822,11 @@ struct iovec ![](images/5c4c61b07d207.png) -writev 的第一个参数,是文件描述符,因此向控制台输出数据,ptr 是存有待发送数据信息的 iovec 数组指针。第三个参数为 2,因此,从 ptr 指向的地址开始,共浏览 2 个 iovec 结构体变量,发送这些指针指向的缓冲数据。 +writev 的第一个参数是文件描述符,本例中传入 1(标准输出),因此数据会输出到控制台。ptr 是存有待发送数据信息的 iovec 数组指针;第三个参数为 2,表示从 ptr 指向的地址开始,共读取 2 个 iovec 结构体变量,并将这些指针指向的缓冲数据一并发送。 下面是 writev 函数的使用方法: -- [writev.c](ch13/writev.c) +- [writev.c](writev.c) ```c #include @@ -3846,14 +3864,14 @@ ABC1234 Write bytes: 7 ``` -下面介绍 readv 函数,功能和 writev 函数正好相反.函数为: +下面介绍 readv 函数,功能和 writev 函数正好相反。函数为: ```c #include ssize_t readv(int filedes, const struct iovec *iov, int iovcnt); /* 成功时返回接收的字节数,失败时返回 -1 -filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 write 一样向其传递文件或标准输出描述符. +filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 write 一样向其传递文件或标准输出描述符。 iov: iovec 结构体数组的地址值,结构体 iovec 中包含待数据保存的位置和大小信息 iovcnt: 第二个参数中数组的长度 */ @@ -3861,7 +3879,7 @@ iovcnt: 第二个参数中数组的长度 下面是示例代码: -- [readv.c](ch13/readv.c) +- [readv.c](readv.c) ```c #include @@ -3908,7 +3926,7 @@ gcc readv.c -o rv #### 13.2.2 合理使用 readv & writev 函数 -实际上,能使用该函数的所有情况都适用。例如,需要传输的数据分别位于不同缓冲(数组)时,需要多次调用 write 函数。此时可通过 1 次 writev 函数调用替代操作,当然会提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用 read 函数,而是利用 1 次 readv 函数就能大大提高效率。 +实际上,凡是适合使用 readv/writev 的场景,都建议使用它们。例如,需要传输的数据分别位于不同缓冲(数组)时,需要多次调用 write 函数。此时可通过 1 次 writev 函数调用替代操作,当然会提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用 read 函数,而是利用 1 次 readv 函数就能大大提高效率。 其意义在于减少数据包个数。假设为了提高效率在服务器端明确禁用了 Nagle 算法。其实 writev 函数在不采用 Nagle 算法时更有价值,如图: @@ -3976,11 +3994,11 @@ ioctlsocket(sock, FIONBIO, &mode); 1. **下列关于 MSG_OOB 可选项的说法错误的是**? - 答:以下加粗的字体代表说法正确。 + 答:以下加粗的字体代表说法正确。说法错误的是第 2、3 项。 - 1. MSG_OOB 指传输 Out-of-band 数据,是通过其他路径高速传输数据 + 1. **MSG_OOB 指传输 Out-of-band 数据,是通过其他路径高速传输数据** 2. MSG_OOB 指通过其他路径高速传输数据,因此 TCP 中设置该选项的数据先到达对方主机 - 3. **设置 MSG_OOB 是数据先到达对方主机后,以普通数据的形式和顺序读取。也就是说,只是提高了传输速度,接收方无法识别这一点**。 + 3. 设置 MSG_OOB 是数据先到达对方主机后,以普通数据的形式和顺序读取。也就是说,只是提高了传输速度,接收方无法识别这一点。 4. **MSG_OOB 无法脱离 TCP 的默认数据传输方式,即使脱离了 MSG_OOB ,也会保持原有的传输顺序。该选项只用于要求接收方紧急处理**。 2. **利用 readv & writev 函数收发数据有何优点?分别从函数调用次数和 I/O 缓冲的角度给出说明**。 @@ -4009,27 +4027,28 @@ ioctlsocket(sock, FIONBIO, &mode); // 无数据或出错 } ``` + ## 第 14 章 多播与广播 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 ### 14.1 多播 -多播(Multicast)方式的数据传输是基于 UDP 完成的。因此 ,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。 +多播(Multicast)方式的数据传输是基于 UDP 完成的。因此,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。 #### 14.1.1 多播的数据传输方式以及流量方面的优点 多播的数据传输特点可整理如下: - 多播服务器端针对特定多播组,只发送 1 次数据。 -- 即使只发送 1 次数据,但该组内的所有客户端都会接收数据 +- 即使只发送 1 次数据,该组内的所有客户端也都会接收数据。 - 多播组数可以在 IP 地址范围内任意增加 多播组是 D 类IP地址(224.0.0.0~239.255.255.255),「加入多播组」可以理解为通过程序完成如下声明: > 在 D 类IP地址中,我希望接收发往目标 239.234.218.234 的多播数据 -多播是基于 UDP 完成的,也就是说,多播数据包的格式与 UDP 数据包相同。只是与一般的 UDP 数据包不同。向网络传递 1 个多播数据包时,路由器将复制该数据包并传递到多个主机。像这样,多播需要借助路由器完成。如图所示: +多播是基于 UDP 完成的,也就是说,多播数据包的格式与 UDP 数据包相同。只是与一般的 UDP 数据包不同,向网络传递 1 个多播数据包时,路由器将复制该数据包并传递到多个主机。像这样,多播需要借助路由器完成。如图所示: ![](images/5c4d310daa6be.png) @@ -4039,7 +4058,7 @@ ioctlsocket(sock, FIONBIO, &mode); #### 14.1.2 路由(Routing)和 TTL(Time to Live,生存时间),以及加入组的办法 -为了传递多播数据包,必须设置 TTL 。TTL 是 Time to Live的简写,是决定「数据包传递距离」的主要因素。TTL 用整数表示,并且每经过一个路由器就减一。TTL 变为 0 时,该数据包就无法再被传递,只能销毁。因此,TTL 的值设置过大将影响网络流量。当然,设置过小,也无法传递到目标。 +为了传递多播数据包,必须设置 TTL。TTL 是 Time to Live 的简写,是决定「数据包传递距离」的主要因素。TTL 用整数表示,并且每经过一个路由器就减一。TTL 变为 0 时,该数据包就无法再被传递,只能销毁。因此,TTL 的值设置过大将影响网络流量。当然,设置过小,也无法传递到目标。 ![](images/5c4d3960001eb.png) @@ -4087,8 +4106,8 @@ struct ip_mreq 下面是两个代码: -- [news_sender.c](ch14/news_sender.c) -- [news_receiver.c](ch14/news_receiver.c) +- [news_sender.c](news_sender.c) +- [news_receiver.c](news_receiver.c) 编译运行: @@ -4103,11 +4122,11 @@ gcc news_receiver.c -o receiver ![](images/5c4e85a9aabcc.png) -通过结果可以看出,使用 sender 多播信息,通过 receiver 接收广播,如果延迟运行 receiver 将无法接受之前发送的信息。 +通过结果可以看出,使用 sender 多播信息,通过 receiver 接收多播数据,如果延迟运行 receiver 将无法接收之前发送的信息。 ### 14.2 广播 -广播(Broadcast)在「一次性向多个主机发送数据」这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接受数据。相反,广播只能向同一网络中的主机传输数据。 +广播(Broadcast)在「一次性向多个主机发送数据」这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。 #### 14.2.1 广播的理解和实现方法 @@ -4120,7 +4139,7 @@ gcc news_receiver.c -o receiver 反之,本地广播中使用的IP地址限定为 255.255.255.255 。例如,192.32.24 网络中的主机向 255.255.255.255 传输数据时,数据将传输到 192.32.24 网络中所有主机。 -**数据通信中使用的IP地址是与 UDP 示例的唯一区别。默认生成的套接字会阻止广播,因此,只需通过如下代码更改默认设置。** +**广播与普通 UDP 示例的区别在于:目标 IP 地址使用广播地址,且需通过 SO_BROADCAST 选项启用广播。默认生成的套接字会阻止广播,因此需要通过如下代码更改默认设置。** ```c int send_sock; @@ -4132,12 +4151,12 @@ setsockopt(send_sock,SOL_SOCKET,SO_BROADCAST,(void*)&bcast,sizeof(bcast)); ... ``` -### 14.2.2 实现广播数据的 Sender 和 Receiver +#### 14.2.2 实现广播数据的 Sender 和 Receiver 下面是广播数据的 Sender 和 Receiver的代码: -- [news_sender_brd.c](ch14/news_sender_brd.c) -- [news_receiver_brd.c](ch14/news_receiver_brd.c) +- [news_sender_brd.c](news_sender_brd.c) +- [news_receiver_brd.c](news_receiver_brd.c) 编译运行: @@ -4212,7 +4231,7 @@ setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (char*)&bcast, sizeof(bcast)); 1. 头文件:Windows 使用 `winsock2.h`,Linux 使用 `sys/socket.h` 等头文件 2. 初始化:Windows 需要调用 `WSAStartup` 初始化 Winsock,使用完后调用 `WSACleanup` -3. 套接字类型:Windows 使用 `SOCKET` 类型(实际是 `unsigned __int64`),Linux 使用 `int` +3. 套接字类型:Windows 使用 `SOCKET` 类型(`UINT_PTR`,32 位系统为 `unsigned int`,64 位系统为 `unsigned __int64`),Linux 使用 `int` 4. 关闭套接字:Windows 使用 `closesocket`,Linux 使用 `close` 5. 指针类型转换:Windows 下 `setsockopt` 的第四个参数通常转换为 `char*`,Linux 下转换为 `void*` @@ -4226,24 +4245,30 @@ setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (char*)&bcast, sizeof(bcast)); 2. **多播与广播的异同点是什么?请从数据通信的角度进行说明**。 - 答:在「一次性向多个主机发送数据」这一点上与多播类似,但传输的数据范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接受数据。相反,广播只能向同一网络中的主机传输数据。 + 答:在「一次性向多个主机发送数据」这一点上与多播类似,但传输的数据范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。 -3. **下面关于多播的说法描述错误的是**? +3. **下面关于多播的说法描述错误的是?** + + 1. 多播是用来向加入多播组的所有主机传输数据的协议 + 2. 主机连接到同一网络才能加入到多播组,也就是说,多播组无法跨越多个网络 + 3. 能够加入多播组的主机数并无限制,但只能有 1 个主机(Sender)向该组发送数据 + 4. 多播时使用的套接字是 UDP 套接字,因为多播是基于 UDP 进行数据通信的 答:第 2 项描述错误。正确说明如下: 1. 多播是用来向加入多播组的所有主机传输数据的协议 2. ~~主机连接到同一网络才能加入到多播组,也就是说,多播组无法跨越多个网络~~(错误。多播可以跨越多个网络,只要路由器支持多播功能,主机就可以加入跨网络的多播组。即使路由器不支持,也可以通过隧道技术实现。) - 3. 能够加入多播组的主机数并无限制,但只能有 1 个主机(Sender)向该组发送数据 + 3. 能够加入多播组的主机数并无限制,向该组发送数据的主机数也无限制(任何主机均可向多播组地址发送数据,且无需先加入该组) 4. 多播时使用的套接字是 UDP 套接字,因为多播是基于 UDP 进行数据通信的 4. **多播也对网络流量有利,请比较 TCP 交换方式解释其原因** - 答:TCP 是必须建立一对一的连接,如果要向1000个主机发送文件,就得传递1000次。但是此时用多播方式传输数据,就只需要发送一次。 + 答:TCP 必须建立一对一的连接,如果要向 1000 个主机发送文件,就得传递 1000 次。但是此时用多播方式传输数据,就只需要发送一次。 5. **多播方式的数据通信需要 MBone 虚拟网络。换言之,MBone 是用于多播的网络,但它是虚拟网络。请解释此处的「虚拟网络」** 答:可以理解为「通过网络中的特殊协议工作的软件概念上的网络」。也就是说,MBone 并非可以触及的物理网络。它是以物理网络为基础,通过软件方法(隧道技术)实现的多播通信必备虚拟网络。MBone(Multicast Backbone)是互联网的多播骨干网,通过在支持多播的路由器之间建立隧道,将不支持多播的网络连接起来,从而构建一个覆盖全球的虚拟多播网络。 + ## 第 15 章 套接字和标准I/O 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -4261,7 +4286,7 @@ setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (char*)&bcast, sizeof(bcast)); ![](images/5c500e53ad9aa.png) -假设使用 fputs 函数进行传输字符串 「Hello」时,首先将数据传递到标准 I/O 缓冲,然后将数据移动到套接字输出缓冲,最后将字符串发送到对方主机。 +假设使用 fputs 函数传输字符串「Hello」时,首先将数据传递到标准 I/O 缓冲,然后将数据移动到套接字输出缓冲,最后将字符串发送到对方主机。 设置缓冲的主要目的是为了提高性能。从以下两点可以说明性能的提高: @@ -4277,13 +4302,13 @@ setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (char*)&bcast, sizeof(bcast)); 下面是利用系统函数的示例: -- [syscpy.c](ch15/syscpy.c) +- [syscpy.c](syscpy.c) 下面是使用标准 I/O 函数复制文件 -- [stdcpy.c](ch15/stdcpy.c) +- [stdcpy.c](stdcpy.c) -对于以上两个代码进行测试,明显基于标准 I/O 函数的代码跑的更快。这是因为标准 I/O 函数通过缓冲区减少了系统调用的次数,每次系统调用都有一定的开销(用户态与内核态的切换),而缓冲机制可以将多次小数据量的 I/O 操作合并为较少次数的系统调用,从而提高性能。 +对于以上两个代码进行测试,明显基于标准 I/O 函数的代码跑得更快。这是因为标准 I/O 函数通过缓冲区减少了系统调用的次数,每次系统调用都有一定的开销(用户态与内核态的切换),而缓冲机制可以将多次小数据量的 I/O 操作合并为较少次数的系统调用,从而提高性能。 #### 15.1.3 标准 I/O 函数的几个缺点 @@ -4291,7 +4316,7 @@ setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (char*)&bcast, sizeof(bcast)); - 不容易进行双向通信 - 有时可能频繁调用 fflush 函数 -- 需要以 FILE 结构体指针的形式返回文件描述符。 +- 需要将文件描述符转换为 FILE 结构体指针才能使用。 ### 15.2 使用标准 I/O 函数 @@ -4311,7 +4336,7 @@ mode : 将要创建的 FILE 结构体指针的模式信息 以下为示例: -- [desto.c](ch15/desto.c) +- [desto.c](desto.c) ```c #include @@ -4361,7 +4386,7 @@ int fileno(FILE *stream); 示例: -- [todes.c](ch15/todes.c) +- [todes.c](todes.c) ```c #include @@ -4392,8 +4417,8 @@ int main() 代码如下: -- [echo_client.c](ch15/echo_client.c) -- [echo_stdserv.c](ch15/echo_stdserv.c) +- [echo_client.c](echo_client.c) +- [echo_stdserv.c](echo_stdserv.c) 编译运行: @@ -4412,7 +4437,7 @@ gcc echo_stdserv.c -o eserver > 以下答案仅代表本人个人观点,可能不是正确答案。 -1. **请说明标准 I/O 的 2 个优点。他为何拥有这 2 个优点?** +1. **请说明标准 I/O 的 2 个优点。它为何拥有这 2 个优点?** 答:①具有很高的移植性②有良好的缓冲提高性能。 @@ -4434,13 +4459,14 @@ gcc echo_stdserv.c -o eserver fputs("Hello", fp); fflush(fp); // 强制将缓冲区数据发送 ``` + ## 第 16 章 关于 I/O 流分离的其他内容 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 ### 16.1 分离 I/O 流 -「分离 I/O 流」是一种常用表达。有 I/O 工具可区分二者,无论采用哪种方法,都可以认为是分离了 I/O 流。 +「分离 I/O 流」是一种常用表达。只要用 I/O 工具区分输入和输出,无论采用哪种方法,都可以认为是分离了 I/O 流。 #### 16.1.1 2 次 I/O 流分离 @@ -4470,12 +4496,12 @@ gcc echo_stdserv.c -o eserver shutdown(sock,SHUT_WR); ``` -当时说过调用 shutdown 函数的基于半关闭的 EOF 传递方法。第十章的 [echo_mpclient.c](ch10/echo_mpclient.c) 添加了半关闭的相关代码。但是还没有讲采用 fdopen 函数怎么半关闭。那么是否是通过 fclose 函数关闭流呢?我们先试试 +当时说过调用 shutdown 函数的基于半关闭的 EOF 传递方法。第十章的 [echo_mpclient.c](../ch10/echo_mpclient.c) 添加了半关闭的相关代码。但是还没有讲采用 fdopen 函数怎么半关闭。那么是否是通过 fclose 函数关闭流呢?我们先试试 -下面是服务端和客户端码: +下面是服务端和客户端代码: -- [sep_clnt.c](ch16/sep_clnt.c) -- [sep_serv.c](ch16/sep_serv.c) +- [sep_clnt.c](sep_clnt.c) +- [sep_serv.c](sep_serv.c) 编译运行: @@ -4496,17 +4522,17 @@ gcc sep_serv.c -o serv ### 16.2 文件描述符的复制和半关闭 -#### 16.2.1 终止「流」时无法半关闭原因 +#### 16.2.1 终止「流」时无法半关闭的原因 -下面的图描述的是服务端代码中的两个FILE 指针、文件描述符和套接字中的关系。 +下面的图描述的是服务端代码中的两个 FILE 指针、文件描述符和套接字之间的关系。 ![](images/5c5121da89955.png) -从图中可以看到,两个指针都是基于同一文件描述符创建的。因此,针对于任何一个 FILE 指针调用 fclose 函数都会关闭文件描述符,如图所示: +从图中可以看到,两个指针都是基于同一文件描述符创建的。因此,针对任何一个 FILE 指针调用 fclose 函数都会关闭文件描述符,如图所示: ![](images/5c51224051802.png) -从图中看到,销毁套接字时再也无法进行数据交换。那如何进入可以进入但是无法输出的半关闭状态呢?如下图所示: +从图中看到,销毁套接字时再也无法进行数据交换。那如何进入可以接收输入但无法输出的半关闭状态呢?如下图所示: ![](images/5c5122a45c5f1.png) @@ -4522,7 +4548,7 @@ gcc sep_serv.c -o serv #### 16.2.2 复制文件描述符 -与调用 fork 函数不同,调用 fork 函数将复制整个进程,此处讨论的是同一进程内完成对文件描述符的复制。如图: +与 fork 函数不同,fork 会复制整个进程,而此处讨论的是在同一进程内复制文件描述符。如图: ![](images/5c512579c45b6.png) @@ -4543,9 +4569,9 @@ fd2 : 明确指定的文件描述符的整数值 */ ``` -dup2 函数明确指定复制的文件描述符的整数值。向其传递大于 0 且小于进程能生成的最大文件描述符值时,该值将成为复制出的文件描述符值。下面是代码示例: +dup2 函数明确指定复制的文件描述符的整数值。向其传递大于等于 0 且小于进程允许的最大文件描述符值时,该值将成为复制出的文件描述符值。下面是代码示例: -- [dup.c](ch16/dup.c) +- [dup.c](dup.c) ```c #include @@ -4576,7 +4602,7 @@ int main(int argc, char *argv[]) 编译运行: -``` +```shell gcc dup.c -o dup ./dup ``` @@ -4587,13 +4613,13 @@ gcc dup.c -o dup #### 16.2.4 复制文件描述符后「流」的分离 -下面更改 [sep_clnt.c](ch16/sep_clnt.c) 和 [sep_serv.c](ch16/sep_serv.c) 可以使得让它正常工作,正常工作是指通过服务器的半关闭状态接收客户端最后发送的字符串。 +下面更改 [sep_clnt.c](sep_clnt.c) 和 [sep_serv.c](sep_serv.c) 使其正常工作,这里的正常工作是指让服务器通过半关闭状态接收客户端最后发送的字符串。 下面是代码: -- [sep_serv2.c](ch16/sep_serv2.c) +- [sep_serv2.c](sep_serv2.c) -这个代码可以与 [sep_clnt.c](ch16/sep_clnt.c) 配合起来使用,编译过程和上面一样,运行结果为: +这个代码可以与 [sep_clnt.c](sep_clnt.c) 配合起来使用,编译过程和上面一样,运行结果为: ![](images/5c513d54a27e0.png) @@ -4619,13 +4645,14 @@ gcc dup.c -o dup - 第 2 项正确:即使未终止文件描述符,调用 shutdown 函数关闭输出流时也会发送 EOF,这正是半关闭的实现方式。 - 第 3 项错误:即使复制了文件描述符,也可以通过 shutdown 函数单独关闭输出流并发送 EOF,而不需要关闭所有文件描述符。 - 第 4 项正确:即使复制了文件描述符,shutdown 函数仍然可以对套接字本身进行半关闭操作,不受文件描述符复制的影响。 + ## 第 17 章 优于 select 的 epoll 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 ### 17.1 epoll 理解及应用 -select 复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时介入上百个客户端。这种 select 方式并不适合以 web 服务器端开发为主流的现代开发环境,所以需要学习 Linux 环境下的 epoll +select 复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端。这种 select 方式并不适合以 web 服务器端开发为主流的现代开发环境,所以需要学习 Linux 环境下的 epoll #### 17.1.1 基于 select 的 I/O 复用技术速度慢的原因 @@ -4634,9 +4661,9 @@ select 复用方法由来已久,因此,利用该技术后,无论如何优 - 调用 select 函数后常见的针对所有文件描述符的循环语句 - 每次调用 select 函数时都需要向该函数传递监视对象信息 -上述两点可以从 [echo_selectserv.c](ch12/echo_selectserv.c) 得到确认,调用 select 函数后,并不是把发生变化的文件描述符单独集中在一起,而是通过作为监视对象的 fd_set 变量的变化,找出发生变化的文件描述符(54,56行),因此无法避免针对所有监视对象的循环语句。而且,作为监视对象的 fd_set 会发生变化,所以调用 select 函数前应该复制并保存原有信息,并在每次调用 select 函数时传递新的监视对象信息。 +上述两点可以从 [echo_selectserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/echo_selectserv.c) 得到确认,调用 select 函数后,并不是把发生变化的文件描述符单独集中在一起,而是通过作为监视对象的 fd_set 变量的变化,找出发生变化的文件描述符(54,56行),因此无法避免针对所有监视对象的循环语句。而且,作为监视对象的 fd_set 会发生变化,所以调用 select 函数前应该复制并保存原有信息,并在每次调用 select 函数时传递新的监视对象信息。 -select 性能上最大的弱点是:每次传递监视对象信息,准确的说,select 是监视套接字变化的函数。而套接字是操作系统管理的,所以 select 函数要借助操作系统才能完成功能。select 函数的这一缺点可以通过如下方式弥补: +select 性能上最大的弱点是:每次传递监视对象信息,准确地说,select 是监视套接字变化的函数。而套接字是操作系统管理的,所以 select 函数要借助操作系统才能完成功能。select 函数的这一缺点可以通过如下方式弥补: > 仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项 @@ -4664,7 +4691,7 @@ select 的兼容性比较高,这样就可以支持很多的操作系统,不 select 函数中为了保存监视对象的文件描述符,直接声明了 fd_set 变量,但 epoll 方式下的操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时用的函数就是 epoll_create 。 -此外,为了添加和删除监视对象文件描述符,select 方式中需要 FD_SET、FD_CLR 函数。但在 epoll 方式中,通过 epoll_ctl 函数请求操作系统完成。最后,select 方式下调用 select 函数等待文件描述符的变化,而 epoll_wait 调用 epoll_wait 函数。还有,select 方式中通过 fd_set 变量查看监视对象的状态变化,而 epoll 方式通过如下结构体 epoll_event 将发生变化的文件描述符单独集中在一起。 +此外,为了添加和删除监视对象文件描述符,select 方式中需要 FD_SET、FD_CLR 函数。但在 epoll 方式中,通过 epoll_ctl 函数请求操作系统完成。最后,select 方式下调用 select 函数等待文件描述符的变化,而 epoll 方式下调用 epoll_wait 函数。还有,select 方式中通过 fd_set 变量查看监视对象的状态变化,而 epoll 方式通过如下结构体 epoll_event 将发生变化的文件描述符单独集中在一起。 ```c struct epoll_event @@ -4681,7 +4708,7 @@ typedef union epoll_data { ``` -声明足够大的 epoll_event 结构体数组候,传递给 epoll_wait 函数时,发生变化的文件描述符信息将被填入数组。因此,无需像 select 函数那样针对所有文件描述符进行循环。 +声明足够大的 epoll_event 结构体数组后,传递给 epoll_wait 函数时,发生变化的文件描述符信息将被填入数组。因此,无需像 select 函数那样针对所有文件描述符进行循环。 #### 17.1.4 epoll_create @@ -4744,7 +4771,7 @@ epoll_ctl(A,EPOLL_CTL_DEL,B,NULL); > 从 epoll 例程 A 中删除文件描述符 B -从上述示例中可以看出,从监视对象中删除时,不需要监视类型,因此向第四个参数可以传递为 NULL +从上述示例中可以看出,从监视对象中删除时,不需要监视类型,因此第四个参数可以传递 NULL 下面是第二个参数的含义: @@ -4752,7 +4779,7 @@ epoll_ctl(A,EPOLL_CTL_DEL,B,NULL); - EPOLL_CTL_DEL:从 epoll 例程中删除文件描述符 - EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况 -epoll_event 结构体用于保存事件的文件描述符结合。但也可以在 epoll 例程中注册文件描述符时,用于注册关注的事件。该函数中 epoll_event 结构体的定义并不显眼,因此通过调用语句说明该结构体在 epoll_ctl 函数中的应用。 +epoll_event 结构体用于保存发生事件的文件描述符集合。但也可以在 epoll 例程中注册文件描述符时,用于注册关注的事件。该函数中 epoll_event 结构体的定义并不显眼,因此通过调用语句说明该结构体在 epoll_ctl 函数中的应用。 ```c struct epoll_event event; @@ -4807,9 +4834,9 @@ event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1); #### 17.1.7 基于 epoll 的回声服务器端 -下面是回声服务器端的代码(修改自第 12 章 [echo_selectserv.c](ch12/echo_selectserv.c)): +下面是回声服务器端的代码(修改自第 12 章 [echo_selectserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/echo_selectserv.c)): -- [echo_epollserv.c](ch17/echo_epollserv.c) +- [echo_epollserv.c](echo_epollserv.c) 编译运行: @@ -4824,12 +4851,12 @@ gcc echo_epollserv.c -o serv 可以看出运行结果和以前 select 实现的和 fork 实现的结果一样,都可以支持多客户端同时运行。 -但是这里运用了 epoll 效率高于 select +但是这里运用了 epoll,效率高于 select。 总结一下 epoll 的流程: 1. epoll_create 创建一个保存 epoll 文件描述符的空间(size 参数仅作为建议传递) -2. 动态分配内存,给将要监视的 epoll_wait +2. 动态分配内存,用于保存 epoll_wait 返回的事件 3. 利用 epoll_ctl 控制 添加 删除,监听事件 4. 利用 epoll_wait 来获取改变的文件描述符,来执行程序 @@ -4856,9 +4883,9 @@ select 和 epoll 的区别: #### 17.2.2 掌握条件触发的事件特性 -下面代码修改自 [echo_epollserv.c](ch17/echo_epollserv.c) 。epoll 默认以条件触发的方式工作,因此可以通过该示例验证条件触发的特性。 +下面代码修改自 [echo_epollserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch17/echo_epollserv.c) 。epoll 默认以条件触发的方式工作,因此可以通过该示例验证条件触发的特性。 -- [echo_EPLTserv.c](ch17/echo_EPLTserv.c) +- [echo_EPLTserv.c](echo_EPLTserv.c) 上面的代码把调用 read 函数时使用的缓冲大小缩小到了 4 个字节,插入了验证 epoll_wait 调用次数的验证函数。减少缓冲大小是为了阻止服务器端一次性读取接收的数据。换言之,调用 read 函数后,输入缓冲中仍有数据要读取,而且会因此注册新的事件并从 epoll_wait 函数返回时将循环输出「return epoll_wait」字符串。 @@ -4883,7 +4910,7 @@ gcc echo_EPLTserv.c -o serv 代码: -- [echo_EDGEserv.c](ch17/echo_EDGEserv.c) +- [echo_EDGEserv.c](echo_EDGEserv.c) 编译运行: @@ -4905,7 +4932,7 @@ gcc echo_EDGEserv.c -o serv - 通过 errno 变量验证错误原因 - 为了完成非阻塞(Non-blocking)I/O ,更改了套接字特性。 -Linux 套接字相关函数一般通过 -1 通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误的时候提额外的信息,Linux 声明了如下全局变量: +Linux 套接字相关函数一般通过 -1 通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误的时候提供额外的信息,Linux 声明了如下全局变量: ```c int errno; @@ -4915,7 +4942,7 @@ int errno; > read 函数发现输入缓冲中没有数据可读时返回 -1,同时在 errno 中保存 EAGAIN 常量 -下面是 Linux 中提供的改变和更改文件属性的办法: +下面是 Linux 中提供的更改文件属性的方法: ```c #include @@ -4934,7 +4961,7 @@ int flag = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flag | O_NONBLOCK); ``` -通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞 O_NONBLOCK 标志。调用 read/write 函数时,无论是否存在数据,都会形成非阻塞文件(套接字)。fcntl 函数的适用范围很广。 +通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞 O_NONBLOCK 标志。调用 read/write 函数时,无论是否存在数据,都会以非阻塞方式操作文件(套接字)。fcntl 函数的适用范围很广。 #### 17.2.4 实现边缘触发回声服务器端 @@ -4948,13 +4975,13 @@ fcntl(fd, F_SETFL, flag | O_NONBLOCK); 下面是以边缘触发方式工作的回声服务端代码: -- [echo_EPETserv.c](ch17/echo_EPETserv.c) +- [echo_EPETserv.c](echo_EPETserv.c) 编译运行: ```shell gcc echo_EPETserv.c -o serv -./serv +./serv 9190 ``` 结果: @@ -5004,7 +5031,7 @@ gcc echo_EPETserv.c -o serv 3. select 方式和 epoll 方式的最大差异在于监视对象文件描述符传递给操作系统的方式。请说明具体差异,并解释为何存在这种差异。 - 答:select 函数每次调用都要传递所有的监视对象信息,而 epoll 函数仅向操作系统传递 1 次监视对象,监视范围或内容发生变化时只通知发生变化的事项。select 采用这种方法是为了保持兼容性。 + 答:select 函数每次调用都要传递所有的监视对象信息,而 epoll 函数仅向操作系统传递 1 次监视对象,监视范围或内容发生变化时只通知发生变化的事项。存在这种差异是因为 epoll 在内核中维护了持久的监视对象列表(epoll 例程),只需注册一次;而 select 没有内核侧的持久状态,每次调用都需重新传递并还原监视对象信息。 4. 虽然 epoll 是 select 的改进方案,但 select 也有自己的优点。在何种情况下使用 select 更加合理。 @@ -5016,7 +5043,8 @@ gcc echo_EPETserv.c -o serv 6. 采用边缘触发时可以分离数据的接收和处理时间点。请说明其优点和原因。 - 答:分离接收数据和处理数据的时间点,给服务端的实现带来很大灵活性。 + 答:边缘触发方式下,输入缓冲收到数据时只通知一次事件,服务器可在通知后用非阻塞 read 一次性读出全部数据(直到返回 EAGAIN),而不必在每次数据到达时立即处理。因此服务器可以先接收数据,再选择合适时机处理和转发,给服务端实现带来很大灵活性。 + ## 第 18 章 多线程服务器端的实现 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -5036,7 +5064,7 @@ gcc echo_EPETserv.c -o serv 只有一个 CPU 的系统是将时间分成多个微小的块后分配给了多个进程。为了分时使用 CPU ,需要「上下文切换」的过程。「上下文切换」是指运行程序前需要将相应进程信息读入内存,如果运行进程 A 后紧接着需要运行进程 B ,就应该将进程 A 相关信息移出内存(或保存到寄存器),并读入进程 B 相关信息。这就是上下文切换。上下文切换需要保存和恢复进程的上下文信息(寄存器、程序计数器、栈指针等),这个过程会带来一定的开销,即使通过优化加快速度,也会存在一定的局限。 -为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入的线程(Thread)的概念。这是为了将进程的各种劣势降至最低程度(不是直接消除)而设立的一种「轻量级进程」。线程比进程具有如下优点: +为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入了线程(Thread)的概念。这是为了将进程的各种劣势降至最低程度(不是直接消除)而设立的一种「轻量级进程」。线程比进程具有如下优点: - 线程的创建和上下文切换比进程的创建和上下文切换更快 - 线程间交换数据无需特殊技术 @@ -5045,7 +5073,7 @@ gcc echo_EPETserv.c -o serv 线程是为了解决:为了得到多条代码执行流而复制整个内存区域的负担太重。 -每个进程的内存空间都由保存全局变量的「数据区」、向 malloc 等函数动态分配提供空间的堆(Heap)、函数运行时间使用的栈(Stack)构成。每个进程都有独立的这种空间,多个进程的内存结构如图所示: +每个进程的内存空间都由保存全局变量的「数据区」、向 malloc 等函数动态分配提供空间的堆(Heap)、函数运行时使用的栈(Stack)构成。每个进程都有独立的这种空间,多个进程的内存结构如图所示: ![](images/5c55aa57db3c7.png) @@ -5099,7 +5127,7 @@ arg : 通过第三个参数传递的调用函数时包含传递参数信息的 下面通过简单示例了解该函数功能: -- [thread1.c](ch18/thread1.c) +- [thread1.c](thread1.c) ```c #include @@ -5165,7 +5193,7 @@ status : 保存线程的 main 函数返回值的指针的变量地址值 作用就是调用该函数的进程(或线程)将进入等待状态,直到第一个参数为 ID 的线程终止为止。而且可以得到线程的 main 函数的返回值。下面是该函数的用法代码: -- [thread2.c](ch18/thread2.c) +- [thread2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread2.c) ```c #include @@ -5229,13 +5257,13 @@ gcc thread2.c -o tr2 -lpthread #### 18.2.2 可在临界区内调用的函数 -在同步的程序设计中,临界区块(Critical section)指的是一个访问共享资源(例如:共享设备或是共享存储器)的程序片段,而这些共享资源有无法同时被多个线程访问的特性。 +在同步的程序设计中,临界区(Critical section)指的是一个访问共享资源(例如:共享设备或是共享存储器)的程序片段,而这些共享资源有无法同时被多个线程访问的特性。 -当有线程进入临界区块时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区块的进入点与离开点实现,以确保这些共享资源是被异或的使用,例如:semaphore。 +当有线程进入临界区块时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区块的进入点与离开点实现,以确保这些共享资源被互斥地使用,例如:信号量(semaphore)。 只能被单一线程访问的设备,例如:打印机。 -一个最简单的实现方法就是当线程(Thread)进入临界区块时,禁止改变处理器;在uni-processor系统上,可以用“禁止中断(CLI)”来完成,避免发生系统调用(System Call)导致的上下文交换(Context switching);当离开临界区块时,处理器恢复原先的状态。 +一个最简单的实现方法就是当线程(Thread)进入临界区块时,禁止改变处理器;在uni-processor系统上,可以用“禁止中断(CLI)”来完成,避免发生系统调用(System Call)导致的上下文切换(Context switching);当离开临界区块时,处理器恢复原先的状态。 根据临界区是否引起问题,函数可以分为以下 2 类: @@ -5278,7 +5306,7 @@ gcc -D_REENTRANT mythread.c -o mthread -lpthread 下面是代码: -- [thread3.c](ch18/thread3.c) +- [thread3.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread3.c) ```c #include @@ -5324,11 +5352,11 @@ gcc thread3.c -D_REENTRANT -o tr3 -lpthread ![](images/5c55c53d70494.png) -可以看出计算结果正确,两个线程都用了全局变量 sum ,证明了 2 个线程共享保存全局变量的数据区。 +可以看出计算结果正确,两个线程都用了全局变量 sum,证明了 2 个线程共享保存全局变量的数据区。 但是本例子本身存在问题。存在临界区相关问题,可以从下面的代码看出,下面的代码和上面的代码相似,只是增加了发生临界区错误的可能性,即使在高配置系统环境下也容易产生的错误: -- [thread4.c](ch18/thread4.c) +- [thread4.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread4.c) ```c #include @@ -5393,11 +5421,11 @@ gcc thread4.c -D_REENTRANT -o tr4 -lpthread ### 18.3 线程存在的问题和临界区 -下面分析 [thread4.c](ch18/thread4.c) 中产生问题的原因,并给出解决方案。 +下面分析 [thread4.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread4.c) 中产生问题的原因,并给出解决方案。 #### 18.3.1 多个线程访问同一变量是问题 - [thread4.c](ch18/thread4.c) 的问题如下: + [thread4.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread4.c) 的问题如下: > 2 个线程正在同时访问全局变量 num @@ -5457,7 +5485,7 @@ void *thread_des(void *arg) #### 18.4.2 互斥量 -互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全域变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。 +互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。 通俗地说,互斥量就是一把锁,当临界区被占据的时候就上锁,等占用完毕然后再放开。 @@ -5500,7 +5528,7 @@ int pthread_mutex_unlock(pthread_mutex_t *mutex); */ ``` -函数本身含有 lock unlock 等词汇,很容易理解其含义。进入临界区前调用的函数就是 pthread_mutex_lock。调用该函数时,发现有其他线程已经进入临界区,则 pthread_mutex_lock 函数不会返回,直到里面的线程调用 pthread_mutex_unlock 函数退出临界区位置。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。接下来整理一下代码的编写方式: +函数本身含有 lock unlock 等词汇,很容易理解其含义。进入临界区前调用的函数就是 pthread_mutex_lock。调用该函数时,发现有其他线程已经进入临界区,则 pthread_mutex_lock 函数不会返回,直到里面的线程调用 pthread_mutex_unlock 函数退出临界区。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。接下来整理一下代码的编写方式: ```c pthread_mutex_lock(&mutex); @@ -5510,9 +5538,9 @@ pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); ``` -简言之,就是利用 lock 和 unlock 函数围住临界区的两端。此时互斥量相当于一把锁,阻止多个线程同时访问,还有一点要注意,线程退出临界区时,如果忘了调用 pthread_mutex_unlock 函数,那么其他为了进入临界区而调用 pthread_mutex_lock 的函数无法摆脱阻塞状态。这种情况称为「死锁」。需要格外注意,下面是利用互斥量解决示例 [thread4.c](ch18/thread4.c) 中遇到的问题代码: +简言之,就是利用 lock 和 unlock 函数围住临界区的两端。此时互斥量相当于一把锁,阻止多个线程同时访问,还有一点要注意,线程退出临界区时,如果忘了调用 pthread_mutex_unlock 函数,那么其他为了进入临界区而调用 pthread_mutex_lock 的线程无法摆脱阻塞状态。这种情况称为「死锁」。需要格外注意,下面是利用互斥量解决示例 [thread4.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread4.c) 中遇到的问题代码: -- [mutex.c](ch18/mutex.c) +- [mutex.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/mutex.c) ```c #include @@ -5602,11 +5630,11 @@ void *thread_inc(void *arg) #### 18.4.3 信号量 -信号量(英语:Semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release)时,计数值加一。当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态. +信号量(英语:Semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release)时,计数值加一。当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。semaphore对象的计数值大于 0,为 signaled 状态;计数值等于 0,为 nonsignaled 状态。 semaphore对象适用于控制一个仅支持有限个用户的共享资源,是一种不需要使用忙碌等待(busy waiting)的方法。 -信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程当前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在linux系统中,二进制信号量(binary semaphore)又称互斥锁(Mutex)。 +信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程当前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在 Linux 系统中,二进制信号量(binary semaphore)与互斥锁(Mutex)功能相似,但二者并不完全等同——互斥锁具有所有权语义,必须由加锁的线程解锁;信号量没有所有权限制。 下面介绍信号量,在互斥量的基础上,很容易理解信号量。此处只涉及利用「二进制信号量」(只用 0 和 1)完成「控制线程顺序」为中心的同步方法。下面是信号量的创建及销毁方法: @@ -5634,7 +5662,7 @@ sem : 传递保存信号量读取值的变量地址值,传递给 sem_post 的 */ ``` -调用 sem_init 函数时,操作系统将创建信号量对象,此对象中记录着「信号量值」(Semaphore Value)整数。该值在调用 sem_post 函数时增加 1,调用 sem_wait 函数时减 1。但信号量的值不能小于 0,因此,在信号量为 0 的情况下调用 sem_wait 函数时,调用的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post 函数,信号量的值将变为 1,而原本阻塞的线程可以将该信号重新减为 0 并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为 1): +调用 sem_init 函数时,操作系统将创建信号量对象,此对象中记录着「信号量值」(Semaphore Value)整数。该值在调用 sem_post 函数时增加 1,调用 sem_wait 函数时减 1。但信号量的值不能小于 0,因此,在信号量为 0 的情况下调用 sem_wait 函数时,调用的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post 函数,信号量的值将变为 1,而原本阻塞的线程可以将该信号量重新减为 0 并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为 1): ```c sem_wait(&sem);//信号量变为 0... @@ -5650,7 +5678,7 @@ sem_post(&sem);//信号量变为 1... 下面是代码: -- [semaphore.c](ch18/semaphore.c) +- [semaphore.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/semaphore.c) ```c #include @@ -5750,8 +5778,8 @@ thread : 终止的同时需要销毁的线程 ID 下面是多个客户端之间可以交换信息的简单聊天程序。 -- [chat_server.c](ch18/chat_server.c) -- [chat_clnt.c](ch18/chat_clnt.c) +- [chat_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/chat_server.c) +- [chat_clnt.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/chat_clnt.c) 上面的服务端示例中,需要掌握临界区的构成,访问全局变量 clnt_cnt 和数组 clnt_socks 的代码将构成临界区,添加和删除客户端时,变量 clnt_cnt 和数组 clnt_socks 将同时发生变化。因此下列情形会导致数据不一致,从而引发错误: @@ -5814,6 +5842,7 @@ gcc chat_clnt.c -D_REENTRANT -o cclnt -lpthread - **调用 pthread_detach 函数**:该函数将线程设置为分离状态,不会阻塞调用线程。分离状态的线程终止时,系统会自动回收其资源。适用于不关心线程返回值的场景。 注意:一个线程要么被 pthread_join,要么被 pthread_detach,不能两者都调用。未被 join 或 detach 的线程终止后其资源不会被释放,会造成资源泄漏。 + ## 第 24 章 制作 HTTP 服务器端 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 @@ -5824,7 +5853,7 @@ gcc chat_clnt.c -D_REENTRANT -o cclnt -lpthread #### 24.1.1 理解 Web 服务器端 -web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客户端的服务器端。 +Web 服务器端就是要基于 HTTP 协议,将网页对应文件传输给客户端的服务器端。 #### 24.1.2 HTTP @@ -5842,7 +5871,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 -从图中可以看出,请求消息可以分为请求行、消息头、消息体 3 个部分。其中,请求行含有请求方式(请求目的)信息。典型的请求方式有 GET 和 POST ,GET 主要用于请求数据,POST 主要用于传输数据。为了降低复杂度,我们实现只能响应 GET 请求的 Web 服务器端,下面解释图中的请求行信息。其中「GET/index.html HTTP/1.1」 具有如下含义: +从图中可以看出,请求消息可以分为请求行、消息头、消息体 3 个部分。其中,请求行含有请求方式(请求目的)信息。典型的请求方式有 GET 和 POST ,GET 主要用于请求数据,POST 主要用于传输数据。为了降低复杂度,我们实现只能响应 GET 请求的 Web 服务器端,下面解释图中的请求行信息。其中「GET /index.html HTTP/1.1」具有如下含义: > 请求(GET)index.html 文件,通常以 1.1 版本的 HTTP 协议进行通信。 @@ -5852,7 +5881,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 #### 24.1.4 响应消息(Response Message)的结构 -下面是 Web 服务器端向客户端传递的响应信息的结构。从图中可以看出,该响应消息由状态行、头信息、消息体等 3 个部分组成。状态行中有关于请求的状态信息,这是与请求消息相比最为显著的区别。 +下面是 Web 服务器端向客户端传递的响应信息的结构。从图中可以看出,该响应消息由状态行、头信息、消息体等 3 个部分组成。状态行中有关于请求的状态信息,这是与请求消息相比最为显著的区别。 @@ -5865,7 +5894,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 消息头中含有传输的数据类型和长度等信息。图中的消息头含有如下信息: -> 服务端名为 SimpleWebServer ,传输的数据类型为 text/html。数据长度不超过 2048 个字节。 +> 服务端名为 SimpleWebServer,传输的数据类型为 text/html。数据长度不超过 2048 个字节。 最后插入一个空行后,通过消息体发送客户端请求的文件数据。以上就是实现 Web 服务端过程中必要的 HTTP 协议。 @@ -5879,7 +5908,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 下面是代码: -- [webserv_linux.c](ch24/webserv_linux.c) +- [webserv_linux.c](webserv_linux.c) ```c #include @@ -6052,7 +6081,7 @@ gcc webserv_linux.c -D_REENTRANT -o web_serv -lpthread -经过测试,这个简单的 HTTP 服务器可以正常的显示出页面。 +经过测试,这个简单的 HTTP 服务器可以正常地显示出页面。 ### 24.3 习题 @@ -6060,12 +6089,12 @@ gcc webserv_linux.c -D_REENTRANT -o web_serv -lpthread 1. **下列关于 Web 服务器端和 Web 浏览器端的说法错误的是**? - 答:**选项 5 是错误的**。 + 答:**选项 2 和选项 5 是错误的**。选项 2 错误在于:HTTP 是无状态协议,一次请求-响应完成后即断开连接,并不"保持较长的客户端连接";Web 服务器端基于 TCP 是为了可靠传输,而非保持长连接。选项 5 错误在于:任何能发起 HTTP 请求的客户端(如 curl、wget 等)都可以访问 Web 服务器端。 1. Web 浏览器是通过自身创建的套接字连接服务端的客户端 2. Web 服务器端通过 TCP 套接字提供服务,因为它将保持较长的客户端连接并交换数据 3. 超文本与普通文本的最大区别是其具有可跳转的特性 - 4. Web 浏览器可视为向浏览器提供请求文件的文件传输服务器端 + 4. Web 服务器端可视为向浏览器提供请求文件的文件传输服务器端 5. **除 Web 浏览器外,其他客户端都无法访问 Web 服务器端。(错误:任何能发起 HTTP 请求的客户端都可以访问 Web 服务器端,如 curl、wget、编程实现的 HTTP 客户端等)** 2. **下列关于 HTTP 协议的描述错误的是**? diff --git a/ch01/README.md b/ch01/README.md index 282ba01..e51dc02 100644 --- a/ch01/README.md +++ b/ch01/README.md @@ -4,7 +4,7 @@ ### 1.1 理解网络编程和套接字 -#### 1.1.1构建打电话套接字 +#### 1.1.1 构建打电话套接字 以电话机打电话的方式来理解套接字。 @@ -32,7 +32,7 @@ int socket(int domain, int type, int protocol); ```c #include -int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); +int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); //成功时返回0,失败时返回-1 ``` @@ -40,7 +40,7 @@ int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); **调用 listen 函数(连接电话线)时进行的对话**: -> 问:已架设完电话机后是否只需链接电话线? +> 问:已架设完电话机后是否只需连接电话线? > > 答:对,只需要连接就能接听电话。 @@ -62,11 +62,11 @@ int listen(int sockfd, int backlog); ```c #include -int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //成功时返回文件描述符,失败时返回-1 ``` -网络编程中和接受连接请求的套接字创建过程可整理如下: +网络编程中接受连接请求的套接字创建过程可整理如下: 1. 第一步:调用 socket 函数创建套接字。 2. 第二步:调用 bind 函数分配IP地址和端口号。 @@ -106,11 +106,11 @@ gcc hello_client.c -o hclient ./hclient 127.0.0.1 9190 ``` -运行的时候,首先在 9190 端口启动服务,然后 hserver 就会一直等待客户端进行连接,当客户端连接位于本地的 IP 为 127.0.0.1 的地址的 9190 端口时,客户端就会收到服务端的回应,输出`Hello World!` +运行的时候,首先在 9190 端口启动服务,然后 hserver 就会一直等待客户端进行连接,当客户端连接到本地地址 127.0.0.1 的 9190 端口时,客户端就会收到服务端的回应,输出`Hello World!` ### 1.2 基于 Linux 的文件操作 -讨论套接字的过程中突然谈及文件也许有些奇怪。但是对于 Linux 而言,socket 操作与文件操作没有区别,因而有必要详细了解文件。在 Linux 世界里,socket 也被认为是文件的一种,因此在网络数据传输过程中自然可以使用 I/O 的相关函数。Windows 与 Linux 不同,是要区分 socket 和文件的。因此在 Windows 中需要调用特殊的数据传输相关函数。 +讨论套接字的过程中突然谈及文件也许有些奇怪。但是对于 Linux 而言,socket 操作与文件操作没有区别,因而有必要详细了解文件。在 Linux 世界里,socket 也被认为是文件的一种,因此在网络数据传输过程中自然可以使用 I/O 的相关函数。Windows 与 Linux 不同,是要区分套接字和文件的。因此在 Windows 中需要调用特殊的数据传输相关函数。 #### 1.2.1 底层访问和文件描述符 @@ -126,7 +126,7 @@ gcc hello_client.c -o hclient 文件描述符也被称为「文件句柄」,但是「句柄」主要是 Windows 中的术语。因此,在本书中如果涉及 Windows 平台将使用「句柄」,如果是 Linux 将使用「描述符」。 -#### 1.2.2 打开文件: +#### 1.2.2 打开文件 ```c #include @@ -229,7 +229,7 @@ file data: Let's go! #### 1.2.6 文件描述符与套接字 -下面将同时创建文件和套接字,并用整数型态比较返回的文件描述符的值. +下面将同时创建文件和套接字,并用整数类型比较返回的文件描述符的值。 代码见:[fd_seri.c](fd_seri.c) @@ -244,8 +244,8 @@ gcc fd_seri.c -o fds ``` file descriptor 1: 3 -file descriptor 2: 15 -file descriptor 3: 16 +file descriptor 2: 4 +file descriptor 3: 5 ``` ### 1.3 基于 Windows 平台的实现 @@ -262,17 +262,17 @@ file descriptor 3: 16 1. 套接字在网络编程中的作用是什么?为何称它为套接字? - > 答:操作系统会提供「套接字」(socket)的部件,套接字是网络数据传输用的软件设备。因此,「网络编程」也叫「套接字编程」。「套接字」就是用来连接网络的工具。 + > 答:操作系统会提供「套接字」(socket)的部件,套接字是网络数据传输用的软件设备,因此「网络编程」也叫「套接字编程」。「套接字」是 socket 的中文译名,socket 原意为「插座、插口」,在网络编程中引申为数据通信的连接端点,即用来连接网络的工具。 2. 在服务器端创建套接字以后,会依次调用 listen 函数和 accept 函数。请比较二者作用。 - > 答:调用 listen 函数将套接字转换成可受连接状态(监听),调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止。 + > 答:调用 listen 函数将套接字转换成可接受连接状态(监听),调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止。 3. Linux 中,对套接字数据进行 I/O 时可以直接使用文件 I/O 相关函数;而在 Windows 中则不可以。原因为何? - > 答:在 Linux 中,套接字(socket)被视为文件的一种,遵循「一切皆文件」的设计哲学。因此可以使用标准的文件 I/O 函数(如 `read`、`write`、`close`)对套接字进行操作。而在 Windows 中,套接字和文件是区分开的,套接字操作需要使用专门的 Winsock 函数(如 `send`、`recv`、`closesocket`),不能使用文件 I/O 函数(如 `ReadFile`、`WriteFile`)直接操作套接字。 + > 答:在 Linux 中,套接字(socket)被视为文件的一种,遵循「一切皆文件」的设计哲学。因此可以使用标准的文件 I/O 函数(如 `read`、`write`、`close`)对套接字进行操作。而在 Windows 中,套接字和文件是区分开的,套接字操作需要使用专门的 Winsock 函数(如 `send`、`recv`、`closesocket`),通常不使用文件 I/O 函数(如 `ReadFile`、`WriteFile`)直接操作套接字。 -4. 创建套接字后一般会给他分配地址,为什么?为了完成地址分配需要调用哪个函数? +4. 创建套接字后一般会给它分配地址,为什么?为了完成地址分配需要调用哪个函数? > 答:套接字被创建之后,只有为其分配了IP地址和端口号后,客户端才能够通过IP地址及端口号与服务器端建立连接,需要调用 bind 函数来完成地址分配。 diff --git a/ch01/fd_seri.c b/ch01/fd_seri.c index 8524a5d..f68c2c5 100644 --- a/ch01/fd_seri.c +++ b/ch01/fd_seri.c @@ -8,7 +8,7 @@ int main() int fd1, fd2, fd3; //创建一个文件和两个套接字 fd1 = socket(PF_INET, SOCK_STREAM, 0); - fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC); + fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC, 0644); fd3 = socket(PF_INET, SOCK_DGRAM, 0); //输出之前创建的文件描述符的整数值 printf("file descriptor 1: %d\n", fd1); diff --git a/ch01/low_open.c b/ch01/low_open.c index 96ed216..1f88329 100644 --- a/ch01/low_open.c +++ b/ch01/low_open.c @@ -9,7 +9,7 @@ int main() int fd; char buf[] = "Let's go!\n"; // O_CREAT | O_WRONLY | O_TRUNC 是文件打开模式,将创建新文件,并且只能写。如存在 data.txt 文件,则清空文件中的全部数据。 - fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC); + fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd == -1) error_handling("open() error!"); printf("file descriptor: %d \n", fd); diff --git a/ch01/low_read.c b/ch01/low_read.c index 79b0f5a..30d5d50 100644 --- a/ch01/low_read.c +++ b/ch01/low_read.c @@ -15,8 +15,10 @@ int main() error_handling("open() error!"); printf("file descriptor: %d \n", fd); - if (read(fd, buf, sizeof(buf)) == -1) + ssize_t read_cnt = read(fd, buf, sizeof(buf) - 1); + if (read_cnt == -1) error_handling("read() error!"); + buf[read_cnt] = '\0'; printf("file data: %s", buf); close(fd); return 0; diff --git a/ch02/README.md b/ch02/README.md index 2da044c..fe435b2 100644 --- a/ch02/README.md +++ b/ch02/README.md @@ -1,6 +1,6 @@ ## 第二章 套接字类型与协议设置 -本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到,直接点连接可能进不去。 +本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到,直接点链接可能进不去。 本章仅需了解创建套接字时调用的 socket 函数。 @@ -34,7 +34,7 @@ protocol: 计算机间通信中使用的协议信息 | PF_PACKET | 底层套接字的协议族 | | PF_IPX | IPX Novell 协议族 | -本书着重讲 PF_INET 对应的 IPv4 互联网协议族。其他协议并不常用,或并未普及。**另外,套接字中采用的最终的协议信息是通过 socket 函数的第三个参数传递的。在指定的协议族范围内通过第一个参数决定第三个参数。** +本书着重讲 PF_INET 对应的 IPv4 互联网协议族。其他协议并不常用,或并未普及。**另外,套接字中采用的最终的协议信息是通过 socket 函数的第三个参数传递的。在指定的协议族(第一个参数)和套接字类型(第二个参数)范围内,即可确定第三个参数(具体协议)。** #### 2.1.3 套接字类型(Type) @@ -52,9 +52,9 @@ protocol: 计算机间通信中使用的协议信息 这种情形适用于之前说过的 write 和 read 函数 -> 传输数据的计算机通过调用3次 write 函数传递了 100 字节的数据,但是接受数据的计算机仅仅通过调用 1 次 read 函数调用就接受了全部 100 个字节。 +> 传输数据的计算机通过调用3次 write 函数传递了 100 字节的数据,但是接收数据的计算机仅仅通过调用 1 次 read 函数调用就接收了全部 100 个字节。 -收发数据的套接字内部有缓冲(buffer),简言之就是字节数组。只要不超过数组容量,那么数据填满缓冲后过 1 次 read 函数的调用就可以读取全部,也有可能调用多次来完成读取。 +收发数据的套接字内部有缓冲(buffer),简言之就是字节数组。只要不超过数组容量,那么数据填满缓冲后通过 1 次 read 函数的调用就可以读取全部,也有可能调用多次来完成读取。 **套接字缓冲已满是否意味着数据丢失?** @@ -62,7 +62,7 @@ protocol: 计算机间通信中使用的协议信息 套接字联机必须一一对应。面向连接的套接字可总结为: -**可靠地、按序传递的、基于字节的面向连接的数据传输方式的套接字。** +**可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字。** #### 2.1.5 面向消息的套接字(SOCK_DGRAM) @@ -73,13 +73,13 @@ protocol: 计算机间通信中使用的协议信息 - 传输的数据有边界 - 限制每次传输数据的大小 -面向消息的套接字比面向连接的套接字更具有传输速度,但可能丢失。特点可总结为: +面向消息的套接字比面向连接的套接字传输速度更快,但数据可能丢失。特点可总结为: -**不可靠的、不按序传递的、以数据的高速传输为目的套接字。** +**不可靠的、不按序传递的、以数据的高速传输为目的的套接字。** #### 2.1.6 协议的最终选择 -socket 函数的第三个参数决定最终采用的协议。前面已经通过前两个参数传递了协议族信息和套接字数据传输方式,这些信息还不够吗?为什么要传输第三个参数呢? +socket 函数的第三个参数决定最终采用的协议。前面已经通过前两个参数传递了协议族信息和套接字数据传输方式,这些信息还不够吗?为什么要传递第三个参数呢? > 可以应对同一协议族中存在的多个数据传输方式相同的协议,所以数据传输方式相同,但是协议不同,需要用第三个参数指定具体的协议信息。 @@ -99,8 +99,8 @@ int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); 需要对第一章的代码做出修改,修改好的代码如下: -- [tcp_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch02/tcp_client.c) -- [tcp_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch02/tcp_server.c) +- [tcp_client.c](tcp_client.c) +- [tcp_server.c](tcp_server.c) 编译: @@ -231,7 +231,7 @@ gcc tcp_server_win.c -o hserver -lws2_32 > 答:①传输过程中数据不会消失②按序传输数据③传输的数据不存在数据边界(Boundary) -3. 下面那些是面向消息的套接字的特性? +3. 下面哪些是面向消息的套接字的特性? - **传输数据可能丢失** - 没有数据边界(Boundary) diff --git a/ch03/README.md b/ch03/README.md index 0cc7ae1..d009fc5 100644 --- a/ch03/README.md +++ b/ch03/README.md @@ -6,7 +6,7 @@ ### 3.1 分配给套接字的 IP 地址与端口号 -IP 是 Internet Protocol(网络协议)的简写,是为收发网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为了区分程序中创建的套接字而分配给套接字的序号。 +IP 是 Internet Protocol(网际协议)的简写,是为收发网络数据而分配给计算机的值。端口号并非赋予计算机的值,而是为了区分程序中创建的套接字而分配给套接字的序号。 #### 3.1.1 网络地址(Internet Address) @@ -47,7 +47,7 @@ IPv4 标准的 4 字节 IP 地址分为网络地址和主机(指计算机) IP 地址用于区分计算机,只要有 IP 地址就能向目标主机传输数据,但是只有这些还不够,我们需要把信息传输给具体的应用程序。 -所以计算机一般有 NIC(网络接口卡)数据传输设备。通过 NIC 接收的数据内有端口号,操作系统参考端口号把信息传给相应的应用程序。 +所以计算机一般配有 NIC(网络接口卡)这一数据传输设备。通过 NIC 接收的数据内有端口号,操作系统参考端口号把信息传给相应的应用程序。 端口号由 16 位构成,可分配的端口号范围是 0~65535。但是 0~1023 是知名端口,一般分配给特定的应用程序,所以应当分配给此范围之外的值。 @@ -103,7 +103,7 @@ struct in_addr - 成员 sin_family -每种协议适用的地址族不同,比如,IPv4 使用 4 字节的地址族,IPv6 使用 16 字节的地址族。 +每种协议适用的地址族不同,比如,IPv4 使用 4 字节的地址,IPv6 使用 16 字节的地址。 > 地址族 @@ -125,7 +125,7 @@ AF_LOCAL 只是为了说明具有多种地址族而添加的。 - 成员 sin_zero - 无特殊含义。只是为结构体 sockaddr_in 结构体变量地址值将以如下方式传递给 bind 函数。 + 无特殊含义。它的存在是为了让 sockaddr_in 结构体的大小与 sockaddr 结构体保持一致,以便 sockaddr_in 结构体变量的地址值能以如下方式传递给 bind 函数。 在之前的代码中 @@ -144,7 +144,7 @@ AF_LOCAL 只是为了说明具有多种地址族而添加的。 }; ``` - 此结构体 sa_data 保存的地址信息中需要包含 IP 地址和端口号,剩余部分应该填充 0,但是这样对于包含地址的信息非常麻烦,所以出现了 sockaddr_in 结构体,然后强制转换成 sockaddr 类型,则生成符合 bind 条件的参数。 + 此结构体 sa_data 保存的地址信息中需要包含 IP 地址和端口号,剩余部分应该填充 0,但是这样手动填充地址信息非常麻烦,所以出现了 sockaddr_in 结构体,然后强制转换成 sockaddr 类型,则生成符合 bind 条件的参数。 ### 3.3 网络字节序与地址变换 @@ -162,7 +162,7 @@ AF_LOCAL 只是为了说明具有多种地址族而添加的。 00000001 00000000 00000000 00000000 ``` -两种一种是顺序保存,一种是倒序保存。 +一种是顺序保存,一种是倒序保存。 #### 3.3.1 字节序(Order)与网络字节序 @@ -178,7 +178,7 @@ CPU 保存数据的方式有两种,这意味着 CPU 解析数据的方式也 ![zijiexu.png](images/5c3aca956c8e9.png) -因为这种原因,所以在通过网络传输数据时必须约定统一的方式,这种约定被称为网络字节序(Network Byte Order),非常简单,统一为大端序。即,先把数据数组转化成大端序格式再进行网络传输。 +因为这种原因,所以在通过网络传输数据时必须约定统一的方式,这种约定被称为网络字节序(Network Byte Order),非常简单,统一为大端序。即,先把数据转换成大端序格式再进行网络传输。 #### 3.3.2 字节序转换 @@ -200,7 +200,7 @@ unsigned long ntohl(unsigned long); 下面的代码是示例,说明以上函数调用过程: -[endian_conv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch03/endian_conv.c) +[endian_conv.c](endian_conv.c) ```c #include @@ -240,7 +240,7 @@ Host ordered address: 0x12345678 Network ordered address: 0x78563412 ``` -这是在小端 CPU 的运行结果。大部分人会得到相同的结果,因为 Intel 和 AMD 的 CPU 都是小端序为标准。 +这是在小端 CPU 上的运行结果。大部分人会得到相同的结果,因为 Intel 和 AMD 的 CPU 都以小端序为标准。 ### 3.4 网络地址的初始化与分配 @@ -256,7 +256,7 @@ in_addr_t inet_addr(const char *string); 具体示例: -[inet_addr.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch03/inet_addr.c) +[inet_addr.c](inet_addr.c) ```c #include @@ -268,13 +268,13 @@ int main(int argc, char *argv[]) unsigned long conv_addr = inet_addr(addr1); if (conv_addr == INADDR_NONE) - printf("Error occured! \n"); + printf("Error occurred! \n"); else printf("Network ordered integer addr: %#lx \n", conv_addr); conv_addr = inet_addr(addr2); if (conv_addr == INADDR_NONE) - printf("Error occured! \n"); + printf("Error occurred! \n"); else printf("Network ordered integer addr: %#lx \n", conv_addr); return 0; @@ -292,7 +292,7 @@ gcc inet_addr.c -o addr ``` Network ordered integer addr: 0x4030201 -Error occured! +Error occurred! ``` 1 个字节能表示的最大整数是 255,所以代码中 addr2 是错误的 IP 地址。从运行结果看,inet_addr 不仅可以转换地址,还可以检测有效性。 @@ -311,7 +311,7 @@ addr: 保存转换结果的 in_addr 结构体变量的地址值 函数调用示例: -[inet_aton.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch03/inet_aton.c) +[inet_aton.c](inet_aton.c) ```c #include @@ -366,7 +366,7 @@ char *inet_ntoa(struct in_addr adr); 示例: -[inet_ntoa.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch03/inet_ntoa.c) +[inet_ntoa.c](inet_ntoa.c) ```c #include @@ -414,12 +414,12 @@ Dotted-Decimal notation3: 1.2.3.4 ```c struct sockaddr_in addr; -char *serv_ip = "211.217.168.13"; //声明IP地址族 +char *serv_ip = "211.217.168.13"; //声明IP地址字符串 char *serv_port = "9190"; //声明端口号字符串 memset(&addr, 0, sizeof(addr)); //结构体变量 addr 的所有成员初始化为0 -addr.sin_family = AF_INET; //制定地址族 +addr.sin_family = AF_INET; //指定地址族 addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化 -addr.sin_port = htons(atoi(serv_port)); //基于字符串的IP地址端口号初始化 +addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化 ``` ### 3.5 基于 Windows 的实现 diff --git a/ch03/inet_addr.c b/ch03/inet_addr.c index b6204bf..820de09 100644 --- a/ch03/inet_addr.c +++ b/ch03/inet_addr.c @@ -7,13 +7,13 @@ int main(int argc, char *argv[]) unsigned long conv_addr = inet_addr(addr1); if (conv_addr == INADDR_NONE) - printf("Error occured! \n"); + printf("Error occurred! \n"); else printf("Network ordered integer addr: %#lx \n", conv_addr); conv_addr = inet_addr(addr2); if (conv_addr == INADDR_NONE) - printf("Error occured! \n"); + printf("Error occurred! \n"); else printf("Network ordered integer addr: %#lx \n", conv_addr); return 0; diff --git a/ch04/README.md b/ch04/README.md index 0ea0872..0b93d11 100644 --- a/ch04/README.md +++ b/ch04/README.md @@ -16,7 +16,7 @@ TCP/IP 协议栈共分为 4 层,可以理解为数据收发分成了 4 个层 #### 4.1.2 链路层 -链路层是物理链接领域标准化的结果,也是最基本的领域,专门定义LAN、WAN、MAN等网络标准。若两台主机通过网络进行数据交换,则需要物理连接,链路层就负责这些标准。 +链路层是物理连接领域标准化的结果,也是最基本的领域,专门定义LAN、WAN、MAN等网络标准。若两台主机通过网络进行数据交换,则需要物理连接,链路层就负责这些标准。 #### 4.1.3 IP 层 @@ -28,11 +28,11 @@ IP 是面向消息的、不可靠的协议。每次传输数据时会帮我们 IP 层解决数据传输中的路径选择问题,只需照此路径传输数据即可。TCP 和 UDP 层以 IP 层提供的路径信息为基础完成实际的数据传输,故该层又称为传输层。UDP 比 TCP 简单,现在我们只解释 TCP 。 TCP 可以保证数据的可靠传输,但是它发送数据时以 IP 层为基础(这也是协议栈层次化的原因)。 -IP 层只关注一个数据包(数据传输基本单位)的传输过程。因此,即使传输多个数据包,每个数据包也是由 IP 层实际传输的,也就是说传输顺序及传输本身是不可靠的。若只利用IP层传输数据,则可能导致后传输的数据包B比先传输的数据包A提早到达。另外,传输的数据包A、B、C中可能只收到A和C,甚至收到的C可能已经损毁 。反之,若添加 TCP 协议则按照如下对话方式进行数据交换。 +IP 层只关注一个数据包(数据传输基本单位)的传输过程。因此,即使传输多个数据包,每个数据包也是由 IP 层实际传输的,也就是说传输顺序及传输本身是不可靠的。若只利用IP层传输数据,则可能导致后传输的数据包B比先传输的数据包A提早到达。另外,传输的数据包A、B、C中可能只收到A和C,甚至收到的C可能已经损毁。反之,若添加 TCP 协议则按照如下对话方式进行数据交换。 > 主机A:正确接收第二个数据包 > -> 主机B:恩,知道了 +> 主机B:嗯,知道了 > > 主机A:正确收到第三个数据包 > @@ -48,7 +48,7 @@ IP 层只关注一个数据包(数据传输基本单位)的传输过程。 ### 4.2 实现基于 TCP 的服务器/客户端 -#### 4.2.1 TCP 服务端的默认函数的调用程序 +#### 4.2.1 TCP 服务端的默认函数调用顺序 ![](images/5c3c2782a7810.png) @@ -86,13 +86,13 @@ accept 函数受理连接请求队列中待处理的客户端连接请求。函 #### 4.2.4 回顾 Hello World 服务端 -- 代码:[hello_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/hello_server.c) +- 代码:[hello_server.c](hello_server.c) 重新整理一下代码的思路 1. 服务端实现过程中首先要创建套接字,此时的套接字并非是真正的服务端套接字 2. 为了完成套接字地址的分配,初始化结构体变量并调用 bind 函数。 -3. 调用 listen 函数进入等待连接请求状态。连接请求状态队列的长度设置为5.此时的套接字才是服务端套接字。 +3. 调用 listen 函数进入等待连接请求状态。连接请求等待队列的长度设置为 5。此时的套接字才是服务端套接字。 4. 调用 accept 函数从队头取 1 个连接请求与客户端建立连接,并返回创建的套接字文件描述符。另外,调用 accept 函数时若等待队列为空,则 accept 函数不会返回,直到队列中出现新的客户端连接。 5. 调用 write 函数向客户端传送数据,调用 close 关闭连接 @@ -118,13 +118,13 @@ addrlen: 第二个结构体参数 servaddr 变量的字节长度 - 服务端接受连接请求 - 发生断网等异常状况而中断连接请求 -注意:**接受连接**不代表服务端调用 accept 函数,其实只是服务器端把连接请求信息记录到等待队列。因此 connect 函数返回后并不应该立即进行数据交换。 +注意:**接受连接**不代表服务端调用了 accept 函数,其实只是服务器端把连接请求信息记录到等待队列。connect 函数返回时 TCP 三次握手已完成、连接已建立,客户端可以发送数据(数据会暂存在内核缓冲区,待服务端调用 accept 后读取),但服务端应用层可能尚未调用 accept 处理该连接。 -客户端在调用 connect 函数时自动分配主机的 IP,随机分配端口。无需调用显式的 bind 函数进行分配。 +客户端在调用 connect 函数时自动分配主机的 IP,随机分配端口。无需显式调用 bind 函数进行分配。 #### 4.2.6 回顾 Hello World 客户端 -- 代码:[hello_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/hello_client.c) +- 代码:[hello_client.c](hello_client.c) 重新理解这个程序: @@ -132,7 +132,7 @@ addrlen: 第二个结构体参数 servaddr 变量的字节长度 2. 结构体变量 serv_addr 中初始化IP和端口信息。初始化值为目标服务器端套接字的IP和端口信息。 3. 调用 connect 函数向服务端发起连接请求 4. 完成连接后,接收服务端传输的数据 -5. 接收数据后调用 close 函数关闭套接字,结束与服务器端的连接。(对套接字调用close函数,对应于向建立连接的对应套接字发送EOF。即,如果客户端的套接字调用了close函数,服务端read时候会返回0。) +5. 接收数据后调用 close 函数关闭套接字,结束与服务器端的连接。(对套接字调用 close 函数,相当于向建立连接的对端套接字发送 EOF。即,如果客户端的套接字调用了 close 函数,服务端 read 时会返回 0。) #### 4.2.7 基于 TCP 的服务端/客户端函数调用关系 @@ -167,8 +167,8 @@ addrlen: 第二个结构体参数 servaddr 变量的字节长度 以下是服务端与客户端的代码: -- [echo_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/echo_server.c) -- [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/echo_client.c) +- [echo_server.c](echo_server.c) +- [echo_client.c](echo_client.c) 编译: @@ -234,7 +234,7 @@ Windows 平台下的 Socket 编程(Winsock)与 Linux 平台基本类似, 2. **请说出 TCP/IP 协议栈中链路层和IP层的作用,并给出二者关系** - 答:链路层是物理链接领域标准化的结果,专门定义网络标准。若两台主机通过网络进行数据交换,则首先要做到的就是进行物理链接。IP层:为了在复杂的网络中传输数据,首先需要考虑路径的选择。关系:链路层负责进行一系列物理连接,而IP层负责选择正确可行的物理路径。 + 答:链路层是物理连接领域标准化的结果,专门定义网络标准。若两台主机通过网络进行数据交换,则首先要做到的就是进行物理连接。IP层:为了在复杂的网络中传输数据,首先需要考虑路径的选择。关系:链路层负责进行一系列物理连接,而IP层负责选择正确可行的物理路径。 3. **为何需要把 TCP/IP 协议栈分成 4 层(或7层)?开放式回答。** @@ -242,7 +242,7 @@ Windows 平台下的 Socket 编程(Winsock)与 Linux 平台基本类似, 4. **客户端调用 connect 函数向服务器端发送请求。服务器端调用哪个函数后,客户端可以调用 connect 函数?** - 答:服务端调用 listen 函数后,客户端可以调用 connect 函数。因为,服务端调用 listen 函数后,服务端套接字才有能力接受请求连接的信号。 + 答:服务端调用 listen 函数后,客户端可以调用 connect 函数。因为服务端调用 listen 函数后,服务端套接字才具备接收连接请求的能力。 5. **什么时候创建连接请求等待队列?它有何种作用?与 accept 有什么关系?** diff --git a/ch04/echo_client.c b/ch04/echo_client.c index 04eb33e..f6a6562 100644 --- a/ch04/echo_client.c +++ b/ch04/echo_client.c @@ -45,6 +45,8 @@ int main(int argc, char *argv[]) write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE - 1); + if (str_len == -1) + error_handling("read() error!"); message[str_len] = 0; printf("Message from server: %s", message); } diff --git a/ch05/My_op_client.c b/ch05/My_op_client.c index 1c7c6f7..cc7c804 100644 --- a/ch05/My_op_client.c +++ b/ch05/My_op_client.c @@ -35,6 +35,7 @@ int main(int argc, char *argv[]) char temp[20]; puts("请输入你要计算的数字个数:"); scanf("%d", &n); + message[0] = '\0'; sprintf(temp, "%d", n); strcat(temp, " "); strcat(message, temp); diff --git a/ch05/My_op_server.c b/ch05/My_op_server.c index d271f6e..3c9720a 100644 --- a/ch05/My_op_server.c +++ b/ch05/My_op_server.c @@ -8,7 +8,7 @@ #define BUF_SIZE 10240 void error_handling(char *message); -char res[10]; +char res[16]; char *calc(char *s) { int len = strlen(s), i; @@ -93,7 +93,10 @@ int main(int argc, char *argv[]) if (clnt_sock == -1) error_handling("accept() error"); str_len = read(clnt_sock, message, BUF_SIZE); - write(clnt_sock, calc(message), str_len); + if (str_len == -1) error_handling("read() error"); + message[str_len] = 0; + char *result_str = calc(message); + write(clnt_sock, result_str, strlen(result_str)); close(clnt_sock); close(serv_sock); return 0; diff --git a/ch05/README.md b/ch05/README.md index 3917bbe..9241988 100644 --- a/ch05/README.md +++ b/ch05/README.md @@ -22,7 +22,7 @@ write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE - 1); ``` -二者都在循环调用 read 和 write 函数。实际上之前的回声客户端将 100% 接收字节传输的数据,只不过接收数据时的单位有些问题。扩展客户端代码回顾范围,下面是客户端的代码: +二者都在循环调用 read 和 write 函数。实际上之前的回声客户端会接收服务器传输的全部数据,只不过接收数据时的单位有些问题。扩展客户端代码回顾范围,下面是客户端的代码: ```c while (1) @@ -48,7 +48,7 @@ while (1) - [echo_client2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch05/echo_client2.c) -这样修改为了接收所有传输数据而循环调用 read 函数。测试及运行结果可参考第四章。 +这样修改后,客户端为接收所有传输数据而循环调用 read 函数。测试及运行结果可参考第四章。 #### 5.1.3 如果问题不在于回声客户端:定义应用层协议 @@ -57,7 +57,7 @@ while (1) 现在写一个小程序来体验应用层协议的定义过程。要求: 1. 服务器从客户端获得多个数组和运算符信息。 -2. 服务器接收到数字后对齐进行加减乘运算,然后把结果传回客户端。 +2. 服务器接收到数字后对其进行加减乘运算,然后把结果传回客户端。 例: @@ -114,7 +114,7 @@ gcc op_server.c -o opserver #### 5.2.1 TCP 套接字中的 I/O 缓冲 -TCP 套接字的数据收发无边界。服务器即使调用 1 次 write 函数传输 40 字节的数据,客户端也有可能通过 4 次 read 函数调用每次读取 10 字节。但此处也有一些疑问,服务器一次性传输了 40 字节,而客户端竟然可以缓慢的分批接收。客户端接收 10 字节后,剩下的 30 字节在何处等候呢? +TCP 套接字的数据收发无边界。服务器即使调用 1 次 write 函数传输 40 字节的数据,客户端也有可能通过 4 次 read 函数调用每次读取 10 字节。但此处也有一些疑问,服务器一次性传输了 40 字节,而客户端竟然可以缓慢地分批接收。客户端接收 10 字节后,剩下的 30 字节在何处等候呢? 实际上,write 函数调用后并非立即传输数据,read 函数调用后也并非马上接收数据。如图所示,write 函数调用瞬间,数据将移至输出缓冲;read 函数调用瞬间,从输入缓冲读取数据。 @@ -140,7 +140,7 @@ I/O 缓冲特性可以整理如下: 数据收发也是如此,因此 TCP 中不会因为缓冲溢出而丢失数据。 -**write 函数在数据传输完成时返回。** +**write 函数在数据成功移入输出缓冲时返回(并非等到对端接收完毕)。** #### 5.2.2 TCP 内部工作原理 1:与对方套接字的连接 @@ -180,11 +180,11 @@ TCP 在实际通信中也会经过三次对话过程,因此,该过程又被 > 刚才传输的 SEQ 为 1000 的数据包接收无误,现在请传递 SEQ 为 1001 的数据包。 -对于主机 A 首次传输的数据包的确认消息(ACK 1001)和为主机 B 传输数据做准备的同步消息(SEQ 2000)捆绑发送。因此,此种类消息又称为 SYN+ACK。 +对于主机 A 首次传输的数据包的确认消息(ACK 1001)和为主机 B 传输数据做准备的同步消息(SEQ 2000)捆绑发送。因此,此类消息又称为 SYN+ACK。 收发数据前向数据包分配序号,并向对方通报此序号,这都是为了防止数据丢失做的准备。通过向数据包分配序号并确认,可以在数据包丢失时马上查看并重传丢失的数据包。因此 TCP 可以保证可靠的数据传输。 -通过这三个过程,这样主机 A 和主机 B 就确认了彼此已经准备就绪。 +通过这三个过程,主机 A 和主机 B 就确认了彼此已经准备就绪。 #### 5.2.3 TCP 内部工作原理 2:与对方主机的数据交换 @@ -217,7 +217,7 @@ TCP 套接字的结束过程也非常优雅。如果对方还有数据需要传 ![](images/5c3ed7503c18c.png) -图中数据包内的 FIN 表示断开连接。也就是说,双方各发送 1 次 FIN 消息后断开连接。此过程经历 4 个阶段,因此又称四次握手(Four-way handshaking)。SEQ 和 ACK 的含义与之前讲解的内容一致,省略。图中,主机 A 传递了两次 ACK 5001,也许这里会有困惑。其实,第二次 FIN 数据包中的 ACK 5001 只是因为接收了 ACK 消息后未接收到的数据重传的。 +图中数据包内的 FIN 表示断开连接。也就是说,双方各发送 1 次 FIN 消息后断开连接。此过程经历 4 个阶段,因此又称四次握手(Four-way handshaking)。SEQ 和 ACK 的含义与之前讲解的内容一致,省略。图中,主机 A 传递了两次 ACK 5001,也许这里会有困惑。其实,图中第二次出现的 ACK 5001 是重传确认:若主机 B 未收到主机 A 第一次发出的 ACK,B 会重传 FIN,主机 A 收到后再次发送 ACK 5001 予以确认。 ### 5.3 基于 Windows 的实现 diff --git a/ch05/homework/tcp_client_kehou6.c b/ch05/homework/tcp_client_kehou6.c index 6d59eb7..9fed4d7 100644 --- a/ch05/homework/tcp_client_kehou6.c +++ b/ch05/homework/tcp_client_kehou6.c @@ -28,6 +28,7 @@ int main(int argc, char *argv[]) scanf("%s", file_name); //打开文件名 fp=fopen(file_name, "wb"); + if(fp==NULL) error_handling("fopen() error"); //创建套接字 sd=socket(PF_INET, SOCK_STREAM, 0); @@ -37,7 +38,8 @@ int main(int argc, char *argv[]) serv_adr.sin_addr.s_addr=inet_addr(argv[1]); serv_adr.sin_port=htons(atoi(argv[2])); - connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); + if(connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1) + error_handling("connect() error"); //写入要传输的文件 write(sd, file_name, strlen(file_name)+1); diff --git a/ch05/homework/tcp_server_kehou6.c b/ch05/homework/tcp_server_kehou6.c index 951d977..88ea3ea 100644 --- a/ch05/homework/tcp_server_kehou6.c +++ b/ch05/homework/tcp_server_kehou6.c @@ -54,9 +54,8 @@ int main(int argc, char *argv[]) } write(clnt_sd, buf, BUF_SIZE); } + fclose(fp); } - - fclose(fp); close(clnt_sd); close(serv_sd); return 0; } diff --git a/ch06/README.md b/ch06/README.md index 0ebff26..32e7918 100644 --- a/ch06/README.md +++ b/ch06/README.md @@ -24,7 +24,7 @@ TCP 与 UDP 的区别很大一部分来源于流控制。也就是说 TCP 的生 #### 6.1.3 UDP 的高效使用 -UDP 也具有一定的可靠性。对于通过网络实时传递的视频或者音频时情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点: +UDP 也具有一定的可靠性。当通过网络实时传递视频或音频时,情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点: - 收发数据前后进行的连接设置及清除过程。 - 收发过程中为保证可靠性而添加的流控制。 @@ -64,7 +64,7 @@ addrlen: 传递给参数 to 的地址值结构体变量长度 */ ``` -上述函数与之前的 TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收 UDP 数据的函数。UDP 数据的发送并不固定,因此该函数定义为可接受发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息。 +上述函数与之前的 TCP 输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收 UDP 数据的函数。UDP 数据的发送并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回 UDP 数据包中的发送端信息。 ```c #include @@ -89,7 +89,7 @@ addrlen: 保存参数 from 的结构体变量长度的变量地址值。 代码: -- [uecho_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_client.c) +- [uecho_client.c](uecho_client.c) - [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) 编译运行: @@ -97,7 +97,7 @@ addrlen: 保存参数 from 的结构体变量长度的变量地址值。 ```shell gcc uecho_client.c -o uclient gcc uecho_server.c -o userver -./server 9190 +./userver 9190 ./uclient 127.0.0.1 9190 ``` @@ -119,7 +119,7 @@ UDP 程序中,调用 sendto 函数传输数据前应该完成对套接字的 #### 6.3.1 存在数据边界的 UDP 套接字 -前面说得 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」 +前面说的 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」 相反,UDP 是具有数据边界的协议,传输中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数和输出函数的调用次数应该完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。通过一个例子来进行验证: @@ -145,13 +145,13 @@ host1 是服务端,host2 是客户端,host2 一次性把数据发给服务 #### 6.3.2 已连接(connect)UDP 套接字与未连接(unconnected)UDP 套接字 -TCP 套接字中需注册待传传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段: +TCP 套接字中需注册待传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段: - 第 1 阶段:向 UDP 套接字注册目标 IP 和端口号 - 第 2 阶段:传输数据 - 第 3 阶段:删除 UDP 套接字中注册的目标地址信息。 -每次调用 sendto 函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接 connected 套接字。显然,UDP 套接字默认属于未连接套接字。当一台主机向另一台主机传输很多信息时,上述的三个阶段中,第一个阶段和第三个阶段占整个通信过程中近三分之一的时间,缩短这部分的时间将会大大提高整体性能。 +每次调用 sendto 函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为已连接(connected)套接字。显然,UDP 套接字默认属于未连接套接字。当一台主机向另一台主机传输很多信息时,上述的三个阶段中,第一个阶段和第三个阶段占整个通信过程中近三分之一的时间,缩短这部分的时间将会大大提高整体性能。 #### 6.3.3 创建已连接 UDP 套接字 @@ -170,7 +170,7 @@ connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 之后就与 TCP 套接字一致,每次调用 sendto 函数时只需传递信息数据。因为已经指定了收发对象,所以不仅可以使用 sendto、recvfrom 函数,还可以使用 write、read 函数进行通信。 -下面的例子把之前的 [uecho_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_client.c) 程序改成了基于已连接 UDP 的套接字的程序,因此可以结合 [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) 程序运行。代码如下: +下面的例子把之前的 [uecho_client.c](uecho_client.c) 程序改成了基于已连接 UDP 的套接字的程序,因此可以结合 [uecho_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_server.c) 程序运行。代码如下: - [uecho_con_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/uecho_con_client.c) @@ -188,7 +188,7 @@ connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 1. **UDP 为什么比 TCP 快?为什么 TCP 传输可靠而 UDP 传输不可靠?** - 答:为了提供可靠的数据传输服务,TCP 在不可靠的IP层进行流控制,而 UDP 缺少这种流控制。所以 UDP 是不可靠的传输方式。 + 答:UDP 比 TCP 快主要有两点原因:一是 UDP 不需要收发数据前后的连接建立与拆除过程;二是 UDP 不进行为保证可靠性而添加的流控制。而 TCP 传输可靠,是因为它在不可靠的 IP 层之上进行了流控制等可靠性保障;UDP 缺少这些保障,所以是不可靠的传输方式。 2. **下面不属于 UDP 特点的是?** @@ -200,7 +200,7 @@ connect(sock, (struct sockaddr *)&adr, sizeof(adr)); 4. **UDP 套接字和 TCP 套接字可以共存。若需要,可以同时在同一主机进行 TCP 和 UDP 数据传输。** 5. 针对 UDP 函数也可以调用 connect 函数,此时 UDP 套接字跟 TCP 套接字相同,也需要经过 3 次握手阶段。 - 答:第2句和第5句不属于 UDP 的特点(即这两句话是错误的)。第2句错误是因为 UDP 只需一个套接字就可以向多个目标传输数据;第5句错误是因为 UDP 调用 connect 函数只是注册目标地址信息,不会进行 TCP 那样的三次握手过程。 + 答:第2句、第3句和第5句不属于 UDP 的特点(即这三句话是错误的)。第2句错误是因为 UDP 只需一个套接字就可以向多个目标传输数据;第3句错误是因为 TCP 和 UDP 的端口空间相互独立,UDP 套接字可以使用与 TCP 相同的端口号;第5句错误是因为 UDP 调用 connect 函数只是注册目标地址信息,不会进行 TCP 那样的三次握手过程。 3. **UDP 数据报向对方主机的 UDP 套接字传递过程中,IP 和 UDP 分别负责哪些部分?** diff --git a/ch06/bound_host1.c b/ch06/bound_host1.c index 4a1108f..1f79ef6 100644 --- a/ch06/bound_host1.c +++ b/ch06/bound_host1.c @@ -38,6 +38,8 @@ int main(int argc, char *argv[]) adr_sz = sizeof(your_adr); str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&your_adr, &adr_sz); + if (str_len == -1) + error_handling("recvfrom() error"); printf("Message %d: %s \n", i + 1, message); } close(sock); diff --git a/ch06/bound_host2.c b/ch06/bound_host2.c index 42b9104..db73c98 100644 --- a/ch06/bound_host2.c +++ b/ch06/bound_host2.c @@ -16,7 +16,6 @@ int main(int argc, char *argv[]) char msg3[] = "Nice to meet you"; struct sockaddr_in your_adr; - socklen_t your_adr_sz; if (argc != 3) { printf("Usage : %s \n", argv[0]); diff --git a/ch06/homework/uchar_client.c b/ch06/homework/uchar_client.c index b58e0c3..0c36765 100644 --- a/ch06/homework/uchar_client.c +++ b/ch06/homework/uchar_client.c @@ -32,12 +32,12 @@ int main(int argc, char* argv[]) while(1) { - fputs("Inset message(q to Quit): ", stdout); + fputs("Insert message(q to Quit): ", stdout); fgets(message, sizeof(message), stdin); if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break; - sendto(sock, message, BUF_SIZE, 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); + 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); diff --git a/ch06/uecho_client.c b/ch06/uecho_client.c index 0e7c817..9599b31 100644 --- a/ch06/uecho_client.c +++ b/ch06/uecho_client.c @@ -41,7 +41,7 @@ int main(int argc, char *argv[]) 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, + str_len = recvfrom(sock, message, BUF_SIZE - 1, 0, (struct sockaddr *)&from_adr, &adr_sz); message[str_len] = 0; printf("Message from server: %s", message); diff --git a/ch06/uecho_con_client.c b/ch06/uecho_con_client.c index 3cd3b9e..719209b 100644 --- a/ch06/uecho_con_client.c +++ b/ch06/uecho_con_client.c @@ -13,9 +13,7 @@ int main(int argc, char *argv[]) int sock; char message[BUF_SIZE]; int str_len; - socklen_t adr_sz; //多余变量 - - struct sockaddr_in serv_adr, from_adr; //不需要 from_adr + struct sockaddr_in serv_adr; if (argc != 3) { printf("Usage : %s \n", argv[0]); diff --git a/ch06/uecho_server.c b/ch06/uecho_server.c index c6301ed..33bd70b 100644 --- a/ch06/uecho_server.c +++ b/ch06/uecho_server.c @@ -24,7 +24,7 @@ int main(int argc, char *argv[]) //创建 UDP 套接字后,向 socket 的第二个参数传递 SOCK_DGRAM serv_sock = socket(PF_INET, SOCK_DGRAM, 0); if (serv_sock == -1) - error_handling("UDP socket creation eerror"); + error_handling("UDP socket creation error"); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; diff --git a/ch07/README.md b/ch07/README.md index aeaabfc..b35ffc3 100644 --- a/ch07/README.md +++ b/ch07/README.md @@ -1,8 +1,8 @@ -## 第 7 章 优雅的断开套接字的连接 +## 第 7 章 优雅地断开套接字的连接 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 -本章讨论如何优雅的断开套接字的连接,之前用的方法不够优雅是因为,我们是调用 close 函数或 closesocket 函数单方面断开连接的。 +本章讨论如何优雅地断开套接字的连接,之前用的方法不够优雅是因为,我们是调用 close 函数或 closesocket 函数单方面断开连接的。 ### 7.1 基于 TCP 的半关闭 @@ -14,9 +14,9 @@ Linux 的 close 函数和 Windows 的 closesocket 函数意味着完全断开连 ![](images/5c412a8baa2d8.png) -图中描述的是 2 台主机正在进行双向通信,主机 A 发送完最后的数据后,调用 close 函数断开了最后的连接,之后主机 A 无法再接受主机 B 传输的数据。实际上,是完全无法调用与接受数据相关的函数。最终,由主机 B 传输的、主机 A 必须要接受的数据也销毁了。 +图中描述的是 2 台主机正在进行双向通信,主机 A 发送完最后的数据后,调用 close 函数断开了最后的连接,之后主机 A 无法再接收主机 B 传输的数据。实际上,是完全无法调用与接收数据相关的函数。最终,由主机 B 传输的、主机 A 必须要接收的数据也销毁了。 -为了解决这类问题,「只关闭一部分数据交换中使用的流」的方法应运而生。断开一部分连接是指,可以传输数据但是无法接收,或可以接受数据但无法传输。顾名思义就是只关闭流的一半。 +为了解决这类问题,「只关闭一部分数据交换中使用的流」的方法应运而生。断开一部分连接是指,可以传输数据但是无法接收,或可以接收数据但无法传输。顾名思义就是只关闭流的一半。 #### 7.1.2 套接字和流(Stream) @@ -37,7 +37,7 @@ shutdown 用来关闭其中一个流: int shutdown(int sock, int howto); /* 成功时返回 0 ,失败时返回 -1 -sock: 需要断开套接字文件描述符 +sock: 需要断开的套接字文件描述符 howto: 传递断开方式信息 */ ``` @@ -48,7 +48,7 @@ howto: 传递断开方式信息 - `SHUT_WR` : 断开输出流 - `SHUT_RDWR` : 同时断开 I/O 流 -若向 shutdown 的第二个参数传递`SHUT_RD`,则断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用相关函数。如果向 shutdown 的第二个参数传递`SHUT_WR`,则中断输出流,也就无法传输数据。若如果输出缓冲中还有未传输的数据,则将传递给目标主机。最后,若传递关键字`SHUT_RDWR`,则同时中断 I/O 流。这相当于分 2 次调用 shutdown ,其中一次以`SHUT_RD`为参数,另一次以`SHUT_WR`为参数。 +若向 shutdown 的第二个参数传递`SHUT_RD`,则断开输入流,套接字无法接收数据。即使输入缓冲收到数据也会抹去,而且无法调用相关函数。如果向 shutdown 的第二个参数传递`SHUT_WR`,则中断输出流,也就无法传输数据。如果输出缓冲中还有未传输的数据,则将传递给目标主机。最后,若传递关键字`SHUT_RDWR`,则同时中断 I/O 流。这相当于分 2 次调用 shutdown ,其中一次以`SHUT_RD`为参数,另一次以`SHUT_WR`为参数。 #### 7.1.4 为何要半关闭 @@ -56,15 +56,15 @@ howto: 传递断开方式信息 > 一旦客户端连接到服务器,服务器将约定的文件传输给客户端,客户端收到后发送字符串「Thank you」给服务器端。 -此处「Thank you」的传递是多余的,这只是用来模拟客户端断开连接前还有数据要传输的情况。此时程序的编写难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止的调用输入函数,因为这有可能导致程序**阻塞**。 +此处「Thank you」的传递是多余的,这只是用来模拟客户端断开连接前还有数据要传输的情况。此时程序的编写难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止地调用输入函数,因为这有可能导致程序**阻塞**。 > 是否可以让服务器和客户端约定一个代表文件尾的字符? -这种方式也有问题,因为这意味这文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接受 EOF ,这样可以避免与文件内容冲突。那么问题来了,服务端如何传递 EOF ? +这种方式也有问题,因为这意味着文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接收 EOF ,这样可以避免与文件内容冲突。那么问题来了,服务端如何传递 EOF ? > 断开输出流时向主机传输 EOF。 -当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接受对方传输的数据。换言之,若调用 close 函数关闭流,就无法接受客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。 +当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接收对方传输的数据。换言之,若调用 close 函数关闭流,就无法接收客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。 #### 7.1.5 基于半关闭的文件传输程序 @@ -74,8 +74,8 @@ howto: 传递断开方式信息 下面的代码为编程简便,省略了大量错误处理代码。 -- [file_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch07/file_client.c) -- [file_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch07/file_server.c) +- [file_client.c](file_client.c) +- [file_server.c](file_server.c) 编译运行: @@ -90,7 +90,7 @@ gcc file_server.c -o fserver ![](images/5c4140bc8db2f.png) -客户端接受完成后,服务器会接收到来自客户端的感谢信息。 +客户端接收完成后,服务器会接收到来自客户端的感谢信息。 ### 7.2 基于 Windows 的实现 diff --git a/ch07/file_client.c b/ch07/file_client.c index 7a650e4..a847ae1 100644 --- a/ch07/file_client.c +++ b/ch07/file_client.c @@ -23,7 +23,7 @@ int main(int argc, char *argv[]) exit(1); } - fp = fopen("receive.cpp", "wb"); + fp = fopen("receive.dat", "wb"); sd = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_adr, 0, sizeof(serv_adr)); diff --git a/ch08/README.md b/ch08/README.md index 817fd0a..e6cf210 100644 --- a/ch08/README.md +++ b/ch08/README.md @@ -22,7 +22,7 @@ DNS 是对IP地址和域名进行相互转换的系统,其核心是 DNS 服务 #### 8.2.1 程序中有必要使用域名吗? -一句话,需要,因为IP地址可能经常改变,而且也不容易记忆,通过域名可以随时更改解析,达到更换IP的目的 +一句话,需要,因为IP地址可能经常改变,而且也不容易记忆,通过域名可以随时更改解析,达到更换IP的目的。 #### 8.2.2 利用域名获取IP地址 @@ -53,7 +53,7 @@ struct hostent - h_name:该变量中存有官方域名(Official domain name)。官方域名代表某一主页,但实际上,一些著名公司的域名并没有用官方域名注册。 - h_aliases:可以通过多个域名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可以指定其他域名。这些信息可以通过 h_aliases 获得。 -- h_addrtype:gethostbyname 函数不仅支持 IPv4 还支持 IPv6。因此可以通过此变量获取保存在 h_addr_list 的IP地址族信息。若是 IPv4,则此变量中存有 AF_INET。 +- h_addrtype:该成员用于获取保存在 h_addr_list 中的地址族信息。若是 IPv4,则此变量中存有 AF_INET;若是 IPv6,则存有 AF_INET6。需要注意,gethostbyname 函数本身只返回 IPv4(AF_INET)地址,若需解析 IPv6 地址应改用 getaddrinfo 函数。 - h_length:保存IP地址长度。若是 IPv4 地址,因为是 4 个字节,则保存 4;IPv6 时,因为是 16 个字节,故保存 16。 - h_addr_list:这个是最重要的成员。通过此变量以整数形式保存域名相对应的IP地址。另外,用户比较多的网站有可能分配多个IP地址给同一个域名,利用多个服务器做负载均衡。此时可以通过此变量获取IP地址信息。 @@ -63,7 +63,7 @@ struct hostent 下面的代码通过一个例子来演示 gethostbyname 的应用,并说明 hostent 结构体变量特性。 -- [gethostbyname.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch08/gethostbyname.c) +- [gethostbyname.c](gethostbyname.c) 编译运行: @@ -78,11 +78,10 @@ gcc gethostbyname.c -o hostname 如图所示,显示出了对百度的域名解析 -可以看出,百度有一个域名解析是 CNAME 解析的,指向了`shifen.com`,关于百度具体的解析过程。 +可以看出,百度的一个域名解析使用了 CNAME 记录,指向了 `shifen.com`。关于百度具体的解析过程,可参考下方资料。 > 这一部分牵扯到了很多关于DNS解析的过程,还有 Linux 下关于域名解析的一些命令,我找了一部分资料,可以点下面的链接查看比较详细的: > -> - [关于百度DNS的解析过程](http://zhan.renren.com/starshen?gid=3602888498023142484&checked=true) > - [DNS解析的过程是什么,求详细的?](https://www.zhihu.com/question/23042131/answer/66571369) > - [Linux DNS 查询剖析](https://zhuanlan.zhihu.com/p/45535596) > - [Linux DNS查询命令](http://www.live-in.org/archives/1938.html) @@ -117,7 +116,7 @@ family: 传递地址族信息,IPv4 是 AF_INET,IPv6 是 AF_INET6。 下面的代码演示使用方法: -- [gethostbyaddr.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch08/gethostbyaddr.c) +- [gethostbyaddr.c](gethostbyaddr.c) 编译运行: diff --git a/ch08/gethostbyname.c b/ch08/gethostbyname.c index 388cc53..744153e 100644 --- a/ch08/gethostbyname.c +++ b/ch08/gethostbyname.c @@ -11,7 +11,7 @@ int main(int argc, char *argv[]) struct hostent *host; if (argc != 2) { - printf("Usage : %s \n", argv[0]); + printf("Usage : %s \n", argv[0]); exit(1); } // 把参数传递给函数,返回结构体 @@ -20,7 +20,7 @@ int main(int argc, char *argv[]) error_handling("gethost... error"); // 输出官方域名 printf("Official name: %s \n", host->h_name); - // Aliases 貌似是解析的 cname 域名? + // Aliases 为该主机的别名(常对应 DNS 的 CNAME 记录) for (i = 0; host->h_aliases[i]; i++) printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]); //看看是不是ipv4 diff --git a/ch09/README.md b/ch09/README.md index 5fb776c..74a6f42 100644 --- a/ch09/README.md +++ b/ch09/README.md @@ -8,7 +8,7 @@ #### 9.1.1 套接字多种可选项 -我们之前写得程序都是创建好套接字之后直接使用的,此时通过默认的套接字特性进行数据通信,这里列出了一些套接字可选项。 +我们之前写的程序都是创建好套接字之后直接使用的,此时通过默认的套接字特性进行数据通信,这里列出了一些套接字可选项。 | 协议层 | 选项名 | 读取 | 设置 | | :----: | :----: |:--: | :--: | @@ -32,7 +32,7 @@ 从表中可以看出,套接字可选项是分层的。 -- IPPROTO_IP 可选项是IP协议相关事项 +- IPPROTO_IP 层可选项是 IP 协议相关事项 - IPPROTO_TCP 层可选项是 TCP 协议的相关事项 @@ -72,9 +72,9 @@ optlen: 向第四个参数传递的缓冲大小值(选项值的长度)。 */ ``` -下面的代码可以看出 getsockopt 的使用方法。下面示例用协议层为 SOL_SOCKET 、名为 SO_TYPE 的可选项查看套接字类型(TCP 和 UDP )。 +从下面的代码可以看出 getsockopt 的使用方法。下面示例用协议层为 SOL_SOCKET 、名为 SO_TYPE 的可选项查看套接字类型(TCP 和 UDP )。 -- [sock_type.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch09/sock_type.c) +- [sock_type.c](sock_type.c) 编译运行: @@ -94,7 +94,7 @@ Socket type two: 2 首先创建了一个 TCP 套接字和一个 UDP 套接字。然后通过调用 getsockopt 函数来获得当前套接字的状态。 -用于验证套接类型的 SO_TYPE 是只读可选项,因为**套接字类型只能在创建时决定,以后不能再更改**。 +用于验证套接字类型的 SO_TYPE 是只读可选项,因为**套接字类型只能在创建时决定,以后不能再更改**。 #### 9.1.3 `SO_SNDBUF` & `SO_RCVBUF` @@ -102,7 +102,7 @@ Socket type two: 2 SO_RCVBUF 是输入缓冲大小相关可选项,SO_SNDBUF 是输出缓冲大小相关可选项。用这 2 个可选项既可以读取当前 I/O 大小,也可以进行更改。通过下列示例读取创建套接字时默认的 I/O 缓冲大小。 -- [get_buf.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch09/get_buf.c) +- [get_buf.c](get_buf.c) 编译运行: @@ -122,7 +122,7 @@ Output buffer size: 16384 下面的代码演示了,通过程序设置 I/O 缓冲区的大小 -- [set_buf.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch09/set_buf.c) +- [set_buf.c](set_buf.c) 编译运行: @@ -138,7 +138,7 @@ Input buffer size: 6144 Output buffer size: 6144 ``` -输出结果和我们预想的不是很相同,缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行。 +输出结果和我们预想的不完全相同,缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行。 ### 9.2 `SO_REUSEADDR` @@ -146,15 +146,15 @@ Output buffer size: 6144 在学习 SO_REUSEADDR 可选项之前,应该好好理解 Time-wait 状态。看以下代码的示例: -- [reuseadr_eserver.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch09/reuseadr_eserver.c) +- [reuseadr_eserver.c](reuseadr_eserver.c) -这是一个回声服务器的服务端代码,可以配合第四章的 [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/echo_client.c) 使用,在这个代码中,客户端通知服务器终止程序。在客户端控制台输入 Q 可以结束程序,向服务器发送 FIN 消息并经过四次握手过程。当然,输入 CTRL+C 也会向服务器传递 FIN 信息。强制终止程序时,由操作系统关闭文件套接字,此过程相当于调用 close 函数,也会向服务器发送 FIN 消息。 +这是一个回声服务器的服务端代码,可以配合第四章的 [echo_client.c](../ch04/echo_client.c) 使用,在这个代码中,客户端通知服务器终止程序。在客户端控制台输入 Q 可以结束程序,向服务器发送 FIN 消息并经过四次握手过程。当然,输入 CTRL+C 也会向服务器传递 FIN 信息。强制终止程序时,由操作系统关闭套接字,此过程相当于调用 close 函数,也会向服务器发送 FIN 消息。 -这样看不到是什么特殊现象,考虑以下情况: +这样并不会出现什么特殊现象,考虑以下情况: > 服务器端和客户端都已经建立连接的状态下,向服务器控制台输入 CTRL+C ,强制关闭服务端 -如果用这种方式终止程序,如果用同一端口号再次运行服务端,就会输出「bind() error」消息,并且无法再次运行。但是在这种情况下,再过大约 3 分钟就可以重新运行服务端。 +如果用这种方式终止程序,再用同一端口号再次运行服务端,就会输出「bind() error」消息,并且无法再次运行。但是在这种情况下,再过大约 3 分钟就可以重新运行服务端。 #### 9.2.2 `Time-wait` 状态 @@ -162,11 +162,11 @@ Output buffer size: 6144 ![](images/5c42db182cade.png) -假设图中主机 A 是服务器,因为是主机 A 向 B 发送 FIN 消息,故可想象成服务器端在控制台中输入 CTRL+C 。但是问题是,套接字经过四次握手后并没有立即消除,而是要经过一段时间的 Time-wait 状态。当然,只有先断开连接的(先发送 FIN 消息的)主机才经过 Time-wait 状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在 Time-wait 过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind 函数调用过程中会发生错误。 +假设图中主机 A 是服务器,因为是主机 A 向 B 发送 FIN 消息,故可想象成服务器端在控制台中输入 CTRL+C 。但是问题是,套接字经过四次握手后并没有立即销毁,而是要经过一段时间的 Time-wait 状态。当然,只有先断开连接的(先发送 FIN 消息的)主机才经过 Time-wait 状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在 Time-wait 过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind 函数调用过程中会发生错误。 **实际上,不论是服务端还是客户端,都要经过一段时间的 Time-wait 过程。先断开连接的套接字必然会经过 Time-wait 过程,但是由于客户端套接字的端口是任意指定的,所以无需过多关注 Time-wait 状态。** -那到底为什么会有 Time-wait 状态呢,在图中假设,主机 A 向主机 B 传输 ACK 消息(SEQ 5001 , ACK 7502 )后立刻消除套接字。但是最后这条 ACK 消息在传递过程中丢失,没有传递主机 B ,这时主机 B 就会试图重传。但是此时主机 A 已经是完全终止状态,因此主机 B 永远无法收到从主机 A 最后传来的 ACK 消息。基于这些问题的考虑,所以要设计 Time-wait 状态。 +那到底为什么会有 Time-wait 状态呢?在图中假设,主机 A 向主机 B 传输 ACK 消息(SEQ 5001 , ACK 7502 )后立刻销毁套接字。但是最后这条 ACK 消息在传递过程中丢失,没有传递主机 B ,这时主机 B 就会试图重传。但是此时主机 A 已经是完全终止状态,因此主机 B 永远无法收到从主机 A 最后传来的 ACK 消息。基于这些问题的考虑,所以要设计 Time-wait 状态。 #### 9.2.3 地址再分配 @@ -174,9 +174,9 @@ Time-wait 状态看似重要,但是不一定讨人喜欢。如果系统发生 ![](images/5c42dec2ba42b.png) -从图上可以看出,在主机 A 四次握手的过程中,如果最后的数据丢失,则主机 B 会认为主机 A 未能收到自己发送的 FIN 信息,因此重传。这时,收到的 FIN 消息的主机 A 将重启 Time-wait 计时器。因此,如果网络状况不理想, Time-wait 将持续。 +从图上可以看出,在主机 A 四次握手的过程中,如果最后的数据丢失,则主机 B 会认为主机 A 未能收到自己发送的 FIN 信息,因此重传。这时,收到重传 FIN 消息的主机 A 将重启 Time-wait 计时器。因此,如果网络状况不理想, Time-wait 将持续。 -解决方案就是在套接字的可选项中更改 SO_REUSEADDR 的状态。适当调整该参数,可将 Time-wait 状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR 的默认值为 0.这就意味着无法分配 Time-wait 状态下的套接字端口号。因此需要将这个值改成 1 。具体作法已在示例 [reuseadr_eserver.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch09/reuseadr_eserver.c) 给出,只需要把注释掉的东西解除注释即可。 +解决方案就是在套接字的可选项中更改 SO_REUSEADDR 的状态。适当调整该参数,可将 Time-wait 状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR 的默认值为 0,这就意味着无法分配 Time-wait 状态下的套接字端口号。因此需要将这个值改成 1 。具体作法已在示例 [reuseadr_eserver.c](reuseadr_eserver.c) 给出,只需要取消相关代码的注释即可。 ```c optlen = sizeof(option); @@ -190,7 +190,7 @@ setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen); #### 9.3.1 `Nagle` 算法 -为了防止因数据包过多而发生网络过载,`Nagle` 算法诞生了。它应用于 TCP 层。它是否使用会导致如图所示的差异: +为了防止因数据包过多而发生网络过载,`Nagle` 算法诞生了。它应用于 TCP 层。是否使用它会导致如图所示的差异: ![](images/5c42e12abc5b8.png) @@ -198,7 +198,7 @@ setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen); **只有接收到前一数据的 ACK 消息, `Nagle` 算法才发送下一数据。** -TCP 套接字默认使用 `Nagle` 算法交换数据,因此最大限度的进行缓冲,直到收到 ACK 。左图也就是说一共传递 4 个数据包以传输一个字符串。从右图可以看出,发送数据包一共使用了 10 个数据包。由此可知,不使用 `Nagle` 算法将对网络流量产生负面影响。即使只传输一个字节的数据,其头信息都可能是几十个字节。因此,为了提高网络传输效率,必须使用 `Nagle` 算法。 +TCP 套接字默认使用 `Nagle` 算法交换数据,因此最大限度地缓冲,直到收到 ACK 。左图也就是说一共传递 4 个数据包以传输一个字符串。从右图可以看出,发送数据包一共使用了 10 个数据包。由此可知,不使用 `Nagle` 算法将对网络流量产生负面影响。即使只传输一个字节的数据,其头信息都可能是几十个字节。因此,为了提高网络传输效率,通常应使用 `Nagle` 算法。 `Nagle` 算法并不是什么情况下都适用,网络流量未受太大影响时,不使用 `Nagle` 算法要比使用它时传输速度快。最典型的就是「传输大文件数据」。将文件数据传入输出缓冲不会花太多时间,因此,不使用 `Nagle` 算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而在无需等待 ACK 的前提下连续传输,因此可以大大提高传输速度。 @@ -220,7 +220,7 @@ opt_len = sizeof(opt_val); getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len); ``` -如果正在使用`Nagle` 算法,那么 opt_val 值为 0,如果禁用则为 1. +如果正在使用`Nagle` 算法,那么 opt_val 值为 0,如果禁用则为 1。 关于这个算法,可以参考这个回答:[TCP连接中启用和禁用TCP_NODELAY有什么影响?](https://www.zhihu.com/question/42308970/answer/246334766) @@ -234,13 +234,13 @@ getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len); 1. **下列关于 Time-wait 状态的说法错误的是?** - 答:错误的说法是第 1、3、4 项。正确的说法是第 2 项(加粗显示)。 - 1. ~~Time-wait 状态只在服务器的套接字中发生~~(错误:客户端先断开连接时也会进入 Time-wait 状态) 2. **断开连接的四次握手过程中,先传输 FIN 消息的套接字将进入 Time-wait 状态。**(正确) 3. ~~Time-wait 状态与断开连接的过程无关,而与请求连接过程中 SYN 消息的传输顺序有关~~(错误:Time-wait 状态与断开连接的四次握手过程直接相关) 4. ~~Time-wait 状态通常并非必要,应尽可能通过更改套接字可选项来防止其发生~~(错误:Time-wait 状态对于保证 TCP 连接可靠关闭是必要的,但在某些紧急重启场景下可通过 SO_REUSEADDR 重用端口) + 答:错误的说法是第 1、3、4 项。正确的说法是第 2 项(加粗显示)。 + 2. **TCP_NODELAY 可选项与 Nagle 算法有关,可通过它禁用 Nagle 算法。请问何时应考虑禁用 Nagle 算法?结合收发数据的特性给出说明。** - 答:当网络流量未受太大影响时,不使用 Nagle 算法要比使用它时传输速度快,比如说在传输大文件时。 + 答:当网络流量未受太大影响、且需要连续传输大量数据(如传输大文件)时,应考虑禁用 Nagle 算法。此时数据能快速填满输出缓冲,无需等待前一数据的 ACK 即可连续发送数据包,既不会明显增加数据包数量,又能避免等待 ACK 带来的延迟,从而提高传输速度。 diff --git a/ch09/set_buf.c b/ch09/set_buf.c index 537280e..5c59957 100644 --- a/ch09/set_buf.c +++ b/ch09/set_buf.c @@ -12,7 +12,6 @@ int main(int argc, char *argv[]) socklen_t len; sock = socket(PF_INET, SOCK_STREAM, 0); - len = sizeof(snd_buf); state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf)); if (state) error_handling("setsockopt() error"); diff --git a/ch10/README.md b/ch10/README.md index 128be7a..e67a057 100644 --- a/ch10/README.md +++ b/ch10/README.md @@ -24,7 +24,7 @@ #### 10.1.3 进程 ID -在介绍进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的,所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程 ID」,其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。接下来观察在 Linux 中运行的进程。 +在介绍进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的,所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程 ID」,其值为大于 1 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。接下来观察在 Linux 中运行的进程。 ```shell ps au @@ -53,7 +53,7 @@ fork 函数将创建调用的进程副本。也就是说,并非根据完全不 ![](images/5c43da5412b90.png) -从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11,将局部变量 lval 的值增加到 25,因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回值区分父子进程。父进程的 lval 的值增加 1,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 1 也不会影响到父进程的 gval。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子: +从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11,将局部变量 lval 的值增加到 25,因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回值区分父子进程。父进程的 lval 的值减少 2,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 2 也不会影响到父进程的 gval。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子: - [fork.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/fork.c) @@ -100,7 +100,7 @@ gcc fork.c -o fork 进程的工作完成后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作「僵尸进程」,这也是给系统带来负担的原因之一。 -> 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源 +> 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出,子进程被 init 接管,子进程退出后 init 会回收其占用的相关资源。 **维基百科**: @@ -169,7 +169,7 @@ gcc zombie.c -o zombie ![](images/5c4439a751b11.png) -通过 `ps au` 命令可以看出,子进程仍然存在,并没有被销毁,僵尸进程在这里显示为 `Z+`.30秒后,红框里面的两个进程会同时被销毁。 +通过 `ps au` 命令可以看出,子进程仍然存在,并没有被销毁,僵尸进程在这里显示为 `Z+`。30 秒后,红框里面的两个进程会同时被销毁。 > 利用 `./zombie &`可以使程序在后台运行,不用打开新的命令行窗口。 @@ -188,7 +188,7 @@ pid_t wait(int *statloc); 调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数返回值,main 函数的 return 返回值)将保存到该函数的参数所指的内存空间。但函数参数指向的单元中还包含其他信息,因此需要用下列宏进行分离: - WIFEXITED 子进程正常终止时返回「真」 -- WEXITSTATUS 返回子进程时的返回值 +- WEXITSTATUS 返回子进程正常终止时的返回值 也就是说,向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码: @@ -230,7 +230,7 @@ int main(int argc, char *argv[]) else { printf("Child PID: %d \n", pid); - wait(&status); //之间终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁 + wait(&status); //之前终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁 if (WIFEXITED(status)) //通过 WIFEXITED 来验证子进程是否正常终止。如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值 printf("Child send one: %d \n", WEXITSTATUS(status)); @@ -270,7 +270,7 @@ pid_t waitpid(pid_t pid, int *statloc, int options); 成功时返回终止的子进程ID 或 0 ,失败时返回 -1 pid: 等待终止的目标子进程的ID,若传 -1,则与 wait 函数相同,可以等待任意子进程终止 statloc: 与 wait 函数的 statloc 参数具有相同含义 -options: 传递头文件 sys/wait.h 声明的常量 WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。 +options: 传递头文件 sys/wait.h 声明的常量 WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。 */ ``` @@ -327,9 +327,9 @@ gcc waitpid.c -o waitpid #### 10.3.1 向操作系统求助 -子进程终止的识别主题是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序 +子进程终止的识别主体是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序。 -为了实现上述的功能,引入信号处理机制(Signal Handing)。此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外,为了响应该消息,执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。 +为了实现上述的功能,引入信号处理机制(Signal Handling)。此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外,为了响应该消息,执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。 #### 10.3.2 信号与 signal 函数 @@ -388,7 +388,7 @@ unsigned int alarm(unsigned int seconds); // 返回0或以秒为单位的距 SIGALRM 信号发生所剩时间 ``` -如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0,则之前对 SIGALRM 信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用 signal 函数)终止进程,不做任何处理。 +如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0,则之前对 SIGALRM 信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则 SIGALRM 信号按默认处理方式终止进程。 - [signal.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/signal.c) @@ -412,7 +412,7 @@ int main(int argc, char *argv[]) int i; signal(SIGALRM, timeout); //注册信号及相应处理器 signal(SIGINT, keycontrol); - alarm(2); //预约 2 秒候发生 SIGALRM 信号 + alarm(2); //预约 2 秒后发生 SIGALRM 信号 for (i = 0; i < 3; i++) { @@ -539,7 +539,7 @@ Time out! 下面利用子进程终止时产生 SIGCHLD 信号这一点,来用信号处理来消灭僵尸进程。看以下代码: -- [remove_zomebie.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/remove_zomebie.c) +- [remove_zombie.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/remove_zombie.c) ```c #include @@ -603,7 +603,7 @@ int main(int argc, char *argv[]) 编译运行: ```shell -gcc remove_zomebie.c -o zombie +gcc remove_zombie.c -o zombie ./zombie ``` @@ -627,9 +627,9 @@ Child send: 24 wait ``` -请仔细观察结果,结果中的每一个空行代表间隔了 5 秒。程序先创建了两个子进程,子进程在 10 秒之后会返回值。第一个 wait 由于子进程在执行,所以直接被唤醒,然后这两个子进程正在睡眠 10 秒,所以 5 秒之后第二个 wait 开始执行,又过了 5 秒,两个子进程同时被唤醒。所以剩下的 wait 也被唤醒。 +请仔细观察结果,结果中的每一个空行代表间隔了 5 秒(父进程每轮循环 sleep 5 秒)。程序先创建了两个子进程,二者各自睡眠 10 秒后终止。父进程进入循环,每轮打印一次 wait 并 sleep 5 秒:前两轮期间子进程仍在睡眠;约第 10 秒时子进程终止,操作系统向父进程发送 SIGCHLD 信号,中断父进程的 sleep,转而执行信号处理函数,在其中通过 waitpid 回收子进程并获取其返回值,随后父进程继续后续轮次的循环。 -所以在本程序的过程中,当子进程终止时候,会向系统发送一个信号,然后调用我们提前写好的处理函数,在处理函数中使用 waitpid 来处理僵尸进程,获取子进程返回值。 +所以在本程序的过程中,当子进程终止时,操作系统会向父进程发送 SIGCHLD 信号,进而调用我们提前注册的处理函数,在处理函数中使用 waitpid 来回收僵尸进程,获取子进程返回值。 ### 10.4 基于多任务的并发服务器 @@ -642,8 +642,8 @@ wait 从图中可以看出,每当有客户端请求时(连接请求),回声服务器都创建子进程以提供服务。如果请求的客户端有 5 个,则将创建 5 个子进程来提供服务,为了完成这些任务,需要经过如下过程: - 第一阶段:回声服务器端(父进程)通过调用 accept 函数受理连接请求 -- 第二阶段:此时获取的套接字文件描述符创建并传递给子进程 -- 第三阶段:进程利用传递来的文件描述符提供服务 +- 第二阶段:通过 fork 创建子进程,并将此时获取的套接字文件描述符传递给子进程 +- 第三阶段:子进程利用传递来的文件描述符提供服务 #### 10.4.2 实现并发服务器 @@ -655,7 +655,7 @@ wait ```shell gcc echo_mpserv.c -o eserver -./eserver +./eserver 9190 ``` 结果: @@ -666,7 +666,7 @@ gcc echo_mpserv.c -o eserver 示例中给出了通过 fork 函数复制文件描述符的过程。父进程将 2 个套接字(一个是服务端套接字另一个是客户端套接字)文件描述符复制给了子进程。 -调用 fork 函数时赋值父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。 +调用 fork 函数时复制父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。 ![](images/kP7Rjx.png) @@ -686,7 +686,7 @@ gcc echo_mpserv.c -o eserver ![](images/kPbhkD.png) -从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进程传输。 +从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进行传输。 分割 I/O 程序的另外一个好处是,可以提高频繁交换数据的程序性能,如下图所示: diff --git a/ch10/echo_mpclient.c b/ch10/echo_mpclient.c index 751ce4d..f47fbe8 100644 --- a/ch10/echo_mpclient.c +++ b/ch10/echo_mpclient.c @@ -44,8 +44,8 @@ void read_routine(int sock, char *buf) { while (1) { - int str_len = read(sock, buf, BUF_SIZE); - if (str_len == 0) + int str_len = read(sock, buf, BUF_SIZE - 1); + if (str_len <= 0) return; buf[str_len] = 0; @@ -59,7 +59,7 @@ void write_routine(int sock, char *buf) fgets(buf, BUF_SIZE, stdin); if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")) { - shutdown(sock, SHUT_WR); //向服务器端传递 EOF,因为fork函数复制了文件描述度,所以通过1次close调用不够 + shutdown(sock, SHUT_WR); //向服务器端传递 EOF,因为 fork 函数复制了文件描述符,所以通过 1 次 close 调用不够 return; } write(sock, buf, strlen(buf)); diff --git a/ch10/echo_mpserv.c b/ch10/echo_mpserv.c index 6532048..ca22af8 100644 --- a/ch10/echo_mpserv.c +++ b/ch10/echo_mpserv.c @@ -23,7 +23,7 @@ int main(int argc, char *argv[]) char buf[BUF_SIZE]; if (argc != 2) { - printf("Usgae : %s \n", argv[0]); + printf("Usage : %s \n", argv[0]); exit(1); } act.sa_handler = read_childproc; //防止僵尸进程 @@ -58,7 +58,7 @@ int main(int argc, char *argv[]) if (pid == 0) //子进程运行区域,此部分向客户端提供回声服务 { close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程 - while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0) + while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) write(clnt_sock, buf, str_len); close(clnt_sock); diff --git a/ch10/remove_zomebie.c b/ch10/remove_zombie.c similarity index 100% rename from ch10/remove_zomebie.c rename to ch10/remove_zombie.c diff --git a/ch10/sigaction.c b/ch10/sigaction.c index b16204a..fed5d85 100644 --- a/ch10/sigaction.c +++ b/ch10/sigaction.c @@ -11,10 +11,9 @@ void timeout(int sig) int main(int argc, char *argv[]) { - int i; struct sigaction act; act.sa_handler = timeout; //保存函数指针 - sigemptyset(&act.sa_mask); //将 sa_mask 函数的所有位初始化成0 + sigemptyset(&act.sa_mask); //将 sa_mask 成员的所有位初始化成0 act.sa_flags = 0; //sa_flags 同样初始化成 0 sigaction(SIGALRM, &act, 0); //注册 SIGALRM 信号的处理器。 diff --git a/ch10/signal.c b/ch10/signal.c index b9c72eb..97f807f 100644 --- a/ch10/signal.c +++ b/ch10/signal.c @@ -18,7 +18,7 @@ int main(int argc, char *argv[]) int i; signal(SIGALRM, timeout); //注册信号及相应处理器 signal(SIGINT, keycontrol); - alarm(2); //预约 2 秒候发生 SIGALRM 信号 + alarm(2); //预约 2 秒后发生 SIGALRM 信号 for (i = 0; i < 3; i++) { diff --git a/ch10/test_server.c b/ch10/test_server.c index 744c331..d2e2fa3 100644 --- a/ch10/test_server.c +++ b/ch10/test_server.c @@ -23,7 +23,7 @@ int main(int argc, char *argv[]) char buf[BUF_SIZE]; if (argc != 2) { - printf("Usgae : %s \n", argv[0]); + printf("Usage : %s \n", argv[0]); exit(1); } act.sa_handler = read_childproc; //防止僵尸进程 @@ -60,7 +60,7 @@ int main(int argc, char *argv[]) { printf("子进程的 serv_sock:%d,clnt_sock:%d\n", serv_sock, clnt_sock); close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程 - while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0) + while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) write(clnt_sock, buf, str_len); close(clnt_sock); diff --git a/ch10/wait.c b/ch10/wait.c index c09ef64..8f7276b 100644 --- a/ch10/wait.c +++ b/ch10/wait.c @@ -23,7 +23,7 @@ int main(int argc, char *argv[]) else { printf("Child PID: %d \n", pid); - wait(&status); //之间终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁 + wait(&status); //之前终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁 if (WIFEXITED(status)) //通过 WIFEXITED 来验证子进程是否正常终止。如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值 printf("Child send one: %d \n", WEXITSTATUS(status)); diff --git a/ch10/waitpid.c b/ch10/waitpid.c index 669c261..0dd478e 100644 --- a/ch10/waitpid.c +++ b/ch10/waitpid.c @@ -14,7 +14,7 @@ int main(int argc, char *argv[]) } else { - //调用waitpid 传递参数 WNOHANG ,这样之前有没有终止的子进程则返回0 + // 调用 waitpid 传递参数 WNOHANG,这样如果没有终止的子进程则返回 0 while (!waitpid(-1, &status, WNOHANG)) { sleep(3); diff --git a/ch10/zombie.c b/ch10/zombie.c index cb4fb3f..c836fc5 100644 --- a/ch10/zombie.c +++ b/ch10/zombie.c @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) } if (pid == 0) - puts("End child proess"); + puts("End child process"); else puts("End parent process"); return 0; diff --git a/ch11/README.md b/ch11/README.md index 76c6952..39d82e1 100644 --- a/ch11/README.md +++ b/ch11/README.md @@ -2,7 +2,7 @@ 本章代码,在[TCP-IP-NetworkNote](https://github.com/riba2534/TCP-IP-NetworkNote)中可以找到。 -进程间通信,意味着两个不同的进程中可以交换数据 +进程间通信,意味着两个不同的进程之间可以交换数据。 ### 11.1 进程间通信的基本概念 @@ -24,9 +24,9 @@ filedes[1]: 通过管道传输数据时使用的文件描述符,即管道入 */ ``` -父进程调用函数时将创建管道,同时获取对应于出入口的文件描述符,此时父进程可以读写同一管道。但父进程的目的是与子进程进行数据交换,因此需要将入口或出口中的 1 个文件描述符传递给子进程。下面的例子是关于该函数的使用方法: +父进程调用函数时将创建管道,同时获取对应于出入口的文件描述符,此时父进程可以读写同一管道。但父进程的目的是与子进程进行数据交换,因此需要通过 fork 让子进程继承这两个文件描述符,父子进程各自使用其中一端进行通信(实际编程中还应关闭各自不需要的一端)。下面的例子是关于该函数的使用方法: -- [pipe1.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/pipe1.c) +- [pipe1.c](pipe1.c) ```c #include @@ -80,7 +80,7 @@ Who are you? 下面是双向通信的示例: -- [pipe2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/pipe2.c) +- [pipe2.c](pipe2.c) ```c #include @@ -129,7 +129,7 @@ Parent proc output: Who are you? Child proc output: Thank you for your message ``` -运行结果是正确的,但是如果注释掉代码中子进程里的 `sleep(2);`(第18行),就会出现问题,导致一直等待下去。因为数据进入管道后变成了无主数据。也就是通过 read 函数先读取数据的进程将得到数据,即使该进程将数据传到了管道。因为,注释掉 `sleep(2);` 会产生问题。子进程可能读回自己向管道发送的数据。结果父进程调用 read 函数后,无限期等待数据进入管道。 +运行结果是正确的,但是如果注释掉代码中子进程里的 `sleep(2);`(第18行),就会出现问题,导致一直等待下去。因为数据进入管道后变成了无主数据。也就是通过 read 函数先读取数据的进程将得到数据,即使该进程将数据传到了管道。此时子进程可能在父进程读取前就把自己写入的数据读回,导致父进程的 read 调用无限期阻塞,等待永远不会到来的数据。 当一个管道不满足需求时,就需要创建两个管道,各自负责不同的数据流动,过程如下图所示: @@ -137,7 +137,7 @@ Child proc output: Thank you for your message 下面采用上述模型改进 `pipe2.c` 。 -- [pipe3.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/pipe3.c) +- [pipe3.c](pipe3.c) ```c #include @@ -176,13 +176,13 @@ int main(int argc, char *argv[]) #### 11.2.1 保存消息的回声服务器 -下面对第 10 章的 [echo_mpserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpserv.c) 进行改进,添加一个功能: +下面对第 10 章的 [echo_mpserv.c](../ch10/echo_mpserv.c) 进行改进,添加一个功能: > 将回声客户端传输的字符串按序保存到文件中 实现该任务将创建一个新进程,从向客户端提供服务的进程读取字符串信息,下面是代码: -- [echo_storeserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch11/echo_storeserv.c) +- [echo_storeserv.c](echo_storeserv.c) 编译运行: @@ -191,13 +191,13 @@ gcc echo_storeserv.c -o serv ./serv 9190 ``` -此服务端配合第 10 章的客户端 [echo_mpclient.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpclient.c) 使用,运行结果如下图: +此服务端配合第 10 章的客户端 [echo_mpclient.c](../ch10/echo_mpclient.c) 使用,运行结果如下图: ![](images/kFUCct.png) ![](images/kFUAHS.png) -从图上可以看出,服务端已经生成了文件,把客户端的消息保存了下来,只保存了10次消息。 +从图上可以看出,服务端已经生成了文件,把客户端的消息保存了下来,只保存了 10 条消息(代码中固定循环读取 10 次)。 ### 11.3 习题 diff --git a/ch11/echo_storeserv.c b/ch11/echo_storeserv.c index e648156..6400895 100644 --- a/ch11/echo_storeserv.c +++ b/ch11/echo_storeserv.c @@ -24,7 +24,7 @@ int main(int argc, char *argv[]) char buf[BUF_SIZE]; if (argc != 2) { - printf("Usgae : %s \n", argv[0]); + printf("Usage : %s \n", argv[0]); exit(1); } act.sa_handler = read_childproc; //防止僵尸进程 @@ -48,7 +48,7 @@ int main(int argc, char *argv[]) { FILE *fp = fopen("echomsg.txt", "wt"); char msgbuf[BUF_SIZE]; - int i, len; + int len; for (int i = 0; i < 10; i++) { len = read(fds[0], msgbuf, BUF_SIZE); @@ -69,7 +69,7 @@ int main(int argc, char *argv[]) if (pid == 0) //子进程运行区域,此部分向客户端提供回声服务 { close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程 - while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0) + while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) { write(clnt_sock, buf, str_len); write(fds[1], buf, str_len); diff --git a/ch11/homework/kehou4.c b/ch11/homework/kehou4.c index 170caa0..614e7a1 100644 --- a/ch11/homework/kehou4.c +++ b/ch11/homework/kehou4.c @@ -7,7 +7,7 @@ int main(int argc, char *argv[]) { int fds1[2], fds2[2]; //const char* 以"\0"作为结束符 - char str1[] = "Do you like cooffee?"; + char str1[] = "Do you like coffee?"; char str2[] = "I like coffee"; char str3[] = "I like long legs"; char * str_arr[] = {str1, str2, str3}; diff --git a/ch12/README.md b/ch12/README.md index 4a67aa8..a9fcc92 100644 --- a/ch12/README.md +++ b/ch12/README.md @@ -39,7 +39,7 @@ I/O 复用技术可以解决这个问题。 > 「好像不能同时说话?」 -实际上,因为是在进行对话,所以很少发生同时说话的情况。也就是说,上述系统采用的是**「时分复用」**技术。因为说话人声频率不同,即使在同时说话也能进行一定程度上的区分(杂音也随之增多)。因此,也可以说是「频分复用技术」。 +实际上,因为是在进行对话,所以很少发生同时说话的情况。也就是说,上述系统采用的是**「时分复用」**技术。因为说话人的声音频率不同,即使在同时说话也能进行一定程度上的区分(杂音也随之增多)。因此,也可以说是「频分复用技术」。 #### 12.1.3 复用技术在服务器端的应用 @@ -67,7 +67,7 @@ select 函数是最具代表性的实现复用服务器的方法。在 Windows > 术语:「事件」。当发生监视项对应情况时,称「发生了事件」。 -select 函数的使用方法与一般函数的区别并不大,更准确的说,他很难使用。但是为了实现 I/O 复用服务器端,我们应该掌握 select 函数,并运用于套接字编程当中。认为「select 函数是 I/O 复用的全部内容」也并不为过。select 函数的调用过程如下图所示: +select 函数的使用方法与一般函数的区别并不大,更准确地说,它很难使用。但是为了实现 I/O 复用服务器端,我们应该掌握 select 函数,并运用于套接字编程当中。认为「select 函数是 I/O 复用的全部内容」也并不为过。select 函数的调用过程如下图所示: ![](images/kAtdRs.png) @@ -116,7 +116,7 @@ timeout: 调用 select 函数后,为防止陷入无限阻塞的状态,传递 1. 文件描述符的监视(检查)范围是? 2. 如何设定 select 函数的超时时间? -第一,文件描述符的监视范围和 select 的第一个参数有关。实际上,select 函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在 fd_set 变量中的文件描述符数。但每次新建文件描述符时,其值就会增加 1 ,故只需将最大的文件描述符值加 1 再传递给 select 函数即可。加 1 是因为文件描述符的值是从 0 开始的。 +第一,文件描述符的监视范围和 select 的第一个参数有关。实际上,select 函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在 fd_set 变量中的文件描述符数。但文件描述符的值通常随创建而增大(已关闭的描述符可能被系统复用),故只需将最大的文件描述符值加 1 再传递给 select 函数即可。加 1 是因为文件描述符的值是从 0 开始的。 第二,select 函数的超时时间与 select 函数的最后一个参数有关,其中 timeval 结构体定义如下: @@ -142,7 +142,7 @@ select 返回正整数时,怎样获知哪些文件描述符发生了变化? 下面是一个 select 函数的例子: -- [select.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/select.c) +- [select.c](select.c) 编译运行: @@ -161,7 +161,7 @@ gcc select.c -o select 下面通过 select 函数实现 I/O 复用服务器端。下面是基于 I/O 复用的回声服务器端。 -- [echo_selectserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/echo_selectserv.c) +- [echo_selectserv.c](echo_selectserv.c) 编译运行: @@ -196,6 +196,11 @@ gcc echo_selectserv.c -o selserv 3. **复用服务器端需要 select 函数。下列关于 select 函数使用方法的描述错误的是?** + ① 调用 select 函数前需要集中待监视的文件描述符。 + ② 调用 select 函数后无需再次注册监视对象。 + ③ 复用服务器端无法同时向多个客户端提供服务。 + ④ 基于 select 的复用服务器只需要 1 个进程,可减少创建多进程带来的负担。 + 答:错误的描述是 2 和 3。说明如下: - 描述 1 正确:调用 select 函数前需要集中 I/O 监视对象的文件描述符。 diff --git a/ch12/echo_selectserv.c b/ch12/echo_selectserv.c index ca04783..97dff30 100644 --- a/ch12/echo_selectserv.c +++ b/ch12/echo_selectserv.c @@ -59,6 +59,8 @@ int main(int argc, char *argv[]) { adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); + if (clnt_sock == -1) + continue; FD_SET(clnt_sock, &reads); //注册一个clnt_sock if (fd_max < clnt_sock) @@ -68,7 +70,13 @@ int main(int argc, char *argv[]) else //不是服务端套接字时 { str_len = read(i, buf, BUF_SIZE); //i指的是当前发起请求的客户端 - if (str_len == 0) + if (str_len == -1) + { + FD_CLR(i, &reads); + close(i); + printf("closed client(read error): %d \n", i); + } + else if (str_len == 0) { FD_CLR(i, &reads); close(i); diff --git a/ch12/select.c b/ch12/select.c index 527c4a4..459008e 100644 --- a/ch12/select.c +++ b/ch12/select.c @@ -39,6 +39,11 @@ int main(int argc, char *argv[]) if (FD_ISSET(0, &temps)) //验证发生变化的值是否是标准输入端 { str_len = read(0, buf, BUF_SIZE); + if (str_len == -1) + { + puts("read error!"); + continue; + } buf[str_len] = 0; printf("message from console: %s", buf); } diff --git a/ch13/README.md b/ch13/README.md index 71c46cd..db6950c 100644 --- a/ch13/README.md +++ b/ch13/README.md @@ -13,7 +13,7 @@ ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags); /* 成功时返回发送的字节数,失败时返回 -1 -sockfd: 表示与数据传输对象的连接的套接字和文件描述符 +sockfd: 表示与数据传输对象连接的套接字文件描述符 buf: 保存待传输数据的缓冲地址值 nbytes: 待传输字节数 flags: 传输数据时指定的可选项信息 @@ -50,8 +50,8 @@ send & recv 函数的可选项意义: MSG_OOB 可选项用于创建特殊发送方法和通道以发送紧急消息。下面为 MSG_OOB 的示例代码: -- [oob_recv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/oob_recv.c) -- [oob_send.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/oob_send.c) +- [oob_recv.c](oob_recv.c) +- [oob_send.c](oob_send.c) 编译运行: @@ -76,7 +76,7 @@ fcntl(recv_sock, F_SETOWN, getpid()); 的意思是: -> 文件描述符 recv_sock 指向的套接字引发的 SIGURG 信号处理进程变为 getpid 函数返回值用作 ID 进程. +> 将文件描述符 recv_sock 指向的套接字引发 SIGURG 信号时的处理进程,设置为以 getpid 函数返回值作为 ID 的进程。 上述描述中的「处理 SIGURG 信号」指的是「调用 SIGURG 信号处理函数」。但是之前讲过,多个进程可以拥有 1 个套接字的文件描述符。例如,通过调用 fork 函数创建子进程并同时复制文件描述符。此时如果发生 SIGURG 信号,应该调用哪个进程的信号处理函数呢?可以肯定的是,不会调用所有进程的信号处理函数。因此,处理 SIGURG 信号时必须指定处理信号所用的进程,而 getpid 返回的是调用此函数的进程 ID 。上述调用语句指当前为处理 SIGURG 信号的主体。 @@ -84,7 +84,7 @@ fcntl(recv_sock, F_SETOWN, getpid()); > 通过 MSG_OOB 可选项传递数据时只返回 1 个字节,而且也不快 -的确,通过 MSG_OOB 并不会加快传输速度,而通过信号处理函数 urg_handler 也只能读取一个字节。剩余数据只能通过未设置 MSG_OOB 可选项的普通输入函数读取。因为 TCP 不存在真正意义上的「外带数据」。实际上,MSG_OOB 中的 OOB 指的是 Out-of-band ,而「外带数据」的含义是: +的确,通过 MSG_OOB 并不会加快传输速度,而通过信号处理函数 urg_handler 也只能读取一个字节。剩余数据只能通过未设置 MSG_OOB 可选项的普通输入函数读取。因为 TCP 不存在真正意义上的「带外数据」。实际上,MSG_OOB 中的 OOB 指的是 Out-of-band ,而「带外数据」的含义是: > 通过完全不同的通信路径传输的数据 @@ -102,7 +102,7 @@ MSG_OOB 的真正意义在于督促数据接收对象尽快处理数据。这是 send(sock, "890", strlen("890"), MSG_OOB); ``` -图上是调用这个函数的缓冲状态。如果缓冲最左端的位置视作偏移量 0 。字符 0 保存于偏移量 2 的位置。另外,字符 0 右侧偏移量为 3 的位置存有紧急指针(Urgent Pointer)。紧急指针指向紧急消息的下一个位置(偏移量加一),同时向对方主机传递以下信息: +图上是调用这个函数的缓冲状态。如果将缓冲最左端的位置视作偏移量 0,字符 '0' 保存于偏移量 2 的位置。另外,字符 '0' 右侧偏移量为 3 的位置即为紧急指针(Urgent Pointer)所指向的位置。紧急指针指向紧急消息的下一个位置(偏移量加一),同时向对方主机传递以下信息: > 紧急指针指向的偏移量为 3 之前的部分就是紧急消息。 @@ -117,14 +117,14 @@ TCP 数据包实际包含更多信息。TCP 头部包含如下两种信息: 指定 MSG_OOB 选项的数据包本身就是紧急数据包,并通过紧急指针表示紧急消息所在的位置。 -紧急消息的意义在于督促消息处理,而非紧急传输形式受限的信息。 +紧急消息的意义在于督促接收方尽快处理数据,而非指紧急传输的信息本身在形式上受限。 #### 13.1.4 检查输入缓冲 同时设置 MSG_PEEK 选项和 MSG_DONTWAIT 选项,以验证输入缓冲是否存在接收的数据。设置 MSG_PEEK 选项并调用 recv 函数时,即使读取了输入缓冲的数据也不会删除。因此,该选项通常与 MSG_DONTWAIT 配合,用于以非阻塞方式验证待读数据存在与否。下面的示例是二者的含义: -- [peek_recv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/peek_recv.c) -- [peek_send.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/peek_send.c) +- [peek_recv.c](peek_recv.c) +- [peek_send.c](peek_send.c) 编译运行: @@ -149,14 +149,14 @@ readv & writev 函数的功能可概括如下: > 对数据进行整合传输及发送的函数 -也就是说,通过 writev 函数可以将分散保存在多个缓冲中的数据一并发送,通过 readv 函数可以由多个缓冲分别接收。因此,适用这 2 个函数可以减少 I/O 函数的调用次数。下面先介绍 writev 函数。 +也就是说,通过 writev 函数可以将分散保存在多个缓冲中的数据一并发送,通过 readv 函数可以由多个缓冲分别接收。因此,使用这 2 个函数可以减少 I/O 函数的调用次数。下面先介绍 writev 函数。 ```c #include ssize_t writev(int filedes, const struct iovec *iov, int iovcnt); /* 成功时返回发送的字节数,失败时返回 -1 -filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 read 一样向其传递文件或标准输出描述符. +filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 read 一样向其传递文件或标准输出描述符。 iov: iovec 结构体数组的地址值,结构体 iovec 中包含待发送数据的位置和大小信息 iovcnt: 向第二个参数传递数组长度 */ @@ -176,11 +176,11 @@ struct iovec ![](images/5c4c61b07d207.png) -writev 的第一个参数,是文件描述符,因此向控制台输出数据,ptr 是存有待发送数据信息的 iovec 数组指针。第三个参数为 2,因此,从 ptr 指向的地址开始,共浏览 2 个 iovec 结构体变量,发送这些指针指向的缓冲数据。 +writev 的第一个参数是文件描述符,本例中传入 1(标准输出),因此数据会输出到控制台。ptr 是存有待发送数据信息的 iovec 数组指针;第三个参数为 2,表示从 ptr 指向的地址开始,共读取 2 个 iovec 结构体变量,并将这些指针指向的缓冲数据一并发送。 下面是 writev 函数的使用方法: -- [writev.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/writev.c) +- [writev.c](writev.c) ```c #include @@ -218,14 +218,14 @@ ABC1234 Write bytes: 7 ``` -下面介绍 readv 函数,功能和 writev 函数正好相反.函数为: +下面介绍 readv 函数,功能和 writev 函数正好相反。函数为: ```c #include ssize_t readv(int filedes, const struct iovec *iov, int iovcnt); /* 成功时返回接收的字节数,失败时返回 -1 -filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 write 一样向其传递文件或标准输出描述符. +filedes: 表示数据传输对象的套接字文件描述符。但该函数并不仅限于套接字,因此,可以像 write 一样向其传递文件或标准输出描述符。 iov: iovec 结构体数组的地址值,结构体 iovec 中包含待数据保存的位置和大小信息 iovcnt: 第二个参数中数组的长度 */ @@ -233,7 +233,7 @@ iovcnt: 第二个参数中数组的长度 下面是示例代码: -- [readv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch13/readv.c) +- [readv.c](readv.c) ```c #include @@ -280,7 +280,7 @@ gcc readv.c -o rv #### 13.2.2 合理使用 readv & writev 函数 -实际上,能使用该函数的所有情况都适用。例如,需要传输的数据分别位于不同缓冲(数组)时,需要多次调用 write 函数。此时可通过 1 次 writev 函数调用替代操作,当然会提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用 read 函数,而是利用 1 次 readv 函数就能大大提高效率。 +实际上,凡是适合使用 readv/writev 的场景,都建议使用它们。例如,需要传输的数据分别位于不同缓冲(数组)时,需要多次调用 write 函数。此时可通过 1 次 writev 函数调用替代操作,当然会提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用 read 函数,而是利用 1 次 readv 函数就能大大提高效率。 其意义在于减少数据包个数。假设为了提高效率在服务器端明确禁用了 Nagle 算法。其实 writev 函数在不采用 Nagle 算法时更有价值,如图: @@ -348,11 +348,11 @@ ioctlsocket(sock, FIONBIO, &mode); 1. **下列关于 MSG_OOB 可选项的说法错误的是**? - 答:以下加粗的字体代表说法正确。 + 答:以下加粗的字体代表说法正确。说法错误的是第 2、3 项。 - 1. MSG_OOB 指传输 Out-of-band 数据,是通过其他路径高速传输数据 + 1. **MSG_OOB 指传输 Out-of-band 数据,是通过其他路径高速传输数据** 2. MSG_OOB 指通过其他路径高速传输数据,因此 TCP 中设置该选项的数据先到达对方主机 - 3. **设置 MSG_OOB 是数据先到达对方主机后,以普通数据的形式和顺序读取。也就是说,只是提高了传输速度,接收方无法识别这一点**。 + 3. 设置 MSG_OOB 是数据先到达对方主机后,以普通数据的形式和顺序读取。也就是说,只是提高了传输速度,接收方无法识别这一点。 4. **MSG_OOB 无法脱离 TCP 的默认数据传输方式,即使脱离了 MSG_OOB ,也会保持原有的传输顺序。该选项只用于要求接收方紧急处理**。 2. **利用 readv & writev 函数收发数据有何优点?分别从函数调用次数和 I/O 缓冲的角度给出说明**。 diff --git a/ch13/oob_recv.c b/ch13/oob_recv.c index d29f102..2094f41 100644 --- a/ch13/oob_recv.c +++ b/ch13/oob_recv.c @@ -62,6 +62,8 @@ void urg_handler(int signo) int str_len; char buf[BUF_SIZE]; str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB); + if (str_len <= 0) + return; buf[str_len] = 0; printf("Urgent message: %s \n", buf); } diff --git a/ch13/peek_recv.c b/ch13/peek_recv.c index b13893d..9bad659 100644 --- a/ch13/peek_recv.c +++ b/ch13/peek_recv.c @@ -39,6 +39,8 @@ int main(int argc, char *argv[]) str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT); if (str_len > 0) break; + if (str_len == 0) // 对端关闭连接,避免死循环 + error_handling("connect closed"); } buf[str_len] = 0; diff --git a/ch14/README.md b/ch14/README.md index 0f8d164..87e9aae 100644 --- a/ch14/README.md +++ b/ch14/README.md @@ -4,21 +4,21 @@ ### 14.1 多播 -多播(Multicast)方式的数据传输是基于 UDP 完成的。因此 ,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。 +多播(Multicast)方式的数据传输是基于 UDP 完成的。因此,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。 #### 14.1.1 多播的数据传输方式以及流量方面的优点 多播的数据传输特点可整理如下: - 多播服务器端针对特定多播组,只发送 1 次数据。 -- 即使只发送 1 次数据,但该组内的所有客户端都会接收数据 +- 即使只发送 1 次数据,该组内的所有客户端也都会接收数据。 - 多播组数可以在 IP 地址范围内任意增加 多播组是 D 类IP地址(224.0.0.0~239.255.255.255),「加入多播组」可以理解为通过程序完成如下声明: > 在 D 类IP地址中,我希望接收发往目标 239.234.218.234 的多播数据 -多播是基于 UDP 完成的,也就是说,多播数据包的格式与 UDP 数据包相同。只是与一般的 UDP 数据包不同。向网络传递 1 个多播数据包时,路由器将复制该数据包并传递到多个主机。像这样,多播需要借助路由器完成。如图所示: +多播是基于 UDP 完成的,也就是说,多播数据包的格式与 UDP 数据包相同。只是与一般的 UDP 数据包不同,向网络传递 1 个多播数据包时,路由器将复制该数据包并传递到多个主机。像这样,多播需要借助路由器完成。如图所示: ![](images/5c4d310daa6be.png) @@ -28,7 +28,7 @@ #### 14.1.2 路由(Routing)和 TTL(Time to Live,生存时间),以及加入组的办法 -为了传递多播数据包,必须设置 TTL 。TTL 是 Time to Live的简写,是决定「数据包传递距离」的主要因素。TTL 用整数表示,并且每经过一个路由器就减一。TTL 变为 0 时,该数据包就无法再被传递,只能销毁。因此,TTL 的值设置过大将影响网络流量。当然,设置过小,也无法传递到目标。 +为了传递多播数据包,必须设置 TTL。TTL 是 Time to Live 的简写,是决定「数据包传递距离」的主要因素。TTL 用整数表示,并且每经过一个路由器就减一。TTL 变为 0 时,该数据包就无法再被传递,只能销毁。因此,TTL 的值设置过大将影响网络流量。当然,设置过小,也无法传递到目标。 ![](images/5c4d3960001eb.png) @@ -76,8 +76,8 @@ struct ip_mreq 下面是两个代码: -- [news_sender.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_sender.c) -- [news_receiver.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_receiver.c) +- [news_sender.c](news_sender.c) +- [news_receiver.c](news_receiver.c) 编译运行: @@ -92,11 +92,11 @@ gcc news_receiver.c -o receiver ![](images/5c4e85a9aabcc.png) -通过结果可以看出,使用 sender 多播信息,通过 receiver 接收广播,如果延迟运行 receiver 将无法接受之前发送的信息。 +通过结果可以看出,使用 sender 多播信息,通过 receiver 接收多播数据,如果延迟运行 receiver 将无法接收之前发送的信息。 ### 14.2 广播 -广播(Broadcast)在「一次性向多个主机发送数据」这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接受数据。相反,广播只能向同一网络中的主机传输数据。 +广播(Broadcast)在「一次性向多个主机发送数据」这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。 #### 14.2.1 广播的理解和实现方法 @@ -109,7 +109,7 @@ gcc news_receiver.c -o receiver 反之,本地广播中使用的IP地址限定为 255.255.255.255 。例如,192.32.24 网络中的主机向 255.255.255.255 传输数据时,数据将传输到 192.32.24 网络中所有主机。 -**数据通信中使用的IP地址是与 UDP 示例的唯一区别。默认生成的套接字会阻止广播,因此,只需通过如下代码更改默认设置。** +**广播与普通 UDP 示例的区别在于:目标 IP 地址使用广播地址,且需通过 SO_BROADCAST 选项启用广播。默认生成的套接字会阻止广播,因此需要通过如下代码更改默认设置。** ```c int send_sock; @@ -121,12 +121,12 @@ setsockopt(send_sock,SOL_SOCKET,SO_BROADCAST,(void*)&bcast,sizeof(bcast)); ... ``` -### 14.2.2 实现广播数据的 Sender 和 Receiver +#### 14.2.2 实现广播数据的 Sender 和 Receiver 下面是广播数据的 Sender 和 Receiver的代码: -- [news_sender_brd.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_sender_brd.c) -- [news_receiver_brd.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch14/news_receiver_brd.c) +- [news_sender_brd.c](news_sender_brd.c) +- [news_receiver_brd.c](news_receiver_brd.c) 编译运行: @@ -201,7 +201,7 @@ setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (char*)&bcast, sizeof(bcast)); 1. 头文件:Windows 使用 `winsock2.h`,Linux 使用 `sys/socket.h` 等头文件 2. 初始化:Windows 需要调用 `WSAStartup` 初始化 Winsock,使用完后调用 `WSACleanup` -3. 套接字类型:Windows 使用 `SOCKET` 类型(实际是 `unsigned __int64`),Linux 使用 `int` +3. 套接字类型:Windows 使用 `SOCKET` 类型(`UINT_PTR`,32 位系统为 `unsigned int`,64 位系统为 `unsigned __int64`),Linux 使用 `int` 4. 关闭套接字:Windows 使用 `closesocket`,Linux 使用 `close` 5. 指针类型转换:Windows 下 `setsockopt` 的第四个参数通常转换为 `char*`,Linux 下转换为 `void*` @@ -215,20 +215,25 @@ setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (char*)&bcast, sizeof(bcast)); 2. **多播与广播的异同点是什么?请从数据通信的角度进行说明**。 - 答:在「一次性向多个主机发送数据」这一点上与多播类似,但传输的数据范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接受数据。相反,广播只能向同一网络中的主机传输数据。 + 答:在「一次性向多个主机发送数据」这一点上与多播类似,但传输的数据范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。 -3. **下面关于多播的说法描述错误的是**? +3. **下面关于多播的说法描述错误的是?** + + 1. 多播是用来向加入多播组的所有主机传输数据的协议 + 2. 主机连接到同一网络才能加入到多播组,也就是说,多播组无法跨越多个网络 + 3. 能够加入多播组的主机数并无限制,但只能有 1 个主机(Sender)向该组发送数据 + 4. 多播时使用的套接字是 UDP 套接字,因为多播是基于 UDP 进行数据通信的 答:第 2 项描述错误。正确说明如下: 1. 多播是用来向加入多播组的所有主机传输数据的协议 2. ~~主机连接到同一网络才能加入到多播组,也就是说,多播组无法跨越多个网络~~(错误。多播可以跨越多个网络,只要路由器支持多播功能,主机就可以加入跨网络的多播组。即使路由器不支持,也可以通过隧道技术实现。) - 3. 能够加入多播组的主机数并无限制,但只能有 1 个主机(Sender)向该组发送数据 + 3. 能够加入多播组的主机数并无限制,向该组发送数据的主机数也无限制(任何主机均可向多播组地址发送数据,且无需先加入该组) 4. 多播时使用的套接字是 UDP 套接字,因为多播是基于 UDP 进行数据通信的 4. **多播也对网络流量有利,请比较 TCP 交换方式解释其原因** - 答:TCP 是必须建立一对一的连接,如果要向1000个主机发送文件,就得传递1000次。但是此时用多播方式传输数据,就只需要发送一次。 + 答:TCP 必须建立一对一的连接,如果要向 1000 个主机发送文件,就得传递 1000 次。但是此时用多播方式传输数据,就只需要发送一次。 5. **多播方式的数据通信需要 MBone 虚拟网络。换言之,MBone 是用于多播的网络,但它是虚拟网络。请解释此处的「虚拟网络」** diff --git a/ch14/news_receiver.c b/ch14/news_receiver.c index 1224d56..8fec267 100644 --- a/ch14/news_receiver.c +++ b/ch14/news_receiver.c @@ -31,12 +31,12 @@ int main(int argc, char *argv[]) //初始化结构体 join_adr.imr_multiaddr.s_addr = inet_addr(argv[1]); //多播组地址 join_adr.imr_interface.s_addr = htonl(INADDR_ANY); //待加入的IP地址 - //利用套接字选项 IP_ADD_MEMBERSHIP 加入多播组,完成了接受指定的多播组数据的所有准备 + //利用套接字选项 IP_ADD_MEMBERSHIP 加入多播组,完成了接收指定的多播组数据的所有准备 setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&join_adr, sizeof(join_adr)); while (1) { - //通过 recvfrom 函数接受多播数据。如果不需要知道传输数据的主机地址信息,可以向recvfrom函数的第5 6参数分贝传入 NULL 0 + //通过 recvfrom 函数接收多播数据。如果不需要知道传输数据的主机地址信息,可以向 recvfrom 函数的第 5、6 个参数分别传入 NULL、0 str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0); if (str_len < 0) break; diff --git a/ch14/news_receiver_brd.c b/ch14/news_receiver_brd.c index 131ca9e..619a2c8 100644 --- a/ch14/news_receiver_brd.c +++ b/ch14/news_receiver_brd.c @@ -29,7 +29,7 @@ int main(int argc, char *argv[]) error_handling("bind() error"); while (1) { - //通过 recvfrom 函数接受数据。如果不需要知道传输数据的主机地址信息,可以向recvfrom函数的第5 6参数分贝传入 NULL 0 + //通过 recvfrom 函数接收数据。如果不需要知道传输数据的主机地址信息,可以向 recvfrom 函数的第 5、6 个参数分别传入 NULL、0 str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0); if (str_len < 0) break; diff --git a/ch14/news_sender.c b/ch14/news_sender.c index 290631c..629bd84 100644 --- a/ch14/news_sender.c +++ b/ch14/news_sender.c @@ -31,9 +31,8 @@ int main(int argc, char *argv[]) if ((fp = fopen("news.txt", "r")) == NULL) error_handling("fopen() error"); - while (!feof(fp)) //如果文件没结束就返回0 + while (fgets(buf, BUF_SIZE, fp) != NULL) //逐行读取,读到文件末尾返回 NULL { - fgets(buf, BUF_SIZE, fp); sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr *)&mul_adr, sizeof(mul_adr)); sleep(2); } diff --git a/ch14/news_sender_brd.c b/ch14/news_sender_brd.c index 7c2b0c7..937c091 100644 --- a/ch14/news_sender_brd.c +++ b/ch14/news_sender_brd.c @@ -17,7 +17,7 @@ int main(int argc, char *argv[]) int so_brd = 1; if (argc != 3) { - printf("Usage : %s \n", argv[0]); + printf("Usage : %s \n", argv[0]); exit(1); } send_sock = socket(PF_INET, SOCK_DGRAM, 0); //创建 UDP 套接字 @@ -29,9 +29,8 @@ int main(int argc, char *argv[]) if ((fp = fopen("news.txt", "r")) == NULL) error_handling("fopen() error"); - while (!feof(fp)) //如果文件没结束就返回0 + while (fgets(buf, BUF_SIZE, fp) != NULL) //逐行读取,读到文件末尾返回 NULL { - fgets(buf, BUF_SIZE, fp); sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr *)&broad_adr, sizeof(broad_adr)); sleep(2); } diff --git a/ch15/README.md b/ch15/README.md index 51c5038..6bb6bed 100644 --- a/ch15/README.md +++ b/ch15/README.md @@ -15,7 +15,7 @@ ![](images/5c500e53ad9aa.png) -假设使用 fputs 函数进行传输字符串 「Hello」时,首先将数据传递到标准 I/O 缓冲,然后将数据移动到套接字输出缓冲,最后将字符串发送到对方主机。 +假设使用 fputs 函数传输字符串「Hello」时,首先将数据传递到标准 I/O 缓冲,然后将数据移动到套接字输出缓冲,最后将字符串发送到对方主机。 设置缓冲的主要目的是为了提高性能。从以下两点可以说明性能的提高: @@ -31,13 +31,13 @@ 下面是利用系统函数的示例: -- [syscpy.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/syscpy.c) +- [syscpy.c](syscpy.c) 下面是使用标准 I/O 函数复制文件 -- [stdcpy.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/stdcpy.c) +- [stdcpy.c](stdcpy.c) -对于以上两个代码进行测试,明显基于标准 I/O 函数的代码跑的更快。这是因为标准 I/O 函数通过缓冲区减少了系统调用的次数,每次系统调用都有一定的开销(用户态与内核态的切换),而缓冲机制可以将多次小数据量的 I/O 操作合并为较少次数的系统调用,从而提高性能。 +对于以上两个代码进行测试,明显基于标准 I/O 函数的代码跑得更快。这是因为标准 I/O 函数通过缓冲区减少了系统调用的次数,每次系统调用都有一定的开销(用户态与内核态的切换),而缓冲机制可以将多次小数据量的 I/O 操作合并为较少次数的系统调用,从而提高性能。 #### 15.1.3 标准 I/O 函数的几个缺点 @@ -45,7 +45,7 @@ - 不容易进行双向通信 - 有时可能频繁调用 fflush 函数 -- 需要以 FILE 结构体指针的形式返回文件描述符。 +- 需要将文件描述符转换为 FILE 结构体指针才能使用。 ### 15.2 使用标准 I/O 函数 @@ -65,7 +65,7 @@ mode : 将要创建的 FILE 结构体指针的模式信息 以下为示例: -- [desto.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/desto.c) +- [desto.c](desto.c) ```c #include @@ -115,7 +115,7 @@ int fileno(FILE *stream); 示例: -- [todes.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/todes.c) +- [todes.c](todes.c) ```c #include @@ -146,8 +146,8 @@ int main() 代码如下: -- [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/echo_client.c) -- [echo_stdserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch15/echo_stdserv.c) +- [echo_client.c](echo_client.c) +- [echo_stdserv.c](echo_stdserv.c) 编译运行: @@ -166,7 +166,7 @@ gcc echo_stdserv.c -o eserver > 以下答案仅代表本人个人观点,可能不是正确答案。 -1. **请说明标准 I/O 的 2 个优点。他为何拥有这 2 个优点?** +1. **请说明标准 I/O 的 2 个优点。它为何拥有这 2 个优点?** 答:①具有很高的移植性②有良好的缓冲提高性能。 diff --git a/ch15/desto.c b/ch15/desto.c index 7d65163..995211c 100644 --- a/ch15/desto.c +++ b/ch15/desto.c @@ -4,7 +4,7 @@ int main() { FILE *fp; - int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC); //创建文件并返回文件描述符 + int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644); //创建文件并返回文件描述符 if (fd == -1) { fputs("file open error", stdout); diff --git a/ch15/echo_client.c b/ch15/echo_client.c index ae3b122..5c92c72 100644 --- a/ch15/echo_client.c +++ b/ch15/echo_client.c @@ -51,7 +51,8 @@ int main(int argc, char *argv[]) printf("Message from server: %s", message); } fclose(writefp); - fclose(readfp); + /* readfp 与 writefp 共用同一文件描述符,fclose(writefp) 已关闭底层套接字, + 再次 fclose(readfp) 属冗余关闭(返回 EBADF);半关闭问题见第 16 章 dup 方案 */ return 0; } diff --git a/ch15/echo_stdserv.c b/ch15/echo_stdserv.c index cdad9d1..0f49bd4 100644 --- a/ch15/echo_stdserv.c +++ b/ch15/echo_stdserv.c @@ -52,15 +52,13 @@ int main(int argc, char *argv[]) readfp = fdopen(clnt_sock, "r"); writefp = fdopen(clnt_sock, "w"); - while (!feof(readfp)) + while (fgets(message, BUF_SIZE, readfp) != NULL) { - fgets(message, BUF_SIZE, readfp); fputs(message, writefp); fflush(writefp); } - fclose(readfp); - fclose(writefp); + fclose(writefp); /* readfp/writefp 共享同一 fd,关闭一次即可;半关闭见第 16 章 dup 方案 */ } close(serv_sock); return 0; diff --git a/ch15/stdcpy.c b/ch15/stdcpy.c index ea523ba..a7fdb2e 100644 --- a/ch15/stdcpy.c +++ b/ch15/stdcpy.c @@ -1,16 +1,16 @@ #include -#define BUF_SZIE 3 +#define BUF_SIZE 3 int main(int argc, char *argv[]) { FILE *fp1; FILE *fp2; - char buf[BUF_SZIE]; + char buf[BUF_SIZE]; fp1 = fopen("news.txt", "r"); fp2 = fopen("cpy.txt", "w"); - while (fgets(buf, BUF_SZIE, fp1) != NULL) + while (fgets(buf, BUF_SIZE, fp1) != NULL) fputs(buf, fp2); fclose(fp1); fclose(fp2); diff --git a/ch15/todes.c b/ch15/todes.c index ef9a131..eb53a0f 100644 --- a/ch15/todes.c +++ b/ch15/todes.c @@ -4,7 +4,7 @@ int main() { FILE *fp; - int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC); + int fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { fputs("file open error",stdout); diff --git a/ch16/README.md b/ch16/README.md index 1de169b..b709f7e 100644 --- a/ch16/README.md +++ b/ch16/README.md @@ -4,7 +4,7 @@ ### 16.1 分离 I/O 流 -「分离 I/O 流」是一种常用表达。有 I/O 工具可区分二者,无论采用哪种方法,都可以认为是分离了 I/O 流。 +「分离 I/O 流」是一种常用表达。只要用 I/O 工具区分输入和输出,无论采用哪种方法,都可以认为是分离了 I/O 流。 #### 16.1.1 2 次 I/O 流分离 @@ -34,12 +34,12 @@ shutdown(sock,SHUT_WR); ``` -当时说过调用 shutdown 函数的基于半关闭的 EOF 传递方法。第十章的 [echo_mpclient.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpclient.c) 添加了半关闭的相关代码。但是还没有讲采用 fdopen 函数怎么半关闭。那么是否是通过 fclose 函数关闭流呢?我们先试试 +当时说过调用 shutdown 函数的基于半关闭的 EOF 传递方法。第十章的 [echo_mpclient.c](../ch10/echo_mpclient.c) 添加了半关闭的相关代码。但是还没有讲采用 fdopen 函数怎么半关闭。那么是否是通过 fclose 函数关闭流呢?我们先试试 -下面是服务端和客户端码: +下面是服务端和客户端代码: -- [sep_clnt.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_clnt.c) -- [sep_serv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_serv.c) +- [sep_clnt.c](sep_clnt.c) +- [sep_serv.c](sep_serv.c) 编译运行: @@ -60,17 +60,17 @@ gcc sep_serv.c -o serv ### 16.2 文件描述符的复制和半关闭 -#### 16.2.1 终止「流」时无法半关闭原因 +#### 16.2.1 终止「流」时无法半关闭的原因 -下面的图描述的是服务端代码中的两个FILE 指针、文件描述符和套接字中的关系。 +下面的图描述的是服务端代码中的两个 FILE 指针、文件描述符和套接字之间的关系。 ![](images/5c5121da89955.png) -从图中可以看到,两个指针都是基于同一文件描述符创建的。因此,针对于任何一个 FILE 指针调用 fclose 函数都会关闭文件描述符,如图所示: +从图中可以看到,两个指针都是基于同一文件描述符创建的。因此,针对任何一个 FILE 指针调用 fclose 函数都会关闭文件描述符,如图所示: ![](images/5c51224051802.png) -从图中看到,销毁套接字时再也无法进行数据交换。那如何进入可以进入但是无法输出的半关闭状态呢?如下图所示: +从图中看到,销毁套接字时再也无法进行数据交换。那如何进入可以接收输入但无法输出的半关闭状态呢?如下图所示: ![](images/5c5122a45c5f1.png) @@ -86,7 +86,7 @@ gcc sep_serv.c -o serv #### 16.2.2 复制文件描述符 -与调用 fork 函数不同,调用 fork 函数将复制整个进程,此处讨论的是同一进程内完成对文件描述符的复制。如图: +与 fork 函数不同,fork 会复制整个进程,而此处讨论的是在同一进程内复制文件描述符。如图: ![](images/5c512579c45b6.png) @@ -107,9 +107,9 @@ fd2 : 明确指定的文件描述符的整数值 */ ``` -dup2 函数明确指定复制的文件描述符的整数值。向其传递大于 0 且小于进程能生成的最大文件描述符值时,该值将成为复制出的文件描述符值。下面是代码示例: +dup2 函数明确指定复制的文件描述符的整数值。向其传递大于等于 0 且小于进程允许的最大文件描述符值时,该值将成为复制出的文件描述符值。下面是代码示例: -- [dup.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/dup.c) +- [dup.c](dup.c) ```c #include @@ -140,7 +140,7 @@ int main(int argc, char *argv[]) 编译运行: -``` +```shell gcc dup.c -o dup ./dup ``` @@ -151,13 +151,13 @@ gcc dup.c -o dup #### 16.2.4 复制文件描述符后「流」的分离 -下面更改 [sep_clnt.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_clnt.c) 和 [sep_serv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_serv.c) 可以使得让它正常工作,正常工作是指通过服务器的半关闭状态接收客户端最后发送的字符串。 +下面更改 [sep_clnt.c](sep_clnt.c) 和 [sep_serv.c](sep_serv.c) 使其正常工作,这里的正常工作是指让服务器通过半关闭状态接收客户端最后发送的字符串。 下面是代码: -- [sep_serv2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_serv2.c) +- [sep_serv2.c](sep_serv2.c) -这个代码可以与 [sep_clnt.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch16/sep_clnt.c) 配合起来使用,编译过程和上面一样,运行结果为: +这个代码可以与 [sep_clnt.c](sep_clnt.c) 配合起来使用,编译过程和上面一样,运行结果为: ![](images/5c513d54a27e0.png) diff --git a/ch17/README.md b/ch17/README.md index a0966cf..2efff8b 100644 --- a/ch17/README.md +++ b/ch17/README.md @@ -4,7 +4,7 @@ ### 17.1 epoll 理解及应用 -select 复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时介入上百个客户端。这种 select 方式并不适合以 web 服务器端开发为主流的现代开发环境,所以需要学习 Linux 环境下的 epoll +select 复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端。这种 select 方式并不适合以 web 服务器端开发为主流的现代开发环境,所以需要学习 Linux 环境下的 epoll #### 17.1.1 基于 select 的 I/O 复用技术速度慢的原因 @@ -15,7 +15,7 @@ select 复用方法由来已久,因此,利用该技术后,无论如何优 上述两点可以从 [echo_selectserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/echo_selectserv.c) 得到确认,调用 select 函数后,并不是把发生变化的文件描述符单独集中在一起,而是通过作为监视对象的 fd_set 变量的变化,找出发生变化的文件描述符(54,56行),因此无法避免针对所有监视对象的循环语句。而且,作为监视对象的 fd_set 会发生变化,所以调用 select 函数前应该复制并保存原有信息,并在每次调用 select 函数时传递新的监视对象信息。 -select 性能上最大的弱点是:每次传递监视对象信息,准确的说,select 是监视套接字变化的函数。而套接字是操作系统管理的,所以 select 函数要借助操作系统才能完成功能。select 函数的这一缺点可以通过如下方式弥补: +select 性能上最大的弱点是:每次传递监视对象信息,准确地说,select 是监视套接字变化的函数。而套接字是操作系统管理的,所以 select 函数要借助操作系统才能完成功能。select 函数的这一缺点可以通过如下方式弥补: > 仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项 @@ -43,7 +43,7 @@ select 的兼容性比较高,这样就可以支持很多的操作系统,不 select 函数中为了保存监视对象的文件描述符,直接声明了 fd_set 变量,但 epoll 方式下的操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时用的函数就是 epoll_create 。 -此外,为了添加和删除监视对象文件描述符,select 方式中需要 FD_SET、FD_CLR 函数。但在 epoll 方式中,通过 epoll_ctl 函数请求操作系统完成。最后,select 方式下调用 select 函数等待文件描述符的变化,而 epoll_wait 调用 epoll_wait 函数。还有,select 方式中通过 fd_set 变量查看监视对象的状态变化,而 epoll 方式通过如下结构体 epoll_event 将发生变化的文件描述符单独集中在一起。 +此外,为了添加和删除监视对象文件描述符,select 方式中需要 FD_SET、FD_CLR 函数。但在 epoll 方式中,通过 epoll_ctl 函数请求操作系统完成。最后,select 方式下调用 select 函数等待文件描述符的变化,而 epoll 方式下调用 epoll_wait 函数。还有,select 方式中通过 fd_set 变量查看监视对象的状态变化,而 epoll 方式通过如下结构体 epoll_event 将发生变化的文件描述符单独集中在一起。 ```c struct epoll_event @@ -60,7 +60,7 @@ typedef union epoll_data { ``` -声明足够大的 epoll_event 结构体数组候,传递给 epoll_wait 函数时,发生变化的文件描述符信息将被填入数组。因此,无需像 select 函数那样针对所有文件描述符进行循环。 +声明足够大的 epoll_event 结构体数组后,传递给 epoll_wait 函数时,发生变化的文件描述符信息将被填入数组。因此,无需像 select 函数那样针对所有文件描述符进行循环。 #### 17.1.4 epoll_create @@ -123,7 +123,7 @@ epoll_ctl(A,EPOLL_CTL_DEL,B,NULL); > 从 epoll 例程 A 中删除文件描述符 B -从上述示例中可以看出,从监视对象中删除时,不需要监视类型,因此向第四个参数可以传递为 NULL +从上述示例中可以看出,从监视对象中删除时,不需要监视类型,因此第四个参数可以传递 NULL 下面是第二个参数的含义: @@ -131,7 +131,7 @@ epoll_ctl(A,EPOLL_CTL_DEL,B,NULL); - EPOLL_CTL_DEL:从 epoll 例程中删除文件描述符 - EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况 -epoll_event 结构体用于保存事件的文件描述符结合。但也可以在 epoll 例程中注册文件描述符时,用于注册关注的事件。该函数中 epoll_event 结构体的定义并不显眼,因此通过调用语句说明该结构体在 epoll_ctl 函数中的应用。 +epoll_event 结构体用于保存发生事件的文件描述符集合。但也可以在 epoll 例程中注册文件描述符时,用于注册关注的事件。该函数中 epoll_event 结构体的定义并不显眼,因此通过调用语句说明该结构体在 epoll_ctl 函数中的应用。 ```c struct epoll_event event; @@ -188,7 +188,7 @@ event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1); 下面是回声服务器端的代码(修改自第 12 章 [echo_selectserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch12/echo_selectserv.c)): -- [echo_epollserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch17/echo_epollserv.c) +- [echo_epollserv.c](echo_epollserv.c) 编译运行: @@ -203,12 +203,12 @@ gcc echo_epollserv.c -o serv 可以看出运行结果和以前 select 实现的和 fork 实现的结果一样,都可以支持多客户端同时运行。 -但是这里运用了 epoll 效率高于 select +但是这里运用了 epoll,效率高于 select。 总结一下 epoll 的流程: 1. epoll_create 创建一个保存 epoll 文件描述符的空间(size 参数仅作为建议传递) -2. 动态分配内存,给将要监视的 epoll_wait +2. 动态分配内存,用于保存 epoll_wait 返回的事件 3. 利用 epoll_ctl 控制 添加 删除,监听事件 4. 利用 epoll_wait 来获取改变的文件描述符,来执行程序 @@ -237,7 +237,7 @@ select 和 epoll 的区别: 下面代码修改自 [echo_epollserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch17/echo_epollserv.c) 。epoll 默认以条件触发的方式工作,因此可以通过该示例验证条件触发的特性。 -- [echo_EPLTserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch17/echo_EPLTserv.c) +- [echo_EPLTserv.c](echo_EPLTserv.c) 上面的代码把调用 read 函数时使用的缓冲大小缩小到了 4 个字节,插入了验证 epoll_wait 调用次数的验证函数。减少缓冲大小是为了阻止服务器端一次性读取接收的数据。换言之,调用 read 函数后,输入缓冲中仍有数据要读取,而且会因此注册新的事件并从 epoll_wait 函数返回时将循环输出「return epoll_wait」字符串。 @@ -262,7 +262,7 @@ gcc echo_EPLTserv.c -o serv 代码: -- [echo_EDGEserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch17/echo_EDGEserv.c) +- [echo_EDGEserv.c](echo_EDGEserv.c) 编译运行: @@ -284,7 +284,7 @@ gcc echo_EDGEserv.c -o serv - 通过 errno 变量验证错误原因 - 为了完成非阻塞(Non-blocking)I/O ,更改了套接字特性。 -Linux 套接字相关函数一般通过 -1 通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误的时候提额外的信息,Linux 声明了如下全局变量: +Linux 套接字相关函数一般通过 -1 通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误的时候提供额外的信息,Linux 声明了如下全局变量: ```c int errno; @@ -294,7 +294,7 @@ int errno; > read 函数发现输入缓冲中没有数据可读时返回 -1,同时在 errno 中保存 EAGAIN 常量 -下面是 Linux 中提供的改变和更改文件属性的办法: +下面是 Linux 中提供的更改文件属性的方法: ```c #include @@ -313,7 +313,7 @@ int flag = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flag | O_NONBLOCK); ``` -通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞 O_NONBLOCK 标志。调用 read/write 函数时,无论是否存在数据,都会形成非阻塞文件(套接字)。fcntl 函数的适用范围很广。 +通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞 O_NONBLOCK 标志。调用 read/write 函数时,无论是否存在数据,都会以非阻塞方式操作文件(套接字)。fcntl 函数的适用范围很广。 #### 17.2.4 实现边缘触发回声服务器端 @@ -327,13 +327,13 @@ fcntl(fd, F_SETFL, flag | O_NONBLOCK); 下面是以边缘触发方式工作的回声服务端代码: -- [echo_EPETserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch17/echo_EPETserv.c) +- [echo_EPETserv.c](echo_EPETserv.c) 编译运行: ```shell gcc echo_EPETserv.c -o serv -./serv +./serv 9190 ``` 结果: @@ -383,7 +383,7 @@ gcc echo_EPETserv.c -o serv 3. select 方式和 epoll 方式的最大差异在于监视对象文件描述符传递给操作系统的方式。请说明具体差异,并解释为何存在这种差异。 - 答:select 函数每次调用都要传递所有的监视对象信息,而 epoll 函数仅向操作系统传递 1 次监视对象,监视范围或内容发生变化时只通知发生变化的事项。select 采用这种方法是为了保持兼容性。 + 答:select 函数每次调用都要传递所有的监视对象信息,而 epoll 函数仅向操作系统传递 1 次监视对象,监视范围或内容发生变化时只通知发生变化的事项。存在这种差异是因为 epoll 在内核中维护了持久的监视对象列表(epoll 例程),只需注册一次;而 select 没有内核侧的持久状态,每次调用都需重新传递并还原监视对象信息。 4. 虽然 epoll 是 select 的改进方案,但 select 也有自己的优点。在何种情况下使用 select 更加合理。 @@ -395,4 +395,4 @@ gcc echo_EPETserv.c -o serv 6. 采用边缘触发时可以分离数据的接收和处理时间点。请说明其优点和原因。 - 答:分离接收数据和处理数据的时间点,给服务端的实现带来很大灵活性。 + 答:边缘触发方式下,输入缓冲收到数据时只通知一次事件,服务器可在通知后用非阻塞 read 一次性读出全部数据(直到返回 EAGAIN),而不必在每次数据到达时立即处理。因此服务器可以先接收数据,再选择合适时机处理和转发,给服务端实现带来很大灵活性。 diff --git a/ch17/echo_EDGEserv.c b/ch17/echo_EDGEserv.c index 06735de..4550df1 100644 --- a/ch17/echo_EDGEserv.c +++ b/ch17/echo_EDGEserv.c @@ -43,7 +43,7 @@ int main(int argc, char *argv[]) event.events = EPOLLIN; //需要读取数据的情况 event.data.fd = serv_sock; - epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 + epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 event 中的事件 while (1) { diff --git a/ch17/echo_EPETserv.c b/ch17/echo_EPETserv.c index 2d5ec8a..b6eeccc 100644 --- a/ch17/echo_EPETserv.c +++ b/ch17/echo_EPETserv.c @@ -47,7 +47,7 @@ int main(int argc, char *argv[]) setnonblockingmode(serv_sock); event.events = EPOLLIN; //需要读取数据的情况 event.data.fd = serv_sock; - epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 + epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 event 中的事件 while (1) { @@ -85,7 +85,7 @@ int main(int argc, char *argv[]) } else if (str_len < 0) { - if (errno == EAGAIN) //read 返回-1 且 errno 值为 EAGAIN ,意味读取了输入缓冲的全部数据 + if (errno == EAGAIN) //read 返回-1 且 errno 值为 EAGAIN ,意味着已读取输入缓冲的全部数据 break; } else diff --git a/ch17/echo_EPLTserv.c b/ch17/echo_EPLTserv.c index d45ae19..617e2df 100644 --- a/ch17/echo_EPLTserv.c +++ b/ch17/echo_EPLTserv.c @@ -43,7 +43,7 @@ int main(int argc, char *argv[]) event.events = EPOLLIN; //需要读取数据的情况 event.data.fd = serv_sock; - epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 + epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 event 中的事件 while (1) { diff --git a/ch17/echo_epollserv.c b/ch17/echo_epollserv.c index 0a3792a..78cd8d9 100644 --- a/ch17/echo_epollserv.c +++ b/ch17/echo_epollserv.c @@ -43,7 +43,7 @@ int main(int argc, char *argv[]) event.events = EPOLLIN; //需要读取数据的情况 event.data.fd = serv_sock; - epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 enevt 中的事件 + epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //例程epfd 中添加文件描述符 serv_sock,目的是监听 event 中的事件 while (1) { diff --git a/ch17/homework/char_EPETserv.c b/ch17/homework/char_EPETserv.c index 9c09449..07536f7 100644 --- a/ch17/homework/char_EPETserv.c +++ b/ch17/homework/char_EPETserv.c @@ -89,17 +89,18 @@ int main(int argc, char *argv[]) epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); close(ep_events[i].data.fd); - for(i = 0; i < clnt_cnt; ++i) + int target_fd = ep_events[i].data.fd; + for(int j = 0; j < clnt_cnt; ++j) { - if(ep_events[i].data.fd == clnt_socks[i]) + if(target_fd == clnt_socks[j]) { - while(i++ < clnt_cnt-1) - clnt_socks[i] = clnt_socks[i+1]; + while(j++ < clnt_cnt-1) + clnt_socks[j] = clnt_socks[j+1]; break; } } --clnt_cnt; - printf("closed client: %d \n", ep_events[i].data.fd); + printf("closed client: %d \n", target_fd); break; } else if(str_len < 0) diff --git a/ch17/homework/char_EPLTserv.c b/ch17/homework/char_EPLTserv.c index 0059346..de31421 100644 --- a/ch17/homework/char_EPLTserv.c +++ b/ch17/homework/char_EPLTserv.c @@ -86,12 +86,13 @@ int main(int argc, char *argv[]) close(ep_events[i].data.fd); printf("closed client: %d \n", ep_events[i].data.fd); - for(i = 0; i < clnt_cnt; ++i) + int target_fd = ep_events[i].data.fd; + for(int j = 0; j < clnt_cnt; ++j) { - if(clnt_sock == clnt_socks[i]) + if(target_fd == clnt_socks[j]) { - while(i++ < clnt_cnt - 1) - clnt_socks[i] = clnt_socks[i + 1]; + while(j++ < clnt_cnt - 1) + clnt_socks[j] = clnt_socks[j + 1]; break; } } diff --git a/ch18/README.md b/ch18/README.md index 0fd78ac..eec9930 100644 --- a/ch18/README.md +++ b/ch18/README.md @@ -17,7 +17,7 @@ 只有一个 CPU 的系统是将时间分成多个微小的块后分配给了多个进程。为了分时使用 CPU ,需要「上下文切换」的过程。「上下文切换」是指运行程序前需要将相应进程信息读入内存,如果运行进程 A 后紧接着需要运行进程 B ,就应该将进程 A 相关信息移出内存(或保存到寄存器),并读入进程 B 相关信息。这就是上下文切换。上下文切换需要保存和恢复进程的上下文信息(寄存器、程序计数器、栈指针等),这个过程会带来一定的开销,即使通过优化加快速度,也会存在一定的局限。 -为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入的线程(Thread)的概念。这是为了将进程的各种劣势降至最低程度(不是直接消除)而设立的一种「轻量级进程」。线程比进程具有如下优点: +为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入了线程(Thread)的概念。这是为了将进程的各种劣势降至最低程度(不是直接消除)而设立的一种「轻量级进程」。线程比进程具有如下优点: - 线程的创建和上下文切换比进程的创建和上下文切换更快 - 线程间交换数据无需特殊技术 @@ -26,7 +26,7 @@ 线程是为了解决:为了得到多条代码执行流而复制整个内存区域的负担太重。 -每个进程的内存空间都由保存全局变量的「数据区」、向 malloc 等函数动态分配提供空间的堆(Heap)、函数运行时间使用的栈(Stack)构成。每个进程都有独立的这种空间,多个进程的内存结构如图所示: +每个进程的内存空间都由保存全局变量的「数据区」、向 malloc 等函数动态分配提供空间的堆(Heap)、函数运行时使用的栈(Stack)构成。每个进程都有独立的这种空间,多个进程的内存结构如图所示: ![](images/5c55aa57db3c7.png) @@ -80,7 +80,7 @@ arg : 通过第三个参数传递的调用函数时包含传递参数信息的 下面通过简单示例了解该函数功能: -- [thread1.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread1.c) +- [thread1.c](thread1.c) ```c #include @@ -210,13 +210,13 @@ gcc thread2.c -o tr2 -lpthread #### 18.2.2 可在临界区内调用的函数 -在同步的程序设计中,临界区块(Critical section)指的是一个访问共享资源(例如:共享设备或是共享存储器)的程序片段,而这些共享资源有无法同时被多个线程访问的特性。 +在同步的程序设计中,临界区(Critical section)指的是一个访问共享资源(例如:共享设备或是共享存储器)的程序片段,而这些共享资源有无法同时被多个线程访问的特性。 -当有线程进入临界区块时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区块的进入点与离开点实现,以确保这些共享资源是被异或的使用,例如:semaphore。 +当有线程进入临界区块时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区块的进入点与离开点实现,以确保这些共享资源被互斥地使用,例如:信号量(semaphore)。 只能被单一线程访问的设备,例如:打印机。 -一个最简单的实现方法就是当线程(Thread)进入临界区块时,禁止改变处理器;在uni-processor系统上,可以用“禁止中断(CLI)”来完成,避免发生系统调用(System Call)导致的上下文交换(Context switching);当离开临界区块时,处理器恢复原先的状态。 +一个最简单的实现方法就是当线程(Thread)进入临界区块时,禁止改变处理器;在uni-processor系统上,可以用“禁止中断(CLI)”来完成,避免发生系统调用(System Call)导致的上下文切换(Context switching);当离开临界区块时,处理器恢复原先的状态。 根据临界区是否引起问题,函数可以分为以下 2 类: @@ -305,7 +305,7 @@ gcc thread3.c -D_REENTRANT -o tr3 -lpthread ![](images/5c55c53d70494.png) -可以看出计算结果正确,两个线程都用了全局变量 sum ,证明了 2 个线程共享保存全局变量的数据区。 +可以看出计算结果正确,两个线程都用了全局变量 sum,证明了 2 个线程共享保存全局变量的数据区。 但是本例子本身存在问题。存在临界区相关问题,可以从下面的代码看出,下面的代码和上面的代码相似,只是增加了发生临界区错误的可能性,即使在高配置系统环境下也容易产生的错误: @@ -438,7 +438,7 @@ void *thread_des(void *arg) #### 18.4.2 互斥量 -互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全域变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。 +互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。 通俗地说,互斥量就是一把锁,当临界区被占据的时候就上锁,等占用完毕然后再放开。 @@ -481,7 +481,7 @@ int pthread_mutex_unlock(pthread_mutex_t *mutex); */ ``` -函数本身含有 lock unlock 等词汇,很容易理解其含义。进入临界区前调用的函数就是 pthread_mutex_lock。调用该函数时,发现有其他线程已经进入临界区,则 pthread_mutex_lock 函数不会返回,直到里面的线程调用 pthread_mutex_unlock 函数退出临界区位置。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。接下来整理一下代码的编写方式: +函数本身含有 lock unlock 等词汇,很容易理解其含义。进入临界区前调用的函数就是 pthread_mutex_lock。调用该函数时,发现有其他线程已经进入临界区,则 pthread_mutex_lock 函数不会返回,直到里面的线程调用 pthread_mutex_unlock 函数退出临界区。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。接下来整理一下代码的编写方式: ```c pthread_mutex_lock(&mutex); @@ -491,7 +491,7 @@ pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); ``` -简言之,就是利用 lock 和 unlock 函数围住临界区的两端。此时互斥量相当于一把锁,阻止多个线程同时访问,还有一点要注意,线程退出临界区时,如果忘了调用 pthread_mutex_unlock 函数,那么其他为了进入临界区而调用 pthread_mutex_lock 的函数无法摆脱阻塞状态。这种情况称为「死锁」。需要格外注意,下面是利用互斥量解决示例 [thread4.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread4.c) 中遇到的问题代码: +简言之,就是利用 lock 和 unlock 函数围住临界区的两端。此时互斥量相当于一把锁,阻止多个线程同时访问,还有一点要注意,线程退出临界区时,如果忘了调用 pthread_mutex_unlock 函数,那么其他为了进入临界区而调用 pthread_mutex_lock 的线程无法摆脱阻塞状态。这种情况称为「死锁」。需要格外注意,下面是利用互斥量解决示例 [thread4.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread4.c) 中遇到的问题代码: - [mutex.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/mutex.c) @@ -583,11 +583,11 @@ void *thread_inc(void *arg) #### 18.4.3 信号量 -信号量(英语:Semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release)时,计数值加一。当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态. +信号量(英语:Semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release)时,计数值加一。当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。semaphore对象的计数值大于 0,为 signaled 状态;计数值等于 0,为 nonsignaled 状态。 semaphore对象适用于控制一个仅支持有限个用户的共享资源,是一种不需要使用忙碌等待(busy waiting)的方法。 -信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程当前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在linux系统中,二进制信号量(binary semaphore)又称互斥锁(Mutex)。 +信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程当前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在 Linux 系统中,二进制信号量(binary semaphore)与互斥锁(Mutex)功能相似,但二者并不完全等同——互斥锁具有所有权语义,必须由加锁的线程解锁;信号量没有所有权限制。 下面介绍信号量,在互斥量的基础上,很容易理解信号量。此处只涉及利用「二进制信号量」(只用 0 和 1)完成「控制线程顺序」为中心的同步方法。下面是信号量的创建及销毁方法: @@ -615,7 +615,7 @@ sem : 传递保存信号量读取值的变量地址值,传递给 sem_post 的 */ ``` -调用 sem_init 函数时,操作系统将创建信号量对象,此对象中记录着「信号量值」(Semaphore Value)整数。该值在调用 sem_post 函数时增加 1,调用 sem_wait 函数时减 1。但信号量的值不能小于 0,因此,在信号量为 0 的情况下调用 sem_wait 函数时,调用的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post 函数,信号量的值将变为 1,而原本阻塞的线程可以将该信号重新减为 0 并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为 1): +调用 sem_init 函数时,操作系统将创建信号量对象,此对象中记录着「信号量值」(Semaphore Value)整数。该值在调用 sem_post 函数时增加 1,调用 sem_wait 函数时减 1。但信号量的值不能小于 0,因此,在信号量为 0 的情况下调用 sem_wait 函数时,调用的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post 函数,信号量的值将变为 1,而原本阻塞的线程可以将该信号量重新减为 0 并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为 1): ```c sem_wait(&sem);//信号量变为 0... diff --git a/ch18/chat_clnt.c b/ch18/chat_clnt.c index 02a0f83..5db1121 100644 --- a/ch18/chat_clnt.c +++ b/ch18/chat_clnt.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #define BUF_SIZE 100 @@ -73,7 +74,7 @@ void *recv_msg(void *arg) // 读取消息 while (1) { str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1); - if (str_len == -1) + if (str_len <= 0) return (void *)-1; name_msg[str_len] = 0; fputs(name_msg, stdout); diff --git a/ch18/chat_server.c b/ch18/chat_server.c index e321126..2bfce7f 100644 --- a/ch18/chat_server.c +++ b/ch18/chat_server.c @@ -22,7 +22,7 @@ int main(int argc, char *argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; - int clnt_adr_sz; + socklen_t clnt_adr_sz; pthread_t t_id; if (argc != 2) { @@ -52,7 +52,10 @@ int main(int argc, char *argv[]) clnt_socks[clnt_cnt++] = clnt_sock; //写入新连接 pthread_mutex_unlock(&mutx); //解锁 - pthread_create(&t_id, NULL, handle_clnt, (void *)&clnt_sock); //创建线程为新客户端服务,并且把clnt_sock作为参数传递 + int *clnt_sock_ptr = malloc(sizeof(int)); + *clnt_sock_ptr = clnt_sock; + pthread_create(&t_id, NULL, handle_clnt, (void *)clnt_sock_ptr); + // handle_clnt 中读取后需 free(arg) //创建线程为新客户端服务,并且把clnt_sock作为参数传递 pthread_detach(t_id); //引导线程销毁,不阻塞 printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr)); //客户端连接的ip地址 } @@ -63,6 +66,7 @@ int main(int argc, char *argv[]) void *handle_clnt(void *arg) { int clnt_sock = *((int *)arg); + free(arg); int str_len = 0, i; char msg[BUF_SIZE]; @@ -74,8 +78,11 @@ void *handle_clnt(void *arg) { if (clnt_sock == clnt_socks[i]) { - while (i++ < clnt_cnt - 1) + while (i < clnt_cnt - 1) + { clnt_socks[i] = clnt_socks[i + 1]; + i++; + } break; } } diff --git a/ch18/homework/echo_client.c b/ch18/homework/echo_client.c index e1e78a8..8155c5a 100644 --- a/ch18/homework/echo_client.c +++ b/ch18/homework/echo_client.c @@ -3,6 +3,7 @@ #include #include #include +#include #define BUF_SIZE 1024 void error_handling(char* message); diff --git a/ch18/homework/echo_threadserv.c b/ch18/homework/echo_threadserv.c index f9f6540..9316e8b 100644 --- a/ch18/homework/echo_threadserv.c +++ b/ch18/homework/echo_threadserv.c @@ -61,7 +61,10 @@ void * handle_clnt(void * arg) pthread_mutex_lock(&mutx); str_len = read(clnt_sock, buf, sizeof(buf)); if(str_len <= 0) + { + pthread_mutex_unlock(&mutx); break; + } else write(clnt_sock, buf, str_len); pthread_mutex_unlock(&mutx); diff --git a/ch18/thread1.c b/ch18/thread1.c index 126a5ac..71f7197 100644 --- a/ch18/thread1.c +++ b/ch18/thread1.c @@ -19,7 +19,6 @@ int main(int argc, char *argv[]) } void *thread_main(void *arg) //传入的参数是 pthread_create 的第四个 { - int i; int cnt = *((int *)arg); for (int i = 0; i < cnt; i++) { diff --git a/ch18/thread2.c b/ch18/thread2.c index 9498859..e88fad2 100644 --- a/ch18/thread2.c +++ b/ch18/thread2.c @@ -2,6 +2,7 @@ #include #include #include +#include void *thread_main(void *arg); int main(int argc, char *argv[]) @@ -30,7 +31,7 @@ void *thread_main(void *arg) //传入的参数是 pthread_create 的第四个 int i; int cnt = *((int *)arg); char *msg = (char *)malloc(sizeof(char) * 50); - strcpy(msg, "Hello,I'am thread~ \n"); + strcpy(msg, "Hello, I'm thread~ \n"); for (int i = 0; i < cnt; i++) { sleep(1); diff --git a/ch18/thread4.c b/ch18/thread4.c index 69f09f6..e7dd62f 100644 --- a/ch18/thread4.c +++ b/ch18/thread4.c @@ -13,7 +13,7 @@ int main(int argc, char *argv[]) pthread_t thread_id[NUM_THREAD]; int i; - printf("sizeof long long: %d \n", sizeof(long long)); + printf("sizeof long long: %zu \n", sizeof(long long)); for (i = 0; i < NUM_THREAD; i++) { if (i % 2) diff --git a/ch24/README.md b/ch24/README.md index c874dbe..39ce1a1 100644 --- a/ch24/README.md +++ b/ch24/README.md @@ -8,7 +8,7 @@ #### 24.1.1 理解 Web 服务器端 -web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客户端的服务器端。 +Web 服务器端就是要基于 HTTP 协议,将网页对应文件传输给客户端的服务器端。 #### 24.1.2 HTTP @@ -26,7 +26,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 -从图中可以看出,请求消息可以分为请求行、消息头、消息体 3 个部分。其中,请求行含有请求方式(请求目的)信息。典型的请求方式有 GET 和 POST ,GET 主要用于请求数据,POST 主要用于传输数据。为了降低复杂度,我们实现只能响应 GET 请求的 Web 服务器端,下面解释图中的请求行信息。其中「GET/index.html HTTP/1.1」 具有如下含义: +从图中可以看出,请求消息可以分为请求行、消息头、消息体 3 个部分。其中,请求行含有请求方式(请求目的)信息。典型的请求方式有 GET 和 POST ,GET 主要用于请求数据,POST 主要用于传输数据。为了降低复杂度,我们实现只能响应 GET 请求的 Web 服务器端,下面解释图中的请求行信息。其中「GET /index.html HTTP/1.1」具有如下含义: > 请求(GET)index.html 文件,通常以 1.1 版本的 HTTP 协议进行通信。 @@ -36,7 +36,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 #### 24.1.4 响应消息(Response Message)的结构 -下面是 Web 服务器端向客户端传递的响应信息的结构。从图中可以看出,该响应消息由状态行、头信息、消息体等 3 个部分组成。状态行中有关于请求的状态信息,这是与请求消息相比最为显著的区别。 +下面是 Web 服务器端向客户端传递的响应信息的结构。从图中可以看出,该响应消息由状态行、头信息、消息体等 3 个部分组成。状态行中有关于请求的状态信息,这是与请求消息相比最为显著的区别。 @@ -49,7 +49,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 消息头中含有传输的数据类型和长度等信息。图中的消息头含有如下信息: -> 服务端名为 SimpleWebServer ,传输的数据类型为 text/html。数据长度不超过 2048 个字节。 +> 服务端名为 SimpleWebServer,传输的数据类型为 text/html。数据长度不超过 2048 个字节。 最后插入一个空行后,通过消息体发送客户端请求的文件数据。以上就是实现 Web 服务端过程中必要的 HTTP 协议。 @@ -63,7 +63,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客 下面是代码: -- [webserv_linux.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch24/webserv_linux.c) +- [webserv_linux.c](webserv_linux.c) ```c #include @@ -236,7 +236,7 @@ gcc webserv_linux.c -D_REENTRANT -o web_serv -lpthread -经过测试,这个简单的 HTTP 服务器可以正常的显示出页面。 +经过测试,这个简单的 HTTP 服务器可以正常地显示出页面。 ### 24.3 习题 @@ -244,12 +244,12 @@ gcc webserv_linux.c -D_REENTRANT -o web_serv -lpthread 1. **下列关于 Web 服务器端和 Web 浏览器端的说法错误的是**? - 答:**选项 5 是错误的**。 + 答:**选项 2 和选项 5 是错误的**。选项 2 错误在于:HTTP 是无状态协议,一次请求-响应完成后即断开连接,并不"保持较长的客户端连接";Web 服务器端基于 TCP 是为了可靠传输,而非保持长连接。选项 5 错误在于:任何能发起 HTTP 请求的客户端(如 curl、wget 等)都可以访问 Web 服务器端。 1. Web 浏览器是通过自身创建的套接字连接服务端的客户端 2. Web 服务器端通过 TCP 套接字提供服务,因为它将保持较长的客户端连接并交换数据 3. 超文本与普通文本的最大区别是其具有可跳转的特性 - 4. Web 浏览器可视为向浏览器提供请求文件的文件传输服务器端 + 4. Web 服务器端可视为向浏览器提供请求文件的文件传输服务器端 5. **除 Web 浏览器外,其他客户端都无法访问 Web 服务器端。(错误:任何能发起 HTTP 请求的客户端都可以访问 Web 服务器端,如 curl、wget、编程实现的 HTTP 客户端等)** 2. **下列关于 HTTP 协议的描述错误的是**? diff --git a/ch24/webserv_linux.c b/ch24/webserv_linux.c index 1b136af..715c6a3 100644 --- a/ch24/webserv_linux.c +++ b/ch24/webserv_linux.c @@ -17,10 +17,9 @@ void error_handling(char *message); int main(int argc, char *argv[]) { - int serv_sock, clnt_sock; + int serv_sock; struct sockaddr_in serv_adr, clnt_adr; int clnt_adr_size; - char buf[BUF_SIZE]; pthread_t t_id; if (argc != 2) { @@ -41,10 +40,11 @@ int main(int argc, char *argv[]) while (1) { clnt_adr_size = sizeof(clnt_adr); - clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_size); + int *clnt_sock_p = malloc(sizeof(int)); + *clnt_sock_p = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_size); printf("Connection Request : %s:%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port)); - pthread_create(&t_id, NULL, request_handler, &clnt_sock); + pthread_create(&t_id, NULL, request_handler, clnt_sock_p); pthread_detach(t_id); } close(serv_sock); @@ -54,6 +54,7 @@ int main(int argc, char *argv[]) void *request_handler(void *arg) { int clnt_sock = *((int *)arg); + free(arg); char req_line[SMALL_BUF]; FILE *clnt_read; FILE *clnt_write; @@ -70,7 +71,7 @@ void *request_handler(void *arg) send_error(clnt_write); fclose(clnt_read); fclose(clnt_write); - return; + return NULL; } strcpy(method, strtok(req_line, " /")); strcpy(file_name, strtok(NULL, " /")); @@ -80,7 +81,7 @@ void *request_handler(void *arg) send_error(clnt_write); fclose(clnt_read); fclose(clnt_write); - return; + return NULL; } fclose(clnt_read); send_data(clnt_write, ct, file_name); @@ -99,6 +100,7 @@ void send_data(FILE *fp, char *ct, char *file_name) if (send_file == NULL) { send_error(fp); + fclose(fp); return; } @@ -108,7 +110,7 @@ void send_data(FILE *fp, char *ct, char *file_name) fputs(cnt_len, fp); fputs(cnt_type, fp); - //传输请求数据 + //传输响应体数据 while (fgets(buf, BUF_SIZE, send_file) != NULL) { fputs(buf, fp); @@ -121,9 +123,13 @@ char *content_type(char *file) { char extension[SMALL_BUF]; char file_name[SMALL_BUF]; + char *ext; strcpy(file_name, file); strtok(file_name, "."); - strcpy(extension, strtok(NULL, ".")); + ext = strtok(NULL, "."); + if (ext == NULL) + return "text/plain"; + strcpy(extension, ext); if (!strcmp(extension, "html") || !strcmp(extension, "htm")) return "text/html";