自学内容网 自学内容网

Linux《Socket编程UDP》

在之前的网络基础当中我们已经了解了网络基本的概念,了解了计算机当中网络基本的体系结构,并且了解了网络传输的基本流程,学习了在Socket基础的知识,那么接下来在本篇当中我们将来具体的学习Socket编程当中的UDP套接字编程。在此将会使用Socket当中提供的接口来实现客户端和服务器之间的通信,本篇当中将会实现字典翻译和简单聊天室两个基于UDP套接字实现的具体实例,接下来就开始本篇的学习吧!!



1.Socket通信接口

在使用UDP来实现服务器和客户端之间的通信之前,先来详细的了解bind等Socket套接字当中提供的通信接口。

创建socket套接字

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

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

参数:
domin:协议族
    常见的有:AF_INET:IPv4
             AF_INET6:IPv6
             AF_UNIN:本地通信
type:套接字类型
     常见的有:SOCK_STREAM:有连接的TCP
               SOCK_DGRAM:无连接UDP
protocol:指定协议,正常填0即可,系统会自动选择合适的协议


返回值:成功返回套接字的文件描述符,失败返回-1

进行套接字的创建之后就需要使用到bind来进行ip和端口号的绑定

绑定端口


#include <sys/types.h>         
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
    sockfd:套接字文件描述符,即socket的返回值
    addr:一个addr结构体的指针,在该结构体当中存储对应的ip和端口
    addrlen:addr结构体大小

返回值: 
    当绑定成功时返回0,否则返回-1
    

通过以上就可以看出使用bind来进行绑定的,其中函数的参数包括之前使用socket创建出来的套接字文件描述符,除此之外还需要传入一个sockaddr的结构体指针。

在此我时需要使用UDP来进行客户端和服务器之间的连接通信,那么就是进行网络通信,那么在此就需要传一个sockaddr_in的结构体,并且在传入的时候要强制类型转换为sockaddr。

通过之前的学习我们知道sockaddr_in内有以下的成员变量:

struct sockaddr_in {
    sa_family_t    sin_family;  // 地址族:AF_INET(IPv4)
    in_port_t      sin_port;    // 端口号(网络字节序)
    struct in_addr sin_addr;    // IP地址
    unsigned char  sin_zero[8]; // 填充对齐
};

在创建结构体之后只需要将其对象内部的成员变量进行对应的赋值即可。

在进行绑定之后由于UDP是无连接的,那么这时候就可以直接进行数据的传输和接收了,那么接下来就来了解提供进行数据传输和接收的接口。

发数据接口:
 

  #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:使用socket创建的sockfd套接字
    buf:接收数据的缓冲区
    len:接收数据的缓冲区大小
    flags:阻塞式IO标志位
    src_addr:保存发送方指针
    addrlen:发送方指针大小

返回值:
    返回发送成功的数据大小,发送失败时返回-1



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


参数:
    sockfd:使用socket创建的sockfd套接字
    buf:需要发送的数据所在的缓冲区
    len:发送数据缓冲区的大小
    flags:阻塞式IO标志位
    dest_addr:接收方的指针
    addrlen:接收方指针的大小

返回值:
    返回值大于等于0表示发送数据的大小,为-1时表示数据发送失败
    

2.基于UDP套接字实现简单通信

在以上我们已经了解了基于socket套接字进行UDP通信需要使用到的接口,那么接下来就试着来使用以上的接口实现一个客户端和服务器之间的简单通信。实现的效果是客户端向服务器发送对应的数据之后服务器将该数据进行处理接下来再发回给客户端。

在此使用三个文件来实现,分别是在Server.hpp内实现服务器通信调用的Server类;在Server.cc当中实现Server对象的创建,并且调用内部对应的函数;在Client当中实现客户端接发数据的功能。

Server.hpp

首先先将Server.hpp当中Server类基本的框架实现

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include "log.hpp"


const int defaultfd = -1;
class UdpServer
{
public:
    //实现服务器当中各个接口
private:
    //Server类当中的成员变量
    
};

以上实现的框架就先将之前实现的日志文件头文件添加,在Server类当中主要实现两个函数,其中一个是进行socket创建的以及bind绑定的Init,另外一个是进行数据发送和数据接收的Start函数。

在Server当中的成员变量就需要有要进行绑定的端口号以及IP地址,并且还需要使用一个标志位来标识当前的进程是否处于运行的状态。

    //套接字
    int _sockfd;
    //端口号
    uint16_t _port;
    //ip地址
    std::string _ip;
    //进程是否运行标志位
    bool _isrunning;
    

