簡單的UDP網絡程序:多人群聊系統

本章重點

能夠實現一個簡單的udp客戶端/服務器;

1.創建套接字

我們把服務器封裝成一個類,當我們定義出一個服務器對象后需要馬上初始化服務器,而初始化服務器需要做的第一件事就是創建套接字。

?參數說明:

  • domain:創建套接字的域或者叫做協議家族,也就是創建套接字的類型。該參數就相當于struct sockaddr結構的前16個位。如果是本地通信就設置為AF_UNIX,如果是網絡通信就設置為AF_INET(IPv4)或 AF_INET6(IPv6)。
  • type:創建套接字時所需的服務類型。其中最常見的服務類型是SOCK_STREAMSOCK_DGRAM,如果是基于UDP的網絡通信,我們采用的就是SOCK_DGRAM,叫做用戶數據報服務,如果是基于TCP的網絡通信,我們采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服務。
  • protocol:創建套接字的協議類別。你可以指明為TCPUDP,但該字段一般直接設置為0就可以了,設置為0表示的就是默認,此時會根據傳入的前兩個參數自動推導出你最終需要使用的是哪種協議。

這里我們使用我們之前寫的Log.hpp文件來方便觀察輸出信息。

#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;path = "./log/";}void Enable(int method){printMethod = method;}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);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"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); // "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);// 格式:默認部分+自定義部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暫時打印printLog(level, logtxt);}private:int printMethod;std::string path;
};

當我們在進行初始化服務器創建套接字時,就是調用socket函數創建套接字,創建套接字時我們需要填入的協議家族就是AF_INET,因為我們要進行的是網絡通信,而我們需要的服務類型就是SOCK_DGRAM,因為我們現在編寫的UDP服務器是面向數據報的,而第三個參數之間設置為0即可。

