linux内核分析及应用 -- Linux 网络层数据流分析(下)

8.3 netfilter 和 lvs

通过上一节的介绍我们已经知道,在网络中收发数据最后都会经过 netfilter 层,本节我们继续分析 netfilter 层的具体逻辑。另外,很多网络软件都是在 netfilter 层设置 hook 点来过滤网络包,比如 lvs,本节也会简单介绍。

8.3.1 netfilter

Linux 在网络层提供了一套包过滤机制,即 netfilter,它在整个网络流程的关键点上提供了一些 hook 点,便于程序员挂上特殊的钩子处理函数进行包处理的工作来实现特殊场景下的功能,比如 NAT。

我们在分析 accept 等待新的连接到来的时候,就接触过 ip_local_deliver 函数,其中的 NF_HOOK 就是 netfilter 的 hook 点。一个数据包通过 netfilter 的流程如图8-7所示。netfilter 的架构就是在整个网络流程的若干位置放置了一些检测点(hook),而在每个检测点上登记了一些处理函数进行处理(如包过滤、NAT 等,甚至可以是用户自定义的功能)。

图8-7 数据包经过 netfilter hook 点的流程

图中的 hook 点介绍如下:

  • NF_IP_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号、校验和等检测),进行目的地址转换。

  • NF_IP_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行。

  • NF_IP_FORWARD:要转发的包通过此检测点,FORWORD 包过滤在此点进行。

  • NF_IP_LOCAL_OUT:本机进程发出的包通过此检测点,OUTPUT 包过滤在此点进行。

  • NF_IP_POST_ROUTING:所有马上便要通过网络设备出去的包通过此检测点,进行源地址转换功能(包括地址伪装)在此点进行。

那么,Linux 如何来使用这些 hook 点呢?

在 IP 层代码中,有一些带有 NF_HOOK 宏的语句,例如:

NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
                net, NULL, skb, skb->dev, rt->dst.dev,
                ip_forward_finish);

我们接着来看 NF_HOOK 的实现:

static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk,
    struct sk_buff *skb,
    struct net_device *in, struct net_device *out,
    int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
    return NF_HOOK_THRESH(pf, hook, net, sk, skb, in, out, okfn, INT_MIN);
}

NF_HOOK 里其实调用的是 NF_HOOK_THRESH:

static inline int
NF_HOOK_THRESH(uint8_t pf, unsigned int hook, struct net *net, struct sock
    *sk,
        struct sk_buff *skb, struct net_device *in,
        struct net_device *out,
        int (*okfn)(struct net *, struct sock *, struct sk_buff *),
        int thresh)
{
    int ret = nf_hook_thresh(pf, hook, net, sk, skb, in, out, okfn, thresh);
    if (ret == 1)
        ret = okfn(net, sk, skb);
    return ret;
}

NF_HOOK_THRESH 宏调用了 nf_hook_thresh 函数:

static inline int nf_hook_thresh(u_int8_t pf, unsigned int hook,
                struct net *net,
                struct sock *sk,
                struct sk_buff *skb,
                struct net_device *indev,
                struct net_device *outdev,
                int (*okfn)(struct net *, struct sock *, struct sk_buff *),
                int thresh)
{
    struct list_head *hook_list = &net->nf.hooks[pf][hook];

    if (nf_hook_list_active(hook_list, pf, hook)) {
        struct nf_hook_state state;

        nf_hook_state_init(&state, hook_list, hook, thresh,
                    pf, indev, outdev, sk, net, okfn);
        return nf_hook_slow(skb, &state);
    }
    return 1;
}

nf_hook_thresh 在拿到 hook 链表之后,调用 nf_hook_slow:

int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state)
{
    struct nf_hook_ops *elem;
    unsigned int verdict;
    int ret = 0;
…
    elem = list_entry_rcu(state->hook_list, struct nf_hook_ops, list);
next_hook:
    verdict = nf_iterate(state->hook_list, skb, state, &elem);
    if (verdict == NF_ACCEPT || verdict == NF_STOP) {
        ret = 1;
    } else if ((verdict & NF_VERDICT_MASK) == NF_DROP) {
        kfree_skb(skb);
        ret = NF_DROP_GETERR(verdict);
        if (ret == 0)
            ret = -EPERM;
    } else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {
        int err = nf_queue(skb, elem, state,
                    verdict >> NF_VERDICT_QBITS);
        if (err < 0) {
            if (err == -ESRCH &&
                (verdict & NF_VERDICT_FLAG_QUEUE_BYPASS))
                goto next_hook;
            kfree_skb(skb);
        }
    }
    …
    return ret;
}

