err.h
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
lockGuard.h
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex // 自己不维护锁,有外部传入
{
public:
Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
{}
void lock()
{
pthread_mutex_lock(_pmutex);
}
void unlock()
{
pthread_mutex_unlock(_pmutex);
}
~Mutex()
{}
private:
pthread_mutex_t *_pmutex;
};
class LockGuard // 自己不维护锁,有外部传入
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
RingQueue.h
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
static const int N = 50;
template <class T>
class RingQueue
{
private:
void P(sem_t &s)
{
sem_wait(&s);
}
void V(sem_t &s)
{
sem_post(&s);
}
void Lock(pthread_mutex_t &m)
{
pthread_mutex_lock(&m);
}
void Unlock(pthread_mutex_t &m)
{
pthread_mutex_unlock(&m);
}
public:
RingQueue(int num = N) : _ring(num), _cap(num)
{
sem_init(&_data_sem, 0, 0);
sem_init(&_space_sem, 0, num);
_c_step = _p_step = 0;
pthread_mutex_init(&_c_mutex, nullptr);
pthread_mutex_init(&_p_mutex, nullptr);
}
// 生产
void push(const T &in)
{
// 1. 可以不用在临界区内部做判断,就可以知道临界资源的使用情况
// 2. 什么时候用锁,什么时候用sem?你对应的临界资源,是否被整体使用!
P(_space_sem); // P()
Lock(_p_mutex); //? 1
// 一定有对应的空间资源给我!不用做判断,是哪一个呢?
_ring[_p_step++] = in;
_p_step %= _cap;
Unlock(_p_mutex);
V(_data_sem);
}
// 消费
void pop(T *out)
{
P(_data_sem);
Lock(_c_mutex); //?
*out = _ring[_c_step++];
_c_step %= _cap;
Unlock(_c_mutex);
V(_space_sem);
}
~RingQueue()
{
sem_destroy(&_data_sem);
sem_destroy(&_space_sem);
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
private:
std::vector<T> _ring;
int _cap; // 环形队列容器大小
sem_t _data_sem; // 只有消费者关心
sem_t _space_sem; // 只有生产者关心
int _c_step; // 消费位置
int _p_step; // 生产位置
pthread_mutex_t _c_mutex;
pthread_mutex_t _p_mutex;
};
Thread.h
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <pthread.h>
#include <functional>
class Thread
{
public:
typedef enum
{
NEW = 0,
RUNNING,
EXITED
} ThreadStatus;
// typedef void (*func_t)(void *);
using func_t = std::function<void ()>;//无参返回值是void的函数
public:
Thread(int num, func_t func) : _tid(0), _status(NEW), _func(func)
{
char name[128];
snprintf(name, sizeof(name), "thread-%d", num);
_name = name;
}
int status() { return _status; }
std::string threadname() { return _name; }
pthread_t threadid()
{
if (_status == RUNNING)
return _tid;
else
{
return 0;
}
}
// 是不是类的成员函数,而类的成员函数,具有默认参数this,需要static
// 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数
static void *runHelper(void *args)
{
Thread *ts = (Thread*)args; // 就拿到了当前对象
// _func(_args);
(*ts)();
return nullptr;
}
void operator ()() //仿函数
{
if(_func != nullptr) _func();
}
void run()
{
int n = pthread_create(&_tid, nullptr, runHelper, this);
if(n != 0) exit(1);
_status = RUNNING;
}
void join()
{
int n = pthread_join(_tid, nullptr);
if( n != 0)
{
std::cerr << "main thread join thread " << _name << " error" << std::endl;
return;
}
_status = EXITED;
}
~Thread()
{
}
private:
pthread_t _tid;
std::string _name;
func_t _func; // 线程未来要执行的回调
ThreadStatus _status;
};
udp_client.h
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
udp_client.cc
#include "udp_client.hpp"
#include <cstring>
// 127.0.0.1: 本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}
//客户端再启动的时候要知道服务器的ip和端口,但是客户端不用考虑自己的ip和端口
// ./udp_client serverip serverport
int main(int argc, char *argv[])//需要传递服务器的ip地址和端口号
{
if(argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKET_ERR);
}
// client 这里要不要bind呢?要的!socket通信的本质[clientip:clientport, serverip:serverport]
// 要不要自己bind呢?不需要自己bind,也不要自己bind,OS自动给我们进行bind -- 为什么?client的port要随机让OS分配防止client不同的进程之间出现端口号冲突的问题
// 那为什么server 为什么要自己bind?1. server的端口不能随意改变,因为一个服务器会给众多的端口号提供服务的,众所周知且不能随意改变的,比如说官方的电话号码是不可以随意改变的2. 同一家公司可以有多台服务器,其port号需要统一规范化
// 明确server是谁,sendto需要采用服务器的套接字
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());//服务器端要填写服务器端的ip地址(我们设置的是INADDR_ANY),而客户端也要填写服务器端的ip地址
while(true)
{
//多线程化??
// 用户输入
std::string message;
std::cout << "please send ";
std::cin >> message;//cin获取信息是以空格为间隔的。比如输入hello wjj,message存放的只有hello。
//std::getline(std::cin,message);//从标准输入cin中获取字符放入message,这样就可以执行ls -a -l的命令了,如果用上面的cin的话ls -a -l执行的结果和ls的结果是一样的
// 什么时候bind的?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文
//发送
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
//接受
char buffer[2048];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);//temp清空与否无所谓,每一次生成都是乱码,recvfrom会对其进行填充
int n = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
udp_server.h
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unordered_map>
#include "err.hpp"
#include "RingQueue.hpp"
#include "lockGuard.hpp"
#include "Thread.hpp"
namespace ns_server
{
const static uint16_t default_port = 8080;
using func_t = std::function<std::string(std::string)>;//定义了一个函数,参数是string,返回值也是string
class UdpServer
{
public:
// UdpServer(std::string ip, uint16_t port = default_port): ip_(ip), port_(port)
// {
// std::cout << "server addr: " << ip << " : " << port_ << std::endl;
// }
//由于云服务器的原因现在构造函数不需要ip地址这个参数来进行构造了
UdpServer(uint16_t port = default_port) : port_(port)
{
std::cout << "server addr: " << port_ << std::endl;
pthread_mutex_init(&lock, nullptr);
p = new Thread(1, std::bind(&UdpServer::Recv, this));
c = new Thread(1, std::bind(&UdpServer::Broadcast, this));
}
void start()
{
// 1. 创建socket接口,打开网络文件
sock_ = socket(AF_INET, SOCK_DGRAM, 0);//int socket(int domain, int type, int protocol);创建成功返回文件描述符
//AF_INET标识生成网络通信套接字,SOCK_DGRAM提供的是数据报通信,第三个参数标识采用协议,默认为0,由系统根据第二个参数type进行判定。
if (sock_ < 0)
{
std::cerr << "create socket error: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << sock_ << std::endl; // 3
// 2. 给服务器指明IP地址(??)和Port
struct sockaddr_in local; // 这个 local 在哪里定义呢?用户空间的特定函数的栈帧上,不在内核中!
bzero(&local, sizeof(local));//void bzero(void *s, size_t n);将local变量直接清零
local.sin_family = AF_INET; 也可以写成PF_INET,标识网络间通信,也就是该结构体的前16个比特位
local.sin_port = htons(port_);//port_是端口号,htons是为了是将主机字节序转换成网络字节序,使网络程序具有可移植性,使同样的c代码在大端和小段计算机编译后。
// inet_addr: 1,2
// 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
// 2. 需要将主机序列转化成为网络序列
// 3. 云服务器,或者一款服务器,一般不要指明某一个确定的IP。因为服务器可能有多张网卡,有多个ip地址,如果我们指明了确定的ip地址那么意味着服务器只能接收某一个ip上面递交的数据报,可能会导致服务器处理的数据量减少。我们希望只要是到我这台机器的数据,我们不用ip地址来进行甄别而是只要给我的特定端口的都转给我。
//local.sin_addr.s_addr = inet_addr(ip_.c_str());//in_addr_t inet_addr(const char *cp)是将一个点分十进制风格的字符串地址转换成int类型地址,同时也将主机序列转换成为了网络序列
local.sin_addr.s_addr = INADDR_ANY; //让我们的udpserver在启动的时候,bind本主机上的任意IP或者所有ip,INADDR_ANY是全零值,转不转网络序列是无所谓的。
if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)//bind是把已经填充好的local套接字字段和文件字段进行绑定关联,从而称为网络文件
{
std::cerr << "bind socket error: " << strerror(errno) << std::endl;
exit(B IND_ERR);
}
std::cout << "bind socket success: " << sock_ << std::endl; // 3
p->run();
c->run();
}
void addUser(const std::string &name, const struct sockaddr_in &peer)
{
//?
// onlineuserp[name] = peer;
LockGuard lockguard(&lock);
auto iter = onlineuser.find(name);
if (iter != onlineuser.end())
return;
// onlineuser.insert(std::make_pair<const std::string, const struct sockaddr_in>(name, peer));
onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));
}
void Recv()//一个线程收消息
{
char buffer[1024];
while (true)//服务端是一直都在运行的
{
// 收
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 这里一定要写清楚,未来你传入的缓冲区大小
int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);buf是自己定义的缓冲区,len是缓冲区长度,flags是读取方式默认以阻塞方式读取0,返回实际读取到的字节数,src_addr和addrlen是输出型参数,用来标识是接收的信息来自于哪个客户端。跨网络的进程间通信我们需要采用套接字(sockaddr结构体里面包含ip和port)来进行标识
//sizeof(buffer)之所以用来减1,是因为我们并不知道对方发送的消息是什么格式,我们只能假设消息是一个一个的字符串,而我们c风格的字符串是以\0结尾的,我们系统调用其实在读取数据的时候是以2进制的方式读取的,数据是什么样子得由我们自己区解释,所以减去一个1为我们的缓冲区预留一个空间方便添加\0当作字符串来看。
if (n > 0)
buffer[n] = '\0';
else
continue;
std::cout << "recv done ..." << std::endl;
// 提取client信息 -- debug
std::string clientip = inet_ntoa(peer.sin_addr);//peer是直接从网络中拿到的,所以要转换成为网络序列。inet_ntoa将4字节的ip转换成为字符串的ip
uint16_t clientport = ntohs(peer.sin_port);//peer是从网络中读取的,一定是网络序列,所以需要在这里转成为主机序列。
std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;
// 构建一个用户,并检查
std::string name = clientip;
name += "-";
name += std::to_string(clientport);
// 如果该客户端不存在,就插入到onlineuser,如果存在,什么都不做
addUser(name, peer);
rq.push(buffer);//将来自于客户端的消息push到环形队列当中,相当于一个生产过程
// 做业务处理
// std::string message = service_(buffer);//service_是一个业务处理函数,吃一个string类型的参数,也就是服务器收到的信息,返回一个string也就是服务器对来自客户端信息的处理结果。
// 发
// sendto(sock_, message.c_str(), message.size(), 0, (struct sockaddr*)&peer, sizeof(peer));//后面两个参数是标识服务器发送数据的目标进程,是输入型参数,和recvfrom是不一样的概念。peer是直接从网络里面来的,我已不用再转换成为网络序列了。
//sendto(sock_, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, sizeof(peer));//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)发数据,但我们需要发送的目标地址dest_addr和addrlen,而目标地址就是我们之前recvfrom的最后两个输出型参数。 当我们向文件写入的时候,\0并不用写入到文件当中,因为\0是c语言的规定,并不是我们网络的规定,所以接收的如果是按字符串处理,我们需要在接收的时候在缓冲区内预留一个位置来存储\0。
}
}
void Broadcast()//一个线程广播消息
{
while (true)
{
std::string sendstring;//用来存储环形队列中的消息
rq.pop(&sendstring);//从环形队列中拿到消息
std::vector<struct sockaddr_in> v;
{
LockGuard lockguard(&lock); // 加锁,此时让onlineuser这个公共资源处于保护状态(因为Recv函数也会使用该资源),我们将onlineuser中的客户端信息sockaddr_in信息放到v容器当中,然后解锁,再遍历v容器进行发送信息这种io工作,这样占用公共资源onlineuser的时间可以减少。
for (auto user : onlineuser)
{
v.push_back(user.second);
}
}
for (auto user : v)
{
// std::cout << "Broadcast message to " << user.first << sendstring << std::endl;
sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&(user), sizeof(user));
std::cout << "send done ..." << sendstring << std::endl;
}
}
}
~UdpServer()
{
pthread_mutex_destroy(&lock);
c->join();
p->join();
delete c;
delete p;
}
private:
int sock_;//关于套接字的文件描述符
uint16_t port_;
// func_t service_; // 我们的网络服务器刚刚解决的是网络IO的问题,要进行业务处理
std::unordered_map<std::string, struct sockaddr_in> onlineuser;//用来存储客户端的套接字信息
pthread_mutex_t lock;//对onlineuser进行保护
RingQueue<std::string> rq;
Thread *c;//定义两个线程
Thread *p;
// std::string ip_; // 此ip地址是字符串风格的,比如:"1.1.1.1"。
};
} // end NS_SERVER
udp_server.cc
#include "udp_server.hpp"
#include <memory>
#include <string>
#include <cstdio>
using namespace ns_server;
using namespace std;
static void usage(string proc)//我们所对应的云服务器,不需要bind io地址,需要让云服务器自己指定ip地址。自己本地装的虚拟机,或者物理机是允许bind的。
{
std::cout << "Usage:\n\t" << proc << " port\n"
<< std::endl;
}
// 上层的业务处理,不关心网络发送,只负责信息处理即可,通过回调的方式从而实现网络部分和业务部分的解耦合
std::string transactionString(std::string request) // request就是来自客户端一个string,服务器的任务就是将来自客户端的request字符串进行大小写转换
{
std::string result;
char c;
for (auto &r : request)
{
if (islower(r))
{
c = toupper(r);
result.push_back(c);
}
else
{
result.push_back(r);
}
}
return result;
}
static bool isPass(const std::string &command)
{
bool pass = true;
auto pos = command.find("rm");
if(pos != std::string::npos) pass=false;
pos = command.find("mv");
if(pos != std::string::npos) pass=false;
pos = command.find("while");
if(pos != std::string::npos) pass=false;
pos = command.find("kill");
if(pos != std::string::npos) pass=false;
return pass;
}
// 客户端把命令给我,server再把在server上面执行客户端命令的结果给你!
// ls -a -l
//我们可以先创建子进程,对command做字符串解析喂给程序替换函数,让子进程执行完,在创建子进程之前将管道创建好,父进程把命令喂给子进程,子进程将结果通过管道拿回来。
std::string excuteCommand(std::string command) // command就是一个命名
{
// 1. 安全检查
if(!isPass(command)) return "you are bad man!";
// 2. 业务逻辑处理
FILE *fp = popen(command.c_str(), "r");// FILE *popen(const char *command, const char *type),第一个参数是执行的命令,第二个参数是对执行结果是想写、还是追加、还是读。1、创建管道;2、创建子进程;3、通过FILE*将结果进行返回,可以让用户以读取文件的方式获得命令的执行结果
//将popen执行command命令并将执行结果以文件指针的方式返回
if(fp == nullptr) return "None";
// 3. 读取成功,获取结果了
char line[1024];
std::string result;
while(fgets(line, sizeof(line), fp) != NULL)//fgets从文件流中获取结果,由于是c式接口,读到了会自动添加'\0'
{
result += line;
}
pclose(fp);
return result;
}
// ./udp_server port 不用ip地址了
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);//atoi简单的处理一下将字符串转换成为整型
// unique_ptr<UdpServer> usvr(new UdpServer("120.78.126.148", 8082));
// unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port));
// unique_ptr<UdpSe rver> usvr(new UdpServer(excuteCommand, port));
unique_ptr<UdpServer> usvr(new UdpServer(port));//现在启动服务器并不需要ip地址,因为云服务器无法绑定,只需要端口号。
// usvr->InitServer(); // 服务器的初始化
usvr->start();
return 0;
}