# 实验05 Linux 网络通信编程 ## 实验目的 1. 掌握 Socket 编程的基本流程(socket/bind/listen/accept/connect) 2. 理解 TCP 客户端/服务器模型 3. 学会实现文件下载和远程 shell 等网络应用 4. 掌握 HTTP 协议的基本交互方式 5. 了解静态网页和动态网页的生成原理 ## 涉及知识点 - Socket 地址结构:`sockaddr_in`、`inet_ntoa`、`htonl`/`htons` - TCP 服务器流程:`socket` -> `bind` -> `listen` -> `accept` -> `read/write` - TCP 客户端流程:`socket` -> `connect` -> `read/write` - HTTP 请求/响应格式 - 文件传输与 `send`/`recv` - 进程与网络 I/O 结合(远程 shell) - Wrapper 库辅助函数:`open_listen_sock`、`open_client_sock` --- ## 任务一:toggle 服务器测试 ### 任务要求 测试课程提供的 toggle 服务器和客户端程序,理解基本的 Socket 通信流程。 ### 操作步骤 ```bash # 编译 gcc -o toggles toggle_server.c -L. -lwrapper gcc -o togglec toggle_client.c -L. -lwrapper # 终端 1:启动服务器 ./toggles 8080 # 终端 2:启动客户端 ./togglec localhost 8080 ``` ### TCP 服务器基本流程 ``` socket() -- 创建套接字 | bind() -- 绑定地址和端口 | listen() -- 监听连接 | while(1) { accept() -- 接受客户端连接 | read() -- 读取请求 | write() -- 发送响应 | close() -- 关闭连接 } ``` ### 常见问题 | 问题 | 原因 | 解决方法 | |------|------|----------| | `bind` 失败 "Address already in use" | 端口被占用 | 等待几分钟或用 `setsockopt` 设置 `SO_REUSEADDR` | | 客户端连接超时 | 服务器未启动或防火墙 | 检查服务器状态和端口是否开放 | | 中文乱码 | 编码不一致 | 统一使用 UTF-8 | --- ## 任务二:weblet 服务器 ### 任务要求 测试课程提供的 weblet 服务器,理解 HTTP 请求处理: 1. 静态网页服务(返回 HTML 文件) 2. 动态网页生成(CGI 方式) ### 操作步骤 ```bash # 编译 gcc -o weblet weblet.c -L. -lwrapper # 启动 weblet 服务器 ./weblet 8080 # 在浏览器访问 # http://localhost:8080/index.html # http://localhost:8080/cgi-bin/hello ``` ### HTTP 请求格式 ``` GET /index.html HTTP/1.1 Host: localhost:8080 Connection: close ``` ### HTTP 响应格式 ``` HTTP/1.1 200 OK Content-Type: text/html Content-Length: 123 ... ``` ### 常见问题 | 问题 | 原因 | 解决方法 | |------|------|----------| | 404 Not Found | 文件路径错误 | 检查 `DocumentRoot` 和请求路径 | | 中文乱码 | Content-Type 缺少 charset | 添加 `Content-Type: text/html; charset=utf-8` | | 浏览器无法访问 | 防火墙或端口未开放 | 关闭防火墙或开放对应端口 | --- ## 任务三:task83s.c / task83c.c —— 文件下载 ### 任务要求 实现一个简单的文件下载服务: - 客户端发送文件名请求 - 服务器查找文件并返回文件内容 - 客户端接收并保存到本地 ### 关键代码提示 **服务器 task83s.c:** ```c #include #include #include #include #include #include #include #include #define MAXLINE 8192 int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "用法: %s <端口>\n", argv[0]); exit(1); } int port = atoi(argv[1]); int listen_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(listen_fd, 5); printf("文件下载服务器启动,端口 %d\n", port); while (1) { struct sockaddr_in cliaddr; socklen_t clien = sizeof(cliaddr); int conn_fd = accept(listen_fd, (struct sockaddr *)&cliaddr, &clien); printf("客户端 %s:%d 已连接\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); // 读取文件名 char filename[MAXLINE]; int n = recv(conn_fd, filename, MAXLINE - 1, 0); filename[n] = '\0'; // 打开并发送文件 int file_fd = open(filename, O_RDONLY); if (file_fd < 0) { send(conn_fd, "FILE_NOT_FOUND", 14, 0); } else { char buf[MAXLINE]; while ((n = read(file_fd, buf, MAXLINE)) > 0) send(conn_fd, buf, n, 0); close(file_fd); } close(conn_fd); } close(listen_fd); return 0; } ``` **客户端 task83c.c:** ```c #include #include #include #include #include #include #include #include #define MAXLINE 8192 int main(int argc, char **argv) { if (argc != 4) { fprintf(stderr, "用法: %s <主机> <端口> <文件名>\n", argv[0]); exit(1); } // 解析服务器地址 struct hostent *hp = gethostbyname(argv[1]); int port = atoi(argv[2]); int sock_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; memcpy(&servaddr.sin_addr, hp->h_addr, hp->h_length); servaddr.sin_port = htons(port); connect(sock_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 发送文件名 send(sock_fd, argv[3], strlen(argv[3]), 0); // 接收文件内容 char buf[MAXLINE]; int n; char save_name[256]; snprintf(save_name, sizeof(save_name), "downloaded_%s", argv[3]); int out_fd = open(save_name, O_WRONLY | O_CREAT | O_TRUNC, 0644); while ((n = recv(sock_fd, buf, MAXLINE, 0)) > 0) { write(out_fd, buf, n); } close(out_fd); close(sock_fd); printf("文件已下载为 %s\n", save_name); return 0; } ``` ### 常见问题 | 问题 | 原因 | 解决方法 | |------|------|----------| | 下载的文件不完整 | `send`/`recv` 短传 | 循环发送/接收,检查返回值 | | 文件名含路径 | 安全隐患 | 生产环境应过滤 `..` 等路径遍历 | | 大文件传输失败 | 缓冲区不够大 | 分块传输,每块用 `send` 发送 | --- ## 任务四:task84s.c / task84c.c —— 远程 shell ### 任务要求 实现一个远程 shell 服务: - 客户端发送 shell 命令 - 服务器执行命令并返回输出结果 - 客户端显示命令输出 ### 关键代码提示 **服务器 task84s.c:** ```c #include #include #include #include #include #include #include #define MAXLINE 8192 int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "用法: %s <端口>\n", argv[0]); exit(1); } int port = atoi(argv[1]); int listen_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); int optval = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(listen_fd, 5); printf("远程 shell 服务器启动,端口 %d\n", port); while (1) { struct sockaddr_in cliaddr; socklen_t clien = sizeof(cliaddr); int conn_fd = accept(listen_fd, (struct sockaddr *)&cliaddr, &clien); printf("客户端 %s 已连接\n", inet_ntoa(cliaddr.sin_addr)); // 用 dup2 将命令输出重定向到 socket if (fork() == 0) { close(listen_fd); dup2(conn_fd, STDOUT_FILENO); dup2(conn_fd, STDERR_FILENO); char cmd[MAXLINE]; int n; while ((n = recv(conn_fd, cmd, MAXLINE - 1, 0)) > 0) { cmd[n] = '\0'; // 去除换行符 if (cmd[n - 1] == '\n') cmd[n - 1] = '\0'; system(cmd); } exit(0); } close(conn_fd); } close(listen_fd); return 0; } ``` **客户端 task84c.c:** ```c #include #include #include #include #include #include #include #define MAXLINE 8192 int main(int argc, char **argv) { if (argc != 3) { fprintf(stderr, "用法: %s <主机> <端口>\n", argv[0]); exit(1); } struct hostent *hp = gethostbyname(argv[1]); int port = atoi(argv[2]); int sock_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; memcpy(&servaddr.sin_addr, hp->h_addr, hp->h_length); servaddr.sin_port = htons(port); connect(sock_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); printf("已连接到 %s:%d\n", argv[1], port); char cmd[MAXLINE]; while (1) { printf("remote> "); fflush(stdout); if (fgets(cmd, sizeof(cmd), stdin) == NULL) break; send(sock_fd, cmd, strlen(cmd), 0); // 接收输出 char buf[MAXLINE]; int n; // 简单方式:等待并读取(生产环境需要更复杂的协议) usleep(100000); // 等待服务器执行 while ((n = recv(sock_fd, buf, MAXLINE - 1, MSG_DONTWAIT)) > 0) { buf[n] = '\0'; printf("%s", buf); } } close(sock_fd); return 0; } ``` ### 常见问题 | 问题 | 原因 | 解决方法 | |------|------|----------| | 命令输出不完整 | `recv` 时机不确定 | 使用长度前缀协议或特殊结束标记 | | 服务器僵尸进程 | 未处理 `SIGCHLD` | 注册 `SIGCHLD` handler 用 `waitpid` 回收 | | 安全风险 | `system()` 执行任意命令 | 仅用于实验,生产环境需严格限制命令 | --- ## 实验总结 通过本实验,应掌握以下能力: 1. 使用 Socket API 实现 TCP 客户端/服务器 2. 理解 HTTP 协议的基本请求/响应格式 3. 实现文件下载服务,理解数据传输流程 4. 实现远程 shell,理解 I/O 重定向与网络结合 5. 掌握 `dup2` 在网络编程中的应用