在 nf_hook_slow 中遍历了注册的 hook 函数并在 nf_iterate 中进行调用:

unsigned int nf_iterate(struct list_head *head,
            struct sk_buff *skb,
            struct nf_hook_state *state,
            struct nf_hook_ops **elemp)
{
    unsigned int verdict;
    …
    list_for_each_entry_continue_rcu((*elemp), head, list) {
        if (state->thresh > (*elemp)->priority)
            continue;
repeat:
        verdict = (*elemp)->hook((*elemp)->priv, skb, state);
        …
            if (verdict != NF_REPEAT)
                return verdict;
            goto repeat;
        }
    }
    return NF_ACCEPT;
}

8.3.2 lvs

lvs 是章文嵩博士在十几年前开发的基于 Linux 的网络负载平衡模块,主要思路就是基于 netfilter 的实现,所以只有掌握了 netfilter 的核心概念才能搞明白 lvs 的原理。具体来说,lvs 是基于 netfilter hook 点的注册来实现的,下面为 lvs 的 hook 点注册实现:

static struct nf_hook_ops ip_vs_ops[] __read_mostly = {
    // 包过滤之后,为 VS/NAT 转换源地址
    {
        .hook                = ip_vs_reply4,
        .pf                  = NFPROTO_IPV4,
        .hooknum             = NF_INET_LOCAL_IN,
        .priority            = NF_IP_PRI_NAT_SRC - 2,
    },
    // 在包过滤中,通过 VS/DR、 VS/TUN 或 VS/NAT(转换目标地址),可以给 IPVS 应用过滤规则
    {
        .hook                = ip_vs_remote_request4,
        .pf                  = NFPROTO_IPV4,
        .hooknum             = NF_INET_LOCAL_IN,
        .priority            = NF_IP_PRI_NAT_SRC - 1,
    },
    // 在 ip_vs_in 前,给 VS/NAT 转换源地址
    {
        .hook                = ip_vs_local_reply4,
        .pf                  = NFPROTO_IPV4,
        .hooknum             = NF_INET_LOCAL_OUT,
        .priority            = NF_IP_PRI_NAT_DST + 1,
    },
    // mangle 后, 调度和转发本地请求
    {
        .hook                = ip_vs_local_request4,
        .pf                  = NFPROTO_IPV4,
        .hooknum             = NF_INET_LOCAL_OUT,
        .priority            = NF_IP_PRI_NAT_DST + 2,
    },
    // 在包过滤后(但在 ip_vs_out_icmp 前),为 IPVS 进入的连接抓获目标地址为0.0.0.0/0的 icmp 包
    {
        .hook                = ip_vs_forward_icmp,
        .pf                  = NFPROTO_IPV4,
        .hooknum             = NF_INET_FORWARD,
        .priority            = 99,
    },
    // 在包过滤之后,给 VS/NAT 转换源地址
    {
        .hook                = ip_vs_reply4,
        .pf                  = NFPROTO_IPV4,
        .hooknum             = NF_INET_FORWARD,
        .priority            = 100,
    },
#ifdef CONFIG_IP_VS_IPV6
    // 在包过滤之后
    {
        .hook                = ip_vs_reply6,
        .pf                  = NFPROTO_IPV6,
        .hooknum             = NF_INET_LOCAL_IN,
        .priority            = NF_IP6_PRI_NAT_SRC - 2,
    },
    // 在包过滤中,通过 VS/DR、 VS/TUN 或 VS/NAT(转换目标地址),可以给 IPVS 应用过滤规则
    {
        .hook                = ip_vs_remote_request6,
        .pf                  = NFPROTO_IPV6,
        .hooknum             = NF_INET_LOCAL_IN,
        .priority            = NF_IP6_PRI_NAT_SRC - 1,
    },
    // 在 ip_vs_in 前,为 VS/NAT 转换源地址
    {
        .hook                = ip_vs_local_reply6,
        .pf                  = NFPROTO_IPV6,
        .hooknum             = NF_INET_LOCAL_OUT,
        .priority            = NF_IP6_PRI_NAT_DST + 1,
    },
    // 在 mangle 后,调度和转发本地请求
    {
        .hook                = ip_vs_local_request6,
        .pf                  = NFPROTO_IPV6,
        .hooknum             = NF_INET_LOCAL_OUT,
        .priority            = NF_IP6_PRI_NAT_DST + 2,
    },
    // 在包过滤后(但在 ip_vs_out_icmp 前),为 IPVS 进入的连接抓获目标地址为0.0.0.0/0的 icmp 包
    {
        .hook                = ip_vs_forward_icmp_v6,
        .pf                  = NFPROTO_IPV6,
        .hooknum             = NF_INET_FORWARD,
        .priority            = 99,
    },
    // 在包过滤后,给 VS/NAT 转换源地址
    {
        .hook                = ip_vs_reply6,
        .pf                  = NFPROTO_IPV6,
        .hooknum             = NF_INET_FORWARD,
        .priority            = 100,
    },
#endif
};

