前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux】:Socket编程UDP(EchoServer(聊天)| DictServer(中译英字典)| ChatServer(简单聊天室))

【Linux】:Socket编程UDP(EchoServer(聊天)| DictServer(中译英字典)| ChatServer(简单聊天室))

作者头像
IsLand1314
发布2025-01-17 16:00:28
发布2025-01-17 16:00:28
6400
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

1. 前言

在上篇文章 【Linux】: Socket 编程 里面已经关于 socket 网络编程的前置知识,这里我们就来实际运用一下其 套接字 来实现相关的套接字编程吧

老样子,先写 Makefile 文件,如下:

代码语言:javascript
代码运行次数:0
复制
.PHONY:all
all:server_udp client_udp

server_udp:UdpServerMain.cc
	g++ -o $@ $^ -std=c++17 -lpthread
client_udp:UdpClientMain.cc
	g++ -o $@ $^ -std=c++17 -lpthread

.PHONY:clean 
clean:
	rm -f server_udp client_udp

先写出我们要实现的大概框架,将之前我们实现的 日志封装 Log.hpp【Linux】:日志策略 + 线程池(单例模式) 以及 互斥量封装【【Linux】:多线程(互斥 && 同步)Mutex.hpp 文件复制粘贴到当前文件夹下,基本运行框架如下:

2. 代码框架

UdpServerMain.cc

代码语言:javascript
代码运行次数:0
复制
#include "UdpServer.hpp"

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

    return 0;
}

然后再编写一个头文件,为了后面的编程方便和美观,就将其另开一个头文件

Common.hpp

代码语言:javascript
代码运行次数:0
复制
#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
};

分析如下:

这里我们定义了基本的错误码、一个终止程序的宏 Die 和一个类型转换宏 CONV,主要用于网络编程(套接字编程),通过宏来简化错误处理和类型转换的代码

3. 基本实现 -- EchoServer

🍉 UdpServer.hpp
代码语言:javascript
代码运行次数:0
复制
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__


#include <iostream>
#include <string>
#include <memory>
#include <cstring>

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

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

using namespace LogModule;

const static int gsockfd = -1;
const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
// 这里的测试端口号用 8080,这里端口号可以随机,但是 1024 以内的不行,1024 以上包括 1024 是可以的,3306 也不大行,因为 3306 是 mySQL 端口号,尽量不要和系统端口号冲突,因此我们可以用 8080 8081 没有人用的端口号
const static uint16_t gdefaultport = 8080; 

class UdpServer
{
public:
    UdpServer(const std::string &ip = gdefaultip, uint16_t port = gdefaultport)
    : _sockfd(gsockfd),
      _ip(ip),
      _port(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; // 测试
        
        // 2. 填充网络信息,并且进行绑定
        // 2.1 这里没有把 socket信息设置进入内核,只是填充了结构体
        struct sockaddr_in local;
        // 因为 sockaddr_in 开始里面有填充字段,因此我们要清0
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        
        local.sin_port = ::htons(_port); // 要被发送给对方,既要发到网络中,把端口号由 主机序列 转成 网络序列(htons)
        // 两步骤: 1. string ip -> 4 bytes  2. 4 bytes -> network order 
        // 这里直接赋值会报错,是因为 C 语言中结果体只能整体初始化,而不能被赋值,因此我们只需给 结构体 的某个特定数据赋值就行
        local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); 

        // 左手文件信息(1) 右手网络信息(2),将两者结合得到 绑定信息
        // 2.2 bind: 设置进入内核中
        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]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
            if(n > 0)
            {
                // 1. 消息内容 && 2. who 发 me 的
                inbuffer[n] = 0;
                LOG(LogLevel::DEBUG) << "client say@" << inbuffer; 

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

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

    ~UdpServer()
    {}
private:
    int _sockfd;
    uint16_t _port; // 服务器未来的端口号 38m-9s
    std::string _ip; // 服务器对应的 ip-TODO 地址
    bool _isrunning; // 服务器运行状态
};

#endif
🥝 代码片段分析:
  • bind 绑定 函数如下:

  • sockaddr_in 结构体 如下,其头文件有 <netinet/in.h><arpa/inet.h>

  • 在 InitServer 中,我们用到了 bzero 函数,来对数据进行清 0

在将 主机 IP 和 主机端口号 转化为 网络序列 时候,我们需要了解一些相关函数,而且也会遇到一些问题,如下:

  • 上面函数可以把 IP 地址可以以字符串或整数形式存在
  • 由于 socket fd 既可以发,也可以收 -- 全双工,因此我们要用两个函数来接收和传递信息,如下:

全双工模式:允许数据同时在两个方向上传输,要求发送和接收色好吧都有独立的接收和发送能力

