世界簡(jiǎn)訊:【網(wǎng)絡(luò)】udp_socket編程

2023-01-03 18:30:03 來(lái)源:51CTO博客

在上一講中我們知道了網(wǎng)絡(luò)傳輸?shù)幕玖鞒蹋竟?jié)我們要更加深刻的理解一下兩臺(tái)主機(jī)之間交互的本質(zhì)。

我們?cè)诰W(wǎng)絡(luò)通信的時(shí)候,只要讓兩臺(tái)主機(jī)能夠通信就可以了嗎??

實(shí)際上,在進(jìn)行通信的時(shí)候不僅僅要考慮兩臺(tái)主機(jī)間相互交互數(shù)據(jù)!!本質(zhì)上將,進(jìn)行數(shù)據(jù)交互的時(shí)候是用戶和用戶在進(jìn)行交互用戶的身份,通常是用程序體現(xiàn)的!!程序一定是在運(yùn)行中 --> 進(jìn)程!!


(相關(guān)資料圖)

因此主機(jī)間通信的本質(zhì)是:在各自的主機(jī)上的兩個(gè)進(jìn)程在互相交互數(shù)據(jù)!!也就是進(jìn)程間通信

IP地址可以完成主機(jī)和主機(jī)的通信,而主機(jī)上各自的通信進(jìn)程才是發(fā)送和接受數(shù)據(jù)的一方。

因此我們用IP--確保主機(jī)的唯一性,再加端口號(hào)(prot)來(lái)確保該主機(jī)上的進(jìn)程的唯一性.

IP:PORT = 標(biāo)識(shí)互聯(lián)網(wǎng)中唯一的一個(gè)進(jìn)程!!

因此我們把IP:PORT叫做socket(套接字),因此網(wǎng)絡(luò)通信的本質(zhì):也就是進(jìn)程間通信!!

我們通常把本地的進(jìn)程間通信稱作SystemV進(jìn)程間通信。

網(wǎng)絡(luò)間的進(jìn)程間通信用的是process,Socket.

1.認(rèn)識(shí)端口號(hào)

我們剛說(shuō)過(guò)端口號(hào)PROT是用來(lái)確保主機(jī)上進(jìn)程的唯一性:他要告訴主機(jī)以后把消息交給哪一個(gè)進(jìn)程。

端口號(hào)(port)是傳輸層協(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)處理IP地址+端口號(hào) 能夠標(biāo)識(shí)網(wǎng)絡(luò)上的某一臺(tái)主機(jī)的某一個(gè)進(jìn)程一個(gè)端口號(hào)只能被一個(gè)進(jìn)程占用

1.1 理解端口號(hào)和進(jìn)程ID

我們之前在學(xué)習(xí)進(jìn)程的時(shí)候,學(xué)習(xí)到過(guò)pid是用來(lái)標(biāo)識(shí)進(jìn)程的,那么此處的端口號(hào)也是唯一表示一個(gè)進(jìn)程的。那么這兩者又有什么關(guān)系呢?

端口號(hào)是個(gè)數(shù)字,標(biāo)定進(jìn)程唯一性,更加是一種證明,證明這個(gè)進(jìn)程要進(jìn)行網(wǎng)絡(luò)通信,沒有端口號(hào),這個(gè)進(jìn)程可能只是本地進(jìn)程。因此端口號(hào)和進(jìn)程ID的差別就是這樣。

因此一個(gè)進(jìn)程可以綁定多個(gè)端口號(hào),但是一個(gè)端口號(hào)只能綁定一個(gè)進(jìn)程!!

1.2 理解源端口號(hào)和目的端口號(hào)

源端口號(hào)和目的端口號(hào)是傳輸層協(xié)議(tcp和udp)的數(shù)據(jù)段中有兩個(gè)端口號(hào),就是在描述“數(shù)據(jù)是誰(shuí)發(fā)的,要發(fā)給誰(shuí)”。

2.認(rèn)識(shí)TCP協(xié)議

此處我們先對(duì)TCP(Transmission Control Protocolc 傳輸控制協(xié)議)有一個(gè)直觀的認(rèn)識(shí),后面我們?cè)僭敿?xì)討論TCP的一些細(xì)節(jié)