至于如何来修改数据包达到负载平衡的过程,限于篇幅就不在这里展开了,有兴趣的话可以自己阅读上面的 hook 函数,需要花点时间才能研究透的。

8.4 网络相关的一些参数

我们在编写网络应用或进行系统调优时,会遇到不少相关参数。下面介绍一些在开发过程中会经常遇到的网络相关的参数。

8.4.1 Java socket 相关的参数

1.TCP_NODELAY

如果开启该选项,说白了就是把多个小包组合成一个大包发送,在某些场景下可以减少 TCP 包头发送量,减少网络流量。

2.SO_REUSEADDR

可以使多个 Socket 对象绑定在同一个端口上。

3.SO_LINGER

这个 socket 选项可以影响 close 方法的行为。在默认情况下,当调用 close 方法后,将立即返回;如果这时仍然有未被送出的数据包,那么这些数据包将被丢弃。如果将 linger 参数设为一个正整数 n 时(n 的值最大是65535),在调用 close 方法后,将最多被阻塞 n 秒。在这 n 秒内,系统尽量将未送出的数据包发送出去;如果超过了 n 秒还有未发送的数据包,这些数据包将全部被丢弃;而 close 方法会立即返回。如果将 linger 设为0,和关闭 SO_LINGER 选项的作用是一样的。

如果底层的 socket 实现不支持 SO_LINGER,都会抛出 SocketException 异常。当给 linger 参数传递负数值时,setSoLinger 还会抛出一个 IllegalArgumentException 异常。可以通过 getSoLinger 方法得到延迟关闭的时间,如果返回-1,则表明 SO_LINGER 是关闭的。例如,下面的代码将延迟关闭的时间设为1分钟:

if(socket.getSoLinger() == -1) socket.setSoLinger(true, 60);

4.SO_TIMEOUT

连接的读写超时时间。

5.SO_KEEPALIVE

如果将这个 socket 选项打开,客户端 socket 每间隔一段时间(大约两个小时)就会利用空闲的连接向服务器发送一个数据包。这个数据包并没有其他的作用,只是为了检测一下服务器是否仍处于活动状态。如果服务器未响应这个数据包,在大约11分钟后,客户端 socket 再发送一个数据包,如果在12分钟内,服务器还没响应,那么客户端 socket 将关闭。如果将 socket 选项关闭,客户端 socket 在服务器无效的情况下可能长时间不会关闭。

6.SO_OOBINLINE

如果这个 socket 选项打开,可以通过 socket 类的 sendUrgentData 方法向服务器发送一个单字节的数据。这个单字节数据并不经过输出缓冲区,而是立即发出。虽然在客户端并不是使用 OutputStream 向服务器发送数据,但在服务端程序中这个单字节的数据是和其他的普通数据混在一起的。因此,在服务端程序中并不知道客户端的数据是由 OutputStream 还是由 sendUrgentData 发过来的。