🥝 InitServer() 方法
  • 用于初始化服务器,包括:
    1. 创建套接字:通过 ::socket(AF_INET, SOCK_DGRAM, 0) 创建一个 UDP 套接字。
    2. 绑定端口:填写 sockaddr_in 结构体并调用 ::bind 将套接字绑定到指定的 IP 和端口。
      • local.sin_family = AF_INET:设置协议族为 IPv4。
      • local.sin_port = ::htons(_port):将端口号转换为网络字节序。
      • local.sin_addr.s_addr = ::inet_addr(_ip.c_str()):将 IP 地址字符串转换为网络字节序。
      • ::bind 将本地地址与套接字关联起来,CONV(&local)local 结构体转换为 sockaddr* 类型。
🥝 Start() 方法
  • 启动服务器并进入一个无限循环,等待并处理客户端请求:
    1. 接收数据:通过 ::recvfrom 接收来自客户端的数据。inbuffer 用于存储接收到的数据,peer 用于存储客户端的地址信息。
    2. 处理数据:接收到数据后,打印接收到的消息并构造一个回显字符串。
    3. 发送数据:通过 ::sendto 将回显字符串发送回客户端。

此时我们的代码实现的差不多了,运行如下:

  • 套接字创建成功
  • 绑定成功
  • 说明我们上面代码运行暂时无误了

为了证明这个服务器是成功跑了起来,查看在 Linux 中为网络服务是否启动 -- netstat -uap

🔥 现在我们就可以知道我们的服务已经跑起来了,其实现在我们也可以知道 网络服务 就是一个进程,或者进程池,或者 一堆进程 / 进程池。

📕 现在我们就可以来实现我们的客户端了,服务器是个被动的,其必须得知道目标服务器的 IP 和 端口号,我们就可以通过 main(argc, argv[]) 来拿到,因此下面我们实现了一个简单的 UDP 客户端,能够向指定的服务器发送消息并接收回显

🍉 UdpClientMain.cc
代码语言:javascript
代码运行次数:0
复制
#include "UdpClient.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Common.hpp"

// 客户端 访问 目标服务器
// ./client_udp sereverip 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 端口号   
            // OS 不允许一个端口号绑定多个进程,必须要有唯一性,但是 一个进程可以绑定 多个端口号  
            // 保证当前端口号唯一性,和别人不冲突就行
        // 2.如何理解 server 要显示 bind
            // 服务器的端口号必须稳定
            // 而且必须是众所周知而且不能轻易改变的

        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[64];
        n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&tmp), &len);

        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}
🍒 代码片段分析:

1. 命令行参数检查

代码语言:javascript
代码运行次数:0
复制
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]);
  • 这部分代码检查命令行参数是否正确。如果参数不足或不正确,打印错误信息并终止程序。正确的用法是传入目标服务器的 IP 地址和端口号。
  • argv[1] 为服务器的 IP 地址,argv[2] 为服务器的端口号。

2. 创建 UDP 套接字

代码语言:javascript
代码运行次数:0
复制
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
    std::cerr << "socket error" << std::endl;
    Die(SOCKET_ERR);
}
  • 创建一个 UDP 套接字,使用 AF_INET(IPv4)、SOCK_DGRAM(UDP)协议。
  • 如果创建失败(sockfd 小于 0),则输出错误信息并终止程序。

3. 填充服务器信息

代码语言:javascript
代码运行次数:0
复制
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());
  • 填充 sockaddr_in 结构体,用来指定目标服务器的地址和端口信息。
    • server.sin_family = AF_INET; 设置为 IPv4 地址族。
    • server.sin_port = htons(serverport); 将端口号转换为网络字节序(大端序)。
    • server.sin_addr.s_addr = ::inet_addr(serverip.c_str()); 将服务器的 IP 地址转换为网络字节序的 in_addr

4. 发送消息到服务器

代码语言:javascript
代码运行次数:0
复制
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;
  • 客户端提示用户输入消息,并将用户输入的消息通过 ::sendto 发送到服务器。
    • sockfd 为套接字文件描述符。
    • message.c_str() 获取字符串的 C 风格字符指针。
    • sizeof(server) 表示服务器地址的大小。
  • sendto 函数会将数据包发送到目标服务器。在这里,客户端并没有显式绑定本地端口,操作系统会自动为客户端分配一个临时的端口进行发送。

5. 接收服务器的回显

代码语言:javascript
代码运行次数:0
复制
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[64];
n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&tmp), &len);

if(n > 0)
{
    buffer[n] = 0;
    std::cout << buffer << std::endl;
}
  • 使用 recvfrom 接收来自服务器的回显数据。
    • buffer 存储接收到的消息。
    • tmp 用于存储服务器地址信息,len 用于获取地址的大小。
    • recvfrom 会阻塞,直到从服务器接收到数据。
    • 接收到的数据后,将其打印出来。
🍒 注意:

客户端自动绑定端口

  • 客户端无需显式调用 bind,操作系统会在首次调用 sendto 时为客户端自动分配一个临时端口。操作系统会确保每个进程的端口号是唯一的,并避免端口冲突。

