UDP网络程序

上一章中,我们介绍了socket,以及TCP/UDP协议。这一章带大家实现几个UDP协议的网络服务。我们需要一个 服务端和一个客户端。

1.服务端实现

1.1socket函数

#include <sys/types.h>     
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明:

  • domain:域,标识了这个套接字的通信类型(网络通信/本地通信),这个就是sockaddr结构体的前16个bit位。如果是本地通信就是AF_UNIX,如果是网络通信就是AF_INET。 
  • type:套接字提供的服务类型,常见的就是SOCK_STREAMSOCK_DGRAM,如果我们是基于TCP协议的通信,就使用SOCK_STREAM,表示的是流式服务。如果我们是基于UDP协议通信的,就使用SOCK_DGRAM,表示的是数据包服务。
  • protocol:创建套接字的类型(TCP/UDP),但是这个参数可以由前两个参数决定。所以通常设置为0

返回值:

成功会返回一个文件描述符,失败返回-1,错误码被设置

在系统的文件操作中,也会返回文件描述符,与socket返回的文件描述符不同的是,普通文件的文件缓冲区对应的是磁盘,而socket返回的文件描述符的文件缓冲区对应的是网卡。用户将数据写到缓冲区,由操作系统自动将缓冲区中的数据刷新到网卡中,网卡会负责将这个数据发送到对端主机上。

class UdpServer
{
public:
    UdpServer()
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(1);
        }
       std::cout << "create socket success, socket: " << _socket << std::endl;
    }

    ~UdpServer()
    {
        close(_socket);
    }

private:
    int _socket;
};

调用socket函数,就能创建一个套接字了,第一个参数我们填AF_INET,表示的是我们是网络通信。第二个参数我们填SOCK_DGRAM,表示我们是UDP服务(数据报)。由于一个进程启动时,默认会打开标准输入,标准输出,标准错误这三个文件描述符,而文件描述符的创建规则就是从0开始,向上找到第一个没有使用的,所以我们可以猜测以下_sock的值为3.


1.2bind函数

我们创建完套接字以后,也只是打开了一个文件。还没有将这个文件和网络关联起来,所以我们需要使用bind函数,将IP+port和文件绑定

#include <sys/types.h>     
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数介绍:

  • sockfd:socket函数的返回值。
  • addr:通用结构体,包括协议家族,IP地址,端口号port
  • addrlen:addr的长度

返回值介绍:

成功返回0,错误返回-1

sockaddr_in结构体

struct sockaddr_in
  {
    short int sin_family;
    in_port_t sin_port;			
    struct in_addr sin_addr;	

    unsigned char sin_zero[8];
  };
  • sin_family:协议家族,表示通信类型(AF_INET网络通信,PF_INET本地通信)
  • sin_port:端口号(网络字节序)
  • sin_addr:IP地址。
  • sin_zero:填充字段,让sizeof(sockaddr_in) = 16

既然第二步需要添加IP和端口号,所以我们要对代码修改一下,给UDP的构造函数添加port和str(IP的点分十进制表示形式)

class UdpServer
{
public:
    UdpServer(const uint16_t &port, const std::string &str = "")
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(1);
        }
        std::cout << "create socket success, socket: " << _socket << std::endl;

        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(str.c_str());

        int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
        if (n < 0)
        {
            std::cout << "bind error" << std::endl;
            exit(2);
        }
        std::cout << "bind success " << std::endl;
    }

    ~UdpServer()
    {
        close(_socket);
    }

private:
    int _socket;
};

需要注意的是在填写sockaddr_in的参数的时候,

  • sin_family需要和socket套接字创建时的domain相同。
  • 由于端口号和IP将来是要发送到网络的,而网络数据流统一采用大端的形式,所以为了代码的可移植性,我们不管自己是大端还是小端,统一调用函数hton转化成大端。
  • 对于IP来说,如果直接发送的是点分十进制形式如(192.168.12.80)的形式,则需要占用过多的字节数,对网络传输无疑是一种很大的消耗,所以我们采用一个32位的整数来表示IP,在网络传输中,要将点分十进制的IP转化成整数,再进行传输。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);

in_addr_t inet_addr(const char *cp); //点分十进制转整数

in_addr_t inet_network(const char *cp);

char *inet_ntoa(struct in_addr in); //整数转点分十进制

