网络编程实战

期中大作业丨题目以及解答剖析

盛延敏

前大众点评云平台首席架构师

你好,今天是期中大作业讲解课。诚如一位同学所言,这次的大作业不是在考察网络编程的细节,而是在考如何使用系统API完成cd、pwd、ls等功能。不过呢,网络编程的框架总归还是要掌握的。

我研读了大部分同学的代码,基本上是做得不错的,美中不足的是能动手完成代码编写和调试的同学偏少。我还是秉持一贯的看法,计算机程序设计是一门实战性很强的学科,如果只是单纯地听讲解,没有自己动手这一环,对知识的掌握总归还是差那么点意思。

代码我已经push到这里,你可以点进链接看一下。

客户端程序

废话少说,我贴下我的客户端程序:

#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;
            } 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;
                 }
                free(result);
            } 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);

}

服务器端程序需要两层循环,第一层循环控制多个客户端连接,当然咱们这里没有考虑使用并发,这在第三个模块中会讲到。严格来说,现在的服务器端程序每次只能服务一个客户连接。

第二层循环控制和单个连接的数据交互,因为我们不止完成一次命令交互的过程,所以这一层循环也是必须的。

大部分同学都完成了这个两层循环的设计,我觉得非常棒。

在第一层循环里通过accept完成了连接的建立,获得连接套接字。

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

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

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

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

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

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

这次的期中大作业,主要考察了客户端-服务器编程的基础知识。

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

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

不过,服务器端程序目前只能一次服务一个客户端连接,不具备并发服务的能力。如何编写一个具备高并发服务能力的服务器端程序,将是我们接下来课程的重点。我们将会重点讲述基于I/O多路复用的事件驱动模型,并以此为基础设计一个高并发网络编程框架,通过这个框架,实现一个HTTP服务器。挑战和难度越来越高,你准备好了吗?

教程推荐

进程通信在线教程

Android在线教程

C#在线教程

JSoup在线教程

大数据学习指南在线教程

JavaScript 设计模式在线教程

随机推荐

法丽兹饼干-膨化可靠性如何?这就是评测结果!

怡颗莓蓝莓评价怎么样?深度评测揭秘内情!

卡诗洗发水使用体验怎么样?深度剖析功能特点!

特仑苏牛奶乳品实际效果怎样?专业老用户评测?

4DRC遥控车好不好,值得购买吗?图文评测爆料分析!

4DRC遥控车选购技巧有哪些?真相揭秘实际情况!