【Linux网络编程】--UDP__SCOKE

下面我们先对服务器端进行封装。

一、简单回显服务器

我们要实现一个简单回显服务器和客服端的数据回显,客户端进行发送消息,服务器收到消息后,再将这个消息回显给我们的用户端。

就类似我们在Linux上使用的echo指令。

整个程序的核心就是套接字的相关接口还有使用UDP协议进行实现。

首先我们需要两个部分,一个是服务器端,用户端。

如上所示。

下面我们先对服务器端进行封装。

EchoServer.hpp

1、初始化Init

首先,对于一个服务器,我们初始时要对其进行初始化操作,然后再去启动,那么我们需要一个Init接口,然后初始化完成,我们再进行启动,所以还需要一个启动接口:Start

前面我们学习系统部分的时候讲到,Linux下一切皆文件,不论我们是使用UDP还是使用TCP,那么我们进行网络通信前,都要将网络文件先打开,那么我们要打开一个网络文件要使用下面这个接口:

要使用其要包含头文件<sys/socket.h>

下面我们来看其返回值:

其返回值是一个整数,那么其返回值:打开成功那么就返回的是一个文件描述符fd。

然后失败返回-1并且会设置其错误信息。

下面是对其参数的认识:

首先是domain,其是一个域,其实就是我们未来创建这个网络文件,这个文件的使用范围,就比如我们是要进行本地通信还是跨网络通信

如上所示,如果我们想要的是本地通信,那么就传入AF_UNIX

如果是网络通信那么就传入AF_INET。其他的我们不需要太过关心。

然后是type:

这个参数就是确定我们通信的类型,就比如说是我们的字节流还是用户数据报的方式。

第三个参数,就是我们要使用的那种协议,就比如是使用UDP协议还是TCP协议,但是前面,第二个参数设置好了,那么通信的协议基本就确定了,所以一般这个参数我们设置为0.

所以对于socket的参数我们设置的也基本是固定的:

第一个参数我们设置为AF_INET,第二个参数就在S0CK_STREAM和SCOK_DGRAM上选择,第三个参数就设置为0即可。

所以就只有第二个参数是变化的,那么其第一个就是我们的TCP第二个就是UDP。

所以未来,我们的服务器端,需要一个文件描述符,表示我们打开的网络文件的fd。

如上,我们就可以将一个网络文件套接字创建了,那么对于错误信息的输出,我们可以引入前面我们写的日志系统进行打印。

那么我们上面将创建好的网络文件的文件描述符进行打印,那么其文件描述符肯定为3:

那么我们初始化,首先要创建一个套接字socke他,那么其本质上就是创建一个网络文件。

那么我们的网络有其IP信息还有其端口号,那么我们也要将其进行设置,那么我们的成员变量还要设置一个ip和端口号,但是有个问题就是,我们前面所看到的端口号是:192.1.1.0这种样式的,这个叫做字符串类型的IP,但是我们发现其是由4个整数和三个点组成的,这种IP地址为字符串风格的点分十进制IP地址,是让人看的。那么其大小就要达到32字节了,所以在网络端其是会转换成4字节IP的。

如上就是我们的IP和端口号,对于服务器来说其_port必须是固定的。

这个是由用户进行设置的。

未来在使用上,我们希望其和我们在Linux下使用一样,./server_udp ip port这个样式进行使用,那么,在我们的主程序中,我们可以如下进行设置:

这个是描述网络socket的结构体,可以看到其中包括了sin_port这个就是我们的端口号,然后还有的就是sin_addr这个就是我们的id。后面是sin_zero是填充字段,我们不需要关注。

然后其还包含了AF_INET,但是有个疑问了,就是我们创建socket的时候,就已经传入了AF_INET了,那么为啥我们这边也要有呢?

这是给我们后续使用别的函数接口的时候进行判断使用的。

但是我们发现上面的结构体并没有出现AF_INET,其实是在第一个那个宏定义中:

其就被替换成下面的东西,然后上面的##,在宏中,其会将两边的进行拼接,那么我们传入的sin_,那么就会被替换成sin_family,那么总的就是sa-family_t sin_famliy。

这个数据类型实际上就是一个无符号长整型。

所以我们对于网络信息的填充如下:

然后有个问题,就是这个数据,是否已经存在我们打开的网络文件呢?

实际上并没有,所以下面我们还需要将这个信息和我们打开的socket进行绑定。

首先,发送方的IP和port都要带入到网络中 ,所以我们在服务器端,要将IP和port进行处理。

如下:

下面进行socket和网络信息进行绑定。

要使用下面这个接口:

首先,第一个参数,就是我们要进行绑定的socket,返回值一样成功返回0,失败返回-1,并且设置错误信息。

第二个参数,我们发现是一个struct sockaddr类型的结构体,那么我们需要将我们的信息进行强制类型转换,第三个参数就是我们的长度。

如下:

所以总的初始化代码如下:

复制代码
void Init()
    {
        // 第一步创建套接字socket,本质就是打开一个网络文件
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socketfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create scoket success,socket:" << _socketfd;

        // 第二步,设置网络信息IP,
        struct sockaddr_in local;
        bzero(&local, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        local.sin_port = htons(_port);

        // 第三步,将信息设置到socket中
        int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"create socket error";
            exit(BIND_ERR);  
        }
        LOG(LogLevel::INFO)<<"bind socket success,ip:"<<_ip<<"port:"<<_port;
    }

2、运行Start

在服务器运行中,其实际上是一个死循环,但是对于数据的发送和接收,其不再是前面的字节流文件流等,所以我们使用下面的函数:

首先是其返回值,就是我们所读到多少字节的数据。失败就返回-1。

下面我们来看其参数:

第一个参数:scokfd,就是我们要绑定的那个网络文件。

第二个参数:其是一个缓冲区,未来就是用来存放我们收到的数据的,实际上是一个void*类型 的,那么其未来可以接收不同类型的数据。

第三个参数:size_t len就设置为我们缓冲区的长度即可。

第四个参数:就是我们要其阻塞读取还是非阻塞读取,我们就设置为0,阻塞读取即可。

第五个参数:和上面使用bind的类似。用来获取对方ip和port的数据

第六个参数:和bind的第三个参数一样

补充:

未来我们获取对方的发送的信息的时候,那么我们不仅需要信息本身,还需要获取到发送方是谁,方便后续回复,所以需要第五个参数,获取发送方的IP和prot。

代码如下:

对于收信息错误,我们并不需要退出。上面我们对于数据,我们先认为传递的是字符串。

那么我们服务器端,接收到消息,那么就要将这个消息发送给接收方用户。

那么就要使用到下面这个接口:

首先其返回值,其失败的话,也是返回-1,成功那么就会返回发送的字节数。

对于发送数据的问题,有下面三个疑问:

首先是我们要发送什么数据,然后我们要发送给谁,如何发送给对方。

那么在这个发送函数中,也要我们传入一个缓冲区,就是第二个参数。

那么发送给谁,那么就是发送给对方的IP+prot。那么就是最后两个参数。

发送的方式,是通过对应的sockfd决定。

我们接收到消息的时候,对方也传入了其对应的IP和prot,所以我们可以如下使用:

代码如下:

复制代码
void Start()
    {
        char inbuffer[1024];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);

            ssize_t n=recvfrom(_socketfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&peer,&len);

            if(n>0)
            {
                inbuffer[n]=0;
                LOG(LogLevel::DEBUG)<<"cilent say#"<<inbuffer;
                std::string echo_string="server  echo#";
                echo_string+=inbuffer;

                sendto(_socketfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);
            }
            else
            {
                LOG(LogLevel::ERROR)<<"recvform error";
            }
        }
        
    }

如上,对于服务器端的设计就大致完成了。

EchoClient.cc

首先,我们的客户端需要进行网络通信,那么其也需要创建一个网络文件,那么其也得进行网络文件的创建:

那么我们的用户端的socket就创建好了,下面我们的用户端也要有其对应的IP和port,这是在网络中标识自己的唯一标志。

但是要注意的是,我们对于客户端的bind,不要去显示bind,这是为啥呢?

因为对于服务端,其未来是1:n的情况,那么我们服务端的端口号一般是固定的。

但是对于用户端,我们对其是多少并不关心,其只需要存在就可以。

并且未来用户端也会打开多个服务端,那么如果显示绑定,那么可能会出现端口号冲突的问题。

所以对于用户端,我们使用随机端口的方式,由OS进行选择。

而且我们发现,日常使用软件的时候,都是先给服务端进行数据发送,所以OS底层会隐式自动进行获取随机端口,然后进行bind IP和prot。

下面我们完成对用户端的消息发送:

首先我们要获取到用户的输入:

但是有个问题,我们得知道要进行通信的服务器,所以这个要我们的用户进行输入,所以在使用这个程序前,我们要告诉用户输入对于服务器的IP和prot。

但是有个问题,就是我们用户又是如何知道对方的IP和prot的呢?

这个信息是内置到客户端中的,是直接包含在内的。所以我们是直接进行硬编码的。

上面我们就把发送方式,还有发送的数据是啥搞定,下面就是发送给谁了,所以我们还需要一个struct socket_in 结构体变量。

未来,我们的发送方要将服务端的IP和prot也要发送给服务器,所以其IP和prot也要转换为网络序列的。

给服务器端发送完数据后,那么就要接收服务器端发送来的数据了。

下面我们来测试两个端口是否能够进行通信。

首先,我们先来进行本地通信,那么在对于本地通信,那么在输入IP的时候,要设置为127.0.0.1

然后对于端口号,我们可以先随便设置,下面我们设置为8080。

上面我们使用的127.0.0.1IP地址,其是本地环回地址,其只是将数据在协议栈中走一遍,不会经过网络。

可以用于本地通信或者网络代码的测试。

那么我们要通过网络进行数据交流又如何操作呢?

首先,我们先补充下面几个问题:

1、云服务器的公网IP,是禁止被显示bind的

2、在服务器开发的时候,不建议用户显示bind的IP地址

3、云服务器的公网IP,是禁止被bind的,但是并不是不能被使用

在我们的云服务器中,是存在多个IP地址的,如果我们的服务器bind了IP,那么其未来只能和这个IP的机器进行通信了,但是未来我们的服务器是要和多个IP的用户进行通信的,所以这也是为啥不能显示bind其IP。

所以我们是需要服务器和任意IP绑定,主要其端口号一样即可。

所以我们将服务器端的IP绑定的IP进行设置为INADDR_ANY。

那么下面我们再来运行看看:

可以看到服务器端和用户端此时可以进行通信

在我们的服务器端,我们也要将用户端的IP和prot获取下来,所以我们在收到用户数据的时候要在服务器端进行获取。

未来,我们的服务器是会被多个用户访问的,所以我们可以将这些用户进行管理。

下面我们来认识一个指令:

netstat

那么其可以把我们当前机器中和网络和通信相关的信息打印出来。

其有如下选项:

二、简单的英译汉的⽹络字典

上面我们实现的是实现网络中进行通信,但是没有任何的业务,所以下面我们将上面的,对于数据进行翻译处理。

上面,我们对于收到数据,对于数据的处理也放在了服务器端,那么下面,我们想让服务器只负责数据的收发,而数据的处理交给别的层次去进行。

代码如下:

复制代码
#pragma once

#include <iostream>
#include <string>
#include <unordered_map>

using namespace NS_LOG_MODULE;

static const std::string default_dict = "./dict.txt";
static const std::string sep = ": "; // 分隔符

class Dict
{
public:
    Dict(const std::string &dict_path = default_dict)
        : _dict_path(dict_path)
    {
        LoadDict();
    }
    ~Dict() {}