struct in_addr inet_makeaddr(int net, int host);

in_addr_t inet_lnaof(struct in_addr in);

in_addr_t inet_netof(struct in_addr in);

这里重点学习两个,inet_addr和inet_ntoa。


1.3运行服务器

首先我们要知道,服务器就是一个死循环,在启动之后永远不会退出。

#include <iostream>
#include <memory>
#include "UdpServer.hpp"

void Usage()
{
    std::cout << "Please enter:  ./UdpServer [ip] port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage();
        return 3;
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[argc - 1]);
    std::unique_ptr<UdpServer> p(new UdpServer(port, ip));
    p->Start();

    return 0;
}

我们可以使用命令行参数的形式,在启动这个程序时就把IP和端口号传递过去。


1.4IP的绑定

ifconfig操作系统中用于配置和显示网络接口参数的命令行工具。它主要用于查看和设置网络接口(如以太网和 Wi-Fi)的详细信息,包括 IP 地址、子网掩码、广播地址、MAC 地址等。

这里的127.0.0.1表示的时本地环回,用于在本地进行测试,如果本地测试成功,将来网络测试出现问题,那么大概率就网络的问题。本地换回顾名思义就是只会在本地不会发送到网络当中。

当我们运行上面的程序的时候,也可以时netstat函数查看网络情况,

  • -a或--all 显示所有连线中的Socket。
  • -l或--listening 显示监控中的服务器的Socket。
  • -n或--直接使用IP地址(数字),而不是域名。
  • -p或--显示正在使用Socket的程序识别码和程序名称。
  • -t或--tcp 显示TCP传输协议的连线状况。
  • -u或--udp 显示UDP传输协议的连线状况。

我们可以看到确实能看到我们刚刚运行的程序成功bind了一个IP和端口号。

  • 但是在实际情况下,服务器时不建议绑定一个固定IP的。
  • 安全性:攻击者可能会针对这个固定的IP地址进行攻击,比如DDoS攻击或者其他类型的网络攻击。相比之下,使用动态IP或者负载均衡等技术可以更好地分散攻击,提高系统的安全性。
  • 可用性:在某些情况下,固定IP可能并不总是可用的。例如,在云服务环境中,IP地址可能会因为服务器的迁移或重新部署而发生变化。如果服务端绑定到这样的固定IP,当IP地址发生变化时,服务端可能无法正常工作,导致服务中断。
  • 一个服务器可能会有多个IP(多张网卡),当客户端发送数据时,每张网卡都会收到该数据,如果我们想访问指定的某一个端口8080,并且如果指定了IP,那么只能由那一个指定的IP接受数据,但是如果我们绑定的是任意IP,那么只要是发送给8080端口的,任意一个网卡接受到了都会向上交付给服务器。

 addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());

所以我们修改一下,如果创建服务端的时候,传了IP,那就用传的IP,如果没用,那就用我们INADDR_ANY其实就是(0.0.0.0),绑定之后,只要是发送到这台主机上,端口号为XXX(我这里是8080)的,就将数据全部交给这个进程。

1.5读取数据recvfrom

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

参数介绍:

  • sockfd:从哪个套接字中读取
  • buf:读到的数据存放的缓冲区
  • len:读len个字节
  • flags:读取方式,0表示阻塞读取
  • src_addr:发送端的信息
  • addrlen:输出型参数,表示src_addr的长度(必须初始化为sizeof(src_addr))
void Start()
    {
         while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            socklen_t addrlen = sizeof(addr);
            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            std::string ip = inet_ntoa(addr.sin_addr);
            uint16_t port = ntohs(addr.sin_port);
            if (n > 0)
            {
                printf("[%s:%d]# %s", ip.c_str(), port, temp);
            }
        }
    }

src_addr用来保存发送端的信息,我们在前面使用bind函数的时候,也介绍过sockaddr结构,里面包含了发送端的IP和端口port,但是因为是网络序列,我们需要将他转化成主机序列,所以使用ntoh函数,ip是32位整数,我们想转化成点分十进制形式方便观看,于是可以使用inet_ntoa函数。

1.6服务端整体代码

#pragma once

