alin的学习之路(Linux网络编程:七)(UDP广播、UDP组播、本地套接字)

alin的学习之路(Linux网络编程:七)(UDP广播、UDP组播、本地套接字)

1. UDP 广播

通过UDP 广播可以将数据发送给同一网段下的所有指定端口号的进程。但是不能够在Internet上传输数据,只能在局域网中。

注意:只要是存在在与服务器同一网段内的主机,都会收到广播的数据,无论是否需要。

  • 广播地址:192.168 .xxx. 255
  • 服务器的地址(IP+port)不那么重要了。 可以依赖隐式绑定。
  • 客户端的port,必须要固定。不能再依赖隐式绑定。
  • 只适用于 “局域网” 通信。

1. 开放广播权限

使用 setsockopt() 函数

int setsockopt(int sockfd, int level, int optname,
           const void *optval, socklen_t optlen);
// 示例:
int flag = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));
	sockfd:套接字fd
    level:SOL_SOCKET
    optname:SO_BROADCAST   (表示广播)
    optval:&flag
	optlen:flag的长度。

2. 实现广播的流程

server端实现流程

  1. socket() 函数创建套接字sockfd
  2. bind() 函数绑定自己的ip+port。可以依赖隐式绑定即不写该代码。因为:1.不用建立连接。2.数据不发回
  3. setsockopt() 函数开放广播权限
  4. 初始化客户端clt_addr,不能依赖隐式绑定,ip要设为指定的 “192.168.xxx.255”,端口号设置一个固定的值
  5. sendto(sockfd,&clt_addr);
  6. close(sockfd);

client端实现流程

  1. socket() 函数创建套接字sockfd
  2. 初始化客户端自己的地址结构clt_addr,port必须是服务器设置的值
  3. bind() 函数绑定设置好的地址结构
  4. recvfrom() 函数接收数据
  5. close(sockfd);

2. 代码实现

server端

#include 
#include 
#include 
#include 
#include 
#include 

#define SRV_PORT 8000
#define BROADCAST_PORT 9000
#define BROADCAST_IP "192.168.124.255"


int main()
{
    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in srv_addr,clt_addr;

    /*
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    bind(sockfd,(struct sockaddr*)&srv_addr, sizeof(srv_addr));
    */
    
    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (void*)&opt, sizeof(opt));

    clt_addr.sin_family = AF_INET;
    clt_addr.sin_port = htons(BROADCAST_PORT);
    inet_pton(AF_INET, BROADCAST_IP, &clt_addr.sin_addr.s_addr);

    char buf[BUFSIZ];
    int i=0;
    while(1)
    {
        sprintf(buf,"broadcast %dth message\n",i++);
        sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&clt_addr, sizeof(clt_addr));
        sleep(1);
    }

    return 0;
}

client 端

#include 
#include 
#include 

#define BROADCAST_PORT 9000

int main()
{
    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in clt_addr;
    clt_addr.sin_family = AF_INET;
    clt_addr.sin_port = htons(BROADCAST_PORT);
    clt_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    bind(sockfd,(struct sockaddr*)&clt_addr,sizeof(clt_addr));

    char buf[BUFSIZ];
    while(1)
    {
        int n = recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL);
        write(STDOUT_FILENO, buf, n);
    }

    return 0;
}

2. UDP 组播(多播)

既适用于 “局域网”, 也适用于 “广域网”。

组播组可以是永久的也可以是临时的。组播组地址中,有一部分由官方分配的,称为永久组播
组。永久组播组保持不变的是它的ip地址,组中的成员构成可以发生变化。永久组播组中成员的
数量都可以是任意的,甚至可以为零。那些没有保留下来供永久组播组使用的ip组播地址,可以
被临时组播组利用。

1. 组播预备知识

1. 组播地址

224.0.0.0224.0.0.255 为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
224.0.1.0224.0.1.255 是公用组播地址,可以用于Internet;欲使用需申请。
224.0.2.0238.255.255.255 为用户可用的组播地址(临时组地址),全网范围内有效;
239.0.0.0239.255.255.255 为本地管理组播地址,仅在特定的本地范围内有效。 —— 测试学习使用 该组播地址。如:239.0.0.7

