【Linux网络编程】基于udp套接字实现的网络通信
目录
一、实现目标:
二、实验步骤:
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;
// }