傳輸層協(xié)議有連接可靠傳輸面向字節(jié)流

有連接:類似于打電話,兩者要建立連接

可靠傳輸:仍然是打電話,我說(shuō)的話你都聽到了,你都收到了。

3.認(rèn)識(shí)UDP協(xié)議

UDP(User Datagram Protocol 用戶數(shù)據(jù)報(bào)協(xié)議)

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

無(wú)連接:類似于發(fā)郵箱,不管你在不在,我都可以給你發(fā)郵件。

不可靠傳輸:仍然是發(fā)郵件,我發(fā)送給你,你看不看是你的事情。

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

我們已經(jīng)知道,內(nèi)存中的多字節(jié)數(shù)據(jù)相對(duì)于內(nèi)存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對(duì)于文件中的偏移地址也有大端小端之分,網(wǎng)絡(luò)數(shù)據(jù)流同樣有大端小端之分。那么如何定義網(wǎng)絡(luò)數(shù)據(jù)流的地址呢?

發(fā)送主機(jī)通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出接收主機(jī)把從網(wǎng)絡(luò)上接到的字節(jié)一次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存因此,網(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é)序來(lái)發(fā)送/接受數(shù)據(jù)如果當(dāng)前發(fā)送主機(jī)是小端,就需要先將數(shù)據(jù)轉(zhuǎn)成大端,否則就忽略,直接發(fā)送即可。

主機(jī)序列轉(zhuǎn)網(wǎng)絡(luò)序列<-->網(wǎng)絡(luò)序列轉(zhuǎn)主機(jī)序列

記住一個(gè)不變的準(zhǔn)則,網(wǎng)絡(luò)序列一定是大端的,再根據(jù)你本地主機(jī)的字節(jié)序選擇轉(zhuǎn)或者不轉(zhuǎn)。

為使網(wǎng)絡(luò)程序具有可移植性,是同樣的C代碼在大端和小端計(jì)算機(jī)上編譯后都能正常運(yùn)行,可調(diào)用以庫(kù)函數(shù)做網(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(uint32_t netshort);
這些函數(shù)名很好記,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é)序,例如將IP地址轉(zhuǎn)換后準(zhǔn)備發(fā)送如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回如果主機(jī)是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回

5.socket編程接口

5.1socket常見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);

// 開始監(jiān)聽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);

我們發(fā)現(xiàn)很多函數(shù)都有一個(gè)sockaddr結(jié)構(gòu),那么這是一個(gè)什么結(jié)構(gòu)呢?

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

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

因此我們只要拿到sockaddr結(jié)構(gòu)我們強(qiáng)轉(zhuǎn)成sockaddr類型,讀取前16位,如果是AF_INET則是跨網(wǎng)絡(luò)通信,如果是AF_UNIX則是域間通信。因此我們這里都是網(wǎng)絡(luò)通信,因此前16位都是AF_INET.

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)轉(zhuǎn)換成sockaddr_in;這樣的好處是程序的通用性,可以接受IPv4,IPv6,以及UNIX Domain Socket各種類型的sockaddr結(jié)構(gòu)體指針作為參數(shù)。

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

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

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

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

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

6.簡(jiǎn)單的UDP網(wǎng)絡(luò)程序

上面的準(zhǔn)備工作結(jié)束,我們可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的英譯漢的功能

6.1創(chuàng)建套接字

int socket(int domain,int type,int protocol);

返回值:如果成功,返回一個(gè)新的文件描述符。失敗返回-1。

6.2 綁定網(wǎng)絡(luò)信息 指明IP+prot

6.3 udpServe代碼

6.3.1 udpServe通用服務(wù)器

