国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序

這篇具有很好參考價值的文章主要介紹了網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

一.簡單的TCP網(wǎng)絡(luò)程序

1.服務(wù)端創(chuàng)建套接字

我們將TCP服務(wù)器封裝成一個類,當我們定義出一個服務(wù)器對象后需要馬上對服務(wù)器進行初始化,而初始化TCP服務(wù)器要做的第一件事就是創(chuàng)建套接字。

TCP服務(wù)器在調(diào)用socket函數(shù)創(chuàng)建套接字時,參數(shù)設(shè)置如下:

  • 協(xié)議家族選擇AF_INET,因為我們要進行的是網(wǎng)絡(luò)通信。
  • 創(chuàng)建套接字時所需的服務(wù)類型應(yīng)該是SOCK_STREAM,因為我們編寫的是TCP服務(wù)器,SOCK_STREAM提供的就是一個有序的、可靠的、全雙工的、基于連接的流式服務(wù)。
  • 協(xié)議類型默認設(shè)置為0即可。

如果創(chuàng)建套接字后獲得的文件描述符是小于0的,說明套接字創(chuàng)建失敗,此時也就沒必要進行后續(xù)操作了,直接終止程序即可。

class TcpServer
{
public:
	void InitServer()
	{
		//創(chuàng)建套接字
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
	}
	~TcpServer()
	{
		if (_sock >= 0){
			close(_sock);
		}
	}
private:
	int _sock; //套接字
};

說明一下:

  • 實際TCP服務(wù)器創(chuàng)建套接字的做法與UDP服務(wù)器是一樣的,只不過創(chuàng)建套接字時TCP需要的是流式服務(wù),而UDP需要的是用戶數(shù)據(jù)報服務(wù)。
  • 當析構(gòu)服務(wù)器時,可以將服務(wù)器對應(yīng)的文件描述符進行關(guān)閉。

2.服務(wù)端綁定

套接字創(chuàng)建完畢后我們實際只是在系統(tǒng)層面上打開了一個文件,該文件還沒有與網(wǎng)絡(luò)關(guān)聯(lián)起來,因此創(chuàng)建完套接字后我們還需要調(diào)用bind函數(shù)進行綁定操作。

綁定的步驟如下:

  • 定義一個struct sockaddr_in結(jié)構(gòu)體,將服務(wù)器網(wǎng)絡(luò)相關(guān)的屬性信息填充到該結(jié)構(gòu)體當中,比如協(xié)議家族、IP地址、端口號等。
  • 填充服務(wù)器網(wǎng)絡(luò)相關(guān)的屬性信息時,協(xié)議家族對應(yīng)就是AF_INET,端口號就是當前TCP服務(wù)器程序的端口號。在設(shè)置端口號時,需要調(diào)用htons函數(shù)將端口號由主機序列轉(zhuǎn)為網(wǎng)絡(luò)序列。
  • 在設(shè)置服務(wù)器的IP地址時,我們可以設(shè)置為本地環(huán)回127.0.0.1,表示本地通信。也可以設(shè)置為公網(wǎng)IP地址,表示網(wǎng)絡(luò)通信。
  • 如果使用的是云服務(wù)器,那么在設(shè)置服務(wù)器的IP地址時,不需要顯示綁定IP地址,直接將IP地址設(shè)置為INADDR_ANY即可,此時服務(wù)器就可以從本地任何一張網(wǎng)卡當中讀取數(shù)據(jù)。此外,由于INADDR_ANY本質(zhì)就是0,因此在設(shè)置時不需要進行網(wǎng)絡(luò)字節(jié)序的轉(zhuǎn)換。
  • 填充完服務(wù)器網(wǎng)絡(luò)相關(guān)的屬性信息后,需要調(diào)用bind函數(shù)進行綁定。綁定實際就是將文件與網(wǎng)絡(luò)關(guān)聯(lián)起來,如果綁定失敗也沒必要進行后續(xù)操作了,直接終止程序即可。

由于TCP服務(wù)器初始化時需要服務(wù)器的端口號,因此在服務(wù)器類當中需要引入端口號,當實例化服務(wù)器對象時就需要給傳入一個端口號。而由于我當前使用的是云服務(wù)器,因此在綁定TCP服務(wù)器的IP地址時不需要綁定公網(wǎng)IP地址,直接綁定INADDR_ANY即可,因此我這里沒有在服務(wù)器類當中引入IP地址。

class TcpServer
{
public:
	TcpServer(int port)
		: _sock(-1)
		, _port(port)
	{}
	void InitServer()
	{
		//創(chuàng)建套接字
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
		//綁定
		struct sockaddr_in local;
		memset(&local, '\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;
		
		if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
			std::cerr << "bind error" << std::endl;
			exit(3);
		}
	}
	~TcpServer()
	{
		if (_sock >= 0){
			close(_sock);
		}
	}
private:
	int _sock; //監(jiān)聽套接字
	int _port; //端口號
};

當定義好struct sockaddr_in結(jié)構(gòu)體后,最好先用memset函數(shù)對該結(jié)構(gòu)體進行清空,也可以用bzero函數(shù)進行清空。bzero函數(shù)也可以對特定的一塊內(nèi)存區(qū)域進行清空,bzero函數(shù)的函數(shù)原型如下:

void bzero(void *s, size_t n);

說明一下:

  • TCP服務(wù)器綁定時的步驟與UDP服務(wù)器是完全一樣的,沒有任何區(qū)別。

3.服務(wù)端監(jiān)聽

UDP服務(wù)器的初始化操作只有兩步,第一步就是創(chuàng)建套接字,第二步就是綁定。而TCP服務(wù)器是面向連接的,客戶端在正式向TCP服務(wù)器發(fā)送數(shù)據(jù)之前,需要先與TCP服務(wù)器建立連接,然后才能與服務(wù)器進行通信。

因此TCP服務(wù)器需要時刻注意是否有客戶端發(fā)來連接請求,此時就需要將TCP服務(wù)器創(chuàng)建的套接字設(shè)置為監(jiān)聽狀態(tài)。

listen函數(shù)

設(shè)置套接字為監(jiān)聽狀態(tài)的函數(shù)叫做listen,該函數(shù)的函數(shù)原型如下:

int listen(int sockfd, int backlog);

參數(shù)說明:

  • sockfd:需要設(shè)置為監(jiān)聽狀態(tài)的套接字對應(yīng)的文件描述符。
  • backlog:全連接隊列的最大長度。如果有多個客戶端同時發(fā)來連接請求,此時未被服務(wù)器處理的連接就會放入連接隊列,該參數(shù)代表的就是這個全連接隊列的最大長度,一般不要設(shè)置太大,設(shè)置為5或10即可。

返回值說明:

  • 監(jiān)聽成功返回0,監(jiān)聽失敗返回-1,同時錯誤碼會被設(shè)置。

服務(wù)器監(jiān)聽

TCP服務(wù)器在創(chuàng)建完套接字和綁定后,需要再進一步將套接字設(shè)置為監(jiān)聽狀態(tài),監(jiān)聽是否有新的連接到來。如果監(jiān)聽失敗也沒必要進行后續(xù)操作了,因為監(jiān)聽失敗也就意味著TCP服務(wù)器無法接收客戶端發(fā)來的連接請求,因此監(jiān)聽失敗我們直接終止程序即可。

#define BACKLOG 5

class TcpServer
{
public:
	void InitServer()
	{
		//創(chuàng)建套接字
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
		//綁定
		struct sockaddr_in local;
		memset(&local, '\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;
		
		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
			std::cerr << "bind error" << std::endl;
			exit(3);
		}
		//監(jiān)聽
		if (listen(_listen_sock, BACKLOG) < 0){
			std::cerr << "listen error" << std::endl;
			exit(4);
		}
	}
private:
	int _listen_sock; //監(jiān)聽套接字
	int _port; //端口號
};

說明一下:

  • 初始化TCP服務(wù)器時創(chuàng)建的套接字并不是普通的套接字,而應(yīng)該叫做監(jiān)聽套接字。為了表明寓意,我們將代碼中套接字的名字由sock改為_listen_sock 。
  • 在初始化TCP服務(wù)器時,只有創(chuàng)建套接字成功、綁定成功、監(jiān)聽成功,此時TCP服務(wù)器的初始化才算完成。

4.服務(wù)端獲取連接

TCP服務(wù)器初始化后就可以開始運行了,但TCP服務(wù)器在與客戶端進行網(wǎng)絡(luò)通信之前,服務(wù)器需要先獲取到客戶端的連接請求。

accept函數(shù)

獲取連接的函數(shù)叫做accept,該函數(shù)的函數(shù)原型如下:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

參數(shù)說明:

  • sockfd:特定的監(jiān)聽套接字,表示從該監(jiān)聽套接字中獲取連接。
  • addr:對端網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號等。
  • addrlen:調(diào)用時傳入期望讀取的addr結(jié)構(gòu)體的長度,返回時代表實際讀取到的addr結(jié)構(gòu)體的長度,這是一個輸入輸出型參數(shù)。

返回值說明:

  • 獲取連接成功返回接收到的套接字的文件描述符,獲取連接失敗返回-1,同時錯誤碼會被設(shè)置。

accept函數(shù)返回的套接字是什么?

調(diào)用accept函數(shù)獲取連接時,是從監(jiān)聽套接字當中獲取的。如果accept函數(shù)獲取連接成功,此時會返回接收到的套接字對應(yīng)的文件描述符。

