当前位置: 首页 > article >正文

Socket编程-udp

1. 前言

最先使用udp进行socket编程,最直接的原因就是因为udp简单,方便我们快速熟悉socket各种系统调用

我们一共会完成三份代码,第一份我们会实现两台主机之间的简单聊天系统;第二份在第一份的前提下,我们加上一个翻译的业务,当clientserver发送一个英文单词,server会给client返回该单词的中文意思;第三份在第一份的前提下,实现简单的群聊系统

2. udp_echo_server

首先,使用socket函数创建socket文件,socket函数返回一个文件描述符fd,这里可以简单认为是打开网卡

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
# domain -> AF_INET
# type -> SOCK_DGRAM
# protocol -> 0

其次,我们知道服务器启动时一定要与某个端口号绑定,未来客户端要拿着服务器的IP地址和port访问服务器

使用bind函数将服务器的IP地址和port绑定到内核当中

其中,绑定IP地址和port时需要将主机序列转为网络序列,这里有现成的接口直接使用

#include <sys/types.h
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); # 将16位的port主机序列转网络序列

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
# 1. 将字符串类型的ip地址转结构化类型
# 2. 将ip地址主机序列转网络序列
void Initialize()
{
    // 1. 创建socket文件
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_sockfd < 0)
    {
        LOG(FATAL, "create sockfd error");
        exit(1);
    }
    LOG(DEBUG, "create sockfd success, sockfd:%d", _sockfd);

    // 2.bind
    struct sockaddr_in local;
    socklen_t len = sizeof(local);
    local.sin_family = AF_INET;
    local.sin_port = htons(_localport);
    local.sin_addr.s_addr = inet_addr(_localip.c_str());
    int n = bind(_sockfd, (const struct sockaddr*)&local, len);
    if(n < 0)
    {
        LOG(FATAL, "bind error");
        exit(1);
    }
    LOG(DEBUG, "bind success");
}

服务器运行起来后,从sockfd中读取数据,对于udp,读写不能使用read/write,应当使用recvfrom/sendto函数;接收到客户端的消息后,我们将客户端发送的消息打印出来,再发送回去

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
# flags -> 默认为0
# dest_addr -> 要发送的主机

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
# src_addr -> 谁发送,将来也要向对方发送
void Start()
{
    _isrunning = true;
    while(_isrunning)
    {
        char buffer[1024] = { 0 };
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if(n > 0)
        {
            buffer[n] = '\0';
            std::cout << "[client echo]#" << buffer << std::endl;

            sendto(_sockfd, buffer, sizeof(buffer)-1, 0, (const struct sockaddr*)&peer, len);
        }
        else
        {
            break;
        }
    }
    _isrunning = false;
}

对于客户端,第一步同样需要创建socket文件,但与服务器不同,客户端也需要绑定IP地址和port到内核,但不需要显示绑定,当客户端第一次发送数据是,由OS自动绑定

这是因为服务器是每个公司所有的,由公司选择IP地址和port,但我们的手机、电脑上有很多软件的客户端,如果是绑定指定的port,其他软件的客户端也要绑定同一个port时会发生冲突;因此客户端的IP地址和port由OS系统随机绑定,后面会验证客户端确实绑定了

客户端拿着服务器的IP地址和port,向服务器发送数据,再等待服务器回应,将回应打印

这里就需要提前知道服务器的IP地址和port,所以,我们使用命令行参数的方式将服务器的IP地址和port交给客户端

而服务器我们指定IP地址和port

int main()
{
    std::string ip = "127.0.0.1"; # 本地环回
    uint16_t port = 8888;

    std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(ip, port);
    udp_server->Initialize();
    udp_server->Start();
    return 0;
}
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cout << "Usage:" << argv[0] << " serverip serverport" << std::endl;
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cout << "create socket error";
        exit(1);
    }
    std::cout << "create socket success" << std::endl;

    // client需要绑定 ip和port,但不需要显示绑定
    // 当第一次发送数据时,OS会自动绑定

    struct sockaddr_in server;
    socklen_t len = sizeof(server);
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        std::string line;
        std::cout << "please enter->";
        getline(std::cin, line);

        ssize_t n = sendto(sockfd, line.c_str(), line.size(), 0, (const struct sockaddr*)&server, len);
        if(n > 0)
        {
            char buffer[1024] = { 0 };
            struct sockaddr_in temp;
            socklen_t length = sizeof(temp);
            ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &length);
            if(m > 0)
            {
                buffer[m] = '\0';

                std::cout << buffer << std::endl;
            }
            else
            {
                break;
            }
        }
    }

    close(sockfd);
    return 0;
}

