《TCP IP网路编程》第九章

news2024/11/28 20:43:05

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

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

        从表中可以看出,套接字可选项是分层的。

  • IPPROTO_IP 可选项是IP协议相关事项

  • IPPROTO_TCP 层可选项是 TCP 协议的相关事项

  • SOL_SOCKET 层是套接字的通用可选项。

        可选项的读取和设置通过以下两个函数来完成:getsockopt & setsockopt

#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使用方法:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int tcp_sock, udp_sock;
    int sock_type;
    socklen_t optlen;
    int state;

    optlen = sizeof(sock_type);
    //创建TCP和UDP套接字
    tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
    udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
    printf("SOCK_STREAM: %d\n", SOCK_STREAM);
    printf("SOCK_DGRAM: %d\n", SOCK_DGRAM);
    // 获取TCP套接字的类型,并将其存储在sock_type变量中
    state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
    if (state)
        error_handling("getsockopt() error");
    printf("Socket type one: %d \n", sock_type);
    // 获取UDP套接字的类型,并将其存储在sock_type变量中
    state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
    if (state)
        error_handling("getsockopt() error");
    printf("Socket type two: %d \n", sock_type);
    return 0;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

        运行结果:       

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

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

        SO_SNDBUF & SO_RCVBUF:

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    int snd_buf, rcv_buf, state;
    socklen_t len;
    // 创建TCP套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    len = sizeof(snd_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
    if (state)
        error_handling("getsockopt() error");

    len = sizeof(rcv_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
    if (state)
        error_handling("getsockopt() error");

    printf("Input buffer size: %d \n", rcv_buf);// 打印接收缓冲区大小
    printf("Output buffer size: %d \n", snd_buf);// 打印发送缓冲区大小

    return 0;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

运行结果:

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

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock; // 套接字描述符
    int snd_buf = 1024 * 3, rcv_buf = 1024 * 3; // 初始化发送缓冲区大小和接收缓冲区大小
    int state; // 状态变量
    socklen_t len; // 用于存储选项的长度变量

    sock = socket(PF_INET, SOCK_STREAM, 0); // 创建TCP套接字

    // 设置接收缓冲区大小
    len = sizeof(rcv_buf);
    state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));
    if (state)
        error_handling("setsockopt() 错误");

    // 设置发送缓冲区大小
    state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
    if (state)
        error_handling("setsockopt() 错误");

    // 获取设置后的发送缓冲区大小
    len = sizeof(snd_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
    if (state)
        error_handling("getsockopt() 错误");

    // 获取设置后的接收缓冲区大小
    len = sizeof(rcv_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
    if (state)
        error_handling("getsockopt() 错误");

    printf("输入缓冲区大小: %d \n", rcv_buf); // 打印接收缓冲区大小
    printf("输出缓冲区大小: %d \n", snd_buf); // 打印发送缓冲区大小

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

运行结果:

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

SO_REUSEADDR:

        在学习 SO_REUSEADDR 可选项之前,应该好好理解 Time-wait 状态。看以下代码的示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

#define TRUE 1
#define FALSE 0

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock; // 服务器套接字和客户端套接字
    char message[30]; // 存储接收和发送的消息
    int option, str_len; // 选项变量和接收数据的长度
    socklen_t optlen, clnt_adr_sz; // 选项长度变量和客户端地址结构长度
    struct sockaddr_in serv_adr, clnt_adr; // 服务器地址和客户端地址结构

    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    // 创建TCP套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error");

    /*
    // 可选:设置SO_REUSEADDR选项,用于端口复用
    optlen = sizeof(option);
    option = TRUE;
    setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);
    */

    // 初始化服务器地址结构
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有网络接口
    serv_adr.sin_port = htons(atoi(argv[1])); // 指定监听的端口号

    // 绑定服务器套接字到指定端口
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    // 开始监听客户端连接
    if (listen(serv_sock, 5) == -1)
        error_handling("listen error");

    clnt_adr_sz = sizeof(clnt_adr);

    // 接受客户端连接请求,创建客户端套接字
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);

    // 进入循环,接收客户端发送的消息并回复
    while ((str_len = read(clnt_sock, message, sizeof(message))) != 0)
    {
        // 将接收到的消息回复给客户端
        write(clnt_sock, message, str_len);

        // 在服务器端打印收到的消息
        write(1, message, str_len); // 1代表标准输出
    }

    // 关闭客户端套接字和服务器套接字
    close(clnt_sock);
    close(serv_sock);

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

        这是一个回声服务器的服务端代码,可以配合第四章的 echo_client.c 使用,在这个代码中,客户端通知服务器终止程序。在客户端控制台输入 Q 可以结束程序,向服务器发送 FIN 消息并经过四次握手过程。当然,输入 CTRL+C 也会向服务器传递 FIN 信息。强制终止程序时,由操作系统关闭文件套接字,此过程相当于调用 close 函数,也会向服务器发送 FIN 消息。

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

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

        如果用这种方式终止程序,如果用同一端口号再次运行服务端,就会输出「bind() error」消息,并且无法再次运行。但是在这种情况下,再过大约 3 分钟就可以重新运行服务端。    

        运行结果:

    

        上述2种运行方式唯一的区别就是谁先传输FIN消息,但结果却迥然不同。观察下图:

   

        假设图中主机 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 状态看似重要,但是不一定讨人喜欢。如果系统发生故障紧急停止,这时需要尽快重启服务起以提供服务,但因处于 Time-wait 状态而必须等待几分钟。因此,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 给出,只需要把注释掉的东西解除注释即可。

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

TCP_NODELAY:

        为了防止因数据包过多而发生网络过载,Nagle 算法诞生了。它应用于 TCP 层。它是否使用会导致如图所示的差异:

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

        TCP 套接字默认使用 Nagle 算法交换数据,因此最大限度的进行缓冲,直到收到 ACK 。左图也就是说一共传递 4 个数据包以传输一个字符串。从右图可以看出,发送数据包一共使用了 10 个数据包。由此可知,不使用 Nagle 算法将对网络流量产生负面影响。即使只传输一个字节的数据,其头信息都可能是几十个字节。因此,为了提高网络传输效率,必须使用 Nagle 算法。

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

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

禁用 Nagle 算法应该使用:

//将套接字可选项TCP_NODELAY改为1(真)
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.


习题:

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

        Nagle算法是一种流控制算法,它通过在发送数据时进行数据包的延迟,以尝试优化网络传输效率。它的原理是将多个较小的数据包组合成一个较大的数据包发送,从而减少网络上的传输次数,节省网络带宽和降低网络负载。虽然Nagle算法对某些应用场景非常有效,但在某些情况下,它可能会引入显著的传输延迟,这时候可以考虑禁用Nagle算法。

当应该考虑禁用Nagle算法呢?

  1. 低延迟应用:对于某些实时应用,如实时游戏、视频通话或实时金融交易等,需要尽可能减少数据包的传输延迟,因为即时响应性是非常重要的。禁用Nagle算法可以立即发送数据,而不需要等待数据包组合。

  2. 小数据包传输:对于只包含少量数据的小数据包传输,Nagle算法可能会导致数据包被延迟发送,从而引入不必要的延迟。禁用Nagle算法可以确保这些小数据包能够及时发送,减少传输延迟。

  3. 交互式应用:某些交互式应用,如SSH(Secure Shell)会话或远程桌面连接,需要实时的用户输入和输出。禁用Nagle算法可以确保用户输入的及时传输和命令的实时响应。

        需要注意的是,禁用Nagle算法可能会导致网络拥塞,因为会增加网络上的传输次数。因此,在选择禁用Nagle算法时,需要确保网络负载不会过重,并且明确知道该设置符合特定应用场景的要求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/779741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[java安全]TemplatesImpl在Shiro550反序列化

文章目录 【java安全】TemplatesImpl在Shiro550反序列化Shiro的原理Shiro反序列化产生演示攻击过程payload使用key加密 构造不含数组的GadGets简单调用链 改造cc6为CommonsCollctionsShiro完整POC触发Shiro550漏洞进阶POC总结 【java安全】TemplatesImpl在Shiro550反序列化 Sh…

论文工具——写论文好用的绘图工具(甘特图+流程图+网络模型图+泳道图)

文章目录 引言正文手动画图的在线画图工具tldraw开源免费ProcessOnDraw.io 网络模型图工具NN-SVG设置参数自动生成Netron上传模型自动生成PlotNeuralNet编码生成 总结 引言 在写HiFi-GAN论文的代码阅读过程中&#xff0c;我发现仅仅通过文字来描述网络结构&#xff0c;不够详细…

ORB-SLAM2学习笔记4之KITTI开源数据集运行ORB-SLAM2生成轨迹并用evo工具评估轨迹

文章目录 0 引言1 KITTI数据集1.1 下载数据1.2 真值轨迹格式转换 2 单目ORB-SLAM22.1 运行ORB-SLAM22.2 evo评估轨迹(tum格式)2.2.1 载入和对比轨迹2.2.2 计算绝对轨迹误差 3 双目ORB-SLAM23.1 运行ORB-SLAM23.2 evo评估轨迹(kitti格式)3.2.1 载入和对比轨迹3.2.2 计算绝对轨迹…

Linux服务器安装部署MongoDB数据库 - 无公网IP远程连接「内网穿透」

文章目录 前言1.配置Mongodb源2.安装MongoDB数据库3.局域网连接测试4.安装cpolar内网穿透5.配置公网访问地址6.公网远程连接7.固定连接公网地址8.使用固定公网地址连接 前言 MongoDB是一个基于分布式文件存储的数据库。由 C 语言编写&#xff0c;旨在为 WEB 应用提供可扩展的高…

升级dubbo3方案

dubbo3 新特性 1. Dubbo3 应用级服务发现设计 显著降低服务发现过程的资源消耗&#xff0c;包括提升注册中心容量上限、降低消费端地址解析资源占用等&#xff0c;使得 Dubbo3 框架能够支持更大规模集群的服务治理&#xff0c;实现无限水平扩容。适配底层基础设施服务发现模型…

【VScode/VS】解决头文件路径问题

vs 中明明包含了头文件所在路径&#xff0c;但是却找不到头文件 首先&#xff0c;将要添加的压缩包解压&#xff0c;放在任意一个盘里&#xff0c;注意&#xff0c;我们在代码里要添加的头文件路径是 #include <tensorflow/c/c_api.h> 接下来我们要添加在VS中的所有路径…

CHI协议保序之trans order保序

一致性系统中&#xff0c;使用三种保序方式&#xff1b; Transaction ordering □ 除了 comp response 来规定 RN 发出的 requeset 的执行顺序之外&#xff0c;还有一种 order 机制来定义RN<->HN,HN<->SN 之间&#xff0c;命令执行的顺序&#xff1a; □ 该机制通…

C语言编程---案例练习

文章目录 格式化输出 格式化输出 %d&#xff0c;输出整数&#xff1b; %f&#xff0c;输出浮点数&#xff1b;%.3f 保留三位小数&#xff1b; %e&#xff0c;输出双精度浮点数&#xff1b; %c&#xff0c;输出单个字符&#xff1b;将字符格式化%d&#xff0c;即转ASCII码&…

解决JMeter+Grafana+influxdb 配置出现transaction无数据情形

问题描述 JMeterGrafanainfluxdb 配置时&#xff0c;Darren洋发现jmeter中明明已经配置好了事务条件以及接口实例信息&#xff0c;但就是在grafana的头部导航栏中的transaction按钮下来没有相应事务数据信息&#xff0c;经过相关资料查询&#xff0c;Darren洋发现执行以下两个步…

EAP系统如何助力光伏制造行业实现数据采集和控制的自动化?

光伏制造行业作为清洁能源领域的重要组成部分&#xff0c;随着市场的扩大和技术的进步&#xff0c;对生产效率、产品质量和成本控制的要求也越来越高。在这个竞争激烈的行业中&#xff0c;企业需要寻求自动化解决方案来提高生产效率和降低人工成本。 图.光伏面板生产&#xff0…

155.最小栈-C++

题目来源&#xff1a;力扣 题目描述&#xff1a; 设计一个支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初始化堆栈对象。 void push(int val) 将元素val推入堆栈。 void pop() 删除堆栈顶部…

数据库redis作业

数据库redis作业 redis9种数据类型的基本操作 redis持久化&#xff1a;分别启用rdb和aof&#xff0c;并查看是否有对应文件生成 作业1&#xff1a;redis9种数据类型的基本操作 1、key操作 key * #查询所有的key keys *exists 参数 #参数&#xff1a;key #判断该key是否存…

网络安全防御篇之安全问题及防火墙简介

网络安全常识及术语 网络的脆弱性 什么样的网络是安全的

20230723将红米redmi note 5 pro由默认的12小时显示修改为24小时显示

20230723将红米redmi note 5 pro由默认的12小时显示修改为24小时显示 2023/7/23 18:51 redmi note 5 pro 24小时 显示 https://jingyan.baidu.com/article/95c9d20dae4c42ad4e7561e7.html 红米手机24小时制怎么设置 播报文章 原创|浏览&#xff1a;169|更新&#xff1a;2021-02…

【网络】应用层——协议定制 | 序列化和反序列化 | 初识http

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《网络》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 在前面本喵已经带大家见识过了scoket网络通信的样子&#xff0c;现在开始深入学习网络的原理&#xff…

华为OD机试真题 Java 实现【经典屏保】【2023 B卷 100分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、补充说明四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、再输入4、再输出 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&…

【微信小程序】使用iView组件库的ActionSheet组件实现底部选择功能

效果1 效果2 要在微信小程序中使用iView组件库的ActionSheet组件&#xff0c;可以按照以下步骤进行&#xff1a; 首先&#xff0c;确保已经引入了iView组件库的样式和脚本文件。可以在app.wxss中引入iView的样式文件&#xff1a; import "/path/to/iview/weapp/dist/sty…

RocketMQ深入分析

RocketMQ深入分析 1. 消息存储 目前的MQ中间件从存储模型来&#xff0c;分为需要持久化和不需要持久化的两种模型&#xff0c;现在大多数的是支持持久化存储的&#xff0c;比如ActiveMQ、RabbitMQ、Kafka、RocketMQ&#xff0c;ZeroMQ却不需要支持持久化存储而业务系统也大多…

Upgrading kubeadm clusters from v1.27.3 to v1.27.4

文章目录 1. Before you begin2. Notes3. Master3.1 Login into the first node and upgrade the kubeadm tool only3.2 Verify the upgrade plan3.3 Drain the control plane node3.4 kubeadm upgrade3.5 Uncordon the control plane node3.6 Upgrade kubelet and kubectl3.7 …

蓝桥杯专题-真题版含答案-【饮料换购】【方格填数】【四平方和】【垒骰子_递归】

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…