Linux網絡編程:UDP 的echo server

目錄

前言:

一、服務端的實現

1、創建socket套接字

2、綁定地址信息

3、執行啟動程序

二、用戶端的實現

總結:


前言:

大家好啊,前面我們介紹了一些在網絡編程中的一些基本的概念知識。

今天我們就借著上節課提到的,傳輸層的UDP協議,來寫一個簡單的服務端與用戶段的通信。

由于我們還有很多知識沒有學習,所以這個板塊的目的是為了讓大家先看一下代碼的,接口的使用。

這種協議的使用很多情況都是重復一樣的,所以使用多了就記住了。

一、服務端的實現

1、創建socket套接字

我們一般在進行通信時,其實都是服務端與用戶端的交流。我們平時使用的微信,QQ這個app,就是用戶端,他會與遠程的服務端口進行數據的交互。如果你是對其他用戶發送的消息,就會把這個消息再傳給其他用戶。

由于我們目前所學還是比較簡陋的,所以這里我們要實現的服務端只需要要求滿足做一個echo響應就行了。因為我們沒有學具體的協議,所以這里就不對發送的消息做加工處理。

首先,我們需要先定義好我們的一個服務端的頭文件,這個還是很基礎的,然后聲明我們的命名空間,定義一個服務端的類。因為我們之后會使用日志來進行打印,所以我們就先把日志也給包含進來:

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include "log.hpp"using namespace LogModule;namespace UdpServerModule
{class UdpServer{public:UdpServer(){}~UdpServer(){}private:};
}#endif

在去試著寫一下我們的Main.cc啟動文件。

#include"UdpServer.hpp"using namespace UdpServerModule;
int main()
{std::unique_ptr<UdpServer> svr_ptr=std::make_unique<UdpServer>();//我們先創建一個服務器對象,并用智能指針管理它//那我們是不是要先初始化一下我們的服務器對象呢?svr_ptr->InitServer(); //假設UdpServer類有一個InitServer方法來初始化服務器//初始化好了,我們是不是應該啟動我們的服務端。由于服務端一般都是啟動了不會停止的,所以我們可以使用while循環svr_ptr->Start();
}

這里我們智能指針對應的頭文件加入之后,我們提出了兩個概念,初始化服務段與啟動我們的服務端。

毫無疑問,這是需要我們在UdpServer.hpp中實現的成員方法。

我們繼續來寫hpp。首先,我們如何初始化我們的服務端口。就需要先創建我們的socket套接字。

創建 Socket 是網絡通信的第一步,因為它是操作系統提供的網絡通信端點,負責數據的發送和接收。

這里我們需要用到socket接口:

這個接口的第一個參數是domin,是協議族的意思,決定了 Socket 使用的底層協議和地址格式。

我們一般情況下都填的AF_INET表示IPv4協議族。

?第二個參數是通信類型的意思:

可以看到這連個通信類型剛好符合我們上一篇文章所說的UDP與TCP協議的特點。我們這里使用UDP,所以選擇填SOCK_DGRAM。

第三個參數一般不用管,我們默認填0就行。

所以我們就要創建一個socket套接字在我們的服務端初始化時,這個socket的返回值是一個文件描述符,所以本質上其實就像是在給我們創建一個文件。(記得加上所需的頭文件:

?#include <sys/socket.h>

?#include<sys/types.h>

由于這個文件描述符我們肯定是后面經常用到的,所以我們這里可以寫一個成員變量_socket來記錄該文件描述符,并且可以通過這個的值判斷是否申請套接字成功。(記得修改一下我們的構造函數)

如果小于0,代表創建失敗,我們此時可以通過日志來打印并讓服務端執行Die:

LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);
// 如果socket創建失敗,我們記錄一條FATAL級別的日志,并返回。
Die(1);
#define Die(code) do{exit(code);}while(0)

?否則我們執行LOG打印成功

 LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;

2、綁定地址信息

當我們創建好一個socket之后,我們必須要把他與我們的地址信息做綁定。?

Socket通信本質上是端點對端點的通信,一個完整的通信端點需要兩個要素:

  • 傳輸協議(由socket()函數指定)

  • 地址標識(由bind()函數指定)

如果不綁定地址,Socket就相當于只有"通話功能"但沒有"電話號碼",其他主機無法定向發送數據。?

所以我們要介紹一下我們的綁定接口:bind:

可以看出,bind的第一個參數就是我們使用socket返回的文件描述符,他的第二個參數,大家看著是否眼熟呢?

