LinuxUDP通讯

前言

在学习网络通讯的时候会经常听到UDP通讯,那UDP是什么呢?如何使用UDP呢?今天这一篇文章就给大家介绍一下UDP通讯。

一、UDP通讯

1.UDP通讯概述

UDP是一种面向于无连接的用户数据报协议,在传输数据前是不需要先建立连接就可以直接进行通信的。

目标主机在接收到UDP报文后是不需要给出任何确认的。

2.UDP的特点

1.UDP相当于TCP来说速度会稍快一点。

2.简单的请求/应答程序可以使用UDP

3.广播和多播应用是使用UDP

3.UDP的应用

一般UDP使用以下领域:

1.DNS域名解析

2.NFS网络文件系统

3.RTP流媒体

4.语音和视频通话

二、UDP基本通讯

UDP通讯流程很简单,可以用下面的这张图来进行概括:

img

首先要分为服务端和客户端,服务端主要是负责让客户端进行连接,然后接收客户端给的数据或者指令进行分析然后发送分析结果给客户端进行一次反馈。

客户端就是发送数据的那一方。

流程非常的简单,我们现在来开始书写代码,但在书写代码前需要先了解在这个通讯过程中使用的函数。

1.socket函数

这个函数是我们在网络编程中会经常看到的一个函数,这个函数主要的功能是创建套接字,然后通过操作这个套接字实现网络通讯,函数的原型:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:
    建立一个用于交流的端点并且返回一个描述符
参数:
    domain:AF_INET
    type:确定通信语句
    	SOCK_STREAM 提供有序的,可靠的,双向的,基于字节流的通讯。可能支持带外传输,一般使用TCP的时候用这个。
       	SOCK_DGRAM 提供数据报(不面向连接的, 不可靠的固定最大长度的信息)一般使用UDP的时候用这个。
       	SOCK_SEQPACKET 提供有序的,可靠的,双向的,基于固定最大长度的数据报传输路径;需要一个读取整个伴有输入系统调用的包的用户。
       	SOCK_RAW 提供未加工(raw)的网络协议通道。
      	SOCK_RDM 提供可靠的数据报层,但是不保证顺序。
    	SOCK_NONBLOCK 设置  O_NONBLOCK 的标志于新打开的文件描述符。 通过这个标志可以不用调用 fcntl(2) 来达到相同的结果。
		SOCK_CLOEXEC 设置 close-on-exec  (FD_CLOEXEC)  的标志于新打开的文件描述符。参见 open(2) 中关于 O_CLOEXEC 的描述,因为一些原因这个标志很有用。
    protocol:指定一个协议用于套接字,一般为0
返回值:
    成功:文件描述符
    失败:-1

使用的方法很简单,比如说我要创建一个套接字用来实现UDP的通讯,那代码我可以这样写:

int sockfd = -1;   // 接收套接字变量
sockfd = socket(AF_INET, SOCK_DGRAM);  // 创建套接字
if (sockfd == -1){
    // 错误判断
    perror("socket is not ok");
    return -1;
}

socket返回-1的时候我们就可以知道它创建失败了,这个时候我们可以使用perror()这个函数将它错误信息打印出来,这个函数的使用方法很简单,里面填写提示内容,如果出错就会将错误打印到提示内容后面。

但是并不是所有的函数都可以使用perror()这个函数的,使用这个函数的一个唯一条件就是看errno是否被置位,如果没有被置位,那就没办法使用

img

2.bind函数

在服务端中必须要这个bind函数,这个函数是将你服务端的IP地址和端口号绑定到套接字中,如果不绑定,客户端就没办法找到你了,函数原型如下:

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
功能:
    给套接字绑定端口和ip
参数:
    sockfd:套接字
    addr:ipv4套接字结构体地址
    addrlen:ipv4套接字结构体的大小
返回值:
    成功0
    失败-1

其中sockaddr结构体是最重要的,这个就是将我们需要绑定的内容放入结构体里面,相当于一个配置,这个结构体的原型如下:

