【Linux】Socket编程-UDP构建自己的C++服务器

news2025/1/5 6:18:46

🌈 个人主页:Zfox_
🔥 系列专栏:Linux

目录

  • 一:🔥 UDP 网络编程
    • 🦋 接口讲解
    • 🦋 V1 版本 - echo server
    • 🦋 V2 版本 - DictServer
    • 🦋 V3 版本 - 简单聊天室
  • 二:🔥 观察者模式
  • 三:🔥 补充参考内容
    • 🦋 地址转换函数
    • 🦋 关于 inet_ntoa
  • 四:🔥 补充网络命令
    • 🦋 Ping 命令
    • 🦋 netstat
    • 🦋 pidof
  • 五:🔥 共勉

一:🔥 UDP 网络编程

🦋 接口讲解

socket

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

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

domain:/ 协议家族
	AF_INET      IPv4 Internet protocols
	AF_INET6     IPv6 Internet protocols

type: 报文类型
    SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
    SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.

protocol: 传输层类型
	默认为0

On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately.

bind

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

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.


// 2. 填充网络信息,并bind绑定
// 2.1 没有把socket信息设置进入内核
struct sockaddr_in local;
bzero(&local, sizeof(local));       // string.h
local.sin_family = AF_INET;
local.sin_port = ::htons(_port);   // 要被发送给对方,既要发送到网络中!   主机序列转换为网络序列 大小端转换 网络中都是大端 #include <arpa/inet.h>
local.sin_addr.s_addr = ::inet_addr(_ip.c_str());    // 1. string ip -> 4bytes 2. network order   #include <sys/socket.h>    #include <netinet/in.h>  #include <arpa/inet.h>
local.sin_addr.s_addr = INADDR_ANY;

// 2.1 bind    这里设置进入内核
int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());

📚 我们之前在调用socket的时候,明明已经填充了一次 AF_INET, 为什么这里还需要一次呢?

创建套接字的时候填充的 AF_INET 是给操作系统文件系统里的网络文件接口,告诉我们的操作系统我们要创建一个网络的套接字。

这里则是用来填充 sockaddr_in 网络信息,只有套接字的结构和这里的结构一样,操作系统才能绑定成功。

📚 必带四件套

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

recvfrom

#include <sys/types.h>
#include <sys/socket.h>
       
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

sockfd:			 指定的套接字
buf:                存放收到的消息内容缓冲区
len:    			 期望收取多少个字节
flag:  			 收方式,默认为0,阻塞收

输出型参数:
sockaddr *src_addr: 谁发来的消息(ip + port)远端
addrlen:

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.

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

sockfd:			 指定的套接字
buf:                发送的字符串
len:    			 
flag:  			 收方式,默认为0,阻塞发

输出型参数:
sockaddr *dest_addr: 目标主机(ip + port)远端
addrlen:

🦋 V1 版本 - echo server

简单的回显服务器和客户端代码

备注: 代码中会用到 地址转换函数 . 参考接下来的章节.

UdpServer.hpp

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <string.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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

using namespace LogModule;

const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1";      // 表示本地主机
const static uint16_t gdefaultport = 8080;

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

class UdpServer
{
public:
    UdpServer(func_t func, uint16_t port = gdefaultport)
        :_sockfd(gsockfd)
        ,_addr(port)
        ,_isrunning(false)
        ,_func(func)
    {
    }

    // 都是套路
    void InitServer()
    {
        // 1. 创建socket
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);     // IP?PORT?网络?本地?
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;

        // 2. 填充网络信息,并bind绑定
        // 2.1 没有把socket信息设置进入内核
        // struct sockaddr_in local;
        // bzero(&local, sizeof(local));
        // local.sin_family = AF_INET;
        // local.sin_port = ::htons(_port);   // 要被发送给对方,既要发送到网络中!   主机序列转换为网络序列 大小端转换 网络中都是大端
        // // local.sin_addr.s_addr = ::inet_addr(_ip.c_str());            // 1. strinf ip -> 4bytes 2. network order
        // local.sin_addr.s_addr = INADDR_ANY;

