资讯动态

纯C实现生产级FTP客户端:Socket编程实战与RFC959落地

发布时间:2026/10/26 17:41:05 来源:尧图企业网站定制
简介这是一份面向C语言网络编程初学者与高校计算机专业学生的Socket FTP实践项目资源聚焦FTP协议原理与TCP套接字底层实现帮助学习者掌握客户端/服务器双向通信、控制连接与数据连接协同、PASV模式文件传输等核心技能。压缩包共6个文件含2个关键源码ftp_client.cpp、ftp_server.cpp、2个可执行程序ftp_client.exe、ftp_server.exe、1份说明文档无法运行的请看说明.txt及1份扩展参考Easy FTP 相关文档.doc总大小258KB结构精炼便于编译调试与协议流程分析。已有203人下载学习资源源自实验4 socket编程教学任务完整覆盖套接字创建、connect/bind/listen调用、FTP命令交互USER/PASS/LIST等、错误处理及文件流分块传输等实操要点特别适合通过代码反推协议机制、构建轻量级FTP工具或完成课程设计。1. 用 C 语言手写一个能连上真实 FTP 服务器的 Socket 客户端不是 demo是能 ls / 上传下载、带响应解析、抗超时重连的生产级骨架你在网上搜“C socket ftp”十有八九看到的是教科书式 democonnect → send(USER anonymous\r\n) → recv → send(PASS\r\n) → 然后戛然而止。但真实场景里FTP 不是 HTTP 那种一问一答的协议——它分控制连接21 端口和数据连接主动模式 PORT 命令协商端口被动模式 PASV 返回新端口命令有同步/异步依赖响应码要按 2xx/3xx/4xx/5xx 分类处理超时、粘包、断连、防火墙 NAT 后的端口不可达……全是黑匣子。这个标题里的FTP_socket.rar_C和socket ftp不是玩具代码而是指一套可编译、可调试、可嵌入到嵌入式设备或 Linux 服务中、不依赖 libcurl 或 OpenSSL 的纯 C Socket 实现。它解决的是在资源受限环境比如 ARM Cortex-A9 板卡、旧版 CentOS 6 内核、或需要完全掌控网络行为如定制重试逻辑、日志埋点、证书绕过、代理穿透时如何从零构建一个真正可用的 FTP 客户端。适合嵌入式工程师、C 运维工具开发者、CTF 网络协议题手撕党——不是学完 TCP 就能跑通而是得把 RFC 959 里“PORT 命令格式”“PASV 响应解析”“NLST 与 LIST 区别”“RETR 断点续传边界”全吃透才能落地。2. 控制连接建立与命令交互从 connect 到 USER/PASS 的完整握手链路FTP 协议本质是文本协议但它的状态机比表面看起来复杂得多。控制连接必须维持长连接每个命令后必须等待对应响应码如220表示欢迎331表示用户名正确需密码且响应可能跨多个 recv() 调用到达TCP 粘包。我们不用 select/poll 做多路复用先聚焦单线程阻塞式实现——这是所有高级封装的底层基座。2.1 初始化 socket 并连接控制端口21#include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h #include netdb.h int ftp_connect_control(const char* host, int port) { struct hostent *he; struct sockaddr_in serv_addr; int sockfd; if ((he gethostbyname(host)) NULL) { perror(gethostbyname); return -1; } if ((sockfd socket(AF_INET, SOCK_STREAM, 0)) 0) { perror(socket); return -1; } memset(serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family AF_INET; serv_addr.sin_port htons(port); serv_addr.sin_addr *((struct in_addr *)he-h_addr); // 关键设置 SO_RCVTIMEO 防止 recv 永久阻塞 struct timeval timeout {10, 0}; // 10 秒超时 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, timeout, sizeof(timeout)); if (connect(sockfd, (struct sockaddr*)serv_addr, sizeof(serv_addr)) 0) { perror(connect); close(sockfd); return -1; } return sockfd; }提示SO_RCVTIMEO是救命参数。很多初学者卡在recv()死等结果服务器没回包就挂住。这里设 10 秒后续所有recv_response()都基于此超时机制做重试判断。gethostbyname在现代系统中已 deprecated但为兼容旧环境如某些嵌入式 uClibc仍保留生产环境建议改用getaddrinfo()。2.2 发送命令并解析响应码核心状态机驱动FTP 响应以三位数字开头后跟空格和描述文本结尾为\r\n。但注意有些响应如226 Transfer complete是单行而目录列表LIST响应后会跟数据连接内容——此时控制连接仍在等待226结束信号。所以我们必须严格区分“控制响应”和“数据响应”。// 读取一行响应自动处理 \r\n int recv_line(int sockfd, char* buf, size_t bufsize) { ssize_t n; size_t len 0; char c; while (len bufsize - 1) { n recv(sockfd, c, 1, 0); if (n 0) return -1; if (c \r) { // 尝试读取下一个字符是否为 \n n recv(sockfd, c, 1, MSG_PEEK); if (n 0 c \n) { recv(sockfd, c, 1, 0); // consume \n } break; } buf[len] c; } buf[len] \0; return len; } // 发送命令 读取响应码返回 220/331/230 等整数 int ftp_send_cmd(int sockfd, const char* cmd) { char response[1024]; int code; // 发送命令确保 \r\n if (send(sockfd, cmd, strlen(cmd), 0) ! strlen(cmd)) { perror(send command); return -1; } // 读取响应行 if (recv_line(sockfd, response, sizeof(response)) 0) { fprintf(stderr, No response to: %s\n, cmd); return -1; } // 解析三位响应码忽略空格后内容 if (sscanf(response, %3d, code) ! 1) { fprintf(stderr, Invalid response format: %s\n, response); return -1; } printf( %s - %s (%d)\n, cmd, response, code); return code; }参数说明recv_line()是关键——它逐字节读直到遇到\r再 peek 是否有\n避免因 TCP 分包导致\r\n被拆开。ftp_send_cmd()返回整数响应码后续所有逻辑如if (code 230)都基于此判断状态流转。注意send()后不 sleep直接recv_line()这是阻塞式协议的正确节奏。2.3 完整登录流程USER → PASS → SYST → FEAT验证服务器能力int ftp_login(int sockfd, const char* user, const char* pass) { char cmd[256]; // 1. USER snprintf(cmd, sizeof(cmd), USER %s\r\n, user); if (ftp_send_cmd(sockfd, cmd) ! 331) { fprintf(stderr, USER failed\n); return -1; } // 2. PASS snprintf(cmd, sizeof(cmd), PASS %s\r\n, pass); if (ftp_send_cmd(sockfd, cmd) ! 230) { fprintf(stderr, PASS failed\n); return -1; } // 3. 可选获取系统类型确认是否支持 PASV if (ftp_send_cmd(sockfd, SYST\r\n) 0) { fprintf(stderr, SYST failed, continue anyway\n); } // 4. 获取扩展功能重点看是否有 PASV 支持 if (ftp_send_cmd(sockfd, FEAT\r\n) 211) { char feat_buf[4096]; while (recv_line(sockfd, feat_buf, sizeof(feat_buf)) 0) { if (strstr(feat_buf, PASV)) { printf(Server supports PASV mode\n); break; } } } return 0; } // 使用示例 int main() { int ctrl_sock ftp_connect_control(192.168.1.100, 21); if (ctrl_sock 0) return 1; if (ftp_login(ctrl_sock, admin, 123456) 0) { close(ctrl_sock); return 1; } printf(Login success!\n); close(ctrl_sock); return 0; }逻辑说明FEAT命令返回的扩展列表里必须包含PASV才能启用被动模式——这是现代网络尤其 NAT 后唯一可靠的方式。SYST返回UNIX或WINDOWS影响后续路径分隔符/vs\但多数客户端忽略此细节。整个流程强调状态驱动USER必须返回331才发PASSPASS必须返回230才算登录成功任何一步失败立即退出。3. 数据连接建立PASV 模式下解析 IP:Port 并创建新 socket主动模式PORT要求客户端开放随机端口并告知服务器但在 NAT/防火墙环境下几乎必跪PASV 模式由服务器分配数据端口并返回客户端主动连接——这才是实际部署的唯一选择。难点在于PASV 响应格式不统一RFC 959 规定逗号分隔但有些服务器用空格或括号且 IP 地址可能是服务器内网地址需映射。3.1 发送 PASV 命令并解析响应中的 IP 和端口typedef struct { char ip[16]; uint16_t port; } ftp_pasv_addr; // 解析 PASV 响应如 227 Entering Passive Mode (192,168,1,100,197,145) int parse_pasv_response(const char* response, ftp_pasv_addr* addr) { char ip_part[4][4]; // 存储四个 IP 段 int p1, p2; // 端口高字节、低字节 int n; // 先找括号内的内容兼容多种格式 const char* start strchr(response, (); if (!start) { fprintf(stderr, No ( in PASV response\n); return -1; } start; const char* end strchr(start, )); if (!end) { fprintf(stderr, No ) in PASV response\n); return -1; } // 尝试匹配逗号分隔格式a,b,c,d,e,f n sscanf(start, %[^,],%[^,],%[^,],%[^,],%d,%d, ip_part[0], ip_part[1], ip_part[2], ip_part[3], p1, p2); if (n ! 6) { // 备用空格分隔某些服务器 n sscanf(start, %s %s %s %s %d %d, ip_part[0], ip_part[1], ip_part[2], ip_part[3], p1, p2); if (n ! 6) { fprintf(stderr, Failed to parse PASV: %s\n, start); return -1; } } snprintf(addr-ip, sizeof(addr-ip), %s.%s.%s.%s, ip_part[0], ip_part[1], ip_part[2], ip_part[3]); addr-port (p1 8) | p2; // 端口 高字节 * 256 低字节 printf(PASV resolved: %s:%u\n, addr-ip, addr-port); return 0; } int ftp_pasv_connect(int ctrl_sock, ftp_pasv_addr* addr) { char pasv_cmd[64]; int code; // 发送 PASV strcpy(pasv_cmd, PASV\r\n); code ftp_send_cmd(ctrl_sock, pasv_cmd); if (code ! 227) { fprintf(stderr, PASV command failed: %d\n, code); return -1; } // 读取响应行PASV 响应在 send 后立即返回 char response[1024]; if (recv_line(ctrl_sock, response, sizeof(response)) 0) { return -1; } if (parse_pasv_response(response, addr) 0) { return -1; } // 创建数据 socket 并连接 struct sockaddr_in data_addr; int data_sock socket(AF_INET, SOCK_STREAM, 0); if (data_sock 0) { perror(data socket); return -1; } memset(data_addr, 0, sizeof(data_addr)); data_addr.sin_family AF_INET; data_addr.sin_port htons(addr-port); inet_pton(AF_INET, addr-ip, data_addr.sin_addr); if (connect(data_sock, (struct sockaddr*)data_addr, sizeof(data_addr)) 0) { perror(data connect); close(data_sock); return -1; } return data_sock; }参数说明parse_pasv_response()是血泪经验——不同 FTP 服务器vsftpd、proftpd、FileZilla Server返回格式差异极大。我们优先匹配(a,b,c,d,e,f)失败后 fallback 到空格分隔。端口计算(p1 8) | p2是 RFC 强制要求不能直接p1*256p2虽然数学等价但位运算更清晰。ftp_pasv_connect()返回的是数据连接 socket后续LIST/RETR的数据都走这个 fd控制连接仍保持活跃。3.2 执行 LIST 命令获取目录列表带数据连接收发// 执行 LIST 命令并打印目录内容 int ftp_list(int ctrl_sock, int data_sock) { char cmd[128]; char buf[4096]; ssize_t n; // 发送 LIST 命令 strcpy(cmd, LIST\r\n); if (ftp_send_cmd(ctrl_sock, cmd) ! 150) { fprintf(stderr, LIST command failed\n); return -1; } // 从数据连接读取目录列表直到 EOF 或 recv 返回 0 printf(Directory listing:\n); while ((n recv(data_sock, buf, sizeof(buf)-1, 0)) 0) { buf[n] \0; printf(%s, buf); } if (n 0) { perror(recv list data); } // 等待数据连接关闭后的 226 响应 int code ftp_send_cmd(ctrl_sock, ); // 空命令不发只等响应 if (code ! 226) { fprintf(stderr, LIST data transfer not completed: %d\n, code); return -1; } return 0; } // 使用示例接 login 之后 ftp_pasv_addr pasv; int data_sock ftp_pasv_connect(ctrl_sock, pasv); if (data_sock 0) { close(ctrl_sock); return 1; } ftp_list(ctrl_sock, data_sock); close(data_sock); close(ctrl_sock);逻辑说明LIST流程分三步1) 控制连接发LIST→ 期望150数据连接即将打开2) 从data_sock读取原始目录文本Unix 格式drwxr-xr-x 1 user group 4096 Jan 1 12:00 dir/3) 数据连接关闭后控制连接必须收到226表示传输完成。注意ftp_send_cmd(ctrl_sock, )是个 trick我们不发新命令只是调用recv_line()等待服务器主动推送226因为数据连接关闭触发该响应。4. 文件上传与下载RETR/STOR 命令 二进制流处理 断点续传基础FTP 默认是 ASCII 模式但传图片、压缩包必须切到TYPE IImage/Binary。RETR下载和STOR上传本质相同都是打开数据连接 → 读/写文件流 → 等待226。区别在于方向和文件打开模式。4.1 下载文件RETR边 recv 边写入磁盘支持大文件流式处理int ftp_retr(int ctrl_sock, int data_sock, const char* remote_file, const char* local_file) { char cmd[256]; FILE* fp; char buf[8192]; ssize_t n; // 切换二进制模式必须否则图片损坏 if (ftp_send_cmd(ctrl_sock, TYPE I\r\n) ! 200) { fprintf(stderr, Failed to set TYPE I\n); return -1; } // 发送 RETR 命令 snprintf(cmd, sizeof(cmd), RETR %s\r\n, remote_file); if (ftp_send_cmd(ctrl_sock, cmd) ! 150) { fprintf(stderr, RETR %s failed\n, remote_file); return -1; } // 打开本地文件二进制写 fp fopen(local_file, wb); if (!fp) { perror(fopen local file); return -1; } // 流式接收并写入 printf(Downloading %s - %s...\n, remote_file, local_file); while ((n recv(data_sock, buf, sizeof(buf), 0)) 0) { if (fwrite(buf, 1, n, fp) ! (size_t)n) { perror(fwrite); fclose(fp); return -1; } } fclose(fp); // 等待 226 if (ftp_send_cmd(ctrl_sock, ) ! 226) { fprintf(stderr, RETR transfer incomplete\n); return -1; } printf(Download complete.\n); return 0; }参数说明wb模式确保 Windows/Linux 换行符不被转换recv()缓冲区设为 8KB 是平衡内存占用与吞吐的常见值太小频繁 syscall太大浪费内存ftp_send_cmd(ctrl_sock, )再次用于等待226。注意RETR前必须TYPE I否则文本模式会把\r\n转成\n破坏二进制完整性。4.2 上传文件STOR分块 read send避免内存加载整文件int ftp_stor(int ctrl_sock, int data_sock, const char* local_file, const char* remote_file) { char cmd[256]; FILE* fp; char buf[8192]; ssize_t n; if (ftp_send_cmd(ctrl_sock, TYPE I\r\n) ! 200) { return -1; } snprintf(cmd, sizeof(cmd), STOR %s\r\n, remote_file); if (ftp_send_cmd(ctrl_sock, cmd) ! 150) { fprintf(stderr, STOR %s failed\n, remote_file); return -1; } fp fopen(local_file, rb); if (!fp) { perror(fopen local file); return -1; } printf(Uploading %s - %s...\n, local_file, remote_file); while ((n fread(buf, 1, sizeof(buf), fp)) 0) { if (send(data_sock, buf, n, 0) ! n) { perror(send data); fclose(fp); return -1; } } fclose(fp); if (ftp_send_cmd(ctrl_sock, ) ! 226) { fprintf(stderr, STOR transfer incomplete\n); return -1; } printf(Upload complete.\n); return 0; }逻辑说明fread()/send()对称于recv()/fwrite()保证流式处理。关键点STOR命令后服务器打开数据连接等待接收我们fread一块就send一块全程无内存缓存整个文件——这对 GB 级日志上传至关重要。TYPE I同样不可省略。4.3 断点续传基础REST 命令 fseek 设置偏移真正生产级客户端必须支持断点续传。FTP 协议提供REST命令指定起始偏移配合RETR即可续下。// 续传下载先获取远程文件大小SIZE 命令再对比本地已下载字节数 int ftp_retr_resume(int ctrl_sock, int data_sock, const char* remote_file, const char* local_file) { char cmd[256], response[1024]; FILE* fp; long local_size 0, remote_size 0; char buf[8192]; ssize_t n; // 获取远程文件大小 snprintf(cmd, sizeof(cmd), SIZE %s\r\n, remote_file); if (ftp_send_cmd(ctrl_sock, cmd) 213) { if (recv_line(ctrl_sock, response, sizeof(response)) 0) { remote_size atol(response); } } // 获取本地文件当前大小 fp fopen(local_file, rb); if (fp) { fseek(fp, 0, SEEK_END); local_size ftell(fp); fclose(fp); } if (local_size 0 local_size remote_size) { // 发送 REST 命令跳过已下载部分 snprintf(cmd, sizeof(cmd), REST %ld\r\n, local_size); if (ftp_send_cmd(ctrl_sock, cmd) ! 350) { fprintf(stderr, REST failed, starting from beginning\n); local_size 0; } } // 重新打开文件追加模式 fp fopen(local_file, local_size 0 ? wb : ab); if (!fp) { perror(fopen resume file); return -1; } // 发送 RETR此时从 REST 指定位置开始 snprintf(cmd, sizeof(cmd), RETR %s\r\n, remote_file); if (ftp_send_cmd(ctrl_sock, cmd) ! 150) { fclose(fp); return -1; } // 接收剩余数据 while ((n recv(data_sock, buf, sizeof(buf), 0)) 0) { if (fwrite(buf, 1, n, fp) ! (size_t)n) { perror(fwrite resume); fclose(fp); return -1; } } fclose(fp); if (ftp_send_cmd(ctrl_sock, ) ! 226) { return -1; } printf(Resume download complete (%ld/%ld bytes)\n, local_size ftell(fp), remote_size); return 0; }参数说明SIZE命令返回213后紧跟字节数如213 123456789atol()提取fseek(fp, 0, SEEK_END)ftell()获取本地文件长度REST必须在RETR前发送且服务器返回350表示接受续传。fopen(..., ab)确保写入追加而非覆盖。这是断点续传最小可行方案未处理SIZE不支持的情况可 fallback 到MDTM获取修改时间做粗略判断。5. 避坑指南5 个让 C Socket FTP 客户端集体翻车的真实问题写完上面代码你以为能跑通现实是90% 的失败不在协议逻辑而在环境细节。以下是我在 ARM 设备、CentOS 6、Windows Subsystem for Linux 上踩过的坑按现象→原因→解决整理每条都附带strace或tcpdump验证方法。5.1 现象connect()成功但recv_line()一直阻塞strace显示recvfrom()挂起原因服务器启用了ftp_idle_timeout如 vsftpd 的idle_session_timeout300控制连接空闲超时后静默断开但recv()不报错只永远等待。解决在recv_line()前加心跳保活——每 60 秒发NOOP\r\n命令。修改recv_line()顶层加计时器或单独起线程定时ftp_send_cmd(sockfd, NOOP\r\n)。注意NOOP响应码是200无需处理。5.2 现象PASV返回227但解析出的 IP 是192.168.x.xconnect()失败原因FTP 服务器在 NAT 后PASV响应中 IP 是内网地址客户端无法直连。这是被动模式经典陷阱。解决强制使用EPSV扩展被动模式替代PASV。EPSV返回229 Entering Extended Passive Mode (|||5001|)端口直接给出IP 由控制连接的源地址隐含即服务器知道该连谁。代码只需替换PASV为EPSV解析逻辑改为提取|包裹的端口号。若服务器不支持EPSV老版本则需配置服务器pasv_address为公网 IP。5.3 现象RETR下载的 PNG 文件打不开file命令显示data而非PNG image原因漏掉TYPE I命令FTP 以 ASCII 模式传输\r\n被转义二进制头损坏。解决所有RETR/STOR前必须调用ftp_send_cmd(ctrl_sock, TYPE I\r\n)且检查返回码为200。可在ftp_login()后默认执行一次避免遗漏。5.4 现象LIST返回空但ftp_send_cmd()显示150recv()立即返回 0原因数据连接建立后服务器因权限问题拒绝列出目录如匿名用户无ls权限但错误发生在数据连接层面控制连接仍发150然后静默关闭数据连接。解决recv()返回 0 时不要认为成功立即调用ftp_send_cmd(ctrl_sock, )读取控制连接的最终响应——大概率是425 Cant open data connection或550 Permission denied。把recv()的 0 返回视为错误分支统一处理。5.5 现象同一台机器并发运行两个客户端第二个bind()失败报Address already in use原因socket()创建后未设置SO_REUSEADDRTIME_WAIT 状态端口被占。FTP 客户端虽不 bind但connect()时内核自动分配本地端口高频短连接易耗尽 ephemeral port。解决在socket()后立即设置int opt 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));同时Linux 下调大端口范围echo net.ipv4.ip_local_port_range 1024 65535 /etc/sysctl.conf。这不是 FTP 协议问题而是操作系统网络栈常识。6. 生产就绪技巧超时重试、日志埋点、与嵌入式环境适配的最后三步写完功能只是起点。真正投入生产还得补上这三板斧让失败可追溯、让重试有策略、让代码能在资源紧张的设备上呼吸。6.1 带退避策略的命令重试避免雪崩FTP 服务器偶尔抖动如磁盘满、进程卡死简单while (ftp_send_cmd(...) ! 230) sleep(1)会压垮服务器。我们实现指数退避int ftp_send_cmd_retry(int sockfd, const char* cmd, int max_retries) { int base_delay 100; // 毫秒 int code; for (int i 0; i max_retries; i) { code ftp_send_cmd(sockfd, cmd); if (code 0) return code; // 成功 if (i max_retries) { usleep(base_delay); base_delay * 2; // 指数增长100ms → 200ms → 400ms... } } return code; // 最后一次失败 } // 使用ftp_send_cmd_retry(ctrl_sock, USER admin\r\n, 3);技巧说明usleep()比sleep()精确避免秒级等待。max_retries3对应总等待约 700ms既防瞬时故障又不拖慢流程。所有对外命令USER/PASS/PASV/RETR都应包装此函数。6.2 轻量级日志埋点不依赖 syslog输出到文件或串口嵌入式设备常无syslogd我们用fprintf(stderr, [FTP] %s\n, msg)重定向到文件FILE* ftp_log_fp NULL; void ftp_init_log(const char* log_path) { if (log_path) { ftp_log_fp fopen(log_path, a); if (!ftp_log_fp) { ftp_log_fp stderr; // fallback } } else { ftp_log_fp stderr; } } #define FTP_LOG(fmt, ...) \ do { \ fprintf(ftp_log_fp, [FTP %d] %s:%d fmt \n, \ (int)getpid(), __FILE__, __LINE__, ##__VA_ARGS__); \ fflush(ftp_log_fp); \ } while(0) // 在 ftp_send_cmd 中插入FTP_LOG(Sending: %s, cmd);参数说明fflush()确保日志实时写入避免缓冲丢失getpid()标识多实例__FILE__/__LINE__方便定位。日志级别可扩展为FTP_LOG_ERR/FTP_LOG_INFO但生产环境够用即可。6.3 嵌入式适配内存池替代 malloc禁用 DNS 查询在 32MB RAM 的 ARM 设备上gethostbyname()可能因 libc DNS 缓存不足崩溃。终极方案是预置 IP// 替换 ftp_connect_control() 中的 gethostbyname int ftp_connect_by_ip(const char* ip_str, int port) { struct sockaddr_in serv_addr; int sockfd; sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd 0) return -1; memset(serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family AF_INET; serv_addr.sin_port htons(port); if (inet_pton(AF_INET, ip_str, serv_addr.sin_addr) 0) { close(sockfd); return -1; } // ... 后续 connect 相同 }技巧说明配置文件中直接写192.168.1.100而非ftp.example.com彻底规避 DNS。inet_pton()是 POSIX 标准比gethostbyname()更轻量。内存方面所有char buf[4096]改为静态分配栈空间足够避免malloc碎片化。我当年在某电力终端项目里就是靠这三步让 FTP 模块在 256MB RAM 的 i.MX6 上稳定运行三年零重启。后来发现最有效的 debug 工具不是 gdb而是tcpdump -i any port 21 -w ftp.pcap抓包后用 Wireshark 看协议流——它会告诉你服务器到底发了什么而不是你的代码以为它发了什么。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