当前位置: 首页 > article >正文

UDP客户端服务器通信

        在这篇博客中,我们将探索 UDP(用户数据报协议) 通信,简要地说,UDP 是一种无连接、快速但不可靠的通信协议,适用于需要快速数据传输但对丢包容忍的场景,比如视频流和在线游戏。就像《我是如此相信》的图片所表达的那样,UDP 没有过多的保障机制,它直接、简单、快捷,就像信念一样,直接而坚定。 

1、网络协议中的下三层主要解决的是,数据安全可靠得发送到远端机器

2、用户使用应用层软件,完成数据的发送和接收,先把软件启动起来,那就是进程,因此网络通信的本质就是进程间的通信。

一、认识端口号

端口号无论是客户端还是服务器,都能唯一的标识该主机上的一个特定应用层的进程,ip地址唯一的标识一个主机,端口号只能用来标识该主机上的唯一进程。IP+Port能够标识全网唯一的进程。

二、套接字

客户端和服务器通信的时候就好像插头和插座的关系

端口号VS进程pid,但是pid已经可以唯一标识一个进程,为什么还要端口号?

1、不是所有的进程都要网络通信

2、pid属于操作系统中的进程管理,但是要是系统出问题,网络部分也要改,牵一发而动全身,因此要解耦。

我们的客户端怎么知道服务器的端口号?每个服务器的端口必须是众所周知的,被客户端知道。

一个进程可以绑定多个端口号,一个端口号不可以被多个进程绑定。经过哈希找的进程就不一样了

2、  套接字的种类:域间(本地通信)、原始(编写网络工具,抓包)和网络套接字

三、UDP和TCP

TCP(Transmission Control Protocol,传输控制协议)网络必须连通

  • 面向连接:在数据传输之前,发送端和接收端需要通过三次握手建立连接。这一过程确保了双方都准备好进行数据传输,并且知道对方的存在和状态,为可靠的数据传输奠定基础。例如,当你使用浏览器访问一个网站时,浏览器与网站服务器之间会先建立 TCP 连接,然后才开始传输网页数据.
  • 可靠性高:TCP 协议通过多种机制来确保数据的准确无误传输。它使用确认机制,接收方在收到数据后会向发送方发送确认消息,发送方只有在收到确认后才会继续发送下一部分数据,否则会重传未确认的数据。同时,TCP 还具备重传机制,对于在传输过程中丢失或损坏的数据,发送方会自动重新发送,直到数据被正确接收。此外,流量控制和拥塞控制也是 TCP 保证可靠性的重要手段,它可以根据网络状况和接收方的处理能力,动态调整数据传输的速率,避免网络拥塞导致数据丢失.
  • 数据有序:TCP 会对数据进行编号和排序,确保数据按照发送的顺序到达接收端。即使数据在网络中经过不同的路径传输,到达接收端时也会被正确地重组,保证应用程序能够接收到完整、有序的数据。例如,在文件传输过程中,文件的各个部分会按照顺序依次传输并在接收端正确拼接。
  • 适用于多种应用:由于其可靠性高,TCP 适用于对数据准确性要求严格的应用场景,如文件传输(FTP)、电子邮件(SMTP、POP3 等)、远程登录(Telnet)、网页浏览(HTTP、HTTPS)等,这些应用需要确保数据的完整性和准确性,不容许数据丢失或出错。

UDP(User Datagram Protocol,用户数据报协议)

  • 无连接:UDP 在发送数据之前不需要与接收方建立连接,发送方可以随时向目标地址发送数据报。这使得 UDP 的传输过程更加简单、快速,减少了连接建立和拆除的开销1.
  • 不可靠传输:UDP 不提供数据传输的可靠性保证,它不会对数据进行确认、重传或排序等操作。数据报在网络中可能会出现丢失、重复或乱序的情况,并且 UDP 本身不会对这些问题进行处理,而是由应用层来负责处理数据的可靠性问题。不过,在一些对实时性要求较高但对数据准确性要求相对较低的应用中,这种不可靠性是可以接受的,例如在线游戏中的玩家位置信息更新、视频会议中的语音和视频数据传输等,偶尔丢失一两个数据包不会对整体效果产生太大影响1.
  • 低延迟:由于不需要建立连接和进行复杂的可靠性控制,UDP 的传输延迟相对较低,能够快速地将数据发送到目标地址。这对于实时性要求高的应用非常有利,如音频流、视频流等实时多媒体应用,可以在保证一定流畅度的前提下,尽可能减少延迟1.
  • 高效性:UDP 协议的头部开销较小,只有 8 个字节,相比 TCP 的 20 个字节头部,UDP 可以在相同的网络带宽下传输更多的数据,提高了数据传输的效率1.
  • 适用于特定应用:UDP 常用于一些对实时性和效率要求较高,但对数据可靠性要求相对较低的应用场景,如网络视频会议、在线游戏、音频流播放、实时监控等。例如,在网络视频会议中,即使偶尔丢失一些视频帧,也不会对会议的进行产生严重影响,而较低的延迟可以保证参会者之间的交互更加流畅。

 四、网络字节序 

