套接口选项
在前面的几章中,我们讨论了使用套接口的基础内容。现在我们要来探讨一些可用的其他的特征。在我们掌握了这一章的概念之后,我们就为后面的套接口的高级主题做好了准备。在这一章,我们将会专注于下列主题:
如何使用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选项。如下面的一段摘录的代码:
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秒的超时时限:
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)套接口。这些选项用来在当前主机的本地套接口上控制与传递凭证。这也许是最难掌握的一个主题。就现在而言,我们只需要简单的注意到,如果我们希望编写服务本地主机客户的服务程序时,我们也许会对这两个选项感兴趣。