服务器端口和客户端端口的区别

  • 服务器端口:必须是固定的,且通常是众所周知的(例如 HTTP 使用 80 端口)。服务器需要显式地绑定端口,以确保能够接受客户端的连接。
  • 客户端端口:不需要显式绑定,操作系统会为每次连接自动分配一个临时端口。

结果如下:

通过 netstat -nuap 指令查询如下:

我们这不仅仅打印相关消息,而且也要知道是谁发的信息,因此我们需要 去修改 UdpServer.hpp 文件,如下:

🍉 UdpServer.hpp 修改 -- 打印谁发的信息

打印结果如下:

我们这应该要进行网络通信啊,而不是仅仅局限于本地通信

🍉 UdpServerMain.cc 修改 -- 网络通信
代码语言:javascript
代码运行次数:0
复制
#include "UdpServer.hpp"

// ./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 的信息。

因此我们这里代码书写还是有问题的,在正常的服务器其实是不用 IP 的,只需要端口,在填充的时候,也就需要做出相应修改

  • 表示服务器可以接收任何信息,不管是来自哪个 IP 发给 这个主机上的,一律接收,只需要发过来的端口号是我需要的端口号就行,因此我们对我们之前的代码仍需要做修改

结果运行如下:

使用 netstat -nuap 查询显示如下:

4. Linux -> Linux 通信验证

将我们运行生成的 client_udp 文件打包到当前主机的桌面上,然后用另外一个 Linux 用户进行打开解包安装如下:

由于此时的文件默认没有可执行,因此我们还需要给其加上权限

验证如下:

现在我们已经能实现 udp 编程了,然后我们再对我们的代码进行调整

InetAddr.hpp
代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>

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

#include "Common.hpp"

class InetAddr
{
private:
    void PortNet2Host()
    {
        _port = ::ntohs(_net_addr.sin_port);
    }
    void IpNet2Host()
    {
        char ipbuffer[64];
        const char* ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        (void)ip;
        _ip = ipbuffer;
    }

public:
    InetAddr()
    {}

    InetAddr(const struct 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 GetIp()         { return _ip; }
    uint16_t GetPort()          { return _port; }

    ~InetAddr()
    {}


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

UdpServer.hpp

代码语言:javascript
代码运行次数:0
复制
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__


#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.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 LogMudule;

const static int gsockfd = -1;
// 这里的测试端口号用 8080,这里端口号可以随机,但是 1024 以内的不行,1024 以上包括 1024 是可以的,3306 也不大行,因为 3306 是 mySQL 端口号,尽量不要和系统端口号冲突,因此我们可以用 8080 8081 没有人用的端口号
const static uint16_t gdefaultport = 8080; 

class UdpServer
{
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 Start()
    {   
        _isrunning = true; // 启动服务器
        
        while(true) // 服务器不能停
        {
            char inbuffer[1024]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
            if(n > 0)
            {
                // 1. 消息内容 && 2. who 发 me 的 
                InetAddr cli(peer);
                inbuffer[n] = 0;

                std::string clientinfo = cli.GetIp() + ":" + std::to_string(cli.GetPort()) + " # " + inbuffer;

                LOG(LogLevel::DEBUG) << 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;
    InetAddr _addr;
    bool _isrunning; // 服务器运行状态
};


#endif

运行结果如下:

5. 中译英字典 -- DictServer

🔥 我们这里先中途插入一个趣味的翻译显示实验,在 EchoServer 的基础上来实现,大部分代码基本都没变,修改了一少部分代码,大家可以仔细看看

先给定一些等会我们要翻译的单词数据 dict.txt

代码语言:javascript
代码运行次数:0
复制
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
IsLand1314: 本人

Common.hpp 修改如下:

Dictionary.hpp

代码语言:javascript
代码运行次数:0
复制
#pragma once

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

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

using namespace LogModule;

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;
        }
         
        std::string line;
        while(std::getline(in, line)) // operator bool
        {
            // happy: 开心的
            std::string key, value;
            if(SplitString(line, &key, &value, gsep))
            {   // line -> key, value
                _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();
        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(auto &item : _dictionary)
        {
            std::cout << item.first << ": " << item.second << std::endl;
        }
    }

    ~Dictionary()
    {

    }

private:
    std::unordered_map<std::string, std::string> _dictionary;
    std::string _path;
    std::string _filename;
};

UdpServer.hpp 修改如下:

代码语言:javascript
代码运行次数:0
复制
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__


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

#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 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; // 测试
        

        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]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
            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;
    bool _isrunning; // 服务器运行状态

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


#endif

UdpServerMain.cc 修改如下:

代码语言:javascript
代码运行次数:0
复制
#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>([&dict_sptr](const std::string &word){
        std::cout << "|" << word << "|" << std::endl;
        return dict_sptr->Translate(word);
    }, port);

