Socket编程UDP

发布于:2025-04-06 ⋅ 阅读:(24) ⋅ 点赞:(0)

1、V1版本——EchoServer

在这里插入图片描述
首先给出EchoServer目录结构:服务器的类我们实现在UdpServer.hpp中,然后在UdpServerMain.cc中启动服务器。客户端相关代码我们就直接在UdpClientMain.cc中实现了,如果有兴趣后续你可以自己在UdpClient.hpp中封装,常用的部分我们放在Common.hpp中,然后将直接写的策略模式日志拿过来方便测试,最后使用make/makefile来自动化构建项目。


1、创建套接字:

在这里插入图片描述
在这里插入图片描述
使用socket创建套接字,第一个参数domain表示域或协议家族,AF_INET表示网络通信、AF_UNIX表示本地通信,我们直接使用AF_INET。
在这里插入图片描述
第二个参数type,我们只关注两个:SOCK_STREAM和SOCK_DGRAM。
SOCK_STREAM:提供有序的、可靠的,双向的,建立连接的,面向字节流,这个就是TCP通信。
SOCK_DGRAM:提供不可靠的,无连接的,面向数据报的,这个就是UDP通信。
今天我们写的是UDP通信,所以第二个参数传SOCK_DGRAM。第三个参数表示协议,可以传,不过通过前两个参数就可以确定了,所以我们默认传0就行。

在这里插入图片描述
成功返回文件描述符,失败返回-1错误码被设置。

我们实现一个UdpServer的类,创建套接字后会返回文件描述符,所以我们需要一个int类型的sockfd变量,同时之后还需要设置服务器的IP和端口,另外我们再加一个变量判断服务器是否启动。所以代码如下:先创建套接字

// Common.hpp
#pragma once

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

enum{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};
// UdpServer.hpp
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include "Common.hpp"
#include "Log.hpp"

using namespace LogModule;

const static int gsockfd = -1;
const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; 

class UdpServer
{
public:
    UdpServer(const std::string& ip = gdefaultip, const uint16_t port = gdefaultport)
    :_sockfd(gsockfd)
    ,_ip(ip)
    ,_port(port)
    ,_isrunning(false)
    {}


    void InitServer()
    {
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;
    }

    void Start()
    {
        
    }

    ~UdpServer()
    {

    }
private:
    int _sockfd;        // 创建socket返回的fd
    std::string _ip;    // 服务器IP
    uint16_t _port;     // 服务器端口号
    bool _isrunning;    // 服务是否启动
};

在这里插入图片描述
127.0.0.1表示本地环回地址,仅用于本地内部通信。本地环回地址(Loopback Address)是计算机网络中用于测试本地网络协议栈的保留IP地址。发送到该地址的数据包不会离开主机,而是直接返回给本地系统,常用于网络软件开发和故障排查。在构造函数中我们给了缺省值,默认端口号就是8080。

注意:云服务器需要添加防火墙规则,否则后面无法实现通信效果,以下分别是腾讯云和阿里云的添加示例:
在这里插入图片描述
在这里插入图片描述


2、填充网络信息并进行bind:

在这里插入图片描述
在这里插入图片描述
使用bind进行绑定,第一个参数sockfd就是调用socket返回的文件描述符,第二个参数const struct sockaddr*是输入型参数,我们需要在外面将一个struct sockaddr_in对象填充好传进去,第三个参数表示我们传入对象的大小。这个函数会将sockfd我们传入的struct sockaddr_in绑定。
bind成功返回0,失败返回-1错误码被设置。

而使用struct sockaddr_in这个结构我们需要包含两个头文件:
在这里插入图片描述
在这里插入图片描述
我们先看一下sockaddr_in这个结构,第一个成员变量是个宏,将sin_传过去,进行宏替换就变成了:sa_family_t sin_family表示协议家族。第二个sin_port表示的是端口号,实际上就是uint16_t,16位整数类型。第三个参数是一个结构体,里面是一个uint32_t的32位整数类型。

而进行网络通信需要保证网络字节序——大端。所以设置struct sockaddr_in结构体我们还需要以下接口:
在这里插入图片描述
htons表示主机转网络,h就是host,n就是network,s表示short16位整数,l表示long32位整数。网络转主机我们使用ntohs。

