本節重點
-
認識IP地址, 端口號, 網絡字節序等網絡編程中的基本概念;
-
學習socket api的基本用法;
一、預備知識
1.理解源IP地址和目的IP地址
?在IP數據包頭部中,有兩個IP地址,分別叫做源IP地址和目的IP地址。
思考: 我們光有IP地址就可以完成通信了嘛?想象一下發qq消息的例子,有了IP地址能夠把消息發送到對方的機器上,但是還需要有一個其他的標識來區分出,這個數據要給哪個程序進行解析,此時就需要我們的目的IP地址。
2.認識端口號
在進行網絡通信的時候,是不是我們的兩臺機器在進行通信呢?本質是應用層在通信
幾乎任何層的協議,都要在報頭中提供,決定將自己的有效載荷交付給上層的哪一個協議的能力,怎么做到的呢?端口號(port)是傳輸層協議的內容.
- 端口號是一個2字節16位的整數;
- 端口號用來標識一個進程,告訴操作系統,當前的這個數據要交給哪一個進程來處理;
- IP地址 + 端口號能夠標識網絡上的某一臺主機的某一個進程;
- 一個端口號只能被一個進程占用.
3.理解 "端口號" 和 "進程ID"
我們之前在學習系統編程的時候,學習了 pid 表示唯一一個進程;此處我們的端口號也是唯一表示一個進程,那么這兩者之間是怎樣的關系?
?另外,一個進程可以綁定多個端口號;但是一個端口號不能被多個進程綁定。
4.理解源端口號和目的端口號
傳輸層協議(TCP和UDP)的數據段中有兩個端口號, 分別叫做源端口號和目的端口號. 就是在描述 "數據是誰發的, 要發給誰"。
socket通信本質上就是兩個進程之間在進行通信,只不過這里是跨網絡的進程間通信。比如看QQ和刷抖音的動作,實際就是手機上的QQ進程和抖音進程在和對端服務器主機上的QQ服務進程和抖音服務進程之間在進行通信。因此進程間通信的方式除了管道、消息隊列、信號量、共享內存等方式外,還有套接字,只不過前者是不跨網絡的,而后者是跨網絡的。
?理解socket這個名字
socket在英文上有“插座”的意思,插座上有不同規格的插孔,我們將插頭插入到對應的插孔當中就能夠實現電流的傳輸。在進行網絡通信時,客戶端就相當于插頭,服務端就相當于一個插座,但服務端上可能會有多個不同的服務進程(多個插孔),因此當我們在訪問服務時需要指明服務進程的端口號(對應規格的插孔),才能享受對應服務進程的服務。
5.認識TCP協議和UDP協議
此處我們先對TCP(Transmission Control Protocol 傳輸控制協議)有一個直觀的認識; 后面我們再詳細討論TCP的一 些細節問題.
- 傳輸層協議
- 有連接
- 可靠傳輸
- 面向字節流
此處我們也是對UDP(User Datagram Protocol 用戶數據報協議)有一個直觀的認識; 后面再詳細討論.
- 傳輸層協議
- 無連接
- 不可靠傳輸
- 面向數據報
TCP協議是一種可靠的傳輸協議,使用TCP協議能夠在一定程度上保證數據傳輸時的可靠性,而UDP協議是一種不可靠的傳輸協議,UDP協議的存在有什么意義?
首先,可靠是需要我們做更多的工作的,TCP協議雖然是一種可靠的傳輸協議,但這一定意味著TCP協議在底層需要做更多的工作,因此TCP協議底層的實現是比較復雜的,我們不能只看到TCP協議面向連接可靠這一個特點,我們也要能看到TCP協議對應的缺點。同樣的,UDP協議雖然是一種不可靠的傳輸協議,但這一定意味著UDP協議在底層不需要做過多的工作,因此UDP協議底層的實現一定比TCP協議要簡單,UDP協議雖然不可靠,但是它能夠快速的將數據發送給對方,雖然在數據在傳輸的過程中可能會出錯。
6.網絡字節序
我們已經知道,內存中的多字節數據相對于內存地址有大端和小端之分, 磁盤文件中的多字節數據相對于文件中的偏 移地址也有大端小端之分, 網絡數據流同樣有大端小端之分. 那么如何定義網絡數據流的地址呢?
- 發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出;
- 接收主機把從網絡上接到的字節依次保存在接收緩沖區中,也是按內存地址從低到高的順序保存;
- 因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,后發出的數據是高地址.
- TCP/IP協議規定,網絡數據流應采用大端字節序,即低地址高字節.
- 不管這臺主機是大端機還是小端機, 都會按照這個TCP/IP規定的網絡字節序數將數據據;來發送/接收
- 如果當前發送主機是小端, 就需要先轉成大端; 否則就忽略, 直接發送即可;
為使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調用以下庫函數做網絡 字節序和主機字節序的轉換。
- 這些函數名很好記,h表示host,n表示network,l表示32位長整數,s表示16位短整數。
- 例如htonl表示將32位的長整數從主機字節序轉換為網絡字節序,例如將IP地址轉換后準備發送。
- 如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回;
- 如果主機是大端字節序,這些函數不做轉換,將參數原封不動地
二、socket編程接口
1.socket 常見API
// 創建 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);
2.sockaddr結構
socket API是一層抽象的網絡編程接口,適用于各種底層網絡協議,如IPv4、IPv6,以及后面要講的UNIX Domain Socket. 然而, 各種網絡協議的地址格式并不相同.
為什么沒有用void*代替struct sockaddr*類型?
我們可以將這些函數的struct sockaddr*參數類型改為void*,此時在函數內部也可以直接指定提取頭部的16個比特位進行識別,最終也能夠判斷是需要進行網絡通信還是本地通信,那為什么還要設計出sockaddr這樣的結構呢?實際在設計這一套網絡接口的時候C語言還不支持void*,于是就設計出了sockaddr這樣的解決方案。并且在C語言支持了void*之后也沒有將它改回來,因為這些接口是系統接口,系統接口是所有上層軟件接口的基石,系統接口是不能輕易更改的,否則引發的后果是不可想的,這也就是為什么現在依舊保留sockaddr結構的原因。
- Pv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結構體表示,包括16位地址類型, 16位端口號和32位IP地址.
- IPv4、IPv6地址類型分別定義為常數AF_INET、AF_INET6. 這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容.
- socket API可以都用struct sockaddr *類型表示, 在使用的時候需要強制轉化成sockaddr_in; 這樣的好處是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各種類型的sockaddr結構體指針做為參數;
?sockaddr 結構
?sockaddr_in 結構
雖然socket api的接口是sockaddr, 但是我們真正在基于IPv4編程時, 使用的數據結構是sockaddr_in; 這個結構里主要有三部分信息: 地址類型,端口號,IP地址,可是我們怎么沒看到套接字的域呢?
這里使用了我們的"##",我們來回憶一下它的用法
所以上面定義的宏傳入的參數是sin_,然后宏里面使用了"##"拼接family,這樣就看到了我們的套接字的域。
?in_addr結構
可以看到,struct sockaddr_in
當中的成員如下:
- sin_family:表示協議家族。
- sin_port:表示端口號,是一個16位的整數。
- sin_addr:表示IP地址,是一個32位的整數。
in_addr用來表示一個IPv4的IP地址. 其實就是一個32位的整數。