UDP网络程序

发布于:2024-04-20 ⋅ 阅读:(27) ⋅ 点赞:(0)

上一章中,我们介绍了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号文件描述符)重定向到管道文件中,就能成功将读写分离。