enum
{SOCKET_ERR = 1
};class UdpServer
{
public:UdpServer(){}void Init(){// 1.創建udp socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) // 創建套接字失敗{log.operator()(Fatal, "sockfd create error: %d", _sockfd);exit(SOCKET_ERR);}log(Info, "socket create success, sockfd: %d", _sockfd); // 3}~UdpServer() {}private:int _sockfd;    // 網絡文件描述符
};

我們來運行一下:

2.綁定端口號

?注意:編寫的是UDP協議的服務器

現在套接字已經創建成功了,但作為一款服務器來講,如果只是把套接字創建好了,那我們也只是在系統層面上打開了一個通道,用戶并不知道將來并不知道是要將數據傳給哪個服務器,此時客戶端服務器還沒有與服務端服務器關聯起來,所以我們就要提前講服務器的ip地址,端口號和套接字綁定起來。

?參數說明:

  • sockfd:綁定的文件的文件描述符。也就是我們創建套接字時獲取到的文件描述符。
  • addr:網絡相關的屬性信息,包括協議家族、IP地址、端口號等。
  • addrlen:傳入的addr結構體的長度。

?返回值說明:

  • 綁定成功返回0,綁定失敗返回-1,同時錯誤碼會被設置。

套接字創建完畢后我們就需要進行綁定了,但在綁定之前我們需要先定義一個struct sockaddr_in結構,將對應的網絡屬性信息填充到該結構當中。由于該結構體當中還有部分選填字段,因此我們最好在填充之前對該結構體變量里面的內容進行清空。

然后再將協議家族、端口號、IP地址等信息填充到該結構體變量當中。需要注意的是,在發送到網絡之前需要將端口號設置為網絡序列,由于端口號是16位的,因此我們需要使用前面說到的htons函數將端口號轉為網絡序列。

此外,由于網絡當中傳輸的是整數IP,我們如何快速的將字符串IP和整數IP快速轉化呢?

那么上面的這個需要我們自己來實現嘛?那網絡用起來也太繁瑣了吧!不要緊,操作系統為我們提供了方法:inet_addr,能將字符串分隔的地址轉成網絡序列的的四字節整數,我們需要調用inet_addr函數將字符串IP轉換成整數IP,然后再將轉換后的整數IP進行設置。

這里有一點細節需要注意:

因此我們這里需要使用in_add的成員s_addr才能將我們的類型進行很好的匹配。

我們的struct sockaddr_in local在哪呢?它在進程地址空間中用戶的棧上面,也就是在用戶區,我們給結構體填入所有的內容都是在用戶區填入的,但是socket套接字是系統調用,在內核區,也就是說此時我們并沒有和內核中的套接字相關聯,所以我們此時就要綁定bind.

由于bind函數提供的是通用參數類型,因此在傳入結構體地址時還需要將struct sockaddr_in*強轉為struct sockaddr*類型后再進行傳入。

class UdpServer
{
public:UdpServer(uint16_t port = defaultport, const string &ip = defaultip): _port(port), _ip(ip){}void Init(){// 1.創建udp socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) // 創建套接字失敗{log.operator()(Fatal, "sockfd create error: %d", _sockfd);exit(-1);}log(Info, "socket create success, sockfd: %d", _sockfd); // 3// 2.綁定端口號struct sockaddr_in local;bzero(&local, sizeof(local)); // 清空結構體local.sin_family = AF_INET;local.sin_port = htons(_port);//需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的//1.string -> uint_32_t//2.必須是網絡序列的local.sin_addr.s_addr = inet_addr(_ip.c_str());int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));if(n < 0) //綁定失敗{log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}log(Info,"bind create success"); }void Run() {}~UdpServer() {}private:int _sockfd;    // 網絡文件描述符string _ip;     // 服務器的ipuint16_t _port; // 服務器進程的端口號
};

我們來運行一下:

3.服務器運行并接收處理請求

接下里我們就要讓我們的服務器跑起來,作為一款服務器,它應該是24小時始終運行的。服務器實際上就是在周而復始的為我們提供某種服務,服務器之所以稱為服務器,是因為服務器運行起來后就永遠不會退出,因此服務器實際執行的是一個死循環代碼。由于UDP服務器是不面向連接的,因此只要UDP服務器啟動后,就可以直接讀取客戶端發來的數據,怎么讀取呢?UDP服務器不是面向字節流的所以不能用read,它是面向數據報的,所以要使用recvfrom

?參數說明:

  • sockfd:對應操作的文件描述符。表示從該文件描述符索引的文件當中讀取數據。
  • buf:讀取數據的存放位置。
  • len:期望讀取數據的字節數。
  • flags:讀取的方式。一般設置為0,表示阻塞讀取。
  • src_addr:用戶端網絡相關的屬性信息,包括協議家族、IP地址、端口號等,輸出型參數。
  • addrlen:調用時傳入期望讀取的src_addr結構體的長度,返回時代表實際讀取到的src_addr結構體的長度,這是一個輸入輸出型參數。

?返回值說明:

  • 讀取成功返回實際讀取到的字節數,讀取失敗返回-1,同時錯誤碼會被設置。

?注意:

  • 由于UDP是不面向連接的,因此我們除了獲取到數據以外還需要獲取到用戶端網絡相關的屬性信息,包括IP地址和端口號等。
  • 在調用recvfrom讀取數據時,必須將addrlen設置為你要讀取的結構體對應的大小。
  • 由于recvfrom函數提供的參數也是struct sockaddr*類型的,因此我們在傳入結構體地址時需要將struct sockaddr_in*類型進行強轉。

4.服務器發送請求結果

發送數據的函數叫做sendto,該函數的函數原型如下:

?參數說明:

  • sockfd:對應操作的文件描述符。表示將數據寫入該文件描述符索引的文件當中。
  • buf:待寫入數據的存放位置。
  • len:期望寫入數據的字節數。
  • flags:寫入的方式。一般設置為0,表示阻塞寫入。
  • dest_addr:對端網絡相關的屬性信息,包括協議家族、IP地址、端口號等,輸入型參數。
  • addrlen:傳入dest_addr結構體的長度,輸入型參數。

?返回值說明:

  • 寫入成功返回實際寫入的字節數,寫入失敗返回-1,同時錯誤碼會被設置。

?注意:

  • 由于UDP不是面向連接的,因此除了傳入待發送的數據以外還需要指明對端網絡相關的信息,包括IP地址和端口號等。
  • 由于sendto函數提供的參數也是struct sockaddr*類型的,因此我們在傳入結構體地址時需要將struct sockaddr_in*類型進行強轉。
class UdpServer
{
public:UdpServer(uint16_t port = defaultport, const string &ip = defaultip): _port(port), _ip(ip){}void Init(){// 1.創建udp socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) // 創建套接字失敗{log.operator()(Fatal, "sockfd create error: %d", _sockfd);exit(-1);}log(Info, "socket create success, sockfd: %d", _sockfd); // 3// 2.綁定端口號struct sockaddr_in local;bzero(&local, sizeof(local)); // 清空結構體local.sin_family = AF_INET;local.sin_port = htons(_port);//需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的//1.string -> uint_32_t//2.必須是網絡序列的local.sin_addr.s_addr = inet_addr(_ip.c_str());int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));if(n < 0) //綁定失敗{log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}log(Info,"bind create success"); }void Run() {// 服務器一直在運行_isrunning = true;char inbuffer[1024];while(_isrunning){// 獲取用戶端的ip,端口號,用戶發送的請求struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){log(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = '\0'; //當作字符串來看// 數據的處理string info = inbuffer;string echo_string = "sever echo#" + info;// 數據發送給用戶sendto(_sockfd, echo_string.c_str(), echo_string.size(),0,(const struct sockaddr*)&client, len);}}~UdpServer() {}private:int _sockfd;    // 網絡文件描述符string _ip;     // 服務器的ipuint16_t _port; // 服務器進程的端口號bool _isrunning; // 服務器是否在運行
};

此時程序運行起來了,但是服務器到底有沒有啟動呢?我們可以通過netstat指令查看

netstat -nlup 是一個在類Unix系統(如Linux和macOS)中常用的命令,用于查看網絡連接狀態。這個命令結合了幾個選項來提供特定的輸出信息。下面是對每個選項的解釋:

  • -n:表示以數字形式顯示IP地址和端口號,而不是嘗試將其解析為主機名和服務名稱。
  • -l:顯示正在監聽的套接字。
  • -u:顯示UDP協議的連接信息。
  • -p:顯示與每個連接或監聽端口關聯的進程ID和進程名稱。

并且此時能看到服務器的ip是0.0.0.0,端口號是8080

我們現在使用的是輕量級服務器,我們可以嘗試一下綁定我們的遠端服務器的ip,看看此時有什么效果。

此時綁定失敗了,為什么呢?云服務器禁止直接綁定公網ip,云服務的ip地址可能有多個,如果你指綁定一個,其他的ip就收不到請求。怎么解決呢?將綁定操作中的IP地址設為0,代表“任意IP地址”。這意味著相應的網絡服務將會監聽并接受來自該主機上任何IP地址的所有網絡接口上的連接請求。bind(IP:0):凡是發給我這臺主機的數據,我們都要根據端口號向上交付,這種方式叫做任意地址綁定,所以我們剛剛綁定的ip就可以這樣寫啦!

從此以后,凡是發給我這臺主機的數據,可以忽略ip地址,只需要使用端口號向上交付。然后我們在恢復之前的ip地址的形式,隨后我們剛剛給我們端口號設置的是8080,現在我們設置成80,結果咋樣呢?

提示沒有權限,好,我們提權sudo

此時的端口號通過提權就能綁定成功,但是為什么剛剛8080的端口號就不需要提權呢?[0,1023]:系統內定的端口號, 一般都要有固定的應用層協議使用,http: 80 https: 443 mysq: 3606...,期望我們綁定的端口號都在1024以上。所以我們的端口號該怎么處理呢?使用我們的命令行參數決定綁定哪一個端口號。

void Usage(string proc)
{cout << "\n\tUsage: " << proc << " port[1024+]" << endl;
}// ./udpserver port
int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = stoi(argv[1]);unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run();return 0;
}

運行結果:

那我們總得看效果吧,光讓服務器跑起來沒啥用處啊,接下里我們就來寫一個客戶端。

5.編寫客戶端

1.本地網絡通信

?細節問題:

直接來看代碼:

void Usage(string proc)
{cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 我怎么知道服務器是誰呀 - 命令行參數來解決struct sockaddr_in server;bzero(&server, sizeof(server)); // 清空結構體server.sin_family = AF_INET;server.sin_port = htons(serverport); // 需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的// 1.string -> uint_32_t// 2.必須是網絡序列的server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) // 創建套接字失敗{cout << "sockfd create error";exit(1);}log(Info, "socket create success, sockfd: %d", sockfd); // 3// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// client 要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送數據的時候string message;char buffer[1024];while (true){cout << "Please Enter: ";getline(cin, message);// 發送信息給服務端sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);// 接收服務端的信息// recvform輸出型參數struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = '\0';cout << buffer << endl;}}close(sockfd);return 0;
}

?注意:我們要傳入云服務器的私有ip,不是我們的公網ip喲!后面解釋!!!

我們來看看運行結果:

此時你只要擁有了客戶端的這個代碼,ip和端口號,那么你就可以給我這臺機器隨便發消息,服務器都能收到!!!

但是現在我們不想在服務器處理用戶發過來的數據,我們在用戶來處理數據,這份做法本質是讓代碼進行分層,方便維護。

std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;return res;
}

同時這里用戶還可以隨意設置自己想要的結果,所以我們現在就可以寫一點好玩的。我們可以把傳入的字符串當作指令來處理。

popen 是一個在 C 語言中使用的函數,用于通過創建一個管道(pipe)來啟動一個子進程,并執行一個shell命令。這個函數允許父進程與子進程之間進行輸入/輸出通信。popen 函數的主要特點和用途包括:

  1. 創建管道:它首先創建一個管道,這是一個半雙工的通信機制,允許數據在兩個進程間單向流動。

  2. 啟動子進程:接著,通過 fork() 系統調用創建一個子進程。子進程繼承了父進程的管道描述符。

  3. 執行命令:在子進程中,使用 execl() 或相似的函數來執行一個shell命令。這使得父進程能夠間接地執行系統命令或外部程序。

  4. 返回文件指針popen 函數返回一個 FILE * 類型的文件指針。如果命令執行成功,這個文件指針可以用作 fread()fwrite()fgets() 等標準I/O函數的參數,從而讀取子進程的輸出的數據。

  5. 關閉管道:當完成通信后,應該使用 pclose() 函數來關閉管道并等待子進程結束。pclose() 也會返回子進程的退出狀態。

std::string ExcuteCommand(const std::string &cmd)
{// popen創建一個管道并來啟動一個子進程執行shell命令// 隨后通過管道將執行的命令給父進程// 父進程可以通過打開文件來看執行結果FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}// 拿執行結果std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}

此時我們就可以根據字符串執行相應的指令了,但是我們還是要檢測一下指令的輸入,萬一客戶端來個刪庫的命令哪咋辦,所以我們要處理一下,保證指令是一個安全的指令。

bool SafeCheck(const string &cmd)
{vector<string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for (auto e : key_word){auto pos = cmd.find(e);if (pos != string::npos){return false;}}return true;
}std::string ExcuteCommand(const std::string &cmd)
{if(!SafeCheck(cmd)) //安全檢查return "Bad man";// popen創建一個管道并來啟動一個子進程執行shell命令// 隨后通過管道將執行的命令給父進程// 父進程可以通過打開文件來看執行結果FILE *fp = popen(cmd.c_str(), "r");if (nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while (true){char *ok = fgets(buffer, sizeof(buffer), fp);if (ok == nullptr)break;result += buffer;}pclose(fp);return result;
}

之前都是使用的云服務器的ip地址,現在我們學習一個新的ip地址,127.0.0.1是一個特殊的IP地址,用于回環測試。它也被稱為本地主機或回環地址。當一個設備向這個IP地址發送數據時,數據并不會離開該設備,而是在設備自身的網絡堆棧中進行循環。這通常用于在不涉及外部網絡的情況下,在本地機器上測試網絡應用程序和服務。現在我們在構造的時候傳入127.0.0.1的ip地址。

?127.0.0.1:本地環回地址,通常用它來進行cs的測試

其實上面的用戶端就類似于我們的xshell,我們每次登錄的時候都需要ip地址去連接遠端服務器,我們在xshell里面輸入的字符串,服務器會接收到并處理好返回給我們。

完整代碼展示:

makefile:

.PHONY:all
all:udpserver udpclient
udpserver:main.cppg++ -o $@ $^ -std=c++11
udpclient:udpClient.cppg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient

udpClient.cpp:

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;void Usage(string proc)
{cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 我怎么知道服務器是誰呀 - 命令行參數來解決struct sockaddr_in server;bzero(&server, sizeof(server)); // 清空結構體server.sin_family = AF_INET;server.sin_port = htons(serverport); // 需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的// 1.string -> uint_32_t// 2.必須是網絡序列的server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) // 創建套接字失敗{cout << "sockfd create error";exit(1);}// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// client 要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送數據的時候string message;char buffer[1024];while (true){cout << "Please Enter: ";getline(cin, message);// 發送信息給服務端sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);// 接收服務端的信息// recvform輸出型參數struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = '\0';cout << buffer << endl;}}close(sockfd);return 0;
}

udpServer.hpp:

#pragma once#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <unistd.h>
#include <sys/types.h>
#include <cstdlib>
#include <functional>using namespace std;using func_t = function<string(const string&)>;
// 類似于
// typedef function<string(const string&)> func_t;Log lg;enum
{SOCKET_ERR = 1,BIND_ERR = 2
};uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";class UdpServer
{
public:UdpServer(uint16_t port = defaultport, const string &ip = defaultip): _port(port), _ip(ip), _sockfd(0),_isrunning(false){}void Init(){// 1.創建udp socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) // 創建套接字失敗{lg.operator()(Fatal, "sockfd create error: %d", _sockfd);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", _sockfd); // 3// 2.綁定端口號struct sockaddr_in local;bzero(&local, sizeof(local)); // 清空結構體local.sin_family = AF_INET;local.sin_port = htons(_port);//需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的//1.string -> uint_32_t//2.必須是網絡序列的local.sin_addr.s_addr = inet_addr(_ip.c_str());//local.sin_addr.s_addr = INADDR_ANY; // 任意ip地址int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));if(n < 0) //綁定失敗{lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info,"bind create success"); }void Run(func_t func) {// 服務器一直在運行_isrunning = true;char inbuffer[1024];while(_isrunning){// 獲取用戶端的ip,端口號,用戶發送的請求struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(_sockfd, inbuffer, 1023, 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = '\0'; //當作字符串來看// 數據的處理string info = inbuffer;string echo_string = func(info);// cout << echo_string << endl;// 數據發送給用戶sendto(_sockfd, echo_string.c_str(), echo_string.size(),0,(const struct sockaddr*)&client, len);}}~UdpServer() {if(_sockfd > 0)close(_sockfd);}private:int _sockfd;    // 網絡文件描述符string _ip;     // 服務器的ip  任意地址綁定uint16_t _port; // 服務器進程的端口號bool _isrunning; // 服務器是否在運行
};

main.cpp:

#include "udpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>// "120.78.126.148" 點分十進制字符串風格的IP地址
// 4個字節ip地址,但是用戶不關心void Usage(string proc)
{cout << "\n\tUsage: " << proc << " port[1024+]" << endl;
}std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;return res;
}bool SafeCheck(const string &cmd)
{vector<string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for (auto e : key_word){auto pos = cmd.find(e);if (pos != string::npos){return false;}}return true;
}std::string ExcuteCommand(const std::string &cmd)
{if (!SafeCheck(cmd))// 安全檢查return "Bad man";// popen創建一個管道并來啟動一個子進程執行shell命令// 隨后通過管道將執行的命令給父進程// 父進程可以通過打開文件來看執行結果FILE *fp = popen(cmd.c_str(), "r");if (nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while (true){char *ok = fgets(buffer, sizeof(buffer), fp);if (ok == nullptr)break;result += buffer;}pclose(fp);return result;
}// ./udpserver port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = stoi(argv[1]);unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run(Handler);return 0;
}

2.跨平臺網絡通信

linux的套接字接口和windows的套接字接口一樣嗎?雖然操作系統是不同的,但是它們都遵守網絡標準,底層的網絡協議棧是相同的,所以它們的套接字接口都是一樣的,所以兩個不同的平臺也可以進行網絡通信,那咱們來試試!!!

#pragma warning(disable:4996) //inet_addr,不安全,直接禁掉#include <iostream>
#include <winsock2.h>
#include <Windows.h>
#include <string>#pragma comment(lib,"ws2_32.lib")using namespace std;#define IP ""172.17.40.254""
#define PORT 8080int main(int argc, char* argv[])
{//初始化網絡環境WSADATA wsa;if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0){cout << "WSAStartup failed" << endl;return -1;}// 申明一個網絡地址信息的結構體,保存服務器的地址信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(PORT);server.sin_addr.s_addr = inet_addr(IP);//建立一個udp的socketSOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);if (sockClient < 0){cout << "create socket failed" << endl;return -1;}string message;char buffer[1024];while (true){cout << "Please Enter: ";getline(cin, message);// 發送信息給服務端sendto(sockClient, message.c_str(), (int)message.size(), 0, (const struct sockaddr*)&server, sizeof(server));// 接收服務端的信息// recvform輸出型參數struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockClient, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = '\0';cout << buffer << endl;}}//關閉sockClientclosesocket(sockClient);//清理網絡環境WSACleanup();system("pause");return 0;
}

那咱們來運行一下哈

3.簡易的群聊系統

首先我們就需要獲取到用戶的端口號和ip地址,我們可以在服務器哪里進行獲取。

此時就成功獲取了用戶那端的ip和端口號,此時我只有一個主機,服務端和客戶端都在同一臺主機上,所以ip地址都一樣,這在群聊中相當于自己發信息給自己。

作為服務器,服務器不僅收到了用戶發送的信息,還接收了到了用戶的ip地址,因此我們就可以通過ip地址來標識用戶,所以我們可以維護一個登錄列表,看看當前有多少用戶登錄了服務器,直接寫代碼。

#pragma once#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <unistd.h>
#include <sys/types.h>
#include <cstdlib>
#include <functional>
#include <unordered_map>using namespace std;using func_t = function<string(const string &, const string &, uint16_t &)>;
// 類似于
// typedef function<string(const string&)> func_t;Log lg;enum
{SOCKET_ERR = 1,BIND_ERR = 2
};uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";class UdpServer
{
public:UdpServer(uint16_t port = defaultport, const string &ip = defaultip): _port(port), _ip(ip), _sockfd(0), _isrunning(false){}void Init(){// 1.創建udp socket// udp 的socket是全雙工的,允許被同時讀寫的_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0) // 創建套接字失敗{lg.operator()(Fatal, "sockfd create error: %d", _sockfd);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", _sockfd); // 3// 2.綁定端口號struct sockaddr_in local;bzero(&local, sizeof(local)); // 清空結構體local.sin_family = AF_INET;local.sin_port = htons(_port); // 需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的// 1.string -> uint_32_t// 2.必須是網絡序列的local.sin_addr.s_addr = inet_addr(_ip.c_str());// local.sin_addr.s_addr = INADDR_ANY; // 任意ip地址int n = bind(_sockfd, (const struct sockaddr *)&local, sizeof(local));if (n < 0) // 綁定失敗{lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind create success");}void CheckUser(const struct sockaddr_in &client, const string& clientip, uint16_t& clientport){auto iter = _onlineuser.find(clientip); // 找ipif (iter == _onlineuser.end()){// 添加用戶 - 入群_onlineuser.insert({clientip, client});cout << "[" << clientip << ":" << clientport << "] add to online user" << endl;}else{return;}}void Broadcast(const string& info, const string& clientip, uint16_t& clientport){for(const auto& user: _onlineuser){// 數據的處理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[1024];while (_isrunning){// 獲取用戶端的ip,端口號,用戶發送的請求struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(_sockfd, inbuffer, 1023, 0, (struct sockaddr *)&client, &len);if (n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = '\0'; // 當作字符串來看// 拿到用戶端的端口號和ip地址uint16_t clientport = ntohs(client.sin_port);string clientip = inet_ntoa(client.sin_addr);// 判斷是否是一個新用戶CheckUser(client, clientip, clientport);// 數據發送給所有用戶string info = inbuffer;Broadcast(info, clientip, clientport);}}~UdpServer(){if (_sockfd > 0)close(_sockfd);}private:int _sockfd;     // 網絡文件描述符string _ip;      // 服務器的ip  任意地址綁定uint16_t _port;  // 服務器進程的端口號bool _isrunning; // 服務器是否在運行unordered_map<string, struct sockaddr_in> _onlineuser;
};

我們來看看運行結果:

但是我們的客戶端是一個單進程,向服務器發信息和從服務器收到信息都在同一個進程,并且我們是先發送信息的,所以就有一點尷尬,我們任意一個用戶只有發一個信息才能收到其他用戶發出的信息,如果它不發信息,getline便會阻塞住,代碼就不能繼續向后執行,盡管此時別的服務器給我發送了信息,但是我們的代碼還在getline那里阻塞者呢,我們還沒執行到從服務器接收信息的代碼,所以此時不能收到信息,但是群聊的時候,我們不發消息也能收到其他人的信息呀!此時我們就需要多線程來解決,向服務器發信息和從服務器收到信息使用多線程,讓它倆互不干擾。

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>using namespace std;struct ThreadData
{struct sockaddr_in server;int sockfd;
};void Usage(string proc)
{cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}void *recv_message(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while (true){// 接收服務端的信息// recvform輸出型參數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';cout << buffer << endl;}}
}void *send_message(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string message;socklen_t len = sizeof(td->server);while (true){cout << "Please Enter: ";getline(cin, message);// 發送信息給服務端sendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&td->server, len);}
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);struct ThreadData td;// 我怎么知道服務器是誰呀 - 命令行參數來解決bzero(&td.server, sizeof(td.server)); // 清空結構體td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport); // 需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的// 1.string -> uint_32_t// 2.必須是網絡序列的td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0) // 創建套接字失敗{cout << "sockfd create error";exit(1);}// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// client 要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送數據的時候pthread_t recvr, sender;pthread_create(&recvr, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);pthread_join(recvr, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}

運行一下:

但是此時我們的輸入和輸出都在一個窗口,看著比較混亂,我們可以多開幾個終端來讓它們分開,linux下一些皆文件,我們的終端也是文件。

此時我們可以發現我們這個終端其實就是dev/pts目錄下的0號文件,所以我們可以借助這個文件向終端輸入內容,那我們怎么通過代碼來執行呢?

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string terminal = "/dev/pts/0";
using namespace std;
int main()
{int fd = open(terminal.c_str(), O_WRONLY);if (fd < 0){std::cerr << "open terminal error" << std::endl;exit(1);}//cout << fd << endl;dup2(fd, 1); // 重定向標準輸出到/dev/pts/0printf("hello world\n");close(fd);return 0;
}

運行一下:

緊接著我們立馬把它應用到客戶端,讓客戶端的輸入和輸出在兩個終端。

Treminal.hpp

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string terminal = "/dev/pts/0";
using namespace std;int OpenTerminal()
{int fd = open(terminal.c_str(), O_WRONLY);if (fd < 0){std::cerr << "open terminal error" << std::endl;exit(1);}//cout << fd << endl;// 由于線程的文件描述符是共享的,所以我換一個dup2(fd, 2); // 重定向標準錯誤到/dev/pts/0return 0;
}

udpClient.cpp

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.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;struct ThreadData
{struct sockaddr_in server;int sockfd;
};void Usage(string proc)
{cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}void *recv_message(void *args)
{OpenTerminal();ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while (true){// 接收服務端的信息// recvform輸出型參數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; //使用標準錯誤// 此時我們的標準錯誤就已經重定向到終端}}
}void *send_message(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string message;socklen_t len = sizeof(td->server);while (true){cout << "Please Enter: ";getline(cin, message);// 發送信息給服務端sendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&td->server, len);}
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);struct ThreadData td;// 我怎么知道服務器是誰呀 - 命令行參數來解決bzero(&td.server, sizeof(td.server)); // 清空結構體td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport); // 需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的// 1.string -> uint_32_t// 2.必須是網絡序列的td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0) // 創建套接字失敗{cout << "sockfd create error";exit(1);}// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// client 要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送數據的時候pthread_t recvr, sender;pthread_create(&recvr, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);pthread_join(recvr, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}

運行結果:

此時就完成了簡單的群聊系統。還有一種簡單的方法,我們上面的用戶發出信息是使用的標準輸出,而收到信息是標準錯誤,兩個文件描述符是不同的,所以我們可以直接將用戶端輸入ip和端口號的地方標準錯誤重定向到我們的終端下即可。

./udpclient 172.17.40.254 8080 2 > /dev/pts/0

現在我們再來優化一下,每個用戶在一旦訪問我們的服務器的時候,我們可以設置一條歡迎語。

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"
#include <string.h>using namespace std;struct ThreadData
{struct sockaddr_in server;int sockfd;std::string serverip;
};void Usage(string proc)
{cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}void *recv_message(void *args)
{OpenTerminal();ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while (true){memset(buffer, 0, sizeof(buffer));// 接收服務端的信息// recvform輸出型參數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; // 使用標準錯誤// 此時我們的標準錯誤就已經重定向到終端}}
}void *send_message(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string message;socklen_t len = sizeof(td->server);// 發送歡迎語std::string welcome = td->serverip;welcome += " 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, (const struct sockaddr *)&td->server, len);}
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);struct ThreadData td;// 我怎么知道服務器是誰呀 - 命令行參數來解決bzero(&td.server, sizeof(td.server)); // 清空結構體td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport); // 需要保證我的端口號是網絡字節序列(大端),因為該端口號是要給對方發送的// 1.string -> uint_32_t// 2.必須是網絡序列的td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0) // 創建套接字失敗{cout << "sockfd create error";exit(1);}td.serverip = serverip;// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// client 要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送數據的時候pthread_t recvr, sender;pthread_create(&recvr, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);pthread_join(recvr, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}

其實這個誰誰conmming,就相當于誰誰已經加入群聊啦!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/15791.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/15791.shtml
英文地址,請注明出處:http://en.pswp.cn/web/15791.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Nginx代理配置(專業版)

寫在前面提醒&#xff1a;使用代理&#xff0c;如果可以&#xff0c;請盡量支持雙協議&#xff0c;http、https均要支持哈。 注意&#xff1a;監控系統只是運行代碼&#xff0c;是否支持https&#xff0c;需要運維同學在你們的服務器上配置https證書&#xff0c;配置好證書&…

在 CentOS 上安裝 PostgreSQL 的全面指南

PostgreSQL 是一種功能強大的開源關系型數據庫管理系統&#xff0c;廣泛應用于各種領域。它提供了諸如事務處理、并發控制和數據完整性等高級功能&#xff0c;因此深受開發者和企業的歡迎。本指南將逐步引導您在 CentOS 上安裝 PostgreSQL&#xff0c;以便您充分利用其眾多優勢…

決定了,將ChatGPTer開源!主打一個大模型人人可用。

一個快速上手且極易部署的類ChatGPT開源應用&#xff0c;可接入 OPENAI API 或 通義千問API 開源地址&#xff1a; https://github.com/isnl/EsChat 大聲(偷偷)告訴你&#xff1a;通義千問有免費API額度可白嫖&#xff01;&#xff01;&#xff01; 版本特性 OPENAI 和 通義千…

點云AABB、OBB包圍盒計算顯示

目錄 一、簡介 1)AABB包圍盒 2)OBB包圍盒 二、計算代碼 三、加載計算結果

什么是Promise

Promise 是 JavaScript 中的一個對象&#xff0c;用于處理異步操作。它代表了一個最終可能完成&#xff08;也可能被拒絕&#xff09;的異步操作及其結果值。Promise 對象用于更復雜的異步編程模式&#xff0c;包括使用 .then() 和 .catch() 鏈式調用來處理異步操作的結果。 P…

算法提高之區間最大公約數

算法提高之區間最大公約數 核心思想&#xff1a;線段樹 1.在區間上加一個數 差分 2.求一段區間的最gcd 求[l,r]的gcd 可以拆解為求**[1,l].sum(差分數組 求出來時l點的值)和[l1,r]**做gcd #include <iostream>#include <cstring>#include <algorithm>usi…

1738. 找出第 K 大的異或坐標值

1738. 找出第 K 大的異或坐標值 題目鏈接&#xff1a;1738. 找出第 K 大的異或坐標值 代碼如下&#xff1a; //列前綴異或和 //參考鏈接:https://leetcode.cn/problems/find-kth-largest-xor-coordinate-value/solutions/2790359/liang-chong-fang-fa-er-wei-qian-zhui-yi-68…

Docker數據卷(volume)

數據卷 數據卷是一個虛擬目錄&#xff0c;是容器內目錄與宿主機目錄之間映射的橋梁。&#xff08;容器內目錄與宿主機目錄對應的橋梁&#xff0c;修改宿主機對應的目錄&#xff0c;docker會映射到容器內部&#xff0c;相當于修改了容器內的&#xff0c;反之也一樣&#xff09;數…

利用英特爾 Gaudi 2 和至強 CPU 構建經濟高效的企業級 RAG 應用

檢索增強生成 (Retrieval Augmented Generation&#xff0c;RAG) 可將存儲在外部數據庫中的新鮮領域知識納入大語言模型以增強其文本生成能力。其提供了一種將公司數據與訓練期間語言模型學到的知識分開的方式&#xff0c;有助于我們在性能、準確性及安全隱私之間進行有效折衷。…

任推邦:實力強勁的APP推廣拉新平臺,號稱不扣量

任推邦簡介 任推邦是國內數一數二的項目分發平臺&#xff0c;也是一個不扣量的項目APP推廣拉新平臺&#xff0c;隸屬于聚名科技集團股份有限公司。聚名科技成立時間在2012年&#xff0c;是安徽省老牌互聯網企業&#xff0c;歷經11年的飛速發展&#xff0c;聚名科技成功布局打造…

小程序的這些知識你知道嗎?

一:導航傳參 無論是編程式還是聲明式導骯傳參都是在url?keyvalue&key1value1,無論是否是tabbar頁面. 對于回退頁面,沒辦法傳參. 這個參數是,跳轉到頁面的時候,跳轉到另一個頁面,這個頁面就是剛開始執行,等數據執行之后,觸發onload,傳遞的參數放在內存中,跳轉是內部底層觸…

云端力量:利用移動云服務器高效部署Spring Boot Web應用

文章目錄 一、移動云介紹二、移動云產品選擇三、體驗云主機ECS四、使用移動云服務器部署SpringBoot Web應用4.1移動云ECS安裝JDK4.2移動云ECS安裝MySQL4.3移動云ECS數據庫插入數據4.4移動云ECS部署Spring Boot Web應用 總結 一、移動云介紹 移動云是中國移動基于自研的先進技術…

Linux中常見的基本指令(上)

目錄 一、ls指令 1. ls 2. ls -l 3. ls -a 4.ls -F 二、qwd指令 三、cd指令 1. cd .. 2. cd / / / 3. cd ../ / / 4. cd ~ 5. cd - 五、mkdir指令 六、rmdir指令和rm指令 一、ls指令 語法 &#xff1a; ls [ 選項 ][ 目錄或文件 ] 。 功能 &#xff1a;對于目錄…

桶排序和基數排序

前言&#xff1a; 這篇文章&#xff0c;我們就來了解一些鮮為人知的排序&#xff0c;桶排序和基數排序。 桶排序&#xff1a; 桶排序的思想&#xff1a; 桶排序的思想就是把待排序的數盡量均勻地放到各個桶中&#xff0c;再對各個桶進行局部的排序&#xff0c;最后再按序將各…

AI Agent: Agent框架+7個實例

何謂Agent Agent 作為一種新興的人工智能技術&#xff0c;正在受到越來越多的關注。要說清楚什么是 Agent&#xff0c;先得看看人工智能的本質是什么。 人工智能這個名稱來自它試圖通過計算機程序或機器來模擬、擴展和增強人類智能的 一些方面。在這個定義中&#xff0c;“人…

C# WPF入門學習(四)—— 按鈕控件

上期介紹了WPF的實現架構和原理&#xff0c;之后我們開始來使用WPF來學習各種控件。 一、嘗試插入一個按鈕&#xff08;方法一&#xff09; 1. VS2019 在界面中&#xff0c;點擊工具欄中的視圖&#xff0c;在下拉菜單中選擇工具箱。 至于編譯器中的視圖怎么舒服怎么來布置&am…

Cocos Creator 幀動畫播放組件制作詳解

Cocos Creator 是一個強大的游戲開發工具&#xff0c;提供了豐富的功能和組件&#xff0c;其中幀動畫播放組件是游戲開發中常用的組件之一&#xff0c;通過幀動畫播放組件可以實現角色動畫、特效動畫等效果。本文將詳細介紹如何使用 Cocos Creator 制作幀動畫播放組件&#xff…

infoq學習筆記-云原生網關當道,三大主流廠商如何“競 技”?

注基礎組件的質量&#xff0c;這些基礎組件是用戶看不到的。這些組件包括代碼質量、自動化的CI/CD、端對端測試、混沌測試等。在APISIX中&#xff0c;我們內置了大 量的測試案例代碼&#xff0c;包括單元測試、E2E測試、混沌測試&#xff0c;以及一些基準測試等&#xff0c;從而…

沈陽師范大學文學院副教授傅贏

女&#xff0c;生于1971年6月&#xff0c;遼寧遼陽人&#xff0c;1995年6月畢業于沈陽師范學院中文系漢語言文學教育專業&#xff0c;2000年6月于東北師范大學獲中國現當代文學專業文學碩士學位&#xff0c;現為文學院漢語國際教育專業教師&#xff0c;副教授。 主要從事對外漢…

藍橋杯練習系統(算法訓練)ALGO-934 序列

資源限制 內存限制&#xff1a;256.0MB C/C時間限制&#xff1a;1.0s Java時間限制&#xff1a;3.0s Python時間限制&#xff1a;5.0s 問題描述 王神想要知道n的所有排列的逆序對數和&#xff0c;但是他覺得太水了&#xff0c;于是讓你算。 輸入格式 一行一個整數n 輸…