You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
9.5 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 期中大作业丨题目以及解答剖析
你好今天是期中大作业讲解课。诚如一位同学所言这次的大作业不是在考察网络编程的细节而是在考如何使用系统API完成cd、pwd、ls等功能。不过呢网络编程的框架总归还是要掌握的。
我研读了大部分同学的代码,基本上是做得不错的,美中不足的是能动手完成代码编写和调试的同学偏少。我还是秉持一贯的看法,计算机程序设计是一门实战性很强的学科,如果只是单纯地听讲解,没有自己动手这一环,对知识的掌握总归还是差那么点意思。
代码我已经push到[这里](https://github.com/froghui/yolanda/tree/master/mid-homework),你可以点进链接看一下。
## 客户端程序
废话少说,我贴下我的客户端程序:
```
#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服务器。挑战和难度越来越高你准备好了吗?