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

在这里插入图片描述

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

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

套接字多种可选项

我们之前写的程序都是创建好套接字后(未经特别操作) 直接使用的, 此时通过默认的套机字特性进行数据通信. 之前的示例较为简单, 无需特别操作套接字特性, 但有时的的确需要更改.
在这里插入图片描述
在这里插入图片描述
从表9-1 中可以看出, 套接字可选项是分层的. IPPROTO_IP 层可选项是IP 协议相关事项, IPPROTO_TCP 层可选项是TCP协议相关的事项, SOL_SOCKET层是套接字相关的通用可选项.

也许有人看到表格会产生畏惧感, 但现在不需全部背下来或理解, 因此不必有负担. 实际能够设置可选项是表9-1的好几倍, 也无需一下子理解所有可选项, 实际工作中逐一掌握即可. 接触的可选项多了, 自然会掌握大部分重要的. 本书也只是介绍一部分重要的可选项含义及更改方法.

getsockopt & setsockopt

我们几乎可以针对表9-1中的所有可选项进行读取(Get)和设置(Set)(当然, 有些可选项只能进行一种操作). 可选项的读取和设置通过如下2个函数完成.
在这里插入图片描述
上述函数用于读取套接字可选项, 并不难. 接下来介绍更改可选项是调用的函数
在这里插入图片描述
在这里插入图片描述
接下来介绍这些函数的调用方法. 关于setsockopt 函数的调用方法在其他示例中给出, 先介绍getsockopt函数的调用方法. 下列实例用协议层为SOL_SOCKET, 名为SO_TYPE的可选项查看套接字类型(TCP或UDP).

#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_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);

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

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

运行结果:
在这里插入图片描述
上述实例给出了调用getsockopt函数查看套接字信息的方法. 另外, 用于验证套接字类型的SO_TYPE 是典型的只读可选项, 这一点可以通过下面这句话解析:
在这里插入图片描述

SO_SNDBUF & SO_RCVBUF

前面介绍过, 创建套接字将同时生成I/O缓冲. 如果各位忘了这部分内容, 可以复习第5章.接下来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;

    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 <string.h>
#include <unistd.h>
#include <sys/socket.h>

void error_hangling(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);
    state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
    if (state)
    {
        error_hangling("setsockopt() error");
    }

    state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
    if (state)
    {
        error_hangling("setsockopt() error");
    }

    len = sizeof(snd_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
    if (state)
    {
        error_hangling("getsockopt() error");
    }

    len = sizeof(rcv_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&rcv_buf, &len);
    if (state)
    {
        error_hangling("getsockopt() error");
    }

    printf("Input buffer size: %d \n", rcv_buf);
    printf("Output buffer size: %d \n", snd_buf);

    return 0;
}

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

运行结果:
在这里插入图片描述
输出结果跟我们预想的完全不同, 但也算合理. 缓冲大小的设置需谨慎处理, 因此不会完全按照我们的要求进行, 只是通过调用setsockopt 函数向系统传递我们的要求. 如果把输出缓冲区设置为0并如实反映这种设置, TCP 协议将如何进行? 如果要实现流控制和错误发生时的重传机制, 至少要有一些缓冲空间吧? 上述示例虽没有100%按照我们的请求设置缓冲大小, 但也大致反映出了通过setsockopt函数设置的缓存大小.

9.2 SO_REUSEADDR

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

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

学习SO_REUSEADDR 可选项之前, 应理解好 Time-wait 状态. 我们读完下列实例后再讨论后续内容.

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

#define TRUE 1
#define FALSE 0

void error_handling(char *message);

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

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handling("socket() error");
    }

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

    close(clnt_sock);
    close(serv_sock);
    return 0;
}

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

