【Socket 编程 】基于UDP协议实现通信并添加简单业务

前言

在了解了Socket编程的相关接口之后,我们来尝试通过代码实现简单场景来理解UDP协议通信

实现echo server

下面通过代码来简单的回显服务器和客户端之间的输入输出通信

对于服务器端

  1. 建立套接字(IPv4协议和UDP协议
  2. 绑定套接字
  3. 读取接收到的消息
  4. 发送响应
  5. 结束连接,关闭套接字

对于客户端

  1. 获取服务器端的IP和端口号
  2. 建立套接字
  3. 发送消息
  4. 读取服务器端发来的响应
  5. 结束连接,关闭套接字

为什么客户端不用bind绑定套接字呢,这是因为客户端在发送消息的时候,操作系统会自动地绑定本机IP和一个随机的端口号到sockfd,这是为了避免端口号冲突(指定一个端口号可能是其它进程占有的)

下面给出具体实现的代码:

UdpServer.hpp文件

该文件定义了一个服务器类,用来生成一个服务器对象,根据构造时传入的端口号来绑定套接字,且允许服务器在所有可用的网络接口上进行监听。这样,服务器可以接收发往任何本地IP地址的连接请求
代码如下:

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "LockGuard.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
#include "InetAddr.hpp"
using namespace log_ns;

static const int gsockfd = -1;
static const uint16_t glocalport = 8888;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

class UdpServer : public nocopy
{
private:
public:
    UdpServer(uint16_t localport = glocalport)
        : _localport(localport), _isrunning(false), _sockfd(gsockfd)
    {
    }

    void InitServer()//初始化服务端
    {
        // 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议和UDP协议的套接字
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket create error!\n");
            exit(1);
        }
        LOG(DEBUG, "socket create success,sockfd is %d\n", _sockfd);
        // 绑定端口号和IP
        // 绑定之前创建一个sockaddr_in对象,存储本地地址信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定
        local.sin_port = htons(_localport);

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "sockfd bind error!\n");
            exit(1);
        }
        LOG(DEBUG, "sockfd bind success!\n");
    }

    void Start()//服务的开始工作
    {
        _isrunning = true;
        char inbuff[1024];
        while (_isrunning)
        {
            // 获取数据
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            ssize_t n = recvfrom(_sockfd, inbuff, sizeof(inbuff) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n < 0)
            {
                LOG(FATAL, "recvfrom error!\n");
            }
            else
            {
                LOG(DEBUG, "recvfrom success!\n");
                InetAddr addr(peer);
                inbuff[n] = '\0';
                cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuff << endl;
                string echo_string = "[udp_server echo]# ";
                echo_string += inbuff;
                // 发送响应给客户端

                ssize_t res = sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
                if (res < 0)
                {
                    LOG(FATAL, "echo sendto error!\n");
                }
                else
                {
                    LOG(DEBUG, "echo sendto success!\n");
                }
            }
        }
    }

    ~UdpServer()
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }

private:
    int _sockfd;
    uint16_t _localport;
    bool _isrunning;
};

nococpy.hpp文件

该文件实现了一个不可拷贝构造和赋值拷贝的类,该类的所有派生类都不可以拷贝构造和赋值拷贝

#pragma once

// 设计一个禁止拷贝和赋值的类,其派生类也不允许拷贝和赋值
class nocopy
{
public:
    nocopy(){};
    ~nocopy(){};
    nocopy(const nocopy &) = delete;
    const nocopy &operator=(const nocopy &) = delete;
};

InetAddr.hpp头文件

该文件实现了一个可以将网络序列包括IP和端口号转化成主机序列的类

#pragma conce
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
// 创建一个类用于将网络序列地址转换成主机序列地址
class InetAddr
{
private:
    void ToHest(const sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }

public:
    InetAddr(const sockaddr_in &addr)
    {
        ToHest(addr);
    }

