《TCP/IP 网络编程》第 9 章——套接字的多种可选项(学习笔记)

本文深入探讨了套接字编程的高级配置,包括更改I/O缓冲大小、理解SO_REUSEADDR的作用于Time-wait状态,以及如何禁用Nagle算法以优化网络传输效率。

代码链接

第 9 章 套接字的多种可选项

本章将介绍更改套接字可选项的方法,并以此为基础进一步观察套接字内部。

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

我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解这些特性并根据实际需要进行更改也十分重要。

9.1.1 套接字多种可选项

我们之前写的程序都是创建好套接字后(未经特别操作)直接使用的,此时通过默认的套接字特性进行数据通信。下面列出一部分套接字可选项。

协议层选项名读取设置
SOL_SOCKETSO_SNDBUFoo
SOL_SOCKETSO_RCVBUFoo
SOL_SOCKETSO_REUSEADDRoo
SOL_SOCKETSO_KEEPALIVEoo
SOL_SOCKETSO_BROADCASToo
SOL_SOCKETSO_DONTROUTEoo
SOL_SOCKETSO_OOBINLINEoo
SOL_SOCKETSO_ERRORox
SOL_SOCKETSO_TYPEox
IPPROTO_IPIP_TOSoo
IPPROTO_IPIP_TTLoo
IPPROTO_IPIP_MULTICAST_TTLoo
IPPROTO_IPIP_MULTICAST_LOOPoo
IPPROTO_IPIP_MULTICAST_IFoo
IPPROTO_TCPTCP_KEEPALIVEoo
IPPROTO_TCPTCP_NODELAYoo
IPPROTO_TCPTCP_MAXSEGoo

从上表可以看出,套接字可选项是分层的。IPPROTO_IP 层可选项是 IP 协议相关事项,IPPROTO_TCP 层可选项是 TCP 协议相关的事项,SOL_SOCKET 层是套接字相关的通用可选项。

上表无需记忆,实际工作中逐一掌握即可。本书只介绍一部分重要的可选项含义及更改方法。

9.1.2 getsockopt & setsockopt

getsockopt 函数

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

setsockopt 函数

#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:向第四个参数 optval 传递的可选项信息的字节数。
*/

接下来介绍这些函数的调用方法。先介绍 getsockopt 函数的调用方法。下列示例用协议层为 SOL_SOCKET、名为 SO_TYPE 的可选项查看套接字类型(TCP 或 UDP)。

代码参考 sock_type.c 文件

运行结果

wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ gcc sock_type.c -o socktype.exe
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ ./socktype.exe
SOCK_STREAM: 1
SOCK_DGRAM: 2
Socket type one: 1 
Socket type two: 2

TCP 套接字获得,SOCK_STREAM 常数值是 1;

UDP 套接字获得,SOCK_DGRAM 常数值 2。

上面示例给出了调用 getsockopt 函数查看套接字信息的方法。SO_TYPE 是典型的只读可选项,因为套接字类型只能在创建时决定,以后不能再更改。

9.1.3 SO_SNDBUF & SO_RCVBUF

创建套接字将同时生成 I/O 缓冲。

SO_RCVBUF 是输入缓冲大小相关可选项,SO_SNDBUF 是输出缓冲大小相关可选项。用这 2 个可选项既可以读取当前 I/O 缓冲大小,也可以进行更改。通过下列示例读取创建套接字时默认的 I/O 缓冲大小。

代码参考 get_buf.c 文件

运行结果

wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ gcc get_buf.c -o getbuf.exe
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ ./getbuf.exe
Input buffer size: 87380 
Output buffer size: 16384

下面介绍更改 I/O 缓冲大小。

代码参考 set_buf.c 文件

运行结果

wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ gcc get_buf.c -o setbuf.exe
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ ./setbuf.exe
Input buffer size: 87380 
Output buffer size: 16384

设置缓冲区并不一定完全按照我们的要求进行,但也大致反映出了通过 setsockopt 函数设置的缓冲大小。(搞了半天缓冲区不变)

9.2 SO_REUSEADDR

本节的可选项 SO_REUSEADDR 及其相关的 Time-wait 状态很重要,务必理解并掌握。

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

在学习 SO_REUSEADDR 可选项之前,应理解好 Time-wait 状态。

阅读下列示例代码,代码参考 reuseadr_eserver.c 文件

编译运行

gcc reuseadr_eserver.c -o reuseadr_eserver.exe
./reuseadr_eserver.exe 9190

这示例是之前已实现过多次的回声服务器端,可以结合第四章介绍过的回声客户端运行。下面运行该示例,通过如下方式终止程序:

在客户端控制台输入 Q 消息,或通过 CTRL+C 终止程序

也就是说,让客户端先通知服务器端终止程序。客户端输入 Q 或 CTRL+C 都会向服务器传毒 FIN 消息。强制终止程序时,由操作系统关闭文件及套接字,此过程相当于调用 close 函数,也会向服务器端传递 FIN 消息。

看不到特殊现象

接下来实验在客户端和服务端已建立连接的状态下,向服务器端控制台输入 CTRL+C,即强制关闭服务器端。模拟了服务器端向客户端发送 FIN 消息的情景。如果以这种方式终止程序,那服务器端重新运行时将产生问题。如果用同一端口号重新运行服务端,将输出『bind() error』消息,并且无法再次运行。大约过 3 分钟即可重新运行服务器端。

为什么产生这种情况?

9.2.2 Time-wait 状态

套接字经过四次握手并非立即消除,而是经过一段时间的 Time-wait 状态。只有先断开连接的(先发送 FIN 消息的)主机才经过 Time-wait 状态,因此若服务器端先断开连接,则无法立即重新运行。套接字处在 Time-wait 过程时,相应端口是正在使用的状态。

