在进行网络编程的时候,经常需要査看或者设置套接字的某些特性,例如设置地址复用、读写数据的超时时间、对读缓冲区的大小进行调整等操作。获得套接字选项设置情况 的函数是getsockopt(),设置套接字选项的函数为setsockopt()。
功能描述:
获取或者设置与某个套接字关联的选项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该将层的值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。例如,为了表示一个选项由TCP协议解析,层应该设定为协议号TCP。
用法:
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
参数:
- sock:将要被设置或者获取选项的套接字。
- level:选项所在的协议层。
- optname:需要访问的选项名。
- optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。
- optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。
返回说明:
成功执行时,返回0。失败返回-1,errno被设为以下的某个值
- EBADF:sock不是有效的文件描述词
- EFAULT:optval指向的内存并非有效的进程空间
- EINVAL:在调用setsockopt()时,optlen无效
- ENOPROTOOPT:指定的协议层不能识别选项
- ENOTSOCK:sock描述的不是套接字
level参数:
level指定控制套接字的层次.可以取三种值:
- 1)SOL_SOCKET:通用套接字选项.
- 2)IPPROTO_IP:IP选项.
- 3)IPPROTO_TCP:TCP选项.
选项名称 说明 数据类型
========================================================================
SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSERADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
========================================================================
IPPROTO_IP
------------------------------------------------------------------------
IP_HDRINCL 在数据包中包含IP首部 int
IP_OPTINOS IP首部选项 int
IP_TOS 服务类型
IP_TTL 生存时间 int
========================================================================
IPPRO_TCP
------------------------------------------------------------------------
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int
========================================================================
(1)SO_TYPE
这个选项用于设置或者获得套接字的类型,例如SOCK_STREAM或者SOCK_DGRAM等表不套接字类型的数值。这个套接字选项经常用在忘记自己套接字类型或者不知道套接字类型的情况。
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socketopt_type.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
int main(int argc,char **argv)
{
int err = -1; /*错误*/
int s = -1; /* Socket */
int so_type = -1; /* Socket 类型 */
socklen_t len = -1; /* 选项值长度 */
/*
* 建立一个流式套接字
*/
s = socket(AF_INET,SOCK_STREAM,0);
if(-1 == s){
printf("socket error\n");
return -1;
}
/*
* 获得SO_TYPE的值
*/
len = sizeof(so_type);
err = getsockopt(s, SOL_SOCKET, SO_TYPE, &so_type,&len);
if(err == -1){
printf("getsockopt error\n");
close(s);
return -1;
}
/*
* 输出结果
*/
printf("socket fd: %d\n",s);
printf(" SO_TYPE : %d\n",so_type);
printf(" SO_STREAM = %d\n",SOCK_STREAM);
close(s);
return 0;
}
(2)SO_REUSERADDR
这个参数表示允许重复使用本地地址和端口,这个设置在服务器程序中经常使用。
例如某个服务器进程占用了TCP的80端口进行侦听,当再次在此端U侦听时会返回错误。设置SO_REUSEADDR可以解决这个问题,允许共用这个端口。某些非正常退出服务器程序,可能需要占用端段时间才能允许其他进程使用,即使这个程序已经死掉内核仍然要一段时间才能释放此端口,不设置SO_REUSEADDR将不能正确绑定端口。
optval = 1; /* 重用有效 */
optlen = sizeof(optval);
err=setsockopt(s, SOL_SOCKET, SO_REUSEADDR,(char *)&optval, optlen);
if(err!= -1)
{ /* 设置失败 */
printf("套接字可重用设置失败!\n");
return -1;
}
(3)SO_KEEPALIVE
选项SO_KEEPALIVE用于设置TCP连接的保持,当设置此项后,连接会测试连接的状态。这个选项用于可能长时间没有数据交流的连接,通常在服务器端进行设置。
当设置SO_KEEPALIVE选项后,如果在两个小时内没有数据通信时,TCP会自动发送一个活动探测数据报文,对方必须对此进行响应,通常有如下3种情况。
- TCP的连接正常,发送一个ACK响应,这个过程应用层是不知道的。再过两个小时,又会再发送一个。
- 对方发送RST响应,对方在2个小时内进行了重启或者崩溃。之前的连接己经失效,套接字收到一个ECONNRESET错误,之前的套接字关闭。
- 如果对方没有任何响应,则本机会发送另外8个活动探测报文,时间的间隔为75s,当第一个活动报文发送11分15秒后仍然没有收到对方的任何响应,则放弃探测,套接字错误类型设置为ETIMEOUT,并关闭套接字连接。如果收到一个ICMP控制报文响应,此时套接字也关闭,这种情况通常收到的是一个主机不可达的ICMP报文,此时套接字错误类型设置为EHOSTUNREACH,并关闭套接字连接。
SO_KEEPALIVE的使用场景主要是在可能发送长时间无数据响应的TCP连接,例如Telnet会话,经常会出现打开一个telnet客户端后,长时间不用的情况,这需要服务器或 者客户端有一个探测机制知道对方是否仍然活动。根据探测结果服务器会释放己经失效的客户端,保证服务器资源的有效性,例如有的telnet客户端没有按照正常步骤进行关闭。
socketopt_keepalive_server.c
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socketopt_keepalive_server.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<error.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<unistd.h>
#define PORT 8888 /* 服务器侦听端口为8888 */
#define BACKLOG 8 /* 最大侦听排队数量为8 */
static int alive = 1; /* 是否退出 */
/* 用于处理SIGPIP和SIGINT信号的函数 */
static int sigpipe(int signo)
{
alive = 0;
}
int main(int argc, char *argv[])
{
/* s为服务器的侦听套接字描述符,sc为客户端连接成功返回的描述符 */
int s, sc;
/* local_addr本地地址,client_addr客户端的地址 */
struct sockaddr_in local_addr,client_addr;
int err = -1; /* 错误返回值 */
socket_t optlen = -1; /* 整型的选项类型值 */
int optval = -1; /* 选项类型值长度 */
/* 截取SIGPIPE和SIGINT由函数signo处理 */
signal(SIGPIPE, signo);
signal(SIGINT,signo);
/* 创建本地监听套接字 */
s = socket(AF_INET,SOCK_STREAM,0);
if( s == -1)
{
printf("套接字创建失败!\n");
return -1;
}
/* 设置地址和端口重用 */
optval = 1; /* 重用有效 */
optlen = sizeof(optval);
err=setsockopt(s, SOL_SOCKET, SO_REUSEADDR,(char *)&optval, optlen);
if(err!= -1)
{ /* 设置失败 */
printf("套接字可重用设置失败!\n");
return -1;
}
/* 初始化本地协议族,端口和IP地址 */
bzero(&local_addr, 0, sizeof(local_addr)); /* 清理 */
local_addr.sin_family=AF_INET; /* 协议族 */
local_addr.sin_port=htons(PORT); /* 端口 */
local_addr.sin_addr.s_addr=INADDR_ANY; /* 任意本地地址 */
/* 绑定套接字 */
err = bind(s, (struct sockaddr *)&local_addr, sizeof(struct sockaddr);
if(err == -1)
{ /* 绑定失败 */
printf("绑定失败!\n");
return -1;
}
/* 设置最大接收缓冲区和最大发送缓冲区 */
optval = 128*1024; /* 缓冲区大小为128K */
optlen = sizeof(optval);
err = setsockopt(s, SOL_SOCKET, SO_RCVBUF, &optval, optlen);
if(err == -1)
{/* 设置接收缓冲区大小失败 */
printf("设置接收缓冲区失败\n");
}
err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &optval, optlen);
if(err == -1)
{/* 设置发送缓冲区大小失败 */
printf("设置发送缓冲区失败\n");
}
/* 设置发送和接收超时时间 */
struct timeval tv;
tv.tv_sec = 1; /* 1秒 */
tv.tv_usec = 200000;/* 200ms */
optlen = sizeof(tv);
err = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, optlen); /* 设置接收超时时间 */
if(err == -1)
{/* 设置接收超时时间失败 */
printf("设置接收超时时间失败\n");
}
err = setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, optlen);/* 设置发送超时时间 */
if(err == -1){
printf("设置发送超时时间失败\n");
}
/* 设置监听 */
err = listen(s,BACKLOG);
if( err ==-1)
{ /* 设置监听失败 */
printf("设置监听失败!\n");
return -1;
}
printf("等待连接...\n");
fd_set fd_r; /* 读文件描述符集 */
struct timeval tv;
tv.tv_usec = 200000; /* 超时时间为200ms */
tv.tv_sec = 0;
while(alive)
{
//有连接请求时进行连接
socklen_t sin_size=sizeof(struct sockaddr_in);
/* 此处每次会轮询是否有客户端连接到来,间隔时间为200ms */
FD_ZERO(&fd_r); /* 清除文件描述符集 */
FD_SET(s, &fd_r); /* 将侦听描述符放入 */
switch (select(s + 1, &fd_r, NULL, &tv))
{ /* 监视文件描述符集fd_r */
case -1: /* 错误发生 */
case 0: /* 超时 */
continue;
break;
default: /* 有连接到来 */
break;
}
/* 有连接到来,接收... */
sc = accept(s, (struct sockaddr *)&client_addr,&sin_size);
if( sc ==-1)
{ /* 失败 */
perror("接受连接失败!\n");
continue;
}
/* 设置连接探测超时时间 */
optval = 10; /* 10秒 */
optlen = sizeof(optval);/**/
err = setsockopt(sc, IPPROTO_TCP, SO_KEEPALIVE, (char*)&optval, optlen);/* 设置... */
if( err == -1)
{/* 失败 */
printf("设置连接探测间隔时间失败\n");
}
/* 设置禁止Nagle算法 */
optval = 1; /* 禁止 */
optlen = sizeof(optval);
err = setsockopt(sc, IPPROTO_TCP, TCP_NODELAY, (char*)&optval, optlen);/* 设置... */
if( err == -1)
{/* 失败 */
printf("禁止Nagle算法失败\n");
}
/* 设置连接延迟关闭为立即关闭 */
struct linger;
linger.l_onoff = 1; /* 延迟关闭生效 */
linger.l_linger = 0; /* 立即关闭 */
optlen = sizeof(linger);
err = setsockopt(sc, SOL_SOCKET, SO_LINGER, (char*)&linger, optlen);/* 设置... */
if( err == -1)
{/* 失败 */
printf("设置立即关闭失败\n");
}
/* 打印客户端IP地址信息 */
printf("接到一个来自%s的连接\n",inet_ntoa(client_addr.sin_addr));
err = send(sc,"连接成功!\n",10,0);
if(err == -1)
{
printf("发送通知信息失败!\n");
}
/* 关闭客户端连接 */
close(sc);
}
/* 关闭服务器端 */
close(s);
return 0;
}
(4)SO_RCVBUF/SO_SNDBUF
选项SO_RCVBUF和SO_SNDBUF用于操作发送缓冲区和接收缓冲区的大小,对于每个套接字对应均有发送缓冲区和接收缓冲区。接收缓冲区用于保存网络协议栈收到的数据,直到应用程序成功地读取:发送缓冲区则需要保存发送的数据直到发送成功。
这两个选项在TCP连接和UDP连接中的含义有所不同
在UDP连接中,由于它是无状态连接,发送缓冲区在数据通过网络设备发送后就可以丢弃,不用保存。而接收缓冲区则需要保存数据直到应用程序读取,由于UDP没有流量 控制,当缓冲区过小时,发送端局部时间内会产生爆发性数据传输,由于接收端来不及读取数据,很容易造成缓冲区溢出,将原来的数据覆盖,淹没接收端。因此使用UDP连接时, 需要将接收的缓冲区调整为比较大的值。
在TCP连接中,接收缓冲区大小就是滑动窗口大小。TCP的接收缓冲区不可能溢出,因为不允许对方发送超过接收缓冲区大小的数据,当对方发送的数据超过滑动窗口大小,接收方会将数据丢弃。
设置TCP接收缓冲区大小的时机很重要,因为接收缓冲区与滑动窗口的大小是一致的,而滑动窗口的协商是在建立连接时通过SYN获得的。对于客户端程序,接收缓冲区的大小要在connect()函数调用之前进行设置,因为connect()需要通过SYN建立连接。而对于服务器程序,需要在listen()之前进行设置接收缓冲区的大小,因为accept()返回的套接字描述符是继承了listen()的描述符属性,此时的滑动窗口都己经进行了设置。
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socketopt_bufsize.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
int main(int argc,char **argv)
{
int err = -1; /* 返回值 */
int s = -1; /* socket描述符 */
int snd_size = 0; /* 发送缓冲区大小 */
int rcv_size = 0; /* 接收缓冲区大小 */
socklen_t optlen; /* 选项值长度 */
/*
* 建立一个TCP套接字
*/
s = socket(PF_INET,SOCK_STREAM,0);
if( s == -1){
printf("建立套接字错误\n");
return -1;
}
/*
* 先读取缓冲区设置的情况
* 获得原始发送缓冲区大小
*/
optlen = sizeof(snd_size);
err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen);
if(err){
printf("获取发送缓冲区大小错误\n");
}
/*
* 打印原始缓冲区设置情况
*/
printf(" 发送缓冲区原始大小为: %d 字节\n",snd_size);
printf(" 接收缓冲区原始大小为: %d 字节\n",rcv_size);
/*
* 获得原始接收缓冲区大小
*/
optlen = sizeof(rcv_size);
err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
if(err){
printf("获取接收缓冲区大小错误\n");
}
/*
* 设置发送缓冲区大小
*/
snd_size = 4096; /* 发送缓冲区大小为8K */
optlen = sizeof(snd_size);
err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen);
if(err){
printf("设置发送缓冲区大小错误\n");
}
/*
* 设置接收缓冲区大小
*/
rcv_size = 8192; /* 接收缓冲区大小为8K */
optlen = sizeof(rcv_size);
err = setsockopt(s,SOL_SOCKET,SO_RCVBUF, &rcv_size, optlen);
if(err){
printf("设置接收缓冲区大小错误\n");
}
/*
* 检查上述缓冲区设置的情况
* 获得修改后发送缓冲区大小
*/
optlen = sizeof(snd_size);
err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen);
if(err){
printf("获取发送缓冲区大小错误\n");
}
/*
* 获得修改后接收缓冲区大小
*/
optlen = sizeof(rcv_size);
err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
if(err){
printf("获取接收缓冲区大小错误\n");
}
/*
* 打印结果
*/
printf(" 发送缓冲区大小为: %d 字节\n",snd_size);
printf(" 接收缓冲区大小为: %d 字节\n",rcv_size);
close(s);
return 0;
}
(5)OPTION_SHOW
显示设备中的所有选项默认参数值,可以使用下面的代码:
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socket_show.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#include <netinet/tcp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h>
#include <unistd.h>
/* 结构保存获取结果 */
typedef union optval
{
int val; /*整型值*/
struct linger linger; /*linger结构*/
struct timeval tv; /*时间结构*/
unsigned char str[16]; /*字符串*/
}val;
/*数值类型*/
typedef enum valtype
{
VALINT, /*int类型*/
VALLINGER, /*struct linger类型*/
VALTIMEVAL, /*struct timeval类型*/
VALUCHAR, /*字符串*/
VALMAX /*错误类型*/
}valtype;
/* 用于保存套接字选项的结构 */
typedef struct sopts
{
int level; /*套接字选项级别*/
int optname; /*套接字选项名称*/
char *name; /*套接字名称*/
valtype valtype;/*套接字返回参数类型*/
}sopts;
static val optval;/*用于保存数值*/
sopts sockopts[] =
{
{SOL_SOCKET, SO_BROADCAST, "SO_BROADCAST", VALINT},
{SOL_SOCKET, SO_DEBUG, "SO_DEBUG", VALINT},
{SOL_SOCKET, SO_DONTROUTE, "SO_DONTROUTE", VALINT},
{SOL_SOCKET, SO_ERROR, "SO_ERROR", VALINT},
{SOL_SOCKET, SO_KEEPALIVE, "SO_KEEPALIVE", VALINT},
{SOL_SOCKET, SO_LINGER, "SO_LINGER", VALINT},
{SOL_SOCKET, SO_OOBINLINE, "SO_OOBINLINE", VALINT},
{SOL_SOCKET, SO_RCVBUF, "SO_RCVBUF", VALINT},
{SOL_SOCKET, SO_RCVLOWAT, "SO_RCVLOWAT", VALINT},
{SOL_SOCKET, SO_RCVTIMEO, "SO_RCVTIMEO", VALTIMEVAL},
{SOL_SOCKET, SO_SNDTIMEO, "SO_SNDTIMEO", VALTIMEVAL},
{SOL_SOCKET, SO_TYPE, "SO_TYPE", VALINT},
{IPPROTO_IP, IP_HDRINCL, "IP_HDRINCL", VALINT},
{IPPROTO_IP, IP_OPTIONS, "IP_OPTIONS", VALINT},
{IPPROTO_IP, IP_TOS, "IP_TOS", VALINT},
{IPPROTO_IP, IP_TTL, "IP_TTL", VALINT},
{IPPROTO_IP, IP_MULTICAST_TTL, "IP_MULTICAST_TTL", VALUCHAR},
{IPPROTO_IP, IP_MULTICAST_LOOP, "IP_MULTICAST_LOOP", VALUCHAR},
{IPPROTO_TCP, TCP_KEEPCNT, "TCP_KEEPCNT", VALINT},
{IPPROTO_TCP, TCP_MAXSEG, "TCP_MAXSEG", VALINT},
{IPPROTO_TCP, TCP_NODELAY, "TCP_NODELAY", VALINT},
{0, 0, NULL, VALMAX}/*结尾,主程序中判断VALMAX*/
};
/* 显示查询结果 */
static void disp_outcome(sopts *sockopt, int len, int err)
{
if(err == -1){/* 错误 */
printf("optname %s NOT support\n",sockopt->name);
return;
}
switch(sockopt->valtype){/*根据不同的类型进行信息打印*/
case VALINT:/*整型*/
printf("optname %s: default is %d\n",sockopt->name,optval.val);
break;
case VALLINGER:/*struct linger*/
printf("optname %s: default is %d(ON/OFF), %d to linger\n",
sockopt->name, /*名称*/
optval.linger.l_onoff,/*linger打开*/
optval.linger.l_linger);/*延时时间*/
break;
case VALTIMEVAL:/*struct timeval结构*/
printf("optname %s: default is %.06f\n",
sockopt->name,/*名称*/
((((double)optval.tv.tv_sec*100000+(double)optval.tv.tv_usec))/(double)1000000));/*浮点型结构*/
break;
case VALUCHAR:/*字符串类型,循环打印*/
{
int i = 0;
printf("optname %s: default is ",sockopt->name);/*选项名称*/
for(i = 0; i < len; i++){
printf("%02x ", optval.str[i]);
}
printf("\n");
}
break;
default:
break;
}
}
int main(int argc, char *argv[])
{
int err = -1;
int len = 0;
int i = 0;
int s = socket(AF_INET, SOCK_STREAM, 0);/*建立一个流式套接字*/
while(sockopts[i].valtype != VALMAX)
{/*判断是否结尾,否则轮询执行*/
len = sizeof(sopts);/*计算结构长度*/
err = getsockopt(s, sockopts->level, sockopts->optname, &optval, &len);/*获取选项状态*/
disp_outcome(&sockopts[i], len, err);/*显示结果*/
i++;/*递增*/
}
close(s);
return 0;
}
文章内容来源于《linux网络编程》