【计算机网络】UDP服务器实现网络聊天室

前言

上一篇文章我们简单了解了一下什么是套接字编程,这篇文章我们利用UDP套接字来实现一个简单的网络聊天室。

编写UDP套接字服务器

成员变量

// 1. socket的id,相当于文件id
int _sock;
// 2. port
uint16_t _port;

// 3 一个线程负责收放消息;另一个线程负责发消息
Thread *c; // 收消息
Thread *p; // 发消息

// 4 环形队列来实现生产消费
RingQueue<std::string> rq;

// 5 信息要群发给所有人,要记录所有人的ip和端口号
std::unordered_map<std::string, sockaddr_in> _users;

// 6 一把锁保证在读取要发送给哪些人时是安全的
//  在读取_users时,另一个线程收到了消息,会修改这个map
pthread_mutex_t _mmtx;

成员函数

  • 构造函数:

今天我们知道,对于服务器而言,需要指定端口号。我们在初始化服务器的时候,对锁和线程都进行初始化。

UdpServer(uint16_t port = default_port) : _port(port)
{
    pthread_mutex_init(&_mmtx,nullptr);

    p = new Thread(1,std::bind(&UdpServer::Recv,this));
    c = new Thread(1,std::bind(&UdpServer::BroadCast,this));
}

  • 析构函数

析构函数完成资源释放,不要忘记等待进程。

~UdpServer()
{
    pthread_mutex_destroy(&_mmtx);

    p->join();
    c->join();

    delete p;
    delete c;
}

  • Start
void start()
{
    // 1 创建socket接口,打开网络文件
    _sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKED_ERR);
    }
    std::cout << "create socket success" << std::endl;

    // 2 给服务器指明IP地址和Port
    struct sockaddr_in local;
    memset(&local, sizeof(local), 0);

    local.sin_family = AF_INET;
    local.sin_port = htons(_port);      // host to network short
    local.sin_addr.s_addr = INADDR_ANY; // bind本主机任意ip
    if (bind(_sock, (sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind socket error" << std::endl;
        exit(BIND_ERR);
    }
    std::cout << "bind socket success" << std::endl;

    c->run();
    p->run();
}

  • addUser

因为UDP不面向连接,要构建一个聊天室,就必须要记录下曾经所有给服务器发送过消息的主机,这样才能转发信息。因为是向map里插入数据,本身是线程不安全的,需要加锁。

void addUser(std::string &name, sockaddr_in &peer)
{
    LockGuard lg(&_mmtx);
    _users[name] = peer;
}

  • recv
    接口细节我们已经在上一节着重说过,这里不做赘述。
void Recv()
{
    char buffer[1024];
    // 网络服务都是循环!
    while (true)
    {
        // recv里有两个输出型参数
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len);
        if (n > 0)
        {
            buffer[n] = 0;
        }
        else
        {
            continue;
        }

        // 发消息人的ip和port
        std::string client_ip = inet_ntoa(peer.sin_addr); // net->ascii
        uint16_t client_port = ntohs(peer.sin_port);

        // 构造了发消息人的姓名
        std::string name = client_ip + "-" + to_string(client_port);

        addUser(name, peer);
        rq.push(name + buffer);
    }
}

  • broadcast
    这里我们用了一个临时数组来存储要广播发送的主机,再向这个数组内的所有主机发送队列里的消息。那这样做有什么好处呢?

先把用户拷贝走,再让这个线程去独立发送数据,这样可以让使用map的其他线程更快得到锁,因为拷贝的过程要比发送数据快得多。

void BroadCast()
{
    while (true)
    {
        std::string send_string;
        rq.pop(&send_string);

        std::vector<sockaddr_in> v; // 临时数组,存放要发给那些人

        {
            LockGuard lg(&_mmtx);
            for (auto user : _users)
            {
                v.push_back(user.second);
            }
        }

        for (auto user : v)
        {
            sendto(_sock, send_string.c_str(), send_string.size(), 0, (sockaddr *)&user, sizeof(user));
        }
    }
}

编写UDP客户端

客户端的编写和服务器端编写大同小异。有一个地方需要我们注意一下,套接字编程需要绑定ip和端口号,我们在服务端也确实这样做了,那么客户端需不需要bind端口号和ip呢

答案是肯定需要,只是这个工作不是由我们来干了,而是操作系统替我们做了,在第一次发送的时候自动绑定ip和端口号。

原因也是很好想的,对于客户端来说,端口号都是随机的,自然应该让操作系统帮忙。

void *recv(void *args)
{
    int sock = *(static_cast<int *>(args));
    char buffer[1024];
    sockaddr_in temp;
    socklen_t len = sizeof(temp);

    while (true)
    {
        int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&temp, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "请输入程序名 + 对方ip + 对方端口号" << std::endl;
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKED_ERR);
    }
    // 系统在我们第一次调用系统调用发送的时候,会自动给我绑定ip和端口号

    // 填充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 r_id;
    pthread_create(&r_id, nullptr, recv, (void*)&sock);

    while(true)
    {
        std::string message;
        std::cout << "请输入..." << std::endl;
        getline(std::cin,message);

        sendto(sock,message.c_str(),message.size(),0,(sockaddr*)&server,sizeof(server));
    }

    pthread_join(r_id,nullptr);

    return 0;
}

效果测试

这样一个简单的Udp服务器就完成了。
【计算机网络】UDP服务器实现网络聊天室_第1张图片

你可能感兴趣的:(计算机网络,网络,计算机网络,udp)