TCP/IP网络编程 (九):套接字的多种可选项

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

套接字多种可选项

之前的程序创建好套接字之后是直接使用的,此时通过默认的套接字特性进行数据通信。

示例有时需要特别操作套接字特性,就要更改一些可选项。



套接字可选项分层:

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

IPPROTO_TCP层是TCP协议相关的事项

SOL_SOCKET层是套接字相关的通用可选项



函数getsockopt & setsockopt

getsockopt函数读取套接字可选项:


setsockopt更改可选项:



getsockopt()调用方法及其作用:

/* 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)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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);
	//输出创建套接字时传入的SOCK_STREAM,SOCK_DGRAM.
	
	/* getsockopt()获取套接字信息
	 * TCP套接字将获得SOCK_STREAM常数值1,UDP套接字将获得SOCK_DGRAM常数值2 */
	state = getsockopt(tcp_sock,SOL_SOCKET,SO_TYPE,(void*)&sock_type,&optlen);  //SO_TYPE表示查看套接字的类型
	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;
}

运行结果:./sock_type



可选项SO_SNDBUF & SO_RCVBUF

创建套接字将同时生成I/O缓冲。SO_RCVBUF是输入缓冲大小可选项。SO_SNDBUF是输出缓冲大小可选项。

用这两个可选项既可以读取当前I/O缓冲大小,也可以进行更改。


通过以下示例读取创建套接字时默认I/O缓冲大小:

/* 读取I/O缓冲大小 */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
void error_handling(char *message)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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);		//SO_SNDBUF代表读取输出(发送)缓冲区
	if (state)
		error_handling("getsockopt() error!");

	len	  = sizeof(rcv_buf);
	state = getsockopt(sock,SOL_SOCKET,SO_RCVBUF,(void*)&rcv_buf,&len);		//SO_RCVBUF代表读取输入(接收)缓冲区
	if (state)
		error_handling("getsockopt() error!");

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

运行结果: ./get_buf



下例setsockopt()函数改变I/O缓冲大小:

/* setsockopt()设置套接字一些特性
 * 本例设置I/O缓冲大小
 * */

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
void error_handling(char *message)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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));
							//设置输入缓冲长度为rcv_buf:3
	if (state)
		error_handling("setsockopt() error!");


	state = setsockopt(sock,SOL_SOCKET,SO_SNDBUF,(void*)&snd_buf,sizeof(snd_buf));
							//设置输出缓冲长度为snd_buf:3
	if (state)
		error_handling("setsockopt() error!");

	len = sizeof(snd_buf);
	state = getsockopt(sock,SOL_SOCKET,SO_SNDBUF,(void*)&snd_buf,&snd_buf);
	if (state)
		error_handling("getsockopt() error!");

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

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

运行结果:

(并不完全按照我们的要求进行)



SO_REUSEADDR

可选项SO_REUSEADDR及其相关的Time_wait状态很重要!


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


/* 回声服务器端 */
#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)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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,(void*)&option,optlen);    //使套接字在Time-wait状态下的端口号可以重新分配给新的套接字
	 */

	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是标准输出,将message中的消息输出
	}
	close(clnt_sock);
	close(serv_sock);
	return 0;
}

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

(调用close函数,向服务器端发送FIN消息并经过四次握手过程)


通常都是由客户端先请求断开连接,这种情况重新运行服务器端不成问题。


若是在服务器端控制台输入CTRL+C,强制关闭服务器端(模拟服务器端向客户端发送FIN消息),服务器端重新运行会产生问题,用同一端口运行服务器端,将输出bind() error的消息。


上述两种情况唯一的区别是谁先传输了FIN消息,结果却迥然不同,原因何在呢?


Time-wait状态

重温四次握手过程:

                            



假设A是服务器端,主机A向B发送FIN信息相当于在服务器端控制台输入CTRL+C。

