RPC 服务器之【多进程描述符传递】高阶模型

【代码】flask通过uds传输socket描述符实践 #!/usr/bin/python3 # coding:utf-8 import os import socket from http import HTTPStatus import struct import logging from time import sleep pa, pb = socket.socketpair() pid = os.fork() from socketserver import BaseRequestHandler, _SocketWriter from werkzeu 阅读详情

今天老师要给大家介绍一个比较特别的 RPC 服务器模型,这个模型不同于 Nginx、不同于 Redis、不同于 Apache、不同于 Tornado、不同于 Netty,它的原型是 Node Cluster 的多进程并发模型。

Nginx 并发模型

我们知道 Nginx 的并发模型是一个多进程并发模型,它的 Master 进程在绑定监听地址端口后 fork 出了多个 Slave 进程共同竞争处理这个服务端套接字接收到的很多客户端连接。

640?wx_fmt=png

这多个 Slave 进程会共享同一个处于操作系统内核态的套接字队列,操作系统的网络模块在处理完三次握手后就会将套接字塞进这个队列。这是一个生产者消费者模型,生产者是操作系统的网络模块,消费者是多个 Slave 进程,队列中的对象是客户端套接字。

这种模型在负载均衡上有一个缺点,那就是套接字分配不均匀,形成了类似于贫富分化的局面,也就是「闲者愈闲,忙者愈忙」的状态。这是因为当多个进程竞争同一个套接字队列时,操作系统采用了 LIFO 的策略,最后一个来 accept 的进程最优先拿到 套接字。越是繁忙的进程越是有更多的机会调用 accept,它能拿到的套接字也就越多。

640?wx_fmt=png

Node Cluster 并发模型

Node Cluster 为了解决负载均衡问题,它采用了不同的策略。它也是多进程并发模型,Master 进程会 fork 出多个子进程来处理客户端套接字。但是不存在竞争问题,因为负责 accept 套接字的只能是 Master 进程,Slave 进程只负责处理客户端套接字请求。那就存在一个问题,Master 进程拿到的客户端套接字如何传递给 Slave 进程。

640?wx_fmt=png

这时,神奇的 sendmsg 登场了。它是操作系统提供的系统调用,可以在不同的进程之间传递文件描述符。sendmsg 会搭乘一个特殊的「管道」将 Master 进程的套接字描述符传递到 Slave 进程,Slave 进程通过 recvmsg 系统调用从这个「管道」中将描述符取出来。这个「管道」比较特殊,它是 Unix 域套接字。普通的套接字可以跨机器传输消息,Unix 域套接字只能在同一个机器的不同进程之间传递消息。同管道一样,Unix 域套接字也分为有名套接字和无名套接字,有名套接字会在文件系统指定一个路径名,无关进程之间都可以通过这个路径来访问 Unix 域套接字。而无名套接字一般用于父子进程之间,父进程会通过 socketpair 调用来创建套接字,然后 fork 出来子进程,这样子进程也会同时持有这个套接字的引用。后续父子进程就可以通过这个套接字互相通信。

640?wx_fmt=png

注意这里的传递描述符,本质上不是传递,而是复制。父进程的描述符并不会在 sendmsg 自动关闭自动消失,子进程收到的描述符和父进程的描述符也不是同一个整数值。但是父子进程的描述符都会指向同一个内核套接字对象。

有了描述符的传递能力,父进程就可以将 accept 到的客户端套接字轮流传递给多个 Slave 进程,负载均衡的目标就可以顺利实现了。

接下来我们就是用 Python 代码来撸一遍 Node Cluster 的并发模型。因为 sendmsg 和 recvmsg 方法到了 Python3.5 才内置进来,所以下面的代码需要使用 Python3.5+才可以运行。

我们看 sendmsg 方法的定义

socket.sendmsg(buffers[, ancdata[, flags[, address]]])

