文章目錄
-
- 🍓1 伺服器準備連接配接🍓
-
- 🍇1.1 socket():預準備手機🍇
- 🍇1.2 bind():綁定電話卡🍇
- 🍇1.3 listen():真正的可用手機🍇
- 🍇1.4 accept():電話鈴聲響起🍇
- 🍓2 用戶端發請求連接配接🍓
-
- 🍇2.1 connect():撥打伺服器的電話🍇
- 🍇2.2 淺讀一下TCP三向交握🍇
前面學習了IPv4、IPv6、本地套接字的相關知識,現在學習一下怎麼去使用前面所學的知識——使用對應套接字格式完成網絡連接配接的建立
先有服務而後有客戶
🍓1 伺服器準備連接配接🍓
🍇1.1 socket():預準備手機🍇
要建立一個套接字需要用到下面的函數
- domian:PF_INET、PF_INET6、PF_LOCAL(AF_XXX)等
- type:
- SOCK_STREAM:表示的是位元組流,對應TCP
- SOCK_DGRAM:表示的是資料報,對應UDP
- SOCK_RAW:表示原始套接字(少用到)
- protocol:原用于指定通信協定,因前兩個參數已基本固定一個協定,是以這裡基本上設定為0即可
🍇1.2 bind():綁定電話卡🍇
由前面
socket()
函數準備好的套接字如果要被使用,那麼需要給它提供一個信号源,這就需要用到
bind()
函數把套接字和套接字位址綁定,也就是将手機卡裝入手機的SIM卡槽
- fd:表示的是我們前面所建立出來的初始socket句柄
- addr:通用位址格式,用以适配IPv4、IPv6、本地套接字格式等
- len:通用位址的長度,通過這個參數确定傳入的位址格式是哪個類型的
如果是雙卡雙待手機,那該怎麼綁定手機卡呢?
這個時候,可以利用通配位址的能力幫助我們解決這個問題
對IPv4位址來說,通配位址的設定通過INADDR_ANY來完成;對于IPv6位址來說,則是通過INADDR6_ANY來完成的
struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4通配位址 */
除了設定位址外,還有一個參數就是端口号。如果把端口号設定為0,那麼系統核心會按照一定的算法來選擇一個空閑的端口,以完成socket的綁定(這在伺服器端中并不常用,因為一般來說,伺服器端需要綁定一個已知的位址和端口号,否則客戶就不知道怎麼聯系上伺服器)
下面是初始化IPv4 TCP socket的例子
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int make_socket (uint16_t port)
{
int sock;
struct sockaddr_in name;
/* 建立位元組流類型的IPV4 socket. */
sock = socket (PF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror ("socket");
exit (EXIT_FAILURE);
}
/* 綁定到port和ip. */
name.sin_family = AF_INET; /* IPV4 */
name.sin_port = htons (port); /* 指定端口 */
name.sin_addr.s_addr = htonl (INADDR_ANY); /* 通配位址 */
/* 把IPV4位址轉換成通用位址格式,同時傳遞長度 */
if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
{
perror ("bind");
exit (EXIT_FAILURE);
}
return sock
}
🍇1.3 listen():真正的可用手機🍇
經過綁定電話卡後,一個正常可用的手機就誕生了,此時的手機都是自己準備好,以便可以使用的,這一系列過程都是主動性的,但是作為伺服器端,需要“高冷”一點,不能太過主動,應該等待别人的主動。
listen()
将原本的主動性socket轉化為被動性的socket,以告訴系統核心:“我現在隻會等待别人的主動通知,我是用來被請求連接配接的!”
- socketfd:套接字描述符,即上面綁定好手機卡後的socket
- backlog:在Linux中表示已完成(ESTABLISHED)且未accept的隊列大小,這個參數的大小決定了可以接收的并發數目。參數越大理論上并發數目越大,但是數目過大會占用系統資源
到這裡,伺服器端已經準備的差不多了,即将進入最後的等待模式
什麼是并發?
百度了一下的知識,好像了解了但是又有點疑惑。
-
:并發當有多個線程在操作時,如果系統隻有一個CPU,則它根本不可能真正同時進行一個以上的線程,它隻能把CPU運作時間劃分成若幹個時間段,再将時間 段配置設定給各個線程執行,在一個時間段的線程代碼運作時,其它線程處于挂起狀。.這種方式我們稱之為并發(Concurrent)并發
-
:當系統有一個以上CPU時,則線程的操作有可能非并發。當一個CPU執行一個線程時,另一個CPU可以執行另一個線程,兩個線程互不搶占CPU資源,可以同時進行,這種方式我們稱之為并行(Parallel)。并行
上面的兩個概念摘自并發,這裡并行很好了解,但是并發裡說的
多個線程運作在一個時間段裡同時運作
怎麼了解呢?這個先記下來後面看明白了再回來補充
🍇1.4 accept():電話鈴聲響起🍇
當用戶端的連接配接請求到達時,伺服器端應答成功,連接配接建立,這個時候作業系統核心需要把這個事件通知到應用程式,并讓應用程式感覺到這個連接配接。這個過程,就好比電信營運商完成了一次電話連接配接的建立, 應答方的電話鈴聲響起,通知有人撥打了号碼,這個時候就需要拿起電話筒開始應答。
- listensockfd:從名字上可以看出就是經過
轉化後的被動socketlisten()
- cliaddr:通過指針擷取用戶端的位址
-
addrlen:告訴我們位址的大小
這個函數的過程可以了解成當我們拿起電話機時,看到了來電顯示,知道了對方的号碼
需要注意的是,傳入的是監聽式套接字描述符,傳回的是已連接配接的套接字描述符,兩者不是同一個描述符
為什麼不把輸入的套接字描述符和傳回的描述符并為同一個?
這裡就用到了上面所說到的
并發
知識
和打電話的情形非常不一樣的地方就在于,打電話一旦有一個連接配接建立,别人是不能再打進來的,隻會得到語音播報:“您撥的電話正在通話中。”而網絡程式的一個重要特征就是并發處理,不可能一個應用程式運作之後隻能服務一個客戶,如果是這樣, 雙 11 搶購得需要多少伺服器才能滿足全國 “剁手黨 ” 的需求?是以監聽套接字一直都存在,它是要為成千上萬的客戶來服務的,直到這個監聽套接字關閉;而一旦一個客戶和伺服器連接配接成功,完成了 TCP 三次握手,作業系統核心就為這個客戶生成一個已連接配接套接字,讓應用伺服器使用這個已連接配接套接字和客戶進行通信處理。如果應用伺服器完成了對這個客戶的服務,比如一次網購下單,一次付款成功,那麼關閉的就是已連接配接套接字,這樣就完成了 TCP 連接配接的釋放。請注意,這個時候釋放的隻是這一個客戶連接配接,其它被服務的客戶連接配接可能還存在。最重要的是,監聽套接字一直都處于“監聽”狀态,等待新的客戶請求到達并服務
🍓2 用戶端發請求連接配接🍓
第一步還是和服務端一樣,要建立一個套接字,方法和前面是一樣的。不一樣的是用戶端需要調用 connect 向服務端發起請求。
🍇2.1 connect():撥打伺服器的電話🍇
- sockfd:
建立的套接字描述符socket()
- servaddr:指向套接字位址結構的指針
- addrlen:套接字位址結構的長度大小
connect()之前不需要bind()嗎?
用戶端在調用
connect()
之前不一定需要調用
bind()
函數,用戶端作為主動方,發起的請求連接配接目标可以是多個,是以并不需要使用
bind()
進行目标綁定。如果需要使用
bind()
的話,核心會确定源IP位址,按照一定算法配置設定一個臨時端口。
回想上一章節中的知識,(TCP socket)當程式執行到用戶端的
connect()
時,就已經觸發了TCP中的三次握手機制了,而且僅在連接配接建立成功或者出錯後才傳回。連接配接建立成功傳回很好了解,那什麼是連接配接出錯呢?
- 三次握手無法建立,用戶端發出的 SYN 包沒有任何響應,于是傳回 TIMEOUT 錯誤。這種情況比較常見的原因是對應的服務端 IP 寫錯。
- 用戶端收到了 RST(複位)回答,這時候用戶端會立即傳回 CONNECTION REFUSED 錯誤。這種情況比較常見于用戶端發送連接配接請求時的請求端口寫錯,因為 RST 是 TCP 在發生錯誤時發送的一種 TCP 分節。産生 RST 的三個條件是:目的地為某端口的 SYN 到達,然而該端口上沒有正在監聽的伺服器(如前所述);TCP 想取消一個已有連接配接;TCP 接收到一個根本不存在的連接配接上的分節。
- 客戶發出的 SYN 包在網絡上引起了"destination unreachable",即目的不可達的錯誤。這種情況比較常見的原因是用戶端和伺服器端路由不通。
🍇2.2 淺讀一下TCP三向交握🍇
網上搜尋到的簡單講解:
- 用戶端的協定棧向伺服器端發送了 SYN 包,并告訴伺服器端目前發送序列号 j,用戶端進入 SYNC_SENT 狀态;
- 伺服器端的協定棧收到這個包之後,和用戶端進行 ACK 應答,應答的值為 j+1,表示對 SYN 包 j 的确認,同時伺服器也發送一個 SYN 包,告訴用戶端目前我的發送序列号為 k,伺服器端進入 SYNC_RCVD 狀态;
- 用戶端協定棧收到 ACK 之後,使得應用程式從 connect 調用傳回,表示用戶端到伺服器端的單向連接配接建立成功,用戶端的狀态為 ESTABLISHED,同時用戶端協定棧也會對伺服器端的 SYN 包進行應答,應答資料為 k+1;
- 應答包到達伺服器端後,伺服器端協定棧使得 accept 阻塞調用傳回,這個時候伺服器端到用戶端的單向連接配接也建立成功,伺服器端也進入 ESTABLISHED 狀态。
再以打電話為例子來淺讀一下這幾個知識點:(W:卧底,P:指揮官)
- W給P打電話:“摩西摩西,我上線了,我方暗号是j,收到請回答,over”
- P接通W的電話,聽到了W的暗号内容,确認了W的身份
- 确認W身份後,回複到:“我收到了你的暗号j,我方已準備好,随時可以行動,代号為k,請接受指令”
- W收到P的回複後,表示:“收到指令k,over。Just do it”