21-期中大作业

期中大作业—动手编写一个自己的程序吧!

开篇词

客户端程序:

主要考察使用 select 多路复用,一方面从标准输入接收字节流,另一方面通过套接字读写,以及使用 shutdown 关闭半连接的能力。

服务器端程序:
考察套接字读写的能力,以及对端连接关闭情况下的异常处理等能力。

题干

请你分别写一个客户端程序和服务器程序,客户端程序连接上服务器之后,通过敲命令和服务器进行交互,支持的交互命令包括:

  • pwd:显示服务器应用程序启动时的当前路径。
  • cd:改变服务器应用程序的当前路径。
  • ls:显示服务器应用程序当前路径下的文件列表。
  • quit:客户端进程退出,但是服务器端不能退出,第二个客户可以再次连接上服务器端。

客户端程序要求

可以指定待连接的服务器端 IP 地址和端口。
在输入一个命令之后,回车结束,之后等待服务器端将执行结果返回,客户端程序需要将结果显示在屏幕上。

样例输出如下所示。


第一次连接服务器
$./telnet-client 127.0.0.1 43211
pwd
/home/vagrant/shared/Code/network/yolanda/build/bin
cd ..
pwd
/home/vagrant/shared/Code/network/yolanda/build
cd ..
pwd
/home/vagrant/shared/Code/network/yolanda
ls
build
chap-11
chap-12
chap-13
chap-14
chap-15
chap-16
chap-17
chap-18
chap-20
chap-21
chap-22
chap-23
chap-25
chap-26
chap-27
chap-28
chap-4
chap-5
chap-6
chap-7
clean.sh
cmake-build-debug
CMakeLists.txt
lib
mid-homework
README.md


cd -
pwd
/home/vagrant/shared/Code/network/yolanda
cd /home
pwd
/home
ls
ubuntu
vagrant
quit

//再次连接服务器
$./telnet-client 127.0.0.1 43211
pwd
/home/vagrant/shared/Code/network/yolanda/build
ls
bin
chap-11
chap-12
chap-13
chap-15
chap-16
chap-17
chap-18
chap-20
chap-21
chap-22
chap-23
chap-25
chap-26
chap-28
chap-4
chap-5
chap-6
chap-7
CMakeCache.txt
CMakeFiles
cmake_install.cmake
lib
Makefile
mid-homework

quit

服务器程序要求

  1. 暂时不需要考虑多个客户并发连接的情形,只考虑每次服务一个客户连接。
  2. 要把命令执行的结果返回给已连接的客户端。
  3. 服务器端不能因为客户端退出就直接退出。

CMakeLists.txt

//CMakeLists.txt
add_executable(telnet-client telnet-client.c)
target_link_libraries(telnet-client yolanda)

add_executable(telnet-server telnet-server.c)
target_link_libraries(telnet-server yolanda)

客户端程序

#include "lib/common.h"

#define  MAXLINE     1024


int main(int argc, char **argv) {
    if (argc != 3) {
        error(1, 0, "usage: tcp_client <IPaddress> <port>");
    }
    int port = atoi(argv[2]);
    int socket_fd = tcp_client(argv[1], port);

    char recv_line[MAXLINE], send_line[MAXLINE];
    int n;

    fd_set readmask;
    fd_set allreads;
    FD_ZERO(&allreads);
    FD_SET(0, &allreads);
    FD_SET(socket_fd, &allreads);

    for (;;) {
        readmask = allreads;
        int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);

        if (rc <= 0) {
            error(1, errno, "select failed");
        }

        if (FD_ISSET(socket_fd, &readmask)) {
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {
                error(1, errno, "read error");
            } else if (n == 0) {
                printf("server closed \n");
                break;
            }
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        }

        if (FD_ISSET(STDIN_FILENO, &readmask)) {
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
                int i = strlen(send_line);
                if (send_line[i - 1] == '\n') {
                    send_line[i - 1] = 0;
                }

                if (strncmp(send_line, "quit", strlen(send_line)) == 0) {
                    if (shutdown(socket_fd, 1)) {
                        error(1, errno, "shutdown failed");
                    }
                }

                size_t rt = write(socket_fd, send_line, strlen(send_line));
                if (rt < 0) {
                    error(1, errno, "write failed ");
                }
            }
        }
    }

    exit(0);
}


客户端的代码:

  • 使用 select 同时处理标准输入和套接字;
  • 我看到有同学使用 fgets 来循环等待用户输入,然后再把输入的命令通过套接字发送出去,当然也是- 可以正常工作的,只不过不能及时响应来自服务端的命令结果;
  • 所以,我还是推荐使用 select 来同时处理标准输入和套接字。

这里 select 如果发现标准输入有事件,读出标准输入的字符,就会通过调用 write 方法发送出去。如果发现输入的是 quit,则调用 shutdown 方法关闭连接的一端。