struct sockaddr{
    sa_family_t sa_family; /*AF_xxx*/
    char sa_data[14];	/*通用的地址*/
}

但这个是一个通用的结构体,在网络中我们有ipv4ipv6,有两个IP地址的格式,如果我们要分开这两个IP并分开配置的话就要一个函数写两遍,为了不这么浪费,就设计了一个通用的结构体来进行配置,你配置ipv4的IP和配置ipv6的IP都可以直接使用这个结构体。

但是这个结构体还是比较复杂,我们可以通过ipv4配置结构体配置完后强制类型转换成这个通用的结构体然后来使用,ipv4的配置结构体如下:

struct sockaddr_in {
	sa_family_t sin_family; /* 地址族: AF_INET */
	u_int16_t sin_port; /* 按网络字节次序的端口 */
	struct in_addr sin_addr; /* internet地址 */
};

/* Internet地址. */
struct in_addr {
	u_int32_t s_addr; /* 按网络字节次序的地址 */
};

如果你要配置ipv6的,也有一个结构体进行配置:

struct sockaddr_in6 {
	sa_family_t     sin6_family;   /* AF_INET6 */
	in_port_t       sin6_port;     /* port number */
	uint32_t        sin6_flowinfo; /* IPv6 flow information */
	struct in6_addr sin6_addr;     /* IPv6 address */
	uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */
};

struct in6_addr {
	unsigned char   s6_addr[16];   /* IPv6 address */
};

但我们使用ipv4的情况还是比较多,毕竟现在还是以ipv4的情况比较多。

使用的方法很简单,上面我们不是创建好一个套接字了吗,我现在需要将我本机的IP和端口绑定到这个结构体中,然后使用bind函数进行绑定即可:

struct sockaddr_in serveraddr = {0};  // 创建配置ipv4的结构体
serveraddr.sin_family = AF_INET;  // 配置地址族
serveraddr.sin_port = htons(9999);   // 配置端口,这里的端口自己指定
serveraddr.sin_addr.s_addr = inet_addr("192.168.1.5");    // 设置服务端的IP
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){  // 绑定IP和端口
    // 错误判断
    perror("fail not bind");
    return -2;
}

在上面出现了两个没有见过的函数,一个是htons和一个inet_addr,现在给大家解释一下。

2.1 主机字节序和网络字节序

这里需要重新回忆一下C语言中的大端存储和小端存储了。其实也就是每个人的计算机的字节序存储方式不一样,有些是大端存储,而有些是小端存储,如果直接进行通讯就像中国人和英语人交流信笺一样,两边的人都看不懂对象书写的内容,这个时候如果在写信的时候将自己写的内容转换成对方看得懂的内容是不是就能很好的解决看不懂的问题了。

为了使网络程序具有可移植性,使同样的C代码在大端计算机和小端计算机上编译后都能正常运行,就可以使用以下库函数做网络字节序和主机字节序的转换

主机字节序转换为网络字节序

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
功能:
    将无符号整数hostlong从主机字节序转换为网络字节序
参数:
    hostlong:需要转换的主机字节序
返回值:
    转换后的网络字节序
uint16_t htons(uint16_t hostshort);
功能:
    将无符号短整型hostshort从主机字节序转换为网络字节序
参数:
    hostshort:需要转换的主机字节序
返回值:
    转换后的网络字节序   

网络字节序转换成主机字节序

#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);
功能:
    将无符号短整数netlong转换为主机字节序
参数:
    netlong:需要转换的网络字节序
返回值:
    转换后的主机字节序
uint16_t ntohs(uint16_t netshort);
功能:
    将无符号短整数netshort转换为主机字节序
参数:
    netshort:需要转换的网络字节序
返回值:
    转换后的主机字节序

2.2 点分制十进制转换

当你选择已经有一个需要发送的IP:192.168.1.2,你如果直接发送,网络是没办法知道你这个是什么内容的,需要进行一次转换将192.168.1.2转换为192,168,1,2这种使用数组存放的形式。而网络传给主机的IP地址为192,168,1,2这种使用数组的IP地址,所以需要经过转换变成192.168.1.2的形式。

