文章目錄
- 一.前言
- 二.協議
- 協議分層
- 分層的好處
- OSI七層模型
- TCP/IP五層(或四層)模型
- 為什么要有TCP/IP協議
- TCP/IP協議與操作系統的關系(宏觀上是如何實現的)
- 什么是協議
- 三.網絡傳輸基本流程
- 局域網(以太網為例)通信原理
- MAC地址
- 令牌環網
- 封裝與解包分用
- 四.IP地址
- IP VS Mac地址
- 五.Socket編程預備
- 理解源IP地址和目的IP地址
- 端口號
- 端口號范圍劃分
- 認識TCP協議與UDP協議
- 網絡字節序(大小端)
- 網絡主機相互轉換接口
- socket編程接口
- sockaddr結構
- 六.Socket 編程 UDP
- echo server 服務端
- 收數據
- Client 客戶端
- 注意事項
- 整體的代碼與效果
- 效果
- 七.網絡命令
- Ping 命令
- netstat 命令
- pidof 命令
一.前言
一開始時,計算機都相互獨立,只能通過軟盤,才能把數據交給另一臺計算機
后面因為效率原因,就有了服務器,都從服務區上拿數據
美國一開始有貝爾實驗室、麻省理工實驗室…一開始實驗室各搞各的,所以局域網有很多種類的以太網、令牌網…
新技術的產生,一定產生了新設備(芯片技術應用到筆記本,就叫做計算機;用到汽車上,就叫做智能汽車),所以計算機的發展,肯定伴隨著新的設備(比如網絡通信,得有網線)
1994年中國進入互聯網,不僅讓騰訊阿里發展很好,華為和其它通信的公司也發展起來。
當計算機太多時,就有了交換機和路由器
計算機越來越多,就有了廣域網
二.協議
協議就是一種約定。
比如:1+1=2是公認,它也可以說成One plus one equals two。語言可以不同,但只能1+1=2。這就是一種協議。
計算機廠商上有很多,但要讓計算機之間有需要一種協議
能定制標準的都是世界公認的組織或公司,但定標準,不一定要實現標準。比如:華為定的5G標準,不一定要華為自己實現,但其它公司要做5G,得按照華為定的標準實現。
協議分層
協議本身就是軟件,為了更好的模塊化,解耦合,最好也要分層。
分層的好處
以兩個人說話方式為例:
這是分兩層,語言層和通信設備層:
這就是模塊化的解耦合,一層出現問題,另一層并不影響,下一層出現了問題,替換了協議,上一層完全不受影響。(軟件層劃分的越合理,越好,比如封裝、STL容器)
出了問題也好排查。張三聽不清楚李四打電話,無非就是語言不同或者交流方式出現了問題。
OSI七層模型
OSI(Open System Interconnection,開放系統互連)七層網絡模型稱為開放式系統互聯參考模型,是一個邏輯上的定義和規范。
定標準和實現標準是兩批人,實現由頂級的工程師實現,windows和Linux能通信就是因為兩者之間遵守著同一個協議。
分層名稱 | 功能 | 每層功能概述 | |
---|---|---|---|
7 | 應用層 | 針對特定應用的協議 | ![]() |
6 | 表示層 | 設備固有數據格式和網絡標準數據格式的轉換 | ![]() |
5 | 會話層 | 通信管理。負責建立和斷開通信連接(數據流動的邏輯通路)。管理傳輸層以下的分層 | ![]() |
4 | 傳輸層 | 管理兩個節點之間的數據傳輸。負責可靠傳輸(確保數據被可靠地傳送到目標地址) | ![]() |
3 | 網絡層 | 地址管理與路由選擇。 | ![]() |
2 | 數據鏈路層 | 互連設備之間傳送和識別數據幀。 | ![]() |
1 | 物理層 | 以"0"、"1"代表電壓的高地、燈光的閃滅 | ![]() |
實際真正落地的不是OSI七層模型,而是5層協議,會話層、表示層、應用層這三個規劃為一個大的應用層,因為會話層和表示層是不可能接入到操作系統中的。
TCP/IP五層(或四層)模型
TCP是傳輸層的協議,IP是網絡層的協議。TCP/IP是一組協議的代名詞。
5層分別為:
物理層 | 規定什么是’0’、‘1’,特定的01序列是什么含義。通信媒介不一樣,規定傳遞的方式。 |
---|---|
數據鏈路層 | 設備和設備之間的傳遞和識別,有以太網、令牌環網、無線LAN等保證設備之間的通信。 |
網絡層 | 兩個跨網絡的設備需要IP地址傳輸。 |
傳輸層 | 數據傳輸長距離傳輸時丟包或亂序怎么處理? |
應用層 | 設備A從北京送到在西安的設備B,其中傳數據的目的,如何應用? |
其中物理層是光電信號的傳遞,所以只談剩下的4層。
為什么要有TCP/IP協議
首先,即便是單個計算機,都是存在協議的,比如與磁盤通信會有磁盤相關的協議:SATA、IDE、SCSI等。設備之間的協議都是在計算機內部,比如內存和磁盤距離個10厘米,通信的成本低,問題就少。
網絡因為距離很遠,由于物理特性就會出現很多問題。比如男女朋友,在同一個學校,出現問題,解決問題的成本低;異地戀,則會提高成本。
假設主機A發送數據給主機C的過程中,依次會產生的問題:
1.主機A發送給主機C,得把數據交給路由器。(異地戀給女朋友買東西,得交給快遞小哥)
2.網絡上這么多主機,如何準確的找到主機C。
3.如果我的數據在發送的過程中,在某一個節點丟失了怎么辦?(快遞小哥轉發錯地方了)
4.發送數據不是目的,只是手段,讓主機C使用數據是目的。(主機A把‘你好’發給主機C,主機C需要處理‘你好’,是以讀不回還是以讀亂回)
所以有了各種協議,去解決上面的問題。
TCP/IP協議是一種解決方案,本質問題是兩臺通信的主機距離變遠了,所以有了各種問題,需要通過TCP/IP協議這種解決方案去解決。
TCP/IP協議與操作系統的關系(宏觀上是如何實現的)
Windows和Linxu底層是不一樣的,傳輸層(典型協議是TCP)和網絡層(典型協議是IP)在操作系統內部實現的,雖然操作系統實現本身不一樣,但網絡部分必須是一樣的,這就是為什么Windows和Linux能通訊。
不同主機或廠商所對應不同設備之間可以通信的秘密就在于,大家實現的是相同的網絡協議棧。
什么是協議
OS一般都是用C/C++寫的代碼。
假設有兩個主機A、B,兩個主機的內核的代碼不一樣,但傳輸層和網絡層的的代碼一樣。在傳輸層有一個結構體protocol,主機A發送struct protocol data給主機B。
問題:在通信的時候是傳輸二進制數據,主機B能識別data,并精確的識別a=10,b=20,c=30嗎?
答:是可以的;因為雙方都有同一個結構體struct protocol,所以主機B能立馬識別abc的值。
所謂協議:就是通信雙方都認識的結構化的數據類型。
因為協議是分層的,所以每層雙方都有協議,同層之間,互相可以認識對方的協議。
以網上購物發快遞為例子:
買鍵盤,實際就想要一個鍵盤。但實際給我的,除了鍵盤還給了盒子上貼的一張快遞單,我看到這個快遞單子,就能確認這是我的快遞。快遞單+鍵盤兩個共同構成了一個完整的報文。
快遞單上所有的信息都是發快遞的人填的,快遞小哥和我也明白快遞單上面的內容。這個單子就是三者之間的約定,這個約定是快遞公司定的(定標準的人)。發快遞的人、快遞小哥與收快遞的人一起遵守這個約定。
用C語言表示這個快遞單,不就是一個結構體。快遞單就是協議的報頭,鍵盤就是有效載荷,合在一起叫做報文。
發送方構建快遞單叫做構建報頭,把鍵盤裝起來叫做封裝。
綜上所述:協議就是約定。
三.網絡傳輸基本流程
以太網、無線LAN等局域網之間的標準是可以不同的。
局域網(以太網為例)通信原理
問題:兩臺主機在同一個局域網,是否可以直接通信?
答:是可以的;宿舍的無線路由器,你和舍友連著同一個局域網,和隔壁宿舍連著是不同的局域網,小時候玩<<我的世界>>開個熱點把4G網絡關上依舊能一起玩,在同一局域網。
故事:在教室里,老師喊張三,問作業為什么沒做,站起來!教室里的同學都聽到了,張三分析老師剛剛說的信息,提取目的地址叫做張三,就站起來了,然后張三跟老師說:我作業交了。其他人分析剛剛老師說的消息里面的一個目的地址叫做張三,跟自己沒關系,直接把聽到的這句話丟棄掉,不做響應。
雙方在通信的整個過程,老師與張三都互相認為在單獨通信。周圍其他人也能聽到這個消息,但不做響應。
其中教室叫做局域網,老師為主機A,張三為主機B,兩者通信時只要知道對方名字就能通信,其他人能收到但不做響應,這就是局域網通信的基本原理。
總結:局域網能直接通信,每臺主機(局域網)都要有一個唯一的標識:MAC地址
MAC地址
MAC地址是數據鏈路層中相連的節點。(就是標識主機的唯一性)
長度為48位,6個字節,一半用16進制+冒號的形式來表示。
可以用ifconfig指令查看:
ifconfig
Mac地址是集成在網卡中的地址,在出廠時就設置好了,可以說在全球具有唯一性。
問題:操作系統時如何獲取Mac地址?
答:開機時,網卡的驅動程序會根據網卡的協議,會把Mac地址讀到操作系統。
不僅僅網卡有唯一標識,很多設備都有唯一標識,比如磁盤也是有全球唯一標識,叫做序列號。
虛擬機中的Mac地址不是真實的地址,而是虛擬出來的,可能會產生沖突。
以主機A用以太網給主機E發送數據為例:
以太網通信的原理:
1.在以太網中,任何時刻,只允許一臺機器向網絡中發送數據。
2.如果有多臺同時發送,就會產生數據干擾,稱為數據碰撞。(發送的01序列,之間可能干擾)
3.所有發送數據的主機要進行碰撞檢測和碰撞避免。(主機A把數據發出去了,檢測數據有沒有發送成功,主機A自己也是一臺主機,所以自己發送的數據自己能到,跟發的數據和收的數據做比較,查看是否碰撞,一旦發生碰撞了,就要進行碰撞避免。比如主機A與主機B發生了碰撞,兩主機都要休眠一定的隨機時間,再次碰撞的概率減小,同時這個局域網中少了兩臺主機,可以讓其它主機趁著這個間隙發送數據)
根據上面三個原理,很像一種臨界資源(公共的數據),只允許一種主機,訪問該臨界資源里的資源。
4.沒有交換機的情況下,一個以太網就是一個碰撞域。
5.局域網通信的過程中,主機對收到的報文確定是否發給自己的,是通過目標Mac地址判定。
可以通過系統角度來理解局域網通信。
令牌環網
主機A持有數據,就能發消息。主機B持有數據,也能發消息。這不就是一把鎖。
封裝與解包分用
在同一個局域網內,發送消息的過程:
其中每層都有協議,所以當進行上述傳輸流程時,要進行封裝和解包:
明確概念:
以網絡層為例:網絡層報頭為報頭。傳輸層報頭、應用層報頭與“你好”為有效載荷。
從上到下添加報頭,可以想象成一個入棧的結構,往上交付的過程就是出棧的過程,所以我們也稱為網絡協議棧:
數據在網絡中發送的時候,一定最終要在硬件上跑,主機2的網卡先收到數據,然后再向上解包分用。就比如你在三樓宿舍要去3樓教學樓,只能從宿舍的三樓跑到1樓,然后再從教學樓的1樓跑到3樓:
應用層存在上百種協議,傳輸層有tcp/udp…,網絡層有IP/ICMP…鏈路層有mac
若發送數據應用層用http,傳輸層用tcp,網絡層用ip,鏈路層用mac,那問題在于接收層,網絡層中有ip/icmp…等協議,如何知道發送方的網絡層用到什么協議?
在前面說了,報頭(協議)就是一種結構體,那么封裝的時候,他會把有效載體封裝進結構體中:
總結:網絡協議有兩個共性:
1.報頭和有效載荷分離的問題 – 解包。
2.除了應用層,每一層協議都得解決一個問題:自己的有載荷,應該交給上一層的那一種協議 – 分層。
快遞就有分用的功能,快遞小哥到當地了,給每一個收快遞的人打電話,不就是一種分用。電話是什么時候填的?是在發快遞時候填好了(封裝)。
報文 = 報頭 + 有效載荷
在不同場景下,不同的報文有不同的名字(比如在學校叫某同學,工作某同事)
補充:
1.應用層數據通過協議棧發到網絡上時,每層協議都要加上一個數據首部(header),稱為封裝。
2.首部信息中包含了一些類似于首部有多長, 載荷(payload)有多長, 上層協議是什么等信息。
3.數據封裝成幀后發到傳輸介質上,到達目的主機后每層協議再剝掉相應的首部, 根據首部中的 “上層協議字段” 將數據交給對應的上層協議處理。
四.IP地址
上面講述的都是在同一局域網中通信,那么不在同一局域網如何通信?
路由器:路由器要能夠跨網絡轉發,至少連接兩個網絡,就得配上多個網絡與驅動程序。
下圖主機A的路由器與主機B的路由器分別配兩張網卡:1個網卡連接的是以太網;1個連接著令牌環網。
因為用的局域網不同,所以主機A用的以太網的驅動程序,主機B用的令牌環網的驅動程序。
其中,IP地址是標識網絡中唯一性的,IP協議有兩個版本:IPv4和IPv6。
IPv4是一個4字節,32位的整數,通常用“點分十進制”的字符串標識IP地址,例如:192.168.0.1;用點分隔的每一個數字為一個字節,數字區間為0~255。
IPv6是一個16字節,128位的整數。
Linux下的IP地址:
Windows下看網絡地址:ipconfig
緊接著上面用戶A與用戶B通信,本質就是網絡協議棧之間的通信:
路由器和用戶A屬于一個局域網,路由器和用戶B也屬于通一個局域網。他們兩兩配置的網卡是相似的,在同一網段就能通信,以Windows為例:
在網絡層不僅要添加報頭,還要路由。(路由:信息要去172.168.22,肯定不是自己這個局域網(在相同的局域網中,主機都是同樣的開頭,比如192開頭),所以肯定不是交給內網,要交給路由器)
路由:發現不是發給自己局域網主機的報文,就推送給服務器。
網絡層到數據鏈路層的報頭是:macA幀(其中包含scr:macA,代表是主機A發送的。還有dst:macleft),其余主機會收到該報文,但發現dst的地址不是他們自己,就不做處理,只有路由器會收到。路由器收到該報文,確認報文是自己的,向上交付給網絡層:
網絡層再次路由,然后路由器發現你要去dst:172.168.2.2和網卡對應的IP地址(172.168.2.1)差不多,于是再把報文再進行向下交付,重新封裝(報頭:src:macRight,dst:macB)
后有數據電路層收到報文并解包交給上層。
這個過程我們發現:主機A網絡層拿到的報文、路由器網絡層拿到的報文、主機B網絡層拿到的報文,他們原IP、目的IP、數據是一模一樣的:
結論:網絡層(就是IP層)向上(包括網絡層)看到的所有的報文都是一樣的,所以IP可以屏蔽底層網絡的差異!
這就是為什么用的手機用無線網、令牌環、油量…都能相互通信!
IP VS Mac地址
IP地址和mac地址的區別:
故事:
唐僧去西天取經
其中發現上面有兩套地址中,其中車遲國領主、女兒國女王、火焰山領主就相當于一張路由表,方便我們查詢。
唐僧問車遲國國王下一站去哪里時,國王想的過程就是查詢一次路由表的過程。
唐僧提供地址信息,車遲國國王提供的路由表的信息,兩個信息一結合就知道接下來唐僧要去女兒國。
其中IP地址不變,變得都是mac地址。
問題:為什么車遲國國王提供的信息是女兒國,不是其它的國家?
答:你和朋友在北京,要去云南旅游:
其中mac地址受IP地址影響。(階段性目標受最終目標影響),比如最終目標是成為一名博士,前一個階段得先是研究生。
五.Socket編程預備
理解源IP地址和目的IP地址
IP在網絡中是標識主機唯一性的,跨網絡的。
mac地址是在局域網中標識唯一性的,只在局域網內有效,出了局域網就把“衣服”脫掉了。
問題:數據傳輸到主機是目的嗎?主機A把數據交給主機B就完了嗎?
答:不是的。因為數據都是給人用的,比如:聊天是人與人聊天,游覽網頁是人在游覽。人是通過QQ來聊天、通過游覽器游覽網頁。啟動QQ和游覽器都是進程,進程是人在系統中代表,只要數據交給進程,人就相當于拿到了數據:
傳輸數據到主機不是目的,而是手段。到達主機內部,在交給主機內的進程,才是目的。
為了在任意主機內標識進程,引入了一個概念:端口號。
端口號
端口號(port)是傳輸層協議的內容:
1.端口號是一個2字節16位的整數;
2.端口號用來標識一個進程,告訴操作系統,當前的這個數據要交給哪一個進程來處理;
在傳輸層報頭當中,會把端口號(port)寫入其中:
需要網絡通信的這類進程(比如QQ、B站…)啟動的時候,需要自己綁定特定的端口號。
傳輸層收到報文時,通過報文中的端口號進行轉發給指定的進程,我們在未來寫代碼的時候,就一定要和指定的port進行關聯:
通信是兩個人在通信,也可以說是兩個進程在通信。
IP + PORT = 互聯網中唯一的一個進程:
主機收到數據不是目的,而是手段,真正收到數據的是進程。
綜上所述:網絡通信的本質是進程間通信!!!
進程之間通信需要:
1.保證獨立性。
2.看到一份公共資源(網絡)。
其中IP地址+PORTA(端口)叫做socket通信。
問題:PID也是標識進程唯一性的,為什么不能用PID標識,而用port標識?
答:1.從技術上是可以做到的,但是PID是系統級別的概念,而port是網絡的概念,讓傳輸層用PID把數據交道應用層,就會產生強耦合。如果一個PID發生變換,那么整個網絡部分都要調整。所以要做到系統是系統,網絡是網絡,系統與網絡要做到解耦。
2.所有的進程都有PID,但不是所有的進程都想網絡通信,所以在系統中,只有少量通信的進程,需要端口號,未來可以用端口號來分別該進程是否需要網絡通信。
例子:在學校,每個人都有學號,且每個人都有身份證號,為什么學校不用身份證號來分辨而多出個學號呢?
方便管理,這就很好的做到了社會層面和學校層面的解耦,學號變了不影響身份證號。
注意:一個端口號只能被一個進程占用。
端口號范圍劃分
騰訊的QQ號不是所有的都能用,其內部保留了一些。
端口號也不是都能用的,一共有0~65535個端口號,說明一個服務器可以啟動65536個網絡服務,但不會那么多,3、4個服務已經夠多了。
0~1023:知名端口號,HTTP,FTP,SSH等這些廣為使用的應用層協議,他們的端口號都是固定的,深度捆綁。(這就跟110、120電話的性質類似)
1024~65535:操作系統動態分配的端口號(可以自己手動綁定)、客戶端程序的端口號。
認識TCP協議與UDP協議
TCP協議:傳輸層協議、有連接、可靠傳輸、面向字節流。
UDP協議:傳輸層協議、無連接、不可靠傳輸、面向數據報。
傳輸數據時,如果數據丟失了,TCP協議可以提供相對應的策略(比如:丟失后重傳、亂了進行重組排序),UDP協議丟了就是不管。
那就說了,直接用TCP協議不就得了,其實可靠傳輸是其的特征,不是優缺點,因為要保證可靠傳輸,說明要做更多的工作,得知道哪些丟了、哪些沒丟。UDP協議不用做這么多工作,使用上更簡單。
TCP協議常用于轉賬的領域;UDP協議常用于直播領域:
網絡字節序(大小端)
我們學習C語言時就明白,存在大小端的問題,就是數據的存儲方式。
把低權值位放到高地址就是大端機,把低權值位放到低地址處就是小端機:
不同的機器存儲的方式是由差別的。
問題:如果主機A以大端的方式發送數據,主機B以小端的方式接收數據,不就反了?
答:定了個標準,網絡通信必須大端!!即低地址高字節。
網絡主機相互轉換接口
未來為了使我們寫的程序具有可移植性,在寫通信的過程中大部分的大小端會自動幫我們轉了,但端口號和IP地址之類的需要自己轉化,系統提供了函數:
socket編程接口
// 創建 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)
發現很多函數都用了sockaddr結構體,先說該結構體。
sockaddr結構
socket API是一層抽象的網絡編程接口,適用于各種底層網絡協議,如IPv4、IPv6。
socket的發明者不僅僅想讓進程可以完成網絡通信,也可以完成本主機通信:
網絡在設計的時候,就考慮到讓用戶使用同一套的接口,能用來表示不同種類的通信方式,這不就是多態嘛!!
六.Socket 編程 UDP
echo server 服務端
首先要創建套接字。
socket函數:
其中AF_UNIX與套接字SOCK_DGRAM都是宏:
返回值:成功時,返回文件描述符;出錯時,返回-1
創建套接字可以理解成把網卡打開了,在Linux系統中一切皆文件,網卡也是文件,所以可以通過返回的文件描述符對網卡設備進行讀寫(IO)。
未來進行收消息或發消息都需要sockfd(套接字):
- 創建socket文件:
void InitServer() // 初始化_sockfd
{// 1.創建socket文件_sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(FATAL, "create socket error");exit(SOCKET_ERROR);}LOG(DEBUG, "create socket success,sockfd :%d", _sockfd);
}
- 綁定:
客戶端和服務器都要有IP地址與端口號:
bind函數,讓套接字信息(IP、端口號…)與套接字(socket)關聯起來。
struct sockaddr_in 里面的內容:
- sin_family儲存AF_INET(網絡的信息)。
- sin_port儲存16位的端口號。
- sin_addr存儲IP36位的IP地址。
發短信時,主機A不僅要把信息發送給主機B,主機A還把IP地址與端口交給主機B,因為主機B要給主機A應答。說明IP地址與端口都要走網絡,所以IP地址與端口號要轉成網絡序列(大端)。
平常我們喜歡用string類型的變量當IP地址,但是使用時需要4字節的網絡序列,所以需要把string類型轉換為4字節的網絡序列。
就有了接口:inet_addr
// 2.bindstruct sockaddr_in local;memset(&local,0,sizeof(local)); //置空local.sin_family = AF_INET;local.sin_port = htons(_localport); //主機轉網絡序列local.sin_addr.s_addr = inet_addr(_locallip.c_str()); //1.4字節 2.網絡序列int n = ::bind(_sockfd,(struct sockaddr*)&local,sizeof(local)); if(n < 0){LOG(FATAL,"bind error");exit(BIND_ERROR);}LOG(DEBUG,"socket bind success");
服務器運行之后就不會關,就比如某殺毒軟件,你關了也關不上。
所以啟動是死循環,干什么事情在里面填寫:
void Start(){_isrunning = true;while(_isrunning){//otherthing}}
整體代碼如下:
#pragma once#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "NoCopy.hpp"
#include "Log.hpp"using namespace log_ns;enum
{SOCKET_ERROR = 1,BIND_ERROR,
};int gsockfd = -1;
uint16_t glocalport = 888;// UdpServer user("192.1.1.1",8899);
class UdpServer : public nocopy //繼承單例
{
public:UdpServer(std::string &locallip,uint16_t localport = glocalport): _sockfd(gsockfd),_localport(localport),_locallip(locallip),_isrunning(false){}void InitServer(){// 1.創建socket文件_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(FATAL, "create socket error");exit(SOCKET_ERROR);}LOG(DEBUG, "create socket success,sockfd :%d", _sockfd);// 2.bindstruct sockaddr_in local;memset(&local,0,sizeof(local)); //置空local.sin_family = AF_INET;local.sin_port = htons(_localport); // 主機轉網絡序列local.sin_addr.s_addr = inet_addr(_locallip.c_str()); //1.string轉成4字節 2.主機轉網絡網絡序列int n = ::bind(_sockfd,(struct sockaddr*)&local,sizeof(local));if(n < 0){LOG(FATAL,"bind error");exit(BIND_ERROR);}LOG(DEBUG,"socket bind success");}void Start(){_isrunning = true;while(_isrunning){//otherthing}}~UdpServer(){}private:int _sockfd;uint16_t _localport;std::string _locallip;bool _isrunning;
};
可以用指令netstat查看:
收數據
雖然sockfd和文件描述符相似,但不能直接使用write和read…因為sockfd是數據報的,不是字節流的。
要用recvfrom來讀:
返回值:失敗返回-1;返回收到信息的大小。
發送消息:sendto函數
返回值:失敗返回-1;返回發送信息的大小:
代碼:
void Start(){_isrunning = true;char buffer[1024];while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int n = recvfrom(_sockfd,&buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);}}
Client 客戶端
- 首先客戶端一定在未來知道服務器的IP地址和端口號的。
IP地址:比如在網頁登入QQ時,www.qq.com,這是域名,登入時會轉化成IP地址。
端口號:跟服務端是強關聯的(比如:我們平常說打報警電話,很少說打報警電話110)。 - 其次客服端一定要有自己的IP與端口,來區分客戶端的唯一性。
server的端口號,必須由用戶指明,而且是明確的,不能隨意改變。(淘寶、百度…)
client的端口號,一般不讓用戶自己設定,而是讓client OS隨機選擇。
原因:服務端是被多個人訪問的,所以不能隨意改變端口號。客戶端,以手機為例,有微信、京東…不同公司的客戶端,比如淘寶喜歡888的端口號,京東也喜歡888的端口號,在點擊啟動的時候,因為一個進程綁定888后,其余進程就用不了888了,就造成淘寶能打開,京東就打不開了。所以不能固定綁定客戶端的端口號。
綜上所述:client 需要 bind 它自己的IP和端口號,但是client 不需要 自己手動填充 bind它自己的IP和端口。
問題:client如何選擇端口號,什么時候選擇端口號?
答:客戶端在首次向服務器發送數據的時候,OS會自動給client bind 它自己家的IP和端口,在首次調用sendto函數時,綁定IP與端口號。
client總的代碼:
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "NoCopy.hpp"
#include "Log.hpp"// ./udp_client server-ip server-port
// ./udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << "server-ip server-port" << std::endl;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 創建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(1);}struct sockaddr_in server;memset(&server, 0, sizeof(server)); //初始化server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverip.c_str());server.sin_port = htons(serverport);while (1){std::string line;std::cout << "Please Enter# ";getline(std::cin, line);int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server)); // 發數據if (n > 0){struct sockaddr_in temp;socklen_t len = sizeof(temp);char in_buffer[1024];int m = recvfrom(sockfd, in_buffer, sizeof(in_buffer)-1, 0, (struct sockaddr *)&temp, &len); //收數據if (m > 0){in_buffer[m] = 0;std::cout << in_buffer << std::endl;}else{break;}}else{break;}}return 0;
}
注意事項
- 若服務器和客戶端在同一臺機器上,通信的過程不會走到網絡里,這樣保證雙方的軟件內部不會出錯,測試通過再引入跨網絡通信。
- 云服務器綁定IP比較特殊,服務端不能直接(也強烈不建議)bind自己的公網IP:
因為云服務器的共享IP是虛擬出來的,服務器上本身ip就沒有101.200.125.68這個IP。
下面這兩個才是真正的服務器IP。
內網IP是可以綁定的:
但綁定了內網IP,是無法從外網上收消息了。
為了解決上面的兩種情況,一般把云服務的IP地址設為0:
0代表的是:可以讓服務器bind任意IP。
舉個例子:比如服務器配了倆共享IP,IP1和IP2,端口號:8888,那么這個服務器可以收到IP1:8888的報文,也可以收到IP2:8888的報文:
綁定的IP為0的服務器,就能收到端口號相同的任何IP報文了。
整體的代碼與效果
Log.hpp 用來檢查用的日志,直接調用LOG(level,內容);即可
#pragma once#include <iostream>
#include <unistd.h>
#include <time.h>
#include <cstdarg>
#include <pthread.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "LockGuard.hpp"#define SCREEN_TYPE 1
#define FILE_TYPE 2namespace log_ns
{std::string glogfile = "./log.txt";pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;enum{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string LevelToString(int level) // 日志等級{switch (level){case DEBUG:return "DEBUG";case INFO:return "INFO";case WARNING:return "WARNING";case ERROR:return "Error";case FATAL:return "FATAL";default:return "UNKONE";}}std::string GetCurrTime() // 目前的時間{time_t now = time(nullptr);struct tm *curr_time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",curr_time->tm_year + 1900,curr_time->tm_mon + 1,curr_time->tm_mday,curr_time->tm_hour,curr_time->tm_min,curr_time->tm_sec);return buffer;}class Logmessage // 日志的內容{public:std::string _level;pid_t _pid;std::string _filename;int _filenumber;std::string _time;std::string _messageinfo;};class Log{public:Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}void Enable(int type){_type = type;}void FlushLogToScreen(const Logmessage &lg){printf("[%s][%d][%s][%d][%s][%s]\n",lg._level.c_str(),lg._pid,lg._filename.c_str(),lg._filenumber,lg._time.c_str(),lg._messageinfo.c_str());}void FlushLogToFile(const Logmessage &lg){char logtxt[2048];snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s][%s]\n",lg._level.c_str(),lg._pid,lg._filename.c_str(),lg._filenumber,lg._time.c_str(),lg._messageinfo.c_str());int fd = open(_logfile.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0)return perror("open file false");write(fd, logtxt, strlen(logtxt));close(fd);}void FlushLog(const Logmessage &lg) // 判斷刷新到到屏幕還是文件{if (lg._level == "DEBUG")return;LockGuard lockguard(&glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;default:printf("_type error \n");break;}}void LogMessage(std::string filename, int filenumber, int level, const char *format, ...) // 刷新的內容{Logmessage lg;lg._level = LevelToString(level);lg._pid = getpid();lg._filename = filename;lg._filenumber = filenumber;lg._time = GetCurrTime();va_list ap;va_start(ap, format);char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap);lg._messageinfo = log_info;FlushLog(lg);}~Log(){}private:int _type; // 打印到屏幕還是文件中std::string _logfile; // 哪個文件出了問題};Log lg;#define LOG(level, format, ...) \do \{ \lg.LogMessage(__FILE__, __LINE__, level, format, ##__VA_ARGS__); \} while (0)
#define EnableScrean() \do \{ \lg.Enable(SCREEN_TYPE); \} while (0)
#define EnableFILE() \do \{ \lg.Enable(FILE_TYPE); \} while (0)
}
NoCopy.hpp 單例模式
#pragma onceclass nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy&) = delete;const nocopy& operator =(const nocopy& ) = delete;
};
UdpServer.hpp:
#pragma once#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "NoCopy.hpp"
#include "Log.hpp"using namespace log_ns;enum
{SOCKET_ERROR = 1,BIND_ERROR,
};int gsockfd = -1;
uint16_t glocalport = 8888;// UdpServer user(8899);
class UdpServer : public nocopy
{
public:UdpServer(uint16_t localport = glocalport): _sockfd(gsockfd),_localport(localport),_isrunning(false){}void InitServer(){// 1.創建socket文件_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(FATAL, "create socket error");exit(SOCKET_ERROR);}// LOG(DEBUG, "create socket success,sockfd :%d", _sockfd);// 2.bindstruct sockaddr_in local;memset(&local, 0, sizeof(local)); // 置空local.sin_family = AF_INET;local.sin_port = htons(_localport); // 1.網絡序列local.sin_addr.s_addr = INADDR_ANY;int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind error");exit(BIND_ERROR);}LOG(INFO, "socket bind success");}void Start(){_isrunning = true;char buffer[1024];while (_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0) // 返回收到的信息{std::string ip = inet_ntoa(peer.sin_addr); // 4字節-》stringuint16_t port = htons(peer.sin_port); // 網絡轉主機buffer[n] = 0;std::cout << "[" << ip << ":" << port << "]#" << buffer << std::endl;// LOG(DEBUG, "recvfrom return is :%d", n);std::string echo_server = "[udp_server echo] #";echo_server += buffer;LOG(DEBUG, "echo_server :%s", echo_server.c_str());sendto(_sockfd, echo_server.c_str(), sizeof(echo_server), 0, (struct sockaddr *)&peer, len);}}}~UdpServer(){if (_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;uint16_t _localport;bool _isrunning;
};
UdpServerMain.cc
#include "UdpServer.hpp"
#include <memory>// ./udp_server local-port
// ./udp_server 8888
int main(int argc, char *argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << "server-port" << std::endl;}EnableScrean();uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->InitServer();usvr->Start();return 0;
}
UdpClientMain.cc
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "NoCopy.hpp"
#include "Log.hpp"using namespace log_ns;// ./udp_client server-ip server-port
// ./udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << "server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 創建socketint sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(1);}struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverip.c_str());server.sin_port = htons(serverport);while (1){std::string line;std::cout << "Please Enter# ";getline(std::cin, line);LOG(DEBUG,"%s",line.c_str());int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server)); // 發數據// LOG(DEBUG,"sendto return is %d",n);if (n > 0){struct sockaddr_in temp;socklen_t len = sizeof(temp);char in_buffer[1024];int m = recvfrom(sockfd, in_buffer, sizeof(in_buffer)-1, 0, (struct sockaddr *)&temp, &len);if (m > 0){in_buffer[m] = 0;std::cout << in_buffer << std::endl;}else{break;}}else{break;}}return 0;
}
效果
七.網絡命令
Ping 命令
檢查網絡連通性。
問題:在Windows電腦上有一個云服務器,如何保證電腦與云服務器連通呢?(比如windows沒有連網,沒法連)
答:Ping命令就算檢查兩臺主機是否能聯通。
測試是否能連通網絡,連通百度:
未來寫了一種網絡服務,比如寫了一個TCP的服務,請求時怎樣都拿不到結果,可以先拿Ping命令測試網絡是否連通,若連通,說明網絡服務本身就有問題。
- Ping命令一旦啟動,就不會停止的,若只想Ping 1次,就有了選項-c(cont的意思)
netstat 命令
netstat時一個用來查看網絡狀態的重要工具。
netstat通常用來查看網絡服務,UDP/TCP啟動起來就是一個進程,我們能用ps能查到該進程偏于進程方面的信息,若想查看網絡方面的屬性的字段,就用netstat:
-u | 查看udp服務 |
---|---|
-t | 查看TCP服務 |
-a | 查看所有的服務 |
-p | 查看與哪個進程關聯 |
-n | 把能顯示成數字的,顯示成數字 |
-l | 把處于LISTEN狀態的TCP服務顯示出來 |
watch命令可以每隔x秒讓該命令執行一次:
watch -n 1 netstat -nuap #每隔一秒執行一次
pidof 命令
拿到進程的PID。
服務器有時候處于后端,簡單的ctrl+c是殺不死的,所以需要kill命令。
經常會這么用:
pidof [進程名] | xargs kill -9
xargs的作用:把管道中的數據,轉換成后續的數據。
kill是用標準輸入文件描述符0,來把數據讀到kill中。