監(jiān)聽套接字與accept函數(shù)返回的套接字的作用:

  • 監(jiān)聽套接字:用于獲取客戶端發(fā)來的連接請求。accept函數(shù)會不斷從監(jiān)聽套接字當中獲取新連接。
  • accept函數(shù)返回的套接字:用于為本次accept獲取到的連接提供服務(wù)。監(jiān)聽套接字的任務(wù)只是不斷獲取新連接,而真正為這些連接提供服務(wù)的套接字是accept函數(shù)返回的套接字,而不是監(jiān)聽套接字。

服務(wù)端獲取連接

服務(wù)端在獲取連接時需要注意:

  • accept函數(shù)獲取連接時可能會失敗,但TCP服務(wù)器不會因為獲取某個連接失敗而退出,因此服務(wù)端獲取連接失敗后應(yīng)該繼續(xù)獲取連接。
  • 如果要將獲取到的連接對應(yīng)客戶端的IP地址和端口號信息進行輸出,需要調(diào)用inet_ntoa函數(shù)將整數(shù)IP轉(zhuǎn)換成字符串IP,調(diào)用ntohs函數(shù)將端口號由網(wǎng)絡(luò)序列轉(zhuǎn)換成主機序列。
  • inet_ntoa函數(shù)在底層實際做了兩個工作,一是將網(wǎng)絡(luò)序列轉(zhuǎn)換成主機序列,二是將主機序列的整數(shù)IP轉(zhuǎn)換成字符串風(fēng)格的點分十進制的IP。
class TcpServer
{
public:
	void Start()
	{
		for (;;){
			//獲取連接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<std::endl;
		}
	}
private:
	int _listen_sock; //監(jiān)聽套接字
	int _port; //端口號
};

服務(wù)端接收連接測試

現(xiàn)在我們可以做一下簡單的測試,看看當前服務(wù)器能否成功接收請求連接。在運行服務(wù)端時需要傳入一個端口號作為服務(wù)端的端口號,然后我們用該端口號構(gòu)造一個服務(wù)端對象,對服務(wù)端進行初始化后啟動服務(wù)端即可。

void Usage(std::string proc)
{
	std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
	if (argc != 2){
		Usage(argv[0]);
		exit(1);
	}
	int port = atoi(argv[1]);
	TcpServer* svr = new TcpServer(port);
	svr->InitServer();
	svr->Start();
	return 0;
}

編譯代碼后,以./tcp_server 端口號的方式運行服務(wù)端。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

服務(wù)端運行后,通過netstat命令可以查看到一個程序名為tcp_server的服務(wù)程序,它綁定的端口就是8081,而由于服務(wù)器綁定的是INADDR_ANY,因此該服務(wù)器的本地IP地址是0.0.0.0,這就意味著該TCP服務(wù)器可以讀取本地任何一張網(wǎng)卡里面的數(shù)據(jù)。此外,最重要的是當前該服務(wù)器所處的狀態(tài)是LISTEN狀態(tài),表明當前服務(wù)器可以接收外部的請求連接。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

雖然現(xiàn)在還沒有編寫客戶端相關(guān)的代碼,但是我們可以使用telnet命令遠程登錄到該服務(wù)器,因為telnet底層實際采用的就是TCP協(xié)議。

使用telnet命令連接當前TCP服務(wù)器后可以看到,此時服務(wù)器接收到了一個連接,為該連接提供服務(wù)的套接字對應(yīng)的文件描述符就是4。因為0、1、2是默認打開的,其分別對應(yīng)標準輸入流、標準輸出流和標準錯誤流,而3號文件描述符在初始化服務(wù)器時分配給了監(jiān)聽套接字,因此當?shù)谝粋€客戶端發(fā)起連接請求時,為該客戶端提供服務(wù)的套接字對應(yīng)的文件描述符就是4。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

如果此時我們再用其他窗口繼續(xù)使用telnet命令,向該TCP服務(wù)器發(fā)起請求連接,此時為該客戶端提供服務(wù)的套接字對應(yīng)的文件描述符就是5。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當然,也可以直接用瀏覽器來訪問這個TCP服務(wù)器,因為瀏覽器常見的應(yīng)用層協(xié)議是http或https,其底層對應(yīng)的也是TCP協(xié)議,因此瀏覽器也可以向當前這個TCP服務(wù)器發(fā)起請求連接。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

說明一下:

  • 至于這里為什么瀏覽器一次會向我們的TCP服務(wù)器發(fā)起兩次請求這個問題,這里就不作討論了,我們只是要證明當前TCP服務(wù)器能夠正常接收外部的請求連接。

5.服務(wù)端處理請求

現(xiàn)在TCP服務(wù)器已經(jīng)能夠獲取連接請求了,下面當然就是要對獲取到的連接進行處理。但此時為客戶端提供服務(wù)的不是監(jiān)聽套接字,因為監(jiān)聽套接字獲取到一個連接后會繼續(xù)獲取下一個請求連接,為對應(yīng)客戶端提供服務(wù)的套接字實際是accept函數(shù)返回的套接字,下面就將其稱為“服務(wù)套接字”。

為了讓通信雙方都能看到對應(yīng)的現(xiàn)象,我們這里就實現(xiàn)一個簡單的回聲TCP服務(wù)器,服務(wù)端在為客戶端提供服務(wù)時就簡單的將客戶端發(fā)來的數(shù)據(jù)進行輸出,并且將客戶端發(fā)來的數(shù)據(jù)重新發(fā)回給客戶端即可。當客戶端拿到服務(wù)端的響應(yīng)數(shù)據(jù)后再將該數(shù)據(jù)進行打印輸出,此時就能確保服務(wù)端和客戶端能夠正常通信了。

read函數(shù)

TCP服務(wù)器讀取數(shù)據(jù)的函數(shù)叫做read,該函數(shù)的函數(shù)原型如下:

ssize_t read(int fd, void *buf, size_t count);

參數(shù)說明:

  • fd:特定的文件描述符,表示從該文件描述符中讀取數(shù)據(jù)。
  • buf:數(shù)據(jù)的存儲位置,表示將讀取到的數(shù)據(jù)存儲到該位置。
  • count:數(shù)據(jù)的個數(shù),表示從該文件描述符中讀取數(shù)據(jù)的字節(jié)數(shù)。

返回值說明:

  • 如果返回值大于0,則表示本次實際讀取到的字節(jié)個數(shù)。
  • 如果返回值等于0,則表示對端已經(jīng)把連接關(guān)閉了。
  • 如果返回值小于0,則表示讀取時遇到了錯誤。

read返回值為0表示對端連接關(guān)閉

這實際和本地進程間通信中的管道通信是類似的,當使用管道進行通信時,可能會出現(xiàn)如下情況:

  • 寫端進程不寫,讀端進程一直讀,此時讀端進程就會被掛起,因為此時數(shù)據(jù)沒有就緒。
  • 讀端進程不讀,寫端進程一直寫,此時當管道被寫滿后寫端進程就會被掛起,因為此時空間沒有就緒。
  • 寫端進程將數(shù)據(jù)寫完后將寫端關(guān)閉,此時當讀端進程將管道當中的數(shù)據(jù)讀完后就會讀到0。
  • 讀端進程將讀端關(guān)閉,此時寫端進程就會被操作系統(tǒng)殺掉,因為此時寫端進程寫入的數(shù)據(jù)不會被讀取。

這里的寫端就對應(yīng)客戶端,如果客戶端將連接關(guān)閉了,那么此時服務(wù)端將套接字當中的信息讀完后就會讀取到0,因此如果服務(wù)端調(diào)用read函數(shù)后得到的返回值為0,此時服務(wù)端就不必再為該客戶端提供服務(wù)了。

write函數(shù)

TCP服務(wù)器寫入數(shù)據(jù)的函數(shù)叫做write,該函數(shù)的函數(shù)原型如下:

ssize_t write(int fd, const void *buf, size_t count);

參數(shù)說明:

  • fd:特定的文件描述符,表示將數(shù)據(jù)寫入該文件描述符對應(yīng)的套接字。
  • buf:需要寫入的數(shù)據(jù)。
  • count:需要寫入數(shù)據(jù)的字節(jié)個數(shù)。

返回值說明:

  • 寫入成功返回實際寫入的字節(jié)數(shù),寫入失敗返回-1,同時錯誤碼會被設(shè)置。

當服務(wù)端調(diào)用read函數(shù)收到客戶端的數(shù)據(jù)后,就可以再調(diào)用write函數(shù)將該數(shù)據(jù)再響應(yīng)給客戶端。

服務(wù)端處理請求

需要注意的是,服務(wù)端讀取數(shù)據(jù)是服務(wù)套接字中讀取的,而寫入數(shù)據(jù)的時候也是寫入進服務(wù)套接字的。也就是說這里為客戶端提供服務(wù)的套接字,既可以讀取數(shù)據(jù)也可以寫入數(shù)據(jù),這就是TCP全雙工的通信的體現(xiàn)。

在從服務(wù)套接字中讀取客戶端發(fā)來的數(shù)據(jù)時,如果調(diào)用read函數(shù)后得到的返回值為0,或者讀取出錯了,此時就應(yīng)該直接將服務(wù)套接字對應(yīng)的文件描述符關(guān)閉。因為文件描述符本質(zhì)就是數(shù)組的下標,因此文件描述符的資源是有限的,如果我們一直占用,那么可用的文件描述符就會越來越少,因此服務(wù)完客戶端后要及時關(guān)閉對應(yīng)的文件描述符,否則會導(dǎo)致文件描述符泄漏。

