小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【linux】网络套接字编程(一)端口号port,认识UDP协议和TCP协议,网络套接字,socket,bind——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】网络套接字编程(二)UDP服务器与客户端的实现,recvfrom,sendto,inet_addr,bzero


一、UDP服务器UdpServer.hpp

  1. 关于UDP协议的介绍,以及socket,bind,htons接口的使用,端口号bind的介绍,小编已经在后方蓝字链接文章中进行了讲解 详情请点击<——
  2. 关于IP地址的讲解 详情请点击<——
  3. 关于function包装器的使用 详情请点击<——
  4. 关于日志的实现与基本使用讲解 详情请点击<——
  5. 上述的文章与UDP服务器的编写具有一定的铺垫作用,所以希望读者友友先学习上述文章之后再来进入本文的学习更加轻松
  6. 那么对于UDP的服务器,我们希望进行封装为UdpServer.hpp,并且在包含main函数中的文件中包UDP服务器的头文件UdpServer.hpp进行调用,所以下面我们就来先实现一下UDP服务器

基本框架

  1. 首先我们需要了解一下一个UDP服务器的类UdpServer中应该包含什么成员变量,在之前的文章中,我们知道UDP协议需要使用到网络套接字,所以也就必然要使用到socket创建套接字,即应该有对应的套接字文件描述符sockfd_,那么服务器还应该有本主机的IP地址ip_(由于用户喜欢字符串风格的点分十进制的IP地址,所以IP地址我们在类内的类型是string字符串类型,但是在使用的时候,应该将其转换成整数的网络字节流序列),本主机要绑定的端口号port_
  2. 同时在UdpServer这个类实例化对象的时候,我们先不创建套接字,而是先简单的在构造函数中初始化成员变量对应的初始值,我们希望提供一个Init接口对外可以进行调用,当外部调用了Init接口后,才真正的创建套接字,将IP地址与端口号port绑定到网络套接字
  3. 此时服务器还没有跑起来,我们还应该对外提供一个Run成员函数,Run成员函数接收一个函数方法fun,当外部调用这个Run函数的时候,要传入一个方法,即将你想要让服务器执行什么方法交给上层,实现一定程度上的分层,当调用Run函数,传入方法的时候,服务器才算真正的运行起来
  4. 那么也就意味着UdpServer实例化对象的时候,以及Init的时候,UDP服务器仅仅是进行的初始化,并没有真正的运行,所以我们在UdpServer的成员变量中还应该提供一个字段,即isrunning_用于表示服务器是否在运行
  5. 咋一看,小编,小编怎么这么多头文件,要使用的接口一定很多很难吧,这里小编告诉大家,不要怕,任何的高楼大厦都是一步一步的搭建起来的,下面小编会带领大家一步一步的将这个“高楼大厦”搭建起来
  6. 我们的代码中,例如后续要进行创建的套接字,进行绑定都有可能会出现错误,所以这里我们先使用枚举enum将这些错误枚举出来
  7. 下面我们首先创建一个全局的日志lg用于后面代码编写的日志信息的打印,日志打印的方式小编就不打印到文件中了,而是直接打印到显示器上更为直观,并且日志的设计默认就是打印到显示器上
  8. UDP客户端与服务器之间传输的我们在这里预定,即互相之间传输字符串,那么在UdpServer服务器中就需要对这个字符串进行处理,处理方法我们在UDP服务器中并没有实现,而是在Run中的参数中,将这个处理方法交给了用户去实现,所以为了更好的接收这个参数,既然是字符串,那么这个方法就应该接收一个字符串,将字符串进行处理,最后将 处理的结构进行返回,所以我们就可以使用包装器包装一个参数类型为string的,返回值也为string的一个数据类型,并且使用typedef将这个数据类型重定义为fun_t
  9. 关于IP地址,这里需要补充一下,由于小编这里使用的是云服务器,云服务器禁止直接绑定公网IP地址,所以这里的IP地址小编就不将其设置为小编的云服务器的IP地址了,并且在服务器上,一般不喜欢绑定IP地址,因为一个服务器上一旦绑定好IP地址,运行起来之后,就无法更改绑定的IP地址了,即服务器此时只能接收这一个IP地址的消息
  10. 一台电脑上可能会绑定多个网卡,TCP/IP网络协议栈规定一个网络接口对应一个IP地址,所以一个服务器上有可能对应的不止一个IP地址,即一台主机对应多个IP地址,这没问题,因为IP地址的作用是唯一的标识一台主机,所以多个IP地址标识同一台主机也没问题
  11. 所以一旦服务器将IP地址绑定死,那么它只能接收一个IP地址在网络层向上递达的数据,其它IP地址将无法被服务器接收,所以为了避免这种情况,我们可以将服务器的IP地址设置为0,即对应“0.0.0.0”,这样的话凡是发送我这台主机的数据(如何理解发送:即到网络层的数据帧的报头中的目的IP地址和本主机多个IP地址中的一个IP地址相同即为发送到我这台主机的数据),都要根据端口号向上交付,所以服务端我们定义一个字符串defaultip给IP地址的默认值设置为“0.0.0.0”
  12. 关于端口号port,这里需要补充一下,端口号的大小是2个字节,即16个比特位,同样的端口号不存在负数,所以端口号的数据范围最大是2的16次方 - 1,那么端口号的取值范围即[0,65535],而[0,1023]是系统规定的端口号,一般要由固定的应用层协议去使用,例如http:80,https:443等。所以我们可以使用的端口号的范围应该是[1024,65535]这个区间,那么今天小编就以端口号8080为例进行讲解,所以默认的端口号我们设置8080
  13. 但是在云服务器上端口号还是很坑,因为云服务默认不开放端口号进行绑定,所以我们如果想要在云服务上使用端口号,那么就需要打开云服务器特定的端口号,小编在最初的时候,就是由于不会端口号失败被狠狠的坑了一把,以为代码逻辑上有问题,结果调Bug浪费了好几个小时,最后才发现是之前绑定的端口号失败了,不过还好小编现在已经会绑定腾讯云服务器的端口号了,下面小编就来讲解一波
    在这里插入图片描述
  14. 首先,进入腾讯云服务器的控制台,找到登录并点击上图红色框框内的任意位置,进入服务器主界面
    在这里插入图片描述
  15. 接下来点击上方防火墙后,找到下面的添加规则并点击
    在这里插入图片描述
    在这里插入图片描述
  16. 然后应用类型默认自定义即可,来源选择全部IPv4地址,由于本文小编使用的是UDP协议进行的网络通信数据传输,所以协议类型选择UDP协议,端口则输入你想要绑定的端口号即可,最后下方点击确定即可添加成功端口号
  17. 所以接下来我们就可以设置端口号了,那么端口号的类型是uint16_t类型的,所以我们设置一个默认的端口号defaultport为8080即可,后续要拿到网络文件描述符中的数据需要用到一个缓冲区,所以这里我们定义一下缓冲区的大小size为1024后面直接拿来用即可
  18. 那么接下来就是构造函数了,在构造函数中我们设置缺省参数,然后进行初始化各个成员变量即可,由于仅仅是对字段进行初始化,所以服务器并没有Run运行起来,所以isrunning_字段要设置为false
  19. 值得注意的是,我们在构造函数中尽量要避免做一些有风险的事情,尽量将有风险的事情放在Init函数中做,因为在一些场景中,例如这里的创建套接字失败,绑定失败那么在构造函数进行try_catch异常处理很难受,并且如果出异常的地方是初始化列表,这个try-catch甚至无法使用,进而也就无法得知哪里出问题,该如何进行调试,所以风险较高,无论是从安全,以及后期调试上来看,类的构造函数不适合做有风险的事情,将有风险的事情放到成员函数Init更好
  20. 那么在析构函数中,唯一需要释放是资源就是网络文件描述符sockfd_了,所以我们进行检查网络文件描述符是否有效,如果有效close关闭网络文件描述符即可
