C++基于UDP協(xié)議的群聊服務(wù)器開(kāi)發(fā)實(shí)現(xiàn)
服務(wù)器
在服務(wù)器架構(gòu)設(shè)計(jì)中,模塊解耦是保障系統(tǒng)可維護(hù)性的核心準(zhǔn)則。本方案采用分層架構(gòu)將核心功能拆解為通信層與業(yè)務(wù)處理層兩大模塊。值得注意的是,當(dāng)使用TCP協(xié)議時(shí),開(kāi)發(fā)者往往需要額外設(shè)計(jì)協(xié)議抽象層來(lái)解決其字節(jié)流特性導(dǎo)致的消息邊界模糊問(wèn)題(如粘包/拆包處理),并通過(guò)重傳機(jī)制強(qiáng)化傳輸可靠性。而UDP的面向報(bào)文特性天然規(guī)避了消息邊界問(wèn)題,其無(wú)狀態(tài)傳輸模型大幅簡(jiǎn)化了基礎(chǔ)通信層的設(shè)計(jì)復(fù)雜度——這正是我們選擇UDP構(gòu)建輕量化實(shí)時(shí)群聊系統(tǒng)的關(guān)鍵原因。話不多說(shuō),我們直接開(kāi)始!
框架設(shè)計(jì)
創(chuàng)建核心文件:UdpServer.hpp、UdpServer.cc、UdpClient.cc,當(dāng)然不止于這些,在完成這些文件的過(guò)程中會(huì)延伸出更多文件,如果在這里寫(xiě)顯得有些突兀。
- UdpServer.hpp:服務(wù)器相關(guān)的類以及類方法的實(shí)現(xiàn)——主要完成通信功能。
- UdpServer.cc:服務(wù)器主函數(shù)(main)的實(shí)現(xiàn)——對(duì)服務(wù)器接口的調(diào)用,即啟動(dòng)服務(wù)器。
- UdpClient.cc:客戶端主函數(shù)(main)的實(shí)現(xiàn)——啟動(dòng)客戶端,與服務(wù)器通信。
一、通信
上來(lái)直接創(chuàng)建一個(gè)class UdpServer類,而對(duì)于成員變量和成員函數(shù)的設(shè)定。我們得理一理進(jìn)行通信需要完成什么,它完全是套路式的,模板化的。即:打開(kāi)網(wǎng)絡(luò)文件,綁定端口,收數(shù)據(jù)_處理數(shù)據(jù)_發(fā)數(shù)據(jù)。(針對(duì)UDP協(xié)議通信)
根據(jù)這三點(diǎn)我們?cè)O(shè)計(jì)成員函數(shù):
- int _socketfd:網(wǎng)絡(luò)文件描述符。
- uint16_t _port:端口號(hào)。對(duì)于IP地址我們不期望從外部傳入,所以暫且不用設(shè)。
- 數(shù)據(jù)處理函數(shù):這個(gè)成員到后文數(shù)據(jù)處理再設(shè)計(jì)。
對(duì)于成員函數(shù)
- void Init():完成打開(kāi)網(wǎng)絡(luò)文件,綁定端口。
- void Start():?jiǎn)?dòng)服務(wù),完成收數(shù)據(jù)_處理數(shù)據(jù)_發(fā)數(shù)據(jù),其中處理數(shù)據(jù)以回調(diào)的方式完成(為了讓模塊解耦,方便模塊之間的拼接和替換)。
如下:
class UdpServer { public: UdpServer(uint16_t port) : _socketfd(-1), _port(port) { } void Init(); void Start(); private: int _socketfd; uint16_t _port; //...... };
void Init ()
1.打開(kāi)網(wǎng)絡(luò)文件
socket的使用
socket函數(shù)的聲明:
int socket(int domain, int type, int protocol);
功能:打開(kāi)網(wǎng)絡(luò)文件(套接字)。
參數(shù)domain:確定IP地址類型,如IPv4還是IPv6。
AF_INET
: IPv4。AF_INET6:
IPv6。
參數(shù)type:確定數(shù)據(jù)的傳輸方式。
SOCK_STREAM
: 流式套接字(TCP)。SOCK_DGRAM
: 數(shù)據(jù)報(bào)套接字(UDP)。
參數(shù)protocol:確定協(xié)議類型,如果前面type已經(jīng)能確定了,這里傳入0即可。
返回值:
- 成功:文件描述符。
- 失敗:一個(gè)小于0的數(shù)。
代碼示例:
// 打開(kāi)網(wǎng)絡(luò)文件 IPv4 數(shù)據(jù)包 udp _socketfd = socket(AF_INET, SOCK_DGRAM, 0); if (_socketfd < 0) { LOG(Level::ERROR) << "socket() fail"; exit(1); } else { LOG(Level::INFO) << "socket() succee _socketfd:" << _socketfd; }
說(shuō)明:LOG是我寫(xiě)的一個(gè)打印日志的接口, 大家把它當(dāng)作cout理解就行,當(dāng)然需要日志源碼的可以私信我。
2.綁定ip地址與端口號(hào)
sockaddr_in結(jié)構(gòu)
首先我們需要了解sockaddr_in結(jié)構(gòu),IP地址和端口號(hào)等信息要包裝在這個(gè)結(jié)構(gòu)里面,然后使用bind函數(shù)綁定。
sockaddr_in是用于 IPv4 網(wǎng)絡(luò)編程 的一個(gè)核心數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)套接字地址信息(IP地址和端口號(hào)),除此之外還有sockaddr_in6(IPv6),sockaddr_un(本地通信),sockaddr(用來(lái)屏蔽包括但不止于以上三種結(jié)構(gòu)的底層實(shí)現(xiàn))。
sockaddr_in結(jié)構(gòu)如下:
#include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; // 地址族(Address Family) in_port_t sin_port; // 端口號(hào)(Port Number) struct in_addr sin_addr; // IPv4 地址(IP Address) char sin_zero[8]; // 填充字段(Padding) }; // IPv4 地址結(jié)構(gòu)(嵌套在 sockaddr_in 中) struct in_addr { in_addr_t s_addr; // 32位IPv4地址(網(wǎng)絡(luò)字節(jié)序) };
創(chuàng)建 sockaddr_in 對(duì)成員進(jìn)行設(shè)定:
- sin_family:我們?cè)O(shè)為
AF_INET,即IPv4。
- sin_port:使用成員變量_port,但需要使用函數(shù)htons轉(zhuǎn)為網(wǎng)絡(luò)字節(jié)序(即大端)。
- sin_addr:IP地址通常都是點(diǎn)分十進(jìn)制的字符串,所以需要把IP轉(zhuǎn)成4字節(jié),然后4字節(jié)轉(zhuǎn)成網(wǎng)絡(luò)序列,庫(kù)提供了inet_addr函數(shù),可以完成這個(gè)功能。不過(guò)這里我們把它直接設(shè)為INADDR_ANY,表示本主機(jī)上的所有IP都綁定到服務(wù)器,這樣的話外部客戶端連接任意IP都能連接到該主機(jī)。
- 最后一個(gè)成員暫且用不著,不用管。
bind函數(shù)的使用
bind聲明
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用來(lái)綁定端口。
參數(shù)sockfd:要綁定的套接字描述符(由 socket()
函數(shù)創(chuàng)建)。
參數(shù)sockaddr:指向地址結(jié)構(gòu)體的指針,包含綁定的IP地址和端口號(hào)。
參數(shù)addrlen:地址結(jié)構(gòu)體的長(zhǎng)度(單位:字節(jié))。
返回值:
- 0:成功。
- 非0:失敗。
代碼示例:
sockaddr_in sd; bzero(&sd, sizeof(sd));//初始化為0 sd.sin_family = AF_INET; sd.sin_port = htons(_port); // sd.sin_addr.s_addr = inet_addr(_ip.c_str()); sd.sin_addr.s_addr = INADDR_ANY; // 綁定ip地址與端口號(hào) int n = bind(_socketfd, (const sockaddr *)&sd, sizeof(sd)); if (n != 0) { LOG(Level::FATAL) << "bind fial"; exit(1); } else { LOG(Level::INFO) << "bind success"; }
由于后面會(huì)對(duì)sockaddr_in頻繁操作,所以在這里封裝一個(gè)InetAddr類放在InetAddr文件里,這里就不講解具體的細(xì)節(jié)了,如下:
class InetAddr { public: InetAddr(){} InetAddr(sockaddr_in &peer) : _addr(peer) { _port = ntohs(peer.sin_port);//網(wǎng)絡(luò)序列轉(zhuǎn)為主機(jī)序列 _ip = inet_ntoa(peer.sin_addr);//4字節(jié)轉(zhuǎn)為點(diǎn)分十進(jìn)制 } InetAddr(uint16_t port, string ip) : _port(port), _ip(ip) { _addr.sin_family = AF_INET; _addr.sin_port = htons(_port); _addr.sin_addr.s_addr = inet_addr(_ip.c_str()); } string tostring_port() { return to_string(_port); } string tostring_ip() { return _ip; } bool operator==(InetAddr addr) { return _port == addr._port && _ip == addr._ip; } sockaddr_in &getaddr() { return _addr; } private: uint16_t _port; string _ip; sockaddr_in _addr; };
void start ()
3.接收信息
UDP協(xié)議數(shù)據(jù)的接收使用的是recvfrom函數(shù),recvfrom函數(shù)的使用:
recvfrom函數(shù)聲明:
ssize_t recvfrom( int sockfd, // 套接字描述符 void *buf, // 接收數(shù)據(jù)的緩沖區(qū) size_t len, // 緩沖區(qū)最大長(zhǎng)度 int flags, // 標(biāo)志位(通常設(shè)為0) struct sockaddr *src_addr, // 發(fā)送方的地址信息(可選) socklen_t *addrlen // 地址結(jié)構(gòu)體的長(zhǎng)度(輸入輸出參數(shù)) );
sockfd | UDP套接字的網(wǎng)絡(luò)文件描述符(需已綁定端口)。 |
buf | 指向接收數(shù)據(jù)的緩沖區(qū),用于存儲(chǔ)接收到的數(shù)據(jù)。 |
len | 緩沖區(qū)的最大容量(單位:字節(jié)),防止數(shù)據(jù)溢出。 |
flags | 控制接收行為的標(biāo)志位,常用值: 0(默認(rèn)阻塞)、MSG_DONTWAIT(非阻塞)。 |
src_addr | 指向 struct sockaddr 的指針,用于存儲(chǔ)發(fā)送方的地址信息(IP和端口)。若不需要可設(shè)為 NULL。 |
addrlen | 輸入時(shí)為 src_addr 結(jié)構(gòu)體的長(zhǎng)度,輸出時(shí)為實(shí)際地址長(zhǎng)度。需初始化為 sizeof(struct sockaddr)。 |
返回值 | 含義 |
---|---|
>0 | 成功接收的字節(jié)數(shù)。 |
0 | (僅TCP有意義,UDP一般不會(huì)返回0)。 |
-1 | 發(fā)生錯(cuò)誤,檢查 errno 獲取具體原因 |
注:千萬(wàn)不要把時(shí)間花在記函數(shù)參數(shù)列表上,這么多函數(shù)你是記不了的,只需要看懂就行,函數(shù)的參數(shù)列表在編譯器上通常都會(huì)有提示的。只需要把鼠標(biāo)指針停留在對(duì)應(yīng)的函數(shù)名上,如下:
代碼示例:
while (true) { // 收各個(gè)客戶端發(fā)來(lái)的消息 sockaddr_in client; socklen_t len = sizeof(client); char buffer[1024]; int n = recvfrom(_socketfd, buffer, sizeof(buffer), 0, (sockaddr *)&client, &len); buffer[n] = '\0'; //回調(diào)函數(shù)處理數(shù)據(jù) //...... }
到這里為止通信問(wèn)題就解決了,只需要靜等客戶端發(fā)數(shù)據(jù)就行。接下來(lái)就是數(shù)據(jù)處理。
二、數(shù)據(jù)處理
別忘了我們要做的是群聊服務(wù)器,剛才我們不管三七二十一先把通信問(wèn)題解決,這個(gè)的做法是很正確的,因?yàn)橥ㄐ疟緛?lái)就是一個(gè)模板化的問(wèn)題,其次它和其他模塊是解耦的,在編寫(xiě)過(guò)程中并不用考慮數(shù)據(jù)怎么處理。
群聊服務(wù)器如何實(shí)現(xiàn)?
原理很簡(jiǎn)單,把一個(gè)客戶發(fā)來(lái)的消息再發(fā)送給與它連接的所有客戶。
我們需要做什么呢?
把與它連接的所有客戶的IP和端口號(hào)(InetAddr)都存儲(chǔ)起來(lái),當(dāng)有客戶給它服務(wù)器發(fā)信息,服務(wù)器再把信息轉(zhuǎn)發(fā)給所有客戶。
這個(gè)功能我們單獨(dú)做一個(gè)類Route,用來(lái)做消息路由,放在新建頭文件Route.hpp里。
Route的實(shí)現(xiàn)很簡(jiǎn)單,只需要一個(gè)成員函數(shù)用來(lái)收發(fā)數(shù)據(jù),一個(gè)成員變量用來(lái)存儲(chǔ)與它連接的客戶端信息。如下:
class Route { public: Route(){} void Handler(int socketfd,string message,InetAddr client); private: vector<InetAddr> _data; };
因?yàn)槭諗?shù)據(jù)的功能在通信模塊已經(jīng)做了,直接讓它把網(wǎng)絡(luò)文件描述符,數(shù)據(jù)和客戶端信息傳給Handler就行。其次做兩個(gè)小函數(shù),Push:把客戶端信息插入數(shù)組,Pop:把客戶端信息移除數(shù)組。
代碼示例:
class Route { private: void Push(InetAddr peer) { for (auto val : _data) { //如果已經(jīng)有了,就直接退出 if (val == peer) return; } _data.push_back(peer); LOG(Level::INFO)<<peer.tostring_ip()<<'|'<<peer.tostring_port()<<" online"; } bool Pop(InetAddr peer) { //用戶退出連接后,把它移除數(shù)組 _data.erase(peer); return true; } public: Route(){} void Handler(int socketfd,string message,InetAddr client) { Push(client); // 誰(shuí)發(fā)的信息要知道吧?所以添加客戶的信息 string send_message = client.tostring_ip() + " | " + client.tostring_port() + ": "; send_message += message; // 發(fā)給所有在線的客戶端 for (auto val : _data) { if(val == client) continue; sendto(socketfd, send_message.c_str(), send_message.size(), 0, (sockaddr *)&val.getaddr(), sizeof(val.getaddr())); } } private: vector<InetAddr> _data; };
sendto接口和recvfrom很類似,如下:
sendto的聲明:
ssize_t sendto( int sockfd, // 套接字描述符 const void *buf, // 待發(fā)送數(shù)據(jù)的緩沖區(qū) size_t len, // 數(shù)據(jù)長(zhǎng)度(字節(jié)) int flags, // 控制標(biāo)志(通常設(shè)為0) const struct sockaddr *dest_addr, // 目標(biāo)地址(IP和端口) socklen_t addrlen // 目標(biāo)地址結(jié)構(gòu)體的長(zhǎng)度 );
參數(shù)詳解
sockfd | UDP 套接字的描述符(無(wú)需提前連接)。 |
buf | 指向待發(fā)送數(shù)據(jù)的緩沖區(qū)(如字符串、二進(jìn)制數(shù)據(jù))。 |
len | 數(shù)據(jù)的實(shí)際長(zhǎng)度(單位:字節(jié))。 |
flags | 控制發(fā)送行為的標(biāo)志位,常用值: 0(默認(rèn)阻塞)、MSG_DONTWAIT(非阻塞)。 |
dest_addr | 指向目標(biāo)地址的結(jié)構(gòu)體(如 sockaddr_in),需強(qiáng)制轉(zhuǎn)換為 sockaddr*。 |
addrlen | 目標(biāo)地址結(jié)構(gòu)體的長(zhǎng)度(如 sizeof(struct sockaddr_in))。 |
返回值
返回值 | 含義 |
---|---|
>0 | 成功發(fā)送的字節(jié)數(shù)(可能與 len 不同,需檢查是否完全發(fā)送)。 |
-1 | 發(fā)送失敗,檢查 errno 獲取具體錯(cuò)誤原因。 |
這里有個(gè)很尷尬的事,服務(wù)器并不知道客戶端什么時(shí)候退出,可以說(shuō)是UDP協(xié)議的特點(diǎn)吧,所以Pop函數(shù)什么時(shí)候調(diào)用并不知道,除非客戶在退出時(shí)給服務(wù)器發(fā)一條特殊信息表明客戶要退出。這里先這樣。
現(xiàn)在為止服務(wù)器相關(guān)的通信接口和數(shù)據(jù)處理方法已經(jīng)準(zhǔn)備好了,接下來(lái)實(shí)現(xiàn)UdpClient.cc文件,即main函數(shù),把服務(wù)器調(diào)用起來(lái)。
主要實(shí)現(xiàn)以下幾點(diǎn):
- 要給服務(wù)器設(shè)定端口號(hào),需要從程序外部傳入,即命令行參數(shù)。
- 創(chuàng)建數(shù)據(jù)處理的類(Route)。
- 創(chuàng)建服務(wù)器,把端口號(hào)和數(shù)據(jù)處理方法(即回調(diào)方法)傳入,啟動(dòng)服務(wù)器。
1,2比較簡(jiǎn)單,接下來(lái)講解第3點(diǎn)。
還記得開(kāi)頭在UdpServer里我們?nèi)鄙俚某蓡T變量數(shù)據(jù)處理函數(shù)嗎?現(xiàn)在我們知道它是誰(shuí)了,即:
- void Handler(int socketfd,string message,InetAddr client)
這里我們寫(xiě)規(guī)范一點(diǎn),聲明一個(gè)類型:
- using funcType = function<void(int, string, InetAddr)>;
然后添加成員變量funcType _func,并在構(gòu)造函數(shù)的參數(shù)列表進(jìn)行初始化。
最后在Start中調(diào)用_func函數(shù)(即回調(diào)),如下:
void Start() { while (true) { // 收各個(gè)客戶端發(fā)來(lái)的消息 sockaddr_in client; socklen_t len = sizeof(client); char buffer[1024]; int n = recvfrom(_socketfd, buffer, sizeof(buffer), 0, (sockaddr *)&client, &len); buffer[n] = '\0'; _func(_socketfd,string(buffer),InetAddr(client)); } }
可優(yōu)化點(diǎn):把_func當(dāng)作任務(wù),推入線程池。
現(xiàn)在創(chuàng)建UdpServer兩個(gè)參數(shù),一個(gè)是port(端口號(hào)),另一個(gè)是func(數(shù)據(jù)處理方法),對(duì)于func我們可以以lambda表達(dá)式的方式傳入。如下:
int main(int argc, char *argv[]) { if (argc != 2) { std::cerr << "Usage" << argv[0] << "port" << std::endl; return 1; } std::string port = argv[1]; // 路由 Route rt; // 通信 unique_ptr<UdpServer> us = make_unique<UdpServer>(stoi(port), [&](int socketfd, string meassge, InetAddr client) { rt.Handler(socketfd, meassge, client); }); us->Init(); us->Start(); return 0; }
客戶端
框架設(shè)計(jì)
客戶端將來(lái)是要連接服務(wù)器的,所以需要傳入服務(wù)器IP和端口,而且是從程序外部出入。即給main函數(shù)傳入命令行參數(shù)。注意判斷參數(shù)是否合法。
然后和服務(wù)器一樣需要打開(kāi)網(wǎng)絡(luò)文件。如下:
int main(int argc, char *argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " server_op server_port" << std::endl; return 1; } int socketfd = socket(AF_INET, SOCK_DGRAM, 0); if (socketfd < 0) { LOG(Level::ERROR) << "socket() fail"; exit(1); } else { LOG(Level::INFO) << "socket() succee _socketfd:" << socketfd; } //包裝客戶端信息 InetAddr addr(std::stoi(argv[2]), argv[1]); //信息收發(fā) //...... return 0; }
三、端口綁定
客戶端的實(shí)現(xiàn)可比服務(wù)器簡(jiǎn)單多了,因?yàn)樗?strong>不需要我們手動(dòng)綁定IP和端口號(hào),系統(tǒng)幫我們做了。但要清楚我們是可以自己綁定的,不過(guò)會(huì)有很多問(wèn)題,比如主機(jī)里有很多進(jìn)程,可能端口號(hào)會(huì)綁重,讓系統(tǒng)自動(dòng)分配比較安全。
那服務(wù)器的端口號(hào)為什么不也讓系統(tǒng)動(dòng)態(tài)分配呢?我們自己綁多麻煩。其實(shí)是這樣的,服務(wù)器是需要供給很多客戶去使用,需要客戶端填寫(xiě)服務(wù)器端口。所以服務(wù)器端口一定要明確,系統(tǒng)動(dòng)態(tài)分配的話,在程序外部就無(wú)法知道服務(wù)器端口號(hào)了。
四、收發(fā)信息
收發(fā)信息是一個(gè)不斷重復(fù)的操作,所以寫(xiě)成一個(gè)死循環(huán),但要注意不要把收信息和發(fā)信息寫(xiě)在一起,要不然發(fā)信息阻塞時(shí)就收不到信息,收信息阻塞時(shí)也發(fā)不了信息。
所以它們應(yīng)該并發(fā)地進(jìn)行,即使用兩個(gè)子線程。
代碼示例:
void Write(int socketfd, InetAddr &addr) { //提前發(fā)一條信息告訴服務(wù)器我已經(jīng)上線 string str="online"; sendto(socketfd, str.c_str(), sizeof(str), 0, (const sockaddr *)&addr.getaddr(), sizeof(addr.getaddr())); while (true) { std::string message; cout<<"Please Enter# "; std::getline(std::cin, message); sendto(socketfd, message.c_str(), sizeof(message), 0, (const sockaddr *)&addr.getaddr(), sizeof(addr.getaddr())); } } void Read(int socketfd) { while (true) { sockaddr_in sd; char buffer[1024]; socklen_t len = sizeof(sd); int n = recvfrom(socketfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&sd, &len); buffer[n] = '\0'; std::cout << buffer << std::endl; } }
main函數(shù)中
thread td_read([&](){ Write(socketfd,addr); }); thread td_write([&](){ Read(socketfd); }); td_read.join(); td_read.join();
到這里這個(gè)工程就完成了,下面是運(yùn)行結(jié)果。
效果展示:
五、源碼
UdpServer.hpp
// 用條件編譯,防止頭文件重復(fù)包含 #ifndef UDP_SERVER #define UDP_SERVER #include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <strings.h> #include <string> #include <assert.h> #include <arpa/inet.h> #include <functional> #include "InetAddr.hpp" #include "Log.hpp" using namespace std; using namespace my_log; using funcType = function<void(int, string, InetAddr)>; class task { public: task() {} task(funcType func, int socketfd, string message, InetAddr client) : _func(func), _socketfd(socketfd), _message(message), _client(client) { } void operator()() { assert(_socketfd != -1); _func(_socketfd, _message, _client); } private: funcType _func; int _socketfd; string _message; InetAddr _client; }; class UdpServer { public: UdpServer(uint16_t port, const funcType &func) : _socketfd(-1), _port(port), _func(func) { } void Init() { // 打開(kāi)網(wǎng)絡(luò)文件 IPv4 數(shù)據(jù)包 udp _socketfd = socket(AF_INET, SOCK_DGRAM, 0); if (_socketfd < 0) { LOG(Level::ERROR) << "socket() fail"; exit(1); } else { LOG(Level::INFO) << "socket() succee _socketfd:" << _socketfd; } sockaddr_in sd; bzero(&sd, sizeof(sd)); sd.sin_family = AF_INET; sd.sin_port = htons(_port); // sd.sin_addr.s_addr = inet_addr(_ip.c_str()); sd.sin_addr.s_addr = INADDR_ANY; // 綁定ip地址與端口號(hào) int n = bind(_socketfd, (const sockaddr *)&sd, sizeof(sd)); if (n < 0) { LOG(Level::FATAL) << "bind fial"; exit(1); } else { LOG(Level::INFO) << "bind success"; } } void Start() { while (true) { // 收各個(gè)客戶端發(fā)來(lái)的消息 sockaddr_in client; socklen_t len = sizeof(client); char buffer[1024]; int n = recvfrom(_socketfd, buffer, sizeof(buffer), 0, (sockaddr *)&client, &len); buffer[n] = '\0'; _func(_socketfd,string(buffer),InetAddr(client)); } } ~UdpServer() { } private: int _socketfd; uint16_t _port; funcType _func; }; #endif
UdpServer.cc
#include <iostream> #include <memory> #include "UdpServer.hpp" #include "InetAddr.hpp" #include "Route.hpp" int main(int argc, char *argv[]) { if (argc < 2) { // LOG(Level::FATAL)<<"input error"; std::cerr << "Usage" << argv[0] << "port" << std::endl; return 1; } std::string port = argv[1]; // 路由 Route rt; // 通信 unique_ptr<UdpServer> us = make_unique<UdpServer>(stoi(port), [&](int socketfd, string meassge, InetAddr client) { rt.Handler(socketfd, meassge, client); }); us->Init(); us->Start(); return 0; }
UdpClient.cc
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <strings.h> #include <arpa/inet.h> #include <thread> #include "InetAddr.hpp" #include "Log.hpp" using namespace my_log; void Write(int socketfd, InetAddr &addr) { string str="online"; sendto(socketfd, str.c_str(), sizeof(str), 0, (const sockaddr *)&addr.getaddr(), sizeof(addr.getaddr())); while (true) { std::string message; cout<<"Please Enter# "; std::getline(std::cin, message); sendto(socketfd, message.c_str(), sizeof(message), 0, (const sockaddr *)&addr.getaddr(), sizeof(addr.getaddr())); } } void Read(int socketfd) { while (true) { sockaddr_in sd; char buffer[1024]; socklen_t len = sizeof(sd); int n = recvfrom(socketfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&sd, &len); buffer[n] = '\0'; std::cout << buffer << std::endl; } } int main(int argc, char *argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " server_op server_port" << std::endl; return 1; } int socketfd = socket(AF_INET, SOCK_DGRAM, 0); if (socketfd < 0) { LOG(Level::ERROR) << "socket() fail"; exit(1); } else { LOG(Level::INFO) << "socket() succee _socketfd:" << socketfd; } InetAddr addr((uint16_t)std::stoi(argv[2]), argv[1]); thread td_read([&](){ Write(socketfd,addr); }); thread td_write([&](){ Read(socketfd); }); td_read.join(); td_read.join(); return 0; }
InteAddr.hpp
#pragma once #include <iostream> #include <string> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> using namespace std; class InetAddr { public: InetAddr(){} InetAddr(sockaddr_in &peer) : _addr(peer) { _port = ntohs(peer.sin_port); _ip = inet_ntoa(peer.sin_addr); } InetAddr(uint16_t port, string ip) : _port(port), _ip(ip) { _addr.sin_family = AF_INET; _addr.sin_port = htons(_port); _addr.sin_addr.s_addr = inet_addr(_ip.c_str()); } string tostring_port() { return to_string(_port); } string tostring_ip() { return _ip; } bool operator==(InetAddr addr) { return _port == addr._port && _ip == addr._ip; } sockaddr_in &getaddr() { return _addr; } private: uint16_t _port; string _ip; sockaddr_in _addr; };
Route.hpp
#pragma once #include <iostream> #include <string> #include <vector> #include "Log.hpp" #include "InetAddr.hpp" using namespace std; using namespace my_log; class Route { private: void Push(InetAddr peer) { for (auto val : _data) { if (val == peer) return; } _data.push_back(peer); LOG(Level::INFO)<<peer.tostring_ip()<<'|'<<peer.tostring_port()<<" online"; } bool Pop() { return true; } public: Route(){} void Handler(int socketfd,string message,InetAddr client) { Push(client); // 處理信息 string send_message = client.tostring_ip() + " | " + client.tostring_port() + ": "; send_message += message; // 發(fā)給所有在線的客戶端 for (auto val : _data) { if(val == client) continue; sendto(socketfd, send_message.c_str(), send_message.size(), 0, (sockaddr *)&val.getaddr(), sizeof(val.getaddr())); } } private: vector<InetAddr> _data; };
到此這篇關(guān)于C++基于UDP協(xié)議的群聊服務(wù)器開(kāi)發(fā)實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)C++ UDP 群聊內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言中g(shù)etchar()的返回類型為什么是int詳解
這篇文章主要給大家介紹了關(guān)于C語(yǔ)言中g(shù)etchar()的返回類型為什么是int的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11C語(yǔ)言結(jié)構(gòu)體詳細(xì)圖解分析
C 數(shù)組允許定義可存儲(chǔ)相同類型數(shù)據(jù)項(xiàng)的變量,結(jié)構(gòu)是 C 編程中另一種用戶自定義的可用的數(shù)據(jù)類型,它允許你存儲(chǔ)不同類型的數(shù)據(jù)項(xiàng),本篇讓我們來(lái)了解C 的結(jié)構(gòu)體2022-03-03基于c++ ege圖形庫(kù)實(shí)現(xiàn)五子棋游戲
這篇文章主要為大家詳細(xì)介紹了基于c++ ege圖形庫(kù)實(shí)現(xiàn)五子棋游戲,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12C++ OpenCV制作黑客帝國(guó)風(fēng)格的照片
這篇文章主要介紹了如何通過(guò)C++ OpenCV制作出黑客帝國(guó)風(fēng)格的照片,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)OpenCV有一定幫助,需要的可以參考一下2022-01-01Qt創(chuàng)建SQlite數(shù)據(jù)庫(kù)的示例代碼
本文主要介紹了Qt創(chuàng)建SQlite數(shù)據(jù)庫(kù)的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05利用C++實(shí)現(xiàn)從std::string類型到bool型的轉(zhuǎn)換
利用C++實(shí)現(xiàn)從std::string類型到bool型的轉(zhuǎn)換。需要的朋友可以過(guò)來(lái)參考下。希望對(duì)大家有所幫助2013-10-10