套接字经过四次握手过程后并非立即删除,而是要经过一段时间的Time-wait状态。套接字处在Time-wait状态时,相应端口是正在使用的状态。因此bind()调用过程中当然会发生错误。


先端开连接的(先发送FIN消息)的主机才经过Time-wait状态。


提示:

不管是服务器端还是客户端都会有Time-wait状态。先端开连接的套接字必然会经过Time-wait过程。因为客户端套接字的端口号是任意指定的,与服务器端不同,客户端每次运行程序时都会动态分配端口号,因此无需过多关注Time-wait过程 (所以上例中从客户端结束,再运行服务器端分配同样端口号不会bind() error!)



Time-wait状态的作用:

四次握手过程中, 假设A向B发送完最后一条ACK消息后立即消除套接字:

若主机A向主机B传输最后一条ACK消息(SEQ 7501,ACK 5001)在传递途中丢失,未能传给主机B。主机B没收到确认信号,会认为之前发送的FIN消息(SEQ 7501, ACK 5001)未能抵达主机A,继而重传。若此时A已是完全终止的状态,则主机B永远无法收到从主机A最后传来的ACK消息。相反,若主机A的套接字处于Time-wait状态,则会向主机B重传最后的ACK消息。

基于这些考虑,先传输FIN消息的主机应经过Time-wait过程。



地址再分配

Time-wait有时并不那么方便。若系统发生故障从而紧急停止的情况,需要尽快重启服务器端以提供服务,但因处于Time-wait状态而必须等几分钟。因此,TIme-wait状态也有缺点。


四次握手不得不延长Time-wait过程的情况


                        

若最后的数据丢失,B会重传FIN消息,收到FIN消息的主机A将重启Time-wait计时器。因此,若网络不理想,Time-wait状态将持续。

解决方案:

     在套接字的可选项中更改SO_REUSEADDR的状态。调整该参数,可将Time-wait状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR的默认值是0,意味着无法分配Time-wait状态下的套接字端口号。将此值该为1。(如上示例代码,改动在注释中)

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

此时,处于服务器端变为可随时运行的状态。



TCP_NODELAY

TCP_NODELAY可选项属于IPPROTO_TCP协议层 (头文件是arpa/inet.h)

而要使用TCP_NODELAY可选项,必须包含头文件(netinet/tcp/h)


Nagle算法

Nagle算法防止因数据包过多而发生网络过载。应用与TCP层。是否使用的差异如下图:

                


结论:只有收到前一条数据的ACK消息时,Nagle算法才发送下一数据!


TCP套接字默认使用Nagle算法交换数据,最大限度地进行缓冲,直到收到ACK。


左侧:为了发送Nagle,将其传递到缓冲区。因为头字符N之前没有其他数据(没有需接收的ACK),因此立即传输。等待收到了N的ACK消息(等待过程中agle填入输出缓冲),收到ACK后,将输出缓冲中的agle装入一个数据包发送。    整个过程只用了4个数据包。


右侧:N到e字符依次传到输出缓冲,发送过程与ACK接收与否无关。数据到达缓冲后立即被发送出uq。整个过程用了10个数据包。    因此不使用Nagle算法将对网络流量产生负面影响。


因此,为了提高网络传输效率,必须使用Nagle算法。


提示:上图过程分析是极端情况,实际程序中将字符传给输出缓冲时并不是逐字传递的。



有些情况下Nagle算法也不适用:传输大文件数据时,将文件数据传入输出缓冲不会花太多时间,因此,即便不使用Nagle算法,也会装满输出缓冲时传输数据包。不使用Nagle算法反而可以无需等待ACK连续传输。


禁用Nagle算法

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

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


可以通过TCP_NODELAY的值查看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算法,会增加过多的网络流量,反而影响传输。因此,是否使用Nagle算法应再三斟酌。

猜你喜欢

转载自blog.csdn.net/amoscykl/article/details/80261754
今日推荐