    // func_t f = std::bind(&Dictionary::Translate, dict_sptr.get(), std::placeholders::_1); //std::placeholders::_1 是 C++11 引入的一个占位符,常用于绑定函数参数的操作,特别是在与 std::bind 配合使用
    // std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(f, port);

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

    return 0;
}

测试如下:

6. 网络聊天室 -- ChatServer

基本了解

🔥 话说我们之前 Echoserver 已经实现了给我发信息,信息也已经可以返回给我的功能,但是如果同时有多个人要发信息的话,这个时候发去的信息就需要记录下来发来的人信息,并且进行维护,然后再把维护的信息给多个人一起看,这就实现了 群聊 的功能

在之前的代码当中,Echo 服务器收到发的信息,然后再转发对应的信息,但是有个问题,这里不仅要一个人收消息,后面还要我们自己去转发给所有人,此时收消息转消息都是同一个人,UDP 数据一旦过大,服务器可能就没时间接收数据了,而且我们前面也说过 UDP 套接字本身是全双工的,全双工的意思就是 在收数据的同时,也可以发送数据,下面我们举个例子

如果我们今天收到一个消息,并且将其封装成一个转发的任务,然后由其他线程来做转发, 而本身服务器只负责进行网络读

注意:我们这个代码是基于 EchoServer 基础上进行修改完善的

User.hpp

代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.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 &u) = 0;
};


class User : public UserInterface
{
public:
    User(const InetAddr &id): _id(id)
    {
    }
    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;
    }
    bool operator ==(const InetAddr &u) override
    {
        return _id == u;
    }
    ~User()
    {}
private:
    InetAddr _id;
};


// 对用户消息进行路由
// UserManage 把所有用户先管理起来

// 把一个新用户添加到在线用户列表,一旦要发信息,我们的 UserManage 不做发送,他要调的就是 User 提供的公共方法
// 这种设计模式就称为 观察者模式 -> observer

class UserManage
{
public:
    UserManage()
    {
    }

    void AddUser(InetAddr &id)
    {
        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)); // 构建 User 对象
    }
    void DelUser(const InetAddr &id)
    {
    }
    void Router(int sockfd, const std::string &message) // 消息转发
    {
        for(auto &user : _online_user)
        {
            user->SendTo(sockfd, message);
        }
    }

    ~UserManage()
    {}

private:
    std::list<std::shared_ptr<UserInterface>> _online_user; // 在线用户
};

上面这段代码实现了一个简化的用户管理和消息转发系统。它使用了 C++ 的面向对象编程、智能指针以及观察者设计模式。以下是对代码的逐步分析:

1. UserInterface 类

代码语言:javascript
代码运行次数:0
复制
class UserInterface
{
public:
    virtual ~UserInterface() = default;
    virtual void SendTo(int sockfd, const std::string &message) = 0;
    virtual bool operator ==(const InetAddr &u) = 0;
};

UserInterface 是一个抽象基类,定义了用户类应实现的接口。

  • SendTo 方法用于向指定的套接字发送消息(纯虚函数,子类需要实现)。
  • 重载 operator == 用于比较用户的网络地址(也是纯虚函数,子类需要实现)。
  • 该类的析构函数是虚拟的,以确保在删除派生类对象时能正确调用析构函数。

2. User 类

代码语言:javascript
代码运行次数:0
复制
class UserInterface
{
public:
    virtual ~UserInterface() = default;
    virtual void SendTo(int sockfd, const std::string &message) = 0;
    virtual bool operator ==(const InetAddr &u) = 0;
};
  • User 类继承自 UserInterface,实现了 SendTooperator == 方法。
  • 构造函数通过网络地址 InetAddr 来初始化 _id
  • SendTo 方法通过 sendto 函数将消息发送到指定的用户。日志记录了发送的信息和目标地址。
  • operator == 用于比较两个 User 是否相同,依据是它们的 InetAddr
  • 析构函数为空,析构时会自动释放 User 对象。

3. UserManage 类

代码语言:javascript
代码运行次数:0
复制
class User : public UserInterface
{
public:
    User(const InetAddr &id): _id(id)
    {
    }
    
    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;
    }

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

    ~User()
    {}

private:
    InetAddr _id;  // 用户的网络地址
};
  • UserManage 类负责管理在线用户,主要功能包括:
    • 添加用户AddUser 方法会检查用户是否已经存在,若不存在则将用户加入到 _online_user 列表中。用户通过 InetAddr 标识。
    • 删除用户DelUser 方法目前是空的,可能在后续实现中用于移除用户。
    • 消息路由Router 方法会遍历所有在线用户,并调用每个用户的 SendTo 方法,将消息发送给所有用户。可以理解为消息的广播。
  • _online_user 是一个 shared_ptr 类型的列表,管理所有在线用户,避免手动内存管理的麻烦。

4. 观察者模式

