setsockopt 函数功能及参数详解

1. setsockopt() 函数介绍

setsockopt() 函数用于任意类型、任意状态套接口的设置选项值,其作用和使用说明如下:

int setsockopt(SOCKET s, int level, int optname, const char FAR *optval, int optlen);

第一个参数socket是套接字描述符。第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。option_name指定准备设置的选项,option_name可以有哪些取值,这取决于level,在套接字级别上(SOL_SOCKET),option_name可以有以下取 值:

1. SO_DEBUG,打开或关闭调试信息。
    当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(10)位,或清SOCK_DBG位。
    
2. SO_REUSEADDR,打开或关闭地址复用功能。
    当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为103. SO_DONTROUTE,打开或关闭路由查找功能。
    当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。
    
4. SO_BROADCAST,允许或禁止发送广播数据。
    当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。
    
5. SO_SNDBUF,设置发送缓冲区的大小。
    发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。该操作将sock->sk->sk_sndbuf设置为val * 2,之所以要乘以2,是防止大数据量的发送,突然导致缓冲区溢出。最后,该操作完成后,因为对发送缓冲的大小 作了改变,要检查sleep队列,如果有进程正在等待写,将它们唤醒。
    
6. SO_RCVBUF,设置接收缓冲区的大小。
    接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)256字节。该操作将sock->sk->sk_rcvbuf设置为val * 27. SO_KEEPALIVE,套接字保活。
    如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时 器,否则关闭保活定时器。对于所有协议,该操
    作都会根据option_value置或清 sock->sk->sk_flag中的 SOCK_KEEPOPEN位。

8. SO_OOBINLINE,紧急数据放入普通数据流。
    该操作根据option_value的值置或清sock->sk->sk_flag中的SOCK_URGINLINE位。
    
9. SO_NO_CHECK,打开或关闭校验和。
    该操作根据option_value的值,设置sock->sk->sk_no_check。
    
10. SO_PRIORITY,设置在套接字发送的所有包的协议定义优先权。Linux通过这一值来排列网络队列。
    这个值在06之间(包括06),由option_value指定。赋给sock->sk->sk_priority。
    
11. SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。
    该选项的参数(option_value)是一个linger结构:
        struct linger {
            int   l_onoff;   
            int   l_linger;  
        };
    如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
    
12. SO_PASSCRED,允许或禁止SCM_CREDENTIALS 控制消息的接收。
    该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_PASSCRED位。
    
13. SO_TIMESTAMP,打开或关闭数据报中的时间戳接收。
    该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_RCVTSTAMP位,如果打开,则还需设sock->sk->sk_flag中的SOCK_TIMESTAMP位,同时,将全局变量netstamp_needed加114. SO_RCVLOWAT,设置接收数据前的缓冲区内的最小字节数。
    在Linux中,缓冲区内的最小字节数是固定的,为1。即将sock->sk->sk_rcvlowat固定赋值为115. SO_RCVTIMEO,设置接收超时时间。
    该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。
    
16. SO_SNDTIMEO,设置发送超时时间。
    该选项最终将发送超时时间赋给sock->sk->sk_sndtimeo。
  
17. SO_BINDTODEVICE,将套接字绑定到一个特定的设备上。
    该选项最终将设备赋给sock->sk->sk_bound_dev_if。
 
18. SO_ATTACH_FILTER和SO_DETACH_FILTER。
    关于数据包过滤,它们最终会影响sk->sk_filter。

以上所介绍的都是在SOL_SOCKET层的一些套接字选项,如果超出这个范围, 给出一些不在这一level的选项作为参数,最终会得到ENOPROTOOPT的返回值。但以上的分析仅限。

2. SO_REUSEADDR和SO_REUSEPORT参数详解

2.1 SO_REUSEADDR:

SO_REUSEADDR的使用场景:server端在调用bind函数时

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,(const void *)&reuse , sizeof(int));

目的:当服务端出现timewait状态的链接时,确保server能够重启成功。

注意:SO_REUSEADDR只有针对time-wait链接(linux系统time-wait连接持续时间为1min),确保server重启成功的这一个作用,至于网上有文章说:如果有socket绑定了0.0.0.0:port;设置该参数后,其他socket可以绑定本机ip:port。经过试验后均提示“Address already in use”错误,绑定失败。

