【Linux网络】基于UDP的Socket编程,实现简单聊天室

前言:

上文我们讲到了,基于UDP的Dict Server的Socket编程。【Linux网络】Socket编程实战,基于UDP协议的Dict Server-CSDN博客

本文我们再来实现一个基于UDP的简单聊天室


实现思路

大体实现思路

客户端第一次向服务器发送消息,我们视为登录。

客户端给服务器发送消息,服务器要向消息转发给当前所有的在线用户,包括自己!

客户端不断的接收服务器的消息,实现聊天室的大致功能。

客户端实现思路

1.创建Socket,要bind端口与IP地址,但有操作系统自动绑定!不需要我们显式的绑定

2.使用sendto函数,向服务器发送信息

3.使用recvform函数,接收服务器的信息

4.客户端的发送信息与接收信息,必须使用多线程!不然发送信息与接收信息是串行的,将会导致必须发送信息,才能接收信息的逆天局面!!!

服务器实现思路

1.创建Socket,显式的绑定IP地址与端口号(IP地址绑定任意地址:INADDR_ANY。让任意客户端都可以访问服务器)

2.使用recvform函数,接收服务器的信息。

3.调用对应的方法,将接收到的信息转发给所有在线用户

转发模块实现思路

1.使用vector,作为在线用户表的容器

2.只要服务器调用了该方法,那么执行以下步骤:

判断当前用户是否在在线用户表中;若不在,则进行登录操作:将该用户压入用户表中

若当前用户在在线用户表中,则将该用户发送的信息,转发给全部在线用户(包括自己)

判断用户发送的信息是否为"QUIT",若是则表明用户要退出,将当前用户从在线用户表中删除

代码实现

客户端代码实现

cpp 复制代码
//UdpClient.cc



#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include "Thread.hpp"
using namespace ThreadModule;
using namespace LogModule;

// 客户端也要进行多线程的改造
// 不然客户端,将会出现不发现信息,就不能接收信息的逆天局面

// 全局变量
struct sockaddr_in local;
int sockfd;
pthread_t tid = 0;
bool get_quit = false;

// 标准输出、标准错误 + 命令行的输出重定向:实现一个窗口显示输入信息,一个显示接收信息

// 发送信息
void Send()
{
    while (!get_quit)
    {
        // 向服务器发送信息
        cout << "Please Cin # "; // 1,标准输出
        std::string buff;
        cin >> buff; // 0,标准输入
        // std::getline(std::cin, buff);
        // buff.size()-1 会丢失最后一个字符,应改为 buff.size()
        ssize_t s = sendto(sockfd, buff.c_str(), buff.size(), 0, (struct sockaddr *)&local, sizeof(local));
        if (s < 0)
        {
            LOG(LogLevel::WARNING) << "向服务器发送信息失败";
            exit(1);
        }
    }
}

// 接收信息
void Receive()
{
    while (!get_quit)
    {
        // 接收服务器返回的信息
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t ss = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
        if (ss < 0)
        {
            LOG(LogLevel::WARNING) << "接收服务器信息失败";
            exit(1);
        }
        // printf("%s\n", buffer);
        buffer[ss] = 0;
        cerr << buffer << endl; // 2,标准错误
        // 判断是否退出
        if (strcmp(buffer, "QUIT") == 0)
        {
            get_quit = true;
            LOG(LogLevel::INFO) << "get_quit = true";
            break;
        }
    }
}

// 给出 ip地址 端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Please use: " << argv[0] << " IP " << "PORT " << endl;
        exit(1);
    }

    uint32_t ip = inet_addr(argv[1]); // 注:字符串转合法ip地址
    uint16_t port = stoi(argv[2]);    // 注:字符串转整数

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "创建套接字失败";
        exit(1);
    }
    LOG(LogLevel::INFO) << "创建套接字";

    // 绑定?不用显示绑定,OS会自动的绑定
    // 填写服务器信息
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = ip;
    local.sin_port = htons(port);

    // 创建线程
    Thread send(Send);
    Thread receive(Receive);
    tid = send.Tid();

    // 执行各自的任务:发送信息、接收信息
    send.Start();
    receive.Start();

    // 等待:需要进行等待,不然主线程结束,整个进程就都结束了,连同子线程也会被强制结束
    send.Join();
    receive.Join();
}

服务器代码实现

cpp 复制代码
//UdpServer.hpp


#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;

class udpserver
{
    using func_t = function<void(int &, string, InetAddr &)>;

public:
    udpserver(uint16_t port, func_t func)
        // : _addr(inet_addr(addr.c_str())), // 注:直接将其转换为合法的ip地址
        : _port(port),
          _func(func)
    {
        _running = false;
    }

