网络——套接字编程UDP

        我们现在想要实现一个聊天功能,一个客户端向服务端发送信息,服务端收到信息,并把这个用户对应的信息保存起来,再把消息从服务端发送回客户端;此时我们再来一个用户,一样的操作,这时候两个客户端应该可以看到两个人发送的信息。

// 添加一个成员变量
std::unordered_map<std::string, struct sockaddr_in> _users; // IP-PORT : sockaddr_in

// 修改一下Start成员函数

char buffer[SIZE];
char key[64]; // 将ip-port写到key中
for (;;)
{
    // ...
    ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
    if (s > 0)
    {
        // ...
        snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);
        logMessage(NORMAL, "key: %s", key);
        auto it = _users.find(key); // 把信息写到map中
        if (it == _users.end())
        {
            logMessage(NORMAL, "add new user: %s", key);
            _users.insert({key, peer});
        }
    }

    // 写回数据
    for (auto& iter : _users)
    {
        std::string sendMessage = key;
        sendMessage += "# ";
        sendMessage += buffer; 
        logMessage(NORMAL, "push message to %s", iter.first.c_str());
        sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr*)&iter.second, sizeof(iter.second));
    }
}

        想法很美好,但是现实往往和想象中的不一样,一开始还行,后面打印的都是什么东西,原因就是IO被阻塞了,就是当我们getline拿用户输入的数据的时候,后面的sendto和recvfrom都不会执行,所以现在就可以使用多线程,一个线程发数据,另一个线程负责收数据。

        这就有一个要注意的点,不管是读数据还是写数据都要用sock,如果使用多线程就要把sock设置成全局的,或者再把客户端封装成一个类,成员变量对于整个类也是全局的。那会不会有线程安全的问题呢,这也是没有的,因为在多线程之前就要创建出socket,而线程只是用这个socket,并不会修改它,所以可以并发访问。

        我们再把之前已经封装好的线程拿过来,这样sock会直接传入ThreadData中,所以也就不需要把sock定义成全局的。

#include "thread.hpp"

// port、ip
uint16_t serverport = 0;
std::string serverip;

// .udp_client ip port
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}

static void *udpSend(void *args)
{
    // 拿到sock
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    // 填充服务端信息
    std::string message;
    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::cerr << "请输入:";
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }

    return nullptr;
}

static void *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    char buffer[1024];
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return nullptr;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    // 1.创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    serverport = atoi(argv[2]);
    serverip = argv[1];

    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock)); // 发送线程
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock)); // 接收线程

    sender->start();
    recver->start();

    sender->join();
    recver->join();

    close(sock);

    return 0;
}

        至此多线程就写好了,虽然用的socket都是同一个,但是没有读写冲突的情况,因为UDP是全双工的,可以同时进行收和发,不会受到干扰。

        我们在目录下使用mkfifo创建两个管道文件client1和client2,将客户端输出重定向到管道文件,再使用cat输出重定向,这就好像类似于一个输入框和一个显示框。

上一篇:【mysql基础】mysql 四种类型语句常见操作-一. DDL (Data Definition Language)


下一篇:Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之五 简单局部/整体马赛克效果