#include <iostream>
#include <string>
#include <unordered_set>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class UdpServer
{
public:
    UdpServer(const uint16_t &port, const std::string &str = "")
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(1);
        }
        std::cout << "create socket success, socket: " << _socket << std::endl;

        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());

        int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
        if (n < 0)
        {
            std::cout << "bind error" << std::endl;
            exit(2);
        }
        std::cout << "bind success " << std::endl;
    }

    void Start()
    {
         while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            socklen_t addrlen = sizeof(addr);
            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            std::string ip = inet_ntoa(addr.sin_addr);
            uint16_t port = ntohs(addr.sin_port);
            if (n > 0)
            {
                printf("[%s:%d]# %s", ip.c_str(), port, temp);
            }
        }
    }

    ~UdpServer()
    {
        close(_socket);
    }

private:
    int _socket;
};

2.客户端实现

服务端的创建和客户端类似。我们也需要创建套接字

2.1创建套接字

void Usage()
{
    std::cout << "Please enter:  ./Client [ip] port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        return 3;
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[argc - 1]);
    // 创建套接字
    int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (clientsocket < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }
    std::cout << "create socket success, socket: " << clientsocket << std::endl;

    return 0;
}

2.1绑定问题

客户端必须绑定IP和端口,但是不用显示的bind

这句话是什么意思呢,就是我们需要将网卡文件和IP+端口进行绑定,但是不需要我们自己手动绑定,操作系统会自动帮我们绑定一个端口号。

为什么不能显示绑定呢,首先我们要知道,一个端口只能对应一个进程,现在有两家公司,各推出了一款客户端,如果他们想让端口不冲突,那就不能让自己家的客户端绑定的端口和对方的一样,如果一样就会起冲突,但是互联网上有很多家公司,难道都要协商一下,哪个端口谁来用吗。所以我们采用了让操作系统帮我们自动分配一个没有使用的端口号。

当我们调用了类似与sendto(发送信息)的函数的时候,操作系统会帮我们自动分配一个端口号,也就是说,客户端每次启动时的端口号可能都不相同。

  • 服务端的端口号为什么是固定的

服务端的端口号是众所周知的,如果每次都不一样的话,客户端就找不到服务器了

2.3发送信息sendto

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

参数介绍:

  • sockfd:从哪个套接字中读取
  • buf:将缓冲区中的数据发给对端
  • len:要发送多少个字节
  • flags:写入方式,0表示阻塞写入
  • src_addr:对端主机的信息(包括协议家族,IP,port)
  • addrlen:表示src_addr的长度

参数和recvfrom类似

// 直接给服务器send,系统会自动帮我们bind
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    while (true)
    {
        std::string buffer;
        std::cout << "Please enter# ";
        std::getline(std::cin, buffer);
        sendto(clientsocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
    }

当我们将客户端发送消息的逻辑 完成之后就可以正常开始通信了。

我们先启动服务端,再启动客户端(两个不同的进程)

命令行中提示我们输入

当我们在右边客户端输入的消息之后,就能成功发送给服务端了。当我们再次使用netstat查看网络情况

当我们再次使用netstat查看网络情况,就可以看到客户端和服务端正在运行,并且能看到各自的端口号。

这样我们就实现了一个简单的服务端客户端模型,下面是源码:

2.4源码

UdpServer.hpp

//Server.hpp

#pragma once

#include <iostream>
#include <string>
#include <unordered_set>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class UdpServer
{
public:
    UdpServer(const uint16_t &port, const std::string &str = "")
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(1);
        }
        std::cout << "create socket success, socket: " << _socket << std::endl;

        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());

        int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
        if (n < 0)
        {
            std::cout << "bind error" << std::endl;
            exit(2);
        }
        std::cout << "bind success " << std::endl;
    }

    void Start()
    {
        while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            socklen_t addrlen = sizeof(addr);

            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            std::string ip = inet_ntoa(addr.sin_addr);
            uint16_t port = ntohs(addr.sin_port);
            if (n > 0)
            {
                temp[n] = 0;
                printf("[%s:%d]# %s\n", ip.c_str(), port, temp);
            }
        }
    }

    ~UdpServer()
    {
        close(_socket);
    }

private:
    int _socket;
};


Server.cc

#include <iostream>
#include <memory>
#include "UdpServer.hpp"