#pragma once

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


enum{
    SOCKET_ERR = 1, 
    BIND_ERR
};


Log lg;
typedef std::function<const std::string(const std::string&)> func_t;

const int defaultsockfd = -1;
const std::string defaultip = "0.0.0.0";
const uint16_t defaultport = 8080;
const int size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip)
        : sockfd_(defaultsockfd), ip_(ip), port_(port), isrunning_(false)
    {}

    ~UdpServer()
    {
        if(sockfd_ > 0)
            close(sockfd_);
    }
private:
    int sockfd_;
    std::string ip_;
    uint16_t port_;
    bool isrunning_;
};

Init

  1. 那么在初始化Init部分,我们要完成socket创建套接字,struct sockaddre_in本地属性初始化,bind套接字绑定,下面小编将依次进行讲解
  2. 创建套接字要使用socket,并且我们创建的是UDP网络套接字,UDP是一个使用IPv4地址的网络通信协议,所以socket的第一个参数选择协议家族(域)AF_INET,UDP是面向数据报进行通信的,所以选择协议类型为SOCK_DGRAM,最后一个参数默认为0即可
  3. socket的返回值是一个文件描述符,这里是创建的网络套接字,所以这里socket的返回值是网络文件描述符,既然是文件描述符,那么类型也就是int了
  4. 既然是文件描述符,我们这里可以大胆推测,这个socket的返回值网络文件描述符sockfd_的值是3,因为在此之前系统默认打开了标准输入,标准输出,标准错误三个分别是0,1,2,所以这里我们继续创建一个网络文件对象对应的网络文件描述符应该是3,所以我们使用日志打印一下这个sockfd_的值,在后续的测试中进行测试即可
  5. socket如果创建套接字失败,那么会返回一个小于0的数,所以接下来创建套接字之后,我们通过socket的返回值sockfd_是否小于0判断创建套接字是否失败,如果失败了,那么我们就使用日志lg打印信息即可,并且exit终止返回之前枚举的错误码即可,如果创建套接字成功,那么我们打印日志创建套接字成功,并且还可以打印一下sockfd_的值验证是否是3
  6. 本地属性初始化,网络套接字的属性应该设置在struct sockaddre_in结构体中,它的头文件是#include <netinet/in.h>,如何记忆呢?很简单,我们只需要记忆inet_addr即可,它的头文件中就有#include <netinet/in.h>,而#include <netinet/in.h>中恰好包含struct sockaddre_in
    在这里插入图片描述