        // 2.1 bind    这里设置进入内核
        int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }

    void Start()
    {
        _isrunning = true;
        while(true)
        {
            char inbuffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);   // 必须设置

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer - 1), 0, CONV(&peer), &len);
            if(n > 0)
            {
                inbuffer[n] = 0;
                std::string result = _func(inbuffer);   // 回调 出去还会回来

                ::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    }

    ~UdpServer()
    {
        if(_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    InetAddr _addr;
    // uint16_t _port;     // 服务器未来的端口号
    // std::string _ip;    // 服务器所对应的IP
    bool _isrunning;       // 服务器运行状态

    // 业务 回调方法
    func_t _func;
};

#endif

InetAddr.hpp

#pragma once

#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"

class InetAddr
{
private:
    void PortNettoHost()
    {
        _port = ::ntohs(_net_addr.sin_port);
    }

    void IpNettoHost()
    {
        char ipbuffer[64];
        const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        (void)ip;
    }

public:
    InetAddr()
    {
    }

    InetAddr(const struct sockaddr_in &addr)
        : _net_addr(addr)
    {
        PortNettoHost();
        IpNettoHost();
    }

    InetAddr(uint16_t port)
        : _port(port), _ip("")
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port);
        _net_addr.sin_addr.s_addr = INADDR_ANY;
    }

    struct sockaddr *NetAddr() { return CONV(&_net_addr); }

    socklen_t NetAddrLen() { return sizeof(_net_addr); }

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

    uint16_t Port() { return _port; }

    ~InetAddr()
    {
    }

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

Comm.hpp

#pragma once

#include <iostream>

#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)

#define CONV(v) (struct sockaddr *)(v)

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};
  • Log.hpp 前面的博客已经有了, 这里就不再复制粘贴了
  • 云服务器不允许直接 bind 公有 IP, 我们也不推荐编写服务器的时候, bind 明确的 IP, 推荐直接写成 INADDR_ANY
C++
/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)

在网络编程中, 当一个进程需要绑定一个网络端口以进行通信时, 可以使用 INADDR_ANY 作为 IP 地址参数。 这样做意味着该端口可以接受来自任何 IP 地址的连接请求, 无论是本地主机还是远程主机。 例如, 如果服务器有多个网卡(每个网卡上有不同的 IP 地址) , 使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个 网卡/IP 地址上面获取的。

UdpServerMain.cc

#include "UdpServer.hpp"

// ./server_udp localport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);

    LogModule::ENABLE_CONSOLE_LOG();

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

UdpClientMain.cc

#include "UdpClient.hpp"
#include "Common.hpp"
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        Die(USAGE_ERR);
    }

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

    // 1. 创建 socket
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }

    // 1.1 填充server信息
    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());

    // 2. clientdone
    while (true)
    {
        std::cout << "Please Enter# ";
        std::string message;
        std::getline(std::cin, message);
        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
        (void)n;

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}
  • 📚 client 端要不要显示 bind 的问题
client 不需要bind吗?               socket <-> socket
client 必须也有自己的ip和端口!        客户端不需要自己显示的调用bind!!
                                   而是客户端首次sendto信息的时候,由OS自动进行bind
1. 如何理解client自动随机bind端口号   一个端口号,只能被一个进程bind
2. 如何理解server要显示的bind?       服务器的端口号,必须稳定!必须是众所周知且不能轻易改变的!

🦋 V2 版本 - DictServer

实现一个简单的英译汉的网络字典
dict.txt

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
...

Dictionary.hpp

#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
#include "Common.hpp"

const std::string gpath = "./";
const std::string gdictname = "dict.txt";
const std::string gsep = ": ";

using namespace LockModule;

