简单的UDP网络程序(后端服务器)

目录

 

准备工作

makefile

udpServer.hpp

udpServer.cc

细节1


服务端部署

创建套接字

接口认识1

socket

协议家族

绑定套接字

认识接口2

bind

sockaddr_in结构体类型

细节2

bzero

inet_addr

服务器启动(初启动)

udpServer.hpp

udpServer.cc

细节3

本地回环通信

认识指令1

netstat

细节4

代码整改

整改后代码

udpServer.hpp

udpServer.cc

认识接口3

recvfrom

参数介绍

inet_ntoa

start启动


客户端部署

认识接口4

sendto

同一台云服务器上

不同的服务器上

通信和业务逻辑解耦


全部代码

udpServer.hpp

udpServer.cc

udpClient.hpp

udpClient.cc

makefile


准备工作

这些先在Xshell上创建,后续直接使用VScode来进行编码

makefile

udpServer.hpp

#pragma once

#include <iostream>
#include <string>

namespace Server
{
    class udpServer
    {
    public:
        udpServer()
        {
        }
        void initServer()   //初始化
        {
        }
        void start()        //启动
        {
        }
        ~udpServer()        //析构
        {
        }
    };

}

udpServer.cc

#include "udpServer.hpp"
#include <memory>


using namespace std;
using namespace Server;

int main()
{
    std::unique_ptr<udpServer> usvr(new udpServer());

    usvr->initServer();
    usvr->start();


    return 0;
}

细节1

服务端部署

创建套接字

接口认识1

socket

参数

返回值

读写网络就像读写文件一样

初始化代码写到构造里面了,后面修改

协议家族

绑定套接字

认识接口2

bind

 这里的填充涉及到内存对齐方面,知道就好

sockaddr_in结构体类型

对类型进行了很多层的封装

细节2

从上面看到,我们可以得到一个问题:为什么库里面使用的是整型的ip地址,而我们是用string的ip地址的呢?

点分十进制和整数风格互转

bzero

往一段空间中填 0 

inet_addr

记得包含头文件

getopt

附:当我们想要做命令行解析的时候是可以用下面这个接口的,这里我们不使用(参数太少了,没必要使用)

服务器启动(初启动)

至此服务器已经可以正常启动

udpServer.hpp

#pragma once

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

namespace Server
{
    using namespace std;
    static const string defaultIp = "0.0.0.0";  //TODO

    enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR}; //1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
    class udpServer
    {
    public:
        udpServer(const uint16_t &port, const string& ip = defaultIp): _port(port),_ip(ip),_sockfd(-1)
        {
            //注意这里是直接写在构造里面的,是写错地方了,虽然运行是没有错的,由于修改图片太麻烦,下面统一进行了修改
            //1.创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if(_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR);   //创建套接字失败直接终止进程
            }
            //2.绑定套接字(port, ip)
            struct sockaddr_in local;   //这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
            bzero(&local, sizeof(local));   //先填 0 再修正
            //注意这下面几个名字是拼接出来的,就是那个##拼接而来的
            local.sin_family = AF_INET; //这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
            local.sin_port = htons(_port);//你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的 
            local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1. string->unit32_t  2. htonl(); -> inet_addr 
            int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
            if(n == -1)
            {
                cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
                exit(BIND_ERR);
            }
            //UDP Server 的预备工作完成

        }
        void initServer() // 初始化
        {
        }
        void start() // 启动
        {
            //服务器的本质其实就是一个死循环,
            for(;;)
            {
                sleep(1);
            }
        }
        ~udpServer() // 析构
        {
        }

    private:
        uint16_t _port;
        string _ip; // TODO
        int _sockfd;
    };
}

udpServer.cc

#include "udpServer.hpp"
#include <memory>

using namespace std;
using namespace Server;

static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_ip local_port\n\n"; //命令提示符
}

int main(int argc, char *argv[])
{
    if(argc != 3)   //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[2]);  //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
    string ip = argv[1];

    std::unique_ptr<udpServer> usvr(new udpServer(port, ip));

    usvr->initServer();
    usvr->start();


    return 0;
}

认识接口2

细节3

刚开始测试的地址是直接使用这一个

本地回环通信

127.0.0.1

认识指令1

netstat

查看网络的接口 netstat

细节4

假如绑定其他的公网IP地址需要注意

代码整改

所以一个服务器真实情况下是要接受任意ip发过来的通信,因此我们修改ip的,不需要传ip号了

整改后代码

udpServer.hpp

#pragma once

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

