欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Linux UDP網(wǎng)絡(luò)編程套接字sockets介紹

 更新時(shí)間:2025年08月11日 10:27:25   作者:每天敲200行代碼  
文章主要講解了IP地址、端口號(hào)、Socket及TCP/UDP協(xié)議的基礎(chǔ)知識(shí),重點(diǎn)包括地址結(jié)構(gòu)、字節(jié)序轉(zhuǎn)換、網(wǎng)絡(luò)通信模型與實(shí)現(xiàn),強(qiáng)調(diào)Socket作為應(yīng)用層與網(wǎng)絡(luò)層交互的接口,以及TCP可靠傳輸與UDP高效但不可靠傳輸?shù)膮^(qū)別

一、預(yù)備知識(shí)

1、IP地址

因特網(wǎng)是在網(wǎng)絡(luò)級(jí)進(jìn)行互聯(lián)的,因此,因特網(wǎng)在網(wǎng)絡(luò)層(IP 層)完成地址的統(tǒng)一工作,把不同物理網(wǎng)絡(luò)的地址統(tǒng)一到具有全球惟一性的 IP地址上,IP 層所用到的地址叫作因特網(wǎng)地址,又叫 IP 地址。IP 地址的意義就是標(biāo)識(shí)公網(wǎng)內(nèi)唯一一臺(tái)主機(jī)。

在  IP  數(shù)據(jù)包頭部中 有兩個(gè)  IP  地址, 分別叫做源  IP  地址 和目的 IP 地址。

如果我們的臺(tái)式機(jī)或者筆記本沒有 IP 地址就無法上網(wǎng),而因?yàn)槊颗_(tái)主機(jī)都有 IP 地址,所以注定了數(shù)據(jù)從一臺(tái)主機(jī)傳輸?shù)搅硪慌_(tái)主機(jī)就一定有源 IP 地址和目的 IP 地址,所以在報(bào)頭中就會(huì)包含源IP 地址和目的 IP 地址。

2、端口號(hào)

網(wǎng)絡(luò)通信的本質(zhì)是進(jìn)程間通信,有了 IP 就可以標(biāo)識(shí)公網(wǎng)內(nèi)唯一的一臺(tái)主機(jī),想要完成網(wǎng)絡(luò)通信我們還需要一個(gè)東西來標(biāo)識(shí)一臺(tái)主機(jī)上的某個(gè)進(jìn)程,這個(gè)標(biāo)識(shí)就是端口號(hào)(port)。

端口號(hào)是傳輸層協(xié)議的內(nèi)容,它包括如下幾個(gè)特點(diǎn):

  • 端口號(hào)是一個(gè) 2 字節(jié),16 比特位的整數(shù)。
  • 一臺(tái)主機(jī)中,一個(gè)端口號(hào)只能被一個(gè)進(jìn)程所占用。

IP 地址(標(biāo)識(shí)唯一主機(jī))+ 端口號(hào)(標(biāo)識(shí)唯一進(jìn)程)能夠標(biāo)識(shí)網(wǎng)絡(luò)上的某一臺(tái)主機(jī)的某一個(gè)進(jìn)程(全網(wǎng)唯一的進(jìn)程)

端口號(hào)的解釋:

  1. HTTP 通信使用的端口號(hào)是 80。在瀏覽器中輸入網(wǎng)址并訪問一個(gè)網(wǎng)站時(shí),瀏覽器會(huì)與服務(wù)器進(jìn)行 HTTP 通信。在這個(gè)過程中,瀏覽器將通過端口號(hào) 80 發(fā)送請(qǐng)求,以與服務(wù)器上運(yùn)行的 Web 服務(wù)器進(jìn)行通信。Web 服務(wù)器接收到請(qǐng)求后,會(huì)將相應(yīng)的網(wǎng)頁內(nèi)容返回給瀏覽器,并通過端口號(hào) 80 將響應(yīng)發(fā)送回瀏覽器。因此,端口號(hào) 80 在這種情況下用于標(biāo)識(shí) HTTP 通信。
  2. FTP 通信使用的端口號(hào)是 21。 使用 FTP 客戶端與遠(yuǎn)程服務(wù)器進(jìn)行文件傳輸時(shí),通常使用的端口號(hào)是 21。FTP 客戶端通過端口號(hào) 21 與 FTP 服務(wù)器建立連接并發(fā)送指令來上傳、下載或刪除文件。端口號(hào) 21 被 FTP 協(xié)議保留,用于標(biāo)識(shí) FTP 通信。

每個(gè)端口號(hào)都有特定的作用和用途,例如常見的端口號(hào)有:

  • 20 和 21:FTP
  • 22:SSH
  • 25:SMTP(用于發(fā)送電子郵件)
  • 53:DNS(域名系統(tǒng))
  • 80:HTTP
  • 443:HTTPS 

既然 pid 已經(jīng)做到唯一標(biāo)識(shí)一個(gè)進(jìn)程,為何還要引入端口號(hào)呢?

我們可以從生活的角度去理解這種情況:即然每個(gè)人都有了唯一標(biāo)識(shí)自己的身份照號(hào),為何學(xué)校還要給我們分配學(xué)號(hào)呢?直接用身份照號(hào)不行嗎?

