套接字的多种可选项
下列是针对SOL_SOCKET协议层的
可选项 | 描述 |
---|---|
SO_REUSEADDR | 允许重用本地地址和端口,即使之前的连接处于 TIME_WAIT 状态。 |
SO_KEEPALIVE | 启用 TCP 连接的心跳检测功能,保持连接活动状态。 |
SO_LINGER | 控制关闭连接时的行为。设置为 0 表示立即关闭连接,非零值则表示等待一段时间再关闭。 |
SO_RCVBUF | 控制套接字接收缓冲区的大小。 |
SO_SNDBUF | 控制套接字发送缓冲区的大小。 |
SO_RCVTIMEO | 设置套接字接收操作的超时时间。 |
SO_SNDTIMEO | 设置套接字发送操作的超时时间。 |
SO_BROADCAST | 允许广播发送和接收。 |
SO_DEBUG | 启用调试模式,在调试期间捕获套接字的调试信息。 |
SO_ERROR | 获取套接字错误状态。 |
SO_REUSEPORT | 允许多个套接字绑定到相同的地址和端口上。 |
SO_TYPE | 获取套接字的类型。 |
下列是针对IPPROTO_TCP协议层的
可选项 | 描述 |
---|---|
TCP_NODELAY | 禁用 Nagle 算法,允许小数据包立即发送。 |
TCP_MAXSEG | 设置 TCP 数据包的最大段长度(Maximum Segment Size,MSS)。 |
TCP_KEEPALIVE | 启用 TCP 连接的心跳检测功能,保持连接活动状态。 |
TCP_KEEPIDLE | 设置 TCP 连接的空闲时间阈值,超过阈值会发送心跳包。 |
TCP_KEEPINTVL | 设置 TCP 心跳包的发送间隔。 |
TCP_KEEPCNT | 设置 TCP 心跳包的发送次数,达到次数仍无响应则关闭连接。 |
TCP_LINGER2 | 控制关闭连接时的行为。设置为 0 表示立即关闭连接,非零值则表示等待一段时间再关闭。 |
TCP_SYNCNT | 设置 TCP 发送 SYN 数据包的最大次数。 |
下列是针对IPPROTO_IP协议层的
可选项 | 描述 |
---|---|
IP_TTL | 设置 IP 头部的生存时间(Time To Live),指定数据包在网络中可以传输的最大跳数。 |
IP_HDRINCL | 控制应用程序是否提供自定义 IP 头部。 |
IP_OPTIONS | 设置自定义 IP 选项。 |
IP_RECVOPTS | 允许应用程序接收来自对方发送的 IP 选项。 |
IP_RETOPTS | 允许应用程序发送自定义的返回路径 IP 选项。 |
IP_PKTINFO | 允许应用程序获得接收到的数据包的相关信息,如源地址、目标地址和接口索引等。 |
IP_RECVTOS | 允许应用程序接收来自对方发送的服务类型(Type of Service)字段。 |
IP_RECVTTL | 允许应用程序接收来自对方发送的生存时间(Time To Live)字段。 |
IP_MULTICAST_IF | 控制多播套接字的出站接口。 |
也许有些人看到上述表格会产生畏惧,但现在无需全部背下来或者理解。本章只会介绍少数几种选项的设置。
getsockopt&setsockopt
可选项的设置可以依照下面两个函数完成设置
下面函数是用于读取套接字可选项
cpp
#include<sys/socket.h>
int getsockopt(int sock,int level,int optname,void *optval,socklen_t*optlen);
//成功时返回0,失败时返回-1
sock //用于查看选项套接字文件描述符
level //查看可选项的协议层
optname //要查看的可选项名
optval //保存查看结果的缓冲地址值
optlen //该变量中保存通过第四个参数返回的可选信息的字节数
下面函数是用于设置可选项的函数
cpp
#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 //传递第四个参数的字节数
具体的演示放在下一小节里面展示
SO_SNDBUF&SO_RCVBUF
SO_RCVBUF是输入缓冲大小相关可选项,SO_SNDBUF是输出缓冲大小相关可选项。用这两个可选项即可读取当前IO缓冲大小,可以进行更改。
下面的演示是获取具体缓冲大小
cpp
#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("Output buffer size: %d \n",snd_buf);
return 0;
}
void error_handling(char*message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
下面的演示是修改具体缓冲大小
cpp
#include<"声明与上一个示例相同,故省略">
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);
}
最后输出的结果可能会和我们设置的大小完全不同。因为再缓冲大小上需要谨慎处理,因此不会100%的按照我梦的请求设置缓冲大小,但也大致反映出了通过setsockopt函数设置缓冲大小。
SO_REUSEADDR
先描述一下前面章节的代码情景,让客户端先通知服务器端终止程序。在客户端控制台输入Q消息时调用close函数(参考第4章),向服务器端发送FIN消息并经过四次握手过程。当然,输入CTRL+C时也会向服务器传递FIN消息。强制终止程序时,由操作系统关闭文件及套接字,此过程相当于调用close函数,也会向服务器端传递FIN消息。"但看不到什么特殊现象啊?"
是的,通常都是由客户端先请求断开连接,所以不会发生特别的事情。重新运行服务器端也
不成问题,但按照如下方式终止程序时则不同。
"服务器端和客户端已建立连接的状态下,向服务器端控制台输入CTRL+C,即强
制关闭服务器端。"
这主要模拟了服务器端向客户端发送FIN消息的情景。但如果以这种方式终止程序,那服务器端重新运行时将产生问题。如果用同一端口号重新运行服务器端,将输出"bind() error"消息,并且无法再次运行。但在这种情况下,再过大约3分钟即可重新运行服务器端。
上述2种运行方式唯一的区别就是谁先传输FIN消息,但结果却迥然不同,原因何在呢?
Time_wait状态
相信各位已经对四次握手有了很好的理解,现在再来描述一下上述过程。假设主机A是服务器端,因为是主机A向B发送FIN消息,故可以想象成服务器端在控制台输入CTRL+C。但问题是套接字经过四次握手过程后并非立即消除,而是要经过一段时间的Time-wait状态。当然,只有先断开连接的(先发送FIN消息的)主机才经过Time-wait状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在Time-wait过程时,相应端口是下在使用的状态。因此,就像之前验证过的,bind函数调用过程中当然会发生错误。到底为什么会有Time-wait状态呢?
假设主机A向主机B传输ACK消息后立即消除套接字。但最后这条ACK消息在传递途中丢失,未能传给主机B。这时会发生什么?主机B会认为之前自己发送的FIN消息未能抵达主机A,维而试图重传。但此时主机A已是完全终止的状态,因此主机B永远无法收到从主机A最后传来的ACK消息。相反,若主机A的套接字处在Time-wait状态,则会向主机B重传最后的ACK消息,主机B也可以正常终止。基于这些考虑,先传输FIN消息的主句应经过Time-wait过程。
地址再分配
Time-wait看似重要,但并不一定讨人喜欢。考虑一下系统发生故障从而紧急停止的情况。这时需要尽快重启服务器端以提供服务,但因处于Time-wait状态而必须等待几分钟。因此,Time-wait并非只有优点,而且有些情况下可能引发更大问题。
在主机A的四次握手过程中,如果最后的数据丢失,则主机B会认为主机A未能收到自己发送的FIN消息,因此重传。这时,收到FIN消息的主机A将重启Time-wait计时器。因此,如果网络状况不理想,Time-wait状态将持续。
解决方案就是在套接字的可选项中更改SO_REUSEADDR的状态。适当调整该参数,可将Time-wait状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR的默认值为0(假),这就意味着无法分配Time-wait状态下的套接字端口号。因此需要将这个值改成1(真)。
只需要添加入如下代码即可
cpp
optlen=sizeof(option);
option=TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR,(void*)&option,optlen);
基于Windows的实现
套接字可选项及其相关内容与操作系统无关,特别是本章的可选项,因此在Windows平台和Linux平台下并无区别。接下来介绍读取和设定的两个函数。
cpp
#include <winsock2.h>
int getsockopt(SOCKET sock, int level, int optname, char * optval, int * optlen);
//成功时返回0,失败时返回SOCKET_ERROR。
sock //要查看可选项的套接字句柄。
level //要查看的可选项协议层。
optname //要查看的可选项名。
optval //保存查看结果的缓冲地址值。
optlen //向第四个参数optval传递的缓冲大小。调用结束后,该变量中保存通过第四个参数返回的可
//选项字节数。
可以看到,除了optval类型变成char指针外,与Linux中的getsockopt函数相比并无太大区别(Linux中是void型指针)。将Linux中的示例移植到Windows时,应做适当的类型转换。接下来给出setsockopt函数。
cpp
#include <winsock2.h>
int setsockopt(SOCKET sock, int level, int optname, const char* optval, int
optlen);
//成功时返回0,失败时返回SOCKET_ERROR。
sock //要更改可选项的套接字句柄。
level //要更改的可选项协议层。
optname //要更改的可选项名。
optval //保存要更改的可选项信息的缓冲地址值。
optlen //传入第四个参数optval的可选项信息的字节数。
setsockopt函数也与Linux版的毫无二致。