如果 select 发现套接字流有可读事件,则从套接字中读出数据,并把数据打印到标准输出上;如果读到了 EOF,表示该客户端需要退出,直接退出循环,通过调用 exit 来完成进程的退出。

服务器端程序

服务器端程序:

#include "lib/common.h"

static int count;

static void sig_int(int signo) {
    printf("\nreceived %d datagrams\n", count);
    exit(0);
}

char *run_cmd(char *cmd) {
    char *data = malloc(16384);
    bzero(data, sizeof(data));
    FILE *fdp;
    const int max_buffer = 256;
    char buffer[max_buffer];
    fdp = popen(cmd, "r");
    char *data_index = data;
    if (fdp) {
        while (!feof(fdp)) {
            if (fgets(buffer, max_buffer, fdp) != NULL) {
                int len = strlen(buffer);
                memcpy(data_index, buffer, len);
                data_index += len;
            }
        }
        pclose(fdp);
    }
    return data;
}

int main(int argc, char **argv) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);


    char buf[256];
    count = 0;

    while (1) {
        if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
            error(1, errno, "bind failed ");
        }

        while (1) {
            bzero(buf, sizeof(buf));
            int n = read(connfd, buf, sizeof(buf));
            if (n < 0) {
                error(1, errno, "error read message");
            } else if (n == 0) {
                printf("client closed \n");
                close(connfd);
                break;
            }
            count++;
            buf[n] = 0;
            if (strncmp(buf, "ls", n) == 0) {
                char *result = run_cmd("ls");
                if (send(connfd, result, strlen(result), 0) < 0){
                    return 1;
                }
                free(result);
            } else if (strncmp(buf, "pwd", n) == 0) {
                char buf[256];
                char *result = getcwd(buf, 256);
                if (send(connfd, result, strlen(result), 0) < 0)
                    return 1;
            } else if (strncmp(buf, "cd ", 3) == 0) {
                char target[256];
                bzero(target, sizeof(target));
                memcpy(target, buf + 3, strlen(buf) - 3);
                if (chdir(target) == -1) {
                    printf("change dir failed, %s\n", target);
                }
            } else {
                char *error = "error: unknown input type";
                if (send(connfd, error, strlen(error), 0) < 0)
                    return 1;
            }
        }
    }
    exit(0);

}

服务器端程序需要两层循环:

  • 第一层循环控制多个客户端连接,当然咱们这里没有考虑使用并发,这在第三个模块中会讲到。严格来说,现在的服务器端程序每次只能服务一个客户连接。
  • 第二层循环控制和单个连接的数据交互,因为我们不止完成一次命令交互的过程,所以这一层循环也是必须的。
  1. 在第一层循环里通过 accept 完成了连接的建立,获得连接套接字。

  2. 在第二层循环里,先通过调用 read 函数从套接字获取字节流。我这里处理的方式是反复使用了 buf 缓冲,每次使用之前记得都要调用 bzero 完成初始化,以便重复利用。

  3. 如果读取数据为 0,则说明客户端尝试关闭连接,这种情况下,需要跳出第二层循环,进入 accept 阻塞调用,等待新的客户连接到来。
    我看到有同学使用了 goto 来完成跳转,其实使用 break 跳出就可以了,也有同学忘记跳转了,这里需要再仔细看一下。

  4. 在读出客户端的命令之后,就进入处理环节。通过字符串比较命令,进入不同的处理分支。C 语言的 strcmp 或者 strncmp 可以帮助我们进行字符串比。当然,如果命令的格式有错,需要我们把错误信息通过套接字传给客户端。

  5. 对于“pwd”命令,我是通过调用 getcwd 来完成的,getcwd 是一个 C 语言的 API,可以获得当前的路径。

  6. 对于“cd”命令,我是通过调用 chdir 来完成的,cd 是一个 C 语言的 API,可以将当前目录切换到指定的路径。有的同学在这里还判断支持了“cd ~”,回到了当前用户的 HOME 路径,这个非常棒,我就没有考虑这种情况了。

  7. 对于“ls”命令,我看到有同学是调用了 scandir 方法,获得当前路径下的所有文件列表,再根据每个文件类型,进行了格式化的输出。这个方法非常棒,是一个标准实现。我这里呢,为了显得稍微不一样,通过了 popen 的方法,执行了 ls 的 bash 命令,把 bash 命令的结果通过文件字节流的方式读出,再将该字节流通过套接字传给客户端。我看到有的同学在自己的程序里也是这么做的。

总结

  • 这次的期中大作业,主要考察了客户端 - 服务器编程的基础知识。
  • 客户端程序考察使用 select 多路复用,一方面从标准输入接收字节流,另一方面通过套接字读写,以及使用 shutdown 关闭半连接的能力。
  • 服务器端程序则考察套接字读写的能力,以及对端连接关闭情况下的异常处理等能力。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liufeng2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值