我们只需要关心第二个参数 ancdata,描述符是通过ancdata 参数传递的,它的意思是 「辅助数据」,而 buffers 表示需要传递的消息内容,因为消息内容这里没有意义,所以这个字段可以任意填写,但是必须要有内容,如果没有内容,sendmsg 方法就是一个空调用。

import socket, struct

def send_fds(sock, fd):
    return sock.sendmsg([b'x'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, struct.pack("i", fd))])

# ancdata 参数是一个三元组的列表,三元组的第一个参数表示网络协议栈级别 level,第二个参数表示辅助数据的类型 type,第三个参数才是携带的数据,level=SOL_SOCKET 表示传递的数据处于 TCP 协议层级,type=SCM_RIGHTS 就表示携带的数据是文件描述符。我们传递的描述符 fd 是一个整数,需要使用 struct 包将它序列化成二进制。

再看 recvmsg 方法的定义

msg, ancdata, flags, addr = socket.recvmsg(bufsize[, ancbufsize[, flags]])

同样,我们只需要关心返回的 ancdata 数据,它里面包含了我们需要的文件描述符。但是需要提供消息体的长度和辅助数据的长度参数。辅助数据的长度比较特殊,需要使用 CMSG_LEN 方法来计算,因为辅助数据里面还有我们看不到的额外的头部信息。

bufsize = 1  # 消息内容的长度
ancbufsize = socket.CMSG_LEN(struct.calcsize('i'))  # 辅助数据的长度
msg, ancdata, flags, addr = socket.recvmsg(bufsize, ancbufsize) # 收取消息
level, type, fd_bytes = ancdata[0] # 取第一个元祖,注意发送消息时我们传递的是一个三元组的列表
fd = struct.unpack('i', fd_bytes) # 反序列化

代码实现

下面我来献上完整的服务器代码,为了简单起见,我们在 Slave 进程中处理 RPC 请求使用同步模型。

# coding: utf
# sendmsg recvmsg python3.5+才可以支持

import os
import json
import struct
import socket


def handle_conn(conn, addr, handlers):
    print(addr, "comes")
    while True:
        # 简单起见,这里就没有使用循环读取了
        length_prefix = conn.recv(4)
        if not length_prefix:
            print(addr, "bye")
            conn.close()
            break  # 关闭连接,继续处理下一个连接
        length, = struct.unpack("I", length_prefix)
        body = conn.recv(length)
        request = json.loads(body)
        in_ = request['in']
        params = request['params']
        print(in_, params)
        handler = handlers[in_]
        handler(conn, params)


def loop_slave(pr, handlers):
    while True:
        bufsize = 1
        ancsize = socket.CMSG_LEN(struct.calcsize('i'))
        msg, ancdata, flags, addr = pr.recvmsg(bufsize, ancsize)
        cmsg_level, cmsg_type, cmsg_data = ancdata[0]
        fd = struct.unpack('i', cmsg_data)[0]
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=fd)
        handle_conn(sock, sock.getpeername(), handlers)


def ping(conn, params):
    send_result(conn, "pong", params)


def send_result(conn, out, result):
    response = json.dumps({"out": out, "result": result}).encode('utf-8')
    length_prefix = struct.pack("I", len(response))
    conn.sendall(length_prefix)
    conn.sendall(response)


def loop_master(serv_sock, pws):
    idx = 0
    while True:
        sock, addr = serv_sock.accept()
        pw = pws[idx % len(pws)]
        # 消息数据,whatever
        msg = [b'x']
        # 辅助数据,携带描述符
        ancdata = [(
            socket.SOL_SOCKET,
            socket.SCM_RIGHTS,
            struct.pack('i', sock.fileno()))]
        pw.sendmsg(msg, ancdata)
        sock.close()  # 关闭引用
        idx += 1