1、判断是小端还是大端 小权重放到小地址就是小端(小小小)

大端:数据的高位字节存于低地址,低位字节存于高地址。 

程序:判断机器是大端还是小端?

#include <stdio.h>
int main() {
    int num = 0x12345678;
    char *ptr = (char *)&num;
    if (*ptr == 0x78) {
        printf("Little - Endian\n");
    } else if (*ptr == 0x12) {
        printf("Big - Endian\n");
    } else {
        printf("Error\n");
    }
    return 0;
}

2、网络规定都是大端 ,低地址高字节

五、UDP客户端服务器通信 

步骤:1、创建UDP套接字 下面为创建套接字的接口 socket         

第一个参数表示的是使用的协议家族,域到底是什么,第二个表示定义的套接字类型,流,还是数据报,第三个参数表示协议参数,填0就行

返回值 On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately. 

           2、绑定 bind

插入:如何快速地将整数ip转为字符串? 比如12345667转为0.0.36.123 ,我们经常用的就是点分十进制,但是我们要知道如何转换。有接口inet_addr

【0,255】.【0,255】.【0,255】.【0,255】

struct ip
{
    uint8_t part1;  //占一个字节
    uint8_t part2;
    uint8_t part3;
    uint8_t part4;
}
int src_ip=12345678;
struct ip *p=(struct ip*)src_ip; //由于 src_ip 是一个 32 位的整数,而 struct ip 是一个包含四个 uint8_t 类型字段的结构体,直接把 src_ip 赋值给 struct ip 类型的变量是不可能的(类型不兼容)。但是,通过指针,我们可以将 src_ip 的内存解释为 struct ip 类型,并按字节访问它。
to_string(p->part1)+"."+to_string(p->part2)+"."+to_string(p->part3)+"."+to_string(p->part4);

就把四字节转换为字符串风格的ip
结果是0.0.3.182

字符串转整数 ”192.168.50.100“  ”192“ ”168“”50“”100“

//192.168.50.100 转换为 123455556
uint32_t Ip; 4个字节
struct ip* x=(struct ip*)&Ip;
x->part1=stoi("192");
x->part2=stoi("168");
x->part3=stoi("50");
x->part4=stoi("100");

查看服务器启动的命令:netstat -nau

 一个关于ip

云服务器禁止直接bind公网ip,一个机器可能有多个ip,bind(IP:0),IP为0叫做任意地址绑定凡是发给我这台主机的数据,我们都要根据端口号向上交付。 

 一个关于port

【0,1023】属于系统内定的端口号,一般都要有固定的应用层协议使用 http:80 https:443 mysql:3306 我们绑定1024以上的

结论:IP地址绑定0,端口号绑定1024以上的

六、程序实现

Log.hpp
#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.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;
    }
    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 "Fatal";
        default:
            return "None";
        }
    }

    // void logmessage(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[SIZE * 2];
    //     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    //     // printf("%s", logtxt); // 暂时打印
    //     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); // "log.txt"
        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); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log()
    {
    }
    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[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }

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

// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }

//     va_end(s); //s = NULL
//     return sum;
// }
Main.cc
#include "UdpServer.hpp"
#include <memory>
#include "Log.hpp"
Log log;

void Usage(std::string proc)
{
    std::cout<<"\n\rUsage:"<<proc<<"proc[1024+]"<<std::endl;
}
//./udpserver port
//"192.168.1.123"点分十进制字符串风格的ip地址,一共占了13个字节
int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port=std::stoi(argv[1]);  //字符串转整数
    std::unique_ptr<UdpServer> svr(new UdpServer());
    svr->Init();
    svr->Run();
    return 0;
}

Makefile
.PHONY:all
all:Udpserver Udpclient
Udpserver:Main.cc
	g++ -o $@ $^ -std=c++11
Udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++11
.PHONY:
clean: 
	rm -f Udpserver Udpclient