代码采用了观察者模式(Observer Pattern),其中:

  • UserManage 是观察者(Observer),负责管理所有的用户,并能对用户的状态进行操作。
  • User 是被观察者(Subject),通过 SendTo 方法接收并处理来自 UserManage 的消息。
  • 当有新消息需要发送时,UserManage 会通知所有用户调用 SendTo 方法,这样的设计能有效地将消息发送逻辑和用户管理逻辑解耦。

观察者模式概念

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

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

  • 观察者模式的特点
    • 松耦合 : 观察者和被观察者之间是松耦合的,便于扩展和维护。
    • 动态订阅 : 可以动态添加或移除观察者,灵活性高。
    • 单向通信 : 被观察者通知观察者,观察者不能反向修改被观察者的状态。
  • 使用情况:
    • 事件驱动系统:在用户操作界面中,通过监听事件(如按钮点击)触发响应。
    • 系统间通信:系统中某个模块发生变化时,需要通知多个依赖模块。
    • 分布式系统:数据更新时通知所有订阅者,例如推送通知、实时数据同步。
  • 典型场景:
    • GUI 事件处理系统(如按钮点击、窗口关闭事件)。
    • 数据模型与视图同步更新(如MVC架构中的数据绑定)。·股票价格更新通知订阅者。

举个例子:就相当于我们刷抖音,关注了一些博主,那么当博主发视频的时候,它的视频通知就会发送到每个关注它的粉丝那

UdpServer.hpp 修改如下:

代码语言:javascript
代码运行次数:0
复制
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__


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

#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 uint16_t gdefaultport = 8080; 

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

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

    // 都是套路
    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 Start()
    {   
        _isrunning = true; // 启动服务器
        
        while(true) // 服务器不能停
        {
            char inbuffer[1024]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
            if(n > 0)
            {   
                // 1. 消息内容 && 谁发的
                InetAddr cli(peer);
                inbuffer[n] = 0;

                // 2. 新增用户
                _adduser(cli);

                std::string clientinfo = cli.GetIp() + ":" + std::to_string(cli.GetPort()) + " # " + inbuffer;

                // LOG(LogLevel::DEBUG) << "client say@" << inbuffer; 
                LOG(LogLevel::DEBUG) << 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;
    InetAddr _addr;
    bool _isrunning; // 服务器运行状态

    // 新增用户
    adduser_t _adduser;
};


#endif

UdpServerMain.cc 修改如下:

代码语言:javascript
代码运行次数:0
复制
#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::shared_ptr<UserManage> um = std::make_shared<UserManage>();
    // 网络模块
    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&um](InetAddr &id){
                                                                    um->AddUser(id);}, port);

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

    return 0;
}

测试如下:

引入线程池

上面只是一点点小小的修改,现在我们要开始引入我们之前写的 线程池,详情可以参考 文章

【Linux】:日志策略 + 线程池(单例模式) 单例模式下的线程池代码

ThreadPool.hpp 修改如下:

UdpServer.hpp 修改如下:

代码语言:javascript
代码运行次数:0
复制
#ifndef _UDP_SERVER_HPP__
#define _UDP_SERVER_HPP__


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

#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"
#include "ThreadPool.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

const static int gsockfd = -1;
const static uint16_t gdefaultport = 8080; 

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

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

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

    // 都是套路
    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)
    {
        _adduser = adduser;
        _route = route;
    }

    void Start()
    {   
        _isrunning = true; // 启动服务器
        
        while(true) // 服务器不能停
        {
            char inbuffer[1024]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len) ; // -1 预留一个空位置给 \0,这里 0 表示采用阻塞的方式进行等待
            if(n > 0)
            {   
                // 1. 消息内容 && 谁发的
                InetAddr cli(peer);
                inbuffer[n] = 0;
                std::string message = inbuffer;

                // 2. 新增用户
                _adduser(cli);

                // 3. 构建转发任务,推送给线程池,让线程池进行转发
                task_t task = std::bind(UdpServer::_route, _sockfd, message); // route 是回调方法,不是成员方法 不用 this
                ThreadPool<task_t>::getInstance()->Equeue(task);

                // std::string clientinfo = cli.GetIp() + ":" + std::to_string(cli.GetPort()) + " # " + inbuffer;

                // // LOG(LogLevel::DEBUG) << "client say@" << inbuffer; 
                // LOG(LogLevel::DEBUG) << 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;
    InetAddr _addr;
    bool _isrunning; // 服务器运行状态

    // 新增用户
    adduser_t _adduser;
    route_t _route;
};


#endif

UdpServerMain.cc 修改如下:

代码语言:javascript
代码运行次数:0
复制
#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::shared_ptr<UserManage> um = std::make_shared<UserManage>();
    // 网络模块
    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&um](InetAddr &id){
                                                                    um->AddUser(id);}, port);
    svr_uptr->InitServer();
    svr_uptr->RegisterService([&um](InetAddr &id)
                            {um->AddUser(id);},
                            [&um](int sockfd, const std::string &message)
                            {um->Router(sockfd, message);});

    svr_uptr->Start();
    
    return 0;
}