class Dictionary
{
private:
    bool LoadDictionary()
    {
        std::string file = _path + _filename;
        std::ifstream in(file.c_str());
        if(!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open file " << file << " error";
            return false;
        }

        // happy: 快乐的
        std::string line;
        while(std::getline(in, line))
        {
            std::string key;
            std::string value;
            if(SplitString(line, &key, &value, gsep))
            {
                _dictionary.insert(std::make_pair(key, value));
            }  
        }

        in.close();
        return true;
    }
public:
    Dictionary(const std::string &path = gpath, const std::string &filename = gdictname)
        :_path(path)
        ,_filename(filename)
    {
        LoadDictionary();
    }

    std::string Translate(const std::string &word)
    {
        auto iter = _dictionary.find(word);
        if(iter == _dictionary.end()) return "None";
        return iter->second;
    }

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

UdpServer.hpp

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <string.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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

using namespace LogModule;

const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1";      // 表示本地主机
const static uint16_t gdefaultport = 8080;

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

class UdpServer
{
public:
    UdpServer(func_t func, uint16_t port = gdefaultport)
        :_sockfd(gsockfd)
        ,_addr(port)
        ,_isrunning(false)
        ,_func(func)
    {
    }

    // 都是套路
    void InitServer()
    {
        // 1. 创建socket
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);     // IP?PORT?网络?本地?
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;

        // 2. 填充网络信息,并bind绑定
        // 2.1 没有把socket信息设置进入内核
        // struct sockaddr_in local;
        // bzero(&local, sizeof(local));
        // local.sin_family = AF_INET;
        // local.sin_port = ::htons(_port);   // 要被发送给对方,既要发送到网络中!   主机序列转换为网络序列 大小端转换 网络中都是大端
        // // local.sin_addr.s_addr = ::inet_addr(_ip.c_str());            // 1. strinf ip -> 4bytes 2. network order
        // local.sin_addr.s_addr = INADDR_ANY;

        // 2.1 bind    这里设置进入内核
        int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }

    void Start()
    {
        _isrunning = true;
        while(true)
        {
            char inbuffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);   // 必须设置

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer - 1), 0, CONV(&peer), &len);
            if(n > 0)
            {
                inbuffer[n] = 0;
                std::string result = _func(inbuffer);   // 回调 出去还会回来

                ::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    }

    ~UdpServer()
    {
        if(_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    InetAddr _addr;
    // uint16_t _port;     // 服务器未来的端口号
    // std::string _ip;    // 服务器所对应的IP
    bool _isrunning;       // 服务器运行状态

    // 业务 回调方法
    func_t _func;
};

#endif

UdpServerMain.cc

#include "UdpServer.hpp"
#include "Dictionary.hpp"

// ./server_udp localport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);

    LogModule::ENABLE_CONSOLE_LOG();

    std::shared_ptr<Dictionary> dict_sptr = std::make_shared<Dictionary>();

    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_sptr](const std::string &word) { return dict_sptr->Translate(word); }, port);
    
    // func_t f = std::bind(&Dictionary::Translate, dict_sptr.get(), std::placeholders::_1);
    // std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(f, port);

    svr_uptr->InitServer();
    svr_uptr->Start();

    return 0;
}

UdpClientMain.cc

