socket编程UDP

发布于:2025-08-17 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

UDP网络编程

V1版本-Echo Server

V2版本-Dict  Server

V3版本-Chat Server

地址转换函数

实现


UDP网络编程

V1版本-Echo Server

我们利用上篇基础接口,写一个简单的网络通信代码。

框架:客户端向服务端发送消息,服务端处理信息,同时给客户端回显消息。

服务端UdpServer.hpp:

注意:将服务端IP绑定设置成INADDR_ANY(是一个特殊常量(值为 0.0.0.0))的理由是,它可以设置将监听主机上的所有网络接口,也就是说无论主机有多少个网卡或动态 IP,将来客户端只需要拿着主机某一个IP和确定端口就可以进行网络通信。

UdpServer.cc:

UdpClient.cc:

代码:

UdpClient.cc:

// ./UdpClient server_ip server_port
int main(int agrc, char *agrv[])
{
    if (agrc != 3)
    {
        std::cerr << "Usage: " << agrv[0] << "ip port" << std::endl;
        return 1;
    }
    std::string server_ip = agrv[1];
    uint16_t server_port = std::stoi(agrv[2]);
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }
    // 不需要绑定
    // 填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 清0
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    while (true)
    {
        std::string input;
        std::cout<<"Please enter:";
        std::getline(std::cin, input);
        // 发送
        sendto(sockfd, input.c_str(), input.size(),
               0, (struct sockaddr *)&server, sizeof(server));
        // 收
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1,
                         0, (struct sockaddr *)&peer, &len);
        //打印
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

UdpServer.hpp:

using func_t = std::function<std::string(const std::string &)>;
const int sockdefault = -1;
// 网络通信类
class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(sockdefault), _port(port), _func(func), _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建套接字 本质是网络文件 返回文件描述符
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) // 创建失败
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success! socketfd:" << _sockfd;
        // 2填充sockaddr_in 结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清0
        // 表示使用 IPv4 协议 进行网络通信
        local.sin_family = AF_INET;
        // 将来需要将自己的端口号和ip发送至网络
        // 需要将端口号和ip转成网络格式,再发送至网络
        local.sin_port = htons(_port); // 填充端口号
        // ip需要先转成4字节,再变网络格式
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 填充ip
        local.sin_addr.s_addr =  INADDR_ANY;
        // 3.绑定socket信息,ip和端口号
        // IP和端口必须是众所周知且不能轻易改变的
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success,socket:" << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1.收消息,会收到哪个ip,哪个进程发送的消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1,
                                 0, (struct sockaddr *)&peer, &len);
            if (s > 0) // 返回实际接收的字节数
            {
                // 将网络端口序列转化出来
                int peer_port = ntohs(peer.sin_port);
                // 将4字节的网络格式ip转化出来
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;
                std::string result=_func(buffer);
                // 打印
                LOG(LogLevel::DEBUG) << "[" << peer_ip << ":"
                                     << peer_port << "]# " << buffer;
                // 2.给对应的客户端发消息
                sendto(_sockfd, result.c_str(), result.size(),
                       0, (struct sockaddr *)&peer, len);
            }
        }
    }
private:
    int _sockfd;     // 套接字fd
    uint16_t _port;  // 端口号
    bool _isrunning;
    func_t _func;
};

UdpServer.cc:

// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}
//./UdpServer port
int main(int agrc, char *agrv[])
{
    if (agrc != 2)
    {
        std::cerr << "Usage: " << agrv[0] << "port" << std::endl;
        return 1;
    }
    // std::string ip = agrv[1];
    uint16_t port = std::stoi(agrv[1]);
    Enable_Console_Log_Strategy();
    std::unique_ptr<UdpServer> us = std::make_unique<UdpServer>(port,defaulthandler);
    us->Init();
    us->Start();
    return 0;
}

V2版本-Dict  Server

实现一个翻译功能,当客户端发送一个单词给服务端,服务端翻译,将结果返回给服务端。

核心功能:

服务端:需要实现翻译,我们可以这样,一个字典文档存放着单词和汉字的映射数据,一个字典类(里面有着哈希表),首先需要将字典文档中的映射信息加载到字典类的哈希表中,然后将字典类中的翻译接口传给服务端,将来服务端收到客户端的消息,就执行这个回调函数(翻译接口)。

字典类(Dict):

翻译功能:

代码:

Dict.hpp:

const std::string defalutdict = "./dictionary.txt";
const std::string sep = ": ";
class Dict
{
public:
    Dict(const std::string &path = defalutdict)
        : _dict_path(path)
    {
    }
    bool LoadDict()
    {
        // 以读方式打开
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典:" << _dict_path << "错误";
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            // apple: 苹果
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::ERROR) << "解析:" << line << "失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::ERROR) << "没有有效内容:" << line;
                continue;
            }
            // 正常数据
            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::INFO) << "加载:" << line;
        }
        in.close();
        return true;
    }
    std::string Translate(const std::string &word, InetAddr &cli)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end())
        {
            LOG(LogLevel::INFO) << "进入翻译功能:" << cli.Ip() << ":"
                                << cli.Port() << ":#" << word << "->" << "None";
            return "None";
        }
        LOG(LogLevel::INFO) << "进入翻译功能:" << cli.Ip() << ":"
                            << cli.Port() << ":#" << word << "->" << iter->second;
        return iter->second;
    }

private:
    std::string _dict_path;                             // 路径+文件名
    std::unordered_map<std::string, std::string> _dict; // 映射
};

其他代码比较冗余,不过多列举。

V3版本-Chat Server

我们要实现一个简单的聊天室。

客户端多线程,一个发,一个收,当客户端没发消息时,它能收到其他客户端的消息,发消息时,服务端收到消息,做路由服务,将路由服务插入线程池队列,由多个线程消费路由服务。

我们先讲讲地址转换函数。

地址转换函数

字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接⼝是 void *addrptr。

关于inet_ntoa:

inet_ntoa这个函数返回了⼀个char*,很显然是这个函数⾃⼰在内部为我们申请了⼀块内存来保存ip的 结果.那么是否需要调⽤者⼿动释放呢?

inet_ntoa函数,是把这个返回结果放到了静态存储区.这个时候不需要我们⼿动进⾏释放。

inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区,这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果

如果有多个线程调⽤inet_ntoa,是否会出现异常情况呢?

在APUE中,明确提出inet_ntoa不是线程安全的函数,但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁。在多线程环境下,推荐使⽤inet_ntop,这个函数由调⽤者提供⼀个缓冲区保存结果,可以规避线程 安全问题;

实现

我们可以将Ip和端口主机和网络之间的转化,提前写一个类,可以避免代码冗余情况。

InetAddr类:将ip和端口进行网络和主机之间的转化。

 

UdpServer.cc,UdpServer.hpp(服务端):

UdpClient.cc(客户端):

Route.hpp(路由服务):

代码:

Route.hpp:

class Route
{
private:
    bool IsExist(InetAddr &peer)
    {
        for (auto &user : _online_user)
        {
            if (user == peer)
                return true;
        }
        return false;
    }
    void AddUser(InetAddr &peer)
    {
        LOG(LogLevel::INFO) << "新添加一个新用户" << peer.StringAddr();
        _online_user.push_back(peer);
    }
    void DeleteUser(InetAddr &peer)
    {
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == peer)
            {
                LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";
                _online_user.erase(iter);
                break;
            }
        }
    }

public:
    void MessageRoute(int sockfd, std::string &message, InetAddr &peer)
    {
        LockGuard lock(_mutex);
        if (!IsExist(peer))
        {
            AddUser(peer);
        }
        std::string send_message = peer.StringAddr() + "#" + message;
        // 给每个用户都发送一下信息
        for (auto &user : _online_user)
        {
            sendto(sockfd, send_message.c_str(), send_message.size(), 0,
                   (struct sockaddr *)&user.NetAddr(), sizeof(user.NetAddr()));
        }
        //退出
        if (message == "quit")
        {
            LOG(LogLevel::INFO) << "删除一个在线用户" << peer.StringAddr();
            DeleteUser(peer);
        }
    }