namespace Server
{
    using namespace std;
    static const string defaultIp = "0.0.0.0"; // 直接使用这个缺省值

    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR
    }; // 1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
    class udpServer
    {
    public:
        udpServer(const uint16_t &port, const string &ip = defaultIp) : _port(port), _ip(ip), _sockfd(-1)
        {}
        void initServer() // 初始化
        {
            // 1.创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR); // 创建套接字失败直接终止进程
            }
            // 2.绑定套接字(port, ip)
            struct sockaddr_in local;     // 这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
            bzero(&local, sizeof(local)); // 先填 0 再修正
            // 注意这下面几个名字是拼接出来的,就是那个##拼接而来的
            local.sin_family = AF_INET;                     // 这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
            local.sin_port = htons(_port);                  // 你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
            local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->unit32_t  2. htonl(); -> inet_addr
            // local.sin_addr.s_addr = htonl(INADDR_ANY);  //可以主机转网络,不够也可以不处理,直接赋值也行
            int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n == -1)
            {
                cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
                exit(BIND_ERR);
            }
            // UDP Server 的预备工作完成
        }
        void start() // 启动
        {
            // 服务器的本质其实就是一个死循环,
            for (;;)
            {
                sleep(1);
            }
        }
        ~udpServer() // 析构
        {
        }

    private:
        uint16_t _port;
        // 实际上,一款网络服务器,不建议指明一个IP,因为一个服务器可以能有多个ip,万一用户使用其他的ip地址来访问该端口号(这里是8080,就收不到了),这也是我们为什么使用0.0.0.0的IP缺省值
        string _ip;
        int _sockfd;
    };
}

udpServer.cc

#include "udpServer.hpp"
#include <memory>

using namespace std;
using namespace Server;

static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_port\n\n"; //命令提示符
}

int main(int argc, char *argv[])
{
    if(argc != 2)   //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);  //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
    //string ip = argv[1];

    std::unique_ptr<udpServer> usvr(new udpServer(port));

    usvr->initServer();
    usvr->start();


    return 0;
}

        注意这里是服务端是不需要IP,可以接受任意IP地址发来的请求,但是客户端是需要的,这一点后面再细谈,所以并不会造成淘宝的请求跑到京东去了

        这里为什么使用8080的因为服务器可以有很多端口号,当服务器收到了大量的数据,并不是全部都是由一个端口号来进行处理的,也可能是8081之类的端口号,这时候的端口号是没有意义的,后序会详谈,其实不同的端口号是有指定的绑定的不能任意绑定,这是因为只有我自己使用

        且无论是UDP还是TCP都是采取这样的形式,接受任意IP的数据,通过端口号来确定谁是谁处理

认识接口3

recvfrom

读取数据

参数介绍

socket_t

inet_ntoa

        将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)

 

start启动

void start() // 启动
        {
            // 服务器的本质其实就是一个死循环
            char buffer[gnum]; // 定义一个数组来充当缓冲区
            for (;;)
            {
                // 读取数据
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); // 必须设置成这个结构体的大小,当作为输入时,告诉recvfrom的长度的多少
                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                // 关系两件事情
                // 1.数据是什么? 2. 谁发的?
                if (s > 0)
                {
                    buffer[s] = 0;
                    // 因为是从网络上读取的,所以一定要转,可以使用接口
                    string clientip = inet_ntoa(peer.sin_addr); // 1.网络序列 2.整数 -> 点分十进制的ip
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;

                    cout << clientip << "[" << clientport << "]# " << message << endl;
                }
            }
        }

至此服务器端基本完成,停下来处理客户端

客户端部署

认识接口4

sendto

sendto告诉客户端要发给谁

0~1023在云服务器上已经被绑定了 

同一台云服务器上

不同的服务器上

现在无法跨主机发送消息,权限问题,后续解决

sz:下载到本地

rz:上传到服务器

chmod:修改权限

注意一台新的云服务器

至于如何打开端口后续文章介绍 -- 未完持续

通信和业务逻辑解耦

        我们可以添加function来对业务逻辑进行解耦操作,融入下面代码

        function对server通信和业务逻辑解耦!

全部代码

udpServer.hpp

#pragma once

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

