CSAPP--第十一章:網(wǎng)絡(luò)編程
客戶端-服務(wù)器模型
-
網(wǎng)絡(luò)應(yīng)用都是基于客戶端-服務(wù)器模型却盘。
一個應(yīng)用由一個服務(wù)器進(jìn)程和多個客戶端進(jìn)程構(gòu)成窘游。事務(wù):模型中的基本操作,包含四步:
①客戶端進(jìn)程發(fā)送請求
②服務(wù)器進(jìn)程接收請求寺庄,并與其存儲設(shè)備交互進(jìn)行處理艾蓝。
③服務(wù)器進(jìn)程發(fā)送響應(yīng)。
④客戶端進(jìn)程處理響應(yīng)斗塘。
事務(wù)
網(wǎng)絡(luò)
客戶端和服務(wù)端通常運(yùn)行在不同主機(jī)上赢织,通過計算機(jī)網(wǎng)絡(luò)的硬件和軟件來通信。
-
LAN(Local Area Network)
局域網(wǎng)馍盟,其包含的主機(jī)位置范圍最小于置,通常在一個建筑內(nèi)。
以太網(wǎng)(Ethernet): 現(xiàn)在最流行的局域網(wǎng)技術(shù)贞岭。
局域網(wǎng)內(nèi)八毯,各主機(jī)通過集線器進(jìn)行連接。如下圖:
網(wǎng)絡(luò)硬件組成
-
網(wǎng)橋(bridge):用于連接多個以太網(wǎng)段瞄桨,形成較大的局域網(wǎng)(橋接以太網(wǎng))话速。
如下圖:
-
WAN(Wide-Area network):
廣域網(wǎng),其將不同局域網(wǎng)連接在一起芯侥,因?yàn)榈乩矸秶染钟蚓W(wǎng)大泊交,取名廣域網(wǎng)。
如下圖:
網(wǎng)絡(luò)協(xié)議
不同的主機(jī),其采用的局域網(wǎng)和廣域網(wǎng)技術(shù)可能不兼容活合,所以提出一個協(xié)議雏婶,消除主機(jī)間的通信障礙。
-
協(xié)議功能:
①命名機(jī)制:采取同一套地址命名機(jī)制白指。
②傳送機(jī)制:定義一種通用的數(shù)據(jù)包:包頭 + 有效荷載留晚。包頭記載了源和目的主機(jī)的地址、及包大小等信息告嘲。
全球IP因特網(wǎng)
-
TCP/IP協(xié)議(Transmission Control Protocol错维,傳輸控制協(xié)議 / Internet Protocol,互聯(lián)網(wǎng)絡(luò)協(xié)議)
TCP/IP是一個協(xié)議簇橄唬,每個協(xié)議提供不同功能赋焕。TCP比IP更復(fù)雜。
-
IP協(xié)議:提供了基本命名方式和傳送機(jī)制仰楚。
實(shí)現(xiàn)從一個主機(jī)隆判,往另一個主機(jī)傳送包,也稱數(shù)據(jù)報僧界。
其對丟包不會試圖恢復(fù)侨嘀。
UDP協(xié)議(Unreliable Datagram Protocol,不可靠數(shù)據(jù)報協(xié)議)
-
TCP協(xié)議:構(gòu)建在IP協(xié)議之上的復(fù)雜協(xié)議捂襟,提供進(jìn)程間可靠的雙向連接咬腕。
因特網(wǎng)應(yīng)用程序的軟件和硬件
-
-
IPv4、IPv6
因特網(wǎng)協(xié)議版本4 (32位地址)
1996年提出的因特網(wǎng)版本協(xié)議6 (128位地址):如今用戶還不多葬荷。
IP地址 //以下針對 IPv4
ip地址是32位無符號整數(shù)涨共。網(wǎng)絡(luò)程序?qū)p地址存放在:ip地址結(jié)構(gòu)中。
ps:其導(dǎo)致了麻煩的操作宠漩,每次使用要建立結(jié)構(gòu)體举反,再取其中的值,很麻煩:骞隆U罩!
struct in_addr{
unit32_t s_addr; /* 網(wǎng)絡(luò)字節(jié)順序中統(tǒng)一為大端順序 */
}瘦陈;
網(wǎng)絡(luò)字節(jié)順序(network byte order)為大端字節(jié)順序凝危。
所以對于小端機(jī)器中,會進(jìn)行字節(jié)順序(32位)轉(zhuǎn)換晨逝。
Unix提供的轉(zhuǎn)換函數(shù):
#inlcude<arpa/inet.h>
uint32_t htonl(uint32_t hostlong ); //host to network
uint16_t htonl(uint16_t hostshort );
uint32_t ntohl(uint32_t netlong ); //network to host
uint16_t ntohl(uint16_t netshort );
ps:沒有對應(yīng)的64位的函數(shù)蛾默。
-
點(diǎn)分十進(jìn)制表示法
用來表示ip地址的一種方法。
如0x00000000 -> 0.0.0.0
? 二進(jìn)制 點(diǎn)分十進(jìn)制
應(yīng)用程序通過inet_pton 捉貌、inet_ntop函數(shù)來實(shí)現(xiàn)轉(zhuǎn)換焚碌。
#include<arpa/inet.h> int inet_pton(AF_INET, const char *src, void *dst); //成功返回1,src非法時點(diǎn)分十進(jìn)制地址為0支竹,出錯為-1; const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size); //成功返回指向點(diǎn)分十進(jìn)制字符串的指針急前,錯誤為NULL
說明:以上兩個函數(shù)中,網(wǎng)絡(luò)地址的值是在結(jié)構(gòu)體中瀑构,所以要先設(shè)置結(jié)構(gòu)體裆针,然后取其中的值。
編寫中出錯了蠻多寺晌。
具體看linux 中的網(wǎng)絡(luò)編程文件夾下的ntop.c 世吨、 pton.c。
-
因特網(wǎng)域名
域名:一串用句號分隔的單詞(字母呻征、數(shù)字耘婚、破折號)。例如:www.baidu.com
域名層次結(jié)構(gòu)(樹狀):
分為多層域名--
第一層為ICANN協(xié)會定義的陆赋,包括com沐祷、edu、gov攒岛、org戈轿、net等。
第二層為先到先分配的阵子。一個組織得到了二級域名后,就可以在子域(節(jié)點(diǎn)的子樹)中創(chuàng)建任何新的域名胜蛉。如:cmu.edu cs.cmu.edu
-
域名挠进、IP地址映射
1988年之前,都是用HOSTS.TXT的 文本文件來手工維護(hù)誊册。
1988年后领突,通過世界范圍內(nèi)的數(shù)據(jù)庫DNS(Domain Name System:域名系統(tǒng))來維護(hù)。
DNS數(shù)據(jù)庫:有上百萬條 主機(jī)條目結(jié)構(gòu) 組成案怯,每條主機(jī)條目結(jié)構(gòu)定義了一組域名和IP地址之間的映射君旦。
-
映射關(guān)系:
一對一 映射
多個域名映射一個IP地址
一個域名映射多個IP地址(同一組)
多個域名映射同一組多個IP地址
-
因特網(wǎng)連接
因特網(wǎng)中,客戶端和服務(wù)器通過在連接上發(fā)送和接收字節(jié)流來通信嘲碱。
從連接一對進(jìn)程而言金砍,其是:點(diǎn)對點(diǎn)的。
從數(shù)據(jù)雙向流動而言麦锯,其是:全雙工的恕稠。
-
套接字(socket):
連接的一個端點(diǎn)。
其內(nèi)容為IP地址加上16位的整數(shù)端口組成扶欣。(非硬件而是軟件端口)
如:127.0.0.1:51212
-
臨時端口(ephemeral port):
當(dāng)客戶端發(fā)起連接請求時鹅巍,客戶端套接字地址中的端口千扶,是由內(nèi)核自動分配的。稱為臨時端口骆捧。
-
知名端口:
服務(wù)器中澎羞,套接字地址的端口常常是和服務(wù)綁定的,如web服務(wù)器通常使用端口80敛苇,電子郵件服務(wù)器通常使用端口25等妆绞。稱為知名端口。
-
套接字對(socket pair):
由連接兩端的套接字接谨,唯一確定摆碉。(cliaddr : cliport,servaddr:servport)
網(wǎng)絡(luò)套接字
-
-
套接字接口(socket inerface)
其是一組函數(shù)脓豪,與Unix I/O結(jié)合起來巷帝,用以創(chuàng)建網(wǎng)絡(luò)應(yīng)用。
套接字地址結(jié)構(gòu)
①從內(nèi)核角度看扫夜,套接字是通信的一個端點(diǎn)楞泼。
②從linux程序看,套接字是一個有相應(yīng)描述符的打開文件笤闯。
因特網(wǎng)的套接字地址存放在一個結(jié)構(gòu)體中:sockaddr_in (16字節(jié))
/* IP socket addr structure */ struct sockaddr_in{ uint16_t sin_family; /*socket family(always AF_INET)*/ uint16_t sin_port; /*port number in network byte order*/ struct in_addr sin_addr;/*IP address in network byte order*/ unsigned char sin_zeor[8];/*pad to sizeof(struct sockaddr)*/ }; /*generic socket addr structure(for connect,bind,accept)*/ struct sockaddr{ uint16_t sa_family; /*protocol family*/ char sa_data[14];/*address data*/ };
實(shí)際客戶端-服務(wù)器的訪問流程如下圖:
-
相關(guān)函數(shù)
-
socket函數(shù)
客戶端和服務(wù)器使用socket函數(shù)來創(chuàng)建一個套接字描述符(socket descriptor)
客戶端和服務(wù)器都首先要用這個函數(shù)堕阔。
#include<sys/types.h> #include<sys/socket.h> int socket(int domain,int type,int protocol); //成功返回非負(fù)數(shù)描述符,出錯返回-1颗味; /* 如:clientFd = socket(AF_INET, SOCK_STREAM超陆,0);AF_INET:指明32位IP地址。SOCK_STREAM:這個套接字是連接的一個端點(diǎn)浦马。*/
此時僅是部分打開时呀,并不能進(jìn)行讀寫,根據(jù)客戶端和服務(wù)器使用不同的方式完全打開晶默。
這幾個函數(shù)最好用getaddrinfo函數(shù)來賦予參數(shù)谨娜。
-
connect函數(shù)
客戶端用來與套接字地址為addr 的服務(wù)器建立網(wǎng)絡(luò)連接。
#incldue<sys/sokcet.h> int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen); //成功返回0磺陡,出錯返回-1. 描述符保存在clientfd中趴梢。 //addrlen=sizeof(sockaddr_in)
當(dāng)成功時,clientfd就可以讀寫了币他。此時套接字對為:
(x:y , addr.sin_addr : addr.sin_port)
x:客戶端IP坞靶。y:客戶端臨時端口。其唯一確定了客戶端主機(jī)的某個進(jìn)程圆丹。
-
bind函數(shù)
socket滩愁、connect函數(shù):客戶端用來與服務(wù)器建立連接。(依次)
socket辫封、bind硝枉、listen廉丽、accpet:服務(wù)器用來與客戶端建立連接。(依次)
bind函數(shù)將一個套接字地址和一個套接字描述符綁定起來(如socket剛剛建立的)妻味。
#include<sys/socket.h> int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //成功范圍0正压,出錯-1.
-
listen函數(shù)
listen函數(shù)將一個主動套接字描述符(如bind轉(zhuǎn)化后的sockfd)轉(zhuǎn)化為監(jiān)聽套接字(listening socket),其可以接收客戶端的連接請求责球。
說明:無論客戶段服務(wù)器焦履,內(nèi)核默認(rèn)套接字為主動套接字。所以服務(wù)器要將其轉(zhuǎn)換為監(jiān)聽套接字雏逾,說明自己是服務(wù)器端嘉裤。
#include<sys/socket.h> int listen(int sockfd, int backlog); //backlog的確切含義要求對TCP\IP協(xié)議有理解。CSAPP將其設(shè)置為較 //大的值如:1024
-
accept函數(shù)
服務(wù)器通過調(diào)用accept函數(shù)來等待來自客戶端的連接請求(connect)
#include<sys/socket.h> int accept(int listenfd, struct sockaddr *addr, int *addrlen); //成功返回已連接描述符栖博,出錯返回-1 //結(jié)構(gòu) addr用來安置客戶端過來的套接字地址(在結(jié)構(gòu)中一一替換)
-
會返回一個已連接描述符屑宠,用這個描述符,可以與客戶端進(jìn)行Unix I/O讀寫仇让。
注意:監(jiān)聽描述符和已連接描述符都會存在于描述符列表中典奉。
實(shí)際步驟:
①服務(wù)器調(diào)用accept,等待連接請求到達(dá)監(jiān)聽描述符丧叽。(假設(shè)=3)
②客戶端調(diào)用connect函數(shù)卫玖,請求一個連接。
③accept函數(shù)打開一個新的已連接描述符(connect descriptor)connfd(假設(shè)=4)踊淳,在clientfd和connfd之間建立連接假瞬,并隨后返回connfd給應(yīng)用程序。
客戶端也從connect返回迂尝,然后笨触,客戶端和服務(wù)器都可以通過讀寫clientfd和connfd來傳送數(shù)據(jù)。
說明:監(jiān)聽描述符是存在于服務(wù)器的生命周期內(nèi)雹舀,只會創(chuàng)建一次。而connfd則是客戶端和服務(wù)器已經(jīng)建立起來的一個端點(diǎn)粗俱。服務(wù)器每次接受連接請求都會創(chuàng)建一次说榆,其只存在于服務(wù)器為一個客戶端服務(wù)的過程中。
ps:有利于并發(fā)編程寸认。
主機(jī)和服務(wù)的轉(zhuǎn)換
-
getaddrinfo函數(shù)
用處:主機(jī)IP地址和服務(wù)(或端口)→ 套接字地址
#include<sys/types.h> #include<sys/socket.h> #include<netdb.h> int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result); //成功返回0签财,錯誤返回非0的錯誤代碼,其可轉(zhuǎn)化為字符串偏塞。 void freeaddrinfo(struct addrinfo *result); const char *gai_strerror(int error); //將錯誤代碼翻譯成字符串唱蒸。
//hints是一個addrinfo結(jié)構(gòu),提供對于返回的套接字地址列表的更好的控制灸叼。只能設(shè)置下列字段:ai_family , si_socktype , ai_protocol , ai_flasg神汹。結(jié)構(gòu)中其他字段為0(NULL).
struct addrinfo{ int ai_flags; int ai_family; int ai_socktype; int ai_protocol; char *ai_cannoname; size_t ai_addrlen; struct sockaddr *ai_addr; struct addrinfo *ai_next; }
-
getnameinfo函數(shù)
用處:套接字地址結(jié)構(gòu) → 主機(jī)IP地址和服務(wù)名字符串
#include<sys/socket.h> #include<netdb.h> int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags); //成功返回0庆捺,錯誤返回非零錯誤代碼。 //flags 位掩碼屁魏。
getnameinfo函數(shù)將套接字地址結(jié)構(gòu)sa滔以,轉(zhuǎn)換成對應(yīng)的主機(jī)和服務(wù)名字符串,將他們復(fù)制到host和service緩沖區(qū)氓拼。
如果返回非零你画,可以用gai_strerror(int errorcode);轉(zhuǎn)化為字符串桃漾。
以上兩個函數(shù)坏匪,如果主機(jī)名和服務(wù)名可以空白一個(NULL代替)。不能兩個都NULL撬统。
實(shí)驗(yàn)代碼在CSAPP p659适滓,或者我的linux 主機(jī)上的網(wǎng)絡(luò)編程。
-
小結(jié):兩個關(guān)于套接字地址的結(jié)構(gòu)體:
sockaddr:包含套接字的IP地址宪摧、端口地址粒竖、協(xié)議類型。
addrinfo:對sockaddr結(jié)構(gòu)體的再包裝几于,附加了一些額外信息蕊苗。
用包裝過的函數(shù)創(chuàng)建連接
說明:將上述的客戶端和服務(wù)器中建立連接的函數(shù),包裝起來成為一個簡潔實(shí)用的函數(shù)沿彭。
-
open_clientfd函數(shù)
客戶端調(diào)用此函數(shù)朽砰,建立與服務(wù)器的連接,其返回一個打開的套接字描述符喉刘。
服務(wù)器運(yùn)行在hostname主機(jī)上的瞧柔,并監(jiān)聽了port端口。
#include<sys/socket.h>
#include<netdb.h>
#include<sys/types.h>
#include<stdio.h>
int open_clientfd(char *hostname, char *port) //針對客戶端睦裳。
{
int clientfd;
struct addrinfo hints, *listp, *p;
/* get alist of server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /*open a connection*/
hints.ai_flags = AI_NUMERICSERV; /*using a numeric(數(shù)字) port argument*/
hints.ai_flags |= AI_ADDRCONFIG; /*recommended for connections*/
getaddrinfo(hostname, port, &hints, &listp);
/*walk(traverl) the list for one that we can successfully connect to */
for(p = listp; p ; p = p->ai_next ){
/*create a socket descriptor */
if((clientfd = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0)
continue; /*socket failed, try the next one */
/*connect to the server */
if(connect(clientfd,p->ai_addr,p->ai_addrlen)!=-1)
break; /*success*/
close(clientfd); /*connect failed, try another*/
}
/* clean up */
freeaddrinfo(listp);
if(!p) return -1; /* all connects failed */
else return clientfd;
}
注意:代碼具有協(xié)議無關(guān)性造锅。因?yàn)閟ocket和connect的參數(shù)都是getaddrinfo自動生成的。
-
open_listenfd
服務(wù)器調(diào)用此函數(shù)廉邑,打開并返回一個監(jiān)聽描述符哥蔚,準(zhǔn)備好在端口port接收請求。
#include<sys/socket.h> #include<netdb.h> #include<sys/types.h> #include<stdio.h> int open_listenfd(char *port) { struct addrinfo hints, *listp, *p; int listenfd, optval=1; /* 得到一張服務(wù)地址表 */ memset(&hints,0 , sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; /*類型為接收連接*/ hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /*針對任何ip地址*/ hints.ai_flags |= AI_NUMRICSERV; /*使用端口數(shù)字*/ getaddrinfo(NULL, port, &hints, &listp); /* 遍歷地址表蛛蒙,找到一個可以用來綁定的 */ for(p = listp ; p ; p = p.ai_next){ /* 增加一個套接字描述符 */ if((listenfd = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0) continue; /* 套接字不可用糙箍,試下一個 */ /* 為bind中,消除“地址早已被使用”的錯誤可能 */ setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int)); /* 綁定描述符和套接字地址 */ if(bind(listenfd, p->ai_addr, p->ai_addrlen) == 0) break; /*成功*/ close(listenfd); /*綁定失敗牵祟,關(guān)閉描述符深夯,試下一個。*/ } /* 釋放 */ freeaddrinfo(listp); if(!p) return -1; /*沒有地址可用*/ /* 使一個監(jiān)聽套接字诺苹,準(zhǔn)備好去接收一個連接請求 */ if(listen(listenfd, LISTENQ)<0){ close(listenfd); return -1; } return listenfd; }