Udpclient
#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
void Usage(std::string proc)
{
    std::cout<<"\n\rUsage:"<<proc<<"serverip serverport"<<std::endl;
}
// ./udpclient serverip serverport 启动客户端的时候,必须要是ip和端口号是是什么
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip=argv[1];
    uint16_t serverport=std::stoi(argv[2]);
    //构建服务器信息
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    //client要绑定,要有自己的IP和端口,只不过不需要用户显示的bind,一般由os随机选择!肯定是客户端先向服务器发起请求,端口号能唯一标识客户端就行,随机分配,但是服务器的端口号必须要准确知道,不然客户端绑定哪一个
    //系统什么时候给我Bind,首次放松数据的时候就绑定,sendto
    string message;
    char buffer[1024];//接收时候的buffer
    while(1)
    {
        cout<<"Please Enter@";
        std::getline(cin,message);
        cout<<message<<endl;
        //1.数据 2.给谁发 &server 发送给server
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);
        struct sockaddr_in temp;
        socklen_t len=sizeof(temp);
        //服务器发消息处理了之后,再给客户端发回来,客户端就要去接收
        ssize_t s=recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len);  //倒数第二个是谁发的
        if(s>0)
        {
            buffer[s]=0;
        }
        cout<<buffer<<endl;
    }
    close(sockfd);
    return 0;
};
Udpserver
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include "Log.hpp"
extern Log log;
enum{
    SOCKET_ERR=1,
    BIND_ERR=2
};
uint16_t defaultport=8080;
std::string defaultip="0.0.0.0";
const int size=1024;
class UdpServer
{
public:
    UdpServer(const uint16_t &port=defaultport,const std::string &ip=defaultip):port_(port),sockfd_(0),ip_(ip),isrunning_(false)//一个服务器要启动起来,端口号要有,Ip地址也要有
    {

    }
    void Init()
    {
        sockfd_=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
        if(sockfd_<0)
        {
            log(Fatal,"socket create error,sockfd:%d",sockfd_);
            exit(SOCKET_ERR);
        }
        printf("socket create success,sockfd:%d\n",sockfd_);
        log(Info,"socket create success,sockfd:%d",sockfd_);
        struct sockaddr_in local;
        bzero(&local,sizeof(local));//把结构体里面的内容全部清零 ,第二个参数是缓冲区的大小
        local.sin_family=AF_INET;
        local.sin_port=htons(port_); //我要给你发数据,你必须知道我的端口号,我要把我的端口号发给你,因此端口号是需要在网络中传输的,必须是网络字节序列
        //local.sin_addr.s_addr=inet_addr(ip_.c_str());  //1.string->uint32_t   将string转char* string.c_str()
        local.sin_addr.s_addr=INADDR_ANY;//0,任意绑定,因为是全0,主机转网络不需要做
        bind(sockfd_,(const struct sockaddr *)&local,sizeof(local));
    }
    //服务器应该周而复始的运行
    void Run()
    {
        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,(sockaddr*)&client,&len);
           if(n<0)
           {
                log(Warning,"recvform error");
                continue;
           }
           inbuffer[n]=0;
           //对字符串进行加工
           std::string info=inbuffer;
           std::string echo_string="server echo#"+info;
           //再把数据发送回给对方 string的长度  string.size
           //你本地的套接字;发送的字符串和长度;默认为0;输入性参数,我知道对方是谁,因为当时对方给我发过消息
           sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);

        }
        
    }
    ~UdpServer()
    {
        if(sockfd_>0)  close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    uint16_t port_; //端口号是两个字节,16个比特位,服务器要有自己的端口号
    std::string ip_;//任意地址绑定,填为0
    bool isrunning_;
};

改进版:

Main.cc
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include "Log.hpp"
Log log;

void Usage(std::string proc)
{
    std::cout<<"\n\rUsage:"<<proc<<"proc[1024+]"<<std::endl;
}
std::string Handler(const std::string &str)
{
    std::string res="Server get a message:";
    res+=str;
    return res;
    //服务器收到消息,回调式的调用你传进来的方法,把数进行加工,加工处理完后返回,最后把这个结果发出去
}
std::string ExecuteCommand(const std::string &cmd)
{
   // SafeCheck(cmd);
    FILE* fp=popen(cmd.c_str(),"r");
    if(nullptr==fp)
    {
        perror("popen");
        return "error";
    }
    std::string result;
    //从fp中把命令执行结果都拿出来
    char buffer[4096];
    while(true)
    {
        char * res=fgets(buffer,4096,fp);//读到一行
        if(nullptr==res) break;
        result+=buffer;
    }
    return result;
    pclose(fp);
}
//./udpserver port
//"192.168.1.123"点分十进制字符串风格的ip地址,一共占了13个字节
int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port=std::stoi(argv[1]);  //字符串转整数
    std::unique_ptr<UdpServer> svr(new UdpServer());
    svr->Init();
    svr->Run(ExecuteCommand);
    return 0;
}

