UDP通信

目录

一.预备知识

1.1IP与MAC

1.2端口号

1.3TCP与UDP协议

2.4网络字节序

二.socket编程接口

2.1socket常见API

2.2sockaddr结构

3.UDP网络程序

3.1服务端

3.1.1服务端创建套接字

3.1.2绑定服务端

3.1.3recvfrom

3.2客户端

3.2.1客户端创建套接字

3.2.2客户端绑定

3.2.3sendto

 3.3引入命令行参数

 3.4多人网络聊天制作

3.4.1客户端

3.4.2服务端

 3.5完整代码:



一.预备知识

1.1IP与MAC

IP地址,用来标识主机

源IP地址:发送方主机的IP地址

目的IP地址:接受方主机的IP地址

MAC地址,用来标识网卡

源MAC地址:发送方主机的MAC

目的MAC地址:接收方主机的MAC

补充:数据基本都是跨局域网传送的,网络层封装的报头当中有IP与MAC,IP地址是不会改变的。

但数据跨网络到达另一个局域网时,其源MAC地址和目的MAC地址就会发生变化。

1.2端口号

作用:用来标识一台计算机中的一个进程。

介绍下socket通信:

通过IP与MAC可找到公网上的唯一主机,通过端口号可找到那台主机上的对应的进程

通过IP与MAC已经可以完成数据的传输了,但本质其实是两台计算机的进程在进行通信,是跨网络的,而端口号就是来标识计算机中的进程,也分为源端口号,目的端口号。

  • 源端口号: 发送方主机的服务进程绑定的端口号,保证接收方能够找到对应的服务
  • 目的端口号 接收方主机的服务进程绑定的端口号,保证发送方能够找到对应的服务

补充内容:

1.端口号是传输层协议的内容。 

2.进程间的通信有共享内存,消息队列等,以及套接字(跨网络)

3.一个端口号只能被一个进程占用。一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。

4.进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。

  底层可采用哈希的方式,建立了端口号和进程PID或PCB之间的映射关系

1.3TCP与UDP协议

TCP:要进行数据传输,二者要先进行连接

  • 传输层协议
  • 有连接的
  • 可靠传输
  • 面向字节流

UDP:进行数据传输时,不需要进行连接,发送数据即可,但无法确定数据的可靠性

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

2.4网络字节序

大小端介绍

           大端:高位存放在低地址,低位存放在高地址

           小端:低位存放在低地址,高位存放在高地址

巧计:低低是小端

 网络上的字节流内存一样,也有大小端之分。

TCP/IP协议规定,网络数据流采用大端字节序,不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。(发送数据时按内存地址从低到高的顺序发出,接受数据时是按内存地址从低到高的顺序保存)

 使网络程序具有可移植性,调用以下库函数实现网络字节序和主机字节序之间的转换

#include 
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • h代表的是host,n代表的是network,s代表的是16位的短整型,l代表的是32位长整形
  • 如果主机是小端字节序,函数会对参数进行处理,进行大小端转换
  • 如果主机是大端字节序,函数不会对这些参数处理,直接返回

二.socket编程接口

2.1socket常见API

创建套接字:(TCP/UDP,客户端+服务器)

int socket(int domain, int type, int protocol);

绑定端口号:(TCP/UDP,服务器)

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

监听套接字socket :(TCP,服务器)

int listen(int sockfd, int backlog);

接收请求:(TCP,服务器)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

建立连接:(TCP,客户端)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2.2sockaddr结构

在这里插入图片描述

其中struct sockaddr_in是用于网络通信,struct sockaddr_un是用于本地通信。sockaddr_in结构体存储了协议家族,端口号,IP等信息,网络通信时可以通过这个结构体把自己的信息发送给对方,也可以通过这个结构体获取对端的这些信息。

为了能让套接字的网络通信和本地通信能够使用同一套函数接口,于是出现了sockeaddr结构体。其中这三个结构体的头部前16位是一样的,这个字段叫做协议家族。所以在用这个函数时可以统一传入sockeadder结构体,在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信。
 

补充内容:

1.IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
2.IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
3.socket API可以都用struct sockaddr* 类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

3.UDP网络程序

介绍:制作一个server服务端,用来提供各种服务。制作一个client客户端,可向服务端发送请求,服务端接受数据并处理后,再将数据返回给客户端。都将二者进行封装为一个类。

3.1服务端

3.1.1服务端创建套接字

创建套接字函数:

int socket(int domain, int type, int protocol);

参数解释:

domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是UDP的网络通信,就填入SOCK_DGRAM,叫做用户数据报服务。如果是TCP的网络通信,就填入SOCK_STREAM,叫做流式套接字,提供的是流式服务。
protocol:创建套接字的协议类别。你可以指明为TCP或UDP。设置为0表示的就是默认,就会根据前两个参数自动推导出协议类型。

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

代码:

class UdpServer
{
public:
    void init()  //初始化
    {
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
        std::cout << "创建套接字成功" << sockfd_ << std::endl;
        if (sockfd_ < 0)
        {
            cerr<< "socket error" << endl;
            exit(-1);
        }
    }

private:
    int sockfd_;     // 文件描述符
};

 补充内容:

1.我们是在应用层编写代码,其我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,我们在应用层调用的接口都是系统接口。

2.进程调用socket函数,会创建一个相对应文件结构体,同样是在当前进程的 task_struct 中的所指向的一个指针数组里填入那个文件结构体地址,建立关联,后将那个指针数组下标返回,就是文件描述符。

3.每一个文件结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等,但对于打开的“网络文件”来说,这里的文件缓冲区对应的是网卡。

3.1.2绑定服务端

当套接字创建成功后,对应的文件就被创建且已被当前进程管理了,当该文件实际还未与网络真正相关联起来。

bind函数:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数解释:

sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。

addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。

addrlen:传入的addr结构体的长度。

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

详细介绍struct sockaddr_in结构体:

UDP通信_第1张图片

  • sin_family:表示协议家族。
  • sin_port:表示端口号,是一个16位的整数。
  • sin_addr:表示IP地址,是一个32位的整数。
  • sin_addr的类型是struct in_addr,其实该结构体当中就只有一个成员,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的
  • UDP通信_第2张图片

 如何去理解绑定?

绑定就是将IP地址和端口号对应的网络文件建立关联,可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法。

 可在构造函数时将IP与端口号就初始化,

代码:

class UdpServer
{
public:
    UdpServer(int port, std::string ip = "")
        : ip_(ip), port_(port)
    {
    }
    void init()
    {
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
        std::cout << "创建套接字成功" << sockfd_ << std::endl;
        if (sockfd_ < 0)
        {
            exit(-1);
        }
        struct sockaddr_in local;
        bzero(&local, sizeof(sockaddr)); //将该结构体内容清空

        local.sin_family = AF_INET;      //进行填充
        local.sin_port = htons(port_);   //主机转网络
        local.sin_addr.s_addr = inet_addr(ip_.c_str());            // 会自动进行h->n转换
        int k = bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)); // 绑定
        if (k == -1)
        {
            exit(-2);
        }
        std::cout << "绑定成功" << k << std::endl;
    }
private:
    int sockfd_;     // 文件描述符
    int port_;       // 端口号
    std::string ip_; // IP地址
};

详细介绍字符串IP与整数IP:

字符串IP:127.239.231.987 这种形式,也做基于字符串的点分十进制IP地址。

整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址。

为何需要进行IP类型的转换?

若以字符串IP进行数据的发送,则需要15个字节的大小。若把字符串ip分为4部分,每个部分都是0-255,这只需要8个比特位,4部分就是32个比特位,也就是4字节。

如何进行转换?

可以运用位段的方式,2个变量共享一块空间。在一个结构体中定义一个32位比特位的整数,再定义一个结构体,运用位段的方式,如:

UDP通信_第3张图片

 操作系统也提供了几个IP类型转换的函数:

UDP通信_第4张图片

3.1.3recvfrom

用来获取客户端发送的数据

参数解释:

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等,方便后序我们对其进行响应
addrlen:输入输出型参数,传入一个想要读取对端src_addr的长度,最后返回实际读到的长度

补充:

recvfrom函数提供的参数是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。

代码:

  void start()
    {
        while (true)
        {
            char inbuffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t size = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct 
            sockaddr *)&peer, &len);

            if (size > 0)
            {
                inbuffer[size] = '\0';
                int port = ntohs(peer.sin_port);
                std::string ip = inet_ntoa(peer.sin_addr);
                std::cout << ip << ":" << port << "# " << inbuffer << std::endl;
            }
            else
            {
                std::cerr << "recvfrom error" << std::endl;
            }
          
        }
    }

3.2客户端

3.2.1客户端创建套接字

class Upclient
{
public:
    Upclient(std::string ip, int port)
        : ip_(ip), port_(port)
    {
    }

