TCI/IP网络编程(7) 多播与广播

应用场景:
假设服务端需要同时向10000个客户端发送同样的通知消息,如果利用TCP,需要维护10000个套接字连接,如果是基于UDP,也需要进行10000数据传输,向大量客户端发送相同的数据,会对服务器端和网络流量产生负面的影响。

1. 多播

多播方式的数据传输是基于UDP完成的,因此其与UDP客户端/服务端的实现方式,区别在于,UDP传输以单一目标地址进行,而多播模式下,数据会同时传递到所有加入注册组的的主机,即采用多播方式的时候,可以同时向多个主机传输相同的数据。

多播的数据传输特点:

  1. 多播服务器端,对特定的多播组,仅发送1次数据
  2. 即使只发送一次数据,多播组内的所有客户端都会接收到数据
  3. 多播组可在IP地址范围内任意增加
  4. 加入特定组即可接收到发往该多播组的数据

多播组是D类IP地址(244.0.0.0~239.255.255.255),因为多播是借助UDP完成的,因此多播数据包的格式与UDP数据包的格式相同。但是在向网络传递一个多播数据包的时候,需要路由器复制多个数据包,并传输到多个主机,也就是说,多播需要借助路由器来完成。

多播路由示意图
标题多播路由示意图

 

通过多播路由示意图可知,因为路由频繁复制同一数据包,也同样会不利于网络流量,但是从另一方面考虑,多播不会向同一区域发送多个相同的数据包。例如,如果通过TCP或者UDP向10000个用户发送相同的数据包,则共需要传递10000次,即使将所有的主机都合到同一个网络,也是如此。如果使用多播,则只需要发送1次数据包,由10000台主机构成的网络中的路由器负责复制文件并传递到主机,由于这种特性,多播主要用于多媒体数据的实时传输。(如在线视频,直播,这是个人理解)。

在实际的应用中,部分路由器不支持多播,或者即使支持多播,也由于网络拥堵的原因而故意阻断多播,因此为了在不支持多播的路由器中完成多播通信,也会采用隧道技术(不做介绍)。

2. 路由和TTL

为了进行多播数据传播,必须设置TTL(Time to live),TTL是决定数据包传输距离的参数,TTL为整数表示的参数,数据包每经过路由器复制一次,TTL的值就会减1,当TTL的值为0时,数据包就会销毁,不再传输。因此TTL的值需要设置的比较合适,过大则会影响影响网络流量,过小则会导致数据包传输不到目标主机,TTL参数的设置通过套接字可选项完成:

// 服务端send socket设置多播模式以及TTL

int sendSocket;
int time_to_live;

sendSocket = socket(PF_INET, SOCK_DGRAM, 0);
// 设置TTL
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*)&time_to_live, sizeof(time_to_live));

加入多播组也需要通过套接字选项完成:

// 客户端recv socket设置加入组播

int recvSocket;
struct ip_mreq join_addr;

recvSocket = socket(PF_INET, SOCK_DGRAM, 0);

join_addr.imr_multiaddr.s_addr = "多播地址";   // D类地址
join_addr.imr_interface.s_addr = "加入多播组的主机地址"

// 设置主机加入多播组
setsockopt(recvSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void*)&join_addr, sizeof(join_addr));

结构体ip_mreq 的定义如下:

struct ip_mreq
{
    struct in_addr imr_multiaddr;      // 加入的多播组的IP地址
    struct in_addr imr_interface;      // 加入多播组的套接字所属的主机IP地址,也可以为INADDR_ANY
}

3.多播的代码实现

Sender发送端代码:

// multicast.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h>       // 这个定义需要放在WinSock2的后面
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

#define TTL        32
#define BUFF_SIZE  64
#define MULTI_CAST_ADDR   "244.1.1.4"
#define MULTI_CAST_PORT   13000 