在學(xué)校我們用學(xué)號(hào),相比于身份證更簡便,假如我的學(xué)號(hào)是2211211023,這樣就能看到我是22級(jí)的,方便閱讀信息??墒浅隽藢W(xué)校,別人并不能通過學(xué)號(hào)辨別你。也許你在不同的學(xué)習(xí)有不同的學(xué)號(hào)。pid和端口號(hào)也是一樣的。

  1. 首先 pid 是系統(tǒng)規(guī)定的,而 port 是網(wǎng)絡(luò)規(guī)定的,這樣就可以把系統(tǒng)和網(wǎng)絡(luò)解耦。
  2. port 標(biāo)識(shí)服務(wù)器的唯一性不能做任何改變,要讓客戶端能找到服務(wù)器,就像 110,120 一樣不能被改變,而 pid 每次啟動(dòng)進(jìn)程,pid 就會(huì)改變。
  3. 不是所有的進(jìn)程都需要提供網(wǎng)絡(luò)服務(wù)或請(qǐng)求(不需要 port),但每個(gè)進(jìn)程都需要 pid。

雖然一個(gè)端口號(hào)只能綁定一個(gè)進(jìn)程,但是一個(gè)進(jìn)程可以綁定多個(gè)端口號(hào)。前面說了有源 IP 和目的 IP,而這里的 port 也有源端口號(hào)和目的端口號(hào)。我們?cè)诎l(fā)送數(shù)據(jù)的時(shí)候也要把自己的 IP 和端口號(hào)發(fā)送過去,因?yàn)閿?shù)據(jù)還要被發(fā)送回來,所以發(fā)送數(shù)據(jù)時(shí)一定會(huì)多出一部分?jǐn)?shù)據(jù)(以協(xié)議的形式呈現(xiàn))。

3、Socket網(wǎng)絡(luò)通信

socket 通信的本質(zhì)就是跨網(wǎng)絡(luò)的進(jìn)程間通信,任何的網(wǎng)絡(luò)客戶端和網(wǎng)絡(luò)服務(wù)如果要進(jìn)行正常的數(shù)據(jù)通信,它們必須要有自己的端口號(hào)和匹配所屬主機(jī)的 IP 地址。

4、認(rèn)識(shí)TCP/UDP協(xié)議

我們進(jìn)行網(wǎng)絡(luò)編程時(shí)通常是在應(yīng)用層編碼,應(yīng)用層下面就是傳輸層。應(yīng)用層往下傳輸數(shù)據(jù)時(shí)不必?fù)?dān)心也沒有必要知道數(shù)據(jù)的傳輸情況如何,這個(gè)具體地交給傳輸層來解決,所以我們有必要簡單了解一下傳輸層的兩個(gè)重要協(xié)議 TCP 和 UDP。

(1)TCP協(xié)議

TCP (Transmission Control Protocol 傳輸控制協(xié)議)的特點(diǎn):

  • 傳輸層協(xié)議
  • 連接(在正式通信前要先建立連接)
  • 可靠傳輸(在內(nèi)部幫我們做可靠傳輸工作)
  • 面向字節(jié)流

(2)UDP協(xié)議

UDP 全稱 User Datagram Protocol,即用戶數(shù)據(jù)報(bào)協(xié)議,它有如下特點(diǎn):

  • 屬于傳輸層協(xié)議
  • 無連接
  • 不可靠傳輸
  • 面向數(shù)據(jù)報(bào)

在我們的認(rèn)知里一定是安全、穩(wěn)定的才好,那傳輸層為什么還要引入一個(gè)不可靠傳輸方式的 UDP 協(xié)議呢?TCP 協(xié)議雖然是可靠傳輸,但是“可靠”是要付出一些效率上的代價(jià)的,可能會(huì)導(dǎo)致傳輸速度比較慢,而且實(shí)現(xiàn)起來相對(duì)復(fù)雜;以這個(gè)角度去看 UDP 協(xié)議,雖然可能在傳輸過程中出現(xiàn)丟包的情況,但效率上是要比 TCP 更快的。通常兩個(gè)協(xié)議我們可以搭配起來使用,網(wǎng)速快時(shí)用 TCP 協(xié)議,網(wǎng)速慢時(shí)用 UDP 協(xié)議,但如果是要傳輸重要數(shù)據(jù)的話就應(yīng)該用 TCP 了。

(3)網(wǎng)絡(luò)字節(jié)序

我們知道,內(nèi)存中的數(shù)據(jù)權(quán)值排列相對(duì)于內(nèi)存地址的大小有大端和小端之分:

  • 小端:低權(quán)值的數(shù)放入低地址。
  • 大端:低權(quán)值的數(shù)放入高地址。

數(shù)據(jù)在發(fā)送時(shí),發(fā)送主機(jī)通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序以字節(jié)為單位發(fā)出接收主機(jī)把接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序以字節(jié)為單位保存的。即先發(fā)出低地址的數(shù)據(jù),后發(fā)出高地址的數(shù)據(jù);接收到的數(shù)據(jù)也是按低地址到高地址的順序接收。

如果發(fā)送端和接收端主機(jī)的存儲(chǔ)字節(jié)序不同,則會(huì)造成發(fā)送的數(shù)據(jù)和識(shí)別出來的數(shù)據(jù)不一致的問題,如下圖所示:

因此,網(wǎng)絡(luò)數(shù)據(jù)流的地址應(yīng)這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址。TCP/IP 協(xié)議規(guī)定:網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,即低地址高字節(jié)。不管這臺(tái)主機(jī)是大端機(jī)還是小端機(jī),都會(huì)按照這個(gè) TCP/IP 規(guī)定的網(wǎng)絡(luò)字節(jié)序來發(fā)送/接收數(shù)據(jù)。如果當(dāng)前發(fā)送主機(jī)是小端,就需要先將數(shù)據(jù)轉(zhuǎn)成大端;否則就忽略,直接發(fā)送即可。 