class TcpServer
{
public:
	void Service(int sock, std::string client_ip, int client_port)
	{
		char buffer[1024];
		while (true){
			ssize_t size = read(sock, buffer, sizeof(buffer)-1);
			if (size > 0){ //讀取成功
				buffer[size] = '\0';
				std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;

				write(sock, buffer, size);
			}
			else if (size == 0){ //對端關(guān)閉連接
				std::cout << client_ip << ":" << client_port << " close!" << std::endl;
				break;
			}
			else{ //讀取失敗
				std::cerr << sock << " read error!" << std::endl;
				break;
			}
		}
		close(sock); //歸還文件描述符
		std::cout << client_ip << ":" << client_port << " service done!" << std::endl;
	}
	void Start()
	{
		for (;;){
			//獲取連接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link [" << client_ip << "]:" << client_port << std::endl;

			//處理請求
			Service(sock, client_ip, client_port);
		}
	}
private:
	int _listen_sock; //監(jiān)聽套接字
	int _port; //端口號
};

6.客戶端創(chuàng)建套接字

同樣的,我們將客戶端也封裝成一個類,當我們定義出一個客戶端對象后也需要對其進行初始化,而初始化客戶端唯一需要做的就是創(chuàng)建套接字。而客戶端在調(diào)用socket函數(shù)創(chuàng)建套接字時,參數(shù)設(shè)置與服務(wù)端創(chuàng)建套接字時是一樣的。

客戶端不需要進行綁定和監(jiān)聽:

  • 服務(wù)端要進行綁定是因為服務(wù)端的IP地址和端口號必須要眾所周知,不能隨意改變。而客戶端雖然也需要IP地址和端口號,但是客戶端并不需要我們進行綁定操作,客戶端連接服務(wù)端時系統(tǒng)會自動指定一個端口號給客戶端。
  • 服務(wù)端需要進行監(jiān)聽是因為服務(wù)端需要通過監(jiān)聽來獲取新連接,但是不會有人主動連接客戶端,因此客戶端是不需要進行監(jiān)聽操作的。

此外,客戶端必須要知道它要連接的服務(wù)端的IP地址和端口號,因此客戶端除了要有自己的套接字之外,還需要知道服務(wù)端的IP地址和端口號,這樣客戶端才能夠通過套接字向指定服務(wù)器進行通信。

class TcpClient
{
public:
	TcpClient(std::string server_ip, int server_port)
		: _sock(-1)
		, _server_ip(server_ip)
		, _server_port(server_port)
	{}
	void InitClient()
	{
		//創(chuàng)建套接字
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
	}
	~TcpClient()
	{
		if (_sock >= 0){
			close(_sock);
		}
	}
private:
	int _sock; //套接字
	std::string _server_ip; //服務(wù)端IP地址
	int _server_port; //服務(wù)端端口號
};

7.客戶端連接服務(wù)器

由于客戶端不需要綁定,也不需要監(jiān)聽,因此當客戶端創(chuàng)建完套接字后就可以向服務(wù)端發(fā)起連接請求。

connect函數(shù)

發(fā)起連接請求的函數(shù)叫做connect,該函數(shù)的函數(shù)原型如下:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

參數(shù)說明:

  • sockfd:特定的套接字,表示通過該套接字發(fā)起連接請求。
  • addr:對端網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號等。
  • addrlen:傳入的addr結(jié)構(gòu)體的長度。

返回值說明:

  • 連接或綁定成功返回0,連接失敗返回-1,同時錯誤碼會被設(shè)置。

客戶端連接服務(wù)器

需要注意的是,客戶端不是不需要進行綁定,而是不需要我們自己進行綁定操作,當客戶端向服務(wù)端發(fā)起連接請求時,系統(tǒng)會給客戶端隨機指定一個端口號進行綁定。因為通信雙方都必須要有IP地址和端口號,否則無法唯一標識通信雙方。也就是說,如果connect函數(shù)調(diào)用成功了,客戶端本地會隨機給該客戶端綁定一個端口號發(fā)送給對端服務(wù)器。

此外,調(diào)用connect函數(shù)向服務(wù)端發(fā)起連接請求時,需要傳入服務(wù)端對應(yīng)的網(wǎng)絡(luò)信息,否則connect函數(shù)也不知道該客戶端到底是要向哪一個服務(wù)端發(fā)起連接請求。

class TcpClient
{
public:
	void Start()
	{
		struct sockaddr_in peer;
		memset(&peer, '\0', sizeof(peer));
		peer.sin_family = AF_INET;
		peer.sin_port = htons(_server_port);
		peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
		
		if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
			std::cout << "connect success..." << std::endl;
			Request(); //發(fā)起請求
		}
		else{ //connect error
			std::cerr << "connect failed..." << std::endl;
			exit(3);
		}
	}
private:
	int _sock; //套接字
	std::string _server_ip; //服務(wù)端IP地址
	int _server_port; //服務(wù)端端口號
};

8.客戶端發(fā)起請求

由于我們實現(xiàn)的是一個簡單的回聲服務(wù)器,因此當客戶端連接到服務(wù)端后,客戶端就可以向服務(wù)端發(fā)送數(shù)據(jù)了,這里我們可以讓客戶端將用戶輸入的數(shù)據(jù)發(fā)送給服務(wù)端,發(fā)送時調(diào)用write函數(shù)向套接字當中寫入數(shù)據(jù)即可。

當客戶端將數(shù)據(jù)發(fā)送給服務(wù)端后,由于服務(wù)端讀取到數(shù)據(jù)后還會進行回顯,因此客戶端在發(fā)送數(shù)據(jù)后還需要調(diào)用read函數(shù)讀取服務(wù)端的響應(yīng)數(shù)據(jù),然后將該響應(yīng)數(shù)據(jù)進行打印,以確定雙方通信無誤。

class TcpClient
{
public:
	void Request()
	{
		std::string msg;
		char buffer[1024];
		while (true){
			std::cout << "Please Enter# ";
			getline(std::cin, msg);

			write(_sock, msg.c_str(), msg.size());

			ssize_t size = read(_sock, buffer, sizeof(buffer)-1);
			if (size > 0){
				buffer[size] = '\0';
				std::cout << "server echo# " << buffer << std::endl;
			}
			else if (size == 0){
				std::cout << "server close!" << std::endl;
				break;
			}
			else{
				std::cerr << "read error!" << std::endl;
				break;
			}
		}
	}
	void Start()
	{
		struct sockaddr_in peer;
		memset(&peer, '\0', sizeof(peer));
		peer.sin_family = AF_INET;
		peer.sin_port = htons(_server_port);
		peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
		
		if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
			std::cout << "connect success..." << std::endl;
			Request(); //發(fā)起請求
		}
		else{ //connect error
			std::cerr << "connect failed..." << std::endl;
			exit(3);
		}
	}
private:
	int _sock; //套接字
	std::string _server_ip; //服務(wù)端IP地址
	int _server_port; //服務(wù)端端口號
};

在運行客戶端程序時我們就需要攜帶上服務(wù)端對應(yīng)的IP地址和端口號,然后我們就可以通過服務(wù)端的IP地址和端口號構(gòu)造出一個客戶端對象,對客戶端進行初始后啟動客戶端即可。

void Usage(std::string proc)
{
	std::cout << "Usage: " << proc << "server_ip server_port" << std::endl;
}
int main(int argc, char* argv[])
{
	if (argc != 3){
		Usage(argv[0]);
		exit(1);
	}
	std::string server_ip = argv[1];
	int server_port = atoi(argv[2]);
	TcpClient* clt = new TcpClient(server_ip, server_port);
	clt->InitClient();
	clt->Start();
	return 0;
}

9.服務(wù)器測試

現(xiàn)在服務(wù)端和客戶端均已編寫完畢,下面我們進行測試。測試時我們先啟動服務(wù)端,然后通過netstat命令進行查看,此時我們就能看到一個名為tcp_server的服務(wù)進程,該進程當前處于監(jiān)聽狀態(tài)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