在C语言中提供了两个函数来处理:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char* cp);
功能:
    将点分制字符串转换为点分制十进制串
参数:
    cp:需要转换的分制字符串
返回值:
    转换后的点分制十进制串
char* inet_ntoa(struct in_addr in);
功能:
    将点分制十进制串转换为点分制字符串
参数:
    in:点分制十进制串
返回值:
    点分制字符串

3.recvfrom接收

绑定完成后就可以进行接收了,接收使用的是recvfrom函数,这个函数的原型如下:

#include <sys/types.h>
#include <sys/socket.h>
size_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
    sockfd:文件描述符,socket的返回值
    buf:保存接收的数据
    len:buf的长度
    flags:标志位
    0 阻塞
    MSG_DONTWAIT 非阻塞
    src_addr:源的网络信息结构体(自动填充,定义变量传参即可)
    addrlen:src_addr的长度
返回值:
    成功:接收的字节数
    失败:‐1

使用的代码如下:

#define SIZE 1024
char buf[SIZE];
int len = sizeof(struct sockaddr);
while(1){
    // 循环接收
    recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, &len);
    // 打印信息
    printf("[%s %d]:%s\n", inet_ntoa(serveraddr.sin_addr), ntohs(serveraddr.sin_port), buf);
}

这样就可以进行一次接收了。

介绍完使用的函数后我们的服务端的UDP程序就已经写完了,代码就是上面的那些。

4.服务端完整代码

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define PORT 9999
#define IP "192.168.1.8"
#define SIZE 1024

int main(){
    int sockfd = -1;
    int len = sizeof(struct sockaddr);
    struct sockaddr_in serveraddr = {0};
    char buf[SIZE];
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1){
        perror("fail not socket");
        return -1;
    }
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(PORT);
    serveraddr.sin_addr.s_addr = inet_addr(IP);
    if (bind(sockfd, (struct sockaddr*)&serveraddr), sizeof(struct sockaddr)){
        perror("fail not bind");
        close(sockfd);   // 这里要关闭一下套接字,因为已经失败了
        return -2;
    }
    // 循环读取数据
    while(1){
        recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, len);
        printf("[%s %d]:%s\n", inet_ntoa(serveraddr.sin_addr), ntohs(serveraddr.sin_port), &buf);
    }
    return 0;
}

5.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:文件描述符,socket的返回值
    buf:要发送的数据
    len:buf的长度
    flags:标志位
    0 阻塞
    MSG_DONTWAIT 非阻塞
    dest_addr:目的网络信息结构体(需要自己指定要给谁发送)
    addrlen:dest_addr的长度
返回值:
    成功:发送的字节数
    失败:‐1

使用的代码如下:

printf("请输入你要发送的信息:");
scanf("%s", buf);
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, len);

这样我们客户端的代码也写完了。

6.客户端完整代码

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define PORT 9999
#define IP "192.168.1.8"
#define SIZE 1024

int main(){
    int sockfd = -1;
    int len = sizeof(struct sockaddr);
    struct sockaddr_in serveraddr = {0};
    char buf[SIZE];
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1){
        perror("fail not socket");
        return -1;
    }
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(PORT);
    serveraddr.sin_addr.s_addr = inet_addr(IP);
    // 循环读取数据
    while(1){
        printf("请输入你要发送的信息:");
        scanf("%s", buf);
        sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, len);
        printf("发送成功\n");
    }
    return 0;
}

这里有一个很重要的问题,就是在客户端里不用进行绑定,那如果不进行绑定怎么让程序知道发送给哪个服务端呢?

这里我们不是使用了一个sendto,而这个函数携带了目标机器的配置结构体,它会通过这个结构体去找到目标。

三、TFTP文件接收程序

1.TFTP概述

TFTP是简单文件传送协议,最初用于引导无盘系统,被设计用来传输小文件。