為使網(wǎng)絡(luò)程序具有可移植性,使同樣的 C 代碼在大端和小端計(jì)算機(jī)上編譯后都能正常運(yùn)行,可以調(diào)用以下庫函數(shù)做網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的轉(zhuǎn)換。

  • h 表示 host,n 表示 network,l 表示 32 位長整數(shù),s 表示 16 位短整數(shù)。
  • 例如 htonl 表示將 32 位的長整數(shù)從主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序,例如將 IP 地址轉(zhuǎn)換后準(zhǔn)備發(fā)送。
  • 如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回。
  • 如果主機(jī)是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回。

二、socket網(wǎng)絡(luò)套接字

1、概念

socket 通常也稱為“套接字”,程序可以通過“套接字”向網(wǎng)絡(luò)發(fā)出請(qǐng)求或者響應(yīng)網(wǎng)絡(luò)請(qǐng)求。socket 位于傳輸層之上、應(yīng)用層之下。socket 編程是通過一系列系統(tǒng)調(diào)用完成應(yīng)用層協(xié)議,如 FTP、Telent、HTTP 等應(yīng)用層協(xié)議都是通過 socket 編程來實(shí)現(xiàn)的。

從套接字所處的位置來講,套接字上連應(yīng)用進(jìn)程,下接網(wǎng)絡(luò)協(xié)議棧,是應(yīng)用程序與網(wǎng)絡(luò)協(xié)議棧進(jìn)行交互的接口。

套接字可以看作是通信的兩個(gè)端點(diǎn),一個(gè)是服務(wù)器端的套接字,另一個(gè)是客戶端的套接字。通過套接字,服務(wù)器端和客戶端可以相互發(fā)送和接收數(shù)據(jù)。

在網(wǎng)絡(luò)通信中,套接字使用網(wǎng)絡(luò)協(xié)議(如 TCP/IP、UDP 等)來完成數(shù)據(jù)的傳輸和通信。根據(jù)所使用的網(wǎng)絡(luò)協(xié)議的不同,套接字可以分為兩種類型:

  1. 流套接字(Stream Socket,也稱為面向連接的套接字):基于 TCP 協(xié)議,提供可靠的、面向連接的通信。使用流套接字時(shí),數(shù)據(jù)可以按照發(fā)送的順序和完整性進(jìn)行傳輸,確保數(shù)據(jù)的準(zhǔn)確性。流套接字的通信方式類似于電話通信,需要在通信前先建立連接。
  2. 數(shù)據(jù)報(bào)套接字(Datagram Socket,也稱為無連接的套接字):基于 UDP 協(xié)議,提供不可靠的、無連接的通信。使用數(shù)據(jù)報(bào)套接字時(shí),數(shù)據(jù)以數(shù)據(jù)包的形式進(jìn)行傳輸,不保證數(shù)據(jù)的順序和完整性。數(shù)據(jù)報(bào)套接字適用于一次性發(fā)送不需要可靠傳輸?shù)臄?shù)據(jù)。

Linux 和 UNIX 的 I/O 內(nèi)涵是系統(tǒng)中的一切都是文件。當(dāng)程序在執(zhí)行任何形式的 I/O 時(shí),程序都是在讀或者在寫一個(gè)文件描述符,從而實(shí)現(xiàn)操作文件,但是,這個(gè)文件可能是一個(gè) socket 網(wǎng)絡(luò)連接、目錄、FIFO、管道、終端、外設(shè)、磁盤上的文件。一樣的道理,socket 也是使用標(biāo)準(zhǔn) Linux 文件描述符和其他網(wǎng)絡(luò)進(jìn)程進(jìn)行通信的。

socket 函數(shù)基本為系統(tǒng)調(diào)用函數(shù),它是操作系統(tǒng)向網(wǎng)絡(luò)通信進(jìn)程提供的函數(shù)接口。

在TCP/IP協(xié)議中, 用 “源IP”, “源端口號(hào)”, “目的IP”, “目的端口號(hào)”, “協(xié)議號(hào)” 這樣一個(gè)五元組來標(biāo)識(shí)一個(gè)網(wǎng)絡(luò)通信,我們可以用 netstat -n 命令查看當(dāng)前主機(jī)下已經(jīng)建立鏈接的網(wǎng)絡(luò)通信

IP地址、端口號(hào)、socket 套接字三者在數(shù)據(jù)結(jié)構(gòu)上的聯(lián)系

2、Socket 的地址結(jié)構(gòu)和一系列轉(zhuǎn)換函數(shù)

(1)socket常見的API

socket

創(chuàng)建 socket 文件描述符(TCP/UDP, 客戶端 + 服務(wù)器)

bind 

綁定端口號(hào)( TCP/UDP, 服務(wù)器)

listen 

開始監(jiān)聽  socket(TCP, 服務(wù)器)

accept 

接收請(qǐng)求( TCP, 服務(wù)器)

connect

建立連接( TCP, 客戶端)

(2)三種 Socket 地址結(jié)構(gòu)體

socket API 是一層抽象的網(wǎng)絡(luò)編程接口,適用于各種底層網(wǎng)絡(luò)協(xié)議,如:IPv4、IPv6,以及后面要講的 UNIX Domain Socket。然而,各種網(wǎng)絡(luò)協(xié)議的地址格式并不相同。