实现Server类的构造函数:

UdpServer(u_int16_t port, std::string &ip)
        : _sockfd(defaultfd),
          _port(port),
          _isrunning(false),
          _ip(ip)
    {
    }

那么接下来就先来实现Init的函数,实现的步骤就是先使用socket来创建出对应的套接字,接下来再使用bind进行对应端口和ip地址的绑定,并且在使用接口的过程当中当出现错误的使用打印出对应的报错日志。

    void Init()
    {
        //创建套接字
        //使用IPv4通信,使用UDP进行通信
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success,sockfd:" << _sockfd;
        //创建sockaddr_in对象
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); 
        local.sin_family = AF_INET;
        //绑定socket信息
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        //显示bind绑定
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success,sockdf:" << _sockfd;
    }

以上就是实现的Init函数,首先使用的就是socket来创建出基于IPv4且使用UDP进行通信的套接字,接下来再创建出一个sockaddr_in结构体的对象之后使用bzero将结构体进行清零初始化。

#include <strings.h>

void bzero(void *s, size_t n);
参数:
    s:对指定的字符串进行清零
    n:字符串的长度

在此以上构造函数当中传入的套接字和ip地址其实都是主机系列的,但是实际上网络当中进行传输的时候使用的是网络系列,那么在此就需要将对应的主机系列转换为对应的网络系列之后才能赋值给local对象当中。

在此使用到的是以下的装换函数:

#include <arpa/inet.h>

//将端口号的主机系列转换为网络系列
uint16_t htons(uint16_t hostshort);

参数:
    hostshort:端口号
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


//将ip地址从主机系列转换为网络序列
in_addr_t inet_addr(const char *cp);

参数:
    cp:字符串形式的ip地址

以上在使用对应的接口将主机序列的ip和端口号转换为网络序列的ip地址和端口号之后接下来就可以将创建出来的local对象以及以上创建的sockdf套接字作为bind函数的参数。并且对bind绑定之后的返回值进行判断,在此判断判定的过程是否成功的进行。

在以上的Init函数当中创建了套接字之后再进行对应的绑定,那么继续在以下的Start函数当中实现客户端和服务器之间的数据通信。

实现代码如下实时:

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            //接收缓冲区
            char buffer[1024];
            //服务器端sockaddr_in对象
            struct sockaddr_in peer;
    
            socklen_t len = sizeof(peer);
            //收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
           //std::cout << buffer << std::endl;
            if (s > 0)
            {
                //将网络系列转换为主机序列
                int perr_prot = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;
                //发消息
                sendto(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&peer, len);
            }
        }
    }

在此使用到以下的转换函数:

#include <arpa/inet.h>

uint16_t ntohs(uint16_t netshort);

参数:
    netshort:网络序列端口号

返回值:
    主机序列的端口号
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


char *inet_ntoa(struct in_addr in);

参数:   
    in:包含网络序列ip的结构体对象

返回值:
    返回字符串形式的ip地址

Server.cc

以上我们在实现了Server.hpp当中的代码之后接下来就可以试着继续将Server.cc当中的代码实现,在该文件当中需要实现的是将用户输入的ip地址和端口号传给实例化出来的Server对象。

实现的代码如下所示:

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



//Server ip port
int main(int argc,char* argv[])
{
    //判断用户输入的命令行参数是否满足要求
    if(argc!=3)
    {
        std::cerr<<"User:"<<argv[0]<<"port"<<std::endl;
        return 1;
    }

    uint16_t port=std::stoi(argv[2]);
    std::string ip=argv[1];
    std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port,ip);
    usvr->Init();
    usvr->Start();


    return 0; 
}

以上就将对应的用户输入的参数传到实例化出来的UdpServer对象当中,接下来再依次的调用usvr当中的Init和Start函数。

Client.cc

以上我们已经将服务器的Server.hpp和Server.cc文件的代码实现,那么接下来就需要来实现服务器的代码。

实现代码如下所示:

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

