Part 1 start network programming:chapter nine:套接字的多种可选项

第九章:套接字的多种可选项

之前的套接字都是用的同一些参数并没有进行一些更改,这里介绍套接字具有的多种特性。
同时更细致得观察套接字内部。

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

套接字编程过程中,关注的重点往往是数据通信过程,而套接字的不同特性也是十分重要的。

9.1.1 套接字多种可选项

之前的实例在创建套接字时几乎使用的都是默认参数,没有进行更改,但其实是可以更改的,下面的图标中展示了其中一部分套接字可选项。
在这里插入图片描述在这里插入图片描述
上表中可以看出,套接字可选项是分层的。
IPPROTO_IP 层是IP协议相关事项。
IPPROTO_TCP层TCP协议相关的事项。
SOL_SOCKET层是套接字相关的通用可选项。

实际上可选项比上表多很多,但是不是都要背下来的,用的时候去查就行啦,用的多就记住了。下面挑选一些进行讲解。

9.1.2 getsockopt & setsockopt

我们几乎可以针对上表中的所有选项进行读取(get)和设置(set)操作。
可选择项的读取和设置通过下面两个函数完成。

第一个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传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。

state1 = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
太抽象太抽象,来个例子。
下面的这个例子中,用协议层为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[])
{
	// define some variables
	int tcp_sock, udp_scok;
	int sock_type;
	socklen_t optlen;
	int state1, state2;

	//define socket
	optlen = sizeof(sock_type);
	tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
	udp_scok = socket(PF_INET, SOCK_DGRAM, 0);

	// what is the keyword?
	printf("SOCK_STREAM : %d\n", SOCK_STREAM);
	printf("SOCK_DGRAM: %d\n", SOCK_DGRAM);

	state1 = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
	if(state1){
		error_handling("getsockopt() error");
	}

	printf("Socket type one : %d \n", sock_type);

	state2 = getsockopt(udp_scok, SOL_SOCKET, SO_TYPE, (void*)&sock_type,&optlen);
	if(state2){
		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);
}

下面是测试结果在这里插入图片描述
用于验证套接字类型的SO_TYPE是典型的只读可选项,套接字类型只能在创建时决定,不能再更改

上面是读取套接字可选项的函数,下面是更改可选项时调用的函数。

#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 传递的可选项信息的 字节数。(与第四个配套的,但是改了结果不会变)

这部分的实例和后面再一起。
总结一下上面的参数含义
sock:将要被设置或者获取选项的套接字。
level:选项所在的协议层。
optname:需要访问的选项名。
optval:对于getsockopt(),指向 存放get得到的值 的地址。对于setsockopt(),指向存放 想要修改的新数值 的地址 。
optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),设置的。

9.1.3 SO_SNDBUF & SORCVBUF(关于IO缓冲的可选项)

第五章中介绍了 创建套接字的同时会生成 I/O缓冲。
SO_RCVBUF是输入缓冲大小相关可选项,SO_SNDBUF是输出缓冲大小相关可选项。
这两个选项既可以读取当前I/O缓冲大小,也可以进行更改。

下面示例读取创建套接字时默认的缓冲大小。

get_buf.c

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

在这里插入图片描述

结果非常简单,输入缓冲大小 87380字节,输出缓冲大小 32767 字节

下面是更改I/O缓冲 大小的程序

set_buf.c

#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,state;
	socklen_t len;

	sock = socket(PF_INET, SOCK_STREAM, 0);
	len = sizeof(snd_buf);
	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函数中 optvaloptlen竟然可以不匹配。 尝试修改之后,发现决定性的参数还是 optval,这个数字决定了我们修改后的 缓冲的大小。

9.2 SO_REUSEADDR(重用地址)

本节主要讲解SO_REUSEADDR及其相关的 Time-wait 状态。

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

我们先从下面这段代码中理解一下 什么是Time-wait 状态。
下面这段代码并不是全新的代码,套路都差不多,其实就是从回声服务器端的代码修改来的。 请认真再看一次。

reuseaddr_eserver.c

#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
#define BUF_SIZE 1024
void error_handling(char* message);

int main(int argc, char* argv[])
{
	int ser_sock , client_sock;
	char message[BUF_SIZE];
	int str_len;

	struct sockaddr_in serv_addr, client_addr;	
	socklen_t client_addr_size;

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

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

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

	memset(&serv_addr,0,sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));


	if(bind(ser_sock, (struct sockaddr*)& serv_addr,sizeof(serv_addr)) == -1){
		error_handling("bind() error");
	}

	if(listen(ser_sock,5) == -1){
		error_handling("listen error");
	}

	client_addr_size = sizeof(client_addr);

	client_sock = accept(ser_sock, (struct sockaddr*)&client_addr, &client_addr_size);
	if(client_sock == -1){
		error_handling("accept error");
	}
	else{
		printf("connected ok!\n");
	}
	

	while((str_len = read(client_sock, message, BUF_SIZE)) != 0)
	{
		write(client_sock, message, str_len);
		//write(1, message, str_len);
	}
	close(client_sock);						// 在for中 如果while接收到 客户端不在发送内容,就会关闭这个客户端套接字连接
	
	close(ser_sock);
	

	return 0;
}

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

客户端代码:

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

