linux网络程序设计——3 socket属性设置

5 linux socket网络编程之socket属性

5.1函数用法

#include <sys/types.h>         
#include <sys/socket.h>
int getsockopt(int sockfd, int level, intoptname, void *optval, socklen_t *optlen);
//函数用于获得某个套接字的属性
int setsockopt(intsockfd, int level, int optname, const void *optval, socklen_t optlen);

//允许限制某个套接字的属性。


5.2函数返回说明

成功执行时,返回0。

失败时,返回-1,并设置errno。

EBADF

The socket argument is not a valid file descriptor。sock不是有效的文件描述符

EINVAL

optlen invalid in setsockopt().  In some cases this error can also occur for an invalid value in optval (e.g., for the IP_ADD_MEMBERSHIP option described in ip(7)). 在调用setsockopt()时,optlen无效

ENOPROTOOPT

The option is unknown at the level indicated.指定的协议层不能识别选项

ENOTSOCK

The file descriptor sockfd does not refer to a socket. sock描述的不是套接字

EFAULT

The address pointed to by optval is not in a valid part of the process address space. For getsockopt(), this error may also be returned if optlen is not in a valid part of the process address space.optval指向的内存并非有效的进程空间

5.3参数说明

int sockfd

套接字描述符(必须指向一个打开的套接字描述符),即获取或者设置哪一个socket的属性。  

int level

指定控制套接字属性的分类,即指定系统中解析选项的代码,或为通用套接字代码。或为某个特定于协议的代码(例如IPv4、IPv6、TCP或SCTP),标识某个协议级别。目前linux中level可以取如下所示多种值:

//定义于sys/socket.h

SOL_SOCKET

Options to be accessed at socket level, not protocol level.通用套接字选项

// 定义于netinet/in.h

IPPROTO_IP

Internet protocol.

IPPROTO_IPV6

Internet Protocol Version 6.

IPPROTO_ICMP

Control message protocol.

IPPROTO_RAW

Raw IP Packets Protocol.

IPPROTO_TCP

Transmission control protocol.

IPPROTO_UDP

User datagram protocol.

 int optname

指定控制的参数,即在某个特定level级别下的选项。



参考:

《linux高级程序设计》

void *optval

获得或者是设置套接字选项值,根据选项名称的数据类型进行转换。对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。

 

套接字的选项粗分为两大基本类型:
Ø  一是启用或禁止某个特性的二元选项(称为标志选项)。
Ø  二是取得并返回我们可以设置或检查的特定值的选项(称为值选项)。

上图中标有"标志"的列指出一个选项是否为标志选项。当给这些标志选项调用getsokopt函数时,*optval是一个整数,*optval中返回的值为0表示相应选项被禁止,不为0表示选项被启用。类似地,setsockopt函数需要一个不为0的*optval值来启用选项,一个为0的*optval值来禁止选项。如果上图中“标志”列不含有“·”,那么相应选项用于在用户进程和系统之间传递所指定数据类型的值。

 例如:设置禁用Nagle算法

sockfd = socket(AF_INET, SOCK_STREAM, 0); 

flag = 1;    

int ret = setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (void *)&flag, sizeof(flag)); 

if (ret == -1) {    

    printf("Couldn't setsockopt(TCP_NODELAY)\n");    

socklen_t *optlen / socklen_t optlen

含义是缓冲区大小,setsockopt函数指定*optval中值存储大小(传值),getsockopt返回查询的属性值的长度(穿指针)。对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。

5.4 setsockopt()使用说明

使用环境Ubuntu 10.04

5.4.1 SO_SNDBUF/SO_RCVBUF设置发送缓存区和接收缓存区大小

int sndbuf_size=64*1024;//设置为64k 

int rcvbuf_size=64*1024;

setsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,(const char *)&sndbuf_size,sizeof(int));

setsockopt(sock_fd,SOL_SOCKET,SO_RCVBUF,(const char *)&rcvbuf_size,sizeif(int));

注意:

1)缺省值sndbuf_size=16k,recvbuf_size=85.3k。