void Usage()
{
    std::cout << "Please enter:  ./UdpServer [ip] port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3 && argc != 2)
    {
        Usage();
        return 3;
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[argc - 1]);
    if (argc == 2)
        ip = "";
    std::unique_ptr<UdpServer> p(new UdpServer(port, ip));
    p->Start();

    return 0;
}

Client.cc

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

void Usage()
{
    std::cout << "Please enter:  ./Client [ip] port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        return 3;
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[argc - 1]);
    // 创建套接字
    int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (clientsocket < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }
    std::cout << "create socket success, socket: " << clientsocket << std::endl;

    // 直接给服务器send,系统会自动帮我们bind
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    while (true)
    {
        std::string buffer;
        std::cout << "Please enter# ";
        std::getline(std::cin, buffer);
        sendto(clientsocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
    }


    return 0;
}

3.更多功能

在前面我们学习了最基础的服务端客户端编写,让客户端给服务端发消息,服务端直接显示,我们可以让服务端在拿到数据后,对数据进行特殊处理。

    void ExecuteCommand()
    {
        while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            memset(&addr, 0, sizeof(addr));
            socklen_t addrlen = sizeof(addr);
            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            std::string ip = inet_ntoa(addr.sin_addr);
            uint16_t port = ntohs(addr.sin_port);
            if (n > 0)
            {
                // 接收到数据
                temp[n] = '\0';
                std::string buffer = temp;
                // 对数据做处理


                // 将数据写回
                sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
            }
        }
    }

我们可以设计出这样一个函数,就是前面的Start,但是在拿到数据后,先将数据进行处理,再将数据发送给客户端。我们下面要完成的就是这个对数据处理的函数。

3.1回显echo

很简单,我们收到用户的数据之后,并不需要处理直接返回即可

void ExecuteCommand()
    {
        while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            memset(&addr, 0, sizeof(addr));
            socklen_t addrlen = sizeof(addr);
            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            std::string ip = inet_ntoa(addr.sin_addr);
            uint16_t port = ntohs(addr.sin_port);
            if (n > 0)
            {
                // 接收到数据
                temp[n] = '\0';
                std::string buffer = temp;
                // 对数据做处理
                EchoMessage(buffer);

                // 将数据写回
                sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
            }
        }
    }

    void EchoMessage(std::string &buffer)
    {
        return;
    }

注意,这里的EchoMessage函数虽然什么都没有做,但是,为了突出用户发的信息是被处理过的,我们还是添加了一个函数。

也是可以成功运行。

3.2大写转换

将客户端发送的小写字母转化成大写字母

    void ExecuteCommand()
    {
        while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            memset(&addr, 0, sizeof(addr));
            socklen_t addrlen = sizeof(addr);
            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            std::string ip = inet_ntoa(addr.sin_addr);
            uint16_t port = ntohs(addr.sin_port);
            if (n > 0)
            {
                // 接收到数据
                temp[n] = '\0';
                std::string buffer = temp;
                // 对数据做处理
                //EchoMessage(buffer);
                Transformed(buffer);

                // 将数据写回
                sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
            }
        }
    }

    void Transformed(std::string &buffer)
    {
        for (auto &ch : buffer)
        {
            if ('a' <= ch && ch <= 'z')
            {
                ch -= 32;
            }
        }
        return;
    }

我们只需要将调用的函数替换一下,就能完成不同的业务。

3.3英译汉词典

将用户发送的英文转化成汉语

我们先创建一个哈希表,保存英语单词以及对应的意思

static std::unordered_map<std::string, std::string> Dict;

void TranslationInit()
{
    Dict.insert({"apple", "苹果"});
    Dict.insert({"pear", "梨子"});
    Dict.insert({"banana", "香蕉"});
    Dict.insert({"orange", "橘子"});
    Dict.insert({"left", "左"});
    Dict.insert({"right", "右"});
    Dict.insert({"sun", "太阳"});
    Dict.insert({"moon", "月亮"});
}
    void ExecuteCommand()
    {
        while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            memset(&addr, 0, sizeof(addr));
            socklen_t addrlen = sizeof(addr);
            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            std::string ip = inet_ntoa(addr.sin_addr);
            uint16_t port = ntohs(addr.sin_port);
            if (n > 0)
            {
                // 接收到数据
                temp[n] = '\0';
                std::string buffer = temp;
                // 对数据做处理
                //EchoMessage(buffer);
                //Transformed(buffer);
                Translation(buffer);


                // 将数据写回
                sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
            }
        }
    }

    void Translation(std::string &buffer)
    {
        auto it = Dict.find(buffer);
        if (it == Dict.end())
        {
            buffer = "not find";
        }
        else 
        {
            buffer = it->second;
        }
    }

