Linux UDP網(wǎng)絡(luò)編程套接字sockets介紹
一、預(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)的解釋:
- 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 通信。
- 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)也是一樣的。
- 首先 pid 是系統(tǒng)規(guī)定的,而 port 是網(wǎng)絡(luò)規(guī)定的,這樣就可以把系統(tǒng)和網(wǎng)絡(luò)解耦。
- port 標(biāo)識(shí)服務(wù)器的唯一性不能做任何改變,要讓客戶端能找到服務(wù)器,就像 110,120 一樣不能被改變,而 pid 每次啟動(dòng)進(jìn)程,pid 就會(huì)改變。
- 不是所有的進(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é)議的不同,套接字可以分為兩種類型:
- 流套接字(Stream Socket,也稱為面向連接的套接字):基于 TCP 協(xié)議,提供可靠的、面向連接的通信。使用流套接字時(shí),數(shù)據(jù)可以按照發(fā)送的順序和完整性進(jìn)行傳輸,確保數(shù)據(jù)的準(zhǔn)確性。流套接字的通信方式類似于電話通信,需要在通信前先建立連接。
- 數(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é)議的地址格式并不相同。
套接字有不少類型,常見的有三種:
- 原始 socket
- 域間 socket
- 網(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ù)器通信流程
- 建立 UDP 套接字
- 綁定套接字到特定的地址
- 等待并接受客戶端信息
- 處理客戶端請(qǐng)求
- 發(fā)送信息給客戶端
- 關(guān)閉套接字
(2)UDP 客戶端通信流程
- 建立 UDP 套接字
- 發(fā)送信息給服務(wù)器
- 接收來自服務(wù)器的信息
- 關(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)文章
Apache服務(wù)器二級(jí)域名的完美實(shí)現(xiàn)
Apache服務(wù)器二級(jí)域名的完美實(shí)現(xiàn) 首先,你的擁有一個(gè)有泛域名解析的頂級(jí)域名,例如: domain.com2008-10-10
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)境搭建方法,結(jié)合具體實(shí)例形式詳細(xì)分析了centos7的nginx服務(wù)器搭建phalcon的具體操作步驟與相關(guān)設(shè)置技巧,需要的朋友可以參考下2019-09-09
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(消息)這個(gè)核心概念,這份文檔詳細(xì)闡述了消息的定義、在模型中的位置、內(nèi)部屬性、約束和使用建議,感興趣的朋友跟隨小編一起學(xué)習(xí)吧2025-08-08
Linux QT Kit丟失及Version為空問題解決方案
這篇文章主要介紹了Linux QT Kit丟失及Version為空問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08