2)在缓存区允许设置的最大值内,缓存区的大小会设置为sndbuf_size/recvbuf_size的2倍(128k)。

3)缓存区允许设置的最大值为262142Bytes(约256k)。

当设置TCP套接口接收缓冲区的大小时,函数调用顺序是很重要的,因为TCP的窗口规模选项是在建立连接时用SYN与对方互换得到的。对于客户,O_RCVBUF选项必须在connect之前设置对于服务器,SO_RCVBUF选项必须在listen前设置

结合原理说明:
1) 每个套接口都有一个发送缓冲区和一个接收缓冲区。 接收缓冲区被TCP和UDP用来将接收到的数据一直保存到由应用进程来读。 TCP:TCP通告另一端的窗口大小。 TCP套接口接收缓冲区不可能溢出,因为对方不允许发出超过所通告窗口大小的数据。这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。 UDP:当接收到的数据报装不进套接口接收缓冲区时,此数据报就被丢弃。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的UDP丢弃数据报。
2) 我们经常听说tcp协议的三次握手,但三次握手到底是什么,其细节是什么,为什么要这么做呢?
         第一次:客户端发送连接请求给服务器,服务器接收;
         第二次:服务器返回给客户端一个确认码,附带一个从服务器到客户端的连接请求,客户机接收,确认客户端到服务器的连接.
         第三次:客户机返回服务器上次发送请求的确认码,服务器接收,确认服务器到客户端的连接.
 tcp协议的特点:连接的,可靠的,全双工的,实际上tcp的三次握手正是为了保证这些特性的实现.tcp的每个连接都需要确认,客户端到服务器和服务器到客户端的连接是独立的.

 注:

<1>如果在发送数据的时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响程序的性能,可将发送缓冲区大小设置为0:

int nZero=0;

setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));

<2>同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):

int nZero=0;

setsockopt(socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));

5.4.2 SO_SNDTIMEO/SO_RCVTIMEO设置发送和接收的超时时间

在TCP连接中,recv等函数默认为阻塞模式(block),即直到有数据到来之前函数不会返回,而我们有时则需要一种超时机制使其在一定时间后返回而不管是否有数据到来。

struct timeval set_time;

set_time.tv_sec=1;

set_time.tv_usec=0;

setsockopt(sock_fd,SOL_SOCKET,SO_SNDTIMEO,&set_time,sizeof(struct timeval));

setsockopt(sock_fd,SOL_SOCKET,SO_RCVTIMEO,&set_time,sizeof(struct timeval));

struct timeval{

    time_t tv_sec;

    time_t tv_usec;

};

注意:

1)对于阻塞态系统调用send(sock_fd,snd_buf,64,0)/ recv(sock_fd,rcv_buf,64,0),setsockopt来设定超时时间是有效的。设定了recv()函数的超时机制,当超过tv_out设定的时间而没有数据到来时recv()就会返回0值。

2)对于非阻塞态系统调用send(sock_fd,snd_buf,64,MSG_DONTWAIT)/ recv(sock_fd,rcv_buf,64,MSG_DONTWAIT),setsockopt来设定超时时间后,recv接收不到数据,立即返回一个错误信息。ps:非阻塞态超时时间如何正确设定还不是很清楚。

5.4.3 TCP_NODELAY设置禁用Nagle算法

const int chOpt = 1;

setsockopt(sock_fd,IPPROTO_TCP,TCP_NODELAY,&chOpt,sizeof(int))

注意:

Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。默认情况下,发送数据采用Nagle 算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Negale 算法。

Nagle算法通过将未确认的数据存入缓冲区直到蓄足一个包一起发送的方法,来减少主机发送的零碎小数据包的数目。但对于某些应用来说,这种算法将降低系统性能。所以TCP_NODELAY可用来将此算法关闭。应用程序编写者只有在确切了解它的效果并确实需要的情况下,才设置TCP_NODELAY选项,因为设置后对网络性能有明显的负面影响。TCP_NODELAY是唯一使用IPPROTO_TCP层的选项,其他所有选项都使用SOL_SOCKET层。
         如果设置了SO_DEBUG选项,套接口供应商被鼓励(但不是必需)提供输出相应的调试信息。但产生调试信息的机制以及调试信息的形式已超出本规范的讨论范围。
         setsockopt()支持下列选项。其中“类型”表明optval所指数据的类型。

 参考:

关于Nagle算法更多细节请参考 http://blog.csdn.net/ithzhang/article/details/8520026
关于设定socket属性的更多细节请参考 http://blog.chinaunix.net/uid-24517549-id-4044883.html,本文只对主要关注的几点进行说明。

 5.4.4 SO_REUSEADDR/SO_REUSEPORT设置地址复用

5.4.4.1 SO_REUSEADDR和SO_REUSEPORT异同

         虽然不同的系统上socket的实现方式有一些差异,但都来源于对BSD socket的实现,因此在讨论其它系统之前了解BSD socket的实现是非常有益的。首先我们需要了解一些基本知识,一个TCP/UDP连接是被一个五元组确定的:
         {源IP地址,源端口,目的IP地址,目的端口,和传输层协议}
         因此,任何两个连接都不可能拥有相同的五元组,否则系统将无法区别这两个连接。
         当使用socket()函数创建套接字的时候,我们就指定了该套接字使用的protocol(协议),bind()函数设置了源地址和源端口号,而目的地址和目的端口号则由connect()函数设定。尽管允许对UDP进行"连接"(在某些情况下这对应用程序的设计非常有帮助)但由于UDP是一个无连接协议,UDP套接字仍然可以不经连接就使用。"未连接"的UDP套接字在数据被第一次发送之前并不会绑定,只有在发送的时候被系统自动绑定,因此未绑定的UDP套接字也就无法收到(回复)数据。未绑定的TCP也一样,它将在连接的时候自动绑定。
         如果你明确绑定一个socket,把它绑定到端口0是可行的,它意味着"anyport"("任意端口")。由于一个套接字无法真正的被绑定到系统上的所有端口,那么在这种情况下系统将不得不选择一个具体的端口号(指的是"any port")。源地址使用类似的通配符,也就是"any address" (IPv4中的0.0.0.0和IPv6中的::)。和端口不同的是,一个套接字可以被绑定到任意地址(any address),这里指的是本地网络接口的所有地址。由于socket无法在连接的时候同时绑定到所有源IP地址,因此当接下来有一个连接过来的时候,系统将不得不挑选一个源IP地址。考虑到目的地址和路由表中的路由信息,系统将会选择一个合适的源地址,并将任意地址替换为一个选定的地址作为源地址。        

默认情况下,任意两个socket都无法绑定到相同的源IP地址和源端口(即源地址和源端口号均相同)。只要源端口号不相同,那么源地址实际上没什么关系。将socketA绑定到地址A和端口X (A:X),socketB绑定到地址B和端口Y (B:Y),只要X != Y,那么这种绑定都是可行的。然而当X==Y的时候只要A != B,这种绑定方式也仍然可行,比如:一个FTP server的socketA绑定为192.168.0.1:21而属于另一个FTP server的socketB绑定为 10.0.0.1:21,这两个绑定都将成功。记住:一个socket可能绑定到本地"any address"。例如一个socket绑定为0.0.0.0:21,那么它同时绑定了所有的本地地址,在这种情况下,不论其它的socket选择什么特定的IP地址,它们都无法绑定到21端口,因为0.0.0.0和所有的本地地址都会冲突。
         上面说的对所有主流操作系统都是一样的。当涉及到地址重用的时候,OS之间的差异就显现出来了,正如之前所说的那样,其它的实现方案都来源于BSD的实现,因此我们首先从BSD说起。

 

BSD定义