我们使用当前ip地址是字符串风格的ip地址,所以需要先将字符串风格ip地址转换成4字节的整数ip,然后再将4字节整数ip转换成网络字节序。
在这里插入图片描述
我们直接使用ient_addr函数,它可以帮助我将字符串风格的ip地址转换成四字节整数ip并且转换成网络字节序。

另外,在对struct sockaddr_in结构进行设置的时候,需要先将结构里的字段全部清0,我们使用bzero函数。
在这里插入图片描述
第一个参数我们把地址传进去,第二个参数表示该类型的大小。

下面实现设置网络信息并绑定:

// Common.hpp
#pragma once

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

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

enum{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};
// UdpServer.hpp
void InitServer()
{
    // 1.创建套接字
    _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
        Die(SOCKET_ERR);
    }
    LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;

    // 2.填充网络信息并绑定
    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());

    int n = ::bind(_sockfd, CONV(&local), sizeof(local));
    if (n < 0)
    {
        LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
        Die(BIND_ERR);
    }
    LOG(LogLevel::INFO) << "bind success";
}
// UdpServerMain.cc
#include "UdpServer.hpp"
#include <memory>


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

在这里插入图片描述


3、实现Start,服务器接受客户端消息,同时把接受到的消息返回给客户端。

UDP是全双工的,既可以收又可以发。
在这里插入图片描述
在这里插入图片描述
使用recvfrom接受客户端发送过来的数据,sockfd表示创建socket的返回值文件描述符,buf表示要将数据存储到哪个数组,len表示数组大小,flags设置为0表示阻塞发送。
我们也要知道是谁给服务器发送消息,所以src_addr是输出型参数,通过这个输出型参数可以将服务器的IP和端口号带出来,addrlen表示src_addr结构的大小的地址。
成功返回接收到的字节数,失败返回-1错误码被设置。

在这里插入图片描述
在这里插入图片描述
发送数据使用sendto函数,第一个参数就是创建socket的文件描述符,buf表示要发送的缓冲区数组,len表示发送的数据大小,flags设置为0表示阻塞式发送,dest_addr表示发送目标主机的信息,addrlen表示dest_addr结构的大小。
调用成功返回发送的字节数,失败返回-1错误码被设置。

我们Start实现的逻辑就是服务器接收到数据后,将数据输出到显示器上,同时把IP和端口号都显示出来,然后再给客户端将数据发送回去。
由于要输出客户端的IP和端口号,所以需要将网络字节序转主机序列。端口号转换我们直接使用ntohs函数,IP地址转换我们使用下面这个函数:

在这里插入图片描述

// UdpServer.hpp
#pragma once

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

using namespace LogModule;

const static int gsockfd = -1;
const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; 

class UdpServer
{
public:
    UdpServer(const std::string& ip = gdefaultip, const uint16_t port = gdefaultport)
    :_sockfd(gsockfd)
    ,_ip(ip)
    ,_port(port)
    ,_isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;

        // 2.填充网络信息并绑定
        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());

        int n = ::bind(_sockfd, CONV(&local), sizeof(local));
        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 clientip = ::inet_ntoa(peer.sin_addr);
                uint16_t clientport = ::ntohs(peer.sin_port);
                LOG(LogLevel::INFO) << clientip << ":" << clientport << "# " << inbuffer;
            }
            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;        // 创建socket返回的fd
    std::string _ip;    // 服务器IP
    uint16_t _port;     // 服务器端口号
    bool _isrunning;    // 服务是否启动
};

4、实现客户端:

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

// ./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.创建套接字
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }
    // 2.填充服务器信息
    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 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 tmp;
        socklen_t len = sizeof(tmp);
        char buffer[1024];
        n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, CONV(&tmp), &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

再来谈细节:
在这里插入图片描述
我们希望客户端启动的方式如mian函数上方的注释,通过命令行参数将服务器ip和端口带进来。

客户端也必须有自己的ip和port,但是客户端不需要自己显示的调用bind。而是在客户端首次调用sendto发送消息的时候由操作系统进行bind。
1、如何理解client自动随机bind端口号?假设如今有两个客户端抖音和淘宝,这两个客户端都显示绑定4000端口号,那么这样就会导致其中一个客户端进程无法启动,因为一个端口号只能被一个进程绑定,而一个进程可以绑定多个端口号。
2、如何理解server要显示的bind?因为服务器的端口号必须稳定,必须是众所周知的且不能随意改变。