UdpClientMain.cc 修改如下:

代码语言:javascript
代码运行次数:0
复制
#include "UdpClient.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#include "Common.hpp"

int sockfd = -1;

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


// 客户端 访问 目标服务器
// ./client_udp sereverip 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);
    }

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

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

    // 2. clientdone
    while(true)
    {
        std::cout << "Please Enter# " << std::endl;

        std::string message;
        std::getline(std::cin, message);
        // client 需不需要 bind? socket <--> socket
        // client 必须也要有 自己 的 ip 和 端口, 但是客户端不需要自己显示的调用 bind
        // 而是客户端首次 sendto 消息 的时候, 由 OS 自动进行 bind
        
        // 1.如何理解 client 自动随机 bind 端口号   
            // OS 不允许一个端口号绑定多个进程,必须要有唯一性,但是 一个进程可以绑定 多个端口号  
            // 保证当前端口号唯一性,和别人不冲突就行
        // 2.如何理解 server 要显示 bind
            // 服务器的端口号必须稳定
            // 而且必须是众所周知而且不能轻易改变的

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


    return 0;
}

测试如下:

使用管道 -- fifo

利用我们之前在

【Linux】IPC 进程间通信(一):管道(匿名管道&命名管道)讲的管道内容以及在 【Linux】: 重定向(补充)重定向内容,我们来演示一下其在管道上的表示

先对上面代码进行一下修改,方便输出信息的分离,UdpClientMain.cc 修改如下

演示如下:

聊天室--管道

  • 注意:在使用管道时候,需要保证读写都已就绪,否则会堵在 管道 open 处
保护临界资源安全

🔥 这里在 User 文件进行操作时,Router 要遍历链表 ,而 Adduser 也要向链表里做插入,而我们的链表可能会被数据接收模块、线程池模块并发地访问它,因此我们这里转发的时候需要做一下修改,必须保证临界资源安全,修改如下:

但是,当我们收到对应的消息 message 我们该怎么区分究竟是哪个人的?

  • 对 UdpServer.hpp 修改如下:

演示结果如下:

多次登录端口号匹配问题

但是其实这里有个小 bug,就是当我们退出重新再登录用户的时候,服务器并没有删掉我们这个用户,但是我们再启动的时候端口号其实已经发生改变了,而服务器保存我的端口号还是老的,这个时候我们,再发消息,服务器可以收到,但是用户收不到

  • 如上,第一次登录的端口号为 57325,第二次登录的时候端口号为 51884,但是在第二次登录的时候收不到信息
  • 因此现在这个阶段,我们在 inetAddr.hpp 文件中进行比较的时候,可以把端口号也带上,一次多启动几个客户端,方便我们做测试
上线消息发送问题

如果我今天上线的话,如果不发信息的话,就不知道我上线了,我也收不到信息,那么能不能把其改成保证一上线就能收到消息

结果如下:

用户器退出给服务器发送消息

我们这里一般都是用 Ctrl + C 退出用户,但是用户的确退出了,但是服务器并不知道用户退出了,因此我们这里就可以用 Signal 来捕捉退出信号,捕捉信号可以参考之前的这篇内容

【Linux】:进程信号(信号概念 & 信号处理 & 信号产生)

icon-default.png?t=O83A
icon-default.png?t=O83A

https://island.blog.csdn.net/article/details/143662179UdpClientMain.cc 修改如下:

ClientQuit 全局实现如下:

代码语言:javascript
代码运行次数:0
复制
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));
    exit(0);
}

然后在 UdpServer.hpp 服务器端我们也要实现一个移除用户的功能

先定义移除用户的类型别名

代码语言:javascript
代码运行次数:0
复制
using remove_t = std::function<void(InetAddr &id)>;

User.hpp 修改如下:

代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <algorithm>

#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)
    {
    }
    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;
    }
    bool operator ==(const InetAddr &u) override
    {
        return _id == u;
    }
    std::string Id() override
    {
        return _id.Addr();
    }
    ~User()
    {}
private:
    InetAddr _id;
};


// 对用户消息进行路由
// UserManage 把所有用户先管理起来

// 把一个新用户添加到在线用户列表,一旦要发信息,我们的 UserManage 不做发送,他要调的就是 User 提供的公共方法
// 这种设计模式就称为 观察者模式 -> observer

class UserManage
{
public:
    UserManage()
    {
    }

    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)); // 构建 User 对象
        PrintUser();
    }
    void DelUser(InetAddr &id)
    {
        // 写法 1
        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();

        // // 写法 2 -- 不对
        // for(auto &user : _online_user)
        // {
        //     if(*user == id){
        //         _online_user.erase(user);
        //         break; // 必须 break,避免迭代器失效问题
        //     }
        // }
        // 在基于范围的 for 循环中,迭代器是隐式管理的。
        // 当调用 erase 后,迭代器 user 失效,但循环仍然尝试继续使用它(即使你使用了 break,某些编译器或情况下仍可能导致问题)。

        // // 写法 3
        // for (auto it = _online_user.begin(); it != _online_user.end(); ) {
        //     if (**it == id) {
        //         it = _online_user.erase(it); // erase 返回下一个有效的迭代器
        //     } else {
        //         ++it; // 继续遍历
        //     }
        // }
    }

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

    ~UserManage()
    {}

