函数原型:
#include
#include
//获得套接字选项设置情况
int getsockopt(int s,int,level,int optname,void *optval,socklen_t *optlen
//设置套接字选项
int setsockopt(int s,int level,int optame,const void *optval,socklen_t optlen);
//s:获取或设置的套接字描述符,socket()获得
//level:选项所在协议层。
//optname:选项名。
//optval:操作的内存缓冲区;getsockopt:获取返回值的缓冲区;setsocket:指向设置的参数缓冲区。
//optlen:第4个参数长度;getsockopt:指向socket_t类型的指针,传入参数,表示传入optval实际长度,传出表示保存optval最大长度。setsockopt:第4个参数实际长度。
选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当对套接字选项进行操作时,必须给出选项所处的层和选项的名称。为了操作套接字层的选项,应该将层的值指定为SOL_SOCKET。
为了操作其他层的选项,必须给出控制选项的协议类型号。例如,为了表示一个选项由TCP 协议解析,层应该设定为协议号 TCP。
getsockopt()函数和 setsockopt()函数的返回值在函数执行成功时,返回值为 0; 函数执行出错时,返回值为-1, 错误代码可以从 errno 中获得,其含义如下所述。
EBADF:参数 s 不是有效的文件描述符。
EFAULT:optval 指向的内存并非有效的进程空间的错误。
EINVAL:在调用 setsockopt()函数时,optlen 无效。
ENOPROTOOPT: 指定的协议层不能识别选项。
ENOTSOCK: s描述的不是套接字描述符。
由于参数选项级别level的值不同,套接字选项大致分为三类:
通用套接字选项:参数 level 的值为 SOL_SOCKET, 用于获取或者设置通用的 一些参数,例如接收和发送的缓冲区大小、地址重用等。
IP选项:参数 level 的值为 IPPROTO_ IP, 用于设置或者获取IP层的参数,例如选项名IP_HDRINCL表示在数据中包含IP头部数据、IP_TOS表示服务类型、IP_TTL表示存活时间等。
TCP选项:参数 level 的值为IPPROTO_TCP, 用于获得或者设置TCP协议层的 一些参数。例如选项名TCP_MAXRT 对最大重传时间进行操 作 、选项名TCP_KEEPALIVE对最大分片大小进行操作、选项名TCP_ KEEPALIVE对保持连接时间进行操作。

optval表示操作选项的类型 ,set 和 get 表示此种选项对 setsockopt()还是对 getsockopt()函数有效( 表示有效,X表示无效)。
获得系统支持的套接字选项的默认值,并将结果打印出来。
#include
#include
#include
#include
#include
#include
/*结构保存获取结果*/
typedef union optval {
int val; /*整型值*/
struct linger linger; /*linger结构*/
struct timeval tv; /*时间结构*/
unsigned char str[16]; /*字符串*/
}val;
static val optval; /*用于保存数值*/
/*数值类型*/
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;
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},
/*结尾,主程序中判断VALMAX*/
{0, 0, NULL, 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;
socklen_t 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;
}

SO_BROADCAST广播选项用于进行广播设置,默认情况下系统的广播是禁止的,因为很容易误用广播的功能造成网络灾难。需要广播功能,用户可以打开此功能。0 时,表示禁止广播,其他值表示允许广播。例如下面的程序允许在局域网内进行广播:
#define YES 1 //设置有效
#define NO 0 //设置无效
int s; //套接字变董
int err; //错误值
int optval = YES; //将选项设置为有效
s = socket(AF_INET, SOCK_DGRAM,0); //建立套接字
err = setsockopt(//设置选项
s,
SOL_SOCKET,
SO_BROADCAST, //SO_BROADCAST选项
&optval, //值为有效
sizeof(optval)); //值的长度
if(err)//判断是否发生错误
perror("setsockopt");//打印错误信息
//setsockopt()函数返回为0, 套接字s已经允许进行广播。
//必须使用socket(AF_INET, SOCK_DGRAM,0)建立一个UDP套接字
//广播功能需要网络类型的支持,例如点对点的网络架构就不能进行广播功能设置。
SO_DEBUG调试选项表示允许调试套接字(仅支持TCP),当打开此选项时,Linux内核程序跟踪在此套接字上的发送和接收的数据,并将调试信息放到一个环形缓存区中。
例如,下面的代码将TCP套接字设置为可调试。进行数据收发后,可以使用命令trpt来查看跟踪结果。
#define YES 1 //设置有效
#define NO 0 //设置无效
int s; //套接字变量
int err; //错误值
int optval = YES; //将选项设置为有效
s = socket(AF_INET,SOCK_STREAM,0); //建立一 个TCP套接字
err = setsockopt( //设置选项
s,
SOL_SOCKET,
SO DEBUG, //SO_DEBUG选项
&optval, //值为有效
sizeof(optval));//值的长度
SO_ DONTROUTE选项的设置使发出的数据分组不经过正常的路由机制。分组将按照发送数据的目的地址和子网掩码选择 一 个合适的网络接口进行发送,而不用经过路由机制。
如果不能由选定的网络接口确定,则会返回ENETUNREACH错误。
选项设置后,网络数据不通过网关发送,只能发送给直接连接的主机或者一个子网内的主机。
可以通过将 send()函数的选项设置中加上MSG_DONTROUTE标志来实现相同的效果。选项的值是布尔型整数的标识。
这个选项可以在两个网卡的局域网内使用,系统根据发送的目的IP地址,自动匹配合适的子网,例如将子网A的数据发送到网络接口B上。
SO_ERROR选项用来获得套接字错误,此套接字选项仅能够获取而不能进行设置。
当套接字发生错误的时候,兼容BSD的网络协议将内核中的变量so_error设置为形如UNIX_Exxx的值。
内核通过两种方式通知用户进程:
如果进程通过使用函数select()阻塞,则函数会返回-1, 并将查询的套接字描述符的集合中 一 个或者两个集合进行设置。当检查可读时,可读的套接字描述符集和错误描述符集进行设置;当检查写时,可写套接字描述符集和错误描述符集进行设置。
如果使用信号驱动IO模型,则进程或者进程组收到SIGIO信号。
进程在返回后,可以通过函数getsockopt()的SO_ERROR选项获得发生的错误号,这个值通过 一 个int类型的变量获得。
注:
当读数据出错时,返回值为-1, 则so_error为非零值,errno为so_error的值;
当返回值为数据长度的时候,so_error为0。
SO_KEEPALIVE用于设置TCP连接的保持,当设置此项后,连接会测试连接的状态。这个选项用于可能长时间没有数据交流的连接,通常在服务器端进行设置。若在两个小时内没有数据通信时,TCP会自动发送一个活动探测数据报文,对方必须对此进行响应,通常有如下3种情况:
TCP的连接正常,发送 一 个ACK 响应,这个过程应用层是不知道的。再过两个小时,又会再发送 一 个。
对方发送RST响应,对方在2个小时内进行了重启或者崩溃。之前的连接已经失效,套接字收到一个ECONNRESET 错误,之前的套接字关闭。
若对方没有任何响应,则本机会发送另外8个活动探测报文,时间的间隔为75s。当第一 个活动报文发送 11分 15秒后仍然没有收到对方的任何响应,则放弃探测,套接字错误类型设置为ETIMEOUT, 并关闭套接字连接。如果收到 一 个ICMP 控制报文响应,此时套接字也关闭,这种情况通常收到的是一个主机不可达的ICMP报文,此时套接字错误类型设置为EHOSTUNREACH,并关闭套接字连接。
SO_KEEPALIVE应用在发送长时间无数据响应的TCP连接,例如telnet会话,经常会出现打开 一 个telnet客户端后长时间不用的情况,这需要服务器或者客户端有一 个探测机制知道对方是否仍然活动。根据探测结果服务器会释放已经失效的客户端,保证服务器资源的有效性,例如有的telnet客户端没有按照正常步骤进行关闭。
#define YES 1 //设置有效
#define NO 0//设置无效
int s;//套接字变量
int err;//错误值
int optval = YES;//将选项设置为有效
err = setsockopt(//设置选项
s,
SOL_SOCKET,
SO_KEEPALIVE,//SO_KEEPALIVE选项
&optval,//值为有效
sizeof(optval));//值的长度
if(err)//判断是否发生错误
perror("setsockopt");//打印错误信息
SO_KEEPALIVE所使用的机制通常会影响到其他应用程序,而且活动探测以2个小时为周期,不能实时地探测连接情况,当没有响应的时候还需要约11分钟的超时时间。但是,这样一种框架确实可以探测空闲的连接,然后自动关闭服务器连接。
SO_LINGER用于设置TCP连接关闭时的行为方式,就是关闭流式连接时,发送缓冲区中的数据如何处理。
①.SO_LINGER选项的含义
Linux内核的默认处理方式是当用户调用close()函数时,函数会立刻返回。
在可能的情况下,尽量发送缓冲区中的数据,但是并不一定保证会发送剩余的数据,这造成了剩余数据的不可确定性。因为close()函数立刻返回,用户没有办法知道剩余数据的处理情况。
使用选项SO_LINGER可以阻塞close()函数的调用,直到剩余数据全部发送到对方。并且这可以保证TCP连接两端的正常关闭,或者获得错误的情况。
如果需要程序立刻关闭,可以设置结构linger中的值,此时调用close()函数的时候,会丢弃发送缓冲区内的数据,并立刻关闭连接。
SO_LINGER的操作是通过结构struct linger来进行的,结构定义如下:
struct linger{
int l_onoff;//是否设置延时关闭
int l_linger;//超时时间
};
l_onoff为0或者非0, 设置结构linger的值可以形成以下3种不同的关闭连接方式:
l_onoff的值为0, 这时成员l_linger将被忽略,使用系统默认关闭行为。这种情况与没有设置SO_LINGER的状态是 一 致的,close()函数会立即返回调用者,发送缓冲区内数据的处理方式未知。
l_onoff的值为1, 此时成员l_linger表示关闭连接的超时时间。此时的I_linger的值变得至关重要,当为非0时,表示的为超时的秒数,会在超时之前发送所有未发送的数据。如果发送成功,则close()函数返回值为0; 发送失败将返回错误,变量errno的值为EWOULDBLOCK。
l_onoff的值为1, l_linger的值设置为0, 表示立刻关闭。此时调用close()函数将立刻返回,并且发送缓冲区里面的剩余数据将被丢弃。
②.套接字关闭的过程
关闭行为的默认情况如下图所示,主机A的close()函数调用后会立刻返回,主机B是否接收到之前write()函数发送的数据的情况不明。