现在我们来通信试试

在这里插入图片描述

上面代码中服务器绑定的是本地环回的IP地址(127.0.0.1),数据经过网络协议栈发送给自身主机,在向上交付;如果是绑定自身云服务器的公网IP地址呢?

在这里插入图片描述

经过测试,我们绑定失败,需要注意的是,不推荐在云服务器下自身的公网IP地址,并且,也不推荐服务器绑定一个指定的IP地址,因为服务器可能有多张网卡,就有多个IP地址,如果只指定一个IP地址绑定,那么就只会收到指定IP地址发送的数据;因此服务器的IP地址建议设置为INADDR_ANY,表示绑定了任意IP,这样只要是发送到该主机下的数据就都能收到

我们再将服务器稍加修改,也使用命令行参数的方式绑定port

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cout << "Usage:" << argv[0] << " port" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port);
    udp_server->Initialize();
    udp_server->Start();
    return 0;
}

我们再来证明客户端确实是由OS帮我们绑定了IP地址和port

服务器一定收到了客户端的信息

char *inet_ntoa(struct in_addr in); # 将网络序列的结构化的ip地址转成字符串类型的ip地址
uint16_t ntohs(uint16_t netshort); # 将port由网络序列转成主机序列
void Start()
{
    _isrunning = true;
    while(_isrunning)
    {
        char buffer[1024] = { 0 };
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
        if(n > 0)
        {
            buffer[n] = '\0';
            InetAddr addr(peer);
            std::cout << addr.AddrStr().c_str() << buffer << std::endl;

            sendto(_sockfd, buffer, sizeof(buffer)-1, 0, (const struct sockaddr*)&peer, len);
        }
        else
        {
            break;
        }
    }
    _isrunning = false;
}

在这里插入图片描述

为了方便IP地址port的打印,将IP地址和port封装

class InetAddr
{
protected:
    void ToHost()
    {
        _ip = inet_ntoa(_addr.sin_addr);
        _port = ntohs(_addr.sin_port);
    }
public:
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        ToHost();
    }

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    std::string AddrStr()
    {
        return "[" + _ip + ":"  + std::to_string(_port) + "]";
    }

    ~InetAddr()
    {
    }

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

这样,我们的第一份udp_echo_server代码就完成了

完整代码:[Practice/Lesson1/1. udp_echo_server · baiyahua/Linux - 码云 - 开源中国 (gitee.com)](https://gitee.com/baiyahua/linux/tree/master/Practice/Lesson1/1. udp_echo_server)

3. dict_server

现在,我们想在上面的代码上加上翻译业务,当客户端给服务器输入一个英文单词,服务器给客户端返回该英文单词的中文

同时,我们希望udp服务器只负责读取和发送数据,也就是IO,而业务逻辑交给另外的模块处理,做到IO逻辑与业务逻辑解耦

这就需要我们将处理英文单词的方法以函数指针的方式交给udp服务器,当udp服务器收到英文单词时,回调该方法,将结果再发送回客户端

using service_t = std::function<std::string(const std::string&)>;

// ....

class UdpServer
{
    // ...
    UdpServer(service_t service, uint16_t port = glocalport)
        :_sockfd(gsockfd)
        ,_localport(port)
        ,_isrunning(false)
        ,_service(service)
    {}

    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024] = { 0 };
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                buffer[n] = '\0';

                std::string result = _service(buffer);
                sendto(_sockfd, result.c_str(), result.size(), 0, (const struct sockaddr*)&peer, len);
            }
            else
            {
                break;
            }
        }
        _isrunning = false;
    }
    
    // ....

    service_t _service;
};

而在单词处理的模块,将字典文件用unordered_map的结构存放,遍历查找

const std::string sep = ": ";