// Clinet ip port
int main(int argc, char *argv[])
{
    //判断用户输入的命令行参数是否符合要求
    if (argc != 3)
    {
        std::cerr << "Usage:" << argv[0] << "server_ip server_port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    //创建socket套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    //填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    while (1)
    {
        //读取用户输入数据
        std::string input;
        std::cout << "Please input:";
        std::getline(std::cin, input);
        
        //向服务器发送数据
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;
        //接收缓冲区
        char buffer[1024];

        //客户端sockaddr_in结构体对象
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        //接收服务器发回的数据
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                    //std::cout << buffer << std::endl;
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

以上大体的代码逻辑就是先通过用户从命令行当中输入的参数得到对应的要连接的服务器的ip地址和端口号,接下来再进行套接字的创建,接着创建对应的sockaddr_in结构体对象,在此需要注意的是再客户端当中一般是不需要用户显示的进行绑定而是让操作系统来完成对应的工作。

实际上客户端只需要进行将数据传输给服务器即可,进行bind的工作交给操作系统来完成,操作系统会根据当前主机的ip分配对应的端口号给当前进行传输的进程,这样就可以避免当前的客户端和其他的客户端出现端口号冲突的问题。总的来说就是内核自动帮你完成了 “本地IP + 临时端口” 的绑定。

代码测试以及调整

以上我们已经实现了对应的客户端和服务器的代码,那么接下来就继续测试以上实现的代码是否能实现客户端和服务器之间的通信。不过在进行测试的时候先要来了解一个指令——ifconfig。

输入以上的指令之后就可以输出类似以下的内容:

以上就发现存在两个ip,这两个ip实际上第一个是该主机在局域网当中的ip,而第二个ip是主机的内外环回地址,其作用是能让主机能和自己通信。那么在此你可能就会又疑惑了,为什么在我们的云服务器当中是没有存在对应的公网ip的,就是之前连接云服务器使用到的ip,其实一般来说云服务器的厂商都是不会将公网ip配置到云服务器当中的,而是通过 NAT(网络地址转换)虚拟网关转发 的方式进行映射,这样做的目的是保证公网的安全。

接下来就试着先使用本地环回地址来进行Server和Client之间的通信:

通过测试就可以看出确实能实现两个进程之间的通信,但是目前的问题是需要客户端二号服务器传入的ip是一样的才能进行通信,例如以下服务器使用内网ip,而客户端使用内网环回地址就无法实现通信。

这就说明当我们显示的绑定服务器的ip之后,客户端在进行连接的时候就只能使用到服务器bind的ip,但是正常来说服务器正常情况下可能存在多个ip地址,因此实际上是不能将客户端的IP进行显示的绑定的。

在此使用到INADDR_ANY这个宏,本质上就是帮服务器绑定的ip可以是任意的值。

这时修改之后Server.hpp的代码如下所示:

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"

using namespace LogModule;

using func_t = std::function<std::string(const std::string &)>;

const int defaultfd = -1;
class UdpServer
{
public:
    UdpServer(u_int16_t port)
        : _sockfd(defaultfd),
          _port(port),
          _isrunning(false)
    {
    }

    void Init()
    {
        //创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success,sockfd:" << _sockfd;
        //创建sockaddr_in对象
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); 
        local.sin_family = AF_INET;
        //绑定socket信息
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        //显示bind绑定
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success,sockdf:" << _sockfd;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            //接收缓冲区
            char buffer[1024];
            //服务器端sockaddr_in对象
            struct sockaddr_in peer;
    
            socklen_t len = sizeof(peer);
            //收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
           //std::cout << buffer << std::endl;
            if (s > 0)
            {
                //将网络系列转换为主机序列
                int perr_prot = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;
                //std::cout << buffer << std::endl;
                //std::string result = _func(buffer);
                //发消息
                sendto(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
    }

private:
    //套接字
    int _sockfd;
    //端口号
    uint16_t _port;
    //进程是否运行标志位
    bool _isrunning;
};

接下来再客户端和服务器使用不同IP的就可以进行通信了:

以上我们使用内网IP或者是本地环回都能进行对应的bind了,但是当前使用公网IP还是无法进行bind。那么解决的方法是什么呢?

实际上这就需要我们再云服务器当中进行开放端口的设置

在以上进行了云服务器当中的开发端口操作之后就可以实现在我们实现的服务器当中进行公网IP的bind。

以上就实现了简单的基于socket套接字的UDP通信,实现客户端和服务器之间的数据交换。

以上实现的数据返回的功能是在Server类当中的Start函数当中实现的,这时通信和数据的处理是在一个函数当中实现的就显得较为冗余,因此当中就可以使用回调函数的方式来实现解耦。

Start函数修改之后实现的代码如下所示:
 

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            //接收缓冲区
            char buffer[1024];
            //服务器端sockaddr_in对象
            struct sockaddr_in peer;
    
            socklen_t len = sizeof(peer);
            //收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
           //std::cout << buffer << std::endl;
            if (s > 0)
            {
                //将网络系列转换为主机序列
                int perr_prot = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;
                //std::cout << buffer << std::endl;
                std::string result = _func(buffer);
                //发消息
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }

将Server.cc修改为以下的形式:

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

std::string defaultHandler(const std::string& message)
{
    std::string ret="Client say:";
    ret+=message;
    return ret;
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        std::cerr<<"User:"<<argv[0]<<"port"<<std::endl;
        return 1;
    }

    uint16_t port=std::stoi(argv[1]);
    std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port,defaultHandler);
    usvr->Init();
    usvr->Start();


    return 0; 
}

编译代码执行程序之后输出的内容如下所示:

3.基于UDP套接字实现字典翻译功能

以上实现的是基于socket套接字简单的UDP通信,那么接下来基于以上的代码来实现一个在客户端当中用户输入指定的英文单词之后即可得到对应的中文翻译,那么此时服务器要实现的就是得到用户输入的英文单词之后再查询对应中英翻译文件,再将对应的中文翻译返回给客户端。

InetAddr.hpp

在此依旧使用通信和数据处理解耦的方式实现,在Dict.hpp当中实现单词翻译的功能,在Server.hpp当中实现数据的接发。在dictionary.txt当中实现 英文:中文 的键值对,文件当中储存对应的翻译。

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
spring: 春天
autumn: 秋天
father: 父亲
mother: 母亲
brother: 兄弟
sister: 姐妹
water: 水
fire: 火
earth: 地球
sky: 天空
sun: 太阳
moon: 月亮
star: 星星
food: 食物
house: 房子
door: 门
window: 窗户
chair: 椅子
…………

在之前创建对应的sockaddr_in对象的时候都是通过调用对应的htons等函数来实现,那么这样在需要创建较多的对象代码当中就会显得较为繁琐,因此在此实现一个InetAddr进行网络序列和主机序列套接字的转换类。这样就只需要将对应的参数传给类当中的函数即可实现序列的转换。

实现代码如下所示:

#pragma once
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>

class InetAddr
{
public:
    //网络序列转主机序列
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }

    //输出端口号
    uint16_t Port()
    {
        return _port;
    }

    //输出IP地址
    std::string IP()
    {
        return _ip;
    }

private:
    //sockaddr_in结构体对象
    struct sockaddr_in _addr;
    //ip地址 
    std::string _ip;
    //端口号
    uint16_t _port;
};

Dict.hpp

实现了以上的代码之后接下来就可以来实现进行翻译功能Dict.hpp文件的代码,在该文件当中需要实现一个Dict类,在该类的内部主要实现两个函数来提供给外部进行调用,其中一个是将对应的Dictionary文件打开,并且在类当中需要创建一个对应的哈希表来将Dictionary.txt文件当中的英文单词二号对应的中文翻译形成对应的键值对存储到哈希表当中。同时在创建对应的键值对时形成相应的日志来表明键值对的创建是否出现问题,如果出现问题形成对应的日志信息。

那么接下来就先将Dict类当中大体代码结构实现:

#pragma once
#include<iostream>
#include<fstream>
#include<unordered_map>
#include"log.hpp"
#include"InetAddr.hpp" 


using namespace LogModule;

//单词文件的路径
const std::string defaultdict="./dictionary.txt";
//键值对之间的分隔符
const std::string sep=":";


class Dict
{

    public:
    //构造函数
    Dict(const std::string & path=defaultdict)
    :_dict_path(path)
    {

    }


    ~Dict()
    {

    }


    //将对应的中英翻译文件打开,将键值对存储到哈希表当中
    bool LoadDict()
    {
      //……
    }


    //进行单词翻译的功能
    std::string Translate(const std::string &word,InetAddr& peer)
    {
        //……

    }


    private:
    //当前Dictionary.txt文件的路径
    std::string _dict_path;
    //储存单词键值对的哈希表
    std::unordered_map<std::string,std::string> _dict;

};

在此在LoadDict当中需要将Dictionary.txt文件打开,那么这时候就需要进行文件的操作,在此时可以选择使用open等系统调用或者时C当中提供的对应的fopen的函数,但是在此使用C++17当中提供的fstream对应的文件操作接口能更简便的实现。

    bool LoadDict()
    {
        //打开文件
        std::ifstream in(_dict_path);
        //判断打开文件是否成功
        if(!in.is_open())
        {
            LOG(LogLevel::DEBUG)<<"打开字典:"<<_dict_path<<"错误";
            return false;
        }
        //进行行读取文件当中的内容
        std::string line;
        while(getline(in,line))
        {
            //查询行当中英文单词的中文翻译对应的分隔符
            auto pos=line.find(sep);
            if(pos==std::string::npos)
            {
                LOG(LogLevel::WARNING)<<"解析:"<<line<<"失败";
                continue;
            }
            //将字符串当中的中英文进行分离
            std::string english=line.substr(0,pos);
            std::string chinese=line.substr(pos+sep.size());
            //判断中英文是否都存在
            if(english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING)<<"没有有效内容:"<<line;
                continue;
            }
            //向哈希表当中插入中英文对应的键值对
            _dict.insert({english,chinese});
            LOG(LogLevel::DEBUG)<<"加载:"<<line;
        }
        //关闭文件
        in.close();
        return true;
    }