当设置延迟关闭的超时时间,主机A会在超时时间内把数据发送成功,主机A接收到主机B的write()成功的确认后,关闭连接,如下图所示。

shutdown()函数进行半连接关闭,主机A先关闭写,当收到主机B对之前写入数据的ACK确认后,再进行关闭。

③.选项SO_LINGER的例子
使用60s的超时时限,在60秒内允许发送数据,当缓冲区内的数据发送完毕,会正常关闭;若不能正常发送,则返回错误。
#define YES 1 //设置有效
#define NO 0 //设置无效
int s; //套接字变量
int err; //错误值
struct linger optval; //建立一个linger 类型的套接字选项变量
optval.l_onoff = YES; //设置linger生效
optval.l_linger= 60; //linger超时时间为60s
s = socket(AF_INET, SOCK_DGRAM,Q);//建立套接字
err = setsockopt( //设置选项
s,
SOL_SOCKET,
SO_LINGER, //SO_LINGER选项
&optval, //值为有效
sizeof(optval));//值的长度
if(err)//判断是否发生错误
perror("setsocket");//打印错误信息
立刻关闭套接字,调用close()函数会立刻返回,并丢弃没有发送的数据。
#define YES 1 //设置有效
#define NO 0 //设置无效
int s; //套接字变量
int err; //错误值
struct linger optval; //建立一个linger 类型的套接字选项变董
optval.l_onoff = YES; //设置linger生效
optval.l_linger= 0; //linger超时时间为Os, 立即生效
s = socket(AF_INET, SOCK_DGRAM,0);//建立套接字
err = setsockopt( //设置选项
s,
SOL_SOCKET,
SO_LINGER //SO_LINGER选项
&optval, //值为有效
sizeof(optval)); //值的长度
if(err) //判断是否发生错误
perror("setsockopt");
带外数据放入正常数据流,在普通数据流中接收带外数据。设置选项,带外数据不再通过另外的通道获得,在普通数据流中可以获得带外数据。
SO_OOBINLINE可以设置使用通用方法来接收带外数据。可以使用下面的代码来完成此项功能设置。
#define YES 1 //设置有效
#define NO 0 //设置无效
int s; //套接字变量
int err; //错误值
int optval = YES; //设置选项值为有效
s = socket (AF_INET, SOCK_DGRAM, 0); //建立套接字
err= setsockopt( //设置选项
s,
SOL_SOCKET,
SO_OOBINLINE,//SO_OOBINLINE选项
&optval, //值为有效
sizeof(optval));//值的长度
if(err)//判断是否发生错误
perror("setsockopt");//打印错误信息
//设置之后,带外数据会与一般数据一起接收,这种方式下,所接收的越界数据与通常数据相同,
//即增加了带宽。
SO_RCVBUF和SO_SNDBUF用于操作发送缓冲区和接收缓冲区的大小,接收缓冲区用于保存网络协议栈收到的数据,直到应用程序成功地读取;发送缓冲区则需要保存发送的数据直到发送成功。
这两个选项在TCP连接和UDP连接中的含义有所不同。
UDP连接中,无状态连接,发送缓冲区在数据通过网络设备发送后就可以丢弃,不用保存。
而接收缓冲区则需要保存数据直到应用程序读取,由于UDP没有流量控制,当缓冲区过小时,发送端局部时间内会产生爆发性数据传输,由于接收端来不及读取数据,很容易造成缓冲区溢出,将原来的数据覆盖,淹没接收端。
使用UDP连接时,需要将接收的缓冲区调整为比较大的值。
TCP连接中,接收缓冲区大小就是滑动窗口大小。TCP的接收缓冲区不可能溢出,因为不允许对方发送超过接收缓冲区大小的数据,当对方发送的数据超过滑动窗口大小,接收方会将数据丢弃。
设置TCP接收缓冲区大小的时机很重要,因为接收缓冲区与滑动窗口的大小是一 致的,而滑动窗口的协商是在建立连接时通过SYN获得的。
客户端程序,接收缓冲区的大小要在connect()函数调用之前进行设置,因为connect()需要通过SYN建立连接。
服务器程序,需要在listen()之前进行设置接收缓冲区的大小,因为accept()返回的套接字描述符是继承了listen()的描述符属性,此时的滑动窗口都已经进行了设置。
发送缓冲区下限选项SO_RCVLOWAT和接收缓冲区下限选项 SO_SNDLOWAT用来调整缓冲区的下限值。函数select()使用发送缓冲区下限和接收缓冲区下限来判断可读和可写。
当select()轮询可读,接收缓冲区中的数据必须达到可写的下限值,select()才返回。对于TCP和UDP, 默认的值均为1, 即接收到一个字节的数据 select()函数就可以返回。
当select()轮询可写,需要发送缓冲区中的空闲空间大小达到下限值,函数才返回。对于TCP通常为2048个字节。UDP的发送缓冲区的可用空间字节数从不发生变化,发送缓冲区的大小,只需UDP套接字发送的数据小于发送缓冲区的大小,就可以发送的。
SO_RCVTIMEO表示接收数据的超时时间,SO_SNDTIMEO表示发送数据的超时时间,默认情况下在接收和发送数据的时候是不会超时的,例如recv()函数当没有数据的时候会永远阻塞。这两个选项影响到的函数有如下两类。
接收超时影响的5个函数为: read()、readv()、recv()、recvfrom()和recvmsg()。
发送超时影响的5个函数为: write()、writev()、send()、sendto()和sendmsg()。
接收超时和发送超时的功能与select()的超时的功能有部分是一致的,不过使用套接字选项进行设置后,不用再每次轮询,其属性会 一 直继承下去。
超时的时间获取和设置通过结构struct timeval的变量来实现。结构struct timeval定义如下:
struct timeval{
time_t tv_sec;//秒数
suseconds_t tv_usec;//微数
};
//设置的时候需要设置timeval.tv _sec和timeval.tv _usec两个值,1秒为1000000微秒。当tv_sec和tv_usec都为0的时候,禁止超时,与默认的设置相同。
SO_REUSERADDR选项表示允许重复使用本地地址和端口。
服务器进程占用了TCP的80端口进行侦听,当再次在此端口侦听时会返回错误,设置SO_REUSEADDR可以解决这个问题,允许共用这个端口。
非正常退出的服务器程序,可能需要占用端口 一 段时间才能允许其他进程使用,即使这个程序已经死掉,内核仍然需要 一 段时间才能释放此端口,不设置 SO_REUSEADDR将不能正确绑定端口。
#define YES 1 //设置有效
#define NO 0 //设置无效
int s; //套接字变量
int err; //错误值
int optval = YES; //设置选项值为有效
s = socket(AF_INET, SOCK_DGRAM,0);//建立套接字
err = setsockopt(//设置选项
s,
SOL_SOCKET,
SO_ERUSEADDR,///SO_REUSEADDR选项
&optval,//值为有效
sizeof(optval);//长度
if(err)//判断是否发生错误
perror("setsockopt");//打印错误信息
//若查询端口复用,使用getsocket()函数
与SO_REUSEADDR相反,SO_EXCLUSIVEADDRUSE选项表示以独占的方式使用端口,不允许其他应用程序占用此端口,此时不能使用SO_REUSEADDR来共享使用某一个端口。
SO_REUSEADDR可以进行多重绑定,如果没有使用选SO_EXCLUSIVEADDRUSE ,则显式设置某一端口的不可绑定状态,即使调用SO_VSEADDR的用户权限低,也可以将某个程序的端口重新绑定在高级权限的端口上。
多个进程可以同时绑定在某个端口上,这是一个非常大的安全隐患,程序可以很容易被监听。如果不想让程序被监听,需要使用本选项进行端口不可绑定的设置。
SO_TPYPE用于设置或者获得套接字的类型,例如SOCKSTREAM或者SOCKDGRAM 等表示套接字类型的数值。SO_TYPE选项经常用在忘记套接字类型或者不知道套接字类型的情况例如,在如下的代码中先建立一个TCP套接字,但是之后忘记这个套接字的类型了,可以使用SO_TYPE选项获取其类型。
s = socket(AF_INET,SOCK_STREAM,0); //建立一个TCP套接字
...
...
//忘记套接字类型
int type;
int length= 4;
err= getsockopt( //获得套接字选项的值
s,
SOL_SOCKET,
SO_TYPE, //SO_TYPE选项
&type, //套接字类型
&length);//值长度
if(SOCK_STREAM == type)
printf("TCP套接字\n");
else if (SOCK_DGRAM == type)
printf ("{,!DP套接字\n");
SO_BSDCOMPAT表示是否与BSD套接字兼容。目前这个选项存在安全漏洞,如果没有特殊的原因不要使用这个选项。
net/core/sock.c文件中,获得套接字选项的sock _getsockopt()函数中,如果设置了SO_ BSDCOMPAT选项的话,其中的参数会被错误初始化,并将值返回给调用的用户,导致信息泄漏。SO_BINDTODEVICE可以将套接字与某个网络设备绑定,这在同 一个主机上存在多个网络设备的情况十分有用,使用这种方法,可以将某些数据显式地指定从哪个网络设备发送。
如下图所示, 一 个主机上有两个网卡 eth0 和 eth1, 建立了两个套接字s0和s1, s0 的数据需要和子网0中的主机进行数据通信,而s1需要和子网1中的主机进行数据通信,而子网0和子网1是不相连通的。
如图所示,套接字s0的数据发送到了子网l中,这在实际使用中造成了很大的困扰,如图所示

使用套接字选项SO _BINDTODEVICE可以解决上面的问题,将s0的数据收发绑定到eth0上,将s1的数据收发绑定到eth1上。
此选项的值是一个表示设备名称的字符串,当为空字符串时,套接字绑定到序号0的网络设备上。当字符串是正确的网络设备名称时,则会绑定到此设备上。如下面代码将套接字s绑定到网卡ethl上。
int s,err;
char ifname[] = "eth1";//绑定的网卡名称
struct ifreq if_eth1;//绑定网卡的结构
strncpy(if_eth1.ifr_name,ifname,IFNAMSIZ);//将网卡名称放到结构成员ifr_name中
...
...
err = setsockopt(s,SOL_SOCKET,SO_BANDTODEVICE,(char*)&if_eth1,sizeof(if_eth1));
//将s绑定到网卡eth1上
if(err){
printf("setsockopt SO_BANDTODEVICE failure\n");
}
//绑定成功,通过s进行的数据收发将由网卡eth1处理。
SO_PRIORITY设置通过此套接字进行发送的报文的优先级,由于Linux中发送报文队列的排队规则是高优先级的数据优先被处理,因此设置这个选项可以调整套接字的优先级。
可以通过optval来设置,优先级的范围是0~6(包含优先级0和优先级6)。下面的代码将套接字s的优先级设置为6。
opt= 6;
setsockopt(s, SOL_SOCKET, SO_PRIORITY, &opt, sizeof(opt));
IPPROTO_IP级别的套接字选项主要是IP层协议的操作。主要包含控制IP头部选项的IP_HDRINCL、IP头部选项信息可控的IP_OPTIONS、服务类型设置的IP_TOS、IP包的生存时间设置的IP_TTL等选项控制命令字。
Linux内核会自动计算和填充IP头部数据。如果套接字是一个原始套接字,设置此选项有效之后,则IP头部需要用户手动填充。
用户在发送数据的时候需要手动填充IP的头部信息,这个选项通常是在需要用户自定义数据包格式的时候使用。
一旦设置此选项生效,用户发送的IP数据包将不再进行分片。因此,用户的数据包不能太大,否则网卡不能进行发送,会造成数据发送失败。
IP_OPTNIOS允许设置IP头部的选项信息,在发送数据的时候会按照用户设置的IP选项进行进行。
IP_OPTIONS选项设置的时候,其参数是指向选项设置信息的指针和选项的长度,选项长度最大为40个字节。
IP_TOS可以设置或者获取服务类型的值(不能获得接收数据的服务类型值。)。对于发送的数据可以将服务类型设置为如下表所示的值,在文件中有定义。

IP_TTL可以设置或者获得发送报文的 TTL 值。一般情况下值为64,对于原始套接字此值为 255。设置 IP 的生存时间值,可以调整网络数据的发送速度。
例如,通过一 个 TCP 连接发送数据,如果 TTL 的值过大,就有各种路由方法可选。调整TCP 的TTL 值之后,比较长的路由路径会被取消。
int current TTL=O; //用于保存当前的TTL
int set_TTL=32;//设置的TTL值
int length_TTL=sizeof(int);
//获取当前的TTL值
getsockopt(s,IPPROTO_IP,IP_TTL, ¤t_TTL,&length_TTL);
//设置新的TTL值
setsockopt (s,IPPROTO_IP,IP_TTL, &set_ttl,length_TTL);
//给远程主机发送数据
sendto(s,buffer,buffer_len,0,(struct sockaddr*)&remote_ip,sizeof(struct sockaddr));
//还原TTL值
setsockopt(s,IPPROTO_IP,IP_TTL,¤t_TTL,length_TTL);
// 与IP_TOS一 样,IP_TTL 选项不能荻得接收报文的生存时间值。
IPPROTO_TCP 级别的套接字选项是对TCP 层的操作。主要包括控制TCP 生存时间的TCP _KEEPALIVE、最大重传时间的 TCP_MAXRT、最大分节大小的 TCP_MAXSEG 、屏蔽nagle 算法的TCP_NODELAY 和TCP_CORE。
TCP _KEEPALIVE 选项用于获取或者设置存活探测的时间间隔,在 SO_KEEPALIVE设置的情况下,此选项才有效。默认情况下,存活时间的值为 7200s, 即两个小时系统进行一 次存活时间探测。
下面的代码将TCP 的存活时间设置为60s。
int alive_time = 60;//存活时间60秒
int length_alive = sizeof(int);
int s = socket(AF_INET,SOCK_STREAM,0);//建立TCP套接字
//设置新的存活时间值为60秒
setsockopt(s,IPPROTO_TCP,TCP_KEEPALIVE,&alive_time,length_alive);
TCP_MAXRT最大重传时间,表示在连接断开之前重传需要经过的时间。此数值以秒为单位,0表示系统默认值,-1表示永远重传。
下面的代码将系统的最大重传时间设置为3秒钟,如果一个TCP 报文在3 秒钟之内没有收到回复,则会进行数据的重传。
int maxrt = 3;//存活时间60秒
int length_maxrt = sizeof(int);
int s = socket(AF_INET,SOCK_STREAM,0);//建立TCP套接字
//设置新的最大重传时间值为60秒
setsockopt(s,IPPROTO_TCP,TCP_MAXRT,&maxrt,length_alive);
TCP_MAXSEG可以获取或设置 TCP 连接的最大分节大小 (MSS)。返回值是TCP 连接中向另一端发送的最大数据大小,它通常使用SYN 与另一 端协商MSS, MSS值的设置选择二者之间的最小值。
TCP _NODELAY 和TCP_CORK 是针对Nagle 算法的关闭而设置的。
①.Nagle 算法简介
Nagle 算法是为解决网络阻塞问题,例如,一 个终端应用程序的每次按键都要发送到服务器上,这样一 个包只有一 个字节的有用数据和 40 个字节的包头,即有4000%负载浪费掉了,当系统资源有限时,很容易造成网络的阻塞和系统的性能下降。Nagle算法将上述情况的终端输入包装为更大的帧来发送,可以有效地解决上述问题。
Nagle 算法的基本原理有如下两点:
将小分组包装为更大的帧进行发送,例如上述情况终端的多次输入一 起打包发送,而不是每输入一 次发送一 次数据。Nagle算法总是最大可能地发送最大分组,将小分组包装,小分组是指小于MSS的分组。在之前的数据被确认之前不再发送数据分组,即 Nagle 算法需要之前数据接收方的响应。
Nagle算法通常在接收端使用延迟确认,在接收到数据后并不马上发送确认,而是要等待一 小段时间。这样可以与接收方的有效数据一起发送ACK , 即接收方向发送方发送的确认分组中包含接收方发送给发送方的有效载荷数据。
②.Nagle 算法的例子
一 个telnet的客户端和服务器连接,客户端按键的输入间隔为200ms, 而网络传输和返回的时间为 500ms, 客户端输入date 命令,则不使用Nagle 算法的执行过程如下图所示。

客户端的date字符串输入的顺序为在第 0ms输入 d, 在第 200ms输入 a, 在第 400ms输入 t, 在第 600ms输入 e。
而网络传输和响应过程由于每次只能发送一 个小的分组,并且必须等待分组响应返回后才能发送第二个分组,所以在第 500ms, d 字符的响应返回;
第l000ms, a 字符的响应返回;第 1500ms, t字符的响应返回;第 2000ms, e 字符的响应返回。
这种过程不能充分地使用网络的带宽,因为在发送a字符的时候缓冲区中已经有字符d了,却不进行发送。
Nagle 算法充分利用了将小包合并的功能,如下图所示。在第 500ms发送第二个包的时候,此时客户端已经有 at两个字符,此时将其一 起发出。

③.CP_NODELAY和TCP_CORK选项 在Nagle算法中的作用
Nagle算法已经是网络协议栈的默认配置,它可以有效地提高网络的有效负载。但是在某些情况下却可能不符合需求。
需要根据Nagle算法的第三条禁止时,使用TCP_NODELAY选项设置。此时客户端的请求不需要和其他分组合并,会尽快地发送到服务器端,可以提高交互性应用程序的响应速度。
TCP_CORK 选项的情况是需要等到发送的数据量达到最大时,一次性地发送全部数据,这样可以充分地利用网络的带宽,提高数据传输的通信性能。
例:先写入一 个标志字符,然后写入数据,最后一 起发送。这种情况在文件传输或者大数据传输中经常用到。若使用Nagle优化,则会在标志字符写入的时候发送一 个字符,造成了浪费。此时可以设置TCP_CORK, 告诉协议栈在达到最大数据分组的时候一 起发送。这种情况就像一 个接水的水桶,当水接满的时候才一 次性地将水桶拿走使用。
intfd cork = 1;
...
//初始化
...
setsockopt(fd,SOL_TCP,TCP_CORK,&cork,sizeof(cork));
//设置TCP_CORK选项
write(fd,...); //用水桶接水
senddata(fd,...);//拿走水桶
... //其他处理
write(fd,...);//用水桶接水
senddata(fd,...);//拿走水桶
...
cork = 0;
setsockopt(fd,SOL_TCP,TCP_SORK,&cork,sizeof(on));
//取消设置TCP_CORK选项
//Apache的HTTPD使用了TCP_NODELAY来发送大块的数据,用于提供性能
①.缓冲区选项使用方法
//读取缓冲区大小
optlen = sizeof(buff_size);
err = getsockopt(s,SOL_SOCKET,SO_SNDBUF/*SO_RCVBUF*/,&snd_size,&optlen);
//设置缓冲区大小
buff_size = 4096;
optlen = sizeof(buff_size);
err = setsockopt(s,SOL_SOCKET,SO_SNDBUF/*SO_RCVBUF*/,&buff_size,optlen);
//当发送或者接收的数据比较小,而计算负载又比较重的时候,经常将缓冲区的大小设置为比较小的值,以节省资源。
//处理的数据量比较大,例如接收实时流媒体,则可以将缓冲区设置得比较大,将接收过程和解码过程进行分离。
②.缓冲区选项使用的例子
本例中建立一个TCP套接字,先查看系统默认的接收缓冲区和发送缓冲区的大小,然后修改接收缓冲区和发送缓冲的大小,最后将修改后的结果打印出来。
#include
#include
#include
#include
#include
#include
#include
#include
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;
}

