docs: 全面校对全部章节文档与示例代码

通过多智能体工作流对 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
跳过,已人工复核)。
This commit is contained in:
riba2534
2026-06-28 12:47:46 +08:00
parent a9ef4b6dc4
commit 5625eea472
76 changed files with 707 additions and 629 deletions

607
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -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 <sys/socket.h>
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 <sys/socket.h>
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 <sys/types.h>
@@ -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 函数来完成地址分配。

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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 <stdio.h>
@@ -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 <stdio.h>
@@ -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 <stdio.h>
@@ -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 <stdio.h>
@@ -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 的实现

View File

@@ -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;

View File

@@ -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 有什么关系?**

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 第一次发出的 ACKB 会重传 FIN主机 A 收到后再次发送 ACK 5001 予以确认
### 5.3 基于 Windows 的实现

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 <sys/socket.h>
@@ -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 已连接connectUDP 套接字与未连接unconnectedUDP 套接字
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 分别负责哪些部分?**

View File

@@ -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);

View File

@@ -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 <IP> <port>\n", argv[0]);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 <IP> <port>\n", argv[0]);

View File

@@ -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;

View File

@@ -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 的实现

View File

@@ -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));

View File

@@ -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_addrtypegethostbyname 函数不仅支持 IPv4 还支持 IPv6。因此可以通过此变量获取保存在 h_addr_list 的IP地址族信息。若是 IPv4则此变量中存有 AF_INET。
- h_addrtype该成员用于获取保存在 h_addr_list 的地址族信息。若是 IPv4则此变量中存有 AF_INET;若是 IPv6则存有 AF_INET6。需要注意gethostbyname 函数本身只返回 IPv4AF_INET地址若需解析 IPv6 地址应改用 getaddrinfo 函数
- h_length保存IP地址长度。若是 IPv4 地址,因为是 4 个字节,则保存 4IPv6 时,因为是 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_INETIPv6 是 AF_INET6。
下面的代码演示使用方法:
- [gethostbyaddr.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch08/gethostbyaddr.c)
- [gethostbyaddr.c](gethostbyaddr.c)
编译运行:

View File

@@ -11,7 +11,7 @@ int main(int argc, char *argv[])
struct hostent *host;
if (argc != 2)
{
printf("Usage : %s <addr>\n", argv[0]);
printf("Usage : %s <domain>\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

View File

@@ -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 带来的延迟,从而提高传输速度

View File

@@ -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");

View File

@@ -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 <stdio.h>
@@ -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 程序的另外一个好处是,可以提高频繁交换数据的程序性能,如下图所示:

View File

@@ -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));

View File

@@ -23,7 +23,7 @@ int main(int argc, char *argv[])
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
printf("Usage : %s <port>\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);

View File

@@ -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 信号的处理器。

View File

@@ -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++)
{

View File

@@ -23,7 +23,7 @@ int main(int argc, char *argv[])
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
printf("Usage : %s <port>\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);

View File

@@ -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));

View File

@@ -14,7 +14,7 @@ int main(int argc, char *argv[])
}
else
{
//调用waitpid 传递参数 WNOHANG ,这样之前有没有终止的子进程则返回0
// 调用 waitpid 传递参数 WNOHANG这样如果没有终止的子进程则返回 0
while (!waitpid(-1, &status, WNOHANG))
{
sleep(3);

View File

@@ -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;

View File

@@ -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 <stdio.h>
@@ -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 <stdio.h>
@@ -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 <stdio.h>
@@ -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 习题

View File

@@ -24,7 +24,7 @@ int main(int argc, char *argv[])
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
printf("Usage : %s <port>\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);

View File

@@ -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};

View File

@@ -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 监视对象的文件描述符。

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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 <sys/uio.h>
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 <stdio.h>
@@ -218,14 +218,14 @@ ABC1234
Write bytes: 7
```
下面介绍 readv 函数,功能和 writev 函数正好相反.函数为:
下面介绍 readv 函数,功能和 writev 函数正好相反函数为:
```c
#include <sys/uio.h>
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 <stdio.h>
@@ -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 缓冲的角度给出说明**

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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和 TTLTime 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 是用于多播的网络,但它是虚拟网络。请解释此处的「虚拟网络」**

View File

@@ -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 个参数分传入 NULL0
str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0);
if (str_len < 0)
break;

View File

@@ -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 个参数分传入 NULL0
str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0);
if (str_len < 0)
break;