这个协议是基于UDP协议实现的,不用进行用户有效性认证。

传输模式分为三种:

  • octet : 二进制模式
  • netascii : 文本模式
  • mail : 已经不再支持

2.TFTP通讯过程

img

实现客户端发送一个请求连接的数据包给服务端,服务端收到后验证一下请求包中的内容:

img

然后服务端收到请求后会发送请求的内容组成的数据包:

img

当客户端收到数据包后需要向服务端发送ACK响应包:

img

如果在这个过程中出现了问题,服务端会发送一个错误的数据包:

img

这里面的错误码有下面的几种:

错误码:

0 未定义,参见错误信息

1 File not found

2 Access violation

3 Disk full or allocation exceeded

4.illegal TFTP operation

5.Unknown transfer ID

6.File already exists

7.No such user

8.Unsuppored option(s) requested

3.TFTP客户端

现在我们来编写一个简单的TFTP客户端来接收服务端的文件,这里的服务端用软件来进行开启,当然你也可以主机写一个,都是使用UDP的,很简单。

流程如下:

img

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <fcntl.h>

#define SIZE 1024
#define DATASIZE 516

void getFile(int sockfd, struct sockaddr_in serveraddr, char* filename){
    char buf[SIZE];
    int fileflag = 0;   // 用来判断文件是否被创建
    int structlen = sizeof(struct sockaddr);
    int len = 0;  // 数据包长度
    int fd = -1;
    int num = 1;  // 包序号
    int n = 0;    // 接收长度
    len = sprintf(buf, "%c%c%s%c%s%c", 0, 1, filename, 0, "octet", 0);  // 拼接请求数据包
    // 发送数据包
    sendto(sockfd, buf, len, 0, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
    // 接收数据包
    n = recvfrom(sockfd, buf, DATASIZE, 0, (struct sockaddr*)&serveraddr, &structlen);
    // 判断数据包
    if (buf[1] == 5){
        // 这个是获取到错误包
        printf("error:%s\n", buf+4);  // 打印错误信息
        return ;
    }
    else if (buf[1] == 3){
        // 获取数据包
        if (fileflag == 0){
            // 文件没有被创建
            fd = open(filename, O_WRONLY | O_CREAT, 0664);
            if (fd == -1){
                perror("fail not open");
                return;
            }
        }
        while(1){
            // 将读取的内容写入文件中
            if (n == DATASIZE && (ntohs(*(unsigned short*)(buf + 2)) == num)){    // 判断数据长度和包序号
                write(fd, buf + 4, n - 4); // 去掉数据头和数据包数
                // 发送ACK响应数据包
                sendto(sockfd, buf, 4, 0, (struct sockaddr*)&serveraddr, len);
                num++;
            }
            else if (n < DATASIZE && (ntohs(*(unsigned short*)(buf + 2)) == num)){   // 数据包长度小于516,接收结束
               write(fd, buf + 4, n - 4);
               sendto(sockfd, buf, 4, 0, (struct sockaddr*)&serveraddr, len);
               break;
            }
        }
        close(fd);   // 关闭文件
        printf("传输完成\n");
    }
    return ;
}

int main(int argv, char* argc[]){
    int sockfd = -1;
    struct sockaddr_in serveraddr = {0}; // TFTP服务端配置结构体
    if (argv != 3){
        printf("./TFTP <IP> <Filename>");
        return -1;
    }
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1){
        perror("fail not socket");
        return -2;
    }
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(69);   // TFTP默认开启69号端口
    serveraddr.sin_addr.s_addr = inet_addr(argc[1]);
    getFile(sockfd, serveraddr, argc[2]);  // 获取文件的函数
    close(sockfd);
    return 0;
}

这里使用服务端的软件是:TFTPd32,下载的链接:tftpd32

上面写的只是下载的代码,当然也可以写一个上传的功能,其实就是将write改变为read,然后通过数据包发送过去即可。

四、UDP广播

1.什么是广播

