文章目錄
- 📝理解源IP地址和目的IP地址
- 🌠 認識端口號
- 🌉端口號范圍劃分
- 🌉理解"端口號"和"進程ID"
- 🌉理解源端口號和目的端口號
- 🌉理解socket
- 🌠傳輸層的典型代表
- 🌉認識TCP協議
- 🌉 認識UDP協議
- 🌠網絡字節序
- 🌠Socket 編程接口
- 🌉Socket 常見API
- 🌉sockaddr 結構
- 🌉`in_addr` 結構
- 🚩總結
📝理解源IP地址和目的IP地址
- IP在網絡中,用來標識主機的唯一性
- 注意:后面我們會講IP的分類,后面會詳細闡述IP的特點
但是這里要思考一個問題:數據傳輸到主機是目的嗎?不是的。因為數據是給人用的。比如:聊天是人在聊天,下載是人在下載,瀏覽網頁是人在瀏覽?
但是人是怎么看到聊天信息的呢?怎么執行下載任務呢?怎么瀏覽網頁信息呢?通過啟動的qq,迅雷,瀏覽器。
而啟動的qq,迅雷,瀏覽器都是進程。換句話說,進程是人在系統中的代表,只要把數據給進程,人就相當于就拿到了數據。
所以:數據傳輸到主機不是目的,而是手段。到達主機內部,在交給主機內的進程,才是目的。
但是系統中,同時會存在非常多的進程,當數據到達目標主機之后,怎么轉發給目標進程?這就要在網絡的背景下,在系統中,標識主機的唯一性。
🌠 認識端口號
端口號(port)是傳輸層協議的內容
- 端口號是一個2字節16位的整數;
- 端口號用來標識一個進程,告訴操作系統,當前的這個數據要交給哪一個進程來處理;
IP
地址+端口號能夠標識網絡上的某一臺主機的某一個進程;- 一個端口號只能被一個進程占用.
🌉端口號范圍劃分
- 0-1023: 知名端口號,HTTP,FTP,SSH等這些廣為使用的應用層協議,他們的端口號都是固定的.
- 1024-65535: 操作系統動態分配的端口號.客戶端程序的端口號,就是由操作系統從這個范圍分配的
🌉理解"端口號"和"進程ID"
我們之前在學習系統編程的時候,學習了pid表示唯一一個進程;此處我們的端口號也是唯一表示一個進程.那么這兩者之間是怎樣的關系?
在Linux系統中,進程ID(PID)和端口號都可用于標識進程,但二者有著不同的概念和用途,它們之間存在一定的關聯關系。具體如下:
- 概念及用途不同:
- 進程ID(PID):是系統為每個運行中的進程分配的唯一標識符,屬于系統級的概念。無論進程是否參與網絡通信,系統都會為其分配PID,用于在內核中管理和區分各個進程,是操作系統進行進程調度、資源分配等操作的重要依據。
- 端口號:是傳輸層協議的內容,是一個16位的整數,用于在網絡通信中標識主機中的進程。它是網絡概念,主要用于區分同一臺主機上不同的網絡服務或應用程序,當數據通過網絡傳輸到主機時,操作系統根據端口號將數據交給對應的進程處理。
- 對應關系:
- 一個進程可以綁定多個端口號。例如,一個Web服務器進程可能同時監聽80端口(用于HTTP協議)和443端口(用于HTTPS協議),就像一個人可以有多個身份標識,在不同場景下使用不同標識一樣。
- 一個端口號只能被一個進程占用(特殊情況除外,如支持多進程監聽的情況,但也是在特定機制下),以保證標識進程的唯一性,否則會導致網絡數據傳輸混亂。
- 查詢方法:
- 已知PID查詢端口號,可以使用
netstat -tulnp | grep <PID>
或lsof -i -P -n | grep <PID>
命令。 - 已知端口號查詢PID,可以使用
netstat -nap | grep <端口號>
或lsof -i :<端口號>
命令。
- 已知PID查詢端口號,可以使用
以10086為例,如果有一個進程號為10086的進程,你想知道它占用了哪些端口,可以使用sudo netstat -tulnp | grep 10086
命令來查看。反之,如果已知某個網絡服務使用的端口號是10086,想知道是哪個進程在占用該端口,則可以使用sudo lsof -i :10086
命令,命令執行后會顯示占用該端口的進程PID及相關信息。
因此:一個進程可以綁定多個端口號;但是一個端口號不能被多個進程綁定;
進程ID
屬于系統概念,技術上也具有唯一性,確實可以用來標識唯一的一個進程,但是這樣做,會讓系統進程管理和網絡強耦合,實際設計的時候,并沒有選擇這樣做。
🌉理解源端口號和目的端口號
傳輸層協議(TCP
和UDP
)的數據段中有兩個端口號,分別叫做源端口號和目的端口號.就是在描述"數據是誰發的,要發給誰"
🌉理解socket
綜上,IP地址用來標識互聯網中唯一的一臺主機,port用來標識該主機上唯一的
一個網絡進程
? IP+Port
就能表示互聯網中唯一的一個進程
? 所以,通信的時候,本質是兩個互聯網進程代表人來進行通信,{srcIp
,srcPort
,dstIp
,dstPort
}這樣的 4元組就能標識互聯網中唯二的兩個進程
? 所以,網絡通信的本質,也是進程間通信
? 我們把ip+port
叫做套接字socket
C++
socket
n.
(電源)插座;(電器上的)插口,插孔,管座;槽;窩;托座;臼;孔穴
vt.
把…裝入插座;給…配插座
🌠傳輸層的典型代表
如果我們了解了系統,也了解了網絡協議棧,我們就會清楚,傳輸層是屬于內核的,那么我們要通過網絡協議棧進行通信,必定調用的是傳輸層提供的系統調用,來進行的網絡通信。
🌉認識TCP協議
此處我們先對TCP(Transmission Control Protocol 傳輸控制協議)有一個直觀的認識;后面我們再詳細討論TCP的一些細節問題.
- 傳輸層協議
- 有連接
- 可靠傳輸
- 面向字節流
🌉 認識UDP協議
此處我們也是對UDP(UserDatagramProtocol 用戶數據報協議)有一個直觀的認識;后面再詳細討論.
- 傳輸層協議
- 無連接
- 不可靠傳輸
- 面向數據報
🌠網絡字節序
我們已經知道,內存中的多字節數據相對于內存地址有大端和小端之分,磁盤文件中的多字節數據相對于文件中的偏移地址也有大端小端之分,網絡數據流同樣有大端小端之分. 那么如何定義網絡數據流的地址呢?
- 發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出;接收主機把從網絡上接到的字節依次保存在接收緩沖區中,也是按內存地址從低到高的順序保存;
- 因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,后發出的數據是高地址.
TCP/IP
協議規定,網絡數據流應采用大端字節序,即低地址高字節.- 不管這臺主機是大端機還是小端機,都會按照這個TCP/IP規定的網絡字節序來發送/接收數據;
- 如果當前發送主機是小端,就需要先將數據轉成大端;否則就忽略,直接發送即可
為使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。
這些函數名很好記,h表示host,n表示network,l表示32位長整數,s表示16位短整數。
- 例如htonl表示將32位的長整數從主機字節序轉換為網絡字節序,例如將IP地址轉換后準備發送。
- 如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回;
- 如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。
🌠Socket 編程接口
🌉Socket 常見API
C// 創建 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);// 開始監聽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);
socket()
- 創建網絡端點
int socket(int domain, int type, int protocol);
- 功能:創建一個網絡通信的端點(socket描述符),類似于文件描述符,用于后續的網絡操作。
- 參數:
domain
:協議族(地址族),指定網絡通信的地址類型,常見值:AF_INET
:IPv4協議AF_INET6
:IPv6協議AF_UNIX
:本地進程間通信(Unix域socket)
type
:套接字類型,決定通信方式:SOCK_STREAM
:流式套接字,對應TCP協議(可靠、面向連接)SOCK_DGRAM
:數據報套接字,對應UDP協議(不可靠、無連接)SOCK_RAW
:原始套接字,用于直接訪問底層協議(如ICMP)
protocol
:具體協議,通常設為0表示根據前兩個參數自動選擇默認協議
- 返回值:成功返回非負的socket描述符,失敗返回-1并設置
errno
bind()
- 綁定地址和端口
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
- 功能:將socket與特定的IP地址和端口號綁定(主要用于服務器端)。
- 參數:
socket
:socket()
返回的描述符address
:指向struct sockaddr
(或其衍生結構,如struct sockaddr_in
)的指針,包含要綁定的IP和端口address_len
:address
結構體的長度
- 典型用法(服務器端):
struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; // IPv4 serv_addr.sin_addr.s_addr = INADDR_ANY; // 綁定到所有本地網卡 serv_addr.sin_port = htons(8080); // 端口號(需轉換為網絡字節序) bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
- 返回值:成功返回0,失敗返回-1
listen()
- 監聽連接請求
int listen(int socket, int backlog);
- 功能:將流式套接字(TCP)轉為被動監聽狀態,準備接收客戶端連接(僅服務器端使用)。
- 參數:
socket
:已綁定的socket描述符backlog
:未處理連接的最大隊列長度(超過此數的連接會被拒絕)
- 注意:僅用于TCP協議,UDP無需監聽
- 返回值:成功返回0,失敗返回-1
accept()
- 接收客戶端連接
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
- 功能:從監聽隊列中取出一個客戶端連接請求,創建新的socket用于與該客戶端通信(僅TCP服務器使用)。
- 參數:
socket
:處于監聽狀態的socket描述符(監聽套接字)address
:輸出參數,用于存儲客戶端的地址信息address_len
:輸入輸出參數,傳入緩沖區長度,返回實際地址長度
- 特點:
- 阻塞調用,若無連接會一直等待
- 返回新的socket描述符(連接套接字),與客戶端的通信通過該描述符進行
- 原監聽 socket繼續監聽新的連接
- 返回值:成功返回新的socket描述符,失敗返回-1
connect()
- 建立連接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:客戶端主動向服務器發起連接(僅TCP客戶端使用)。
- 參數:
sockfd
:客戶端的socket描述符addr
:指向服務器地址結構的指針(包含服務器IP和端口)addrlen
:地址結構的長度
- 特點:
- 觸發TCP三次握手過程
- 阻塞調用,直到連接建立或失敗
- 返回值:成功返回0,失敗返回-1
典型工作流程
- TCP服務器:
socket()
→bind()
→listen()
→accept()
→ 讀寫數據 - TCP客戶端:
socket()
→connect()
→ 讀寫數據 - UDP(無連接):通常只需
socket()
和bind()
(服務器),無需listen()
/accept()
/connect()
🌉sockaddr 結構
socket API 是一層抽象的網絡編程接口,適用于各種底層網絡協議,如IPv4、IPv6,以及后面要講的UNIXDomainSocket.然而,各種網絡協議的地址格式并不相同.
IPv4
和IPv6
的地址格式定義在netinet/in.h
中,IPv4
地址用sockaddr_in
結構體表示,包括16
位地址類型,16
位端口號和32
位IP
地址.IPv4、IPv6
地址類型分別定義為常數AF_INET
、AF_INET6
.這樣,只要取得某種sockaddr
結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容.socketAPI
可以都用struct sockaddr
*類型表示, 在使用的時候需要強制轉化成sockaddr_in
; 這樣的好處是程序的通用性,可以接收IPv4, IPv6, 以及UNIXDomainSocket
各種類型的sockaddr
結構體指針做為參數;
sockaddr
結構
/* Structure describing a generic socket address. */
struct __attribute_struct_may_alias__ sockaddr{__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */};
sockaddr_in
結構
/* Structure describing an Internet socket address. */
struct __attribute_struct_may_alias__ sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};
雖然socket api的接口是sockaddr, 但是我們真正在基于IPv4編程時,使用的數據結構是sockaddr_in; 這個結構里主要有三部分信息:地址類型,端口號,IP地址.
🌉in_addr
結構
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr{in_addr_t s_addr;};
in_addr
用來表示一個IPv4
的IP
地址.其實就是一個32位的整數