Q:客户端套接字不会经过 Time-wait 过程吗?

A:存在,因为客户端套接字的端口号是会动态分配的,因此无需过多关注 Time-wait 状态。

Q:为什么会有 Time-wait 状态呢?

A:如果 A 主机第四次挥手 ACK 丢失,B 主机第三次挥手超时重传需要 A 主机处于开启状态。

9.2.3 地址再分配

Time-wait 看似重要,但是不一定讨人喜欢,可能需要尽快重启服务器。

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

9.3.1 Nagel 算法

详见 《TCP/IP 详解 卷一》第 19 章

为防止因数据包过多而发生网络过载,Nagle 算法在 1984 年诞生了。它应用于 TCP 层,非常简单。

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

TCP 套接字默认使用 Nagle 算法交换数据。(据说现在默认关闭,下面进行实验)

Nagle 算法并不是什么时候都适用的。如『传输大文件数据』。所以需要根据数据特性判断是否禁用 Nagle 算法。

9.3.2 禁用 Nagle 算法

参数所在头文件

#include <netinet/tcp.h>
#include <arpa/inet.h>

禁用方法如下

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

查看 Nagle 算法的设置状态方法如下

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

实验 Nagle 默认设置状态。代码参考 nagle_test.c 文件

运行结果 禁用测试将注释去掉即可

wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ gcc nagle_test.c -o nagle_test.exe
wzy@wzypc:~/TCP-IP-NetworkNote/chapter-09$ ./nagle_test.exe 9100
Nagle state: 0

opt_val 为 0 表示正在使用,1 表示已禁用。

9.4 基于 Windows 的实现

9.5 习题

以下是我的理解,详细题目参照原书

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

2

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

网络流量未受太大影响时,禁用 Nagle 算法传输速度更快,如大文件传输时。

Nagle 算法收发特性,只有接收到前一数据的 ACK 消息时,Nagle 算法才发送下一数据

### sys/socket 头文件用法及功能介绍 `<sys/socket.h>` 是 Unix/Linux 平台上的核心网络编程头文件之一,主要用于提供套接字(Socket)接口的相关声明和定义。以下是关于 `<sys/socket.h>` 的详细介绍: #### 1. 功能概述 该头文件提供了创建、配置和管理网络通信所需的核心函数和数据结构。它支持多种协议族(如 IPv4, IPv6),并允许开发者通过套接字实现客户端和服务端之间的通信。 主要功能包括但不限于: - 套接字的创建 (`socket()` 函数)[^2]。 - 绑定本地地址到套接字 (`bind()` 函数)。 - 监听传入连接请求 (`listen()` 函数)。 - 接受新的连接 (`accept()` 函数)。 - 发送和接收数据 (`send()`, `recv()` 等函数)。 #### 2. 使用场景 在网络应用开发中,`<sys/socket.h>` 被广泛用于构建服务器程序和客户端程序。例如,在 C/C++ 编程环境下编写 HTTP 服务器或 FTP 客户端时,通常需要引入此头文件以调用其提供的 API。 #### 3. 配合使用的其他头文件 为了完成完整的网络编程任务,除了 `<sys/socket.h>` 外,还需要与其他一些重要的头文件配合使用: - **`<arpa/inet.h>`**: 提供 IP 地址转换等功能 (e.g., `inet_addr`, `htonl`) [^4]。 - **`<netinet/in.h>`**: 定义 Internet 协议相关的常量和结构体 (e.g., `struct sockaddr_in`) [^4]。 - **`<unistd.h>`**: 包含通用 UNIX I/O 操作的支持 (e.g., `read`, `write`) [^3]。 #### 4. 示例代码 下面是一个简单的 TCP 服务器示例,展示了如何利用这些头文件建立基本的服务端逻辑: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> // inet_ntoa() #include <sys/socket.h> // socket(), bind(), listen(), accept() #define PORT 8080 #define MAX_CLIENTS 5 int main(void){ int server_fd, new_socket; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); char buffer[1024] = {0}; const char *hello = "Hello from server"; // 创建套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置重用选项 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt failed"); close(server_fd); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); // 将套接字绑定到指定地址 if(bind(server_fd,(struct sockaddr *)&address,sizeof(address))<0){ perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 开始监听来自客户端的连接请求 if(listen(server_fd,MAX_CLIENTS)<0){ perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); while(1){ if((new_socket=accept(server_fd,(struct sockaddr*)&address, (socklen_t*)&addrlen))<0){ perror("accept failed"); continue; } read(new_socket ,buffer,1024); printf("Client message: %s\n",buffer ); send(new_socket , hello , strlen(hello), 0); printf("Response sent to client.\n"); close(new_socket); } return 0; } ``` 上述代码片段演示了一个基础版的回显服务器,能够接受客户端发送的消息并将预设字符串返回给对方。 --- ### 关于头文件梳理的方法 针对提问中的困惑——即如何有效整理众多头文件的知识体系,可以采取以下策略: - 利用在线资源查阅官方文档或者权威书籍学习标准库的具体细节; - 实践驱动型学习方式,每次遇到新需求时主动查找对应解决方案及其依赖关系; - 构建个人笔记系统记录常见问题解决路径以及关联知识点; 具体而言,对于像 `uint32_t` 这样的类型定义可查找到位于 `<stdint.h>` 或者 `<cstdint>` 中; 对于延时操作所需的 `sleep()` 函数,则应包含 `<unistd.h>` 下才能正常使用. 至于文件描述符的操作命令比如打开(`open()`)关闭(`close())`,它们确实共享相同的来源 —— 同样出自 `<unistd.h>` 文件之中. ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值