一、端口號
1. 認識端口號
實際上我們兩臺機器在進行通信時,是應用層在進行通信,應用層必定會推動下層和對方的上層進行通信。
其實網(wǎng)絡協(xié)議棧中的下三層,主要解決的是數(shù)據(jù)安全可靠的送到遠端機器。而用戶使用應用層軟件,完成數(shù)據(jù)發(fā)送和接收的。那么用戶要使用軟件,首先需要把這個軟件啟動起來!所以軟件啟動起來,本質(zhì)就是進程!所以兩臺機器進行通信,本質(zhì)是兩臺機器之上的應用層在通信,也就是兩個進程之間在互相交換數(shù)據(jù)!所以網(wǎng)絡通信的本質(zhì)就是進程間通信!只不過在網(wǎng)絡通信中的公共資源是網(wǎng)絡,通過網(wǎng)絡協(xié)議棧利用網(wǎng)絡資源,讓兩個不同的進程看到了同一份資源!
在網(wǎng)絡協(xié)議棧中,在傳輸層怎么把數(shù)據(jù)正確交給上層應用層呢?怎么知道交給哪一個應用呢?所以就要求上層應用層和傳輸層之間必須協(xié)商一種方案,讓我們把數(shù)據(jù)準確交給上層,這個方案我們稱為端口號。所以在傳輸層的報頭中,必須要有原端口號和目的端口號,也就是根據(jù)目的端口號就可以決定這個數(shù)據(jù)的有效載荷要交給上層應用的哪一個!所以對于端口號無論對于客戶端和服務端,都能唯一的標識該主機上的一個網(wǎng)絡應用層的進程!
我們可以這樣理解,其實在傳輸層當中,操作系統(tǒng)會形成一張哈希表,哈希表中的類型是 task_struct*,每一個應用層都要和該哈希表綁定端口號,本質(zhì)就是根據(jù)端口號在哈希表里做哈希運算,如果該位置已經(jīng)被占用了,就不能被綁定了,因為一個端口號只能被一個進程綁定;如果該位置沒有被使用,就把該進程的pcb地址放在該位置上。
2. socket
因為在公網(wǎng)上,IP地址 能表示唯一的一臺主機,端口號 port,用來標識該主機上的唯一的一個進程,所以 IP + port 就可以標識全網(wǎng)唯一的一個進程!那么我們在網(wǎng)絡通信時,只需要在對應的報頭上填上原IP和目的IP,原port和目的port,就可以將報文交給另一個主機的進程,這種基于 IP + port 的通信方式,我們稱為 socket.
那么端口號和進程pid有什么區(qū)別呢?進程pid也能標識一臺主機上的唯一進程?。恳驗槭紫?,不是所有的進程都要通信,但是所有的進程都要有pid!其次是為了使系統(tǒng)和網(wǎng)絡功能解耦!
二、認識TCP協(xié)議和UDP協(xié)議
下面我們先認識一下兩個傳輸層協(xié)議:
1. TCP協(xié)議
此處我們先對TCP(Transmission Control Protocol 傳輸控制協(xié)議)有一個直觀的認識;后面我們再詳細討論 TCP 的一些細節(jié)問題。
- 傳輸層協(xié)議
- 有連接
- 可靠傳輸
- 面向字節(jié)流
2. UDP協(xié)議
此處我們也是對UDP(User Datagram Protocol 用戶數(shù)據(jù)報協(xié)議)有一個直觀的認識;后面再詳細討論。
- 傳輸層協(xié)議
- 無連接
- 不可靠傳輸
- 面向數(shù)據(jù)報
三、網(wǎng)絡字節(jié)序
我們已經(jīng)知道,內(nèi)存中的多字節(jié)數(shù)據(jù)相對于內(nèi)存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對于文件中的偏移地址也有大端小端之分,網(wǎng)絡數(shù)據(jù)流同樣有大端小端之分。那么如何定義網(wǎng)絡數(shù)據(jù)流的地址呢?
- 發(fā)送主機通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出;
- 接收主機把從網(wǎng)絡上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存;
- 因此,網(wǎng)絡數(shù)據(jù)流的地址應這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址.
- TCP/IP協(xié)議規(guī)定,網(wǎng)絡數(shù)據(jù)流應采用大端字節(jié)序,即低地址高字節(jié).
- 不管這臺主機是大端機還是小端機,都會按照這個TCP/IP規(guī)定的網(wǎng)絡字節(jié)序來發(fā)送/接收數(shù)據(jù);
- 如果當前發(fā)送主機是小端, 就需要先將數(shù)據(jù)轉(zhuǎn)成大端;否則就忽略,直接發(fā)送即可;
為使網(wǎng)絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調(diào)用以下庫函數(shù)做網(wǎng)絡字節(jié)序和主機字節(jié)序的轉(zhuǎn)換:
- 這些函數(shù)名很好記,h 表示 host;n 表示 network,l 表示 32 位長整數(shù),s 表示16位短整數(shù);
- 例如 htonl 表示將 32 位的長整數(shù)從主機字節(jié)序轉(zhuǎn)換為網(wǎng)絡字節(jié)序,例如將IP地址轉(zhuǎn)換后準備發(fā)送;
- 如果主機是小端字節(jié)序,這些函數(shù)將參數(shù)做相應的大小端轉(zhuǎn)換然后返回;
- 如果主機是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動地返回。
四、socket 編程
1. socket 常見API
// 創(chuàng)建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務器)
int socket(int domain, int type, int protocol);
// 綁定端口號 (TCP/UDP, 服務器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 開始監(jiān)聽socket (TCP, 服務器)
int listen(int socket, int backlog);
// 接收請求 (TCP, 服務器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
2. sockaddr 結(jié)構(gòu)
socket API 是一層抽象的網(wǎng)絡編程接口,適用于各種底層網(wǎng)絡協(xié)議,如IPv4、IPv6,以及后面要講的 UNIX Domain Socket;然而,各種網(wǎng)絡協(xié)議的地址格式并不相同
- IPv4 和 IPv6 的地址格式定義在 netinet/in.h 中,IPv4地址用 sockaddr_in 結(jié)構(gòu)體表示,包括16位地址類型, 16位端口號和32位IP地址.;
- IPv4、IPv6 地址類型分別定義為常數(shù) AF_INET、AF_INET6,這樣,只要取得某種 sockaddr 結(jié)構(gòu)體的首地址,不需要知道具體是哪種類型的 sockaddr 結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容;
- socket API 可以都用 struct sockaddr* 類型表示,在使用的時候需要強制轉(zhuǎn)化成 sockaddr_in;這樣的好處是程序的通用性,可以接收IPv4,IPv6,以及 UNIX Domain Socket 各種類型的 sockaddr 結(jié)構(gòu)體指針做為參數(shù)。
3. 編寫 UDP 服務器
(1)socket()
下面我們編寫一個 UDP 服務器。首先需要做的是創(chuàng)建套接字,使用到的接口是 socket()
:
第一個參數(shù)是我們創(chuàng)建的套接字的域,即使用 IPv4 的網(wǎng)絡協(xié)議還是 IPv6 的網(wǎng)絡協(xié)議,目前我們只需要關注這兩個即可,如下圖:
第二個參數(shù)表示當前 socket 對應的類型,也就是相當于這個套接字未來給我們提供什么服務,是面向字節(jié)流的還是面向用戶數(shù)據(jù)報的,如下:
第三個參數(shù)表示的是協(xié)議類型,目前我們不需要傳這個參數(shù)。
而返回值相當于是一個文件描述符,所以創(chuàng)建一個套接字的本質(zhì),在底層就相當于是打開一個文件,只不過以前的 struct file 指向的是鍵盤、顯示器這樣的設備;而現(xiàn)在指向的是網(wǎng)卡設備。
(2)bind()
創(chuàng)建套接字成功之后,接下來就要綁定端口號,使用到的接口是 bind()
,如下:
其中第一個參數(shù)就是創(chuàng)建套接字時的返回值;第二個參數(shù)是一個結(jié)構(gòu)體;第三個參數(shù)是結(jié)構(gòu)體的長度。但是我們在網(wǎng)絡套接字編程的時候不用第二個參數(shù)類型的結(jié)構(gòu)體,這個結(jié)構(gòu)體它只是設計接口用,我們實際用的是 sockaddr_in 類型的結(jié)構(gòu)體,只需要在傳參的時候進行強轉(zhuǎn)即可。我們可以使用 bzero() 接口將該結(jié)構(gòu)體清0;
我們是要使用 bind 來讓套接字和我們往該結(jié)構(gòu)體中填充的網(wǎng)絡信息要關聯(lián)起來,所以我們需要想該結(jié)構(gòu)體中填充對應的字段。該結(jié)構(gòu)體中有如下字段:
對應下圖:
其中 sin_zero 為該結(jié)構(gòu)體的填充字段,也就是這些字段不用填充,當作占位符即可;sin_addr 代表 ip 地址;sin_port 代表服務器所使用的端口號;sin_family 代表該結(jié)構(gòu)體對應的網(wǎng)絡協(xié)議類型,IPv4 或者 IPv6.
因為我們在給對方發(fā)送數(shù)據(jù)的時候,我們也一定需要讓對方知道我們是誰,所以我們需要將端口號攜帶上,發(fā)送給對方,這樣對方把數(shù)據(jù)處理完,就可以給我們響應回來。所以端口號是要在網(wǎng)絡里來回發(fā)送的,也就是需要保證我們的端口號是網(wǎng)路字節(jié)序列,因為該端口號是要給對方發(fā)送的。所以這里我們就需要用到主機序列轉(zhuǎn)網(wǎng)絡序列的接口,由于端口號是兩個字節(jié),所以用到的接口為 htons()
:
由于我們用戶一般用的都是點分十進制字符串風格的 IP 地址,也就是 0.0.0.0 這種風格,每個點分的范圍是 0~255,每個字符一個字節(jié),遠遠超過結(jié)構(gòu)體中要求的 32 位 ip 地址,也就是四字節(jié)。所以我們需要將該字符串類型轉(zhuǎn)換為 uint32_t 的類型,那么用到的接口是 inet_addr()
,它的作用就是將字符串風格的 ip 地址轉(zhuǎn)化為網(wǎng)絡風格的 uint32_t 類型,如下圖:
同端口號一樣,IP 地址也需要保證是網(wǎng)絡字節(jié)序列。那么它的返回值類型 in_addr_t 其實就是符合網(wǎng)路字節(jié)序列的 uint32_t 的類型。
上面我們已經(jīng)把準備工作做好了,接下來我們就需要使用 bind() 接口進行綁定,本質(zhì)就是把我們定義的 struct 結(jié)構(gòu)體設置進內(nèi)核,設置進指定的套接字內(nèi)部。
(3)recvfrom()
接下來我們就需要在指定的一個套接字里獲取數(shù)據(jù)內(nèi)容,使用到的接口是 recvfrom()
,如下圖:
第一個參數(shù)就是網(wǎng)絡文件描述符;第二個參數(shù)和第三個參數(shù)分別表示我們提供的緩沖區(qū)和它的長度,讀到的數(shù)據(jù)就會放在緩沖區(qū)中;第三個參數(shù)設為0就是默認使用阻塞方式;最后兩個參數(shù)又是熟悉的結(jié)構(gòu)體,由于我們需要知道這些數(shù)據(jù)是誰給我們發(fā)的,因為我們有可能也要將數(shù)據(jù)給對方返回。所以最后兩個參數(shù)其實是輸出型參數(shù)。
返回值成功就是對應的長度,否則就是-1,如下:
(4)sendto()
將數(shù)據(jù)發(fā)送回給對方使用到的接口為 sendto()
,如下:
參數(shù)和 recvfrom() 的參數(shù)類似,這里不再介紹了。而最后兩個參數(shù)是輸入型參數(shù),我們要將數(shù)據(jù)發(fā)回給對方,首先需要知道對方是誰,而我們上面已經(jīng)通過 recvfrom() 獲取到了對方的結(jié)構(gòu)體信息,所以直接使用該結(jié)構(gòu)體信息即可。
(5)udp 服務端和客戶端
其中通過使用上面的接口編寫的一個簡單的接收客戶端的字符串信息,并進行簡單的加工的 udp 服務器代碼鏈接為:UDP.
其中 udp server 的代碼如下:
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <cstring>
#include <functional>
#include "log.hpp"
using func_t = std::function<std::string(const std::string&)>;
//typedef std::function<std::string(const std::string&)> func_t;
std::string default_ip = "0.0.0.0";
uint16_t default_port = 8080;
log lg;
class UdpServer
{
public:
UdpServer(const uint16_t &port = default_port, const std::string &ip = default_ip)
: _port(port), _ip(ip), _isrunning(false), _sockfd(0)
{}
void Init()
{
// 1.創(chuàng)建 udp 套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET == PF_INET
if (_sockfd < 0)
{
lg(Fatal, "socket create faild, sockfd: %d", _sockfd);
exit(1);
}
lg(Info, "socket create success, sockfd: %d", _sockfd);
// 2.綁定端口號
// 2.1 準備數(shù)據(jù)
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 主機序列轉(zhuǎn)網(wǎng)絡序列
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string -> uint32_t 2.保證uint32_t是網(wǎng)絡序列
local.sin_addr.s_addr = htonl(INADDR_ANY);
// 2.2 開始bind
int n = bind(_sockfd, (const sockaddr *)&local, sizeof(local));
if (n < 0)
{
lg(Fatal, "bind faild, errno: %d, err message: %s", errno, strerror(errno));
exit(2);
}
lg(Info, "bind success, errno: %d, err message: %s", errno, strerror(errno));
}
void Run(func_t func)
{
_isrunning = true;
char buffer[1024];
while (_isrunning)
{
// 記錄客戶端發(fā)來時的結(jié)構(gòu)體信息
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);
if (n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err message: %s", errno, strerror(errno));
continue;
}
buffer[n] = 0;
// 對數(shù)據(jù)進行簡單的加工
std::string info = buffer;
std::string echo_string = func(info);
// 發(fā)送回給對方
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len);
}
}
~UdpServer()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd;
uint16_t _port;
std::string _ip;
bool _isrunning;
};
udp client 的代碼如下:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(string proc)
{
cout << "\n\rUsage: " << proc << " serverip serverport\n" << endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
string server_ip = argv[1];
uint16_t server_port = stoi(argv[2]);
sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_port = htons(server_port);
socklen_t len = sizeof(server);
// client 也需要 bind,只不過不需要用戶顯示 bind,一般由OS自由隨機選擇
// 系統(tǒng)會在首次發(fā)送數(shù)據(jù)的時候給我們bind
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
string message;
char buffer[1024];
while(true)
{
cout << "Plase Enter@ ";
getline(cin, message);
// 發(fā)送數(shù)據(jù)
sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr*)&server, len);
// 當服務器進行簡單的加工處理后會發(fā)送回來,此時客戶端再次獲取
sockaddr_in temp;
socklen_t size = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = 0;
cout << buffer << endl;
}
}
close(sockfd);
return 0;
}
main 函數(shù):
#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include "UdpServer.hpp"
using namespace std;
void Usage(string proc)
{
cout << "\n\rUsage: " << proc << " port[1024+]\n" << endl;
}
// 處理字符串的方法
string Handler(const std::string& str)
{
string res = "Server get a message: ";
res += str;
cout << res << endl;
return res;
}
// 遠程執(zhí)行指令的方法
string ExcuteCommand(const string& cmd)
{
FILE* fp = popen(cmd.c_str(), "r");
if(nullptr == fp)
{
perror("popen");
return "error";
}
string result;
char buffer[4096];
while(true)
{
char* tmp = fgets(buffer, sizeof(buffer), fp);
if(tmp == nullptr) break;
result = buffer;
}
pclose(fp);
return result;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = stoi(argv[1]);
unique_ptr<UdpServer> svr(new UdpServer(port, "127.0.0.1"));
svr->Init();
svr->Run(ExcuteCommand);
return 0;
}
有關代碼中的細節(jié):
- 有關 IP 地址
云服務器禁止直接bind公網(wǎng)ip;bind ip 地址為0,表示的含義是任意地址綁定,這種是比較推薦的做法。當 IP 地址為 127.0.0.1 時,表示進行的是本地傳輸測試,不會進行跨網(wǎng)傳輸。
- 有關 port
其中 0~1023 的端口號是系統(tǒng)內(nèi)定的端口號,一般都要有固定的應用層協(xié)議使用,例如 http:80,https:443;所以我們一般綁端口號,一般綁1024以上的。
- popen() 系統(tǒng)調(diào)用
popen() 是一個被封裝起來的管道和子進程執(zhí)行命令的應用。
它的第一個參數(shù)就是需要執(zhí)行的命令,在底層它會幫我們進行 fork() 創(chuàng)建子進程,并讓父子進程建立管道,然后讓子進程把它的運行結(jié)果通過管道再返回給調(diào)用方。如果調(diào)用方想得到 command 指令的運行結(jié)果,可以通過文件指針的方式讀取。第二個參數(shù)相當于是打開這個命令的方式,我們使用 “r” 即可。使用完畢后使用 pclose() 關閉該文件指針即可。
其中,我們可以使用 netstat -nlup
查看系統(tǒng)中所有的 udp 信息,并且把進程信息也顯示出來。
我們還可以將以上代碼修改成為多線程代碼,鏈接為:多線程UDP.
4. 地址轉(zhuǎn)換函數(shù)
(1)相關接口
我們只介紹基于 IPv4 的 socket 網(wǎng)絡編程,sockaddr_in 中的成員 struct in_addr sin_addr 表示32位 的 IP 地址,但是我們通常用點分十進制的字符串表示 IP 地址,以下函數(shù)可以在字符串表示和 in_addr 表示之間轉(zhuǎn)換。我們在上面的 bind() 中也使用了地址轉(zhuǎn)換函數(shù) inet_addr().
-
字符串轉(zhuǎn) in_addr 的函數(shù):
#include <arpa/inet.h> int inet_aton(const char* strptr, struct in_addr* addrptr); in_addr_t inet_addr(const char* strptr); int inet_pton(int family, const char* strptr, void* addrptr);
-
in_addr 轉(zhuǎn)字符串的函數(shù):
char* inet_ntoa(struct in_addr inaddr); const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);
其中 inet_pton 和 inet_ntop 不僅可以轉(zhuǎn)換 IPv4 的 in_addr,還可以轉(zhuǎn)換 IPv6 的 in6_addr,因此函數(shù)接口是 void* addrptr.
(2)關于 inet_ntoa
inet_ntoa 這個函數(shù)返回了一個 char*,很顯然是這個函數(shù)自己在內(nèi)部為我們申請了一塊內(nèi)存來保存 ip 的結(jié)果,那么是否需要調(diào)用者手動釋放呢?
man 手冊上說,inet_ntoa 函數(shù),是把這個返回結(jié)果放到了靜態(tài)存儲區(qū)。這個時候不需要我們手動進行釋放。
5. 編寫 TCP 服務器
(1)listen()
TCP 是面向連接的,服務器一般是比較被動的,所以服務器一直處于一種等待連接到來的狀態(tài),這個工作叫做監(jiān)聽狀態(tài),使用到的接口是 listen()
,如下:
第一個參數(shù)為指定的套接字,通過該套接字等待新連接的到來。第二個參數(shù)我們后面再介紹,暫時設為10左右即可。返回值,成功返回0,失敗返回-1.
(2)accept()
因為 TCP 是面向連接的,所以在正式通信之前,先要把連接建立起來,使用到的接口為 accept()
,該接口的作用是獲取一個新的連接,如下:
第一個參數(shù)為我們剛剛設置為監(jiān)聽狀態(tài)的套接字;后兩個參數(shù)和 recvfrom()
的后兩個參數(shù)一樣,都是輸出型參數(shù),也就是誰給我們發(fā)的 TCP 報文,那么對應的套接字信息就會通過這兩個參數(shù)返回出來。
而返回值成功返回一個文件描述符;否則返回-1;那么返回值也是一個文件描述符,我們原本也有一個文件描述符,為什么會有兩個 sockfd 呢?我們該用哪個呢?其實它們分工是明確的,我們原本定義的 sockfd,即被創(chuàng)建的,被 bind 的,被監(jiān)聽的套接字,它的工作是從底層獲取新的連接;而未來真正提供通信服務的,是 accept() 返回的套接字!
至此,我們可以使用 telnet
進行指定服務的一個遠程連接,后面跟上 IP 地址和端口號即可;它在底層默認使用的就是 TCP.
(3)con
由于在 TCP 中,客戶端是要連接服務器的,所以服務端需要有一個能夠向服務器發(fā)起連接的接口,該接口為 connect()
,如下:
該接口的作用是通過指定的套接字,向指定的網(wǎng)絡目標地址發(fā)起連接。后兩個參數(shù)和 sendto() 的后兩個參數(shù)一樣。返回值成功返回0,失敗返回-1.
TCP 客戶端也需要 bind,但是和 UDP 一樣,不需要顯示的 bind,系統(tǒng)會在客戶端發(fā)起 connect 的時候,進行自動隨機 bind.
我們可以使用 netstat -nltp
查看系統(tǒng)中所有 TCP 的信息,并把進程信息顯示出來。
(4)守護進程
在我們登錄 Linux 的時候,Linux 系統(tǒng)會給我們形成一個會話,而且會為每個會話創(chuàng)建一個 bash 進程,這個 bash 就可以為用戶提供命令行服務。每個會話中只能存在一個前臺進程,但是可以存在多個后臺進程,而鍵盤信號只能發(fā)送給前臺進程。前臺和后臺進程的區(qū)別就是是否擁有鍵盤文件,它們都可以向顯示器打印,而只有前臺進程才能從鍵盤,即標準輸入獲取數(shù)據(jù)!
如果我們不想后臺進程向顯示器打印的數(shù)據(jù)影響我們,我們可以將它的打印數(shù)據(jù)重定向到文件中,例如:
其中 [1]
表示后臺任務號,后面數(shù)字表示進程 PID.
而查看后臺任務的指令為:jobs
,如下:
如果我們想把后臺進程提到前臺,可以使用 fg 任務號
,如下:
如果想把它重新放回后臺,我們可以使用 ctrl + z
將該進程暫停。然后使用 bg 任務號
將該進程重新啟動,如下:
接下來我們再運行幾個后臺進程,例如使用 sleep,方便觀察 Linux 中的進程間關系,使用 ps axj | head -1 && ps axj | grep -Ei 'a.out|sleep'
查看它們的進程信息:
其中 PPID、PID 我們都認識,而 PGID 表示的是進程組ID,SID 表示 session id,即會話 id.
而系統(tǒng)中可能會存在多個 session,所以系統(tǒng)需要管理多個 session.
我們可以看到,./a.out
進程的 PID 和 PGID 是一樣的,所以它就是自成進程組的。而三個 sleep 分別是三個不同的進程,但是它們的 PGID 卻是同一個,而且是用管道建立的進程的第一個進程的 PID,所以它們?nèi)齻€自成一組,而組長是多個進程中的第一個。那么進程組和任務有什么關系呢?任務是要指派給進程組的!所以我們需要校正一下以前的說法,我們把前臺進程稱為前臺任務,后臺進程稱為后臺任務,因為可能某一個后臺任務里面,可能會包含多個進程。但是無論有幾個進程組完成對應的任務,在同一個會話內(nèi)啟動的,SID 是一樣的!那么上面中的 SID 到底是誰呢?我們可以查看一下:
如上圖,我們可以看到,它是 bash!所以就是以 bash 的 pid 去構(gòu)建了一個 session!
這種后臺進程會收到用戶登錄和退出的影響,如果我們不想受到任何用戶登錄和注銷的影響,我們可以將進程守護進程化。什么是守護進程呢?我們把自成進程組自成會話的進程稱為守護進程!那么我們該如何做到呢?下面我們認識一個接口:setsid()
,如下:
該接口的作用就是,哪個進程調(diào)用該接口,就把該進程的組ID設置為會話ID,也就是讓進程獨立成會話。
返回值成功返回進程的ID,否則返回-1.
注意,該接口不能由進程組的組長直接調(diào)用,那么怎么才能保證不是組長調(diào)用呢?所以我們可以使用 fork() 創(chuàng)建子進程調(diào)用!所以守護進程的本質(zhì),也是孤兒進程!
(5)tcp 服務端和客戶端
接下來我們結(jié)合上面所學的知識,編寫一個 TCP 服務器,并將它守護進程化,代碼鏈接:
其中在守護進程中,我們的代碼中是充滿大量的打印的,而這些打印默認是向標準輸出打的,也就是向顯示器上打了,而對于守護進程來說,就不應該向顯示器上打了,所以我們需要一個解決方案。而 Linux 中存在一種字符文件,叫做 /dev/null
,只要我們向該文件寫入,都會被該文件丟棄掉,如果我們向該文件讀取,什么也讀取不到。所以我們只需要將所有的輸出向該文件寫入即可。我們也可以將打印信息寫入文件中。
另外,TCP 在通信時是全雙工的,也就是可以同時讀寫的。在底層操作系統(tǒng)給 TCP 提供兩個緩沖區(qū),一個發(fā)送緩沖區(qū),一個接收緩沖區(qū),我們在用 TCP 的同時,別人也在用,所以別人也會有上面兩個緩沖區(qū),所以當我們發(fā)送數(shù)據(jù),是先把我們的數(shù)據(jù)拷貝到我們的 TCP 的發(fā)送緩沖區(qū),然后通過網(wǎng)絡會發(fā)送到對方的接收緩沖區(qū),反過來也同理,如下圖:文章來源:http://www.zghlxwxcb.cn/news/detail-837065.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-837065.html
到了這里,關于【計算機網(wǎng)絡】socket 網(wǎng)絡套接字的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!