然后再通過./tcp_client IP地址 端口號的形式運行客戶端,此時客戶端就會向服務(wù)端發(fā)起連接請求,服務(wù)端獲取到請求后就會為該客戶端提供服務(wù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當客戶端向服務(wù)端發(fā)送消息后,服務(wù)端可以通過打印的IP地址和端口號識別出對應(yīng)的客戶端,而客戶端也可以通過服務(wù)端響應(yīng)回來的消息來判斷服務(wù)端是否收到了自己發(fā)送的消息。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

如果此時客戶端退出了,那么服務(wù)端在調(diào)用read函數(shù)時得到的返回值就是0,此時服務(wù)端也就知道客戶端退出了,進而會終止對該客戶端的服務(wù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

注意: 此時是服務(wù)端對該客戶端的服務(wù)終止了,而不是服務(wù)器終止了,此時服務(wù)器依舊在運行,它在等待下一個客戶端的連接請求。

10.單執(zhí)行流服務(wù)器的弊端

當我們僅用一個客戶端連接服務(wù)端時,這一個客戶端能夠正常享受到服務(wù)端的服務(wù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

但在這個客戶端正在享受服務(wù)端的服務(wù)時,我們讓另一個客戶端也連接服務(wù)器,此時雖然在客戶端顯示連接是成功的,但這個客戶端發(fā)送給服務(wù)端的消息既沒有在服務(wù)端進行打印,服務(wù)端也沒有將該數(shù)據(jù)回顯給該客戶端。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

只有當?shù)谝粋€客戶端退出后,服務(wù)端才會將第二個客戶端發(fā)來是數(shù)據(jù)進行打印,并回顯該第二個客戶端。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

單執(zhí)行流的服務(wù)器

通過實驗現(xiàn)象可以看到,這服務(wù)端只有服務(wù)完一個客戶端后才會服務(wù)另一個客戶端。因為我們目前所寫的是一個單執(zhí)行流版的服務(wù)器,這個服務(wù)器一次只能為一個客戶端提供服務(wù)。

當服務(wù)端調(diào)用accept函數(shù)獲取到連接后就給該客戶端提供服務(wù),但在服務(wù)端提供服務(wù)期間可能會有其他客戶端發(fā)起連接請求,但由于當前服務(wù)器是單執(zhí)行流的,只能服務(wù)完當前客戶端后才能繼續(xù)服務(wù)下一個客戶端。

客戶端為什么會顯示連接成功?

當服務(wù)端在給第一個客戶端提供服務(wù)期間,第二個客戶端向服務(wù)端發(fā)起的連接請求時是成功的,只不過服務(wù)端沒有調(diào)用accept函數(shù)將該連接獲取上來罷了。

實際在底層會為我們維護一個連接隊列,服務(wù)端沒有accept的新連接就會放到這個連接隊列當中,而這個連接隊列的最大長度就是通過listen函數(shù)的第二個參數(shù)來指定的,因此服務(wù)端雖然沒有獲取第二個客戶端發(fā)來的連接請求,但是在第二個客戶端那里顯示是連接成功的。

如何解決?

單執(zhí)行流的服務(wù)器一次只能給一個客戶端提供服務(wù),此時服務(wù)器的資源并沒有得到充分利用,因此服務(wù)器一般是不會寫成單執(zhí)行流的。要解決這個問題就需要將服務(wù)器改為多執(zhí)行流的,此時就要引入多進程或多線程。

二.多進程版的TCP網(wǎng)絡(luò)程序

我們可以將當前的單執(zhí)行流服務(wù)器改為多進程版的服務(wù)器。

當服務(wù)端調(diào)用accept函數(shù)獲取到新連接后不是由當前執(zhí)行流為該連接提供服務(wù),而是當前執(zhí)行流調(diào)用fork函數(shù)創(chuàng)建子進程,然后讓子進程為父進程獲取到的連接提供服務(wù)。

由于父子進程是兩個不同的執(zhí)行流,當父進程調(diào)用fork創(chuàng)建出子進程后,父進程就可以繼續(xù)從監(jiān)聽套接字當中獲取新連接,而不用關(guān)心獲取上來的連接是否服務(wù)完畢。

子進程繼承父進程的文件描述符表

需要注意的是,文件描述符表是隸屬于一個進程的,子進程創(chuàng)建后會繼承父進程的文件描述符表。比如父進程打開了一個文件,該文件對應(yīng)的文件描述符是3,此時父進程創(chuàng)建的子進程的3號文件描述符也會指向這個打開的文件,而如果子進程再創(chuàng)建一個子進程,那么子進程創(chuàng)建的子進程的3號文件描述符也同樣會指向這個打開的文件。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

但當父進程創(chuàng)建子進程后,父子進程之間會保持獨立性,此時父進程文件描述符表的變化不會影響子進程。最典型的代表就是匿名管道,父子進程在使用匿名管道進行通信時,父進程先調(diào)用pipe函數(shù)得到兩個文件描述符,一個是管道讀端的文件描述符,一個是管道寫端的文件描述符,此時父進程創(chuàng)建出來的子進程就會繼承這兩個文件描述符,之后父子進程一個關(guān)閉管道的讀端,另一個關(guān)閉管道的寫端,這時父子進程文件描述符表的變化是不會相互影響的,此后父子進程就可以通過這個管道進行單向通信了。

對于套接字文件也是一樣的,父進程創(chuàng)建的子進程也會繼承父進程的套接字文件,此時子進程就能夠?qū)μ囟ǖ奶捉幼治募M行讀寫操作,進而完成對對應(yīng)客戶端的服務(wù)。

等待子進程問題

當父進程創(chuàng)建出子進程后,父進程是需要等待子進程退出的,否則子進程會變成僵尸進程,進而造成內(nèi)存泄漏。因此服務(wù)端創(chuàng)建子進程后需要調(diào)用wait或waitpid函數(shù)對子進程進行等待。

阻塞式等待與非阻塞式等待:

  • 如果服務(wù)端采用阻塞的方式等待子進程,那么服務(wù)端還是需要等待服務(wù)完當前客戶端,才能繼續(xù)獲取下一個連接請求,此時服務(wù)端仍然是以一種串行的方式為客戶端提供服務(wù)。
  • 如果服務(wù)端采用非阻塞的方式等待子進程,雖然在子進程為客戶端提供服務(wù)期間服務(wù)端可以繼續(xù)獲取新連接,但此時服務(wù)端就需要將所有子進程的PID保存下來,并且需要不斷花費時間檢測子進程是否退出。

總之,服務(wù)端要等待子進程退出,無論采用阻塞式等待還是非阻塞式等待,都不盡人意。此時我們可以考慮讓服務(wù)端不等待子進程退出。

不等待子進程退出的方式

讓父進程不等待子進程退出,常見的方式有兩種:

  • 捕捉SIGCHLD信號,將其處理動作設(shè)置為忽略。
  • 讓父進程創(chuàng)建子進程,子進程再創(chuàng)建孫子進程,最后讓孫子進程為客戶端提供服務(wù)。

1.捕捉SIGCHLD信號

實際當子進程退出時會給父進程發(fā)送SIGCHLD信號,如果父進程將SIGCHLD信號進行捕捉,并將該信號的處理動作設(shè)置為忽略,此時父進程就只需專心處理自己的工作,不必關(guān)心子進程了。

該方式實現(xiàn)起來非常簡單,也是比較推薦的一種做法。

class TcpServer
{
public:
	void Start()
	{
		signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信號
		for (;;){
			//獲取連接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				//處理請求
				Service(sock, client_ip, client_port);
				exit(0); //子進程提供完服務(wù)退出
			}
		}
	}
private:
	int _listen_sock; //監(jiān)聽套接字
	int _port; //端口號
};

代碼測試

重新編譯程序運行服務(wù)端后,可以通過以下監(jiān)控腳本對服務(wù)進程進行監(jiān)控。

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

此時可以看到,一開始沒有客戶端連接該服務(wù)器,此時服務(wù)進程只有一個,該服務(wù)進程就是不斷獲取新連接的進程,而獲取到新連接后也是由該進程創(chuàng)建子進程為對應(yīng)客戶端提供服務(wù)的。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