3.4执行命令

执行用户发送的shell命令,例如pwd,ls等。

我们介绍一个函数

 #include <stdio.h>

 FILE *popen(const char *command, const char *type);

 int pclose(FILE *stream);

popen函数会fork创建子进程,并且让子进程程序替换执行command命令,最终把结果写到一个文件当中。type就是以什么方式打开这个文件(w/r/a)。

std::unordered_set<std::string> forbid = {
    "rm",
    "mv",
    "kill",
    "cp"
};
    
void ExecuteCommand(std::string &buffer)
{
    // 查询有无禁止命令
    if (!sercharforbid(buffer))
    {
        buffer = "you can't do that\n";
        return;
    }

    // 使用popen执行用户命令,并将结果写入fp中
    if (buffer == "ll")
    {
        buffer = "ls -l --color=auto";
    }
    FILE *fp = popen(buffer.c_str(), "r");
    if (fp == nullptr)
    {
        buffer = "command is unknow";
        return;
    }
    // 读取信息
    buffer.clear();
    char temp[1024];
    while (fgets(temp, sizeof(temp), fp) != NULL)
    {
        buffer += temp;
    }

    pclose(fp);
}

bool sercharforbid(const std::string &buffer)
{
    for (auto com : forbid)
    {
        int pos = buffer.find(com);
        if (pos != std::string::npos)
        {
            return false;
        }
    }

    return true;
}

我们将一些命令保存在哈希桶中,并且不让用户执行这些命令,例如rm删除之类。

3.5网络聊天室

将来会有很多用户加入这个聊天室,一个用户发送消息,能让其他用户都看到这条消息。

我们先创建一个类,这个类对sockaddr_in进行封装。

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

class InetAddr
{
public:
    InetAddr(const sockaddr_in addr)
        : _addr(addr)
    {
        _ip = inet_ntoa(addr.sin_addr);
        _port = ntohs(addr.sin_port);
    }

    std::string GetUser()
    {
        std::string temp;
        temp += _ip;
        temp += " : ";
        temp += std::to_string(_port);

        return temp;
    }

    std::string& GetIp()
    {
        return _ip;
    }

    uint16_t GetPort()
    {
        return _port;
    }

    sockaddr_in& GetAddr()
    {
        return _addr;
    }

private:
    std::string _ip;
    uint16_t _port;
    sockaddr_in _addr;
};

这个类会提取出sockaddr_in中的IP和端口并保存,GetUser函数会返回一个IP+端口号的字符串。

我们再创建一个保存用户信息的哈希桶

std::unordered_map<std::string, InetAddr> _users;

void AddUser(InetAddr &user)
{
    std::string userMessage = user.GetUser();
    if (_users.find(userMessage) != _users.end())
    {
        return;
    }
    _users.insert({userMessage, user});
}

每当有用户发消息时,我们根据用户的sockaddr_in就能提取出这个用户的IP+端口,如果这个用户是第一次发消息,我们就把他的信息保存起来。

void Route(size_t sock, std::string message)
{
    //将message发送给每一个用户
    for (auto user : _users)
    {
        sockaddr_in addr = user.second.GetAddr();
        sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&addr, sizeof(addr));
    }
}

最后再根据哈希桶中保存的用户信息,就能再将数据发送给每一个用户了。

我们就完成了一个简单的网络聊天室,但是我们通过实验会发现还存在很大的问题,由于我们的服务端是单线程阻塞式读取,所以当别人发送数据的时候,我们可能正在阻塞,并不会显示数据,只有在写之后,数据才会重新打印出来。

上面我们的测试也能反应这一点,我们的执行顺序是,从上至下从左至右,每次发一条消息,一共两次。可以看到第二次左上那个进程才收到了右上第一次发的消息,这很显然不复合实际中的网络通信。

所以我们需要将服务端改成线程池版本,服务端改成两个线程(一个读一个写)

#pragma once