(1) SO_REUSEADDR

         如果在绑定一个socket之前设置了SO_REUSEADDR,除非两个socket绑定的源地址和端口号都一样,那么这两个绑定都是可行的。也许你会疑惑这跟之前的有什么不一样?关键是SO_REUSEADDR改变了在处理源地址冲突时对通配地址("any ip address")的处理方式。
         当没有设置SO_REUSEADDR的时候,socketA先绑定到0.0.0.0:21,然后socketB绑定到192.168.0.1:21的时候将会失败(EADDRINUSE错误),因为0.0.0.0意味着"任意本地IP地址”,也就是"所有本地IP地址“,因此包括192.168.0.1在内的所有IP地址都被认为是已经使用了。但是在设置SO_REUSEADDR之后socketB的绑定将会成功,因为0.0.0.0和192.168.0.1事实上不是同一个IP地址,一个是代表所有地址的通配地址,另一个是一个具体的地址。注意上面的表述对于socketA和socketB的绑定顺序是无关的,没有设置SO_REUSEADDR,它们将失败,设置了SO_REUSEADDR,它将成功。


下面给出了一个表格列出了所有的可能组合:

SO_REUSEADDR       socketA          socketB             Result

------------------------------------------------------------------------------------------------------------------------------------

  ON/OFF         192.168.0.1:21    192.168.0.1:21    Error (EADDRINUSE)

  ON/OFF         192.168.0.1:21      10.0.0.1:21       OK

  ON/OFF          10.0.0.1:21      192.168.0.1:21      OK

   OFF             0.0.0.0:21       192.168.1.0:21     Error (EADDRINUSE)

   OFF            192.168.1.0:21      0.0.0.0:21       Error (EADDRINUSE)

   ON             0.0.0.0:21        192.168.1.0:21     OK

   ON             192.168.1.0:21      0.0.0.0:21       OK

  ON/OFF          0.0.0.0:21          0.0.0.0:21       Error (EADDRINUSE)