接下来实现了以上创建出对应打开指定文件再哈希表的函数之后,那么接下来再来实现将英文当中翻译为中文的Translate函数。

    //进行翻译功能函数
    std::string Translate(const std::string &word,InetAddr& peer)
    {
        //在储存单词的哈希表当中查询是否存在当前要进行翻译的单词
         auto find=_dict.find(word);
        //单词不存在
         if(find==_dict.end())
         {
            LOG(LogLevel::INFO)<<"用户:"<<peer.IP()<<",端口:"<<peer.Port()<<"翻译"<<"["<<word<<":"<<"None]";
            return "None"; 
         }
         //单词存在,返回对应键值对当中val的值
          LOG(LogLevel::INFO)<<"用户:"<<peer.IP()<<",端口:"<<peer.Port()<<"翻译"<<"["<<find->first<<":"<<find->second<<"]";

         return find->second;

    }

以上我们在实现了对应的翻译功能的文件之后就可以接着来改造之前实现的Server.hpp、Server.cc和Client.cc文件了。

实现代码如下所示:
 

Server.hpp

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include"InetAddr.hpp"

using namespace LogModule;

//将对应的回调函数的参数修改为InetAddr对象
using func_t = std::function<std::string(const std::string &,InetAddr peer)>;

const int defaultfd = -1;
class UdpServer
{
public:
    UdpServer(u_int16_t port, func_t func)
        : _sockfd(defaultfd),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }

    void Init()
    {
        //创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success,sockfd:" << _sockfd;
        //创建sockaddr_in对象
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        //绑定socket信息
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        //显示bind绑定
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success,sockdf:" << _sockfd;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            //std::cout << buffer << std::endl;
            if (s > 0)
            {
                
                int perr_prot = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);
                buffer[s] = 0;
                //std::cout << buffer << std::endl;
                //根据以上的sockaddr_in对象实例化出InetAddr对象
                InetAddr inet(peer);
                std::string result = _func(buffer,peer);
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;
};