7.SO_RCVBUF

在默认情况下,输入流的接收缓冲区是8096个字节(8KB)。这个值是 Java 所建议的输入缓冲区的大小。如果这个默认值不能满足要求,可以用 setReceiveBufferSize 方法来重新设置缓冲区的大小。但最好不要将输入缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。

如果底层的 socket 实现不支持 SO_RCVBUF 选项,这两个方法将会抛出 SocketException 异常。必须将 size 设为正整数,否则 setReceiveBufferSize 方法将抛出 IllegalArgumentException 异常。

8.SO_SNDBUF

在默认情况下,输出流的发送缓冲区是8096个字节(8K)。这个值是 Java 建议的输出缓冲区的大小。如果这个默认值不能满足要求,可以用 setSendBufferSize 方法来重新设置缓冲区的大小。但最好不要将输出缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。

如果底层的 socket 实现不支持 SO_SENDBUF 选项,这两个方法将会抛出异常 Socket-Exception。必须将 size 设为正整数,否则 setSendBufferedSize 方法将抛出异常 Illegal-Argument-Exception。

TCP 连接的 sync 超时时间设置:

socket.connect(new InetSocketAddress(host, port), timeout);

这里的 timeout 如果不设置,默认为0,将会永久等待下去。

8.4.2 Linux TCP 相关队列

TCP 相关的参数都可以通过 /etc/sysctl.conf 文件或者 sysctl-a 命令来查看,比较常用的参数有以下几个:

  • sysctl -w net.core.somaxconn=4096## 监听队列长度。

  • ss–lt ## 可以查看指定端口的监听队列长度。

  • sysctl -w net.core.netdev_max_backlog=16384##backlog 队列的长度。

  • sysctl -w net.ipv4.tcp_max_syn_backlog=8192##sync 队列的长度。

  • sysctl -w net.ipv4.tcp_syncookies=1## 当出现 SYN 等待队列溢出时,启用 cookies 来处理,可防范少量 SYN 攻击,默认为0,表示关闭。

  • sysctl -w net.ipv4.ip_local_port_range="102465535"## 表示向外连接的端口范围。

  • sysctl -w net.ipv4.tcp_tw_recycle=1## 快速回收,复用 time_wait 状态的连接。

  • sysctl -w net.ipv4.tcp_congestion_control=cubic##TCP 拥塞控制算法。

下面是一组设置 TCP 缓冲区大小的参数:

  • sysctl -w net.core.rmem_max=16777216

  • sysctl -w net.core.wmem_max=16777216

  • sysctl -w net.ipv4.tcp_rmem="40968738016777216"

  • sysctl -w net.ipv4.tcp_wmem="40961638416777216"

下面是有关 TCP 超时时间的参数:

  • sysctl -w net.ipv4.tcp_keepalive_time=7200##keepalive 心跳包,默认2小时发送一次

  • sysctl -w net.ipv4.tcp_fin_timeout=60## 在一个 tcp 会话过程中,在会话结束时,A 首先向 B 发送一个 fin 包,在获得 B 的 ack 确认包后,A 就进入 FIN WAIT2 状态等待 B 的 fin 包然后给 B 发 ack 确认包。这个参数就是用来设置 A 进入 FIN WAIT2 状态等待对方 fin 包的超时时间。如果时间到了仍未收到对方的 fin 包就主动释放该会话。参数值为整数,单位为秒,默认为180秒。

最后,我们一定要牢记 TCP 连接的状态流转图(见图8-8):

图8-8 TCP 连接状态图

8.5 Nginx 服务器监听 socket 初始化过程

本节通过分析 Nginx 服务器监听 socket 的初始化流程,进一步加深理解应用程序如何调用 Linux 提供的 socket 调用来监听网络事件。

本章前面已经对 Linux socket 相关的系统调用实现做了分析。服务端建立 socket 监听的过程在图8-6中已经描述得比较清楚了,服务器建立 socket 监听必然会调用 sys_socket、sys_bind、sys_listen。

接下来我们通过图8-9来分析一下 Nginx 的启动初始化过程,通常 socket 监听都会在初始化的过程中建立。

