Linux基礎(chǔ)(四)——網(wǎng)絡(luò)編程
一、網(wǎng)絡(luò)結(jié)構(gòu)模式

在C/S結(jié)構(gòu)中,客戶端和服務(wù)器之間通過網(wǎng)絡(luò)進行通信。客戶端通常是一個運行在用戶計算機上的應(yīng)用程序或瀏覽器,它向服務(wù)器發(fā)送請求并等待響應(yīng)。服務(wù)器則是一個運行在服務(wù)器計算機上的應(yīng)用程序,它接收客戶端請求并提供服務(wù)。
C/S結(jié)構(gòu)的優(yōu)點包括:
可以將應(yīng)用程序的處理任務(wù)分配給不同的計算機,使得處理負載更加均衡,提高系統(tǒng)的可伸縮性和性能;
可以通過服務(wù)器控制和管理應(yīng)用程序,提高系統(tǒng)的安全性和穩(wěn)定性;
可以實現(xiàn)更高級別的應(yīng)用程序功能和復雜度,例如數(shù)據(jù)庫管理系統(tǒng)。
C/S結(jié)構(gòu)的缺點包括:
客戶端和服務(wù)器之間需要網(wǎng)絡(luò)連接,因此需要考慮網(wǎng)絡(luò)帶寬和延遲等因素;
應(yīng)用程序的部署和維護需要更多的工作,例如安裝和配置客戶端軟件和服務(wù)器軟件;
由于服務(wù)器需要處理來自多個客戶端的請求,因此服務(wù)器的設(shè)計和實現(xiàn)需要更高的技術(shù)水平和經(jīng)驗。
總之,C/S結(jié)構(gòu)是一種常見的計算機系統(tǒng)架構(gòu)模式,具有一些優(yōu)點和缺點,適用于需要高級別應(yīng)用程序功能和復雜度的網(wǎng)絡(luò)應(yīng)用程序。

在B/S結(jié)構(gòu)中,瀏覽器作為客戶端向服務(wù)器發(fā)送請求并接收響應(yīng)。瀏覽器通常是一個Web瀏覽器,例如Chrome或Firefox。服務(wù)器是一個Web服務(wù)器,它接收瀏覽器請求并提供服務(wù)。
B/S結(jié)構(gòu)的優(yōu)點包括:
可以跨平臺和跨瀏覽器運行,因為Web瀏覽器是幾乎所有計算機和移動設(shè)備上的標準軟件;
應(yīng)用程序的部署和維護比C/S結(jié)構(gòu)更方便,因為只需要在Web服務(wù)器上安裝和配置應(yīng)用程序;
可以輕松地更新應(yīng)用程序和內(nèi)容,因為應(yīng)用程序和內(nèi)容都存儲在服務(wù)器上。
B/S結(jié)構(gòu)的缺點包括:
應(yīng)用程序的響應(yīng)速度受到網(wǎng)絡(luò)帶寬和延遲等因素的影響;
應(yīng)用程序的功能和復雜度受到Web瀏覽器的限制,因為Web瀏覽器只能執(zhí)行JavaScript等有限的客戶端腳本語言。
總之,B/S結(jié)構(gòu)是一種常見的計算機系統(tǒng)架構(gòu)模式,適用于基于Web的應(yīng)用程序。它具有一些優(yōu)點和缺點,可以根據(jù)具體應(yīng)用場景和需求來選擇使用。
二、MAC地址、IP地址、端口

MAC地址是由網(wǎng)絡(luò)接口控制器(NIC)分配的,用于標識網(wǎng)絡(luò)上的設(shè)備,如計算機、路由器、交換機等。每個網(wǎng)絡(luò)接口控制器都有一個唯一的MAC地址,它在制造過程中被寫入,無法更改。
MAC地址的前24位(前6個十六進制數(shù))稱為組織唯一標識符(OUI),用于標識廠商或組織。后24位為設(shè)備標識符,用于標識特定設(shè)備。
在網(wǎng)絡(luò)通信中,MAC地址用于在局域網(wǎng)中識別和定位設(shè)備。當一個設(shè)備發(fā)送數(shù)據(jù)包時,它會在數(shù)據(jù)包中包含目標設(shè)備的MAC地址,以便路由器或交換機可以將數(shù)據(jù)包傳遞給正確的設(shè)備。因此,MAC地址在局域網(wǎng)中起著非常重要的作用。
在計算機上,可以使用命令行工具(如ipconfig/ifconfig)來查看MAC地址。在路由器或交換機上,可以使用命令行工具(如show mac-address-table)來查看MAC地址。


IP地址編址方式有兩種:IPv4和IPv6。
IPv4:IPv4使用32位二進制數(shù)字編址,共有42億個地址。IPv4地址通常使用點分十進制表示法表示。IPv4地址分為A、B、C、D和E五類地址,其中A、B、C三類地址用于互聯(lián)網(wǎng)上的主機,D和E地址則是特殊用途的地址。IPv4地址還有私有地址和保留地址等特殊地址。
IPv6:IPv6使用128位二進制數(shù)字編址,共有340萬億億億億個地址。IPv6地址通常使用冒號分隔的八組十六進制數(shù)表示。IPv6地址采用一個新的編址方式,將地址空間分為子網(wǎng)前綴、接口標識符和全球路由前綴三個部分。IPv6還支持任意長度的前綴子網(wǎng)掩碼,可以更靈活地分配地址。
總之,IP地址是用于在互聯(lián)網(wǎng)上標識設(shè)備的32位(IPv4)或128位(IPv6)二進制數(shù)字,可以使用點分十進制或冒號分隔的十六進制數(shù)表示。IPv4和IPv6有不同的編址方式,各有優(yōu)缺點,可以根據(jù)具體需求選擇使用。

A類地址:A類地址是以0開頭的8位二進制數(shù)作為網(wǎng)絡(luò)地址,其余24位二進制數(shù)作為主機地址,共有2^24-2個主機地址。A類地址范圍是1.0.0.0~126.0.0.0,其中1.0.0.0是保留地址,用于識別本地地址。A類地址適用于大型網(wǎng)絡(luò),如全球互聯(lián)網(wǎng)。
B類地址:B類地址是以10開頭的16位二進制數(shù)作為網(wǎng)絡(luò)地址,其余16位二進制數(shù)作為主機地址,共有2^16-2個主機地址。B類地址范圍是128.0.0.0~191.255.0.0。B類地址適用于中等規(guī)模的網(wǎng)絡(luò)。
C類地址:C類地址是以110開頭的24位二進制數(shù)作為網(wǎng)絡(luò)地址,其余8位二進制數(shù)作為主機地址,共有2^8-2個主機地址。C類地址范圍是192.0.0.0~223.255.255.0。C類地址適用于小型網(wǎng)絡(luò),如局域網(wǎng)。
D類地址:D類地址用于多播(Multicast),即一次向多個設(shè)備發(fā)送數(shù)據(jù)。D類地址是以1110開頭的32位二進制數(shù),范圍是224.0.0.0~239.255.255.255。
E類地址:E類地址是保留地址,用于實驗和研究,范圍是240.0.0.0~255.255.255.255。
特殊的網(wǎng)址包括:
0.0.0.0:表示本地主機,通常用于初始化網(wǎng)絡(luò)接口。
127.0.0.1:表示本地主機環(huán)回地址,也稱為localhost,用于測試網(wǎng)絡(luò)接口是否正常。
255.255.255.255:表示廣播地址,用于向同一子網(wǎng)內(nèi)的所有主機廣播消息。
169.254.x.x:表示自動配置地址,也稱為APIPA(Automatic Private IP Addressing),用于在沒有DHCP服務(wù)器的情況下自動分配IP地址。
總之,A、B、C、D、E五類IP地址用于不同規(guī)模和用途的網(wǎng)絡(luò),特殊的網(wǎng)址包括本地主機地址、環(huán)回地址、廣播地址和自動配置地址等。