def prefork(serv_sock, n):
    pws = []
    for i in range(n):
        # 开辟父子进程通信「管道」
        pr, pw = socket.socketpair()
        pid = os.fork()
        if pid < 0:  # fork error
            return pws
        if pid > 0:
            # 父进程
            pr.close()  # 父进程不用读
            pws.append(pw)
            continue
        if pid == 0:
            # 子进程
            serv_sock.close()  # 关闭引用
            pw.close()  # 子进程不用写
            return pr
    return pws


if __name__ == '__main__':
    serv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serv_sock.bind(("localhost", 8080))
    serv_sock.listen(1)
    pws_or_pr = prefork(serv_sock, 10)
    if hasattr(pws_or_pr, '__len__'):
        if pws_or_pr:
            loop_master(serv_sock, pws_or_pr)
        else:
            # fork 全部失败,没有子进程,Game Over
            serv_sock.close()
    else:
        handlers = {
            "ping": ping
        }
        loop_slave(pws_or_pr, handlers)

父进程使用 fork 调用创建了多个子进程,然后又使用 socketpair 调用为每一个子进程都创建一个无名套接字用来传递描述符。父进程使用 roundrobin 策略平均分配接收到的客户端套接字。子进程接收到的是一个描述符整数,需要将描述符包装成套接字对象后方可读写。打印对比发送和接收到的描述符,你会发现它们俩的值并不相同,这是因为 sendmsg 将描述符发送到内核后,内核给描述符指向的内核套接字又重新分配了一个新的描述符对象。

思考题

  1. sendmsg/recvmsg 除了可以发送描述符外还可以用来干什么?

  2. sendmsg/recvmsg 发送接收描述符在内核态具体是如何工作的?

本文节选之在线技术小册《深入理解 RPC》,感兴趣的读者请点击「阅读原文」继续阅读《深入理解 RPC》全书内容。

640?wx_fmt=other

640?wx_fmt=jpeg

阅读更多深度技术文章,扫一扫上面的二维码关注微信公众号「码洞」

Reactor反应堆 本文系统介绍了Linux下Reactor多线程模型的设计与实现,重点分析了事件驱动机制、线程唤醒方式和多进程扩展方案。核心内容包括:1. Reactor模式原理,基于epoll的I/O多路复用实现;2. 线程唤醒机制比较,包括FIFO管道和效率更高的eventfd方案;3. 多线程任务分配策略,包括主线程调度和子线程自举竞争模式;4. 多进程扩展方案,详解连接分配的三种实现方式及其适用场景。文章通过技术对比和典型应用场景分析,为开发高并发网络服务提供了实践指导,最后以Nginx多进程模型为例展示了React 阅读详情

相关推荐

多线程、事件驱动与推荐引擎框架选型

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。多线程是另一种常用编程范式,并且更容易理解。   高性能通用型C++网络框架Nebula是基于事件驱动的多进程网络框架(适用于即时通讯、数据采集、实时计算、消息推送等应用场景),已有即时通讯、埋点数据采集及实时分析的生产应用案例。经常有人问Nebula的每个...

changqingcao的博客 417

【RPC-Python】多进程同步模型

上节我们完成了一个简单的多线程服务器,可以并发处理多个客户端连接。但是 Python 里多线程使用的并不常见,因为 Python 的 GIL 致使单个进程只能占满一个 CPU 核心,多线程并不能充分利用多核的优势。所以多数 Python 服务器推荐使用多进程模型。我们将使用 Python 内置的 进程模块创建子进程。 multiprocessing 多进程运行模型如下: # serv...

柠檬柠檬C的博客 1424

C++高性能游戏服务器架构:从Reactor模型到内存池的实战设计

在分布式系统与高并发编程领域,服务器架构设计是支撑海量用户实时交互的核心基础。其核心原理在于通过高效的I/O模型、进程线程管理与数据结构优化,来最大化硬件资源利用率,降低响应延迟。这一技术对于需要处理数千甚至上万并发连接、要求毫秒级响应的在线服务具有极高价值,尤其在大型多人在线游戏、实时通信平台等场景中至关重要。本文聚焦于游戏服务器这一典型应用,深入探讨如何基于C++语言特性,结合Reactor网络模型、自定义二进制协议以及内存池等热词技术,构建一套高性能、可扩展的服务器框架,以应对游戏逻辑处理、状态同步等