#include "UdpClient.hpp"
#include "Common.hpp"
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        Die(USAGE_ERR);
    }

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

    // 1. 创建 socket
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }

    // 1.1 填充server信息
    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());

    // 2. clientdone
    while (true)
    {
        std::cout << "Please Enter# ";
        std::string message;
        std::getline(std::cin, message);
        // client 不需要bind吗?     socket <-> socket
        // client 必须也有自己的ip和端口!        客户端不需要自己显示的调用bind!!
        // 而是客户端首次sendto信息的时候,由OS自动进行bind
        // 1. 如何理解client自动随机bind端口号   一个端口号,只能被一个进程bind
        // 2. 如何理解server要显示的bind?       服务器的端口号,必须稳定!必须是众所周知且不能轻易改变的!
        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
        (void)n;

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

🦋 V3 版本 - 简单聊天室

UdpServer.hpp

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <string.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
#include "ThreadPool.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1";      // 表示本地主机
const static uint16_t gdefaultport = 8080;

using adduser_t = std::function<void(InetAddr &id)>;
using remove_t = std::function<void(InetAddr &id)>;

using task_t = std::function<void()>;
using route_t = std::function<void(int sockfd, const std::string &message)>;

// 编程技巧 继承nocopy的类就都不会被拷贝了
class nocopy
{
public:
    nocopy() {}
    nocopy(const nocopy &) = delete;
    const nocopy &operator=(const nocopy &) = delete;
    ~nocopy() {}
};

class UdpServer : public nocopy
{
public:
    UdpServer(uint16_t port = gdefaultport)
        : _sockfd(gsockfd), _addr(port), _isrunning(false)
    {
    }

    // 都是套路
    void InitServer()
    {
        // 1. 创建socket
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;

        int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }

    void RegisterService(adduser_t adduser, route_t route, remove_t removeuser)
    {
        _adduser = adduser;
        _route = route;
        _removeuser = removeuser;
    }

    void Start()
    {
        _isrunning = true;
        while (true)
        {
            char inbuffer[4096];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设置

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);
            if (n > 0)
            {
                InetAddr cli(peer);
                inbuffer[n] = 0;
                std::string message;
                if (strcmp(inbuffer, "QUIT") == 0)
                {
                    // 移除观察者
                    _removeuser(cli);
                    message = cli.Addr() + "# " + "我走了, 你们聊!";
                }
                else
                {
                    // 2. 新增用户
                    _adduser(cli);
                    message = cli.Addr() + "# " + inbuffer;
                }

                // task_t task = [this, message]()
                // {
                //     this->_route(this->_sockfd, message);
                // };

                // 3. 构建转发任务,推送给线程池,让线程池进行转发  线程池是无参数的所以要绑定
                task_t task = std::bind(UdpServer::_route, _sockfd, message);
                ThreadPool<task_t>::getInstance()->Equeue(task);

                // std::string echo_string = "echo# ";
                // echo_string += inbuffer;

                // ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    }

    ~UdpServer()
    {
        if (_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    InetAddr _addr;
    bool _isrunning; // 服务器运行状态

    // 新增用户
    adduser_t _adduser;
    // 移除用户
    remove_t _removeuser;
    // 数据转发
    route_t _route;
};

#endif

UdpClientMain.cc

#include "UdpClient.hpp"
#include "Common.hpp"
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

int sockfd = -1;
struct sockaddr_in server;

void ClientQuit(int signo)
{
    (void)signo;
    const std::string quit = "QUIT";
    int n = ::sendto(sockfd, quit.c_str(), quit.size(), 0, CONV(&server), sizeof(server));
    (void)n;
    exit(0);
}

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

// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        Die(USAGE_ERR);
    }

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

    // 1. 创建 socket
    sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }

    signal(2, ClientQuit);

    // 1.1 填充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());

    pthread_t tid;
    ::pthread_create(&tid, nullptr, Recver, nullptr);

    // 1.2 启动的时候 给服务器推送消息即可
    const std::string online = " ... 来了哈!";
    int n = ::sendto(sockfd, online.c_str(), online.size(), 0, CONV(&server), sizeof(server));

    // 2. clientdone
    while (true)
    {
        fprintf(stdout, "Please Enter# ");
        fflush(stdout);
        std::string message;
        std::getline(std::cin, message);

        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
        (void)n;
    }

    return 0;
}

User.hpp

#pragma once

#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"

using namespace LogModule;
using namespace LockModule;

class UserInterface
{
public:
    virtual ~UserInterface() = default;
    virtual void SendTo(int sockfd, const std::string &message) = 0;
    virtual bool operator==(const InetAddr &u) = 0;
    virtual std::string Id() = 0;
};

class User : public UserInterface
{
public:
    User(const InetAddr &id)
        : _id(id)
    {
    }

    virtual void SendTo(int sockfd, const std::string &message) override
    {
        LOG(LogLevel::DEBUG) << "send message to " << _id.Addr() << " info:" << message;
        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());
        (void)n;
    }

    virtual bool operator==(const InetAddr &u) override
    {
        return _id == u;
    }

    virtual std::string Id() override
    {
        return _id.Addr();
    }

    ~User()
    {
    }

private:
    InetAddr _id;
};

// 对用户消息进行路由
// UserManager 把所有用户先管理起来!
// 观察者模式 : observer
class UserManager
{
public:
    UserManager()
    {
    }

    void AddUser(InetAddr &id)
    {
        LockGuard lockguard(_mutex);
        for (auto &user : _online_user)
        {
            if (*user == id)     // 父类的指针调用 必须重写 父类必须也重载 继承的话也得用子类的指针访问
            {
                LOG(LogLevel::INFO) << id.Addr() << " 用户已经存在";
                return;
            }
        }
        LOG(LogLevel::INFO) << " 新增该用户: " << id.Addr();
        _online_user.push_back(std::make_shared<User>(id));
        PrintUser();
    }

    void DelUser(InetAddr &id)
    {
        auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&id](std::shared_ptr<UserInterface> &user){ 
            return *user == id;
        });
        _online_user.erase(pos, _online_user.end());
        PrintUser();
    }

    // 通知观察者
    void Router(int sockfd, const std::string &message)
    {
        LockGuard lockguard(_mutex);
        for(auto &user : _online_user)
        {
            user->SendTo(sockfd, message);
        }
    }

    void PrintUser()
    {
        for(auto &user : _online_user)
        {
            LOG(LogLevel::DEBUG) << "在线用户-> " << user->Id();
        }
    }

    ~UserManager()
    {
    }