Nginx 的启动过程说明如下:

1)调用 ngx_get_options 解析命令参数。

2)调用 ngx_time_init 初始化并更新时间,如全局变量 ngx_cached_time。

图8-9 Nginx 启动流程

3)调用 ngx_log_init 初始化日志,如初始化全局变量 ngx_prefix,打开日志文件 ngx_log_file.fd。

4)清零全局变量 ngx_cycle,并为 ngx_cycle.pool 创建大小为 1024KB 的内存池。

5)调用 ngx_save_argv 保存命令行参数至全局变量 ngx_os_argv、ngx_argc、ngx_argv 中。

6)调用 ngx_process_options 初始化 ngx_cycle 的 prefix、conf_prefix、conf_file、conf_param 等字段。

7)调用 ngx_os_init 初始化系统相关变量,如内存页面大小 ngx_pagesize、ngx_cache-line_size、最大连接数 ngx_max_sockets 等。

8)调用 ngx_crc32_table_init 初始化 CRC 表(后续的 CRC 校验通过查表进行,效率高);

9)调用 ngx_add_inherited_sockets 继承 sockets:

a)解析环境变量 NGINX_VAR=“NGINX”中的 sockets,并保存至 ngx_cycle.listening 数组;

b)设置 ngx_inherited=1。

c)调用 ngx_set_inherited_sockets 逐一对 ngx_cycle.listening 数组中的 sockets 进行设置。

10)初始化每个 module 的 index,并计算 ngx_max_module。

11)调用 ngx_init_cycle 进行初始化,该过程主要对 ngx_cycle 结构进行。

12)若有信号,则进入 ngx_signal_process 处理。

13)调用 ngx_init_signals 初始化信号;主要完成信号处理程序的注册。

14)若无继承 sockets,且设置了守护进程标识,则调用 ngx_daemon 创建守护进程。

15)调用 ngx_create_pidfile 创建进程记录文件;(非 NGX_PROCESS_MASTER=1 进程,不创建该文件)。

16)进入进程主循环。

a)若为 NGX_PROCESS_SINGLE=1 模式,则调用 ngx_single_process_cycle 进入进程循环。

b)否则为 master-worker 模式,调用 ngx_master_process_cycle 进入进程循环。

在 main 函数执行过程中,有一个非常重要的函数 ngx_init_cycle,该函数在这个阶段做了什么呢?下面分析 ngx_init_cycle 的初始化过程:

1)更新 timezone 和 time。

2)创建内存池。

3)给 cycle 指针分配内存。

4)保存安装路径、配置文件、启动参数等。

5)初始化打开文件句柄。

6)初始化共享内存。

7)初始化连接队列。

8)保存 hostname。

9)调用各 NGX_CORE_MODULE 的 create_conf 方法。

10)解析配置文件。

11)调用各 NGX_CORE_MODULE 的 init_conf 方法。

12)打开新的文件句柄。

13)创建共享内存。

14)处理监听 socket。

15)创建 socket 进行监听。

16)调用各模块的 init_module。

通过对 ngx_init_cycle 的分析,我们知道了 Nginx 的 socket 监听是在这一步创建的。

在创建监听之前,我们首先要搞明白,Nginx 的监听配置是从哪里来的,一般我们会做以下服务器配置:

server {
    server_name api.test.cn;
    listen 127.0.0.1:8080;
}

通过这个配置可以发现,用于设置监听 socket 的指令主要有两个:server_name 和 listen。server_name 指令用于实现虚拟主机的功能,会设置每个 server 块的虚拟主机名,在处理请求时根据请求行中的 host 来转发请求;而 listen 用于设置监听 socket 的信息。

Nginx 需要先进行配置文件的解析工作:对于 server_name 和 listen 的配置解析,都是通过 ngx_http_core_module.c 模块来完成。

其中,解析 server_name 和 listen 的回调函数定义如下:

    …
{ ngx_string("listen"),
    NGX_HTTP_SRV_CONF|NGX_CONF_1MORE,
    ngx_http_core_listen,
    NGX_HTTP_SRV_CONF_OFFSET,
    0,
    NULL },