    // 初始化:1.创建套接字 2.填充并绑定地址信息
    void Init()
    {
        // 1.创建套接字
        // 返回套接字描述符 地址族 数据类型 传输协议
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "创建套接字失败!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "创建套接字";

        // 2.绑定信息
        // 2.1填充信息
        struct sockaddr_in local;
        // 将指定内存块的所有字节清零
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET; // IPv4地址族
        // local.sin_addr.s_addr = _addr;   //IP地址(主机序列转化为网络序列)
        local.sin_addr.s_addr = INADDR_ANY; // 赋值为INADDR_ANY,表示任意地址
        local.sin_port = htons(_port);      // 端口号

        // 2.2绑定信息
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "绑定失败";
            exit(1);
        }
        LOG(LogLevel::INFO) << "绑定成功";
    }

    // 启动运行:一直运行不停止;1.接收客户端的信息 2.对客户端发送来的信息进行回显
    void Start()
    {
        // 一定是死循环
        _running = true;
        while (_running)
        {
            // 接收客户端的信息
            char buff[1024];
            struct sockaddr_in peer;
            unsigned int len = sizeof(peer);
            // 套接字描述符,数据存放的缓冲区,接收方式:默认,保存发送方的ip与端口,输入输出参数:输入peer的大小,输出实际读取的数据大小
            ssize_t s = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);

            // 显示发送方的ip与prot
            InetAddr iaddr(peer);
            cout << iaddr.ip() << " : " << iaddr.prot() << " : ";
            // 显示发送的信息
            buff[s] = 0;
            printf("%s\n", buff);

            // 回显消息
            if (s > 0)
            {
                // 调用自定义方法
                _func(_sockfd, string(buff), iaddr);

                // 将数据发送给客户端
                // 套接字描述符,要发送的信息,发送方式:默认,接收方的ip与端口信息
                // ssize_t t = sendto(_sockfd, ss.c_str(), ss.size(), 0, (struct sockaddr *)&peer, len);
                // if (t < 0)
                // {
                //     LOG(LogLevel::WARNING) << "信息发送给客户端失败";
                // }
            }
            memset(&buff, 0, sizeof(buff)); // 清理缓存
        }
    }

private:
    int _sockfd;
    uint32_t _addr;
    uint16_t _port;
    bool _running;

    // 回调方法
    func_t _func;
};
cpp 复制代码
//UdpServer.cc


#include "UdpServer.hpp"
#include <cstdlib>
#include "InetAddr.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;

// 实现聊天:
// 服务器端,要将从客户端收到的信息,转发给说有在线用户!

// 给出 端口号
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cout << "Please use: " << argv[0] << " PORT" << endl;
    }
    else
    {

        // 1.数据路由模块
        Route route;

        udpserver us(port, [&route](int &sockfd, std::string message, InetAddr &addr)
                    { return route.MessageRoute(sockfd, message, addr); });
        us.Init();
        us.Start();
    }
}
cpp 复制代码
//InetAddr.hpp


#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>

// 实现网络地址与主机地址的转换

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _prot = ntohs(_addr.sin_port);   // 网络地址转主机地址
        _ip = inet_ntoa(_addr.sin_addr); // 将4字节网络风格的IP -> 点分十进制的字符串风格的IP
    }

    uint16_t prot()
    {
        return _prot;
    }

    string ip()
    {
        return _ip;
    }

    // 运算符重载
    bool operator==(InetAddr &addr)
    {
        return _prot == addr._prot && _ip == addr._ip;
    }

    string Getname()
    {
        return _ip + ':' + to_string(_prot);
    }

private:
    struct sockaddr_in _addr;
    uint16_t _prot;
    std::string _ip;
};

转发模块代码实现

cpp 复制代码
//Route.hpp


// 消息路由模块:将客户端的信息转发给所有在线用户

#pragma once
#include <iostream>
#include <vector>
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace LogModule;

class Route
{
private:
    bool IsExit(InetAddr &addr)
    {
        for (auto &e : _online_user)
        {
            if (e == addr)
                return true;
        }
        return false;
    }

    void AddUesr(InetAddr &addr)
    {
        _online_user.push_back(addr);
        LOG(LogLevel::INFO) << "用户:" << addr.Getname() << "登录";
    }

    void DeleteUser(InetAddr &addr)
    {
        for (auto it = _online_user.begin(); it < _online_user.end(); it++)
        {
            if (addr == *it)
            {
                // erase参数只能是迭代器
                _online_user.erase(it);
                LOG(LogLevel::INFO) << "用户" << it->Getname() << "退出登录";
            }
        }
    }

public:
    void MessageRoute(int sockfd, std::string &message, InetAddr &addr)
    {

        // 判断当前用户是否在,在线用户中
        if (!IsExit(addr))
        {
            // 没在,则添加
            AddUesr(addr);
        }

        // 当前用户一定在
        // 将当前用户的消息转发给所有人,包括自己
        for (auto &user : _online_user)
        {
            sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&user, sizeof(user));
        }

        // 输入QUIT,表示用户退出
        if (message == "QUIT")
        {
            DeleteUser(addr);
        }
    }

