Files
obsidian/操作系统/实验/实验05_网络通信.md

424 lines
10 KiB
Markdown
Raw Permalink Normal View History

2026-06-13 23:46:22 +08:00
# 实验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
<html>...</html>
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 404 Not Found | 文件路径错误 | 检查 `DocumentRoot` 和请求路径 |
| 中文乱码 | Content-Type 缺少 charset | 添加 `Content-Type: text/html; charset=utf-8` |
| 浏览器无法访问 | 防火墙或端口未开放 | 关闭防火墙或开放对应端口 |
---
## 任务三task83s.c / task83c.c —— 文件下载
### 任务要求
实现一个简单的文件下载服务:
- 客户端发送文件名请求
- 服务器查找文件并返回文件内容
- 客户端接收并保存到本地
### 关键代码提示
**服务器 task83s.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#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` 在网络编程中的应用