    string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

private:
    string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

Log.hpp头文件

该文件定义了一个日志类,并提供了宏来进行便捷的打印日志

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
#include <fstream>
#include <sys/types.h>
#include <cstdarg>

#include <ctime>
#include "LockGuard.hpp"

namespace log_ns
{
    enum // 日志等级
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level) // 转换日志等级为字符串
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    struct logmessage
    {
    public:
        std::string _level;
        pid_t id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    class Log
    {
    private:
        std::string CurrTimeToString()
        {
            time_t now = time(nullptr);
            struct tm *curr_time = localtime(&now);
            char buff[128];
            snprintf(buff, sizeof(buff), "%d-%02d-%02d %02d:%02d:%02d", curr_time->tm_year - 1900, curr_time->tm_mon + 1, curr_time->tm_mday, curr_time->tm_hour, curr_time->tm_min, curr_time->tm_sec);
            return buff;
        }

        void FlushLogToScreen(const logmessage &lg) // 将日志信息输出到显示器文件中
        {
            printf("[%s][%d][%s][%d][%s]:%s \n", lg._level.c_str(), lg.id, lg._filename.c_str(), lg._filenumber, lg._curr_time.c_str(), lg._message_info.c_str());
        }

        void FlushLogToFile(const logmessage &lg) // 将日志信息输出到指定文件中
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
            {
                return;
            }
            char buff[2048] = {'\0'};
            snprintf(buff, sizeof(buff), "[%s][%d][%s][%d][%s]:%s \n", lg._level.c_str(), lg.id, lg._filename.c_str(), lg._filenumber, lg._curr_time.c_str(), lg._message_info.c_str());
            out.write(buff, sizeof(buff));

            out.close();
        }

    public:
        Log(const std::string &file = glogfile)
            : _logfile(file), _type(SCREEN_TYPE)
        {
        }
        // 打印日志
        void PrintLog(const logmessage &lg)
        {
            LockGuard lockguard(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }
        // 切换输出方式
        void Enable(int type)
        {
            _type = type;
        }

        // 构造logmessage对象,并打印
        void LogMessage(std::string filename, int filenumber, int level, const char *format, ...) //...表示可变参数列表
        {
            logmessage lg;

            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._level = LevelToString(level);
            // std::cout << lg._level << std::endl;
            lg.id = getpid();
            lg._curr_time = CurrTimeToString();
            // lg._message_info = CurrTimeToString();
            va_list ap;           // 获取所有参数
            va_start(ap, format); // 取出参数包的部分
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap); // 格式转换
            va_end(ap);
            lg._message_info = log_info;
            // 打印这个日志
            PrintLog(lg);
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;
#define LOG(LeveL, Format, ...)                                          \
    do                                                                   \
    {                                                                    \
        lg.LogMessage(__FILE__, __LINE__, LeveL, Format, ##__VA_ARGS__); \
    } while (0)

#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)

#define EnableFile()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)
};

UdpServerMain.cpp源文件

实现了服务器端的处理逻辑

#include "UdpServer.hpp"
#include <iostream>
#include <memory>
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cerr << "Usage:" << argv[0] << " local-port" << endl;
        exit(0);
    }

    uint16_t port = stoi(argv[1]);
    EnableScreen();
    unique_ptr<UdpServer> usvr = make_unique<UdpServer>(port);
    usvr->InitServer();
    usvr->Start();
    return 0;
}

UdpClientMain.cpp源文件

简单模拟了一个客户端,包括处理数据的逻辑

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include <memory>
using namespace std;
int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        cerr << "Usage:" << argv[0] << " local-port" << endl;
        exit(0);
    }
    string ip = argv[1];
    uint16_t port = stoi(argv[2]);
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cerr << "client create sockfd error!\n"
             << endl;
    }
    // 客户端一般不用绑定套接字,操作系统会在第一次发送消息时自动绑定本机ip和一个随机的port
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(ip.c_str());
    server.sin_port = htons(port);
    // 执行向服务端发送数据以及接收服务端响应
    while (true)
    {
        string line;
        cout << "please input: ";
        getline(cin, line);
        ssize_t res = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));

        if (res > 0)
        {
            struct sockaddr_in temp;
            memset(&temp, 0, sizeof(temp));
            char buff[1024];
            socklen_t len = 0;
            int n = recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&temp, &len);
            if (n > 0)
            {
                buff[n] = '\0';
                cout << buff << endl;
            }
            else
            {
                cerr << "client recvfrom error" << endl;
                break;
            }
        }
        else
        {
            cout << "client sendto error" << endl;
            break;
        }
    }
    return 0;
}

运行结果

在这里插入图片描述
查看网络连接的信息
在这里插入图片描述