接下来编译分别运行服务端和客户端程序:

在这里插入图片描述
右侧我们使用netstat -nau查看启动的udp通信,可以看到有一个本地环回地址和端口号为8080的服务,然后远端地址为全0表示可以接受来自所有ip地址的客户端消息。
可以看到本地客户端和服务器的通信就实现了。


但是我们不是要进行网络通信吗?所以下面我们将服务器程序改为类似客户端的启动方式:

// UdpServerMain.cc
#include "UdpServer.hpp"
#include <memory>

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

    ENABLE_CONSOLE_LOG();

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

再次运行:
在这里插入图片描述
绑定失败,这是因为云服务器禁止绑定公网IP,而虚拟机可以绑定你的任何IP。
之所以这样是因为云服务器一般会有服务在跑,一台主机可能有两张网卡,那就有多个IP地址,如果只绑定了一个IP地址,那么该进程就只能收到这个IP地址的报文,但是客户端可能通过多个IP地址同一个端口号发送数据。

所以服务器并不需要IP地址,那么填充网络信息的时候就可以这么写:
在这里插入图片描述
直接将sin_addr.s_addr设置为INADDR_ANY,INADDR_ANY本质就是0。这样表示以后不管客户端发送给哪个IP地址,只要是这个端口号的那都会接受。

下面对UdpServer.hpp和UdpServerMain.cc进行修改:

// UdpServer.hpp
#pragma once

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

using namespace LogModule;

const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; 

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

    void InitServer()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;

        // 2.填充网络信息并绑定
        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());
        local.sin_addr.s_addr = INADDR_ANY;

        int n = ::bind(_sockfd, CONV(&local), sizeof(local));
        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 clientip = ::inet_ntoa(peer.sin_addr);
                uint16_t clientport = ::ntohs(peer.sin_port);
                LOG(LogLevel::INFO) << clientip << ":" << clientport << "# " << inbuffer;
                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;        // 创建socket返回的fd
    // std::string _ip;    // 服务器IP
    uint16_t _port;     // 服务器端口号
    bool _isrunning;    // 服务是否启动
};
// UdpServerMain.cc
#include "UdpServer.hpp"
#include <memory>

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

    ENABLE_CONSOLE_LOG();

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

运行结果:
在这里插入图片描述

下面我使用另一台机器进行测试:
在这里插入图片描述
如图,实现了两台不同主机进行通信。


封装实现InetAddr,快速实现主机转网络:

// InetAddr.hpp
#pragma once

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

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

    void IpNet2Host()
    {
        char ipbuffer[64];
        _ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
    }

public:
    InetAddr()
    {}

    InetAddr(const sockaddr_in& addr)
    :_net_addr(addr)
    {
        PortNet2Host();
        IpNet2Host();
    }

    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;
};
// UdpServer.hpp
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; 

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

    void InitServer()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;

        // 2.填充网络信息并绑定
        // 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());
        // local.sin_addr.s_addr = INADDR_ANY;

        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 clientip = ::inet_ntoa(peer.sin_addr);
                // uint16_t clientport = ::ntohs(peer.sin_port);
                InetAddr cli(peer);

                std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;
                LOG(LogLevel::INFO) << clientinfo;
                
                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;        // 创建socket返回的fd
    InetAddr _addr;     
    // std::string _ip;    // 服务器IP
    // uint16_t _port;     // 服务器端口号
    bool _isrunning;    // 服务是否启动
};

在这里插入图片描述

在这里插入图片描述
上面我们网络转主机IP使用的inet_ntop,前面使用的inet_ntoa返回值是静态数组,所以如果下次再调用就会把里面的值改了,不是线程安全的。所以我们使用inet_ntop自己传入一个缓冲区,inet_ntop会将转换后的字符串IP放在传入的dst数组里面。


2、网络命令

2.1、ping

有时候连不上服务器可能就是你的网络有问题,或者有时候服务器启动了,却接受不到客户端的数据也有可能是网络的问题。所以我们可以使用ping命令来测试网络是否联通:
在这里插入图片描述
出现以上信息说明网络联通,但是这样需要ctrl c终止掉。

