[linux网络]UDP网络编程[网络·叁]

🌟 各位看官好,我是!****

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!

UDP网络编程

话不多说,我们直接编写代码,我们要实现的版本如下,再从实践转到理论中来:

v1版本 - Echo server(主要熟悉接口)

v2版本 - DictServer

v2版本 - DictServer封装版

v3版本 - 简单聊天室

Echo server

服务端

bash 复制代码
static const int gdefaultsockfd = -1;
class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip)
    :   _port(port),
        _sockfd(gdefaultsockfd),
        _ip(ip),
        _isrunning(false)
    {}
    
    ~UdpServer(){}
private:
    int _sockfd; 
    uint16_t _port;
    std::string _ip; // 暂时,"192.168.1.1"

    bool _isrunning;
};
创建套接字
bash 复制代码
        // 1. 创建socket fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3
绑定套接字
bash 复制代码
        // 我们有没有实现,把socket和file关联起来呢??没有!!!
        // 2. bind
        // 2.1: 填充IP和Port
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);

        //inet_addr:  1. 字符串转整数ip 2. 整数ip是网络序列的
        local.sin_addr = inet_addr(_ip.c_str()); 

        // 2.2 和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3

实际上local.sin_addr是有错误的,为什么呢?因为 sin_addr 本身是一个结构体(in_addr),而不是一个整数值。

不能直接将一个整数值赋值给一个结构体。s_addr 才是 in_addr 结构体中真正存放网络序IP地址的32位无符号整型成员。

typedef uint32_t in_addr_t;

struct in_addr

{

in_addr_t s_addr;

};

不应该有个疑惑么?

为什么不直接local.s_addr呢?非要单独设计一个结构体?

历史导致的多样性:在4.2BSD的早期实现中,in_addr 结构体可能更复杂,它可能是一个联合体(union),

允许多种方式来解释同一个IP地址。

bash 复制代码
local.sin_addr.s_addr = inet_addr(_ip.c_str());

要是绑ip地址,只能收到这个ip发来的消息.但是一台主机可能会被多个ip指向.只要是给我这个主机发的,我就都要!因此成员变量的ip就不需要啊

bash 复制代码
local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind

正确代码:

bash 复制代码
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind

        // 2.2 和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
读取数据

UDP独有:

// 读取数据报

ssize_t recv(int sockfd, void buf.len, size_t len, int flags);

ssize_t recvfrom(int sockfd, void bufrestrict .len, size_t len, int flags, struct sockaddr *_Nullable restrict src_addr, socklen_t *_Nullable restrict addrlen);

参数:

  • buf:输出型参数,把数据读取到缓冲区.
  • flags:设为0,阻塞读
  • restrict src_addr:当我收到消息内容的时候,未来我还有给别人会消息所以,我必须知道对方是谁! --> 我必须知道对方的socket信息,即IP+Port --> 因此是一个输出型参数,把对方的socket带出来.
  • addrlen:输入输出型参数,把不真实的src_addr大小带进去,把真实的src_addr大小带出来.

返回值:

These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. 返回小于0表示出错.

bash 复制代码
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; // 清空缓冲区
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 读取数据
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
                (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                //client是谁啊??ip和端口给我!
                //...
            }
        }
发送数据

// 发送数据报

ssize_t send(int sockfd, const void buf.len, size_t len, int flags);

ssize_t sendto(int sockfd, const void buf.len, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

dest_addr:目标地址

addrlen:目标地址长度
结论1:sockfd既可以读,又可以写,UDP支持全双工通信

不需要把\0发送给对方,因为向文件里写的时候,不需要往文件写入\0,\0是C语言规定,和网络、文件无关

bash 复制代码
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);

                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << "[" << clientip 
                    << ":" << clientport << "]# " << buffer;

                std::string echo_string = "server echo# ";
                echo_string += buffer;

                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
                    (struct sockaddr*)&peer, len);

客户端

创建套接字
bash 复制代码
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
绑定套接字

client要不要:显示的bind自己的ip和端口,即程序员自己是否需要绑定bind函数绑定ip和端口?不要!!!

为什么客户端不大大方方地把自己的socket和对方的socket进行bind,非要扭扭捏捏的?如果程序员进行bind,知道绑哪一个吗?client会在自己OS的帮助下,随机bind端口号,客户端之间彼此不会出现冲突.