Server.cc

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

std::string defaultHandler(const std::string& message)
{
    std::string ret="Client say:";
    ret+=message;
    return ret;
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        std::cerr<<"User:"<<argv[0]<<"port"<<std::endl;
        return 1;
    }
    //实例化出进行翻译功能的Dict对象
    Dict dict;
    //打开对应的dictionary.txt创建出对应翻译的哈希表
    dict.LoadDict();

    
    uint16_t port=std::stoi(argv[1]);
    //使用lambda创建出UdpServer的第二个对象参数,该函数执行的功能是调用Dict对象当中的LoadDict
    std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port,[&dict](const std::string message,InetAddr peer)->std::string{
        return dict.Translate(message,peer);
    });
    usvr->Init();
    usvr->Start();


    return 0; 
}

Client.cc

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

// Clinet ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage:" << argv[0] << "server_ip server_port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    //创建socket套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    //填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    while (1)
    {
        std::string input;
        std::cout << "Please input:";       
        std::getline(std::cin, input);
        
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                    //std::cout << buffer << std::endl;
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

以上本质上整体实现的逻辑就是在服务器当中获取到回调方法进行调用,接着将得到的字符串传给对应的函数,接下来通过该函数得到对应英文的翻译。

将以上的编码进行编译之后,效果如下所示:

通过以上的输出结果就可以看到我们以上实现的代码是没有问题的。

4.基于UDP套接字实现简单聊天室

以上我们实现的UDP套接字实现字典翻译的功能以及以及最开始的简单的通信,那么这时是可以实现了服务器和客户端之间的通信,那么这时是否能使用UDP来实现一个多个客户端之间的通信功能也就是实现出不同用户之间进行通信的版本。

在此大体实现的逻辑就是当客服端将的对应的消息传输给服务器之后,服务器会将该消息转发给当前其他的和服务器进行数据传输的用户。

那么接下来实现的过程还是基于以上的Server.hpp来实现通信,让具体的任务通过回调函数的方式到具体的环节当中进行消息转发的功能。在此创建一个Route.hpp的文件来实现消息转发的功能。

在实现Route.hpp文件的代码之前先将以上实现InetAddr.hpp的文件代码再进行修改补全,之前我们只是实现网络序列到主机序列的转换,那么接下来就将主机序列到网络序列转换的函数也实现。

InetAddr.hpp

InerAddr.hpp实现代码如下所示:

#pragma once
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <cstring>

class InetAddr
{
public:
    // 网络系列转主机系列
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        //_ip = inet_ntoa(_addr.sin_addr);
        char buffer[64];
        inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer));
        _ip=buffer;
    }

    // 主机系列转网络系列
    InetAddr(const std::string ip, int port)
        : _ip(ip),
          _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
    }

    //返回端口号
    uint16_t Port()
    {
        return _port;
    }

    //返回字符串形式的套接字信息
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }

    //返回套接字
    const struct sockaddr_in &GetAddr()
    {
        return _addr;
    }

    //套接字相等运算符重载函数
    bool operator==(InetAddr &peer)
    {
        return _ip == peer._ip && _port == peer._port;
    }