结果表示,当TCP套接字没有接收数据的时候,其接收缓冲区的原始大小为 0字节 ,发送缓冲区的大小为 16K 字节 。在设置缓冲区大小的时候 ,真实的设置情况为用户输入值的2倍大小。
③缓冲区的内核策略
分析程序的运行结果,似乎实际所得的缓冲区大小为设定值的两倍。分析Linux的内核源代码可以清晰地获得其算法。设置缓冲区大小的内核代码在文件net/core/sock.c中,不同的内核版本,其算法可能会有差异。
在内核中设置发送缓冲区的策略。下面是设置发送缓冲区 SO_SNDBUF 的代码 :
case SO_SNDBUF:
/*Don't error on this BSD doesn't and if you think
about it this is right. Otherwise apps have to
play'guess the biggest size'games. RCVBUF/SNDBUF
are treated in BSD as hints*/
if(val>sysctl_wmem_max)
val = sysctl_weme_max;
set_sndbuf:
sk->sk_userlocks |=SOCK_SNDBUF_LOCK;
if((val *2) <SOCK_MIN_SNDBUF);
sk->sk_sndbuf = SOCK_MIN_SNDBUF;
else
sk->sk_sndbuf = val*2;
/*
Wake up sending tasks if we
upped the value
*/
sk->sk_write_space(sk);
break;
//变量val存放用户设置的缓冲区大小 。
对发送缓冲区大小设置的规则如下:
sysctl_wmem_max大时,将发送缓冲区设置为sysctl_wmem_max。SOCK_MIN_SNDBUF 还要小时,将发送缓冲区大小设置为SOCK_MIN_SNDBUF。#define SOCK_MIN_SNDBUF 2048//SOCK_MIN_SNDBUF是最小发送缓冲区值
#define SOCK_MIN_RCVBUF 256//SOCK_MIN_RCVBUF是最小接收缓冲区的值
sysctl_wmem _max是在sk_init()函数中初始化的,其代码如下所示。当内存比较小的时候,它的值为32K-1(32767);当系统的内存比较大的时候,值为128k-1(131071),:
if (num_physpages <= 4096) {
sysctl_wmem_max = 32767;
sysctl_rmem_max = 32767;
sysctl_wmem_default = 32767;
sysctl_rmem_default = 32767;
} else if (num_physpages >= 131072){
sysctl_wmem_max = 131071;
sysctl_rmem_rnax = 131071;
}
内核中设置接收缓冲区的策略,设置SO_RCVBUF:
case SO_RCVBUF:
/*Don't error on this BSD doesn't and if you think
about it this is right. Otherwise apps have to
play' guess the biggest size'games. RCVBUF/SNDBUF
are treated in BSD as hints*/
if(val >sysctl_rmem_max)
val = sysctl_rmem_max;
set_rcvbuf:
sk->sk_userlocks |=SOCK_RCVBUF_LOCK;
/*We double it on the way in to account for
"struct sk buff" etc. overhead. Applicati.ons
assume that _the SO_RGVBUF setting they make will
allow that much actual data to be received on that socket.
Applications are unaware that "struct sk buff" and
other ove.rheads allocate from the receive buffer
during socket·buffer allocation.
And after considering the po.ssible alter,natives,
returning the value we actually used in getsockopt
is the most desirable behacior
*/
if((val *2) <SOCK_MIN_RCVBUF)
sk->sk_rcvbuf = SOCK_MIN_RCVBUF;
else
sk->sk_rcvbuf = val * 2;
break;
//变量val存放了用户设置的缓冲区大小
对接收缓冲区大小设置的规则如下:
sysctl _wmem_max大时,将发送缓冲区设置为sysctl_wmem max。SOCK_MIN_RCVBUF还要小时,将接收缓冲区大小设置为SOCK_MIN_RCVBUF。在建立一个流式套接字之后,使用getsockopt()函数的SO_TYPE,获得当前套接字的类型,查看套接字的类型是否符合建立套接字的类型(流式):
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
len = sizeof(so_type);
err = getsockopt(s, SOL_SOCKET, SO_TYPE, &so_type,&len);
/*获得SO_TYPE的值*/
if(err == -1){
printf("getsockopt error\n");
close(s);
return -1;
}
/*输出结果*/
printf("socket fd: %d\n",s);
printf(" SO_TYPE : %d\n",so_type);
close(s);
return 0;
}

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 8888 // 服务器侦听端口为8888
#define BACKLOG 8 //最大侦听排队数量为8
//用于截取SIGPIP和SIGINT信号的函数
//客户端断开,服务器接收SIGPIPE信号,通知客户端被端开。
//用户Crtl+C向进程发送SIGINT信号,通知进程被打断。
//收到SIGPIPE和SIGINT信号时会设置全局变量alive,循环的主程序自动退出。
static int alive = 1; //是否退出
static void sigpipe(int signo)
{
alive = 0;
}
int main(int argc, char* argv[])
{
//s为服务器的侦听套接字描述符,sc为客户端连接成功返回的描述符
int s, sc;
//local_addr本地地址,client客户端地址
struct sockaddr_in local_addr, clinet_addr;
int err = -1;//错误返回值
socklen_t optlen = -1;//整型的选项类型值
int optval = -1;//选项类型值长度
//截取SIGPIPE和SIGINT由函数sigpipe处理
signal(SIGPIPE, sigpipe);
signal(SIGINT, sigpipe);
//建立本地监听套接字,使用SOCK_STREAM将地址设置为可复用。
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, optval);
if (err != -1) {
printf("套接字可重用设置失败\n");
return -1;
}
//初始化本地协议族,端口和IP地址
bzero(&local_addr, 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;//1s
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;//读文件描述符集
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, NULL, &tv)) {
//监视文件描述符集fd_r
case -1: //错误发生
case 0: //超时
break;
default: //有连接到来
break;
}
//有连接到来,接收...
socklen_t sin_size = sizeof(struct sockaddr_in);
sc = accept(s, (struct sockaddr*)&clinet_addr, &sin_size);
if (sc == -1) { //失败
perror("接收连接失败!\n");
}
//设置连接探测超时时间
optval = 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;
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(clinet_addr.sin_addr));
err = send(sc, "连接成功!\n", 10, 0);
if (err == -1) {
printf("发送通知信息失败!\n");
}
//关闭客户端
close(sc);
}
//关闭服务器端
close(s);
return 0;
}
ioctl()含用于与内核中的网络协议栈进行交互,函数原型:
int ioctl(int d,int request,...);