View File

@@ -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);
}

View File

@@ -17,7 +17,7 @@ int main(int argc, char *argv[])
int so_brd = 1;
if (argc != 3)
{
printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
printf("Usage : %s <BroadcastIP> <PORT>\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);
}

View File

@@ -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 <stdio.h>
@@ -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 <stdio.h>
@@ -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 个优点?**
答:①具有很高的移植性②有良好的缓冲提高性能。

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,16 +1,16 @@
#include <stdio.h>
#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);

View File

@@ -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);

View File

@@ -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 <stdio.h>
@@ -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)

View File

@@ -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-blockingI/O ,更改了套接字特性。
Linux 套接字相关函数一般通过 -1 通知发生了错误。虽然知道发生了错误但仅凭这些内容无法得知产生错误的原因。因此为了在发生错误的时候提额外的信息Linux 声明了如下全局变量:
Linux 套接字相关函数一般通过 -1 通知发生了错误。虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误的时候提额外的信息Linux 声明了如下全局变量:
```c
int errno;
@@ -294,7 +294,7 @@ int errno;
> read 函数发现输入缓冲中没有数据可读时返回 -1同时在 errno 中保存 EAGAIN 常量
下面是 Linux 中提供的改变和更改文件属性的法:
下面是 Linux 中提供的更改文件属性的法:
```c
#include <fcntl.h>
@@ -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而不必在每次数据到达时立即处理。因此服务器可以先接收数据再选择合适时机处理和转发,给服务端实现带来很大灵活性。

View File

@@ -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)
{

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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 <stdio.h>
@@ -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...

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#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);

View File

@@ -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;
}
}

View File

@@ -3,6 +3,7 @@
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#define BUF_SIZE 1024
void error_handling(char* message);

View File

@@ -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);

View File

@@ -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++)
{

View File

@@ -2,6 +2,7 @@
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
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);

View File

@@ -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)

View File

@@ -8,7 +8,7 @@
#### 24.1.1 理解 Web 服务器端
web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客户端的服务器端。
Web 服务器端就是要基于 HTTP 协议,将网页对应文件传输给客户端的服务器端。
#### 24.1.2 HTTP
@@ -26,7 +26,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客
<!-- 原图片链接已失效: https://i.loli.net/2019/02/07/5c5bcbb75202f.png -->
<!-- 图示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」具有如下含义:
> 请求GETindex.html 文件,通常以 1.1 版本的 HTTP 协议进行通信。
@@ -36,7 +36,7 @@ web服务器端就是要基于 HTTP 协议,将网页对应文件传输给客
#### 24.1.4 响应消息Response Message的结构
下面是 Web 服务器端向客户端传递的响应信息的结构。从图中可以看出,该响应消息由状态行、头信息、消息体等 3 个部分组成。状态行中有关于请求的状态信息,这是与请求消息相比最为显著的区别。
下面是 Web 服务器端向客户端传递的响应信息的结构。从图中可以看出该响应消息由状态行、头信息、消息体等 3 个部分组成。状态行中有关于请求的状态信息,这是与请求消息相比最为显著的区别。
<!-- 原图片链接已失效: https://i.loli.net/2019/02/07/5c5bf9ad1b5f9.png -->
<!-- 图示HTTP 响应消息结构(状态行、头信息、消息体) -->
@@ -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 <stdio.h>
@@ -236,7 +236,7 @@ gcc webserv_linux.c -D_REENTRANT -o web_serv -lpthread
<!-- 原图片链接已失效: https://i.loli.net/2019/02/07/5c5c19cbb3718.png -->
<!-- 图示:浏览器访问 HTTP 服务器截图 -->
经过测试,这个简单的 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 协议的描述错误的是**

View File

@@ -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";