最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

【网络编程】搭建一个简单的UDP通信服务器和客户端

网站源码admin3浏览0评论

【网络编程】搭建一个简单的UDP通信服务器和客户端

搭建UDP服务器

搭建UDP服务器的主要流程任务如下:

根据流程搭建UDP服务器完整代码如下:

代码语言:javascript代码运行次数:0运行复制
//服务器

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>

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

#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"

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

Log lg;

enum
{
    SOCKET_ERR=1,
    BIND_ERR
};

uint16_t defaultport = 8080;            //端口号绑1024以上,因为[0-1023]被系统绑定了
std::string defaultip = "0.0.0.0";      //ip地址写0表示只要是我这台主机的信息我都接收
const int size = 1024;

//封装服务器类
class UdpServer
{
public:
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false)
    {}


    void Init()
    {
        // 1. 创建udp socket
        //        socket(域(表明你支持哪种协议IPv4?IPv6?),套接字类型(面向数据报还是字节流),协议类型(一般可填0))     返回的是一个文件
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
        if(sockfd_ < 0)//如果创建套接字失败
        {
            lg(Fatal, "socket create error, sockfd: %d", sockfd_);
            exit(SOCKET_ERR);
        }

        lg(Info, "socket create success, sockfd: %d", sockfd_);


        // 2. 绑定bind socket(告诉服务器我们要使用的端口号)

        struct sockaddr_in local;       //sockaddr_in结构体里包含[1]16位协议类型family[2]16位端口号port[3]32位IP地址addr[4]8位填充字段zero
        bzero(&local, sizeof(local));   //把一段指定大小的内存初始化为0

        local.sin_family = AF_INET;     //IPv4协议
        local.sin_port = htons(port_); //端口号和IP在网络里是互相传递的,所以需要保证大家发送的端口号统一是网络字节序列,这样才不会被不同机器大小端影响
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string(字符串ip) -> uint32_t(4字节ip) 2. uint32_t(4字节ip)必须是网络序列的 
                              //inet_addr:把字符串风格的Ip转化为网络序列四字节ip
        // local.sin_addr.s_addr = htonl(INADDR_ANY);       //任意地址ip

        //bind绑定的本质是把你在用户态设定的一个套接字变量local绑定到内核里
        int n = bind(sockfd_, (const struct sockaddr*)&local, sizeof(local));
        if( n < 0)  //如果bind绑定失败
        {
            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));
    }

    //func是用户传入的数据处理方法
    void Run(func_t func) // 对代码进行分层
    {
        isrunning_ = true;
        char inbuffer[size];
        while(isrunning_)//服务器一定是一直在运行的
        {
            //服务器运行的第一件事,从udp读取数据
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            //                                                                \|/这个是用来接收对方的端口ip信息的,记录下来方便回消息能找到对方
            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 = "server echo# " + info;

            //把字符串数据交给func函数处理
            std::string echo_string = func(info);
            
            //std::cout<<"client echo@"<<info<<std::endl;

            //加工好数据,返回给对方
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);
        }
    }


    ~UdpServer()
    {
        if(sockfd_>0) close(sockfd_);
    }


private:
    int sockfd_;     // 网路文件描述符
    std::string ip_; // 任意地址bind 0
    uint16_t port_;  // 表明服务器进程的端口号
    bool isrunning_;
};

搭建UDP客户端

搭建UDP客户端的主要流程任务如下:

根据流程搭建UDP客户端完整代码如下:

代码语言:javascript代码运行次数:0运行复制
#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.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\n"<<std::endl;
}

