16.1 Introduction html
Chapter15講的是同一個machine之間不一樣進程的通訊,這一章內容是不一樣machine之間經過network通訊,切入點是socket。 node
16.2 Socket Descriptorslinux
socket抽象上是一個communication endpoint,具體就是一個int型變量。生成socket的函數以下:git
int socket(int domain, int type, int protocol)github
函數有點兒相似open,即打開一個socket descriptor。編程
函數返回的就是 socket descriptor(是file descriptor)的一種。ubuntu
三個輸入參數:小程序
domain : 整數枚舉類型,決定了nature of communication,其中包括address formatvim
type : 整數枚舉類型,決定了communication characterisitcs;主要包括SOCK_STREAM、SOCK_DGRAM兩種;具體還沒太理清楚,可是前者是須要server與client先connet再交換數據的,後者是能夠直接在server與client之間交換數據的網絡
protocol : 整數枚舉類型,通常設爲0(由於protocol通常跟domain+type匹配,前兩個參數決定了,protocol參數就決定了)
在unix系統設計的時候,一些能夠操做file descriptor的函數,也能夠操做socket descriptor,好比:close dup dup2 read write等等。
可是socket有本身特殊的地方,socket是雙向做用的,有接口函數用來關閉socket的某個方向上的功能。
int shutdown(int sockfd, int how)
how : 整數枚舉類型,若是how是SHUT_RD,則關閉的是read功能;若是how是SHUT_RDWR,則關閉的是read和write。
已經有close能夠關閉socket,爲何還要有shutdown這個函數呢?
(1)因爲socket也是一種file,所以須要全部與socket相關的reference都關閉了才能真的把這個socket給close了。尤爲在network這種狀況下,每每一個socket會dup出來好多reference。而shutdown的操做不受到reference都關閉的限制。
(2)有時候,須要關閉單方面的操做,read或者write。
由於有了上面的需求,因此纔開發出了shutdown這種接口函數。
16.3 Addressing
socket函數至關於在server端和client端分別產生communication endpoint,這個endpoint就是int類型的變量。(即,若是把server和client比做兩個老城市,有了socket就至關於有了兩個城市分別有了郵電局,有了郵電局就具有了通訊的基本條件)
server和client兩端光有socket還不夠,要想在兩者之間通訊必須告訴socket「到哪」、「跟誰」、「怎麼」通訊。
這裏「到哪」至關於找到address(即城市郵電局的總機號碼),「跟誰」至關於找到port(即撥通了總機號碼以後,去撥哪一個分機號),「怎麼」至關於通訊的格式(即至關於電話線中電磁波怎麼發送和接受)。
按照上述的思路,能夠串起來這一章節的內容。
1. Byte Ordering
全部的通訊都要約定最基礎的底層的數據格式。其中byte ordering就是一個基礎問題。
舉例來講,簡單說一個32-bits的整數,由4個byte來表示,0x04030201
在真實的存儲中能夠有兩種狀況
(1)一種是真的按照上面16進制的順序表示即從前日後存放的是0x04,0x03,0x02,0x01,這種叫little-endian。
(2)另外一種存放的順序正好相反,從前日後存放的是0x01,0x02,0x03,0x04,這種叫big-endian。
在真實的通訊中,須要server和client都清楚對方發來的是什麼樣的byte ordering,只有雙方都清楚了才能保證通訊正常進行。(即,至關於雙方寫信,必須讓對方知道,是「從左往右讀、仍是從右往左讀」的約定)
值得注意的是TCP/IP protocol系列都是big-endian套路的,而後不少系統Processor architecture用的都是little-endian套路的(好比最多見的X86架構);所以,在進行程序不一樣machine 不一樣平臺之間交換數據的時候,要注意是否是一樣的字節續,跟網絡續是否匹配。系統已經封裝了幾個函數供咱們使用,來自動進行轉換(htonl htons ntohl ntohs,其中h表明host,n表明network s表示16bit的short l表示32bit的long)。後面的例子中會體現這一點。
這個blog講述的字節序、網絡序比較易懂:http://songlee24.github.io/2015/05/02/endianess/。
2. Address Formats
用一種數據結構來表示不一樣的通訊格式以及其內容。這種數據結構就是struct sockaddr。
struct sockaddr{
sa_family_t sa_family; /*address family*/
char sa_data[]; /*variable-length address*/
...
}
不一樣的系統可能對上面的某些成員可能會不一樣,通用的規則大概以下:
(1)sa_family這個參數通常都是格式肯定的,這個標明地址格式。
(2)sa_data這個參數不一樣的系統實現全部不一樣,可是含義都是表達通訊的地址具體內容。
3. Address Lookup
前面說過,要想完成通訊必須知道通訊雙方的地址信息+端口信息(即郵局在哪裏,具體是郵局的哪一個窗口)。進行這種address lookup的方式有兩種:
(1)直接給現成的。好比某臺機器的ssh服務:IP爲166.111.170.1 port爲22,知道了這兩項內容就找到了通訊目的機器和端口,系統直接按照有效的IP和有效的端口號所指定的機器端口尋找通訊目標。
(2)經過名稱間接找。上面是最直接的地址查找方式,另外一種更人性化的方式就是輸入hostname和servicename,再映射到具體的IP數值和port數值上。(這相似叫一我的不會直接去叫他的身份證號,而是叫他的名字)。
所以引入一個重要的函數,能夠兼容上面兩種查找的方式,以及囊括了IP和port兩項內容。
int getaddrinfo(const char *restrict host,
const char *restrict service,
const struct addrinfo *restrict hint,
struct addrinfo **restrict res);
前兩個輸入參數以下
(1)host : 若是host是合理的IP地址,就不去/etc/hosts中搜索了;不然,去/etc/hosts中去找hostname對應的IP地址。
(2)service : 若是service是合理的port數值,就不去/etc/services;不然,去/etc/services中去找services name對應的port數值。
第三個參數以下
(3)hint : 起到一個「過濾器」的做用。由於,符合host name + service name條件的address可能有多個;好比166.111.170.1:22這樣的組合,符合條件的既有tcp也有udp的。經過/etc/services文件能夠驗證:
好比,我只想要全部services中只提供tcp通訊的address,那麼經過ip+port顯然是沒法作到的,所以能夠經過在hint中設定過濾條件來達到目的。hint是一個struct addrinfo結構,具體成員和含義以下:
struct addrinfo{
int ai_flags; /*customize behavior*/
int ai_family; /*address family*/
int ai_socktype; /*socket type*/
int ai_protocol; /*protocol*/
socklen_t ai_addrlen; /*length in bytes of address*/
struct sockaddr *ai_addr; /*address*/
char *ai_canonname; /*canonical name of host*/
struct addrinfo *ai_next; /*next int the list*/
...
}
其中,ai_socktype是一個重要的過濾選項,後面跟着例子一塊兒看。
(4)res : 這是一個result-valued argument,即執行這個函數最終但願得到的內容。這個res最終指向一個addrinfo的鏈表頭元素。即知足「host+service+hint過濾」的全部address集合。
下面看一個例子,體會一下這一部分的內容:
1 #include "apue.h" 2 #if defined(SOLARIS) 3 #include <netinet/in.h> 4 #endif 5 #include <netdb.h> 6 #include <arpa/inet.h> 7 #if defined(BSD) 8 #include <sys/socket.h> 9 #include <netinet/in.h> 10 #endif 11 12 void print_family(struct addrinfo *aip) 13 { 14 printf(" family "); 15 switch (aip->ai_family){ 16 case AF_INET: 17 printf("inet"); 18 break; 19 case AF_INET6: 20 printf("inet6"); 21 break; 22 case AF_UNIX: 23 printf("unix"); 24 break; 25 case AF_UNSPEC: 26 printf("unspecified"); 27 break; 28 default: 29 printf("unkown"); 30 } 31 } 32 33 void print_type(struct addrinfo *aip) 34 { 35 printf(" type "); 36 switch (aip->ai_socktype){ 37 case SOCK_STREAM: 38 printf("stream"); 39 break; 40 case SOCK_DGRAM: 41 printf("datagram"); 42 break; 43 case SOCK_SEQPACKET: 44 printf("seqpacket"); 45 break; 46 case SOCK_RAW: 47 printf("raw"); 48 break; 49 default: 50 printf("unknown (%d)", aip->ai_socktype); 51 } 52 } 53 54 void print_flags(struct addrinfo *aip) 55 { 56 printf(" flags "); 57 if (aip->ai_flags == 0) { 58 printf(" 0"); 59 } 60 else { 61 if (aip->ai_flags & AI_PASSIVE) { 62 printf(" passive"); 63 } 64 if (aip->ai_flags & AI_CANONNAME) { 65 printf(" canon"); 66 } 67 if (aip->ai_flags & AI_NUMERICHOST) { 68 printf(" numhost"); 69 } 70 if (aip->ai_flags & AI_NUMERICSERV) { 71 printf(" numserv"); 72 } 73 if (aip->ai_flags & AI_V4MAPPED) { 74 printf(" v4mapped"); 75 } 76 if (aip->ai_flags & AI_ALL) { 77 printf(" all"); 78 } 79 } 80 } 81 82 void print_protocol(struct addrinfo *aip) 83 { 84 printf(" protocol "); 85 switch (aip->ai_protocol) { 86 case 0: 87 printf("default"); 88 break; 89 case IPPROTO_TCP: 90 printf("TCP"); 91 break; 92 case IPPROTO_UDP: 93 printf("UDP"); 94 break; 95 case IPPROTO_RAW: 96 printf("raw"); 97 break; 98 default: 99 printf("unknown (%d)", aip->ai_protocol); 100 } 101 } 102 103 int main(int argc, char *argv[]) 104 { 105 struct addrinfo *ailist, *aip; 106 struct addrinfo hint; 107 struct sockaddr_in *sinp; 108 const char *addr; 109 int err; 110 char abuf[INET_ADDRSTRLEN]; 111 112 if (argc != 3) { 113 err_quit("usage: %s nodename servcie", argv[0]); 114 } 115 hint.ai_flags = AI_CANONNAME; 116 hint.ai_family = 0; 117 hint.ai_socktype = 0; 118 hint.ai_protocol = 0; 119 hint.ai_addrlen = 0; 120 hint.ai_canonname = NULL; 121 hint.ai_addr = NULL; 122 hint.ai_next = NULL; 123 124 /*getaddrinfo功能是找特定host 特定service的addrinfo信息*/ 125 /*argv[1] : host 126 *argv[2] : service 127 hint : 過濾用的addrinfo模板 128 ailist : linked list存放全部符合條件的addrinfo structure*/ 129 if ((err = getaddrinfo(argv[1], argv[2], &hint, &ailist))!=0) { 130 err_quit("getaddrinfo error: %s", gai_strerror(err)); 131 } 132 133 for (aip = ailist; aip != NULL; aip = aip->ai_next) 134 { 135 print_flags(aip); 136 print_family(aip); 137 print_type(aip); 138 print_protocol(aip); 139 printf("\n\thost %s", aip->ai_canonname ? aip->ai_canonname:"-"); 140 if (aip->ai_family == AF_INET) { /*只關心ipv4這個family的addrinfo信息*/ 141 sinp = (struct sockaddr_in *)aip->ai_addr; /*取出socket address信息*/ 142 addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN); 143 printf(" address %s", addr ? addr : "unkown"); 144 printf(" port %d", ntohs(sinp->sin_port)); 145 } 146 printf("\n"); 147 } 148 exit(0); 149 }
在個人機器上實驗結果以下:
能夠看到,tcp和udp以及後面跟了兩個其餘的內容(具體我不瞭解)。
對line117代碼作修改:ai_socktype = SOCK_STREAM,結果以下:
這樣hint的過濾功能就體現出來了。
這裏用到一個轉換函數inet_ntop
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size)
這個函數將addrinfo所指的address轉化成一個字符串,並返回字符串的地址。
在上面的例子中,IPV4的地址是做爲一個unit32_t整形存在系統中的。所以,咱們作以下的代碼改動(line145 line146增長兩行)
結果以下:
暫時忽略中間出現的幾個struct數據結構,只關注最後輸出的tmp.s_addr。這是一個unit32_t類型的變量,佔4個byte;那麼其值是16777343是如何獲得的呢?這個地址確定是與127.0.0.1是等價的。
咱們先寫一個小程序,以下:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> int main(int argc, char *argv[]) { uint32_t num = 16777343; char l = *((char *)&num); char h = *((char *)&num+3); printf("low address byte:%d\n",l); printf("high address byte:%d\n",h); return 0; }
執行結果以下:
所以在系統中地址由小到大,以此存放的4個byte爲:12七、0、0、1。我用的是Linux系統,按照little-endian原則,地址最小排在最低的byte(127),地址最大的排在最高的byte(1),所以有了以下的計算公式:1*256*256*256 + 127 = 16777343。 這個unit32_t類型的值也就獲得了。
前面提過,若是前兩個參數host和service輸入的不是name,而是有效的數值,getaddrinfo也能處理並返回結果。測試結果以下:
能夠獲得以下結論:
A. 若是輸入的是有效的數值,那麼getaddrinfo就不會去/etc/hosts和/etc/services中用name去分別找主機的地址和端口號,而是用一種約定的套路去進行地址的轉換。
B. 這種特定的方法就是,用4bytes存放一個IP。若是輸出的是number-and-dots這種格式,那麼會進行以下轉換(能夠man inet_aton來查看)
(1) 前三種狀況a.b.c.d、a.b.c、a.b都好說,number-and-dots從左往右,一段對應一ip的一個byte;若是不夠4段了,最後那段的數字就升級爲16bits或者24bits的,最前面段的數還照常。
(2)若是就是光禿禿一個數,那它就必須所有頂上,升級爲32bits的。所以客觀上「1」就被解釋成了0.0.0.1,但爲何不是1.0.0.0呢?前面說過TCP/IP是big-endian的,所以一個32bits的數字1,最高8bits的值是1,而最高8bits的值被當成了IP最後一段的值,所以也就是被系統結石爲0.0.0.1了。同理,16777343,按照一樣的方法就被解釋成了1.0.0.127。經過這個例子,對order byte有個印象,遇到問題了知道去查找各類轉換函數就OK了。
4. Associating Address with Sockets
要想理解好這個部分,須要去詳細讀unix network programming (unp)volume 1 chapter 4。我的以爲Richard Stevens把network相關的精髓都寫在了unp這本書上;而apue這本書的network部分只能作一個提綱挈領的參考;不然,不具有相關基礎,直接看apue比較困難。
最主要的是介紹了一個函數:bind函數
int bind(int sockfd, const struct sockaddr *addr, socklen_t len)
能夠這麼理解這個函數的設計思路:16.2講的是socket;16.3.1 16.3.2 16.3.3講的是address的問題;16.4.4就把socket和address給串起來了。
這個bind的做用是讓socket的操做與sockaddr指向的地址上。並且書上還說這個bind操做在server端是必須的,可是在client端不是必須的。這兩點第一次接觸理解起來比較抽象,個人具體理解就是:
(1)socket就至關於一個接線員(它具有接線的能力,負責哪一個線路都OK),sockaddr至關於綜合了總機號(ip)和分機號(port)的信息;而bind作的事情就是個讓「接線員」把它後面提供的服務接到這個「總機號+分機號」
(2)因爲server端要持續提供service,而且每次必定要保證讓client可以經過固定的ip+port找到服務,所以server端的socket必定要與ip+port綁定,意思就是告訴server端其餘的服務「這個IP的這個端口被我佔用了」。
(3)輪到client端,是不須要bind這個操做的。這就至關於一個客戶在本地郵電局,而且知道撥通哪一個總機號和分機號就能接通異地郵電局的特定的服務;所以,他只須要找到一個接線員(client端socket)而且告訴接線員對方的總機(server端的ip)號和分機號(server端的port);這個接線員(client端socket)就能夠在自家郵電局選一個能跟對方(server端)撥通號碼的總機(client端ip)號和分機號(client端port)便可,不用非得指定必定是是自家哪一個總機號(好比client端有多個網卡,多個ip,找一個能用的就行)和分機號(有多個port可用,選一個能用的就OK了)。
上述整個過程當中,客戶並不須要知道接線員是經過本地郵電局的哪一個總機號+分機號去接通對方郵電局的服務的,只知道能接通就能夠了。若是實在想知道,還能夠經過getsockname函數來了解。
16.4 Connection Establishment
若是是connection-oriented network service,須要在server和client之間創建connection的關係。
1. 對於client端來講,創建聯繫的方法就是調用connect函數。具體以下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t len)
這裏的sockfd是client端創建的socket;addr是須要鏈接的server端的sockaddr信息。
2. 對於server端來講,創建聯繫的方法就是調用listen和accept函數。具體以下:
(1)listen函數對應的是client端server函數的請求,標明server端願意接收與sockfd相關的connect請求
int listen(int sockfd, int backlog)
(2)accept函數有些地方須要注意一下,先看函數原型:
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len)
sockfd :server端提供具體service的socket file descriptor
addr & len : 這兩個參數屬於value-resulted argument參數;若是不爲NULL,則函數執行以後,會指向發起鏈接請求的client端的address信息;若是爲NULL,則沒有做用。
還有一個重要的地方,accept函數的返回值也是一個socket file descriptor;這個socket fd的做用就是專門用來處理與發送請求的這個client的通訊問題。那麼,這個返回的socket fd與傳入參數的sockfd是什麼關係呢?這個部分我是看了後面的代碼纔看懂的。這裏爲了方便闡述,姑且把傳入參數的sockfd叫作old fd,返回的sockfd叫作new fd:
a. old fd負責的事情比較專注,專門負責接收client發來的請求,重點在「接收」(至關於酒店的前臺經理,來一個客人接待一下)
b. new fd負責的是具體提供服務的後續操做,重點在「服務」。(至關於酒店的服務員,把前臺經理接收的客人帶到具體的房間)
c. old fd每次負責接待完就OK了,剩下的服務的具體事情就交給new fd去作(前臺經理接收一個客人以後,立刻喊過來一個小弟服務員;而後經理繼續幹前臺接待的事情,服務員小弟就去具體接待客人)這種結構就是典型的fork編程模型,後面的具體代碼會看到。
16.5 Data Transfer
前面的工做都作好了以後,就能夠傳輸數據了。
發送數據。
這裏重點用了兩個函數:
1. send函數
ssize_t send(int sockfd, const void *buf, size_t nbyte, int flags)
這個函數能使用的前提是server和client之間必須已經創建好鏈接。
2. sendto函數
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct *destaddr, socklen_t destlen)
這個函數在使用的時候能夠不用管server和client之間是否創建好鏈接。
上面這兩個函數,執行成功並返回了,並不意味這數據已經「送到了」,而僅僅是「送出去了」。
接收數據。
與發送對應,這裏也用了兩個函數:
recv函數 & recvfrom函數
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags)
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen)
若是不關心數據的sender是誰,能夠用recv;若是須要知道數據sender的信息,能夠用recvfrom,sender的信息就存放在addr中了。
上面講了那麼多函數,用unix network programming volume 1 chapter 4中的一張圖來作一下總結:
有了上面這個圖,頂過不少文字解釋,各個函數的調用順序以及關係都比較清晰了。
16.7 server & client 通訊綜合例子
把這一章前面全部的內容綜合到下面的例子中。
兩個例子實現的功能都是同樣的,就是client發起請求,獲取server端uptime命令的執行結果,而且輸出到client端的終端上;不一樣的在於一個是基於connetion的通訊,一個是connectionless的通訊。其中關於connection的通訊還有兩種不一樣的實現方法。這個例子雖然實現的功能簡單,可是包含的細節還挺多的。第一次接觸這樣的程序,只能摸着石頭過河,先給一個本身理解回頭有問題再修正。
下面兩個例子執行成功的前提是,須要在實驗的機器上修改/etc/services文件,在後面加上兩行,以下:
意思就是把咱們以後要用到的service name及其端口號肯定,這樣就方便不少。這一步須要root權限,若是麼有root權限還能夠想其餘辦法。
例子一,基於connection的server & client通訊
上代碼以前,先總結一下server端和client端的執行流程:
1. server端作的事情就是:
(1)得到hostname
(2)使本身變成一個daemon process (用到chapter 13 daemon process的知識)
(3)獲取全部這個'ruptime'服務的可用address(ip+port),並挑一個可用的address
(4)用這個地址以此執行socket bind listen操做,並返回可用的socket file descriptor(這個sockfd至關於前面提到的大堂經理)
(5)基於(4)返回的sockfd,監聽client發來的請求;一旦收到了請求,則執行accept的操做,生成一個能夠用的socket file descriptor(這個sockefd至關於前面提到的服務員小弟)
(6)接着就是想辦在讓uptime command執行,而且講結果send回client端。有兩種達到這個目的的方法:一種是利用popen函數實現;另外一種是利用裸寫fork實現。兩種實現方法分別對應了server.c中的serve1函數和serve2函數。
(7)處理完客戶端的此次請求,繼續等着其餘客戶端發送請求
其中(4)被單獨封裝成一個initserver.c文件,其他的部分都在一個server.c文件中。具體代碼以下:
initserver.c文件:
1 #include "cs.h" 2 #include <netdb.h> 3 #include <errno.h> 4 #include <sys/socket.h> 5 6 int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen) 7 { 8 int fd; 9 int err = 0; 10 int reuse = 1; 11 12 if ((fd = socket(addr->sa_family, type, 0))<0) { /*根據family和type 讓系統選擇與這倆搭配的protocal*/ 13 return -1; 14 } 15 errno = err; 16 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))<0) { 17 goto errout; 18 } 19 if (bind(fd, addr, alen)<0) { /*將server相關的fd 與特定port綁定*/ 20 printf("errno:%d %s\n", gai_strerror(errno)); 21 goto errout; 22 } 23 if (type==SOCK_STREAM || type == SOCK_SEQPACKET) { /*若是是TCP的 再讓server啓動監聽 並限定監聽隊列的最大長度是qlen*/ 24 if (listen(fd, qlen)<0) { 25 goto errout; 26 } 27 } 28 printf("1.1\n"); 29 return fd; 30 errout: 31 err = errno; 32 close(fd); 33 errno = err; 34 return -1; 35 }
server.c文件:
1 #include "cs.h" 2 #include <netdb.h> 3 #include <errno.h> 4 #include <syslog.h> 5 #include <sys/socket.h> 6 7 #define BUFLEN 128 8 #define QLEN 10 /*設定server的監聽隊列長度*/ 9 10 #ifndef HOST_NAME_MAX /*若是沒有定義HOST_NAME_MAX 則給出一個默認值*/ 11 #define HOST_NAME_MAX 256 12 #endif 13 14 //extern int initserver(int, const struct sockaddr *, socklen_t, int); 15 16 int serve1(int sockfd) 17 { 18 int clfd; 19 FILE *fp; 20 char buf[BUFLEN]; 21 printf("3\n"); 22 set_cloexec(sockfd); 23 printf("4\n"); 24 for(;;) 25 { 26 printf("5\n"); 27 if ((clfd = accept(sockfd, NULL, NULL))<0) { 28 syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno)); 29 exit(1); 30 } 31 /*popen中用了pipe+fork+exec的編程模型 產生一個child process專門用來執行uptime命令 32 * 而clfd是用來與client關聯的socket 這個是不須要繼承到child process中的 因此要經過設定flag位來控制*/ 33 set_cloexec(clfd); 34 if ((fp = popen("/usr/bin/uptime","r"))==NULL) { /*架設與uptime關聯的管道 'r'表示從uptime中讀*/ 35 sprintf(buf, "error: %s\n", strerror(errno)); 36 send(clfd, buf, strlen(buf), 0); 37 } 38 else { 39 while (fgets(buf, BUFLEN, fp)!=NULL) { /*不斷經過pipe從uptime中讀數據*/ 40 send(clfd, buf, strlen(buf), 0); /*讀到的數據向client發送數據*/ 41 } 42 pclose(fp); /*pipe中的數據讀完了*/ 43 } 44 close(clfd); /*此次處理client的請求完畢 關閉與client鏈接的socket*/ 45 } 46 } 47 48 int serve2(int sockfd) 49 { 50 int clfd, status; 51 pid_t pid; 52 53 set_cloexec(sockfd); 54 for(;;) 55 { 56 if ((clfd = accept(sockfd, NULL, NULL))<0) { 57 syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno)); 58 exit(1); 59 }; 60 if ((pid = fork())<0) { 61 syslog(LOG_ERR, "ruptimed: fork error: %s", strerror(errno)); 62 exit(1); 63 } 64 else if (pid==0) { 65 /*1. 讓server end的stdout和stderr都輸出到client end 66 *2. stdin已經連着了/dev/null 不會有其餘的輸出影響server end*/ 67 if (dup2(clfd, STDOUT_FILENO)!=STDOUT_FILENO || 68 dup2(clfd, STDERR_FILENO)!=STDERR_FILENO) { 69 syslog(LOG_ERR, "ruptimed: unexpected error"); 70 exit(1); 71 } 72 close(clfd); 73 execl("/usr/bin/uptime", "uptime", (char *)0); 74 syslog(LOG_ERR, "ruptimed: unexpected return from exec: %s", strerror(errno)); 75 } 76 else { 77 close(clfd); 78 waitpid(pid, &status, 0); 79 } 80 81 } 82 } 83 84 int main(int argc, char *argv[]) 85 { 86 struct addrinfo *ailist, *aip; 87 struct addrinfo hint; 88 int sockfd, err, n; 89 char *host; 90 91 if (argc != 1) { 92 err_quit("usage: ruptimed"); 93 } 94 if ((n = sysconf(_SC_HOST_NAME_MAX))<0) { /*host name的長度限制*/ 95 n = HOST_NAME_MAX; 96 } 97 if ((host = malloc(n))==NULL) { /*分配一個足夠長的存放host name的字符串*/ 98 err_sys("malloc error"); 99 } 100 if (gethostname(host, n)<0) { /*得到host name*/ 101 err_sys("gethostname error"); 102 } 103 /*使得當前的執行的這個process成爲daemon 104 * 傳入的參數cmd有兩個目的: 105 * 1. 這個daemon出錯的時候 知道是哪一個cmd出錯了 106 * 2. daemon出錯的時候 知道把syslog往哪裏引*/ 107 daemonize("ruptimed"); 108 host = "localhost"; 109 memset(&hint, 0, sizeof(hint)); 110 hint.ai_flags = AI_CANONNAME; 111 hint.ai_family = 0; 112 hint.ai_socktype = 0; 113 hint.ai_addrlen = 0; 114 hint.ai_canonname = NULL; 115 hint.ai_addr = NULL; 116 hint.ai_next = NULL; 117 118 if ((err = getaddrinfo(host, "ruptime", &hint, &ailist))!=0) { /*ruptime 人爲設定一個port*/ 119 syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err)); 120 exit(1); 121 } 122 printf("1\n"); 123 for ( aip=ailist; aip!=NULL; aip=aip->ai_next) 124 { 125 printf("2\n"); 126 if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN))>=0) { 127 serve2(sockfd); /*只對第一個返回的addrinfo執行serve操做*/ 128 exit(0); 129 } 130 else { 131 printf("sockfd:%d\n",sockfd); 132 } 133 } 134 exit(1); 135 }
這裏line 33和line 53用到的是set_cloexec函數也單獨封裝到一個setfd.c的文件中,具體以下:
#include "cs.h" #include <fcntl.h> int set_cloexec(int fd) { int val; if ((val = fcntl(fd, F_GETFD, 0)) < 0) return(-1); val |= FD_CLOEXEC; /* enable close-on-exec */ return(fcntl(fd, F_SETFD, val)); }
另外還有line 107的daemonize函數也被單獨封裝在一個daemonize.c的文件中,具體以下:
1 #include "cs.h" 2 #include <syslog.h> 3 #include <signal.h> 4 #include <fcntl.h> 5 #include <sys/resource.h> 6 7 void 8 daemonize(const char *cmd) 9 { 10 int i, fd0, fd1, fd2; 11 pid_t pid; 12 struct rlimit rl; 13 struct sigaction sa; 14 15 /* 16 * Clear file creation mask. 17 */ 18 umask(0); 19 20 /* 21 * Get maximum number of file descriptors. 22 */ 23 if (getrlimit(RLIMIT_NOFILE, &rl) < 0) 24 err_quit("%s: can't get file limit", cmd); 25 26 /* 27 * Become a session leader to lose controlling TTY. 28 */ 29 if ((pid = fork()) < 0) 30 err_quit("%s: can't fork", cmd); 31 else if (pid != 0) /* parent */ 32 exit(0); 33 setsid(); 34 35 /* 36 * Ensure future opens won't allocate controlling TTYs. 37 */ 38 sa.sa_handler = SIG_IGN; 39 sigemptyset(&sa.sa_mask); 40 sa.sa_flags = 0; 41 if (sigaction(SIGHUP, &sa, NULL) < 0) 42 err_quit("%s: can't ignore SIGHUP", cmd); 43 if ((pid = fork()) < 0) 44 err_quit("%s: can't fork", cmd); 45 else if (pid != 0) /* parent */ 46 exit(0); 47 48 /* 49 * Change the current working directory to the root so 50 * we won't prevent file systems from being unmounted. 51 */ 52 if (chdir("/") < 0) 53 err_quit("%s: can't change directory to /", cmd); 54 55 /* 56 * Close all open file descriptors. 57 */ 58 if (rl.rlim_max == RLIM_INFINITY) 59 rl.rlim_max = 1024; 60 for (i = 0; i < rl.rlim_max; i++) 61 close(i); 62 63 /* 64 * Attach file descriptors 0, 1, and 2 to /dev/null. 65 */ 66 fd0 = open("/dev/null", O_RDWR); 67 fd1 = dup(0); 68 fd2 = dup(0); 69 70 /* 71 * Initialize the log file. 72 */ 73 openlog(cmd, LOG_CONS, LOG_DAEMON); 74 if (fd0 != 0 || fd1 != 1 || fd2 != 2) { 75 syslog(LOG_ERR, "unexpected file descriptors %d %d %d", 76 fd0, fd1, fd2); 77 exit(1); 78 } 79 }
上面的代碼設計到的一些技術細節以下:
(1)守護進程daemon process。server.c中的line 107爲何要調用daemonize讓當前進程變成守護進程?這個問題須要看過apue chapter13 daemon process才能徹底理解(詳情可見以前的學習筆記http://www.cnblogs.com/xbf9xbf/p/4923491.html)。
a. 什麼是daemon process,爲啥要給server端變成一個daemon?簡單說,server端這個進程至關於一個純粹的服務進程,不想受到任何terminal的影響(不會由於終端斷了這個進程或者結束會話就掛了);這個進程的stdin stdout stderr都指向/dev/null這個黑洞(具體能夠去google到底什麼是/dev/null),不會主動受到stdin stdout stderr的影響;只要機器不斷電,這個進程不被終止,就會一直在後臺運行。綜合以上幾點,這個進程真的是很是沉默的躲在後臺,像一個幽靈(daemon)同樣在運行着。
b. 要想實現daemon process須要一套流程,即daemonize.c文件中的這套流程。太多的細節不解釋了,最主要的是daemonize.c中的line 67~68,這個daemon process的file descriptor中的0,1,2都被佔用了(由於都指向/dev/null了)。若是對dup這樣的技術細節還要深究,能夠回顧apue chapter 3 FILE I/O的內容(詳情可見以前的學習筆記http://www.cnblogs.com/xbf9xbf/p/4930496.html)。
(2)管道通訊pipe。server.c中的line 33~44利用的是pipe的方式實現server端內部的IPC的。什麼是pipe能夠參見apue chapter15 IPC的pipe內容(詳情可見以前的學習筆記http://www.cnblogs.com/xbf9xbf/p/5018177.html)。簡單說,這部分代碼就是在另開一個child process,並在新開的child process中執行/usr/bin/uptime這個命令,而後再利用pipe把uptime執行完成的結果讀到parent process的buffer中,而後再send回client中。
a. 爲何要set_cloexec(clfd)?由於後面調用的popen函數,其內部實現機制用了fork+pipe+exec的機制。一旦有了fork和exec,就涉及到了parent process的memory layout複製到child process memory layout的問題。即,child process也能夠有一個clfd,而且child process的clfd也與client關聯的那個socket。以下圖:
咱們知道,child process的做用就是執行一個uptime command,是不須要跟client發生什麼關聯的。因此,能夠經過set_cloexec這個函數來給clfd的flag置位。目的就是在fork+exec以後,將clfd給close了,這裏的close並非直接把sockfd給刪除了,而是讓其斷開與某個client的聯繫。爲了加深印象,截取了apue chapter 8的一段原文:
(3)另外一種處理client請求的方式:fork+dup2。除了serve1函數中用pipe處理client請求的辦法,在serve2中還介紹了一種fork+dup2的處理方法。
a. 先對比一下這serve1和serve2兩種處理的方式:
b. 分析一下serve2中fork+dup2方法的特色:減小了child到parent的數據傳到中間過程,直接讓child與client交換數據。書上還提到,若是child的執行時間太長,用serve2這種方式可能對效率產生影響。
c. 關於dup2的技術細節。回顧apue書上P543的popen函數實現(見以前chapter15 IPC的學習筆記http://www.cnblogs.com/xbf9xbf/p/5018177.html),凡是執行dup2(A, B)以前,都要檢查A是否等於B。可是serve2中執行dup2函數卻沒有作這樣的檢查?不用檢查的緣由是由於clfd和STDOUT_FILENO以及STDERR_FILENO不可能相等。
(c1)在執行serve2以前已經執行了daemonize函數,daemonize函數的line66~78已經保證了0、一、2三個file descriptor都已經被占上了。所以child的file descriptor的0、一、2也都必定被占上了。
(c2)所以,再執行serve2的時候,0、一、2三個file descriptor都占上了,執行clfd = accept(..., ..., ...)的時候,系統要給clfd分配一個最小的非負可用的int值。顯然0、一、2都已經被占上了,所以clfd至少從3開始取值,天然也就不可能和STDOUT(數值爲1)、STDERR_FILENO(數值爲2)衝突了。
這種技術細節要想理解,須要對daemonize的機制很是熟悉才能夠。
2. client端作的事情就是:
(1)獲取server端的IP和服務對應的port號(這裏其實有點兒偷懶,由於server跟client是一臺機器)
(2)用這個address執行socket和connect操做(若是一次不成功,就retry嘗試connect)
(3)在connect成功後,用recv接收從server端發來的數據,並輸出到終端
client.c文件以下:
1 #include "cs.h" 2 #include <netdb.h> 3 #include <errno.h> 4 #include <sys/socket.h> 5 6 7 #define BUFLEN 128 8 9 //extern int connect_retry(int, int, int, const struct sockaddr *, socklen_t); 10 11 void print_uptime(int sockfd) 12 { 13 int n; 14 char buf[BUFLEN]; 15 while ((n=recv(sockfd, buf, BUFLEN, 0))>0) { /*從socket接收數據 直到所有收完爲止*/ 16 write(STDOUT_FILENO, buf, n); 17 } 18 if (n<0) { 19 err_sys("recv error"); 20 } 21 } 22 23 int main(int argc, char *argv[]) 24 { 25 struct addrinfo *ailist, *aip; 26 struct addrinfo hint; 27 int sockfd, err; 28 29 if (argc != 2) { 30 err_quit("usage: ruptime hostname"); 31 } 32 memset(&hint, 0, sizeof(hint)); 33 hint.ai_socktype = SOCK_STREAM; 34 hint.ai_canonname = NULL; 35 hint.ai_addr = NULL; 36 hint.ai_next = NULL; 37 38 if ((err = getaddrinfo(argv[1],"ruptime", &hint, &ailist))!=0) { 39 err_quit("getaddrinfo error: %s", gai_strerror(err)); 40 } 41 for (aip=ailist; aip!=NULL; aip = aip->ai_next) 42 { 43 if ((sockfd = connect_retry(aip->ai_family, SOCK_STREAM,0,aip->ai_addr,aip->ai_addrlen))<0) { 44 err = errno; 45 } 46 else { 47 print_uptime(sockfd); 48 exit(0); 49 } 50 } 51 err_exit(err, "can't connect to %s", argv[1]); 52 return -1; 53 }
connect_retry.c文件具體以下:
1 #include "cs.h" 2 #include <sys/socket.h> 3 4 #define MAXSLEEP 128 5 6 int connect_retry(int domain, int type, int protocol, const struct sockaddr *addr, socklen_t alen) 7 { 8 int numsec, fd; 9 for (numsec=1; numsec<=MAXSLEEP; numsec<<=1) 10 { 11 if ((fd = socket(domain, type, protocol))<0) { 12 return -1; 13 } 14 if (connect(fd, addr, alen)==0) { 15 return fd; 16 } 17 close(fd); 18 if (numsec<=MAXSLEEP/2) { 19 sleep(numsec); 20 } 21 } 22 return -1; 23 }
client端的代碼細節就是在connect_retry函數中,一旦一次鏈接不成功,並不能立刻第二次鏈接:一是從新搞一個socket,二是等待一段時間再去connect。具體的原理參見apue書上P607。
上面講述完了基於connection的client server通訊的基本流程。
在server端,執行代碼後能夠看到多了一個守護進程(parent pid=1, tty=? ,pgid=sig典型的daemon process):
在client端,每執行一次代碼,至關於向server端發送一個請求;接收到server返回的數據後,將結果顯示到terminal上:
例子二,基於connectionless的server & client通訊
對比connection的通訊,connectionless通訊少了connect和accept環節。
server端主要是server-dg.c代碼:
1 #include "cs.h" 2 #include <netdb.h> 3 #include <errno.h> 4 #include <syslog.h> 5 #include <sys/wait.h> 6 7 #define BUFLEN 128 8 #define MAXADDRLEN 256 9 10 #ifndef HOST_NAME_MAX 11 #define HOST_NAME_MAX 256 12 #endif 13 14 void serve(int sockfd) 15 { 16 int n; 17 socklen_t alen; 18 FILE *fp; 19 char buf[BUFLEN]; 20 char abuf[MAXADDRLEN]; 21 struct sockaddr *addr = (struct sockaddr *)abuf; 22 23 set_cloexec(sockfd); 24 for(;;) 25 { 26 alen = MAXADDRLEN; 27 /*1. 阻塞 等着client向這個地方發送數據 28 *2. 這裏receive了多少數據不是關鍵 關鍵是得到client的addr信息*/ 29 if ((n=recvfrom(sockfd,buf, BUFLEN, 0, addr, &alen))<0) { 30 syslog(LOG_ERR, "ruptimed: recvfrom error: %s", strerror(errno)); 31 exit(1); 32 } 33 if ((fp = popen("/usr/bin/uptime","r"))==NULL) { 34 /*出錯了也知道往哪一個client發送error信息*/ 35 sprintf(buf, "error: %s\n", strerror(errno)); 36 sendto(sockfd, buf, strlen(buf), 0, addr, alen); 37 } 38 else { 39 if (fgets(buf, BUFLEN, fp)!=NULL) { 40 sendto(sockfd, buf, strlen(buf), 0, addr, alen); 41 } 42 pclose(fp); 43 } 44 } 45 } 46 47 int main(int argc, char *argv[]) 48 { 49 struct addrinfo *ailist, *aip; 50 struct addrinfo hint; 51 int sockfd, err, n; 52 char *host; 53 54 if (argc != 1) { 55 err_quit("usage: ruptimed"); 56 } 57 if ((n=sysconf(_SC_HOST_NAME_MAX))<0) { 58 n = HOST_NAME_MAX; 59 } 60 if ((host=malloc(n))==NULL) { 61 err_sys("malloc error"); 62 } 63 if (gethostname(host,n)<0) { 64 err_sys("gethostname error"); 65 } 66 host = "localhost"; 67 daemonize("ruptimed"); 68 memset(&hint, 0, sizeof(hint)); 69 hint.ai_flags = AI_CANONNAME; 70 hint.ai_socktype = SOCK_DGRAM; 71 hint.ai_canonname = NULL; 72 hint.ai_addr = NULL; 73 hint.ai_next = NULL; 74 if ((err = getaddrinfo(host, "ruptime", &hint, &ailist))!=0) { 75 syslog(LOG_ERR, "ruptimed: getaddrinfo error: %s", gai_strerror(err)); 76 exit(1); 77 } 78 for (aip = ailist; aip!=NULL; aip=aip->ai_next) 79 { 80 if ((sockfd = initserver(SOCK_DGRAM, aip->ai_addr, aip->ai_addrlen,0))>=0) { 81 serve(sockfd); 82 exit(0); 83 } 84 } 85 exit(1); 86 }
上述server端的代碼,調用initserver.c文件中的函數,執行了listen操做以後,就直接能夠執行recvfrom接收從client端發來的數據。(固然,這裏是一個最簡單的狀況,只要n>0即證實收到client端的數據,就知道client端發送了請求,要求得到server端的uptime command命令執行結果)。
client端的代碼client-dg.c以下:
1 #include "cs.h" 2 #include <netdb.h> 3 #include <errno.h> 4 #include <sys/socket.h> 5 6 #define BUFLEN 128 7 #define TIMEOUT 20 8 9 10 void sigalrm(int signo){} 11 12 void print_uptime(int sockfd, struct addrinfo *aip) 13 { 14 int n; 15 char buf[BUFLEN]; 16 17 buf[0] = 0; 18 if (sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen)<0) { 19 err_sys("sendto error"); 20 } 21 alarm(TIMEOUT); 22 if ((n=recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL))<0) { 23 if (errno!=EINTR) { 24 alarm(0); 25 err_sys("recv error"); 26 } 27 } 28 alarm(0); 29 write(STDOUT_FILENO, buf, n); 30 } 31 32 int main(int argc, char *argv[]) 33 { 34 struct addrinfo *ailist, *aip; 35 struct addrinfo hint; 36 int sockfd, err; 37 struct sigaction sa; 38 39 if (argc != 2) { 40 err_quit("usage: ruptime hostname"); 41 } 42 sa.sa_handler = sigalrm; 43 sa.sa_flags = 0; 44 sigemptyset(&sa.sa_mask); 45 if (sigaction(SIGALRM, &sa, NULL)<0) { 46 err_sys("sigaction error"); 47 } 48 memset(&hint, 0, sizeof(hint)); 49 hint.ai_socktype = SOCK_DGRAM; 50 hint.ai_canonname = NULL; 51 hint.ai_addr = NULL; 52 hint.ai_next = NULL; 53 if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist))!=0) { 54 err_quit("getaddrinfo error: %s", gai_strerror(err)); 55 } 56 for (aip = ailist; aip != NULL; aip = aip->ai_next) 57 { 58 if ((sockfd = socket(aip->ai_family, SOCK_DGRAM, 0))<0) { 59 err = errno; 60 } 61 else { 62 print_uptime(sockfd, aip); 63 exit(0); 64 } 65 } 66 67 fprintf(stderr, "can't contact %s: %s\n", argv[1], strerror(err)); 68 exit(1); 69 }
能夠看到,client端的代碼,只產生了socket,而後就直接執行sendto操做,再執行recvfrom操做了。這中間並無connect以及retry的過程。
這種connectionless的方式有兩點須要注意:
第一,這種connectless的通訊方式對應的必定不能是TCP,能夠是UDP。
第二,在設定hint參數的時候應該是SOCK_DGRAM而不是SOCK_STREAM。
第三,在/etc/services中必定要給ruptime這個服務註冊一個udp協議通訊版本。
最後,上兩個其他的文件cs.h文件,以及error.c錯誤處理函數文件,這兩個文件都是後面寫makefile用到的。
error.c文件以下:
1 #include "cs.h" 2 #include <string.h> 3 #include <errno.h> /* for definition of errno */ 4 #include <stdarg.h> /* ISO C variable aruments */ 5 6 #define MAXLINE 4096 7 8 static void err_doit(int, int, const char *, va_list); 9 10 /* 11 * Nonfatal error related to a system call. 12 * Print a message and return. 13 */ 14 void 15 err_ret(const char *fmt, ...) 16 { 17 va_list ap; 18 19 va_start(ap, fmt); 20 err_doit(1, errno, fmt, ap); 21 va_end(ap); 22 } 23 24 /* 25 * Fatal error related to a system call. 26 * Print a message and terminate. 27 */ 28 void 29 err_sys(const char *fmt, ...) 30 { 31 va_list ap; 32 33 va_start(ap, fmt); 34 err_doit(1, errno, fmt, ap); 35 va_end(ap); 36 exit(1); 37 } 38 39 /* 40 * Nonfatal error unrelated to a system call. 41 * Error code passed as explict parameter. 42 * Print a message and return. 43 */ 44 void 45 err_cont(int error, const char *fmt, ...) 46 { 47 va_list ap; 48 49 va_start(ap, fmt); 50 err_doit(1, error, fmt, ap); 51 va_end(ap); 52 } 53 54 /* 55 * Fatal error unrelated to a system call. 56 * Error code passed as explict parameter. 57 * Print a message and terminate. 58 */ 59 void 60 err_exit(int error, const char *fmt, ...) 61 { 62 va_list ap; 63 64 va_start(ap, fmt); 65 err_doit(1, error, fmt, ap); 66 va_end(ap); 67 exit(1); 68 } 69 70 /* 71 * Fatal error related to a system call. 72 * Print a message, dump core, and terminate. 73 */ 74 void 75 err_dump(const char *fmt, ...) 76 { 77 va_list ap; 78 79 va_start(ap, fmt); 80 err_doit(1, errno, fmt, ap); 81 va_end(ap); 82 abort(); /* dump core and terminate */ 83 exit(1); /* shouldn't get here */ 84 } 85 86 /* 87 * Nonfatal error unrelated to a system call. 88 * Print a message and return. 89 */ 90 void 91 err_msg(const char *fmt, ...) 92 { 93 va_list ap; 94 95 va_start(ap, fmt); 96 err_doit(0, 0, fmt, ap); 97 va_end(ap); 98 } 99 100 /* 101 * Fatal error unrelated to a system call. 102 * Print a message and terminate. 103 */ 104 void 105 err_quit(const char *fmt, ...) 106 { 107 va_list ap; 108 109 va_start(ap, fmt); 110 err_doit(0, 0, fmt, ap); 111 va_end(ap); 112 exit(1); 113 } 114 115 /* 116 * Print a message and return to caller. 117 * Caller specifies "errnoflag". 118 */ 119 static void 120 err_doit(int errnoflag, int error, const char *fmt, va_list ap) 121 { 122 char buf[MAXLINE]; 123 124 vsnprintf(buf, MAXLINE-1, fmt, ap); 125 if (errnoflag) 126 snprintf(buf+strlen(buf), MAXLINE-strlen(buf)-1, ": %s", 127 strerror(error)); 128 strcat(buf, "\n"); 129 fflush(stdout); /* in case stdout and stderr are the same */ 130 fputs(buf, stderr); 131 fflush(NULL); /* flushes all stdio output streams */ 132 }
cs.h文件以下:
#include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <string.h> #include <sys/socket.h> int connect_retry(int, int, int, const struct sockaddr*, socklen_t); void daemonize(const char *); void err_ret(const char *,...); void err_sys(const char *,...); void err_exit(int, const char *,...); void err_quit(const char *,...); int initserver(int, const struct sockaddr*, socklen_t, int); int set_cloexec(int);
16.8 makefile學習
1. 學習驅動力
(1)以前一直得過且過,不想學寫makefile,都是把各類函數丟進一個c文件就直接編譯連接運行了。
(2)可是上面的代碼各個函數實在太多了:像initserver.c屬於4個程序公用的庫函數,若是不單獨提煉出來,別說維護了,調試4份代碼都很麻煩。因而下決心學習一下makefile。
2. 學習的過程以下
(1)看教程。比較幸運,發現了這個很是好的針對makefile的wiki:http://wiki.ubuntu.org.cn/跟我一塊兒寫Makefile。對我這樣的初學者來講,這個教程深刻淺出,有驅動有例子,看這一個入門足以。大概一天時間掃了一遍,100頁的教程。原本想多寫一些makefile的東西,後來仍是放棄了。一則時間不太夠,二則上面這個wiki寫的已經很是好了,看一遍再動手足以。
(2)嘗試寫一個簡單的例子。這裏我入門的就是給lib文件夾下(包括daemonize.c,initserver.c,error.c,setfd.c,connect_retry.c)寫了一個makefile。在寫的過程當中回頭再看教程,再加深一下理解,完善第一個makefile。
(3)嘗試稍微複雜一些的例子。這裏我就是將server client通訊的全部代碼都組織成一個工程文件夾中:具體包括include文件夾(只有cs.h),lib文件夾(包括上面提到的5個.c文件),以及根目錄下的4個文件(server.c client.c server-dg.c client-dg.c)。爲何要這麼設計結構,由於apue書上給的源碼就是這麼設計的文件夾的,我若是也這麼設計文件夾結構,就能夠學習apue做者的makefile的寫法。這個階段屬於提升階段,光參考教程已經不夠了,必須參照一些高手的工做,模仿並體會。
3. makefile的好處
(1)方便。寫好makefile以後,編譯連接自動執行,只要在工程跟目錄下一個make命令就OK了。
(2)高效。makefile會自動檢測工程中哪一個文件更新了,並只從新編譯連接與更新過的那個文件相關的其餘文件,沒受影響的不用從新編譯連接。
(3)便於維護。若是makefile設計的好,工程中新增長一個庫函數之類的,只須要在makefile裏面作少許的修改,整個工程其他的部分不須要多大改動就能夠繼續運行
4. 成果
最後把本身學習以後寫的makefile成果記錄一下。首先看一下工程的文件夾結構:
工程文件夾是client-server文件夾,其中包含lib和include兩個子文件夾;各個文件夾的內容如上所示。
其中在lib目錄下有一個makefile文件,這個makefile只管庫函數的編譯。最終的目標是將全部庫函數都編譯,而且封裝到libcs.a的庫文件中。
這個makefile的依賴關係比較簡單,其內容以下所示(直接截圖是vim彩色的,效果比直接文字黑白的看得清):
在工程根目錄client-server文件夾下也有一個makefile,這個makefile是整個工程的總的makefile。即負責調用lib下的makefile編譯庫函數,並且負責檢查工程中各個代碼的依賴關係。其具體內容以下:
上面這個makefile寫的過程還遇到寫小問題,辦法就是參照apue做者在書上源代碼中寫的makefile,模仿學習,並參照教材體會。這個makefile能實現基本的makefile的功能,可是設計上確定還有不少能夠改進的地方,留着之後再改。
最後還有一個文件,至關因而makefile的include文件,Make.defines.linux,內容以下:
這個文件內容很簡單,就是記錄一些命令。其實仍是效仿apue的做者寫的,功能就是若是不一樣操做系統,這些參數可能會有所調整。
最後的最後再曬一下make的執行結果:
緊接着,若是我只對server.c文件作一些修改(加回車符),則再在根目錄client-server下執行make效果以下:
能夠看到,對其餘文件沒有從新編譯,只對server.c及其相關的內容從新編譯連接。
若是,我對lib/initserver.c中作些修改(仍是加一個回車),則再在根目錄client-server下執行make效果以下:
能夠看到,首先lib中只有initserver.c文件從新編譯了,而且libcs.a庫文件從新打包了。又由於makefile中設定了根目錄下四個程序與libcs.a的依賴關係,所以四個文件的連接過程所有從新執行了。可是,因爲四個程序文件自己沒有改動,因此四個文件的編譯過程並麼有從新執行。
經過上面的一些展現,能夠感覺寫好makefile是很重要的,優秀的makefile設計能夠給工程編譯維護過程帶來巨大的方便。
以上。