private:
    //套接字结构体
    struct sockaddr_in _addr;
    //ip地址
    std::string _ip;
    //端口号
    uint16_t _port;
};

以上当到了inet_ntopinet_pton来实现网络序列和网络序列之间IP的转换,函数的使用如下所示:

#include <arpa/inet.h>

//IP地址网络序列转主机序列
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

参数:
    af:地址族:AF_INET(IPv4) 或 AF_INET6(IPv6)
    src:输入字符串
    dst:输出缓冲区
    size:输出缓冲区大小
返回值:
    当转换成功时输出对应的字符串的指针,失败适输出NULL


//IP地址主机序列转网络序列
int inet_pton(int af, const char *src, void *dst);

参数:
    af:地址族:AF_INET(IPv4) 或 AF_INET6(IPv6)
    src:输入字符串
    dst:输出缓冲区
返回值:
    当转换成功时返回值为1,否则返回0或者-1


但是当前的问题就来了以上将网络序列准转换为主机序列使用到的时inet_ntoa,而之前使用到的是inet_ntop,这两个函数都能使用对应的转换,那么这两个函数实现的原理上有什么样的区别呢?

实际上在inet_ntoa使用到的缓冲区是静态的缓冲区,那么这就使得在使用该函数的过程当中只有一份的缓冲区可以使用,这就会使得在使用多个inet_ntohs的时候会出现覆盖的问题。
例如以下的代码:

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


int main()
{
    struct in_addr a1,a2;
    inet_pton(AF_INET,"192.168.0.1",&a1);
    inet_pton(AF_INET,"0.0.0.0",&a2);
    const char* p1=inet_ntoa(a1);
    const char* p2=inet_ntoa(a2);

    printf("IP1:%s\n",p1);
    printf("IP2:%s\n",p2);


    return 0;
}

以上的代码当中就是先创建出两个in_addr结构体对象,接下来再使用inet_pton将对应的主机序列IP转换为网络序列,接下来使用inet_ntoa将对应网络序列转换为主机序列并得到对应的字符串指针,那么这时将两个指针指向的内容进行打印就会发现两个组织指向的地址是一样的。

那么这时就可以看出确实在缓冲区当中的数据是被覆盖的,这时输出的结果就都是0.0.0.0

而使用到inet_ntop的时候就不会出现以上的问题了,因为该函数的缓冲区是使用用户创建的,那么这时就不会再出现覆盖的问题。

例如以下代码:

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


int main()
{
    struct in_addr a1,a2;
    inet_pton(AF_INET,"192.168.0.1",&a1);
    inet_pton(AF_INET,"0.0.0.0",&a2);
    char buffer1[1024],buffer2[1024];
    const char* p1=inet_ntop(AF_INET,&a1,buffer1,sizeof(buffer1));
    const char* p2=inet_ntop(AF_INET,&a2,buffer2,sizeof(buffer2));
    printf("IP1:%s\n",p1);
    printf("IP2:%s\n",p2);


    return 0;
}

输出的结果如下所示:

以上就可以将两个IP都正常的输出而不会出现覆盖的问题。

以上将InetAddr.hpp的代码进行修改之后接下来就来将对应实现将一个用户的所有的消息发送给当前所有的用户的功能实现,在此在Route.hpp当中实现,大致需要实现对的功能就包括两个方面:1.判断当前的用户是否之前是否已经进行数据的传输,若没有将用户的数据添加到对应的哈希表当中 2.将从客户端当中得到的数据转发给其他的客户端当中

