柚子快報(bào)激活碼778899分享:【網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(一)
柚子快報(bào)激活碼778899分享:【網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(一)
網(wǎng)絡(luò)編程套接字(一)
文章目錄
一、預(yù)備知識(shí)1.1端口號(hào)1.2傳輸層的TCP協(xié)議與UDP協(xié)議TCP協(xié)議UDP協(xié)議
1.3網(wǎng)絡(luò)字節(jié)序
二、socket編程接口2.1 socket常見(jiàn)API2.2 sockaddr結(jié)構(gòu)
三、簡(jiǎn)單的UDP網(wǎng)絡(luò)程序3.1創(chuàng)建UDP套接字3.2服務(wù)端綁定字符串IP & 整數(shù)IP
3.3運(yùn)行3.4簡(jiǎn)易echo服務(wù)器實(shí)現(xiàn)
一、預(yù)備知識(shí)
1.1端口號(hào)
上網(wǎng)的行為一般可以歸結(jié)為兩種:
把遠(yuǎn)端的數(shù)據(jù)拉取到本地;把本地的數(shù)據(jù)推送到遠(yuǎn)端。
數(shù)據(jù)拉取到本地的過(guò)程我們可以理解為輸入,數(shù)據(jù)推送到遠(yuǎn)端的過(guò)程我們可以理解為輸出。
所以,上網(wǎng)的本質(zhì)就是IO,再具體點(diǎn),網(wǎng)絡(luò)通信的本質(zhì)就是進(jìn)程間通信。
進(jìn)程間通信的前提是讓不同的進(jìn)程看到同一份公共資源,很明顯這個(gè)公共資源就是網(wǎng)絡(luò)。
那么如何在茫茫網(wǎng)絡(luò)中找到兩個(gè)進(jìn)程呢?
IP(IP地址)+port(端口號(hào))=互聯(lián)網(wǎng)中唯一的一個(gè)進(jìn)程。
IP地址可以讓我們?cè)诨ヂ?lián)網(wǎng)中找到唯一的一臺(tái)主機(jī)。port端口號(hào)可以讓我們找到這臺(tái)主機(jī)上唯一的一個(gè)進(jìn)程。
端口號(hào)(port)的作用實(shí)際就是標(biāo)識(shí)一臺(tái)主機(jī)上的一個(gè)進(jìn)程。
端口號(hào)是傳輸層協(xié)議的內(nèi)容。端口號(hào)是一個(gè)2字節(jié)16位的整數(shù)。端口號(hào)用來(lái)標(biāo)識(shí)一個(gè)進(jìn)程,告訴操作系統(tǒng),當(dāng)前的這個(gè)數(shù)據(jù)要交給哪一個(gè)進(jìn)程來(lái)處理。一個(gè)進(jìn)程可以綁定多個(gè)端口號(hào),但是一個(gè)端口號(hào)不能被多個(gè)進(jìn)程同時(shí)綁定。
我們可以將port端口號(hào)與進(jìn)程綁定,這樣進(jìn)程就可以通過(guò)端口號(hào)來(lái)唯一標(biāo)識(shí)了。
為什么不使用進(jìn)程ID實(shí)現(xiàn)這部分功能?
專事專辦,雖然進(jìn)程ID也能夠唯一區(qū)分進(jìn)程,但是這畢竟分屬了兩個(gè)領(lǐng)域:操作系統(tǒng)和網(wǎng)絡(luò),你可以理解為有一部分解耦的因素,同時(shí)你也應(yīng)該意識(shí)到專事專辦的思想是一種正確的系統(tǒng)設(shè)計(jì)思維。
底層如何通過(guò)port找到對(duì)應(yīng)進(jìn)程的?
實(shí)際底層采用哈希的方式建立了端口號(hào)和進(jìn)程PID或PCB之間的映射關(guān)系,當(dāng)?shù)讓幽玫蕉丝谔?hào)時(shí)就可以直接執(zhí)行對(duì)應(yīng)的哈希算法,然后就能夠找到該端口號(hào)對(duì)應(yīng)的進(jìn)程。
1.2傳輸層的TCP協(xié)議與UDP協(xié)議
TCP協(xié)議
TCP協(xié)議叫做傳輸控制協(xié)議(Transmission Control Protocol),TCP協(xié)議是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議。
TCP協(xié)議是面向連接的,如果兩臺(tái)主機(jī)之間想要進(jìn)行數(shù)據(jù)傳輸,那么必須要先建立連接,當(dāng)連接建立成功后才能進(jìn)行數(shù)據(jù)傳輸。其次,TCP協(xié)議是保證可靠的協(xié)議,數(shù)據(jù)在傳輸過(guò)程中如果出現(xiàn)了丟包、亂序等情況,TCP協(xié)議都有對(duì)應(yīng)的解決方法。
UDP協(xié)議
UDP協(xié)議叫做用戶數(shù)據(jù)報(bào)協(xié)議(User Datagram Protocol),UDP協(xié)議是一種無(wú)需建立連接的、不可靠的、面向數(shù)據(jù)報(bào)的傳輸層通信協(xié)議。
使用UDP協(xié)議進(jìn)行通信時(shí)無(wú)需建立連接,如果兩臺(tái)主機(jī)之間想要進(jìn)行數(shù)據(jù)傳輸,那么直接將數(shù)據(jù)發(fā)送給對(duì)端主機(jī)就行了,但這也就意味著UDP協(xié)議是不可靠的,數(shù)據(jù)在傳輸過(guò)程中如果出現(xiàn)了丟包、亂序等情況,UDP協(xié)議無(wú)法處理。
有關(guān)TCP協(xié)議和UDP協(xié)議的可靠性問(wèn)題:
UDP協(xié)議不可靠性并不是一種缺點(diǎn),因?yàn)門(mén)CP協(xié)議對(duì)于數(shù)據(jù)傳輸錯(cuò)誤等情況可以做出處理就意味著TCP協(xié)議更復(fù)雜,實(shí)現(xiàn)了更多的接口,而UDP協(xié)議也必定更為簡(jiǎn)單。
所以這兩種協(xié)議并不好壞之分,只是區(qū)別于使用場(chǎng)景,比如TCP協(xié)議適用遠(yuǎn)程登錄:SSH(安全外殼協(xié)議)和Telnet(遠(yuǎn)程登錄協(xié)議)使用TCP協(xié)議來(lái)確保遠(yuǎn)程登錄會(huì)話的可靠性和安全性。
而UDP協(xié)議適用于流媒體傳輸:如在線視頻和音頻播放等應(yīng)用,需要快速的數(shù)據(jù)傳輸和低延遲,但對(duì)數(shù)據(jù)的完整性和準(zhǔn)確性要求不高。在這些場(chǎng)景下,即使部分?jǐn)?shù)據(jù)丟失或出錯(cuò),也不會(huì)對(duì)用戶體驗(yàn)產(chǎn)生太大影響。
1.3網(wǎng)絡(luò)字節(jié)序
計(jì)算機(jī)在存儲(chǔ)數(shù)據(jù)時(shí)是有大小端的概念的:
大端模式: 數(shù)據(jù)的高字節(jié)內(nèi)容保存在內(nèi)存的低地址處,數(shù)據(jù)的低字節(jié)內(nèi)容保存在內(nèi)存的高地址處。小端模式: 數(shù)據(jù)的高字節(jié)內(nèi)容保存在內(nèi)存的高地址處,數(shù)據(jù)的低字節(jié)內(nèi)容保存在內(nèi)存的低地址處。
如果編寫(xiě)的程序只在本地機(jī)器上運(yùn)行,那么是不需要考慮大小端問(wèn)題的,因?yàn)橥慌_(tái)機(jī)器上的數(shù)據(jù)采用的存儲(chǔ)方式都是一樣的,要么采用的都是大端存儲(chǔ)模式,要么采用的都是小端存儲(chǔ)模式。但如果涉及網(wǎng)絡(luò)通信,那就必須考慮大小端的問(wèn)題,否則對(duì)端主機(jī)識(shí)別出來(lái)的數(shù)據(jù)可能與發(fā)送端想要發(fā)送的數(shù)據(jù)是不一致的。
而TCP/IP協(xié)議解決這一問(wèn)題的方式非常簡(jiǎn)單:規(guī)定網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,即低地址高字節(jié)。
如果發(fā)送端是小端,需要先將數(shù)據(jù)轉(zhuǎn)成大端,然后再發(fā)送到網(wǎng)絡(luò)當(dāng)中。如果發(fā)送端是大端,則可以直接進(jìn)行發(fā)送。如果接收端是小端,需要先將接收到數(shù)據(jù)轉(zhuǎn)成小端后再進(jìn)行數(shù)據(jù)識(shí)別。如果接收端是大端,則可以直接進(jìn)行數(shù)據(jù)識(shí)別。
為什么網(wǎng)絡(luò)字節(jié)序采用的是大端?而不是小端?
網(wǎng)絡(luò)字節(jié)序采用的是大端,而主機(jī)字節(jié)序一般采用的是小端,那為什么網(wǎng)絡(luò)字節(jié)序不采用小端呢?如果網(wǎng)絡(luò)字節(jié)序采用小端的話,發(fā)送端和接收端在發(fā)生和接收數(shù)據(jù)時(shí)就不用進(jìn)行大小端的轉(zhuǎn)換了。
該問(wèn)題有很多不同說(shuō)法,下面列舉了兩種說(shuō)法:
說(shuō)法一: TCP在Unix時(shí)代就有了,以前Unix機(jī)器都是大端機(jī),因此網(wǎng)絡(luò)字節(jié)序也就采用的是大端,但之后人們發(fā)現(xiàn)用小端能簡(jiǎn)化硬件設(shè)計(jì),所以現(xiàn)在主流的都是小端機(jī),但協(xié)議已經(jīng)不好改了。 說(shuō)法二: 大端序更符合現(xiàn)代人的讀寫(xiě)習(xí)慣。
為使網(wǎng)絡(luò)程序具有可移植性,使同樣的C代碼在大端和小端計(jì)算機(jī)上編譯后都能正常運(yùn)行,系統(tǒng)提供了四個(gè)函數(shù),可以通過(guò)調(diào)用以下庫(kù)函數(shù)實(shí)現(xiàn)網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序之間的轉(zhuǎn)換。
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
函數(shù)名當(dāng)中的h表示host,n表示network,l表示32位長(zhǎng)整數(shù),s表示16位短整數(shù)。
例如htonl表示將32位長(zhǎng)整數(shù)從主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序。
如果主機(jī)是小端字節(jié)序,則這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回。如果主機(jī)是大端字節(jié)序,則這些函數(shù)不做任何轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回。
二、socket編程接口
2.1 socket常見(jiàn)API
// 創(chuàng)建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務(wù)器)
int socket(int domain, int type, int protocol);
// 綁定端口號(hào) (TCP/UDP, 服務(wù)器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 開(kāi)始監(jiān)聽(tīng)socket (TCP, 服務(wù)器)
int listen(int socket, int backlog);
// 接收請(qǐng)求 (TCP, 服務(wù)器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
以上接口是一層抽象的網(wǎng)絡(luò)編程接口,適用于各種底層網(wǎng)絡(luò)協(xié)議,如ipv4、ipv6以及后面的UNIX Domain Socket,然而,各種網(wǎng)絡(luò)協(xié)議的地址格式并不相同。那么我們?nèi)绾伟巡煌刂犯袷降牡刂纷優(yōu)榻y(tǒng)一的地址格式交給以上API呢?引入了sockaddr結(jié)構(gòu),
2.2 sockaddr結(jié)構(gòu)
套接字不僅支持跨網(wǎng)絡(luò)的進(jìn)程間通信,還支持本地的進(jìn)程間通信(域間套接字),而很明顯本地的進(jìn)程間通信是不需要IP和PORT的,因此提供了sockaddr_in結(jié)構(gòu)體(ipv6—sockaddr_in6)和sockaddr_un結(jié)構(gòu)體,sockaddr_in用于網(wǎng)絡(luò)通信,sockaddr_un用于本地通信。
為了統(tǒng)一地質(zhì)結(jié)構(gòu)的表示方法,于是就出現(xiàn)了sockeaddr結(jié)構(gòu)體,它用于統(tǒng)一地址結(jié)構(gòu)的表示方法,使得不同的地址結(jié)構(gòu)可以被bind()、connect()、recvfrom()、sendto()等函數(shù)調(diào)用。
該結(jié)構(gòu)體與sockaddr_in和sockaddr_un的結(jié)構(gòu)都不相同,但這三個(gè)結(jié)構(gòu)體頭部的16個(gè)比特位都是一樣的,這個(gè)字段叫做協(xié)議家族。
此時(shí)當(dāng)我們?cè)趥鲄r(shí),就不用傳入sockeaddr_in或sockeaddr_un這樣的結(jié)構(gòu)體,而統(tǒng)一傳入sockeaddr這樣的結(jié)構(gòu)體從而實(shí)現(xiàn)了統(tǒng)一的API接口。
在這些API內(nèi)部就可以提取sockeaddr結(jié)構(gòu)頭部的16位進(jìn)行識(shí)別,然后執(zhí)行對(duì)應(yīng)的操作。此時(shí)我們就通過(guò)通用sockaddr結(jié)構(gòu),將參數(shù)類型進(jìn)行了統(tǒng)一。
其實(shí)這種設(shè)計(jì)模式就是早期的多態(tài)。
三、簡(jiǎn)單的UDP網(wǎng)絡(luò)程序
首先說(shuō)明下我們的編程思路,首先肯定需要?jiǎng)?chuàng)建一個(gè)服務(wù)端對(duì)象,并初始化這個(gè)服務(wù)端開(kāi)啟服務(wù)。
3.1創(chuàng)建UDP套接字
那么對(duì)于初始化服務(wù)端,首先要做的一定是創(chuàng)建socket套接字。
int socket(int domain, int type, int protocol);
參數(shù)說(shuō)明:
domain:創(chuàng)建套接字的域或者叫做協(xié)議家族,也就是創(chuàng)建套接字的類型。該參數(shù)就相當(dāng)于struct sockaddr結(jié)構(gòu)的前16個(gè)位。如果是本地通信就設(shè)置為AF_UNIX,如果是網(wǎng)絡(luò)通信就設(shè)置為AF_INET(IPv4)或AF_INET6(IPv6)。 type:創(chuàng)建套接字時(shí)所需的服務(wù)類型。其中最常見(jiàn)的服務(wù)類型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的網(wǎng)絡(luò)通信,我們采用的就是SOCK_DGRAM,叫做用戶數(shù)據(jù)報(bào)服務(wù),如果是基于TCP的網(wǎng)絡(luò)通信,我們采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服務(wù)。 protocol:創(chuàng)建套接字的協(xié)議類別。你可以指明為T(mén)CP或UDP,但該字段一般直接設(shè)置為0就可以了,設(shè)置為0表示的就是默認(rèn),此時(shí)會(huì)根據(jù)傳入的前兩個(gè)參數(shù)自動(dòng)推導(dǎo)出你最終需要使用的是哪種協(xié)議。
返回值說(shuō)明:
套接字創(chuàng)建成功返回一個(gè)文件描述符,創(chuàng)建失敗返回-1,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
socket函數(shù)底層做了什么?
當(dāng)我們調(diào)用socket函數(shù)創(chuàng)建套接字時(shí),實(shí)際相當(dāng)于我們打開(kāi)了一個(gè)“網(wǎng)絡(luò)文件”,打開(kāi)后在內(nèi)核層面上就形成了一個(gè)對(duì)應(yīng)的struct file結(jié)構(gòu)體,同時(shí)該結(jié)構(gòu)體被連入到了該進(jìn)程對(duì)應(yīng)的文件雙鏈表,并將該結(jié)構(gòu)體的首地址填入到了fd_array數(shù)組當(dāng)中下標(biāo)為3的位置,此時(shí)fd_array數(shù)組中下標(biāo)為3的指針就指向了這個(gè)打開(kāi)的“網(wǎng)絡(luò)文件”,最后3號(hào)文件描述符作為socket函數(shù)的返回值返回給了用戶。
其中每一個(gè)struct file結(jié)構(gòu)體中包含的就是對(duì)應(yīng)打開(kāi)文件各種信息,比如文件的屬性信息、操作方法以及文件緩沖區(qū)等。其中文件對(duì)應(yīng)的屬性在內(nèi)核當(dāng)中是由struct inode結(jié)構(gòu)體來(lái)維護(hù)的,而文件對(duì)應(yīng)的操作方法實(shí)際就是一堆的函數(shù)指針(比如read*和write*)在內(nèi)核當(dāng)中就是由struct file_operations結(jié)構(gòu)體(方法集)來(lái)維護(hù)的。
而文件緩沖區(qū)對(duì)于打開(kāi)的普通文件來(lái)說(shuō)對(duì)應(yīng)的一般是磁盤(pán),但對(duì)于現(xiàn)在打開(kāi)的“網(wǎng)絡(luò)文件”來(lái)說(shuō),這里的文件緩沖區(qū)對(duì)應(yīng)的就是網(wǎng)卡。
對(duì)于一般的普通文件來(lái)說(shuō),當(dāng)用戶通過(guò)文件描述符將數(shù)據(jù)寫(xiě)到文件緩沖區(qū),然后再把數(shù)據(jù)刷到磁盤(pán)上就完成了數(shù)據(jù)的寫(xiě)入操作.
而對(duì)于現(xiàn)在socket函數(shù)打開(kāi)的“網(wǎng)絡(luò)文件”來(lái)說(shuō),當(dāng)用戶將數(shù)據(jù)寫(xiě)到文件緩沖區(qū)后,操作系統(tǒng)會(huì)定期將數(shù)據(jù)刷到網(wǎng)卡里面,而網(wǎng)卡則是負(fù)責(zé)數(shù)據(jù)發(fā)送的,因此數(shù)據(jù)最終就發(fā)送到了網(wǎng)絡(luò)當(dāng)中。
3.2服務(wù)端綁定
現(xiàn)在套接字已經(jīng)創(chuàng)建好了,我們還沒(méi)有將這個(gè)套接字與網(wǎng)絡(luò)進(jìn)行綁定,即通信方式、IP和PORT等等都是未知的,而這些內(nèi)容都存放在sockaddr結(jié)構(gòu)體中,所以我們需要利用bind函數(shù)將socket與sockaddr進(jìn)行綁定,即改變網(wǎng)絡(luò)文件當(dāng)中文件操作函數(shù)的指向,將對(duì)應(yīng)的操作函數(shù)改為對(duì)應(yīng)網(wǎng)卡的操作方法,此時(shí)讀數(shù)據(jù)和寫(xiě)數(shù)據(jù)對(duì)應(yīng)的操作對(duì)象就是網(wǎng)卡了,所以綁定實(shí)際上就是將文件和網(wǎng)絡(luò)關(guān)聯(lián)起來(lái)。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數(shù)說(shuō)明:
sockfd:綁定的文件的文件描述符。也就是我們創(chuàng)建套接字時(shí)獲取到的文件描述符。addr:網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號(hào)等。addrlen:傳入的addr結(jié)構(gòu)體的長(zhǎng)度。
返回值說(shuō)明:
綁定成功返回0,綁定失敗返回-1,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
這里我們采用的是網(wǎng)絡(luò)通信,所以我們需要傳入sockaddr_in結(jié)構(gòu)體的地址,注意強(qiáng)轉(zhuǎn)為sockaddr*類型。
struct sockaddr_in結(jié)構(gòu)體
成員:
sin_family:表示協(xié)議家族。sin_port:表示端口號(hào),是一個(gè)16位的整數(shù)。sin_addr:表示IP地址,是一個(gè)32位的整數(shù)。
其中sin_addr的類型是struct in_addr,實(shí)際該結(jié)構(gòu)體當(dāng)中就只有一個(gè)成員s_addr,該成員就是一個(gè)32位的整數(shù),IP地址實(shí)際就是存儲(chǔ)在這個(gè)整數(shù)當(dāng)中的。
套接字創(chuàng)建完畢后我們就需要進(jìn)行綁定了,但在綁定之前我們需要先定義一個(gè)struct sockaddr_in結(jié)構(gòu),將對(duì)應(yīng)的網(wǎng)絡(luò)屬性信息填充到該結(jié)構(gòu)當(dāng)中。由于該結(jié)構(gòu)體當(dāng)中還有部分選填字段,因此我們最好在填充之前對(duì)該結(jié)構(gòu)體變量里面的內(nèi)容進(jìn)行清空,然后再將協(xié)議家族、端口號(hào)、IP地址等信息填充到該結(jié)構(gòu)體變量當(dāng)中。
需要注意的是,在發(fā)送到網(wǎng)絡(luò)之前需要將端口號(hào)設(shè)置為網(wǎng)絡(luò)序列,由于端口號(hào)是16位的,因此我們需要使用前面說(shuō)到的htons函數(shù)將端口號(hào)轉(zhuǎn)為網(wǎng)絡(luò)序列。此外,由于網(wǎng)絡(luò)當(dāng)中傳輸?shù)氖钦麛?shù)IP,我們需要調(diào)用inet_addr函數(shù)將字符串IP轉(zhuǎn)換成整數(shù)IP,然后再將轉(zhuǎn)換后的整數(shù)IP進(jìn)行設(shè)置。
當(dāng)網(wǎng)絡(luò)屬性信息填充完畢后,由于bind函數(shù)提供的是通用參數(shù)類型,因此在傳入結(jié)構(gòu)體地址時(shí)還需要將struct sockaddr_in*強(qiáng)轉(zhuǎn)為struct sockaddr*類型后再進(jìn)行傳入。
UdpServer(uint16_t port) : _sockfd(sockfddefault), _port(port), _ip(ip), _isrunning(false) {}
void InitServer()
{
// 1.創(chuàng)建UDP套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 返回文件描述符
if (_sockfd < 0)
{
LOG(FATAL, "socket error,%s,%d", strerror(errno), errno);
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success,sockfd:%d", _sockfd);
// 2.0填充sockaddr_in結(jié)構(gòu)
struct sockaddr_in local; // struct sockaddr_in 系統(tǒng)提供的數(shù)據(jù)類型。local是變量,用戶棧上開(kāi)辟空間。
bzero(&local, sizeof(local)); // 將從&local開(kāi)始的sizeof(local)大小的內(nèi)存區(qū)域置零
local.sin_family = AF_INET; // 設(shè)置網(wǎng)絡(luò)通信方式
local.sin_port = htons(_port); // port要經(jīng)過(guò)網(wǎng)絡(luò)傳輸給對(duì)面,所有需要從主機(jī)序列轉(zhuǎn)換為網(wǎng)絡(luò)序列
// a. 字符串風(fēng)格的點(diǎn)分十進(jìn)制的IP地址轉(zhuǎn)成 4 字節(jié)IP
// b. 主機(jī)序列,轉(zhuǎn)成網(wǎng)絡(luò)序列
// in_addr_t inet_addr(const char *cp) -> 該函數(shù)可以同時(shí)完成 a & b
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串風(fēng)格的點(diǎn)分十進(jìn)制的IP地址 -> 4字節(jié)IP
// 2.1bind綁定sockfd和網(wǎng)絡(luò)信息(IP+PORT)
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error,%s,%d", strerror(errno), errno);
exit(BIND_ERROR);
}
LOG(INFO, "socket bind success");
}
服務(wù)端綁定一般不指定IP,為什么?
當(dāng)一個(gè)服務(wù)器的帶寬足夠大時(shí),一臺(tái)機(jī)器接收數(shù)據(jù)的能力就約束了這臺(tái)機(jī)器的IO效率,因此一臺(tái)服務(wù)器底層可能裝有多張網(wǎng)卡,此時(shí)這臺(tái)服務(wù)器就可能會(huì)有多個(gè)IP地址,但一臺(tái)服務(wù)器上端口號(hào)為8081的服務(wù)只有一個(gè)。這臺(tái)服務(wù)器在接收數(shù)據(jù)時(shí),這里的多張網(wǎng)卡在底層實(shí)際都收到了數(shù)據(jù),如果這些數(shù)據(jù)也都想訪問(wèn)端口號(hào)為8081的服務(wù)。
**此時(shí)如果服務(wù)端在綁定的時(shí)候是指明綁定的某一個(gè)IP地址,那么此時(shí)服務(wù)端在接收數(shù)據(jù)的時(shí)候就只能從綁定IP對(duì)應(yīng)的網(wǎng)卡接收數(shù)據(jù)。**而如果服務(wù)端綁定的是INADDR_ANY(宏,值為0,表示任意IP),那么只要是發(fā)送給端口號(hào)為8081的服務(wù)的數(shù)據(jù),系統(tǒng)都會(huì)可以將數(shù)據(jù)自底向上交給該服務(wù)端。
所以這里我們對(duì)代碼做修改:
UdpServer(uint16_t port) : _sockfd(sockfddefault), _port(port), _isrunning(false) {}
void InitServer()
{
// 1.創(chuàng)建UDP套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 返回文件描述符
if (_sockfd < 0)
{
LOG(FATAL, "socket error,%s,%d", strerror(errno), errno);
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success,sockfd:%d", _sockfd);
// 2.0填充sockaddr_in結(jié)構(gòu)
struct sockaddr_in local; // struct sockaddr_in 系統(tǒng)提供的數(shù)據(jù)類型。local是變量,用戶棧上開(kāi)辟空間。
bzero(&local, sizeof(local)); // 將從&local開(kāi)始的sizeof(local)大小的內(nèi)存區(qū)域置零
local.sin_family = AF_INET; // 設(shè)置網(wǎng)絡(luò)通信方式
local.sin_port = htons(_port); // port要經(jīng)過(guò)網(wǎng)絡(luò)傳輸給對(duì)面,所有需要從主機(jī)序列轉(zhuǎn)換為網(wǎng)絡(luò)序列
// a. 字符串風(fēng)格的點(diǎn)分十進(jìn)制的IP地址轉(zhuǎn)成 4 字節(jié)IP
// b. 主機(jī)序列,轉(zhuǎn)成網(wǎng)絡(luò)序列
// in_addr_t inet_addr(const char *cp) -> 該函數(shù)可以同時(shí)完成 a & b
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串風(fēng)格的點(diǎn)分十進(jìn)制的IP地址 -> 4字節(jié)IP
local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY宏的值為0,給local.sin_addr.s_addr設(shè)置為0代表任意IP,因?yàn)橐粋€(gè)服務(wù)器有多個(gè)IP,為了確保所有請(qǐng)求_port端口的請(qǐng)求都能得到相應(yīng),所以設(shè)置為0
// 2.1bind綁定sockfd和網(wǎng)絡(luò)信息(IP+PORT)
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error,%s,%d", strerror(errno), errno);
exit(BIND_ERROR);
}
LOG(INFO, "socket bind success");
}
字符串IP & 整數(shù)IP
IP地址的表現(xiàn)形式有兩種:
字符串IP:類似于192.168.233.123這種字符串形式的IP地址,叫做基于字符串的點(diǎn)分十進(jìn)制IP地址,這種ip是給人看的。整數(shù)IP:IP地址在進(jìn)行網(wǎng)絡(luò)傳輸時(shí)所用的形式,用一個(gè)32位的整數(shù)來(lái)表示IP地址,這種ip是網(wǎng)絡(luò)傳輸用的。
為什么要分兩種IP表現(xiàn)形式?
如果我們?cè)诰W(wǎng)絡(luò)傳輸時(shí)直接以基于字符串的點(diǎn)分十進(jìn)制IP的形式進(jìn)行IP地址的傳送,那么此時(shí)一個(gè)IP地址至少就需要15個(gè)字節(jié),但實(shí)際并不需要耗費(fèi)這么多字節(jié)。
點(diǎn)分十進(jìn)制IP地址實(shí)際可以劃分為四個(gè)區(qū)域,其中每一個(gè)區(qū)域的取值都是0~255,而這個(gè)范圍的數(shù)字只需要用8個(gè)比特位就能表示,因此我們實(shí)際只需要32個(gè)比特位就能夠表示一個(gè)IP地址。其中這個(gè)32位的整數(shù)的每一個(gè)字節(jié)對(duì)應(yīng)的就是IP地址中的某個(gè)區(qū)域,我們將IP地址的這種表示方法稱之為整數(shù)IP,此時(shí)表示一個(gè)IP地址只需要4個(gè)字節(jié)。
所以在網(wǎng)絡(luò)編程中會(huì)涉及到字符串IP與整數(shù)IP之間的轉(zhuǎn)換,而系統(tǒng)也提供給了我們轉(zhuǎn)換的函數(shù)。
字符串IP轉(zhuǎn)換為整數(shù)IP:
in_addr_t inet_addr(const char *cp);
該函數(shù)使用起來(lái)非常簡(jiǎn)單,我們只需傳入待轉(zhuǎn)換的字符串IP,該函數(shù)返回的就是轉(zhuǎn)換后的整數(shù)IP。除此之外,inet_aton函數(shù)也可以將字符串IP轉(zhuǎn)換成整數(shù)IP,不過(guò)該函數(shù)使用起來(lái)沒(méi)有inet_addr簡(jiǎn)單。
整數(shù)IP轉(zhuǎn)化為字符串IP:
char *inet_ntoa(struct in_addr in);
需要注意的是,傳入inet_ntoa函數(shù)的參數(shù)類型是in_addr,因此我們?cè)趥鲄r(shí)不需要選中in_addr結(jié)構(gòu)當(dāng)中的32位的成員(即s_addr)傳入,直接傳入in_addr結(jié)構(gòu)體即可。
3.3運(yùn)行
以上創(chuàng)建套接字和綁定的操作都是屬于初始化服務(wù)端的內(nèi)容,那么接下來(lái)我們就需要編寫(xiě)服務(wù)端運(yùn)行過(guò)程的代碼,讓服務(wù)端啟動(dòng)服務(wù)了。
服務(wù)器實(shí)際上就是在周而復(fù)始的為我們提供某種服務(wù),服務(wù)器之所以稱為服務(wù)器,是因?yàn)榉?wù)器運(yùn)行起來(lái)后就永遠(yuǎn)不會(huì)退出,因此服務(wù)器實(shí)際執(zhí)行的是一個(gè)死循環(huán)代碼。
由于UDP服務(wù)器是不面向連接的,因此只要UDP服務(wù)器啟動(dòng)后,就可以直接讀取客戶端發(fā)來(lái)的數(shù)據(jù)。
接收數(shù)據(jù)的函數(shù):
ssize_t recvfrom(int sockfd
, void *buf
, size_t len
, int flags
, struct sockaddr *src_addr
, socklen_t *addrlen);
參數(shù)說(shuō)明:
sockfd:對(duì)應(yīng)操作的文件描述符。表示從該文件描述符索引的文件當(dāng)中讀取數(shù)據(jù)。 buf:讀取數(shù)據(jù)的存放位置。 len:期望讀取數(shù)據(jù)的字節(jié)數(shù)。 flags:讀取的方式。一般設(shè)置為0,表示阻塞讀取。 src_addr:對(duì)端網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號(hào)等。 addrlen:調(diào)用時(shí)傳入期望讀取的src_addr結(jié)構(gòu)體的長(zhǎng)度,返回時(shí)代表實(shí)際讀取到的src_addr結(jié)構(gòu)體的長(zhǎng)度,這是一個(gè)輸入輸出型參數(shù)。
返回值說(shuō)明:
讀取成功返回實(shí)際讀取到的字節(jié)數(shù),讀取失敗返回-1,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
注意:
由于UDP是不面向連接的,因此我們除了獲取到數(shù)據(jù)以外還需要獲取到對(duì)端網(wǎng)絡(luò)相關(guān)的屬性信息,包括IP地址和端口號(hào)等。在調(diào)用recvfrom讀取數(shù)據(jù)時(shí),必須將addrlen設(shè)置為你要讀取的結(jié)構(gòu)體對(duì)應(yīng)的大小。由于recvfrom函數(shù)提供的參數(shù)也是struct sockaddr*類型的,因此我們?cè)趥魅虢Y(jié)構(gòu)體地址時(shí)需要將struct sockaddr_in*類型進(jìn)行強(qiáng)轉(zhuǎn)。
發(fā)送數(shù)據(jù)的函數(shù):
ssize_t sendto(int sockfd
, const void *buf
, size_t len
, int flags
, const struct sockaddr *dest_addr
, socklen_t addrlen);
參數(shù)說(shuō)明:
sockfd:對(duì)應(yīng)操作的文件描述符。表示將數(shù)據(jù)寫(xiě)入該文件描述符索引的文件當(dāng)中。 buf:待寫(xiě)入數(shù)據(jù)的存放位置。 len:期望寫(xiě)入數(shù)據(jù)的字節(jié)數(shù)。 flags:寫(xiě)入的方式。一般設(shè)置為0,表示阻塞寫(xiě)入。 dest_addr:對(duì)端網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號(hào)等。 addrlen:傳入dest_addr結(jié)構(gòu)體的長(zhǎng)度。
返回值說(shuō)明:
寫(xiě)入成功返回實(shí)際寫(xiě)入的字節(jié)數(shù),寫(xiě)入失敗返回-1,同時(shí)錯(cuò)誤碼會(huì)被設(shè)置。
注意:
由于UDP不是面向連接的,因此除了傳入待發(fā)送的數(shù)據(jù)以外還需要指明對(duì)端網(wǎng)絡(luò)相關(guān)的信息,包括IP地址和端口號(hào)等。由于sendto函數(shù)提供的參數(shù)也是struct sockaddr*類型的,因此我們?cè)趥魅虢Y(jié)構(gòu)體地址時(shí)需要將struct sockaddr_in*類型進(jìn)行強(qiáng)轉(zhuǎn)。
3.4簡(jiǎn)易echo服務(wù)器實(shí)現(xiàn)
以上主要是為了讓大家認(rèn)識(shí)以下網(wǎng)絡(luò)編程的接口,那么接下來(lái)用一個(gè)例子帶大家初步了解網(wǎng)絡(luò)編程的思路。
下面我們實(shí)現(xiàn)一個(gè)簡(jiǎn)易echo服務(wù)器,他的功能就是客戶端向服務(wù)端發(fā)送什么數(shù)據(jù),服務(wù)端再將數(shù)據(jù)發(fā)送回來(lái)。
也就是說(shuō)服務(wù)端需要接收客戶端發(fā)送的數(shù)據(jù)recvfrom,然后還需要將數(shù)據(jù)發(fā)送出去sendto。
啟動(dòng)服務(wù)端服務(wù)
void Start()
{
// 一直運(yùn)行,直到管理者不想運(yùn)行了, 服務(wù)器都是死循環(huán)
// UDP是面向數(shù)據(jù)報(bào)的協(xié)議
_isrunning = true;
while (true)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必須初始化為sizeof(peer),不能是0
// 1.要先讓server接收數(shù)據(jù)
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
InetAddr addr(peer);
LOG(DEBUG, "get message from [%s:%d]: %s\n", addr.Ip().c_str(), addr.Port(), buffer);
// 2. 我們要將server收到的數(shù)據(jù),發(fā)回給對(duì)方
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
}
}
_isrunning = false;
}
InetAddr類的實(shí)現(xiàn)
我們想要將IP和PORT輸出到屏幕上,這就必須進(jìn)行一些轉(zhuǎn)換工作,比如整數(shù)IP到點(diǎn)分十進(jìn)制的IP轉(zhuǎn)換,網(wǎng)絡(luò)字節(jié)序到主機(jī)字節(jié)序的轉(zhuǎn)換等,所以我們可以實(shí)現(xiàn)一個(gè)類,讓類內(nèi)部幫我們進(jìn)行轉(zhuǎn)換。
#pragma once
#include
#include
#include
#include
#include
// 這是一個(gè)可以獲取點(diǎn)分十進(jìn)制格式IP地址和Port端口號(hào)的類
class InetAddr
{
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
private:
void GetAddress(std::string *ip, uint16_t *port)
{
*port = ntohs(_addr.sin_port);
*ip = inet_ntoa(_addr.sin_addr); // inet_ntoa是一個(gè)用于將網(wǎng)絡(luò)字節(jié)序的 IP 地址轉(zhuǎn)換為點(diǎn)分十進(jìn)制的字符串格式(如 "192.168.1.1")的函數(shù)
}
public:
InetAddr(const struct sockaddr_in &addr) : _addr(addr)
{
GetAddress(&_ip, &_port);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr() {}
};
客戶端程序的編寫(xiě)
客戶端也需要進(jìn)行類似服務(wù)端的初始化工作,即套接字的創(chuàng)建,綁定等。
#include
#include
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1.創(chuàng)建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
}
// 構(gòu)建目標(biāo)主機(jī)的socket信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server)); // bzero
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str()); // inet_addr用于將點(diǎn)分十進(jìn)制的 IPv4 地址字符串轉(zhuǎn)換成一個(gè)長(zhǎng)整型數(shù)(通常是 u_long 或 in_addr_t 類型)。
// 客戶端要不要bind?
std::string message;
// 2.直接通信即可(Start)
while (true)
{
std::cout << "Please Enter# ";
std::getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
客戶端要不要綁定?
答案是肯定的,因?yàn)榫W(wǎng)絡(luò)通信的前提就是需要客戶端的IP和PORT,服務(wù)端的IP和PORT,通過(guò)他們兩個(gè)網(wǎng)絡(luò)中的進(jìn)程才可以進(jìn)行通信。但是客戶端不能像服務(wù)端一樣顯式的bind,設(shè)想一個(gè)場(chǎng)景,淘寶寫(xiě)了一個(gè)客戶端,顯示綁定了端口號(hào)8080,而微信寫(xiě)的客戶端也顯示綁定的8080端口號(hào),那此時(shí)就會(huì)因?yàn)槎丝跊_突導(dǎo)致你只能使用一項(xiàng)服務(wù),這很明顯是不現(xiàn)實(shí)的,所以客戶端綁定端口的操作由操作系統(tǒng)自動(dòng)完成,就是為了防止客戶端端口號(hào)沖突,一般在首次發(fā)送數(shù)據(jù)的時(shí)候綁定。
我們已經(jīng)實(shí)現(xiàn)好了服務(wù)端的類,和客戶端程序,接下來(lái)我們只需要再實(shí)現(xiàn)一個(gè)程序,調(diào)用服務(wù)端對(duì)象的初始化和啟動(dòng)方法:
#include
#include
#include "UdpServer.hpp"
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " local_port\n"
<< std::endl;
}
// ./udpserver port
// 云服務(wù)器的port默認(rèn)都是禁止訪問(wèn)的。云服務(wù)器放開(kāi)端口8080 ~ 8085
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
EnableScreen();
uint16_t port = std::stoi(argv[1]);
std::unique_ptr
usvr->InitServer();
usvr->Start();
return 0;
}
本地測(cè)試
首先利用127.0.0.1(回環(huán)地址),進(jìn)行本地測(cè)試。
127.0.0.1 是一個(gè)特殊的IPv4地址,被稱為“回環(huán)地址”或“l(fā)ocalhost”。它通常用于指代本地計(jì)算機(jī)上的網(wǎng)絡(luò)服務(wù),而不是網(wǎng)絡(luò)上的另一臺(tái)計(jì)算機(jī)。在開(kāi)發(fā)或測(cè)試階段,開(kāi)發(fā)人員經(jīng)常需要在本地計(jì)算機(jī)上運(yùn)行多個(gè)服務(wù)實(shí)例,并使用127.0.0.1來(lái)訪問(wèn)它們。
網(wǎng)絡(luò)測(cè)試
在網(wǎng)絡(luò)測(cè)試前,你需要確保你的云服務(wù)器安全組配置已經(jīng)打開(kāi)了你所希望綁定的端口號(hào),就像這樣:
或者通過(guò)命令行的方式添加開(kāi)放端口規(guī)則和重新加載:
sudo ufw allow xx/udp
sudo ufw reload
我們可以利用netstat查看網(wǎng)絡(luò)信息:
我們發(fā)現(xiàn)綁定的IP為0.0.0.0即任意IP,端口號(hào)8888,鏈接方式UDP。
netstat的命令行參數(shù):
-n:number的意思,即IP和端口號(hào)都用數(shù)字的形式展示。-p:顯示哪個(gè)進(jìn)程或程序正在使用套接字(socket)。-u:僅顯示 UDP 連接。-a:顯示所有活動(dòng)的網(wǎng)絡(luò)連接和監(jiān)聽(tīng)的服務(wù)器套接字。
請(qǐng)注意,由于它顯示了進(jìn)程信息,因此你可能需要具有適當(dāng)?shù)臋?quán)限才能運(yùn)行它。在某些系統(tǒng)上,你可能需要使用 sudo 來(lái)運(yùn)行此命令,如 sudo netstat -npua。
青年人珍重的描寫(xiě)罷,時(shí)間正翻著書(shū)頁(yè),請(qǐng)你著筆! —青年人 實(shí)例,并使用127.0.0.1來(lái)訪問(wèn)它們。
網(wǎng)絡(luò)測(cè)試
在網(wǎng)絡(luò)測(cè)試前,你需要確保你的云服務(wù)器安全組配置已經(jīng)打開(kāi)了你所希望綁定的端口號(hào),就像這樣:
[外鏈圖片轉(zhuǎn)存中…(img-P0BkEsE1-1720966085373)]
或者通過(guò)命令行的方式添加開(kāi)放端口規(guī)則和重新加載:
sudo ufw allow xx/udp
sudo ufw reload
[外鏈圖片轉(zhuǎn)存中…(img-s8ieJtHZ-1720966085374)]
我們可以利用netstat查看網(wǎng)絡(luò)信息:
[外鏈圖片轉(zhuǎn)存中…(img-xXMCimr7-1720966085374)]
我們發(fā)現(xiàn)綁定的IP為0.0.0.0即任意IP,端口號(hào)8888,鏈接方式UDP。
netstat的命令行參數(shù):
-n:number的意思,即IP和端口號(hào)都用數(shù)字的形式展示。-p:顯示哪個(gè)進(jìn)程或程序正在使用套接字(socket)。-u:僅顯示 UDP 連接。-a:顯示所有活動(dòng)的網(wǎng)絡(luò)連接和監(jiān)聽(tīng)的服務(wù)器套接字。
請(qǐng)注意,由于它顯示了進(jìn)程信息,因此你可能需要具有適當(dāng)?shù)臋?quán)限才能運(yùn)行它。在某些系統(tǒng)上,你可能需要使用 sudo 來(lái)運(yùn)行此命令,如 sudo netstat -npua。
青年人珍重的描寫(xiě)罷,時(shí)間正翻著書(shū)頁(yè),請(qǐng)你著筆! —青年人
柚子快報(bào)激活碼778899分享:【網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(一)
文章鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。