private:
    std::list<std::shared_ptr<UserInterface>> _online_user; // 在线用户
    Mutex _mutex;
};
  • remove_if() 并不会实际移除序列[start,end)中的元素
  • 如果在一个容器上应用 remove_if(),容器的长度并不会改变,所有的元素都还在容器里面(但是逻辑上已经无法访问)
  • remove_if() 将所有应该移除的元素都移动到容器尾部并返回一个分界的迭代器、为了实际移除元素,必须对容器自行调用 erase() 以擦除需要移除的元素

然后在服务器端 实现相应功能

最终演示结果如下:

nocopy

这里我们还有个小细节,一般我们的服务器都不想被拷贝或继承,这里我们就写个基类,然后UdpServer 继承它即可,就不用把拷贝私有化了,直接继承这个基类就行

7. 补充

🎈网络命令

🐋 ping

🐋 netstat

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

代码语言:javascript
代码运行次数:0
复制
1. n拒绝显示别名,能显示数字的全部转化成数字
2. I仅列出有在 Listen(监听)的服務状态
3. p 显示建立相关链接的程序名
4. t(tcp)仅显示 tcp 相关选项
5. u(udp)仅显示 udp 相关选项
6. a(all)显示所有选项,默认不显示 LISTEN 相关
  • watch 是一个周期性执行 Linux 指令的一个命令
代码语言:javascript
代码运行次数:0
复制
// 每个 1s 执行一次 netstat -nuap
$ watch -n 1 netstat -nuap

🐋 pidof

代码语言:javascript
代码运行次数:0
复制
// 查看进程 id 的两种方式,显然第二种更简便
lighthouse@VM-8-10-ubuntu:~$ ps ajx | head -1 && ps axj | grep server_udp
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
2648810 2649427 2649427 2648810 pts/2    2649427 S+    1001   0:00 ./server_udp 8888
2638397 2649450 2649449 2638397 pts/3    2649449 S+    1001   0:00 grep --color=auto server_udp
lighthouse@VM-8-10-ubuntu:~$ pidof server_udp
2649427

// 杀死进程的两种方式
lighthouse@VM-8-10-ubuntu:~$ kill -9 2649427
lighthouse@VM-8-10-ubuntu:~$ pidof server_udp | xargs kill -9
  • 这里的 xargs 其中就相当于,我们之前说过管道是一个文件,这里通过 Pidof 把文件的 id 通过管道传给 kill 命令,实际上这个 kill 命令它是通过标准输入文件描述符 0 来把数据读到 kill 命令中,但是 kill 要要杀死一个进程,是需要把进程 pid 放到自己的命令行参数当中传递给 kill 命令的

打个比方:

  • 这里就是把 ls 的输出结果通过管道交给 cat,而 cat 本身就是输入什么就回显什么,这里就说明这里的管道已经做过重定向了,cat 默认从标准输入里读,而 ls 的输出已经重定向成为 标准输入的读端了,因此会出现这样的结果
  • 但是这里的 kill 不想从标准输入里去读,而是想从命令行参数里面去读,那么这里 xargs 把管道当中传过来的数据 转化 成为后续命令的后续命令行参数,拼接到后面,所以这里我们就省略了手动操作了,可以直接经过管道来杀死进程了
🎈 地址转化函数

🥑 上面只介绍基于IPv4的socket网络编程,sockaddr in 中的成员 structin addrsin addr表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在 字符串表示 和 in_addr 表示 之间转换;

字符串转 in addr 的函数:

in addr 转字符串的函数:

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

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include<arpa/inet.h>
int main(){
    struct sockaddr_in addr:
    inet_aton("127.0.0.1",&addr.sin_addr);
    uint32_t* ptr =(uint32_t*)(&addr.sin_addr);
    printf("addr:%x\n",*ptr);
    printf("addr_str:%s\n",inet_ntoa(addr.sin_addr));
    return 0;
}
🎈 关于 inet_ntoa

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

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

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
	struct sockaddr_in addr1;
	struct sockaddr_in addr2;
	addr1.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;

	char* ptr1 = inet_ntoa(addr1.sin_addr);
	char* ptr2 = inet_ntoa(addr2.sin_addr);
	printf("ptr1:%s,ptr2:%s\n", ptr1, ptr2);
	return 0;
}

运行结果如下:

  • 因为 inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果.

在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数

🎈 测试 windows 作为 client 来访问 Linux

