【Socket 编程】基于UDP协议建立多人聊天室

思路

对于服务端来说,除了要接收消息之外,还要实现一个路由转发模块,该路由转发模块可以将相应发送给所有连接的客户端。而对于客户端来说,除了要发送消息给聊天室,还要能实时看到其它所有客户端的消息。
下面来看看具体实现的代码,代码只给出核心模块,其余代码模块代码可以借鉴我之前的博客。具体来说,服务器类实现的功能只有收消息。

路由功能实现

Route.hpp头文件

该头文件实现了一个路由类,该路由类维护了一张用户表和一把锁。该类向外提供转发消息功能的接口。为了实现并发转发消息,这里使用到了之前博客写的线程池,将转发功能的函数提供给线程池里的线程去处理。

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <cstring>
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

#include "LockGuard.hpp"
using namespace std;
using task_t = function<void()>;

class Route
{
private:
public:
    Route()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }

    void CheckOnlineUser(InetAddr &who)
    {
        // 用户表是临界资源,需要加锁
        LockGuard lockguard(&_mutex);
        for (auto &user : _online_user)
        {
            if (user == who)
            {
                LOG(DEBUG, "%s is exists!\n", who.AddrStr().c_str());
                return;
            }
        }

        LOG(DEBUG, "%s is not exists,add it!\n", who.AddrStr().c_str());
        _online_user.push_back(who);
    }
    // 下线
    void Offline(InetAddr &who)
    {
        LockGuard lockguard(&_mutex);
        vector<InetAddr>::iterator st = _online_user.begin();
        while (st != _online_user.end())
        {
            if (*st == who)
            {
                _online_user.erase(st);
                LOG(DEBUG, "%s is offline\n", who.AddrStr().c_str());
                break;
            }
            st++;
        }
    }
    void ForwardHelper(int sockfd, const string &message, InetAddr who)
    {
        LockGuard lockguard(&_mutex);
        string send_message = "[" + who.AddrStr() + "]# " + message;

        // 遍历在线用户表,转发给他们
        for (auto &user : _online_user)
        {
            struct sockaddr_in peer = user.Addr();
            LOG(DEBUG, "Forward message to %s\n", user.AddrStr().c_str());
            sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
        }
    }

    // 转发消息给在线用户表
    void Forward(int sockfd, const string &message, InetAddr &who)
    {
        // 查看该用户是否在线,不在线就添加到在线用户中
        CheckOnlineUser(who);

        // 如果用户退出
        if (message == "QUIT" || message == "quit")
        {
            // 下线
            Offline(who);
            return;
        }

        // 创建单例线程池帮助转发

        task_t t = bind(&Route::ForwardHelper, this, sockfd, message, who);
        ThreadPool<task_t>::GetInstance()->Push(t);
    }
    ~Route()
    {
        pthread_mutex_destroy(&_mutex);
    }

private:
    vector<InetAddr> _online_user; // 在线用户表
    pthread_mutex_t _mutex;
};

服务端

UdpServerMain.cpp

服务器的处理逻辑

#include "UdpServer.hpp"
#include <iostream>
#include <string>
#include <memory>
#include "Route.hpp"
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cerr << "Usage:" << argv[0] << " local-port" << endl;
        exit(0);
    }

    uint16_t port = stoi(argv[1]);
    EnableScreen();
    Route messageRoute;
    server_t message_route = bind(&Route::Forward,
                                  &messageRoute, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(message_route, port); // C++14的标准
    usvr->InitServer();
    usvr->Start();
    return 0;
}

UdpServer.hpp

实现一个服务器类

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <cstring>
#include "LockGuard.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include <functional>
using namespace log_ns;
using namespace std;
static const int gsockfd = -1;
static const uint16_t glocalport = 8888;
using server_t = function<void(int, const string &, InetAddr &)>;
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

class UdpServer : public nocopy
{
private:
public:
    UdpServer(server_t func, uint16_t localport = glocalport)
        : _localport(localport), _isrunning(false), _sockfd(gsockfd), _fun(func)
    {
    }

