引言
在前一篇文章中,我們詳細(xì)介紹了UDP協(xié)議和TCP協(xié)議的特點(diǎn)以及它們之間的異同點(diǎn)。本文將延續(xù)上文內(nèi)容,重點(diǎn)討論簡單的UDP網(wǎng)絡(luò)程序模擬實(shí)現(xiàn)。通過本文的學(xué)習(xí),讀者將能夠深入了解UDP協(xié)議的實(shí)際應(yīng)用,并掌握如何編寫簡單的UDP網(wǎng)絡(luò)程序。讓我們一起深入探討UDP網(wǎng)絡(luò)程序的實(shí)現(xiàn)細(xì)節(jié),為網(wǎng)絡(luò)編程的學(xué)習(xí)之旅添上一份精彩的實(shí)踐經(jīng)驗(yàn)。
一、UDP協(xié)議
UDP(User Datagram Protocol)是一種無連接的、輕量級的網(wǎng)絡(luò)傳輸協(xié)議,它提供了快速、簡單的數(shù)據(jù)傳輸服務(wù)。下面是一個(gè)簡單的UDP程序?qū)崿F(xiàn)示例,包括一個(gè)UDP服務(wù)器和一個(gè)UDP客戶端。詳介紹可以看上一篇文章:UDP協(xié)議介紹 | TCP協(xié)議介紹 | UDP 和 TCP 的異同
二、UDP網(wǎng)絡(luò)程序模擬實(shí)現(xiàn)
1. 預(yù)備代碼
?makefile文件
.PHONY:all
all:udpserver udpclient
udpserver:Main.cc
g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f udpserver udpclient
這段代碼是一個(gè)簡單的 Makefile 文件,用于編譯 UDP 服務(wù)器(udpserver)和 UDP 客戶端(udpclient)的程序。在這個(gè) Makefile 中定義了兩個(gè)規(guī)則:
- all:表示默認(rèn)的目標(biāo),依賴于 udpserver 和 udpclient 目標(biāo),即執(zhí)行 make 命令時(shí)會編譯 udpserver 和 udpclient。
- clean:用于清理生成的可執(zhí)行文件 udpserver 和 udpclient。
在 Makefile 中使用了一些特殊的關(guān)鍵字和變量:
- .PHONY:聲明 all 和 clean 是偽目標(biāo),不是真正的文件名。
- $@:表示目標(biāo)文件名。
- $^:表示所有依賴文件列表。
- -std=c++11:指定 C++ 的編譯標(biāo)準(zhǔn)為 C++11。
- -lpthread:鏈接 pthread 庫,用于多線程支持。
?打印日志文件
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen; // 默認(rèn)輸出方式為屏幕打印
path = "./log/"; // 默認(rèn)日志文件存放路徑
}
void Enable(int method)
{
printMethod = method; // 設(shè)置日志輸出方式(屏幕、單個(gè)文件、分類文件)
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl; // 屏幕打印日志信息
break;
case Onefile:
printOneFile(LogFile, logtxt); // 將日志信息追加寫入單個(gè)文件
break;
case Classfile:
printClassFile(level, logtxt); // 將日志信息追加寫入分類文件
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname; // 構(gòu)建日志文件的完整路徑
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // 打開文件,如果文件不存在則創(chuàng)建
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size()); // 將日志信息寫入文件
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // 構(gòu)建分類文件名,例如"log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt); // 將日志信息追加寫入分類文件
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默認(rèn)部分+自定義部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt); // 打印日志信息
}
private:
int printMethod; // 日志輸出方式
std::string path; // 日志文件存放路徑
};
該代碼實(shí)現(xiàn)了一個(gè)簡單的日志記錄類(Log),其中包括設(shè)置日志輸出方式(屏幕、單個(gè)文件、分類文件)和打印日志信息的功能。
-
Log
類是一個(gè)用于記錄日志的類。 -
Enable
函數(shù)用于設(shè)置日志輸出方式,可以選擇屏幕打印、單個(gè)文件或分類文件。 -
printLog
函數(shù)根據(jù)設(shè)置的日志輸出方式,將日志信息打印到屏幕、追加寫入單個(gè)文件或分類文件。 -
printOneFile
函數(shù)用于將日志信息追加寫入單個(gè)文件。 -
printClassFile
函數(shù)用于將日志信息追加寫入分類文件。 -
levelToString
函數(shù)將日志級別轉(zhuǎn)換為對應(yīng)的字符串表示。 -
operator()
函數(shù)是重載的函數(shù)調(diào)用運(yùn)算符,用于打印日志信息。 -
path
是日志文件存放路徑,默認(rèn)為"./log/"。 -
printMethod
是日志輸出方式,默認(rèn)為屏幕打印。 -
SIZE
定義了緩沖區(qū)大小。 -
Info
、Debug
、Warning
、Error
、Fatal
是日志級別的定義。 -
Screen
、Onefile
、Classfile
是日志輸出方式的定義。 -
LogFile
是單個(gè)文件名的定義。
?打開指定的終端設(shè)備文件,并將其作為標(biāo)準(zhǔn)錯(cuò)誤輸出的目標(biāo)文件描述符
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 定義要打開的終端設(shè)備文件路徑
std::string terminal = "/dev/pts/6";
// 打開指定的終端設(shè)備文件,并將其作為標(biāo)準(zhǔn)錯(cuò)誤輸出的目標(biāo)文件描述符
int OpenTerminal()
{
// 使用open函數(shù)以只寫方式打開終端設(shè)備文件
int fd = open(terminal.c_str(), O_WRONLY);
if(fd < 0)
{
// 如果打開終端設(shè)備文件失敗,則輸出錯(cuò)誤信息到標(biāo)準(zhǔn)錯(cuò)誤輸出
std::cerr << "open terminal error" << std::endl;
return 1; // 返回錯(cuò)誤代碼
}
// 將終端設(shè)備文件的文件描述符復(fù)制給標(biāo)準(zhǔn)錯(cuò)誤輸出的文件描述符
// 這樣標(biāo)準(zhǔn)錯(cuò)誤輸出就會重定向到指定的終端設(shè)備上
dup2(fd, 2);
// 如果需要在此處輸出信息到標(biāo)準(zhǔn)錯(cuò)誤輸出,可以使用printf等函數(shù)
// 關(guān)閉文件描述符
// close(fd);
return 0; // 返回成功代碼
}
這段代碼的作用是打開一個(gè)終端設(shè)備文件 “/dev/pts/6”,將其作為標(biāo)準(zhǔn)錯(cuò)誤輸出(stderr)的目標(biāo)文件描述符,實(shí)現(xiàn)將錯(cuò)誤信息輸出到指定的終端設(shè)備上。
-
terminal
變量存儲了要打開的終端設(shè)備文件路徑 “/dev/pts/6”。 -
OpenTerminal
函數(shù)嘗試打開指定的終端設(shè)備文件,并將其作為標(biāo)準(zhǔn)錯(cuò)誤輸出的目標(biāo)文件描述符。- 首先使用
open
函數(shù)打開終端設(shè)備文件,以只寫方式(O_WRONLY)。 - 如果成功打開終端設(shè)備文件,則將其文件描述符復(fù)制給標(biāo)準(zhǔn)錯(cuò)誤輸出的文件描述符(2),即
dup2(fd, 2)
,這樣標(biāo)準(zhǔn)錯(cuò)誤輸出就會重定向到該終端設(shè)備上。 - 如果打開終端設(shè)備文件失敗,則輸出錯(cuò)誤信息到標(biāo)準(zhǔn)錯(cuò)誤輸出,并返回錯(cuò)誤代碼 1。
- 最后函數(shù)返回0表示成功。
- 首先使用
2. UDP 服務(wù)器端實(shí)現(xiàn)(UdpServer.hpp)
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
// 使用Log類記錄日志信息
Log lg;
enum {
SOCKET_ERR = 1,
BIND_ERR
};
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer {
public:
UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip)
: sockfd_(0), port_(port), ip_(ip), isrunning_(false)
{}
void Init() {
// 1. 創(chuàng)建UDP socket
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
if (sockfd_ < 0) {
lg(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", sockfd_);
// 2. 綁定socket
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_); // 端口號需要轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序
local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 將IP地址轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序
if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0) {
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
}
void CheckUser(const struct sockaddr_in& client, const std::string clientip, uint16_t clientport) {
// 檢查用戶是否已經(jīng)存在在線用戶列表中
auto iter = online_user_.find(clientip);
if (iter == online_user_.end()) {
online_user_.insert({clientip, client});
std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
}
}
void Broadcast(const std::string& info, const std::string clientip, uint16_t clientport) {
// 廣播消息給所有在線用戶
for (const auto& user : online_user_) {
std::string message = "[";
message += clientip;
message += ":";
message += std::to_string(clientport);
message += "]# ";
message += info;
socklen_t len = sizeof(user.second);
sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
}
}
void Run() {
isrunning_ = true;
char inbuffer[size];
while (isrunning_) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 接收客戶端發(fā)送的消息
ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
if (n < 0) {
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
// 獲取客戶端的IP地址和端口號
uint16_t clientport = ntohs(client.sin_port);
std::string clientip = inet_ntoa(client.sin_addr);
// 檢查用戶是否已經(jīng)存在在線用戶列表中
CheckUser(client, clientip, clientport);
std::string info = inbuffer;
// 將接收到的消息廣播給所有在線用戶
Broadcast(info, clientip, clientport);
}
}
~UdpServer() {
if (sockfd_ > 0)
close(sockfd_);
}
private:
int sockfd_; // 網(wǎng)絡(luò)文件描述符
std::string ip_; // 服務(wù)器IP地址
uint16_t port_; // 服務(wù)器端口號
bool isrunning_; // 服務(wù)器運(yùn)行狀態(tài)
std::unordered_map<std::string, struct sockaddr_in> online_user_; // 在線用戶列表
};
-
Log.hpp
是用于記錄日志信息的頭文件。 -
lg
是一個(gè)Log
類的對象,用于輸出日志信息。 -
enum
定義了兩個(gè)錯(cuò)誤類型:SOCKET_ERR
和BIND_ERR
,分別表示 socket 創(chuàng)建錯(cuò)誤和綁定錯(cuò)誤。 -
defaultport
和defaultip
分別設(shè)置默認(rèn)的端口號和 IP 地址。 -
size
定義接收緩沖區(qū)的大小為 1024 字節(jié)。 -
UdpServer
類封裝了一個(gè) UDP 服務(wù)器。 - 構(gòu)造函數(shù)
UdpServer
接受端口號和 IP 地址作為參數(shù),并初始化成員變量。 -
Init
函數(shù)用于初始化 UDP 服務(wù)器,其中:- 創(chuàng)建 UDP socket,并檢查創(chuàng)建是否成功。
- 綁定 socket 到指定的 IP 地址和端口號,并檢查綁定是否成功。
-
CheckUser
函數(shù)用于檢查用戶是否已經(jīng)存在在線用戶列表中,如果不存在則將其添加到列表中。 -
Broadcast
函數(shù)用于向所有在線用戶廣播消息,其中:- 消息格式為
[發(fā)送者IP:發(fā)送者端口號]# 消息內(nèi)容
。 - 使用
sendto
函數(shù)發(fā)送消息給每個(gè)在線用戶。
- 消息格式為
-
Run
函數(shù)是 UDP 服務(wù)器的主循環(huán),其中:- 循環(huán)接收客戶端發(fā)送的消息,并將其廣播給所有在線用戶。
- 對每個(gè)客戶端,獲取其 IP 地址和端口號,并進(jìn)行用戶檢查和消息廣播。
-
~UdpServer
析構(gòu)函數(shù)關(guān)閉網(wǎng)絡(luò)文件描述符。 -
sockfd_
是網(wǎng)絡(luò)文件描述符,用于創(chuàng)建和管理網(wǎng)絡(luò)連接。 -
ip_
是服務(wù)器的 IP 地址。 -
port_
是服務(wù)器的端口號。 -
isrunning_
表示服務(wù)器的運(yùn)行狀態(tài),用于控制循環(huán)退出。 -
online_user_
是一個(gè)無序映射,用于保存在線用戶的 IP 地址和對應(yīng)的sockaddr_in
結(jié)構(gòu)體。
3. UDP 客戶端實(shí)現(xiàn)(main函數(shù))
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"
using namespace std;
// 函數(shù)聲明:打印程序的使用方法
void Usage(std::string proc);
// 結(jié)構(gòu)體:用于傳遞線程參數(shù)
struct ThreadData
{
struct sockaddr_in server; // 服務(wù)器地址結(jié)構(gòu)體
int sockfd; // socket 文件描述符
std::string serverip; // 服務(wù)器 IP 地址
};
// 線程函數(shù):接收消息
void *recv_message(void *args);
// 線程函數(shù):發(fā)送消息
void *send_message(void *args);
// 主函數(shù)
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]); // 打印使用方法
exit(0);
}
// 解析命令行參數(shù)
std::string serverip = argv[1]; // 服務(wù)器 IP 地址
uint16_t serverport = std::stoi(argv[2]); // 服務(wù)器端口號
// 初始化 ThreadData 結(jié)構(gòu)體
struct ThreadData td;
bzero(&td.server, sizeof(td.server)); // 清零服務(wù)器地址結(jié)構(gòu)體
td.server.sin_family = AF_INET; // 設(shè)置地址族為 IPv4
td.server.sin_port = htons(serverport); // 設(shè)置端口號(轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序)
td.server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 設(shè)置服務(wù)器 IP 地址(轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序)
// 創(chuàng)建 UDP socket
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0)
{
cout << "socket error" << endl;
return 1;
}
td.serverip = serverip; // 存儲服務(wù)器 IP 地址
pthread_t recvr, sender; // 定義接收消息和發(fā)送消息的線程
pthread_create(&recvr, nullptr, recv_message, &td); // 創(chuàng)建接收消息線程
pthread_create(&sender, nullptr, send_message, &td); // 創(chuàng)建發(fā)送消息線程
// 等待接收消息和發(fā)送消息的線程退出
pthread_join(recvr, nullptr);
pthread_join(sender, nullptr);
close(td.sockfd); // 關(guān)閉 socket
return 0;
}
// 函數(shù)實(shí)現(xiàn):打印程序的使用方法
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
// 線程函數(shù)實(shí)現(xiàn):接收消息
void *recv_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 強(qiáng)制類型轉(zhuǎn)換為 ThreadData 結(jié)構(gòu)體指針
char buffer[1024]; // 接收消息的緩沖區(qū)
while (true)
{
memset(buffer, 0, sizeof(buffer)); // 清空緩沖區(qū)
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len); // 接收消息
if (s > 0)
{
buffer[s] = 0;
cerr << buffer << endl; // 輸出接收到的消息
}
}
}
// 線程函數(shù)實(shí)現(xiàn):發(fā)送消息
void *send_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 強(qiáng)制類型轉(zhuǎn)換為 ThreadData 結(jié)構(gòu)體指針
string message; // 存儲用戶輸入的消息
socklen_t len = sizeof(td->server); // 服務(wù)器地址的長度
// 發(fā)送歡迎消息
std::string welcome = td->serverip + " comming...";
sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (struct sockaddr *)&(td->server), len);
while (true)
{
cout << "Please Enter@ ";
getline(cin, message); // 獲取用戶輸入的消息
sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len); // 發(fā)送消息給服務(wù)器
}
}
溫馨提示
感謝您對博主文章的關(guān)注與支持!如果您喜歡這篇文章,可以點(diǎn)贊、評論和分享給您的同學(xué),這將對我提供巨大的鼓勵(lì)和支持。另外,我計(jì)劃在未來的更新中持續(xù)探討與本文相關(guān)的內(nèi)容。我會為您帶來更多關(guān)于Linux以及C++編程技術(shù)問題的深入解析、應(yīng)用案例和趣味玩法等。如果感興趣的話可以關(guān)注博主的更新,不要錯(cuò)過任何精彩內(nèi)容!文章來源:http://www.zghlxwxcb.cn/news/detail-844587.html
再次感謝您的支持和關(guān)注。我們期待與您建立更緊密的互動,共同探索Linux、C++、算法和編程的奧秘。祝您生活愉快,排便順暢!文章來源地址http://www.zghlxwxcb.cn/news/detail-844587.html
到了這里,關(guān)于【探索Linux】P.28(網(wǎng)絡(luò)編程套接字 —— 簡單的UDP網(wǎng)絡(luò)程序模擬實(shí)現(xiàn))的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!