为什么服务端要固定呢?是具体的公司,一定是只能访问某个公司的服务端.
client 要不要 隐式绑定 IP和端口?

隐式绑定: 程序员不手动调用 bind() ,而是由操作系统内核在特定时机自动地、隐式地为套接字分配一个可用的IP地址和端口号.

特定时机:这个时机就是在你首次尝试与服务器通信 的时候。对于TCP,是调用 connect() 函数时;对于UDP,是调用 sendto()connect() 函数时。

所以客户端不需要绑定套接字!!!

写数据
bash 复制代码
    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());

    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);

        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
    }
读数据
bash 复制代码
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
代码测试

netstat是用来查看网络状态的

netstat -u(带udp) -a(所有udp) -n(显示数字) -p(进程)

那如果一个是本地环回,一个是本地ip呢?数据发不过去.

那是否可以绑定公网IP呢?发现并不能,因为云服务器的服务端禁止用户bind公网ip(即便可以)

最佳实践:不建议服务端bind固定的IP!!!

附源码

UdpServer.hpp

bash 复制代码
static const int gdefaultsockfd = -1;
class UdpServer
{
public:
    UdpServer(uint16_t port)
    :   _port(port),
        _sockfd(gdefaultsockfd),
        _isrunning(false)
    {}
    void Init()
    {
        // 1. 创建socket fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3

        // 2. bind
        // 2.1: 填充IP和Port
        // 我们有没有实现,把socket和file关联起来呢??没有!!!
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        // local.sin_addr.s_addr = INADDR_ANY; // 任意IPbind
        // 什么叫做任意IP bind? 不明确具体IP,只要是发给我对应的主机,对应的port
        // 我都能收到!
        local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind
        //local.sin_addr = inet_addr(_ip.c_str()); //inet_addr:  1. 字符串转整数ip 2. 整数ip是网络序列的
        // ?
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); //inet_addr:  1. 字符串转整数ip 2. 整数ip是网络序列的


        // 2.2 和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
    }
    void Start()
    {
        // 所有的服务器都是死循环
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; // 清空缓冲区
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 读取数据
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
                (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                //client是谁啊??ip和端口给我!
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);

                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << "[" << clientip 
                    << ":" << clientport << "]# " << buffer;

                std::string echo_string = "server echo# ";
                echo_string += buffer;

                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
                    (struct sockaddr*)&peer, len);
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~UdpServer(){}
private:
    int _sockfd; 
    uint16_t _port;
    // std::string _ip; // 暂时,"192.168.1.1"

    bool _isrunning;
};

UdpServer.cc

bash 复制代码
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}

// ./udp_server serverport
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

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

    EnableConsoleLogStrategy();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->Init();
    usvr->Start();


    return 0;
}

UdpClient.cc

bash 复制代码
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        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 errror" << std::endl;
        return 0;
    }
    // [srcip, srcport] [dstip, dstport]
    // client要不要:显示的bind自己的ip和端口?不要!!!
    // client 要不要 隐式bind IP和端口?
    // 为什么?client会在自己OS的帮助下,随机bind端口号

    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());

    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);

        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));

        //读
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

DictServer

熟悉了网络相关接口后,这里写一个支持中文翻译成英文的工作.

翻译工作

dict.txt

bash 复制代码
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
: 冬天
winter1:
:

我们把中英翻译放到一个txt文档里,将来写一个类翻译Dictionary类,在上层指定中英文档路径.

bash 复制代码
class Dictionary
{
public:
    Dictionary(const std::string &path):_path(path)
    {
        LOG(LogLevel::INFO) << "construct Dictionary obj";
        LoadConf();
    }

    ~Dictionary()
    {}
private:
    std::string _path;
    std::unordered_map<std::string, std::string> _dict;
};

将 dict.txt 文档中合法的中英文对照条目载入到_dict 哈希表中。

  1. 判断该路径是否存在,在判断该文件是否打开;
  2. 按行读取,将": "作为分隔符,若读到npos说明没有找到分隔符,即该行数据是非法的,跳到下一行;
  3. 即使找到分隔符,若英文或中文缺一也是非法的;
  4. 此时该行数据一定是合法的,插入到_dict表中.
