This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="期中大作业丨动手编写一个自己的程序吧!" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e7/8d/e77de7324232befc46d3f9f6c6cc3f8d.mp3"></audio>
你好,我们之前已经学习了网络编程的基础篇和提高篇。经过近两个月的学习,不知道你对这些内容的掌握程度如何呢?
我之前说过网络编程是一个既重视理论又重视实战的内容模块。一味地消化理论并不足以让你掌握网络编程只有自己亲自动手写代码编写程序才能对TCP、UDP、套接字这些内容有更深切的体会才能切实感受到它们是如何帮助我们的程序进行互联互通的。
网络编程就像一个魔法棒,我们之前已经学习了一些“咒语”,但上手操纵才能真实地施展魔法。所以我在专栏中安排了一个期中作业,借由这个作业让你上手编写代码,相信你在这个过程中也会更有成就感。
我在这里再提供一些“咒语”提示,方便你回顾之前的内容,以便在解题的时候更加胸有成竹。
客户端程序可以以[第11篇文章](https://time.geekbang.org/column/article/126126)的程序例子为原型这里主要考察使用select多路复用一方面从标准输入接收字节流另一方面通过套接字读写以及使用shutdown关闭半连接的能力。
服务器端程序则考察套接字读写的能力,以及对端连接关闭情况下的异常处理等能力。
题目不难,相信你可以做好。
## 题干
请你分别写一个客户端程序和服务器程序,客户端程序连接上服务器之后,通过敲命令和服务器进行交互,支持的交互命令包括:
- pwd显示服务器应用程序启动时的当前路径。
- cd改变服务器应用程序的当前路径。
- ls显示服务器应用程序当前路径下的文件列表。
- quit客户端进程退出但是服务器端不能退出第二个客户可以再次连接上服务器端。
## 客户端程序要求
1. 可以指定待连接的服务器端IP地址和端口。
1. 在输入一个命令之后,回车结束,之后等待服务器端将执行结果返回,客户端程序需要将结果显示在屏幕上。
1. 样例输出如下所示。
```
第一次连接服务器
$./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. 暂时不需要考虑多个客户并发连接的情形,只考虑每次服务一个客户连接。
1. 要把命令执行的结果返回给已连接的客户端。
1. 服务器端不能因为客户端退出就直接退出。
你可以把自己编写的程序代码放到GitHub上并在评论里留下链接。我会认真查看这些代码并在周五给出自己的反馈意见以及题目分析。由于时间有限无法尽数查看后续我会以答疑或者加餐的形式再做补充。
期待你的成果!

View File

@@ -0,0 +1,227 @@
<audio id="audio" title="期中大作业丨题目以及解答剖析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e3/93/e36747798ad19a50a3d65a53165a8693.mp3"></audio>
你好今天是期中大作业讲解课。诚如一位同学所言这次的大作业不是在考察网络编程的细节而是在考如何使用系统API完成cd、pwd、ls等功能。不过呢网络编程的框架总归还是要掌握的。
我研读了大部分同学的代码,基本上是做得不错的,美中不足的是能动手完成代码编写和调试的同学偏少。我还是秉持一贯的看法,计算机程序设计是一门实战性很强的学科,如果只是单纯地听讲解,没有自己动手这一环,对知识的掌握总归还是差那么点意思。
代码我已经push到[这里](https://github.com/froghui/yolanda/tree/master/mid-homework),你可以点进链接看一下。
## 客户端程序
废话少说,我贴下我的客户端程序:
```
#include &quot;lib/common.h&quot;
#define MAXLINE 1024
int main(int argc, char **argv) {
if (argc != 3) {
error(1, 0, &quot;usage: tcp_client &lt;IPaddress&gt; &lt;port&gt;&quot;);
}
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(&amp;allreads);
FD_SET(0, &amp;allreads);
FD_SET(socket_fd, &amp;allreads);
for (;;) {
readmask = allreads;
int rc = select(socket_fd + 1, &amp;readmask, NULL, NULL, NULL);
if (rc &lt;= 0) {
error(1, errno, &quot;select failed&quot;);
}
if (FD_ISSET(socket_fd, &amp;readmask)) {
n = read(socket_fd, recv_line, MAXLINE);
if (n &lt; 0) {
error(1, errno, &quot;read error&quot;);
} else if (n == 0) {
printf(&quot;server closed \n&quot;);
break;
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs(&quot;\n&quot;, stdout);
}
if (FD_ISSET(STDIN_FILENO, &amp;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, &quot;quit&quot;, strlen(send_line)) == 0) {
if (shutdown(socket_fd, 1)) {
error(1, errno, &quot;shutdown failed&quot;);
}
}
size_t rt = write(socket_fd, send_line, strlen(send_line));
if (rt &lt; 0) {
error(1, errno, &quot;write failed &quot;);
}
}
}
}
exit(0);
}
```
客户端的代码主要考虑的是使用select同时处理标准输入和套接字我看到有同学使用fgets来循环等待用户输入然后再把输入的命令通过套接字发送出去当然也是可以正常工作的只不过不能及时响应来自服务端的命令结果所以我还是推荐使用select来同时处理标准输入和套接字。
这里select如果发现标准输入有事件读出标准输入的字符就会通过调用write方法发送出去。如果发现输入的是quit则调用shutdown方法关闭连接的一端。
如果select发现套接字流有可读事件则从套接字中读出数据并把数据打印到标准输出上如果读到了EOF表示该客户端需要退出直接退出循环通过调用exit来完成进程的退出。
## 服务器端程序
下面是我写的服务器端程序:
```
#include &quot;lib/common.h&quot;
static int count;
static void sig_int(int signo) {
printf(&quot;\nreceived %d datagrams\n&quot;, 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, &quot;r&quot;);
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(&amp;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, &amp;on, sizeof(on));
int rt1 = bind(listenfd, (struct sockaddr *) &amp;server_addr, sizeof(server_addr));
if (rt1 &lt; 0) {
error(1, errno, &quot;bind failed &quot;);
}
int rt2 = listen(listenfd, LISTENQ);
if (rt2 &lt; 0) {
error(1, errno, &quot;listen failed &quot;);
}
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 *) &amp;client_addr, &amp;client_len)) &lt; 0) {
error(1, errno, &quot;bind failed &quot;);
}
while (1) {
bzero(buf, sizeof(buf));
int n = read(connfd, buf, sizeof(buf));
if (n &lt; 0) {
error(1, errno, &quot;error read message&quot;);
} else if (n == 0) {
printf(&quot;client closed \n&quot;);
close(connfd);
break;
}
count++;
buf[n] = 0;
if (strncmp(buf, &quot;ls&quot;, n) == 0) {
char *result = run_cmd(&quot;ls&quot;);
if (send(connfd, result, strlen(result), 0) &lt; 0)
return 1;
} else if (strncmp(buf, &quot;pwd&quot;, n) == 0) {
char buf[256];
char *result = getcwd(buf, 256);
if (send(connfd, result, strlen(result), 0) &lt; 0){
return 1;
}
free(result);
} else if (strncmp(buf, &quot;cd &quot;, 3) == 0) {
char target[256];
bzero(target, sizeof(target));
memcpy(target, buf + 3, strlen(buf) - 3);
if (chdir(target) == -1) {
printf(&quot;change dir failed, %s\n&quot;, target);
}
} else {
char *error = &quot;error: unknown input type&quot;;
if (send(connfd, error, strlen(error), 0) &lt; 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服务器。挑战和难度越来越高你准备好了吗?