上面的表格假定socketA已经成功绑定,然后创建socketB绑定给定地址在是否设置SO_REUSEADDR的情况下的结果。Result代表socketB的绑定行为是否会成功。如果第一列是ON/OFF,那么SO_REUSEADDR的值将是无关紧要的。
         现在我们知道SO_REUSEADDR对通配地址有影响,但这不是它唯一影响到的方面。还有一个众所周知的影响同时也是大多数人在服务器程序上使用SO_REUSEADDR的首要原因。为了了解其它SO_REUSEADDR重要的使用方式,我们需要深入了解TCP协议的工作方式。
         一个socket有一个发送缓冲区,当调用send()函数成功后,这并不意味着所有数据都真正被发送出去了,它只意味着数据都被送到了发送缓冲区中。对于UDP socket来说,如果不是立刻发送的话,数据通常也会很快的发送出去,但对于TCP socket,在数据加入到缓冲区和真正被发送出去之间的时延会相当长。这就导致当我们close一个TCP socket的时候,可能在发送缓冲区中保存着等待发送的数据(由于send()成功返回,因此你也许认为数据已经被发送了)。如果TCP的实现是立刻关闭socket,那么所有这些数据都会丢失而你的程序根本不可能知道。TCP被称为可靠协议,像这种丢失数据的方式就不那么可靠了。这也是为什么当我们close一个TCP socket的时候,如果它仍然有数据等待发送,那么该socket会进入TIME_WAIT状态。这种状态将持续到数据被全部发送或者发生超时。
         在内核彻底关闭socket之前等待的总时间(不管是否有数据在发送缓冲区中等待发送)叫做Linger Time。Linger Time在大部分系统上都是一个全局性的配置项而且在默认情况下时间相当长(在大部分系统上是两分钟)。当然对于每个socket我们也可以使用socket选项SO_LINGER进行配置,可以将等待时间设置的更长一点儿或更短一点儿甚至禁用它。禁用Linger Time绝对是一个坏主意,虽然优雅的关闭socket是一个稍微复杂的过程并且涉及到来回的发送数据包(以及在数据包丢失后重发它们),并且这个过程还受到Linger Time的限制。如果禁用Linger Time,socket可能丢失的不仅仅是待发送的数据,而且还会粗暴的关闭socket,在绝大部分情况下,都不应该这样使用。如何优雅的关闭TCP连接的细节不在这里进行讨论,如果你想了解更多,我建议你阅读:http://www.freesoft.org/CIE/Course/Section4/11.htm。而且如果你用SO_LINGER禁用了Linger Time,而你的程序在显式的关闭socket之前就终止的话,BSD(其它的系统也有可能)仍然会等待,而不管已经禁用了它。这种情况的一个例子就是你的程序调用了exit() (在小的服务器程序很常见)或者进程被信号杀死(也有可能是进程访问了非法内存而终止)。这样的话,不管在什么情况下,你都无法对某一个socket禁用linger了。
         问题在于,系统是怎样看待TIME_WAIT状态的?如果SO_REUSEADDR还没有设置,一个处在TIME_WAIT的socket仍然被认为绑定在源地址和端口,任何其它的试图在同样的地址和端口上绑定一个socket行为都会失败直到原来的socket真正的关闭了,这通常需要等待Linger Time的时长。所以不要指望在一个socket关闭后立刻将源地址和端口绑定到新的socket上,在绝大部分情况下,这种行为都会失败。然而,在设置了SO_REUSEADDR之后试图这样绑定(绑定相同的地址和端口)仅仅只会被忽略,而且你可以将相同的地址绑定到不同的socket上。注意当一个socket处于TIME_WAIT状态,而你试图将它绑定到相同的地址和端口,这会导致未预料的结果,因为处于TIME_WAIT状态的socket仍在"工作",幸运的是这种情况极少发生。
         对于SO_REUSEADDR你需要知道的最后一点是只有在你想绑定的socket开启了地址重用(address reuse)之后上面的才会生效,不过这并不需要检查之前已经绑定或处于TIME_WAIT的socket在它们绑定的时候是否也设置这个选项。也就是说,绑定的成功与否只会检查当前bind的socket是否开启了这个标志,不会查看其它的socket。

 (2)SO_REUSEPORT

         SO_REUSEPORT允许你将多个socket绑定到相同的地址和端口只要它们在绑定之前都设置了SO_REUSEPORT。如果第一个绑定某个地址和端口的socket没有设置SO_REUSEPORT,那么其他的socket无论有没有设置SO_REUSEPORT都无法绑定到该地址和端口直到第一个socket释放了绑定。设定了SO_REUSEPORT就等于已经设定了SO_REUSEADDR。

         SO_REUSEPORT并不表示SO_REUSEADDR。这意味着如果一个socket在绑定时没有设置SO_REUSEPORT,那么同预期的一样,其它的socket对相同地址和端口的绑定会失败,但是如果绑定相同地址和端口的socket正处在TIME_WAIT状态,新的绑定也会失败。当有个socket绑定后处在TIME_WAIT状态(释放时)时,为了使得其它socket绑定相同地址和端口能够成功,需要设置SO_REUSEADDR或者在这两个socket上都设置SO_REUSEPORT。当然,在socket上同时设置SO_REUSEPORT和SO_REUSEADDR也是可行的。

         关于SO_REUSEPORT除了它在被添加到系统的时间比SO_REUSEADDR晚就没有其它需要说的了,这也是为什么在有些系统的socket实现上你找不到这个选项,因为这些系统的代码都是在这个选项被添加到BSD之前fork了BSD,这样就不能将两个socket绑定到真正相同的“地址” (address+port)。

 (3)Connect() Returning EADDRINUSE?

         绝大部分人都知道bind()可能失败返回EADDRINUSE,然而当你开始使用地址重用(addressreuse),你可能会碰到奇怪的情况:connect()失败返回同样的错误EADDRINUSE。怎么会出现这种情况了?