可以给ping命令带-c选项,可以指定次数,比如指定3次:
在这里插入图片描述




2.2、netstat

netstat是一个用来查看网络状态的重要工具。
语法:netstat [选项]
功能:查看网络状态。

1、带-u选项表示显示udp相关选项。
2、带-a选项表示显示所有选项,默认不显示LESTEN相关。

在这里插入图片描述

3、带-p选项表示显示建立相关连接的程序名
在这里插入图片描述
其他的看不到是因为其他可能是系统起的,也可能是root账户起的,如果想看可以使用sudo提权。

4、带-n选项表示拒绝显示别名,能显示数字的全部转换为数字。
在这里插入图片描述

5、带-t选项表示仅显示tcp相关选项。
6、带-l表示仅显示有在Listen的服务。

在这里插入图片描述

使用watch命令每秒执行一次netstat -tlnp:
在这里插入图片描述


2.3、pidof

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

在这里插入图片描述

在这里插入图片描述
使用上面这行命令可以杀掉该进程。
分析:使用pidof获取server_udp的进程PID,然后使用管道,因此kill -9的标准输入重定向成文件,现在kill -9从文件里面读。但是由于进程PID需要跟在kill -9后面,所以使用xargs,xargs表示将结果拼接到kill -9后面。


3、验证UDP——Windows作为client访问Linux

#include <iostream>
#include <string>
#include <cstring>
#include <WinSock2.h>
#include <Windows.h>

#pragma warning(disable : 4996)     // 去除使用inet_addr的警告
#pragma comment(lib, "ws2_32.lib")  // 指定要链接的库

std::string serverip = "47.117.157.14";  // 服务器IP
uint16_t serverport = 8080;				 // 服务器端口号

int main()
{
	WSADATA wsd;  // 定义winsock初始化信息结构体
	WSAStartup(MAKEWORD(2, 2), &wsd);  // 初始化winsock库

	SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd == SOCKET_ERROR)
	{
		std::cout << "socket error" << std::endl;
		return 1;
	}

	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 message;
		std::getline(std::cin, message);
		sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr*)&server, sizeof(server));
		
		struct sockaddr_in tmp;
		int len = sizeof(tmp);
		char buffer[1024];
		int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &len);
		if (n > 0)
		{
			buffer[n] = 0;
			std::string ip = inet_ntoa(tmp.sin_addr);
			uint16_t port = ntohs(tmp.sin_port);
			std::cout << "[" << ip << ":" << port << "]" << buffer << std::endl;
		}
	}
	closesocket(sockfd);
	WSACleanup(); // 清理并释放winsock资源
	return 0;
}

这里有一份代码,在windows下使用vs实现的一个udp客户端。除了小部分代码不同以外,其他创建socket发送接受的代码都是一样的。

在这里插入图片描述
如图:实现了windows作为客户端访问远程服务器。由于vs和Linux编码不同的问题,如果输入中文就会乱码。


4、V2版本——DictServer

现在我们要实现一个英汉词典,相当于是客户端发送英文单词,服务器接受后查询词典是否有该单词的中文意思,如果有就获取返回给客户端,如果没有就给客户端返回None。
在这里插入图片描述
我首先在该目录下创建了一个dict.txt文件,里面存储了英文和中文的映射信息。

然后我们要实现Dictionary.hpp,Dictionary.hpp里面实现一个Dictionary类,该类存储了英文到中文的映射信息,构造函数打开dict.txt文件读取数据将英文和中文切分出来,然后通过哈希表建立映射。并实现一个翻译接口,传入英文单词查询中文意思返回。

下面是Dictionary.hpp的实现:

#pragma once

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

using namespace LogModule;

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