2. 查看网卡信息

  1. 命令:ip address ---- ip ad

    ip ad
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
        inet6 ::1/128 scope host 
           valid_lft forever preferred_lft forever
    2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
        link/ether 00:0c:29:1e:51:21 brd ff:ff:ff:ff:ff:ff
        inet 192.168.124.128/24 brd 192.168.124.255 scope global dynamic ens33
           valid_lft 1473sec preferred_lft 1473sec
        inet6 fe80::20c:29ff:fe1e:5121/64 scope link 
           valid_lft forever preferred_lft forever
    
  2. 获取网卡对应编号

    #include 
    
    unsigned int if_nametoindex(const char *ifname);
    	参数:网卡名称(借助 ip ad 命令查看)
    	返回值:网卡序号(编号)
    

3. 开放组播权限和加入组播组

使用 setsockopt() 函数

int setsockopt(int sockfd, int level, int optname,
           const void *optval, socklen_t optlen);
// 例:
struct ip_mreqn group;
初始化 group
// 开发组播权限 --- server端
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof(group));
// 加入到指定的组播组 --- client端
setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));

4. 结构体 struct ip_mreqn

  • 组播地址结构,用于向 setsockopt() 函数传参
  • 可以使用命令 grep -r "struct ip_mreqn {" /usr/bin/include/ -n ,查找结构体的定义
#include 
struct ip_mreqn {
    struct in_addr  imr_multiaddr;    /* 组播IP地址:如:“239.0.0.7” */
    struct in_addr  imr_address;     /* 本机IP地址:192.168.0.105、0.0.0.0 */
    int   imr_ifindex;    /* 网卡序号:命令获取、函数获取 */
};

// 举例:
定义组播地址:239.0.0.7
struct ip_mreqn group;
inet_pton(AF_INET, "239.0.0.7", &group.imr_multiaddr);  // 设置组播IP地址
inet_pton(AF_INET, "0.0.0.0", &group.imr_address);  // 设置IP地址 == INADDR_ANY
group.imr_ifindex = if_nametoindex("ens33");  // 设置网卡序号

2. 实现流程

server端

  1. 创建套接字 sockfd = socket()
  2. bind() 绑定服务器的ip地址和端口号,可省略。因为:1. 不需要建立连接。2. 不写回数据
  3. 创建 struct ip_mreqn group ,并绑定组播的ip地址,设置主机ip地址,设置网卡序号
  4. 使用 setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof(group)); 开放组播权限
  5. 初始化通信对端的地址结构 clt_addr,初始化ip地址为组播地址,端口号为指定端口号,客户端也要使用这个端口号
  6. sendto() 函数指定发送给 clt_addr
  7. close(sockfd)

client端

  1. 创建套接字 sockfd = socket()

  2. bind() 绑定服务器的ip地址和端口号,端口号是服务器端绑定的指定端口号,不可省略

  3. 创建 struct ip_mreqn group ,并绑定组播的ip地址,设置主机ip地址,设置网卡序号

  4. 使用 setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)); 加入组播组

  5. recvfrom() 接收客户端发送的数据,最后两个参数可传NULL,不关心发送端的地址结构

  6. close(sockfd);

3. 代码实现

server端

#include 
#include 
#include 
#include 
#include 

#define MULTICAST_IP "239.0.0.7"
#define PORT 9001
#define SRV_PORT 8001

int main()
{
    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    struct sockaddr_in clt_addr;
    clt_addr.sin_family = AF_INET;
    clt_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, MULTICAST_IP, &clt_addr.sin_addr.s_addr);

    struct ip_mreqn group;
    inet_pton(AF_INET, MULTICAST_IP, &group.imr_multiaddr);
    inet_pton(AF_INET, "0.0.0.0", &group.imr_address);
    group.imr_ifindex = if_nametoindex("ens33");

    setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group,sizeof(group));

    int i=0;
    char buf[BUFSIZ];
    while(1)
    {
        sprintf(buf, "muticast %dth message\n", i++);
        sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&clt_addr, sizeof(clt_addr));
        sleep(1);
    }

    return 0;
}