#include #include #include #include #include #include #include #include #include #include #include "Log.hpp"static void Usage(const std::string proc){    std::cout<<"Usage:\n\t"<4字節(jié)IP->uint32_t ip        // INADDR_ANY(0) 程序員不關(guān)心會(huì)bind到哪一個(gè)ip,任意地址bind,強(qiáng)烈推薦的做法        // inet_addr:指定填充確定的ip,特殊用途,測(cè)試時(shí)使用;除了做轉(zhuǎn)換,還會(huì)自動(dòng)給我做序列轉(zhuǎn)換        local.sin_addr.s_addr = ip_.empty()?htonl(INADDR_ANY):inet_addr(ip_.c_str());        // 2.2 bind網(wǎng)絡(luò)信息        if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local)) == -1)        {            logMessage(FATAL,"bind:%s:%d",strerror(errno),sockfd_);            exit(2);        }        logMessage(DEBUG,"socket bind success : %d",sockfd_);        // done     }    void start()    {        //服務(wù)器設(shè)計(jì)的時(shí)候都是死循環(huán)        //將來(lái)讀取到的數(shù)據(jù)都放在這里        char inbuffer[1024];        //將來(lái)發(fā)送的數(shù)據(jù)都放在這里        char outbuffer[1024];        while(true)        {            // struct sockaddr_in peer;//遠(yuǎn)端 輸出型參數(shù)            // socklen_t len = sizeof(peer);//輸入輸出型參數(shù)            // //demo 2            // //后面的兩個(gè)參數(shù)是輸出型參數(shù),誰(shuí)給你發(fā)的消息 和客戶端多長(zhǎng)            // ssize_t s = recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,            //     (struct sockaddr*)&peer,&len);                        logMessage(NOTICE,"serve 提供 service中...");            sleep(1);        }    }private:    //服務(wù)器端口號(hào)信息    uint16_t port_;    //服務(wù)器必須得有ip地址    std::string ip_;    int sockfd_;//套接字信息};// ./udpSever port [ip]int main(int argc,char* argv[]){    if(argc != 2 && argc != 3)    {        Usage(argv[0]);        exit(3);    }      uint16_t port = atoi(argv[1]);    std::string ip;    if(argc == 3)    {        ip = argv[2];    }    UdpServer svr(port,ip);    svr.init();    svr.start();    return 0;}

我們?cè)谟弥噶钜部梢圆榭串?dāng)前的服務(wù)信息

netstat -lnup

第一列為服務(wù)類型(Proto):當(dāng)前為udp第二列表示有沒有收到消息(Recv-Q):0表示沒有第三列表示有沒有發(fā)送消息(Send-Q):0表示沒有第四列表示本地綁定的ip地址:為全0表示任意地址綁定,綁定的端口號(hào)是8080Foreign Address :表示允許遠(yuǎn)端的任何主機(jī)任何端口給我發(fā)消息

注意:云服務(wù)器不能顯示的寫端口號(hào),因?yàn)樵品?wù)器不允許你綁定云服務(wù)器公網(wǎng)IP

因此我們一旦實(shí)現(xiàn)綁定云服務(wù)器ip會(huì)報(bào)錯(cuò)。我們直接不寫讓他任意綁定即可

6.3.2 實(shí)現(xiàn)簡(jiǎn)單通信的服務(wù)器

我們完成服務(wù)器編寫的基本框架后,我們想要實(shí)現(xiàn)一個(gè)可以將客戶端傳入的字符進(jìn)行大小寫轉(zhuǎn)換的服務(wù)。具體服務(wù)如下:

void start()    {        //服務(wù)器設(shè)計(jì)的時(shí)候都是死循環(huán)        //將來(lái)讀取到的數(shù)據(jù)都放在這里        char inbuffer[1024];        //將來(lái)發(fā)送的數(shù)據(jù)都放在這里        char outbuffer[1024];        while(true)        {            struct sockaddr_in peer;//遠(yuǎn)端 輸出型參數(shù)            socklen_t len = sizeof(peer);//輸入輸出型參數(shù)            //demo 2            //后面的兩個(gè)參數(shù)是輸出型參數(shù),誰(shuí)給你發(fā)的消息 和客戶端多長(zhǎng)            ssize_t s = recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,                (struct sockaddr*)&peer,&len);            if(s>0)            {                inbuffer[s] = 0;//當(dāng)做字符串            }            else if(s == -1)            {                logMessage(WARINING,"recvfrom:%s:%d",strerror(errno),sockfd_);                continue;//服務(wù)器不能退出            }            //走到這里一定成功了 除了讀取到對(duì)方的數(shù)據(jù) 還要讀取對(duì)方的網(wǎng)絡(luò)地址[ip:port]            std::string peerIp = inet_ntoa(peer.sin_addr);//拿到了對(duì)方的ip            uint32_t peerPort = ntohs(peer.sin_port);//拿到了對(duì)方的port            //打印出來(lái)客戶端給服務(wù)器發(fā)送過(guò)來(lái)的消息            logMessage(NOTICE,"[%s:%d]]# %s",peerIp.c_str(),peerPort,inbuffer);            // logMessage(NOTICE,"serve 提供 service中...");            // sleep(1);        }    }

6.4 udpClient代碼

我們服務(wù)器寫完之后,現(xiàn)在來(lái)編寫客戶端的代碼

客戶端在編寫的時(shí)候不需要bind,為什么?

所謂的"不需要"指的是:不需要用戶自己bind端口信息 因?yàn)镺S會(huì)自動(dòng)給你綁定,如果我們非要自己bind 可以 但是不推薦!!!