    void LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::FATAL) << "open" << _dict_path << "error";
            exit(1);
        }

        std::string line;
        while (std::getline(in, line))
        {
            LOG(LogLevel::FATAL) << "load:" << line << "success";
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "Format:" << line << "success";
                continue;
            }
            std::string k = line.substr(0, pos);
            std::string v = line.substr(pos + sep.size());
            _dict.insert(std::make_pair(k, v));
        }

        in.close();
    }

    std::string Translate(std::string word)
    {
        auto iter = _dict.find(word);
        if (iter != _dict.end())
        {
            return iter->second;
        }
        else
        {
            return "None";
        }
    }

private:
    std::string _dict_path;
    std::unordered_map<std::string, std::string> _dict;
};

三、多用户的服务器

上面我们的服务器和用户端,我们发现用户端都是单用户的,所以下面我们可以通过多线程的方式实现。

那么我们服务器端,将用户进行管理起来,多各个用户进行任何的收发。

我们可以将前面写的线程池引入,我们的服务器端将数据发送到线程池,通过线程池进行任务的转发。

后续我们服务端,对于用户端的管理,我们对于IP和prot进行管道,来确定用户的唯一性。

代码如下:

复制代码
#pragma onec

#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

class UserManager
{

public:
    UserManager()
    {
    }

    void AddUser(const InetAddr &addr)
    {
        if (SearchUser(addr))
        {
            return;
        }
        _users.push_back(addr);
    }

    void DelUser(const InetAddr &addr)
    {

        if (!SearchUser(addr))
        {
            return;
        }

        for (auto iter = _users.begin(); iter != _users.end(); iter++)
        {
            if (*iter == addr)
            {
                _users.erase(iter);
                break;
            }
        }
    }

    bool SearchUser(const InetAddr &addr)
    {
        for (auto &users : _users)
        {
            if (users == addr)
            {
                return true;
            }
        }
        return false;
    }

    bool ModUser(const InetAddr &addr)
    {
        DelUser(addr);
        AddUser(addr);
    }

    ~UserManager()
    {
    }

private:
    std::vector<InetAddr> _users;
};

这是对多用户进行管理的部分。

那么我们服务器的对于数据转发到我们的用户,所以下面我们再设置一个路由转发的功能:

代码如下:

复制代码
#pragma once

#include<iostream>
#include<string>
#include<memory>
#include<sys/socket.h>
#include"Mutex.hpp"
#include"UserManager.hpp"

class Route
{
public:
    Route()
    :_uma(std::unique_ptr<UserManager>())
    {

    }

    void CheckUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->AddUser(addr);
    }

    void OfflineUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->DelUser(addr);
    }

    void Broadcast(int sockfd,std::string message)
    {
        LockGuard lockguard(_lock);
        auto &users=_uma->Users();
        for(auto &user:users)
        {
            sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)user.GetNetAddress(),user.Len());
        }
    }

    

    ~Route();

private:
    std::unique_ptr<UserManager> _uma;
    Mutex _lock;
};
相关推荐
进击切图仔2 小时前
Ubuntu 挂载 XFS 磁盘并免密读写
linux·网络·ubuntu
qetfw2 小时前
Debian Apache2 执行 Shell CGI:动态时间页面配置与验证
linux·运维·debian
Shadow(⊙o⊙)2 小时前
CMake第一章——1.0CMake命令行,拉取源码、平台兼容的门槛技术。
linux·运维·服务器
呱呱巨基2 小时前
CMake基础
linux·c++·笔记·学习
爱丶狸11 小时前
Grafana_Zabbix_ImageRenderer_部署与前端操作手册
linux·前端·zabbix·grafana·kylin
k4m7v2pz12 小时前
把 Bevy 0.14 游戏移植到 R36S 掌机:一场与“无窗口系统“的搏斗
linux·rust·bevy·r36s·rk3326·开源掌机
木子欢儿13 小时前
Intel 架构的 MacBook Pro 运行 Linux 发热量高解决
linux·运维·服务器
会编程的土豆17 小时前
Go 模板初识
linux·运维·服务器
regret~18 小时前
【记录】网络路由、转发与网卡隔离
linux·服务器·网络