namespace Server
{
    using namespace std;
    static const string defaultIp = "0.0.0.0"; // 直接使用这个缺省值
    static const int gnum = 1024;
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR
    }; // 1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误

    typedef function<void(string, uint16_t, string)> func_t;

    class udpServer
    {
    public:
        udpServer(const func_t &cd, const uint16_t &port, const string &ip = defaultIp)
            : _callback(cd), _port(port), _ip(ip), _sockfd(-1)
        {
        }
        void initServer() // 初始化
        {
            // 1.创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR); // 创建套接字失败直接终止进程
            }
            cout << "udpServer success: "
                 << " : " << _sockfd << endl;

            // 2.绑定套接字(port, ip)
            // 未来服务器要明确的port, 不能随意改变 -- 变了别人就找不到了
            struct sockaddr_in local;     // 这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
            bzero(&local, sizeof(local)); // 先填 0 再修正
            // 注意这下面几个名字是拼接出来的,就是那个##拼接而来的
            local.sin_family = AF_INET;                     // 这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
            local.sin_port = htons(_port);                  // 你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
            local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->unit32_t  2. htonl(); -> inet_addr
            // local.sin_addr.s_addr = htonl(INADDR_ANY);  //可以主机转网络,不够也可以不处理,直接赋值也行
            int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n == -1)
            {
                cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
                exit(BIND_ERR);
            }
            // UDP Server 的预备工作完成
        }
        void start() // 启动
        {
            // 服务器的本质其实就是一个死循环
            char buffer[gnum]; // 定义一个数组来充当缓冲区
            for (;;)
            {
                // 读取数据
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); // 必须设置成这个结构体的大小,当作为输入时,告诉recvfrom的长度的多少
                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                // 关系两件事情
                // 1.数据是什么? 2. 谁发的?
                if (s > 0)
                {
                    buffer[s] = 0;
                    // 因为是从网络上读取的,所以一定要转,可以使用接口
                    // inet_ntoa 将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)
                    string clientip = inet_ntoa(peer.sin_addr); // 1.网络序列 2.整数 -> 点分十进制的ip
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;

                    cout << clientip << "[" << clientport << "]# " << message << endl;

                    //我们只把数据读上来就完了吗? 我们要对数据进行处理 -- 所以我们用回调函数的方式来解决
                    _callback(clientip, clientport, message);
                }
            }
        }
        ~udpServer() // 析构
        {
        }

    private:
        uint16_t _port;
        // 实际上,一款网络服务器,不建议指明一个IP,因为一个服务器可以能有多个ip,万一用户使用其他的ip地址来访问该端口号(这里是8080,就收不到了),这也是我们为什么使用0.0.0.0的IP缺省值
        string _ip;
        int _sockfd;
        func_t _callback; // 回调函数,用以处理数据
    };
}

udpServer.cc

#include "udpServer.hpp"
#include <memory>

using namespace std;
using namespace Server;

static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_port\n\n"; //命令提示符
}


void handlerMessage(string clientip, uint16_t clientport, string message)
{
    //这里就可以对message进行特定的业务处理,而不关心message怎么来的 --- 这就是server通信和业务逻辑解耦!
}

int main(int argc, char *argv[])
{
    if(argc != 2)   //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);  //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
    //string ip = argv[1];

    std::unique_ptr<udpServer> usvr(new udpServer(handlerMessage, port));

    usvr->initServer();
    usvr->start();


    return 0;
}

udpClient.hpp

#pragma once

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

namespace Client
{
    using namespace std;
    class udpClient
    {
    public:
        udpClient(const string &serverip, const uint16_t &serverport)
            : _serverip(serverip), _serverprot(serverport), _sockfd(-1), _quit(false)
        {
        }
        void initClient()
        {
            // 1. 创建socket
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(1); // 创建套接字失败直接终止进程
            }
            cout << "socket success: " << " : " << _sockfd << endl;   //成功打印出来

            // 2. client要不要bind(必须要!)因为bind就是和系统、网络产生联系. client要不要显示的bind -> 需不需要程序员bind? 名字不重要,重要的是唯一性的,和服务端是不一样的
            // 就像宿舍号是几不重要,有就行了。一个端口号只能被一个客户端绑定,就像是服务端是明星,客户端是民众,民众名字不重要
            // 服务端是具体的一家公司,比如抖音是字节的,就像一个手机有很多app比如抖音,快手这样的客户端,不能让它们固定bind端口号,万一其他公司也用了用一个端口就冲突其他不来了
            // 写服务器的是一家公司,写client是无数家公司 -- 有OS自动形成端口进行bind!不需要自己操作,包括ip地址也不需要,OS自己会处理,当然也可以自己写
            // 那么OS在什么时候,如何bind
        }
        void run()
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverprot);

            string messages;
            while (!_quit)
            {
                cout << "Please Enter# ";
                cin >> messages;

                sendto(_sockfd, messages.c_str(), sizeof(messages), 0, (struct sockaddr *)&server, sizeof(server));
            }
        }
        ~udpClient()
        {
        }

    private:
        int _sockfd;
        string _serverip;
        uint16_t _serverprot;
        bool _quit;
    };

}

udpClient.cc

#include "udpClient.hpp"
#include <memory>

using namespace Client;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n"; //命令提示符
}

// ./udpClient server_ip server_port    第一个是运行方式 要知道要发送的服务端的ip地址 和 端口号
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);


    unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));

    ucli->initClient();
    ucli->run();

    return 0;
}

makefile

cc=g++

.PHONY:all
all:udpClient udpServer


udpClient:udpClient.cc
	$(cc) -o $@ $^ -std=c++11 
udpServer:udpServer.cc
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpClient udpServer

至于如何打开端口后续文章介绍 -- 简单的UDP网络程序·续写

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清风玉骨

爱了!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值