Socket 一說起來肯定都并不陌生,大概一說出來所有人的第一反應(yīng)就是糠悯,這是個做端到端長連接通信的帮坚。一點問題沒有,很對互艾。但是如果要是有人問起试和,基于UDP的Socket和基于TCP的Socket有什么區(qū)別?一個Socket的連接都經(jīng)歷了什么纫普?監(jiān)聽Socket和真正Socket傳輸是一個Socket嗎阅悍?Socket編程都要設(shè)置啥參數(shù)?等等的一系列問題昨稼。如果這些問題你能夠做到胸有成竹节视,大概這邊文章對你也沒啥用處。如果想要了解下的可以接著往下看假栓。
建立Socket需要設(shè)置很多參數(shù)寻行?
并不是的,其實學(xué)習(xí)它并不需要記住很多亂七八糟意義的參數(shù)但指,因為Socket 編程進行的是端到端的通信寡痰,往往意識不到中間經(jīng)過多少局域網(wǎng),多少路由棋凳,拦坠,因而能夠設(shè)置的參數(shù),也只能是端到端協(xié)議之上網(wǎng)絡(luò)層和傳輸層的剩岳。
在網(wǎng)絡(luò)層贞滨,Socket 函數(shù)需要指定到底是 IPv4 還是 IPv6,分別對應(yīng)設(shè)置為 AF_INET 和 AF_INET6拍棕。另外晓铆,還要指定到底是 TCP 還是 UDP。TCP 協(xié)議是基于數(shù)據(jù)流的绰播,所以設(shè)置為 SOCK_STREAM骄噪,而 UDP 是基于數(shù)據(jù)報的,因而設(shè)置為 SOCK_DGRAM蠢箩。
監(jiān)聽Socket和真正Socket傳輸是一個Socket嗎链蕊?
上面講到了,Socket會分為是基于TCP的還是基于UDP的谬泌。
對于TCP而言滔韵,首先說明,監(jiān)聽和傳輸并不是一個Socket掌实。為什么呢陪蜻?那就要先說明下基于TCP的Socket的連接過程。
TCP 的服務(wù)端要先監(jiān)聽一個端口贱鼻,一般是先調(diào)用 bind 函數(shù)宴卖,給這個 Socket 賦予一個 IP 地址和端口。當(dāng)服務(wù)端有了 IP 和端口號忱嘹,就可以調(diào)用 listen 函數(shù)進行監(jiān)聽嘱腥。在 TCP 的狀態(tài)里面,有一個 listen 狀態(tài)拘悦,當(dāng)調(diào)用這個函數(shù)之后齿兔,服務(wù)端就進入了這個狀態(tài),這個時候客戶端就可以發(fā)起連接了础米。在內(nèi)核中分苇,為每個 Socket 維護兩個隊列。一個是已經(jīng)建立了連接的隊列屁桑,這時候連接三次握手已經(jīng)完畢医寿,處于 established 狀態(tài);一個是還沒有完全建立連接的隊列蘑斧,這個時候三次握手還沒完成靖秩,處于 syn_rcvd 的狀態(tài)须眷。接下來,服務(wù)端調(diào)用 accept 函數(shù)沟突,拿出一個已經(jīng)完成的連接進行處理花颗。如果還沒有完成,就要等著惠拭。在服務(wù)端等待的時候扩劝,客戶端可以通過 connect 函數(shù)發(fā)起連接。先在參數(shù)中指明要連接的 IP 地址和端口號职辅,然后開始發(fā)起三次握手棒呛。內(nèi)核會給客戶端分配一個臨時的端口。一旦握手成功域携,服務(wù)端的 accept 就會返回另一個 Socket簇秒。
對于 UDP 來講,過程有些不一樣秀鞭。UDP 是沒有連接的宰睡,所以不需要三次握手,也就不需要調(diào)用 listen 和 connect气筋,但是拆内,UDP 的的交互仍然需要 IP 和端口號,因而也需要 bind宠默。UDP 是沒有維護連接狀態(tài)的麸恍,因而不需要每對連接建立一組 Socket,而是只要有一個 Socket搀矫,就能夠和多個客戶端通信抹沪。也正是因為沒有連接狀態(tài),每次通信的時候瓤球,都調(diào)用 sendto 和 recvfrom融欧,都可以傳入 IP 地址和端口。
基于TCP和基于UDP的Socket連接過程是怎樣的卦羡?
上面基本介紹了Socket的連接過程噪馏,不過需要注意的是以下幾點:
1.TCP的服務(wù)端要先監(jiān)聽一個端口,一般是先調(diào)用 bind 函數(shù)绿饵,給這個 Socket 賦予一個 IP 地址和端口欠肾。為什么需要端口呢?要知道拟赊,你寫的是一個應(yīng)用程序刺桃,當(dāng)一個網(wǎng)絡(luò)包來的時候,內(nèi)核要通過 TCP 頭里面的這個端口吸祟,來找到你這個應(yīng)用程序瑟慈,把包給你桃移。
2.基于TCP的Scoket為什么要 IP 地址呢?有時候葛碧,一臺機器會有多個網(wǎng)卡谴轮,也就會有多個 IP 地址,你可以選擇監(jiān)聽所有的網(wǎng)卡吹埠,也可以選擇監(jiān)聽一個網(wǎng)卡,這樣疮装,只有發(fā)給這個網(wǎng)卡的包缘琅,才會給你。
3.說 TCP 的 Socket 在 Linux 中就是以文件的形式存在的廓推。除此之外刷袍,還存在文件描述符。寫入和讀出樊展,也是通過文件描述符呻纹。
有限的服務(wù)器資源如何接更多的連接?
首先沒事下為什么要考慮這個問題专缠?答案很簡單雷酪,就是如何在有限的資源內(nèi)支持更多的連接。
一般來說涝婉,服務(wù)器通常固定在某個本地端口上監(jiān)聽哥力,等待客戶端的連接請求。因此墩弯,服務(wù)端端 TCP 連接四元組中只有對端 IP, 也就是客戶端的 IP 和對端的端口吩跋,也即客戶端的端口是可變的,因此渔工,最大 TCP 連接數(shù) = 客戶端 IP 數(shù)×客戶端端口數(shù)锌钮。對 IPv4,客戶端的 IP 數(shù)最多為 2 的 32 次方引矩,客戶端的端口數(shù)最多為 2 的 16 次方梁丘,也就是服務(wù)端單機最大 TCP 連接數(shù),約為 2 的 48 次方旺韭。
當(dāng)然兰吟,服務(wù)端最大并發(fā) TCP 連接數(shù)遠不能達到理論上限。首先主要是文件描述符限制茂翔,按照上面的原理混蔼,Socket 都是文件,所以首先要通過 ulimit 配置文件描述符的數(shù)目珊燎;另一個限制是內(nèi)存惭嚣,按上面的數(shù)據(jù)結(jié)構(gòu)遵湖,每個 TCP 連接都要占用一定內(nèi)存,操作系統(tǒng)是有限的晚吞。
這里我提出一個現(xiàn)在我個人覺得最好的一種方式:基于消息的IO多路復(fù)用延旧。
所謂的多路復(fù)用通俗的類比一個例子就是,一個項目組照看多個項目槽地,每個項目組都應(yīng)該有個項目進度墻迁沫,將自己組看的項目列在那里,一旦某個項目有了進展捌蚊,就派人去盯一下集畅。這個“項目進度墻”,是指一個文件描述符集合 fd_set 缅糟,由于 Socket 是文件描述符挺智,所以某個線程盯的所有的 Socket,都會被存放在里面窗宦。
如果有了改變赦颇,就會觸發(fā)事件進行通知,從而進行處理赴涵。能完成這件事情的函數(shù)叫 epoll媒怯,它在內(nèi)核中的實現(xiàn)不是通過輪詢的方式,而是通過注冊 callback 函數(shù)的方式髓窜,當(dāng)某個文件描述符發(fā)送變化的時候沪摄,就會主動通知。
如圖所示纱烘,假設(shè)進程打開了 Socket m, n, x 等多個文件描述符杨拐,現(xiàn)在需要通過 epoll 來監(jiān)聽是否這些 Socket 都有事件發(fā)生。其中 epoll_create 創(chuàng)建一個 epoll 對象擂啥,也是一個文件哄陶,也對應(yīng)一個文件描述符,同樣也對應(yīng)著打開文件列表中的一項哺壶。在這項里面有一個紅黑樹屋吨,在紅黑樹里,要保存這個 epoll 要監(jiān)聽的所有 Socket山宾。
當(dāng) epoll_ctl 添加一個 Socket 的時候至扰,其實是加入這個紅黑樹,同時紅黑樹里面的節(jié)點指向一個結(jié)構(gòu)资锰,將這個結(jié)構(gòu)掛在被監(jiān)聽的 Socket 的事件列表中敢课。當(dāng)一個 Socket 來了一個事件的時候,可以從這個列表中得到 epoll 對象,并調(diào)用 call back 通知它直秆。
這種通知方式使得監(jiān)聽的 Socket 數(shù)據(jù)增加的時候濒募,效率不會大幅度降低,能夠同時監(jiān)聽的 Socket 的數(shù)目也非常的多了圾结。上限就為系統(tǒng)定義的瑰剃、進程打開的最大文件描述符個數(shù)。因而筝野,epoll 被稱為解決 C10K 問題的利器晌姚。