bash 复制代码
    static const std::string sep = ": ";

    void LoadConf()
    {
        std::ifstream in(_path);
        if(!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open file error: " << _path;
            return;
        }
        std::string line;
        while(std::getline(in, line))
        {
            LOG(LogLevel::DEBUG) << "load dict message: " << line;
            //dog: 狗
            auto pos = line.find(sep);
            if(pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "format error: " << line;
                continue;
            }
            std::string word = line.substr(0, pos); // [)
            std::string value = line.substr(pos + sep.size());
            if(word.empty() || value.empty())
            {
                LOG(LogLevel::WARNING) << "format error, word or value is empty: " << line;
                continue;
            }
            _dict.insert(std::make_pair(word, value));
        }

        in.close();
    }

当客户端向服务端发送数据时(双方约定该操作对应翻译任务),服务端读取数据并提交给上层模块处理。此时系统会调用 Translate 函数,从_dict 哈希表中查找该英文对应的中文,最终将查找到的中文结果返回给客户端。

bash 复制代码
    std::string Translate(const std::string &word,
         const std::string &whoip, uint16_t whoport)
    {
        (void)whoip;
        (void)whoport;
        auto iter = _dict.find(word);
        if(iter == _dict.end())
        {
            return "unknown";
        }
        return iter->first + "->" +  iter->second;
    }

服务端

服务端完全不知道 接收到的数据报对应的数据类型是字符串、单词、整数还是二进制数据等等.即UDP 给了你一个 "完整的盒子",但盒子上依然没有标签 ------ 标签需要你自己提前和对方说好怎么贴、怎么看

DictServer.hpp

bash 复制代码
using callback_t = std::function<std::string \
    (const std::string &word, const std::string &whoip, uint16_t whoport)>;

std::string word = buffer;
std::string result = _cb(word, clientip, clientport);

DictServer.cc

bash 复制代码
std::unique_ptr<DictServer> usvr = std::make_unique<DictServer>(port, 
    [&dict](const std::string &word, const std::string &whoip, uint16_t whoport) ->  
    std::string{ return dict.Translate(word, whoip, whoport);}
);

附源码

DictServer.hpp

bash 复制代码
using callback_t = std::function<std::string \
    (const std::string &word, const std::string &whoip, uint16_t whoport)>;

static const int gdefaultsockfd = -1;

class DictServer
{
public:
    DictServer(uint16_t port, callback_t cb)
    :   _port(port),
        _sockfd(gdefaultsockfd),
        _isrunning(false),
        _cb(cb)
    {}
    void Init()
    {
        // 1. 创建socket fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3

        // 2. bind
        // 2.1: 填充IP和Port
        // 我们有没有实现,把socket和file关联起来呢??没有!!!
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        // local.sin_addr.s_addr = INADDR_ANY; // 任意IPbind
        // 什么叫做任意IP bind? 不明确具体IP,只要是发给我对应的主机,对应的port
        // 我都能收到!
        local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind
        //local.sin_addr = inet_addr(_ip.c_str()); //inet_addr:  1. 字符串转整数ip 2. 整数ip是网络序列的
        // ?
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); //inet_addr:  1. 字符串转整数ip 2. 整数ip是网络序列的


        // 2.2 和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
    }
    void Start()
    {
        // 所有的服务器都是死循环
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; // 清空缓冲区
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 读取数据
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
                (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                buffer[n] = 0;
                //client是谁啊??ip和端口给我!
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);

                std::string word = buffer;
                LOG(LogLevel::DEBUG) << "用户查找: " << word;
                // 回调!
                std::string result = _cb(word, clientip, clientport);

                // buffer[n] = 0;
                // LOG(LogLevel::DEBUG) << "[" << clientip 
                //     << ":" << clientport << "]# " << buffer;

                // std::string echo_string = "server echo# ";
                // echo_string += buffer;

                sendto(_sockfd, result.c_str(), result.size(), 0,
                    (struct sockaddr*)&peer, len);
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~DictServer(){}
private:
    int _sockfd; 
    uint16_t _port;
    // std::string _ip; // 暂时,"192.168.1.1"
    callback_t _cb;
    bool _isrunning;
};

DictServer.cc

bash 复制代码
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}

// std::string Translate(const std::string &word, const std::string &whoip, uint16_t whoport)
// {
//     return "哈哈";
// }

