拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C与Python跨语言通信实战:基于UDP的高效数据传输方案

如果你是一名C语言开发者正在为嵌入式设备、高性能服务器或物联网终端编写网络通信模块你很可能面临一个经典困境核心业务逻辑用C写得高效稳定但上层应用、数据分析或控制界面却普遍使用Python、Java等高级语言。如何让这两种“语言世界”的组件可靠地对话直接使用TCP握手、重传、流控在轻量级、高频次场景下可能显得笨重。这时一个被低估但极其高效的方案浮出水面UDP用户数据报协议。很多人对UDP的印象停留在“不可靠”、“用于音视频”认为它不适合严肃的数据传输。这其实是一个巨大的误解。在跨语言通信的特定场景下UDP凭借其无连接、低开销、高吞吐的特性恰恰能解决TCP带来的复杂性和性能瓶颈。关键在于我们能否通过精心的应用层设计将UDP的“不可靠”转化为我们需要的“可控”。本文将以C语言作为服务端Python作为客户端构建一个完整的UDP跨语言通信实战案例。这不是一个简单的“Hello World”演示而是会深入探讨以下核心问题为什么选择UDP进行跨语言通信对比TCP分析UDP在延迟敏感、服务发现、简单请求-响应场景下的独特优势。如何设计应用层协议来保证UDP通信的可靠性我们将实现一个包含序列号、简单确认和超时重传的轻量级可靠机制。如何处理字节序和结构体对齐等跨语言、跨平台的核心难题这是实际项目中最容易出错的地方。从代码到调试有哪些必须绕开的“坑”例如bind地址复用、缓冲区大小、以及网络调试工具的使用。通过本文你将获得一套可直接复用的代码框架和经过验证的最佳实践能够快速将UDP跨语言通信方案应用到你的实际项目中。1. 重新认识UDP为什么它是跨语言通信的“利器”在讨论实现之前我们必须先扭转一个观念UDP并非“劣化版TCP”。它们是设计目标不同的两种工具。TCP追求的是可靠的字节流像打电话保证你说的每个字对方都能按顺序听到。而UDP追求的是高效的数据报文像寄明信片只管发出不保证对方一定收到也不保证顺序。在跨语言通信场景中UDP的以下特性使其成为“利器”协议简单各语言实现统一UDP的Socket APIsocket,bind,sendto,recvfrom,close在所有主流语言中几乎一致概念清晰降低了跨语言对接的理解成本。无连接资源消耗低无需维护连接状态服务器可以轻松应对海量客户端。对于物联网中成千上万的设备上报心跳或传感器数据UDP是更自然的选择。头部开销小传输效率高每个UDP数据报只有8字节头部远小于TCP的20字节通常更多。在传输小数据包如控制指令、状态更新时效率优势明显。无拥塞控制延迟可预测TCP的拥塞控制算法如慢启动在应对丢包时会主动降低发送速率导致延迟抖动。UDP没有这个机制延迟更稳定适合对实时性要求高的场景如游戏状态同步、实时控制。支持广播和组播UDP可以轻松地将一个数据包发送给多个接收者这是TCP无法直接实现的非常适合服务发现或群组通知。那么UDP的“不可靠”怎么办这正是应用层设计的价值所在。我们不需要像TCP那样实现全面的可靠性而是根据业务需求实现恰到好处的可靠性。例如对于心跳包可以容忍丢失下次再发即可。对于关键指令可以实现简单的“请求-确认-重传”机制。对于文件传输可以在应用层实现分块、校验和重传。结论如果你的跨语言通信场景符合以下特征UDP是比TCP更优的选择数据传输以独立的“消息”或“数据报”为单位。对延迟敏感或需要广播/组播。通信模式简单如一问一答、周期性上报。你愿意并能够在应用层实现业务所需的、定制化的可靠性逻辑。2. 核心概念与项目设计2.1 Socket编程基础无论C还是Python进行UDP通信都遵循相同的Socket编程模型创建套接字指定地址族如IPv4的AF_INET和协议类型SOCK_DGRAM代表UDP。绑定地址通常用于接收方将套接字与一个本地IP地址和端口号绑定。发送数据使用sendto函数需要指定目标地址和端口。接收数据使用recvfrom函数可获取数据及发送方的地址。关闭套接字释放资源。2.2 应用层协议设计为了演示一个稍具实用性的案例我们设计一个简单的“传感器数据上报”协议。C端服务端/数据采集端模拟一个温度传感器周期性生成数据并发送给Python端。Python端客户端/数据处理端接收数据解析并显示同时发送一个简单的确认消息给C端。数据包格式设计 我们需要定义一个双方都能理解的数据结构。这里设计一个包含传感器ID、时间戳、温度值和校验和的简单结构。C语言结构体#pragma pack(1) // 非常重要取消结构体对齐保证跨平台/语言时内存布局一致 typedef struct { uint32_t sensor_id; // 传感器ID uint64_t timestamp; // 时间戳毫秒 float temperature; // 温度值 uint16_t checksum; // 校验和简单求和校验 } SensorData; #pragma pack()注意#pragma pack(1)是确保结构体在内存中紧密排列1字节对齐的关键。没有它编译器可能会在成员之间插入填充字节导致C语言发出的数据包与Python端解析的预期长度和布局不一致。Python端对应解析 Python没有原生的结构体但可以使用struct模块按照相同的格式字符串进行打包和解包。对应的格式字符串为“I Q f H”I: 无符号int,Q: 无符号long long,f: float,H: 无符号short。2.3 轻量级可靠传输设计我们实现一个极简的可靠机制序列号每个数据包带一个递增的序列号。确认ACK接收方成功解析数据后回传一个包含对应序列号的ACK包。超时重传发送方发出数据后启动一个定时器若超时未收到ACK则重传该数据包本文示例为简化将用循环模拟实际项目需用非阻塞Socket或多线程处理。3. 环境准备与工具C语言开发环境编译器GCC (Linux/macOS) 或 MinGW-w64 (Windows)文本编辑器或IDEVS Code, CLion, 或任意你熟悉的编辑器。Python环境Python 3.6 或以上版本。无需额外库仅使用标准库socket和struct。网络调试工具可选但强烈推荐NetAssist 一个常用的Windows网络调试助手可用于测试UDP服务端/客户端在开发初期验证基础通信是否通畅。Wireshark 强大的网络封包分析工具可以抓取并详细查看我们收发的每一个UDP数据包是排查复杂问题的终极利器。操作系统 示例代码在Linux和Windows上均可运行注意Socket API的细微差别如头文件。4. C语言UDP服务端实现我们将C语言端作为数据发送方服务端。4.1 完整代码c_udp_server.c/** * C语言UDP服务端 (数据发送方) * 编译: gcc c_udp_server.c -o c_udp_server * 运行: ./c_udp_server 目标IP 目标端口 */ #include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h // Linux/macOS 网络头文件 #include sys/socket.h #include time.h #include stdint.h // 对于Windows需包含以下头文件并链接ws2_32.lib // #include winsock2.h // #pragma comment(lib, ws2_32.lib) #pragma pack(1) typedef struct { uint32_t sensor_id; uint64_t timestamp; float temperature; uint16_t checksum; // 简单校验和所有前面字段的字节和 } SensorData; #pragma pack() #define BUFFER_SIZE 1024 #define MAX_RETRY 3 #define TIMEOUT_SEC 2 // 计算校验和 uint16_t calculate_checksum(SensorData* data) { uint8_t* bytes (uint8_t*)data; uint16_t sum 0; // 计算除checksum本身外所有字节的和 for (size_t i 0; i sizeof(SensorData) - sizeof(data-checksum); i) { sum bytes[i]; } return sum; } int main(int argc, char *argv[]) { if (argc ! 3) { printf(用法: %s 目标IP 目标端口\n, argv[0]); printf(示例: %s 127.0.0.1 8888\n, argv[0]); return -1; } const char* target_ip argv[1]; int target_port atoi(argv[2]); int sockfd; struct sockaddr_in server_addr, client_addr; socklen_t addr_len sizeof(client_addr); // 1. 创建UDP套接字 if ((sockfd socket(AF_INET, SOCK_DGRAM, 0)) 0) { perror(socket创建失败); exit(EXIT_FAILURE); } // 2. 可选绑定本地地址和端口如果不绑定系统会自动分配一个临时端口 // 对于纯发送的客户端通常不需要bind。 // memset(server_addr, 0, sizeof(server_addr)); // server_addr.sin_family AF_INET; // server_addr.sin_addr.s_addr INADDR_ANY; // 监听所有本地IP // server_addr.sin_port htons(0); // 端口0表示由系统分配 // if (bind(sockfd, (struct sockaddr*)server_addr, sizeof(server_addr)) 0) { // perror(bind失败); // close(sockfd); // exit(EXIT_FAILURE); // } // 3. 设置目标地址Python客户端地址 memset(client_addr, 0, sizeof(client_addr)); client_addr.sin_family AF_INET; client_addr.sin_port htons(target_port); if (inet_pton(AF_INET, target_ip, client_addr.sin_addr) 0) { perror(无效的IP地址); close(sockfd); exit(EXIT_FAILURE); } printf(C语言UDP发送端已启动目标: %s:%d\n, target_ip, target_port); uint32_t sequence 0; char ack_buffer[BUFFER_SIZE]; while (1) { sequence; // 4. 准备传感器数据 SensorData data; data.sensor_id 1001; data.timestamp (uint64_t)time(NULL) * 1000; // 当前时间戳毫秒 data.temperature 25.0 (rand() % 100) / 10.0; // 模拟25.0-35.0度之间的温度 data.checksum calculate_checksum(data); int retry_count 0; int ack_received 0; while (retry_count MAX_RETRY !ack_received) { // 5. 发送数据 printf([Seq:%u] 发送数据: ID%u, 时间%llu, 温度%.2f\n, sequence, data.sensor_id, data.timestamp, data.temperature); ssize_t sent_len sendto(sockfd, data, sizeof(data), 0, (struct sockaddr*)client_addr, addr_len); if (sent_len ! sizeof(data)) { perror(sendto失败); break; } // 6. 等待ACK使用select实现简单超时 fd_set readfds; struct timeval tv; FD_ZERO(readfds); FD_SET(sockfd, readfds); tv.tv_sec TIMEOUT_SEC; tv.tv_usec 0; int rv select(sockfd 1, readfds, NULL, NULL, tv); if (rv 0 FD_ISSET(sockfd, readfds)) { // 收到数据可能是ACK ssize_t recv_len recvfrom(sockfd, ack_buffer, BUFFER_SIZE, 0, (struct sockaddr*)client_addr, addr_len); if (recv_len 0) { // 简单判断ACK消息以ACK:开头后面跟着序列号 if (strncmp(ack_buffer, ACK:, 4) 0) { uint32_t ack_seq atoi(ack_buffer 4); if (ack_seq sequence) { printf( 收到确认 ACK for Seq %u\n, ack_seq); ack_received 1; } else { printf( 收到过时或错误的ACK (Seq: %u)\n, ack_seq); } } } } else if (rv 0) { // 超时 printf( 等待ACK超时准备重试 (%d/%d)\n, retry_count 1, MAX_RETRY); retry_count; } else { perror(select错误); break; } } if (!ack_received) { printf([Seq:%u] 达到最大重试次数数据可能丢失。\n, sequence); } sleep(3); // 等待3秒后发送下一条数据 } close(sockfd); return 0; }4.2 关键代码解析#pragma pack(1) 这是跨语言通信的生命线。它告诉C编译器取消结构体的内存对齐确保结构体的每个成员在内存中连续存放没有编译器插入的“填充字节”。这样我们sendto发送出去的字节流其布局才能被Python的struct.unpack准确解析。校验和计算calculate_checksum函数计算了除checksum字段本身外所有字节的简单累加和。这是一种非常基础的数据完整性验证方式。接收方Python端可以用同样的算法计算接收数据的校验和并与数据包中的checksum字段对比如果不一致则说明数据在传输中可能损坏。超时与重传 使用select系统调用来监听Socket是否有数据可读并设置超时时间TIMEOUT_SEC。如果超时则增加重试计数并重新发送。这是一个简单的应用层可靠机制实现。网络字节序htons主机到网络短整型和htonl主机到网络长整型函数用于将多字节整数从主机字节序转换为网络字节序大端序。在我们的结构体中uint32_t和uint16_t需要转换。但注意我们发送的是整个结构体的内存映像其中的整数已经是主机字节序。更严谨的做法是在填充结构体字段时就对每个整型字段调用htonl等函数。为了简化示例我们在同一台机器同字节序上运行暂未处理。这是跨平台通信的另一个关键点下文会详细讨论。5. Python UDP客户端实现Python端作为数据接收方和确认发送方。5.1 完整代码python_udp_client.py#!/usr/bin/env python3 Python UDP客户端 (数据接收方) 运行: python python_udp_client.py 监听端口 import socket import struct import sys from datetime import datetime # 定义与C语言端完全对应的数据包格式 # I: uint32_t (4字节), Q: uint64_t (8字节), f: float (4字节), H: uint16_t (2字节) SENSOR_DATA_FORMAT I Q f H SENSOR_DATA_SIZE struct.calcsize(SENSOR_DATA_FORMAT) # 应为 484218 字节 def calculate_checksum(data_bytes): 计算校验和与C语言端算法一致除最后2字节外所有字节的和 # 校验和字段本身不参与计算所以取除最后2字节外的所有部分 sum_val 0 for byte in data_bytes[:-2]: # 排除最后两个字节即checksum字段 sum_val byte # 取低16位 return sum_val 0xFFFF def main(): if len(sys.argv) ! 2: print(f用法: {sys.argv[0]} 监听端口) print(f示例: {sys.argv[0]} 8888) sys.exit(1) listen_port int(sys.argv[1]) server_ip 0.0.0.0 # 监听所有网络接口 # 1. 创建UDP Socket sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 允许地址复用 # 2. 绑定地址和端口 server_address (server_ip, listen_port) try: sock.bind(server_address) print(fPython UDP接收端已启动监听在 {server_ip}:{listen_port}) except socket.error as e: print(f绑定端口失败: {e}) sock.close() sys.exit(1) print(等待接收C语言端发送的传感器数据...\n) while True: try: # 3. 接收数据 data_bytes, client_address sock.recvfrom(1024) # 缓冲区大小 received_len len(data_bytes) if received_len ! SENSOR_DATA_SIZE: print(f警告: 收到数据长度异常 ({received_len} 字节)期望 {SENSOR_DATA_SIZE} 字节。可能来自未知源。) continue # 4. 解析数据 try: sensor_id, timestamp, temperature, received_checksum struct.unpack(SENSOR_DATA_FORMAT, data_bytes) except struct.error as e: print(f解析数据包失败: {e}) continue # 5. 验证校验和 calculated_checksum calculate_checksum(data_bytes) if calculated_checksum ! received_checksum: print(f校验和错误! 接收的: {received_checksum}, 计算的: {calculated_checksum}。数据可能已损坏丢弃。) continue # 6. 处理数据这里简单打印 # 注意timestamp是从C语言端传来的网络字节序如果C端做了转换需要转换回主机字节序。 # 由于我们在示例中C端未做htonll转换且在同一台机器所以直接使用。 # 严谨的做法是使用 socket.ntohl, socket.ntohs 等函数转换。 dt datetime.fromtimestamp(timestamp / 1000.0) # 转换为秒 print(f[来自 {client_address[0]}:{client_address[1]}]) print(f 传感器ID: {sensor_id}) print(f 时间戳: {timestamp} ({dt.strftime(%Y-%m-%d %H:%M:%S)})) print(f 温度: {temperature:.2f} °C) print(f 校验和: {received_checksum} (验证通过)) # 7. 发送ACK确认 ack_message fACK:{sensor_id} # 简单ACK包含传感器ID作为序列号参考 sock.sendto(ack_message.encode(), client_address) print(f 已发送ACK: {ack_message}\n) except KeyboardInterrupt: print(\n用户中断程序退出。) break except socket.error as e: print(fSocket错误: {e}) break sock.close() if __name__ __main__: main()5.2 关键代码解析struct模块 这是Python处理二进制数据与C语言结构体对应的核心。SENSOR_DATA_FORMAT I Q f H定义了数据的格式必须与C语言结构体的成员类型和顺序严格对应。struct.calcsize可以计算出这个格式对应的字节数用于验证接收数据的完整性。校验和验证calculate_checksum函数复现了C语言端的算法。它遍历接收到的字节数据data_bytes忽略最后两个字节即传输过来的checksum字段本身计算累加和。然后将计算结果与数据包中解析出的received_checksum对比。这是保证数据在传输过程中未被篡改或损坏的关键一步。SO_REUSEADDR选项sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)这行代码非常重要。它允许Socket在关闭后立即复用相同的地址和端口避免出现“Address already in use”的错误这在快速重启服务器进行调试时非常有用。异常处理 代码包含了数据长度检查、解析异常和校验和错误处理。在实际网络中可能会收到任何数据健壮的程序必须能处理这些异常情况而不是直接崩溃。6. 编译、运行与效果验证6.1 在Linux/macOS上运行编译C程序gcc c_udp_server.c -o c_udp_server首先启动Python客户端接收端python3 python_udp_client.py 8888输出应显示Python UDP接收端已启动监听在 0.0.0.0:8888然后在另一个终端启动C语言服务端发送端./c_udp_server 127.0.0.1 8888输出应显示C语言UDP发送端已启动目标: 127.0.0.1:88886.2 在Windows上运行编译C程序 如果你使用MinGW命令相同。如果使用Visual Studio需要创建项目并编译。启动顺序相同 先启动Python客户端再启动C语言服务端。注意Windows命令行中运行Python可能是python而不是python3。6.3 预期运行结果Python客户端输出示例Python UDP接收端已启动监听在 0.0.0.0:8888 等待接收C语言端发送的传感器数据... [来自 127.0.0.1:54321] 传感器ID: 1001 时间戳: 1712345678901 (2024-04-05 12:34:38) 温度: 28.70 °C 校验和: 12345 (验证通过) 已发送ACK: ACK:1001 [来自 127.0.0.1:54321] 传感器ID: 1001 时间戳: 1712345678904 (2024-04-05 12:34:38) 温度: 31.20 °C 校验和: 23456 (验证通过) 已发送ACK: ACK:1001C语言服务端输出示例C语言UDP发送端已启动目标: 127.0.0.1:8888 [Seq:1] 发送数据: ID1001, 时间1712345678901, 温度28.70 收到确认 ACK for Seq 1 [Seq:2] 发送数据: ID1001, 时间1712345678904, 温度31.20 收到确认 ACK for Seq 2你会看到数据在两者之间成功传输、校验、确认并模拟了超时重传的逻辑。7. 核心挑战与最佳实践7.1 字节序Endianness问题这是跨平台如x86与ARM通信的头号杀手。x86架构通常使用小端序而网络协议规定使用大端序网络字节序。问题 如果C语言端在Intel CPU小端序上运行直接发送一个uint32_t sensor_id 1001的内存映像。在不做转换的情况下接收方如果是大端序机器解析出来的将是完全不同的数字。解决方案发送前转换在C语言端填充结构体时对所有整型字段使用htonl(),htons(),htonll()对于64位进行转换。接收后转换在Python端解析后对整型字段使用socket.ntohl(),socket.ntohs()转换回主机字节序。协议约定 双方明确约定所有多字节整数均采用网络字节序大端序。这是最规范的做法。修改C语言端结构体填充部分data.sensor_id htonl(1001); // 转换为网络字节序 data.timestamp htonll((uint64_t)time(NULL) * 1000); // float类型通常没有字节序问题但跨平台时需注意浮点格式一致性。 data.checksum htons(calculate_checksum(data)); // 校验和也要转换修改Python端解析后处理部分sensor_id_raw, timestamp_raw, temperature, received_checksum_raw struct.unpack(SENSOR_DATA_FORMAT, data_bytes) sensor_id socket.ntohl(sensor_id_raw) received_checksum socket.ntohs(received_checksum_raw) # timestamp 如果是64位需要自定义转换或使用 struct.unpack(Q, ...) 直接按大端序解析7.2 结构体内存对齐与填充如前所述#pragma pack(1)或GCC的__attribute__((packed))是必须的。否则一个struct { char a; int b; }在64位系统上可能占用8字节而不是5字节多出的3字节就是填充会导致Python端解析错位。7.3 数据序列化与反序列化的更优方案对于更复杂的协议手动处理struct和字节序非常繁琐且易错。建议考虑Google Protocol Buffers (protobuf) 定义.proto文件可自动生成C和Python的编解码代码完美解决跨语言和版本兼容问题。MessagePack 一种高效的二进制序列化格式有丰富的语言支持。JSON 长度前缀 如果对性能要求不高可以将数据转为JSON字符串然后在前面加上一个固定长度的报文头表示JSON的长度。这种方式可读性好扩展性强。7.4 UDP数据包大小限制UDP数据报的最大理论长度是65535字节包括IP头。但在实际网络中需要避免超过MTU最大传输单元通常1500字节否则数据包会在IP层被分片增加丢失概率和延迟。一个安全的做法是将应用层数据包控制在1400字节以下。7.5 错误处理与日志生产环境代码必须有完善的错误处理和日志记录。检查所有系统调用返回值socket,bind,sendto,recvfrom,select等都可能失败。使用errno/WSAGetLastError() 获取具体的错误原因。记录日志 记录发送/接收的序列号、时间、对方地址、数据摘要等便于问题追踪。8. 常见问题排查表问题现象可能原因排查步骤解决方案C程序编译失败缺少头文件或链接库Windows。检查错误信息。在Windows上确保链接了ws2_32.lib。Linux/macOS:#include arpa/inet.hWindows:#include winsock2.h并#pragma comment(lib, ws2_32.lib)bind(): Address already in use端口被占用或程序上次异常退出未释放。使用netstat -an | grep 端口号(Linux) 或netstat -ano | findstr 端口号(Windows) 查看占用进程。1. 使用SO_REUSEADDR选项。2. 更换端口。3. 等待系统释放通常几分钟。能发送但收不到数据/ACK防火墙阻止、IP/端口错误、绑定地址错误、路由问题。1. 双方用netstat确认Socket状态。2. 用ping测试网络连通性。3.使用Wireshark抓包看数据包是否真的到达网卡。1. 关闭防火墙或添加规则。2. 检查代码中的IP和端口。3. 服务端绑定0.0.0.0以监听所有接口。Python端解析数据出错1. 字节序不匹配。2. 结构体对齐不一致。3. 数据包长度不对。1. 打印接收到的原始字节 (data_bytes.hex())。2. 与C语言端发送前的内存 (hexdump) 对比。3. 确认双方的结构体定义和struct格式字符串完全一致。1. 统一使用网络字节序并正确转换。2. C语言端使用#pragma pack(1)。3. 验证SENSOR_DATA_SIZE。数据校验和失败网络传输中数据损坏罕见或校验和算法不一致。在双方计算校验和的位置打印中间值进行比对。确保校验和算法的实现完全一致包括哪些字节参与计算。程序运行一段时间后卡死recvfrom是阻塞调用如果没有数据会一直等待。检查发送方是否停止发送或网络是否中断。1. 使用settimeout设置超时。2. 使用select/poll等多路复用机制。3. 使用多线程处理网络I/O。9. 总结与扩展方向通过这个完整的实战项目我们不仅实现了C语言与Python之间基于UDP的通信更深入理解了跨语言网络编程的核心挑战协议一致性。这不仅仅是“能通”更是“可靠地通”。本文的核心价值在于提供了可落地的解决方案一个可工作的代码框架 包含完整的发送、接收、校验、确认、重传逻辑。对关键陷阱的深入剖析 字节序、结构体对齐是纸上谈兵时最容易忽略实际开发中最容易导致诡异Bug的根源。从调试到生产的实践指南 从如何使用Wireshark抓包到生产环境的最佳实践。你可以在此基础上进行扩展多线程/异步处理 让服务端能同时处理多个客户端或同时进行发送和接收。实现真正的可靠文件传输 设计包含分块、序号、确认、滑动窗口的完整协议。集成到现有项目 将C语言端嵌入你的嵌入式设备代码将Python端作为上位机数据收集与分析服务。尝试其他序列化方案 用protobuf替代手动的struct打包体验更工程化、更安全的开发流程。UDP就像一把精巧的瑞士军刀在正确的场景下使用它能带来远超TCP的简洁与高效。希望这篇文章能帮助你在下一个需要打通C与Python世界的项目中自信地选择并实现UDP通信方案。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门