class Dictionary
{
    bool LoadDictionary()
    {
        std::string file = _path + _filename;
        std::ifstream in(file); // 默认打开以只读打开
        if (!in.is_open())
        {
            LOG(LogLevel::FATAL) << "open file " << file << "error";
            return false;
        }
        std::string line;
        while (std::getline(in, line))  // getline重载了operator bool
        {
            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 = gfilename)
    :_path(path)
    ,_filename(filename)
    {
        LoadDictionary();
        // Print();
    }

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

    void Print()
    {
        for (const auto& iter : _dictionary)
        {
            std::cout << iter.first << ":" << iter.second << std::endl;
        }
    }

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

字符串切分函数在Common.hpp中实现:

bool SplitString(const std::string& line, std::string* key, std::string* value, const std::string& sep)
{
    auto pos = line.find(sep);
    if (pos == std::string::npos) return false;
    *key = line.substr(0, pos);
    *value = line.substr(pos + sep.size());
    return true;
}

在UdpServer中添加回调函数:

#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <functional>
#include <memory>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

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

const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; 

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

    void InitServer()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;

        // 2.填充网络信息并绑定
        // 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());
        // local.sin_addr.s_addr = INADDR_ANY;

        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 clientip = ::inet_ntoa(peer.sin_addr);
                // uint16_t clientport = ::ntohs(peer.sin_port);
                InetAddr cli(peer);

                std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;
                LOG(LogLevel::INFO) << clientinfo;

                std::string echo_string = _func(inbuffer);
                
                // 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;        // 创建socket返回的fd
    InetAddr _addr;     
    // std::string _ip;    // 服务器IP
    // uint16_t _port;     // 服务器端口号
    bool _isrunning;    // 服务是否启动

    func_t _func;
};

在UdpServerMain.cc中引入Dictionary.hpp头文件,并创建Dictionary对象,将回调函数通过bind或者lambda传入UdpServer中:

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

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

    ENABLE_CONSOLE_LOG();

    std::shared_ptr<Dictionary> dict_sptr = std::make_shared<Dictionary>();
    // std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(std::bind(&Dictionary::Translate, 
    //     dict_sptr.get(), std::placeholders::_1), port);

    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_sptr](const std::string& word){
        return dict_sptr->Translate(word);
    }, port);

    svr_uptr->InitServer();
    svr_uptr->Start();
    return 0;
}

程序运行结果:
在这里插入图片描述
如图实现了单词的翻译,使用vs是因为编码不同导致的乱码。

在这里插入图片描述
如图使用bind或者lambda,在UdpServer构造函数对成员变量_func初始化,然后在接收到客户端数据后回调_func函数,并将客户端输入的英文单词传入,这时候就会去调用Dictionary里的Translate函数,然后返回中文意思,再将该字符串发送给客户端。


5、V3版本——简单聊天室

在这里插入图片描述
上面我们实现的是单进程,如果实现多人聊天的话需要将所有人IP保存起来,然后在客户端发送数据服务器接收到之后,需要转发给所有IP,但是如果是单进程来做就比较难受。如果创建子进程呢?创建子进程如果父进程收到数据要转给子进程需要实现进程间通信,有点麻烦。其实我们可以实现一个消息转发模块,服务端收到用户消息就注册到转发模块中,然后再交给线程池去处理。

在这里插入图片描述


相较于V1版本,我们需要引入之前封装的线程池,而线程池又用了互斥量、条件变量、线程模块,所以都需要引入。然后自己实现一个用户管理模块user.hpp。

// User.hpp
#pragma once

#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.hpp"
#include "Log.hpp"

using namespace LogModule;

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

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

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

    bool operator==(const InetAddr& id)
    {
        return _id == id;
    }

    ~User()
    {}
private:
    InetAddr _id;
};


// 用户管理
// 观察者模式observer
class UserManager
{
public:
    UserManager(){}

    void AddUser(InetAddr& id)
    {
        for (auto& user_sptr : _online_user)
        {
            if (*user_sptr == id)
            {
                LOG(LogLevel::INFO) << "用户已经存在";
                return;
            }
        }
         LOG(LogLevel::INFO) << "新增该用户: " << id.Addr();
        _online_user.push_back(std::make_shared<User>(id));
    }

    void DelUser(InetAddr& id)
    {

    }

    void Router(int sockfd, const std::string& message)
    {
        for (auto& user_sptr : _online_user)
        {
            user_sptr->SendTo(sockfd, message);
        }
    }

    ~UserManager(){}
private:
    std::list<std::shared_ptr<UserInterface>> _online_user;
};

首先实现了添加用户和消息转发的功能,这种设计模式为观察者模式,User添加到用户管理中,所有的用户就相当于一个观察者,一旦未来有某种事情发生了,通过router通知用户。

由于添加用户需要判断用户是否存在,遍历链表进行判断,所以抽象类和User类都需要实现operator==重载。