inet_addr
  1. 那么inet_addr是干什么的呢?inet_addr是用于将字符串类型的点分十进制的IP地址转换为网络字节序列对应的大端序列,即如果想要将字符串风格点分十进制的IP地址转换为网络字节序列,传入c风格的字符串给inet_addr,它的返回值就是网络字节序列,那么它的原理,即如何将字符串转换为整数呢?如下,但是我们先来看一下如何将整数转换为字符串
    在这里插入图片描述
  2. 如上第二行即为要转换成的点分十进制的字符串风格的IP地址,原始整数是32比特位的src_ip,首先定义一个struct ip的结构体,这个结构体中包含四个字段,都是8个比特位,即uint_t类型的,part1,part2,part3,part4,恰好这四个字段合起来4 * 8 = 32正好对应一个整数,所以接下来我们就可以开始转换了
  3. 首先将src_ip整数强转为struct ip*的一个指针,然后让类型为struct ip*的p指针指向整数强转后的结果,所以此时p指针指向了整数src_ip,那么所以p指针的类型是struct ip*,所以其中四个字段分别对应这个整数的一个字节,即8个比特位,所以可以使用p->part的方式取出8个比特位,那么而点分十进制分隔的恰好就是一个字节,8个比特位,所以依次取出p->part使用to_string转换为字符串并且加上点分隔即可将整数转换为字符串
    在这里插入图片描述
  4. 那么将点分十进制的字符串风格的IP地址转换为整数,首先就要将这个点分十进制的字符串风格的IP地址按照点进行字符串分隔,得到被点分隔的四个字符串,然后定义一个32比特位大小的整数IP,接下来由于是要对这个整数IP进行修改,所以我们将其取地址,然后强转为struct ip*类型,并且让类型为struct ip*的指针x指向它,接下来就可以使用x->part访问到这个整数四个字节上的每一个字节了,所以将之前的四个字符串使用stoi转换为整数按照次序赋值即可,所以此时就完成了字符串到整数的转换
  5. 同样的值得注意的是,这个IP地址将来是要放到套接字中将来发送到网络中的,所以同样还要将这个IP地址转换为网络字节序列,即大端字节序,那么如果你的主机是大端机,那么按照次序进行依次赋值得到的就是大端字节序的地址,即高字节放到低地址处,192即为高地址,使用x->part1访问的就是最开始的地址即为低地址,符合
  6. 那么如果你的机器是小端机,此时网络字节序要求大端字节序,此时就不符合要求了,那么网络字节序,即字节序就是以字节为基本单位的,所以将192赋值到x->part4,187赋值到x->part3,3赋值到x->part2,2赋值到x->part1,即进行倒序赋值即可,此时192是数据的高位,那么x->part4访问的是高地址处,符合小端字节序,即将数据的高位放到高地址处
  7. 小编,小编,这个将字符串类型的点分十进制的IP地址转换为网络字节序列对应的大端序列还是有点麻烦的,有没有什么好用的方法?有的,直接使用inet_addr(ip.c_str())即可,这样就直接完成了两步,即将点分十进制的IP地址转换为整数,将这个整数转换为网络字节序列,即大端序列
bzero

在这里插入图片描述

  1. 所以下面我们终于可以进入本地属性的初始化了,那么首先在本地定义一个struct sockaddre_in类型的网络套接字要进行绑定的对应类型,那么先使用bzero依次传入要设置的字段(struct sockaddre_in结构体)的地址,然后传入字段(struct sockaddre_in结构体)的大小,然后bzero就会将其字段全部设置为0(其实这里也可以使用memset,只不过这里小编是为了拓展一下bzero的使用),接下来正式初始化其中的字段即可,那么都有什么字段呢?如下
    在这里插入图片描述

  2. 那么首先sin_即sa_family_t类型,即协议家族(域),UDP使用的协议家族是AF_INET进行初始化,即表示使用IPv4地址的一个网络套接字对应的协议

  3. 那么sin_port即网络套接字,所以这里我们使用类中定义的port_进行初始化即可,但是这里不可以直接初始化,应该先将port_端口号使用htons转化为网络字节序列才可以,因为这个端口号将来是要发送到网络中的,而再网络中的字节序列必须是大端,所以要先使用htons将port_端口号转化为网络字节序列对应的大端字节序列

  4. 接下来在struct sockaddre_in中的sin_addr网络IP地址居然还是一个结构体类型的,那么我们看一下这个结构体中包含什么?只有一个in_addr_t s_addr,其中这个in_addr_t就是uint32_t类型的整数了,即对应IP地址,所以在访问修改的时候,我们要使用local.sin_addr.s_addr的方式才可以最终访问的到IP地址进而进行修改,那么紧接着就要将类内的封装的IP地址ip_进行赋值,可是这里不敢直接赋值的,因为类内的ip_还是一个字符串类型,我们要将其转换为整数,转换为网络字节序列才可以,那么直接使用inet_addr一步到位即可

  5. 此时由于我们定义的类型为struct sockaddre_in的结构体仅仅是在栈上,即用户区,而网络套接字是在linux内核中,即内核区的概念,所以我们还需要将这个类型为struct sockaddre_in的结构体对应的端口号,IP地址等字段绑定到内核中的网络套接字里,所以如何绑定呢?那么使用bind即可
    在这里插入图片描述
    在这里插入图片描述

  6. 接下来我们看一下bind绑定,第一个参数传入网络文件描述符sockfd_,那么我们直接传入接口,接下来就需要将本地类型为struct sockaddre_in的结构体取地址,然后强转为struct sockaddr*类型的,然后最后传入结构体的大小即可,但是这个大小是socklen_t类型的,本质还是无符号整数,所以这里我们将sizeof的结果强转一下使类型匹配即可

  7. 紧接着有可能绑定也会失败呀,如果绑定失败了,那么自然也就无法进行网络通信了,那么结果就是服务器启动失败了,非常严重,如何判断呢?根据bind的返回值即可,如果绑定失败,那么返回一个小于0的数,即-1,那么我们使用if语句判断一下即可,如果绑定失败,那么就是一个非常严重的事故,所以使用日志,日志等级是Fatal非常严重,那么打印语句和对应的错误码即可,如果绑定成功了,那么使用日志打印绑定成功的即可

