《TCP/IP网络编程》 第9章 套接字的多种可选项

套接字具有多种特性,这些特性可通过可选项更改。本章将介绍更改套接字可选项的方法,并以此为基础进一步观察套接字内部。

9.1 套接字可选项和I/O缓冲大小

1.1 套接字多种可选项

我们之前写得程序都是创建好套接字之后直接使用的,此时通过默认的套接字特性进行数据通信,这里列出了一些套接字可选项。
在这里插入图片描述
从表中可以看出,套接字可选项是分层的。
IPPROTO_IP层:IP协议相关事项;
IPPROTO_TCP层:TCP协议相关的事项;
SOL_SOCKERT层:是套接字相关的通用可选项;

1.2 getsockpot &setsockopt

我们几乎可以针对上表中的所有可选项进行读取(Get)和设置(Set)(有些选项只能进行一种操作)。可选项的读取和设置通过如下2个函数完成。

#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
/*
成功时返回 0 ,失败时返回 -1
sock: 		用于查看选项套接字文件描述符
level: 		要查看的可选项协议层
optname: 	要查看的可选项名
optval: 	保存查看结果的缓冲地址值
optlen: 	向第四个参数传递的缓冲大小。调用函数候,该变量中保存通过第四个参数返回的可选项信息的字节数。
*/

上述函数可以用来读取套接字可选项,下面的函数可以更改可选项。

#include <sys/socket.h>

int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
/*
成功时返回 0 ,失败时返回 -1
sock: 用于更改选项套接字文件描述符
level: 要更改的可选项协议层
optname: 要更改的可选项名
optval: 保存更改结果的缓冲地址值
optlen: 向第四个参数传递的缓冲大小。调用函数候,该变量中保存通过第四个参数返回的可选项信息的字节数。
*/

下面介绍getsockopt函数的调用方法。下列示例用协议层SOL_SOCKET、名为SO_TYPE的可选项查看套接字类型(TCP或UDP)。
示例代码:sock_type.c
编译运行:

gcc sock_type.c -o sock_type
./sock_type

结果:

SOCK_STREAM: 1
SOCK_DGRAM: 2
Socket type one: 1
Socket type two: 2

首先创建了一个 TCP 套接字和一个 UDP 套接字。然后通过调用 getsockopt 函数来获得当前套接字的状态。

验证套接类型的 SO_TYPE 是只读可选项,因为套接字类型只能在创建时决定,以后不能再更改。

1.3SO_SNDBUF &SO_RCVBUF

创建套接字将同时生成I/O缓冲(第5章内容),下面将介绍I/O缓冲相关可选项。
SO_RCVBUF是输入缓冲大小相关可选项;
SO_SNDBUF是输出缓冲大小相关可选项;
这两个可选项既可以读取当前I/O缓冲大小,也可以进行更改。
通过下列示例读取创建套接字时默认的I/O缓冲大小。
示例代码:get_buf.c
编译运行:

gcc get_buf.c -o getbuf
./getbuf

运行结果:

Input buffer size: 87380
Output buffer size: 16384

可以看出本机的输入缓冲和输出缓冲大小。

下面的代码演示了,通过程序设置 I/O 缓冲区的大小

示例代码:set_buf.c
编译运行:

gcc get_buf.c -o setbuf
./setbuf

结果:

Input buffer size: 6144
Output buffer size: 6144

输出结果和我们预想的不是很相同,缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行。

9.2SO_REUSEADDR

本节的可选项SO_REUSEADDR以及相关的Time-wait状态很重要!

2.1 发生地址分配错误(Binding Error)

在学习 SO_REUSEADDR可选项之前,应该好好理解Time-wait状态。看以下代码的示例:
示例代码:reuseadr_eserver.c
此示例是之前已实现过多次的回声服务器端,可以结合第4章的echo_client.c使用,在此代码中,通过如下方式终止程序:

在客户端控制台输入Q消息,通知服务器终止程序。

在客户端控制台输入Q消息时调用close函数,向服务器发送FIN消息并经过四次握手过程。当然,输入CTRL + C 也会向服务器传递FIN信息。强制终止程序时,由操作系统关闭文件套接字,此过程相当于调用 close 函数,也会向服务器发送 FIN 消息。

这样看不到是什么特殊现象,考虑以下情况:

服务器端和客户端都已经建立连接的状态下,向服务器控制台输入 CTRL+C ,强制关闭服务端‘

如果用这种方式终止程序,如果用同一端口号再次运行服务端,就会输出「bind() error」消息,并且无法再次运行。但是在这种情况下,再过大约 3 分钟就可以重新运行服务端。
上述2种运行方式唯一的区别就是谁先传输FIN消息,但结果却迥然不同,原因何在呢?