#define BUF_SIZE 1024
void error_handling(char* message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;
	if(argc != 3){
		printf("Usage %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	// 为客户端分配套接字
	sock = socket(PF_INET, SOCK_STREAM, 0); 
	if(sock == -1){
		error_handling("socket () error ");
	}
	// 初始化服务器端的地址信息  这个信息是从main函数的参数中读取的
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = inet_addr(argv[1]);		//字符串形式的 本地字节序转为网络字节序
	serv_adr.sin_port = htons(atoi(argv[2]));


	if(connect(sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1){
		error_handling("connect()  error");
	}
	else{
		puts("Connected...... ");
	}
	
	while(1)
	{
		fputs("Input message(Q to quit):", stdout);
		fgets(message, BUF_SIZE, stdin);

		if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")){
			break;
		}
		
		write(sock, message, strlen(message));
		/*
	 	 *  fd:显示数据接受对象的文件描述  buf:要保存的数据的缓冲地址值  nbytes:要接收数据的最大字节数
	 	 *  ssize_t read(int fd, void* buf,size_t nbytes);
	 	 *  成功时返回接收的字节数,(但遇到文件结尾则返回0),失败返回-1
	 	 */
		
		str_len = read(sock,message,BUF_SIZE - 1);
		message[str_len] = 0;
		printf("Message from server: %s", message);
	}

	close(sock);
	return 0;
}

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

测试结果见 第四章图片。

这里和以前可能没什么区别,但是我们尝试一下,如果先用 ctrl+c断开服务器的连接,再断开客户端,会发生什么?

当我们再次启用服务器端 的时候,如果我们采用相同的端口号, 就会报错bind() error

这是为什么呢?

这样的顺序就还是模拟了服务器端向客户端发送 FIN 消息的情景。 如果以这种方式终止程序 ,大约在3 分钟之后,才能使用相同的端口号进行运行服务器端。

9.2.2 Time-wait 状态

下图为 之前说过的四次握手的流程图。
在这里插入图片描述
其中 A为服务器端, B为客户机端。

图中的顺序是 A向B发送 FIN消息,相当于在服务器端控制台输入了 ctrl + c 。
从图中可以看到,套接字经历了4次握手之后,没有立刻清除。而是经过了一段 Time-wait 状态。

只有先断开连接的(先发送FIN消息的)主机才会经过 Time-wait状态

因此套接字没有清除,所以端口号还是被使用的状态。当然如果再次使用这个端口号就会报错。

注意: 所有先断开连接的套接字都会经历Time-wait 过程。但是客户端不需要考虑,因为每次运行程序时都会动态分配端口号,因此无需过多关注Time-wait状态。

那为什么要有这个 Time-wait 状态呢??

假设上图中A向B传输的最后一条消息ACK肚子,B会一直重复发送上一条FIN消息,然而这时A已经断了,B永远无法接收到最后一条ACK; 如果A处于 Time-wait状态,就会重新向B传输最后的消息, 主机B也就是可以正常终止。

9.2.3 地址再分配

标题的意思是,我们想要把Time-wait状态取消,让这个地址下的端口号可以重新使用。

因为在系统发生故障时,需要尽快开启服务器,Time-wait 的时间可能造成很大的损失。
下图是一个延长Time-wait过程的情况。
在这里插入图片描述
如上图,如果最后的数据丢失,B认为A未能收到自己的FIN消息, 重新传输会让A重新开启 Time-wait 计时器,因此如果网络不理想,这个状态可能还会继续持续下去。。。

解决方法就是 用 setsockopt 函数,控制可选项,改变 SO_REUSEADDR 的状态,调整改参数,能使 Time-wait状态下的套接字端口号中心分配给新的套接字。

参数 SO_REUSEADDR 的默认为:0
我们给改为: 1
就ok啦~

代码在上面服务器代码中 给出了, 只要去掉注释~

int option = TURE;
int optlen = sizeof(option);
setsockopt(ser_sock, SOL_SOCKET, SO_REUSEADDR, (void*)& option,optlen);

9.3 TCP_NODELAY(NO delay)

9.3.1 Nagle算法

TCP套接字默认使用Nagle算法。
先看图
在这里插入图片描述
使用Nagle算法: 只有收到前一个数据的 ACK消息时,Nagle算法才发送下一个数据。

未使用Nagle算法:数据到达输出缓冲后将立即被发送出去,上图是极端情况,产生了10个数据包。

不使用会对网络流量产生负面影响。为了提高网络传输效率,要使用Nagle算法。

但并非什么时候都是用,根据传输数据的特性,网络流量未受太大影响时,不用Nagle算法传输速度要更快。
最典型的是“传输大文件数据”, 文件数据传入输出缓冲很快,即使不使用nagle算法,也会在装满输出缓冲时传输数据包。
从这句话我们可以看出,Nagle算法的目的是为了减少通信带来的数据包增加的情况,但是如果像上面这样的情况下,同样可以瞬间填满输出缓冲,进而直接传输数据包,也不会花费什么数据包的通信流量。

综上: 一般情况下,不选择Nagle算法可以提高传输速度,但是为准去判断数据特性时,不应该禁用。

9.3.2 禁用Nagle算法

只要将套接字选项 TCP_NODELAY 改为1 即可

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

同样也可以查看~

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

如果正在使用Nagle算法,opt_val中保存0,如果禁用了 则保存1
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值