void Init()
{
    //创建套接字
    sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd_ < 0)
    {
        lg(Fatal, "socket create error, errno: %d, errstring: %s", errno, strerror(errno));
        exit(SOCKET_ERR);
    }
    lg(Info, "socket create success, sockfd: %d,errno: %d, err string: %s", sockfd_, errno, strerror(errno));

    //本地属性初始化
    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_);

    //套接字绑定
    if(bind(sockfd_, (struct sockaddr*)&local, (socklen_t)sizeof(local)) < 0)
    {
        lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
        exit(BIND_ERR);            
    }
    lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
}

Run

  1. Run这个成员函数参数要接受上层传入的方法,而这个方法的类型我们已经使用包装器+typedef为func_t了,所以Run的参数即func_t类型的fun,Run这个成员函数要将服务器运行起来,所以最开始要将isrunning_字段设置为true,紧接着定义一个inbuffer的缓冲区用于接收客户端发来的字符串
  2. 既然是服务器,服务器一旦运行起来就是一个死循环,很少停止,例如:你打王者荣耀,你早上可以打,中午可以打,下午可以打,晚上也可以打,甚至凌晨也可以打,所以呢?你手机上的王者荣耀即是一个客户端,并且王者荣耀是一款非常好玩的大型5V5联网多人对战游戏,既然是联网,所以就一定需要网络通信,在大厅里的玩家,进行匹配,打排位,打巅峰赛更新野怪资源等都需要网络,需要而远端腾讯的王者荣耀的服务器要一直运行,因为只有24小时运行,你才可以进行体验
  3. 所以我们的服务器也要设置为死循环,即我们的服务器接收客户端数据,以及向客户端发送数据都要在一个while死循环中,那么我们就可以使用isrunning_作为while死循环的判断条件,如果有拓展,比如我想要正常终止服务器,那么就可以给这个服务器发信号,并且对这个信号进行捕捉,在信号捕捉执行的方法中将isrunning_设置为false,这样服务器就会被终止了,当然,本文小编就不设计了,感兴趣的读者友友可以自行尝试
  4. 服务器从来都是被动的接收客户端的数据,然后对这个数据进行处理,最后将处理完的数据发送给客户端,即进行响应
recvfrom

在这里插入图片描述

  1. 所以此时我们就要先接收数据,使用recvfrom即可,传入网络套接字sockfd,缓冲区(接收到数据后recvfrom就会将这个数据从网络套接字中将这个数据拷贝到缓冲区中),缓冲区的大小(这里缓冲区的大小,我们进行-1,因为是和客户端约定好了,将互相之间发送的是字符串,那么在网络中字符串仅仅是一个一个的字符,没有结尾的’\0’,‘\0’是c语言的规定,规定字符串的结果要有’\0’,而网络套接字是使用文件的方式进行的管理,所以文件中可没有’\0’的规定,所以如果缓冲区真的被放满了,我们也要保证接收到的字符串的结尾可以被放置’\0’)
  2. 接下来就是一个标志位flag我们将其默认设置为0,即阻塞式等待数据,紧接着就是一个struct sockaddr*的类型,用于接收发来的套接字数据中的端口号和IP地址等字段,因为我们作为收数据的一方也要知道是谁发来的数据,并且进行保存起来,这样的话将来我们服务器处理完成数据之后,才可以利用IP地址+端口号port将数据发送回去
  3. 所以我们就需要定义一个本地的struct sockaddr_in类型的结构体,然后计算出这个结构体的大小,将这个struct sockaddr_in类型的结构体取地址,然后强转为struct sockaddr*传参给recvfrom即可
  4. 接下来传参这个结构体的大小即len,值得注意的是这个len进行传参的时候要进行取地址的,所以也就意味着这个len被recvfrom拿到的时候,进行解引用,拿到struct sockaddr_in类型的结构体的大小,然后当发数据的一方的端口号和IP地址等数据已经被放置到struct sockaddr_in类型的结构体中的时候,那么recvfrom会将struct sockaddr_in类型的结构体实际使用的大小放到len中带出去,所以此时在外部就可以通过len拿到这个struct sockaddr_in类型的结构体实际使用的大小,所以len这个参数是一个输入输出型参数
    在这里插入图片描述
  5. 所以接下来我们就来看看recvfrom的返回值,recvfrom会返回实际接收到的数据的大小,如果,如果接收失败,例如网络连接失败等造成函数调用失败,那么将会返回-1,即一个小于0的数,所以我们就可以进行判断了,如果recvfrom的返回值小于0,那么代表接收数据失败,接下来我们打印语句,然后continue继续接收即可
  6. 走到下一步那么缓冲区中一定接收到数据了,那么接下来我们看一下处理数据,由于我们已经将缓冲区接收到的数据当作字符串了,缓冲区的字符串结尾没有’\0’(为什么没有?因为文件中没有规定字符串要以’\0’结尾,所以一般写到文件中的字符串都是没有’\0’的,而客户端发数据就是采用的文件中的网络文件描述符发送的数据,那么也就意味着客户端发送过来的数据的字符串中不会以’\0’结尾,所以才需要我们手动添加’\0’),所以我们给字符串的末尾加上0,这个0也就是代表着’\0’
  7. 那么将这个缓冲区转换成string类型,再将这个string类型的对象传入上层提供的fun函数,那么使用一个string类型的对象接收fun函数的返回值即可,所以此时数据处理完毕