#include <iostream>
#include <unordered_set>
#include <unordered_map>
#include <functional>
#include <unistd.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "LogMessage.hpp"
#include "ExistReason.hpp"
#include "ThreadPool.hpp"
#include "LocalGuard.hpp"
#include "Pthread.hpp"
#include "InetAddr.hpp"


using task_t = std::function<void()>;

class ChatServer
{
public:
    ChatServer(const uint16_t &port, const std::string &str = "")
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket < 0)
        {
            Log::LogMessage(Error, "create socket error");
            exit(CREATE_SOCKET_ERROR);
        }
        Log::LogMessage(Debug, "create socket success, socket: %d", _socket);

        // 进行bind
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());

        int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
        if (n < 0)
        {
            Log::LogMessage(Error, "bind error");
            exit(BIND_ERROR);
        }
        Log::LogMessage(Debug, "bind success");

        pthread_mutex_init(&_user_mutex, nullptr);
        ThreadPool<task_t>::GetInstance()->Start();
    }

    ~ChatServer()
    {
        pthread_mutex_destroy(&_user_mutex);
        close(_socket);
    }

    void Start()
    {
        while (true)
        {
            char temp[1024];
            sockaddr_in addr;
            memset(&addr, 0, sizeof(addr));
            socklen_t addrlen = sizeof(addr);
            int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
            temp[n] = 0;

            InetAddr user(addr);
            if (n > 0)
            {
                // 将用户信息保存起来
                AddUser(user);
                std::string message = "[";
                message += user.GetUser();
                message += "] ";
                message += temp;

                // 将任务push到队列当中
                task_t task = std::bind(&ChatServer::Route, this, _socket, message);
                ThreadPool<task_t>::GetInstance()->Push(task);
            }
            else if (n == 0)
            {
                // 对端关闭连接
                Log::LogMessage(Debug, "close connection");
            }
            else
            {
                Log::LogMessage(Warning, "server recvfrom warning");
            }
        }
    }

private:
    void AddUser(InetAddr &user)
    {
        LockGuard lock(&_user_mutex);
        std::string userMessage = user.GetUser();
        if (_users.find(userMessage) != _users.end())
        {
            return;
        }
        _users.insert({userMessage, user});
    }

    void Route(size_t sock, std::string message)
    {
        //将message发送给每一个用户
        LockGuard lock(&_user_mutex);
        for (auto user : _users)
        {
            sockaddr_in addr = user.second.GetAddr();
            sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&addr, sizeof(addr));
        }
    }

private:
    int _socket;
    std::unordered_map<std::string, InetAddr> _users;
    pthread_mutex_t _user_mutex;
};

我们使用线程池,提前创建好几个线程,将来只要有一个用户发消息了,就指派一个线程去处理数据(将这个信息发送给其他用户)。

客户端

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

#include "LogMessage.hpp"
#include "ExistReason.hpp"
#include "Pthread.hpp"

void Usage()
{
    std::cout << "Please enter:  ./Client [ip] port" << std::endl;
}

struct ThreadDate
{
    ThreadDate(int sock, sockaddr_in addr)
        : _sock(sock), _addr(addr)
    {
    }

    int _sock;
    sockaddr_in _addr;
};

void Sender(ThreadDate date)
{
    while (true)
    {
        std::string buffer;
        std::cout << "Please enter# ";
        std::getline(std::cin, buffer);
        sendto(date._sock, buffer.c_str(), buffer.size(), 0, (sockaddr *)&date._addr, sizeof(date._addr));
        if (sendto <= 0)
        {
            std::cerr << "send error" << std::endl;
        }
    }
}

void Recver(ThreadDate date)
{
    char mes[1024];
    while (true)
    {
        sockaddr_in add;
        socklen_t addlen = sizeof(add);
        int n = recvfrom(date._sock, mes, sizeof(mes) - 1, 0, (sockaddr *)&add, &addlen);
        if (n > 0)
        {
            mes[n] = '\0';
            std::cerr << mes << std::endl;
        }
    }
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        return USE_ERROR_MANUAL;
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[argc - 1]);
    // 创建套接字
    int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (clientsocket < 0)
    {
        exit(1);
    }

    // 直接给服务器send,系统会自动帮我们bind
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());

    ThreadDate date(clientsocket, addr);
    Thread<void, ThreadDate> sender(Sender, date);
    Thread<void, ThreadDate> recver(Recver, date);
    sender.Create();
    recver.Create();

    sender.Jion();
    recver.Jion();

    return 0;
}

