目錄
一、實現目標:
二、實驗步驟:
1、服務端代碼解析:
Init():
Run():
2、客戶端代碼:
主函數邏輯:
send_message發送數據:
recv_message接收數據:
三、實驗結果:
四、拓展:
五、全部代碼:
一、實現目標:
實現基于udp套接字實現的網絡通信,這里我們實現客戶端和服務端
首先在服務端中維護一張哈希表,存儲的kv值是客戶端的ip地址和sockaddr_in,然后服務端用于接收客戶端發送的信息,并進行處理,如果當前客戶端在哈希表中就不做處理,如果不在就添加到哈希表中,并且廣播給哈希表中的所有用戶
對于客戶端,為了完成類似于QQ這樣的方式,能夠一邊發送信息給服務端,并且能夠保證在不發送的時候也能從服務端中讀取到數據,所以就需要用到多線程并發了,一個線程從服務端中讀取數據,并且打印出來看看;另一個線程向服務端中發送數據
通信的原理就是向_sockfd這個網絡文件描述符中同時進行讀寫
二、實現代碼:
其中log.hpp是在系統部分學到的,當時封裝好的一個日志文件
在本次實驗中將服務端進行封裝了,客戶端未進行封裝
1、服務端代碼解析:
如下是服務端的main.cc,就是通過智能指針實現服務端的類,然后初始化,啟動服務器即可
void Usege(char* proc)
{std::cout<<"\n\tUsage: "<<proc<<"port[1024+]\n"<<std::endl;
}int main(int argc,char* argv[])
{if(argc != 2){Usege(argv[0]);exit(1);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run(/*ExcuteCommand*/);return 0;
}
接著是服務端的核心代碼框架:
#pragma once
#include <iostream>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <unordered_map>
#include <arpa/inet.h>#include "log.hpp"
#define SIZE 1024
Log lg;
// enum 搞一個錯誤碼合集
enum
{SOCKET_ERR = 1,BIND_ERR = 2
};
// const uint16_t defaultport = 3306;
const std::string defaultip = "0.0.0.0";
class UdpServer
{
public:UdpServer(const uint16_t& port/* = defaultport*/,const std::string& ip = defaultip):_sockfd(0),_port(port),_ip(ip),_isrunning(false){}void Init(){}void CheckUser(const struct sockaddr_in& client,const uint16_t& clientport,const std::string& clientip){}void Broadcast(const std::string& info,const uint16_t& clientport,const std::string& clientip){}void Run(){}~UdpServer(){}
private:int _sockfd; // 網絡文件描述符std::string _ip; // uint16_t _port; // 服務器進程的端口號bool _isrunning; // 服務器在啟動后要一直保證運行中std::unordered_map<std::string,struct sockaddr_in> online_user;// 將在聊天室中的人都存儲在哈希表中
};
其中成員變量:
_sockfd就是網絡文件描述符
_ip就是指定服務器綁定的IP地址,并且這里給了缺省值,也就是在外部如果沒有傳ip就采用默認值表示綁定所有可用網絡接口
_port表示服務器進程的端口號
_isrunning表示服務器是否在運行中的狀態
online_user是一個哈希表,表示當前聊天室中存在的人
接下來依次實現各個函數的功能即可
Init():
在初始化服務端這里:
首先就是床加你socket,這里是采用的IPV4,所以需要初始化struct sockaddr_in,初始化里面的IP地址和端口號等等的成員變量,然后進行bind綁定,這步就是將棧上的數據綁定到內核中
void Init(){// socket創建,記得加上log日志_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){lg(FATAL,"socket create fail, sockfd : %d",_sockfd);exit(SOCKET_ERR);}lg(INFO,"socket create success, sockfd = %d",_sockfd);// 首先搞一個sockaddr_in,然后將里面都初始化為0,并且初始化內部成員,這里有三個struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family = AF_INET; // 設置為AF_INET表示IPv4local.sin_port = htons(_port); // 端口轉換local.sin_addr.s_addr = inet_addr(_ip.c_str());// local.sin_addr.s_addr = INADDR_ANY; // 定義為0.0.0.0,這里是必須填在sin_addr里面的s.addr的,// 因為第一個sin_addr里面還是一個結構體,這個結構體里面才是s_addr// 在進行bind綁定,這步才是將棧上的數據都綁定到內核中,將數據轉到網絡文件描述符中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 success, errno: %d, err string: %s", errno, strerror(errno));}
Run():
這個函數就是將我們的服務端啟動起來,所以首先要將_isrunning修改為true,接著通過recvfrom接收從客戶端發來的消息,然后進行CheckUser檢查,最后進行Broadcast廣播
void Run(){// 修改_isrunning_isrunning = true;// 進入while循環char inbuffer[SIZE];while(_isrunning){memset(inbuffer, 0, sizeof(inbuffer));// recvfrom函數,這里把接收來的當做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,將第n的位置設置為\0也就是0struct 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){lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}// inbuffer[n] = 0; // 這里為什么要去掉// 這里需要拿到client中的端口號和ip然后傳給CheckUser,進行通信室中的人員是否上線管理uint16_t clientport = ntohl(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);CheckUser(client,clientport,clientip);// 當一個用戶上線后,并且發送消息了,此時將消息和用戶人員進行廣播std::string info = inbuffer; // 此時這個info也就要傳參到Broadcast進行統一處理Broadcast(info,clientport,clientip);}}
接下來就是實現CheckUser和Broadcast了
CheckUser
void CheckUser(const struct sockaddr_in& client,const uint16_t& clientport,const std::string& clientip){// 在哈希表中進行查找,如果沒找到就添加,找到了什么都不用做auto pos = online_user.find(clientip);if(pos == online_user.end()){online_user.insert({clientip,client});std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;}}
Broadcast
void Broadcast(const std::string& info,const uint16_t& clientport,const std::string& clientip){// 遍歷整個哈希表,給這個哈希表中的所有人都發送messagefor(const auto& ch : online_user){std::string message = "[";message += clientip;message += ":";message += to_string(clientport);message += "]#";message += info;socklen_t len = sizeof(ch.second); // 然后處理完后的數據用sendto接口發送回給對方sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)(&ch.second),len);}}
2、客戶端代碼:
客戶端代碼采用兩個線程進行并發執行,并且沒有對客戶端進行封裝,直接就是主函數
如下是框架:
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>#define SIZE 1024void Use(const std::string proc)
{std::cout << "\n\t" << proc << " serveip serveport\n" << std::endl;
}struct ThreadDate
{struct sockaddr_in server;int sockfd;
};void* recv_message(void* argv)
{}void* send_message(void* argv)
{}int main(int argc,char* argv[])
{// pthread_create(&recvr,nullptr,recv_message,&td);// pthread_create(&sender,nullptr,send_message,&td);// pthread_join(recvr,nullptr);// pthread_join(sender,nullptr);
}
主函數邏輯:
首先通過命令行拿到服務端的端口號和服務端的IP地址,接著創建ThreadDate結構體,方便后續的線程中進行傳參,最后創建好線程然后實現好對應的方法即可
// 這個是多線程版本的,思路是創建兩個線程,一個線程從服務端中讀數據,一個線程向服務端中發送數據
int main(int argc,char* argv[])
{// 檢查命令行if(argc != 3){Use(argv[0]);exit(1);}// .udpclient serveip serveportstd::string serveip = argv[1];uint16_t serveport = std::stoi(argv[2]);// 初始化server sockaddr_in,為了在后面sendto給serverThreadDate td;bzero(&td.server,sizeof(td.server)); td.server.sin_family = AF_INET;td.server.sin_port = htons(serveport); // 需要從主機序列轉換成網絡序列td.server.sin_addr.s_addr = inet_addr(serveip.c_str()); // 把點分十進制格式的IPv4地址轉換為網絡字節序的 32 位無符號整數// sockfd,類似與創建文件描述符,在最后記得關閉,其實不關閉也行,畢竟最后程序都結束了,sockfd的生命周期是隨進程的td.sockfd = socket(AF_INET,SOCK_DGRAM,0);if(td.sockfd < 0){std::cout<<"socket error"<<std::endl;exit(2);}// 創建兩個線程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;
}
注意:
client客戶端要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機選擇
一個端口號只能被一個進程bind,對server是如此,對于client,也是如此
其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以
系統什么時候給我bind呢?首次發送數據的時候
send_message發送數據:
我們通過geiline函數在標準輸入流中進行讀取,然后通過sendto接口發送給服務端
void* send_message(void* argv)
{ThreadDate* td = static_cast<ThreadDate*>(argv);std::string message;socklen_t len = sizeof(td->server);while(true){// 從cin中獲得數據std::cout<<"Please enter#";getline(std::cin,message);// std::cout<<message<<std::endl;// sendto發送數據int st = sendto(td->sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&td->server,len);if(st<0){std::cout<<"sendto error"<<std::endl;continue;}}
}
recv_message接收數據:
通過recvfrom接口接收數據后,打印出來看看
void* recv_message(void* argv)
{struct ThreadDate* td = static_cast<ThreadDate*>(argv);char buffer[SIZE];while(true){memset(buffer, 0, sizeof(buffer));// recvfrom接收數據// 在接收消息的時候,可能會從多臺主機上收消息,所以recvfrom后面的參數就不能是上述確定的某一個服務器// 但是又必須要填參數,所以這里新創建一個sockaddr_instruct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);// std::cout << "recvfrom over"<<std::endl;if(s > 0){buffer[s] = 0;std::cerr<<buffer<<std::endl;}}
}
三、實驗結果:
如上,這樣成功的寫出了基于udp套接字實現的網絡通信
首先將服務端進程進行打開
接著打開客戶端
如下是華為云的客戶端:
如下是騰訊云的客戶端:
這里有個細節,為了將我發的消息和接收的消息分開看,我們在編碼的時候是在標準錯誤中打印,所以在啟動的時候可以直接將標準錯誤重定向到別的終端,這樣就能夠進行分離了
接著在騰訊云中發送你好
在服務端中就能夠看到我們上線的消息了
但是在華為云的客戶端中卻不能夠看到,這是因為我們的華為云還沒有上線,接著在華為云中發送haha,就能夠發現華為云這個客戶端也上線了
此時在騰訊云中發送你吃了嗎,在華為云的客戶端中就能夠看到了,注意,在華為云中,我們并沒有將發送消息和接收消息分離
這樣就能夠實現網絡通信了
四、拓展:
在本次實驗中,我們并沒有讓服務端進行處理消息,只是處理了用戶添加到哈希表時上線的消息,如果想讓服務端進行消息的處理,可以使用function包裝器實現服務端網絡通信的功能和處理數據的功能的解耦
思路就是你在服務端代碼中增加包裝器
然后在run這個成員函數中,通過包裝器實現一個回調
// 如下是第一個版本,在這個版本中,是讓網絡接收數據和處理數據實現了解耦void Run(func_t func){// 修改_isrunning_isrunning = true;// 進入while循環char inbuffer[SIZE];while(_isrunning){// recvfrom函數,這里把接收來的當做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,將第n的位置設置為\0也就是0struct 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){lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;// 拼接字符串,這里充當一次數據的處理std::string info = inbuffer;// std::string echo_string = "server echo@" + info;std::string echo_string = func(info);std::cout<<echo_string<<std::endl;// 然后處理完后的數據用sendto接口發送回給對方sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);}}
最后在main.cc中實現想要讓服務端執行的代碼即可
std::string Handler(std::string str)
{std::string res = "Server get a message ";res += str; return res;
}
五、全部代碼:
main.cc
#include <memory>
#include <cstdio>
#include <vector>#include "UdpServer.hpp"void Usege(char* proc)
{std::cout<<"\n\tUsage: "<<proc<<"port[1024+]\n"<<std::endl;
}std::string Handler(std::string str)
{std::string res = "Server get a message ";res += str; return res;
}bool SafeCheck(const std::string& cmd)
{// 搞一個vector的數組,然后遍歷它,進行find查找vector<string> check = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for(const auto& ch : check){auto pos = cmd.find(ch);if(pos != std::string::npos) return false;}return true;
}// 理解遠端指令是怎么一回事
std::string ExcuteCommand(const std::string& cmd)
{std::cout<<"cmd:" << cmd << std::endl;// 根據SafeCheck函數做安全檢查if(!SafeCheck(cmd)) return "error";// 建立好管道// 創建子進程// 子進程執行的結果通過管道交給父進程// 父進程想讀到執行結果可以在FILE*指針也就是fp中讀到FILE* fp = popen(cmd.c_str(),"r");if(fp == nullptr){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char* res = fgets(buffer,sizeof(buffer),fp);if(res == nullptr) break;result += buffer;}pclose(fp);return result;
}int main(int argc,char* argv[])
{if(argc != 2){Usege(argv[0]);exit(1);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run(/*ExcuteCommand*/);return 0;
}
log.hpp
#pragma once#include <iostream>
#include <ctime>
#include <cstdarg>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define INFO 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define SCREEN 1
#define ONEFILE 2
#define MOREFILE 3#define SIZE 1024
#define logname "log.txt"using namespace std;class Log
{
public:Log():printstyle(SCREEN),path("./log/")// 默認路徑是當前路徑下的log文件夾{// mkdir(path.c_str(),0765);}void change(int style){printstyle = style;}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 "NON";}}void operator()(int level, const char *format, ...){// 處理時間time_t now = time(nullptr);// 將時間戳轉為本地時間struct tm *local_time = localtime(&now);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, local_time->tm_min, local_time->tm_sec);// 處理可變參數va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 將兩個消息組合起來成為一個完整的日志消息// 默認部分+自定義部分char logbuffer[SIZE * 2];snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);printlog(level, logbuffer);}void printlog(int level, const string &logbuffer) // 這里引用避免大型字符串的拷貝開銷,優化性能{switch (printstyle){case SCREEN:cout << logbuffer << endl;break;case ONEFILE:printonefile(logname, logbuffer);break;case MOREFILE:printmorefile(level, logbuffer);break;}}void printonefile(const string &_logname, const string &logbuffer){string __logname = path + _logname;int fd = open(__logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0)return;write(fd, logbuffer.c_str(), logbuffer.size());close(fd);}void printmorefile(int level, const string &logbuffer){// 思路:通過不同的文件名進行區分string _logname = logname;_logname += ".";_logname += leveltostring(level);printonefile(_logname, logbuffer);}~Log(){}private:int printstyle;string path;
};
Udpserver.hpp
#pragma once
#include <iostream>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <unordered_map>
#include <arpa/inet.h>#include "log.hpp"// 網絡接收數據和處理數據的耦合度太高了,所以就需要把網絡通信的功能和處理數據的功能做一下適當的解耦
typedef std::function<std::string(const std::string&)> func_t; // function包裝器// 返回值 // 參數#define SIZE 1024Log lg;
// enum 搞一個錯誤碼合集
enum
{SOCKET_ERR = 1,BIND_ERR = 2
};// const uint16_t defaultport = 3077;
const std::string defaultip = "0.0.0.0";class UdpServer
{
public:UdpServer(const uint16_t& port/* = defaultport*/,const std::string& ip = defaultip):_sockfd(0),_port(port),_ip(ip),_isrunning(false){}void Init(){// socket創建,記得加上log日志_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){lg(FATAL,"socket create fail, sockfd : %d",_sockfd);exit(SOCKET_ERR);}lg(INFO,"socket create success, sockfd = %d",_sockfd);// 首先搞一個sockaddr_in,然后將里面都初始化為0,并且初始化內部成員,這里有三個struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family = AF_INET; // 設置為AF_INET表示IPv4local.sin_port = htons(_port); // 端口轉換local.sin_addr.s_addr = inet_addr(_ip.c_str());// local.sin_addr.s_addr = INADDR_ANY; // 定義為0.0.0.0,這里是必須填在sin_addr里面的s.addr的,// 因為第一個sin_addr里面還是一個結構體,這個結構體里面才是s_addr// 在進行bind綁定,這步才是將棧上的數據都綁定到內核中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 success, errno: %d, err string: %s", errno, strerror(errno));}// // 如下是第一個版本,在這個版本中,是讓網絡接收數據和處理數據實現了解耦// void Run(func_t func)// {// // 修改_isrunning// _isrunning = true;// // 進入while循環// char inbuffer[SIZE];// while(_isrunning)// {// // recvfrom函數,這里把接收來的當做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,將第n的位置設置為\0也就是0// 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)// {// lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));// continue;// }// inbuffer[n] = 0;// // 拼接字符串,這里充當一次數據的處理// std::string info = inbuffer;// // std::string echo_string = "server echo@" + info;// std::string echo_string = func(info);// std::cout<<echo_string<<std::endl;// // 然后處理完后的數據用sendto接口發送回給對方// sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);// }// }void CheckUser(const struct sockaddr_in& client,const uint16_t& clientport,const std::string& clientip){// 在哈希表中進行查找,如果沒找到就添加,找到了什么都不用做auto pos = online_user.find(clientip);if(pos == 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 uint16_t& clientport,const std::string& clientip){// 遍歷整個哈希表,給這個哈希表中的所有人都發送messagefor(const auto& ch : online_user){std::string message = "[";message += clientip;message += ":";message += to_string(clientport);message += "]#";message += info;socklen_t len = sizeof(ch.second); // 然后處理完后的數據用sendto接口發送回給對方sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)(&ch.second),len);}}// 這是第二個版本,為了實現基于udp協議的聊天室,我要讓每個人上線后,能夠知道是誰進行發送的void Run(){// 修改_isrunning_isrunning = true;// 進入while循環char inbuffer[SIZE];while(_isrunning){memset(inbuffer, 0, sizeof(inbuffer));// recvfrom函數,這里把接收來的當做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,將第n的位置設置為\0也就是0struct 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){lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}// inbuffer[n] = 0; // 這里為什么要去掉// 這里需要拿到client中的端口號和ip然后傳給CheckUser,進行通信室中的人員是否上線管理uint16_t clientport = ntohl(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);CheckUser(client,clientport,clientip);// 當一個用戶上線后,并且發送消息了,此時將消息和用戶人員進行廣播std::string info = inbuffer; // 此時這個info也就要傳參到Broadcast進行統一處理Broadcast(info,clientport,clientip);}}~UdpServer(){if(_sockfd>0) close(_sockfd);}
private:int _sockfd; // 網絡文件描述符std::string _ip; // uint16_t _port; // 服務器進程的端口號bool _isrunning; // 服務器在啟動后要一直保證運行中std::unordered_map<std::string,struct sockaddr_in> online_user;// 將在聊天室中的人都存儲在哈希表中
};
UdpClient.cc
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>#define SIZE 1024void Use(const std::string proc)
{std::cout << "\n\t" << proc << " serveip serveport\n" << std::endl;
}struct ThreadDate
{struct sockaddr_in server;int sockfd;
};void* recv_message(void* argv)
{struct ThreadDate* td = static_cast<ThreadDate*>(argv);char buffer[SIZE];while(true){memset(buffer, 0, sizeof(buffer));// recvfrom接收數據// 在接收消息的時候,可能會從多臺主機上收消息,所以recvfrom后面的參數就不能是上述確定的某一個服務器// 但是又必須要填參數,所以這里新創建一個sockaddr_instruct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);// std::cout << "recvfrom over"<<std::endl;if(s > 0){buffer[s] = 0;std::cerr<<buffer<<std::endl;}}
}void* send_message(void* argv)
{ThreadDate* td = static_cast<ThreadDate*>(argv);std::string message;socklen_t len = sizeof(td->server);while(true){// 從cin中獲得數據std::cout<<"Please enter#";getline(std::cin,message);// std::cout<<message<<std::endl;// sendto發送數據int st = sendto(td->sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&td->server,len);if(st<0){std::cout<<"sendto error"<<std::endl;continue;}}
}
// 這個是多線程版本的,思路是創建兩個線程,一個線程從服務端中讀數據,一個線程向服務端中發送數據
int main(int argc,char* argv[])
{// 檢查命令行if(argc != 3){Use(argv[0]);exit(1);}// .udpclient serveip serveportstd::string serveip = argv[1];uint16_t serveport = std::stoi(argv[2]);// 初始化server sockaddr_in,為了在后面sendto給serverThreadDate td;bzero(&td.server,sizeof(td.server)); td.server.sin_family = AF_INET;td.server.sin_port = htons(serveport); // 需要從主機序列轉換成網絡序列td.server.sin_addr.s_addr = inet_addr(serveip.c_str()); // 把點分十進制格式的IPv4地址轉換為網絡字節序的 32 位無符號整數// sockfd,類似與創建文件描述符,在最后記得關閉,其實不關閉也行,畢竟最后程序都結束了,sockfd的生命周期是隨進程的td.sockfd = socket(AF_INET,SOCK_DGRAM,0);if(td.sockfd < 0){std::cout<<"socket error"<<std::endl;exit(2);}// 創建兩個線程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;
}// // 如下是單進程版本的
// int main(int argc,char* argv[])
// {
// // 檢查命令行
// if(argc != 3)
// {
// Use(argv[0]);
// exit(1);
// }
// // .udpclient serveip serveport
// std::string serveip = argv[1];
// uint16_t serveport = std::stoi(argv[2]);// // 初始化server sockaddr_in,為了在后面sendto給server
// struct sockaddr_in server;
// bzero(&server,sizeof(server)); // server.sin_family = AF_INET;
// server.sin_port = htons(serveport); // 需要從主機序列轉換成網絡序列
// server.sin_addr.s_addr = inet_addr(serveip.c_str()); // 把點分十進制格式的IPv4地址轉換為網絡字節序的 32 位無符號整數
// socklen_t len = sizeof(server);// // sockfd,類似與創建文件描述符,在最后記得關閉,其實不關閉也行,畢竟最后程序都結束了,sockfd的聲明周期是隨進程的
// int sockfd = socket(AF_INET,SOCK_DGRAM,0);// if(sockfd < 0)
// {
// std::cout<<"socket error"<<std::endl;
// exit(2);
// }// // client客戶端要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機選擇
// // 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此
// // 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以
// // 系統什么時候給我bind呢?首次發送數據的時候// std::string message;
// char buffer[SIZE];
// while(true)
// {
// // 從cin中獲得數據
// std::cout<<"Please enter#";
// getline(std::cin,message);// // std::cout<<message<<std::endl;
// // sendto發送數據
// int st = sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);
// if(st<0)
// {
// std::cout<<"sendto error"<<std::endl;
// continue;
// }// // recvfrom接收數據// // 在接收消息的時候,可能會從多臺主機上收消息,所以recvfrom后面的參數就不能是上述確定的某一個服務器
// // 但是又必須要填參數,所以這里新創建一個sockaddr_in
// struct sockaddr_in temp;
// socklen_t len = sizeof(temp);// ssize_t s = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
// // std::cout << "recvfrom over"<<std::endl;
// if(s > 0)
// {
// buffer[s] = 0;
// std::cout<<buffer<<std::endl;
// }
// }
// close(sockfd);
// return 0;
// }