一个远端地址(remoteaddress)毕竟是connect添加到socket上的,怎么会已经被使用了? 将多个socket连接到相同的远端地址从来没有出现过这样的情况,这是为什么了?

         正如我在开头说过的,一个连接是被一个五元组定义的。同样我也说了任意两个连接的五元组不能完全一样,因为这样的话内核就没办法区分这两个连接了。然而,在地址重用的情况下,你可以把同协议的两个socket绑定到完全相同的源地址和源端口,这意味着五元组中已经有三个元素相同了(协议,源地址,源端口)。如果你尝试将这些socket连接到同样的目的地址和目的端口,你就创建了两个完全相同的连接。这是不行的,至少对TCP不行(UDP实际上没有真实的连接)。如果数据到达这两个连接中的任何一个,那么系统将无法区分数据到底属于谁。因此当源地址和源端口相同时,目的地址或者目的端口必须不同,否则内核无法进行区分,这种情况下,connect()将在第二个socket尝试连接时返回EADDRINUSE。

 (4)Multicast Address(多播地址)

         大部分人都会忽略多播地址的存在,但它们的确存在。单播地址(unicast address)用于单对单通信,多播地址用于单对多通信。大部分人在他们学习了IPv6后才注意到多播地址的存在,但在IPv4中多播地址就有了,尽管它们在公共互联网上用的并不多。

         对多播地址来说,SO_REUSEADDR的含义发生了改变,因为它允许多个socket绑定到完全一样的多播地址和端口,也就是说,对多播地址SO_REUSEADDR的行为与SO_REUSEPORT对单播地址完全一样。事实上,对于多播地址,对SO_REUSEADDR和SO_REUSEPORT的处理完全一样,对所有多播地址,SO_REUSEADDR也就意味着SO_REUSEPORT。

 (5)编写 TCP/SOCK_STREAM 服务程序时,SO_REUSEADDR到底什么意思?

         这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。

         一个套接字由相关五元组构成,协议、本地地址、本地端口、远程地址、远程端口。SO_REUSEADDR 仅仅表示可以重用本地本地地址、本地端口,整个相关五元组还是唯一确定的。所以,重启后的服务程序有可能收到非期望数据。必须慎重使用SO_REUSEADDR 选项。

 (6)各个系统上地址复用选项的异同

FreeBSD/OpenBSD/NetBSD

         它们都是很晚的时候衍生自原生BSD的系统,它们与原生BSD的选项和行为都一样。

MacOS X

MacOS X的内核就是一个BSD类型的UNIX,基于很新的BSD代码,甚至Mac OS 10.3的发布与FreeBSD 5都是同步的,因此MacOS与BSD一样提供相同的选项,处理行为也一样。

IOS

         IOS只是在内核上稍微修改了MacOS,因此选项和处理行为也和MacOS一样。

Linux

     在linux 3.9之前,只存在选项SO_REUSEADDR。除了两个重要的差别,大体上与BSD一样。第一个差别:当一个监听(listening)TCP socket绑定到通配地址和一个特定的端口,无论其它的socket或者是所有的socket(包括监听socket)都设置了SO_REUSEADDR,其它的TCP socket都无法绑定到相同的端口(BSD中可以),就更不用说使用一个特定地址了。这个限制并不用在非监听TCP socket上,当一个监听socket绑定到一个特定的地址和端口组合,然后另一个socket绑定到通配地址和相同的端口,这样是可行的。第二个差别: 当把SO_REUSEADDR用在UDP socket上时,它的行为与BSD上SO_REUSEPORT完全相同,因此两个UDP socket只要都设置了SO_REUSEADDR,那么它们可以绑定到相同的地址和端口。

    Linux 3.9加入了SO_REUSEPORT。这个选项允许多个socket(TCPor UDP)不管是监听socket还是非监听socket只要都在绑定之前都设置了它,那么就可以绑定到完全相同的地址和端口。为了阻止"port 劫持"(Port hijacking)有一个特别的限制:所有希望共享源地址和端口的socket都必须拥有相同的有效用户id(effectiveuser ID)。因此一个用户就不能从另一个用户那里"偷取"端口。另外,内核在处理SO_REUSEPORT socket的时候使用了其它系统上没有用到的"特别魔法":对于UDP socket,内核尝试平均的转发数据报,对于TCP监听socket,内核尝试将新的客户连接请求(由accept返回)平均的交给共享同一地址和端口的socket(监听socket)。这意味着在其他系统上socket收到一个数据报或连接请求或多或少是随机的,但是linux尝试优化分配。例如:一个简单的服务器程序的多个实例可以使用SO_REUSEPORT socket实现一个简单的负载均衡,因为内核已经把复制的分配都做了。