client端

#include 
#include 
#include 
#include 

#define PORT 9001
#define MULTICAST_IP "239.0.124.7"

int main()
{
    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sockaddr_in clt_addr;
    clt_addr.sin_family = AF_INET;
    clt_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, MULTICAST_IP, &clt_addr.sin_addr.s_addr);
    
    bind(sockfd, (struct sockaddr*)&clt_addr, sizeof(clt_addr));

    struct ip_mreqn group;
    inet_pton(AF_INET, MULTICAST_IP, &group.imr_multiaddr);
    inet_pton(AF_INET, "0.0.0.0", &group.imr_address);
    group.imr_ifindex = if_nametoindex("ens33");

    setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group));
    char buf[BUFSIZ];
    while(1)
    {
        int n = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
        write(STDOUT_FILENO, buf, n);

    }

    return 0;
}

3. 本地套接字 domain

本地套接字文件是个伪文件,不占用磁盘空间

1. 常见IPC(进程间通信)

  • 匿名管道 pipe :使用简单
  • 命名管道 fifo : 可以在无血缘关系的进程间通信,一次性读取
  • 共享存储映射 mmap:可以在无血缘关系的进程间通信,通过mmap写入的通信内容会保留在内存中,可以反复读取
  • 信号 signal:开销最小,但传递的信息量有限
  • 本地套接字 domain:稳定性强

2. 对比网络套接字的实现流程

  1. socket() 函数
int socket(int domain, int type, int protocol);
	domain : AF_INET ----> AF_UNIX/AF_LOCAL
    type : SOCK_STREAM 和 SOCK_DGRAM 均可
  1. 地址结构和 bind() 函数
#include 

struct sockaddr_in ------> struct sockaddr_un
struct sockaddr_un {
    sa_family_t sun_family;               /* AF_UNIX */
    char        sun_path[108];            /* Pathname */
};
sun_family = AF_UNIX;
strcpy(sun_path,"xxxx");

bind(lfd, (struct sockaddr*)&xxx_addr, 实际长度len);
实际长度len = offsetof(struct sockaddr_un, sun_path) + strlen(sun_path);

offsetof(类型,变量);   #include <stddef.h>     注意:第二个参数不是具体的变量名,而是定义中的变量名
用于求类型中对应变量的偏移量,所以 offsetof(struct sockaddr_un, sun_path) 相当于AF_UNIX的大小,即为 2
  1. bind() 函数 调用成功后会产生一个文件socket文件(“srv.socket”)。因此为了保证在创建时该目录下没有该文件,通常在 bind() 函数前调用一个 unlink() 函数用于删除一个硬链接,即删除文件。
  2. 客户端不能依赖隐式绑定,客户端亦同,需要显示绑定socket文件名。client 客户端中需要绑定两个地址:srv_addr 和 clt_addr
  3. 其他步骤均同:listen()、accept()、connect()

注意:accept() 函数中 len的类型转换

3. 编码实现

server端

#include 
#include 
#include 
#include 
#include 

#include "wrap.h"

#define SRV_FILENAME "srv.socket"


int main()
{
    int lfd, cfd, ret, len;
    struct sockaddr_un srv_addr, clt_addr;
    srv_addr.sun_family = AF_UNIX;
    strcpy(srv_addr.sun_path, SRV_FILENAME);

    len = offsetof(struct sockaddr_un, sun_path) + strlen(srv_addr.sun_path);

    lfd = Socket(AF_UNIX, SOCK_STREAM, 0);

    unlink(SRV_FILENAME);
    Bind(lfd, (struct sockaddr*)&srv_addr, len);

    Listen(lfd, 128);

    cfd = Accept(lfd, (struct sockaddr*)&clt_addr, (socklen_t*)&len);

    len -= offsetof(struct sockaddr_un, sun_path);
    clt_addr.sun_path[len] = '\0';
    printf("客户端本地套接字文件:%s\n",clt_addr.sun_path);

    char buf[BUFSIZ];
    while(1)
    {
        ret = Read(cfd, buf, sizeof(buf));
        if(0 == ret)
        {
            Close(cfd);
            printf("客户端退出\n");
            break;
        }

        for(int i=0 ;i<ret ;++i)
        {
            buf[i] = toupper(buf[i]);
        }

        Write(cfd, buf, ret);
        Write(STDOUT_FILENO, buf, ret);
    }
    Close(lfd);

    return 0;
}