套接字IO操作的命令请求有6个,它们的第3个参数要求为 一个执行整型数据的指针。其含义如下所述。
SIOCATMARK: 查看TCP连接中是否有带外数据,如果有带外数据,第3个指针的返回值为非0, 否则为0。
SIOCSPGRP和FIOSETOWN: 这两个请求可以获得对套接字的SIGIO和SIGURG信号,进行处理的进程ID号或者进程组ID号,通过第3个参数获得。
SIOCGPGRP和FIOGETOWN: 利用第3个参数,这两个请求可以设置接收此套接字的SIGIO和SIGURG信号的进程ID或者进程组ID。
SIOCGSTAMP: 利用这个请求可以得到最后 一 个数据报文到达的时间,第3 个参数是一 个指向结构struct timeval 的指针。
int main(void)
{
int s = -1;//socket描述符
int err = -1;//返回值
...
...
int request = -1;//请求类型
int para = -1;//ioctl第三个参数
struct timeval tv; //ioctl第三个参数
}
①、SIOCATMARK
para为0无带外数据,非0有带外数据。
request = SIOCATMARM;
err = ioctl(s,request,¶);
if(err){
//错误处理
}
if(para){ //有带外数据
//接收带外数据,处理...
...
}else{ //无带外数据
...
}
②、SIOCSPGRP和FIOSETOWN
para参数保存的为进程的ID号,请求的类型可以为SIOCGPGRP或者FIOGETOWN。
//获得SIGIO和SIGURG信号处理进程ID
request = SIOCGPGRP;//或FIOGETOWN
err = ioctl(s,request,¶);
if(err){
//错误处理
...
}else{
//获得处理信号的进程ID
...
}
③、SIOCGPGRP和FIOGETOWN
para中为可以处理信号的进程ID,请求类型可以为SIOCSPGRP或者FIOSETOWN。
//设置SIGIO和SIGURG信号处理进程ID
request = SIOCSPGRP;
err = ioctl(s,request,¶);
if(err){
//错误处理
...
}else{
//成功设置处理信号的进程ID号
...
}
④、SIOCGSTAMP
请求类型为SIOCGSTAMP,第3个参数为一个指 向结构struct timeval的指针。
//获得数据报文到达的时间
request = SIOCGSTAMP
err = ioctl(s,request,&tv);
if(err){
//错误处理
...
}else{
//获得了最后数据报文到达时间,在参数tv内
...
}
这一组的请求命令都是FIOxxx类型,以FIO开头,除了可以处理套接字外,对通用的文件系统也同样适用。本组有3个,例:
FIONBIO: 用于设置或者清除套接字的非阻塞(xxxNBxxx-NonBlock)标志。当第3个参数为0时,清除非阻塞标志,即设置套接字操作为阻塞方式;当第3个参数为非0时,设置为非阻塞方式。
FIOASYNC: 用于设置或者清除套接字的异步信号(SIGIO)。当第3个参数为0时,清除套接字上的异步信号;当第3个参数为非0时,设置套接字上的异步信号。
FIONREAD: 这个套接字用于获得当前套接字接收缓冲区中的字节数,即有多少个字节的数据可以读取,提前获得接收缓冲区的数据长度可以正确地准备应用层用于接收的缓冲区大小。
网络接口的一些参数,例如IP地址、子网掩码、网络接口名称、最大传输单元等是进
行网络设置或者网络程序设计的时候必须获得的参数。本小节将介绍如何获得上述的参数。
①.网络接口的常用数据结构
//网络接口请求结构
struct ufreq
{
#define IFHWADDRLEN 6 //网络接口硬件结构长度,即MAC长度,为6
union
{
char ifrn_name[IFNAMSIZ];//网络接口名称,例如"ethO"
}ifr_ifrn;
union{
struct sockaddr ifru_addr;//本地IP地址
struct sockaddr ifru_dstaddr;//目标IP地址
struct sockaddr ifru_broadadd;//广播IP地址
struct sockaddr ifru_netmask;//本地子网掩码地址
struct sockaddr ifru _hwaddr;//本地MAC地址
short ifru_flags;//网络接口标记
int ifru_ivalue; //值,不同的请求含义不同
int ifru_mtu; //最大传输单元MTU
struct ifmap ifru_map; //最大地址映射
char ifru_slave[IFNAMSIZ];//占位符
char ifru_newname[IFNAMSIZ];//新名称
void _user* ifru_data;//用户数据
struct if_settings ifru_settings; //设备协议设置
}ifr_ifru;
};
#define ifr_narne ifr_ifrn.ifrn_narne //接口名称
#define ifr hwaddr ifr_ifru.ifru_hwaddr //MAC地址
#define ifr addr ifr_ifrn.ifru_addr //本地IP地址
#define ifr dstaddr ifr_ifrn.ifru_dstaddr //p2p地址
#define ifr broadaddr ifr_ifrn.ifru_broadaddr //广播IP地
#define ifr_netmask ifr_ifrn.ifru_netmask//子网掩码
#define ifr_flags ifr_ifru.ifru_flags//标志
#define ifr_metric ifr_ifru.ifru_ivalue//接口侧度
#define ifr_mtu ifr_ifru.ifru_mtu//最大传输单元
#define ifr_map ifr_ifru.ifru_map//设备地址映射
#define ifr_slave ifr_ifru.ifru_slave//副设备
#define ifr_data ifr_ifru.ifru_data//接口使用
#deifne ifr_ifindex ifr_ifru.ifru_ivalue;//网络接口序号
#define ifr_bandwidth ifr_ifru.ifru_ivalue//连带宽接
#define ifr_qlen ifr_ifrun_ifru.ivalue//传输单元长度
#define ifr_newname ifr_ifru.ifru_newname//新名称
#define ifr_settings ifr_ifru.ifru_settings//设备协议设置
struct ifmap是网卡设备的映射属性,包含开始地址、结束地址、基地址、中断号、DMA和端口号等。
struct ifmap
{
unsigned long mem_start; //开始地址
unsigned long mem_end;//结束地址
unsigned short base_addr; //基地址
unsigned char irq; //中断号
unsigned char dma; //DMA
unsigned char port; //端口
//3字节空闲
};
网络配置结构体为一块缓冲区,可转换为结构ifreq来方便操作,用于读取网络接口配置情况。
// 网络配置接口
struct ifconf
{
int ifc_len; //缓冲区ifr_buf的大小
union
{
char _user *ifcu_bud;//缓冲区指针
struct ifreq_user *ifcu_req;//指向结构ifreq的指针
}ifc_ifcu;
};
#define ifc_buf ifc_ifcu.ifcu_buf//缓冲区地址的宏
#define ifc_req ifc_ifcu.ifcu_req//结构ifc_req的宏
②.获取网络接口的命令选项
SIOCGIFCONF:这个选项用于获得网络接口配置的情况。需要填写结构 struct ifreq 的ifr_name 变量的值,即将需要查询的网络接口的名称放入变量中。返回配置的数据和长度。struct ifreq 的ifr _name 变量的值,即将需要查询的网络接口的名称放入变量中,例如为 eth0。返回值放在ifr _addr 中。SIOCGIFADDR:获取本地 IP 地址。SIOCGIFDSTADDR:获取目的地址IP。SIOCGIFBRDADDR:获取广播地址。SIOCGIFNETMASK:获取子网掩码。配制网络接口选项。与获取 IP 地址相对应,设置IP 地址需要填写ifr_name 变量的值,并将 ifr _addr 设置为用户改变的IP 地址。
SIOCSIFADDR:设置本地 IP 地址。SIOCSIFDSTADDR:设置目的 IP 地址。SIOCSIFBRDADDR: 设置广播 IP 地址。SIOCSIFNETMASK:设置子网掩码。ifr _name 变量,指明网络接口,但是它们的返回值在结构ifreq 的不同数据结构中。SIOCGIFMETRIC:获取METRIC, 返回值在 ifr _metric 中。SIOCGIFMTU:获取MTU, 返回值在 ifr_mtu 中。SIOCGIFHWADDR获取 MAC 地址,返回值在ifr_hwaddr.sa_data 中,6 个字节。SIOCGIFINDEX:获取网络接口的序列号,返回值在 ifr _ifindex 中。SIOCGIFTXQLEN:获取发送缓冲区长度,返回值在 if _qlen 中。SIOCGIFPFLAGS:获取标志,返回值在 ifr_flags 中。SIOCGIFMAP:获取网卡的映射情况,返回值在结构ifr_map 中。SIOCGIFNAME:获得网络接口的名称,需要设置结构 struct ifreq 的 ifr_ifrindex变量指明获得哪个网络接口的名称。返回值在 ifr_name 中。网络接口的底层参数配制选项。需要设置 ifr _name 变量,指明网络接口,它们需要设置与获取命令对应的结构 ifreq 成员。
SIOCSIFMETRIC:设置METRIC, 设置参数 ifr _metric。SIOCSIFMTU:设置MTU, 设置参数 ifr _mtu。SIOCSIFHWADDR:设置硬件地址,设置参数 ifr _hwaddr.sa_data 。SIOCSIFPFLAGS:设置标志,设置参数 ifr_flags。SIOCSIFTXQLEN:设置传输队列长度,设置参数ifr_qlen。SIOCSIFMIP:设置网卡映射情况,设置参数ifr_map,这个参数不要随便修改,会造成系统崩溃。SIOCSIFNAME: 设置网卡名称,需要设置结构struct ifreq的ifr_ ifrindex变量,指明设置哪个网络接口的名称。③.网络接口的获取和配置
下面的程序主要分为4部分进行网络接口请求命令的测试,
第1部分是通过一个序号获得网络接口的名称;
第2部分获取网络接口的常用配置参数;
第3部分获取IP地址;
第4部分修改一 下本机的 IP 地址 。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int s; /*套接字描述符*/
int err = -1; /*错误值*/
/*建立一个数据报套接字*/
s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) {
printf("socket() 出错\n");
return -1;
}
/*获得网络接口的名称*/
{
struct ifreq ifr;
ifr.ifr_ifindex = 2; /*获取第2个网络接口的名称*/
err = ioctl(s, SIOCGIFNAME, &ifr);
if(err){
printf("SIOCGIFNAME Error\n");
}else{
printf("the %dst interface is:%s\n",ifr.ifr_ifindex,ifr.ifr_name);
}
}
/*获得网络接口配置参数*/
{
/*查询网卡“ens33”的情况*/
struct ifreq ifr;
memcpy(ifr.ifr_name, "ens33",5);
/*获取标记*/
err = ioctl(s, SIOCGIFFLAGS, &ifr);
if(!err){
printf("SIOCGIFFLAGS:%d\n",ifr.ifr_flags);
}
/*获取METRIC*/
err = ioctl(s, SIOCGIFMETRIC, &ifr);
if(!err){
printf("SIOCGIFMETRIC:%d\n",ifr.ifr_metric);
}
/*获取MTU*/
err = ioctl(s, SIOCGIFMTU, &ifr);
if(!err){
printf("SIOCGIFMTU:%d\n",ifr.ifr_mtu);
}
/*获取MAC地址*/
err = ioctl(s, SIOCGIFHWADDR, &ifr);
if(!err){
char *hw = ifr.ifr_hwaddr.sa_data;
printf("SIOCGIFHWADDR:%02x:%02x:%02x:%02x:%02x:%02x\n",hw[0],hw[1],hw[2],hw[3],hw[4],hw[5]);
}
/*获取网卡映射参数*/
err = ioctl(s, SIOCGIFMAP, &ifr);
if(!err){
printf("SIOCGIFMAP,mem_start:%ld,mem_end:%ld, base_addr:%d, irq:%d, dma:%d,port:%d\n",
ifr.ifr_map.mem_start, /*开始地址*/
ifr.ifr_map.mem_end, /*结束地址*/
ifr.ifr_map.base_addr, /*基地址*/
ifr.ifr_map.irq , /*中断*/
ifr.ifr_map.dma , /*直接访问内存*/
ifr.ifr_map.port ); /*端口*/
}
/*获取网卡序号*/
err = ioctl(s, SIOCGIFINDEX, &ifr);
if(!err){
printf("SIOCGIFINDEX:%d\n",ifr.ifr_ifindex);
}
/*获取发送队列长度*/
err = ioctl(s, SIOCGIFTXQLEN, &ifr);
if(!err){
printf("SIOCGIFTXQLEN:%d\n",ifr.ifr_qlen);
}
}
/*获得网络接口IP地址*/
{
struct ifreq ifr;
/*方便操作设置指向sockaddr_in的指针*/
struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_addr;
char ip[16]; /*保存IP地址字符串*/
memset(ip, 0, 16);
memcpy(ifr.ifr_name, "ens33",5);/*查询ens33*/
/*查询本地IP地址*/
err = ioctl(s, SIOCGIFADDR, &ifr);
if(!err){
/*将整型转化为点分四段的字符串*/
inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
printf("SIOCGIFADDR:%s\n",ip);
}
/*查询广播IP地址*/
err = ioctl(s, SIOCGIFBRDADDR, &ifr);
if(!err){
/*将整型转化为点分四段的字符串*/
inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
printf("SIOCGIFBRDADDR:%s\n",ip);
}
/*查询目的IP地址*/
err = ioctl(s, SIOCGIFDSTADDR, &ifr);
if(!err){
/*将整型转化为点分四段的字符串*/
inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
printf("SIOCGIFDSTADDR:%s\n",ip);
}
/*查询子网掩码*/
err = ioctl(s, SIOCGIFNETMASK, &ifr);
if(!err){
/*将整型转化为点分四段的字符串*/
inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
printf("SIOCGIFNETMASK:%s\n",ip);
}
}
/*测试更改IP地址*/
{
struct ifreq ifr;
/*方便操作设置指向sockaddr_in的指针*/
struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_addr;
char ip[16]; /*保存IP地址字符串*/
int err = -1;
/*将本机IP地址设置为192.169.1.175*/
printf("Set IP to 192.168.1.175\n");
memset(&ifr, 0, sizeof(ifr)); /*初始化*/
memcpy(ifr.ifr_name, "ens33",5); /*对ens33网卡设置IP地址*/
inet_pton(AF_INET, "192.168.1.175", &sin->sin_addr.s_addr);
/*将字符串转换为网络字节序的整型*/
sin->sin_family = AF_INET; /*协议族*/
err = ioctl(s, SIOCSIFADDR, &ifr); /*发送设置本机IP地址请求命令*/
if(err){ /*失败*/
printf("SIOCSIFADDR error\n");
}else{ /*成功,再读取一下进行确认*/
printf("check IP --");
memset(&ifr, 0, sizeof(ifr)); /*重新清零*/
memcpy(ifr.ifr_name, "ens33",5);/*操作ens33*/
ioctl(s, SIOCGIFADDR, &ifr); /*读取*/
inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16);
/*将IP地址转换为字符串*/
printf("%s\n",ip);/*打印*/
}
}
close(s);
return 0;
}
编译运行:

ifconfig查看IP:

ARP高速缓存表是网络协议栈维护的,它记录了系统运行期间的IP地址和硬件地址的映射表。其操作包括表的创建、更新、回收,在Linux下这个表的名称是arp_tb1。
①.获取ARP高速缓存的命令字
使用ioctl()的ARP请求可以操作下面内容,有3个命令SIOCGARP、SIOCSARP和SIOCDARP。用户可以调用这3个请求命令对ARP高速缓存进行操作,操作是通过类型为struct arpreq的参数进行的。其定义在文件中,结构struct arpreq如下:
//ARP的ioctl请求
struct arpreq{
struct sockaddr arp_pa;//协议地址
struct sockaddr arp_ha;//硬件地址
int arp_flags;//标记
struct sockaddr arp_netmask;//协议地址的子网掩码(仅用于代理arp)
char arp_dev[16];//查询的网络接口名称
};
//ARP的标准值
#define ATF_COM 0x02//查找完成的地址
#define ATF_PERM 0x04//永久记录
#define ATF_PUBL 0x08//发布记录
#define ATF_USETRAILERS 0x10//使用扩展存档名称,不再使用
#define ATF_NETMASK 0x20//使用掩码(仅用于arp代码)
#define ATF_DONTPUB 0x40//不回复
#define ATF_MAGIC 0x80//自动添加的邻居
SIOCDARP: 删除高速缓存中的一个记录,需要在结构struct arpreq中填写成员 arp_pa 和成员arp_ dev, Linux 内核会根据arp_pa 中的IP 地址高速缓存中查找该记录,并把它的状态更新为失败(NUD_FAILED ) , 下一 次垃圾回收过程中会被丢弃掉。
SIOCSARP: 设置或者修改一 个记录,需要在结构struct arpreq 中填写成员 arp_pa、arp_ha 和 arp_flags。若在高速缓存中已经有该记录项,会根据输入的硬件地址修改该记录,没有则建立该项,并将此项的状态设置为永久性的 (ATF_PERM ) , 除非之后用户手动又一 次进行了设置,否则以后不会自动对其进行失效和更新。
SIOCGARP: 获得一个记录。需要在结构struct arpreq 中填写成员arp_pa的值,内核会从高速缓存中查找该项并返回记录。不过一 般查看高速缓存,并不使用 SIOCGARP 请求命令,而是直接从内存映像文件proc/net/arp 中读取。
②.获取ARP高速缓存的例子
先建立SOCK_DGRAM 的数据报套接口,用户输入的IP地址和协议族类型填充结构体 arpreq 的成员 arp_pa, 在 ens33网络接口上进行查询,然后调用 ioctl()的SIOCGARP 请求命令,如果IP 地址对应的硬件地址存在的话,从结构体arpreq 的成员arp_da中取出硬件地址(即MAC地址)。
//获取主机IP 地址对应硬件地址,用户输入需要查询的IP 地址,输出为对应 IP 地址的硬件地址。
//
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int s;
struct arpreq arpreq;
struct sockaddr_in *addr = (struct sockaddr_in*)&arpreq.arp_pa;
unsigned char *hw;
int err = -1;
if(argc < 2){
printf("错误的使用方式,格式为:\nmyarp ip(myarp 127.0.0.1)\n");
return -1;
}
/*建立一个数据报套接字*/
s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) {
printf("socket() 出错\n");
return -1;
}
/*填充arpreq的成员arp_pa*/
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = inet_addr(argv[1]);
if(addr->sin_addr.s_addr == INADDR_NONE){
printf("IP 地址格式错误\n");
}
/*网络接口为ens33*/
strcpy(arpreq.arp_dev, "ens33");
err = ioctl(s, SIOCGARP, &arpreq);
if(err < 0){ /*失败*/
printf("IOCTL 错误\n");
return -1;
}else{/*成功*/
hw = (unsigned char*)&arpreq.arp_ha.sa_data; /*硬件地址*/
printf("%s:",argv[1]);/*打印IP*/
printf("%02x:%02x:%02x:%02x:%02x:%02x\n", /*打印硬件地址*/
hw[0],hw[1],hw[2],hw[3],hw[4],hw[5]);
};
close(s);
return 0;
}
编译运行:


IP 地址 192 .168.1.1 对应的硬件地址为 00 :14:78:c3:ff:54记录,arp -a查询高速缓存记录:结果一致。

ioctl()函数的路由表请求有两种,这两种请求的第三个参数是一·个指向结构的指针 ,在文件中定义。命令选项有两个:SIOCADDRT和 SIOCADDRT。
SIOCADDRT:向路由表中增加一项。SIOCADDRT:从路由表中减去一项。注:使用上面的参数没有办法列出所有路由表中的所有项。
函数fencl()可以对套接字描述符进行操作,也可以对通用文件描述符进行操作。其函数原型如下:
int fcntl(int fd,int cmd,void arg);

fcntl()函数可以使用ioctl()函数可以完全代替,因此使用fcntl()函数对套接字进行操作的使用比较少,通常用ioctl()函数代替。
函数fontl()的命令F_SETFL和F_GETFL命令,与O_ASYNC和O_NONBLOCK搭配
可以获取或者设置套接字的非阻塞属性。常用的设置非阻塞fcntl()操作方式的代码如下:
int flags = -1;//套接字属性值
int err = -1;//错误值
flags = fcntl(s,F_GETFL,0);//获取套接字s的属性值
if(flags<0){//获取套接字属性值操作失败
printf("fcntl F_GETFL ERROR\n");
}
if(!(flags&NON_BLOCK))//查看属性值中是否有非阻塞选项NON_BLOCK
{
flags |=NON_BLOCK;//向属性值中增加非阻塞选项NONBLOCK
err = fcntl(s,F_SETFL,flags);//使用新的属性值设置文件描述符
if(err<0){//设置文件描述符属性失败
printf("fcntl F_SETFL ERROR\n");// 打印失败信息
}
}else{//文件描述符属性已经为非阻塞
printf("socket %d already set to NON_BLOCK\n",s);//打印信息
}
//先读取套接字描述符的属性
//没有设置NON_BLOCK属性的时,添加NON_BLOCK属性
//不要直接设置套接字属性为NON_BLOCK,这会将之前套接字的属性覆盖
//例如,下面的方式是不好的使用习惯:
int flags= NON_BLOCK;
err= fcntl(s, F_SETFL,flags);
套接字设置属主是因为信号SIGIO和SIGURG,这两个信号需要使用命令F_SETOWN设定了进程属主才能生成。
F_SETOWN 的参数 arg 为正数时表示绑定的为进程ID, 为负数时其绝对值为进程组的ID。F_GETOWN获取的值含义与F_SETOWN 一 样。
注: 一 个套接字在使用socket()函数生成的时候是没有属主的,当服务器的accept()函数返回一 个新的套接字描述符时,有了属主,其属主是从监听套接字继承来的。