主进程创建两个线程,一个线程进行等待,一个线程发送数据。

最终我们发现就解决了之前的问题,但是这样又有了新的问题,由于是多进程,向屏幕打印时会有出错。我们可以使用管道,注意一个细节,客户端的收消息进程在收到消息时打印使用的是cerr,我们只需要将标准错误(2号文件描述符)重定向到管道文件中,就能成功将读写分离。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/538236.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【JAVA基础篇教学】第六篇:Java异常处理

博主打算从0-1讲解下java基础教学&#xff0c;今天教学第五篇&#xff1a; Java异常处理。 异常处理是Java编程中重要的一部分&#xff0c;它允许开发人员在程序运行时检测和处理各种错误情况&#xff0c;以保证程序的稳定性和可靠性。在Java中&#xff0c;异常被表示为对象&am…

【 书生·浦语大模型实战营】作业(三):“茴香豆” 搭建你的RAG 智能助理

【 书生浦语大模型实战营】学习笔记&#xff08;三&#xff09;&#xff1a;“茴香豆” 搭建你的RAG 智能助理作业 &#x1f389;AI学习星球推荐&#xff1a; GoAI的学习社区 知识星球是一个致力于提供《机器学习 | 深度学习 | CV | NLP | 大模型 | 多模态 | AIGC 》各个最新AI…

JVM参数列表

-client :设置JVM使用client模式,特点启动较快(神机不明显(I5/8G/SSD)) -server :设置JVM使用server模式。64位JDK默认启动该模式 -agentlib:libname[options] :用于加载本地的lib -agentlib:hprof :用于获取JVM的运行情况 -agentpath:pathnamep[options] :加载制定路径的本…

PHP01——php快速入门 之 使用phpstudy快速搭建PHP环境

PHP01——php快速入门 之 使用phpstudy快速搭建PHP环境 0. 前言1. 下载小皮面板1.1 下载phpstudy&#xff08;小皮面板&#xff09;1.2 启动、简单访问1.2.1 启动Apache1.2.2 访问1.2.3 访问自定义文件或页面 2. 创建网站2.1 创建网站2.2 可能遇到的问题2.2.1 hosts权限问题&am…

极海APM32电机驱动板记录(二)

文章目录 1、解除写保护2、极海驱动板资源概述3、新建工程4、点灯5、嘀嗒定时器6、中断7、串口打印8、adc读取9、i2c尝试10、定时器测试11、电机驱动pwm测试 上一篇文章算是简单了解了一下极海的板子开发环境吧&#xff0c;结果前几天板子来了&#xff0c;然后发现一个大bug&am…

力扣题目 19:删除链表的倒数第N个节点 【python】

&#x1f464;作者介绍&#xff1a;10年大厂数据\经营分析经验&#xff0c;现任大厂数据部门负责人。 会一些的技术&#xff1a;数据分析、算法、SQL、大数据相关、python 欢迎加入社区&#xff1a; 码上找工作http://t.csdnimg.cn/Q59WX 作者专栏每日更新&#xff1a; LeetCod…

Qt-绘制多边形、椭圆、多条直线

1、说明 所有的绘图操作是在绘图事件中进行。mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWi…

C++ 类和对象(一)

目录 0.前言 1.面向过程&面向对象 1.1面向过程编程&#xff08;PP&#xff09; 1.2面向对象编程&#xff08;OOP&#xff09; 1.3从C到C 2.类的引入 2.1C语言中的结构体 2.2C中类的引入 2.3结构体与类的区别 2.4为什么引入类 3.类的定义 3.1声明与定义不分离 …

【Java探索之旅】从输入输出到猜数字游戏

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; Java编程秘籍 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一、输入输出1.1 输出到控制台1.2 从键盘输入 二、猜数字游戏2.1 所需知识&#xff1a…

【动态规划】【01背包】Leetcode 1049. 最后一块石头的重量 II

【动态规划】【01背包】Leetcode 1049. 最后一块石头的重量 II 解法 ---------------&#x1f388;&#x1f388;题目链接&#x1f388;&#x1f388;------------------- 解法 &#x1f612;: 我的代码实现> 动规五部曲 ✒️确定dp数组以及下标的含义 dp[j]表示容量为…