    void init()
    {
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
        if (sockfd_ < 0)
        {
            exit(-1);
        }
        std::cout << "创建套接字成功" << sockfd_ << std::endl;
    }
private:
    int sockfd_;     // 文件描述符
    int port_;       // 端口号
    std::string ip_; // IP地址
};

3.2.2客户端绑定

服务端是给别人提供服务的,当服务器启动后,许多的客户端就可以通过IP与端口号去找到对应服务端的那个进程,且服务端一直都在运行,随时可以被连接,所以IP与端口号不能随意改变。

客户端的端口号是不需要自己主动去绑定的,客户端可以随时退出,若绑定了一个端口,那么这个端口号就只能属于一个进程用,其它客户端就不能使用这个端口号了,其实用sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号,不能自己主动去绑定。

3.2.3sendto

用来向服务端发送请求

sendto函数:

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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。

addrlen:传入dest_addr结构体的长度。

返回值说明:成功返回实际写入的字节数,写入失败返回-1

补充:由于sendto函数提供的参数也是struct sockaddr*类型的,在传入结构体地址时需要将struct sockaddr_in*类型进行强转

代码:

    void strat()
    {
        struct sockaddr_in server;//填入对端网络信息
        bzero(&server, sizeof server);
        server.sin_family = AF_INET;
        server.sin_port = htons(port_);
        server.sin_addr.s_addr = inet_addr(ip_.c_str());

        std::string buffer;
        while (true)
        {
            std::cerr << "Please Enter# ";
            std::getline(std::cin, buffer);
            sendto(sockfd_, buffer.c_str(), buffer.size(), 0,
                   (const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函 
                                          //数的时候,我们的client会自动bind自己的ip和port
        }
    }

 3.3引入命令行参数

先把服务端与客户端完善。

服务端:


int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    std::string ip = "127.0.0.1"; // 本地环回
    int port = atoi(argv[1]);
    UdpServer *p = new UdpServer(port, ip);
    p->init();
    p->start();
    return 0;
}

客户端:

int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[1] << "ip" << argv[2] << "port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    Upclient *c = new Upclient(server_ip, server_port);
    c->init();
    c->strat();
    return 0;
}

结果:

UDP通信_第5张图片

 3.4多人网络聊天制作

介绍:按照前面的介绍,现在客户端可以向服务端发送数据,服务端也可以接受到数据。

改进:当客户端向服务端发送数据时,服务端把相关的客户端记录下来,并把收到的消息发给所有的客户端。同时,客户端也要可以接受服务端发送来的消息。这样就完成了一个多人聊天室。

3.4.1客户端