private:
    // 首次发消息,视为登录
    std::vector<InetAddr> _online_user;
};

服务器优化

上述,我们实现服务器的单线程的!那么存在多个用户同时登录、发消息退出登录的操作吗?当然存在!

所以,我们要对服务器进一步的优化!既:将服务器优化为多线程的,这里我们采用进程池进行优化~

cpp 复制代码
#include "UdpServer.hpp"
#include <cstdlib>
#include "InetAddr.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;

// 实现聊天:
// 服务器端,要将从客户端收到的信息,转发给说有在线用户!

// 给出 端口号
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Please use: " << argv[0] << " PORT" << endl;
    }
    else
    {
        // 相当难理解!!!!!!! 不中了

        // 1.数据路由模块
        Route route;

        // 2.进程池模块
        using func_t = function<void()>;
        ThreadPool<func_t> threadpool(5);

        // 3. 网络服务器对象,提供通信功能
        uint16_t port = stoi(argv[1]); // 注:字符串转整数

        // 启动进程池
        threadpool.Start();

        std::unique_ptr<udpserver> usvr = std::make_unique<udpserver>(port, [&route, &threadpool](int &sockfd, std::string message, InetAddr &peer)
                                                                      {
            func_t t = std::bind(&Route::MessageRoute,&route,sockfd, message, peer);
            threadpool.Equeue(t); });

        // 启动服务端
        usvr->Init();
        usvr->Start();

    }
}

补充

地址转换函数

IP地址转换函数:inet_ntop(网络转主机)、inet_pton(主机转网络)

cpp 复制代码
#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);

参数说明:
af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)
src:指向点分十进制字符串的指针(如 "192.168.1.1")
dst:指向存储转换后二进制地址的缓冲区指针
返回值:
1:转换成功
0:输入不是有效的 IP 地址字符串
-1:地址族不支持或其他错误
cpp 复制代码
#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数说明:
af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)
src:指向网络字节序二进制地址的指针
dst:指向存储转换后字符串的缓冲区指针
size:缓冲区大小
返回值:
成功:返回指向转换后字符串的指针
失败:返回 NULL

端口号转换函数:ntohs、ntohl(网络转主机);htons、htonl(主机转网络)

优化地址转化模块:InetAddr.hpp

cpp 复制代码
//InetAddr.hpp


#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>

// 实现网络地址与主机地址的转换

class InetAddr
{
public:
    // 网络转主机
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _prot = ntohs(_addr.sin_port); // 网络地址转主机地址
        char buff[1024];
        inet_ntop(AF_INET, &addr.sin_addr, buff, sizeof(buff)); // 将4字节网络风格的IP -> 点分十进制的字符串风格的IP
        _ip = std::string(buff);
    }

    // 主机转网络
    InetAddr(std::string ip, uint16_t prot)
        : _ip(ip),
          _prot(prot)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        //&addr.sin_addr 是一个指向 struct in_addr 的指针,其内存地址等价于 &(addr.sin_addr.s_addr)(因为结构体的起始地址就是第一个成员的起始地址)
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_prot);
    }

    // 直接获取sockaddr_in
    sockaddr_in *Getaddr()
    {
        return &_addr;
    }

    uint16_t prot()
    {
        return _prot;
    }

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

    // 运算符重载
    bool operator==(InetAddr &addr)
    {
        return _prot == addr._prot && _ip == addr._ip;
    }

    std::string Getname()
    {
        return _ip + ':' + std::to_string(_prot);
    }

private:
    struct sockaddr_in _addr;
    uint16_t _prot;
    std::string _ip;
};
相关推荐
egoist20232 小时前
[linux仓库]多线程同步:基于POSIX信号量实现生产者-消费者模型[线程·柒]
linux·运维·生产者消费者模型·环形队列·system v信号量
DeeplyMind2 小时前
linux drm子系统专栏介绍
linux·驱动开发·ai·drm·amdgpu·kfd
wanhengidc2 小时前
在云手机中云计算的作用都有哪些?
服务器·网络·游戏·智能手机·云计算
tkevinjd2 小时前
WebServer05
服务器·网络
艾莉丝努力练剑2 小时前
【Linux基础开发工具 (二)】详解Linux文本编辑器:Vim从入门到精通——完整教程与实战指南(上)
linux·运维·服务器·人工智能·ubuntu·centos·vim
拾光Ծ2 小时前
Linux高效编程与实战:自动化构建工具“make/Makefile”和第一个系统程序——进度条
linux·运维·自动化·gcc
Pluchon3 小时前
硅基计划6.0 伍 JavaEE 网络原理
网络·网络协议·学习·tcp/ip·udp·java-ee·信息与通信
せいしゅん青春之我3 小时前
【JavaEE初阶】网络层-IP协议
java·服务器·网络·网络协议·tcp/ip·java-ee
zz-zjx5 小时前
LVS三种模式及调度算法解析
网络·lvs