套接字有不少類型,常見的有三種:

  1. 原始 socket
  2. 域間 socket
  3. 網(wǎng)絡(luò) socket

三種應(yīng)用場(chǎng)景:網(wǎng)絡(luò)套接字主要運(yùn)用于跨主機(jī)之間的通信,也能支持本地通信,而域間套接字只能在本地通信,而原始套接字可以跨過傳輸層(TCP/IP 協(xié)議)訪問底層的數(shù)據(jù)。

為了方便,設(shè)計(jì)者只使用了一套接口,這樣就可以通過不同的參數(shù)來解決所有的通信場(chǎng)景。這里舉兩個(gè)具體的套接字類型:sockaddr_in 和 sockaddr_un:

可以看到 sockaddr_in 和 sockaddr_un 是兩個(gè)不同的通信場(chǎng)景,區(qū)分它們就用 16 地址類型協(xié)議家族的標(biāo)識(shí)符。但是,這兩個(gè)結(jié)構(gòu)體都不用,我們用 sockaddr。

比方說我們想用網(wǎng)絡(luò)通信,雖然參數(shù)是 const struct sockaddr *addr,但實(shí)際傳遞進(jìn)去的卻是 sockaddr_in 結(jié)構(gòu)體(注意要強(qiáng)制類型轉(zhuǎn)換)。在函數(shù)內(nèi)部一視同仁,全部看成 sockaddr 類型,然后根據(jù)前兩個(gè)字節(jié)判斷到底是什么通信類型然后再強(qiáng)轉(zhuǎn)回去??梢园?sockaddr 看成基類,把 sockaddr_in 和 sockaddr_un 看成派生類,構(gòu)成了多態(tài)體系。

  • IPv4 和 IPv6 的地址格式定義在 netinet/in.h 中,IPv4 地址用 sockaddr_in 結(jié)構(gòu)體表示,包括 16 位地址類型,16 位端口號(hào)和 32 位 IP 地址。
  • IPv4、IPv6 地址類型分別定義為常數(shù) AF_INET、AF_INET6。這樣,只要取得某種 sockaddr 結(jié)構(gòu)體的首地址,不需要知道具體是哪種類型的 sockaddr 結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容。
  • socket API 可以都用 struct sockaddr * 類型表示, 在使用的時(shí)候需要強(qiáng)制轉(zhuǎn)化成 sockaddr_in,這樣的好處是程序的通用性,可以接收 IPv4,IPv6,以及 UNIX Domain Socket 各種類型的 sockaddr 結(jié)構(gòu)體指針做為參數(shù)

(1)sockaddr 結(jié)構(gòu)

(2)sockaddr_in 結(jié)構(gòu)

雖然  socket api  的接口是  sockaddr, 但是我們真正在基于  IPv4  編程時(shí), 使用的數(shù)據(jù)結(jié)構(gòu)是  sockaddr_in, 這個(gè)結(jié)構(gòu)里主要有三部分信息: 地址類型, 端口號(hào), IP  地址。

(3)in_addr 結(jié)構(gòu)

in_addr 用來表示一個(gè) IPv4 的 IP 地址,其實(shí)就是一個(gè) 32 位的整數(shù)。 

(3)IP地址轉(zhuǎn)換函數(shù)

IP 地址轉(zhuǎn)換函數(shù)是指完成點(diǎn)分十進(jìn)制數(shù) IP 地址(是一個(gè)字符串)與二進(jìn)制數(shù)IP地址之間的相互轉(zhuǎn)換。IP 地址轉(zhuǎn)換主要由 inet_aton、inet_addr 和 inet_ntoa 這三個(gè)函數(shù)完成,但它們都只能處理 IPv4 地址,而不能處理 IPv6 地址。這三個(gè)函數(shù)的函數(shù)原型及其具體說明如下。

?

1、inet_addr

?

2、inet_aton

?

3、inet_ntoa

?

三、UDP套接字編程

UDP 協(xié)議是非連接非可靠的數(shù)據(jù)傳輸,常用在對(duì)數(shù)據(jù)質(zhì)量要求不高的場(chǎng)合。UDP 服務(wù)器通常是非連接的,因而,UDP 服務(wù)器進(jìn)程不需要像 TCP 服務(wù)器那樣在監(jiān)聽套接字上接收新建的連接;UDP 只需要在綁定的端口上等待客戶機(jī)發(fā)送過來的 UDP 數(shù)據(jù)報(bào)文,并對(duì)其進(jìn)行處理和響應(yīng)。

一個(gè) TCP 服務(wù)進(jìn)程只有在完成了對(duì)某客戶機(jī)的服務(wù)后,才能為其它的客戶機(jī)提供服務(wù)。而 UDP 服務(wù)器只是接收數(shù)據(jù)報(bào)文,處理并返回結(jié)果。UDP 支持廣播和多播,如果要使用廣播和多播,必須使用 UDP 套接字。UDP 套接字沒有連接的建立和終止過程,UDP 只需要兩個(gè)分組來交換一個(gè)請(qǐng)求和答應(yīng)。UDP 不適合海量數(shù)據(jù)的傳輸。

1. UDP 的 C/S 網(wǎng)絡(luò)通信模型

(1)UDP 服務(wù)器通信流程

  1. 建立 UDP 套接字
  2. 綁定套接字到特定的地址
  3. 等待并接受客戶端信息
  4. 處理客戶端請(qǐng)求
  5. 發(fā)送信息給客戶端
  6. 關(guān)閉套接字