Learn SRP 01

学习链接&#xff1a;Custom Render Pipeline (catlikecoding.com) 使用Unity版本&#xff1a;Unity 2022.3.5f1 1.A new Render Pipeline 1.1Project Setup 创建一个默认的3D项目&#xff0c;项目打开后可以到默认的包管理器删掉所有不需要的包&#xff0c;我们只使用Unit…

陆面、生态、水文模拟与多源遥感数据同化

原文链接&#xff1a;陆面、生态、水文模拟与多源遥感数据同化https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247601198&idx6&sn51b9b26b75c9df1f11dcb9a187878261&chksmfa820dc9cdf584df9ac3b997c767d63fef263d79d30238a6523db94f68aec621e1f91df85f6…

算法——字符串

T04BF &#x1f44b;热门专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 此篇文章与大家分享字符串相关算法 如果有不足的或者错误的请您指出! 目录 1.最长公共前缀1.1解析1.2题解 2.最长回文子串2.1解析2.2题解 3.二级制求和3.1解析3.2题解 4.字符串相乘4.1解析4.2…

【环境变量】常见的环境变量 | 相关指令 | 环境变量系统程序的结合理解 | 环境变量表 | 本地变量环境变量 | 外部命令内建命令

目录 常见的环境变量 HOME PWD SHELL HISTSIZE 环境变量相关的指令 echo&env export unset 本地变量 环境变量整体理解 程序现象_代码查看环境变量 ​整体理解 环境变量表 环境变量表的传递 环境变量表的查看 内建命令 少说废话&#x1f197; 每个用…

大型网站系统架构演化

大型网站质量属性优先级&#xff1a;高性能 高可用 可维护 应变 安全 一、单体架构 应用程序&#xff0c;数据库&#xff0c;文件等所有资源都在一台服务器上。 二、垂直架构 应用和数据分离&#xff0c;使用三台服务器&#xff1a;应用服务器、文件服务器、数据服务器 应用服…

JavaEE 初阶篇-深入了解 CAS 机制与12种锁的特征(如乐观锁和悲观锁、轻量级锁与重量级锁、自旋锁与挂起等待锁、可重入锁与不可重入锁等等)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 乐观锁与悲观锁概述 1.1 悲观锁&#xff08;Pessimistic Locking&#xff09; 1.2 乐观锁&#xff08;Optimistic Locking&#xff09; 1.3 区别与适用场景 2.0 轻…

我企业的业务需要制作企业网站吗?11个支持的理由以及5个反对的理由!

如果你的企业经营得还不错&#xff0c;你可能会找出很多理由&#xff0c;说明为什么一个高效的网站对你来说并不那么重要。确实&#xff0c;你明白企业需要在互联网上有一定的存在感&#xff0c;但你可能并不认为一个高效的网站会对你的特定业务产生太大的影响——尤其是当你已…

实战纪实 | 编辑器漏洞之Ueditor-任意文件上传漏洞 (老洞新谈)

UEditor 任意文件上传漏洞 前言 前段时间在做某政府单位的项目的时候发现存在该漏洞&#xff0c;虽然是一个老洞&#xff0c;但这也是容易被忽视&#xff0c;且能快速拿到shell的漏洞&#xff0c;在利用方式上有一些不一样的心得&#xff0c;希望能帮助到一些还不太了解的小伙…

PCIe总线-存储器域和PCIe总线域访问流程(二)

1.概述 PCIe总线的最大特点是像CPU访问DDR一样&#xff0c;可以直接使用地址访问PCIe设备&#xff08;桥&#xff09;&#xff0c;但不同的是DDR和CPU同属于存储器域&#xff0c;而CPU和PCIe设备属于两个不同的域&#xff0c;PCIe设备&#xff08;桥&#xff09;的地址空间属于…

[RK3399 Linux] 使用busybox 1.36.1制作rootfs

一、 编译、安装、配置 busybox 1.1 下载源码 根文件系统是根据busybox来制作的。 下载地址:https://busybox.net/downloads/。 这里就以1.36.1版本为例进行编译安装介绍: 注意:编译linux内核与文件系统中的所有程序要使用相同的交叉编译器。 下载完成后解压: mkdir …