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

424 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实验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` 在网络编程中的应用