(2)UDP 客戶端通信流程

  1. 建立 UDP 套接字
  2. 發(fā)送信息給服務(wù)器
  3. 接收來自服務(wù)器的信息
  4. 關(guān)閉套接字

(3)UDP 服務(wù)器、客戶端通信流程圖

2、UDP 的 C/S 網(wǎng)絡(luò)通信實(shí)現(xiàn)

UDP服務(wù)端基本框架

udp_server.cpp

  • 創(chuàng)建一個(gè)服務(wù)端對(duì)象
  • 初始化服務(wù)端對(duì)象
  • 啟動(dòng)服務(wù)端對(duì)象

(1)UDP服務(wù)端的初始化

創(chuàng)建套接字

在通信之前要先把網(wǎng)卡文件打開,函數(shù)作用:打開一個(gè)文件,把文件和網(wǎng)卡關(guān)聯(lián)起來

  • domain:是一個(gè)域,標(biāo)識(shí)了這個(gè)套接字的通信類型(網(wǎng)絡(luò)或者本地)。

第一個(gè) AF_UNIX 表示本地通信,而 AF_INET 表示網(wǎng)絡(luò)通信 

  • type:套接字提供服務(wù)的類型。

這里我們講的是 UDP,所以使用 SOCK_DGRAM。

  • protocol:想使用的協(xié)議,默認(rèn)為 0 即可。因?yàn)榍懊娴膬蓚€(gè)參數(shù)就已經(jīng)決定了是 TCP 還是 UDP 協(xié)議了。

返回值,成功返回一個(gè)新的套接字描述費(fèi)用,失敗返回-1錯(cuò)誤碼被設(shè)置。

接下來我們創(chuàng)建套接字,創(chuàng)建完套接字我們要bind綁定

綁定 bind

所以我們要先定義一個(gè) sockaddr_in 結(jié)構(gòu)體填充數(shù)據(jù),再傳遞進(jìn)去。

點(diǎn)分十進(jìn)制字符串風(fēng)格的 IP 地址(例:"192.168.110.132" )每一個(gè)區(qū)域取值范圍是 [0-255]:1字節(jié) -> 4個(gè)區(qū)域。理論上,表示一個(gè)IP地址,其實(shí)4字節(jié)就夠了。點(diǎn)分十進(jìn)制字符串風(fēng)格的 IP 地址為4字節(jié)。

返回值,成功0被返回,失敗-1被返回,錯(cuò)誤碼被設(shè)置

這幾個(gè)參數(shù)是什么呢?

struct sockaddr_in {
    short int sin_family;           // 地址族,一般為AF_INET或PF_INET
    unsigned short int sin_port;    // 端口號(hào),網(wǎng)絡(luò)字節(jié)序
    struct in_addr sin_addr;        // IP地址
    unsigned char sin_zero[8];      // 用于填充,使sizeof(sockaddr_in)等于16
};

 創(chuàng)建結(jié)構(gòu)體后要先清空數(shù)據(jù)(初始化),我們可以用 memset,系統(tǒng)也提供了接口:

填充端口號(hào)的時(shí)候要注意端口號(hào)是兩個(gè)字節(jié)的數(shù)據(jù),涉及到大小端問題

接口我們?cè)诰W(wǎng)絡(luò)字節(jié)序具體介紹了

對(duì)于 IP,首先我們要先轉(zhuǎn)成整數(shù),再解決大小端問題。系統(tǒng)給了直接能解決這兩個(gè)問題的接口

為什么這里的IP為什么是local.sin_addr.s_addr 

這里鑲嵌了一個(gè)結(jié)構(gòu)體

進(jìn)行綁定bind

(2)啟動(dòng)服務(wù)端

作為一款網(wǎng)絡(luò)服務(wù)器,是永遠(yuǎn)不退出的。

服務(wù)器啟動(dòng)-> 進(jìn)程 -> 常駐進(jìn)程 -> 永遠(yuǎn)在內(nèi)存中存在,除非掛了

首先要知道服務(wù)器要死循環(huán),永遠(yuǎn)不退出,除非用戶刪除。站在操作系統(tǒng)的角度,服務(wù)器是常駐內(nèi)存中的進(jìn)程,而我們啟動(dòng)服務(wù)器的時(shí)候要傳遞進(jìn)去 IP 和端口號(hào)。

我們要要進(jìn)行網(wǎng)絡(luò)通信,在網(wǎng)絡(luò)基礎(chǔ)1的時(shí)候給大家講了,報(bào)頭包含了對(duì)方的IP和端口號(hào)還包含了自己的IP和端口號(hào)。

讀取數(shù)據(jù) recvfrom

  • sockfd:從哪個(gè)套接字讀。
  • buf:數(shù)據(jù)放入的緩沖區(qū)。
  • len:緩沖區(qū)長度。
  • flags:讀取方式。 0 代表阻塞式讀取。
  • src_addr 和 addrlen:輸出型參數(shù),返回對(duì)應(yīng)的消息內(nèi)容是從哪一個(gè)客戶端發(fā)出的。第一個(gè)是自己定義的結(jié)構(gòu)體,第二個(gè)是結(jié)構(gòu)體長度。

返回值

現(xiàn)在我們想要知道是誰發(fā)送過來的消息,信息都被保存到了 client結(jié)構(gòu)體中,我們知道 IP 信息在 client.sin_addr.s_addr 中。首先這是一個(gè)網(wǎng)絡(luò)序列,要轉(zhuǎn)成主機(jī)序列,其次為了方便觀察,要把它轉(zhuǎn)換成點(diǎn)分十進(jìn)制