所有的客戶端軟件<- ->服務(wù)器 通信的時(shí)候 必須得有client[ip:port] <-->serve[ip:port]。為什么?

因?yàn)閏lient很多,不能給客戶bind指定的port,因?yàn)閜ort可能被別的client使用了. 你的client就無(wú)法啟動(dòng)了

那么Server憑什么要bind呢?

server提供的服務(wù),必須被所有人都知道!而且server不能隨便改變!

#include #include #include #include #include #include #include #include #include #include #include static void Usage(std::string name){    std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;}// udpClient server_ip server_port//  如果一個(gè)客戶端要連接server必須知道server對(duì)應(yīng)的ip和portint main(int argc, char *argv[]){    if (argc != 3)    {        Usage(argv[0]);        exit(1);    }    // 1.根據(jù)命令行 設(shè)置要訪問(wèn)的服務(wù)器IP    std::string server_ip = argv[1];    uint16_t server_port = atoi(argv[2]);    // 2.創(chuàng)建客戶端    //  2.1 創(chuàng)建socket    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);    assert(sockfd > 0);    // 2.2 client 不需要bind    // 所謂的"不需要" 指的是:不需要用戶自己bind端口信息 因?yàn)镺S會(huì)自動(dòng)給你綁定    // 如果我們非要自己bind 可以 但是不推薦    // 所有的客戶端軟件<- ->服務(wù)器 通信的時(shí)候 必須得有client[ip:port] <-->serve[ip:port]    // 為什么?client很多,不能給客戶bind指定的port,因?yàn)閜ort可能被別的client使用了    // 你的client就無(wú)法啟動(dòng)了    // 2.2 填寫服務(wù)器信息    struct sockaddr_in server;    bzero(&server, sizeof server);    server.sin_family = AF_INET;    server.sin_port = htons(server_port);    server.sin_addr.s_addr = inet_addr(server_ip.c_str());    // 3.通信過(guò)程    std::string buffer;    while (true)    {        std::cout << "Please Enter# ";        std::getline(std::cin, buffer);        // 發(fā)送消息給sever        // 首次調(diào)用sendto函數(shù)的時(shí)候 我們的client        sendto(sockfd, buffer.c_str(), buffer.size(), 0,               (const struct sockaddr *)&server, sizeof(server));    }    return 0;}

6.5本地測(cè)試

其中127.0.0.1 是本地?fù)Q回的端口號(hào),也就是本主機(jī)

當(dāng)然如果大家有多臺(tái)主機(jī)就可以跨網(wǎng)絡(luò)傳輸通信啦!

這是我朋友鏈接我的主機(jī)給我發(fā)送的消息(跨主機(jī)網(wǎng)絡(luò)通信)

注意:如果是云服務(wù)器的話,大家一定要手動(dòng)開端口號(hào),否則是不能使用指定端口號(hào)的

(本篇完)

標(biāo)簽: 網(wǎng)絡(luò)通信 進(jìn)程間通信 文件描述

上一篇:焦點(diǎn)觀察:vue切換中英語(yǔ)言制作方法(Element+i18n的使用)
下一篇:前端線上圖片生成馬賽克