运行时创建一个线程,让这个线程去接受服务端发送的数据,主线程可继续向服务端发送数据。

 static void *_recvfrom(void *args)
    {
        while (true)
        {
            Upclient *up = (Upclient *)args;
            char inbuffer[1024];
            struct sockaddr_in tmp;
            socklen_t leng = sizeof(tmp);
            ssize_t size = recvfrom(up->sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, 
           (struct sockaddr *)&tmp, &leng);
            inbuffer[size]='\0';
            std::cout << " " << inbuffer << std::endl;
        }
    }
    void strat()
    {
        struct sockaddr_in server;
        bzero(&server, sizeof server);
        server.sin_family = AF_INET;
        server.sin_port = htons(port_);
        server.sin_addr.s_addr = inet_addr(ip_.c_str());

        std::string buffer;
        while (true)
        {
            std::cerr << "Please Enter# ";
            std::getline(std::cin, buffer);
            // 发送消息给server
            sendto(sockfd_, buffer.c_str(), buffer.size(), 0,
                   (const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函 
                                            //数的时候,我们的client会自动bind自己的ip和port
            pthread_t tid;
            pthread_create(&tid, nullptr, _recvfrom, (void *)this);
         }
    }

3.4.2服务端

 添加  std::unordered_map users 成员变量,用来记录发送数据的客户端的信息。

 void start()
    {
        while (true)
        {
            char inbuffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t size = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);

            if (size > 0)
            {
                inbuffer[size] = '\0';
                int port = ntohs(peer.sin_port);
                std::string ip = inet_ntoa(peer.sin_addr);
                std::cout << ip << ":" << port << "# " << inbuffer << std::endl;
                checkonline(ip, port, peer);  //记录所有
                sendmessage(ip,port,inbuffer);
            }
            else
            {
                std::cerr << "recvfrom error" << std::endl;
            }
          
        }
    }
    void checkonline(std::string ip, uint32_t port, struct sockaddr_in peer)
    {
        std::string key = ip;
        key += ":";
        key += std::to_string(port);
        auto iter = users.find(key);
        if (iter == users.end())
        {
            users.insert({key, peer});
        }
    }
    void sendmessage(std::string ip, uint32_t port, std::string messgae)
    {
        std::string str(ip);
        str += " ";
        str += std::to_string(port);
        str += " ";
        str += messgae;

        for (auto T : users)
        {
            sendto(sockfd_, str.c_str(), str.size(), 0, (const struct sockaddr *)&(T.second), sizeof(T.second));
        }
    }

结果:输出结果有点乱,可以把一个客户端收到的消息重定向到另一个文件中,这里不再演示。

 3.5完整代码:

服务端:

class UdpServer
{
public:
    UdpServer(int port, std::string ip = "")
        : ip_(ip), port_(port)
    {
    }
    void init()
    {
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
        std::cout << "创建套接字成功" << sockfd_ << std::endl;
        if (sockfd_ < 0)
        {
            exit(-1);
        }
        struct sockaddr_in local;
        bzero(&local, sizeof(sockaddr));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = inet_addr(ip_.c_str());                        // 会自动进行h->n转换
        int k = bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)); // 绑定
        if (k == -1)
        {
            exit(-2);
        }
        std::cout << "绑定成功" << k << std::endl;
    }

    void start()
    {
        while (true)
        {
            char inbuffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t size = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);

            if (size > 0)
            {
                inbuffer[size] = '\0';
                int port = ntohs(peer.sin_port);
                std::string ip = inet_ntoa(peer.sin_addr);
                std::cout << ip << ":" << port << "# " << inbuffer << std::endl;
                checkonline(ip, port, peer);
                sendmessage(ip,port,inbuffer);
            }
            else
            {
                std::cerr << "recvfrom error" << std::endl;
            }
          
        }
    }
    void checkonline(std::string ip, uint32_t port, struct sockaddr_in peer)
    {
        std::string key = ip;
        key += ":";
        key += std::to_string(port);
        auto iter = users.find(key);
        if (iter == users.end())
        {
            users.insert({key, peer});
        }
        else
        {
        }
    }
    void sendmessage(std::string ip, uint32_t port, char message[])
    {
        std::string str(ip);
        str += " ";
        str += std::to_string(port);
        str += " ";
        str += message;

        for (auto T : users)
        {
            sendto(sockfd_, str.c_str(), str.size(), 0, (const struct sockaddr *)&(T.second), sizeof(T.second));
        }
    }

private:
    int sockfd_;     // 文件描述符
    int port_;       // 端口号
    std::string ip_; // IP地址
    std::unordered_map users;
};

int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    std::string ip = "127.0.0.1"; // 本地环回
    int port = atoi(argv[1]);
    UdpServer *p = new UdpServer(port, ip);
    p->init();
    p->start();
    return 0;
}

客户端:

class Upclient
{
public:
    Upclient(std::string ip, int port)
        : ip_(ip), port_(port)
    {
    }

    void init()
    {
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
        std::cout << "创建套接字成功" << sockfd_ << std::endl;
        if (sockfd_ < 0)
        {
            exit(-1);
        }
    }

    static void *_recvfrom(void *args)
    {
        while (true)
        {
            Upclient *up = (Upclient *)args;
            char inbuffer[1024];
            struct sockaddr_in tmp;
            socklen_t leng = sizeof(tmp);
            ssize_t size = recvfrom(up->sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&tmp, &leng);
            inbuffer[size]='\0';
            std::cout << " " << inbuffer << std::endl;
        }
    }
    void strat()
    {
        struct sockaddr_in server;
        bzero(&server, sizeof server);
        server.sin_family = AF_INET;
        server.sin_port = htons(port_);
        server.sin_addr.s_addr = inet_addr(ip_.c_str());

        std::string buffer;
        while (true)
        {
            std::cerr << "Please Enter# ";
            std::getline(std::cin, buffer);
            // 发送消息给server
            sendto(sockfd_, buffer.c_str(), buffer.size(), 0,
                   (const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
            pthread_t tid;
            pthread_create(&tid, nullptr, _recvfrom, (void *)this);
        }
    }

private:
    int sockfd_;     // 文件描述符
    int port_;       // 端口号
    std::string ip_; // IP地址
};

int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[1] << "ip" << argv[2] << "port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    Upclient *c = new Upclient(server_ip, server_port);
    c->init();
    c->strat();
    return 0;
}

你可能感兴趣的:(计算机网络,网络,服务器,udp)