目錄
前言:
一、服務端的實現
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
等網絡結構體前先進行清零(memset
或bzero
):
我們這里使用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)接收數據的系統調用,它不僅能獲取數據,還能獲取發送方的地址信息。
參數 | 類型 | 說明 |
---|---|---|
sockfd | int | 套接字文件描述符 |
buf | void* | 接收數據的緩沖區 |
len | size_t | 緩沖區長度 |
flags | int | 控制接收行為的標志位 |
src_addr | struct sockaddr* | 發送方地址信息(可選) |
addrlen | socklen_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)發送數據的系統調用,它允許指定目標地址。
參數 | 類型 | 說明 |
---|---|---|
sockfd | int | 套接字文件描述符 |
buf | const void* | 要發送的數據緩沖區 |
len | size_t | 要發送的數據長度 |
flags | int | 控制發送行為的標志位 |
dest_addr | const struct sockaddr* | 目標地址信息 |
addrlen | socklen_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);}}}}
?
總結:
希望本文對你有所幫助