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

107 阅读8分钟

读取和设置套接字可选项

套接字具有多种特性,这些特性可通过可选项更改。

(一)读取套接字可选项:getsockopt

(1)getsockopt() 函数

#include <sys/socket.h>
// 成功时返回0,失败时返回-1
int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
  • sock: 套接字的文件描述符,表示你要操作的套接字。

  • level: 选项的级别。常见的值包括:

    • SOL_SOCKET:用于套接字层的选项。
    • IPPROTO_TCP:用于TCP协议层的选项。
    • IPPROTO_IP:用于IP协议层的选项。
  • optname: 选项的名称,指定你想要获取的选项。例如,SO_RCVBUF 代表接收缓冲区的大小。

  • optval: 指向存储选项值的内存位置。在函数成功时,optval 指向的内存将被填充为选项的当前值。

  • optlen: 输入和输出参数。输入时,optlen 应包含 optval 所指向的内存区域的大小;输出时,optlen 被设置为选项值的实际大小。

(2)示例

以下示例用协议层为 SOL_SOCKET 、名为 SO_TYPE 的可选项查看套接字类型。

#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 套接字
	tcp_sock=socket(PF_INET, SOCK_STREAM, 0);

	// 生成 UDP 套接字
	udp_sock=socket(PF_INET, SOCK_DGRAM, 0);	
	printf("SOCK_STREAM: %d \n", SOCK_STREAM);
	printf("SOCK_DGRAM: %d \n", SOCK_DGRAM);
	
	// 获取套接字类型信息。如果是 TCP 套接字,将获得 SOCK_STREAM 常数值1;如果是 UDP 套接字,则获得 SOCK_DGRAM 的常数值2
	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);
	
	// 获取套接字类型信息。如果是 TCP 套接字,将获得 SOCK_STREAM 常数值1;如果是 UDP 套接字,则获得 SOCK_DGRAM 的常数值2
	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);
}
image.png

(二)更改套接字可选项:setsockopt

(1)setsockopt 函数

#include <sys/socket.h>
// 成功时返回0,失败时返回-1
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t optlen);
  • sock: 套接字的文件描述符,表示你要设置选项的套接字。

  • level: 选项的级别。常见的值包括:

    • SOL_SOCKET:用于套接字层的选项。
    • IPPROTO_TCP:用于TCP协议层的选项。
    • IPPROTO_IP:用于IP协议层的选项。
  • optname: 选项的名称,指定你想要设置的选项。例如,SO_RCVBUF 代表接收缓冲区的大小。

  • optval: 指向要设置的选项值的内存位置。它的类型是 const void*,可以指向不同类型的数据,根据选项的要求进行转换。

  • optlen: optval 所指向的数据的长度,通常是 sizeof 选项值的大小。

(三)基于 Windows 的实现

(1)getsockopt 函数

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

(2)setsockopt 函数

#include <winsock2.h>
int setsockopt(SOCKET s, int level, int optname, const char* optval, int* optlen);

(3)示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
void ErrorHandling(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 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);
}

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

部分套接字可选项

(一)输出/输入缓冲大小可选项(SO_SNDBUF & SO_RCVBUF

我们已经知道,创建套接字将同时生成 I/O 缓冲。

SO_RCVBUF 是输入缓冲大小相关可选项,SO_SNDBUF 是输出缓冲大小相关可选项。这两个可选项既可以读取当前 I/O 缓冲大小,也可以进行更改

(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, 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);
}
image.png

(2)更改 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);
	// 更改为 3k 字节
	state=setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
	if(state)
		error_handling("setsockopt() error!");
	
	// 更改为 3k 字节
	state=setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
	if(state)
		error_handling("setsockopt() error!");
	
	len=sizeof(snd_buf);
	// 验证 I/O 缓冲的更改
	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);
}
image.png

可以看到缓冲区大小和我们的预期设置大小不同。缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行,只是通过调用 setsockopt 函数向系统传递我们的要求。

(二)地址再分配可选项(SO_REUSEADDR

(1)TIME-WAIT 状态

TIME_WAIT 是 TCP 协议中定义的一个连接状态,表示一个连接的终止过程已完成,但系统还保留了连接的信息,以确保所有的传输数据都得到正确处理。

image.png
  • 只有先断开连接的(先发送 FIN 消息的)主机才经过 TIME-WAIT 状态。
  • TIME_WAIT 状态期间,端口不能立即被重用。这样可以避免在新的连接使用相同的端口时出现混淆或冲突。

(2)地址再分配

当系统生故障从而紧急停止时,需要尽快重启服务器端以提供服务,但因处于 TIME_WAIT 状态而必须等待几分钟。因此 TIME_WAIT 并非只有优点,解决办法是在套接字的可选项中更改 SO_REUSEADDR 的状态。适当调整该参数,可将 TIME_WAIT 状态下的套接字端口号重新分配给新的套接字。

SO_REUSEADDR 的默认值为 0(假),这意味着无法分配 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);
}

未更改之前,当服务器端控制台输入 CTRL + C 时(模拟服务器端向客户端发送 FIN 消息的情景),如果用同一端口号重新运行服务器端,将输出 “bind() error” 消息,并且无法再次运行,过大约3分钟才可重新运行服务器端。

经过更改之后,服务器端已经变为随时可运行的状态

(三)控制 Nagle 算法行为的可选项:TCP_NODELAY

(1)Nagle 算法

Nagle 算法是一种用于减少小数据包数量的机制。其核心思想是:如果发送端在短时间内发送了小数据包,Nagle 算法会将这些小数据包合并成一个较大的数据包,从而减少网络拥塞和提高效率。这个算法主要适用于低带宽、高延迟的网络环境。

具体来说,Nagle 算法会在发送一个小数据包后等待确认(ACK) 或者将多个小数据包合并成一个大数据包再发送。这样做可以减少网络上数据包的数量和大小,从而减少网络拥塞。

使用 Nagle 算法和未使用 Nagle 算法的差别:

image.png

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

(2)禁用 Nagle 算法

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

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

只需将套接字可选项 TCP_NODELAY 改为1(真)即可禁用 Nagle 算法:

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 算法,opt_val 变量中会保存0,否则保存1。