2.2 Time-wait 状态

四次握手过程如下:
在这里插入图片描述
假设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状态。

2.3 地址再分配

Time-wait看似重要,但并不一定讨人喜欢。如果系统发生故障紧急停止,这时需要尽快重启服务起以提供服务,但因处于 Time-wait状态而必须等待几分钟。因此,Time-wait并非只有优点,这些情况下容易引发大问题。下图中展示了四次握手时不得不延长 Time-wait 过程的情况。
123
从图上可以看出,在主机 A 四次握手的过程中,如果最后的数据丢失,则主机 B 会认为主机 A 未能收到自己发送的 FIN 信息,因此重传。这时,收到的 FIN 消息的主机 A 将重启 Time-wait 计时器。因此,如果网络状况不理想, Time-wait 将持续。

解决方案就是在套接字的可选项中更改 SO_REUSEADDR 的状态。
适当调整该参数,可将 Time-wait状态下的套接字端口号重新分配给新的套接字SO_REUSEADDR 的默认值为 0。这就意味着无法分配 Time-wait状态下的套接字端口号。因此需要将这个值改成 1 。具体作法已在示例 reuseadr_eserver.c 给出,只需要把注释掉的东西接解除注释即可。

optlen = sizeof(option);
option = TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);

此时,已经解决了上述问题。

9.3 TCP_NODELAY

3.1 Nagle算法

为了防止因数据包过多而发生网络过载,Nagle算法在1984年诞生了。它应用于TCP层,非常简单。其使用与否会导致如下图所示差异。
在这里插入图片描述
上图展示了通过Nagle算法发送字符串“Nagle”和未使用Nagle算法的差别。可以得到如下结论:

“只有收到前一数据的ACK消息时,Nagle算法才发送下一数据。”

使用Nagle算法过程:
TCP套接字默认使用Nagle算法交换数据,因此最大限度地进行缓冲,知道收到ACK。上图左侧正是这种情况。为了发送字符串“Nagle”,将其传递到输出缓冲。这时头字符“N”之前没有其他数据(没有需接收的ACK),因此立即传输。之后开始等待字符“N”的ACK消息,等待过程中,剩下的“agle”填入输出缓冲。接下来,收到字符“N”的ACK消息后,将输出缓冲的“agle”装入一个数据包发送。也就是说,共需要传递4个数据包以传输1个字符串。

未使用Nagle算法过程:
接下来分析未使用Nagle算法时发送字符串“Nagle”的过程。假设字符“N”到“e”依序传到输出缓冲。此时的发送过程与ACK接收与否无法,因此数据到达输出缓冲后立即被发送出去。从上图右侧可以看到,发送字符串“Nagle”时共需10个数据包。由此可知,不使用Nagle算法将对网络流量(Traffic:指网络负载或混杂程度)产生负面影响。即使只传输1个字节的数据,其头信息都有可能是几十个字节。因此,为了提高网络传输效率,必须使用Nagle算法。

Nagle算法并不是什么情况下都适用,网络流量未受太大影响时,不使用Nagle算法要比使用它时传输速度快。最典型的就是「传输大文数据」。将文件数据传入输出缓冲不会花太多时间,因此,不使用 Nagle算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而在无需等待 ACK 的前提下连续传输,因此可以大大提高传输速度。

所以,未准确判断数据性质时不应禁用 Nagle算法。

3.2 禁用Nagle算法

刚才说过的“大文件数据”应该禁用Nagle算法。
禁用 Nagle算法应该使用:

int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));

通过 TCP_NODELAY的值来查看Nagle 算法的设置状态。

opt_len = sizeof(opt_val);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, opt_len);

如果正在使用Nagle算法,那么 opt_val 值为 0,如果禁用则为 1。

关于这个算法,可以参考这个回答:TCP连接中启用和禁用TCP_NODELAY有什么影响?

9.5 习题

1、下列关于 Time-wait 状态的说法错误的是?

  1. Time-wait 状态只在服务器的套接字中发生
  2. 断开连接的四次握手过程中,先传输 FIN 消息的套接字将进入 Time-wait 状态。
  3. Time-wait 状态与断开连接的过程无关,而与请求连接过程中 SYN 消息的传输顺序有关
  4. Time-wait 状态通常并非必要,应尽可能通过更改套接字可选项来防止其发生

答:1、3、4的说法错误。

2、TCP_NODELAY 可选项与 Nagle 算法有关,可通过它禁用 Nagle 算法。请问何时应考虑禁用 Nagle 算法?结合收发数据的特性给出说明。

答:当网络流量未受太大影响时,不使用 Nagle 算法要比使用它时传输速度快,比如说在传输大文件时。

猜你喜欢

转载自blog.csdn.net/qq_38159549/article/details/89184993