此時我們運行一個客戶端,讓該客戶端連接服務(wù)器,此時服務(wù)進程就會調(diào)用fork函數(shù)創(chuàng)建出一個子進程,由該子進程為這個客戶端提供服務(wù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

如果再有一個客戶端連接服務(wù)器,此時服務(wù)進程會再創(chuàng)建出一個子進程,讓該子進程為這個客戶端提供服務(wù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

最重要的是,由于這兩個客戶端分別由兩個不同的執(zhí)行流提供服務(wù),因此這兩個客戶端可以同時享受到服務(wù),它們發(fā)送給服務(wù)端的數(shù)據(jù)都能夠在服務(wù)端輸出,并且服務(wù)端也會對它們的數(shù)據(jù)進行響應(yīng)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當客戶端一個個退出后,在服務(wù)端對應(yīng)為之提供服務(wù)的子進程也會相繼退出,但無論如何服務(wù)端都至少會有一個服務(wù)進程,這個服務(wù)進程的任務(wù)就是不斷獲取新連接。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

2.讓孫子進程提供服務(wù)

我們也可以讓服務(wù)端創(chuàng)建出來的子進程再次進行fork,讓孫子進程為客戶端提供服務(wù), 此時我們就不用等待孫子進程退出了。

命名說明:

  • 爺爺進程:在服務(wù)端調(diào)用accept函數(shù)獲取客戶端連接請求的進程。
  • 爸爸進程:由爺爺進程調(diào)用fork函數(shù)創(chuàng)建出來的進程。
  • 孫子進程:由爸爸進程調(diào)用fork函數(shù)創(chuàng)建出來的進程,該進程調(diào)用Service函數(shù)為客戶端提供服務(wù)。

我們讓爸爸進程創(chuàng)建完孫子進程后立刻退出,此時服務(wù)進程(爺爺進程)調(diào)用wait/waitpid函數(shù)等待爸爸進程就能立刻等待成功,此后服務(wù)進程就能繼續(xù)調(diào)用accept函數(shù)獲取其他客戶端的連接請求。

不需要等待孫子進程退出

而由于爸爸進程創(chuàng)建完孫子進程后就立刻退出了,因此實際為客戶端提供服務(wù)的孫子進程就變成了孤兒進程,該進程就會被系統(tǒng)領(lǐng)養(yǎng),當孫子進程為客戶端提供完服務(wù)退出后系統(tǒng)會回收孫子進程,所以服務(wù)進程(爺爺進程)是不需要等待孫子進程退出的。

關(guān)閉對應(yīng)的文件描述符

服務(wù)進程(爺爺進程)調(diào)用accept函數(shù)獲取到新連接后,會讓孫子進程為該連接提供服務(wù),此時服務(wù)進程已經(jīng)將文件描述符表繼承給了爸爸進程,而爸爸進程又會調(diào)用fork函數(shù)創(chuàng)建出孫子進程,然后再將文件描述符表繼承給孫子進程。

而父子進程創(chuàng)建后,它們各自的文件描述符表是獨立的,不會相互影響。因此服務(wù)進程在調(diào)用fork函數(shù)后,服務(wù)進程就不需要再關(guān)心剛才從accept函數(shù)獲取到的文件描述符了,此時服務(wù)進程就可以調(diào)用close函數(shù)將該文件描述符進行關(guān)閉。

同樣的,對于爸爸進程和孫子進程來說,它們是不需要關(guān)心從服務(wù)進程(爺爺進程)繼承下來的監(jiān)聽套接字的,因此爸爸進程可以將監(jiān)聽套接字關(guān)掉。

關(guān)閉文件描述符的必要性:

  • 對于服務(wù)進程來說,當它調(diào)用fork函數(shù)后就必須將從accept函數(shù)獲取的文件描述符關(guān)掉。因為服務(wù)進程會不斷調(diào)用accept函數(shù)獲取新的文件描述符(服務(wù)套接字),如果服務(wù)進程不及時關(guān)掉不用的文件描述符,最終服務(wù)進程中可用的文件描述符就會越來越少。
  • 而對于爸爸進程和孫子進程來說,還是建議關(guān)閉從服務(wù)進程繼承下來的監(jiān)聽套接字。實際就算它們不關(guān)閉監(jiān)聽套接字,最終也只會導(dǎo)致這一個文件描述符泄漏,但一般還是建議關(guān)上。因為孫子進程在提供服務(wù)時可能會對監(jiān)聽套接字進行某種誤操作,此時就會對監(jiān)聽套接字當中的數(shù)據(jù)造成影響。
class TcpServer
{
public:
	void Start()
	{
		for (;;){
			//獲取連接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				close(_listen_sock); //child關(guān)閉監(jiān)聽套接字
				if (fork() > 0){
					exit(0); //爸爸進程直接退出
				}
				//處理請求
				Service(sock, client_ip, client_port); //孫子進程提供服務(wù)
				exit(0); //孫子進程提供完服務(wù)退出
			}
			close(sock); //father關(guān)閉為連接提供服務(wù)的套接字
			waitpid(id, nullptr, 0); //等待爸爸進程(會立刻等待成功)
		}
	}
private:
	int _listen_sock; //監(jiān)聽套接字
	int _port; //端口號
};

服務(wù)器測試

重新編譯程序運行客戶端后,繼續(xù)使用監(jiān)控腳本對服務(wù)進程進行實時監(jiān)控。

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

此時沒有客戶端連接服務(wù)器,因此也是只監(jiān)控到了一個服務(wù)進程,該服務(wù)進程正在等待客戶端的請求連接。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

此時我們運行一個客戶端,讓該客戶端連接當前這個服務(wù)器,此時服務(wù)進程會創(chuàng)建出爸爸進程,爸爸進程再創(chuàng)建出孫子進程,之后爸爸進程就會立刻退出,而由孫子進程為客戶端提供服務(wù)。因此這時我們只看到了兩個服務(wù)進程,其中一個是一開始用于獲取連接的服務(wù)進程,還有一個就是孫子進程,該進程為當前客戶端提供服務(wù),它的PPID為1,表明這是一個孤兒進程。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當我們運行第二個客戶端連接服務(wù)器時,此時就又會創(chuàng)建出一個孤兒進程為該客戶端提供服務(wù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

此時這兩個客戶端是由兩個不同的孤兒進程提供服務(wù)的,因此它們也是能夠同時享受到服務(wù)的,可以看到這兩個客戶端發(fā)送給服務(wù)端的數(shù)據(jù)都能夠在服務(wù)端輸出,并且服務(wù)端也會對它們的數(shù)據(jù)進行響應(yīng)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當客戶端全部退出后,對應(yīng)為客戶端提供服務(wù)的孤兒進程也會跟著退出,這時這些孤兒進程會被系統(tǒng)回收,而最終剩下那個獲取連接的服務(wù)進程。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

三.多線程版的TCP網(wǎng)絡(luò)程序

創(chuàng)建進程的成本是很高的,創(chuàng)建進程時需要創(chuàng)建該進程對應(yīng)的進程控制塊(task_struct)、進程地址空間(mm_struct)、頁表等數(shù)據(jù)結(jié)構(gòu)。而創(chuàng)建線程的成本比創(chuàng)建進程的成本會小得多,因為線程本質(zhì)是在進程地址空間內(nèi)運行,創(chuàng)建出來的線程會共享該進程的大部分資源,因此在實現(xiàn)多執(zhí)行流的服務(wù)器時最好采用多線程進行實現(xiàn)。

當服務(wù)進程調(diào)用accept函數(shù)獲取到一個新連接后,就可以直接創(chuàng)建一個線程,讓該線程為對應(yīng)客戶端提供服務(wù)。

當然,主線程(服務(wù)進程)創(chuàng)建出新線程后,也是需要等待新線程退出的,否則也會造成類似于僵尸進程這樣的問題。但對于線程來說,如果不想讓主線程等待新線程退出,可以讓創(chuàng)建出來的新線程調(diào)用pthread_detach函數(shù)進行線程分離,當這個線程退出時系統(tǒng)會自動回收該線程所對應(yīng)的資源。此時主線程(服務(wù)進程)就可以繼續(xù)調(diào)用accept函數(shù)獲取新連接,而讓新線程去服務(wù)對應(yīng)的客戶端。

各個線程共享同一張文件描述符表

文件描述符表維護的是進程與文件之間的對應(yīng)關(guān)系,因此一個進程對應(yīng)一張文件描述符表。而主線程創(chuàng)建出來的新線程依舊屬于這個進程,因此創(chuàng)建線程時并不會為該線程創(chuàng)建獨立的文件描述符表,所有的線程看到的都是同一張文件描述符表。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

因此當服務(wù)進程(主線程)調(diào)用accept函數(shù)獲取到一個文件描述符后,其他創(chuàng)建的新線程是能夠直接訪問這個文件描述符的。

需要注意的是,雖然新線程能夠直接訪問主線程accept上來的文件描述符,但此時新線程并不知道它所服務(wù)的客戶端對應(yīng)的是哪一個文件描述符,因此主線程創(chuàng)建新線程后需要告訴新線程對應(yīng)應(yīng)該訪問的文件描述符的值,也就是告訴每個新線程在服務(wù)客戶端時,應(yīng)該對哪一個套接字進行操作。

參數(shù)結(jié)構(gòu)體

實際新線程在為客戶端提供服務(wù)時就是調(diào)用Service函數(shù),而調(diào)用Service函數(shù)時是需要傳入三個參數(shù)的,分別是客戶端對應(yīng)的套接字、IP地址和端口號。因此主線程創(chuàng)建新線程時需要給新線程傳入三個參數(shù),而實際在調(diào)用pthread_create函數(shù)創(chuàng)建新線程時,只能傳入一個類型為void*的參數(shù)。

這時我們可以設(shè)計一個參數(shù)結(jié)構(gòu)體Param,此時這三個參數(shù)就可以放到Param結(jié)構(gòu)體當中,當主線程創(chuàng)建新線程時就可以定義一個Param對象,將客戶端對應(yīng)的套接字、IP地址和端口號設(shè)計進這個Param對象當中,然后將Param對象的地址作為新線程執(zhí)行例程的參數(shù)進行傳入。

此時新線程在執(zhí)行例程當中再將這個void*類型的參數(shù)強轉(zhuǎn)為Param*類型,然后就能夠拿到客戶端對應(yīng)的套接字,IP地址和端口號,進而調(diào)用Service函數(shù)為對應(yīng)客戶端提供服務(wù)。

class Param
{
public:
	Param(int sock, std::string ip, int port)
		: _sock(sock)
		, _ip(ip)
		, _port(port)
	{}
	~Param()
	{}
public:
	int _sock;
	std::string _ip;
	int _port;
};

文件描述符關(guān)閉的問題

由于此時所有線程看到的都是同一張文件描述符表,因此當某個線程要對這張文件描述符表做某種操作時,不僅要考慮當前線程,還要考慮其他線程。

  • 對于主線程accept上來的文件描述符,主線程不能對其進行關(guān)閉操作,該文件描述符的關(guān)閉操作應(yīng)該又新線程來執(zhí)行。因為是新線程為客戶端提供服務(wù)的,只有當新線程為客戶端提供的服務(wù)結(jié)束后才能將該文件描述符關(guān)閉。
  • 對于監(jiān)聽套接字,雖然創(chuàng)建出來的新線程不必關(guān)心監(jiān)聽套接字,但新線程不能將監(jiān)聽套接字對應(yīng)的文件描述符關(guān)閉,否則主線程就無法從監(jiān)聽套接字當中獲取新連接了。

Service函數(shù)定義為靜態(tài)成員函數(shù)

由于調(diào)用pthread_create函數(shù)創(chuàng)建線程時,新線程的執(zhí)行例程是一個參數(shù)為void*,返回值為void*的函數(shù)。如果我們要將這個執(zhí)行例程定義到類內(nèi),就需要將其定義為靜態(tài)成員函數(shù),否則這個執(zhí)行例程的第一個參數(shù)是隱藏的this指針。

在線程的執(zhí)行例程當中會調(diào)用Service函數(shù),由于執(zhí)行例程是靜態(tài)成員函數(shù),靜態(tài)成員函數(shù)無法調(diào)用非靜態(tài)成員函數(shù),因此我們需要將Service函數(shù)定義為靜態(tài)成員函數(shù)。恰好Service函數(shù)內(nèi)部進行的操作都是與類無關(guān)的,因此我們直接在Service函數(shù)前面加上一個static即可。

class TcpServer
{
public:
	static void* HandlerRequest(void* arg)
	{
		pthread_detach(pthread_self()); //分離線程
		//int sock = *(int*)arg;
		Param* p = (Param*)arg;

		Service(p->_sock, p->_ip, p->_port); //線程為客戶端提供服務(wù)

		delete p; //釋放參數(shù)占用的堆空間
		return nullptr;
	}
	void Start()
	{
		for (;;){
			//獲取連接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			Param* p = new Param(sock, client_ip, client_port);
			pthread_t tid;
			pthread_create(&tid, nullptr, HandlerRequest, p);
		}
	}
private:
	int _listen_sock; //監(jiān)聽套接字
	int _port; //端口號
};

代碼測試

此時我們再重新編譯服務(wù)端代碼,由于代碼當中用到了多線程,因此編譯時需要攜帶上-pthread選項。此外,由于我們現(xiàn)在要監(jiān)測的是一個個的線程,因此在監(jiān)控時使用的不再是ps -axj命令,而是ps -aL命令。

while :; do ps -aL|head -1&&ps -aL|grep tcp_server;echo "####################";sleep 1;done

運行服務(wù)端,通過監(jiān)控可以看到,此時只有一個服務(wù)線程,該服務(wù)線程就是主線程,它現(xiàn)在在等待客戶端的連接到來。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當一個客戶端連接到服務(wù)端后,此時主線程就會為該客戶端構(gòu)建一個參數(shù)結(jié)構(gòu)體,然后創(chuàng)建一個新線程,將該參數(shù)結(jié)構(gòu)體的地址作為參數(shù)傳遞給這個新線程,此時該新線程就能夠從這個參數(shù)結(jié)構(gòu)體當中提取出對應(yīng)的參數(shù),然后調(diào)用Service函數(shù)為該客戶端提供服務(wù),因此在監(jiān)控當中顯示了兩個線程。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當?shù)诙€客戶端發(fā)來連接請求時,主線程會進行相同的操作,最終再創(chuàng)建出一個新線程為該客戶端提供服務(wù),此時服務(wù)端當中就有了三個線程。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

由于為這兩個客戶端提供服務(wù)的也是兩個不同的執(zhí)行流,因此這兩個客戶端可以同時享受服務(wù)端提供的服務(wù),它們發(fā)送給服務(wù)端的消息也都能夠在服務(wù)端進行打印,并且這兩個客戶端也都能夠收到服務(wù)端的回顯數(shù)據(jù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

此時無論有多少個客戶端發(fā)來連接請求,在服務(wù)端都會創(chuàng)建出相應(yīng)數(shù)量的新線程為對應(yīng)客戶端提供服務(wù),而當客戶端一個個退出后,為其提供服務(wù)的新線程也就會相繼退出,最終就只剩下最初的主線程仍在等待新連接的到來。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

四.線程池版的TCP網(wǎng)絡(luò)程序

單純多線程存在的問題

當前多線程版的服務(wù)器存在的問題:

  • 每當有新連接到來時,服務(wù)端的主線程都會重新為該客戶端創(chuàng)建為其提供服務(wù)的新線程,而當服務(wù)結(jié)束后又會將該新線程銷毀。這樣做不僅麻煩,而且效率低下,每當連接到來的時候服務(wù)端才創(chuàng)建對應(yīng)提供服務(wù)的線程。
  • 如果有大量的客戶端連接請求,此時服務(wù)端要為每一個客戶端創(chuàng)建對應(yīng)的服務(wù)線程。計算機當中的線程越多,CPU的壓力就越大,因為CPU要不斷在這些線程之間來回切換,此時CPU在調(diào)度線程的時候,線程和線程之間切換的成本就會變得很高。此外,一旦線程太多,每一個線程再次被調(diào)度的周期就變長了,而線程是為客戶端提供服務(wù)的,線程被調(diào)度的周期變長,客戶端也遲遲得不到應(yīng)答。

解決思路

針對這兩個問題,對應(yīng)的解決思路如下:

  • 可以在服務(wù)端預(yù)先創(chuàng)建一批線程,當有客戶端請求連接時就讓這些線程為客戶端提供服務(wù),此時客戶端一來就有線程為其提供服務(wù),而不是當客戶端來了才創(chuàng)建對應(yīng)的服務(wù)線程。
  • 當某個線程為客戶端提供完服務(wù)后,不要讓該線程退出,而是讓該線程繼續(xù)為下一個客戶端提供服務(wù),如果當前沒有客戶端連接請求,則可以讓該線程先進入休眠狀態(tài),當有客戶端連接到來時再將該線程喚醒。
  • 服務(wù)端創(chuàng)建的這一批線程的數(shù)量不能太多,此時CPU的壓力也就不會太大。此外,如果有客戶端連接到來,但此時這一批線程都在給其他客戶端提供服務(wù),這時服務(wù)端不應(yīng)該再創(chuàng)建線程,而應(yīng)該讓這個新來的連接請求在全連接隊列進行排隊,等服務(wù)端這一批線程中有空閑線程后,再將該連接請求獲取上來并為其提供服務(wù)。

引入線程池

實際要解決這里的問題我們就需要在服務(wù)端引入線程池,因為線程池的存在就是為了避免處理短時間任務(wù)時創(chuàng)建與銷毀線程的代價,此外,線程池還能夠保證內(nèi)核充分利用,防止過分調(diào)度。

其中在線程池里面有一個任務(wù)隊列,當有新的任務(wù)到來的時候,就可以將任務(wù)Push到線程池當中,在線程池當中我們默認創(chuàng)建了5個線程,這些線程不斷檢測任務(wù)隊列當中是否有任務(wù),如果有任務(wù)就拿出任務(wù),然后調(diào)用該任務(wù)對應(yīng)的Run函數(shù)對該任務(wù)進行處理,如果線程池當中沒有任務(wù)那么當前線程就會進入休眠狀態(tài)。

在博主的另一篇博客當中詳細介紹并實現(xiàn)了線程池,這里就直接將線程池的代碼接入到當前的TCP服務(wù)器,因此下面只會講解線程池接入的方法,如果對線程池的實現(xiàn)有疑問的可以去閱讀那篇博客。

#define NUM 5

//線程池
template<class T>
class ThreadPool
{
private:
	bool IsEmpty()
	{
		return _task_queue.size() == 0;
	}
	void LockQueue()
	{
		pthread_mutex_lock(&_mutex);
	}
	void UnLockQueue()
	{
	    pthread_mutex_unlock(&_mutex);
	}
	void Wait()
	{
	    pthread_cond_wait(&_cond, &_mutex);
	}
	void WakeUp()
	{
	    pthread_cond_signal(&_cond);
	}
public:
	ThreadPool(int num = NUM)
		: _thread_num(num)
	{
		pthread_mutex_init(&_mutex, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	~ThreadPool()
	{
	    pthread_mutex_destroy(&_mutex);
	    pthread_cond_destroy(&_cond);
	}
	//線程池中線程的執(zhí)行例程
	static void* Routine(void* arg)
	{
	    pthread_detach(pthread_self());
	    ThreadPool* self = (ThreadPool*)arg;
	    //不斷從任務(wù)隊列獲取任務(wù)進行處理
		while (true){
			self->LockQueue();
			while (self->IsEmpty()){
				self->Wait();
			}
			T task;
			self->Pop(task);
			self->UnLockQueue();
			
			task.Run(); //處理任務(wù)
		}
	}
	void ThreadPoolInit()
	{
		pthread_t tid;
		for (int i = 0; i < _thread_num; i++){
			pthread_create(&tid, nullptr, Routine, this); //注意參數(shù)傳入this指針
		}
	}
	//往任務(wù)隊列塞任務(wù)(主線程調(diào)用)
	void Push(const T& task)
	{
	    LockQueue();
	    _task_queue.push(task);
	    UnLockQueue();
	    WakeUp();
	}
	//從任務(wù)隊列獲取任務(wù)(線程池中的線程調(diào)用)
	void Pop(T& task)
	{
	    task = _task_queue.front();
	    _task_queue.pop();
	}
	
private:
	std::queue<T> _task_queue; //任務(wù)隊列
	int _thread_num; //線程池中線程的數(shù)量
	pthread_mutex_t _mutex;
	pthread_cond_t _cond;
};

服務(wù)類新增線程池成員

現(xiàn)在服務(wù)端引入了線程池,因此在服務(wù)類當中需要新增一個指向線程池的指針成員:

  • 當實例化服務(wù)器對象時,先將這個線程池指針先初始化為空。
  • 當服務(wù)器初始化完畢后,再實際構(gòu)造這個線程池對象,在構(gòu)造線程池對象時可以指定線程池當中線程的個數(shù),也可以不指定,此時默認線程的個數(shù)為5。
  • 在啟動服務(wù)器之前對線程池進行初始化,此時就會將線程池當中的若干線程創(chuàng)建出來,而這些線程創(chuàng)建出來后就會不斷檢測任務(wù)隊列,從任務(wù)隊列當中拿出任務(wù)進行處理。
    現(xiàn)在當服務(wù)進程調(diào)用accept函數(shù)獲取到一個連接請求后,就會根據(jù)該客戶端的套接字、IP地址以及端口號構(gòu)建出一個任務(wù),然后調(diào)用線程池提供的Push接口將該任務(wù)塞入任務(wù)隊列。

這實際也是一個生產(chǎn)者消費者模型,其中服務(wù)進程就作為了任務(wù)的生產(chǎn)者,而后端線程池當中的若干線程就不斷從任務(wù)隊列當中獲取任務(wù)進行處理,它們承擔(dān)的就是消費者的角色,其中生產(chǎn)者和消費者的交易場所就是線程池當中的任務(wù)隊列。。

class TcpServer
{
public:
	TcpServer(int port)
		: _listen_sock(-1)
		, _port(port)
		, _tp(nullptr)
	{}
	void InitServer()
	{
		//創(chuàng)建套接字
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
		//綁定
		struct sockaddr_in local;
		memset(&local, '\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;

		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
			std::cerr << "bind error" << std::endl;
			exit(3);
		}
		//監(jiān)聽
		if (listen(_listen_sock, BACKLOG) < 0){
			std::cerr << "listen error" << std::endl;
			exit(4);
		}

		_tp = new ThreadPool<Task>(); //構(gòu)造線程池對象
	}
	void Start()
	{
		_tp->ThreadPoolInit(); //初始化線程池
		for (;;){
			//獲取連接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			Task task(sock, client_ip, client_port); //構(gòu)造任務(wù)
			_tp->Push(task); //將任務(wù)Push進任務(wù)隊列
		}
	}
private:
	int _listen_sock; //監(jiān)聽套接字
	int _port; //端口號
	ThreadPool<Task>* _tp; //線程池
};

設(shè)計任務(wù)類

現(xiàn)在我們要做的就是設(shè)計一個任務(wù)類,該任務(wù)類當中需要包含客戶端對應(yīng)的套接字、IP地址、端口號,表示該任務(wù)是為哪一個客戶端提供服務(wù),對應(yīng)操作的套接字是哪一個。

此外,任務(wù)類當中需要包含一個Run方法,當線程池中的線程拿到任務(wù)后就會直接調(diào)用這個Run方法對該任務(wù)進行處理,而實際處理這個任務(wù)的方法就是服務(wù)類當中的Service函數(shù),服務(wù)端就是通過調(diào)用Service函數(shù)為客戶端提供服務(wù)的。

我們可以直接拿出服務(wù)類當中的Service函數(shù),將其放到任務(wù)類當中作為任務(wù)類當中的Run方法,但這實際不利于軟件分層。我們可以給任務(wù)類新增一個仿函數(shù)成員,當執(zhí)行任務(wù)類當中的Run方法處理任務(wù)時就可以以回調(diào)的方式處理該任務(wù)。

class Task
{
public:
	Task()
	{}
	Task(int sock, std::string client_ip, int client_port)
		: _sock(sock)
		, _client_ip(client_ip)
		, _client_port(client_port)
	{}
	~Task()
	{}
	//任務(wù)處理函數(shù)
	void Run()
	{
	    _handler(_sock, _client_ip, _client_port); //調(diào)用仿函數(shù)
	}
private:
	int _sock; //套接字
	std::string _client_ip; //IP地址
	int _client_port; //端口號
	Handler _handler; //處理方法
};

注意: 當任務(wù)隊列當中有任務(wù)時,線程池當中的線程會先定義出一個Task對象,然后將這個Task對象作為輸出型參數(shù)調(diào)用任務(wù)隊列的Pop函數(shù),從任務(wù)隊列當中獲取任務(wù),因此Task類除了提供帶參的構(gòu)造函數(shù)以外,還需要提供一個無參的構(gòu)造函數(shù),方便我們可以定義無參對象。

設(shè)計Handler類

此時需要再設(shè)計一個Handler類,在Handler類當中對()操作符進行重載,將()操作符的執(zhí)行動作重載為執(zhí)行Service函數(shù)的代碼。

class Handler
{
public:
	Handler()
	{}
	~Handler()
	{}
	void operator()(int sock, std::string client_ip, int client_port)
	{
		char buffer[1024];
		while (true){
			ssize_t size = read(sock, buffer, sizeof(buffer)-1);
			if (size > 0){ //讀取成功
				buffer[size] = '\0';
				std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;

				write(sock, buffer, size);
			}
			else if (size == 0){ //對端關(guān)閉連接
				std::cout << client_ip << ":" << client_port << " close!" << std::endl;
				break;
			}
			else{ //讀取失敗
				std::cerr << sock << " read error!" << std::endl;
				break;
			}
		}
		close(sock); //歸還文件描述符
		std::cout << client_ip << ":" << client_port << " service done!" << std::endl;
	}
};

實際我們可以讓服務(wù)器處理不同的任務(wù),當前服務(wù)器只是在進行字符串的回顯處理,而實際要怎么處理這個任務(wù)完全是由任務(wù)類當中的handler成員來決定的。

如果想要讓服務(wù)器處理其他任務(wù),只需要修改Handler類當中對()的重載函數(shù)就行了,而服務(wù)器的初始化、啟動服務(wù)器以及線程池的代碼都是不需要更改的,這就叫做把通信功能和業(yè)務(wù)邏輯在軟件上做解耦。

代碼測試

此時我們再重新編譯服務(wù)端代碼,并用以下監(jiān)控腳本查看服務(wù)端的各個線程。

while :; do ps -aL|head -1&&ps -aL|grep tcp_server;echo "####################";sleep 1;done

運行服務(wù)端后,就算沒有客戶端發(fā)來連接請求,此時在服務(wù)端就已經(jīng)有了6個線程,其中有一個是接收新連接的服務(wù)線程,而其余的5個是線程池當中為客戶端提供服務(wù)的線程。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

此時當客戶端連接服務(wù)器后,服務(wù)端的主線程就會獲取該客戶端的連接請求,并將其封裝為一個任務(wù)對象后塞入任務(wù)隊列,此時線程池中的5個線程就會有一個線程從任務(wù)隊列當中獲取到該任務(wù),并執(zhí)行該任務(wù)的處理函數(shù)為客戶端提供服務(wù)。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

當?shù)诙€客戶端發(fā)起連接請求時,服務(wù)端也會將其封裝為一個任務(wù)類塞到任務(wù)隊列,然后線程池當中的線程再從任務(wù)隊列當中獲取到該任務(wù)進行處理,此時也是不同的執(zhí)行流為這兩個客戶端提供的服務(wù),因此這兩個客戶端也是能夠同時享受服務(wù)的。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

與之前不同的是,無論現(xiàn)在有多少客戶端發(fā)來請求,在服務(wù)端都只會有線程池當中的5個線程為之提供服務(wù),線程池當中的線程個數(shù)不會隨著客戶端連接的增多而增多,這些線程也不會因為客戶端的退出而退出。

五.守護進程

1.進程知識補充

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

進程組

  • 進程組就是一個或多個進程的集合,每個進程除了有一個PID外,還屬于一個進程組。
  • 每一個進程組,都有一個唯一的標識PGID,屬于同一個進程組的進程其PGID相同。
  • 進程組中的第一個進程作為組長進程,將其PID作為進程組的PGID

如下,我們同時啟動了3個后臺進程,它們屬于同一進程組,進程組中的第一個進程PID=14378的進程作為組長進程

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

任務(wù):啟動一個進程就是啟動一個任務(wù)

  • 前臺任務(wù):通過終端啟動,并且在啟動后一直占據(jù)終端

  • 后臺任務(wù):啟動時與終端無關(guān),或者通過終端啟動后轉(zhuǎn)入后臺運行(即釋放終端),不影響用戶繼續(xù)在終端中工作

在我們每次登錄XShell后,bash會默認占據(jù)前臺任務(wù),也就是命令行解釋器shell(即占用終端的控制權(quán));當把進程任務(wù)自動切換為前臺任務(wù)時,shell自動切換為后臺任務(wù),我們輸入的命令就無效了

任務(wù)管理命令(Shell中控制進程組的方式):

  • jobs:查看所有任務(wù)
    網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

  • fg:把任務(wù)提到前臺,此時這個任務(wù)就會變成前臺任務(wù),shell自動切換為后臺任務(wù),命令行解釋器失效
    網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

  • ctrl+z:暫停前臺任務(wù)
    網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

  • bg:讓暫停的任務(wù)在后臺繼續(xù)運行
    網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

會話

Linux是多用戶多任務(wù)的分時系統(tǒng),所以必須要支持多個用戶同時使用一個操作系統(tǒng)。當一個用戶登錄一次系統(tǒng)就形成一次會話。在一個會話中,用戶可以與系統(tǒng)進行交互,執(zhí)行命令、操作文件、啟動程序等。

比如,我們先啟動3個后臺進程,在啟動3個前臺進程,后獲取這些進程的信息

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

  • 這些進程分別屬于2個進程組,它們與同一個終端關(guān)聯(lián),屬于同一個會話
  • 啟動進程就是啟動任務(wù),在每一次登錄系統(tǒng)會為我們創(chuàng)建一次會話,會話里至少有bash任務(wù)進行命令行解釋
  • 命令行里可以啟動多個任務(wù),每個任務(wù)最終以進程組的形式在會話里存在。
  • 所以一個會話里可能存在很多進程組
  • 大小概念:會話 >= 進程組 >= 進程

2.守護進程

概念

  • 進程組分為:前臺任務(wù)和后臺任務(wù)
  • 如果把后臺任務(wù)提到前臺,則老的前臺任務(wù)無法運行
  • 在會話中只能有一個前臺任務(wù)在運行,所以當我們在命令行啟動一個進程的時候,bash就無法運行了
  • 如果登錄就是創(chuàng)建一個會話,bash任務(wù)啟動我們的進程就是在當前會話中創(chuàng)建新的前后臺任務(wù),那么我們?nèi)绻顺瞿兀烤蜁N毀會話,可能會影響會話內(nèi)部的所有任務(wù)!

我們之前的服務(wù)器都是這樣的:
每一個服務(wù)端進程組與bash進程組同屬于一個會話,再次登錄Xshell啟動服務(wù)端就會創(chuàng)建新的會話

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

可是一般的網(wǎng)絡(luò)服務(wù)器,為了不受到用戶的登錄注銷的影響,就必須讓服務(wù)端自成進程組,自成會話,使其與終端的狀態(tài)無關(guān),可以一直運行的進程,這樣的進程就稱作守護進程

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

創(chuàng)建守護進程

我們用的setsid函數(shù)自己實現(xiàn)守護進程,不使用linux自帶生成守護進程的接口daemon

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

creates a session and sets the process group ID: 創(chuàng)建會話并設(shè)置進程組ID

這個函數(shù)的使用關(guān)鍵:調(diào)用的進程不能是組長進程

守護進程的創(chuàng)建步驟:

  1. 讓調(diào)用進程忽略掉異常信號
  2. fork()創(chuàng)建子進程,讓父進程直接退出,自己不再是組長進程
  3. 調(diào)用setsid,新建會話, 子進程自成進程組,成為會話的首進程
  4. 將標準輸入、輸出和錯誤重定向到/dev/null中
  5. 調(diào)用close關(guān)閉文件描述符,防止守護進程與終端或其他進程的關(guān)聯(lián)
  6. 調(diào)用setsid,新建會話, 自己成為會話的首進程

dev/null是linux下的特殊文件,會對寫入的內(nèi)容進行丟棄,通常被用作丟棄不需要的輸出或測試程序在遇到寫入錯誤時的行為。

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

#pragma once

#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "log.hpp"
#include "err.hpp"

// 1. setsid();
// 2. setsid(), 調(diào)用進程,不能是組長! 我們怎么保證自己不是組長呢?
// 3. 守護進程, 忽略異常信號 b. 0, 1, 2要特殊處理 c. 進程的工作路徑可能要更改

// 守護進程的本質(zhì): 是孤兒進程的一種
void Daemon()
{
    // 1. 忽略信號
    signal(SIGPIPE,SIG_IGN);
    signal(SIGCHLD,SIG_IGN);

    // 2. 讓自己不要成為組長
    if(fork()>0)      // 父進程直接退出
        exit(0);

    // 3. 新建會話, 自己成為會話的首進程
    pid_t ret=setsid();
    if((int)ret==-1)
    {
        logMessage(Fatal,"deamon error, code: %d, error string: %s",errno,                                  strerror(errno));
        exit(SET_ERR);
    }

    // 4. 可選: 可以更改守護進程的工作路徑
    // chdir("/");

    // 5. 處理后續(xù)對于0,1,2的問題  --- /dev/null 文件就像垃圾桶
    int fd=open("/dev/null",O_RDWR);  
    if(fd<0)
    {
        logMessage(Fatal,"open error, code: %d, error string: %s",errno,strerror(errno));
        exit(SET_ERR);
    }

    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
    close(fd);
}

給服務(wù)端加上該代碼

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

運行結(jié)果:

網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序,Linux網(wǎng)絡(luò)編程,網(wǎng)絡(luò),tcp/ip,網(wǎng)絡(luò)協(xié)議

  • ?表示該進程與終端無關(guān),我們已經(jīng)讓此服務(wù)端以守護進程的方式運行
  • 該進程的PPID為1,說明OS領(lǐng)養(yǎng)了守護進程,守護進程本質(zhì)是孤兒進程的一種

本文到此結(jié)束,碼文不易,還請多多支持哦?。?!文章來源地址http://www.zghlxwxcb.cn/news/detail-742066.html

到了這里,關(guān)于網(wǎng)絡(luò)編程套接字(2)——簡單的TCP網(wǎng)絡(luò)程序的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 網(wǎng)絡(luò)編程套接字( TCP )

    網(wǎng)絡(luò)編程套接字( TCP )

    目錄 1、實現(xiàn)一個TCP網(wǎng)絡(luò)程序(單進程版) ????????1.1、服務(wù)端serverTcp.cc文件 ?????????????????服務(wù)端創(chuàng)建套接字 ?????????????????服務(wù)端綁定 ?????????????????服務(wù)端監(jiān)聽 ?????????????????服務(wù)端獲取連接 ?????????????????服務(wù)

    2024年01月17日
    瀏覽(1815)
  • 【JaveEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    【JaveEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    目錄 1.網(wǎng)絡(luò)編程的基本概念 1.1為什么需要網(wǎng)絡(luò)編程? 1.2服務(wù)端與用戶端 1.3網(wǎng)絡(luò)編程五元組? 1.4套接字的概念 2.UDP套接字編程 2.1UDP套接字的特點 ?2.2UDP套接字API 2.2.1DatagramSocket類 2.2.2DatagramPacket類? 2.2.3基于UDP的回顯程序 2.2.4基于UDP的單詞查詢? 3.TCP套接字編程 3.1TCP套接字的特

    2023年04月13日
    瀏覽(915)
  • 【JavaEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    【JavaEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    目錄 1.網(wǎng)絡(luò)編程的基本概念 1.1為什么需要網(wǎng)絡(luò)編程? 1.2服務(wù)端與用戶端 1.3網(wǎng)絡(luò)編程五元組? 1.4套接字的概念 2.UDP套接字編程 2.1UDP套接字的特點 ?2.2UDP套接字API 2.2.1DatagramSocket類 2.2.2DatagramPacket類? 2.2.3基于UDP的回顯程序 2.2.4基于UDP的單詞查詢? 3.TCP套接字編程 3.1TCP套接字的特

    2023年04月20日
    瀏覽(119)
  • 【網(wǎng)絡(luò)編程】網(wǎng)絡(luò)編程套接字(三)TCP網(wǎng)絡(luò)程序

    【網(wǎng)絡(luò)編程】網(wǎng)絡(luò)編程套接字(三)TCP網(wǎng)絡(luò)程序

    與前邊的UDP網(wǎng)絡(luò)程序相同,創(chuàng)建套接字的接口都是socket,下邊對socket接口進行介紹: 協(xié)議家族選擇AF_INET,因為我們要進行網(wǎng)絡(luò)通信。 而第二個參數(shù),為服務(wù)類型,傳入SOCK_STREAM,我們編寫TCP程序,所以要選擇流式的服務(wù)。 第三個參數(shù)默認傳入0,由前兩個參數(shù)就可以推出這是

    2024年02月16日
    瀏覽(91)
  • 【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP)

    【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP)

    目錄 地址轉(zhuǎn)換函數(shù) 字符串IP轉(zhuǎn)整數(shù)IP 整數(shù)IP轉(zhuǎn)字符串IP 關(guān)于inet_ntoa 簡單的單執(zhí)行流TCP網(wǎng)絡(luò)程序 TCP socket API 詳解及封裝TCP socket? 服務(wù)端創(chuàng)建套接字? 服務(wù)端綁定? 服務(wù)端監(jiān)聽? 服務(wù)端獲取連接? 服務(wù)端處理請求 客戶端創(chuàng)建套接字 客戶端連接服務(wù)器 客戶端發(fā)起請求 服務(wù)器測試

    2024年03月21日
    瀏覽(112)
  • 【Linux】網(wǎng)絡(luò)---->套接字編程(TCP)

    【Linux】網(wǎng)絡(luò)---->套接字編程(TCP)

    TCP的編程流程:大致可以分為五個過程,分別是準備過程、連接建立過程、獲取新連接過程、消息收發(fā)過程和斷開過程。 1.準備過程:服務(wù)端和客戶端需要創(chuàng)建各自的套接字,除此之外服務(wù)端還需要綁定自己的地址信息和進行監(jiān)聽。注意:服務(wù)端調(diào)用listen函數(shù)后,處理監(jiān)聽狀

    2024年02月04日
    瀏覽(101)
  • Linux網(wǎng)絡(luò)編程——tcp套接字

    Linux網(wǎng)絡(luò)編程——tcp套接字

    本章Gitee倉庫:tcp套接字 客戶端: 客戶端: 關(guān)于構(gòu)造和初始化,可以直接在構(gòu)造的時候,將服務(wù)器初始化,那為什么還要寫到 init 初始化函數(shù)里面呢? 構(gòu)造盡量簡單一點,不要做一些“有風(fēng)險”的操作。 tcp 是面向連接的,通信之前要建立連接,服務(wù)器處于等待連接到來的

    2024年02月20日
    瀏覽(95)
  • 網(wǎng)絡(luò)編程套接字之三【TCP】

    網(wǎng)絡(luò)編程套接字之三【TCP】

    目錄 1. ServerSocket API(給服務(wù)器端使用的類) 2. Socket API(既給服務(wù)器使用,也給客戶端使用) 3. 寫TCP回顯—服務(wù)器 4. 使用線程池后的TCP服務(wù)器代碼(最終) 5. 寫回顯-客戶端 6. TCP回顯—客戶端代碼 7. 運行回顯服務(wù)器和客戶端 TCP流套接字編程 ?ServerSocket 是創(chuàng)建TCP服務(wù)端Socket的

    2024年01月19日
    瀏覽(91)
  • 計算機網(wǎng)絡(luò)套接字編程實驗-TCP多進程并發(fā)服務(wù)器程序與單進程客戶端程序(簡單回聲)

    1.實驗系列 ·Linux NAP-Linux網(wǎng)絡(luò)應(yīng)用編程系列 2.實驗?zāi)康?·理解多進程(Multiprocess)相關(guān)基本概念,理解父子進程之間的關(guān)系與差異,熟練掌握基于fork()的多進程編程模式; ·理解僵尸進程產(chǎn)生原理,能基于|sigaction()或signal(),使用waitpid()規(guī)避僵尸進程產(chǎn)生; ·

    2024年02月12日
    瀏覽(36)
  • 計算機網(wǎng)絡(luò)套接字編程實驗-TCP單進程循環(huán)服務(wù)器程序與單進程客戶端程序(簡單回聲)

    1.實驗系列 ·Linux NAP-Linux網(wǎng)絡(luò)應(yīng)用編程系列 2.實驗?zāi)康?·理解并掌握在程序運行時從命令行讀取數(shù)據(jù)的C語言編程方法; ·理解并掌握基于命令參數(shù)設(shè)置并獲取IP與Port的C語言編程方法; ·理解并掌握套接字地址的數(shù)據(jù)結(jié)構(gòu)定義與地址轉(zhuǎn)換函數(shù)應(yīng)用; ·理解并掌握網(wǎng)絡(luò)字節(jié)序

    2024年02月11日
    瀏覽(43)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包