bk_lin的专栏 706

python 实现多进程

from urllib import request from multiprocessing import Process import os #下载器函数 def downloader(url,**kwargs): print('当前进程id:',os.getpid(),os.getppid()) #os.getpid()获取当前进程id,os.getppid()获取当前进程父...

YPL_ZML的博客 233

Android多进程通信(IPC)原理、Binder机制与实战避坑指南

进程间通信(IPC)是操作系统中的基础概念,它允许运行在不同内存空间的进程交换数据与指令,是实现模块化、稳定性和性能扩展的核心技术。其原理通常基于内核提供的共享内存、消息队列或远程过程调用等机制,在内存隔离的进程间建立可控的数据通道。在Android生态中,IPC的技术价值尤为突出,它支撑着应用稳定性(进程隔离防崩溃)、性能优化(突破单进程内存限制)与安全性(系统服务隔离)三大核心诉求。典型的应用场景包括多进程应用架构设计、系统服务调用以及跨应用数据共享。本文聚焦Android平台,深入剖析其IPC基石Bi

weixin_34228387的博客 295

Linux C++工程实践:从HTTP服务器到高性能服务进阶指南

在Linux C/C++开发领域,掌握基础语法只是起点,构建健壮、高性能的服务才是核心目标。这要求开发者深入理解系统编程、网络通信和并发模型等底层原理。通过I/O多路复用等技术,可以高效管理大量连接,显著提升服务吞吐量。这些工程实践能力对于构建AI基础设施、音视频处理、高性能后端等现代应用具有关键价值。本文以构建一个支持并发的HTTP文件服务器为例,系统性地串联了从项目搭建、协议解析到性能优化的完整流程,帮助开发者跨越从“会写代码”到“能交付服务”的鸿沟,为深入特定领域打下坚实基础。

weixin_34186128的博客 507

Hadoop——入门方法

【转】转自:知乎 董西成,Hadoop大数据实践者 个人谈谈阅读hadoop源代码的经验。 首先,不得不说,hadoop发展到现在这个阶段,代码已经变得非常庞大臃肿,如果你直接阅读最新版本的源代码,难度比较大,需要足够的耐心和时间,所以,如果你觉得认真一次,认真阅读一次hadoop源代码,一定要有足够的心理准备和时间预期。 其次,需要注意,阅读Hadoop源代码的效率,因人而异,如果你有足够

AugustWind的专栏 735

快手did did_gt edid的注册过程

接口 https://gdfp.ksapisrv.com/rest/infra/gdfp/report/kuaishou/android did 是本地生成的16进制 或者 获取的 android_id did_gt 是did生成时间戳 1595073601970 public static final Random f16237a = new Random(System.currentTimeMillis()); public static long m19668a() {  &n

HURUWO的技术博客 1万+

【2-5】RPC——不同进程之间的方法调用

学完了Java接口后,本节学习RPC(remote procedure call)远程过程调用协议。就是——不同进程直接的方法调用 CRM暴露了一个接口【地址】,我一访问可以生成一个xml,得到一个类。我生成类之后我可以调用它里面的方法。 左边里有一个代理对象,代理对象里面定义了一个 一调用代理对象,其实是在目标方法里面

tianbwin2995的专栏 2329

洛谷P5707 【深基2.例12】上学迟到(c语言)

第一篇题解 题目描述 yyy 的学校要求早上 8 点前到达。学校到 yyy 的家一共有 s(s\le 10000)s(s≤10000) 米,而 yyy 可以以 v(v<10000)v(v<10000) 米每分钟的速度匀速走到学校。此外在上学路上它还要额外花 10 分钟时间进行垃圾分类。请问为了避免迟到 yyy 最晚什么时候出门?输出 HH:MM 的时间格式,不足两位时补零。由于路途遥远, yyy 可能不得不提前一天出发,不过不可能提前超过一天。 输入格式 两个正整数 s,v,意思已经在题目中给定