了解以上的要求之后接下来就来试着实现文件当中的代码:

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "log.hpp"
#include "InetAddr.hpp"
#include"Mutex.hpp"

using namespace LogModule;
using namespace MutexNamespace;

class Route
{

private:
    // 判断用户是否存在
    bool IsExit(InetAddr &peer)
    {
        for (auto &x : _online_user)
        {
            if (x == peer)
                return true;
        }

        return false;
    }
    // 首次通信添加用户信息
    void AddUser(InetAddr &peer)
    {
        LOG(LogLevel::INFO) << "新增加一个在线用户" << peer.StringAddr();
        _online_user.push_back(peer);
    }

    // 删除用户信息
    void EraseUser(InetAddr &peer)
    {
        auto it = _online_user.begin();
        while (it != _online_user.end())
        {
            if (*it == peer)
            {
                LOG(LogLevel::INFO) << "删除一个在线用户" << peer.StringAddr() << "成功";
                _online_user.erase(it);
                break;
            }
            it++;
        }
    }

public:
    Route()
    {
    }
    ~Route()
    {
    }

    // 消息转发
    void MessageRoute(int sockfd, const std::string &Message, InetAddr &peer)
    {
        LockGuard lock(_mutex);
        //std::cout<<Message<<std::endl;
        // 判断用户是否存在
        if (!IsExit(peer))
        {
            // 添加用户
            AddUser(peer);
        }

        std::string send_message = peer.StringAddr() + "#" + Message;
        for (auto &x : _online_user)
        {
            sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&x.GetAddr(), sizeof(x.GetAddr()));
        }

        // 判断用户是否进行退出请求
        if (Message == "QUIT")
        {
            LOG(LogLevel::INFO) << "删除一个在线用户" << peer.StringAddr();
            EraseUser(peer);
        }
    }

private:
    //存储用户信息的数组
    std::vector<InetAddr> _online_user;
    //访问用户数组使用到的锁
    Mutex _mutex;
};

Server.hpp

以上就实现了对应进行消息转发的函数MessageRwoute,那么这时在Server.hpp当中就可以通过回调函数的方式来实现。该函数的参数有三个分别是对应的套接字、消息字符串、和进行消息发送的InetAddr对象。

接下来就将之前实现的Server.hpp在进行修改,不过修改的内容其实不多,只需要在Start当中做出轻微的调整即可。

以下只将该文件当中相比之前实现不同的内容写出来,其他的不做修改:

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include"InetAddr.hpp"

using namespace LogModule;

//回调函数
using func_t = std::function<void(int sockfd,const std::string &,InetAddr peer)>;

const int defaultfd = -1;
class UdpServer
{
public:
  //……

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
           //std::cout << buffer << std::endl;
            if (s > 0)
            {
                
                buffer[s] = 0;
                //std::cout << buffer << std::endl;
                InetAddr client(peer);
                //将对应的消息处理传给回调函数来处理
                 _func(_sockfd,buffer,client);
                //sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }

  //……

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;
};

Server.cc

将以上的Server.hpp实现之后接下来就继续将Server.cc的代码也进行修改,实际上和之前修改Server.hpp类似也是只需要一小部分的代码即可。

实现代码如下所示:
 

#include<iostream>
#include<memory>
#include"UdpServer.hpp"
#include"Route.hpp"
#include"ThreadPool.hpp"
#include<functional>

using namespace ThreadPoolModule;

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



int main(int argc,char* argv[])
{
    //判断命令行是否符合要求
    if(argc!=2)
    {
        std::cerr<<"User:"<<argv[0]<<"port"<<std::endl;
        return 1;
    }
    // Dict dict;
    // dict.LoadDict();

    //创建进行数据转发功能的Route对象
    Route r;
    //创建线程池对象
    ThreadPool<func_t2>* tp=ThreadPool<func_t2>::GwetInstance();


    uint16_t port=std::stoi(argv[1]);
    //创建Server对象
    std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port,[&r,&tp](int sockfd,const std::string& message,InetAddr peer){
        //使用bind进行参数绑定
        func_t2 t=std::bind(&Route::MessageRoute,&r,sockfd,message,peer);
        //将任务插入到线程池任务队列当中
        tp->Enqueue(t);
    });
    usvr->Init();
    usvr->Start();


    return 0; 
}

