本章重點
能夠實現一個簡單的udp客戶端/服務器;
1.創建套接字
我們把服務器封裝成一個類,當我們定義出一個服務器對象后需要馬上初始化服務器,而初始化服務器需要做的第一件事就是創建套接字。
?參數說明:
- domain:創建套接字的域或者叫做協議家族,也就是創建套接字的類型。該參數就相當于struct sockaddr結構的前16個位。如果是本地通信就設置為AF_UNIX,如果是網絡通信就設置為AF_INET(IPv4)或 AF_INET6(IPv6)。
- type:創建套接字時所需的服務類型。其中最常見的服務類型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的網絡通信,我們采用的就是SOCK_DGRAM,叫做用戶數據報服務,如果是基于TCP的網絡通信,我們采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服務。
- protocol:創建套接字的協議類別。你可以指明為TCP或UDP,但該字段一般直接設置為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
函數的主要特點和用途包括:
創建管道:它首先創建一個管道,這是一個半雙工的通信機制,允許數據在兩個進程間單向流動。
啟動子進程:接著,通過
fork()
系統調用創建一個子進程。子進程繼承了父進程的管道描述符。執行命令:在子進程中,使用
execl()
或相似的函數來執行一個shell命令。這使得父進程能夠間接地執行系統命令或外部程序。返回文件指針:
popen
函數返回一個FILE *
類型的文件指針。如果命令執行成功,這個文件指針可以用作fread()
、fwrite()
、fgets()
等標準I/O函數的參數,從而讀取子進程的輸出的數據。關閉管道:當完成通信后,應該使用
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,就相當于誰誰已經加入群聊啦!!!