20155204 《信息安全系统设计基础》第十三周学习总结
教材内容总结
11.1 客户端-服务器编程模型
- 每个网络应用都是基于客户端-服务器模型的。在该模型中,一个应用是由一个服务器进程和一个或多个客户端进程组成的。
- 服务器管理某种资源。一个Web服务器管理了一组磁盘文件,它会代表客户端进行检索和执行;一个FTP服务器管理了一组磁盘文件,它会为客户端进行存储和检索;一个Emial服务器管理了一些文件,它为客户端进行读和更新。
- 客户端-服务器模型中的基本操作是事务,它由四步组成:
- 客户端向服务器发送一个请求,发起一个事务;
- 服务器收到请求后,解释之,并操作它的资源;
- 服务器给客户端发送一个响应,例如将请求的文件发送回客户端;
- 客户端收到响应并处理它,例如Web浏览器在屏幕上显示网页。
- 认识到客户端和服务器是进程而不是具体的机器或主机是重要的。
11.2 网络
- 对于一个主机而言,网络只是又一种I/O设备,作为数据源和数据接收方。
- 最流行的局域网是以太网(Ethernet),一个以太网段包括一些电缆(通常是双绞线)和一个集线器。每根电缆都有相同的带宽,它们一端连接到主机的适配器,另一端则连接到集线器的一个端口上。集线器不加分辨地从一个端口上收到的每个位复制到其他所有的端口上。
- 每个以太网适配器都有一个全球唯一的48位地址,一台主机可以发送一帧数据到这个网段内的其他主机。每个帧包括了固定数量的头部位,用来标识此帧的源和目的地址,以及此帧的长度,之后便是数据位的有效载荷。每个网络适配器都能看到这个帧,但是只有目的主机才能实际读取它。
- 通过网桥,多个以太网段可以连接成较大的局域网,称为桥接以太网。网桥比集线器更充分地利用了网线带宽。
- 在更高的层次中,多个不兼容的局域网可以通过路由器连接,组成一个互联网。
- 互联网的一个重要特性是,它能连接完全不兼容的局域网和广域网,方法是通过协议软件,它消除了不同网络之间的差异。这种协议必须提供两种基本能力:命名机制,每台主机被分配至少一个互联网络地址,这个地址唯一地标识了这台主机。传送机制。定义包含包头和有效载荷的数据包。
11.3 全球IP因特网
- 从程序员的角度,可以把因特网看做一个世界范围的主机集合,它满足以下特性:
- 主机集合被映射成一组32位的IP地址。
- 这组IP地址被映射成一组叫做因特网域名的标识符。
- 因特网主机上的进程能够通过连接和任何其他因特网主机上的进程通信。
11.3.1 IP地址
- 一个IP地址就是一个32位无符号整数,它存放在一个IP地址结构中。
- TCP/IP协议为整数数据项定义了统一的网络字节顺序(大端字节顺序)。IP地址也被以大端法存放。
- IP地址通常是以一种称为点分十进制表示法来表示的。例如128.2..194.242就是地址0x8002c2f2的点分十进制表示。
11.3.2因特网域名
- 对于人们而言大整数是很难记住的,因此因特网也定义了一组更加人性化的域名,以及一种将域名映射到IP地址的机制。
域名是一串用点分隔的单词(字母、数字和破折号),它有自己的层级结构。
- 除了根节点,第二层是一组一级域名,常见的一级域名有com、edu、gov、org等。
- 二级域名如mit、berkeley、csdn等。
- 域名集合和IP地址集合之间的映射由分布世界范围内的数据库(DNS,域名系统)来维护。
一个域名可以与一个IP地址一一对应;或者多个域名映射到多个IP地址;或者某些合法的域名没有IP地址的映射。
11.3.3因特网连接
- 客户端和服务器的连接是点对点、全双工、可靠的。
- 一个套接字是连接的一个端点。每个套接字都有对应的套接字地址,它由一个IP地址和一个16位的整数端口组成,用“地址:端口”表示。
- 当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口。然而,服务器套接字地址中的端口通常是某个知名的端口,是和这个服务相对应的。例如,Web服务器通常使用端口80,Email服务器通常使用端口25。
- 一个连接是由它两端的套接字地址唯一确定的,这对套接字地址叫做套接字对(socket pair)。由下列元祖来表示:
(cliaddr:cliport,servaddr:servport)
11.4 套接字接口
- 套接字接口是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。
socket 函数
客户端和服务器使用socket函数来创建一个套接字描述符。
```include <sys/types.h>
include <sys/socket.h>
int socket(int domain,int tpye,int protocol);
clientfd=socket(AF_INET,SOCK_STREAM,0);
其中AF_INET表示我们正在使用因特网,而SOCK_STREAM表示这个套接字是因特网连接的一个端点。
##### connect函数
客户端通过调用connect函数来建立与服务器的连接。
include <sys/socket.h>
int connect(int sockfd,struct sockadd *serv_addr,int addrlen);
connect函数试图与套接字地址位serv_addr的服务器建立连接,它被阻塞直到连接成功或发生错误。
##### bind函数
下面的bind、listen和accept函数被服务器用来与客户端建立连接。
include <sys/socket.h>
int bind(int sockfd,struct sockaddr *my_addr,int addrlen);
bind函数告诉内核将套接字地址和套接字描述符联系起来。
##### listen函数
服务器是被动接收客户端连接请求的,listen函数将套接字描述符从主动套接字转化为监听套接字。
##### accept函数
服务器通过调用accept函数来等待来自客户端的连接请求。
include <sys/socket.h>
int accept(int listenfd,struct sockaddr addr,int addrlen);
```
accpet函数等待来自客户端的连接请求到达监听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个连接描述符connfd。
- 监听描述符和连接描述符的区别
监听描述符是供客户端连接请求的使用一个端点,它被创建一次,并存在于服务器的整个生命周期。
连接描述符是客户端和服务器之间已成功连接的一个端点,服务器每次接受连接请求时都会创建一次,它只存在于服务器每次为一个客户端服务的过程中。
区分监听描述符和连接描述符是有必要的,因为这样使得我们可以建立并发服务器,它能够同时处理许多客户端连接。
11.5 Web服务器
11.5.1 Web基础
- Web服务用的是基于文本的应用级协议:HTTP(超文本传输协议)。一个Web客户端(即浏览器)打开一个到服务器的连接,请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。
- Web服务和常规的文件检索服务(如FTP)的主要区别是Web内容可以用HTML(超文本标记语言)编写。HTML可以定义网页显示的内容,以及创建超链接。
11.5.2Web内容
- Web内容是与MIME(多用途网际邮件扩充协议)类型相关的字节序列,包括:HTML页面、无格式文本、Postscript文档、GIF图像、JPEG图像等。
Web服务器以两种不同的方式向客户端提供内容:
静态内容,取一个磁盘文件,并将它的内容返回给客户端。
动态内容,运行一个可执行文件,并将它的输出返回给客户端。每种内容都和某个文件相关联,每个文件都用URL(统一资源定位符)唯一标识。例如
http://www.google.com:80/index.html
可执行文件的URL可以在文件名后包含程序参数,“?”分隔文件名和参数,多参数用“&”分隔开。如
http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213
标识了一个叫做cgi-bin/adder的可执行文件,带有两个参数字符串15000和213。
当用户键入一个URL时,客户端和服务器使用的是URL的不同部分。客户端使用前缀http://bluefish.ics.cs.cmu.edu:8000
- 来决定与在哪里的哪类服务器连接。
- 而服务器使用后缀
/cgi-bin/adder?15000&213
来发现文件系统中的文件。
11.5.3 HTTP事务
HTTP是基于在因特网连接上传送的文本行的,可以使用Unix的TELNET程序来和Web服务器通信。
- HTTP请求:一个HTTP请求的组成是这样的:一个请求行
GET / HTTP/1.1
后面跟随0个或多个请求报头
Host: www.aol.com
再跟随一个空的文本行来终止报头列表。
一个请求行的格式是
<method> <uri> <version>
- HTTP支持许多不同的方法,包括GET/POST/OPTIONS/HEAD/PUT/DELETE/TRACE。其中GET最常用。
- GET方法指导服务器生成和返回URI(统一资源标识符,是URL的后缀,包括文件名和可选的参数)。
- 请求报头为服务器提供了额外的信息,例如浏览器的商标名等。
- HTTP响应:HTTP响应和请求是类似的,它包括:一个响应行,后面跟随0个或多个响应报头,然后是终止报头的空行,再跟随一个响应主体。
一个响应行的格式为:
<version> <status code> <status message>
版本字段描述的是响应所遵循的HTTP版本。状态码则是一个三位的正整数,指明对请求的处理。
习题
11.6
- A. 在doit函数中第一个sscanf语句之后添加下面的语句即可:printf("%s %s %s\n", method, uri, version);
B.
- C. A的结果可以表明,浏览器使用HTTP/1.1
D. 请求行和报头如下:
GET /clockwise.gif HTTP/1.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:20.0) Gecko/20100101 Firefox/20.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive User-Agent: 系统以及浏览器情况 Accept:可以接受的媒体; Accept-Encoding:可以接受的编码方案; Accept-Language:能够接受的语言;
11.7
在get_filetype函数里面添加:
else if(strstr(filename, ".mpg") || strstr(filename, ".mp4"))
strcpy(filetype, "video/mpg");11.8
在main函数之前加入代码:
int chdEnded ; #include <signal.h> void child_signal(int sig) { pid_t pid; while((pid = waitpid(-1, NULL, WNOHANG)) > 0) ; chdEnded = 1; }
在main函数中添加语句
signal(SIGCHILD, child_handle);
每次accept之前,让chdEnded = 0;
并且在doit()中的serve_dynamic之后添加:
while(!chdEnded) pause();//or do sth
删掉serve_dynamic里的wait(NULL);
11.9
serve_static中的存储器映射语句改为:
srcfd = open(filename, O_RDONLY, 0); srcp = (char*)malloc(sizeof(char)*filesize); rio_readn(srcfd, srcp, filesize); close(srcfd); rio_writen(fd, srcp, filesize); free(srcp);
11.10
HTML文件:
<html>
<body>
<form name="input" action="cgi-bin/adder" method="get">
Num1: <input type="text" name="num1"/> <br/>
Num2: <input type="text" name="num2"/> <br/>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
因为提交的表单里面有参数名字(num1=x&num2=y),所以要修改相应的adder.c:
int parseNum(char *s)
{
int i = strlen(s) - 1;
while(i>0 && s[i-1]>=‘0‘&&s[i-1]<=‘9‘ )
i--;
return atoi(s+i);
}
int main(void) {
char *buf, *p;
char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
int n1=0, n2=0;
/* Extract the two arguments */
if ((buf = getenv("QUERY_STRING")) != NULL) {
p = strchr(buf, ‘&‘);
*p = 0;
strcpy(arg1, buf);
strcpy(arg2, p+1);
n1 = parseNum(arg1);
n2 = parseNum(arg2);
}
/* Make the response body */
sprintf(content, "Welcome to add.com: ");
sprintf(content, "%sTHE Internet addition portal.\r\n<p>", content);
sprintf(content, "%sThe answer is: %d + %d = %d\r\n<p>",
content, n1, n2, n1 + n2);
sprintf(content, "%sThanks for visiting!\r\n", content);
/* Generate the HTTP response */
printf("Content-length: %d\r\n", (int)strlen(content));
printf("Content-type: text/html\r\n\r\n");
printf("%s", content);
fflush(stdout);
exit(0);
}
11.11
在client_error,serve_static和serve_dynamic中添加一个参数mtd(改的地方也比较多),表示方法。如果mtd为HEAD,就只打印头部。
结果如下:
20155204@ubuntu:~/CSAPP11/cgi-bin$ telnet localhost 12345
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is ‘^]‘.
HEAD / HTTP/1.1
HTTP/1.0 200 OK
Server: Tiny Web Server
Content-length: 2722
Content-type: text/html
Connection closed by foreign host.
20155204@ubuntu:~/CSAPP11/cgi-bin$ telnet localhost 12345
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is ‘^]‘.
HEAD /clockwise.gif HTTP/1.1
HTTP/1.0 200 OK
Server: Tiny Web Server
Content-length: 126150
Content-type: image/gif
Connection closed by foreign host.
11.12
主要修改的就是doit方法和read_request方法。
下面的程序只能针对参数为文本的情况,且参数总长度最大不超过MAXLINE。
#define M_GET 0
#define M_POST 1
#define M_HEAD 2
#define M_NONE (-1)
void doit(int fd)
{
int is_static;
int rmtd = 0;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;
/*for post*/
int contentLen;
char post_content[MAXLINE];
/* Read request line and headers */
rio_readinitb(&rio, fd);
rio_readlineb(&rio, buf, MAXLINE);
sscanf(buf, "%s %s %s", method, uri, version);
printf("%s %s %s\n", method, uri, version);
if(strcmp(method, "GET") == 0) rmtd = M_GET;
else if(strcmp(method, "POST") == 0) rmtd = M_POST;
else if(strcmp(method, "HEAD") == 0) rmtd = M_HEAD;
else rmtd = M_NONE;
if (rmtd == M_NONE) {
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method", rmtd);
return;
}
contentLen = read_requesthdrs(&rio, post_content, rmtd);
/* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs);
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn‘t find this file", rmtd);
return;
}
if (is_static) {/* Serve static content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn‘t read the file", rmtd);
return;
}
serve_static(fd, filename, sbuf.st_size, rmtd);
}
else {/* Serve dynamic content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn‘t run the CGI program", rmtd);
return;
}
if(rmtd == M_POST) strcpy(cgiargs, post_content);
serve_dynamic(fd, filename, cgiargs, rmtd);
}
}
int read_requesthdrs(rio_t *rp, char* content, int rmtd)
{
char buf[MAXLINE];
int contentLength = 0;
char *begin;
rio_readlineb(rp, buf, MAXLINE);
while(strcmp(buf, "\r\n")) {
rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
if(rmtd == M_POST && strstr(buf, "Content-Length: ")==buf)
contentLength = atoi(buf+strlen("Content-Length: "));
}
if(rmtd == M_POST){
contentLength = rio_readnb(rp, content, contentLength);
content[contentLength] = 0;
printf("POST_CONTENT: %s\n", content);
}
return contentLength;
}
11.13
为了测试EPIPE错误,我在read_requesthdrs里面添加了sleep(5)。
于是,在浏览器里请求之后,立即断开。进程出现错误:
GET /add.html HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:20.0) Gecko/20100101 Firefox/20.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Segmentation fault
20155204@ubuntu:~/CSAPP11$
为了解决这个问题,我用了setjmp和longjmp。
当进程捕捉到SIGPIPE时,进入一个信号处理函数:
jmp_buf buf;
void epipe_signal(int sig)
{
longjmp(buf, 1);
}
而在main函数中,doit部分需要这样改:
rc = setjmp(buf);
if(rc == 0) {
doit(connfd);
close(connfd);
}
教材、代码问题总结
- 问题一:TCP为什么不是两次连接?而是三次握手?
- 问题一解决:如果A与B两个进程通信,如果仅是两次连接。可能出现的一种情况就是:A发送完请报文以后,由于网络情况不好,出现了网络拥塞,即B延时很长时间后收到报文,即此时A将此报文认定为失效的报文。B收到报文后,会向A发起连接。此时两次握手完毕,B会认为已经建立了连接可以通信,B会一直等到A发送的连接请求,而A对失效的报文回复自然不会处理。依次会陷入B忙等的僵局,造成资源的浪费。
- 问题二:零拷贝的实现?
- 问题二解决:对于内核层的实现,底层调用的是系统调用sendFile()方法;
zerocopy技术省去了将操作系统的read buffer拷贝到程序的buffer, 以及从程序buffer拷贝到socket buffer的步骤, 直接将 read buffer 拷贝到 socket buffer;应用层上的实现,对于自定义的结构,一般是交换内部指针(使用C++11,可以使用move操作来实现高效交换结构体)如果是vector等结构,使用其成员函数swap()就能达到高效的交换(类似C++11中的move操作); - 问题三: connect方法会阻塞,请问有什么方法可以避免其长时间阻塞?
- 问题三解决:可以考虑采用异步传输机制,同步传输与异步传输的主要区别在于同步传输中,如果调用recvfrom后会一致阻塞运行,从而导致调用线程暂停运行;异步传输机制则不然,会立即返回。
- 问题四:网络编程中设计并发服务器,使用多进程与多线程的区别?
- 问题四解决:1.进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。
2.线程:相对与进程而言,线程是一个更加接近与执行体的概念,它可以与同进程的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。两者都可以提高程序的并发度,提高程序运行效率和响应时间。线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。 - 问题五:一个自定义消息如何实现。
- 问题五解决:自定义消息共分为3步骤:
- 自定义消息:#defineWM_MYMSG WM_USER+1
- 在头文件中声明函数: afx_msg voidonMyMsg();
- 在消息映射中添加对应关系:
```
//BEGIN_MESSAGE_MAP(CDefMsgDemoDlg,CDialog) //END_MESSAGE_MAP()
ON_MESSAGE(WM_MYMSG,onMyMsg)
```
- 定义函数void onMyMsg();
代码托管
结伴学习
- 同伴学习的是第五章,比较难懂的一章,之前老师们也在一直强调优化代码的重要性,可是由于水平有限,一直没能实践优化这一方面,这次通过同伴的帮助,做了一些代码的优化,也了解了许多优化原则,其中有一个问题令我印象深刻,
使用指针可以提高程序的效率与好像减少指针的使用能提告速度之间的矛盾
,我们找到了指针的一些优点,发现这个问题没有绝对的答案。总而言之,这次学习让我受益匪浅。