int main()
{
    WSADATA wsaData;

    char buffer[BUFF_SIZE];
    memset(buffer, 0, BUFF_SIZE);

    int timeToLive = TTL;

    FILE* fp;      

    SOCKADDR_IN multicastAddr;     // 多播地址

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("Failed to wsaStartUp!\n");
        return -1;
    }

    SOCKET sendSock = socket(PF_INET, SOCK_DGRAM, 0);

    if (sendSock == SOCKET_ERROR)
    {
        printf("Failed to init socket.\n");
        WSACleanup();
        return -1;
    }

    // 设置多播地址
    memset(&multicastAddr, 0, sizeof(multicastAddr));
    multicastAddr.sin_family = AF_INET;
    multicastAddr.sin_addr.s_addr = inet_addr(MULTI_CAST_ADDR);
    multicastAddr.sin_port = htons(MULTI_CAST_PORT);

    // 设置套接字可选项
    setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&timeToLive, sizeof(timeToLive));

    // 读取文件
    if ((fp = fopen("data.txt", "rb")) == NULL)
    {
        printf("Failed to open data file.\n");
        closesocket(sendSock);
        WSACleanup();
        return -1;
    }

    while (!feof(fp))
    {
        memset(buffer, 0, BUFF_SIZE);       // 这里必须每次将缓冲区清空,否则会出现读取的内容出现乱码

        //fread(buffer, BUFF_SIZE, 1, fp);      // fgets读入文件  fread读取和fgets读取的结果是有差距的
        fgets(buffer, BUFF_SIZE, fp);

        // buffer[BUFF_SIZE - 1] = 0;

        int len = strlen(buffer);
        if (len > 0)
        {

            sendto(sendSock, buffer, len, 0, (SOCKADDR*)&multicastAddr, sizeof(multicastAddr));

            printf("Send %d bytes data to the multi cast group.\n", len);

            printf("Content: %s\n", buffer);

            Sleep(1000);        // 延时1s
        }
    }

    fclose(fp);

    // 关闭socket
    closesocket(sendSock);
    WSACleanup();

    return 0;
}

Recevier接收端代码:

// receiver.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h>       // 这个定义需要放在WinSock2的后面
#include <stdio.h>

#define BUFF_SIZE  64
#define MULTI_CAST_ADDR   "244.1.1.4"
#define MULTI_CAST_PORT   13000 

#pragma comment(lib, "Ws2_32.lib")

int main()
{
    WSADATA wsaData;

    char buffer[BUFF_SIZE];

    FILE* fp;

    SOCKADDR_IN addr;     // 接收来自任意地址的报文

    struct ip_mreq join_addr; 

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("Failed to wsaStartUp!\n");
        return -1;
    }

    SOCKET recvSock = socket(PF_INET, SOCK_DGRAM, 0);

    if (recvSock == SOCKET_ERROR)
    {
        printf("Failed to init socket.\n");
        WSACleanup();
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(MULTI_CAST_PORT);

    join_addr.imr_multiaddr.s_addr = inet_addr(MULTI_CAST_ADDR);
    join_addr.imr_interface.s_addr = htonl(INADDR_ANY);

    // 设置套接字选项
    setsockopt(recvSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&join_addr, sizeof(join_addr));

    int res = bind(recvSock, (SOCKADDR*)&addr, sizeof(addr));
    if (res == SOCKET_ERROR)
    {
        printf("Failed to bind the socket.\n");
        closesocket(recvSock);
        WSACleanup();
        return -1;
    }

    fp = fopen("recevie.txt", "a");

    while (true)
    {
        memset(buffer, 0, BUFF_SIZE);

        int len = recvfrom(recvSock, buffer, BUFF_SIZE, 0, NULL, NULL);      // 不需要最后的fromAddr和fromLen这两数据

        if (len < 0)
            break;

        printf("Receive %d Bytes: %s\n", len, buffer);

        fwrite(buffer, len, 1, fp);

        printf("write conten to file.\n");
    }

    // flush(fp);

    fclose(fp);

    closesocket(recvSock);
    WSACleanup();

    return 0;
}

4. 广播

广播也用于一次性向多个主机主机发送数据,与组播相比数据的传输范围有区别。多播即使在跨不同网络的情况下,只要加入了多播组,就能够接收数据,而广播只能向同一网络中的多个主机传输数据。按照广播时使用的IP地址的形式,广播可分为两种:

  • 直接广播 (Direct broadcast)
  • 本地广播 (Local broadcast)

直接广播的IP地址中,除了网络地址外,其余主机地址全部设置为1,可以采用直接广播的方式向特定区域内的所有主机传输数据。例如,需要向网络地址192.12.34中的所有主机传输数据的时候,可以将数据传输到192.12.34.255,此时网络192.12.34中的所有主机都会收到数据。

本地广播使用的IP地址限定为255.255.255.255,如果网络192.12.34中的主机向255.255.255.255传输数据时,网络192.12.34中的所有主机将会收到数据。

广播仅仅需要通过修改UD套接字的可选项即可完成,其余内容与普通UDP通讯完全类似。

5.广播的代码实现

Sender代码示例:

// multicast.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h>       // 这个定义需要放在WinSock2的后面
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

