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

10 KiB
Raw Blame History

实验05 Linux 网络通信编程

实验目的

  1. 掌握 Socket 编程的基本流程socket/bind/listen/accept/connect
  2. 理解 TCP 客户端/服务器模型
  3. 学会实现文件下载和远程 shell 等网络应用
  4. 掌握 HTTP 协议的基本交互方式
  5. 了解静态网页和动态网页的生成原理

涉及知识点

  • Socket 地址结构:sockaddr_ininet_ntoahtonl/htons
  • TCP 服务器流程:socket -> bind -> listen -> accept -> read/write
  • TCP 客户端流程:socket -> connect -> read/write
  • HTTP 请求/响应格式
  • 文件传输与 send/recv
  • 进程与网络 I/O 结合(远程 shell
  • Wrapper 库辅助函数:open_listen_sockopen_client_sock

任务一toggle 服务器测试

任务要求

测试课程提供的 toggle 服务器和客户端程序,理解基本的 Socket 通信流程。

操作步骤

# 编译
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 方式)

操作步骤

# 编译
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

#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

#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

#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

#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 在网络编程中的应用