目录
一、实现目标:
实现基于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表示IPv4
local.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也就是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; // 这里为什么要去掉
// 这里需要拿到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)
{
// 遍历整个哈希表,给这个哈希表中的所有人都发送message
for(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 1024
void 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 serveport
std::string serveip = argv[1];
uint16_t serveport = std::stoi(argv[2]);
// 初始化server sockaddr_in,为了在后面sendto给server
ThreadDate 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_in
struct 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也就是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);
}
}
最后在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 1024
Log 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表示IPv4
local.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)
{
// 遍历整个哈希表,给这个哈希表中的所有人都发送message
for(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也就是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; // 这里为什么要去掉
// 这里需要拿到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 1024
void 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_in
struct 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 serveport
std::string serveip = argv[1];
uint16_t serveport = std::stoi(argv[2]);
// 初始化server sockaddr_in,为了在后面sendto给server
ThreadDate 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;
// }