读取和设置套接字可选项
套接字具有多种特性,这些特性可通过可选项更改。
(一)读取套接字可选项: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);
}
(二)更改套接字可选项: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);
}
(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);
}
可以看到缓冲区大小和我们的预期设置大小不同。缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行,只是通过调用 setsockopt 函数向系统传递我们的要求。
(二)地址再分配可选项(SO_REUSEADDR)
(1)TIME-WAIT 状态
TIME_WAIT 是 TCP 协议中定义的一个连接状态,表示一个连接的终止过程已完成,但系统还保留了连接的信息,以确保所有的传输数据都得到正确处理。
- 只有先断开连接的(先发送
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 算法的差别:
“只有收到前一数据的 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。