20155204 《信息安全系統設計基礎》第十三周學習總結
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();
代碼托管
結伴學習
- 同伴學習的是第五章,比較難懂的一章,之前老師們也在一直強調優化代碼的重要性,可是由於水平有限,一直沒能實踐優化這一方面,這次通過同伴的幫助,做了一些代碼的優化,也了解了許多優化原則,其中有一個問題令我印象深刻,
使用指針可以提高程序的效率與好像減少指針的使用能提告速度之間的矛盾
,我們找到了指針的一些優點,發現這個問題沒有絕對的答案。總而言之,這次學習讓我受益匪淺。
20155204 《信息安全系統設計基礎》第十三周學習總結