实现翻译业务

根据上面的代码我们已经能够基本实现客户端与服务端之间的简单交流,现在可以考虑给服务端添加业务功能,比如一个翻译功能。
于是呢我们的服务端的处理逻辑就变成了:

  • 创建套接字
  • 绑定套接字
  • 接收客户端要查的单词
  • 发送客户端翻译结果
  • 结束连接,关闭套接字

具体的,翻译功能实现如下

Dict.hpp头文件

该文件实现了一个可加载指定路径字典的类,并支持翻译功能。底层数据结构是unordered_map

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include <unistd.h>
#include "Log.hpp"

using namespace log_ns;
using namespace std;

const static string gpath = "./Dict.txt";
const string sep = ": "; // 每行数据的分隔符

class Dict
{
private:
    void LoadDict(const string &path)
    {
        ifstream in(path); // 读取文件的文件流
        if (!in.is_open())
        {
            LOG(FATAL, "open %s failed\n", path.c_str());
            exit(1);
        }

        string line;
        while (getline(in, line))
        {
            LOG(DEBUG, "load info:%s,success\n", line.c_str());
            if (line.size() == 0)
            {
                continue;
            }
            int pos = line.find(sep);
            if (pos == string::npos)
            {
                continue;
            }
            string key = line.substr(0, pos);
            if (key.size() == 0)
                continue;
            string value = line.substr(pos + sep.size());
            if (value.size() == 0)
                continue;
            _dict[key] = value;
        }
        LOG(INFO, "load %s success!\n", path.c_str());
        in.close();
    }

public:
    Dict(const string &path = gpath)
        : _dict_path(path)
    {
        LoadDict(path); // 加载指定文件的字典数据
    }

    string Translate(string key)
    {
        if (!_dict.count(key))
            return "None";
        return _dict[key];
    }

    ~Dict()
    {
    }

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

对原来的服务端代码做出下面更改:

UdpServerMain.cpp

#include "UdpServer.hpp"
#include <iostream>
#include <memory>
#include "Dict.hpp"
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cerr << "Usage:" << argv[0] << " local-port" << endl;
        exit(0);
    }

    uint16_t port = stoi(argv[1]);
    EnableScreen();

    Dict dic;//定义一个字典类
    func_t translate = std::bind(&Dict::Translate, &dic, std::placeholders::_1);
    unique_ptr<UdpServer> usvr = make_unique<UdpServer>(translate, port);
    usvr->InitServer();
    usvr->Start();
    return 0;
}

Udpserver.hpp

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "LockGuard.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include <functional>
using namespace log_ns;

static const int gsockfd = -1;
static const uint16_t glocalport = 8888;
using func_t = function<string(string)>;
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

class UdpServer : public nocopy
{
private:
public:
    UdpServer(func_t func, uint16_t localport = glocalport)
        : _localport(localport), _isrunning(false), _sockfd(gsockfd), _func(func)
    {
    }

    void InitServer()
    {
        // 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议和UDP协议的套接字
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket create error!\n");
            exit(1);
        }
        LOG(DEBUG, "socket create success,sockfd is %d\n", _sockfd);
        // 绑定端口号和IP
        // 绑定之前创建一个sockaddr_in对象,存储本地地址信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定
        local.sin_port = htons(_localport);

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "sockfd bind error!\n");
            exit(1);
        }
        LOG(DEBUG, "sockfd bind success!\n");
    }

    void Start()
    {
        _isrunning = true;
        char inbuff[1024];
        while (_isrunning)
        {
            // 获取数据
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            ssize_t n = recvfrom(_sockfd, inbuff, sizeof(inbuff) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n < 0)
            {
                LOG(FATAL, "recvfrom error!\n");
            }
            else
            {
                LOG(DEBUG, "recvfrom success!\n");
                InetAddr addr(peer);
                inbuff[n] = '\0';
                cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuff << endl;
                string echo_string = "[udp_server echo]# ";
                string result = _func(inbuff);
                echo_string += result;
                // 发送响应给客户端
                ssize_t res = sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
                if (res < 0)
                {
                    LOG(FATAL, "echo sendto error!\n");
                }
                else
                {
                    LOG(DEBUG, "echo sendto success!\n");
                }
            }
        }
    }

    ~UdpServer()
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }

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

运行结果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值