子網(wǎng)掩碼的作用是將IP地址劃分成網(wǎng)絡(luò)地址和主機地址兩部分,以便進行子網(wǎng)劃分和路由選擇。子網(wǎng)掩碼中的連續(xù)的1表示網(wǎng)絡(luò)部分,連續(xù)的0表示主機部分。例如,子網(wǎng)掩碼為255.255.255.0的IP地址,前24位是連續(xù)的1,表示網(wǎng)絡(luò)部分,后8位是0,表示主機部分,可以劃分成256個子網(wǎng),每個子網(wǎng)可以容納256個主機。
子網(wǎng)掩碼與IP地址結(jié)合使用,通過邏輯與運算來確定網(wǎng)絡(luò)地址和主機地址。例如,如果IP地址為192.168.1.100,子網(wǎng)掩碼為255.255.255.0,則通過邏輯與運算,可以得到網(wǎng)絡(luò)地址為192.168.1.0,主機地址為0.0.0.100。
子網(wǎng)掩碼的長度決定了網(wǎng)絡(luò)的大小,長度越長,網(wǎng)絡(luò)越小,可容納的主機數(shù)量越少。在進行子網(wǎng)劃分時,需要根據(jù)實際需求確定子網(wǎng)掩碼的長度和子網(wǎng)的數(shù)量。
總之,子網(wǎng)掩碼是用于劃分IP地址的技術(shù),用于將一個IP地址劃分成網(wǎng)絡(luò)地址和主機地址兩個部分。它由32位二進制數(shù)組成,與IP地址結(jié)合使用,通過邏輯與運算來確定網(wǎng)絡(luò)地址和主機地址。子網(wǎng)掩碼的長度決定了網(wǎng)絡(luò)的大小,需要根據(jù)實際需求進行設(shè)置。

端口類型是指不同用途的端口。根據(jù)端口的用途,端口可以分為以下三種類型:
眾所周知端口(Well-known Port):指0~1023的端口,這些端口被指定為某些服務(wù)的標準端口,如HTTP服務(wù)的80端口、FTP服務(wù)的21端口、SSH服務(wù)的22端口等。這些端口是被廣泛認可和使用的,應(yīng)用程序可以直接使用這些端口來訪問相應(yīng)的服務(wù)。
注冊端口(Registered Port):指1024~49151的端口,這些端口是被分配給某些服務(wù)的,但是并沒有被正式指定為標準端口。這些端口通常被用于一些特定的應(yīng)用程序或服務(wù)。
動態(tài)端口(Dynamic Port):指49152~65535的端口,這些端口是由操作系統(tǒng)動態(tài)分配的,并且通常只在一次會話中使用。當應(yīng)用程序需要建立一個網(wǎng)絡(luò)連接時,操作系統(tǒng)會自動分配一個可用的動態(tài)端口,用于該會話的數(shù)據(jù)傳輸。
總之,端口是計算機網(wǎng)絡(luò)中標識一條數(shù)據(jù)通信鏈路中一端的概念,用于數(shù)據(jù)的發(fā)送和接收。根據(jù)端口的用途和范圍,可以將端口分為眾所周知端口、注冊端口和動態(tài)端口三種類型。了解不同端口類型的作用和使用方法,可以幫助我們更好地理解網(wǎng)絡(luò)通信的過程和應(yīng)用程序的工作原理。
三、網(wǎng)絡(luò)模型


四、協(xié)議





五、網(wǎng)絡(luò)通信的過程

首先,上層協(xié)議將數(shù)據(jù)傳遞給下層協(xié)議,上層協(xié)議稱為“數(shù)據(jù)的應(yīng)用層”或“上層協(xié)議”,下層協(xié)議稱為“數(shù)據(jù)的傳輸層”或“下層協(xié)議”。
下層協(xié)議會將上層數(shù)據(jù)封裝成自己的數(shù)據(jù)格式,通常包括一個協(xié)議頭和一個協(xié)議尾。
協(xié)議頭通常包括一些元數(shù)據(jù),例如源IP地址、目的IP地址、協(xié)議類型等信息,協(xié)議尾通常包括一些用于檢測數(shù)據(jù)完整性和錯誤檢測的校驗和等信息。
封裝完成后,下層協(xié)議將生成的數(shù)據(jù)包傳遞給下一層協(xié)議,下一層協(xié)議再將該數(shù)據(jù)包封裝成自己的格式,以此類推,直到數(shù)據(jù)包被傳輸?shù)侥康牡亍?br>
在接收端,各層協(xié)議會解封數(shù)據(jù)包,將數(shù)據(jù)包還原為原始數(shù)據(jù),并將數(shù)據(jù)傳遞給上一層協(xié)議進行處理。
總之,封裝是將一個協(xié)議的數(shù)據(jù)添加到另一個協(xié)議的數(shù)據(jù)中,形成一個新的數(shù)據(jù)包的過程。封裝是計算機網(wǎng)絡(luò)中實現(xiàn)數(shù)據(jù)傳輸?shù)闹匾椒?,通過封裝,不同層次之間的數(shù)據(jù)可以進行傳輸和交換,從而實現(xiàn)了網(wǎng)絡(luò)通信。

