本篇文章將帶大家了解網絡通信是如何進行的(如包括網絡字節序,端口號,協議等) ;再對socket套接字進行介紹;以及一些udp-socket相關網絡通信接口的介紹及使用;最后進行對基于udp的網絡通信(服務端與客戶端之間)進行測試和外加模擬實現的基于udp通信的簡單的英譯漢詞典與多人聊天室;歡迎閱讀!!!
歡迎拜訪:羑悻的小殺馬特.-CSDN博客
本篇主題:速通基于UDP的簡單網絡通信
制作日期:2025.07.08
隸屬專欄:Linux之旅?
目錄
一·網絡通信相關概念:
1.1認識ip及端口號:
認識ip:
認識端口號:?
端口號劃分:
端口號與進程ID的區別:
理解源端口號與目標端口號:
1.2淺解Socket概念:
淺淺認識TCP/UDP協議:
?TCP協議:
UDP協議:
認識網絡字節序:
認識Socket底層結構:
認識sockaddr_in?結構:
二·網絡通信之Socket-UDP:
2.1Socket-UDP相關網絡通信接口函數認識:
2.1.1創建套接字之socket接口:
2.1.2綁定套接字之bind接口:
2.1.3 接收信息之recvfrom接口:
2.1.4發送信息之sendto接口:
2.2UDP通信特點及注意事項:
UDP通信特性:
?UDP通信時注意事項:
?①客戶端是不需要手動綁定:
②一般服務端是不能直接綁定特定ip的:
2.3實現基于UDP的server-client簡單通信:
?測試效果:
代碼實現:?
主要代碼文件部分:
udpclient.cc:
udpserver.cc:
udpserver.hpp:
間接包含文件:
log.hpp:
Makefile:
mutex.hpp:
2.4基于UDP實現的server-client簡單通信改造的英譯漢翻譯功能:
?測試效果:
代碼實現:
?主要代碼部分:
dict.hpp:
dict.txt:
addr.hpp:
udpclient.cc:
udpserver.cc:
udpserver.hpp:
間接包含部分:
2.5基于UDP實現的server-clients通信改造多人聊天室(服務端多線程版本):
效果展示:
代碼實現:
主要代碼部分:
udpclient.cc:
udpserver.cc:
udpserver.hpp:
addr.hpp:
route.hpp:
間接包含部分:
cond.hpp:
thread.hpp:
threadpoll.hpp:
優化關于網絡序列和本機序列之間的轉化:
之前對port的網絡本機序列轉化:
之前對ip的網絡本機序列轉化:
三·常用的網絡指令:
3.1ifconfig:
3.2netstat:
3.3ping:
3.4pidof:
四·本篇小結:
一·網絡通信相關概念:
1.1認識ip及端口號:
首先,何為網絡通信:
數據傳輸到主機不是目的,而是手段。到達主機內部,在交給主機內的進程,才是目的!
因此上網要么從某處讀取數據,要么向某處發送數據;本質就是數據的交互!因此我們可以理解成:
故通信/數據通過網絡傳輸其實就是兩個指定進程之間通過網絡傳輸!?
認識ip:
IP地址是在IP協議中,用來標識網絡中不同主機的地址;對于IPv4來說,IP地址是一個4字節,32位的整數;我們通常也使用"點分十進制"的字符串表示IP地址,例如192.168.0.1;用點分割的每一個數字表示一個字節,范圍是0-255;這里我們就認為是區域內標定唯一主機的!
認識端口號:?
端口號是一個 2 字節 16 位的整數;?端口號用來標識一個進程, 告訴操作系統, 當前的這個數據要交給哪一個進程來處理!因此可以總結成一個端口號只能對應一個進程;但是一個進程可以有多個端口號! 端口號目的就是從主機中確定一個具體的進程!
端口號劃分:
?0-1023: 知名端口號, HTTP,FTP,SSH 等這些廣為使用的應用層協議,他們的端口號都是固定的。
1024-65535:操作系統動態分配的端口號.客戶端程序的端口號,就是由操作系統從這個范圍分配的。也就是說我們在后面udptcp通信等使用的端口號都是1024-65535這個范圍內的!
端口號與進程ID的區別:
?端口號是進程唯一性的標識也就是說通過指定端口號只能找到一個進程另外,一個進程可以綁定多個端口號;但是一個端口號不能被多個進程綁定!
形象理解下:
可以把進程想象成不同的商店,端口號就像是商店的門牌號。
一個進程可以綁定多個端口號,就好比一家商店可以有多個門,比如大型商場,它可能有正門、側門、后門等多個門(多個端口號),顧客(數據)可以從不同的門進入商場(進程),每個門都可以用來接待不同類型的顧客或者提供不同的服務。
而一個端口號不能被多個進程綁定,是因為如果一個門牌號對應了多家商店,那么顧客就會不知道該進哪家店,數據也會混亂,不知道該把信息送到哪個進程。所以,一個門牌號(端口號)只能對應一家商店(進程),這樣才能保證數據準確無誤地到達對應的進程。
進程 ID 屬于系統概念,技術上也具有唯一性,確實可以用來標識唯一的一個進程,但是這樣做,會讓系統進程管理和網絡強耦合,實際設計的時候,并沒有選擇這樣做。
不是所有的進程,都要進行網絡通信;因此端口號不是每個進程都有的;?這里pid是一個系統概念;端口號是網絡概念;當網絡一變那么進程對應的端口號就會變化;但是pid是不變的;實現了解耦!
理解源端口號與目標端口號:
傳輸層協議(TCP和UDP)的數據段中有兩個端口號,分別叫做源端口號和目的端口號.就是在描述"數據是誰發的,要發給誰"。?
得出結論:IP+Port=全網內唯一的一個進程;
因此,就可以理解網絡通信本質:全網內唯二的兩個進程在進行進程間通信,其中ip確定好網絡內一主機;而端口號就確定的是該主機的某個進程;要完成網絡通信我們就需要:源ip,源port,目的ip,目的端口號,也就是源socket和目標socket;我們在下面會講到!?
1.2淺解Socket概念:
在上面我們也講到了,IP 地址用來標識互聯網中唯一的一臺主機,port 用來標識該主機上唯一的.一個網絡進程!
通信的時候,本質是兩個互聯網進程代表人來進行通信,{srclp,srcPort,dstlp,dstPor}這樣的4元組就能標識互聯網中唯二的兩個進程;網絡通信就是進程間通信!
我們把幣p+port 叫做套接字 socket ;這就是它的由來!
socket和tcp/ip有關也就是和網絡層傳輸層有關-->屬于內核-->受OS控制-->需要使用系統接口:?
因此當我們使用套接字的時候就需要使用系統提供的接口;后面用的時候我們會講解!
淺淺認識TCP/UDP協議:
?TCP協議:
TCP(Transmission Control Protocol 傳輸控制協議):
特點:
做更多工作,復雜,占有資源多
傳輸層協議
有連接
可靠傳輸
面向字節流(類似當初的文件流;耦合性不是特別強;這里我們將TCP通信的時候會明顯察覺)
UDP協議:
UDP(User Datagram Protoco 用戶數據報協議):
特點:
相對做的少;簡單等
傳輸層協議
不可靠傳輸
面向數據報(報比如發送10個信息就收到10個不多也不少;馬上就可以看到了)
這里我們仍舊是需要保存他們倆;各有各的特點;誰更合適就選擇誰!!!?
認識網絡字節序:
先認識下大小端:
?發送主機通常將發送緩沖區中的數據按內存地址從低到高的順序發出;接收主機把從網絡上接到的字節依次保存在接收緩沖區中,也是按內存地址從低到高的順序保存。
因此就有了以下規定:
因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,后發出的數據是高地址;其中TCP/IP 協議規定,網絡數據流應采用大端字節序,即低地址高字節。
大小端機器 都會遵循這個原則;如果當前發送主機是小端,就需要先將數據轉成大端;否則就忽略,直接發送即可(只不過應用了轉化技術)。
但是每次這樣發送和接收的時候我們都進行轉化相對比較麻煩;因此系統就自己封裝了一些函數;自動幫助我們是被本端機器大小端完成網絡和本地的轉化工作!?
助記:
h 表示 host,n 表示 network,表示 32 位長整數,s 表示 16 位短整數。?
大小端轉化問題:
如果主機是小端字節序,這些函數將參數做相應的大小端轉換然后返回;如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。
說白了就是網絡通信要用網絡字節序;其他地方就是主行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。機字節序: 主機字節序-->網絡字節序-->主機字節序!
不僅有 1·本地序列和網絡序列轉化功能 2·還兼容大小端轉化問題->完美!
后面我們應用的時候再做演示說明!
認識Socket底層結構:
首先,先來看一下常見API:
先認識下后面我們在UDP;TCP通信的時候都會用到!
?IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結構體表示,包括16位地址類型,16位端口號和32位IP地址.
IPv4、IPv6地址類型分別定義為常數AF_INET、AF_INET6.這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容.
socket API可以都用struct sockaddr*類型表示,在使用的時候需要強制轉化成sockaddr_in;這樣的好處是程序的通用性,可以接收IPv4,IPv6,以及UNIX DomainSocket各種類型的sockaddr結構體指針做為參數;
總而言之;我們接下來就只拿IPV4地址類型來談;利用它的類型定義及結構體!?
下面我們看張圖理解下對應的IPV4下的本地與網絡通信:
這里socket為什么不強轉void+傳進去:當時語言還不支持;而這樣寫更加能體現出繼承多態那套邏輯。?
但是上面我們說了,無論是網絡通信還是本地通信最后結構體都會被轉出sockaddr形式;那么它該如何區分是網絡通信還是本地通信呢??
判斷首地址為哪個類型就強轉成那個類型指針去訪問!!!?
下面我們利用文字敘述下上面操作的過程:
首先把un或者in的地址傳給這個所謂的'父類':然后它的address的值就被覆蓋成對應傳進來的結構體的首地址了﹔但是類型還是sockaddra因此當我們拿對應指針訪問sockaddr的成員的時候(就是相當于加上之前拿成員對應的移動多少地址的匹配對應關系)-->故訪問到的就是前16位;因此拿著個里面的數據去判斷﹔如果是AFINET就轉成對應的指針﹔此時首地址還是沒有變;只是它的類型變成了對應的sockaddr_in類型就可以訪問了﹔改變指針類型==改變了這種訪問成員的偏移量的匹配機制
因為網絡通信和本地通信大差不差;也就是理解了網絡通信;本地通信就差不多了;那么下面我們就學習一下網絡通信:
認識sockaddr_in?結構:
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)];};
?這里雖然看起來很多參數;大都是typedef出來的;因此不必擔心!
下面我們看張圖;來更清晰認識它:
這樣我們就和之前的那張繼承的結構體圖片為什么那樣寫就完美結合底層結構認識了!!!??
這里我們就先簡單認識下IPV4地址下的網絡通信的底層結構即可;后面我們在使用這些接口的時候會有更清晰認識的!!!?
二·網絡通信之Socket-UDP:
2.1Socket-UDP相關網絡通信接口函數認識:
下面我們將為之后基于UDP實現的網絡通信先進行相關函數接口介紹:
首先就是網絡通信接口必備四大頭文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
2.1.1創建套接字之socket接口:
#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);
成功返回套接字的文件描述符;失敗就小于0!
?對于UDP我們這樣用:
這里其實返回的這個socketfd就是個文件描述符:因此可以理解成我們這樣就創建了一個“網絡通信的文件”。?
2.1.2綁定套接字之bind接口:
#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
這里的socklen_t就是這個結構體的長度字節!?
成功返回0;失敗就小于0!
需要注意的是我們網絡通信的sockaddr_in*需要強轉成sockaddr*類型?!
在我們填寫sockaddr_in的時候需要注意的是:
注意本地序列與網絡序列之間的轉化:
此時就用到我們上面講述的htons了:把這個16位整數按照1·大端的方式2·變成網絡序列?
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);
以及ip地址轉化inet_addr:系統提供的可以把string字符串類型轉化成1·大端;2、·并且轉化成32位數字的網絡序列
in_addr_t inet_addr(const char *cp);
這里一般綁定是對于服務端而言;而客戶端無需綁定(系統自己根據分配進行綁定);然而一般服務端是允許它所可以識別的多個對應ip發起請求的;因此不建議綁死固定ip;后面會講述!
?對于ip地址由本地的字符串樣式轉化成網絡四字節序列模擬操作:
上面過程只需要了解即可!!!?
這里我們提供了inet_addr及inet_pton/addr_ntop等接口大大減輕了我們的負擔!!!?
2.1.3 接收信息之recvfrom接口:
#include <sys/types.h>#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
一般使用的阻塞式用法;成功就返回讀到的個數;失敗就小于0;下面我們結合使用來理解下各個參數:
我們拿到發送方的ip+port是網絡序列;可以轉化成本地進行查看:
2.1.4發送信息之sendto接口:
#include <sys/types.h>#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
成功返回0;失敗就小于0;和上面的那個recvfrom差不多;但是上面傳遞的sockaddr_in是輸出型參數;這里是輸入型參數 !(一些細節地方注意一下;比如取地址等等(一般只有需要修改的時候才會取地址;如len但是sockaddr無論咋樣都是傳遞地址))
如;
上面這些接口對應我們的udp通信就夠用了;其他的就是相關其他轉化接口了;我們會在后面書寫通信代碼的時候提到(如htons系列,inet_addr等)!?
2.2UDP通信特點及注意事項:
UDP通信特性:
udp sockfd,既可以讀,又可以寫。UDP通信,其實是全雙工的;只要有src+des的ip+port就可以發送;這里只有接收緩沖區;無發送緩沖區;對于udp(和tcp不同,后面講的tcp會講解如何處理),只要發了多少就會一次性讀完不會出現“粘報”等情況!
?UDP通信時注意事項:
這里我們拿客戶端然后用des的ip和port(服務端綁定的)就可以給服務端發信息;客戶端本身就綁定了對應服務器的des ip port等等!(服務端與客戶端是一家公司寫的!)
公網IP其實沒有配置到你的IP上。公網IP無法被直接bind;我們bind的要么是本地環回要么就是子網ip!
?①客戶端是不需要手動綁定:
os自己認識用戶端機器的ip+隨機會生成端口號port自動綁定與解除;故無需bind/程序終止立刻解除故查不到狀態!
主機換了個區域的網絡,通常會獲得一個對應新網絡的子網IP;因此如果我們在某個區域進行網絡通信;就會把這個區域分配的子網ip+port綁定然后通信;如果我們還要在另一個不同的網絡區域通信那么就又要解綁再次更換ip+port了有點麻煩!!!---->因此oS就支持了自動分配﹐((因為我們如果給服務器進行網絡通信-->因此客戶端就會首先根據對應主機所在網絡區域分配的ip然后進行自己的綁定-->再拿到對應客戶端內置的服務端的iptport進行發送即可)
下面我們舉一個形象例子幫助理解下:
你進入圖書館時,不需要自己去指定要去哪個書架(手動綁定IP地址)以及具體要坐在書架的什么位置看書(手動綁定端口)。因為圖書館有管理員,他們會根據圖書館的空間使用情況和你的需求,給你安排一個合適的書架和座位,這就像網絡中的 DHCP 服務器和操作系統,它們會自動為客戶端分配可用的IP地址和端口號。
②一般服務端是不能直接綁定特定ip的:
如果綁定了:給服務端發信息就只能通過這個綁定的ip+port進行了!
但是一般這么用,服務端所在的主機會為自己的客戶端內置好自己的主機的一些ip;然后當客戶端去給它發信息就可以拿著不同的ip(但是終歸是服務端所在同一主機的ip)去給服務端發送信息;服務端識別到屬于對應ip族;然后port也相同故可以接收....
2.3實現基于UDP的server-client簡單通信:
下面,我們就基于上面所介紹的接口函數以及注意事項來完成簡單通信;在我們書寫代碼前先看一下,這個模型的形象圖:
下面我們就拿字節旗下的app如抖音等等基于這個模型解釋下:
比如用戶在抖音 ...字節產品的app用戶端進行訪問自己的數據(是首先要和字節的服務器進行網絡通信的;然后服務器發送過來);此時的網絡通信就可以理解成上面所講的;首先因為這些app是字節的肯定內置了字節對服務器的ip(可能不同;但是都是可以找到對應的服務器【由于服務器綁定的是INADDR ANY】);然后0S根據自己所在網絡ip進行客戶端綁定;然后進行通信;最后服務器再給返回來;然后可能有多個用戶端進行訪問也是可以同時訪問成功服務器拿到數據的!!!?
多個app同時進行也是一樣的(套接字最終綁定的是進程)
?測試效果:
代碼實現:?
詳細說明見代碼超詳細注釋:
主要代碼文件部分:
udpclient.cc:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <memory>
#include "log.hpp"using namespace std;
int main(int argc, char *argv[])
{if (argc != 3){cerr << " please use: " << argv[0] << " server_ip server_port" << std::endl;return 1;}// 創建套接字:int sd = socket(AF_INET, SOCK_DGRAM, 0); // 網絡通信/用戶數據報if (sd < 0)use_log(loglevel::DEBUG) << "socket failure!";use_log(loglevel::DEBUG) << "socket success!";// os自己認識用戶端機器的ip+隨機會生成端口號port自動綁定與解除;故無需bind/程序終止立刻解除故查不到狀態string ip = argv[1]; // string內置機制不會直接拷貝指針uint16_t port = stoi(argv[2]); // 先構造string//初始化des套接字:sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = inet_addr(ip.c_str());
//死循環式等待給服務端發送信息: while (1){string mess;getline(cin, mess);socklen_t len = sizeof(local);//發送:int so = sendto(sd, mess.c_str(), mess.size(), 0, (sockaddr *)&local, len); // 輸入型參數if (so < 0)use_log(loglevel::DEBUG) << "sendto failure!";//接收:char buff[1024] = {0};ssize_t rm= recvfrom(sd, buff, sizeof(buff) - 1, 0, (sockaddr *)&local, &len);if (rm< 0) use_log(loglevel::DEBUG) << "recvfrom failure!";cout<<"$server say:"<< buff<<endl;;}
}
udpserver.cc:
#include"udpserver.hpp"string echo_repeat(string mess){string ret;ret+="return ";ret+=mess;return ret;}
int main(int argc ,char* argv[]){// if(argc != 3)// {// std::cerr << "please use: " << argv[0] << " ip"<<" port" << std::endl;// return 1;// }if(argc != 2){std::cerr << "please use: " << argv[0] <<" port" << std::endl;return 1;}consolestrategy;//string ip=argv[1];//string內置機制不會直接拷貝指針// uint16_t port=stoi(argv[2]);//先構造stringuint16_t port=stoi(argv[1]);// unique_ptr<udpserver> ur=make_unique<udpserver>(ip,port);//unique_ptr<udpserver> ur=make_unique<udpserver>(port);//回調:unique_ptr<udpserver> ur=make_unique<udpserver>(port,echo_repeat);ur->init();ur->start();}
udpserver.hpp:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include<memory>
#include<functional>
#include "log.hpp"
using namespace std;
const int delfd = -1;using func_t=function<string(string)>;
// consolestrategy; 不允許全局直接使用操作;只允許定義聲明等等class udpserver
{
public:// udpserver(string ip, uint16_t port) : _ip(ip), _port(port),// _isrunning(0), _socketfd(delfd) {}//這里服務器不能綁死否則只能接受指定的主機發來的信息了//udpserver( uint16_t port) : _port(port), _isrunning(0), _socketfd(delfd) {}udpserver( uint16_t port,func_t func) : _port(port), _isrunning(0), _socketfd(delfd),_func(func) {}void init(){// 1`創建套接字:_socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 網絡通信/用戶數據報if (_socketfd < 0)use_log(loglevel::DEBUG) << "socket failure!";use_log(loglevel::DEBUG) << "socket success!";// 2`socket綁定信息:sockaddr_in local;// char sin_zero[8];// 填充字節,使 sockaddr_in 和 sockaddr 長度相同bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 把對應的16位轉化成網絡序列// 內置的把string轉化成大端類型的網絡序列;也可手動操作(來回轉化比較麻煩系統直接提供)// ntohs()網絡序列轉回本地//. local.sin_addr.s_addr = inet_addr(_ip.c_str());local.sin_addr.s_addr =INADDR_ANY ;// 或者直接輸入0;可以理解成匹配同一主機的不同ip的任意des-port相同的不同進程發來的信息int n = bind(_socketfd, (sockaddr *)&local, sizeof(local));if (n < 0)use_log(loglevel::DEBUG) << "bind failure!";use_log(loglevel::DEBUG) << "bind success!";}void start(){// 服務器用不掛掉->死循環:_isrunning = 1;while (_isrunning){char buff[1024] = {0};sockaddr_in per;socklen_t len = sizeof(per); // 套接字長度就是字節數ssize_t rm = recvfrom(_socketfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&per, &len); // 輸出型參數故取地址if (rm< 0) use_log(loglevel::DEBUG) << "recvfrom failure!";buff[rm]=0;string per_addr= inet_ntoa(per.sin_addr);uint16_t per_port=ntohs(per.sin_port);cout<< "$client :[addr: "<<per_addr<<" port: "<<per_port<<" ] say: "<<buff<<endl;//string res="server say:";// res+=buff;//int so=sendto(_socketfd,res.c_str(),res.size(),0,(sockaddr *)&per,len);//這里接收兩個全都對套接字用的是指針:繼承多態效果string ans= _func(buff);int so=sendto(_socketfd,ans.c_str(),ans.size(),0,(sockaddr *)&per,len);//這里接收兩個全都對套接字用的是指針:繼承多態效果//輸入型參數if (so < 0) use_log(loglevel::DEBUG) << "sendto failure!";}}~udpserver(){}private://string _ip;uint16_t _port;int _socketfd;bool _isrunning;func_t _func;
};
間接包含文件:
log.hpp:
#ifndef __LOG__
#define __LOG__
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <memory>
#include <fstream>
#include <filesystem>
#include <sstream>
#include<ctime>
#include<cstdio>
#include "mutex.hpp"
using namespace std;
#define gsep "\r\n"
// 基類:
class Logstrategy
{
public:Logstrategy() {}virtual void synclog(const string &message) = 0;~Logstrategy() {}
};// 控制臺打印日志:class consolelogstrategy : public Logstrategy
{public:consolelogstrategy() {}void synclog(const string &message) override{// 加鎖完成多線程互斥:{mutexguard md(_mutex);cout << message << gsep;}}~consolelogstrategy() {}private:mutex _mutex;
};// 自定義文件打印日志:
const string P = "./log";
const string F = "my.log";
class fileLogstrategy : public Logstrategy
{public:fileLogstrategy(const string path = P, const string file = F) : _path(path), _file(file){// 如果指定路徑(目錄)不存在進行創建;否則構造直接返回:{mutexguard md(_mutex);if (filesystem::exists(_path))return;try{filesystem::create_directories(_path);}catch (filesystem::filesystem_error &e){cout << e.what() << gsep;}}}void synclog(const string &message) override{// 得到指定文件名:{mutexguard md(_mutex);string name = _path + (_path.back() == '/' ? "" : "/") + _file;// 打開文件進行<<寫入:ofstream out(name, ios::app); // 對某文件進行操作的類對象if (!out.is_open())return; // 成功打開out << message << gsep;out.close();}}~fileLogstrategy() {}private:string _path;string _file;mutex _mutex;
};// 用戶調用日志+指定打印:
// 日志等級:
enum class loglevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};
// 完成枚舉值對應由數字到原值轉化:
string trans(loglevel &lev)
{switch (lev){case loglevel::DEBUG:return "DEBUG";case loglevel::INFO:return "INFO";case loglevel::WARNING:return "WARNING";case loglevel::ERROR:return "ERROR";case loglevel::FATAL:return "FATAL";default:return "ERROR";}return"";
}// 從時間戳提取出當前時間:
string gettime()
{time_t curtime=time(nullptr);struct tm t;localtime_r(&curtime,&t);char buff[1024];sprintf(buff,"%4d-%02d-%02d %02d:%02d:%02d",t.tm_year+1900,//注意struct tm成員性質t.tm_mon+1,t.tm_mday,t.tm_hour,t.tm_min,t.tm_sec);return buff;}
class Log
{
public:// Log刷新策略:void console() { _fflush_strategy = make_unique<consolelogstrategy>(); }void file() { _fflush_strategy = make_unique<fileLogstrategy>(); }Log(){// 默認是控制臺刷新:console();}// 我們想讓一個類重載了<<支持連續的<<輸入并且希望每行結束就進行刷新;因此這個meesage類// 析構就可以執行刷新;-->內部類天然就是外部類的友元類;可以訪問外部類所有成員變量及函數class Logmess{public:Logmess(loglevel &lev, string filename, int line, Log &log) : _lev(lev),_time(gettime()), _pid(getpid()), _filename(filename), _log(log), _linenum(line){stringstream ss;ss << "[" << _time << "] "<< "[" << trans(_lev) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _linenum << "] "<< "<--> ";_mergeinfo=ss.str();}template<class T>Logmess& operator <<(const T& data){stringstream ss;ss<<data;_mergeinfo +=ss.str();return *this;}~Logmess(){_log._fflush_strategy->synclog(_mergeinfo);}private:loglevel _lev;string _time;pid_t _pid;string _filename;int _linenum;string _mergeinfo;Log &_log;};Logmess operator()(loglevel l,string f,int le){ //返回的是匿名對象(臨時對象)-->也就是作用完當前行//(執行完logmess的<< <<后自動調用logmess的析構也就是直接策略打印) return Logmess(l,f,le,*this);}~Log() {}private:unique_ptr<Logstrategy> _fflush_strategy;
};Log l;#define use_log(x) l(x,__FILE__,__LINE__)//自動判斷是哪行哪個文件#define filestrategy l.file()#define consolestrategy l.console()
#endif
Makefile:
.PHONY:all
all:udpclient udpserver
udpclient:udpclient.ccg++ -o $@ $^ -std=c++17
udpserver:udpserver.ccg++ -o $@ $^ -std=c++17
.PHONY:clean
clean:rm -f udpclient udpserver
mutex.hpp:
#pragma once
#include<pthread.h>
//封裝鎖:
class mutex{public:mutex(){int n= pthread_mutex_init(&_mutex,nullptr);(void)n;}void Lock(){ pthread_mutex_lock(&_mutex);}void Unlock(){ pthread_mutex_unlock(&_mutex);}pthread_mutex_t*getmutex(){return &_mutex;}~mutex(){int n= pthread_mutex_destroy(&_mutex);(void)n;}
private: pthread_mutex_t _mutex;
};
//自動上鎖與解鎖
class mutexguard{public://初始化為上鎖;mutexguard(mutex &mg):_mg(mg){ _mg.Lock() ; }//引用//析構為解鎖:~mutexguard(){_mg.Unlock() ; }private:mutex &_mg;//注意引用:確保不同線程上鎖與解鎖的時候拿到同一把鎖;不能是直接賦值
};
2.4基于UDP實現的server-client簡單通信改造的英譯漢翻譯功能:
這里實現翻譯的功能;只不過是給服務端接收到信息進行多了一個回調的函數來完成的;所以改動不大;相當于加個回調方法即可。
實現思路:
首先服務端進行詞典的加載也就是調用査詢的函數內部把對應的英語和漢語的對應關系放入map里;然后用戶端給dict. hpp:服務端發送對應的英文串;然后服務端拿著對應的英文串通過回調去査詢函數尋找second;找到了就sendto回去對應second:否則sendto none!!!
下面我們增加了個翻譯功能的類使得能從指定文件中加載對應映射關系;以及處理功能等!
?測試效果:
先loading詞典到本地:
下面等待用戶端輸入;進行查詢發出:
?
代碼實現:
?主要代碼部分:
dict.hpp:
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "log.hpp"
#include "addr.hpp"
using namespace std;
const string sep = ": ";
string pathname = "./dict.txt";
class dict
{
public:dict(const string p = pathname) : _pathname(p) {}bool loaddict(){ifstream in(_pathname);if (!in.is_open()){use_log(loglevel::DEBUG) << "打開字典: " << _pathname << " 錯誤";return false;}string line;while (getline(in, line)){int pos = line.find(sep);if(pos==string::npos) { use_log(loglevel::DEBUG) << "當前加載錯誤,繼續向下加載";continue;} string word = line.substr(0, pos);string chinese = line.substr(pos + 2,string::npos);if (word.size() == 0 || chinese.size() == 0){use_log(loglevel::DEBUG) << "加載錯誤";continue;}_dict.insert({word, chinese});use_log(loglevel::DEBUG) << "成功加載:"<<line;;}in.close();return true;}string translate(const string src, inetaddr client)//const類型對象只能調用非const修飾的成員函數等{if (!_dict.count(src)){use_log(loglevel::DEBUG) << "有用戶進入到了翻譯模塊, client: [" << client.ip() << " : " << client.port() << "]# 查詢 " << src << "->None";return "None";}auto iter = _dict.find(src);use_log(loglevel::DEBUG) << "有用戶進入到了翻譯模塊, client: [" << client.ip() << " : " << client.port() << "]# 查詢 " << src << "->" << iter->second;return iter->second;}~dict() {}private:string _pathname;unordered_map<string, string> _dict;
};
dict.txt:
robot: 機器人
flower: 花
tree: 樹
table: 桌子
chair: 椅子
cup: 杯子
bowl: 碗
spoon: 勺子
knife: 刀
fork: 叉子
music: 音樂
movie: 電影
game: 游戲
park: 公園
zoo: 動物園
school: 學校
hospital: 醫院
restaurant: 餐館
supermarket: 超市
bank: 銀行
post office: 郵局
library: 圖書館
street: 街道
road: 路
mountain: 山
river: 河流
lake: 湖泊
beach: 海灘
cloud: 云
rain: 雨;下雨
snow: 雪;下雪
wind: 風
sun: 太陽
moon: 月亮
star: 星星
sweet: 甜的
sour: 酸的
bitter: 苦的
spicy: 辣的
cold: 寒冷的;冷的
hot: 炎熱的;熱的
warm: 溫暖的
cool: 涼爽的
big: 大的
small: 小的
long: 長的
short: 短的;矮的
fat: 胖的;肥的
thin: 瘦的;薄的
tall: 高的
low: 低的
fast: 快的;快速地
slow: 慢的;緩慢地
easy: 容易的
difficult: 困難的
beautiful: 美麗的
ugly: 丑陋的
kind: 善良的
cruel: 殘忍的
clever: 聰明的
stupid: 愚蠢的
strong: 強壯的
weak: 虛弱的
open: 打開;開著的
close: 關閉;關著的
clean: 干凈的;清潔
dirty: 臟的
new: 新的
old: 舊的;老的
young: 年輕的
old-fashioned: 老式的;過時的
modern: 現代的;時髦的
expensive: 昂貴的
cheap: 便宜的
light: 輕的;燈
heavy: 重的
empty: 空的
full: 滿的
remember: 記得;記住
forget: 忘記
begin: 開始
end: 結束;結尾
start: 開始;出發
stop: 停止;阻止
give: 給
take: 拿;取;帶走
buy: 買
sell: 賣
: 起飛
bug: borrow: 借(入)
lend: 借(出)
arrive: 到達
leave: 離開;留下
find: 找到;發現
lose: 丟失;失去
dream: 夢想;做夢
think: 思考;認為
believe: 相信;認為
doubt: 懷疑
hope: 希望
wish: 愿望;希望;祝愿
addr.hpp:
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
class inetaddr
{public:inetaddr(const sockaddr_in &addr) : _addr(addr){_port = ntohs(_addr.sin_port);_ip = inet_ntoa(_addr.sin_addr);}string ip() { return _ip; }uint16_t port() { return _port; }~inetaddr() {}private:sockaddr_in _addr;string _ip;uint16_t _port;
};
udpclient.cc:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <memory>
#include "log.hpp"using namespace std;
int main(int argc, char *argv[])
{if (argc != 3){cerr << " please use: " << argv[0] << " server_ip server_port" << std::endl;return 1;}// 創建套接字:int sd = socket(AF_INET, SOCK_DGRAM, 0); // 網絡通信/用戶數據報if (sd < 0)use_log(loglevel::DEBUG) << "socket failure!";use_log(loglevel::DEBUG) << "socket success!";// os自己認識用戶端機器的ip+隨機會生成端口號port自動綁定與解除;故無需bind/程序終止立刻解除故查不到狀態string ip = argv[1]; // string內置機制不會直接拷貝指針uint16_t port = stoi(argv[2]); // 先構造string//初始化des套接字:sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = inet_addr(ip.c_str());
//死循環式等待給服務端發送信息: while (1){string mess;getline(cin, mess);socklen_t len = sizeof(local);//發送:int so = sendto(sd, mess.c_str(), mess.size(), 0, (sockaddr *)&local, len); // 輸入型參數if (so < 0)use_log(loglevel::DEBUG) << "sendto failure!";//接收:char buff[1024] = {0};ssize_t rm= recvfrom(sd, buff, sizeof(buff) - 1, 0, (sockaddr *)&local, &len);if (rm< 0) use_log(loglevel::DEBUG) << "recvfrom failure!";cout<<"查詢結果是:"<< buff<<endl;;}
}
udpserver.cc:
#include"udpserver.hpp"
#include "addr.hpp"
#include"dict.hpp"
// string echo_repeat(string mess,inetaddr addr){
// string ret;
// ret+="return ";
// ret+=mess;
// return ret;// }
int main(int argc ,char* argv[]){// if(argc != 3)// {// std::cerr << "please use: " << argv[0] << " ip"<<" port" << std::endl;// return 1;// }if(argc != 2){std::cerr << "please use: " << argv[0] <<" port" << std::endl;return 1;}consolestrategy;//string ip=argv[1];//string內置機制不會直接拷貝指針// uint16_t port=stoi(argv[2]);//先構造stringuint16_t port=stoi(argv[1]);// unique_ptr<udpserver> ur=make_unique<udpserver>(ip,port);//unique_ptr<udpserver> ur=make_unique<udpserver>(port);//回調://1`加載翻譯詞典:dict dt;dt.loaddict();//2`服務端啟動接受信息后進行查找功能:unique_ptr<udpserver> ur=make_unique<udpserver>(port,[&dt](string mess,inetaddr ar)->string{return dt.translate(mess,ar);});ur->init();ur->start();}
udpserver.hpp:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include<memory>
#include<functional>
#include "log.hpp"
#include "addr.hpp"
using namespace std;
const int delfd = -1;using func_t=function<string(string,inetaddr)>;
// consolestrategy; 不允許全局直接使用操作;只允許定義聲明等等class udpserver
{
public:// udpserver(string ip, uint16_t port) : _ip(ip), _port(port),// _isrunning(0), _socketfd(delfd) {}//這里服務器不能綁死否則只能接受指定的主機發來的信息了//udpserver( uint16_t port) : _port(port), _isrunning(0), _socketfd(delfd) {}udpserver( uint16_t port,func_t func) : _port(port), _isrunning(0), _socketfd(delfd),_func(func) {}void init(){// 1`創建套接字:_socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 網絡通信/用戶數據報;返回文件描述符if (_socketfd < 0)use_log(loglevel::DEBUG) << "socket failure!";use_log(loglevel::DEBUG) << "socket success!";// 2`socket綁定信息:sockaddr_in local;// char sin_zero[8];填充字節,使 sockaddr_in 和 sockaddr 長度相同//套接字結構體初始化:bzero(&local, sizeof(local));local.sin_family = AF_INET;//網絡通信local.sin_port = htons(_port); // 把對應的16位轉化成網絡序列// 內置的把string轉化成大端類型的網絡序列;也可手動操作(來回轉化比較麻煩系統直接提供)// ntohs()網絡序列轉回本地//. local.sin_addr.s_addr = inet_addr(_ip.c_str());local.sin_addr.s_addr =INADDR_ANY ;// 或者直接輸入0;可以理解成匹配同一主機的不同ip(多個網卡)的任意des-port相同的不同進程發來的信息int n = bind(_socketfd, (sockaddr *)&local, sizeof(local));//程序終止bind的網絡信息自動解除if (n < 0)use_log(loglevel::DEBUG) << "bind failure!";use_log(loglevel::DEBUG) << "bind success!";}void start(){// 服務器用不掛掉->死循環:_isrunning = 1;while (_isrunning){char buff[1024] = {0};sockaddr_in per;socklen_t len = sizeof(per); // 套接字長度就是字節數ssize_t rm = recvfrom(_socketfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&per, &len); // 輸出型參數故取地址if (rm< 0) use_log(loglevel::DEBUG) << "recvfrom failure!";buff[rm]=0;string per_addr= inet_ntoa(per.sin_addr);uint16_t per_port=ntohs(per.sin_port);cout<< "$client :[addr: "<<per_addr<<" port: "<<per_port<<" ] say: "<<buff<<endl;//string res="server say:";// res+=buff;//int so=sendto(_socketfd,res.c_str(),res.size(),0,(sockaddr *)&per,len);//這里接收兩個全都對套接字用的是指針:繼承多態效果//回調函數,最后傳給clientstring ans= _func(buff,per);int so=sendto(_socketfd,ans.c_str(),ans.size(),0,(sockaddr *)&per,len);//這里接收兩個全都對套接字用的是指針:繼承多態效果//輸入型參數if (so < 0) use_log(loglevel::DEBUG) << "sendto failure!";}}~udpserver(){}private://string _ip;uint16_t _port;int _socketfd;bool _isrunning;func_t _func;
};
間接包含部分:
就是上面展示的log.hpp/mutex.hpp/Makefile等等;這里就不展示了!!!
2.5基于UDP實現的server-clients通信改造多人聊天室(服務端多線程版本):
實現思路:
可能存在多個ip不同的用戶端給服務端發信息;而服務端需要全部給這些連接服務端的用戶端全部發送一遍(服務端收到的信息)--〉客戶端任務:給服務端發信息;服務端任務:全部轉給客戶端一遍。妳因為客戶端如果先recv再sendto;那么這里會阻塞住;因此效果不太好!
我們想要的是只有服務端收到信息就發給客戶端-->因此客戶端的接受和發送兩個線程同時進行。而服務端一個線程來回處理的時候只能處理完一個發送任務再去按收,效率太慢了,因此改成線程池;但是對應這個route;它底層需要插入連按客戶的addr;因此我們這里用的vector;臨界資源-->線程不安全;加個鎖就ok了(規定用戶只要QUIT;然后服務端轉發一遍;用用戶端自動結束recv和sendto線程)! ! !
因此,我們把服務端多線程化去執行客戶端發來的信息然后去群發(應用多線程);但是儲存客戶ip+port的數組是全局的(線程不安全)故還需加鎖!!!【引入多線程就要考慮是否加鎖問題】
下面我們?采用線程池來模擬多線程(但是用戶上限有限制),群發調用回調方法進行發送:
效果展示:
當多個客戶端進行連接通信的時候類似這樣:
? ? ? ?這里我們采用一個機器輸入另一個窗口進行顯示群發消息;因此又對客戶端標準錯誤進行了重定向到?另一臺機器!
客戶端recvfrom處:
echo "1">/dev/pts/2//查詢屬于哪臺機器
./udpclient 127.0.0.1 8080 2>/dev/pts/2//進行對應重定向
因此我們就可以采取上述方式進行執行客戶端程序!
效果:
代碼實現:
主要代碼部分:
udpclient.cc:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <memory>
#include "log.hpp"
#include"thread.hpp"
using namespace std;
using namespace td;
//先設置成全局的方便函數內使用
string ip;
uint16_t port;
pthread_t id;//方便后序退出時候回收對應線程
int flag=0;//標記用戶QUIT后方便終止讀與收兩個線程
int sd;
void Recv(){while(1){char buff[1024] = {0};sockaddr_in other;socklen_t len;ssize_t rm= recvfrom(sd, buff, sizeof(buff) - 1, 0, (sockaddr *)&other, &len);if (rm< 0) use_log(loglevel::DEBUG) << "recvfrom failure!";buff[rm]=0;cerr<<buff<<endl;/////////////////方便后序重定向}
}void Send(){sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = inet_addr(ip.c_str());socklen_t len = sizeof(local);string tip="我上線了 ";int so = sendto(sd, tip.c_str(), tip.size(), 0, (sockaddr *)&local, len); //cout<<so<<endl;while (1){ //cout<<"Please Enter#"<<endl;string mess;getline(cin, mess);//發送:int so = sendto(sd, mess.c_str(), mess.size(), 0, (sockaddr *)&local, len); // 輸入型參數if (so < 0) use_log(loglevel::DEBUG) << "sendto failure!";if(mess=="QUIT") {flag=1;//終止讀線程pthread_cancel(id);}}
}
int main(int argc, char *argv[])
{if (argc != 3){cerr << " please use: " << argv[0] << " server_ip server_port" << std::endl;return 1;}// 創建套接字:sd = socket(AF_INET, SOCK_DGRAM, 0); // 網絡通信/用戶數據報if (sd < 0)use_log(loglevel::DEBUG) << "socket failure!";use_log(loglevel::DEBUG) << "socket success!";// os自己認識用戶端機器的ip+隨機會生成端口號port自動綁定與解除;故無需bind/程序終止立刻解除故查不到狀態ip = argv[1]; port = stoi(argv[2]); //創建兩個線程去執行接收和發送兩個任務;Thread t1(Recv);Thread t2(Send);t1.start();t2.start();id=t2.Id();t2.join();if(flag==1) pthread_cancel(t1.Id());t1.join();}
udpserver.cc:
#include"udpserver.hpp"
#include "addr.hpp"
#include"route.hpp"
#include"threadpool.hpp"
#include<functional>
// string echo_repeat(string mess,inetaddr addr){
// string ret;
// ret+="return ";
// ret+=mess;
// return ret;// }using fn=function<void()>;
int main(int argc ,char* argv[]){// if(argc != 3)// {// std::cerr << "please use: " << argv[0] << " ip"<<" port" << std::endl;// return 1;// }if(argc != 2){std::cerr << "please use: " << argv[0] <<" port" << std::endl;return 1;}consolestrategy;//string ip=argv[1];//string內置機制不會直接拷貝指針// uint16_t port=stoi(argv[2]);//先構造stringuint16_t port=stoi(argv[1]);// unique_ptr<udpserver> ur=make_unique<udpserver>(ip,port);//unique_ptr<udpserver> ur=make_unique<udpserver>(port);//回調:Route r;//2`服務端啟動接受信息后進行查找功能://單線程:
// unique_ptr<udpserver> ur=make_unique<udpserver>(port,[&r](int fd,string mess,inetaddr ar){
// r.messroute(fd,mess,ar);
// });//線程池版本:auto tp=Threadpool<fn>::getinstance();unique_ptr<udpserver> ur=make_unique<udpserver>(port,[&r,&tp](int fd, string mess,inetaddr ar){//因此可以把messroute綁定成無參對象用bind:fn tk=std::bind(&Route::messroute,&r,fd,mess,ar);//此時就不用給線程池任務傳參了;這里this指針必須給出值tp->equeue(tk);//只允許傳遞無參數無返回值的對象或者函數進行處理
});ur->init();ur->start();}
udpserver.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <memory>
#include <functional>
#include "log.hpp"
#include "addr.hpp"
using namespace std;
const int delfd = -1;using func_t = function<void(int, string, inetaddr)>;
// consolestrategy; 不允許全局直接使用操作;只允許定義聲明等等class udpserver
{
public:// udpserver(string ip, uint16_t port) : _ip(ip), _port(port),// _isrunning(0), _socketfd(delfd) {}// 這里服務器不能綁死否則只能接受指定的主機發來的信息了// udpserver( uint16_t port) : _port(port), _isrunning(0), _socketfd(delfd) {}udpserver(uint16_t port, func_t func) : _port(port), _isrunning(0), _socketfd(delfd), _func(func) {}void init(){// 1`創建套接字:_socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 網絡通信/用戶數據報;返回文件描述符if (_socketfd < 0)use_log(loglevel::DEBUG) << "socket failure!";use_log(loglevel::DEBUG) << "socket success!";// 2`socket綁定信息:sockaddr_in local;// char sin_zero[8];填充字節,使 sockaddr_in 和 sockaddr 長度相同// 套接字結構體初始化:bzero(&local, sizeof(local));local.sin_family = AF_INET; // 網絡通信local.sin_port = htons(_port); // 把對應的16位轉化成網絡序列// 內置的把string轉化成大端類型的網絡序列;也可手動操作(來回轉化比較麻煩系統直接提供)// ntohs()網絡序列轉回本地//. local.sin_addr.s_addr = inet_addr(_ip.c_str());local.sin_addr.s_addr = INADDR_ANY;// 或者直接輸入0;可以理解成匹配同一主機的不同ip(多個網卡)的任意des-port相同的不同進程發來的信息int n = bind(_socketfd, (sockaddr *)&local, sizeof(local)); // 程序終止bind的網絡信息自動解除if (n < 0)use_log(loglevel::DEBUG) << "bind failure!";use_log(loglevel::DEBUG) << "bind success!";}void start(){// 服務器用不掛掉->死循環:_isrunning = 1;while (_isrunning){ // cout<<"再次準備"<<endl;char buff[1024] = {0};sockaddr_in per;socklen_t len = sizeof(per);// cout<<"準備讀信息"<<endl; // 套接字長度就是字節數ssize_t rm = recvfrom(_socketfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&per, &len); // 輸出型參數故取地址if (rm < 0)use_log(loglevel::DEBUG) << "recvfrom failure!";buff[rm] = 0;string per_addr = inet_ntoa(per.sin_addr);uint16_t per_port = ntohs(per.sin_port);// cout<< "$client :[addr: "<<per_addr<<" port: "<<per_port<<" ] say: "<<buff<<endl;// string res="server say:";// res+=buff;// int so=sendto(_socketfd,res.c_str(),res.size(),0,(sockaddr *)&per,len);//這里接收兩個全都對套接字用的是指針:繼承多態效果// 回調函數,最后傳給聊天室所有用戶:inetaddr ir(per);_func(_socketfd, buff, ir);}}~udpserver(){}private:// string _ip;uint16_t _port;int _socketfd;bool _isrunning;func_t _func;
};
addr.hpp:
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<cstring>
using namespace std;
class inetaddr
{public:// 網絡序列轉主機:inetaddr(sockaddr_in &addr) : _addr(addr){_port = ntohs(_addr.sin_port);char buff[1024];inet_ntop(AF_INET,&_addr.sin_addr,buff,sizeof(_addr));_ip=buff;}// 客戶端主機轉網絡:inetaddr(const string ip, uint16_t port) : _ip(ip), _port(port){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);}// 服務端主機轉網絡:inetaddr( uint16_t port) : _port(port){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);}sockaddr_in *addrptr() { return &_addr; }socklen_t addrlen(){return sizeof(_addr);}string ip() { return _ip; }uint16_t port() { return _port; }bool operator==(const inetaddr sockin){return _ip == sockin._ip && _port == sockin._port;}sockaddr_in &sockaddr() { return _addr; } // 這里返回引用否則右值無地址可取(sendto)string get_userinfo(){return ip() + " : " + to_string(port());}~inetaddr() {}private:sockaddr_in _addr;string _ip;uint16_t _port;
};
route.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <memory>
#include <functional>
#include <vector>
#include "log.hpp"
#include "addr.hpp"
using namespace std;class Route
{private:bool is_exit(inetaddr ir){for(int i=0;i<_peers.size();i++){if(_peers[i]==ir) return 1;}return 0;}void addpeer(inetaddr ir){_peers.push_back(ir);use_log(loglevel::DEBUG)<<"成功添加一個用戶:"<<ir.get_userinfo();}void deletepeer(inetaddr ir){for(auto iter=_peers.begin();iter!=_peers.end();iter++ ){if(*iter==ir){_peers.erase(iter);use_log(loglevel::DEBUG)<<"成功刪除一個用戶:"<<ir.get_userinfo();}iter=_peers.end()-1;//迭代器失效 再使用會coredump// break ;}}
public:Route() {}void messroute(int sockfd,string mess,inetaddr ir){mutexguard md(_m);//多線程互斥;vector是共享的;stl不是線程安全的if(!is_exit(ir)) addpeer(ir);string ans=" ["+ir.get_userinfo()+"] say: "+mess;for(auto&peer:_peers){int so=sendto(sockfd,ans.c_str(),ans.size(),0,(sockaddr *)&(ir.sockaddr()),sizeof(ir.sockaddr()));if (so < 0) use_log(loglevel::DEBUG) << "sendto failure!";}if(mess=="QUIT") {deletepeer(ir); }}~Route() {}private:vector<inetaddr> _peers;mutex _m;
};
間接包含部分:
cond.hpp:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <unistd.h>
#include <vector>
#include <pthread.h>
#include "mutex.hpp"class cond
{
public:cond() { pthread_cond_init(&_cond, nullptr); }void Wait(mutex &mx)
{
int n = pthread_cond_wait(&_cond, mx.getmutex());
(void)n;
}void notify(){int n = pthread_cond_signal(&_cond);(void)n;}void allnotify(){int n = pthread_cond_broadcast(&_cond);(void)n;}~cond() { pthread_cond_destroy(&_cond); }private:pthread_cond_t _cond;
};
thread.hpp:
#ifndef THREAD_H
#define THREAD_H
#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
#include<unistd.h>
#include<vector>
#include<queue>
using namespace std;namespace td
{static uint32_t num=1; class Thread{using func_t = function<void()>;public:Thread(func_t func) : _tid(0), _res(nullptr),_func(func), _isrunning(false),_isdetach(false){_name="Thread-"+to_string(num++);}static void *Routine(void *arg){Thread *self =static_cast<Thread*>(arg);//需要查看是否進行了start前的detach操作:pthread_setname_np(self->_tid, self->_name.c_str());// cout<<self->_name.c_str()<<endl;self->_isrunning=1;if(self->_isdetach) pthread_detach(self->_tid);self->_func();return nullptr;}bool start(){if(_isrunning) return false;int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0){//cerr << "create thread error: " << strerror(n) << endl;return false;}else{//cout << _name << " create success" << endl;return true;}}bool stop(){if(_isrunning){int n= pthread_cancel(_tid);if (n != 0){//cerr << "cancel thread error: " << strerror(n) << endl;return false;}else{_isrunning = false;// cout << _name << " stop" << endl;return true;}}return false;}bool detach(){if(_isdetach) return false;if(_isrunning)pthread_detach(_tid);//創建成功的線程進行分離操作_isdetach=1;//未創線程進行分離只進行標記return true;}bool join(){if(_isdetach) {// cout<<"線程 "<<_name<<"已經被分離;不能進行join"<<endl;return false;}//只考慮運行起來的線程了:int n = pthread_join(_tid, &_res);if (n != 0){//std::cerr << "join thread error: " << strerror(n) << std::endl;}else{//std::cout << "join success" << std::endl;}return true;}pthread_t Id() {return _tid;}~Thread() {}private:pthread_t _tid;string _name;void *_res;func_t _func;bool _isrunning;bool _isdetach;};
}#endif
threadpoll.hpp:
#pragma once
#include "log.hpp"
#include "cond.hpp"
#include "thread.hpp"
using namespace td;
const int N = 5;
template <class T>
class Threadpool
{
private:Threadpool(int num = N) : _size(num),_sleepingnums(0),_isrunning(0){for (int i = 0; i < _size; i++){_threads.push_back(Thread([this](){ this->handletask(); }));}}// 單例只允許實例出一個對象Threadpool(const Threadpool<T> &t) = delete;Threadpool<T> &operator=(const Threadpool<T> &t) = delete;void Start(){if (_isrunning)//勿忘標記位return;_isrunning = true;for (int i = 0; i < _size; i++){// use_log(loglevel::DEBUG) << "成功啟動一個線程";;_threads[i].start();}}public:static Threadpool<T> *getinstance()//必須采用靜態(不創建對象的前提下進行獲得類指針){if (_ins == nullptr) //雙重判斷-->假設一個線程很快完成單例化;然后后面的一群線程正好來了;如果沒有雙層判斷;就會阻塞一個個發現不是空返回_ins;//非常慢;為了提高效率這樣就不用加鎖在一個個判斷了還能保證線程安全。{{mutexguard mg(_lock);//靜態鎖;if (_ins == nullptr){_ins = new Threadpool<T>();use_log(loglevel::DEBUG) << "創建一個單例";_ins->Start();//創建單例自啟動}}}use_log(loglevel::DEBUG) << "獲得之前創建的一個單例";return _ins;}void stop()//不能立刻停止如果隊列有任務還需要線程完成完然后從handl函數退出即可{mutexguard mg(_Mutex);//這里為了防止多線程調用線程池但是單例化杜絕了這點if (_isrunning){_isrunning = 0;//因此只搞個標記use_log(loglevel::DEBUG) << "喚醒所有線程";//被喚醒后沒有搶到鎖的線程雖然休眠但是不用再次喚醒了;os內設它就會讓它所只要出現就去競爭_Cond.allnotify();//萬一還有其他線程還在休眠就需要都喚醒-->全部子線程都要退出}return;}void join(){// mutexguard mg(_Mutex);這里不能加鎖;如果join的主線程快的話;直接就拿到鎖了// 即使喚醒子線程;他們都拿不到鎖故繼續休眠等待鎖;而主線程join這一直拿著 鎖等子線程// 故造成了---->死鎖問題// 但是可能出現多線程同時訪問;后面把它設置單單例模式就好了use_log(loglevel::DEBUG) << "回收線程";for (int i = 0; i < _size; i++)_threads[i].join();}bool equeue(const T &tk){mutexguard mg(_Mutex);if (_isrunning){_task.push(tk);if (_sleepingnums == _size)_Cond.notify(); // 全休眠必須喚醒一個執行//use_log(loglevel::DEBUG) << "成功插入一個任務并喚醒一個線程";return true;}return false;}void handletask(){ // 類似popqueuechar name[128];//在線程局部存儲開;不用加鎖非全局就行pthread_getname_np(pthread_self(), name, sizeof(name));while (1){T t;{mutexguard gd(_Mutex);while (_task.empty() && _isrunning)//休眠條件{_sleepingnums++;_Cond.Wait(_Mutex);_sleepingnums--;// cout<<1<<endl;}if (_task.empty() && !_isrunning)//醒來后發現符合退出條件就退出{use_log(loglevel::DEBUG) << name << "退出";break; // 代碼塊執行完了;鎖自動釋放}t = _task.front();_task.pop();}t();}}~Threadpool() {}private:vector<Thread> _threads;int _size;mutex _Mutex;cond _Cond;queue<T> _task;bool _isrunning;int _sleepingnums;//僅僅只是聲明static Threadpool<T> *_ins;static mutex _lock;
};
//類內聲明內外定義初始化
template<class T>
Threadpool<T>*Threadpool<T> ::_ins=nullptr;
template<class T>
mutex Threadpool<T> ::_lock;
剩下的就是mutex/log.hpp見上面代碼展示;這里就不重復了!
優化關于網絡序列和本機序列之間的轉化:
之前對port的網絡本機序列轉化:
uint16 t per port=ntohs(per.sin_port)
local.sin port = htons( port);
之前對ip的網絡本機序列轉化:
local.sin addr.s addr = inet addr( ip.c str());
string per addr= inet ntoa(per.sin_addr);
?直接上結論:
對port的操作可以不用變;但是對于ip此時就要更換了;
原因:
對于這個ip之間轉換是不安全的;也就是存存儲在靜態區的塊地址;當多次調用它會被覆蓋掉!此外由于靜態區也不是線程安全的!
雖然不需要手動釋放內存(new);man 手冊上說,inet_ntoa 函數,是把這個返回結果放到了靜態存儲區.這個時候不需要我們手動進行釋放;但是每次調用這個函數它都會從原來的位置進行覆蓋!
看一下效果:?
?明顯就出現問題了!!!
因此對于ip的轉換我們引入了新的函數(這里p就代表進程):
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);
#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
結合例子解釋下:
?
因此總結下當我們想port的網絡本機之間轉化就用ntohs/htons;當ip的本機與網絡轉換就用inet_pton/inet_ntop!?
這倆函數我們在上面實現聊天室的時候對于封裝的addr.hpp內就重新應用了!!!?
其次就是對于服務端;也就是server.hpp我們是不允許拷貝等等的;因此需要禁用掉類似的接口-->此時我們采取繼承的方式;此時構建server的類的時候需要先構建它繼承的基類;發現基類被禁用了直接報錯-->后續實現tcp的時候我們再采用!
三·常用的網絡指令:
3.1ifconfig:
使用該指令進行查看配置信息:
上面的是子網ip;下面的是本地環回ip!
本地環回:要求c、s必須在一臺機器上,表明我們是本地通信,client發送的數據,不會被推送到網絡而是在OS內部,轉一圈直接交給對應的服務器端;如果被通信的端綁定的是這個環回地址:
再進行通信那么就是本地通信了;不會發送到網絡;經常用來進行網絡代碼的測試!
子網ip:由本地局域網(LAN)自動分配給連接的設備的ip!(我們進行本地實現udp通信就是拿它或者本地環回作為目的ip進行連接服務端的)
3.2netstat:
netstat 是一個用來查看網絡狀態的重要工具!
用法:netstat+ -【選項】
選項:
n 拒絕顯示別名, 能顯示數字的全部轉化成數字l 僅列出有在 Listen (監聽) 的服務狀態
?p 顯示建立相關鏈接的程序名
t (tcp)僅顯示 tcp 相關選項
u (udp)僅顯示 udp 相關選項
a (all)顯示所有選項, 默認不顯示 LISTEN 相關->之后我們的TCP通信會用到!
下面我們來演示下效果(選項誰在前誰在后無影響) :
根據需要進行顯示:
程序終止后自動解除這個信息:?
3.3ping:
使用 ping 工具來測試本地計算機與服務器之間的網絡連接情況。
下面我們測試一下:
ping www.qq.com
這里了解下即可!!!?
3.4pidof:
在查看服務器的進程 id 時非常方便!
用法 :pidof [進程名]來查看對應pid
四·本篇小結:
通過本篇文章;在有網絡概念的基礎上;來更清楚認識網絡通信是如何進行的,關鍵(ip+port->socket);之后又認識了相關socket相關地址結構體底層結構,簡單協議介紹以及一些網絡通信(udp)的一些api接口后來基于UDP網絡通信實現的簡單的server-client之間的應答/詞典翻譯/多人聊天室等小項目最后補充了點相關小指令;博主學習這塊也是用了好幾天;然后整理筆記;最近有時間復盤一下整理的博客;歡迎大家閱讀;下期找時間更新TCP網絡通信!!!