操作系統(tǒng)給了一個(gè)接口能夠解決這兩個(gè)問題:

inet_ntoa 這個(gè)函數(shù)返回了一個(gè) char*,很顯然是這個(gè)函數(shù)自己在內(nèi)部為我們申請(qǐng)了一塊內(nèi)存來保存 ip 的結(jié)果。那么是否需要調(diào)用者手動(dòng)釋放呢?

man 手冊(cè)上說,inet_ntoa 函數(shù)是把這個(gè)返回結(jié)果放到了靜態(tài)存儲(chǔ)區(qū)。這個(gè)時(shí)候不需要我們手動(dòng)進(jìn)行釋放。

同樣獲取端口號(hào)的時(shí)候也要由網(wǎng)絡(luò)序列轉(zhuǎn)成主機(jī)序列: 

4字節(jié)的網(wǎng)絡(luò)序列要轉(zhuǎn)化回本主機(jī)的字符串風(fēng)格的IP

我們收到了消息,把數(shù)據(jù)發(fā)回 

發(fā)回消息sendto

  • sockfd:套接字文件描述符
  • buf:發(fā)送數(shù)據(jù)空間首地址
  • len:發(fā)送數(shù)據(jù)長度
  • flags:默認(rèn)為0
  • dest_addr:目的IP地址存放空間首地址
  • addrlen:目的地址的長度

完整代碼

UdpServe.hpp

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <cerrno>
#include "log.hpp"


Log lg;
const std::string defaultip = "0.0.0.0";

enum
{
    SockfdErr = 1,
    BindErr,

};
class UdpServer
{
public:
    UdpServer(const uint16_t& port = 8080,const std::string& ip = defaultip)
    :_port(port)
    ,_ip(ip)
    ,sockfd(-1)
    {}
    void init()
    {
        //1.創(chuàng)建套接字
        sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(sockfd < 0)
        {
            lg(Fatal,"sockfd create error %d", sockfd);
            exit(SockfdErr);
        }
        lg(Info,"sockfd create success %d", sockfd);
        //2.bind:將用戶設(shè)置的的ip和port在內(nèi)核中和我們當(dāng)前的進(jìn)程相關(guān)聯(lián)
        struct sockaddr_in local;
        bzero(&local,0);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port); //主機(jī)轉(zhuǎn)網(wǎng)絡(luò) h表示host,n表示network
        local.sin_addr.s_addr = inet_addr(_ip.c_str());  //1. string -> uint32_t 2. uint32_t必須是網(wǎng)絡(luò)序列的 //
        if(bind(sockfd,(const struct sockaddr*)&local,sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BindErr);
        }
        lg(Info,"bind success!");
    }
    void start()
    {
        char buffer[1024];
        while(true)
        {
            //純輸出型參數(shù)
            struct sockaddr_in client;
            //清空數(shù)據(jù)
            bzero(&client,0);
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom( sockfd , buffer,sizeof(buffer)-1, 0 , (struct sockaddr*)&client , &len);
            if(n < 0)
            {
                lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            } 
            buffer[n] = 0;
            uint16_t port = ntohs(client.sin_port); //client是從網(wǎng)絡(luò)來,我們要轉(zhuǎn)為主機(jī)字序
            std::string ip = inet_ntoa(client.sin_addr); //4字節(jié)的網(wǎng)絡(luò)序列要轉(zhuǎn)化回本主機(jī)的字符串風(fēng)格的IP
            sendto(sockfd, buffer, strlen(buffer), 0, (const sockaddr*)&client, len);
        }
    }
    ~UdpServer()
    {
        if(sockfd>0) close(sockfd);
    }

private:
    int sockfd; //網(wǎng)絡(luò)套接字描述符
    std::string _ip;    //IP地址
    uint16_t _port;        //端口號(hào)
};

main.cc 

#include "UdpServer.hpp"
#include <memory>

void Usage(std::string s)
{
    std::cout << "\n\rUsage: " << s << " port[1024+]\n" << std::endl;
}
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    UdpServer* ptr = new UdpServer(port);  //創(chuàng)建一個(gè)服務(wù)器對(duì)象
    ptr->init();  //初始化服務(wù)器對(duì)象
    ptr->start();  //啟動(dòng)服務(wù)器

    return 0;
}

log.hpp是我們自己寫的日志文件系統(tǒng),在之前的博客也有講解。

運(yùn)行結(jié)果如下:

這里阻塞了,因?yàn)槲覀冞€沒寫客戶端

這里有個(gè)補(bǔ)充,我們多次使用inet_ntoa這個(gè)函數(shù)時(shí),我們先來看一段代碼

因?yàn)?inet_ntoa 把結(jié)果放到自己內(nèi)部的一個(gè)靜態(tài)存儲(chǔ)區(qū),這樣第二次調(diào)用時(shí)的結(jié)果會(huì)覆蓋掉上一次的結(jié)果。

如果有多個(gè)線程調(diào)用 inet_ntoa,是否會(huì)出現(xiàn)異常情況呢?

  • 在 APUE 中,明確提出 inet_ntoa 不是線程安全的函數(shù)。
  • 但是在 centos7 上測(cè)試并沒有出現(xiàn)問題,可能內(nèi)部的實(shí)現(xiàn)加了互斥鎖。
  • 在多線程環(huán)境下,推薦使用 inet_ntop,這個(gè)函數(shù)由調(diào)用者提供一個(gè)緩沖區(qū)保存結(jié)果,可以規(guī)避線程安全問題。

