2021-2022-1-diocs-TCP/IP和網路程式設計
一、任務詳情
自學教材第13章,提交學習筆記(10分)
知識點歸納以及自己最有收穫的內容 (3分)
問題與解決思路(2分)
實踐內容與截圖,程式碼連結(3分)
...(知識的結構化,知識的完整性等,提交markdown文件,使用openeuler系統等)(2分)
二、知識點總結
本章論述了TCP/IP 和網路程式設計,分為兩個部分。第一部分論述了TCP/IP協議及其應用,具體包括 TCP/IP 棧、IP地址、主機名、DNS、IP資料包和路由器;介紹了TCP/IP 網路中的UDP和 TCP 協議、埠號和資料流;闡述了伺服器-客戶機計算模型和套接字程式設計介面;通過使用UDP和TCP套接字的示例演示了網路程式設計。第一個程式設計專案可實現一對通過網際網路執行檔案操作的 TCP伺服器-客戶機,可讓使用者定義其他通訊協議來可靠地傳輸檔案內容。
本章的第二部分介紹了Web和CGI程式設計,解釋了HTTP程式設計模型、Web 頁面和 Web瀏覽器;展示瞭如何配置 Linux HTTPD伺服器來支援使用者 Web 頁面、PHP和CGI程式設計;闡釋了客戶機和伺服器端動態 Web 頁面;演示瞭如何使用PHP和 CGI建立伺服器端動態Web 頁面。
1.TCP/IP協議
TCP/IP(Comer 1988,2001;RFC1180 1991)是網際網路的基礎。TCP代表傳輸控制協議。IP 代表網際網路協議。目前有兩個版本的IP,即IPv4和IPv6。IPv4使用32位地址,IPv6則使用128位地址。本節圍繞IPv4 進行討論,它仍然是目前使用最多的IP版本。TCP/IP 的組織結構分為幾個層級,通常稱為TCP/IP堆疊。如圖所示為 TCP/IP 的各個層級以及每一層級的代表性元件及其功能。
程序與主機之間的傳輸層或其上方的資料傳輸只是邏輯傳輸。實際資料傳輸發生在網際網路(IP)和鏈路層,這些層將資料包分成資料幀,以便在物理網路之間傳輸。下圖所示為 TCP/IP 網路中的資料流路徑。
2.IP主機和IP地址
IP地址分為兩部分,即 NetworkID 欄位和HostID欄位。根據劃分,IP 地址分為A~E 類。例如,一個B類IP地址被劃分為一個16位NetworkID,其中前2位是10,然後是一個16位的 HostID欄位。發往IP地址的資料包首先被髮送到具有相同 networkID的路由器。路由器將通過 HostID 將資料包轉發到網路中的特定主機。每個主機都有一個本地主機名localhost,預設 IP地址為 127.0.0.1。本地主機的鏈路層是一個回送虛擬裝置,它將每個資料包路由回同一個localhost。這個特性可以讓我們在同一臺計算機上執行TCP/IP 應用程式,而不需要實際連線到網際網路。
3.IP協議
IP協議用於在 IP主機之間傳送/接收資料包。IP盡最大努力執行。IP 主機只向接收主機發送資料包,但它不能保證資料包會被髮送到它們的目的地,也不能保證按順序傳送。這意味著IP 並非可靠的協議。必要時,必須在IP 層的上面實現可靠性。下圖所示是IP頭格式:
4.UDP/TCP
UDP(使用者資料報協議)(RFC768 1980;Comer 1988)在IP上執行,用於傳送/接收資料報。與IP類似,UDP不能保證可靠性,但是快速高效。它可用於可靠性不重要的情況。
TCP(傳輸控制協議)是一種面向連線的協議,用於傳送/接收資料流。TCP也可在IP 上執行,但它保證了可靠的資料傳輸。通常,UDP類似於傳送郵件的USPS,而TCP類似於電話連線。
5.埠編號
應用程式 =(主機 IP,協議,埠號)
其中,協議是TCP或 UDP,埠號是分配給應用程式的唯一無符號短整數。要想使用UDP或 TCP,應用程式(程序)必須先選擇或獲取一個埠號。前1024個埠號已被預留。其他埠號可供一般使用。應用程式可以選擇一個可用埠號,也可以讓作業系統核心分配埠號。下圖給出了在傳輸層中使用TCP 的一些應用程式及其預設埠號。
6.TCP/Ip網路中的資料流
在圖中,應用程式層的資料被傳遞到傳輸層,傳輸層給資料新增一個TCP或UDP 報頭來標識使用的傳輸協議。合併後的資料被傳遞到IP 網路層,新增一個包含 IP地址的IP 報頭來標識傳送和接收主機。然後,合併後的資料再被傳遞到網路鏈路層,網路鏈路層將資料分成多個幀,並添加發送和接收網路的地址,用於在物理網路之間傳輸。IP地址到網路地址的對映由地址解析協議(ARP)執行(ARP1982)。在接收端,資料編碼過程是相反的。每一層通過剝離資料頭來解包接收到的資料、重新組裝資料並將資料傳遞到上一層。傳送主機上的應用程式原始資料最終會被傳遞到接收主機上的相應應用程式。
7.套接字程式設計
(1)套接字地址
struct sockaddr_in { sa_family_t sin_family; // AF_INET for TCP/IP // port number in_port_t sin_port; struct in_addr sin_addr;// IP address ); // internet address struct in_addr { // IP address in network byte order s_addr; uint32_t );
在套接字地址結構中,
● TCP/IP 網路的 sin_family 始終設定為 AF_INET。
● sin_port包含按網路位元組順序排列的埠號。
●sin addr是按網路位元組順序排列的主機IP地址。
(2)套接字API
伺服器必須建立一個套接字,並將其與包含伺服器IP 地址和埠號的套接字地址繫結。它可以使用一個固定埠號,或者讓作業系統核心選擇一個埠號(如果 sin port為0)。為了與伺服器通訊,客戶機必須建立一個套接字。對於UPD套接字,可以將套接字繫結到伺服器地址。如果套接字沒有繫結到任何特定的伺服器,那麼它必須在後續的 sendto()/recvfrom()呼叫中提供一個包含伺服器IP 和埠號的套接字地址。
(3)TCP/UDP套接字
UDP 套接字使用 sendto(/recvfrom(來發送/接收資料報。
ssize_t sendto(int soCkfd,const void *buf,size_t len,int flags, const struct sockaddr *dest_addr,socklen_t addrlen); ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags, struct sockaddr *src_addr,socklen_t *addrlen);
在建立套接字並將其繫結到伺服器地址之後,TCP伺服器使用listen()和 accept()來接收來自客戶機的連線
int listen(int sockfd, int backlog);
listen()將 sockfd引用的套接字標記為將用於接收連人連線的套接字。backlog 引數定義了等待連線的最大佇列長度。
int accept(int sockfd, struct sockaddr *addr, socklen t *addrlen);
(4)通用套接字地址結構
通用套接字地址結構:sockaddr
struct sockaddr { uint8_t sa_len; sa_family_t sa_family; char sa_data[14]; }; IPv6套接字地址結構IPv6套接字地址結構在<netinet/in.h>標頭檔案中定義
struct in6_addr
{
unit8_t s6_add[16];};
#define SIN6_LEN struct sockaddr_in6 { uint8_t sin6_len; sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; };
新的struct sockaddr_storage足以容納系統所支援的任何套接字地址結構。sockaddr_storage結構在<netinet/in.h>標頭檔案中定義
struct sockaddr_storage { uint8_t ss_len; sa_family_t ss_family; };
8.位元組排序函式
小端和大端(記憶體中儲存兩個位元組有兩種方法)
小端(little-endian):將低序位元組儲存在起始地址
大端(big-endian):將高序位元組儲存在起始地址
主機位元組序:某個給定系統所用的位元組序
輸出位元組序的程式:
#iclude"unp.h" int main(int argc,char **argv) { union{ short s; char c[sizeof(short)]; }un; un.s=0x0102; printf("%s:",CUP_VENDOR_OS); if(sizeof(short)==2){ if(un.c[0]==1&&un.c[1]==2) printf("big-endian\n"); else if (un.c[0]==2&&un.c[1]==1) printf("little-endian\n"); else printf("unknown\n"); }else printf("sizeof(short)=%d\n",sizeof(short)); exit(0); }
9.位元組操縱函式
bzero:bzero把目標位元組串指定數目的位元組置為0。我們常用該函式把一個套接字地址結構初始化為0.
bocpy:指定數目的位元組從源位元組串移動到目標位元組串。
bcmp:比較兩個任意的位元組串,若相同返回值為0,否則返回值為非0.
memset:把目標位元組串指定數目的位元組置為c。
mencmp:比較兩個任意的字串,若相同為0,否則返回一個非0值,是大於0還是小於0則取決於第一個不等的位元組。
支援IPv4的inet_pton函式的簡單定義:
int inet_pton(int family,const char *strptr,void *addrptr) { if(family==AF_INET) { struct in_addr in_val; if(inet_aton(strptr,&in_val)) { memcpy(addrptr,&in_val,sizeof(struct int_addr)); return(1); } return(0); } errno=EAFNOSUPPROT; return(-1); }
三、最有收穫的內容
Web和CGI程式設計
全球資訊網(WWW)或 Web 是網際網路上的資源和使用者組合,它使用超文字傳輸協議(HTTP)(RFC2616 1999)進行資訊交換。自 20世紀 90年代初問世以來,隨著網際網路能力的不斷擴充套件,Web 已經成為世界各地人們日常生活中不可或缺的一部分。因此,對於電腦科學的學生來說,瞭解這項技術非常重要。在本節中,我們將介紹 HTTP和Web程式設計的基礎知識。Web 程式設計通常包括Web開發中涉及的編寫、標記和編碼,其中包括Web 內容、Web 客戶機和伺服器指令碼以及網路安全。狹義上,Web程式設計指的是建立和維護 Web 頁面。Web程式設計中最常用的語言是HTML、XHTML、JavaScript、Perl5和 PHP。
Http程式設計模型
HTTP是一種基於伺服器-客戶機的協議,用於網際網路上的應用程式。它在TCP上執行,因為它需要可靠的檔案傳輸。圖13.10所示為HTTP程式設計模型。
在HTTP 中,客戶機可發出多個URL,將請求傳送到不同的HTTP伺服器。客戶機與特定伺服器保持永久連線不但沒有必要,也不可取。客戶機連線到伺服器只是為了傳送請求,傳送完畢後會關閉連線。同樣,伺服器連線到客戶機也只是為了傳送應答,傳送完畢後會再次關閉連線。每個請求或應答都需要一個單獨的連線。這意味著 HTTP是一種無狀態協議,因為在連續的請求或應答之間不需要維護任何資訊。自然,這將導致大量系統開銷和效率低下。為彌補這一缺乏狀態資訊的問題,HTTP 伺服器和客戶機可使用 cookie 來提供和維護它們之間的一些狀態資訊。
Web介面
Web 頁面是用HTML標記語言編寫的檔案。Web檔案通過一系列HTML元素指定Web 頁面的佈局,可在 Web 瀏覽器上解釋和顯示。常用的Web 瀏覽器有Internet Explorer、Firefox、Google Chrome 等。建立 Web 頁面相當於使用HTML 元素作為構建塊建立文字檔案。與其說它是程式設計,不如說是文書類工作。因此,我們不討論如何建立Web 頁面。相反,我們將只使用一個示例 HTML檔案來說明Web 頁面的本質。下面給出了一個簡單的HTML Web 檔案。
CGI程式設計
CGI代表通用閘道器介面(RFC 3875 2004)。它是一種協議,允許 Web伺服器執行程式,根據使用者輸入動態生成Web 頁面。使用CGI.Web 伺服器不必維護數百萬個靜態Web 頁面檔案來滿足客戶機請求。相反,它通過動態生成Web 頁面來滿足客戶機請求。圖13.14顯示了CGI程式設計模型。
在 CGI程式設計模型中,客戶機發送一個請求,該請求通常是一個HTML表單,包含供伺服器執行的 CGI程式的輸入和名稱。在接收到請求後,httpd伺服器會派生一個子程序來執行 CGI程式。CGI程式可以使用使用者輸入來查詢資料庫系統,如 MySQL,從而根據使用者輸入生成 HTML 檔案。當子程序結束時,httpd伺服器將生成的HTML 檔案傳送回客戶機。CGI 程式可用任何程式語言編寫,如 C語言、sh 指令碼和Perl。
四、實踐內容(截圖、程式碼連結)
程式碼連結:
https://gitee.com/two_thousand_and_thirteen/zx-code/issues/I4J8R1
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <time.h> #include <string.h> #include <unistd.h>#define MAXLINE 256
#define PORT 7777
void sys_err(char *msg){
perror(msg);
exit(-1);
}
int main(int argc , char **argv){</span><span style="color: #0000ff;">int</span><span style="color: #000000;"> sockFd,n; </span><span style="color: #0000ff;">char</span><span style="color: #000000;"> recvLine[MAXLINE]; </span><span style="color: #0000ff;">struct</span><span style="color: #000000;"> sockaddr_in servAddr; </span><span style="color: #0000ff;">if</span> (argc != <span style="color: #800080;">2</span><span style="color: #000000;">) { sys_err(</span><span style="color: #800000;">"</span><span style="color: #800000;">usage: a.out <IPaddress></span><span style="color: #800000;">"</span><span style="color: #000000;">); } sockFd</span>=socket(AF_INET,SOCK_STREAM,<span style="color: #800080;">0</span><span style="color: #000000;">); memset(</span>&servAddr,<span style="color: #800080;">0</span>,<span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(servAddr)); servAddr.sin_family </span>=<span style="color: #000000;"> AF_INET; servAddr.sin_port </span>=<span style="color: #000000;"> htons(PORT); </span><span style="color: #0000ff;">if</span> (inet_pton(AF_INET,argv[<span style="color: #800080;">1</span>],&servAddr.sin_addr) <= <span style="color: #800080;">0</span><span style="color: #000000;">) { sys_err(</span><span style="color: #800000;">"</span><span style="color: #800000;">inet_pton error</span><span style="color: #800000;">"</span><span style="color: #000000;">); } connect(sockFd,(</span><span style="color: #0000ff;">struct</span> sockaddr *)&servAddr,<span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(servAddr)); </span><span style="color: #0000ff;">while</span>((n=read(sockFd,recvLine,MAXLINE)) ><span style="color: #800080;">0</span><span style="color: #000000;"> ){ recvLine[n] </span>= <span style="color: #800000;">'</span><span style="color: #800000;">\0</span><span style="color: #800000;">'</span><span style="color: #000000;">; </span><span style="color: #0000ff;">if</span>(fputs(recvLine,stdout) ==<span style="color: #000000;"> EOF){ sys_err(</span><span style="color: #800000;">"</span><span style="color: #800000;">fputs error</span><span style="color: #800000;">"</span><span style="color: #000000;">); } } </span><span style="color: #0000ff;">if</span>(n <<span style="color: #800080;">0</span><span style="color: #000000;">){ sys_err(</span><span style="color: #800000;">"</span><span style="color: #800000;">read error</span><span style="color: #800000;">"</span><span style="color: #000000;">); } </span><span style="color: #0000ff;">return</span> <span style="color: #800080;">0</span><span style="color: #000000;">;
}
程式碼執行截圖:
五、問題與解決思路
Linux系統下進行套接字程式設計中存在五大隱患都有什麼?
隱患 1.忽略返回狀態
第一個隱患很明顯,但它是開發新手最容易犯的一個錯誤。如果您忽略函式的返回狀態,當它們失敗或部分成功的時候,您也許會迷失。反過來,這可能傳播錯誤,使定位問題的源頭變得困難。
捕獲並檢查每一個返回狀態,而不是忽略它們。考慮清單 1 顯示的例子,一個套接字 send 函式。
1. 忽略 API 函式返回狀態
int status, sock, mode;/ Create a new stream (TCP) socket /sock =
socket( AF_INET, SOCK_STREAM, 0 );
...status = send( sock, buffer, buflen, MSG_DONTWAIT );
if (status == -1) {/ send failed /printf( "send failed: %s\n",?
strerror(errno) );
} else {/ send succeeded -- or did it? /}
清單 1 探究一個函式片斷,它完成套接字 send 操作(通過套接字傳送資料)。函式的錯誤狀態被捕獲並測試,但這個例子忽略了 send 在無阻塞模式(由 MSG_DONTWAIT 標誌啟用)下的一個特性。
send API 函式有三類可能的返回值:如果資料成功地排到傳輸佇列,則返回 0。 如果排隊失敗,則返回 -1(通過使用 errno 變數可以瞭解失敗的原因)。 如果不是所有的字元都能夠在函式呼叫時排隊,則最終的返回值是傳送的字元數。
由於 send 的 MSG_DONTWAIT 變數的無阻塞性質,函式呼叫在傳送完所有的資料、一些資料或沒有傳送任何資料後返回。在這裡忽略返回狀態將導致不完全的傳送和隨後的資料丟失。
隱患 2.對等套接字閉包
UNIX 有趣的一面是您幾乎可以把任何東西看成是一個檔案。檔案本身、目錄、管道、裝置和套接字都被當作檔案。這是新穎的抽象,意味著一整套的 API 可以用在廣泛的裝置型別上。
考慮 read API 函式,它從檔案讀取一定數量的位元組。read 函式返回讀取的位元組數(最高為您指定的最大值);或者 -1,表示錯誤;或者 0,如果已經到達檔案末尾。
如果在一個套接字上完成一個 read 操作並得到一個為 0 的返回值,這表明遠端套接字端的對等層呼叫了 close API 方法。該指示與檔案讀取相同 —— 沒有多餘的資料可以通過描述符讀取(參見 清單 2)。
2.適當處理 read API 函式的返回值
int sock, status;sock = socket( AF_INET, SOCK_STREAM, 0 );...status = read( sock, buffer, buflen );
if (status > 0) {/ Data read from the socket /} else if (status == -1)
{/ Error, check errno, take action... /} else if (status ==
0) {/ Peer closed the socket, finish the close /close( sock );
/ Further processing... /}
同樣,可以用 write API 函式來探測對等套接字的閉包。在這種情況下,接收 SIGPIPE 訊號,或如果該訊號阻塞,write 函式將返回 -1 並設定 errno 為 EPIPE。
隱患 3.地址使用錯誤(EADDRINUSE)
3.使用 SO_REUSEADDR 套接字選項避免地址使用錯誤
int sock, ret, on;struct sockaddr_in servaddr;/ Create a new stream (TCP) socket /sock =
socket( AF_INET, SOCK_STREAM, 0 ):
/ Enable address reuse /on = 1;
ret = setsockopt( sock, SOL_SOCKET, SO_REUSEADDR,
&on, sizeof(on) );/* Allow connections to
port 8080 from any available interface
*/memset( &servaddr, 0, sizeof(servaddr) );
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
servaddr.sin_port = htons( 45000 );
/* Bind to the address (interface/port)
*/ret = bind( sock, (struct sockaddr *)&servaddr, sizeof(servaddr) );
在應用了 SO_REUSEADDR 選項之後,bind API 函式將允許地址的立即重用。
隱患 4.傳送結構化資料
套接字是傳送無結構二進位制位元組流或 ASCII 資料流(比如 HTTP 上的 HTTP 頁面,或 SMTP 上的電子郵件)的完美工具。但是如果試圖在一個套接字上傳送二進位制資料,事情將會變得更加複雜。
比如說,您想要傳送一個整數:您可以肯定,接收者將使用同樣的方式來解釋該整數嗎?執行在同一架構上的應用程式可以依賴它們共同的平臺來對該型別的資料做出相同的解釋。但是,如果一個執行在高位優先的 IBM PowerPC 上的客戶端傳送一個 32 位的整數到一個低位優先的 Intel x86,那將會發生什麼呢?位元組排列將引起不正確的解釋。
隱患 5.TCP 中的幀同步假定
TCP 不提供幀同步,這使得它對於面向位元組流的協議是完美的。這是 TCP 與 UDP(User Datagram Protocol,使用者資料報協議)的一個重要區別。UDP 是面向訊息的協議,它保留髮送者和接收者之間的訊息邊界。TCP 是一個面向流的協議,它假定正在通訊的資料是無結構的。
5.tcpdump 工具的用法模式
Display all traffic on the eth0 interface forthe local host$ tcpdump -l -i eth0Show all traffic
on the network coming from or going
to host plato$ tcpdump host platoShow all HTTP traffic
for host camus$ tcpdump host camus and (port http)View
traffic coming from or going
to TCP port 45000 on the local host$ tcpdump tcp port 45000
tcpdump 和 tcpflow 工具有大量的選項,包括建立複雜過濾表示式的能力。查閱下面的 參考資料 獲取更多關於這些工具的資訊。