mirror of
https://github.com/riba2534/TCP-IP-NetworkNote.git
synced 2026-02-04 02:23:21 +08:00
@@ -8,7 +8,7 @@ TCP 是内容较多的一个协议,而本章中的 UDP 内容较少,但是
|
||||
|
||||
#### 6.1.1 UDP 套接字的特点
|
||||
|
||||
通过寄信来说明 UDP 的工作原理,这是讲解 UDP 时使用的传统示例,它与 UDP 的特点完全相同。寄信前应现在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认信件是否被收到。邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式,UDP 也是一种不可靠的数据传输方式。
|
||||
通过寄信来说明 UDP 的工作原理,这是讲解 UDP 时使用的传统示例,它与 UDP 的特点完全相同。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认信件是否被收到。邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式,UDP 也是一种不可靠的数据传输方式。
|
||||
|
||||
因为 UDP 没有 TCP 那么复杂,所以编程难度比较小,性能也比 TCP 高。在更重视性能的情况下可以选择 UDP 的传输方式。
|
||||
|
||||
@@ -26,7 +26,7 @@ TCP 与 UDP 的区别很大一部分来源于流控制。也就是说 TCP 的生
|
||||
|
||||
UDP 也具有一定的可靠性。对于通过网络实时传递的视频或者音频时情况有所不同。对于多媒体数据而言,丢失一部分数据也没有太大问题,这只是会暂时引起画面抖动,或者出现细微的杂音。但是要提供实时服务,速度就成为了一个很重要的因素。因此流控制就显得有一点多余,这时就要考虑使用 UDP 。TCP 比 UDP 慢的原因主要有以下两点:
|
||||
|
||||
- 收发数据前后进行的连接设置及清楚过程。
|
||||
- 收发数据前后进行的连接设置及清除过程。
|
||||
- 收发过程中为保证可靠性而添加的流控制。
|
||||
|
||||
如果收发的数据量小但是需要频繁连接时,UDP 比 TCP 更高效。
|
||||
@@ -39,7 +39,7 @@ UDP 中的服务端和客户端不像 TCP 那样在连接状态下交换数据
|
||||
|
||||
#### 6.2.2 UDP 服务器和客户端均只需一个套接字
|
||||
|
||||
TCP 中,套接字之间应该是一对一的关系。若要向 10 个客户端提供服务,除了守门的服务器套接字之外,还需要 10 个服务器套接字。但在 UDP 中,不管事服务器端还是客户端都只需要 1 个套接字。只需要一个 UDP 套接字就可以向任意主机传输数据,如图所示:
|
||||
TCP 中,套接字之间应该是一对一的关系。若要向 10 个客户端提供服务,除了守门的服务器套接字之外,还需要 10 个服务器套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。只需要一个 UDP 套接字就可以向任意主机传输数据,如图所示:
|
||||
|
||||