private:
    std::list<std::shared_ptr<UserInterface>> _online_user;

    Mutex _mutex;
};
  • UDP 协议支持全双工, 一个 sockfd, 既可以读取, 又可以写入, 对于客户端和服务端同样如此
  • 多线程客户端, 同时读取和写入
  • 测试的时候, 使用管道进行演示

在这里插入图片描述

二:🔥 观察者模式

观察者模式(发布订阅模式)是一种行为型设计模式,用于定义对象之间的一种一对多的依赖关系,使得一个对象状态发生变化时,所有依赖它的对象都会收到通知并自动更新。

它的目的就是将观察者和被观察者代码解耦,使得一个对象或者说事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。

📚 观察者模式的特点:

  1. 松耦合 : 观察者和被观察者之间是松耦合的,便于扩展和维护。
  2. 动态订阅 : 可以动态添加或移除观察者,灵活性高。
  3. 单向通信 : 被观察者通知观察者,观察者不能反向修改被观察者的状态。

📚 一般用在什么场景?

  1. 事件驱动系统:在用户操作界面中,通过监听事件(如按钮点击)触发响应。
  2. 系统间通信:系统中某个模块发生变化时,需要通知多个依赖模块。
  3. 分布式系统:数据更新时通知所有订阅者,例如推送通知、实时数据同步。

📚 典型场景:

  • GUI 事件处理系统(如按钮点击、窗口关闭事件)。
  • 数据模型与视图同步更新(如MVC架构中的数据绑定)。·股票价格更新通知订阅者。

在本篇博客中每一个user就是一个观察者,一旦收到了消息,就通知所有注册过的观察者。

三:🔥 补充参考内容

🦋 地址转换函数

本节只介绍基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr.sin_addr 表示 32 位 的 IP 地址
但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示 和 in_addr 表示之间转换;

  • 字符串转 in_addr 的函数:
    在这里插入图片描述
  • in_addr 转字符串的函数:
    在这里插入图片描述

