39. 从零实现UDP服务器实战(带源码) V1版本 - Echo server

在上一章中,我们已经学过了socket的接口,本章,我们先实现简单的UDP通信,并过渡到需要连接,更有难度的

其中客户端与服务端的创建套接字,转换等操作相似,后面我们可以再进行封装,解耦,使代码逻辑更清晰

UdpServer.hpp

bash 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
    #include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
// #include "InetAddr.hpp"
using namespace LogModule;
int defaultsockfd = -1;
class UdpServer
{
public:
    UdpServer(std ::string &ip, uint16_t port)
        : _sockfd(defaultsockfd), _ip(ip), _port(port), _isrunning(false)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket0 error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success,sockfd: " << _sockfd;
        // 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
        // 2.1 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 初始化
        // 我会不会把我的IP地址和端口号发送给对方?
        // IP信息和端口信息,一定要发送到网络!
        // 本地格式->网络序列
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        // IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
        // 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
        // 此时只开在了栈上,要通过系统调用写入内核
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n == -1)
        {
            LOG(LogLevel::FATAL) << "bind fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success!";
    }
    void Start()
    {
        while (_isrunning)
        {

            _isrunning = true;
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                // 发送消息
                int peer_port = ntohs(peer.sin_port);           // 从网络拿到的网络序列转换
                std::string peer_ip = inet_ntoa(peer.sin_addr); /// 4字节网络风格的IP -> 点分十进制的字符串风格的IP
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer; // 1. 消息内容 2. 谁发的??
                // 2. 发消息
                std::string echo_string = "server echo#";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
            }
            else
            {
                LOG(LogLevel::FATAL) << "recvfrom fail";
                exit(1);
            }
        }
    }

    ~UdpServer() {}

private:
    int _sockfd;
    uint16_t _port;
    std::string _ip;
    // 用的是字符串风格,点分十进制, "192.168.1.1"
    bool _isrunning;
};

UdpClient.cc

bash 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include"Log.hpp"
using namespace LogModule;
int main(int args,char *argv[])
{
    if(args!=3)
    {
       std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
       return 1;
    }
     // 1. 创建socket
     int sockfd=socket(AF_INET,SOCK_DGRAM,0);
     if(sockfd<0)
     {
        std::cerr<<"socket fail"<<std::endl;
        return 2;
     }
     // 2. 本地的ip和端口是什么?要不要和上面的"文件"关联呢?
    // 问题:client要不要bind?需要bind.
    //       client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
    //   为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
    //   client端的端口号是几,不重要,只要是唯一的就行!
    // 填写服务器信息
    std::string server_ip=argv[1];
    std::uint16_t server_port=std::stoi(argv[2]);//转数字
     struct sockaddr_in  server;
     memset(&server,0,sizeof(server));
     server.sin_family=AF_INET;
    server.sin_port =htons(server_port);
     server.sin_addr.s_addr=inet_addr(server_ip.c_str());
     while(true)
     {
        //接受信息
        std::string input;
        std::cout<<"PLEASE Enter# ";
        std::getline(std::cin,input);
        int n=sendto(sockfd,input.c_str(),input.size(),0,(struct sockaddr*)&server,sizeof(server));
        (void)n;
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);

        int m=recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
        if(m>0)
        {
            buffer[m]=0;
            std::cout<<buffer<<std::endl;
        }
     }
     return 0;
    }

注意事项

_ip地址是使用的string,但进行绑定时,却是要是数字,原因 数字就分成4个,每个数字表示0到255 只需4字节

bind时,服务器要显示绑定,但客服端不用,OS会随机绑定,防止(OS知道IP,随机绑定端口号)你绑定的会占用其他进程

UdpServer.cc

bash 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"

int main(int argc,char*argv[])
{
     if(argc!=3)
     {
         std::cerr << "Usage: " << argv[0] << " ip port" <<std::endl;
        return 1;
     }
     std ::string ip =argv[1];
     uint16_t port=std::stoi(argv[2]); //xiecuo
     Enable_Console_Log_Strategy();
     std::unique_ptr<UdpServer> usvr =std::make_unique<UdpServer>(ip,port);
     usvr->Init();
     usvr->Start();
    return 0;
}

实验不足,修改

实验发现,绑定的ip地址为服务器公网IP无用,是因为我们OS并未按照公网IP

云服务器可以通过映射,将我们的内网IP与公网IP一一对应只有内网IP

本地环回,要求c s必须在同一条机器,表面我们是本地通信,client发送的数据,不会被推送到网络,而是在OS转一圈发给对应的服务端(经常用来进行网络代码测试)

bind不能绑定公网IP

bind 绑定内网IP,或者本地环回IP,客户端使用另一个依旧无法通信,是因为如果我们显示进行地址bind,client访问,就必须使用servet绑定的,所以不推荐,修改

bash 复制代码
 // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        local.sin_addr.s_addr = INADDR_ANY;

而我们传输是为了让服务端进行处理数据的,那么我们也可以不再传ip了,而是传回调函数,进行处理(使用functional),这样就能进行处理了