Udp_client.cc

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h> // Windows Sockets API 的头文件,方便使用套接字进行网络通信
#include <Windows.h>


#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")

std::string serverip = ""; // 填写云服务 ip
uint16_t serverport = 8888; // 填写云服务所开放的端口号


int main()
{
	WSADATA wsd; // 保留初始化 返回的信息
	WSAStartup(MAKEWORD(2, 2), &wsd);

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

	SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	if (sockfd == SOCKET_ERROR)
	{
		std::cout << "socker error" << std::endl;
		return 1;
	}

	std::string message;
	char buffer[1024];

	while (true)
	{
		std::cout << "Please Enter@ ";
		std::getline(std::cin, message);
		if (message.empty()) continue;
		sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *) &server, sizeof(server));
		
		struct sockaddr_in tmp;
		int len = sizeof(tmp);
		int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &len);
		if (s > 0)
		{
			buffer[s] = 0;
			std::cout << buffer << std::endl;
		}
	}

	closesocket(sockfd);
	WSACleanup();
	return 0;
}
  1. Winsock2.h 是 Windows Sockets API(应用程序接口)的头文件,用于在 Windows 平台上进行网络编程。它包含了 Windows Sockets 2(Winsock2)所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字(sockets)进行网络通信。
  2. 在编写使用 Winsock2 的程序时,需要在源文件中包含 Winsock2.h 头文件。这样,编译器就能够识别并理解 Winsock2 中定义的数据类型和函数,从而能够正确地编译和链接网络相关的代码。
  3. 此外,与 Winsock2.h 头文件相对应的是 ws232.1ib 库文件。在链接阶段,需要将这个库文件链接到程序中,以确保运行时能够找到并调用 Winsock2API 中实现的函数。

在 winsock2.h 中定义了一些重要的数据类型和函数,如:

  1. WSADATA:保存初始化Winsock库时返回的信息。
  2. SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字
  3. sockaddr in:IPv4地址结构体,用于存储IP地址和端口号等信息。socket():创建一个新的套接字。
  4. bind():将套接字与本地地址绑定。
  5. listen():将套接字设置为监听模式,等待客户端的连接请求。
  6. accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端进行通信

WSAStartup 函数

WSAStartup 函数是 Windows Sockets API 的初始化函数,它用于初始化Winsock库。该函数在应用程序或 DLL 调用任何 Windows 套接字函数之前必须首先执行,它扮演着初始化的角色。 以下是 WSAStartup 函数的一些关键点:

  • 它接受两个参数:wVersionRequested 和 lpWSAData。wVersionRequested 用于指定所请求的 Winsock版本,通常使用MAKEWORD(major,minor)宏,其中 major 和 minor 分别表示请求的主版本号和次版本号。lpWSAData 是一个指向 WSADATA 结构的指针,用于接收初始化信息。
  • 如果函数调用成功,它会返回0,否则,返回错误代码。
  • WSAStartup 函数的主要作用是向操作系统说明我们将使用哪个版本的Winsock库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后, Winsock 库的状态会被初始化,应用程序就可以使用 Winsock 提供的一系列套接字服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程序可以与底层的网络协议栈进行交互,实现网络通信。
  • 在调用 WSAStartup 函数后,如果应用程序完成了对请求的Socket库的使用,应调用WSACleanup函数来解除与Socket库的绑定并释放所占用的系统资源。

验证结果如下:

🎈注意事项(解决一些问题)

🔥 我们在进行 通信验证的时候,在打包到另一个用户上的时候,运行 client_udp ,输入对应信息的时候, server_udp 文件可能没有反应,这个原因是因为我们的云服务器防火墙问题,因此我们就需要进入我们的云服务器,找到防火墙一栏,进行添加规则,如下:

8. 共勉

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !💞!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言
  • 2. 代码框架
  • 3. 基本实现 -- EchoServer
    • 🍉 UdpServer.hpp
      • 🥝 代码片段分析:
      • 🥝 InitServer() 方法
      • 🥝 Start() 方法
    • 🍉 UdpClientMain.cc
      • 🍒 代码片段分析:
      • 🍒 注意:
    • 🍉 UdpServer.hpp 修改 -- 打印谁发的信息
    • 🍉 UdpServerMain.cc 修改 -- 网络通信
  • 4. Linux -> Linux 通信验证
    • InetAddr.hpp
  • 5. 中译英字典 -- DictServer
  • 6. 网络聊天室 -- ChatServer
    • 基本了解
    • 引入线程池
    • 使用管道 -- fifo
    • 保护临界资源安全
    • 多次登录端口号匹配问题
    • 上线消息发送问题
    • 用户器退出给服务器发送消息
    • nocopy
  • 7. 补充
    • 🎈网络命令
    • 🎈 地址转化函数
    • 🎈 关于 inet_ntoa
    • 🎈 测试 windows 作为 client 来访问 Linux
    • 🎈注意事项(解决一些问题)
  • 8. 共勉
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档