這正是我們在上篇文章所提到的:

這個是包含地址信息的結構體指針,

  • IPv4 使用?struct sockaddr_in

  • IPv6 使用?struct sockaddr_in6

  • 本地 Unix 域套接字使用?struct sockaddr_un

我們這里自然使用的是struct sockaddr_in,我們可以看一下這個結構體的定義:

struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port;			/* Port number.  */struct in_addr sin_addr;		/* Internet address.  *//* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};

根據上面那個圖我們可以知道,由上到下分別一一對應,有人說?__SOCKADDR_COMMON (sin_);是什么意思,我們可以繼續看這個宏的定義:

#define	__SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family

所以轉化過來就是sa_family_t sin_family;,這個就代表著我們的地址族。

繼續來講第三個參數,第三個參數就是第二個參數的大小,我們可以通過sizeof來獲取。

這里值得一提的是,第二個參數是一個輸入型參數,所以我們需要先定義一個sockaddr_in結構體,隨后對這個結構體進行初始化填充,把我們的IP地址,端口號這些信息全部填充到這個結構體內。

但是這里有一個問題:

            // 1.創建一個socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 這里我們使用了C標準庫的socket函數來創建一個UDP socket// 注意:在實際的代碼中,我們需要檢查socket函數的返回值,以確保// socket創建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket創建失敗,我們記錄一條FATAL級別的日志,并返回。Die(1);}LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.綁定地址信息struct sockaddr_in local;local.sin_family = AF_INET; // 設置地址族為IPv4local.sin_port = ;local.sin_addr=;

我們的端口號和IP地址從哪里來?

我們這里可以定義兩個默認的參數,代表默認的IP與端口地址,如果不想使用默認的,就要求你從外界傳入IP與端口:

static std::string defaultip = "127.0.0.1"; // 默認的IP地址,代表本地IP地址
static uint16_t defaultport = 8080;         // 默認的端口號,用來測試

我們順便在我們的服務端類中新增兩個變量來記錄IP地址與該服務端綁定的端口:
?

         UdpServer(const std::string& ip=defaultip,const uint16_t port=defaultport): _sockfd(defaultfd),_ip(ip),_port(port){}
      private:int _sockfd;std::string _ip ;// 默認IP地址uint16_t _port ; // 默認端口號

所以此時我們就給我們的sockaddr_in結構體中的成員初始化這個值:

            // 2.綁定地址信息struct sockaddr_in local;local.sin_family = AF_INET; // 設置地址族為IPv4local.sin_port = _port;// 設置端口號local.sin_addr = _ip;

這里有兩個問題,我們先說第一個,首先你的_port是要發送到網絡中的,協議規定端口號在報文中必須用網絡字節序(大端)傳輸。所以你這個時候必須進行大小端轉化,將主機序列轉化為網絡序列。

如何轉化呢?

我們在上文提到過:

所以這里我們采取htons來進行轉化。

但是我們還有一個新的問題,初始化ip地址時報錯了,這是為什么呢?

他說類型不匹配,我們查看一下sockaddr_in結構體內部:

發現sin_addr居然是一個結構體,而C語言的結構體不支持賦值。

我們繼續查看該結構體in_addr的內容:

struct in_addr{in_addr_t s_addr;};

發現里面就一個成員變量,所以我們只需要把這個成員變量顯式初始化就行了。但還是有個問題,我們的IP地址時點分十進制的啊!!所以我們需要對這個ip地址進行轉化。

如何將人類可讀的點分十進制IP地址(如?"127.0.0.1")轉換為網絡字節序的二進制形式,并正確賦值給?sockaddr_in?結構體?

我們有這些接口可以使用:

大家有興趣的可以去查一下,為了簡便我們這里就使用inet_addr?,這個接口,用于將點分十進制格式的 IP 地址字符串轉換為 32 位網絡字節序的二進制值。

這里還有一個究極細節,為了防止結構體填充與未初始化風險,我們在設置sockaddr_in等網絡結構體前先進行清零(memsetbzero):

我們這里使用bzero來完成清零的操作:

bzero(&local, sizeof(local)); // 清空結構體

完成對sockaddr_in結構體的設置后,我們就可以使用bind函數來綁定地址了:

bind(_sockfd, (struct sockaddr *)&local, sizeof(local));

?我們可以在下面進行一個if判斷,并對結果進行相應的日志輸出:

            int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(2);}LOG(LogLevel::INFO) << "bind success "; 

所以我們服務端目前的代碼如下,大家可以借助注釋理解:

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include "log.hpp"
#include <string.h>
#include <string>
#include <memory>#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace LogModule;#define Die(code)   \do              \{               \exit(code); \} while (0)static int defaultfd = -1;
static std::string defaultip = "127.0.0.1"; // 默認的IP地址,代表本地IP地址
static uint16_t defaultport = 8080;         // 默認的端口號,用來測試namespace UdpServerModule
{class UdpServer{public:UdpServer(const std::string &ip = defaultip, const uint16_t port = defaultport): _sockfd(defaultfd),_ip(ip),_port(port){}~UdpServer(){}void InitServer(){// 1.創建一個socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 這里我們使用了C標準庫的socket函數來創建一個UDP socket// 注意:在實際的代碼中,我們需要檢查socket函數的返回值,以確保// socket創建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket創建失敗,我們記錄一條FATAL級別的日志,并返回。Die(1);}LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.綁定地址信息struct sockaddr_in local;bzero(&local, sizeof(local)); // 清空結構體// 這里我們使用了bzero函數來清空local結構體,確保沒有殘留數據,垃圾值local.sin_family = AF_INET;                     // 設置地址族為IPv4local.sin_port = htons(_port);                         // 設置端口號local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 將IP地址轉換為網絡字節序int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(2);}LOG(LogLevel::INFO) << "bind success "; }void Start(){}private:int _sockfd;std::string _ip; // 默認IP地址uint16_t _port;  // 默認端口號};
}#endif
#include"UdpServer.hpp"using namespace UdpServerModule;
int main()
{std::unique_ptr<UdpServer> svr_ptr=std::make_unique<UdpServer>();//我們先創建一個服務器對象,并用智能指針管理它//那我們是不是要先初始化一下我們的服務器對象呢?svr_ptr->InitServer(); //假設UdpServer類有一個InitServer方法來初始化服務器//初始化好了,我們是不是應該啟動我們的服務端。由于服務端一般都是啟動了不會停止的,所以我們可以使用while循環svr_ptr->Start();
}


3、執行啟動程序

目前為止,我們的初始化工作的代碼就已經完成了。

目前為止的這些代碼都是套路,我們這里就先看一下,把寫出來。后面我們講網絡原理這些會懂。

接下來就來實現一下我們的start啟動功能。

首先,服務端一般都是啟動之后就不會停止的,就像抖音一樣,你晚上可以使用,白天也能使用。

所以我們這里就可以使用while循環,由于我們今天的目標只是實現一個簡單的echo server,所以我們只需要在服務端接收到用戶端的消息,隨后打印消息結果,并返回就行了。

在寫while循環前我們應該再增加一個成員變量:is_running,表示目前服務器的運行狀態。一開始默認為false,在我們執行start后變為true。

        void Start(){is_running = true;while(is_running){}}

我們要接受從用戶端發來的消息,應該如何接收呢?

這就要拜托給我們的recvfrom了,recvfrom()?是用于無連接套接字(如 UDP)接收數據的系統調用,它不僅能獲取數據,還能獲取發送方的地址信息。

參數類型說明
sockfdint套接字文件描述符
bufvoid*接收數據的緩沖區
lensize_t緩沖區長度
flagsint控制接收行為的標志位
src_addrstruct sockaddr*發送方地址信息(可選)
addrlensocklen_t*地址結構體長度指針

?所以為了接收消息,我們需要先自己定義一個緩沖區,以及存儲我們發送方地址信息的結構體和長度。這個flag我們使用默認的0就行。

注意最后兩個參數是一個輸出型參數。

        void Start(){is_running = true;while(is_running){char buffer[1024];struct sockaddr_in peer;//輸出型參數socklen_t len =sizeof(peer);//也是一個輸出型參數ssize_t n=::recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);if(n>0){buffer[n] = '\0'; // 確保字符串以null結尾LOG(LogLevel::INFO) << "client say: " << buffer;}}}

我們這里的sockfd既可以用來發消息,也可以用來收消息,這就是全雙工的特性。

所以我們還可以在后面添加一個返回消息的邏輯,這里的返回消息我們用的是?sendto,sendto()?是用于無連接套接字(如 UDP)發送數據的系統調用,它允許指定目標地址。

參數類型說明
sockfdint套接字文件描述符
bufconst void*要發送的數據緩沖區
lensize_t要發送的數據長度
flagsint控制發送行為的標志位
dest_addrconst struct sockaddr*目標地址信息
addrlensocklen_t地址結構體長度

在sendto函數中,后面兩個參數是我們要發送的目標的地址信息與參數,而這個參數,我們在使用recvrom的時候就已經獲取到peer里了。

        void Start(){is_running = true;while (is_running){char buffer[1024];struct sockaddr_in peer;      // 輸出型參數socklen_t len = sizeof(peer); // 也是一個輸出型參數ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = '\0'; // 確保字符串以null結尾LOG(LogLevel::INFO) << "client say: " << buffer;std::string echo_str = "echo:"; // 我們要給客戶端回顯一條消息echo_str += buffer;// 發送回顯消息ssize_t m = ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);}}}}

那么到目前為止,我們的服務端接口就簡單的寫完了。

我們可以運行一下服務端的接口,隨后在新的shell中輸入以下指令:

netstat -nuap

可以看見我們的服務端進程已經啟動起來了,并且地址就是我們一開始設置的127.0.0.8080.


二、用戶端的實現

我們的服務端已經建立起來了,接下來我們要實現的就是用戶端的代碼。

其實,二者的代碼極具相似度,由于時間原因我選擇直接復制粘貼一下代碼(這也就是為什么我說那些代碼都是套路的原因,使用方法順序幾乎一致)

值得一提的是,用戶端的類我們要求初始化時必須有的目的地的IP地址與端口號(也就是服務端)

隨后我們新增一個類成員變量_server負責記錄我們會使用的sockaddr_in結構體數據,并且,在InitClient中我們需要對sockaddr_in進行初始化,這是為了方便我們后面的使用。

最后去除我們InitClient中的bind相關的代碼,這是為什么呢?

這是因為客戶端不需要自己顯示的調用bind!!

客戶端首次sendto消息的時候,由OS自動進行bind!!此時操作系統會隨機分配一個空閑的端口號!

我們在start中的改變還是比較多的,首先我們需要讓客戶端先輸入消息,也就是可以創建一個string字符串,使用getline接受消息,并且通過sendto發送到目的IP與端口,這里所使用的sockaddr_in結構體正是我們的成員變量_server。

之后,由于服務端調用了sendto,所以我們可以在后面進行recvfrom的使用。

代碼整體如下,大家可以看注釋幫助理解:

#ifndef __UDP_CLIENT_HPP__
#define __UDP_CLIENT_HPP__#include "log.hpp"
#include <string.h>
#include <string>
#include <memory>#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace LogModule;#define Die(code)   \do              \{               \exit(code); \} while (0)static int defaultfd = -1;namespace UdpClientModule
{class UdpClient{public:UdpClient(const std::string &ip, const uint16_t port): _sockfd(defaultfd),_ip(ip),_port(port){}~UdpClient(){}void InitClient(){// 1.創建一個socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 這里我們使用了C標準庫的socket函數來創建一個UDP socket// 注意:在實際的代碼中,我們需要檢查socket函數的返回值,以確保// socket創建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket創建失敗,我們記錄一條FATAL級別的日志,并返回。Die(3);}// 如果socket創建成功,我們記錄一條INFO級別的日志,表示socket創建成功。LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.綁定地址信息memset(&_server, 0, sizeof(_server)); // 清空結構體// 這里我們使用了bzero函數來清空local結構體,確保沒有殘留數據,垃圾值_server.sin_family = AF_INET;                     // 設置地址族為IPv4_server.sin_port = htons(_port);                  // 設置端口號_server.sin_addr.s_addr = inet_addr(_ip.c_str()); // 將IP地址轉換為網絡字節序// client必須也要有自己的ip和端口!但是客戶端,不需要自己顯示的調用bind!!// 而是,客戶端首次sendto消息的時候,由OS自動進行bind// 1. 如何理解client自動隨機bind端口號? 一個端口號,只能被一個進程bind// 2. 如何理解server要顯示的bind?服務器的端口號,必須穩定!!必須是眾所周知且不能改變輕易改變的!// 如果服務端改變,那么他所服務對接的眾多客戶端都無法正常運行}void Start(){while (true){std::cout << "Please input your message: ";std::string message;getline(std::cin, message); // 從標準輸入讀取一行消息// 發送消息ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)(&temp), &len);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}}private:int _sockfd;                // socket文件描述符std::string _ip;            // IP地址uint16_t _port;             // 端口號struct sockaddr_in _server; // 我們的類初始化時必須傳入目的地的IP與端口};
}#endif

那我們的main.cc文件如何實現呢?我們的客戶端的要求運行時必須傳入目的地的IP與端口,所以我們需要用到系統學到的知識命令行參數。

#include "UdpClient.hpp"using namespace UdpClientModule;int main(int argc, char *argv[])
{if (argc != 3) // 客戶端必須傳入我們要發送的目的地的IP和端口號{std::cout << "Usage: ./client ip port" << std::endl;return 1;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UdpClient> client = std::make_unique<UdpClient>(ip, port);client->InitClient();client->Start();return 0;
}

我們先運行服務端,隨后運行客戶端:

可以看到,我們已經能夠實現簡單的本地通信了。


三、優化

我們服務端的代碼雖然能夠正常運行了,但是我們還是覺得不夠優美。所以我們可以在優化一下。

首先就是我們的那一大串的各種轉換了。

我們想要快速的顯示IP地址主機轉網絡網絡轉主機,快速得到端口號。

該怎么做呢?

首先我們可以定義一個新的頭文件:

InetAddr.hpp,表示我們將在這個頭文件中實現一個類,這個類中必須實現我們的各種轉換,所以就會包含各種成員函數調用接口。即把源代碼中的:

這一部分給優化到一個類中。

所以我們的這個類成員變量必須包含一個sockaddr_in類型的結構體:

#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>class InetAddr
{
public:InetAddr(){}~InetAddr(){}private:struct sockaddr_in addr; // 用于存儲IP地址和端口號的結構體
};

但是我們實際上看的是點分十進制的IP地址與端口,所以我們還可以新建兩個成員變量來表示:

    std::string _ip;uint16_t _port;

我們想要在服務端收消息時知道客戶端的IP地址,所以我們重載一個構造函數,使得其支持從外部傳進來一個sockaddr_in的結構體給我們的addr初始化,并且在這里面調用相關接口,實現我們的網絡端口與IP的網絡轉主機。

這樣一來如果我們在start里的sendto之前,就可以通過struct sockaddr_in初始化的新建一個InetAddr變量,調用此構造函數對我們的IP地址以及端口進行自動化處理,隨后我們在通過一些返回調用就能在打印出來。

class InetAddr
{private:void PortNet2Host(){_port=::ntohs(_addr.sin_port);}void IpNet2Host(){char ip[64];::inet_ntop(AF_INET, &_addr.sin_addr, ip, sizeof(ip));_ip = std::string(ip);}
public:InetAddr(){}InetAddr(const struct sockaddr_in &addr):_addr(addr){PortNet2Host(); // 將網絡字節序的端口轉換為主機字節序IpNet2Host();   // 將網絡字節序的IP地址轉換為主機字節序}~InetAddr(){}private:struct sockaddr_in _addr; // 用于存儲IP地址和端口號的結構體std::string _ip;uint16_t _port;
};

不同于之前,我們在這里將IP地址轉換為主機字節序時用到的接口是:

inet_ntop

我們之前所使用的是inet_addr。

這個接口并不是很安全,因為返回值是一個char*指針,在多線程中有錯誤的風險。

但是inet_ntop就比較安全了,因為我們需要自己創建一個區域來存儲地址。在多線程中,名義上是規定了線程的棧區資源是不共享的。?

我們打印是要用到IP地址與端口,所以可以寫調用來返回:

    std::string GetIp(){return _ip;}uint16_t GetPort(){return _port;}
  void Start(){is_running = true;while (is_running){char buffer[1024];struct sockaddr_in peer;      // 輸出型參數socklen_t len = sizeof(peer); // 也是一個輸出型參數ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr temp(peer); // 通過上面的peer來進行初始化,這樣以來我們就能獲取到相關ip地址與端口并打印buffer[n] = '\0';    // 確保字符串以null結尾LOG(LogLevel::INFO) << "client ip: " << temp.GetIp() << ", port: " << temp.GetPort() << "client say: " << buffer;std::string echo_str = "echo:"; // 我們要給客戶端回顯一條消息echo_str += buffer;// 發送回顯消息ssize_t m = ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);}}}}

?這樣一來我們也能看見客戶端的IP地址了。


由于我們設定的監控任意接口,所以我們在UdpServer.hpp里不需要IP來給我們的成員變量初始化,只需要端口。

所以我們就再重載一個端口版的構造函數?

    InetAddr(const uint16_t port):_port(port),_ip(""){_addr.sin_family=AF_INET; // 設置地址族為IPv4_addr.sin_port=htons(port); // 將端口轉換為網絡字節序_addr.sin_addr.s_addr=INADDR_ANY; // 設置IP地址為任}

至此,我們只需要在服務端的成員變量中新增InetAddr類型,便可注釋掉我們之前的成員變量port與ip,這個端口版的構造函數主要是給我們的成員變量中新增InetAddr類型進行初始化使用的。

要代替的正是我們Init里面的對sockaddr_in結構體進行初始化的代碼。

        InetAddr local; // 本地地址信息// std::string _ip; // 默認IP地址// uint16_t _port;  // 默認端口號

另外由于使用bind的時候要獲取結構體地址與長度,所以我們可以新加接口在內部返回地址與長度。

?

    struct sockaddr* Getsockaddr(){return (struct sockaddr*)&_addr;}size_t GetSockaddrLen(){return sizeof(_addr);}

代碼總體如下:

#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>class InetAddr
{private:void PortNet2Host(){_port=::ntohs(_addr.sin_port);}void IpNet2Host(){char ip[64];::inet_ntop(AF_INET, &_addr.sin_addr, ip, sizeof(ip));_ip = std::string(ip);}
public:InetAddr(){}InetAddr(const struct sockaddr_in &addr):_addr(addr){PortNet2Host(); // 將網絡字節序的端口轉換為主機字節序IpNet2Host();   // 將網絡字節序的IP地址轉換為主機字節序}InetAddr(const uint16_t port):_port(port),_ip(""){_addr.sin_family=AF_INET; // 設置地址族為IPv4_addr.sin_port=htons(port); // 將端口轉換為網絡字節序_addr.sin_addr.s_addr=INADDR_ANY; // 設置IP地址為任}~InetAddr(){}struct sockaddr* Getsockaddr(){return (struct sockaddr*)&_addr;}size_t GetSockaddrLen(){return sizeof(_addr);}std::string GetIp(){return _ip;}uint16_t GetPort(){return _port;}private:struct sockaddr_in _addr; // 用于存儲IP地址和端口號的結構體std::string _ip;uint16_t _port;
};

UdpServer.hpp涉及到的更改的地方如下:

添加成員變量

  private:int _sockfd;    // socket文件描述符InetAddr local; // 本地地址信息// std::string _ip; // 默認IP地址// uint16_t _port;  // 默認端口號bool is_running; // 服務器是否在運行

?構造函數修改:
?

         UdpServer(const std::string &ip = defaultip, const uint16_t port = defaultport): _sockfd(defaultfd),local(port), // 初始化本地地址信息is_running(false){}

Init函數省略優化:

        void InitServer(){// 1.創建一個socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// 這里我們使用了C標準庫的socket函數來創建一個UDP socket// 注意:在實際的代碼中,我們需要檢查socket函數的返回值,以確保// socket創建成功。if (_sockfd < 0){LOG(LogLevel::FATAL) << "Failed to create socket" << strerror(errno);// 如果socket創建失敗,我們記錄一條FATAL級別的日志,并返回。Die(1);}// 如果socket創建成功,我們記錄一條INFO級別的日志,表示socket創建成功。LOG(LogLevel::INFO) << "Socket created successfully, sockfd: " << _sockfd;// 2.綁定地址信息// struct sockaddr_in local;// bzero(&local, sizeof(local)); // 清空結構體// // 這里我們使用了bzero函數來清空local結構體,確保沒有殘留數據,垃圾值// local.sin_family = AF_INET;    // 設置地址族為IPv4// local.sin_port = htons(_port); // 設置端口號// // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 將IP地址轉換為網絡字節序// local.sin_addr.s_addr = INADDR_ANY; // 綁定到任意IP地址,這樣服務器可以接收來自任何IP的消息int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){// 如果bind函數返回小于0,表示綁定失敗,我們記錄一條FATAL級別的日志,并返回。// 這里我們使用了strerror函數來獲取錯誤信息,并將其記錄到日志中。LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(2);}// 如果綁定成功,我們記錄一條INFO級別的日志,表示綁定成功。LOG(LogLevel::INFO) << "bind success ";}

?statr新增打印客戶端IP地址信息:
?

       void Start(){is_running = true;while (is_running){char buffer[1024];struct sockaddr_in peer;      // 輸出型參數socklen_t len = sizeof(peer); // 也是一個輸出型參數ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr temp(peer); // 通過上面的peer來進行初始化,這樣以來我們就能獲取到相關ip地址與端口并打印buffer[n] = '\0';    // 確保字符串以null結尾LOG(LogLevel::INFO) << "client ip: " << temp.GetIp() << ", port: " << temp.GetPort() << "client say: " << buffer;std::string echo_str = "echo:"; // 我們要給客戶端回顯一條消息echo_str += buffer;// 發送回顯消息ssize_t m = ::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);}}}}

?

總結:

希望本文對你有所幫助

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/91304.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/91304.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/91304.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AI+金融,如何跨越大模型和場景鴻溝?

文&#xff5c;白 鴿編&#xff5c;王一粟當AI大模型已開始走向千行百業之時&#xff0c;備受看好的金融行業&#xff0c;卻似乎陷入了落地瓶頸。打開手機銀行想查下貸款額度&#xff0c;對著屏幕說了半天&#xff0c;AI客服卻只回復 “請點擊首頁貸款按鈕”&#xff1b;客戶經…

深度解析:從零構建跨平臺對象樹管理系統(YongYong框架——QT對象樹機制的現代化替代方案)

一、技術背景與核心價值 1.1 QT對象樹的局限性 在Qt框架中&#xff0c;QObject通過對象樹機制實現了革命性的對象管理&#xff1a; #mermaid-svg-SvqKmpFjg76R02oL {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Sv…

力扣46:全排列

力扣46:全排列題目思路代碼題目 給定一個不含重復數字的數組 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意順序 返回答案。 思路 看到所有可能首先想到的就是回溯。 回溯的結束條件也很好寫&#xff0c;用數組的長度來判斷即可。這道題的難點主要是如何進行判…

mac環境配置rust

rustup 是一個命令行工具&#xff0c;用于管理 Rust 編譯器和相關工具鏈 sh 體驗AI代碼助手 代碼解讀復制代碼curl --proto ‘https’ --tlsv1.2 -sSf https://sh.rustup.rs | sh使得 Rust 的安裝在當前 shell 環境中生效 如果你使用的是 bash, zsh 或其他類似的 shell&#xf…

腳手架搭建React項目

腳手架搭建項目 1. 認識腳手架工具 1.1. 前端工程的復雜化 1.1.1. 如果只是開發幾個小的demo程序&#xff0c;那么永遠不要考慮一些復雜的問題&#xff1a; 比如目錄結構如何組織劃分&#xff1b;比如如何關鍵文件之間的相互依賴&#xff1b;比如管理第三方模塊的依賴&#xff…

Golang 調試技巧:在 Goland 中查看 Beego 控制器接收的前端字段參數

&#x1f41b; Golang 調試技巧&#xff1a;在 Goland 中查看 Beego 控制器接收的前端字段參數 在使用 Beego 開發 Web 項目時&#xff0c;我們常常會在控制器中通過 c.GetString()、c.GetInt() 等方法獲取前端頁面傳過來的字段值。而在調試過程中&#xff0c;如何在 Goland 中…

sqli-labs:Less-2關卡詳細解析

1. 思路&#x1f680; 本關的SQL語句為&#xff1a; $sql"SELECT * FROM users WHERE id$id LIMIT 0,1";注入類型&#xff1a;數值型提示&#xff1a;參數id無需考慮閉合問題&#xff0c;相對簡單 2. 手工注入步驟&#x1f3af; 我的地址欄是&#xff1a;http://l…

TRAE 軟件使用攻略

摘要TRAE 是一款集成了人工智能技術的開發工具&#xff0c;旨在為開發者提供高效、智能的編程體驗。它包括三個主要組件&#xff1a;TRAE IDE、TRAE SOLO 和 TRAE 插件。無論是編程新手還是經驗豐富的開發者&#xff0c;都可以通過 TRAE 提高工作效率和代碼質量。標題一&#x…

將開發的軟件安裝到手機:環境配置、android studio設置、命令行操作

將開發的軟件安裝到手機環境配置android studio4.1.2安裝命令行操作環境配置 注意&#xff1a;所有的工具的版本都需要根據當下自己的軟件需要的。 Node&#xff1a;14.16.0 &#xff08;如果安裝了npm&#xff0c;可以使用npm進行當前使用node版本的更改&#xff09; &#x…

Jmeter 命令行壓測、HTML 報告、Jenkins 配置目錄

Jmeter 命令行壓測 & 生成 HTML 測試報告 通常 Jmeter 的 GUI 模式僅用于調試&#xff0c;在實際的壓測項目中&#xff0c;為了讓壓測機有更好的性能&#xff0c;多用 Jmeter 命令行來進行壓測。 官方&#xff1a;Jmeter 最佳實踐 同時&#xff0c;JMeter 也支持生成 HT…

記錄幾個SystemVerilog的語法——覆蓋率

1. 前言 通常說的覆蓋率有兩種類型&#xff1a;code coverage(代碼覆蓋率)和functional coverage(功能覆蓋率)。代碼覆蓋率是使用EDA工具自動從設計代碼里提取出來的&#xff0c;功能覆蓋率是用戶指定的&#xff0c;用于衡量測試設計意圖和功能進展。因此&#xff0c;功能覆蓋…

深度學習基礎—2

第一章、參數初始化 我們在構建網絡之后&#xff0c;網絡中的參數是需要初始化的。我們需要初始化的參數主要有權重和偏置&#xff0c;偏重一般初始化為 0 即可&#xff0c;而對權重的初始化則會更加重要&#xff0c;我們介紹在 PyTorch 中為神經網絡進行初始化的方法。 1.1 常…

PyTorch深度學習快速入門學習總結(三)

現有網絡模型的使用與調整 VGG — Torchvision 0.22 documentation VGG 模型是由牛津大學牛津大學&#xff08;Oxford University&#xff09;的 Visual Geometry Group 于 2014 年提出的卷積神經網絡模型&#xff0c;在 ImageNet 圖像分類挑戰賽中表現優異&#xff0c;以其簡…

是否需要買一個fpga開發板?

糾結要不要買個 FPGA 開發板&#xff1f;真心建議搞一塊&#xff0c;尤其是想在數字電路、嵌入式領域扎根的同學&#xff0c;這玩意兒可不是可有可無的擺設。入門級的選擇不少&#xff0c;全新的像 Cyclone IV、Artix 7 系列&#xff0c;幾百塊就能拿下&#xff0c;要是去二手平…

【模型細節】MHSA:多頭自注意力 (Multi-head Self Attention) 詳細解釋,使用 PyTorch代碼示例說明

MHSA:使用 PyTorch 實現的多頭自注意力 (Multi-head Self Attention) 代碼示例&#xff0c;包含詳細注釋說明&#xff1a;線性投影 通過三個線性層分別生成查詢(Q)、鍵(K)、值(V)矩陣&#xff1a; QWq?x,KWk?x,VWv?xQ W_qx, \quad K W_kx, \quad V W_vxQWq??x,KWk??x…

PGSQL運維優化:提升vacuum執行時間觀測能力

本文是 IvorySQL 2025 生態大會暨 PostgreSQL 高峰論壇上的演講內容&#xff0c;作者&#xff1a;NKYoung。 6 月底濟南召開的 HOW2025 IvorySQL 生態大會上&#xff0c;我在內核論壇分享了“提升 vacuum 時間觀測能力”的主題&#xff0c;提出了新增統計信息的方法&#xff0c…

神奇的數據跳變

目的 上周遇上了一個非常奇怪的問題,就是軟件的數據在跳變,本來數據應該是158吧,數據一會變成10,一會又變成158,數據在不斷地跳變,那是怎么回事?? 這個問題非常非常的神奇,讓人感覺太不可思議了。 這是這段時間,我遇上的最神奇的事了,沒有之一,最神奇的事,下面…

【跨國數倉遷移最佳實踐3】資源消耗減少50%!解析跨國數倉遷移至MaxCompute背后的性能優化技術

本系列文章將圍繞東南亞頭部科技集團的真實遷移歷程展開&#xff0c;逐步拆解 BigQuery 遷移至 MaxCompute 過程中的關鍵挑戰與技術創新。本篇為第3篇&#xff0c;解析跨國數倉遷移背后的性能優化技術。注&#xff1a;客戶背景為東南亞頭部科技集團&#xff0c;文中用 GoTerra …

【MySQL集群架構與實踐3】使用Dcoker實現讀寫分離

目錄 一. 在Docker中安裝ShardingSphere 二 實踐&#xff1a;讀寫分離 2.1 應用場景 2.2 架構圖 2.3 服務器規劃 2.4 啟動數據庫服務器 2.5. 配置讀寫分離 2.6 日志配置 2.7 重啟ShardingSphere 2.8 測試 2.9. 負載均衡 2.9.1. 隨機負載均衡算法示例 2.9.2. 輪詢負…