🐮 其中 inet_ptoninet_ntop 不仅可以转换 IPv4 的 in_addr, 还可以转换 IPv6 的 in6_addr, 因此函数接口是 void *addrptr。

  • 📚 代码示例:
    在这里插入图片描述

🦋 关于 inet_ntoa

inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果. 那么是否需要调用者手动释放呢?
在这里插入图片描述

man 手册上说, inet_ntoa 函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

在这里插入图片描述
📚 运行结果如下:
在这里插入图片描述
因为 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数;
  • 但是在 centos7 上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 大家可以自己写程序验证一下在自己的机器上 inet_ntoa 是否会出现多线程的问题;
  • 在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

📚 多线程调用 inet_ntoa 代码示例如下:(大家自行去测试)

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

void* Func1(void* p) {
	struct sockaddr_in* addr = (struct sockaddr_in*)p;
	while (1) {
		char* ptr = inet_ntoa(addr->sin_addr);
		printf("addr1: %s\n", ptr);
	}
	return NULL;
} 

void* Func2(void* p) {
	struct sockaddr_in* addr = (struct sockaddr_in*)p;
	while (1) 
	{
		char* ptr = inet_ntoa(addr->sin_addr);
		printf("addr2: %s\n", ptr);
	}
	return NULL;
}

int main() 
{
	pthread_t tid1 = 0;
	struct sockaddr_in addr1;
	struct sockaddr_in addr2;
	addr1.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;
	pthread_create(&tid1, NULL, Func1, &addr1);
	pthread_t tid2 = 0;
	pthread_create(&tid2, NULL, Func2, &addr2);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	return 0;
}

四:🔥 补充网络命令

🦋 Ping 命令

Ping (Packet Internet Groper),因特网包探索器,用于测试网络连接量的程序。Ping 发送一个ICMP;回声请求消息给目的地并报告是否收到所希望的 ICMP echo (ICMP回声应答)。它是用来检查网络是否通畅或者网络连接速度的命令。

  • 📚 Ping 的原理:

🎯 向指定的网络地址发送一定长度的数据包,按照约定,若指定网络地址存在的话,会返回同样大小的数据包,当然,若在特定时间内没有返回,就是“超时”,会被认为指定的网络地址不存在。

🦋 netstat

netstat 是一个用来查看网络状态的重要工具.

📚 语法: netstat [选项]
📚 功能: 查看网络状态

✨ 常用选项:

  • n 拒绝显示别名, 能显示数字的全部转化成数字
  • l 仅列出有在 Listen (监听) 的服務状态
  • p 显示建立相关链接的程序名
  • t (tcp) 仅显示 tcp 相关选项
  • u (udp) 仅显示 udp 相关选项
  • a (all) 显示所有选项, 默认不显示 LISTEN 相关
// 每个 1s 执行一次 netstat -nltp
$ watch -n 1 netstat -nltp

🦋 pidof

✨ 在查看服务器的进程 id 时非常方便.

📚 语法: pidof [进程名]
📚 功能: 通过进程名, 查看进程 id

$ ps axj | head -1 && ps ajx | grep tcp_server
PPID     PID      PGID     SID      TTY    TPGID    STAT   UID    TIME   COMMAND
2958169  2958285  2958285  2958169  pts/2  2958285  S+     1002   0:00   ./tcp_server 8888 

$ pidof tcp_server
2958285

五:🔥 共勉

以上就是我对 【【Linux】Socket编程-UDP构建自己的C++服务器 的理解,想要完整代码可以私信博主噢!觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2269870.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式系统 第七讲 ARM-Linux内核

• 7.1 ARM-Linux内核简介 • 内核&#xff1a;是一个操作系统的核心。是基于硬件的第一层软件扩充&#xff0c; 提供操作系统的最基本的功能&#xff0c;是操作系统工作的基础&#xff0c;它负责管理系统的进程、内存、设备驱动程序、文件和网络系统&#xff0c; 决定着系统的…