class Dict
{
protected:
    void LoadDict()
    {
        std::ifstream in(_dict_path.c_str());
        if(!in.is_open())
        {
            LOG(FATAL, "open %s error", _dict_path.c_str());
            exit(1);
        }

        std::string line;
        while(getline(in, line))
        {
            if(line.empty()) continue;
            size_t pos = line.find(sep);
            if(pos == std::string::npos) continue;
            std::string key = line.substr(0, pos);
            if(key.empty()) continue;
            std::string value = line.substr(pos+sep.size());
            if(value.empty()) continue;

            _dict[key] = value;
            LOG(DEBUG, "load %s success", line.c_str());
        }
        LOG(DEBUG, "load dict done....");
    }
public:
    Dict(const std::string &path)
        :_dict_path(path)
    {
        LoadDict();
    }

    std::string Translate(const std::string &word)
    {
        for(auto &[x, y] : _dict)
            if(x == word) return y;
        return "None";
    }

    ~Dict()
    {}
protected:
    std::unordered_map<std::string, std::string> _dict;
    std::string _dict_path;
};

udp服务器的调用中,将Dict::Translate作为方法传给udp服务器

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cout << "Usage:" << argv[0] << " port" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    Dict dict("./dict.txt");
    service_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1);
    std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(translate, port);
    udp_server->Initialize();
    udp_server->Start();
    return 0;
}

在这里插入图片描述

[Practice/Lesson1/2. dict_server · baiyahua/Linux - 码云 - 开源中国 (gitee.com)](https://gitee.com/baiyahua/linux/tree/master/Practice/Lesson1/2. dict_server)

4. chat_server

在第一份代码的基础上,我们想实现一个简单的群聊系统,udp服务器只负责IO,将收到的数据交给消息转发模块,消息转发模块根据在线用户列表转发给每一个在线的人(包括发送信息的人)

在这里插入图片描述

要想IO逻辑与业务逻辑解耦,就需要给udp服务器传递消息转发的函数指针,当服务器拿到数据时,去回调方法

而我们的消息转发模块,就是根据在线用户列表依次转发消息

#include "InetAddr.hpp"
#include "Log.hpp"

#include <iostream>
#include <vector>

class Route
{
protected:
    void CheckOnline(InetAddr& who)
    {
        for(auto &user : _online_user)
        {
            if(user == who)
            {
                return; // _ip和port都相等,这样一款软件能起多个client
                LOG(DEBUG, "%s is online", who.AddrStr().c_str());
            }
        }
        _online_user.push_back(who);
        LOG(DEBUG, "%s is not online, add it...", who.AddrStr().c_str());
    }

    void Offline(InetAddr& who)
    {
        std::vector<InetAddr>::iterator it = _online_user.begin();
        while(it != _online_user.end())
        {
            if((*it) == who)
            {
                _online_user.erase(it);
                break;
            }
        }
    }

    void ForwardHelper(int sockfd, const std::string &message)
    {
        for(auto &user : _online_user)
        {
            struct sockaddr_in peer = user.Addr();
            sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&peer, sizeof(peer));
        }
    }

public:
    Route()
    {}

    void Forward(int sockfd, const std::string& message, InetAddr& who)
    {
        // 1. 检查 who 在不在 _online_user中,如果不在,添加到 _online_user
        CheckOnline(who);

        // 如果 "quit" || "q",将退出的信息也转发给其他所有人
        if(message == "quit" || message == "q")
            Offline(who);

        ForwardHelper(sockfd, message); 
    }

    ~Route()
    {}
protected:
    std::vector<InetAddr> _online_user;
};

在这里插入图片描述

这里消息转发模块我们想使用多进程的方式,提高效率

将我们之前写过的多线程引入进来

void Forward(int sockfd, const std::string& message, InetAddr& who)
{
    // 1. 检查 who 在不在 _online_user中,如果不在,添加到 _online_user
    CheckOnline(who);

    // 如果 "quit" || "q",将退出的信息也转发给其他所有人
    if(message == "quit" || message == "q")
        Offline(who);

    //ForwardHelper(sockfd, message); 
    task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message);
    ThreadPool<task_t>::GetInstance()->Equeue(t);
}

但此时仍有问题,我们发现服务端能同时读写,证明udp是支持全双工通信的,但客户端如果不输入发送的信息,收到的数据就会堆积在OS内部,上部不能接受,因此,我们的客户端也要写成多线程

using namespace byh;

int Initialize()
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cout << "create socket error";
        exit(1);
    }
    std::cout << "create socket success" << std::endl;
    return sockfd;
}

