Socket编程-udp

1. 前言

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

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

2. udp_echo_server

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

shell 复制代码
#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时需要将主机序列转为网络序列,这里有现成的接口直接使用

shell 复制代码
#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地址主机序列转网络序列
c++ 复制代码
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函数;接收到客户端的消息后,我们将客户端发送的消息打印出来,再发送回去

c++ 复制代码
#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 -> 谁发送,将来也要向对方发送
c++ 复制代码
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

c++ 复制代码
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;
}
c++ 复制代码
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

c++ 复制代码
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

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

shell 复制代码
char *inet_ntoa(struct in_addr in); # 将网络序列的结构化的ip地址转成字符串类型的ip地址
uint16_t ntohs(uint16_t netshort); # 将port由网络序列转成主机序列
c++ 复制代码
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封装

c++ 复制代码
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服务器收到英文单词时,回调该方法,将结果再发送回客户端

c++ 复制代码
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的结构存放,遍历查找

c++ 复制代码
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服务器

c++ 复制代码
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,将收到的数据交给消息转发模块,消息转发模块根据在线用户列表转发给每一个在线的人(包括发送信息的人) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/254aec1457e4479d982eeb8ab302a2ea.png) 要想IO逻辑与业务逻辑解耦,就需要给`udp`服务器传递消息转发的函数指针,当服务器拿到数据时,去回调方法 而我们的消息转发模块,就是根据在线用户列表依次转发消息 ```c++ #include "InetAddr.hpp" #include "Log.hpp" #include #include 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::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 _online_user; }; ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/66a466ff3a8444fab15913423813e8aa.png) 这里消息转发模块我们想使用多进程的方式,提高效率 将我们之前写过的多线程引入进来 ```c++ 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::GetInstance()->Equeue(t); } ``` 但此时仍有问题,我们发现服务端能同时读写,证明`udp`是支持全双工通信的,但客户端如果不输入发送的信息,收到的数据就会堆积在OS内部,上部不能接受,因此,我们的客户端也要写成多线程 ```c++ 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`,需要加锁 我们将客户端收到的数据向标准错误打印,在启动客户端的使用重定向将标准错误重定向到管道文件或指定终端文件下,就能直接看到客户端收到的数据 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/2c1f3012cf9b4fbfafdcef9b0c011ebf.png) \[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地址存放在静态区中 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f2e143f93a3642c8905bfb9a13ec654e.png) 经过测试,发现如果第二次再使用`inet_ntoa`进行地址转换,会将原来的空间覆盖 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/26231099a94e4428a6fa1dfe359041b0.png) 如果在多线程下,需要考虑线程线程安全 有更安全的转换函数 ```shell #include # 字符换类型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); ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/614fa4a7b807479186325ccb59cae650.png)

相关推荐
小糖学代码7 小时前
LLM系列:1.python入门:3.布尔型对象
linux·开发语言·python
shizhan_cloud7 小时前
Shell 函数的知识与实践
linux·运维
Deng8723473487 小时前
代码语法检查工具
linux·服务器·windows
霍夫曼9 小时前
UTC时间与本地时间转换问题
java·linux·服务器·前端·javascript
月熊10 小时前
在root无法通过登录界面进去时,通过原本的普通用户qiujian如何把它修改为自己指定的用户名
linux·运维·服务器
大江东去浪淘尽千古风流人物10 小时前
【DSP】向量化操作的误差来源分析及其经典解决方案
linux·运维·人工智能·算法·vr·dsp开发·mr
赖small强11 小时前
【Linux驱动开发】NOR Flash 技术原理与 Linux 系统应用全解析
linux·驱动开发·nor flash·芯片内执行
IT运维爱好者12 小时前
【Linux】LVM理论介绍、实战操作
linux·磁盘扩容·lvm
LEEE@FPGA12 小时前
ZYNQ MPSOC linux hello world
linux·运维·服务器
郝学胜-神的一滴12 小时前
Linux定时器编程:深入理解setitimer函数
linux·服务器·开发语言·c++·程序人生