而修改后,客户端无论是绑定本地环回ip还是内网ip都可以两者相互交流

UdpServer.cc

bash 复制代码
#include <iostream>
#include <memory>
#include <functional>
#include "UdpServer.hpp"
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}
int main(int argc,char*argv[])
{
     if(argc!=2)
     {
         std::cerr << "Usage: " << argv[0] << " port" <<std::endl;
        return 1;
     }
    //  std ::string ip =argv[1];
     uint16_t port=std::stoi(argv[1]); //xiecuo
     Enable_Console_Log_Strategy();
     std::unique_ptr<UdpServer> usvr =std::make_unique<UdpServer>(port,defaulthandler);
     usvr->Init();
     usvr->Start();
    return 0;
}

UdpServer.hpp

bash 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
// #include "InetAddr.hpp"
using namespace LogModule;
using func_t =std::function<std::string(const std::string&)>;

int defaultsockfd = -1;
class UdpServer
{
public:
    UdpServer( uint16_t port,func_t func)
        : _sockfd(defaultsockfd), _func(func), _port(port), _isrunning(false)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket0 error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success,sockfd: " << _sockfd;
        // 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
        // 2.1 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 初始化
        // 我会不会把我的IP地址和端口号发送给对方?
        // IP信息和端口信息,一定要发送到网络!
        // 本地格式->网络序列
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        local.sin_addr.s_addr = INADDR_ANY;
        // IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
        // 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
        // 此时只开在了栈上,要通过系统调用写入内核
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n == -1)
        {
            perror("bind:");
            LOG(LogLevel::FATAL) << "bind fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success!";
    }
    void Start()
    {
        _isrunning=true;
        while (_isrunning)
        {

            _isrunning = true;
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                // 发送消息
                int peer_port = ntohs(peer.sin_port);           // 从网络拿到的网络序列转换
                std::string peer_ip = inet_ntoa(peer.sin_addr); /// 4字节网络风格的IP -> 点分十进制的字符串风格的IP
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer; // 1. 消息内容 2. 谁发的??
                // 2. 发消息
                std::string echo_string = "server echo#";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
            }
            else
            {
                LOG(LogLevel::FATAL) << "recvfrom fail";
                exit(1);
            }
        }
    }

    ~UdpServer() {}

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip;
    // 用的是字符串风格,点分十进制, "192.168.1.1"
    func_t _func;
    bool _isrunning;
};

使用的相关接口

bash 复制代码
NAME
       send, sendto, sendmsg - send a message on a socket

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

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
bash 复制代码
NAME
       recv, recvfrom, recvmsg - receive a message from a socket

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

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
bash 复制代码
INET(3)                                                                    Linux Programmer's Manual                                                                    INET(3)

NAME
       inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr, inet_lnaof, inet_netof - Internet address manipulation routines

SYNOPSIS
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

       int inet_aton(const char *cp, struct in_addr *inp);

       in_addr_t inet_addr(const char *cp);

       in_addr_t inet_network(const char *cp);

       char *inet_ntoa(struct in_addr in);

       struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);

       in_addr_t inet_lnaof(struct in_addr in);

       in_addr_t inet_netof(struct in_addr in);
bash 复制代码
NAME
       bind - bind a name to a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);
bash 复制代码
NAME
       inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr, inet_lnaof, inet_netof - Internet address manipulation routines

SYNOPSIS
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

       int inet_aton(const char *cp, struct in_addr *inp);

       in_addr_t inet_addr(const char *cp);

       in_addr_t inet_network(const char *cp);

       char *inet_ntoa(struct in_addr in);

       struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);

       in_addr_t inet_lnaof(struct in_addr in);

而上面的makefile进行的操作,可以在多用户间进行通信,即你可以与你好友通信

bash 复制代码
.PHONY:all
# 修正目标名拼写(upserver → udpserver)
all:udpclient udpserver

# 修正编译命令:添加-o选项
udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++17 -lpthread

udpserver:UdpServer.cc
	g++ -o $@ $^ -std=c++17 -lpthread

# 可选:添加清理目标
.PHONY:clean
clean:
	rm -f udpclient udpserver

🔥个人主页: Milestone-里程碑

❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>

🌟心向往之行必能至

相关推荐
Book思议-1 小时前
【数据结构实战】:基于C语言单链表实现红旗渠景区年卡信息管理系统
c语言·开发语言·数据结构
Chase_______1 小时前
【快速入手 Python 基础 | 第1章】:数据存储与运算
开发语言·python
骇客野人1 小时前
Java springboot里注解大全和使用指南
java·开发语言·spring boot
谪星·阿凯1 小时前
CSRF&SSRF漏洞攻击:溯源解析与实战指南
安全·web安全·php·csrf
芥子沫1 小时前
Linux下编程有什么优势?
linux·运维·服务器
add45a1 小时前
C++与自动驾驶系统
开发语言·c++·算法
&星痕&1 小时前
从零开始手搓 (1)计算图 (c++,python语言实现)
c++·python·深度学习·机器学习
坚持学习前端日记1 小时前
python对接comfyui的过程
开发语言·网络·python