在这里插入图片描述
由于User类重写的虚函数SendTo在发送消息前打印了日志信息,所以InetAddr还需实现一个Addr函数,将用户IP+端口号拼接起来并返回。


在UdpServer中加入回调函数:

#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <functional>
#include <memory>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; 

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

class UdpServer
{
public:
    UdpServer(adduser_t adduser, router_t router, const uint16_t port = gdefaultport)
    :_sockfd(gsockfd)
    ,_addr(port)
    ,_isrunning(false)
    ,_adduser(adduser)
    ,_router(router)
    {}

    void InitServer()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;

        // 2.填充网络信息并绑定
        // 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());
        // local.sin_addr.s_addr = INADDR_ANY;

        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 clientip = ::inet_ntoa(peer.sin_addr);
                // uint16_t clientport = ::ntohs(peer.sin_port);
                InetAddr cli(peer);

                // 添加用户
                _adduser(cli);

                std::string clientinfo = "[" + cli.Addr() + "]" + inbuffer;
                // LOG(LogLevel::INFO) << clientinfo;
                ThreadPool<task_t>::GetInstance()->Equeue(std::bind(_router, _sockfd, clientinfo));

                
                // 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;        // 创建socket返回的fd
    InetAddr _addr;     
    // std::string _ip;    // 服务器IP
    // uint16_t _port;     // 服务器端口号
    bool _isrunning;    // 服务是否启动

    adduser_t _adduser;
    router_t _router;
};
#include "UdpServer.hpp"
#include "User.hpp"


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

    ENABLE_CONSOLE_LOG();

    // 用户管理模块
    std::unique_ptr<UserManager> um = std::make_unique<UserManager>();

    // 网络模块
    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(
        [&um](const InetAddr& id){ um->AddUser(id); },
        [&um](int sockfd, const std::string& message){ um->Router(sockfd, message); },
        port
    );


    svr_uptr->InitServer();
    svr_uptr->Start();
    return 0;
}

在这里插入图片描述

在UdpServerMain.cc中引入用户管理模块,创建智能指针对象,通过lambda表达式传入UdpServer的构造函数初始化_adduser和_router,在Start函数内部当接收到客户端发来的消息回调_adduser并传入InetAddr对象,里面会判断用户是否已经存在,如果存在就不会添加,不存在就就会添加。然后使用bind再将回调函数_router加入任务队列中,线程就会从任务队列中取任务,然后执行回调方法将消息转发给所有用户。
在这里插入图片描述
运行程序我们发现消息确实可以发送给所有用户了,但是有个问题,我们客户端循环内是先获取用户输入,然后再发送,发送了数据才能接受到客户端转发的数据。因此,我们需要让客户端创建一个新线程来接受服务器发送的数据,然后主线程获取用户输入数据发送给服务器。


下面修改客户端创建新线程来接受服务器发送的数据:

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

int sockfd;
struct sockaddr_in server;

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

// ./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.创建套接字
    sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }
    // 2.填充服务器信息
    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);

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

    pthread_join(tid, nullptr);
    return 0;
}

基于以上实现,现在已经可以进行多个人通信了。但是还有些细节:
1、客户端启动后,需要先获取用户输入的数据然后给服务器发送消息,这样服务器才会将用户信息注册到用户管理模块中,才能接收到其他人发的消息。所以我们应该在启动之后直接让客户端给服务器发送一个消息,这样服务器添加到用户管理模块中,这样哪怕用户不输入也能接收到其他人的消息。
2、我们让客户端ctrl c退出,客户端退出后服务器的用户管理模块中应该将用户信息从用户管理模块中删除掉,也就是上方的DelUser我们还没实现。我们可以对2号信号进行捕捉,然后执行自定义动作,给服务器发送QUIT信息,服务器对接收到的信息进行判断,如果是QUIT说明客户端要退出了将用户信息从管理模块中移除。
3、用户管理模块添加用户、删除用户、遍历所有用户发送消息等是多线程访问的,所以需要加锁保护,我们引入互斥锁封装Mutex.hpp保护。

// User.hpp
#pragma once

#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.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&) = 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::INFO) << "send message to " << "[" << _id.Addr() << "]" << " info: " << message;
        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());
        (void)n;
    }

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

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

    ~User()
    {}