sendto
  1. 接下来我们就要发送数据了,可是如何发送数据给客户端呢?很简单,使用sendto即可
    在这里插入图片描述
  2. 那么对于sendto,作用就是将缓冲区的数据通过网络套接字发送到网络中,那么依次传入,网络文件描述符sockfd,接下来是一段缓冲区,这段缓冲区,我们就采用fun的返回值对应的字符串使用c_str()转换成c式的字符串,这个字符串就可以指向一段空间,这个空间内存放的就是一个一个的字符,最后一个一个的字符组成字符串,并且字符串的结尾有一个’\0’
  3. 那么接下来就是这个字符串的长度,首先这个字符串的长度,我们那么我们直接使用string自带的size计算即可,size计算方式是统计’\0’之前的字符个数,也正好符合我们的需求,因为文件中的字符串仅仅是一个一个的字符,文件中的字符串并不需要’\0’结尾
  4. 那么接下来传入标志位flags为0,即阻塞式发送
  5. 那么给谁发送呢?给客户端发送,那么我有客户端的IP地址和端口号port吗?有的,而且保存了,什么时候获取的?使用recvfrom接收客户端数据的时候,客户端的数据包中自带了客户端的对应的源IP地址,源端口号port,并且我们使用了类型为struct sockaddr_in的结构体对象client进行了保存,同时还知道它的实际使用的空间len,所以我们取出struct sockaddr_in的结构体对象client的地址,然后强制类型转换为(struct sockaddr*)传入sendto即可,最后将它的实际使用的空间len也传入即可
  6. 所以至此,UDP服务器我们就写完了,怎么样,虽然咋一看很难,但是经过不断的拆分之后,发现其实也就那个样子,人最大的对手是自己,在解决问题的时候,不要自己吓自己,先克服自己内心的恐惧,将问题拆分逐个解决,那么所谓的困难也就迎刃而解了
void Run(func_t fun)
{
    isrunning_ = true;
    char inbuffer[size];
    while(isrunning_)
    {
        //接收数据
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
        if(n < 0)
        {
            lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
            continue;
        }
        
        //处理数据
        inbuffer[n] = 0;
        std::string info = inbuffer;
        std::string echo_string = fun(info);

        //发送数据
        sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
    }
}

二、Main.cc

  1. 那么我们在Main.cc主要负责main函数的编写,首先Main.cc主要用于编写调用UDP服务器的逻辑,所以自然要包含服务器的头文件UdpServer.hpp了,值得注意的是,服务器和客户端一定是两个进程,可以是同主机上的两个进程,也可以是不同主机上的分别的一个进程
  2. 我们期望用户如何调用服务端呢?服务端的构造函数中需要传入IP地址和端口号port,由于服务端的IP地址已经设置为了默认值"0.0.0.0",可以接收任意向该主机对应的多个IP地址中的一个IP地址发来的数据,所以IP地址我们不需要用户手动传入了
  3. 所以就剩下端口号port需要用户传入了,那么就只能在命令行中进行传入,所以我们需要使用到命令行参数了,如何在main函数中使用命令行参数呢?在后方蓝字链接文章的第六点命令行参数中有进行的讲解 详情请点击<——
  4. 那么我们期望用户使用这种方式调用服务器,即./udpserver 8080 那么我们就可以给main函数传参了,那么if语句判断如果argc个数不等于2,那么说明用户传参错误,那么我们就给用户打印一下如何传参,即Usage,打印./udpserver 端口号调用应该是1024到65535之间的一个数值,然后由于用户传参错误,即调用服务器错误了,即服务器无法得到端口号,即服务器启动失败,所以我们就直接exit终止进程即可
  5. 那么到了下一步说明用户已成使用了./udpserver 8080 的形式调用了服务器,那么端口号在哪里?在argv[1]中,此时这个端口号还是一个字符串,所以我们要将这个端口号使用stoi转换成整数,注意此时这个端口号并不是网络字节序列,而是仅仅是符合本主机上的存储方式,我们将把端口号转换成网络字节序列的工作交给服务器即可
  6. 那么这个服务器我们希望new完之后自动释放,所以这里我们引进unique_ptr,包含#include <memory>这个头文件,智能指针的使用 详情请点击<——,所以new完UdpServer对象之后,此时服务器就被创建了
  7. 那么调用服务器提供的Init接口初始化服务器的各个字段
  8. 那么我们期望服务器执行什么方法呢?执行Handler方法,在Handler方法中,我们接收一个字符串,返回一个字符串,在接受的字符串前简单的添加一个服务器收到了这个消息的语句,然后返回处理完成的字符串即可
  9. 紧接着调用服务器的Run函数,传入Handler方法,此时服务器就运行起来,并且收到了消息后就会自动调用这个Handler方法去处理字符串了