#define BUFF_SIZE  32
#define BROADCAST_ADDR  "255.255.255.255"
#define BROADCAST_PORT  18500

int main()
{
    WSADATA wsaData;

    char buffer[BUFF_SIZE];
    memset(buffer, 0, BUFF_SIZE);

    FILE* fp;   

    SOCKADDR_IN boradAddr;

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("Failed to wsaStartUp!\n");
        return -1;
    }

    SOCKET sendSock = socket(PF_INET, SOCK_DGRAM, 0);

    if (sendSock == SOCKET_ERROR)
    {
        printf("Failed to init socket.\n");
        WSACleanup();
        return -1;
    }

    // 设置广播地址
    memset(&boradAddr, 0, sizeof(boradAddr));
    boradAddr.sin_family = AF_INET;
    boradAddr.sin_addr.s_addr = inet_addr(BROADCAST_ADDR);
    boradAddr.sin_port = htons(BROADCAST_PORT);

    // 设置套接字可选项
    int enableBoradcast = 1;
    setsockopt(sendSock, SOL_SOCKET, SO_BROADCAST, (char*)&enableBoradcast, sizeof(enableBoradcast));   // 使能SO_BROADCAST

    // 读取文件
    if ((fp = fopen("data.txt", "rb")) == NULL)
    {
        printf("Failed to open data file.\n");
        closesocket(sendSock);
        WSACleanup();
        return -1;
    }

    while (!feof(fp))
    {
        memset(buffer, 0, BUFF_SIZE);       // 这里必须每次将缓冲区清空,否则会出现读取的内容出现乱码

        //fread(buffer, BUFF_SIZE, 1, fp);      // fgets读入文件  fread读取和fgets读取的结果是有差距的
        fgets(buffer, BUFF_SIZE, fp);           // fread用于读取按字节读取快数据(结构体),fget用于读取文本文件,遇到换行符结束
                                                // fgets实际每次读取BUFF_SIZE-1个字符,最后一个字符自动添加"\0";
        // buffer[BUFF_SIZE - 1] = 0;

        int len = strlen(buffer);
        if (len > 0)
        {

            sendto(sendSock, buffer, len, 0, (SOCKADDR*)&boradAddr, sizeof(boradAddr));     // 广播消息

            printf("Send %d bytes data to the multi cast group.\n", len);

            printf("Content: %s\n", buffer);

            Sleep(1000);        // 延时1s
        }
    }

    fclose(fp);

    // 关闭socket
    closesocket(sendSock);
    WSACleanup();

    return 0;
}

Receiver实例代码:

// receiver.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h>       // 这个定义需要放在WinSock2的后面
#include <stdio.h>

#define BUFF_SIZE  32
#define MULTI_CAST_PORT   18500 

#pragma comment(lib, "Ws2_32.lib")

int main()
{
    WSADATA wsaData;

    char buffer[BUFF_SIZE];

    FILE* fp;

    SOCKADDR_IN addr;     // 接收来自任意地址的报文

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("Failed to wsaStartUp!\n");
        return -1;
    }

    SOCKET recvSock = socket(PF_INET, SOCK_DGRAM, 0);

    if (recvSock == SOCKET_ERROR)
    {
        printf("Failed to init socket.\n");
        WSACleanup();
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(MULTI_CAST_PORT);

    int res = bind(recvSock, (SOCKADDR*)&addr, sizeof(addr));
    if (res == SOCKET_ERROR)
    {
        printf("Failed to bind the socket.\n");
        closesocket(recvSock);
        WSACleanup();
        return -1;
    }

    fp = fopen("recevie.txt", "a");

    while (true)
    {
        memset(buffer, 0, BUFF_SIZE);

        int len = recvfrom(recvSock, buffer, BUFF_SIZE, 0, NULL, NULL);      // 不需要最后的fromAddr和fromLen这两数据

        if (len < 0)
            break;

        printf("Receive %d Bytes: %s\n", len, buffer);

        // fwrite(buffer, len, 1, fp);
        fputs(buffer, fp);

        printf("write conten to file.\n");
    }

    // flush(fp);

    fclose(fp);

    closesocket(recvSock);
    WSACleanup();

    return 0;
}

运行结果:
TCI/IP网络编程(7) 多播与广播

原文链接: https://www.cnblogs.com/ncepubye/p/17012742.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    TCI/IP网络编程(7) 多播与广播

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/396390

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月6日 上午11:17
下一篇 2023年4月6日 上午11:17

相关推荐