网络编程实战
盛延敏
前大众点评云平台首席架构师
立即订阅
6034 人已学习
课程目录
已完结 39 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 学好网络编程,需要掌握哪些核心问题?
免费
第一模块:基础篇 (9讲)
01 | 追古溯源:TCP/IP和Linux是如何改变世界的?
02 | 网络编程模型:认识客户端-服务器网络模型的基本概念
03丨套接字和地址:像电话和电话号码一样理解它们
04 | TCP三次握手:怎么使用套接字格式建立连接?
05 | 使用套接字进行读写:开始交流吧
06 | 嗨,别忘了UDP这个小兄弟
07 | What? 还有本地套接字?
08 | 工欲善其事必先利其器:学会使用各种工具
09丨答疑篇:学习网络编程前,需要准备哪些东西?
第二模块:提高篇 (10讲)
10 | TIME_WAIT:隐藏在细节下的魔鬼
11 | 优雅地关闭还是粗暴地关闭 ?
12 | 连接无效:使用Keep-Alive还是应用心跳来检测?
13 | 小数据包应对之策:理解TCP协议中的动态数据传输
14丨UDP也可以是“已连接”?
15 | 怎么老是出现“地址已经被使用”?
16 | 如何理解TCP的“流”?
17 | TCP并不总是“可靠”的?
18 | 防人之心不可无:检查数据的有效性
19丨提高篇答疑:如何理解TCP四次挥手?
期中复习周 (2讲)
期中大作业丨动手编写一个自己的程序吧!
免费
期中大作业丨题目以及解答剖析
免费
第三模块:性能篇 (12讲)
20 | 大名⿍⿍的select:看我如何同时感知多个I/O事件
21 | poll:另一种I/O多路复用
22 | 非阻塞I/O:提升性能的加速器
23 | Linux利器:epoll的前世今生
24 | C10K问题:高并发模型设计
25 | 使用阻塞I/O和进程模型:最传统的方式
26 | 使用阻塞I/O和线程模型:换一种轻量的方式
27 | I/O多路复用遇上线程:使用poll单线程处理所有I/O事件
28 | I/O多路复用进阶:子线程使用poll处理连接I/O事件
29 | 渐入佳境:使用epoll和多线程模型
30 | 真正的大杀器:异步I/O探索
31丨性能篇答疑:epoll源码深度剖析
第四模块:实战篇 (4讲)
32 | 自己动手写高性能HTTP服务器(一):设计和思路
33 | 自己动手写高性能HTTP服务器(二):I/O模型和多线程模型实现
34 | 自己动手写高性能HTTP服务器(三):TCP字节流处理和HTTP协议实现
35 | 答疑:编写高性能网络编程框架时,都需要注意哪些问题?
结束语 (1讲)
结束语丨我相信这不是结束,让我们江湖再见
网络编程实战
登录|注册

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

盛延敏 2019-09-20
00:00
00:22
讲述:冯永吉 大小:356.18K
你好,今天是期中大作业讲解课。诚如一位同学所言,这次的大作业不是在考察网络编程的细节,而是在考如何使用系统 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 服务器。挑战和难度越来越高,你准备好了吗?
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《网络编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(11)

  • Steiner
    为什么select要用两个fd_set来操作,只有一个fd_set会出现什么问题?

    作者回复: 因为每次select操作完就会把传入的fd_set修改掉,以便告知应用程序什么样的事件发生了,所以需要一个初始化的fd_set保存感兴趣的待检测fd_set。

    2019-09-20
    1
  • Geek_Wison
    老师您好,上面服务器端代码的free result 你写错地方了,应该写到83行后面,但是你写到了90行了。

    作者回复: 是的,两个地方都需要free掉这块内存。

    2019-10-08
  • Steiner
    对于cd我直接用了chdir,出现Permission denied怎么办

    作者回复: 看一下你当前启动程序的user,还有你的目录属主权限,这个说明你当前启动程序的用户不是当前目录的属主,没有权限进行chdir操作。

    2019-09-27
  • LDxy
    服务器端程序里面的count++是做何用的?

    作者回复: 仅仅是为了计算连接的数量。

    2019-09-21
  • 沉淀的梦想
    测了一些strcmp,好像写大小写敏感的,更接近Java里的equals,而不是equalsIgnoreCase吧?

    作者回复: 是的。

    2019-09-21
  • ( ̄_ ̄ )
    我用的把dup2把标准输入输出重定向到套接字,用system调用命令

    作者回复: 👍

    2019-09-20
  • _CountingStars
    可能老师的程序只是为了给我们演示。我发现老师程序主体都在一个main函数写的,没有分开组织成多个小函数,有时 if 的嵌套有点深,其实可以把异常情况直接 return 回去,这样嵌套就会少很多。这样代码也会容易理解一些。

    作者回复: 嗯,只是一个演示,你们可以自行优化。拆分成多个函数当然是可以的。

    2019-09-20
  • 骏Jero
    老师,有个问题想问下 UDP报文最大长度? 之前你的专栏udp那篇的提问,我编写代码在局域网试了下可以达到65507个字节。然后参照往上一些资料有些根据mtu来进行计算,但是为什么实际种事65507而不是mtu 1500字节计算出来的1472字节

    作者回复: 你是怎么测试的?贴上代码来看看,很感兴趣的说。

    2019-09-20
    1
  • 刘丹
    malloc的内存没有被释放?

    作者回复: 是的,已修正。一会更新下。

    2019-09-20
  • MoonGod
    老师有个问题没有想清楚,就是服务端代码中,在读取到客户端发送的EOF后,会打印printf("server closed \n");。但我在实践的过程中,发现这行日志总是在客户端重新连接后,并发送第一条指令后,才在服务端的控制台打印出来,为什么不是在客户端发送quit之后立马打印出来的呢?

    作者回复: 我这里的现象是客户端quit之后会打印。你是什么系统?

    2019-09-20
  • 传说中的成大大
    我觉得网络不难 难的是你让我去程序里面 ls pwd cd ../o(╥﹏╥)o

    作者回复: 哈哈,不是都搞定了么

    2019-09-20
    1
收起评论
11
3
返回
顶部