client端

#include 
#include 
#include 
#include 
#include 

#include "wrap.h"

#define SRV_FILENAME "srv.socket"
#define CLT_FILENAME "clt.socket"


int main()
{
    int cfd, ret, len;
    struct sockaddr_un srv_addr, clt_addr;
    
    cfd = Socket(AF_UNIX, SOCK_STREAM, 0);

    clt_addr.sun_family = AF_UNIX;
    strcpy(clt_addr.sun_path, CLT_FILENAME);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(clt_addr.sun_path);

    unlink(CLT_FILENAME);
    Bind(cfd, (struct sockaddr*)&clt_addr, len);

    srv_addr.sun_family = AF_UNIX;
    strcpy(srv_addr.sun_path, SRV_FILENAME);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(srv_addr.sun_path);

    Connect(cfd, (struct sockaddr*)&srv_addr, len);
    printf("客户端与服务器连接成功\n");

    char buf[BUFSIZ];
    while(1)
    {
        Write(cfd, "hello\n", strlen("hello\n"));
        int n = Read(cfd, buf, sizeof(buf));
        Write(STDOUT_FILENO, buf, n);
        sleep(1);
    }

    return 0;
}

4. 本地套接字与网络套接字的比较

网络套接字 本地套接字
server lfd = socket(AF_INET, SOCK_STREAM, 0); lfd = socket(AF_UNIX, SOCK_STREAM, 0);
bzero() ---- struct sockaddr_in serv_addr; bzero() ---- struct sockaddr_un serv_addr, clie_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(8888);
serv_addr.sun_family = AF_UNIX;
strcpy(serv_addr.sun_path, “套接字文件名”)
len = offsetof(sockaddr_un, sun_path) + strlen(serv_addr.sun_path);
bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); unlink(“套接字文件名”);
bind(lfd, (struct sockaddr *)&serv_addr, len); 创建新文件
Listen(lfd, 128); Listen(lfd, 128);
cfd = Accept(lfd, ()&clie_addr, &len); cfd = Accept(lfd, ()&clie_addr, &len);
client lfd = socket(AF_INET, SOCK_STREAM, 0); lfd = socket(AF_UNIX, SOCK_STREAM, 0);
" 隐式绑定 获取 IP+port" bzero() ---- struct sockaddr_un clie_addr;
clie_addr.sun_family = AF_UNIX;
strcpy(clie_addr.sun_path, “client套接字文件名”)
len = offsetof(sockaddr_un, sun_path) + strlen();
unlink( “client套接字文件名”);
bind(lfd, (struct sockaddr *)&clie_addr, len);
bzero() ---- struct sockaddr_in serv_addr; bzero() ---- struct sockaddr_un serv_addr;
serv_addr.sin_family = AF_INET; serv_addr.sun_family = AF_UNIX;
inet_pton(AF_INT, “服务器IP”, &sin_addr.s_addr);
serv_addr.sin_port = htons(“服务器端口”);
strcpy(serv_addr.sun_path, “server套接字文件名”)
len = offsetof(sockaddr_un, sun_path) + strlen();
connect(lfd, &serv_addr, sizeof()); connect(lfd, &serv_addr, len);

4. some small point

  1. tcp的拥塞机制对应滑动窗口,udp没有拥塞机制
  2. udp 无连接

你可能感兴趣的:(本地套接字,广播,组播)