cc_love_program的博客 1万+

VScode搭建Opencv(C++开发环境)

VScode配置Opencv一、准备软件二 、下载软件1、MinGw下载2、 Cmake下载3、Opencv下载三、编译1、cmake-gui2、make3、install四、 VScode配置1、launch.json2、c_cpp_properties.json3、tasks.json五、测试 用了半天事件,看了各种的博客终于配置成功了,在这里记录并分享一下,让更多的人少走弯路。 本次环境是Win 10. 一、准备软件 我们本次用到的3个软件,(建议读者文件路径,文件版本 尽量都和我相同,这样不容易出错

河旬的博客 8万+

python视频教程的优服务_深入理解 RPC : 基于 Python 自建分布式高并发 RPC 服务(HTML)...

深入理解 RPC : 基于 Python 自建分布式高并发 RPC 服务(HTML)QQ截图20190524093130.png (42.31 KB, 下载次数: 13)2019-5-24 09:31 上传〖课程介绍〗:适宜人群掌握 Python 语法基础,有至少1-2年后端开发经验,想系统深入的掌握 RPC 服务开发;不满足于只埋头搞业务的普通开发者和 CRUD 熟手;期待持续进阶的 Pytho...

weixin_39703561的博客 206

AI 技术新手入门与实战指南

仅仅能对话还不够,实际应用中,我们往往需要让 AI 处理特定的业务数据。比如,从日志文件中提取错误码,或者将非结构化的用户反馈整理成 JSON 格式。这时候,自定义输入和严格的结果验证就显得尤为重要。假设我们需要让 AI 分析一段服务器日志,并只返回错误的级别和简述。请分析以下日志片段,提取所有 ERROR 级别的信息。要求输出格式为 JSON 列表,包含 'timestamp' 和 'message' 两个字段。不要输出任何多余的解释文字。日志内容: {

2600_94936330的博客 2681

多线程网络编程:粘包问题、多线程/多进程服务器实战与常见问题解析

网络编程的核心是“处理不确定性”——不确定的网络延迟、不确定的数据包顺序、不确定的连接状态。通过合理的协议设计和并发模型选择,才能构建健壮的网络服务。

w20010915的博客 1114

嵌入式主板系统配置实战:OTG切换、双网共存与开机自启详解

在嵌入式Linux系统开发中,系统配置是连接硬件平台与应用功能的关键桥梁。其核心原理在于通过内核参数、文件系统接口和系统服务等机制,实现对硬件资源和系统行为的精细控制。掌握这些配置技术,对于提升开发效率、保障系统稳定性和实现产品差异化至关重要。无论是工业物联网网关、智能终端还是边缘计算设备,都需要对网络、USB、启动项等基础功能进行可靠配置。本文聚焦于嵌入式主板开发中的高频需求,例如OTG功能模式切换、双网共存路由策略以及基于systemd的开机自启动实现,通过具体的命令和脚本示例,为开发者提供一套可直接复

weixin_30827827的博客 322

AI实时盯盘与素材更新迭代功能开发——千川素材投流系统完全指南

《AI驱动的千川素材投流系统完全指南》摘要 本文针对抖音电商生态中巨量千川平台的素材投放挑战,提出了一套AI驱动的实时盯盘与素材更新迭代解决方案。系统通过分层架构设计,整合数据采集、实时监控、AI分析和自动化执行模块,实现分钟级数据追踪与智能决策。核心功能包括实时数据采集、异常检测、自动化投放调整和素材生命周期管理,有效解决人工盯盘效率低、素材生命周期短、决策响应慢等痛点。系统采用Python+FastAPI技术栈,结合机器学习和自动化工具,提供从数据采集到决策执行的全链路解决方案,帮助商家提升投放效率和R

huanghm88的专栏 240

‌Android多进程架构:Binder如何实现进程隔离与协作

Binder在Android多进程架构中扮演着双重角色:一方面,通过内核驱动和权限控制实现严格的进程隔离,提升系统安全性和稳定性;另一方面,通过高效的RPC模型和序列化机制,实现无缝进程协作,优化性能。这种平衡是Android高效运行的基础,确保了应用间的安全交互。Binder的设计体现了IPC机制的创新,其影响可扩展到其他系统领域,如微内核架构。

qqquxgsjdgxjkwh的博客 718

【云原生】阿里云SLS日志自定义字段标签实现日志告警

样例:一个典型的java log初始化日志格式把日志中的INFO提取出来做为一个标签,在开源日志框架中,配置采集器实现- regex:在阿里云SLS日志服务同样可以实现,首先我们进入到一个日志库,查看数据接入,我这里使用的是Logtail工具在【处理配置】中,选择【处理插件组合】,新增提取字段插件原始字段:填写要被提取的日志,对应原始字段的内容做正则结果字段:正则表达式对应的捕获组对应结果:捕获组 匹配内容 说明1 ywgy-app-service 服务名称。

CN_Eden 743

码洞原创深度技术文章大全 —— 高端面试必备

《快学 Go 语言》第 11 课 —— 协程《快学 Go 语言》第 10 课 —— 错误与异常《快学 Go 语言》第 9 课 —— 接口《快学 Go 语言》第 8 课 —...

codehole_的博客 293

Simulink仿真模块 - Data Type Conversion

Data Type Conversion将输入信号转换为指定的数据类型 在仿真库中的位置为: Simulink / 常用模块 Simulink / Signal Attributes 模型为: 说明 DataTypeConversion模块可将任何 Simulink®数据类型的输入信号转换为您指定的数据类型。 注意 要通过指定模块参数来控制输出数据类型,或者要从下游模块继承数据类型,使用DataTypeConversion模块。要...

jk_101的博客 2521

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

八股文7:操作系统与IO模型

学自jksj,做个笔记 操作系统基础 总览(内核) 什么是操作系统 内存管理 进程(CPU)管理 IO管理(键盘、鼠标、打印机、显示器等统称为 I/O设备) 文件(硬盘)管理 网卡(网络通信)管理

qq_39227541的博客 673

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

python多进程共享对象_Python multiprocessing 跨进程对象共享

继续写关于Pythonmultiprocessing的使用手记,继上次的进程模型之后,这次展开讨论一下multiprocessing当中的跨进程对象共享的问题。在mp库当中,跨进程对象共享有三种方式,第一种仅适用于原生机器类型,即python.ctypes当中的类型,这种在mp库的文档当中称为sharedmemory方式,即通过共享内存共享对象;另外一种称之为server process,即有一个...

weixin_39673972的博客 1492

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Java多进程编程实战:从ProcessBuilder到进程池的完整指南

在并发编程领域,多线程是处理单进程内任务并发的核心技术,通过共享内存实现高效协作。然而,当应用场景扩展到跨语言调用、资源隔离或构建微服务架构时,多进程技术便成为关键解决方案。其核心原理在于操作系统提供的进程隔离机制,每个进程拥有独立的内存空间和系统资源,这为系统带来了更高的健壮性和故障隔离能力。从技术价值看,多进程不仅实现了跨语言生态整合,还能有效避免单一进程崩溃导致的系统级故障。在实际工程实践中,Java通过ProcessBuilder和Runtime.exec()等API支持多进程编程,广泛应用于调度外

weixin_34126557的博客 436
上一篇: 老板们都应该学一学 Redis,它能管理上亿对象,你们呢?
下一篇: 见微知著 —— Redis 字符串内部结构源码分析
codehole_
博客等级 码龄7年 34粉丝 132原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值