private:
    Mutex _mutex;
    std::vector<InetAddr> _online_user; // 在线用户
};

InetAddr.hpp:

class InetAddr
{
public:
    // 两个构造函数
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        // 网络转主机序列
        _port = ntohs(_addr.sin_port);
        // _ip = inet_ntoa(_addr.sin_addr);
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    InetAddr(const std::string &ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        // 主机转网络序列
        memset(&_addr, 0, sizeof(_addr)); // 清0
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, ip.c_str(), &_addr.sin_addr);
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    bool operator==(InetAddr &add)
    {
        return add._ip == _ip && add._port == _port;
    }
    struct sockaddr_in &NetAddr()
    {
        return _addr;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

UdpServer.hpp:

using func_t = std::function<void(int sockfd, const std::string &, InetAddr &)>;
const int sockdefault = -1;
// 网络通信类
class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(sockdefault), _port(port), _func(func), _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建套接字 本质是网络文件 返回文件描述符
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) // 创建失败
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket create success! socketfd:" << _sockfd;
        //可以这样
        uint16_t port = _port;
        InetAddr local("0", port);
        int n = bind(_sockfd, (struct sockaddr *)(&(local.NetAddr())),
                     sizeof(local.NetAddr()));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success,socket:" << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1.收消息,会收到哪个ip,哪个进程发送的消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1,
                                 0, (struct sockaddr *)&peer, &len);
            if (s > 0) // 返回实际接收的字节数
            {
                // 网络转主机序列
                InetAddr client(peer);
                buffer[s] = 0;
                _func(_sockfd, buffer, client); // 回调函数
            }
        }
    }
private:
    int _sockfd;    // 套接字fd
    uint16_t _port; // 端口号
    bool _isrunning;
    func_t _func;
};

UdpServer.cc:

using task_t = std::function<void()>;

//./UdpServer port
int main(int agrc, char *agrv[])
{
    if (agrc != 2)
    {
        std::cerr << "Usage: " << agrv[0] << "port" << std::endl;
        return 1;
    }
    uint16_t port = std::stoi(agrv[1]);
    // 路由服务
    Route r;
    // 多线程处理
    auto tp = ThreadPool<task_t>::GetInstance();

    Enable_Console_Log_Strategy();
    std::unique_ptr<UdpServer> us = std::make_unique<UdpServer>(port,
         [&tp,&r](int sockfd,const std::string& message,InetAddr& peer){
        task_t t =std::bind(&Route::MessageRoute,&r,sockfd,message,peer);
        tp->Enqueue(t); 
    });
    
    us->Init();
    us->Start();
    return 0;
}
void Recv()
{
    while (true)
    {
        // 收
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1,
                         0, (struct sockaddr *)&peer, &len);
        // 打印
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl;//2
        }
    }
}
void Send()
{
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 清0
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    //发
    const std::string online = "inline";
    sendto(sockfd, online.c_str(), online.size(), 0,
           (struct sockaddr *)&server, sizeof(server));
    while (true)
    {
        std::string input;
        std::cout << "Please enter:";//1
        std::getline(std::cin, input);//0
        // 发送
        sendto(sockfd, input.c_str(), input.size(),
               0, (struct sockaddr *)&server, sizeof(server));
        if (input == "quit")
        {
            pthread_cancel(id);
            break;
        }
    }
}
// ./UdpClient server_ip server_port
int main(int agrc, char *agrv[])
{
    if (agrc != 3)
    {
        std::cerr << "Usage: " << agrv[0] << "ip port" << std::endl;
        return 1;
    }
    server_ip = agrv[1];
    server_port = std::stoi(agrv[2]);
    // 1.创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }
    // 发送和接受消息多线程
    Thread recver(Recv);
    Thread sender(Send);

    recver.Start();
    sender.Start();

    // id = recver.Id();

    recver.Join();
    sender.Join();
    return 0;
}

好了,我们下期见。


网站公告

今日签到

点亮在社区的每一天
去签到