[Qt] 信号和槽(1) | 本质 | 使用 | 自定义

目录 一、信号和槽概述 二、本质 底层实现 1. 函数间的相互调用 2. 类成员中的特殊角色 三、使用 四. 自定义信号和槽 1. 基本语法 (1) 自定义信号函数书写规范 (2) 自定义槽函数书写规范 (3) 发送信号 (4) 示例 A. 示例一 B. 示例二 —— 老师说“上课了”&…

2024 年发布的 Android AI 手机都有什么功能?

大家好&#xff0c;我是拭心。 2024 年是 AI 快速发展的一年&#xff0c;这一年 AI 再获诺贝尔奖&#xff0c;微软/苹果/谷歌等巨头纷纷拥抱 AI&#xff0c;多款强大的 AI 手机进入我们的生活。 今年全球 16% 的智能手机出货量为 AI 手机&#xff0c;到 2028 年&#xff0c;这…

Mac连接云服务器工具推荐

文章目录 前言步骤1. 下载2. 安装3. 常用插件安装4. 连接ssh测试5. 连接sftp测试注意&#xff1a;ssh和sftp的区别注意&#xff1a;不同文件传输的区别解决SSL自动退出 前言 Royal TSX是什么&#xff1a; Royal TSX 是一款跨平台的远程桌面和连接管理工具&#xff0c;专为 mac…

StarRocks 存算分离在得物的降本增效实践

编者荐语&#xff1a; 得物优化数据引擎布局&#xff0c;近期将 4000 核 ClickHouse 迁移至自建 StarRocks&#xff0c;成本降低 40%&#xff0c;查询耗时减半&#xff0c;集群稳定性显著提升。本文详解迁移实践与成果&#xff0c;文末附丁凯剑老师 StarRocks Summit Asia 2024…

【操作系统进程与线程管理:从PCB到多线程并发编程】

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 文章目录 操作系统管理进程PCB核心属性线程&多线程编程为什么线程比进程更轻量&#xff1f;为什么线程创…

超越TF-IDF:信息检索之BM25

深入解析BM25&#xff1a;信息检索的优化利器 搜索系列相关文章&#xff08;置顶&#xff09; 1.原始信息再加工&#xff1a;一文读懂倒排索引 2.慧眼识词&#xff1a;解析TF-IDF工作原理 3.超越TF-IDF&#xff1a;信息检索之BM25 4.深入浅出 Beam Search&#xff1a;自然语言处…

C#控件开发4—仪表盘

目录 思路&#xff08;GDI绘图&#xff09;1.定义属性2.绘制图形3.最后生成&#xff08;自定义各种监控值显示&#xff09;End 如何让温度、湿度、压力等有量程的监控值如仪表盘&#xff08;DashBoard&#xff09;一样显示&#xff1f; 思路&#xff08;GDI绘图&#xff09; 定…

提升口语发音水平,中英文发音评测系统实现

在全球化的浪潮中&#xff0c;语言不再是障碍&#xff0c;而是连接世界的桥梁。掌握一门流利的英语&#xff0c;意味着打开了通往世界的大门。但是&#xff0c;如何确保你的英语口语如同母语者一样自然流畅&#xff1f;这正是我们存在的意义。 我们的中英文口语发音评测服务&a…

JVM对象创建过程

1 类加载检查 jvm通过new指令开始创建对象jvm执行new指令时&#xff0c;首先通过指令参数从常量池中取到需要创建的类名检查该类是否被加载&#xff0c;解析&#xff0c;和初始化过如果没有&#xff0c;则执行类的加载过程new指令对应到java语言具体的操作为 new 关键字创建对象…

什么是Sight Words(信号词)