    void InitServer()
    {
        // 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议和UDP协议的套接字
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket create error!\n");
            exit(1);
        }
        LOG(DEBUG, "socket create success,sockfd is %d\n", _sockfd);
        // 绑定端口号和IP
        // 绑定之前创建一个sockaddr_in对象,存储本地地址信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定
        local.sin_port = htons(_localport);

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "sockfd bind error!\n");
            exit(1);
        }
        LOG(DEBUG, "sockfd bind success!\n");
    }

    void Start()
    {
        _isrunning = true;
        char inbuff[1024];
        while (_isrunning)
        {
            // 获取数据
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            ssize_t n = recvfrom(_sockfd, inbuff, sizeof(inbuff) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n < 0)
            {
                LOG(FATAL, "recvfrom error!\n");
            }
            else
            {
                LOG(DEBUG, "recvfrom success!\n");
                InetAddr addr(peer);
                inbuff[n] = '\0';
                std::string message = inbuff;
                _fun(_sockfd, message, addr);
            }
        }
        _isrunning = false;
    }

    ~UdpServer()
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }

private:
    int _sockfd;
    uint16_t _localport;
    bool _isrunning;
    server_t _fun;
};

客户端

UdpClientMain.cpp

该文件实现了客户端发送消息和接收消息的逻辑,由于发送消息和接收消息需要异步进行,所以可以考虑使用两个线程,分别查看聊天室的消息和向聊天室发送消息

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"
#include "InetAddr.hpp"
#include <memory>
#include <functional>
using namespace std;
using namespace ThreadMoudle;

int InitClient()
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cerr << "create client failed" << endl;
        exit(1);
    }
    return sockfd;
}

// 发送消息
void SendMessage(int sockfd, string serverip, uint16_t serverport, const string &name)
{
    // 客户端一般不用绑定套接字,操作系统会在第一次发送消息时自动绑定本机ip和一个随机的port
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);

    while (true)
    {
        string line;
        cout << name << " #: ";
        getline(cin, line);
        ssize_t res = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if (res <= 0)
        {
            break;
        }
    }
}

// 接收消息
void RecvMessage(int sockfd, const string &name)
{

    while (true)
    {
        struct sockaddr_in peer;
        char buff[1024];
        socklen_t len = 0;
        int n = recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            buff[n] = 0;
            cout << buff << endl;
        }
        else
        {
            cerr << "recvfrom error\n"
                 << endl;
            break;
        }
    }
}

int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        cerr << "Usage:" << argv[0] << " local-port" << endl;
        exit(0);
    }
    string ip = argv[1];
    uint16_t port = stoi(argv[2]);
    int sockfd = InitClient();
    // auto ref = std::bind(&RecvMessage, sockfd, placeholders::_1);
    //  auto senf = std::bind(&SendMessage, sockfd, ip, port, placeholders::_1, placeholders::_2, placeholders::_3);
    Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));
    Thread sender("sender-thread", std::bind(&SendMessage, sockfd, ip, port, std::placeholders::_1));

    recver.Start();
    sender.Start();
    recver.Join();
    sender.Join();

    close(sockfd);
    return 0;
}
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于socket通信的多人聊天可以通过以下步骤实现: 1. 创建一个服务器端程序,使用特定的端口监听客户端的连接请求。可以使用Python中的socket库来实现。 2. 在服务器端程序中,使用socket库的bind()方法将服务器端的IP地址和端口号绑定到一个socket对象上,并使用listen()方法开始监听客户端连接请求。 3. 在服务器端程序中,使用accept()方法来接收客户端的连接请求,并获得一个与客户端通信的socket对象。 4. 为每个客户端连接创建一个新的线程,以便能够同时处理多个客户端的消息。 5. 在服务器端程序中,使用recv()方法接收客户端发送的消息,并将消息广播给所有已连接的客户端。这样所有客户端之间就可以实现即时的多人聊天。 6. 在服务器端程序中,使用send()方法将服务器端接收到的消息发送给所有已连接的客户端。 7. 在客户端程序中,使用socket库的connect()方法连接到服务器端的IP地址和端口号。 8. 在客户端程序中,使用send()方法将客户端发送的消息发送给服务器端。 9. 在客户端程序中,使用recv()方法接收服务器端发送的消息,并将其显示在客户端的窗口上。 需要注意的是,由于socket通信是基于TCP协议的,因此可以实现可靠的数据传输,但是在实现聊天时,需要处理客户端进程退出的情况,并及时清理相关资源,避免程序异常或资源泄漏等问题的出现。 以上是基于socket通信的多人聊天的简要实现步骤,具体实现过程还需要根据编程语言和具体的需求进行进一步的开发和调试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值