广播是由一台主机向该主机所在子网内的所有主机发送数据的方式,例如192.168.1.1主机发送广播消息,192.168.1.1~192.168.1.254都能收到数据。

广播只能用UDP或原始IP实现,不能用TCP。

2.广播特点和用途

用途主要是单个服务器与多个客户主机通信时减少分组流通,下面的几个协议都用到广播:

1、地址解析协议(ARP)

2、动态主机配置协议(DHCP)

3、网络时间协议(NTP)

广播局限于在局域网内使用,离开了局域网后就没有办法进行使用了。

3.广播地址

广播地址分为两种,一种是定向广播地址,另一种是受限广播地址。

定向广播地址是主机ID全为1,比如一个局域网内,IP地址是192.168.1.0,子网掩码是:255.255.255.0,那这个网段中的定向广播地址为:192.168.1.255。

而受限广播地址是:255.255.255.255,路由器从来不转发该广播。

4.广播、单播和多播

4.1 单播

单播是什么?我们可以拿一张图来解释一下:

img

这个过程和之前的UDP通讯一样,这个单播的MAC地址必须要是对方的MAC地址,如果MAC地址不是对方的MAC地址那对方就不会再继续解析。

4.2 广播

img

在广播中,MAC地址是全为1的,对方接收到这个数据包中如果看到MAC地址全为1,不是和自己一样的也会继续解析,相当于就忽略了检查MAC地址的这一步,然后继续解析。

编写一个广播的代码是很简单的,实现我们要了解一下编写的流程:

发送者:

1、创建套接字

2、设置为允许发送广播权限

3、发送内容

接收者:

1、创建套接字

2、将套接字与广播的消息结构体绑定到一起

3.接收消息

4.2.1 设置为允许发送广播权限

这个需要使用到一个函数setsockopt,这个函数在设置广播和多播的时候会使用到,所以我们先了解一下这个函数,函数的原型如下:

#include <sys/socket.h>
int setsockopt(int socket, int level, int option_name,
const void *option_value, socklen_t option_len);
功能:设置一个套接字的选项(属性)
参数:
    socket:文件描述符
    level:协议层次
        SOL_SOCKET 套接字层次
        IPPROTO_TCP tcp层次
        IPPROTO_IP IP层次
    option_name:选项的名称
        SO_BROADCAST 允许发送广播数据(SOL_SOCKET层次的)
        option_value:设置的选项的值
    int类型的值,存储的是bool的数据(1和0)
        0 不允许
        1 允许
    option_len:option_value的长度
返回值:
    成功:0
    失败:‐1

比如说我们要设置一个套接字为发送广播权限,代码可以这样写:

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));

这样就可以设置好发送广播了。

4.2.2 发送端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

#define SIZE 1024

int main(int argv, char* argc[]){
    int sockfd = -1;
    int on = 1;
    int len;
    int ret = -1;
    char buf[SIZE];
    struct sockaddr_in serveraddr = {0};
    if (argv != 3){
        printf("./Send <IP> <PORT>");
        return -1;
    }
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1){
        perror("sockfd not ok");
        return -2;
    }
    // 设置发送广播权限
    ret = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
    if (ret == -1){
        perror("fail not setsockopt");
        close(sockfd);
        return -3;
    }
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argc[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argc[1]);
    len = sizeof(struct sockaddr);
    while(1){
        // 发送数据
        printf("you talk:");
        scanf("%s", buf);
        sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, len);
    }
    close(sockfd);
    return 0;
}

4.2.3 接收端完整代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define SIZE 1024

int main(int argv, char* argc[]){
    int sockfd = -1;
    int len;
    int n;
    char buf[SIZE];
    struct sockaddr_in serveraddr = {0}; // 广播的结构体
    if (argv != 3){
        printf("./recv <IP> <PORT>\n");
        return -1;
    }
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1){
        perror("fail not socket");
        return -2;
    }
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argc[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argc[1]);
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){
        perror("fail not bind");
        close(sockfd);
        return -3;
    }
    len = sizeof(struct sockaddr);
    while(1){
        // 接收信息
        n = recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, &len);
        if (n <= 0){
            printf("no");
            break;
        }
        printf("he talk:%s\n", buf);
    }
    close(sockfd);
    return 0;
}