Android

         尽管整个Android系统与大多数linux发行版都不一样,但是它的内核是个稍加修改的linux内核,因此它的SO_REUSEADDR和SO_REUSEPORT与linux一样。

Windows

         windows上只有SO_REUSEADDR选项,没有SO_REUSEPORT。在windows上设置了SO_REUSEADDR的socket其行为与BSD上设定了SO_REUSEPORT和SO_REUSEADDRd的行为大致一样,只有一个差别:一个设置了SO_REUSEADDR的socket总是可以绑定到已经被绑定过的源地址和源端口,不管之前在这个地址和端口上绑定的socket是否设置了SO_REUSEADDR没有。这种行为在某种程度上有些危险因为它允许一个应用程序从别的应用程序上"偷取"已连接的端口。不用说,这对安全性有极大的影响,Microsoft意识到了这个问题,就加入了另一个socket选项: SO_EXECLUSIVEADDRUSE。设置了SO_EXECLUSIVEADDRUSE的socket确保一旦绑定成功,那么被绑定的源端口和地址就只属于这一个socket,其它的socket不能绑定,甚至他们使用了SO_REUSEADDR也没用。

Solaris

         Solaris是SunOS的后羿,SunOS起源于BSD,SunOS 5和之后的版本则基于SVR4,然而SVR4是BSD,System V和Xenix的集合体,所以从某种程度上说,Solaris也是BSD的分支,而且是相当早的一个分支。这就导致了Solaris只有SO_REUSEADDR而没有SO_REUSEPORT。Solaris上SO_REUSEADDR的行为与BSD的非常相似。从我知道的来看,在Solaris上没办法实现SO_REUSEPORT的行为,也就是说,想把两个socket绑定到相同的源地址和端口上是不可能的。

         与Windows类似,Solaris也有一个选项提供互斥绑定,这个选项叫SO_EXCLBIND。如果在一个socket在绑定之前设置这个选项,那么在其他的socket上设置SO_REUSEADDR将没有任何影响。比如socketA绑定了一个通配地址,socketB设置了SO_REUSEADDR并且绑定到一个非通配地址和相同的端口,那么这个绑定将成功,除非socketA设置了SO_EXCLBIND,在这种情况下,socketB的绑定将失败不管它是否设定了SO_REUSEADDR。

参考资料:

http://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t

三次握手示意图

四次挥手示意图

<实例>

close socket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:

BOOL bReuseaddr=TRUE;

setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));

 注:缺省条件下,一个套接口不能与一个已在使用中的本地地址捆绑(参见bind())。但有时会需要“重用”地址。因为每一个连接都由本地地址和远端地址的组合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。为了通知套接口实现不要因为一个地址已被一个套接口使用就不让它与另一个套接口捆绑,应用程序可在bind()调用前先设置SO_REUSEADDR选项。请注意仅在bind()调用时该选项才被解释,故此无需(但也无害)将一个不会共用地址的套接口设置该选项,或者在bind()对这个或其他套接口无影响情况下设置或清除这一选项。

5.4.5 SO_BROADCAST设置广播选项

一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:

BOOL bBroadcast=TRUE;

setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));

 5.4.6 SO_CONDITIONAL_ACCEPT设置连接延时(系统不一定实现)

在client连接服务器过程中,如果处于非阻塞模式下的socket在connect()的过程中可以设置connect()延时,直到accpet()被呼叫(本函数设置只有在非阻塞的过程中有显著的作用,在阻塞的函数调用中作用不大)

BOOL bConditionalAccept=TRUE;

setsockopt(s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));

 5.4.7 SO_DONTLINGER/SO_LINGER设置延迟关闭

<1>如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历TIME_WAIT的过程:

BOOL bDontLinger = FALSE;

setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));

 <2>如果在发送数据的过程中延迟写send()没有完成,任有数据没发送而调用了closesocket()(延迟关闭),传统一般采取的措施是“从容关闭”shutdown(s,SD_BOTH),但是数据是肯定丢失了,如何设置让程序满足具体应用的要求,即让没发完的数据发送出去后再关闭socket?

struct linger {

  u_short l_onoff;

  u_short l_linger;

};

linger m_sLinger;

m_sLinger.l_onoff=1; //closesocket()调用,但是还有数据没发送完毕的时候容许逗留,如果m_sLinger.l_onoff=0则功能和<1>作用相同;

m_sLinger.l_linger=5;//容许逗留的时间为5

setsockopt(s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));

 注:

两种套接口的选项:一种是布尔型选项,允许或禁止一种特性;另一种是整形或结构选项。允许一个布尔型选项,则将optval指向非零整形数;禁止一个选项optval指向一个等于零的整形数。对于布尔型选项,optlen应等于sizeof(int);对其他选项,optval指向包含所需选项的整形数或结构,而optlen则为整形数或结构的长度。SO_LINGER选项用于控制下述情况的行动:套接口上有排队的待发送数据,且closesocket()调用已执行。参见closesocket()函数中关于SO_LINGER选项对closesocket()语义的影响。应用程序通过创建一个linger结构来设置相应的操作特性:

struct linger {

  int l_onoff;

  int l_linger;

};

为了允许SO_LINGER,应用程序应将l_onoff设为非零,将l_linger设为零或需要的超时值(以秒为单位),然后调用setsockopt()。为了允许SO_DONTLINGER(亦即禁止SO_LINGER),l_onoff应设为零,然后调用setsockopt()。

 5.4.8 SO_KEEPALIVE

         一个应用程序可以通过打开SO_KEEPALIVE选项,使得套接口实现在TCP连接情况下允许使用“保持活动”包。一个套接口实现并不是必需支持“保持活动”,但是如果支持的话,具体的语义将与实现有关。

首先,我想说的是,SO_KEEPALIVE是实现在服务器侧,客户端被动响应,缺省超时时间为120分钟,这是RFC协议标准规范。SO_KEEPALIVE是实现在TCP协议栈(四层),应用层的心跳实现在第七层,本质没有任何区别,但应用层需要自己来定义心跳包格式。之所以实现在服务器侧,是因为与客户端相比,服务器侧的寿命更长,因为服务器侧需要不间断地提供服务,而客户端可能由于用户下班而合上电脑(TCP没有来得及发送FIN关闭连接),这样的话,服务器侧就会有很多不可用的TCP连接(established),这样的连接依然会占用服务器内存资源,于是就设计这个keepalive 来检测客户端是否可用,如果几次重传keepalive ,客户端没有相应,删除连接,释放资源。需要指出的是,超时时间是指TCP连接没有任何数据、控制字传输的时间,如果有任何数据传输,会刷新定时器,重新走表。TCP心跳是一个备受争议的实现,只是一个option,不是强制标准。之所以应用层需要独立实现自己的心跳,是因为超时时间较长,无法给应用层提供快速的反馈。所以类似BGP协议就独立实现了自己的keepalive,最小可以设置一秒钟,三次没有应答即可以Reset连接,最快三秒可以检测到失效。而三秒依然太慢,可以用另外一个协议BFD来提供更快发现链路失效,最快可以配置成10ms,三次超时(30ms)就可以完成失效检测。

 缺点:

l  keepalive只能检测连接是否存活,不能检测连接是否可用。比如服务器因为负载过高导致无法响应请求但是连接仍然存在,此时keepalive无法判断连接是否可用。

l  如果TCP连接中的另一方因为停电突然断网,我们并不知道连接断开,此时发送数据失败会进行重传,由于重传包的优先级要高于keepalive的数据包,因此keepalive的数据包无法发送出去。只有在长时间的重传失败之后我们才能判断此连接断开了。


  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值