根據本系列上兩篇關于網絡的初識介紹,現在我們開始實現一個UDP接口,以加強對該接口的理解。
1 . 服務器端?
在本篇中,主要按照下面內容來實現:
創建并封裝服務端:了解創建服務端的基本步驟
創建并封裝客戶端,測試客戶端和服務端通信:了解創建客戶端的基本步驟和二者通信
測試云服務器與本地進行通信:從本地通信到真正實現網絡通信
根據上面的內容,本次設計的服務器功能就是接受客戶端發送的信息并向客戶端返回服務端收到的信息
老規矩,先設計整體的makefile,目的是生成兩個可執行文件。
CC=g++
LDFLAGE=-o
FLAGE=-std=c++17 -lpthreadServer_src=UDP_Server.cc
Client_src=UDP_Client.cc# SRC=UDP_Client.cc UDP_Server.cc
# OBJ=$(SRC:.cc=.o)Server=UDP_Server
Client=UDP_Client.PHONY:all
all:$(Server) $(Client)$(Server):$(Server_src)@$(CC) $^ $(LDFLAGE) $@ $(FLAGE)$(Client):$(Client_src)@$(CC) $^ $(LDFLAGE) $@ $(FLAGE)@echo "compilation success".PHONY:clean
clean:@rm -rf $(Server) $(Client)@echo "clean done"
?然后搭建Server端的框架:
服務器端首先肯定需要被初始化,然后再永不停息的start起來(預判里面可能會使用while(1)的死循環)
1.1 socket sockaddr?
什么是socket套接字:
套接字是通信的端點,它是一種用于在網絡上進程間通信的機制。可以把它想象成一個管道或者接口,應用程序通過這個管道可以發送和接收網絡數據。
套接字是操作系統提供的一種抽象概念,它屏蔽了底層網絡通信的復雜細節,使得程序員可以方便地進行網絡編程。例如,當我們在瀏覽器中訪問一個網頁時,瀏覽器和網頁服務器之間就通過套接字進行數據傳輸,包括請求網頁內容和返回網頁數據等操作。
現在就要正式開始對服務器的相關信息、接口進行設置。首先需要創建socket文件描述,可以使用
socket
接口,該接口原型如下:int socket(int domain, int type, int protocol);
首先來學習socket接口及其參數,先把兩個頭文件加上去。
????????????????
? ? ? ? 第一個參數表示這個socket套接字想選擇的域或者協議家族(domain),domain(域) 是套接字的一個重要屬性,它指定了套接字所使用的協議族(protocol family)。協議族決定了套接字能夠與哪些類型的網絡地址進行通信(不同類型的網絡地址遵循不同的協議,以下是常用的協議族:
AF_UNIX AF_LOCAL都是用于本地通信的協議族,AF_INET AF_INET是適用于網絡的協議族,前者適應的是IPv4,后者是IPv6。
第二個參數type:
比如常見的,TCP是面向字節流的傳輸方式,type選SOCK_STREAM ; UDP是數據包傳送方式,選擇SOCK_DGRAM。可以說,我們今天是在實現UDP,所以先認識了UDP的特性——事實上,是SOCK_DGRAM的特性決定了UDP的特性
根據Linux手冊描述:
SOCK_STREAM:Provides sequenced, reliable, two-way, connection-based byte streams(提供序列化的、可靠的、雙工的、面向有連接的字節流)
SOCK_DGRAM:Supports datagrams (connectionless, unreliable messages of a fixed maximum length)(支持數據包,即無連接、不可靠的固定長度信息)
?通信模式可分為:
全雙工:雙方同時收發,如同電話通話。
半雙工:雙方均可收發,但同一時間只能一方傳輸,類似對講機。
單工:數據單向傳輸,如電視臺廣播。
第三個參數
第三個參數表示指定采用的具體協議。通常傳入0表示讓系統自動選擇適合
domain
和type
參數的默認協議返回值
該接口返回值為一個新套接字的文件描述符(LINUX下一切皆文件,套接字也是一個文件),否則返回-1并設置錯誤碼
理解socket和sockaddr的關系:
? ? ? ? 初始化一個socket后,我們已經有了這樣一個endpoint for communication(通信端點),但是需要把這個用文件描述符描述的通信端點 綁定 到一個具體的網絡地址(網絡上的一個地址,包括IP地址和端口號等信息。),所以我們還要綁定現在這個socket到我們具體的sockaddr上去。
? ? ? ? 地址多種多樣,可能是AF_INET的地址,也可能是AF_UNIX的地址。
本來AF_INET是用socketaddr_in標記,AF_UNIX用socketaddr_un標記,但是為了實用性把他們兩個給繼承到了一個統一的socketaddr里,所以就變成了socketaddr來標記所有的套接字,在使用的時候通過強轉來找到對應的結構。
在邏輯上再次理解domain域和type?
由AF_INET+SOCK_DGRAM形成的UDP?
由AF_UNIX+TCP形成的本地通信。?
????????
?
?代碼實現:
void InitServer(){ENABLE_FILE_LOG;//日志文件使能//1.創建套接字int sock_fd = ::socket(AF_INET,SOCK_DGRAM,0);if(sock_fd<0){Die(1);LOG(LogLevel::FATAL)<<"socket: "<<strerror(errno);}//創建成功,看看套接字LOG(LogLevel::INFO)<<"socket success , socket fd is :"<<sock_fd;//2.填充網絡信息并bind}
關于LOG:【LINUX操作系統】日志系統——自己實現一個簡易的日志系統-CSDN博客
可以轉到socket參數的宏中去看一下:
????????????????
????????
1.2 關于bind
該接口的第一個參數表示需要綁定的套接字對應的文件描述符,第二個參數表示套接字結構,第三個參數表示套接字結構的大小
????????如果綁定成功,該接口返回0,否則返回-1并設置錯誤碼
????????對于第一個參數來說,就是希望被綁定的套接字;第三個參數表示傳入的第二個sockaddr的長度,因為第二個參數一般情況下存在強轉,需要知道具體這個信息標簽有多長。
下面就第二個參數詳細介紹:
????????在[Socket編程基礎]部分提到sockaddr可以理解為sockaddr_in結構和sockaddr_un的父類,而因為本次創建的是網絡通信,所以要使用的結構就是sockaddr_in(in后綴表示inet,un后綴表示unix),既然參數部分是sockaddr結構而不是sockaddr_in,那么在傳遞實參時就需要進行強制類型轉換。
簡單實現一個強轉的宏
#define CONV(addr) (const sockaddr*)(addr)
現在,我們已經在自己的函數棧上創建好了套接字,也完成了綁定。唯獨就是in_addr的信息還沒有綁定。
????????那么,既然需要用戶傳遞sockaddr_in結構,那么這個結構中就存在一些屬性需要用戶去設置————地址類型、端口號、IP地址共三種字段需要我們去設置。
????????????????????????????????????????????????
注意,此時我們還在“自己和自己玩”,還和其他機器通信毫無關系。sockaddr的這個“派生類”描述的都是自己這個套接字的信息!填充位留在那里就好了
觀察一下sockaddr_in(紅字中的派生類)的結構,知道結構才能賦值。
其中關于sin(sock inet)有一個宏,結構如下:
????????
傳進去的是sin,##表示將兩邊的符號相結合,所以第一個宏定義的參數其實是sin_family
,表示協議簇。也就是我們說的domain。
明明socket的時候都填了一次,怎么還要填一次?
創建套接字的時候域或者協議簇選擇一次(告訴操作系統套接字類型),sockaddr_in內部再賦值一次,兩個要一樣才能讓操作系統綁定上!
最后,關于第三個結構體sin_addr
????????????????????????
C語言不支持結構體直接整體賦值,因此我們需要內部數據一個一個賦值,不過此處的in_addr只有一個數據,就很方便直接賦值。
作為服務器,自己的端口號和IP地址肯定是會被知道的:
?
注意,socketaddr_in還需要引入頭文件<netinet/in.h> <arpa/inet.h>,結合前面兩個,構成網絡四大頭文件? ? ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
可以用指令man inet_addr查到。
不過可不能簡單把_port寫進去,還需要滿足網絡字節序
???????????????????????
?
????????IP用十進制點分法的string表示只是方便觀察,但內核存儲不應該是4字節?所以肯定要想辦法改變。既要從字符串變成一個uint32_t的數據,還要符合網絡字節序列,只使用一個簡單的htonl(host to net long)肯定是不現實的。
????????新的接口 inet_addr:直接將一個const char*的東西返回成一個符合網絡字節序和規則的IP地址。
?inet_addr_t是in_addr的返回值,也就是IP在網絡字節序的存在形式,也是
也是對應結構體的數據類型
??????????????????????????????????????
? ??
現在只需要我們把初始的port和IP設置進來即可。?
? ? ??
127.0.0.1
是一個特殊的 IPv4 地址,稱為 回環地址(Loopback Address)。它用于標識本地計算機本身:
127.0.0.1
用于在本地計算機上進行網絡通信,而無需通過外部網絡。(相當于這個IP發出去時:自己的網卡放出去然后不進入網絡,直接又自己的網卡接受信息)
它常用于測試和調試網絡應用程序,確保程序能夠在本地環境中正常運行。
選擇端口號8080,這是一個不會被使用的端口(0-1023都是綁定好的協議端口號)
為什么sockaddr需要有端口號和IP?
????????因為報文是需要返回的,返回的時候需要知道是哪個socket發出來的(寄包裹總得知道是誰寄出去的)
? ? ? ? 所以報頭必須有原IP和原端口號,服務器一定會把這兩個信息也推送給對方。
現在終于完成了bind。bind之后,這個套接字才算設置進內核中
最后,在進行填數據之前,因為有各種占位的原因,建議先把這個sockaddr_in清空。
?
1.3 Start
? ? ? ? ? ? ? ? ? ?
Start的整體思路:一個isrunning的狀態標記是否啟動,然后需要在一個while(true)下面,不停的通過recvfrom接口和sendto接口以收發消息(還需要一個緩沖數組首發傳的消息)。(這是一個全雙工的接口,支持又讀又寫)
???????????????????????
在while true里進行這個接受。
1.4 recvfrom 與 sendto
recvfrom
和sendto
是兩個用于網絡編程的系統調用,它們是針對數據報套接字(如使用SOCK_DGRAM
類型的套接字)進行通信的接口。
src_addr addrlen都是輸出型參數,recvfrom作為接受方,需要知道誰來連接了我們這個socket(收到包裹的時候,都會有一個快遞單號,上面寫了誰寄出來的)。flags設置為0表示是阻塞式接受,所以如果我們設置為0,進程會卡在原地等待此次網絡通信。
sendto:
兩者的返回值都是成功接受/發送的長度
想從服務器回消息到主機去,就需要一個sendto接口回消息。
void Start(){_isrunning = true;while(true){char inbuffer[SIZE];sockaddr_in* peer;socklen_t peer_len = sizeof(peer);int n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(peer),&peer_len);if(n>0){std::string echo = "#echo: ";LOG(LogLevel::DEBUG)<<inbuffer;echo += inbuffer;int n = sendto(_sockfd,echo.c_str(),echo.length(),0,CONV(peer),peer_len);}}_isrunning = false;}
順便修正一下之前的一個小bug:之前是直接使用的指針,沒有開辟空間。
?下次別這么玩了。直接sockaddr_in一個該多好,整的又是段錯誤又是綁定失敗的..........
補充:可以說,recvfrom和sendto的最后兩個參數都是去描述通信的另一端的,前面的sock_fd都是描述自己這一端的?
驗證實驗成果:?
結合指令:netstat -unap?
netstat
是一個非常有用的網絡工具,用于顯示網絡連接、路由表、接口統計信息、偽裝連接以及多播成員信息等。
2. 客戶端
????????服務器是被別人聯的,所以剛剛的而客戶端的peer根本不需要知道是誰,直接當作輸出型參數即可。而客戶端必須知道服務器的IP和端口,客戶端需要主動給服務器發數據,去申請內容。
這樣的模式稱之為CS(Client Server )模式:
????????CS模式是一種基本的網絡通信模型,它定義了客戶端和服務器之間的通信關系。這種模式在現代網絡應用中非常普遍,因為它提供了一種可靠、可擴展和安全的方式來組織網絡服務。通過CS模式,客戶端可以方便地訪問服務器提供的服務,而服務器則可以集中管理和控制服務的提供。
此處我們不在使用.hpp去封裝他,直接寫到main函數里:
首先,客戶端必須要拿到具體的服務器IP和服務器端口號(實際中可能這兩個東西是封裝在客戶端內部的)
發現此時還是要封裝Die等宏,所以索性都引進到一個Commn頭文件里。
之前的sockaddr_in中的結構體也做過類似思路的行為:
????????
按照之前的邏輯,我們完成如下:
int main(int argc,char* argv[])
{ ENABLE_CONSOLE_LOG;if(argc!=3){LOG(LogLevel::ERROR)<<"Usage "<<argv[0]<<": 127.0.0.1 8080";Die(1);}//0.獲取服務器的套接字信息std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);//1.創建套接字int sock_fd = socket(AF_INET,SOCK_DGRAM,0);if(sock_fd<0){LOG(LogLevel::FATAL)<<"socket fail";Die(2);}//2.填寫server信息//因為后續要在sendto中直接給server通信sockaddr_in server_socket;server_socket.sin_family = AF_INET;server_socket.sin_port = ::htons(server_port);server_socket.sin_addr.s_addr = ::inet_addr(server_ip.c_str());// //3.進行bind,并且設置進內核// int ret = bind(sock_fd,CONV(&server_socket),sizeof(server_socket));while(true){std::cout<<"Please Enter"<<std::endl;std::string Message;std::getline(std::cin,Message);int n = ::sendto(sock_fd,Message.c_str(),Message.length()-1,0,CONV(&server_socket),sizeof(server_socket));}}
注意,我們注釋掉了bind那一步:
客戶端不需要bind
????????實際上,客戶端并不需要綁定IP地址和端口。
注意,不需要不綁定表示:不需要程序員手動綁定,而是由OS自動綁定
理由如下:
比如你的手機,同時有淘寶客戶端,LOL客戶端,美團客戶端,如果三個公司的工程師A\B\C描述自己的sockaddr的時候,都把同一個端口號描述成自己客戶端的端口號,就會出現矛盾。
????????如果客戶端由程序員綁定,那么假設有兩個公司上線的客戶端使用的端口是一樣的,就會出現一個軟件先打開之后可以正常收到服務器發送的數據,但是另外一個軟件的服務器就無法正確發送信息到對應的軟件上,即一個端口只能對應一個進程,但是一個進程可以有多個端口。
????????那么,客戶端難道不需要端口嗎?并不是,如果客戶端沒有端口,那么服務器只能通過IP地址找到具體客戶端設備,但是找不到對應的進程,既然如此,客戶端的端口怎么確定?實際上這個端口由操作系統自行隨機分配。所以,端口號會在第一次sendto之后自動綁定。
?那么服務器端又為什么需要程序員手動綁定端口號??
???????因為服務器端口號如果是隨機的,而軟件中請求服務器的端口號是固定的,那么一個軟件可能在某一天可以正常收到服務器發送的數據,但是下一次因為服務器端口號是變化的,就無法正常收到信息。也就是說,端口號高概率會內置在客戶端中,如果服務器一直變化,就很難通信。
綜上所述,服務器端需要程序員手動綁定IP地址和端口號,而客戶端不需要程序員手動綁定IP地址和端口號,由操作系統自行分配并綁定啟動
? ? ? ? ?服務器端口號不僅不改變,還要盡量做到“眾所周知”,方便大家來連接。
一般在公司中,一個準備上線的項目都需要去公司后臺申請,申請到了對應的端口才能使用。
????????所以在這段代碼中,sock_fd描述的是發送方的動作,server_socket是希望接受發送動作的信息的地址。
? ? ? ? ?因此,兩個東西本來含義就不匹配,在含以上肯定不能bind。結合上面的內容我們可以知道,這個sock_fd最終被OS自動綁定。
最終調試可運行代碼效果:
?
?第一版本到此結束,源碼在這里:
EchoServer_V1 · lsnmjp/code of cpp Linux 算法 - 碼云 - 開源中國
現在能本地通信了,從此我們似乎以及不再需要之前的SYSTEM V體系了.......
準備聯入網絡。
3. demo代碼優化
3.1 從sockaddr_in中取數據
作為服務器,我們希望在傳輸出型變量peer出去之后將peer利用起來。
所以我們學習使用接口:
?????????????????
其中ntohs肯定可以用來獲得port
其次,想要將IP轉化為char*方便我們看,所以有:inet_ntoa
所以:
int n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&peer_len);//此時peer已經獲得了對應的數據 uint16_t client_port = ::ntohs(peer.sin_port); //網絡字節序列轉主機序列 std::string client_IP = ::inet_ntoa(peer.sin_addr);//轉換到主機序列;轉換成字符串
再在服務器echo的時候包裝一下:
57817就是我們的客戶端自動被OS綁定的端口號!
3.2 是否需要IP地址
修改服務器端的綁定網絡:
剛剛一直使用的是127.0.0.1的本地網卡,現在試試手動輸入一個——如果以機器的公網IP為例輸入會發現:
之所以出現這個問題,是因為云服務器的公網IP地址是不允許用戶自行綁定的
解決這個問題之前,我們思考一下,啟動一個服務器真的必須要綁定IP嗎?
?
????????一臺服務器可能有多個IP地址,此時如果服務器固定IP地址,那么此時就會出現服務器只能接收傳送到固定IP地址的信息,就算服務器有很多IP地址也只有一個IP地址可以使用,很明顯這個效果并不符合UDP協議的特點,因為UDP協議是面向無連接的,既然都不需要連接,為什么還需要指定IP地址,所以啟動服務器不需要指定IP地址。
???????????????????????
?
宏INADDR_ANY表示可以接受該機器所擁有的所有IP。注意,并不是通過INADDR_ANY去找機器,找機器的時候還是通過客戶端所內置的IP地址。但是在找到之后,在服務器的視角,只要是屬于自己的IP,都照單全收。
并且在構造函數和private下面可以不再需要IP變量。
???????????????????????
此時的狀態是,server不需輸入IP,但是client依然靠已知的IP去找server。
當下代碼,已經可以在多個LInux上跑了,也就是可以讓多個Linux機器之間互動。
甚至也可以在Linux和win下通過網絡通信(了解):
win的內核層一模一樣:
3.3 字節序列以及設置地址封裝
為了便于之后的sockaddr_in地址和我們希望看到的string ip與uint16_t port更直觀,我們進行以下封裝:
#pragma once#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>namespace Inet_Addr {class InetAddr{private:void Host2Net()//如果先拿到主機,我們希望轉向網絡{_net_addr.sin_family = AF_INET;_net_addr.sin_port = ::htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}//如果先拿到網絡,我們希望將地址轉向主機void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IPNet2Host(){_ip = }public:InetAddr(){}InetAddr(const struct sockaddr_in& net_addr):_net_addr(net_addr)//如果先拿到網絡,我們希望將地址轉向主機{PortNet2Host();IPNet2Host();}InetAddr(uint16_t port):_port(port)//如果先拿到主機,我們希望將地址轉向網絡{Host2Net();}private:struct sockaddr_in _net_addr; // 網絡字節標準uint16_t _port; // 主機標準std::string _ip; // 主機標準}; }
此處的ip本可以用之前學習的inet_ntoa轉換。
但其實,作為一個返回char*的C語言函數并不安全。指針會在inet_ntoa函數內部維護一段靜態空間,在多線程情況,這個空間可能被覆蓋。
3.3.1 線程安全接口
我們采用一種更加線程安全的inet_ntop方法:
void IPNet2Host(){char buffer[64];_ip = ::inet_ntop(AF_INET,&_net_addr.sin_addr,buffer,sizeof(_net_addr));}
會把轉換好的數據先放在buffer里,然后再賦值給_ip。
因為buffer是在線程棧是創造的,所以不會矛盾。
繼續封裝:?
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Commn.hpp"namespace Inet_Addr
{class InetAddr{private:void Host2Net()//如果先拿到主機,我們希望轉向網絡{_net_addr.sin_family = AF_INET;_net_addr.sin_port = ::htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}//如果先拿到網絡,我們希望將地址轉向主機void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IPNet2Host(){char buffer[64];_ip = ::inet_ntop(AF_INET,&_net_addr.sin_addr,buffer,sizeof(_net_addr));}public:InetAddr(){}InetAddr(const struct sockaddr_in& net_addr):_net_addr(net_addr)//如果先拿到網絡,我們希望將地址轉向主機{PortNet2Host();IPNet2Host();}InetAddr(uint16_t port):_port(port)//如果先拿到主機,我們希望將地址轉向網絡{Host2Net();}uint16_t GetPort(){return _port;}std::string& GetIP(){return _ip;}struct sockaddr* NetAddr(){return CONV(&_net_addr);}socklen_t NetAddrLen(){return sizeof(_net_addr);}private:struct sockaddr_in _net_addr; // 網絡字節標準uint16_t _port; // 主機標準std::string _ip; // 主機標準};
}
到客戶端和服務器中去封裝:
????????
改動后的完整代碼:
#ifndef UDP_SERVER__HPP #define UDP_SERVER__HPP#include <iostream> #include <memory> #include <string> #include <string.h>#include "InetAddr.hpp" #include "Log.hpp" #include "Commn.hpp"using namespace LogModule; using namespace Inet_Addr;// global int gsockfd = -1; const static std::string gdefaultIP = "127.0.0.1"; const static uint16_t gdefaultport = 8080;class UdpServer { public:// UdpServer( const std::string& IP = gdefaultIP,uint16_t port = gdefaultport)UdpServer(uint16_t port = gdefaultport): _sockfd(gsockfd), _inetaddr(port)//,_port(port)//,_IP(IP){}void InitServer(){// ENABLE_FILE_LOG;//日志文件使能ENABLE_CONSOLE_LOG;// 1.創建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){Die(1);LOG(LogLevel::FATAL) << "socket: " << strerror(errno);}// 創建成功,看看套接字LOG(LogLevel::INFO) << "socket success , socket fd is :" << _sockfd;// 2.填充網絡信息并bind : 設置進了內核中// struct sockaddr_in* in_addr_;// in_addr_ = new sockaddr_in();// 2.1 填充in_addr的信息// bzero(in_addr_, sizeof(sockaddr_in));//清理// in_addr_->sin_family = AF_INET;// in_addr_->sin_port = ::htons(_port);//端口號// //in_addr_->sin_addr.s_addr = ::inet_addr(_IP.c_str());//1. string ip->4bytes 2. network order// in_addr_->sin_addr.s_addr = INADDR_ANY;// bindint n = bind(_sockfd, CONV(_inetaddr.NetAddr()), _inetaddr.NetAddrLen());if (n < 0){LOG(LogLevel::ERROR) << "bind fail";exit(1);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while (true){char inbuffer[SIZE];sockaddr_in peer;socklen_t peer_len = sizeof(peer);int n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &peer_len);// // 此時peer已經獲得了對應的數據// uint16_t client_port = ::ntohs(peer.sin_port); // 網絡字節序列轉主機序列// std::string client_IP = ::inet_ntoa(peer.sin_addr); // 轉換到主機序列;轉換成字符串if (n > 0){InetAddr client(peer);inbuffer[n] = 0;LOG(LogLevel::DEBUG) << "Client says@ " << inbuffer;std::string echo = "#echo: ";std::string backinfo = client.GetIP() + " " + std::to_string(client.GetPort()) + echo;// std::string client_info = client_IP;// client_info += " : ";// client_info += std::to_string(client_port);// client_info += ' ';// echo += client_info;backinfo += inbuffer;int ret = sendto(_sockfd, backinfo.c_str(), backinfo.length(), 0, CONV(client.NetAddr()), client.NetAddrLen());// if(ret>0)// {// LOG(LogLevel::DEBUG)<<"server sendto success";// }// else// {// LOG(LogLevel::DEBUG)<<"server sendto fail";// }}}_isrunning = false;}~UdpServer(){if (_sockfd != gsockfd)close(_sockfd);}private:int _sockfd; // 訪問套接字的文件描述符InetAddr _inetaddr;// uint16_t _port; //端口號// std::string _IP;//十進制點分IP地址bool _isrunning = false; };#endif
全部代碼:
EchoServer_V2 · lsnmjp/code of cpp Linux 算法 - 碼云 - 開源中國
白框上面是xshell打開的client,白框下面是vscode打開的server端。
4. 下集預告?
下一集我們將利用上述思維和代碼進行一些簡單的業務,比如字典、聊天室等