private:
    InetAddr _id;
};


// 用户管理
// 观察者模式observer
class UserManager
{
public:
    UserManager(){}

    void AddUser(InetAddr& id)
    {
        LockGuard lockguard(_mutex);
        for (auto& user_sptr : _online_user)
        {
            if (*user_sptr == id)
            {
                LOG(LogLevel::INFO) << "用户已经存在";
                return;
            }
        }
        LOG(LogLevel::INFO) << "新增该用户: " << id.Addr();
        _online_user.push_back(std::make_shared<User>(id));
        PrintUser();
    }

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

    void Router(int sockfd, const std::string& message)
    {
        LockGuard lockguard(_mutex);
        for (auto& user_sptr : _online_user)
        {
            user_sptr->SendTo(sockfd, message);
        }
    }

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

    ~UserManager(){}
private:
    std::list<std::shared_ptr<UserInterface>> _online_user;
    Mutex _mutex;
};
// UdpClientMain.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
#include "Common.hpp"

int sockfd;
struct sockaddr_in server;

void handler(int signo)
{
    std::string message = "QUIT";
    int n = ::sendto(sockfd, message.c_str(), message.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 buff[1024];
        int n = ::recvfrom(sockfd, buff, sizeof(buff)-1, 0, CONV(&temp), &len);
        if (n > 0)
        {
            buff[n] = 0;
            std::cout << buff << std::endl;
        }
    }
    return nullptr;
}

// ./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);
    }
    signal(2, handler);
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1.创建套接字
    sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }
    // 2.填充服务器信息
    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);

    std::string msg = "我来了哈!";
    ::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));

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

    pthread_join(tid, nullptr);
    return 0;
}
// UdpServer.hpp
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <functional>
#include <memory>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

const static int gsockfd = -1;
// const std::string gdefaultip = "127.0.0.1"; // 本地环回地址
const uint16_t gdefaultport = 8080; 

using adduser_t = std::function<void(InetAddr&)>;
using deluser_t = std::function<void(InetAddr&)>;
using router_t = std::function<void(int sockfd, const std::string& message)>;
using task_t = std::function<void()>;

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

    void InitServer()
    {
        // 1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is: " << _sockfd;

        // 2.填充网络信息并绑定
        // 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());
        // local.sin_addr.s_addr = INADDR_ANY;

        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, deluser_t deluser, router_t router)
    {
        _adduser = adduser;
        _deluser = deluser;
        _router = router;
    }

    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 clientip = ::inet_ntoa(peer.sin_addr);
                // uint16_t clientport = ::ntohs(peer.sin_port);
                InetAddr cli(peer);
                std::string clientinfo;
                if (strcmp(inbuffer, "QUIT") == 0)
                {
                    _deluser(cli);
                    clientinfo = "[" + cli.Addr() + "]" + "我走了,你们聊!";
                }
                else
                {
                    // 添加用户
                    _adduser(cli);

                    clientinfo = "[" + cli.Addr() + "]" + inbuffer;
                    // LOG(LogLevel::INFO) << clientinfo;
                }

                ThreadPool<task_t>::GetInstance()->Equeue(std::bind(_router, _sockfd, clientinfo));
                // 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;        // 创建socket返回的fd
    InetAddr _addr;     
    // std::string _ip;    // 服务器IP
    // uint16_t _port;     // 服务器端口号
    bool _isrunning;    // 服务是否启动

    adduser_t _adduser;
    deluser_t _deluser;
    router_t _router;
};
// UdpServerMain.cc
#include "UdpServer.hpp"
#include "User.hpp"


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

    ENABLE_CONSOLE_LOG();

    // 用户管理模块
    std::unique_ptr<UserManager> um = std::make_unique<UserManager>();

    // 网络模块
    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);
    
    svr_uptr->RegisterService(
        [&um](InetAddr& id){ um->AddUser(id); },
        [&um](InetAddr& id){ um->DelUser(id); },
        [&um](int sockfd, const std::string& message){ um->Router(sockfd, message); }
    );

    svr_uptr->InitServer();
    svr_uptr->Start();
    return 0;
}

最终效果:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


最后,我们的UdpServer一般是不让拷贝的,所以可以实现一个nocopy类,让UdpServer继承该类即可。
在这里插入图片描述


网站公告

今日签到

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