// ./udp_server serverport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    EnableConsoleLogStrategy();

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

    Dictionary dict("./dict.txt");

    std::unique_ptr<DictServer> usvr = std::make_unique<DictServer>(port,
                                                                    [&dict](const std::string &word, const std::string &whoip, uint16_t whoport) -> std::string
                                                                    {
                                                                        return dict.Translate(word, whoip, whoport);
                                                                    });
    usvr->Init();
    usvr->Start();

    return 0;
}

DictClient.cc

bash 复制代码
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        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 errror" << std::endl;
        return 0;
    }
    // [srcip, srcport] [dstip, dstport]
    // client要不要:显示的bind自己的ip和端口?不要!!!
    // client 要不要 隐式bind IP和端口?
    // 为什么?client会在自己OS的帮助下,随机bind端口号

    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());

    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);

        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));

        //读
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

Dictionary.hpp

bash 复制代码
static const std::string sep = ": ";

class Dictionary
{
private:
    void LoadConf()
    {
        std::ifstream in(_path);
        if(!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open file error: " << _path;
            return;
        }
        std::string line;
        while(std::getline(in, line))
        {
            LOG(LogLevel::DEBUG) << "load dict message: " << line;
            //dog: 狗
            auto pos = line.find(sep);
            if(pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "format error: " << line;
                continue;
            }
            std::string word = line.substr(0, pos); // [)
            std::string value = line.substr(pos + sep.size());
            if(word.empty() || value.empty())
            {
                LOG(LogLevel::WARNING) << "format error, word or value is empty: " << line;
                continue;
            }
            _dict.insert(std::make_pair(word, value));
        }

        in.close();
    }
public:
    Dictionary(const std::string &path):_path(path)
    {
        LOG(LogLevel::INFO) << "construct Dictionary obj";
        LoadConf();
    }
    std::string Translate(const std::string &word,
         const std::string &whoip, uint16_t whoport)
    {
        (void)whoip;
        (void)whoport;
        auto iter = _dict.find(word);
        if(iter == _dict.end())
        {
            return "unknown";
        }
        return iter->first + "->" +  iter->second;
    }
    ~Dictionary()
    {}
private:
    std::string _path;
    std::unordered_map<std::string, std::string> _dict;
};

简单聊天室

该 demo 代码主要实现了群聊功能:当一名用户发送消息时,所有在线用户均能收到该消息.

当用户发送消息时,服务器读取该消息后,会将客户端的消息内容与其对应的套接字打包为一个任务,提交至线程池。线程池检测到新任务后,会唤醒相应线程从中取出任务进行处理 ------ 处理逻辑为实现群聊功能,即把该消息转发给当前所有在线用户。

前置工作

当一个客户端给服务端发消息时,服务端需不需要知道客户端的是谁?需要的,而客户端身份通常通过 IP 地址与端口号组合标识,这就不可避免涉及网络字节序(大端)与主机字节序(可能为小端)的相互转换。这种转换操作具有极强的重复性:每次解析客户端地址、绑定服务端端口、设置目标地址时都需要调用htons()/ntohs()(端口转换)、inet_pton()/inet_ntop()(IP 地址转换)等函数,不仅冗余繁琐,还容易因疏忽导致字节序错误(比如忘记转换直接使用主机序端口)。因此我们可以把它封装在InetAddr.hpp中.

bash 复制代码
#define Conv(addr) ((struct sockaddr*)&addr)

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }

public:
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        Net2Host();
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _port(port), _ip(ip)
    {
        Host2Net();
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    struct sockaddr* Addr()
    {
        return Conv(_addr);
    }
    socklen_t Length()
    {
        return sizeof(_addr);
    }
    std::string ToString()
    {
        return _ip + "-" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return (_ip == addr._ip && _port == addr._port);
        // return (_ip == addr._ip);
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr; // 网络风格地址 --> 网络序列之后可能还要用,所以保存_addr,方便发回去
    // 主机风格地址
    std::string _ip;
    uint16_t _port;
};

路由功能

当服务器读到了客户端发来的message时,需要把这个消息转发给在线用户.那么该如何设计呢?为了保证层与层之间的解耦,这里引出了所谓的路由功能,它帮我们进行消息的转发,而服务端只负责IO,并不负责处理数据.

bash 复制代码
    using callback_t = std::function<void \
        (int sockfd, std::string message, InetAddr addr)>;

    _cb(_sockfd, message, clientaddr);

    // 将打包好的任务push到线程池里,并交由路由进行消息的群发
    std::unique_ptr<ChatServer> usvr = std::make_unique<ChatServer>(port,
       [&r, &tp](int sockfd, std::string message, InetAddr addr){
            task_t task = std::bind(&Route::RouteMessageToAll, r.get(), sockfd, message, addr);
            tp->Enqueue(task);
       }
    );

路由需要把消息转发给所有人?那么需不需要知道在线用户是多少呢?离线用户有哪些?一共有多少在线用户呢?需不需要进行维护呢?需要的,如何维护?先描述,再组织,因此一定存在对路由功能进行描述的类.

bash 复制代码
class Route
{
private:
    bool IsExists(const InetAddr &addr)
    {
        for (auto &user : _online_user)
        {
            if (user == addr)
            {
                return true;
            }
        }
        return false;
    }
    void AddUser(const InetAddr &addr)
    {
        if(!IsExists(addr))
            _online_user.push_back(addr);
    }
    void DeleteUser(const std::string &message, const InetAddr &addr)
    {
        // 权宜之计, 自定义协议部分
        if(message == "QUIT")
        {
            auto iter = _online_user.begin();
            for(; iter != _online_user.end(); iter++)
            {
                if(*iter == addr)
                {
                    _online_user.erase(iter);
                    break;
                }
            }
        }
    }
    void SendMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        for(auto &user : _online_user)
        {
            LOG(LogLevel::DEBUG) << "route [" << message << "] to : " << user.ToString();

            std::string info = addr.ToString();
            info += "# ";
            info += message; // XXXX-PORT# 你好


            sendto(sockfd, info.c_str(), info.size(), 0, user.Addr(), user.Length());
        }
    }
public:
    Route()
    {
    }
    void RouteMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        AddUser(addr);
        // 我们就一定或有在线用户列表
        SendMessageToAll(sockfd, message, addr);

        DeleteUser(message, addr);
    }

    // void RouteMessageToOne()
    // {}

    ~Route()
    {
    }

private:
    // 临界资源
    // 方法1:加锁
    std::vector<InetAddr> _online_user; // 在线用户
};

有一个问题:线程池内进行消息转发时,线程池从任务队列里拿任务,.一个线程在进行广播的时候,另外一个线程有没有可能也要进行广播呢?在广播的同时,有没有可能要新增用户或删除用户,即多线程并发执行RouteMessageToAI时,可能会对在线用户列表进行并发访问,即是一个临界资源啊!

bash 复制代码
    // 方法2:
    // 锁 + 拷贝
    std::vector<InetAddr> _send_list;

    // 方法3:
    std::queue<std::string> _message_queue;

客户端

在微信中,如果我们不进行发消息,能不能收到消息呢?可以的,因此客户端不能是串行执行的,即串行的写和读明显是不对的,应该是多线程的,一个线程专门用来写,一个新线程专门用来读.

bash 复制代码
void recver()
{
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl; // 1->2
        }
    }
}

void sender()
{
    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());

    while (true)
    {
        std::cout << "Please Enter@ "; //1
        std::string line;
        std::getline(std::cin, line); //0

        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }
}

附源码

Route.hpp

bash 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"

class Route
{
private:
    bool IsExists(const InetAddr &addr)
    {
        for (auto &user : _online_user)
        {
            if (user == addr)
            {
                return true;
            }
        }
        return false;
    }
    void AddUser(const InetAddr &addr)
    {
        if(!IsExists(addr))
            _online_user.push_back(addr);
    }
    void DeleteUser(const std::string &message, const InetAddr &addr)
    {
        // 权宜之计, 自定义协议部分
        if(message == "QUIT")
        {
            auto iter = _online_user.begin();
            for(; iter != _online_user.end(); iter++)
            {
                if(*iter == addr)
                {
                    _online_user.erase(iter);
                    break;
                }
            }
        }
    }
    void SendMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        for(auto &user : _online_user)
        {
            LOG(LogLevel::DEBUG) << "route [" << message << "] to : " << user.ToString();

            std::string info = addr.ToString();
            info += "# ";
            info += message; // XXXX-PORT# 你好


            sendto(sockfd, info.c_str(), info.size(), 0, user.Addr(), user.Length());
        }
    } 