举个例子:
server监听9980端口,由于主动关闭链接,产生了一个time-wait状态的链接:
在这里插入图片描述
如果此时server重启,并且server没有设置SO_REUSEADDR参数,server重启失败,报错:“Address already in use”

如果设置SO_REUSEADDR,重启ok;

2.2 SO_REUSEPORT:

SO_REUSEPORT使用场景:linux kernel 3.9 引入了最新的SO_REUSEPORT选项,使得多进程或者多线程创建多个绑定同一个 ip:port 的监听socket,提高服务器的接收链接的并发能力,程序的扩展性更好;此时需要设置SO_REUSEPORT(注意所有进程都要设置才生效)。

setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT,(const void *)&reuse , sizeof(int));

目的:每一个进程有一个独立的监听socket,并且bind相同的ip:port,独立的listen()和accept();提高接收连接的能力。(例如nginx多进程同时监听同一个ip:port)

解决的问题:
(1)避免了应用层多线程或者进程监听同一ip:port的“惊群效应”。
(2)内核层面实现负载均衡,保证每个进程或者线程接收均衡的连接数。
(3)只有effective-user-id相同的服务器进程才能监听同一ip:port (安全性考虑)

2.3 代码示例:server_128.c

#include <stdio.h>                                                                                                               
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
void work () {
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd < 0) {
                perror("listen socket");
                _exit(-1);
        }
        int ret = 0;
        int reuse = 1;
        ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,(const void *)&reuse , sizeof(int));
        if (ret < 0) {
                perror("setsockopt");
                _exit(-1);
        }
        ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT,(const void *)&reuse , sizeof(int));
        if (ret < 0) {
            perror("setsockopt");
            _exit(-1);
        }
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        //addr.sin_addr.s_addr = inet_addr("10.95.118.221");
        addr.sin_addr.s_addr = inet_addr("0.0.0.0");                                                                             
        addr.sin_port = htons(9980);
        ret = bind(listenfd, (struct sockaddr *)&addr, sizeof(addr));
        if (ret < 0) {
                perror("bind addr");
                _exit(-1);
       }
        printf("bind success\n");
        ret = listen(listenfd,10);
        if (ret < 0) {
                perror("listen");
                _exit(-1);
        }
        printf("listen success\n");
        struct sockaddr clientaddr;
        int len = 0;
        while(1) {
                printf("process:%d accept...\n", getpid());
                int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
                if (clientfd < 0) {
                        printf("accept:%d %s", getpid(),strerror(errno));
                        _exit(-1);
                }
                close(clientfd);
                printf("process:%d close socket\n", getpid());
        }
}

int main(){
        printf("uid:%d euid:%d\n", getuid(),geteuid());
        int i = 0;
        for (i = 0; i< 6; i++) {
                pid_t pid = fork();
                if (pid == 0) {
                        work();
                }
                if(pid < 0) {
                        perror("fork");
                        continue;
                }
        }
        int status,id;
        while((id=waitpid(-1, &status, 0)) > 0) {
                printf("%d exit\n", id);
        }
        if(errno == ECHILD) {
                printf("all child exit\n");
        }
        return 0;
}         

上述示例程序,启动了6个子进程,每个子进程创建自己的监听socket,bind相同的ip:port;独立的listen(),accept();如果每个子进程不设置SO_REUSEADDR选项,会提示“Address already in use”错误。

安全性:是不是只要设置了SO_REUSEPORT选项的服务器程序,就能够监听相同的ip:port;并获取报文呢?当然不是,当前linux内核要求后来启动的服务器程序与前面启动的服务器程序的effective-user-id要相同。

举个例子:
在这里插入图片描述
上图中的 server_127 和 server128 两个服务器程序都设置了SO_REUSEPORT,监听同一ip:port,用root用户启动两个服务器程序,因为 effective-user-id 相等,都为0,所以启动没问题。

修改server127的权限:chmod u+s server_127
在这里插入图片描述
启动完server_128后,在启动server_127,提示错误:
在这里插入图片描述

  • 10
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值