Udpserver.hpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <functional>
#include "Log.hpp"
using func_t=std::function<std::string(const std::string&)>; //返回值为string,参数为const std::string&的函数传递给Udpserver
//等价于typedef std::function<std::string(const std::string&)> func_t;
extern Log log;
enum{
    SOCKET_ERR=1,
    BIND_ERR=2
};
uint16_t defaultport=8080;
std::string defaultip="0.0.0.0";
const int size=1024;
class UdpServer
{
public:
    UdpServer(const uint16_t &port=defaultport,const std::string &ip=defaultip):port_(port),sockfd_(0),ip_(ip),isrunning_(false)//一个服务器要启动起来,端口号要有,Ip地址也要有
    {

    }
    void Init()
    {
        sockfd_=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
        if(sockfd_<0)
        {
            log(Fatal,"socket create error,sockfd:%d",sockfd_);
            exit(SOCKET_ERR);
        }
        printf("socket create success,sockfd:%d\n",sockfd_);
        log(Info,"socket create success,sockfd:%d",sockfd_);
        struct sockaddr_in local;
        bzero(&local,sizeof(local));//把结构体里面的内容全部清零 ,第二个参数是缓冲区的大小
        local.sin_family=AF_INET;
        local.sin_port=htons(port_); //我要给你发数据,你必须知道我的端口号,我要把我的端口号发给你,因此端口号是需要在网络中传输的,必须是网络字节序列
        //local.sin_addr.s_addr=inet_addr(ip_.c_str());  //1.string->uint32_t   将string转char* string.c_str()
        local.sin_addr.s_addr=INADDR_ANY;//0,任意绑定,因为是全0,主机转网络不需要做
        bind(sockfd_,(const struct sockaddr *)&local,sizeof(local));
    }
    //服务器应该周而复始的运行
    //这样的好处是实现代码的分层
    void Run(func_t func)//运行的时候需要用户传进来一个怎么处理数据的方法
    {
        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,(sockaddr*)&client,&len);
           if(n<0)
           {
                log(Warning,"recvform error");
                continue;
           }
           inbuffer[n]=0;
           std::string info=inbuffer;
           std::string echo_string =func(info);
           sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);
        }
        
    }
    ~UdpServer()
    {
        if(sockfd_>0)  close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    uint16_t port_; //端口号是两个字节,16个比特位,服务器要有自己的端口号
    std::string ip_;//任意地址绑定,填为0
    bool isrunning_;
};


http://www.kler.cn/a/415159.html

相关文章:

  • 软件测试丨Pytest生命周期与数据驱动
  • 网安瞭望台第4期:nuclei最新poc分享
  • seata 各个微服务回滚的时机
  • ZooKeeper 基础知识总结
  • Jackson:Java对象和JSON字符串的转换处理库使用指南
  • 微信小程序蓝牙writeBLECharacteristicValue写入数据返回成功后,实际硬件内信息查询未存储?
  • 原生微信小程序画表格
  • 嵌入式Rust小探
  • 利用树莓派Pico制作迷你小台灯:C++与硬件设计结合的分享
  • node.js基础学习-url模块-url地址处理(二)
  • JVM 常见面试题及解析(2024)
  • 网络安全(1)_对称加密和非对称加密
  • 本地局域 基于ip地址生成证书
  • 《Vue零基础入门教程》第十二课:双向绑定指令
  • 详细分析 npm run build 基本知识 | 不同环境不同命令
  • 数据库期末复习题库
  • Vue3组件异步懒加载defineAsyncComponent
  • 选择使用whisper.cpp进行语音转文字
  • SpringBoot连接测试InfluxDB时序数据库
  • 学习ASP.NET Core的身份认证(基于Session的身份认证1)
  • 使用命令行创建一个简单的 Maven Web 应用程序
  • MindAgent部署(进行中.....)
  • 23种设计模式-工厂方法(Factory Method)设计模式
  • sqli_labs-10,11,12 --详细解析
  • 叮!您的RK3568系统镜像备份方法请查收
  • 可视化建模以及UML期末复习篇----相关软件安装