linux【网络编程】之UDP网络程序模拟实现

一、开发环境

本次实验是在腾讯云服务器上进行

二、服务端实现

做完这次实验,感受最深的就是函数接口方面的问题,我们先来介绍一下需要用到的接口。

2.1 接口认识

2.1.1 socket创建网络通信套接字


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数domain:域,未来套接字是进行网络通信(AF_INET)还是本地通信(AF_UNIX, AF_LOCAL)

参数type:套接字提供服务的类型,如SOCK_STREAM:流式服务TCP策略,SOCK_DGRAM:数据报服务,UDP策略

参数protocol:缺省为0,可由前两个类型确定

返回值:失败返回-1,成功返回文件描述符

2.1.2 bind:绑定Ip和端口号

绑定端口号和ip
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数sockfd:文件描述符,也就是调用socket的返回值

参数addr:利用struct sockaddr_in强转

参数addrlen:结构体的大小

返回值:成功返回0,失败返回-1

2.1.3 sockaddr_in结构体

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);//协议家族,对应AF_INET
    in_port_t sin_port;		//端口号,in_port_t是对port的重命名
    struct in_addr sin_addr;//IP地址,in_addr结构体里封装了一个32位整数

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

#define	__SOCKADDR_COMMON(sa_prefix) 
  sa_family_t sa_prefix##family
//sa_family_t //16位整数

typedef uint16_t in_port_t;

typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

宏中的**##**是将两个字符串合并成一个新字符串,也就是将接收到的sa_prefix与family合并起来,形成了sa_prefix_family

创建结构体后要先清空数据

#include <strings.h>
void bzero(void *s, size_t n);

2.1.4 IP地址转换函数:inet_addr、inet_ntoa

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
inet_addr内部完成两件事情:1.把字符串转化成整数;2.再把整数转化成对应的网络序列**
in_addr_t inet_addr(const char *cp);
//const char*cp:点分十进制风格的IP地址

//1.网络->主机 2.uint32_t -> 点分十进制
char *inet_ntoa(struct in_addr in);

2.1.5 recvfrom:读取数据

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

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

sockfd:特定的套接字,buf:读取到特定缓冲区,len:结构体大小
flags:读取的方式,默认为0,阻塞读取
src_addr:输入输出型参数,收到消息除了本身,还得知道是那个IP+port发过来的数据
len:大小是多少
返回-1表示失败,成功返回字节数

2.2 头文件udpServer.hpp

头文件中:构造负责初始化参数
initServer函数负责初始化服务器:1.创建套接字;2.将套接字与IP地址和端口号绑定。
start()函数服务器本质是一个死循环,在start启动的时候,通过recvfrom读取通过网络发来的数据、源ip+源端口号分别保存到缓冲区和struct sockaddr这个结构体中

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <functional>
namespace Server
{
    using namespace std;

    //默认的点分十进制IP
    static const string defaultIP="0.0.0.0";
    static const int gnum=1024;//缓冲区大小
    
    enum{USAGE_ERR=1,SOCKET_ERR,BIND_ERR};//退出码

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

    class udpServer
    {
    public:
        udpServer(const func_t& callback,const uint16_t &port,const string& ip=defaultIP)
            :callback_(callback),port_(port),ip_(ip),sockfd_(-1)
        {}
        void initServer()
        {
            //****创建UDP网络通信端口****
            sockfd_=socket(AF_INET,SOCK_DGRAM,0);
            if(sockfd_==-1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success: " << " : " << sockfd_ << endl;

            //******绑定port,ip****
            //服务器要明确绑定port,不能随意改变,需要程序员显示指明
            struct sockaddr_in local;
            bzero(&local,sizeof(local));//对结构体数据清0

            //填充结构体

            local.sin_family=AF_INET;
            //大小端转换,给别人发消息,端口号和IP地址也要发给对方
            local.sin_port=htons(port_);//端口号转网络

            //1.string->uint32_t  2.htonl()---->两个工作统一交给inet_addr
            local.sin_addr.s_addr=inet_addr(ip_.c_str());//ip转网络
            // local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址绑定,服务器真正写法,上面与这个选一个就行

            int n=bind(sockfd_,(struct sockaddr*)&local,sizeof(local));
            if(n==-1)
            {
                cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
                exit(BIND_ERR);
            }
        }
        void start()
        {
            char buffer[gnum];
            for(;;)
            {
                //****读取数据****

                //服务器的本质就是一个死循环
                struct sockaddr_in peer;//做输入输出型参数
                socklen_t len=sizeof(peer);//必填
                //数据获取:IP地址+端口号+数据
                ssize_t s= recvfrom(sockfd_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
                if(s>0)
                {
                    buffer[s]=0;
                    //1.网络->主机 2.uint32_t -> 点分十进制
                    //string clientip=peer.sin_addr.s_addr;需要转两步,不方便 
                    string clientip=inet_ntoa(peer.sin_addr);
                    uint16_t clientport=ntohs(peer.sin_port);
                    string message=buffer;

                    cout<<clientip<<"["<<clientport<<"]#"<<message<<endl;
                    //利用回调方法处理数据,实现解耦
                    callback_(clientip,clientport,message);
                }
            }
        }

        ~udpServer(){}
    private:
        uint16_t port_;//端口号
        string ip_;//IP地址
        int sockfd_;//文件描述符
        func_t callback_;//回调
    };
}

2.3 绑定IP和port问题