接收端主機從網(wǎng)絡(luò)中接收到一個數(shù)據(jù)包,數(shù)據(jù)包中包含了多個數(shù)據(jù)流。
接收端主機的網(wǎng)絡(luò)協(xié)議棧首先對數(shù)據(jù)包進行解封裝,將數(shù)據(jù)包還原為原始數(shù)據(jù)流。
然后,接收端主機的網(wǎng)絡(luò)協(xié)議棧會根據(jù)數(shù)據(jù)包頭部中的端口號信息,分別將數(shù)據(jù)流交給對應(yīng)的應(yīng)用程序處理。這個過程稱為demultiplexing。
在該過程中,每個應(yīng)用程序都會注冊一個或多個端口號,接收端主機的網(wǎng)絡(luò)協(xié)議棧會根據(jù)數(shù)據(jù)包頭部中的端口號信息,將數(shù)據(jù)流分別發(fā)送給對應(yīng)的應(yīng)用程序。
如果數(shù)據(jù)包頭部中的端口號在接收端主機中沒有被注冊,數(shù)據(jù)包將被丟棄。
總之,demultiplexing是將一個數(shù)據(jù)包中的多個數(shù)據(jù)流進行分離的過程。它是計算機網(wǎng)絡(luò)中實現(xiàn)多路復用的重要技術(shù),可以將多個應(yīng)用程序的數(shù)據(jù)流通過一個網(wǎng)絡(luò)連接進行傳輸,從而提高網(wǎng)絡(luò)傳輸?shù)男屎挽`活性。

六、socket 介紹

Socket是一種通用的通信機制,它可以在不同的操作系統(tǒng)和平臺上使用,包括Linux、Windows、Unix等。
Socket通信是基于TCP/IP協(xié)議的,它提供了可靠的數(shù)據(jù)傳輸和錯誤處理機制。
Socket通信是面向連接的,它需要先建立連接,然后才能進行數(shù)據(jù)傳輸。建立連接是通過“三次握手”協(xié)議實現(xiàn)的。
Socket通信提供了兩種類型的套接字:流式套接字和數(shù)據(jù)報套接字。流式套接字提供了面向連接的數(shù)據(jù)傳輸,數(shù)據(jù)報套接字則提供了無連接的數(shù)據(jù)傳輸。
在使用Socket進行通信時,需要指定目標IP地址和端口號,通過這些信息可以建立連接,并進行數(shù)據(jù)傳輸和通信。
總之,Socket是計算機網(wǎng)絡(luò)中一種通信機制,它可以在不同的操作系統(tǒng)和平臺上使用,提供了可靠的數(shù)據(jù)傳輸和錯誤處理機制。Socket通信是基于TCP/IP協(xié)議的,它需要先建立連接,然后才能進行數(shù)據(jù)傳輸。Socket通信提供了兩種類型的套接字,可以根據(jù)需要選擇合適的套接字類型進行數(shù)據(jù)傳輸和通信。
七、字節(jié)序

大端字節(jié)序:在大端字節(jié)序中,高位字節(jié)保存在內(nèi)存的低地址中,低位字節(jié)保存在內(nèi)存的高地址中。例如,整數(shù)0x12345678在內(nèi)存中的排列順序為0x12 0x34 0x56 0x78。
小端字節(jié)序:在小端字節(jié)序中,低位字節(jié)保存在內(nèi)存的低地址中,高位字節(jié)保存在內(nèi)存的高地址中。例如,整數(shù)0x12345678在內(nèi)存中的排列順序為0x78 0x56 0x34 0x12。
在網(wǎng)絡(luò)通信中,由于不同的計算機體系結(jié)構(gòu)和操作系統(tǒng)采用的字節(jié)序不同,因此需要進行字節(jié)序的轉(zhuǎn)換。通常使用網(wǎng)絡(luò)字節(jié)序(也稱為大端字節(jié)序)來進行數(shù)據(jù)傳輸和通信,接收端在接收到數(shù)據(jù)后需要將數(shù)據(jù)從網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為本地字節(jié)序,即進行字節(jié)序的轉(zhuǎn)換。
總之,字節(jié)序是指在多字節(jié)數(shù)據(jù)中字節(jié)的排列順序,通常有大端字節(jié)序和小端字節(jié)序兩種。在進行數(shù)據(jù)交換和通信時,需要進行字節(jié)序的轉(zhuǎn)換。在網(wǎng)絡(luò)通信中,通常使用網(wǎng)絡(luò)字節(jié)序進行數(shù)據(jù)傳輸和通信,接收端在接收到數(shù)據(jù)后需要進行字節(jié)序的轉(zhuǎn)換。
八、字節(jié)序轉(zhuǎn)換函數(shù)

htons()函數(shù):將本地字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序。htons()函數(shù)接受一個16位整數(shù)作為參數(shù),返回一個網(wǎng)絡(luò)字節(jié)序的16位整數(shù)。例如,short port = htons(8080);。
ntohs()函數(shù):將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為本地字節(jié)序。ntohs()函數(shù)接受一個16位整數(shù)作為參數(shù),返回一個本地字節(jié)序的16位整數(shù)。例如,short port = ntohs(0x7a69);。
htonl()函數(shù):將本地字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序。htonl()函數(shù)接受一個32位整數(shù)作為參數(shù),返回一個網(wǎng)絡(luò)字節(jié)序的32位整數(shù)。例如,int ip = htonl(0x0a000001);。
ntohl()函數(shù):將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為本地字節(jié)序。ntohl()函數(shù)接受一個32位整數(shù)作為參數(shù),返回一個本地字節(jié)序的32位整數(shù)。例如,int ip = ntohl(0x0100007f);。
這些函數(shù)通常在網(wǎng)絡(luò)編程中使用,用于將數(shù)據(jù)從本地字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序或從網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為本地字節(jié)序,從而實現(xiàn)跨平臺的數(shù)據(jù)傳輸和通信。
九、socket 地址

通用socket地址包含了協(xié)議族、IP地址、端口號等信息,不同的協(xié)議族和地址類型對應(yīng)的通用socket地址結(jié)構(gòu)體可能會有所不同。在C語言中,通用socket地址通常表示為sockaddr結(jié)構(gòu)體,其定義如下:
struct sockaddr {
? ? unsigned short sa_family; ? ?// 協(xié)議族
? ? char sa_data[14]; ? ? ? ? ? ?// 地址信息
};
其中,sa_family表示協(xié)議族,可以是AF_INET(IPv4協(xié)議族)、AF_INET6(IPv6協(xié)議族)等等;sa_data表示地址信息,該字段的長度取決于具體的協(xié)議族和地址類型。
通用socket地址通常會和其他的函數(shù)一起使用,例如bind()、connect()、accept()等函數(shù),用于指定網(wǎng)絡(luò)地址和端口號。在使用時,通常需要將通用socket地址強制轉(zhuǎn)換為特定的協(xié)議族和地址類型,例如將sockaddr結(jié)構(gòu)體轉(zhuǎn)換為sockaddr_in結(jié)構(gòu)體(用于IPv4地址)或sockaddr_in6結(jié)構(gòu)體(用于IPv6地址)等。
總之,通用socket地址是一種表示通用網(wǎng)絡(luò)地址的結(jié)構(gòu)體,可以用于不同的協(xié)議族和地址類型。通用socket地址常用于網(wǎng)絡(luò)編程中,用于指定網(wǎng)絡(luò)地址和端口號。在使用時,需要將通用socket地址強制轉(zhuǎn)換為特定的協(xié)議族和地址類型。

在網(wǎng)絡(luò)編程中,常用的專用socket地址包括sockaddr_in結(jié)構(gòu)體(用于IPv4地址)和sockaddr_in6結(jié)構(gòu)體(用于IPv6地址)。這些結(jié)構(gòu)體包含了協(xié)議族、IP地址、端口號等信息,用于指定一個特定的網(wǎng)絡(luò)地址。
sockaddr_in結(jié)構(gòu)體定義如下:
struct sockaddr_in {
? ? short sin_family; ? ? ? ? ? // 協(xié)議族
? ? unsigned short sin_port; ? ?// 端口號
? ? struct in_addr sin_addr; ? ?// IPv4地址
? ? char sin_zero[8]; ? ? ? ? ? // 未使用
};
其中,sin_family表示協(xié)議族,固定為AF_INET;sin_port表示端口號,采用網(wǎng)絡(luò)字節(jié)序(使用htons()函數(shù)進行轉(zhuǎn)換);sin_addr表示IPv4地址,使用struct in_addr結(jié)構(gòu)體表示;sin_zero為未使用的8個字節(jié)。
sockaddr_in6結(jié)構(gòu)體定義如下:
struct sockaddr_in6 {
? ? short sin6_family; ? ? ? ? ?// 協(xié)議族
? ? unsigned short sin6_port; ? // 端口號
? ? unsigned int sin6_flowinfo; // 傳輸控制信息
? ? struct in6_addr sin6_addr; ?// IPv6地址
? ? unsigned int sin6_scope_id; // 地址作用域
};
其中,sin6_family表示協(xié)議族,固定為AF_INET6;sin6_port表示端口號,采用網(wǎng)絡(luò)字節(jié)序;sin6_flowinfo表示傳輸控制信息;sin6_addr表示IPv6地址,使用struct in6_addr結(jié)構(gòu)體表示;sin6_scope_id表示地址作用域。
總之,專用socket地址是一種特定協(xié)議族和地址類型的地址結(jié)構(gòu),用于表示一個特定的網(wǎng)絡(luò)地址。常見的專用socket地址包括sockaddr_in結(jié)構(gòu)體(用于IPv4地址)和sockaddr_in6結(jié)構(gòu)體(用于IPv6地址)。這些結(jié)構(gòu)體包含了
十、IP 地址轉(zhuǎn)換函數(shù)

在網(wǎng)絡(luò)編程中,經(jīng)常需要將字符串形式的IP地址轉(zhuǎn)換為整數(shù)形式的IP地址,或者將整數(shù)形式的IP地址轉(zhuǎn)換為字符串形式的IP地址。這可以通過使用inet_addr()和inet_ntoa()函數(shù)來實現(xiàn)。
inet_addr()函數(shù)將一個字符串形式的IP地址轉(zhuǎn)換為一個32位的網(wǎng)絡(luò)字節(jié)序的整數(shù)形式的IP地址,函數(shù)原型如下:
unsigned long inet_addr(const char *cp);
例如,下面的代碼將字符串形式的IP地址"192.168.1.1"轉(zhuǎn)換為整數(shù)形式的IP地址:
unsigned long ip = inet_addr("192.168.1.1");
inet_ntoa()函數(shù)將一個32位的網(wǎng)絡(luò)字節(jié)序的整數(shù)形式的IP地址轉(zhuǎn)換為一個字符串形式的IP地址,函數(shù)原型如下:
char *inet_ntoa(struct in_addr in);
其中,in_addr是一個結(jié)構(gòu)體,表示一個32位的網(wǎng)絡(luò)字節(jié)序的整數(shù)形式的IP地址。例如,下面的代碼將整數(shù)形式的IP地址轉(zhuǎn)換為字符串形式的IP地址:
struct in_addr addr;
addr.s_addr = ip;
char *ip_str = inet_ntoa(addr);
主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序的轉(zhuǎn)換
在網(wǎng)絡(luò)通信中,通常使用網(wǎng)絡(luò)字節(jié)序進行數(shù)據(jù)傳輸和通信,而在計算機內(nèi)部使用的是主機字節(jié)序。因此,在進行數(shù)據(jù)傳輸和通信時,需要進行主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間的轉(zhuǎn)換。這可以通過使用htons()、ntohs()、htonl()和ntohl()等函數(shù)來實現(xiàn)。
htons()函數(shù)將一個16位的主機字節(jié)序的整數(shù)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的整數(shù),函數(shù)原型如下:
uint16_t htons(uint16_t hostshort);
例如,下面的代碼將一個16位的主機字節(jié)序的整數(shù)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的整數(shù):
uint16_t host_num = 12345;
uint16_t net_num = htons(host_num);
ntohs()函數(shù)將一個16位的網(wǎng)絡(luò)字節(jié)序的整數(shù)轉(zhuǎn)換為主機字節(jié)序的整數(shù),函數(shù)原型如下:
uint16_t ntohs(uint16_t netshort);
例如,下面的代碼將一個16位的網(wǎng)絡(luò)字節(jié)序的整數(shù)轉(zhuǎn)換為主機字節(jié)序的整數(shù):
uint16_t net_num = 0x3039;
uint16_t host_num = ntohs(net_num);
htonl()函數(shù)將一個32位的主機字節(jié)序的整數(shù)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的整數(shù),函數(shù)原型如下:
uint32_t htonl(uint32_t hostlong);
例如,下面的代碼將一個32位的主機字節(jié)序的整數(shù)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的整數(shù):
uint32_t host_num = 0x01020304;
uint32_t net_num = htonl(host_num);
ntohl()函數(shù)將一個32位的網(wǎng)絡(luò)字節(jié)序的整數(shù)轉(zhuǎn)換為主機字節(jié)序的整數(shù),函數(shù)原型如下:
uint32_t ntohl(uint32_t netlong);
例如,下面的代碼將一個32位的網(wǎng)絡(luò)字節(jié)序的整數(shù)轉(zhuǎn)換為主機字節(jié)序的整數(shù):
uint32_t net_num = 0x04030201;
uint32_t host_num = ntohl(net_num);
總之,IP地址轉(zhuǎn)換包括字符串IP地址轉(zhuǎn)整數(shù)IP地址和整數(shù)IP地址轉(zhuǎn)字符串IP地址兩種方式,可以通過inet_addr()和inet_ntoa()函數(shù)來實現(xiàn)。主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序的轉(zhuǎn)換可以通過htons()、ntohs()、htonl()和ntohl()等函數(shù)來實現(xiàn)。在進行網(wǎng)絡(luò)通信時,需要進行主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間的轉(zhuǎn)換,以保證數(shù)據(jù)的正確傳輸。
十一、TCP 通信流程

創(chuàng)建Socket:服務(wù)端和客戶端都需要創(chuàng)建一個Socket對象,通過該對象進行通信。在創(chuàng)建Socket時,需要指定協(xié)議族(一般為AF_INET或AF_INET6)、傳輸層協(xié)議(一般為SOCK_STREAM)、以及協(xié)議編號(一般為0)等參數(shù)。
綁定Socket:服務(wù)端需要將Socket綁定到一個固定的IP地址和端口號上。這可以通過bind()函數(shù)實現(xiàn)。如果綁定成功,則表明服務(wù)端已經(jīng)可以接收客戶端的連接請求。
監(jiān)聽連接請求:服務(wù)端在綁定成功后,可以調(diào)用listen()函數(shù)開始監(jiān)聽客戶端的連接請求。listen()函數(shù)的參數(shù)是一個整數(shù)值,表示在等待連接隊列中最多可以容納的連接數(shù)。
接受連接請求:一旦服務(wù)端開始監(jiān)聽連接請求,客戶端就可以通過connect()函數(shù)向服務(wù)端發(fā)送連接請求。服務(wù)端在接收到連接請求后,可以調(diào)用accept()函數(shù)接受該連接請求,并返回一個新的Socket對象,用于與客戶端進行通信。
數(shù)據(jù)傳輸:一旦服務(wù)端和客戶端建立起連接,它們就可以通過Socket對象進行數(shù)據(jù)傳輸。服務(wù)端和客戶端都可以調(diào)用send()函數(shù)和recv()函數(shù)進行數(shù)據(jù)發(fā)送和接收。
關(guān)閉連接:數(shù)據(jù)傳輸完成后,服務(wù)端和客戶端都可以調(diào)用close()函數(shù)關(guān)閉連接。在關(guān)閉連接之前,服務(wù)端和客戶端都應(yīng)該通過shutdown()函數(shù)發(fā)送一個關(guān)閉信號,以確保對方可以正常地收到該信號并完成數(shù)據(jù)傳輸。
需要注意的是,服務(wù)端和客戶端的通信流程可能會因為實際場景的不同而有所差異,例如在使用多線程或多進程時,服務(wù)端需要在accept()函數(shù)中調(diào)用fork()或pthread_create()來創(chuàng)建新的進程或線程來處理新的連接請求。
十二、socket 函數(shù)

socket()
函數(shù)原型:int socket(int domain, int type, int protocol)
作用:創(chuàng)建socket套接字。
參數(shù):
domain:協(xié)議族,常見的有AF_INET(IPv4)和AF_INET6(IPv6)。
type:套接字類型,常見的有SOCK_STREAM(流套接字)和SOCK_DGRAM(數(shù)據(jù)報套接字)。
protocol:協(xié)議編號,常見的有IPPROTO_TCP(TCP協(xié)議)和IPPROTO_UDP(UDP協(xié)議)。
返回值:成功返回一個新的socket的文件描述符(socket descriptor),失敗返回-1。
bind()
函數(shù)原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
作用:將socket與本地的IP地址和端口號綁定。
參數(shù):
sockfd:socket套接字的文件描述符。
addr:指向sockaddr結(jié)構(gòu)體的指針,包含了IP地址和端口號信息。
addrlen:sockaddr結(jié)構(gòu)體的長度。
返回值:成功返回0,失敗返回-1。
listen()
函數(shù)原型:int listen(int sockfd, int backlog)
作用:將socket設(shè)置為監(jiān)聽狀態(tài),等待客戶端的連接請求。
參數(shù):
sockfd:socket套接字的文件描述符。
backlog:等待連接隊列的最大長度。
返回值:成功返回0,失敗返回-1。
accept()
函數(shù)原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
作用:接受客戶端的連接請求,并返回一個新的socket套接字,用于與客戶端進行通信。
參數(shù):
sockfd:socket套接字的文件描述符。
addr:指向sockaddr結(jié)構(gòu)體的指針,用于存儲客戶端的IP地址和端口號信息。
addrlen:sockaddr結(jié)構(gòu)體的長度。
返回值:成功返回一個新的socket的文件描述符,失敗返回-1。
connect()
函數(shù)原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
作用:向指定的IP地址和端口號發(fā)起連接請求。
參數(shù):
sockfd:socket套接字的文件描述符。
addr:指向sockaddr結(jié)構(gòu)體的指針,包含了服務(wù)端的IP地址和端口號信息。
addrlen:sockaddr結(jié)構(gòu)體的長度。
返回值:成功返回0,失敗返回-1。
write() / send()和read() / recv()
函數(shù)原型:
ssize_t write(int fd, const void *buf, size_t count)
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
ssize_t read(int fd, void *buf, size_t count)
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
作用:用于發(fā)送和接收數(shù)據(jù)。
參數(shù):
fd/sockfd:文件描述符或socket的文件描述符。
buf:指向要發(fā)送或接收的數(shù)據(jù)的緩沖區(qū)。
count/len:發(fā)送或接收的數(shù)據(jù)的字節(jié)數(shù)。
flags:發(fā)送或接收數(shù)據(jù)的選項,默認為0。
返回值:成功返回發(fā)送或接收的字節(jié)數(shù),失敗返回-1。
總的來說,socket()、bind()、listen()、accept()、connect()、write()和read()是socket編程中最常用的一些函數(shù),通過它們可以實現(xiàn)網(wǎng)絡(luò)通信的基本功能。
十三、TCP通信實現(xiàn)(服務(wù)器端)

十四、TCP通信實現(xiàn)(客戶端)
1.創(chuàng)建socket對象,使用socket()函數(shù)創(chuàng)建一個socket對象,并指定協(xié)議族、傳輸類型和協(xié)議編號。
2.連接服務(wù)器,使用connect()函數(shù)向服務(wù)器發(fā)起連接請求,指定服務(wù)器的IP地址和端口號。
3.與服務(wù)器進行通信,使用新得到的socket對象與服務(wù)器進行通信,可以使用write()函數(shù)向服務(wù)器發(fā)送數(shù)據(jù),使用read()函數(shù)從服務(wù)器接收數(shù)據(jù)。
4.關(guān)閉socket對象,在通信結(jié)束后,需要使用close()函數(shù)關(guān)閉socket對象。
十五、TCP三次握手

十六、滑動窗口

具體來說,TCP的滑動窗口是由發(fā)送方和接收方各自維護的一個窗口緩存區(qū)。發(fā)送方通過維護一個發(fā)送窗口來控制發(fā)送數(shù)據(jù)的速率,接收方通過維護一個接收窗口來告知發(fā)送方自己可以接收的數(shù)據(jù)的大小。發(fā)送方每次發(fā)送數(shù)據(jù)時,會根據(jù)接收方返回的窗口大小來動態(tài)調(diào)整自己的發(fā)送窗口大小,以保證在網(wǎng)絡(luò)擁塞的情況下不會發(fā)送過多的數(shù)據(jù),從而導致網(wǎng)絡(luò)阻塞或數(shù)據(jù)丟失。
在TCP的滑動窗口中,發(fā)送方和接收方都維護一個窗口大小變量和一個窗口指針變量。發(fā)送方的窗口大小表示自己可以發(fā)送的數(shù)據(jù)量,窗口指針指向下一個可以發(fā)送的數(shù)據(jù),接收方的窗口大小表示自己可以接收的數(shù)據(jù)量,窗口指針指向下一個期望接收的數(shù)據(jù)。
發(fā)送方在發(fā)送數(shù)據(jù)時,會將數(shù)據(jù)按照窗口大小分成多個數(shù)據(jù)塊,每發(fā)送一個數(shù)據(jù)塊,就將發(fā)送窗口大小減去對應(yīng)數(shù)據(jù)塊的大小。接收方在接收數(shù)據(jù)時,會根據(jù)接收到的數(shù)據(jù)更新接收窗口大小和窗口指針,然后將窗口大小和窗口指針發(fā)送給發(fā)送方,以告知發(fā)送方自己可以接收的數(shù)據(jù)量和下一個期望接收的數(shù)據(jù)。
發(fā)送方收到接收方返回的窗口大小后,會根據(jù)窗口大小調(diào)整自己的發(fā)送窗口大小,然后將窗口指針更新到下一個可以發(fā)送的數(shù)據(jù)塊的位置。這樣,發(fā)送方就可以根據(jù)接收方返回的窗口大小和窗口指針來動態(tài)調(diào)整發(fā)送數(shù)據(jù)的速率,以適應(yīng)網(wǎng)絡(luò)狀況的變化。
總的來說,TCP的滑動窗口是一種非常重要的流量控制機制,它可以根據(jù)網(wǎng)絡(luò)狀況的變化動態(tài)調(diào)整發(fā)送和接收數(shù)據(jù)的速率,從而保證數(shù)據(jù)傳輸?shù)目煽啃院托省?/figcaption>
十七、TCP四次揮手

十八、多進程實現(xiàn)并發(fā)服務(wù)器

十九、多線程實現(xiàn)并發(fā)服務(wù)器

二十、TCP狀態(tài)轉(zhuǎn)換

二十一、半關(guān)閉、端口復用

半關(guān)閉可以在以下情況下使用:
1.應(yīng)用程序只需要向遠程主機發(fā)送數(shù)據(jù),而不需要再接收數(shù)據(jù)時,可以關(guān)閉套接字上的寫端(寫半關(guān)閉)。
2.應(yīng)用程序只需要接收遠程主機發(fā)送的數(shù)據(jù),而不需要再向遠程主機發(fā)送數(shù)據(jù)時,可以關(guān)閉套接字上的讀端(讀半關(guān)閉)。
半關(guān)閉的實現(xiàn)可以通過調(diào)用 shutdown() 函數(shù)來完成。例如,如果要寫半關(guān)閉一個套接字,可以使用以下代碼:
shutdown(sock_fd, SHUT_WR);
其中,sock_fd 是套接字文件描述符,SHUT_WR 表示寫半關(guān)閉。
總之,半關(guān)閉是一種在網(wǎng)絡(luò)編程中常見的技術(shù),它可以提高網(wǎng)絡(luò)連接的靈活性和可靠性,但需要應(yīng)用程序和網(wǎng)絡(luò)協(xié)議的支持。

在 Linux 中,端口復用可以通過設(shè)置套接字選項來實現(xiàn)。具體而言,可以使用 setsockopt() 函數(shù)設(shè)置 SO_REUSEADDR 或者 SO_REUSEPORT 套接字選項來開啟端口復用功能。
SO_REUSEADDR 選項
SO_REUSEADDR 選項可用于在一個套接字關(guān)閉后立即釋放其端口,以便其他套接字可以立即綁定到該端口上。如果沒有設(shè)置 SO_REUSEADDR 選項,那么在套接字關(guān)閉后,操作系統(tǒng)會將該端口保留一段時間,以確保不會有任何延遲數(shù)據(jù)包到達。這段時間稱為 TIME_WAIT 狀態(tài),此時其他套接字不能立即綁定到該端口上。
在設(shè)置 SO_REUSEADDR 選項時,需要將其設(shè)置為一個非零值。例如:
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
其中 sockfd 是套接字文件描述符。
SO_REUSEPORT 選項
SO_REUSEPORT 選項可用于在同一 IP 地址和端口上啟動多個套接字監(jiān)聽。如果沒有設(shè)置 SO_REUSEPORT 選項,那么只能有一個套接字綁定到同一 IP 地址和端口上。在設(shè)置 SO_REUSEPORT 選項時,需要將其設(shè)置為一個非零值。例如:
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));
需要注意的是,SO_REUSEPORT 選項只在 Linux 3.9 及以上版本中可用。
總之,端口復用是一種非常有用的技術(shù),它可以提高網(wǎng)絡(luò)應(yīng)用程序的靈活性和可靠性。
端口復用技術(shù)可以讓多個套接字同時綁定到同一個端口,這樣可以實現(xiàn)多個進程或應(yīng)用程序共享同一個端口,避免了端口被占用而導致啟動失敗的情況。但是,多個套接字同時綁定到同一個端口也會帶來一些影響,主要包括以下幾點:
數(shù)據(jù)分發(fā)問題:當多個套接字同時綁定到同一個端口時,如果有數(shù)據(jù)傳輸?shù)竭@個端口,那么操作系統(tǒng)會將數(shù)據(jù)復制到所有綁定到這個端口的套接字中。這可能會導致數(shù)據(jù)分發(fā)不均,某些套接字可能會接收到更多的數(shù)據(jù),而某些套接字可能會接收到較少的數(shù)據(jù)。
進程之間的通信:如果有多個進程或應(yīng)用程序同時綁定到同一個端口,那么它們之間可能會出現(xiàn)通信問題。因為這些進程或應(yīng)用程序是獨立的,它們無法共享數(shù)據(jù),也無法協(xié)調(diào)數(shù)據(jù)的接收和處理。
端口復用相關(guān)的選項:如果多個套接字同時綁定到同一個端口,那么它們之間可能會相互影響,例如SO_REUSEADDR和SO_REUSEPORT選項等。在這種情況下,需要仔細考慮這些選項的使用,并確保它們不會導致意外的行為。
綜上所述,雖然端口復用技術(shù)可以讓多個套接字同時綁定到同一個端口,但是需要注意上述問題,并根據(jù)實際情況進行選擇和使用。
二十二、IO多路復用(多路轉(zhuǎn)接)簡介
I/O多路復用使得程序能同時監(jiān)聽多個文件描述符,能夠提高程序的性能,Linux下實現(xiàn)I/O多路復用的系統(tǒng)調(diào)用主要有select、poll和epoll。

在阻塞等待 BIO 模型中,應(yīng)用程序通過調(diào)用阻塞 I/O 函數(shù)來等待數(shù)據(jù)的到達。例如,使用阻塞等待 BIO 模型實現(xiàn) TCP 服務(wù)器時,可以使用 accept() 函數(shù)阻塞等待客戶端連接。如果沒有客戶端連接到達,accept() 函數(shù)將一直阻塞,直到有新的連接到達為止。

在非阻塞 NIO 模型中,應(yīng)用程序可以通過以下步驟來實現(xiàn)異步 I/O 操作:
創(chuàng)建非阻塞套接字(non-blocking socket),并將其設(shè)置為非阻塞模式。在 Linux 中,可以使用 fcntl() 函數(shù)將套接字設(shè)置為非阻塞模式。
使用 select()、poll()、epoll() 等多路復用函數(shù)來等待 I/O 事件的發(fā)生。這些函數(shù)可以監(jiān)視多個套接字的 I/O 事件,并在有事件發(fā)生時立即返回。在 Linux 中,epoll() 是一種高效的多路復用函數(shù),可以同時監(jiān)視多個套接字,具有較高的性能和可擴展性。
當有 I/O 事件發(fā)生時,應(yīng)用程序可以通過調(diào)用非阻塞 I/O 函數(shù)來進行 I/O 操作。在 Linux 中,可以使用 read()、write()、recv()、send() 等非阻塞 I/O 函數(shù)來實現(xiàn)。非阻塞 NIO 模型是一種高效、可擴展的網(wǎng)絡(luò)編程模型,適用于高并發(fā)、高吞吐量的網(wǎng)絡(luò)應(yīng)用程序。但是,使用非阻塞 NIO 模型需要更多的編程工作,因為應(yīng)用程序需要手動管理 I/O 事件和套接字的狀態(tài)。
二十三、select API介紹

nfds:待檢查的最大文件描述符值加 1。
readfds:包含待檢查的可讀文件描述符的文件描述符集合。
writefds:包含待檢查的可寫文件描述符的文件描述符集合。
exceptfds:包含待檢查的異常文件描述符的文件描述符集合。
timeout:等待超時時間的指針,如果為 NULL,則表示一直等待,直到有事件發(fā)生。
返回值說明:
如果有文件描述符在指定時間內(nèi)發(fā)生了可讀、可寫或異常狀態(tài),則返回一個大于 0 的值,表示就緒的文件描述符的個數(shù)。
如果在指定時間內(nèi)沒有任何文件描述符發(fā)生可讀、可寫或異常狀態(tài),則返回 0。
如果函數(shù)調(diào)用失敗,則返回 -1,并設(shè)置 errno。
使用 select() 函數(shù)的步驟如下:
創(chuàng)建一組文件描述符集合,并將待監(jiān)視的文件描述符添加到對應(yīng)的集合中。
調(diào)用 select() 函數(shù),并傳入待監(jiān)視的文件描述符集合、超時時間等參數(shù)。
如果 select() 函數(shù)返回大于 0 的值,則表示有文件描述符發(fā)生了可讀、可寫或異常狀態(tài)。
使用 FD_ISSET() 宏函數(shù)來遍歷就緒的文件描述符集合,檢查哪些文件描述符發(fā)生了可讀、可寫或異常狀態(tài),并進行相應(yīng)的處理。

FD_SET() 宏用于將一個文件描述符添加到 fd_set 結(jié)構(gòu)體中。參數(shù) fd 表示待添加的文件描述符,參數(shù) set 是一個指向 fd_set 結(jié)構(gòu)體的指針,表示待添加文件描述符的集合。使用 FD_SET() 宏可以將一個文件描述符添加到文件描述符集合中,以便調(diào)用 select() 函數(shù)對其進行監(jiān)視。FD_CLR() 宏用于將一個文件描述符從 fd_set 結(jié)構(gòu)體中刪除。參數(shù) fd 表示待刪除的文件描述符,參數(shù) set 是一個指向 fd_set 結(jié)構(gòu)體的指針,表示待刪除文件描述符的集合。使用 FD_CLR() 宏可以將一個文件描述符從文件描述符集合中刪除,以便在處理完該文件描述符后,避免重復處理已關(guān)閉的文件描述符。FD_ISSET() 宏用于判斷一個文件描述符是否在 fd_set 結(jié)構(gòu)體中。參數(shù) fd 表示待判斷的文件描述符,參數(shù) set 是一個指向 fd_set 結(jié)構(gòu)體的指針,表示待判斷文件描述符的集合。使用 FD_ISSET() 宏可以判斷一個文件描述符是否在文件描述符集合中,以便確定該文件描述符是否處于就緒狀態(tài),需要進行處理。
這四個宏通常與 select() 函數(shù)一起使用,用于構(gòu)建多路復用 I/O 模型。在使用 select() 函數(shù)前,需要先創(chuàng)建一個 fd_set 結(jié)構(gòu)體,并使用 FD_ZERO() 宏將其清零,然后將待監(jiān)視的文件描述符添加到集合中,使用 select() 函數(shù)等待文件描述符就緒,最后使用 FD_ISSET() 宏判斷文件描述符是否就緒,并使用 FD_CLR() 宏將其從集合中刪除,以便下次調(diào)用 select() 函數(shù)時重新添加到集合中。

創(chuàng)建 fd_set 結(jié)構(gòu)體并添加待監(jiān)視的文件描述符
在調(diào)用 select() 函數(shù)前,需要先創(chuàng)建一個 fd_set 結(jié)構(gòu)體,并使用 FD_ZERO() 宏將其清零,然后將待監(jiān)視的文件描述符添加到集合中,使用 FD_SET() 宏將其加入到集合中。
調(diào)用 select() 函數(shù)
調(diào)用 select() 函數(shù)等待文件描述符就緒,當文件描述符就緒時,select() 函數(shù)返回,程序繼續(xù)執(zhí)行。參數(shù) nfds 指定待監(jiān)視的文件描述符集合中的最大文件描述符值加 1,readfds、writefds、exceptfds 分別為指向待監(jiān)視的讀、寫、異常文件描述符集合的指針,timeout 表示等待時間,如果為 NULL 則表示一直等待。
使用 FD_ISSET() 宏判斷文件描述符是否就緒
select() 函數(shù)返回后,需要使用 FD_ISSET() 宏判斷哪些文件描述符已經(jīng)就緒,以便進行相應(yīng)的操作。如果文件描述符已經(jīng)就緒,F(xiàn)D_ISSET() 宏返回真,否則返回假。
使用 FD_CLR() 宏將已處理的文件描述符從集合中刪除
在處理完一個文件描述符后,需要使用 FD_CLR() 宏將其從文件描述符集合中刪除,以便下次調(diào)用 select() 函數(shù)時重新添加到集合中。
在多數(shù)情況下,select() 函數(shù)會被用于實現(xiàn)基于事件驅(qū)動的 I/O 模型。當有 I/O 事件發(fā)生時,select() 函數(shù)會返回就緒的文件描述符,然后程序可以根據(jù)文件描述符進行相應(yīng)的處理,例如讀取數(shù)據(jù)、寫入數(shù)據(jù)等。使用 select() 函數(shù)可以避免阻塞等待單個 I/O 事件發(fā)生,提高程序的并發(fā)性和響應(yīng)性。
二十四、poll API介紹

文件描述符數(shù)量限制
在一些操作系統(tǒng)中,select() 函數(shù)對文件描述符的數(shù)量有限制,通常最多只能監(jiān)視 1024 個文件描述符。如果需要監(jiān)視更多的文件描述符,可以使用 poll() 函數(shù)或 epoll API。
慢速系統(tǒng)調(diào)用
select() 函數(shù)是一個慢速系統(tǒng)調(diào)用,即在調(diào)用 select() 函數(shù)時會阻塞當前進程,直到有文件描述符就緒或等待超時。這會導致程序的響應(yīng)性變差,特別是當需要監(jiān)視大量的文件描述符時。
頻繁的內(nèi)存拷貝
在調(diào)用 select() 函數(shù)時,需要將文件描述符集合從用戶態(tài)拷貝到內(nèi)核態(tài),然后將就緒的文件描述符集合從內(nèi)核態(tài)拷貝回用戶態(tài)。這會導致頻繁的內(nèi)存拷貝,降低程序的性能。
沒有事件通知機制
在使用 select() 函數(shù)時,需要不斷地調(diào)用它來檢查文件描述符是否就緒,這會導致 CPU 的大量消耗。而且 select() 函數(shù)并沒有事件通知機制,即當文件描述符就緒時,它并不能主動通知應(yīng)用程序,而是需要應(yīng)用程序不斷地輪詢。
不支持對文件描述符的動態(tài)增刪
在使用 select() 函數(shù)時,如果需要增加或刪除待監(jiān)視的文件描述符,需要重新創(chuàng)建一個新的文件描述符集合,并將待監(jiān)視的文件描述符重新添加到集合中。這會導致程序的復雜性增加,特別是當需要動態(tài)地增加或刪除文件描述符時。
綜上所述,select() 函數(shù)在某些情況下存在一些缺點,特別是在需要監(jiān)視大量文件描述符或需要動態(tài)增刪文件描述符時。為了解決這些問題,可以使用其他的 I/O 多路復用函數(shù),如 poll() 函數(shù)或 epoll API。

二十五、epoll API介紹

使用 epoll() 多路復用機制的基本流程如下:
創(chuàng)建 epoll 實例,即調(diào)用 epoll_create() 函數(shù),該函數(shù)返回一個文件描述符,用于后續(xù)的 epoll 操作。
向 epoll 實例中添加待監(jiān)視的文件描述符,即調(diào)用 epoll_ctl() 函數(shù),指定操作類型為 EPOLL_CTL_ADD,參數(shù) fd 為待監(jiān)視的文件描述符,參數(shù) event 為待監(jiān)視的事件類型和數(shù)據(jù)。
等待文件描述符就緒,即調(diào)用 epoll_wait() 函數(shù),該函數(shù)會一直阻塞,直到有文件描述符就緒或超時,然后返回就緒的文件描述符列表和事件類型。
處理就緒的文件描述符和事件,即根據(jù)返回的文件描述符列表和事件類型,處理相應(yīng)的 I/O 操作,例如讀取數(shù)據(jù)、寫入數(shù)據(jù)等。
如果需要修改或刪除待監(jiān)視的文件描述符,可以調(diào)用 epoll_ctl() 函數(shù),指定操作類型為 EPOLL_CTL_MOD 或 EPOLL_CTL_DEL,然后重新調(diào)用 epoll_wait() 等待文件描述符就緒。
使用 epoll() 多路復用機制可以避免使用多個線程或進程處理多個文件描述符,從而提高程序的并發(fā)性能和可維護性。同時,epoll() 多路復用機制也具有較低的系統(tǒng)開銷和較高的效率,適用于高并發(fā)的網(wǎng)絡(luò)編程場景。

epoll_create()
epoll_create() 用于創(chuàng)建一個 epoll 實例,并返回一個文件描述符,該文件描述符可以用于后續(xù)的 epoll 操作。epoll_create() 的原型定義如下:
int epoll_create(int size);
參數(shù) size 指定 epoll 實例中可以監(jiān)視的文件描述符數(shù)量,實際上該參數(shù)在 Linux 2.6.8 之后已經(jīng)被忽略,可以傳遞任何值。
epoll_ctl()
epoll_ctl() 用于向 epoll 實例中添加、修改或刪除待監(jiān)視的文件描述符。epoll_ctl() 的原型定義如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數(shù) epfd 是 epoll 實例的文件描述符,op 是操作類型,可以是 EPOLL_CTL_ADD、EPOLL_CTL_MOD 或 EPOLL_CTL_DEL,分別表示添加、修改和刪除操作。參數(shù) fd 是待監(jiān)視的文件描述符,event 是一個 epoll_event 結(jié)構(gòu)體,用于指定待監(jiān)視的事件類型和數(shù)據(jù)。
epoll_event 結(jié)構(gòu)體的定義如下:
typedef union epoll_data {
? ? void *ptr;
? ? int fd;
? ? uint32_t u32;
? ? uint64_t u64;
} epoll_data_t;
struct epoll_event {
? ? uint32_t events; ? ? ?// 監(jiān)視的事件類型(例如 EPOLLIN、EPOLLOUT、EPOLLERR 等)
? ? epoll_data_t data; ? ?// 用戶數(shù)據(jù)(例如文件描述符、指針等)
};
epoll_wait()
epoll_wait() 用于在 epoll 實例上等待文件描述符就緒,并返回就緒的文件描述符列表。epoll_wait() 的原型定義如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
參數(shù) epfd 是 epoll 實例的文件描述符,events 是一個 epoll_event 結(jié)構(gòu)體數(shù)組,用于存儲就緒的文件描述符和事件類型。參數(shù) maxevents 指定 events 數(shù)組的長度,即最多可以返回多少個就緒的文件描述符。參數(shù) timeout 指定等待時間,如果為 -1 則表示一直等待,如果為 0 則表示立即返回,如果大于 0 則表示等待指定的毫秒數(shù)。
epoll API 支持邊緣觸發(fā)(ET)和水平觸發(fā)(LT)兩種工作模式,可以根據(jù)具體的應(yīng)用場景選擇合適的模式。與 select() 和 poll() 函數(shù)相比,epoll API 具有更高效、更靈活的特點,可以監(jiān)視大量的文件描述符,并且支持邊緣觸發(fā)和水平觸發(fā)模式。
二十六、epoll的兩種工作模式

邊緣觸發(fā)(ET)
在邊緣觸發(fā)模式下,當文件描述符上有新的事件發(fā)生時,epoll_wait() 函數(shù)會返回該事件,然后需要立即處理該事件,否則下次調(diào)用 epoll_wait() 函數(shù)時不會再次返回該事件。特別地,對于可讀和可寫事件,只有在文件描述符狀態(tài)從不可讀/不可寫轉(zhuǎn)變?yōu)榭勺x/可寫時才會觸發(fā)該事件。
邊緣觸發(fā)模式相對于水平觸發(fā)模式有更高的效率,因為它只在事件發(fā)生時通知應(yīng)用程序,減少了無用的通知。但是,它也需要應(yīng)用程序具有更高的處理速度,以避免事件丟失。
水平觸發(fā)(LT)
在水平觸發(fā)模式下,當文件描述符上有新的事件發(fā)生時,epoll_wait() 函數(shù)會返回該事件,然后需要一直處理該事件,直到文件描述符上的事件被處理完畢或不再有新的事件發(fā)生為止。對于可讀和可寫事件,只要文件描述符上還有數(shù)據(jù)可讀/可寫,就會一直觸發(fā)該事件。
水平觸發(fā)模式需要應(yīng)用程序一直處理文件描述符上的事件,否則會導致 CPU 的大量消耗。但是,它也更加靈活,可以在任何時候處理文件描述符上的事件,而不必擔心事件丟失。
綜上所述,邊緣觸發(fā)模式和水平觸發(fā)模式各有優(yōu)缺點,應(yīng)根據(jù)具體的應(yīng)用場景選擇合適的模式。如果需要高效地處理事件,可以選擇邊緣觸發(fā)模式;如果需要更加靈活地處理事件,并且可以承受更高的 CPU 消耗,可以選擇水平觸發(fā)模式。
*epoll高效的原因
使用紅黑樹數(shù)據(jù)結(jié)構(gòu):epoll 將待監(jiān)視的文件描述符存儲在紅黑樹中,并使用哈希表來快速查找文件描述符對應(yīng)的數(shù)據(jù)結(jié)構(gòu),從而使得監(jiān)視文件描述符的查找效率更高。
支持邊緣觸發(fā)和水平觸發(fā)模式:epoll 可以選擇邊緣觸發(fā)(ET)和水平觸發(fā)(LT)兩種模式,可以根據(jù)具體的應(yīng)用場景選擇合適的模式,進一步提高程序的效率和性能。
避免大量的系統(tǒng)調(diào)用:epoll 可以在一個系統(tǒng)調(diào)用中同時監(jiān)視多個文件描述符,避免了傳統(tǒng)的 select() 和 poll() 函數(shù)中需要多次調(diào)用的問題,從而減少了系統(tǒng)調(diào)用的次數(shù),提高了程序的效率。
避免了文件描述符集合的限制:epoll 沒有像 select() 和 poll() 函數(shù)那樣限制監(jiān)視的文件描述符數(shù)量,可以處理大量的文件描述符,從而適用于高并發(fā)的網(wǎng)絡(luò)編程場景。
支持文件描述符的復用:epoll 可以通過設(shè)置文件描述符的屬性,使其在多個 epoll 實例中共享,從而避免了文件描述符重復創(chuàng)建的問題,提高了程序的效率和可維護性。
二十七、UDP通信實現(xiàn)

在客戶端代碼中,首先創(chuàng)建UDP Socket,并設(shè)置服務(wù)端的IP地址和端口號。然后,使用sendto函數(shù)向服務(wù)端發(fā)送消息,并使用recvfrom函數(shù)接收服務(wù)端發(fā)送的消息。最后,關(guān)閉Socket。
需要注意的是,在UDP通信中,由于UDP協(xié)議不保證數(shù)據(jù)的可靠性和順序性,因此需要在應(yīng)用程序中自行實現(xiàn)數(shù)據(jù)的校驗和處理。此外,UDP協(xié)議通常用于實時性比較高的應(yīng)用場景,例如視頻和音頻傳輸?shù)取?/figcaption>
二十八、廣播

setsockopt:setsockopt是一個Socket API函數(shù),用于設(shè)置Socket的選項。通過setsockopt函數(shù),可以配置Socket的多種參數(shù),例如超時時間,緩存大小,廣播選項等。setsockopt函數(shù)的原型如下:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
其中,sockfd是Socket的文件描述符,level表示選項所在的協(xié)議層,optname表示選項的名稱,optval表示選項的值,optlen表示選項值的長度。例如,可以使用setsockopt函數(shù)來設(shè)置SO_BROADCAST選項,以啟用廣播模式:
int broadcastEnable = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable));
上述代碼中,sockfd是Socket的文件描述符,SOL_SOCKET表示Socket本身的選項,SO_BROADCAST表示廣播選項,broadcastEnable是一個int類型的變量,設(shè)置為1表示啟用廣播模式。通過調(diào)用setsockopt函數(shù),可以將Socket設(shè)置為廣播模式,從而實現(xiàn)向同一網(wǎng)絡(luò)中的所有設(shè)備發(fā)送消息的功能。
二十九、組播

在IPv4中,組播地址的范圍是224.0.0.0到239.255.255.255,其中224.0.0.0是預留地址,不能用于實際通信,239.255.255.255是全局組播地址,用于向整個Internet發(fā)送消息。在IPv6中,組播地址的范圍是ff00::/8,其中ff01::是節(jié)點本地組播地址,ff02::是本地鏈路組播地址,ff05::是站點本地組播地址,ff08::是組織本地組播地址。
在Linux中,可以使用Socket API中的setsockopt函數(shù)來加入和退出組播,需要設(shè)置IPPROTO_IP或IPPROTO_IPV6協(xié)議的IP_ADD_MEMBERSHIP或IP_DROP_MEMBERSHIP選項。例如,可以使用以下代碼將Socket加入到組播地址組中:
// 加入IPv4組播地址組
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1"); // 組播地址
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 本地IP地址
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
// 加入IPv6組播地址組
struct ipv6_mreq mreq6;
inet_pton(AF_INET6, "ff02::1", &mreq6.ipv6mr_multiaddr); // 組播地址
mreq6.ipv6mr_interface = 0; // 本地接口
setsockopt(sockfd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq6, sizeof(mreq6));
上述代碼中,sockfd是Socket的文件描述符,IPPROTO_IP和IPPROTO_IPV6分別表示IPv4和IPv6協(xié)議,IP_ADD_MEMBERSHIP和IPV6_ADD_MEMBERSHIP表示加入組播的選項,mreq和mreq6是組播地址和本地接口的結(jié)構(gòu)體。通過調(diào)用setsockopt函數(shù),可以將Socket加入到指定的組播地址組中,從而實現(xiàn)組播通信功能。

在客戶端代碼中,同樣首先創(chuàng)建Socket,然后設(shè)置Socket的地址和端口號。然后,通過循環(huán)調(diào)用fgets函數(shù),從標準輸入中獲取用戶輸入的消息,并通過sendto函數(shù)將消息發(fā)送給服務(wù)端??蛻舳瞬恍枰尤虢M播地址組,只需要向服務(wù)端發(fā)送消息即可。
需要注意的是,服務(wù)端和客戶端需要使用相同的組播地址和端口號,才能進行組播通信。此外,服務(wù)端需要在運行前,先運行ifconfig命令,獲取本機的IP地址,并將其設(shè)置為IMR_INTERFACE選項的值。
??
三十、本地套接字通信

在使用本地套接字通信時,需要指定一個本地套接字文件(Socket File),用于標識通信的進程。本地套接字文件儲存在文件系統(tǒng)中,并具有與普通文件相同的權(quán)限和屬性。本地套接字文件的命名規(guī)則為以NUL結(jié)尾的路徑名(路徑名中不能包含斜杠),通常存放在/tmp目錄中。