🔥個人主頁🔥:孤寂大仙V
🌈收錄專欄🌈:Linux
🌹往期回顧🌹: 【Linux筆記】——網絡基礎
🔖流水不爭,爭的是滔滔不息
- 一、UDPsocket編程
- UDPsocket編程的一些基本接口
- 封裝sockaddr_in
- 二、單線程網絡聊天室
- Udpserver.hpp服務端
- Udpclient.cc
- 三、多線程網絡聊天室
- 四、網絡字典
一、UDPsocket編程
UDP(User Datagram Protocol)是一種無連接的傳輸層協議,提供快速但不可靠的數據傳輸。與TCP不同,UDP不保證數據包的順序、可靠性或重復性,適用于實時性要求高的場景(如視頻流、游戲)。
UDPsocket編程的一些基本接口
創建套接字
int socket(int domain, int type, int protocol);
創建UDP套接字,第一個參數是選擇ipv4還是ipv6,第二個參數是選擇UDP還是TCP,第三個參數為0就可以。
ipv4寫AF_INET,ipv6寫 AF_INET6 。UDP寫SOCK_DGRAM,TCP寫SOCK_STREAM 。
套接字綁定本地地址和端口(服務端用)
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
第一個參數是套接字,第二個參數是指向 struct sockaddr 的指針,用于指定本地地址(需要強制類型轉換),第三個參數是這個sockaddr_in的結構體。
向指定目標發送數據
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
第一個參數套接字,第二個參數是存儲發送信息的buffer,第二個是這個buffer的大小,第三個標志位填0就行,第四個發送數據那端的struct sockaddr的指針,第五個是struct sockaddr的大小。
接收數據
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
第一個參數套接字,第二個參數接收buffer,第三個是這個buffer的大小,第四個是標準位寫0就行,第五個是struct sockaddr的指針,第六個是struct sockaddr的大小的指針
關閉套接字
close()
和關閉文件描述符一樣
封裝sockaddr_in
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;class InetAddr
{
public:InetAddr(struct sockaddr_in& Addr) //從網絡中也就是客戶端獲取的信息:_Addr(Addr){_ip=inet_ntoa(_Addr.sin_addr);_port=ntohs(_Addr.sin_port);}string Ip(){return _ip;}uint16_t Port(){return _port;}string StringAddr() const{return _ip+":"+to_string(_port);}const struct sockaddr_in& NAddr(){return _Addr;}bool operator==(const InetAddr& Addr){return Addr._ip==_ip && Addr._port==_port;}~InetAddr(){}private:struct sockaddr_in _Addr;string _ip;uint16_t _port;
};
每次寫服務端或者是客戶端的時候都要寫struct sockaddr_in那一套太麻煩了,所以進行封裝。比如說上面的這個構造,就是從客戶端中傳來的信息轉化為主機地址。
注意啊,ip和端口號,網絡到主機需要轉換序列,主機到網絡也需要轉換序列。
二、單線程網絡聊天室
一個網絡聊天室需要客戶端和服務端,客戶端用戶使用,然后把信息推送給服務端服務器接收到信息把信息路由給所有用戶這時網絡聊天室就形成了。所以客戶端需要有發送的功能,服務器有接收的功能。
Udpserver.hpp服務端
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace std;
using namespace LogModule;
using func_t=function<void(int ,const string&,InetAddr&)>;
class Udpserver
{
public:Udpserver(uint16_t port,func_t func):_port(port),_sockfd(0),_isrunning(false),_func(func){}void Init(){_sockfd =socket(AF_INET, SOCK_DGRAM,0);if(_sockfd <0){LOG(LogLevel::FATAL)<<"創建套接字失敗";exit(1);}LOG(LogLevel::INFO)<<"創建套接字成功";struct sockaddr_in local;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_addr.s_addr=INADDR_ANY;local.sin_port=htons(_port);int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));if(n<0){LOG(LogLevel::FATAL)<<"綁定失敗";exit(1);}LOG(LogLevel::INFO)<<"綁定成功";}void Start(){_isrunning=true;while(true){char buffer[128];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 client(peer);buffer[n]=0;_func(_sockfd,buffer,client);}}}~Udpserver(){}
private:int _sockfd;uint16_t _port;bool _isrunning;func_t _func;};
這里要引入一個結構體,這個結構體是存儲本地ip地址和端口信息的結構體變量。
**struct sockaddr_in local; 是在進行 IPv4 網絡編程時,定義的一個用于存儲本地 IP 地址和端口信息的結構體變量,屬于 IPv4 地址族的套接字地址結構體。**這是一個用于存儲 IPv4 地址信息 的結構體,定義在頭文件 <netinet/in.h> 中。它是給 bind()、connect()、sendto()、recvfrom() 等函數傳參用的,通常你寫服務器或客戶端都要用到它。
void Init(){_sockfd =socket(AF_INET, SOCK_DGRAM,0);if(_sockfd <0){LOG(LogLevel::FATAL)<<"創建套接字失敗";exit(1);}LOG(LogLevel::INFO)<<"創建套接字成功";struct sockaddr_in local;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_addr.s_addr=INADDR_ANY;local.sin_port=htons(_port);int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));if(n<0){LOG(LogLevel::FATAL)<<"綁定失敗";exit(1);}LOG(LogLevel::INFO)<<"綁定成功";}
初始化,創建套接字,綁定本地的地址和端口號,這是socket編程必須寫的。在bind之前創建了套接字地址結構體,用來存儲本地服務器的ip和端口號,注意這個ip我們設置成了INADDR_ANY
,因為這里是服務器不要設置成固定的ip。寫成INADDR_ANY
保證ip是動態的。
void Start(){_isrunning=true;while(true){char buffer[128];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 client(peer);buffer[n]=0;_func(_sockfd,buffer,client);}}}
服務端接收信息,這里的socket_in peer 是客戶端傳來的套接字地址結構體。用recvfrom接收信息如果能夠接收到信息,返回值大于0,執行這個回調函數。回調到udpserver.cc。
#include "Udpserver.hpp"
#include "Route.hpp"
#include <memory>using namespace std;
using namespace LogModule;int main(int argc, char *argv[])
{uint16_t port = stoi(argv[1]);Enable_Console_Log_Strategy(); // 啟用控制臺輸出Route r; // 服務器路由unique_ptr<Udpserver> usvr = make_unique<Udpserver>(port,[&r](int _sockfd, const string &messages, InetAddr &peer){ r.MessageRoute(_sockfd, messages, peer); });usvr->Init();usvr->Start();return 0;
}
Udpserver 作為底層網絡接收模塊,不關心具體如何處理數據,它只負責接收,然后通過你傳入的 回調函數,把接收到的數據“上傳”給上層(這里是 Route::MessageRoute())去決定如何處理 —— 這就是典型的 解耦設計。回調到這里進行路由,路由的對象已經實例化好了,到這里直接在lambda表達式中直接對路由對象的函數進行調用就可以了。下面是路由的方法。
#pragma once#include <iostream>
#include <string>
#include <vector>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Udpserver.hpp"using namespace std;
using namespace LogModule;
using namespace MutexModule;
class Route
{
public:Route(){}bool Exist(InetAddr &peer){for (auto &user : _online_user){if (user == peer){return true;}}return false;}void Adduser(InetAddr &peer){LOG(LogLevel::INFO) << "新增了一個在線用戶" <<peer.StringAddr();_online_user.push_back(peer);}void DeleteUser(InetAddr &peer){for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++){if (*iter == peer){LOG(LogLevel::INFO) << "刪除了一個在線用戶" <<peer.StringAddr();_online_user.erase(iter);break;}}}void MessageRoute(int sockfd, const std::string &message, InetAddr &peer) // 路由功能{LockGuard lockguard(_mutex); // 加鎖if (!Exist(peer)){Adduser(peer);}string send_messages = peer.StringAddr() + "#" + message; // 發過來的信息for (auto &user : _online_user){sendto(sockfd, send_messages.c_str(), send_messages.size(), 0, (struct sockaddr *)&(user.NAddr()), sizeof(user.NAddr()));}// 這個用戶一定已經在線了if (message == "QUIT"){LOG(LogLevel::INFO) << "刪除一個在線用戶: " << peer.StringAddr();DeleteUser(peer);}}~Route(){}private:vector<InetAddr> _online_user;Mutex _mutex;
};
路由服務,就是把服務器收到了信息,分發給所有客戶端的用戶。加鎖線程安全,如果這個用戶不存在就在存儲用戶的數組中新加進去,遍歷整個數組把信息都發回去。如果這個用戶已經在線了就刪除這個用戶。
本項目采用回調機制實現業務邏輯與底層網絡模塊的解耦。Udpserver 僅負責網絡收發,而具體的處理邏輯通過 lambda 回調函數注冊在 main() 中,實現了業務的靈活注入和高擴展性。
Udpclient.cc
客戶端要有發送信息的能力,也有有收到應答的能力,服務器有路由功能客戶端要接收路由的信息。
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace std;
using namespace LogModule;
using namespace ThreadModule;int _sockfd = 0;
string server_ip;
uint16_t server_port = 0;
pthread_t id;void Recv()
{while (true){char buffer[128];struct sockaddr_in peer;socklen_t len=sizeof(peer);int n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;cout << buffer << endl;}}
}void Send()
{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(server_ip.c_str());server.sin_port = htons(server_port);while (true){string input;cout << "請輸入" << endl;getline(cin, input);int n = sendto(_sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));if (n < 0){LOG(LogLevel::FATAL) << "客戶端發送信息失敗";}if (input == "QUIT"){pthread_cancel(id);break;}}
}int main(int argc, char *argv[])
{server_ip = argv[1];server_port = stoi(argv[2]);_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "創建套接字失敗";}LOG(LogLevel::INFO) << "創建套接字成功";// 2. 創建線程Thread recver(Recv);Thread sender(Send);recver.Start();sender.Start();recver.Join();sender.Join();return 0;
}
這里設計的是單線程的,客戶端創建的時候,創建兩個線程一個收一個發。收方法,就還是老一套recvfrom。發方法還是sendto。
單線程版網絡聊天室:源碼
三、多線程網絡聊天室
Udpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "ThreadPool.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace std;
using namespace LogModule;using func_t =function<void (int sockfd,const string&,InetAddr&)>;const int defaultfd = -1;class Udpserver
{
public:Udpserver(uint16_t port,func_t func):_port(port),_sockfd(defaultfd),_isrunning(false),_func(func){}void Inet() //初始化{_sockfd=socket(AF_INET,SOCK_DGRAM,0);//創建套接字if(_sockfd<0){LOG(LogLevel::FATAL)<<"socket false";exit(1);}LOG(LogLevel::INFO)<<"socket success";struct sockaddr_in local;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local)); //綁定if(n<0){LOG(LogLevel::FATAL)<<"bind false";exit(2);}LOG(LogLevel::INFO)<<"bind success";}void Start(){_isrunning=true;while (true){char buffer[1024]; //存收到的信息struct sockaddr_in peer;socklen_t len=sizeof(peer);ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);//收if(n>0){buffer[n]=0;InetAddr cli(peer);_func(_sockfd,buffer,cli);}}}~Udpserver(){}
private:bool _isrunning;int _sockfd;uint16_t _port;func_t _func;
};
服務器與單線程版本一模一樣。
Udpserver.cc
#include "Udpserver.hpp"
#include "Route.hpp"
#include <memory>using namespace std;
using namespace ThreadPoolModule;using task_t = std::function<void()>;int main(int argc,char* argv[])
{Enable_Console_Log_Strategy(); // 啟用控制臺輸出uint16_t port=stoi(argv[1]);Route r;auto tp = ThreadPool<task_t>::GetInstance();// unique_ptr<Udpserver> usvr = make_unique<Udpserver>(port, // [&r](int _sockfd, const string &messages, InetAddr &peer)// { r.MessageRoute(_sockfd, messages, peer); });unique_ptr<Udpserver> u= make_unique<Udpserver>(port,[&r,&tp](int _sockfd, const string &messages, InetAddr &peer){task_t t=bind(&Route::MessageRoute,&r,_sockfd,messages,peer);tp->Enqueue(t);});u->Inet();u->Start();return 0;
}
我們做的是一個多線程 UDP 網絡聊天室服務端。為了提升并發能力,我們引入了一個線程池(ThreadPool)。網絡部分(接收數據)使用的是 Udpserver,它在接收到消息后通過 回調機制 把業務邏輯“丟”出去。回調函數用的是 lambda 表達式,其中 bind 了 Route::MessageRoute() 和收到的參數,形成一個任務。這個任務再被投遞到 線程池 中執行,實現了網絡收發和邏輯處理解耦 + 多線程并發處理。
Udpclient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace std;
using namespace LogModule;
using namespace ThreadModule;int sockfd = 0;
string server_ip;
uint16_t server_port = 0;
pthread_t id;void Recv()
{while(true){char buffer[1024];struct sockaddr_in peer;socklen_t len=sizeof(peer);ssize_t n=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);if(n>0){buffer[n]=0;cout<<buffer<<endl;}}
}void Send()
{struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(server_port);server.sin_addr.s_addr=inet_addr(server_ip.c_str());const std::string online = "inline";sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));while(true){string input;cout<<"請輸入"<<endl;getline(cin,input);ssize_t n=sendto(sockfd,input.c_str(),input.size(),0,(struct sockaddr*)&server,sizeof(server));if (n < 0){LOG(LogLevel::FATAL) << "客戶端發送信息失敗";}if (input == "QUIT"){pthread_cancel(id);break;}}}int main(int argc,char* argv[])
{server_ip=argv[1];server_port=stoi(argv[2]);sockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){LOG(LogLevel::FATAL)<<"socket false";}LOG(LogLevel::INFO)<<"socket success";Thread recver(Recv);Thread sender(Send);recver.Start();sender.Start();id = recver.Id();recver.Join();sender.Join();return 0;
}
基本也與單線程版本一樣。
注意:
這里引入線程池是服務器高并發接收和處理客戶端消息,比如多個客戶端同時發消息,需要并發處理。客戶端還是需要分別創建了兩個線程分別是收線程和發線程。即使服務端用了線程池,客戶端也要至少兩個線程:一個發一個收,這樣聊天室才能像樣地用起來。客戶端的多線程不是為了并發處理請求,是為了讓用戶能“邊說邊聽”。
多線程網絡聊天室:源碼
四、網絡字典
Udpserver.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Dict.hpp"
using namespace LogModule;
using namespace std;const int num = -1;
using func_t = function<string(const string&,InetAddr&)>;//class Udpserver
{
public:Udpserver(uint16_t port,func_t func): _sockfd(num), _port(port), _isrunning(false),_func(func){}void Init(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::ERROR) << "套接字創建失敗";}LOG(LogLevel::INFO) << "套接字創建成功";struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::ERROR) << "綁定失敗";}LOG(LogLevel::INFO) << "綁定成功";}void Start(){_isrunning=true;while(true){char buffer[128];struct sockaddr_in peer;memset(&peer,0,sizeof(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;InetAddr client(peer);string result= _func(buffer,client);//這里設計為收完就發sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);}}}~Udpserver(){}private:int _sockfd;uint16_t _port;bool _isrunning;func_t _func;
};
都是相同的操作,注意一下收到客戶端傳來的信息,回調去翻譯,然后做出應答翻譯完發回去。
Dict.hpp翻譯模塊
#pragma once
#include <iostream>
#include <string>
#include <map>
#include <fstream>
#include "Udpserver.hpp"
#include "InetAddr.hpp"const string dpath = "./dictionary.txt";
const string sep = ": ";using namespace LogModule;
using namespace std;class Dict
{
public:Dict(string path=dpath):_dict_path(path){}bool LoadDict(){ ifstream in(_dict_path);if(!in.is_open()){LOG(LogLevel::WARNINC)<<"字典打開失敗";return false;}string line;while(getline(in,line)){auto pos=line.find(sep);if(pos==string::npos){LOG(LogLevel::WARNINC)<<"解析"<<line<<"失敗";continue;}string english=line.substr(0,pos);string chinese=line.substr(pos + sep.size());if(english.empty() || chinese .empty()){LOG(LogLevel::WARNINC) << "沒有有效內容: " << line;continue;}_dict.insert(make_pair(english,chinese));}in.close();return true;}string Translate(const string& word,InetAddr& client){auto iter=_dict.find(word);if(iter == _dict.end()){LOG(LogLevel::DEBUG)<<"進入翻譯模塊"<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<word<<"->None";return "None";}LOG(LogLevel::DEBUG)<<"進入翻譯模塊"<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<word<<"->iter->sencond";return iter->second;}~Dict(){}private:string _dict_path;unordered_map<string,string> _dict;
};
用文件流的方式打開英漢文本,判斷是否打開成功,從文件流中逐行讀取,找到中文和英文中間的:,如果沒有找到:解釋失敗。截取一行中的中文和英文,把中文和英文插入map中。上面說的可以算是初始化字典。下面Translate是真正的翻譯模塊,在字典中查找要查的單詞,判斷是否有這個單詞,有單詞返回map中的sencod不就是對應的翻譯了嗎。
Udpserver.cc
#include "Udpserver.hpp"
#include "Dict.hpp"
#include <memory>
int main(int argc,char* argv[])
{Enable_Console_Log_Strategy(); // 啟用控制臺輸出uint16_t port=stoi(argv[1]);Dict dict;dict.LoadDict();unique_ptr<Udpserver> u=make_unique<Udpserver>(port,[&dict](const string& word,InetAddr& cli)->string{return dict.Translate(word,cli);});u->Init();u->Start();return 0;
}
創建dict對象,LoadDict初始化字典,這里是udpserver.hpp中回調函數到這里進行翻譯,lambda表達式這里來執行翻譯的操作。
udp網絡字典:源碼
通過上面的單線程網絡聊天室、多線程網絡聊天室、網絡字典、我們發現都用到了回調函數,來對不同部分進行模塊化,解耦合。網絡設計本身就是基于之前我們說過分層模型設計的,網絡回調機制與協議分層理念是一脈相承的 —— 都在追求模塊化、解耦、職責單一。回調機制本質上是 事件驅動 + 分層解耦 的產物。