udp代码(实现客户端和服务端通信)

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
            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;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值