(3)客戶端的實(shí)現(xiàn)

客戶端要能與服務(wù)器進(jìn)行連接,就要知道服務(wù)器的IP和端口號(hào),但這個(gè)實(shí)際是要我們自己填寫的。

client 要不要 bind?

  • 要,但是一般 client 不會(huì)顯示的 bind,程序員不會(huì)自己 bind。
  • client 是一個(gè)客戶端 -> 普通人下載安裝啟動(dòng)使用的 -> 如果程序員自己 bind 了-> client 一定 bind 了一個(gè)固定的 ip 和 port,那萬一其他的客戶端提前占用了這個(gè) port 呢?
  • client 一般不需要顯示的 bind 指定 port,而是讓 OS 自動(dòng)隨機(jī)選擇。

這里我們發(fā)送數(shù)據(jù)用sendto函數(shù)

這里的參數(shù)和前面講的 recvfrom 差不多,而這里的結(jié)構(gòu)體內(nèi)部需要自己填充目的 IP 和目的端口號(hào)。

#include <iostream>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

void Usage(const std::string& s)
{
    std::cout << "\n\rUsage: " << s << " serverip serverport\n" << std::endl;
}
int main(int argc , char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0)
    {
        std::cerr << "socket error!" <<std::endl;
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in Server;
    bzero(&Server,0);
    Server.sin_family = AF_INET;
    Server.sin_port = htons(serverport);
    Server.sin_addr.s_addr = inet_addr(serverip.c_str());
    char buffer[1024];
    std::string message;
    socklen_t len = sizeof(Server);
    while(true)
    {
        std::cout << "Client say# ";
        std::getline(std::cin,message);
        //當(dāng)Client首次給服務(wù)器發(fā)送消息的時(shí)候,OS會(huì)自動(dòng)bind Client的port和IP
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&Server,len);

        struct sockaddr_in temp;
        socklen_t temp_len = sizeof(temp);
        ssize_t n = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(sockaddr*)&temp,&temp_len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "Server say$ " << buffer << std::endl;
        }
    }
    close(sockfd);
   

    return 0;
}

(4)讓服務(wù)器與客戶端實(shí)現(xiàn)通信

我們看到客戶端的端口號(hào)是隨機(jī)值。

這里的 127.0.0.1 叫做本地環(huán)回。client 和 server 發(fā)送數(shù)據(jù)只在本地協(xié)議棧中進(jìn)行數(shù)據(jù)流動(dòng),不會(huì)將我們的數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)中。

作用:用來做本地網(wǎng)絡(luò)服務(wù)器代碼測(cè)試的,意思就是如果我們綁定的 IP 是 127.0.0.1 的話,在應(yīng)用層發(fā)送的消息不會(huì)進(jìn)入物理層,也就不會(huì)發(fā)送出去。

當(dāng)我們運(yùn)行起來后想要查看網(wǎng)絡(luò)情況就可以用指令 netstat,后邊也可以附帶參數(shù):

  • -a:顯示所有連線中的 Socket。
  • -e:顯示網(wǎng)絡(luò)其他相關(guān)信息。
  • -i:顯示網(wǎng)絡(luò)界面信息表單。
  • -l:顯示監(jiān)控中的服務(wù)器的 Socket。
  • -n:直接使用 ip 地址(數(shù)字),而不通過域名服務(wù)器。
  • -p:顯示正在使用 Socket 的程序識(shí)別碼和程序名稱。
  • -t:顯示 TCP 傳輸協(xié)議的連線狀況。
  • -u:顯示 UDP 傳輸協(xié)議的連線狀況。

客戶端多線程處理收到消息和發(fā)送消息

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"

using namespace std;

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

struct ThreadData
{
    struct sockaddr_in server;
    int sockfd;
    std::string serverip;
};

void *recv_message(void *args)
{
    // OpenTerminal();
    ThreadData *td = static_cast<ThreadData *>(args);
    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cerr << buffer << endl;
        }
    }
}

void *send_message(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    string message;
    socklen_t len = sizeof(td->server);

    std::string welcome = td->serverip;
    welcome += " comming...";
    sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);

    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);

        // std::cout << message << std::endl;
        // 1. 數(shù)據(jù) 2. 給誰發(fā)
        sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
    }
}

// 多線程
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct ThreadData td;
    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport); //?
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());

    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (td.sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }

    td.serverip = serverip;

    pthread_t recvr, sender;
    pthread_create(&recvr, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    pthread_join(recvr, nullptr);
    pthread_join(sender, nullptr);

    close(td.sockfd);
    return 0;
}

我們看到客戶端有一個(gè)主線程和兩個(gè)收到發(fā)送的線程 

UDP(Windows 環(huán)境下 C++ 實(shí)現(xiàn))

在 Windows 下寫客戶端,在 Linux 下用 Linux 充當(dāng)服務(wù)器實(shí)現(xiàn)客戶端發(fā)送數(shù)據(jù),服務(wù)器接收數(shù)據(jù)的功能(Windows 下的套接字和 Linux 下的幾乎一樣)。

Windows環(huán)境下的Client.cpp

#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <string>
using namespace std;

