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