#include <iostream>
#include <memory>
#include "UdpServer.hpp"

const std::string Handler(const std::string& str)
{
    std::string ret = "Server get a message: ";
    ret += str;
    std::cout << ret << std::endl;

    return ret;
}

void Usage(std::string str)
{
    std::cout << "\n\tUsage: " << str << " port[1024+]\n" << std::endl; 
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<struct UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run(Handler);

    return 0;
}

三、UDP客户端UdpClient.cc

  1. 首先关于客户端,小编有一个疑问?客户端需要bind吗?
  2. 需要,一定需要,只不过不需要用户显示的对客户端bind,这个bind的工作是由操作系统OS来做,并且一般是由操作系统OS随机选择,为什么?
  3. 对于用户主机的IP地址是固定的,所以操作系统可以直接将用户主机对应的IP地址进行bind,我们所说的操作系统OS随机选择,随机选择的是端口号port,原则:一个端口号只能绑定一个客户端进程,一个客户端进程可以绑定多个端口号port
  4. 例如,你手机上有10多个客户端,王者荣耀,抖音,快手,微信等客户端,我们知道一个端口号只能绑定一个客户端进程,那么王者荣耀客户端先绑定了5555,紧接着抖音也想要绑定5555,而一个端口号只能绑定一个客户端,所以抖音一定会绑定失败,所以如果你想要刷抖音都无法打开了,这是不行的,一定要让用户成功的打开抖音,那么怎么办呢?所以我们就把客户端绑定端口号的工作交给操作系统随机选择,操作系统一定可以采用某种算法保证客户端选择的是不同的端口号,所以从此以后这类问题就被解决了,客户端可以被打开了,可以刷抖音了
  5. 再例如,如果你是一个恶意的客户端,你知道一个客户端进程可以绑定多个端口号port,所以你这个恶意的客户端已启动初始化完成之后,就开始疯狂的将所有的端口号全部绑定了,那么此时其它的任何客户端进程都无法绑定端口号了,因为一个端口号只能绑定一个客户端进程呀,所以你的手机上只能运行这一个恶意的客户端,所以你不仅连抖音都无法打开,甚至王者荣耀也无法打开了,那么这肯定是不行的,我作为用户如果想要玩王者荣耀,那么你一定得保障我可以玩,所以呢?我们将客户端绑定端口号的工作交给操作系统,从此以后你这个恶意的客户端就不能将所有的端口号全部绑定了,那么此时其它的客户端进程也就可以绑定端口号了,所以你可以玩王者荣耀了,可以刷抖音了
  6. 即在客户端将绑定端口号的工作交给操作系统随机选择,那么服务端的端口号呢?为什么小编编写上面的服务端的时候,进行了绑定bind了端口号呢?
  7. 因为服务端不会出现绑定冲突的情况,王者荣耀的服务器和抖音的服务器在同一个主机上吗?不在,并且一定不在,由于是两款不同的软件,即不同的服务器,是分别由不同的公司开发,所以服务器是不在同一台主机上的,那么王者荣耀服务器想要使用1234这个端口号在自己主机上进行绑定,没问题,抖音的服务器也想要使用1234这个端口号在自己的主机上进行绑定,同样也没问题,端口号port的作用是在一台主机上唯一的标识一个进程,两个服务器进程都不在同一台主机上,所以也就不会出现冲突绑定端口号冲突的情况,即也就意味着不同主机的进程的端口号是可以重复的
  8. 其实客户端的端口号究竟是多少用户根本不关心,端口号让操作系统随机选择不是目的,目的是保证主机上进程的唯一性即可
  9. 那么系统究竟是什么时候进行的绑定bind的呢?其实是当客户端进程运行起来,首次sendto发送数据的时候进行的绑定,当绑定成功后,此后这个客户端进程运行的生命周期内都不需要再进行绑定了
  10. 所以我们可以得出客户端进程需要绑定IP地址和端口号,只不过不需要客户端显示绑定而是由操作系统隐式绑定

基本框架

  1. 那么客户端知道要要和哪一个主机相连,要和主机上具体的服务端进程相连吗?不知道,谁知道?用户知道,所以我们期望用户使用 ./udpclient 124.220.4.187 8080 的方式在命令行运行客户端,所以客户端的main函数同样需要参数
  2. 那么if语句判断如果argc个数不等于3,那么说明用户传参错误,那么我们就给用户打印一下如何传参,即Usage,打印./udpserver IP地址 端口号 ,然后由于用户传参错误,即调用客户端错误了,即客户端无法得到服务器的IP地址和端口号,即客户端启动失败,所以我们就直接exit终止进程即可
  3. 那么到了下一步说明用户已成使用了 ./udpclient 124.220.4.187 8080 的形式调用了客户端,那么IP地址在哪里?在argv[1]中,所以此时就得到了IP地址,注意这个IP地址还仅仅是点分十进制的字符串风格,端口号在哪里?在argv[2]中,此时这个端口号还是一个字符串,所以我们要将这个端口号使用stoi转换成整数,注意此时这个端口号并不是网络字节序列,而是仅仅是符合本主机上的存储方式,后续还需要将这个端口号转化成网络字节序列
  4. 我们知道将来客户端是要通过sendto给服务端发送数据的,所以自然也就需要服务端的端口号和IP地址,那么我们在本地定义一个struct sockaddr_in类型的结构体对象server作为将来sendto的参数,使用bzero对这个对象server进行初始化,然后对这个结构体对象的各个字段进行初始化,初始化套路和服务端类似,UDP的协议家族是AF_INET,然后将服务端的点分十进制的字符串的IP地址使用c_str转化为c风格的字符串,然后使用inet_addr_将字符串转换为整数,然后将整数转换为网络字节流序列对象的大端字节序列
  5. 对于端口号,那么我们使用htons将整数转换为网络字节流序列即可,最后求出这个结构体的大小len
