??作者:一只大喵咪1201
??專欄:《網(wǎng)絡(luò)》
??格言:你只管努力,剩下的交給時間!
上篇文章中本喵介紹了UDP網(wǎng)絡(luò)通信的socket代碼,今天介紹TCP網(wǎng)絡(luò)通信的socket代碼。
??TCP網(wǎng)絡(luò)編程
??服務(wù)端實現(xiàn)
和udp
的網(wǎng)絡(luò)通信一樣,tcp
通信也需要服務(wù)器指定端口號,IP地址同樣使用0.0.0.0
,以便客戶端所有對服務(wù)器的網(wǎng)絡(luò)請求都能收到。
class tcpServe
{
public:
// 構(gòu)造函數(shù)
tcpServe(const uint16_t &port)
: _port(port), _listensock(-1)
{}
// 服務(wù)器初始化
void initServer()
{}
// 服務(wù)器運行
void start()
{}
// 析構(gòu)函數(shù)
~tcpServe()
{}
private:
int _listensock; // 不是用來進行數(shù)據(jù)通信的,是用來監(jiān)聽鏈接到來的,獲取新連接
uint16_t _port;
};
服務(wù)器類的框架如上面代碼所示,包括一個文件描述符istensock
,還有一個端口號_port
,這里本喵沒有寫IP地址的成員變量,因為會給它一個缺省值,所以沒有必要,后面本喵會詳細(xì)講解。
下面就是將tcpServe
類中成員函數(shù)的具體邏輯寫出來了。
initServer()
首先是創(chuàng)建套接字,和udp
一樣,使用系統(tǒng)調(diào)用socket
,只是填的參數(shù)不一樣,第一個參數(shù)仍然是AF_INET
表示網(wǎng)絡(luò)間通信,第二個參數(shù)這里使用的是SOCK_STREAN
,表示面向字節(jié)流的,不再是面向用戶數(shù)據(jù)報了,第三個參數(shù)仍然是0。
創(chuàng)建套接字的結(jié)果使用日志函數(shù)logMessage()
來記錄日志。
然后就是將端口號和IP地址使用系統(tǒng)調(diào)用bind
和操作系統(tǒng)綁定,填充的結(jié)構(gòu)體仍然是struct sockaddr_in
,和之前的udp
一樣,只是在填充IP地址的時候,本喵這里使用了INADDR_ANY
,它的意義就是0.0.0.0
。
它在Linux中的定義如上圖所示,我們使用的0.0.0.0
轉(zhuǎn)換成uint16_t
以后就是0x00000000
。
在使用bind
進行綁定的時候,同樣需要將填充的結(jié)構(gòu)體強轉(zhuǎn)成struct sockaddr*
,綁定的結(jié)果使用日志來記錄。
將套接字設(shè)置為listen狀態(tài)
第三步就是將套接字設(shè)置為監(jiān)聽狀態(tài),使用系統(tǒng)調(diào)用listen
,如上圖所示。
系統(tǒng)調(diào)用listen
的作用是將套接字設(shè)置為監(jiān)聽狀態(tài),此時這個套接字和udp
中的套接字不一樣,它不用來通信,只用來檢測客戶端的連接請求的。
第一個參數(shù)sockfd
就是使用socket
創(chuàng)建套接字返回的文件描述符,第二個參數(shù)這里本喵暫時不做講解,等后面會詳細(xì)講解,這里隨便給一個值就可以,不要太大,本喵這里給的是5。
設(shè)置成功返回0,設(shè)置失敗返回-1,并且設(shè)置錯誤碼,同樣使用日志來記錄設(shè)置結(jié)果。
以上三步就是初始化initServe
中的內(nèi)容。
start()
第四步寫在start
函數(shù)中,如上圖所示,使用accept
來接收客戶端的連接請求,有點像udp
中的recvfrom
一樣,只是accept
是用來接收套接字的連接請求,而recvfrom
是接收套接字中的數(shù)據(jù)的。
accept
系統(tǒng)調(diào)用的參數(shù)和recvfrom
中的一樣,如上圖所示,accept
的作用就是接收來自套接字中的連接請求,也就是來自客戶端的連接請求。
返回值:
上面本喵說過,設(shè)置為listen
狀態(tài)的套接字不用了通信,只是用來接收客戶端的網(wǎng)絡(luò)請求,具體體現(xiàn)在accept
的返回值上。
此時第一步中創(chuàng)建的套接字就像是一個門童,使用accept
來接收客戶端的連接請求,如果有連接請求并且接收成功,那么會返回一個文件描述符fd
。
這里的文件描述符sock
和前面的_listensock
不是一個東西,_listensock
是我們創(chuàng)建的,是專門用來接收連接請求的,而accept
返回的sock
是操作系統(tǒng)在接收成功連接請求后新創(chuàng)建的套接字的文件描述符。
sock
指向的文件描述符是服務(wù)端專門用來和客戶端通信的,所以每有一個客戶端向服務(wù)器發(fā)起連接請求,客戶端接收成功夠都會創(chuàng)建一個套接字用來一對一的提供服務(wù)。
- _listensock:我們創(chuàng)建套接字返回的文件描述符,必須設(shè)置為
listen
狀態(tài),專門用來檢測客戶端的連接請求的。- sock:
accept
返回的文件描述符,是操作系統(tǒng)自動創(chuàng)建的套接字的文件描述符,該套接字專門用來和客戶端進行一對一網(wǎng)絡(luò)通信的。
如果accept
接收連接請求失敗,則返回-1,并且設(shè)置錯誤碼。這里的失敗并不是致命的,就像門童拉客一樣,拉客失敗也沒有什么,繼續(xù)進行下一次拉客就行。
所以accept
失敗也沒有什么,繼續(xù)接收下一個連接請求即可,所以本喵在代碼中,如果接收失敗,使用了continue
繼續(xù)接收連接請求。
服務(wù)器的服務(wù)函數(shù):
至此,進行tcp網(wǎng)絡(luò)通信的所有準(zhǔn)備工作已經(jīng)做完,接下來就是進行具體的服務(wù)了,也就是讀取客戶端發(fā)送來的數(shù)據(jù)并做相應(yīng)的處理了。
如上圖代碼所示,就是服務(wù)器指向的具體服務(wù)函數(shù)。
客戶端讀取客戶端發(fā)送來的數(shù)據(jù)時,是從accept
返回的文件描述符sock
指向的套接字中讀取數(shù)據(jù)的,因為這個套接字是專門用來服務(wù)客戶端的。
-
讀取數(shù)據(jù)時,使用的是
read
系統(tǒng)調(diào)用,和讀取普通文件一模一樣。
數(shù)據(jù)讀取成功后,做一些處理,先將讀取的數(shù)據(jù)打印一下,然后加一個serve[echo]
回顯,再給客戶端發(fā)送過去。
-
發(fā)送數(shù)據(jù)時,使用的是
write
系統(tǒng)調(diào)用,寫入的也是sock
指向的套接字,同樣與向普通文件中寫入數(shù)據(jù)一模一樣。
在讀取普通文件的時候,如果文件被讀完了,read
會返回0,表示文件的內(nèi)容被讀取完畢。
但是在使用read
讀取tcp套接字的時候,如果讀取到0,表示客戶端關(guān)閉了它的套接字,代表著客戶端不再進行網(wǎng)絡(luò)通信了,此時服務(wù)端就可以結(jié)束這次通信了,也就是將sock
指向的套接字關(guān)閉。
以上代碼都是在tcpServe.hpp
中寫的,都是tcpServe
類中的成員函數(shù)以及成員變量,它是對服務(wù)器的一個抽象表示。
tcpServe.cpp
同樣使用智能指針unique_ptr
來管理這個服務(wù)器,和udp
網(wǎng)絡(luò)通信一樣。
運行服務(wù)器以后,通過日志信息可以看到,套接字創(chuàng)建成功,bind
成功,并且成功將套接字設(shè)置成了listen
狀態(tài)。
之后服務(wù)器就阻塞不動了,此時它應(yīng)該執(zhí)行的是accept
函數(shù),也就是應(yīng)該正在接收來自客戶端的網(wǎng)絡(luò)請求。但是它此時阻塞不動了,這是因為目前沒有網(wǎng)絡(luò)連接請求到來。
accept
是阻塞執(zhí)行的,在沒有網(wǎng)絡(luò)連接請求的時候,會阻塞等待,直到客戶端的網(wǎng)絡(luò)連接請求到來。
使用指令netstat -nltp
可以查看當(dāng)前機器上的tcp網(wǎng)絡(luò)通信進程,如上圖所示。
綠色框中的就是我們前面寫的tcp服務(wù)器進程,其中IP地址是0.0.0.0
,如上圖藍(lán)色框中所示,端口號是8080,還可以看到進程的pid
是22099
,進程名字是./tcpserve
,說明我們寫的tcp服務(wù)器沒有問題,成功的運行了起來。
??客戶端實現(xiàn)
class tcpClient
{
public:
// 構(gòu)造函數(shù)
tcpClinet(const string& serverip, const uint16_t serverport)
:_serverip(serverip), _serverport(serverport), _sockfd(-1)
{}
// 客戶端初始化
void initClient()
{}
// 客戶端啟動
void start()
{}
// 析構(gòu)函數(shù)
~tcpClinet()
{}
private:
string _serverip;
uint16_t _serverport;
int _sockfd;
};
客戶端需要的成員變量和udp
中一樣,也是需要服務(wù)端的IP地址,服務(wù)端的端口號,以及客戶端自己創(chuàng)建套接字的文件描述符_sockfd
。成員函數(shù)和服務(wù)端類似,接下來要做的就是實現(xiàn)具體的邏輯。
initClient()
首先創(chuàng)建套接字,如果失敗直接打印錯誤信息,客戶端沒有使用日志來記錄。和udp
一樣,tcp
通信的客戶端也不需要顯式綁定,同樣也是由系統(tǒng)自動去綁定。
start()
第三步要發(fā)起連接,使用的系統(tǒng)調(diào)用是connect
。
用法和recfrom
以及accept
非常類似,第一個參數(shù)是創(chuàng)建的套接字的文件描述符,第二個參數(shù)是填充的struct sockaddr_in
結(jié)構(gòu)體,在傳參的時候,需要強轉(zhuǎn)為struct sockaddr*
類型,第三個參數(shù)是結(jié)構(gòu)體的大小。發(fā)起請求成功返回0,失敗返回-1。
用法非常熟悉,只是作用不同,connect
的作用就是在套接字中發(fā)起一個網(wǎng)絡(luò)請求,這個網(wǎng)絡(luò)請求服務(wù)端的監(jiān)聽套接字可以檢測到。
請求發(fā)起成功夠就開始通信了,同樣使用系統(tǒng)調(diào)用read
和write
向套接字中讀取和寫入數(shù)據(jù),和操作普通文件一樣。
這個客戶端程序中,先從標(biāo)準(zhǔn)輸入獲取用戶從鍵盤上輸入的數(shù)據(jù),然后通過write
寫入到套接字中,也就是發(fā)給服務(wù)器,然后使用read
從套接字中讀取服務(wù)端回顯的信息并且打印出來。
以上就是客戶端的代碼,同樣放在一個類中,在使用的時候直接實例化后就可以使用。
tcpClient.cpp
同樣使用智能指針來管理客戶端對象,具體的不再講解。
服務(wù)端進程跑起來后,創(chuàng)建套接字,綁定以及將套接字設(shè)置成監(jiān)聽狀態(tài),當(dāng)這里準(zhǔn)備工作做完以后,在接收連接請求的時候會阻塞等待連接請求的到來。
在客戶端進程跑起來后,服務(wù)端立刻接收到了連接請求,并且創(chuàng)建了一個套接字用戶服務(wù)客戶端,此時客戶端和服務(wù)端就可以進行網(wǎng)絡(luò)通信了。
客戶端發(fā)送什么,服務(wù)端就收到什么,然后再經(jīng)過加工將數(shù)據(jù)返回給客戶端。
使用指令netstat -anlp
可以查看當(dāng)前機器上運行的網(wǎng)絡(luò)通信進程,如上圖所示。
-
紅色框中的是服務(wù)器進程,本地環(huán)回IP地址是
127.0.0.1
,端口號是8080
,與其連接的客戶端端口號是40428
。 -
綠色框中的是客戶端進程,本地環(huán)回IP地址仍然是
127.0.0.1
,端口號是40428
,與其連接的服務(wù)器端口號是8080
。
由于本喵是在一臺機器上測試的,所以查看網(wǎng)絡(luò)通信進程的時候,可以同時看到服務(wù)器的和客戶端的。
在服務(wù)器運行起來后,第一個客戶端可以成功建立連接,第二個客戶端就無法連接了,處于阻塞等待狀態(tài)。
這是因為第一個客戶端連接連接后,服務(wù)器就陷入了死循環(huán),如上圖代碼所示。
在start
中有一個while(1)
,用來不斷接收接收來自客戶端的連接請求,連接成功后進入具體的服務(wù)函數(shù)serviceIO
,在這個函數(shù)中也有一個while(1)
循環(huán),不斷進行客戶端和服務(wù)端的網(wǎng)絡(luò)IO。
所以當(dāng)?shù)谝粋€客戶端連接成功后,服務(wù)器就陷入了serviceIO
的死循環(huán)中,當(dāng)新的客戶端發(fā)起連接請求時,服務(wù)器無法accept
到,所以表現(xiàn)出來的就是上圖所示的現(xiàn)象。
這是本喵為了測試而寫的測試代碼。真正的tcp
網(wǎng)絡(luò)通信中肯定不能這樣,而是要滿足多個客戶端都能和服務(wù)端連接成功。
??多進程版本
為了實現(xiàn)多個客戶端都能和服務(wù)器建立連接,第一種策略就是采用多進程的形式,服務(wù)器每建立一個和客戶端的連接請求就創(chuàng)建一個新的進程,用來一對一服務(wù)客戶端。
在start
函數(shù)中,accept
連接后,開始進行具體的服務(wù),也就是調(diào)用serviceIO
函數(shù),之前是直接調(diào)用,此時需要用fork
創(chuàng)建一個子進程,讓子進程去調(diào)用serviecIO
函數(shù),如上圖綠色框中所示。
- 子進程會繼承父進程的一切,所以父進程中監(jiān)聽套接字的文件描述符
_listensock
也會被繼承下來- 為了防止子進程進行誤操作以及一定程度上節(jié)省資源,在子進程中將
_listnesock
指向的監(jiān)聽套接字關(guān)閉。
為了避免子進程造成內(nèi)存泄漏,當(dāng)子進程退出以后,父進程需要回收子進程資源,可以使用waitpid
來回收子進程,有阻塞等待和非阻塞等待,這里本喵采用阻塞等待,如上圖第二個紅色框。
但是子進程執(zhí)行的serviceIO
是一個死循環(huán),如果子進程沒有退出的話,父進程就會一直阻塞在waitpid
處等待,當(dāng)有新的客戶端請求連接時同樣無法accept
到,就變成和之前的一樣了。
- 使用
waitpid
等待子進程退出時,一般不建議使用非阻塞的方式。
為了解決這個問題,在子進程中再次創(chuàng)建子進程,如上圖藍(lán)色框中所示,子進程fork
后,又創(chuàng)建出一個子進程,該子進程是上一層父進程的孫子進程。
孫子進程一經(jīng)創(chuàng)建,它的父進程就退出,父進程之后的代碼也就是servicIO
由孫子進程來執(zhí)行,孫子進程執(zhí)行完服務(wù)函數(shù)后,關(guān)閉當(dāng)前套接字,并且退出,此時祖父進程就可以繼續(xù)循環(huán)accept
了。
- 這里將原本父進程要執(zhí)行的代碼轉(zhuǎn)交給孫子進程去執(zhí)行,祖父進程從容獲得自己,可以繼續(xù)去
accept
。
可以看到,此時兩個客戶端就可以同時和服務(wù)器建立連接,并且進行通信了,而且兩個客戶端之間互不影響,因為在服務(wù)端,每個客戶端都存在一個套接字進行一對一服務(wù)。
使用指令ps ajx
查看當(dāng)前機器上的進程時,可以看到名字為tcpserve
的進程有3個,第一個進程就是祖父進程,pid為15959
。
后面兩個進程是孫子進程,每個進程對應(yīng)著一個客戶端,它們各自的pid值不一樣,但是ppid都是1
,表示操作系統(tǒng)。
- 由于孫子進程的父進程退出了,所以兩個孫子進程變成了孤兒進程由操作系統(tǒng)領(lǐng)養(yǎng),它兩的資源也由操作系統(tǒng)負(fù)責(zé)回收。
此時就可以實現(xiàn)多客戶端和服務(wù)器之間的網(wǎng)絡(luò)通信了,每多一個客戶端就會在服務(wù)器上多創(chuàng)建一個進程,和一個套接字用來專門進行一對一服務(wù)。
但是多進程版本仍然存在缺陷,每創(chuàng)建一個進程的開銷是很大的,需要創(chuàng)建并維護進程地址空間,頁表,以及相應(yīng)的物理內(nèi)存。
??多線程版本
將多進程改變成多線程就能在一定程度上解決資源消耗大的問題,因為從線程和主線程是共用一份進程地址空間以及頁表的。
將原本的多進程代碼改成如上圖所示的多線程代碼。
在使用pthread_create
創(chuàng)建多線程的時候,傳入的可執(zhí)行任務(wù)不能有this
指針,但是調(diào)用的具體服務(wù)函數(shù)serviceIO
需要this
指針。
- 創(chuàng)建
ThreadData
用來存放新線程需要的參數(shù)
如上圖所示,結(jié)構(gòu)體中存在用于進行通信套接字的文件描述符_sock
,以及服務(wù)器對象tcpserve
的指針_self
。
- 新線程執(zhí)行的任務(wù)
threadRoutine
使用static
修飾。
從線程執(zhí)行完后的線程資源同樣需要回收,如果在主線程使用pthread_join
等待回收資源的話,同樣會造成主線程無法accept
。
所以在從線程中,首先就是使用pthread_detach
分離,讓操作系統(tǒng)來回收該線程的資源,主線程可以繼續(xù)去accept
新的連接請求。
在threadRoutine
中通過服務(wù)器對象的_self
指針來調(diào)用serveicIO
,此時就可以多個客戶端向服務(wù)器發(fā)起連接請求了。
可以看到,結(jié)果和多進程版本一樣,多個客戶端可以同時和服務(wù)器進行通信,每個客戶端在服務(wù)器中對應(yīng)一個線程和一個套接字。
通過指令ps -aL
來查看當(dāng)前機器上的線程,可以看到此時存在3個名字為tcpserve
的線程。
- 其中PID和LWP相同的是主線程,是用來
accept
新客戶端的網(wǎng)絡(luò)連接請求的。- PID和LWP不同的是兩個從線程,是專門用來用來給客戶端提供一對一服務(wù)的。
此時每有一個新的客戶端發(fā)起連接請求,服務(wù)器就會新創(chuàng)建一個線程。但是這樣同樣也有缺陷,在創(chuàng)建線程的時候同樣會存在很大的系統(tǒng)開銷。
??線程池版本
最好的方式就是使用線程池,預(yù)先創(chuàng)建一批線程,每有一個客戶端發(fā)起連接請求,就派一個線程去處理。
- 線程池就直接使用前面本喵寫過的基于環(huán)形隊列的線程池,不再講解線程池。
在服務(wù)器accept
之前,將線程池運行起來,如上圖紅色框中所示,當(dāng)服務(wù)器accept
新的連接請求后,將系統(tǒng)創(chuàng)建的用于服務(wù)的套接字文件描述符和serviceIO
任務(wù)推送到線程池中,讓線程池分配線程去執(zhí)行任務(wù)。
Task.hpp
將Task
類稍作修改,成員變量包含用于通信的套接字文件描述符和回調(diào)函數(shù)。
- 這里本喵將
serviceIO
放在了Task.hpp
中,不再是一個類成員函數(shù)。
此時將服務(wù)器運行起來以后,除了創(chuàng)建套接字等準(zhǔn)備工作外,還有線程池中會起來10個線程,這10個線程就是專門用來服務(wù)客戶端的。
兩個客戶端發(fā)送的信息服務(wù)端都能收到并且給對應(yīng)的回顯,而且互不影響,和前面多進程和多線程效果一樣。
可以看到,此時服務(wù)器上存在10個從線程,主線程仍然在不停的accept
,每有一個客戶端發(fā)起連接請求時,線程池就會安排一個線程去服務(wù),而不會再創(chuàng)建新的線程。
??日志功能
之前本喵在調(diào)用系統(tǒng)調(diào)用或者庫函數(shù)等有返回值的函數(shù)時,會根據(jù)返回值打印一些信息來表示該感受的調(diào)用結(jié)果,有時甚至?xí)褂缅e誤碼。
今天本喵來介紹一下日志,日志就是專門用來記錄程序的執(zhí)行信息的,這部分代碼放在log.hpp
中,同樣可以作為一個小組件。
函數(shù)logMessage
就是用來記錄日志的,其中level
表示日志等級,message
是傳入的具體日志信息。
日志等級也就是程序運行過程中結(jié)果的重要程度:
- DEBUG:表示調(diào)試信息,這是程序員在調(diào)試代碼時看的。
- NORMAL:表示正常信息,就是代碼的運行結(jié)果是符合預(yù)期的。
- WARING:表示警告信息,存在問題但只是警告,可以暫不處理。
- ERROR:表示錯誤信息,代表代碼運行出現(xiàn)了錯誤,需要及時處理。
- FATAL:表示致命信息,代表代碼出現(xiàn)了致命錯誤,無法運行下去了。
這些等級是我們?nèi)藶閯澐值模煌燃墝?yīng)的處理方式也是由我們自己控制的,比如發(fā)生ERROR
時,是結(jié)束程序還是繼續(xù)運行,都是由我們自己控制的。
這是我本喵前面代碼使用的日志記錄函數(shù),真正的日志記錄肯定不可能這么簡單。
如上圖所示,真正的日志函數(shù)采用的是可變參數(shù),如上圖所示代碼。
- 注意:C語言的可變參數(shù)和可變模板參數(shù)不一樣。
logMessage
函數(shù)的第三個參數(shù)...
就表示可變參數(shù),本質(zhì)上是占位符,它的參數(shù)個數(shù)以及類型都是可以變化的。
參數(shù)列表的構(gòu)成:
- 參數(shù)分為兩個部分:固定參數(shù)和可變參數(shù),至少有一個固定參數(shù),聲明和普通函數(shù)一樣。
實現(xiàn)原理:
C語言中使用 va_list 系列變參宏實現(xiàn)變參函數(shù),此處va意為variable-argument(可變參數(shù))。
typedef char * va_list;
// 把 n 圓整到 sizeof(int) 的倍數(shù)
#define _INTSIZEOF(n) ( (sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1) )
// 初始化 ap 指針,使其指向第一個可變參數(shù)。v 是變參列表的前一個參數(shù)
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
// 該宏返回當(dāng)前變參值,并使 ap 指向列表中的下個變參
#define va_arg(ap, type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )
// /將指針 ap 置為無效,結(jié)束變參的獲取
#define va_end(ap) ( ap = (va_list)0 )
這是stdarg.h
頭文件中的定義,可以看到和可變參數(shù)有關(guān)的都是宏。
va_list
本質(zhì)上就是一個char*
類型的指針,指向的變量大小是一個字節(jié)。
- _INTSIZEOF(n):_INTSIZEOF進行內(nèi)存地址對齊,按照sizeof(int)即棧粒度對齊,參數(shù)在內(nèi)存中的地址均為sizeof(int)=4的倍數(shù)。例如,若1≤sizeof(n)≤4,則_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,則_INTSIZEOF(n)=8。
- va_start(ap,v):根據(jù)
va_list
中的參數(shù)v
在棧中的內(nèi)存地址,加上_INTSIZEOF(v)占內(nèi)存大小后,使ap
指向v
的下一個參數(shù)。用這個宏初始化 ap 指針,v 是最后一個固定參數(shù),初始化的結(jié)果是ap
指向第一個變參。
其他那兩個本喵就不介紹了,遇到了自行查閱就好。
這一系列帶v的輸出函數(shù),最后一個參數(shù)類型是va_list
,倒數(shù)第二個參數(shù)是format
,也就是最后一個固定參數(shù)。
這一族函數(shù)就是專門用來處理可變參數(shù)的,這里本喵使用的是vsnprintf
,將可變參數(shù)的內(nèi)容全部放入到logcontent
中。
例如logMessage(NORMAL, "create socket success");
,這里面的字符串被當(dāng)作了可變參數(shù),因為只有一個字符串,所以將其傳給了最后一個固定參數(shù)format
。
在我們的服務(wù)器中,在套接字創(chuàng)建成功后的日志信息中,增加_listensock
,以可變參數(shù)的形式傳給logMessage
,如上圖紅色框中所示。
可以看到,成功打印出了監(jiān)聽套接字的文件描述符,如上圖綠色小框中所示,在前面還有日志的前綴信息[日志等級][時間戳][pid]
。
日志不僅可以將數(shù)據(jù)打印到屏幕上,而且還可以輸出到文件中,將屏蔽掉的代碼解除注釋。
將日志等級為NORMAL
和DEBUG
以及WARNING
放入到LOG_NORMAL
定義的文件路徑中。
將日志等級為ERROR
和FATAL
的放入到LOG_ERROR
定義的文件路徑中。
兩個文件都是以追加寫入的方式打開,寫入完畢后再關(guān)閉文件。
在服務(wù)器中偽造出一些不同等級的日志信息,如上圖所示。
服務(wù)器運行后,日志信息不僅打印在屏幕上,而且還輸出到了日志文件log.error
和log.txt
中。
??守護進程
將服務(wù)器進程運行起來,如上圖所示,再查看當(dāng)前服務(wù)器上的網(wǎng)絡(luò)進程以及進程。
可以看到,IP地址為0.0.0.0
,端口號為8080
,進程名為tcpserve
的進程是存在的。
-
直接關(guān)掉
Xshell
會話窗口,不退出進程。
此時再查看名為tcpserve
的進程,已經(jīng)看不到了,說明它已經(jīng)退出了,但是我們明明沒有讓它退出啊,只是關(guān)掉了Xshell
的窗口而已。
- 每一個
Xshell
窗口都會在服務(wù)器上創(chuàng)建一個會話,準(zhǔn)確的說會運行一個名字為bash
的進程。- 每一個會話中最多只有一個前臺任務(wù),可以有多個后臺任務(wù)(包括0個)。
當(dāng)Xshell
的窗口關(guān)閉后,服務(wù)器上對應(yīng)的會話就會結(jié)束,bash
進程就退出了,bash
維護的所有進程都會退出。所以關(guān)掉Xshell
窗口后tcpserve
進程就會退出。
這樣就存在一個問題,提供網(wǎng)絡(luò)服務(wù)的服務(wù)器難道運行了tcpserve
就不能干別的了嗎?肯定不是。要想關(guān)掉Xshell
后tcpserve
不退出,只能讓tcpserve
自成一個會話。
- 自成一個會話的進程就被叫做守護進程,也叫做精靈進程。
前后臺進程組:
上圖中,sleep 10000 | sleep 20000 | sleep 30000
是通過管道一起創(chuàng)建的3個進程,這三個進程組成一個進程組,也被叫做一個作業(yè)。后面又加了&
表示這個作業(yè)是后臺進程。
使用指令jobs
可以查看當(dāng)前機器上的作業(yè),如上圖所示,有3個作業(yè)在運行,而且都是后臺進程。
- 前面的數(shù)組是進程組的編號,如上圖所示的【1】【2】【3】。
通過指令gb+進程組編號
,可以將后臺進程變成前臺進程,如上圖所示,此時Xshell
窗口就阻塞住了,在做延時,我們無法輸入其他東西。
將該進程組暫停后,繼續(xù)使用jobs
可以看到,進程組1后面的&
沒有了,表示這是一個前臺進程,只是暫停了而已。
使用指令bg+進程組編號
,可以將進程組設(shè)置為后臺進程,如上圖所示,此時進程組1后面的&
又有了,并且進程運行了起來,也不再阻塞了,可以在窗口中繼續(xù)輸入指令了。
可以看到,9個sleep
進程的pid值都不同,因為它們是獨立的進程。
- PGID表示進程組的ID,其中PID和PGID值相同的進程是這個進程組的組長。
PGID
中本喵畫了三個框,每個框中有3個相同的PGID
,所以此時就有3組進程,和前面使用管道創(chuàng)建的進程組結(jié)果一樣。
但是所有進程的PPID
都是16496
,這個進程就是bash
,所以說,bash
就是當(dāng)前會話中所有進程的父進程。
還有一個SID
,表示會話ID,所有進程的SID
都相同,因為它們同屬于一個會話。
- PPID和SID之所以相同,是因為會話的本質(zhì)就是
bash
。
??變成守護進程
要想讓會話關(guān)閉以后進程還在運行,就需要讓這個進程自成一個會話,也就是成為守護進程。
系統(tǒng)調(diào)用setsid
的作用就是將調(diào)用該函數(shù)的進程變成守護進程,也就是創(chuàng)建一個新的會話,這個會話中只有當(dāng)前進程。
- 注意:調(diào)用系統(tǒng)調(diào)用
setsid
的進程在調(diào)用之前不能是進程組的組長,否則無法創(chuàng)建新的會話,也就無法成為守護進程。
如果調(diào)用成功,則返回新的會話id(SID
),調(diào)用失敗,則返回-1,并且設(shè)置錯誤碼。
如上圖代碼,daemonself
就是讓我們的tcpserve
變成守護進程,總共分為四步。
- 讓調(diào)用進程忽略異常信號
調(diào)用進程會變成守護進程,形成一個獨立的會話,此時它的具體運行情況我們是無法得知的。但是它有可能會受到異常信號的干擾而導(dǎo)致進程退出,此時我們根本不知道它退出了。
所以要忽略掉異常信號,尤其是管道信號,因為網(wǎng)絡(luò)服務(wù)程序就是在套接字中讀寫數(shù)據(jù),所以使用signal
系統(tǒng)調(diào)用忽略掉SIGPIPE
信號。
- 讓自己不是組長
setsid
系統(tǒng)調(diào)用要求調(diào)用的進程不能是進程組的組長,所以要想辦法讓我們的tcpserve
不是組長。
同樣采用if(fork()>0) exit(0)
的策略,當(dāng)前進程退出,將后續(xù)代碼交給它的子進程,此時原本的組長就退出了,子進程成為孤兒進程被操作系統(tǒng)收養(yǎng)也就不是組長了。
子進程再調(diào)用setsid
變成守護進程,自成一個會話。
- 守護進程本質(zhì)上就是一個孤兒進程。
- 關(guān)閉或者重定向以前進程默認(rèn)打開的文件
在Linux中存在一個黑洞文件/dev/null
,向該文件中寫入的內(nèi)容會被全部丟棄,從該文件中讀取內(nèi)容時什么也讀不到而且不會發(fā)生錯誤。
每個進程都會默認(rèn)打開文件描述符為0,1,2
的三個文件,而守護進程是脫離終端的,沒有顯示器,沒有鍵盤等,所以要對這三個文件做處理。
- 最好的方式就是將黑洞文件/dev/null重定向到這三個文件。
- 如果無法重定向,那就只能關(guān)閉這三個文件了。
- 進程執(zhí)行路徑發(fā)生更改(可選)
每一進程都有一個cwd
數(shù)據(jù),用來記錄當(dāng)前進程的所屬路徑,所以默認(rèn)情況下,進程文件所在的路徑就是當(dāng)前目錄。
成為守護進程后,如果需要更改tcpserve
的執(zhí)行路徑,就可以通過系統(tǒng)調(diào)用chdir
來改變cwd
屬性,從而更改路徑。這里給的缺省值null
。
如上圖所示,在tcpserve.cpp
中調(diào)用daemonself
,將tcpserve
服務(wù)進程變成守護進程。
在運行服務(wù)端程序后,服務(wù)器進程初始化,然后變成守護進程并且開始運行(這一點我們看不到)。當(dāng)前會話并沒有阻塞,仍然可以數(shù)據(jù)其他指令。
查看當(dāng)前服務(wù)器上的進程時,可以看到守護進程tcpserve
的存在,并且它的PPID
是1(操作系統(tǒng)),PID
,PGID
以及SID
三者都是11251
。
- 守護進程自成會話,自成進程組,和終端設(shè)備無關(guān)。
此時本喵的服務(wù)器仍然可以干其他任務(wù),tcpserve
守護進程也在運行,并且自行按照我們寫的邏輯來接收客戶端的連接請求并進行網(wǎng)絡(luò)通信。
雖然有一個系統(tǒng)調(diào)用daemon
可以讓一個進程變成守護進程,但是它并不太好用,實際應(yīng)用中都通過setsid
自己實現(xiàn)daemon
的,就像我們上面寫的一樣。文章來源:http://www.zghlxwxcb.cn/news/detail-595529.html
??總結(jié)
現(xiàn)在TCP網(wǎng)絡(luò)通信也見過了,雖然不知道原理,但是我們知道套接字的存在,以及網(wǎng)絡(luò)通信的代碼過程,和現(xiàn)象。除此之外本喵還介紹了常用小組件日志
,以及如何讓一個進程變成守護進程。文章來源地址http://www.zghlxwxcb.cn/news/detail-595529.html
到了這里,關(guān)于【網(wǎng)絡(luò)】socket——TCP網(wǎng)絡(luò)通信 | 日志功能 | 守護進程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!