void Receive(int sockfd, std::string name)
{
    while(true)
    {
        char buffer[1024] = { 0 };
        struct sockaddr_in temp;
        socklen_t length = sizeof(temp);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &length);
        if(n > 0)
        {
            buffer[n] = '\0';
            std::cerr << buffer << std::endl;
        }
        else
        {
            break;
        }
    }
}

void Send(int sockfd, uint16_t port, const std::string &ip, std::string name)
{
    struct sockaddr_in server;
    socklen_t len = sizeof(server);
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = inet_addr(ip.c_str());
    
    while(true)
    {
        std::string line;
        std::cout << "please enter->";
        getline(std::cin, line);

        ssize_t n = sendto(sockfd, line.c_str(), line.size(), 0, (const struct sockaddr*)&server, len);
        if(n <= 0)
        {
            break;
        }
    }
}

// client需要知道server的ip和port
// ./udpclient serverip sererport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cout << "Usage:" << argv[0] << " serverip serverport" << std::endl;
        exit(0);
    }

    int sockfd = Initialize();

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    Thread recvt("recvive-thread", std::bind(&Receive, sockfd, std::placeholders::_1));
    Thread sendt("send-thread", std::bind(&Send, sockfd, serverport, serverip, std::placeholders::_1));

    recvt.Start();
    sendt.Start();

    recvt.Join();
    sendt.Join();

    close(sockfd);
    return 0;
}
  1. 当转发消息时,我们也希望知道是谁发送的消息
  2. 检查是否在线、下线、转发逻辑都需要遍历_online_user,需要加锁

我们将客户端收到的数据向标准错误打印,在启动客户端的使用重定向将标准错误重定向到管道文件或指定终端文件下,就能直接看到客户端收到的数据

在这里插入图片描述

[Practice/Lesson1/3. chat_server · baiyahua/Linux - 码云 - 开源中国 (gitee.com)](https://gitee.com/baiyahua/linux/tree/master/Practice/Lesson1/3. chat_server)

5. 地址转化函数

上面我们使用的inet_ntoa函数是将结构化的IP地址转化成字符串类型,根据man手册的描述,转化后的char*类型的IP地址存放在静态区中

在这里插入图片描述

经过测试,发现如果第二次再使用inet_ntoa进行地址转换,会将原来的空间覆盖

在这里插入图片描述

如果在多线程下,需要考虑线程线程安全

有更安全的转换函数

#include <arpa/inet.h>

# 字符换类型ip->结构化ip + 主机序列->网络序列
int inet_pton(int af, const char *src, void *dst);

# 将结构化ip->字符串类型ip
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

在这里插入图片描述


http://www.kler.cn/a/421178.html

相关文章:

  • 详解Java数据库编程之JDBC
  • MongoDB集群分片安装部署手册
  • 【机器学习】CatBoost 模型实践:回归与分类的全流程解析
  • 二百七十八、ClickHouse——将本月第一天所在的那一周视为第一周,无论它是从周几开始的,查询某个日期是本月第几周
  • 云服务器和物理服务器租用哪个好?
  • 【C++boost::asio网络编程】有关异步读写api的笔记
  • 详解版本控制工作原理及优势,常见的版本控制系统对比(HelixCore、Git、SVN等)
  • 【网络安全】网络加密原理 与 无线网络安全 链路加密
  • 深入详解人工智能入门数学基础:理解向量、矩阵及导数的概念
  • 关于数据库数据国际化方案
  • Windows 上安装使用dltviewer
  • C++的类功能整合
  • 【2024 re:Invent现场session参加报告】打造生成式AI驱动的车间智能助手
  • 笔记本电脑如何查看电池的充放电循环次数
  • HTML技术贴:深入理解网页构建基础
  • redis学习1
  • nVisual集成node-red 实现数据采集
  • 利用HTML5获取店铺详情销量:电商数据洞察的新纪元
  • 【算法】——前缀和
  • 利用Python爬虫获取亚马逊商品详情数据:一篇详细的教程
  • kafka-clients之CommonClientConfigs
  • 使用 Apache Commons IO 实现文件读写
  • 二叉树的前中后序遍历(非递归)
  • SpringBoot开发——整合Redis 实现分布式锁
  • Node.js实现WebSocket教程
  • C语言:指针与数组