套接口选项
在前面的几章中,咱们讨论了使用套接口的基础内容。如今咱们要来探讨一些可用的其余的特征。在咱们掌握了这一章的概念以后,咱们就为后面的套接口的高级主题作好了准备。在这一章,咱们将会专一于下列主题:
如何使用getsockopt(2)函数得到套接口选项值
如何使用setsockopt(2)函数设置套接口选项值
如何使用这些经常使用的套接口选项
获得套接口选项
有时,一个程序须要肯定为当前为一个套接口进行哪些选项设置。这对于一个子程序库函数尤为如此,由于这个库函数并不知道为这个套接口进行哪些设置,而这个套接口须要做为一个参数进行传递。程序也许须要知道相似于流默认使用的缓冲区的大小。
容许咱们获得套接口选项值的为getsockopt函数。这个函数的概要以下:
程序员
1 #include <sys/types.h> 2 #include <sys/socket.h> 3 int getsockopt(int s, 4 int level, 5 int optname, 6 void *optval, 7 socklen_t *optlen);
函数参数描述以下:
1 要进行选项检验的套接口s
2 选项检验所在的协议层level
3 要检验的选项optname
4 指向接收选项值的缓冲区的指针optval
5 指针optlen同时指向输入缓冲区的长度和返回的选项长度值
当函数成功时返回0。当发生错误时会返回-1,而错误缘由会存放在外部变量errno中。
协议层参数指明了咱们但愿访问一个选项所在的协议栈。一般咱们须要使用下面中的一个:
SOL_SOCKET来访问套接口层选项
SOL_TCP来访问TCP层选项
咱们在这一章的讨论将会专一于SOL_SOCKET层选项的使用。
参数optname为一个整数值。在这里所使用的值首先是由所选用的level参数来肯定的。在一个指定的协议层,optname参数将会肯定咱们但愿访问哪个选项。下表列出了一些层与选项的组合值:
协议层 选项名字
SOL_SOCKET SO_REUSEADDR
SOL_SOCKET SO_KKEPALIVE
SOL_SOCKET SO_LINGER
SOL_SOCKET SO_BROADCAST
SOL_SOCKET SO_OOBINLINE
SOL_SOCKET SO_SNDBUF
SOL_SOCKET SO_RCVBUF
SOL_SOCKET SO_TYPE
SOL_SOCKET SO_ERROR
SOL_TCP SO_NODELAY
上表所列的大多数选项为套接口选项,其中的层是由SOL_SOCKET指定的。为了比较的目的包含了一个TCP层套接口选项,其中的层是由SOL_TCP指定的。
大多数套接口选项得到后存放在int数据类型中。当查看手册页时,数据类型int一般会有一些假设,除非代表了其余东西。当使用一个布尔值时,当值为非零时,int表示TRUE,而若是为零,则表示FALSE。
应用getsockopt(2)
在这一部分,咱们将会编译并运行一个getsndrcv.c的程序,这个程序会得到并报告一个套接口的发送以及接收缓冲区的大小尺寸。
服务器
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <errno.h> 6 #include <sys/types.h> 7 #include <sys/socket.h> 8 #include <assert.h> 9 10 11 static void bail(const char *on_what) 12 { 13 if(errno != 0) 14 { 15 fputs(strerror(errno),stderr); 16 fputs(": ",stderr); 17 } 18 fputs(on_what,stderr); 19 fputc('/n',stderr); 20 exit(1); 21 } 22 23 int main(int argc,char **argv) 24 { 25 int z; 26 int s=-1; 27 int sndbuf=0; 28 int rcvbuf=0; 29 socklen_t optlen; 30 31 32 s = socket(PF_INET,SOCK_STREAM,0); 33 if(s==-1) 34 bail("socket(2)"); 35 36 37 optlen = sizeof sndbuf; 38 z = getsockopt(s,SOL_SOCKET,SO_SNDBUF,&sndbuf,&optlen); 39 40 if(z) 41 bail("getsockopt(s,SOL_SOCKET," 42 "SO_SNDBUF)"); 43 44 assert(optlen == sizeof sndbuf); 45 46 47 48 optlen = sizeof rcvbuf; 49 z = getsockopt(s,SOL_SOCKET,SO_RCVBUF,&rcvbuf,&optlen); 50 if(z) 51 bail("getsockopt(s,SOL_SOCKET," 52 "SO_RCVBUF)"); 53 54 assert(optlen == sizeof rcvbuf); 55 56 57 printf("Socket s: %d/n",s); 58 printf("Send buf: %d bytes/n",sndbuf); 59 printf("Recv buf: %d bytes/n",rcvbuf); 60 61 close(s); 62 return 0; 63 }
程序的运行结果以下:
$ ./getsndrcv
socket s : 3
Send buf: 65535 bytes
Recv buf: 65535 bytes
设置套接口选项
若是认为套接口的默认发送以及接收缓冲区的尺寸太大时,做为程序设计者的咱们能够将其设计为一个小的缓冲区。当咱们程序一个程序的几个实例同时运行在咱们的系统上时,这显得尤为重要。
能够经过setsockopt(2)函数来设计套接口选项。这个函数的概要以下:
网络
1 #include <sys/types.h> 2 #include <sys/socket.h> 3 int setsockopt(int s, 4 int level, 5 int optname, 6 const void *optval, 7 socklen_t optlen);
这个函数与咱们在上面所讨论的getsockopt函数相似,setsockopt函数的参数描述以下:
1 选项改变所要影响的套接口s
2 选项的套接口层次level
3 要设计的选项名optname
4 指向要为新选项所设置的值的指针optval
5 选项值长度optlen
这个函数参数与上面的getsockopt函数的参数的区别就在于最后一个参数仅是传递参数值。在这种状况下只是一个输入值。
应用setsockopt函数
下面的例子代码为一个套接口改变了发送以及接收缓冲区的尺寸。在设置完这些选项之后,程序会获得并报告实际的缓冲区尺寸。
并发
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <sys/types.h> 7 #include <sys/socket.h> 8 #include <assert.h> 9 10 11 static void bail(const char *on_what) 12 { 13 if(errno!=0) 14 { 15 fputs(strerror(errno),stderr); 16 fputs(": ",stderr); 17 } 18 fputs(on_what,stderr); 19 fputc('/n',stderr); 20 exit(1); 21 } 22 23 int main(int argc,char **argv) 24 { 25 int z; 26 int s=-1; 27 int sndbuf=0; 28 int rcvbuf=0; 29 socklen_t optlen; 30 31 32 s = socket(PF_INET,SOCK_STREAM,0); 33 if(s==-1) 34 bail("socket(2)"); 35 36 37 sndbuf = 5000; 38 z = setsockopt(s,SOL_SOCKET,SO_SNDBUF,&sndbuf,sizeof sndbuf); 39 if(z) 40 bail("setsockopt(s,SOL_SOCKET," 41 "SO_SNDBUF)"); 42 43 44 rcvbuf = 8192; 45 z = setsockopt(s,SOL_SOCKET,SO_RCVBUF,&rcvbuf,sizeof rcvbuf); 46 if(z) 47 bail("setsockopt(s,SOL_SOCKET," 48 "SO_RCVBUF)"); 49 50 51 optlen = sizeof sndbuf; 52 z = getsockopt(s,SOL_SOCKET,SO_SNDBUF,&sndbuf,&optlen); 53 if(z) 54 bail("getsockopt(s,SOL_SOCKET," 55 "SO_SNDBUF)"); 56 57 assert(optlen == sizeof sndbuf); 58 59 60 optlen = sizeof rcvbuf; 61 z = getsockopt(s,SOL_SOCKET,SO_RCVBUF,&rcvbuf,&optlen); 62 if(z) 63 bail("getsockopt(s,SOL_SOCKET" 64 "SO_RCVBUF)"); 65 assert(optlen == sizeof rcvbuf); 66 67 68 printf("Socket s: %d/n",s); 69 printf(" Send buf: %d bytes/n",sndbuf); 70 printf(" Recv buf: %d bytes/n",rcvbuf); 71 72 close(s); 73 return 0; 74 }
程序的运行结果以下:
$ ./setsndrcv
Socket s : 3
Send buf: 10000 bytes
Recv buf: 16384 bytes
$
在 这里咱们要注意程序所报告的结果。他们看上去彷佛是所指定的原始尺寸的两倍。这个缘由能够由Linux内核源码模块net/core/sock.c中查 到。咱们能够查看一下SO_SNDBUF以及SO_RCVBUF的case语句。下面一段是由内核模块sock.c中摘录的一段处理SO_SNDBUF的 代码:
框架
398 case SO_SNDBUF: 399 403 404 if (val > sysctl_wmem_max) 405 val = sysctl_wmem_max; 406 set_sndbuf: 407 sk->sk_userlocks |= SOCK_SNDBUF_LOCK; 408 if ((val * 2) < SOCK_MIN_SNDBUF) 409 sk->sk_sndbuf = SOCK_MIN_SNDBUF; 410 else 411 sk->sk_sndbuf = val * 2; 412 413 417 sk->sk_write_space(sk); 418 break;
由这段代码咱们能够看到实际发生在SO_SNDBUF上的事情:
1 检测SO_SNDBUF选项值来肯定他是否超过了缓冲区的最大值
2 若是步骤1中的SO_SNDBUF选项值没有超过最大值,那么就使用这个最大值,而不会向调用者返回错误代码
3 若是SO_SNDBUF选项值的2倍小于套接口SO_SNDBUF的最小值,那么实际的SO_SNDBUF则会设置为SO_SNDBUF的最小值,不然则会SO_SNDBUF选项值则会设置为SO_SNDBUF选项值的2倍
从这里咱们能够看出SO_SNDBUF的选项值只是所用的一个提示值。内核会最终肯定为SO_SNDBUF所用的最佳值。
查看更多的内核源码,咱们能够看到相似的状况也适用于SO_RCVBUF选项。以下面的一段摘录的代码:
socket
427 case SO_RCVBUF: 428 432 433 if (val > sysctl_rmem_max) 434 val = sysctl_rmem_max; 435 set_rcvbuf: 436 sk->sk_userlocks |= SOCK_RCVBUF_LOCK; 437 452 if ((val * 2) < SOCK_MIN_RCVBUF) 453 sk->sk_rcvbuf = SOCK_MIN_RCVBUF; 454 else 455 sk->sk_rcvbuf = val * 2; 456 break;
取得套接口类型
实际上咱们只能够获得一些套接口选项。SO_TYPE就是其中的一例。这个选项会容许传递套接口的一个子函数来肯定正在处理的是哪种套接口类型。
以下面是一段获得套接口s类型的示例代码:
函数
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <sys/types.h> 7 #include <sys/socket.h> 8 #include <assert.h> 9 10 11 static void bail(const char *on_what) 12 { 13 if(errno!=0) 14 { 15 fputs(strerror(errno),stderr); 16 fputs(": ",stderr); 17 } 18 fputs(on_what,stderr); 19 fputc('/n',stderr); 20 exit(1); 21 } 22 23 int main(int argc,char **argv) 24 { 25 int z; 26 int s = -1; 27 int so_type = -1; 28 socklen_t optlen; 29 30 31 s = socket(PF_INET,SOCK_STREAM,0); 32 if(s==-1) 33 bail("socket(2)"); 34 35 36 optlen = sizeof so_type; 37 z = getsockopt(s,SOL_SOCKET,SO_TYPE,&so_type,&optlen); 38 if(z) 39 bail("getsockopt(s,SOL_SOCKET," 40 "SO_TYPE)"); 41 assert(optlen == sizeof so_type); 42 43 44 printf("Socket s: %d/n",s); 45 printf(" SO_TYPE : %d/n",so_type); 46 printf(" SO_STREAM = %d/n",SOCK_STREAM); 47 48 close(s); 49 return 0; 50 }
程序的运行结果以下:
$./gettype
Socket s: 3
SO_TYPE : 1
SO_STREAM = 1
设置SO_REUSEADDR选项
在第11章,"并发客户端服务器"的第一部分中,提供并测试了一个使用fork系统调用设计的服务器。图12.1显示了在一个telnet命令与服务器创建链接以后的三个步骤。
这些步骤以下:
1 启动服务器进程(PID 926)。他监听客户端链接。
2 启动客户端进程(telnet命令),而且链接到服务器进程(PID 926)。
3 经过fork调用建立服务器子进程,这会保留的原始的父进程(PID 926)而且建立一个新的子进程(PID 927)。
4 链接的客户端套接口由服务器父进程(PID 926)关闭,仅在子进程(PID 927)中保持链接的客户端套接口处理打开状态。
5 telnet命令与服务器子进程(PID 927)随意交互,而独立于父进程(PID 926)。
在步骤5,有两个套接口活动:
服务器(PID 926)监听192.168.0.1:9099
客户端由套接口192.168.0.1:9099进行服务(PID 927),他链接到客户端地址192.168.0.2:1035
客户端由进程ID 927进行服务。这意味着咱们能够杀掉进程ID 926,而客户端仍能够继续被服务。然而,却不会有新的链接链接到服务器,由于并无服务器监听新的链接(监听服务器PID 926已被杀死)
现 在若是咱们重启服务器来监听新的链接,就会出现问题。当新的服务器进程试着绑定IP地址192.168.0.1:9099时,bind函数就会返回 EADDRINUSE的错误代码。这个错误代码代表IP已经在9099端口上使用。这是由于进程PID 927仍然在忙于服务一个客户端。地址192.168.0.1:9099仍为这个进程所使用。
这个问题的解决办法就是杀掉进程927,这个关闭套接口而且释放IP地址和端口。然而,若是正在被服务的客户是咱们所在公司的CEO,这样的作法彷佛不是一个选择。同时,其余的部门也会抱怨咱们为何要从新启动服务器。
这个问题的一个好的解决办法就是使用SO_REUSEADDR套接口选项。全部的服务器都应使用这个选项,除非有一个更好的理由不使用。为了有效的使用这个选项,咱们应在监听链接的服务器中执行下面的操做:
1 使用一般的socket函数建立一个监听套接口
2 调用setsockopt函数设置SO_REUSEADDR为TRUE
3 调用bind函数
套接口如今被标记为可重用。若是监听服务器进程由于任何缘由终止,咱们能够从新启动这个服务器。当一个客户正为另外一个服务器进程使用同一个IP和端口号进行服务时尤为如此。
为了有效的使用SO_REUSEADDR选项,须要考虑下面的状况:
在监听模式下并无一样的IP地址和端口号的其余套接口
全部的同一个IP地址和端口号的套接口必须将SO_REUSEADDR选项设置为TRUE
这就意味着一个指定的IP地址和端口号对上只能够用一个监听器。若是这样的套接口已经存在,那么设置这样的选项将不会达到咱们的目的。
只有全部存在的同一个地址和端口号的套接口有这个选项设置,将SO_REUSEADDR设置为TRUE才会有效。若是存在的套接口没有这个选项设置,那么bind函数就会继续而且会返回一个错误号。
下面的代码显示如何将这个选项设置为TRUE:
测试
1 #define TRUE 1 2 #define FALSE 0 3 int z; 4 int s; 5 int so_reuseaddr = TRUE; 6 z = setsockopt(s,SOL_SOCKET,SO_REUSEADDR, 7 &so_reuseaddr, 8 sizeof so_reuseaddr);
若是须要SO_REUSEADDR选项能够由getsockopt函数进行查询。
设置SO_LINGER选项
另外一个经常使用的选项就是SO_LINGER选项。与SO_REUSEADDR选项所不一样的是这个选项所用的数据类型并非一个简单的int类型。
SO_LINGER选项的目的是控制当调用close函数时套接口如何关闭。这个选项只适用于面向链接的协议,例如TCP。
内核的默认行为是容许close函数当即返回给调用者。若是可能任何未发送的TCP/IP数据将会进行传送,可是并不会保证这样作。由于close函数会当即向调用者返回控制权,程序并无办法知道最后一位的数据是否进行了发送。
SO_LINGER选项能够做用在套接口上,来使得程序阻塞close函数调用,直到全部最后的数据传送到远程端。并且,这会保证两端的调用知道套接口正常关闭。若是失败,指定的选项超时,而且向调用程序返回一个错误。
经过使用不一样的SO_LINGER选项值,能够应用一个最后场景。若是调用程序但愿当即停止通讯,能够在linger结构中设置合适的值。而后,一个到close的调用会初始化一个通讯停止链接,而丢弃全部未发送的数据,并当即关闭套接口。
SO_LINGER的这种操做模式是由linger结构来控制的:
struct linger {
int l_onoff;
int l_linger;
};
成员l_onoff为一个布尔值,非零值表示TRUE,而零则表示FALSE。这个选项的三个值描述以下:
1 设置l_onoff为FALSE使得成员l_linger被忽略,而使用默认的close行为。也就是说,close调用会当即返回给调用者,若是可能将会传输任何未发送的数据。
2 设置l_onoff为TRUE将会使得成员l_linger的值变得重要。当l_linger非零时,这表明应用在close函数调用上的以秒计的超时时 限。若是超时发生以前,有未发送的数据而且成功关闭,函数将会成功返回。不然,将会返回错误,而将变量errno的值设置为EWOULDBLOCK。
3 将l_onoff设置为TRUE而l_linger设置为零时使得链接停止,在调用close时任何示发送的数据都会丢弃。
我 们也许但愿获得一些建议,在咱们的程序中使用SO_LINGER选项,而且提供一个合理的超时时限。而后,能够检测由close函数的返回值来肯定链接是 否成功关闭。若是返回了一个错误,这就告知咱们的程序也许远程程序并不能接收咱们发送的所有数据。相对的,他也许仅意味着链接关闭时发生的问题。
然 而,咱们必须保持清醒,这样的方法在一些服务器设计中会产生新的问题。当在close函数调用上将SO_LINGER选项配置为超时(linger),当 咱们的服务器在close函数调用中执行超时时会阻止其余的客户端进行服务。若是咱们正在一个进程中服务多个客户端进程时就会存在这个问题。使用默认的行 为也许更为合适,由于这容许close函数当即返回。而任何未发送的数据也会为内核继续发送。
最后,若是程序或是服务器知道链接应什么时候停止时可使用停止行为。这也许适用于当服务器认为没有访问权限的用户正试着进行访问的状况。这种状况下的客户并不会获得特别的关注。
下面的代码是一个使用SO_LINGER选项的例子,使用30秒的超时时限:
spa
1 #define TRUE 1 2 #define FALSE 0 3 int z; int s; 4 struct linger so_linger; 5 ... 6 so_linger.l_onoff = TRUE; 7 so_linger.l_linger = 30; 8 z = setsockopt(s, 9 SOL_SOCKET, 10 SO_LINGER, 11 &so_linger, 12 sizeof so_linger); 13 if ( z ) 14 perror("setsockopt(2)");
下面的例子显示了如何设置SO_LINGER的值来停止套接口s上的当前链接:
设计
1 #define TRUE 1 2 #define FALSE 0 3 int z; 4 int s; 5 struct linger so_linger; 6 ... 7 so_linger.l_onoff = TRUE; 8 so_linger.l_linger = 0; 9 z = setsockopt(s, 10 SOL_SOCKET, 11 SO_LINGER, 12 &so_linger, 13 sizeof so_linger); 14 if ( z ) 15 perror("setsockopt(2)"); 16 close(s);
在上面的这个例子中,当调用close函数时,套接口s会当即停止。停止的语义是经过将超时值设置为0来实现的。
设置SO_KKEPALIVE选项
当使用链接时,有时他们会空闲至关长的时间。例如,创建一个telnet会话经过访问股票交易服务。他也许会执行一些初始的查询,而后离开链接而保持服务打开,由于他但愿回来查询更多的内容。然而,同时链接处理空闲状态,也许一次就是一个小时。
任 何一个服务器认为他有一个链接的客户时会为其分配相应的资源。若是服务器是一个派生类型(fork),那么整个Linux进程及其相应的内存都分配给这个 客户。若是事情顺利,这个场景并不会产生任何问题。然而当出现网络崩溃时,困难出现了,咱们全部的578个客户都会从咱们的股票交易服务中失去链接。
在网络服务恢复后,578个客户会试着链接到咱们的服务器,重建链接。这对于咱们来讲是一个真实的问题,由于咱们的服务器在以前并无意识到他失去了空闲客户--SO_KKEPALIVE来解决这个问题。
下面的例子显示了如何在套接口s上使用SO_KKEPALIVE选项,从而一个断开的空闲链接能够被检测到:
1 #define TRUE 1 2 #define FALSE 0 3 int z; int s; 4 int so_keepalive; 5 ... 6 so_keepalive = TRUE; 7 z = setsockopt(s, 8 SOL_SOCKET, 9 SO_KEEPALIVE, 10 &so_keepalive, 11 sizeof so_keepalive); 12 if ( z ) 13 perror("setsockopt(2)");
在上面的例子中设置了SO_KEEPALIVE选项,这样当套接口链接空闲至关长的时间时,一个探测信息(probe message)就会发送到远程端。这一般是在两个小时的无活动后完成的。对于一个保持活动的探测信息会有三个可能的反应。他们分别是:
1 端会合适的返回代表一切正常。并无向程序返回任何指示信息,由于这是程序假定的开始。
2 端响应代表他对链接一无所知。这代表端自上次通讯之后与主机进行从新链接。这样当下次套接口操做时会向程序返回ECONNRESET错误代码。
3 端没有响应。在这种状况下,内核也许作了几回尝试进行链接。若是没有响应请求,TCP一般会在大约11分钟内放弃。当这种状况发生时,在下次套接口操做时会返回ETIMEOUT错误。其余的错误,例如EHOSTUNREACH会在网络再也不能到达主机时返回。
SO_KEEPALIVE 所调用的时间框架会限制他一般的用处。探测信息也只在大约两个小时的无活动后才会发送。而后,当没有响应时,在链接返回错误时还须要另外的11分钟。不管 怎样,这个选项确实容许探测空闲的无链接套接口,而后由服务器进行关闭。相应的,支持长空闲链接的服务器应容许这个特征。
设置SO_BROADCAST选项
咱们如今尚未讨论到使用UDP进行广播的主题。然而,咱们很容易意识到广播功能的误用以及所形成的网络灾难。为了不在没有计划广播时进行广播,套接口禁用了广播功能。若是确实须要广播,那么C程序员要为套接口的这个功能处理相应的麻烦。
SO_BROADCAST是一个布尔标志选项,由int数据类型进行设置。下面的例子显示了如何设置SO_BROADCAST选项:
1 #define TRUE 1 2 #define FALSE 0 3 int z; 4 int s; 5 int so_broadcast; 6 ... 7 so_broadcast = TRUE; 8 z = setsockopt(s, 9 SOL_SOCKET, 10 SO_BROADCAST, 11 &so_broadcast, 12 sizeof so_broadcast); 13 if ( z ) 14 perror("setsockopt(2)");
若是要setsockopt函数返回零,套接口s已经容许进行广播。然而在这里要注意的是所选用的套接口类型必须具备广播功能,例如UDP套接口。
设置SO_OOBINLINE选项
在一些状况下,已发送的数据也许会超过所限制的数据量。一般,这些越界的数据是用不一样于一般的数据接收函数来进行接收的。然而有时却更喜欢使用一般的方式来接收这些越界数据。当选择这种方法时,越界数据做为一般数据流的一部分在一般数据以前到达。
要使用这个特征,咱们能够用下面的代码来完成:
1 #define TRUE 1 2 #define FALSE 0 3 int z; 4 int s; 5 int so_oobinline; 6 ... 7 so_oobinline = TRUE; 8 z = setsockopt(s, 9 SOL_SOCKET, 10 SO_OOBINLINE, 11 &so_oobinline, 12 sizeof so_oobinline); 13 if ( z ) 14 perror("setsockopt(2)");
在设置了SO_OOBINLINE选项以后,越界数据就会与一般数据一块儿接收。在这种方式下,所接收的越界数据与一般数据相同。
SO_PASSCRED与SO_PEERCRED选项 这些选项仅适用于PF_UNIX(PF_LOCAL)套接口。这些选项用来在当前主机的本地套接口上控制与传递凭证。这也许是最难掌握的一个主题。就如今而言,咱们只须要简单的注意到,若是咱们但愿编写服务本地主机客户的服务程序时,咱们也许会对这两个选项感兴趣。