{ ngx_string("server_name"),
    NGX_HTTP_SRV_CONF|NGX_CONF_1MORE,
    ngx_http_core_server_name,
    NGX_HTTP_SRV_CONF_OFFSET,
    0,
    NULL },
    …

ngx_http_core_server_name 函数将 server_name 指令指定的虚拟主机名添加到 ngx_http_core_srv_conf_t 的 server_names 数组中。ngx_http_core_listen 函数主要解析 listen 指令中的 socket 配置选项,并最后调用 ngx_http_add_listen 函数添加监听 socket 的信息。配置结构如图8-10所示。

图8-10 Nginx listen 配置结构

Nginx 配置之后就会针对配置的值进行初始化,主要针对 ngx_cycle_t 中的 listening 数组中的 ngx_listening_t 结构,该逻辑会在 ngx_init_cycle 中进行 init_conf 操作时调用 http 模块的 ngx_http_block 函数,并最终通过 ngx_http_optimize_servers 函数完成 ngx_listening_t 初始化。

ngx_init_cycle 接下来执行真正的打开 socket 并且进行监听的工作,该过程交给 ngx_open_listening_sockets 函数来完成:

ngx_int_t
ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
…
    for (tries = 5; tries; tries--) {
        failed = 0;
        // 遍历所有的 listening socket
        ls = cycle->listening.elts;
        for (i = 0; i < cycle->listening.nelts; i++) {
            if (ls[i].ignore) {
                continue;
            }
            // 已经从 cycle 处继承该 socket,不需重新打开
            if (ls[i].fd != -1) {
                continue;
            }
            …
            // 新建一个 socket,socket(地址结构的协议族,socket 类型为 tcp 或 udp)
            s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);
            …
            if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
                           (const void *) &reuseaddr, sizeof(int))
                == -1)
            {
                ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                              "setsockopt(SO_REUSEADDR) %V failed",
                              &ls[i].addr_text);

                if (ngx_close_socket(s) == -1) {
                    ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                                  ngx_close_socket_n " %V failed",
                                  &ls[i].addr_text);
                }
                return NGX_ERROR;
            }

            …

            if (!(ngx_event_flags & NGX_USE_AIO_EVENT)) {
                // 设置 socket 为非阻塞
                if (ngx_nonblocking(s) == -1) {
                    ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                                  ngx_nonblocking_n " %V failed",
                                  &ls[i].addr_text);

                    if (ngx_close_socket(s) == -1) {
                        ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                                      ngx_close_socket_n " %V failed",
                                      &ls[i].addr_text);
                    }
                    return NGX_ERROR;
                }
            }
            …
            // 将 socket 绑定到要监听的地址
            if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {
                err = ngx_socket_errno;
                if (err == NGX_EADDRINUSE && ngx_test_config) {
                    continue;
                }
                …
                if (ngx_close_socket(s) == -1) {
                    ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                                  ngx_close_socket_n " %V failed",
                                  &ls[i].addr_text);
                }

                …
                failed = 1;
                continue;
            }

            …

            // 设置 socket 为监听状态
            if (listen(s, ls[i].backlog) == -1) {
                ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                              "listen() to %V, backlog %d failed",
                              &ls[i].addr_text, ls[i].backlog);
                if (ngx_close_socket(s) == -1) {
                    ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                                  ngx_close_socket_n " %V failed",
                                  &ls[i].addr_text);
                }
                return NGX_ERROR;
            }
            ls[i].listen = 1;
            ls[i].fd = s;
        }

…
        ngx_msleep(500);
    }
    …
    return NGX_OK;
}

从上面函数执行过程可以发现,Nginx 设置监听 socket 和一般服务器设置 socket 一样,通过调用 socket、bind 和 listen 来实现的。

8.6 本章小结

计算机网络世界博大精深,仅 RFC 文档就有一大堆,一个人研究不过来,而且,不管如何研究,总要将其应用到开发中,而 Linux 的网络模块就是对相关协议的实现,是学习网络技术的最好范本。本章主要围绕数据如何在 Linux 网络层运转的整体架构来分析,便于大家在日后研究细节的时候有切入点,有助于网络层面的定制开发工作。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值