#include <iostream>
#include <string>
#include <cstdio>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;

void Usage(string str)
{
    cout << "\n\tUsage: " << str << " serverip serverport\n" << endl; 
}


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

    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);
    socklen_t len = sizeof(server);


    return 0;
}

创建套接字

  1. 那么接下来我们使用socket创建套接字,那么UDP的协议家族选择AF_INET,即IPv4地址,然后选择协议类型,即选择UDP对应的SOCK_DGRAM数据报,然后使用sockfd接收socket创建套接字之后的网络文件描述符
  2. socket如果创建套接字失败,那么会返回一个小于0的数,所以创建套接字之后,我们通过socket的返回值sockfd是否小于0判断创建套接字是否失败,如果失败了,那么我们就使用打印错误信息即可,并且return返回1即可,即不让进程向后执行了,即让进程终止,因为客户端的套接字都创建失败了,所以客户端自然也就无法进行网络通信了,自然也就无法向服务器发送数据了,所以这里直接将客户端终止
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
    cout << "socket create error" << endl;
    return 1;
}

发送数据,接收数据

  1. 所以接下来,我们定义一个string类型的message对象,用于获取用户的输入,定义一个1024字节大小缓冲区buffer用于接收服务器响应发送回来的数据
  2. 服务端通常一旦启动也是一个死循环的呦,思考一下手机上的抖音,当你不退出的时候它就会一直运行不会退出,所以我们的客户端同样也是一个死循环式的发送数据,接收消息
  3. 那么我们就要给用户打印一个提示消息,让用户输入,其中用户的输入中可能会有空格,例如hello linux其中就有空格,所以我们获取用户输入应该使用getline传入cin以及message获取用户的一行输入,此时我们就成功的获取到了用户的输入
  4. 那么用户的输入就在message中,所以下面我们就要使用sendto发送数据给服务器了,所以那么依次传入参数即可,传入sockfd网络文件描述符sockfd,使用c_str将message中的string类型的字符串转换为c风格的字符串,然后调用size获取字符串的长度传入即可,由于文件中的字符串没有’\0’,使用了size求出的字符串长度即为’\0’之前的字符个数,正好符合我们的需求
  5. 接下来传入0,即阻塞时发送数据,然后sendto是向服务器发送数据,所以需要服务器的IP地址以及端口号等字段,不着急,我们早就初始化好了并且存储在本地的server对象中,所以将这个对象其地址,然后强制类型转换为struct sockaddr*,最后传入server这个结构体对象大小len即可,此时我们就向对方发送数据成功了
  6. 那么接下来的工作就是接收数据,如何接收数据呢?使用recvfrom即可,但是使用recvfrom要传入两个参数,一个是struct sockaddr*,一个是len长度,所以我们还需要定义一个struct sockaddr_in*的对象,然后计算出它的大小len
  7. 那么下面我们就可以使用recvfrom接收数据了,那么首先传入网络文件描述符sockfd,紧接着传入缓冲区buffer,缓冲区的大小,由于文件中不存在0,并且缓冲区期望接收到一个不含有’\0’结尾的字符串,缓冲区实际大小也是1024,所以这边传入字符串的最大的大小为1023,留有一个字节的空间是当缓冲区空间使用满了之后也可以有一个字节的空间将缓冲区的最后一个位置的空间设置为’\0’符合c语言的字符串的规定,即字符串以’\0’结尾
  8. 接下来就是一个标志位flag我们将其默认设置为0,即阻塞式等待数据,紧接着就是一个struct sockaddr*的类型,用于接收发来的套接字数据中的端口号和IP地址等字段,因为我们作为收数据的一方也要知道是谁发来的数据,并且进行保存起来,这样的话将来我们客户端如果想要给发来数据的一方进行回应,就可以利用IP地址+端口号port将数据发送回去
  9. 接下来传参这个结构体的大小即len,值得注意的是这个len进行传参的时候要进行取地址的,所以也就意味着这个len被recvfrom拿到的时候,进行解引用,拿到struct sockaddr_in类型的结构体的大小,然后当发数据一方的IP地址和端口号等数据已经被放置到struct sockaddr_in类型的结构体中的时候,那么recvfrom会将struct sockaddr_in类型的结构体实际使用的大小放到len中带出去,所以此时在外部就可以通过len拿到这个struct sockaddr_in类型的结构体实际使用的大小,所以len这个参数是一个输入输出型参数
  10. recvfrom会返回实际接收到的数据的字节数大小,所以我们使用ssize_t类型的变量s接收recvfrom的返回值,如果s大于0,那么说明recvfrom成功的收到了数据,那么我们就将这个数据当作一个没有以’\0’结尾的字符串,所以在这个字符串的结尾加上’\0’('\0’的ASCII码对应的值就是0,所以这里使用0完全没问题)即可
  11. 所以经过添加完成’\0’之后,此时我们终于拿到了服务端响应发送回来的数据,并且将其当作没有以’\0’结尾的字符串,然后成功的在其结尾添加上了’\0’,最后我们打印一下这个字符串即可
  12. 最后,为了避免内存泄漏,在程序的最后还应该将打开的网络文件描述符close关闭
string message;
char buffer[1024];
while(true)
{
    //发送数据
    cout << "Please Enter@ ";
    getline(cin, message);

    sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

    //接收数据
    sockaddr_in temp;
    socklen_t len1 = sizeof(temp);

    ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len1);
    if(s > 0)
    {
        buffer[s] = 0;
        cout << buffer << endl;
    }
}