此示例是之前已实现多次的回声服务器端, 可以结合第4章介绍过的回声客户端运行. 下面运行该示例, 第28-30 行应保持注释状态. 通过如下方式终止程序:
在这里插入图片描述
也就是说, 让客户端先通知服务器端终止程序. 在客户端控制台输入Q消息时调用close函数(参考第4章的echo_client.c), 向服务器端发送FIN 消息并经过四次握手过程. 当然, 输入CTRL+C时也会向服务器传递FIN消息. 强调终止程序时, 由操作系统关闭文体及套接字, 此时过程相当于调用close 函数, 也会向服务器端传递FIN 消息.
在这里插入图片描述
是的, 通常都是由客户端先请求断开连接, 所有以不会发生特别的事情. 重新运行服务器端也不成问题, 但按照如下方式终止程序时则不同.
在这里插入图片描述
这主要模拟了服务器端向客户端发送FIN 消息的情景. 但如果以这种方式终止程序, 那服务器端重新运行将产生问题. 如果用同一端口号重新运行服务器端, 将输出"bind() error" 消息, 并且无法再次运行. 但在这种情况下, 再过大约3分钟即可重新运行服务器端.

上述2种运行方式唯一的区别就是谁先传输FIN消息, 但结果却不同, 原因何在呢?

Time-wait 状态

相信各位已对四次握手有了很好的理解, 先观察该过程, 如图9-1所示.
在这里插入图片描述
假设图9-1中主机A是服务器端, 因为是主机A向B发送FIN消息, 故可以想象成服务器端在控制台输入CTRL+C. 但问题是, 套接字经过四次握手过程后并非立即消除, 而是要经过一段时间的Time-wait 状态. 当然, 只有先断开连接的(先发送FIN消息的)主机才经过 Time-wait 状态. 因此, 若服务器端先断开连接, 则无法立即重新运行. 套接字处于 Time-wait 状态. 因此, 若服务器端先断开连接, 则无法立即重新运行. 套接字处于 Time-wait 过程时, 相应端口是正在使用的状态. 因此, 就像之前验证过的, bind 函数调用过程中当然会发生错误.
在这里插入图片描述
到底为什么会有 Time-wait 状态呢? 图9-1中假设主机A向主机B传输ACK消息 (SEQ 5001 , ACK 7502) 后立即消除套接字. 但最后这条ACK 消息在传递途中丢失, 未能传递给主机B, 这时会发生什么? 主机B会认为之前自己发送的FIN 消息(SEQ 7501, ACK 5001) 未能抵达主机A, 继续试图重传. 但此时主机A已是完全终止的状态, 因此主机B永远无法收到主机A最后传来的ACK消息. 相反, 若主机A的套接字处在 Time-wait 状态, 则会向主机B重传最后的ACK消息, 主机B也可以正常终止. 基于这些考虑, 先传输FIN 消息的主机应 经过Time-wait 过程.

地址再分配

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

解决方案就是在套接字的可选项中更改SO_REUSEADDR 状态. 适合调整参数, 可将 Time-wait 状态下的套接字端口号重新分配给新的套接字. SO_REUSEADDR 的默认值为0(假), 这就意味着无法分配Time-wait 状态下的套接字端口号. 因此需要将这个值改为1(真). 具体做法已在示例reuseadr_eserver.c 中给出, 只需去掉下述代码的注释即可.
在这里插入图片描述
各位是否去掉了注释? 既然服务器端reuseadr_eserver.c 已变成可随时运行的状态, 希望大家在Time-wait 状态下验证其能否重新运行.

9.3 TCP_NODELAY

我教java 网络编程时, 经常被问及如下问题:
在这里插入图片描述
我被问到这个问题时感到特别开心, 因为开发人员容易忽略的一个问题就是Nagle 算法, 下面进行讲解.

Nagle 算法

为了防止因数据包过多而发生网络过载, Nagle算法在1984年诞生了. 它应用于 应用于 TCP 层, 非常简单. 其使用与否会导致图9-3所示差异.
在这里插入图片描述
图9-3展示了通过Nagle算法发送字符串 “Nagle"和未使用Nagle算法的差别, 可以得到如下结论:
在这里插入图片描述
TCP 套接字默认使用Nagle 算法交换数据, 因此最大限度地进行缓冲, 直到收到ACK, 图9-3 左侧正是这种情况. 为了发送字符串 “Nagle”, 将其传递到输出缓冲. 这时头字符"N” 之前没有其他数据(没有需接收的ACK), 因此立即传输. 之后开始等待字符"N" 的ACK 消息, 等待缓冲的 “agle” 装入一个数据包发送. 也就是说, 共需传递4个数据包以传输1个字符串.