public:
    Route()
    {
    }
    void RouteMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        AddUser(addr);
        // 我们就一定或有在线用户列表
        SendMessageToAll(sockfd, message, addr);

        DeleteUser(message, addr);
    }
    // void RouteMessageToOne()
    // {}
    ~Route()
    {
    }

private:
    // 临界资源
    // 方法1:加锁
    std::vector<InetAddr> _online_user; // 在线用户

    // 方法2:
    // // 锁 + 拷贝
    // std::vector<InetAddr> _send_list;

    // 方法3:
    // std::queue<std::string> _message_queue;
};

ChatServer.hpp

bash 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"

using callback_t = std::function<void \
    (int sockfd, std::string message, InetAddr addr)>;

static const int gdefaultsockfd = -1;

class ChatServer
{
public:
    ChatServer(uint16_t port, callback_t cb)
    :   _port(port),
        _sockfd(gdefaultsockfd),
        _isrunning(false),
        _cb(cb)
    {}
    void Init()
    {
        // 1. 创建socket fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3

        InetAddr local(_port);

        // 2.2 和socketfd进行bind
        int n = bind(_sockfd, local.Addr(), local.Length());
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
    }
    void Start()
    {
        // 所有的服务器都是死循环
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; // 清空缓冲区
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 读取数据
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
                (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                // 约定: 聊天消息
                buffer[n] = 0;
                // 得到对应的client是谁?
                InetAddr clientaddr(peer);
                LOG(LogLevel::DEBUG) << "get a client info # " 
                    << clientaddr.Ip() << "-" << clientaddr.Port() << ": "
                    << buffer;
                

                std::string message = buffer;
                // 回调!
                _cb(_sockfd, message, clientaddr);
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~ChatServer(){}
private:
    int _sockfd; 
    uint16_t _port;
    callback_t _cb;
    bool _isrunning;
};

ServerMain.cc

bash 复制代码
#include "ThreadPool.hpp"
#include "Route.hpp"
#include "ChatServer.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}

// // for debug
// void chat(int sockfd, std::string message, InetAddr addr)
// {
//     LOG(LogLevel::DEBUG) << "sockfd: " << sockfd;
//     LOG(LogLevel::DEBUG) << "message: " << message;
//     LOG(LogLevel::DEBUG) << "client info: " << addr.ToString();
//     sendto(sockfd, message.c_str(), message.size(), 0, addr.Addr(), addr.Length());
// }

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

// ./udp_server serverport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    EnableConsoleLogStrategy();
    uint16_t port = std::stoi(argv[1]);

    // 1. 消息转发功能
    std::unique_ptr<Route> r = std::make_unique<Route>();

    // 2. 线程池对象
    auto tp = ThreadPool<task_t>::GetInstance();

    // 3. 服务器对象
    std::unique_ptr<ChatServer> usvr = std::make_unique<ChatServer>(port,
       [&r, &tp](int sockfd, std::string message, InetAddr addr){
            task_t task = std::bind(&Route::RouteMessageToAll, r.get(), sockfd, message, addr);
            tp->Enqueue(task);
       }
    );
    usvr->Init();
    usvr->Start();

    return 0;
}

ChatClient.cc

bash 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <thread>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}

int sockfd = -1;
std::string serverip;
uint16_t serverport;

void InitClient(const std::string &serverip, uint16_t serverport)
{
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket errror" << std::endl;
    }
}

void recver()
{
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl; // 1->2
        }
    }
}

void sender()
{
    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());

    while (true)
    {
        std::cout << "Please Enter@ "; //1
        std::string line;
        std::getline(std::cin, line); //0

        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    std::cerr << "hahhaha " << std::endl;
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    serverip = argv[1];
    serverport = std::stoi(argv[2]);
    InitClient(serverip, serverport);

    std::thread trecv(recver);
    std::thread tsend(sender);

    trecv.join();
    tsend.join();

    return 0;
}

OS和网络的关系

网络命令

  • 检测网络连通信

ping -c(n) www.baidu.com

  • 查看网络服务

netstat -utapn -l(只显示处于LISTEN状态)

  • 查看特定网络服务特定进程的命令

pidof 名称 -->自动提取pid

pidof 名称|xargs kill -9 (xargs的作用就是把管道当中传递过来的数据转换成后续命令的后续,拼接到后面)