  • 一般情况下,服务器不会绑定某一个确定的IP,避免因绑定一个确定的
    IP而漏掉另一个ip发过来的数据,实际情况是,将ip设置为全0,任何提交到服务器的开放端口的数据都会被该服务器处理
  • 服务器不需要绑定一个固定IP(目的ip有多个,可能是本地环回、内网IP、公网IP),只要是访问服务器上的某个开放的端口,都会把数据拿过来处理,但这并不意味着客户端不需要绑定IP
  • 这个IP是目的IP,是已经收到了数据向上交付的时候,不需要绑定IP,只需要看端口号就行了

在这里插入图片描述
本地环回:客户端与服务端在一台主机上,进行通信的时候数据贯穿协议栈流动,但不会到达物理层(出不去),仅测试
在这里插入图片描述
内网IP:真正属于这个服务器的IP,同一个品牌的服务器可以用内网ip通信
公网IP:云服务器是虚拟的,不能直接bind公网IP
虚拟机或者真正的linux可以绑定
内网IP可以被绑定

2.4 源文件udpServer.cc

服务器端进行测试的时候不需要提供IP地址

#include "udpServer.hpp"

#include <memory>
using namespace std;
using namespace Server;

static void Usage(string proc)
{
    cout << "nUsage:nt" << proc << " local_portnn";
}

void handlerMessage(string,uint16_t,string)
{
    //对message进行处理,完成server通信与业务逻辑解耦
}

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port=atoi(argv[1]);
    std::unique_ptr<udpServer> usvr(new udpServer(handlerMessage,port));
    usvr->initServer();
    usvr->start();

    return 0;
}

三、客户端实现

3.1 接口认识

3.1.1 数据发送:sendto

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

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:发送给哪个套接字,
buf:要发送的消息;len:消息的字节数;flags:0,有数据就发,没数据就阻塞
dest_addr:这个结构体也是由struct sockaddr_in强转,指明向谁发,填充服务器的IP和端口
addrlen:输入型参数

3.2 头文件udpClient.hpp

在这个文件中:构造函数负责初始化变量

initClient函数负责创建套接字但是不需要程序员手动绑定端口号,OS会帮我们绑
服务端明确绑定是因为需要客户端知道,并且不能随便改变;未来是多个客户端访问一个服务端,客户端端口号是多少不重要,保证唯一性就行

run()函数负责发送数据到服务端,需要用到sendto函数,而sendto函数在首次发送数据的时候,OS发现客户端还未绑定端口号,会令sendto函数自动绑定一个端口号

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
namespace Client
{
    using namespace std;

    // //默认的点分十进制IP
    // static const string defaultIP="0.0.0.0";
    // static const int gnum=1024;
    
    enum{USAGE_ERR=1,SOCKET_ERR,BIND_ERR};//退出码

    class udpClient
    {
    public:
        udpClient(const string& serverip,const uint16_t&serverport)
            :serverip_(serverip),serverport_(serverport),sockfd_(-1),quit_(false)
        {}
        void initClient()
        {
            //****1.创建UDP网络通信端口****
            sockfd_=socket(AF_INET,SOCK_DGRAM,0);
            if(sockfd_==-1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success: " << " : " << sockfd_ << endl;

            //****Client必须要bind,但是Client不需要显示bind(不需要自己写)******/
            //客户端端口是多少不重要,只要能保证唯一性就行,让OS自己去绑
        }

        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(serverport_);

            string message;
            while (!quit_) 
            {
                cout<<"Please Enter# "<<endl;
                cin>>message;
                //首次向服务器发送数据的时候,OS识别到还未绑定,sendto自动绑定ip+port
                sendto(sockfd_,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
            }
            
        }

        ~udpClient()
        {}
    private:
        int sockfd_;
        string serverip_;
        uint16_t serverport_;
        bool quit_;
    };
}

3.3 源文件udpClient.cc

客户端进行测试的时候必须要提供IP地址和端口号

#include "udpClient.hpp"
#include <memory>
using namespace std;
using namespace Client;

static void Usage(string proc)
{
    cout << "nUsage:nt" <<proc << " server_ip server_portnn";
}

//udpClient server_ip server_port
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    //客户端必须要知道发给谁
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

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

    return 0;
}

四、结果展示

在这里插入图片描述

无论是客户端还是服务端的测试文件,所要传递的参数(ip、port)与头文件中struct sockaddr_in需要填充的ip、port没有关系

这篇博客只是讲解一下UDP下网络通信的逻辑,关于它的应用方面会在近期推出,敬请关注!!!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>