以上依旧是使用到回调函数的方式来进行,并且在Server对象的创建当中第二个参数是使用lambda表达式,表达式当中的使用bind来进行参数绑定。

Client.cc

以上我们已经将服务器部分的代码实现完毕,那么接下来就只需要来实现Client.cc也就是客户端的代码即可。

实现代码如下所示:

#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "Thread.hpp"

using namespace ThreadModlue;

int sockfd = 0;
//发送消息线程id
pthread_t tid;
//服务器IP地址
std::string server_ip;
//服务器端口号
uint16_t server_port = 0;

//发消息线程执行的函数
void Send()
{
    // 填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    const std::string online = "inline";
    sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));

    while (1)
    {
        std::string input;
        std::cout << "Please input:";
        std::getline(std::cin, input);

        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;
        if (input == "QUIT")
        {
            //std::cout<<"用户退出"<<std::endl;
            pthread_cancel(tid);
            break;
        }
    }
}

//收消息线程执行函数
void Reseve()
{
    pthread_detach(pthread_self());
    while (1)
    {
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        // std::cout << buffer << std::endl;
        if (m > 0)
        {
            buffer[m] = 0;
            //将收到的消息输出到标准错误当中
            std::cerr << buffer << std::endl;
        }
       

    }
}

// Clinet ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage:" << argv[0] << "server_ip server_port" << std::endl;
        return 1;
    }
    server_ip = argv[1];
    server_port = std::stoi(argv[2]);
    // 创建socket套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    //创建两个线程分别来进行消息的接和收
    Thread send(Send);
    Thread rev(Reseve);
   

    send.Start();
    rev.Start();
     tid = send.Tid();

    send.Join();
    //rev.Join();

    return 0;
}


 


以上就将基于UDP套接字实现的简单聊天室实现了,实际上大致的原理图如下所示:

本质上我们实现对的聊天室当中的Server端的实现就是应该生产消费模型,生产者就是对应的获取消息的Server对象,消费者就是线程池当中的线程,交易场所就是线程池当中的任务队列,服务器将得到的消息给线程池当中,接下来线程池就可以进行任务分配的工作。

接下来就将以上的代码进行编译之后形成对应的可执行程序Server和Client。运行程序查看是否能实现客户端和服务器之间的通信。

通过以上的输出可以看出我们实现的代码是能实现基本的通信的,但是目前的问题是用户输入和输出的消息是混杂在一起的,这和我们平时使用的聊天消息分离的正常逻辑是违背的。

实际上我们也可以通过Qt等来实现图形化的聊天室界面,但是目前我们还是使用简单的方式来实现即可,在此我们解决的方案是之前在实现的代码当中将客户端当中输出输出到标准错误当中,那么我们只需要将客户端当中的标准错误输出到另外的shell当中即可。

先查看/dev/pts当中的shell编号,使用ls来进行查看,接下来通过echo来判定不同的shell的编号。

如上所示就让上方的shell来输出服务器发送的消息,使用下方的shell来进行获取用户输入的内容。
启动客户端和服务器之后就能实现以下形式的聊天室。

接下来就试着使用不同的服务器进行通信,在此将可执行程序Client拷贝到另外应的华为云当中,接下来运行该程序就可以看到在聊天室当中能实现对应的消息。

以上就将基于UDP套接字简单聊天室的实现了,完整的代码如下:

UDP_test_chathttps://gitee.com/chasing-dreams-zhuo/linux/tree/master/UDP_test_chat

实际上除了以上的Linux客户端之间对的通信之外还可以使用UDP来实现Linux客户端和Windows之间的通信。

例如以下代码:

#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = "1.12.75.231"; // 填写你的云服务器 ip
uint16_t serverport = 8080; // 填写你的云服务开放的端口号


int main()
{
WSADATA wsd;
WSAStartup(MAKEWORD(2, 2), &wsd);
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());
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == SOCKET_ERROR)
{
std::cout << "socker error" << std::endl;
return 1;
}
std::string message;
char buffer[1024];
while (true)
{
std::cout << "Please Enter@ ";
std::getline(std::cin, message);
if (message.empty()) continue;
sendto(sockfd, message.c_str(), (int)message.size(), 0,(struct sockaddr*)&server, sizeof(server));
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
} 
closesocket(sockfd);
WSACleanup();
return 0;
}

在VS2022当中运行以上的程序就可以实现Windows和Linux之间的消息通信。

以上就是本篇的所有内容了,接下来我们将继续学习TCP套接字及其使用,未完待续……


原文地址:https://blog.csdn.net/2303_81098358/article/details/153685771

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!