接下来分析未使用Nagle 算法时发送字符串 “Nagle” 的过程. 假设字符"N"到"e"依次传输到输出缓冲. 此时的发送过程与ACK 接收与否无关, 因此数据到传输出缓冲后将立即被发送出去. 从图9-3右侧可以看到, 发送字符串"Nagle" 时共需10个数据包. 由此可知, 不使用Nagle算法将对网络流量(Traffic: 指网络负载或混乱程度)产生负面影响. 即使只传输1字节的数据, 其头信息都有可能是几十个字节. 因此, 为了提高网络传输效率, 必须使用Nagle 算法.
在这里插入图片描述
但Nagle 算法并不是什么时候都适用. 根据传输数据的特性, 网络流量未受太多影响时, 不使用Nagle 算法要比使用它时传输速度快. 最经典的是 “传输大文件数据”. 将文件数据传入输出缓冲不会花太多时间, 因此, 即便不使用Nagle 算法, 也会在装满输出缓冲时传输数据包. 这不仅不会增加数据包的数量, 反而会在无需等待ACK 的前提下连续传输, 因此可以大大提高传输速度.

一般情况下, 不适用Nagle 算法可以提高传输速度. 但如果无条件放弃使用Nagle 算法, 就会增加过多的网络流量,反而会影响传输. 因此, 未准确判断数据特性时不应禁用Nagle 算法.

禁用 Nagle 算法

刚才说过的"大文件数据"应禁用Nagle 算法. 换言之, 如果有必要, 就应禁用 Nagle 算法.
在这里插入图片描述
禁用方法非常简单. 从下列代码也可看出, 只需将套接字可选项TCP_NODELAY 改为1(真)即可.
在这里插入图片描述
可以通过TCP_NODELAY 的值查看Nagle 算法的设置状态.
在这里插入图片描述
在这里插入图片描述
如果正在使用Nagle 算法, opt_val 变量中会保存0; 如果禁用Nagle算法, 则保存1.

9.4 基于 Windows 的实现

套接字可选项及其相关内容与操作系统无关, 特别是本章的可选项, 他们是 TCP 套接字的相关内容, 因此在 Windows 平台与 Linux 平台下并无区别, 接下来介绍更改和读取可选项的2个函数.
在这里插入图片描述
可以看到, 除了 optval 类型变成char 指针外, 与Linux 中的getsockopt 函数相比并无太大区别(Linux 中是void 型指针). 将Linux 中的示例移植到 Windows 时, 应做出适当的类型转换. 接下来给出 setsockopt 函数.
在这里插入图片描述
setsockopt 函数也与 Linux 版的毫无二致. 各位应更关注可选项的含义而非设置方法. 最后, 利用上述2个函数编写示例. 之前在 Linux 中验证过套接字I/O 缓冲大小, 现将其改变基于 Windows 的实现.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);
void ShowSocketBufSize(SOCKET sock);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSock;
	int sndBuf, rcvBuf, state;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error");
	}

	hSock = socket(PF_INET, SOCK_STREAM, 0);
	ShowSocketBufSize(hSock);

	sndBuf = 1024 * 3, rcvBuf = 1024 * 3;
	state = setsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, sizeof(sndBuf));
	if (state == SOCKET_ERROR)
	{
		ErrorHandling("setsockopt() error");
	}

	state = setsockopt(hSock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvBuf, sizeof(rcvBuf));
	if (state == SOCKET_ERROR)
	{
		ErrorHandling("setsockopt() error");
	}

	ShowSocketBufSize(hSock);
	closesocket(hSock);
	WSACleanup();
	return 0;
}

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

void ShowSocketBufSize(SOCKET sock)
{
	int sndBuf, rcvBuf, state, len;

	len = sizeof(sndBuf);
	state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, &len);
	if (state == SOCKET_ERROR)
	{
		ErrorHandling("getsockopt() error");
	}

	len = sizeof(rcvBuf);
	state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvBuf, &len);
	if (state == SOCKET_ERROR)
	{
		ErrorHandling("getsockopt() error");
	}

	printf("Input buffer size: %d \n", rcvBuf);
	printf("Output buffer size: %d \n", sndBuf);
}

运行结果:
在这里插入图片描述
系统不同可能导致不同结果. 但可以通过上述示例获得系统默认I/O缓冲大小, 同时也可以得到更改之后实际使用的缓存大小.

结语:

你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/

时间: 2020-06-01

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值