4.3 多播

多播其实就算一个发送端给多个接收端发送消息,但是这些接收端必须是在一个组内才能接收这个消息。

而多播发送端的地址需要从224.0.0开始到239.255.255.254结束,也就是你在设置发送端的时候需要将发送端的IP设置为这个范围内才可以。

多播工作流程如下:

img

4.3.1 多播编写流程

发送者:

1、创建套接字

2、发送数据

接收者:

1、创建套接字

2、设置为加入多播组

3、将套接字和多播消息绑定到一起

4、接收数据

这里看起来很麻烦,特别是设置为加入多播组该如何设置,其实还是用到上面使用的setsockopt函数来进行设置,那如何设置呢?

#include <sys/socket.h>
int setsockopt(int socket, int level, int option_name,
const void *option_value, socklen_t option_len);
功能:设置一个套接字的选项(属性)
参数:
socket:文件描述符
level:协议层次
    IPPROTO_IP IP层次
option_name:选项的名称
    IP_ADD_MEMBERSHIP 加入多播组
option_value:设置的选项的值
    struct ip_mreq
    {
        struct in_addr imr_multiaddr; //组播ip地址
        struct in_addr imr_interface; //主机地址
        INADDR_ANY 任意主机地址(自动获取你的主机地址)
    };
    option_len:option_value的长度
返回值:
    成功:0
    失败:‐1

比如说我将创建好的套接字加入到多播组中,代码就如下:

struct ip_mreq mreq = {0};
mreq.imr_multiaddr.s_addr = inet_addr("224.1.1.1");   // 组播的IP地址
mreq.imr_interface.s_addr = INADDR_ANY;
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(struct ip_mreq));

这样就将接收端加入进多播组中了。

4.3.1 发送端完整代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

#define SIZE 1024

int main(int argv, char* argc[]){
    int sockfd = -1;
    char buf[SIZE];
    struct sockaddr_in serveraddr = {0};
    if (argv != 3){
        printf("./group_send <IP> <PORT>\n");
        return -1;
    }
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1){
        perror("fail not socket");
        return -2;
    }
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argc[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argc[1]);
    while(1){
            printf("you talk:");
            scanf("%s", buf);
        sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    }
    return 0;
}

4.3.2 接收端完整代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define SIZE 1024

int main(int argv, char* argc[]){
    int sockfd = -1;
    int len;
    char buf[SIZE];
    struct ip_mreq mreq = {0};
    struct sockaddr_in serveraddr = {0};
    if (argv != 3){
        printf("./group_recv <IP> <PORT>\n");
        return -1;
    }
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1){
        perror("fail not socket");
        return -2;
    }
    // 添加一个多播组IP
    mreq.imr_multiaddr.s_addr = inet_addr(argc[1]);
    // 添加一个将要添加到多播组的IP
    mreq.imr_interface.s_addr = INADDR_ANY;
    if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))){
        perror("fail not setsockopt");
        close(sockfd);
        return -3;
    }
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argc[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argc[1]);
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){
        perror("fail not bind");
        close(sockfd);
        return -4;
    }
    len = sizeof(struct sockaddr);
    while(1){
        recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, &len);
        printf("[IP:%s PORT:%d]:%s\n", inet_ntoa(serveraddr.sin_addr), ntohs(serveraddr.sin_port), buf);
    }
    return 0;
}

总结

UDP通信就介绍完了,其实UDP通信是很简单的,相比于TCP,UDP的客户端不需要进行连接,只需要使用sendto函数就可以将数据发送给客户端了,大家只需要多多练习就好了,再过几天我给大家介绍一下TCP的通讯,这样就能对UDP有更深的理解了。

posted @ 2023-08-08 12:41  Lavender·edgar  阅读(382)  评论(0编辑  收藏  举报