|
||||
|
||||
@@ -54,7 +54,7 @@ TCP 中,套接字之间应该是一对一的关系。若要向 10 个客户端
|
||||
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags,
|
||||
struct sockaddr *to, socklen_t addrlen);
|
||||
/*
|
||||
成功时返回传输的字节数,失败是返回 -1
|
||||
成功时返回发送的字节数,失败时返回 -1
|
||||
sock: 用于传输数据的 UDP 套接字
|
||||
buff: 保存待传输数据的缓冲地址值
|
||||
nbytes: 待传输的数据长度,以字节为单位
|
||||
@@ -71,7 +71,7 @@ addrlen: 传递给参数 to 的地址值结构体变量长度
|
||||
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
|
||||
struct sockaddr *from, socklen_t *addrlen);
|
||||
/*
|
||||
成功时返回传输的字节数,失败是返回 -1
|
||||
成功时返回接收的字节数,失败时返回 -1
|
||||
sock: 用于传输数据的 UDP 套接字
|
||||
buff: 保存待传输数据的缓冲地址值
|
||||
nbytes: 待传输的数据长度,以字节为单位
|
||||
@@ -109,7 +109,7 @@ TCP 客户端套接字在调用 connect 函数时自动分配IP地址和端口
|
||||
|
||||
#### 6.2.5 UDP 客户端套接字的地址分配
|
||||
|
||||
仔细观察 UDP 客户端可以发现,UDP 客户端缺少了把IP和端口分配给套接字的过程。TCP 客户端调用 connect 函数自动完成此过程,而 UDP 中连接能承担相同功能的函数调用语句都没有。究竟在什么时候分配IP和端口号呢?
|
||||
仔细观察 UDP 客户端可以发现,UDP 客户端缺少了把IP和端口分配给套接字的过程。TCP 客户端调用 connect 函数自动完成此过程,而 UDP 中连能承担相同功能的函数调用语句都没有。究竟在什么时候分配IP和端口号呢?
|
||||
|
||||
UDP 程序中,调用 sendto 函数传输数据前应该完成对套接字的地址分配工作,因此调用 bind 函数。当然,bind 函数在 TCP 程序中出现过,但 bind 函数不区分 TCP 和 UDP,也就是说,在 UDP 程序中同样可以调用。另外,如果调用 sendto 函数尚未分配地址信息,则在首次调用 sendto 函数时给相应套接字自动分配 IP 和端口。而且此时分配的地址一直保留到程序结束为止,因此也可以用来和其他 UDP 套接字进行数据交换。当然,IP 用主机IP,端口号用未选用的任意端口号。
|
||||
|
||||
@@ -121,7 +121,7 @@ UDP 程序中,调用 sendto 函数传输数据前应该完成对套接字的
|
||||
|
||||
前面说得 TCP 数据传输中不存在数据边界,这表示「数据传输过程中调用 I/O 函数的次数不具有任何意义」
|
||||
|
||||
相反,UDP 是具有数据边界的下一,传输中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数和输出函数的调用次数完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。通过一个例子来进行验证:
|
||||
相反,UDP 是具有数据边界的下一,传输中调用 I/O 函数的次数非常重要。因此,输入函数的调用次数和输出函数的调用次数应该完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。通过一个例子来进行验证:
|
||||
|
||||
- [bound_host1.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host1.c)
|
||||
- [bound_host2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch06/bound_host2.c)
|
||||
|
||||
@@ -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 为何要半关闭
|
||||
|
||||
|
||||
@@ -155,6 +155,6 @@ gcc gethostbyaddr.c -o hostaddr
|
||||
|
||||
答:答案就是可行,DNS 服务器是分布式的,一台坏了可以找其他的。
|
||||
|
||||
3. **再浏览器地址输入 www.orentec.co.kr ,并整理出主页显示过程。假设浏览器访问默认 DNS 服务器中并没有关于 www.orentec.co.kr 的地址信息.**
|
||||
3. **在浏览器地址输入 www.orentec.co.kr ,并整理出主页显示过程。假设浏览器访问默认 DNS 服务器中并没有关于 www.orentec.co.kr 的地址信息.**
|
||||
|
||||
答:可以参考一下知乎回答,[在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤?](https://www.zhihu.com/question/34873227/answer/518086565),我用我自己的理解,简单说一下,首先会去向上一级的 DNS 服务器去查询,通过这种方式逐级向上传递信息,一直到达根服务器时,它知道应该向哪个 DNS 服务器发起询问。向下传递解析请求,得到IP地址候原路返回,最后会将解析的IP地址传递到发起请求的主机。
|
||||
|
||||
@@ -68,7 +68,7 @@ sock: 用于更改选项套接字文件描述符
|
||||
level: 要更改的可选项协议层
|
||||
optname: 要更改的可选项名
|
||||
optval: 保存更改结果的缓冲地址值
|
||||
optlen: 向第四个参数传递的缓冲大小。调用函数候,该变量中保存通过第四个参数返回的可选项信息的字节数。
|
||||
optlen: 向第四个参数传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。
|
||||
*/
|
||||
```
|
||||
|
||||
@@ -94,7 +94,7 @@ Socket type two: 2
|
||||
|
||||
首先创建了一个 TCP 套接字和一个 UDP 套接字。然后通过调用 getsockopt 函数来获得当前套接字的状态。
|
||||
|
||||
验证套接类型的 SO_TYPE 是只读可选项,因为**套接字类型只能在创建时决定,以后不能再更改**。
|
||||
用于验证套接类型的 SO_TYPE 是只读可选项,因为**套接字类型只能在创建时决定,以后不能再更改**。
|
||||
|
||||
#### 9.1.3 `SO_SNDBUF` & `SO_RCVBUF`
|
||||
|
||||
@@ -164,9 +164,9 @@ Output buffer size: 6144
|
||||
|
||||
假设图中主机 A 是服务器,因为是主机 A 向 B 发送 FIN 消息,故可想象成服务器端在控制台中输入 CTRL+C 。但是问题是,套接字经过四次握手后并没有立即消除,而是要经过一段时间的 Time-wait 状态。当然,只有先断开连接的(先发送 FIN 消息的)主机才经过 Time-wait 状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在 Time-wait 过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind 函数调用过程中会发生错误。
|
||||
|
||||
**实际上,不论是服务端还是客户端,都要经过一段时间的 Time-wait 过程。先断开连接的套接字必然会经过 Time-wait 过程,但是由于客户端套接字的端口是任意制定的,所以无需过多关注 Time-wait 状态。**
|
||||
**实际上,不论是服务端还是客户端,都要经过一段时间的 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 地址再分配
|
||||
|
||||
@@ -176,7 +176,7 @@ 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](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch09/reuseadr_eserver.c) 给出,只需要把注释掉的东西解除注释即可。
|
||||
|
||||
```c
|
||||
optlen = sizeof(option);
|
||||
@@ -217,7 +217,7 @@ setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));
|
||||
|
||||
```c
|
||||
opt_len = sizeof(opt_val);
|
||||
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, opt_len);
|
||||
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, &opt_len);
|
||||
```
|
||||
|
||||
如果正在使用`Nagle` 算法,那么 opt_val 值为 0,如果禁用则为 1.
|
||||
|
||||
Reference in New Issue
Block a user