close(sockfd);

四、测试

  1. 那么运行服务端,使用netstat -naup可以查看当前主机上的可以进行网络通信的UPD进程

运行结果如下,无误
在这里插入图片描述

  1. 并且我们再上图也成功的看到了服务器启动后,网络文件描述符sockfd的值确实是符合小编之前的预期3,即网络套接字在操作系统中确实是以文件对象的方式管理
  2. 那么下面小编先启动服务器,再启动客户端,让客户端给服务器发数据,然后服务器接收到数据进行处理数据,最后服务器处理完成数据,进行响应将处理完成的数据发送给客户端,客户端对收到的数据进行打印

运行结果如下,无误
在这里插入图片描述

五、源代码

UdpServer.hpp

#pragma once

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


enum{
    SOCKET_ERR = 1, 
    BIND_ERR
};


Log lg;
typedef std::function<const std::string(const std::string&)> func_t;

const int defaultsockfd = -1;
const std::string defaultip = "0.0.0.0";
const uint16_t defaultport = 8080;
const int size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip)
        : sockfd_(defaultsockfd), ip_(ip), port_(port), isrunning_(false)
    {}

    void Init()
    {
        //创建套接字
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            lg(Fatal, "socket create error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        lg(Info, "socket create success, sockfd: %d,errno: %d, err string: %s", sockfd_, errno, strerror(errno));

        //本地属性初始化
        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_);

        //套接字绑定
        if(bind(sockfd_, (struct sockaddr*)&local, (socklen_t)sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);            
        }
        lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }

    void Run(func_t fun)
    {
        isrunning_ = true;
        char inbuffer[size];
        while(isrunning_)
        {
            //接收数据
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            
            //处理数据
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = fun(info);

            //发送数据
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }

    ~UdpServer()
    {
        if(sockfd_ > 0)
            close(sockfd_);
    }
private:
    int sockfd_;
    std::string ip_;
    uint16_t port_;
    bool isrunning_;
};

Main.cc

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

const std::string Handler(const std::string& str)
{
    std::string ret = "Server get a message: ";
    ret += str;
    std::cout << ret << std::endl;

    return ret;
}

void Usage(std::string str)
{
    std::cout << "\n\tUsage: " << str << " port[1024+]\n" << std::endl; 
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<struct UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run(Handler);

    return 0;
}

UdpClient.cc

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

using namespace std;

void Usage(string str)
{
    cout << "\n\tUsage: " << str << " serverip serverport\n" << endl; 
}


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

    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);
    socklen_t len = sizeof(server);
    
    //创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        cout << "socket create error" << endl;
        return 1;
    }

    string message;
    char buffer[1024];
    while(true)
    {
        //发送数据
        cout << "Please Enter@ ";
        getline(cin, message);

        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        //接收数据
        sockaddr_in temp;
        socklen_t len1 = sizeof(temp);

        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len1);
        if(s > 0)
        {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }

    close(sockfd);


    return 0;
}

makefile

all:udpserver udpclient

udpserver:Main.cc
	g++ -o $@ $^ -std=c++11

udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpserver udpclient

Log.hpp

#pragma once

#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SIZE 1024

#define Info   0
#define Debug  1
#define Warning 2
#define Error  3
#define Fatal  4

#define Screen 1     //输出到屏幕上
#define Onefile 2    //输出到一个文件中
#define Classfile 3  //根据事件等级输出到不同的文件中

#define LogFile "log.txt" //日志名称


class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int method) //改变日志打印方式
    {
        printMethod = method;
    }

    ~Log()
    {}

    std::string levelToString(int level)
    {
        switch(level)
        {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Warning:
                return "Warning";
            case Error:
                return "Error";
            case Fatal:
                return "Fata";
            default:
                return "";
        }
    }

    void operator()(int level, const char* format, ...)
    {
        //默认部分 = 日志等级 + 日志时间
        time_t t = time(nullptr);
        struct tm* ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), 
        ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, 
        ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        char logtxt[2 * SIZE];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }

    void printLog(int level, const std::string& logtxt)
    {
        switch(printMethod)
        {
            case Screen:
                std::cout << logtxt << std::endl;
                break;
            case Onefile:
                printOneFile(LogFile, logtxt);
                break;
            case Classfile:
                printClassFile(level, logtxt);
                break;
            default:
                break;
        }
    }

    void printOneFile(const std::string& logname, const std::string& logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if(fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    void printClassFile(int level, const std::string& logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);

        printOneFile(filename, logtxt);
    }


private:
    int printMethod;
    std::string path;
};

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