&#x1f9e1;什么是Sight Words&#xff08;信号词&#xff09; 简单来说&#xff0c;Sight Words就是我们在日常英语中常用的一些基本词汇。可以把它想象成是学练英语的“基础词汇”&#xff0c;这些词在各种考试中经常出现&#xff0c;也是在生活中必不可少的。 &#x1f…

秒鲨后端之MyBatis【3】自定义映射resultMap、动态SQL、MyBatis的缓存、MyBatis的逆向工程、分页插件(30000字)

这里我分享一下尚硅谷的pdf100页笔记和代码&#xff0c;大家可以参考学习。 笔记&#xff1a; 通过网盘分享的文件&#xff1a;MyBatis.pdf 链接: https://pan.baidu.com/s/14Iu1Zs-_5vZoRjBEjmagag?pwdyjh6 提取码: yjh6 --来自百度网盘超级会员v1的分享代码&#xff1a; …

12.31【Linux】shell脚本【运行方式,修改环境变量,数组】思维导图 内附练习

1.思维导图 2练习&#xff1a; 1.尝试将下列指令放到脚本中运行 在家目录下创建目录文件dir1&#xff0c;把/etc/passwd拷贝到dir1中&#xff0c;把/etc/group拷贝到dir1中并重命名为grp.txt&#xff0c;使用tree指令&#xff0c;显示dir1目录的文件树&#xff0c;把dir1&am…

云计算学习架构篇之HTTP协议、Nginx常用模块与Nginx服务实战

一.HTTP协议讲解 1.1rsync服务重构 bash 部署服务端: 1.安装服务 [rootbackup ~]# yum -y install rsync 2.配置服务 [rootbackup ~]# vim /etc/rsyncd.conf uid rsync gid rsync port 873 fake super yes use chroot no max connections 200 timeout 600 ignore erro…

【项目】智能BI洞察引擎 测试报告

目录 一、项目背景BI介绍问题分析项目背景 二、项目功能三、功能测试1、登录测试测试用例测试结果 2、注册测试测试用例测试结果出现的bug 3、上传文件测试测试用例测试结果 4、AI生成图表测试测试用例测试结果 5、分析数据页面测试&#xff08;异步&#xff09;测试用例测试结…

权限菜单之菜单管理 SpringBoot + VUE

一、 数据表设计 新建表sys_menu 表内数据 添加实体类Menu package com.example.demo.demos.web.demo.entity;import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.Ta…

【AIGC篇】AIGC 引擎:点燃创作自动化的未来之火

&#xff1a;羑悻的小杀马特.-CSDN博客 未来都是惊喜。你生来本应为高山。并非草芥。 引言&#xff1a; 在当今数字化的时代&#xff0c;人工智能生成内容&#xff08;AIGC&#xff09;正以一种前所未有的力量改变着我们的创作领域。它就像一个神秘而强大的魔法师&#xff0c;…

UnityRenderStreaming使用记录(三)

测试UnityRenderStreaming在Ubuntu24.04.1LTS上的表现 先放上运行图操作系统 Ubuntu24.04.1LTSUnity测试工程环境相关修改遇到的问题 先放上运行图 操作系统 Ubuntu24.04.1LTS 系统下载地址 https://cn.ubuntu.com/download/desktop安装UnityHub https://blog.csdn.net/AWNUXC…

从0开始的docker镜像制作-ubuntu22.04

从0开始的docker镜像制作-ubuntu22.04 一、拉取基础ubuntu22.04镜像二、进入拉取的docker镜像中&#xff0c;下载自己需要的安装包三、安装需要的系统软件四、打包现有镜像为一个新的镜像五、推送打包的镜像到私有docker服务器1.编辑docker文件&#xff0c;使其允许http传输和对…

多模态论文笔记——CogVLM和CogVLM2(副)

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍多模态模型的LoRA版本——CogVLM和CogVLM2。在SD 3中使用其作为captioner基准模型的原因和优势。 文章目录 CogVLM论文背景VLMs 的任务与挑战现有方法及…