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

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

1.套接字多种可选项

之前的程序都是创建好套接字直接使用,此时通过套接字的默认特性进行数据通信。之前示例简单,无需特别操作套接字特性,但有时确实需要更改。

多种可选项

  • 套接字选项分层
  • IPPROTO_IP层是IP相关协议事项
  • IPPROTO_TCP层是TCP协议相关事项
  • SOL_SOCKET层是套接字相关通用可选项

只介绍一些重要可选项和更该方法

2.getsockopt & setsockopt

可以对表中几乎所有可选项进行读取(GET)和设置(SET) 读取可选项信息

#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传递缓冲大小,调用后保存查看结果(可选项信息)的字节数

设置可选项信息

#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
#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);//生成TCP套接字
	udp_sock=socket(PF_INET, SOCK_DGRAM, 0);//生成UDP套接字
	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);
}

3.SO_SNDBUF & SO_RCVBUF

  • SO_RCVBUF是输入缓冲大小相关可选项
  • SO_SNDBUF是输出缓冲大小相关可选项 用这两个可选项既可以读取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("Outupt 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);
	state=setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
	if(state)
		error_handling("setsockopt() error!");
	
	state=setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
	if(state)
		error_handling("setsockopt() error!");
	
	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);
}

运行可发现输出结果和设置的并不相同。我们只是通过setsockopt函数向系统传递我们想要的需求,并不是强制系统更改,毕竟用户可能会有非法操作,假设缓冲区设置为0,肯定会出问题吧。

9.2 SO_REUSEADDR(和Time——wait有关)

1.发生地址分配错误

#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");
	/*
	optlen=sizeof(option);
	option=TRUE;	
	setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &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)))
		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);
	return 0;
}

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

以上是回声服务端,客户端输入Q或者CTRL+C可以终止程序。 对客户端来说,当客户端输入Q,调用close函数会向服务端传递FIN消息,并四次握手。CTRL+C也会向服务端传递FIN消息。强制终止程序,操作系统关闭套接字,此过程相当于调用close,也会向服务端传递FIN消息

一般也看不出什么特殊现象,因为一般是客户端先请求断开连接。重新运行服务器也不会出现问题。 对服务端来说但是如果在服务端客户端建立连接情况下向服务端控制台输入CTRL+C,也就是强制关闭服务器,模拟了服务端向客户端发送FIN的情形。这种方式终止程序,如果用同一端口号重新运行服务端会出现问题——地址分配错误,再过几分钟才可以重新运行。 上面的两种方式只是谁先传输FIN的区别但是差异巨大

2.Time-wait状态

主动先发送FIN的套接字经过四次握手后并非立即清除,而是经过一段时间的Time-wait状态,因此服务端先断开连接不能立即重新运行,因为套接字处于Time-wait状态,相应的端口正在使用。所以会出现地址分配错误消息。

不管是服务器还是客户端,只要主动断开连接,套接字都会有Time-wait状态,
但是客户端的套接字基本是任意指定的(也就是动态分配的),无需太关注Time-wait状态。

为什么会有Time-wait状态?

假设主机A向主机B最后发送的ACK消息未能到达主机B,主机B会认为之前传输的FIN消息未能到达主机A,
所以会不断试图重传,假设没有Time-wait状态,主机A已经完全终止,永远无法发送ACK给主机B,
主机B就无法正常终止。所以主动断开连接的一方需要Time-wait状态,防止出现以上情况。

3.地址再分配

但是如果系统故障紧急停止,需要尽快重启服务器提供服务,但是因为Time-wait状态无法立即重启,可能会导致巨大问题。 假设主机A四次握手最后的消息丢失,B会认为之前的消息未被A收到,会重传,A收到后又会发送ACK,启动Time-wait计时器,如果网络质量不佳,这种状态会一直持续下去。

解决方案 在套接字可选项中更改SO_REUSEADDR的状态。调整该参数,可以将Time-wait状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR默认为0(false)代表无法分配Time-wait状态下的套接字端口号,所以要改成true。只需把前面的代码注释去掉。

9.3 TCP_NODELAY

1.Nagle算法

TCP默认使用Nagle算法,假设发送“Nagle”消息,N先到达输出缓冲前面没数据,立即发送,之后开始等待‘N’的ACK消息,这时剩下的字符“agle”已经填入输出缓冲。收到ACK后,将“agle”装入一个数据包发送。这时传送端和接收端一共用了4个数据包传输一个字符串。

没使用的情况下,假设字符依次到达缓冲区。此时发送过程与ACK无关,到达输出缓冲立即被发送,这时就需要10个数据包,这会对网络流量产生巨大负面影响,即使只有一个字符,其头信息也是十几个字节。所以必须使用Nagle算法。

在程序中,字符串传给输出缓冲并非逐字节传输,实际情况并非图中那样。
但是每隔一段时间,再把字符串的字符传输到输出缓冲,可以出现类似图中的情况。

并不是所有时候都适合Nagle算法,在网络流量未受太大影响,不使用Nagle算法要比使用的时候更快。例如“传输大文件”。将文件数据传入输出缓冲不会花费太多时间,所以不使用Nagle算法,也会在装满输出缓冲时发送数据包。这不仅不会增加数据包数量,反而在无需等待ACK的情况下连续传输,因此可以大大提高传输速度。

虽然不使用Nagle算法会提高传输速度。但是无条件放弃使用,会导致过多网络流量,反而会导致传输受阻。因此未准确判断数据特性的情况下,不应该禁用。

2.禁用Nagle算法

只需要将TCP_NODELAY设置为1(真)

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

可以查看设置状态

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

正在使用opt_val为0;禁用的话opt_val为1

9.4基于Windows的实现

#include<winsock2.h>
int getsockopt(SOCKET sock,int level,int optname,char * optval,int * optlen);

int setsockopt(SOCKET sock,int level,int optname,char *optval,int optlen);

成功返回0,失败返回SOCK_ERROR

参数含义相同与Linux


本文章使用limfx的vscode插件快速发布