uint16_t port = 8080;
std::string serverip = "8.155.26.31";
#pragma comment(lib, "ws2_32.lib")
int main()//_tmain,要加#include <tchar.h>才能用
{
    WSADATA WSAData;           //初始化信息
    WORD sockVersion = MAKEWORD(2, 2);

    //啟動(dòng)Winsock
    if (WSAStartup(sockVersion,&WSAData) != 0) {
        cout << "WSAStartup Error = " << WSAGetLastError() << endl;
        return 0;
    }
    else {
        cout << "start Success" << endl;
    }

    //創(chuàng)建socket
    SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (clientSocket == SOCKET_ERROR) {
        cout << "socket Error = " << WSAGetLastError() << endl;
        return 1;
    }
    else {
        cout << "socket Success" << endl;
    }

    sockaddr_in dstAddr;
    dstAddr.sin_family = AF_INET;
    dstAddr.sin_port = htons(port);
    dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());
    char buffer[1024];
    while (true)
    {
        std::string message;
        std::cout << "Client say# ";
        std::getline(std::cin, message);
        sendto(clientSocket, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&dstAddr, sizeof(dstAddr));
        struct sockaddr_in temp;
        int len = sizeof(temp);

        int s = recvfrom(clientSocket, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server say$ " << buffer << std::endl;
        }
    }
    //關(guān)閉socket連接
    closesocket(clientSocket);
    WSACleanup();
    return 0;
}

運(yùn)行結(jié)果如下:

這里要實(shí)現(xiàn)正常通信,云服務(wù)器要進(jìn)行被遠(yuǎn)程訪問,就需要開放公網(wǎng) IP 的端口

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • yum安裝CDH5.5 hive、impala的過程詳解

    yum安裝CDH5.5 hive、impala的過程詳解

    這篇文章主要介紹了yum安裝CDH5.5 hive、impala的過程詳解的相關(guān)資料,非常不錯(cuò) 具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2016-10-10
  • 在服務(wù)器上配置僅使用HTTPS通信的教程

    在服務(wù)器上配置僅使用HTTPS通信的教程

    這篇文章主要介紹了在服務(wù)器上配置僅使用HTTPS通信的教程,示例囊括Apache2和Nginx以及Lighttpd服務(wù)器上的操作,需要的朋友可以參考下
    2015-06-06
  • Linux下Apache服務(wù)的部署和配置

    Linux下Apache服務(wù)的部署和配置

    這篇文章介紹了Linux下Apache服務(wù)的部署和配置,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-12-12
  • Apache服務(wù)器二級(jí)域名的完美實(shí)現(xiàn)

    Apache服務(wù)器二級(jí)域名的完美實(shí)現(xiàn)

    Apache服務(wù)器二級(jí)域名的完美實(shí)現(xiàn) 首先,你的擁有一個(gè)有泛域名解析的頂級(jí)域名,例如: domain.com
    2008-10-10
  • Apache中Virtual Host虛擬主機(jī)配置及rewrite參數(shù)說明

    Apache中Virtual Host虛擬主機(jī)配置及rewrite參數(shù)說明

    這篇文章主要介紹了Apache中Virtual Host虛擬主機(jī)配置及rewrite模塊中的重要參數(shù)說明,是在同一個(gè)Apache服務(wù)器軟件上部署多個(gè)站點(diǎn)的基礎(chǔ)方法,需要的朋友可以參考下
    2016-03-03
  • centos7系統(tǒng)nginx服務(wù)器下phalcon環(huán)境搭建方法詳解

    centos7系統(tǒng)nginx服務(wù)器下phalcon環(huán)境搭建方法詳解

    這篇文章主要介紹了centos7系統(tǒng)nginx服務(wù)器下phalcon環(huán)境搭建方法,結(jié)合具體實(shí)例形式詳細(xì)分析了centos7的nginx服務(wù)器搭建phalcon的具體操作步驟與相關(guān)設(shè)置技巧,需要的朋友可以參考下
    2019-09-09
  • Linux系統(tǒng)配置靜態(tài)IP地址的詳細(xì)步驟

    Linux系統(tǒng)配置靜態(tài)IP地址的詳細(xì)步驟

    在安裝Linux后,系統(tǒng)的網(wǎng)絡(luò)IP地址默認(rèn)是自動(dòng)分配的,這將導(dǎo)致每次啟動(dòng)Linux系統(tǒng)后,系統(tǒng)的IP地址都會(huì)發(fā)生改變,此文以CentOS7系統(tǒng)環(huán)境為例,詳細(xì)介紹如何配置Linux系統(tǒng)的靜態(tài)IP地址,需要的朋友可以參考下
    2024-04-04
  • 深入理解Apache?RocketMQ?中Message?消息的核心概念

    深入理解Apache?RocketMQ?中Message?消息的核心概念

    深入理解一下Apache?RocketMQ中Message(消息)這個(gè)核心概念,這份文檔詳細(xì)闡述了消息的定義、在模型中的位置、內(nèi)部屬性、約束和使用建議,感興趣的朋友跟隨小編一起學(xué)習(xí)吧
    2025-08-08
  • Apache 多端口多站點(diǎn)配置方法

    Apache 多端口多站點(diǎn)配置方法

    Apache多端口多站點(diǎn)的配置方法,配置apache服務(wù)器的朋友可以參考下。
    2010-08-08
  • Linux QT Kit丟失及Version為空問題解決方案

    Linux QT Kit丟失及Version為空問題解決方案

    這篇文章主要介紹了Linux QT Kit丟失及Version為空問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-08-08

最新評(píng)論