//
int main(int argc,char* argv[])
{
    //确定用户要给谁发数据
    if(argc != 3) //运行程序的参数:一个可执行程序名和一个ip地址和一个端口号
    {
        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);
    if(sockfd < 0)
    {
        cout<<"socker error"<<endl;
        return 1;
    }

    //客户端要bind绑定,只不过不需要用户显式绑定,一般由操作系统随机选择,因为一个端口号只能被一个进程绑定,操作系统指定有利于化解冲突
    //服务器端口号必须固定,因为不固定用户很难第一下稳定的找到你,客户端不用固定,因为服务端接收客户端的消息时就会有客户端的套接字
    
    string message;
    char buffer[1024];
    while(true)
    {
        //1.接收用户想发送的数据
        cout<<"Please Enter@ ";
        getline(cin,message);

        //cout<<message<<endl;   //客户端回显给用户要发送的数据

        //2.发送请求数据给客户端
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        //2.接收客户端回复的数据
        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;
}

其余工程文件

主函数文件Main

主函数中的代码逻辑分为两部分,第一是主函数逻辑,即创建并运行服务器对象.第二部分是提供服务器数据处理的回调函数,这里提供了三个不同功能的回调函数,分别是字符串大小写转换功能函数,聊天功能函数,简单复读机功能函数,指令执行功能函数.这些回调函数需要在Run服务器的时候当作参数传递给服务器,以便服务器调用这些函数来处理接收到的数据.完整代码如下:

代码语言:javascript代码运行次数:0运行复制
#include "UdpServer.hpp"
#include<iostream>
#include <memory>
#include <string>
#include <vector>
#include <cstdio>

// "120.78.126.148" 点分十进制字符串风格的IP地址

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

//简单处理,只是在数据前加一个数据
std::string Handler(const std::string &str)
{
    std::string res = "Server get a message: ";
    //std::string res = "Server say: ";
    res += str;
    std::cout << res << std::endl;

    return res;
}

//聊天
std::string talker(const std::string &str)
{
    //std::string res = "Server get a message: ";
    std::string res = "client say: ";
    res += str;
    std::cout << res << std::endl;

    std::string qus;
    getline(std::cin,qus);

    return qus;
}

//字符串大小写转换
std::string ToChar(const std::string &str)
{
    std::string res;
    for(const auto e:str)
    {
        if(e>='A'&&e<='Z')
            res+=(e+32);
        else if(e>='a'&&e<='z')
            res+=(e-32);
        else
            res+=e;
    }
    std::cout << "转换后:"<< res << std::endl;
    return res;
}

//检查指令合理
bool SafeCheck(const std::string &cmd)
{
    std::vector<std::string> key_word = {
        "rm",
        "mv",
        "cp",
        "kill",
        "sudo",
        "unlink",
        "uninstall",
        "yum",
        "top",
        "while"
    };
    for(auto &word:key_word)
    {
        auto pos = cmd.find(word);
        if(pos != std::string::npos) return false;
    }
    return true;
}

//执行用户输入指令
std::string ExcuteCommand(const std::string &cmd)
{
    std::cout<<"get a request cmd: "<<cmd<<std::endl;
    //对指令做安全检查
    if(!SafeCheck(cmd)) return "Bad man";

    //直接给指令,popen帮你fork子进程
    FILE *fp = popen(cmd.c_str(), "r");
    if(nullptr == fp)//如果popen失败了
    {
        perror("popen");
        return "error";
    }

    std::string result;
    char buffer[4096];
    //从fp里把指令执行结果取出来
    while(true)
    {
        char *ok = fgets(buffer, sizeof(buffer), fp);
        if(ok == nullptr) break;
        result += buffer;
    }
    pclose(fp);

    return result;
}

// ./udpserver port
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(port));

    //初始化服务器
    svr->Init(/**/);
    //运行服务器
    svr->Run(ExcuteCommand);

    return 0;
}

日志打印文件Log.hpp

该文件提供了一个Log类,支持直接打印日志或分级导出日志到相应文件功能,是一个非常好用的日志小组件.完整代码如下:

代码语言:javascript代码运行次数:0运行复制
#pragma once
#include<iostream>
#include<stdarg.h>
#include<time.h>
#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;
    }


    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 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);
    }
    ~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\n",logtxt);
        printLog(level,logtxt);
    }

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

Makefile文件

主要用于自动化编译工程文件,完整文件如下:

代码语言:javascript代码运行次数:0运行复制
.PHONY:all
all:udpserver udpclient

udpserver:Main
	g++ -o $@ $^ -std=c++11
udpclient:UdpClient
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpserver udpclient
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-03-29,如有侵权请联系 cloudcommunity@tencent 删除服务器客户端通信网络编程udp
发布评论

评论列表(0)

  1. 暂无评论