「前言」文章是關(guān)于網(wǎng)絡(luò)編程的socket套接字方面的,上一篇是網(wǎng)絡(luò)編程socket套接字(三),這篇續(xù)上篇文章的內(nèi)容,下面開始講解!?
「歸屬專欄」網(wǎng)絡(luò)編程
「主頁鏈接」個人主頁
「筆者」楓葉先生(fy)
「楓葉先生有點文青病」「句子分享」
Time?goes?on?and?on,?never?to?an?end?but?crossings.
時間一直走,沒有盡頭,只有路口。?
?——克萊爾·麥克?!稊[渡人》
目錄
五、增加日志功能
六、Linux任務(wù)管理與守護進程
6.1 任務(wù)管理
6.1.1 進程組
6.1.2 作業(yè)概念
6.1.3?會話概念
6.1.4 操作
6.2 守護進程
6.2.1 概念
6.2.2 查看
6.2.3 創(chuàng)建守護進程
七、TCP協(xié)議通訊流程
7.1 三次握手
7.2?數(shù)據(jù)傳輸
7.3 四次揮手
7.4?TCP 和 UDP 對比
五、增加日志功能
文章續(xù)上篇文章的內(nèi)容,給服務(wù)器增加日志功能,即把打印到顯示臺的內(nèi)容,分等級打印到不同等級的文件里面
日志分為五個等級
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
?其中DEBUG、NORMAL、WARNING歸類到一個文件里面,剩下的ERROR、FATAL歸類到另一個文件里面
日志代碼如下?
log.hpp?
#pragma once
#include <iostream>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>
using namespace std;
#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *to_levelstr(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case NORMAL:
return "NORMAL";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
// [日志等級] [時間] [pid] [message]
#define NUM 1024
time_t now = time(nullptr); // 獲取當(dāng)前時間
tm *localTime = localtime(&now); // 將時間轉(zhuǎn)換為結(jié)構(gòu)體
char timeStr[NUM];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", localTime); // 格式化時間為指定格式
char logprefix[NUM];
// 使用格式化后的時間字符串組裝日志前綴
snprintf(logprefix, sizeof(logprefix), "[%s][%s][pid: %d]",
to_levelstr(level), timeStr, getpid());
char logcontent[NUM];
va_list arg;
va_start(arg, format);
vsnprintf(logcontent, sizeof(logcontent), format, arg);
FILE *log = fopen(LOG_NORMAL, "a");
FILE *err = fopen(LOG_ERR, "a");
if (log != nullptr && err != nullptr)
{
FILE *curr = nullptr;
if (level == DEBUG || level == NORMAL || level == WARNING)
curr = log;
if (level == ERROR || level == FATAL)
curr = err;
if (curr)
fprintf(curr, "%s%s\n", logprefix, logcontent);
fclose(log);
fclose(err);
}
}
?其他代碼就不貼了,上傳到Gitee了
Gitee:code_linux/code_202306_16/2_tcp/4_tcpthpool · Maple_fylqh/code - 碼云 - 開源中國 (gitee.com)
測試結(jié)果,服務(wù)器運行,就已經(jīng)把日志打印到文件里面了
六、Linux任務(wù)管理與守護進程
關(guān)閉shell之后,我們運行的進程也跟著銷毀了。即我們運行的服務(wù)器也隨之銷毀,這顯然是不合理了,所以這并不是服務(wù)器真正運行的樣子。所以,下面要解決的就是這個問題,順便介紹Linux任務(wù)管理與守護進程
關(guān)閉第一個shell,該進程的信息已經(jīng)查不到了,說明該進程已經(jīng)銷毀了
??
6.1 任務(wù)管理
6.1.1 進程組
每個進程除了有一個進程ID之外,還屬于一個進程組,進程組是一個或多個進程的集合。
進程組(Process Group)是一組具有相同進程組ID(PGID)的進程的集合。每個進程組都有一個唯一的PGID,用于標(biāo)識進程組。
進程組的主要作用是將一組相關(guān)的進程組織在一起,以便可以對它們進行集體操作。例如,可以向進程組發(fā)送信號,以便同時影響組內(nèi)的所有進程。進程組還可以用于實現(xiàn)作業(yè)控制,其中一個進程組被分配為前臺作業(yè),其他進程組被分配為后臺作業(yè)。?
在Linux系統(tǒng)中,進程組的ID是由內(nèi)核分配的,進程組的ID范圍為正整數(shù)。進程組的ID為0的特殊進程組被稱為“無效進程組”,用于標(biāo)識沒有有效進程組的進程。
需要注意的是,只要在某個進程組中有一個進程存在,則該進程組就存在,這與其組長進程是否終止無關(guān)。
例如,這里的 PGID 就是進程組
6.1.2 作業(yè)概念
在Linux中,作業(yè)(Job)是指在終端或終端仿真器中運行的一個或多個命令的集合
作業(yè)分前臺作業(yè)和后臺作業(yè):
- 前臺作業(yè)(Foreground Job):在終端中直接運行的命令或程序,默認(rèn)情況下,前臺作業(yè)會占用終端的控制權(quán),并且會將輸出直接顯示在終端上
- 后臺作業(yè)(Background Job):在命令的末尾添加&符號,可以將命令放到后臺運行,不會占用終端的控制權(quán),并且會將輸出重定向到一個文件或/dev/null
注:一個前臺作業(yè)可以由多個進程或線程組成,一個后臺作業(yè)也可以由多個進程或線程組成
前臺任務(wù)只能有一個,后臺任務(wù)可以有多個或者沒有?
默認(rèn)情況下,我們登錄 Xshell后,bash會默認(rèn)占據(jù)前臺任務(wù),也就是命令行解釋器shell(即占用終端的控制權(quán))
比如,我們隨便運行一個不會退出的程序,比如上面的服務(wù)端程序
該進程任務(wù)自動切換為前臺任務(wù),shell自動切換為后臺任務(wù),我們輸入的命令就無效了
Linux提供了一些作業(yè)控制命令來管理和控制作業(yè):
- jobs:查看當(dāng)前終端中運行的作業(yè)列表。
- fg:將后臺作業(yè)切換到前臺運行。
- bg:將后臺作業(yè)切換到后臺繼續(xù)運行。
- kill:終止指定作業(yè)的運行。
作業(yè)與進程組的區(qū)別:
如果作業(yè)中的某個進程又創(chuàng)建了子進程,則子進程不屬于作業(yè)。一旦作業(yè)運行結(jié)束,Shell就把自己提到前臺,如果原來的前臺進程還存在,也就是這個被創(chuàng)建的子進程還沒有終止,那么它將自動變?yōu)楹笈_進程組
6.1.3?會話概念
在Linux中,會話(Session)是指從用戶登錄到系統(tǒng)開始,到用戶退出系統(tǒng)結(jié)束的整個時間段。
也就是說一個用戶進行登錄Linux,Linux系統(tǒng)就會分配一個會話給我們,直到我們主動退出這個會話。在一個會話中,用戶可以與系統(tǒng)進行交互,執(zhí)行命令、操作文件、啟動程序等
6.1.4 操作
先創(chuàng)建幾個進程組,為了方便直接用 sleep 代替應(yīng)用程序?
在命令的末尾添加&符號,可以將命令放到后臺運行,不會占用終端的控制權(quán),此時命令行依舊生效
??
?查看一下進程信息,進程組的PGID相同代表的是在同一個進程組
使用?jobs:查看當(dāng)前終端中運行的作業(yè)列表,例如
其中,前面的序號就是任務(wù)編號,用于辨別多個任務(wù)
??
使用 fg?命令可以將后臺作業(yè)切換到前臺運行,后面帶上作業(yè)的編號
由于1號作業(yè)被提至前臺運行,所以其運行狀態(tài)也由S變成了S+,+ 就是代表是前臺任務(wù)
注意:前臺進程只能有一個,當(dāng)一個進程變成前臺進程后,bash會自動變?yōu)楹笈_進程,此時bash就無法進行命令行解釋了?
將一個前臺進程放到后臺運行可以使用Ctrl+Z,但使用Ctrl+Z后該進程就會處于停止?fàn)顟B(tài)(Stopped)
bg:將后臺作業(yè)切換到后臺繼續(xù)運行。?可以讓某個停止的作業(yè)在后臺繼續(xù)運行(Running)
6.2 守護進程
6.2.1 概念
在Linux系統(tǒng)中,守護進程(Daemon Process)是在后臺運行的一種特殊類型的進程。它與用戶交互的終端分離,通常在系統(tǒng)啟動時自動啟動,并在系統(tǒng)運行期間持續(xù)運行,直到系統(tǒng)關(guān)閉或手動停止。守護進程也稱精靈進程,本質(zhì)是孤兒進程的一種
?守護進程通常用于執(zhí)行一些需要持續(xù)運行的任務(wù),比如網(wǎng)絡(luò)服務(wù)、系統(tǒng)監(jiān)控、定時任務(wù)等。與普通進程不同,守護進程沒有終端與之關(guān)聯(lián),也沒有用戶交互。它們在后臺默默地運行,執(zhí)行系統(tǒng)任務(wù),并通過日志文件記錄運行狀態(tài)和輸出信息。
6.2.2 查看
可以用?ps axj
命令查看系統(tǒng)中的進程:
- 參數(shù)a表示不僅列出當(dāng)前用戶的進程,也列出所有其他用戶的進程。
- 參數(shù)x表示不僅列出有控制終端的進程,也列出所有無控制終端的進程。
- 參數(shù)j表示列出與作業(yè)控制相關(guān)的信息
TPGID一欄寫著-1的都是沒有控制終端的進程,也就是守護進程
6.2.3 創(chuàng)建守護進程
創(chuàng)建守護進程一般不喜歡使用系統(tǒng)提供的,因為有未定義行為,一般都是自己寫
daemon函數(shù)是系統(tǒng)提供的
創(chuàng)建守護進程的過程可以分為以下幾個步驟:
-
讓調(diào)用進程忽略掉異常的信號
-
創(chuàng)建子進程:使用fork()系統(tǒng)調(diào)用創(chuàng)建一個子進程。
-
脫離終端(核心):使用setsid()系統(tǒng)調(diào)用使子進程脫離終端,成為一個新的會話組長。
-
關(guān)閉文件描述符:關(guān)閉所有文件描述符,以防止守護進程與終端或其他進程的關(guān)聯(lián)??梢允褂胏lose()系統(tǒng)調(diào)用來關(guān)閉文件描述符。
-
重定向標(biāo)準(zhǔn)輸入輸出、錯誤:將標(biāo)準(zhǔn)輸入、輸出和錯誤重定向到/dev/null或日志文件中??梢允褂胐up2()系統(tǒng)調(diào)用來重定向文件描述符。
-
設(shè)置工作目錄(可選):將工作目錄切換到根目錄,以防止守護進程運行時影響其他目錄??梢允褂胏hdir()系統(tǒng)調(diào)用來切換工作目錄。
代碼如下:
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char *currPath = nullptr)
{
// 1. 讓調(diào)用進程忽略掉異常的信號
signal(SIGPIPE, SIG_IGN);
// 2. 創(chuàng)建子進程
if (fork() > 0)
exit(0);
// 子進程 -- 守護進程,精靈進程,本質(zhì)就是孤兒進程的一種!
// 3.脫離終端:使用setsid()系統(tǒng)調(diào)用使子進程脫離終端,成為一個新的會話組長。
pid_t n = setsid();
assert(n != -1);
// 4. 關(guān)閉文件描述符 或 重定向標(biāo)準(zhǔn)輸入輸出、錯誤
int fd = open(DEV, O_RDWR);
if (fd >= 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
else
{
close(0);
close(1);
close(2);
}
// 5. 可選:進程執(zhí)行路徑發(fā)生更改
if (currPath)
chdir(currPath);
}
/dev/null
/dev/null是Linux操作系統(tǒng)中的一個特殊文件,它會丟棄所有寫入它的數(shù)據(jù),并在從中讀取時返回文件結(jié)束條件。它通常被用作丟棄不需要的輸出或測試程序在遇到寫入錯誤時的行為。/dev/null 形象稱為黑洞,或文件黑洞
setsid函數(shù)
creates a session and sets the process group ID:創(chuàng)建會話并設(shè)置進程組ID?
返回值:函數(shù)調(diào)用成功后,將返回調(diào)用進程的(新)會話ID。出現(xiàn)錯誤時,返回(pid_t)-1,錯誤碼被設(shè)置
如果調(diào)用進程不是進程組的組織,setsid()將創(chuàng)建一個新會話
如何讓自己不是進程組組長??
創(chuàng)建子進程:使用fork()系統(tǒng)調(diào)用創(chuàng)建一個子進程,讓父進程直接退出
注意:創(chuàng)建子進程成立新會話后,子進程自己就成了進程組,與終端設(shè)備無關(guān)
測試
給服務(wù)端加上該代碼,進行測試
編譯運行服務(wù)端
查看進程信息
發(fā)現(xiàn)該進程的TPGID為-1,代表的是守護進程,TTY顯示的是?
,也就意味著該進程已經(jīng)與終端去關(guān)聯(lián)了?
PPID為1,說明OS領(lǐng)養(yǎng)了守護進程,守護進程本質(zhì)是孤兒進程的一種
現(xiàn)在把自己的終端關(guān)掉,重新連接,該進程依舊可以查到,說明進程已經(jīng)守護進程化了,這就是服務(wù)器正確的運行方式
七、TCP協(xié)議通訊流程
TCP協(xié)議通訊流程這里只是淺談,后序再詳談,這里只有簡單認(rèn)識。
TCP協(xié)議的客戶端/服務(wù)器程序的一般流程:?
7.1 三次握手
三次握手就是客戶端向服務(wù)端發(fā)起連接的過程(簡單了解,后序詳談)
?服務(wù)器初始化
- 調(diào)用socket, 創(chuàng)建文件描述符;
- 調(diào)用bind, 將當(dāng)前的文件描述符和ip/port綁定在一起; 如果這個端口已經(jīng)被其他進程占用了, 就會bind失敗;
- 調(diào)用listen, 聲明當(dāng)前這個文件描述符作為一個服務(wù)器的文件描述符, 為后面的accept做好準(zhǔn)備;
- 調(diào)用accecpt, 并阻塞, 等待客戶端連接過來
?建立連接的過程
- 調(diào)用socket, 創(chuàng)建文件描述符;
- 調(diào)用connect, 向服務(wù)器發(fā)起連接請求;
- connect會發(fā)出SYN段并阻塞等待服務(wù)器應(yīng)答; (第一次)
- 服務(wù)器收到客戶端的SYN, 會應(yīng)答一個SYN-ACK段表示"同意建立連接"; (第二次)
- 客戶端收到SYN-ACK后會從connect()返回, 同時應(yīng)答一個ACK段; (第三次)?
這個建立連接的過程, 通常稱為 三次握手
7.2?數(shù)據(jù)傳輸
雙方建立好連接之后就可以進行數(shù)據(jù)傳輸了
?數(shù)據(jù)傳輸?shù)倪^程
- ?建立連接后,TCP協(xié)議提供全雙工的通信服務(wù); 所謂全雙工的意思是, 在同一條連接中, 同一時刻, 通信雙方
- 可以同時寫數(shù)據(jù); 相對的概念叫做半雙工, 同一條連接在同一時刻, 只能由一方來寫數(shù)據(jù);
- 服務(wù)器從accept()返回后立刻調(diào) 用read(), 讀socket就像讀管道一樣, 如果沒有數(shù)據(jù)到達(dá)就阻塞等待;
- 這時客戶端調(diào)用write()發(fā)送請求給服務(wù)器, 服務(wù)器收到后從read()返回,對客戶端的請求進行處理, 在此期間客戶端調(diào)用read()阻塞等待服務(wù)器的應(yīng)答;
- 服務(wù)器調(diào)用write()將處理結(jié)果發(fā)回給客戶端, 再次調(diào)用read()阻塞等待下一條請求;
- 客戶端收到后從read()返回, 發(fā)送下一條請求,如此循環(huán)下去
注意:用 read 讀取數(shù)據(jù)是有問題的,你不能保證數(shù)據(jù)讀取完了,或者數(shù)據(jù)只讀取了一部分,又或者數(shù)據(jù)沒有及時讀取,這些問題后序再談,這就是為什么說 TCP是面向字節(jié)流。
7.3 四次揮手
如果不想通信了,雙方就要斷開連接
斷開連接的過程?
- 如果客戶端沒有更多的請求了, 就調(diào)用close()關(guān)閉連接, 客戶端會向服務(wù)器發(fā)送FIN段(第一次);
- 此時服務(wù)器收到FIN后, 會回應(yīng)一個ACK, 同時read會返回0 (第二次);
- read返回之后, 服務(wù)器就知道客戶端關(guān)閉了連接, 也調(diào)用close關(guān)閉連接, 這個時候服務(wù)器會向客戶端發(fā)送一個FIN; (第三次)
- 客戶端收到FIN, 再返回一個ACK給服務(wù)器; (第四次)?
這個斷開連接的過程, 通常稱為 四次揮手
在學(xué)習(xí)socket API時要注意應(yīng)用程序和TCP協(xié)議層是如何交互的?
- 應(yīng)用程序調(diào)用某個socket函數(shù)時TCP協(xié)議層完成什么動作,比如調(diào)用connect()會發(fā)出SYN段
- 應(yīng)用程序如何知道TCP協(xié)議層的狀態(tài)變化,比如從某個阻塞的socket函數(shù)返回就表明TCP協(xié)議收到了某些段,再比如read()返回0就表明收到了FIN段?
注:以上概念先了解,后序再談
7.4?TCP 和 UDP 對比
- 可靠傳輸 vs 不可靠傳輸
- 有連接 vs 無連接
- 字節(jié)流 vs 數(shù)據(jù)報
到目前為止,我們通過代碼知道 TCP是有連接 和 UDP是無連接,而可靠和不可靠傳輸、面向字節(jié)流和面向數(shù)據(jù)報暫時體會不到,后序談原理的時候就可以理解了文章來源:http://www.zghlxwxcb.cn/news/detail-497659.html
--------------------- END ----------------------文章來源地址http://www.zghlxwxcb.cn/news/detail-497659.html
「 作者 」 楓葉先生
「 更新 」 2023.6.23
「 聲明 」 余之才疏學(xué)淺,故所撰文疏漏難免,
或有謬誤或不準(zhǔn)確之處,敬請讀者批評指正。
到了這里,關(guān)于「網(wǎng)絡(luò)編程」第二講:socket套接字(四 - 完結(jié))_ Linux任務(wù)管理與守護進程 | TCP協(xié)議通訊流程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!