UDP编程流程(UDP客户端、服务器互发消息流程)

一、UDP编程流程

1.1、 UDP概述

UDP,即用户数据报协议,是一种面向无连接的传输层协议。相比于TCP协议,UDP具有以下特点:

  1. 速度较快:由于UDP不需要建立连接和进行复杂的握手过程,因此在传输数据时速度稍快于TCP协议。
  2. 适用于简单的请求/应答应用程序:对于一些简单的、对可靠性要求不高的应用程序,如DNS查询和SNMP请求,UDP能够提供高效的传输服务。
  3. 不适用于海量数据传输:由于UDP不提供可靠的数据传输机制,因此在进行大数据传输时容易出现丢包和乱序的情况,不建议使用UDP进行此类传输。
  4. 广播和多播应用必须使用UDP:UDP支持广播和多播传输,因此对于需要进行广播和多播的应用,如视频直播和组播通信,必须使用UDP协议。

UDP应用:

UDP协议被广泛应用于各种网络应用中,包括但不限于以下几种:

  • DNS(域名解析):DNS使用UDP进行域名解析请求和响应的传输,以提高查询速度。
  • NFS(网络文件系统):NFS使用UDP进行文件操作请求和响应的传输,以提高文件访问速度。
  • RTP(实时传输协议):RTP使用UDP进行实时音视频数据的传输,以减少延迟。

此外,一些实时性要求较高的应用,如在线游戏和VoIP通话,也会选择使用UDP协议来减少延迟,提高用户体验

1.2、网络编程接口socket

Socket,也被称为"套接字",是网络编程中用于实现不同主机上进程间通信的一种技术。它提供了一种将网络通信抽象为文件操作的接口,使得程序员可以通过简单的函数调用来实现复杂的网络通信功能。

Socket的特点

  1. 文件描述符:Socket是一种文件描述符,它代表了一个通信管道的端点。通过Socket,我们可以像操作文件一样,使用read、write、close等函数来发送和接收网络数据。
  2. 通信端点:Socket是网络通信的端点,每个Socket都有一个唯一的地址,通过这个地址,我们可以与远程主机上的Socket进行通信。
  3. 网络数据操作:通过Socket,我们可以方便地进行网络数据的发送和接收。Socket提供了丰富的函数接口,如send、recv、sendto、recvfrom等,用于各种网络数据操作。
  4. Socket函数:要获得一个Socket,我们需要调用socket()函数。该函数返回一个Socket描述符,用于后续的网络通信操作。

Socket的分类

根据使用的协议和通信方式的不同,Socket可以分为以下几种类型:

  • SOCK_STREAM:流式套接字,用于TCP协议。它提供了可靠的、面向连接的通信方式,数据以流的形式传输,可以保证数据的可靠性和顺序性。
  • SOCK_DGRAM:数据报套接字,用于UDP协议。它提供了不可靠、无连接的通信方式,数据以数据报的形式传输,不保证数据的可靠性和顺序性。
  • SOCK_RAW:原始套接字,用于其他层次的协议操作。它允许直接访问网络层数据,可以用于实现自定义的网络协议。

1.3 UDP编程C/S架构

UDP网络编程流程:

服务器: 创建套接字 socket( )

将服务器的ip地址、端口号与套接字进行绑定 bind( )

接收数据 recvfrom()

发送数据 sendto()
客户端: 创建套接字 socket()

发送数据 sendto()

接收数据 recvfrom()

关闭套接字 close()

二、UDP编程-创建套接字

2.1 创建socket套接字

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:
    创建一个套接字,返回一个文件描述符
参数:
    domain:通信域,协议族
    AF_UNIX 本地通信
    AF_INET ipv4网络协议
    AF_INET6 ipv6网络协议
    AF_PACKET 底层接口
type:
  套接字的类型
    SOCK_STREAM 流式套接字(tcp)
    SOCK_DGRAM 数据报套接字(udp)
    SOCK_RAW 原始套接字(用于链路层)
protocol:
    附加协议,如果不需要,则设置为0
返回值:
    成功:文件描述符
    失败:-1

特点

  • 创建套接字时,系统不会分配端口
  • 创建的套接字默认属性是主动的,即主动发起服务的请求;当作为服务器时,往往需要修改为被动的

2.2举例创建套接字

cpp 复制代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>

int main(int argc, char const *argv[]) {
    // 使用socket函数创建套接字
    // 创建一个用于UDP网络编程的套接字
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("fail to socket");
        exit(1);
    }
    printf("sockfd = %d\n", sockfd);
    return 0;
}

使用socket()函数创建了一个UDP类型的套接字,并打印出该套接字的文件描述符。如果创建套接字失败,则会输出错误信息并退出程序。

执行结果

三、UDP编程-发送、绑定、接收数据

3.1 IPv4套接字地址结构

在网络编程中经常使用的结构体

头文件:#include <netinet/in.h>

cpp 复制代码
  struct in_addr
  { 
    in_addr_t s_addr;//ip地址 4字节
  };
cpp 复制代码
struct sockaddr_in
  {
    sa_family_t sin_family;//协议族 2字节
    in_port_t sin_port;//端口号 2字节
    struct in_addr sin_addr;//ip地址 4字节
    char sin_zero[8]//填充,不起什么作用 8字节
  };

为了使不同格式地址能被传入套接字函数,地址须要强制转换成通用套接字地址结构,原因是因为不同场合所使用的结构体不一样,但是调用的函数却是同一个,所以定义一个通用结构体,当在指定场合使用时,在根据要求传入指定的结构体即可

通用结构体 sockaddr

头文件:#include

cpp 复制代码
struct sockaddr
  { 
    sa_family_t sa_family; // 2字节
    char sa_data[14] //14字节
  };

3.2 两种地址结构使用场合

在定义源地址和目的地址结构的时候,选用struct sockaddr_in;

例:

struct sockaddr_in my_addr;

当调用编程接口函数,且该函数需要传入地址结构时需要用struct sockaddr进行强制转换

例:

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

3.3 发送数据---sendto函数

cpp 复制代码
#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);

**功能: sendto**函数用于通过套接字发送数据。

参数:

  • sockfd:文件描述符,由socket函数返回。
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送的数据的长度。
  • flags:标志位,用于指定发送操作的行为。常见的标志位有:
    • 0:阻塞模式,函数会一直等待直到数据发送完成。
    • MSG_DONTWAIT:非阻塞模式,如果数据不能立即发送,函数会立即返回错误。
  • dest_addr:指向目的网络信息结构体的指针,用于指定数据发送的目标地址。
  • addrlen:目的网络信息结构体的长度。

返回值:

  • 成功:返回发送的字节数。
  • 失败:返回-1,并设置errno以指示错误原因。

sendto函数是网络编程中常用的函数之一,它允许我们向指定的目标地址发送数据。在使用该函数时,我们需要正确地指定目的地址和地址长度,以确保数据能够准确地发送到目标。同时,我们也需要注意选择合适的标志位,以满足不同的发送需求。

3.4 向"网络调试助手"发送消息

我们用一个"网络调试助手"的软件来模拟服务器,给服务器发送消息。

客户端的代码编写

cpp 复制代码
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h> 

#define N 128

int main(int argc, char const *argv[])
{
    //./a.out 192.168.3.78 8080
    if(argc < 3)
    {
        fprintf(stderr, "Usage:%s ip port\n", argv[0]);
        exit(1);
    }

    //第一步:创建套接字
    int sockfd;
    if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        perror("fail to socket");
        exit(1);
    }

    printf("sockfd = %d\n", sockfd);

    //第二步:填充服务器网络信息结构体 sockaddr_in
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(serveraddr);

    serveraddr.sin_family = AF_INET; //协议族,AF_INET:ipv4网络协议
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址
    serveraddr.sin_port = htons(atoi(argv[2]));

    //第三步:发送数据
    char buf[N] = "";
    while(1)
    {
        fgets(buf, N, stdin);
        buf[strlen(buf) - 1] = '\0'; //把buf字符串中的\n转化为\0

        if(sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&serveraddr, addrlen) == -1)
        {
            perror("fail to sendto");
            exit(1);
        }
    }

    //第四步:关闭套接字文件描述符
    close(sockfd);

    return 0;
}

执行结果

3.5 绑定--bind函数

UDP网络程序想要收取数据需什么条件?

确定的ip地址

确定的port

怎样完成上面的条件呢?

接收端 使用bind函数,来完成地址结构与socket套接字的绑定,这样ip、port就固定 了 发送端 在sendto函数中指定接收端的ip、port,就可以发送数据了

由于服务器是被动的,客户端是主动的,所以一般先运行服务器,后运行客户端,所以服务 器需要固定自己的信息(ip地址和端口号),这样客户端才可以找到服务器并与之通信,但 是客户端一般不需要bind绑定,因为系统会自动给客户端分配ip地址和端口号

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
    将套接字与网络信息结构体绑定
参数:
    sockfd:文件描述符,socket的返回值
    addr:网络信息结构体

通用结构体(一般不用)
    struct sockaddr
网络信息结构体 
    sockaddr_in

#include <netinet/in.h>
struct sockaddr_in
addrlen:addr的长度
返回值:
    成功:0
    失败:-1

3.6 bind示例

cpp 复制代码
//第二步:将服务器的网络信息结构体绑定前进行填充
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));

//第三步:将网络信息结构体与套接字绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
{
    perror("fail to bind");
    exit(1);
}

3.7 接收数据---recvfrom 函数

cpp 复制代码
#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:文件描述符,socket的返回值
    buf:保存接收的数据
    len:buf的长度
    flags:标志位
        0 阻塞
        MSG_DONTWAIT 非阻塞
    src_addr:源的网络信息结构体(自动填充,定义变量传参即可)
    addrlen:src_addr的长度

返回值:
    成功:接收的字节数
    失败:-1

3.8 接收"网络调试助手"的数据

此时我们把刚刚也用到过的网络调试助手作为客户端,ubuntu的程序作为服务器

设置服务器(ubuntu程序)

cpp 复制代码
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h> 

#define N 128

int main(int argc, char const *argv[])
{
    if(argc < 3)
    {
        fprintf(stderr, "Usage: %s ip port\n", argv[0]);
        exit(1);
    }

    //第一步:创建套接字
    int sockfd;
    if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        perror("fail to socket");
        exit(1);
    }

    //第二步:将服务器的网络信息结构体绑定前进行填充
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //192.168.3.103
    serveraddr.sin_port = htons(atoi(argv[2])); //9999

    //第三步:将网络信息结构体与套接字绑定
    if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
    {
        perror("fail to bind");
        exit(1);
    }

    //接收数据
    char buf[N] = "";
    struct sockaddr_in clientaddr;
    socklen_t addrlen = sizeof(struct sockaddr_in);
    while(1)
    {
        if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&clientaddr, &addrlen) == -1)
        {
            perror("fail to recvfrom");
            exit(1);
        }

        //打印数据
        //打印客户端的ip地址和端口号
        printf("ip:%s, port:%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        //打印接收到数据
        printf("from client: %s\n", buf);
    }

    return 0;
}

执行结果

四、UDP编程-client、server

接下来我们就可以自己实现服务器与客户端互发消息了

4.1 UDP客户端

1、本地IP、本地端口(我是谁)

2、目的IP、目的端口(发给谁)

3、在客户端的代码中,我们只设置了目的IP、目的端口

客户端的本地ip、本地port是我们调用sendto的时候linux系统底层自动给客户端分配 的;分配端口的方式为随机分配,即每次运行系统给的port不一样

cpp 复制代码
//udp客户端的实现
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>

int main(int argc, char const *argv[])
{
    if(argc < 3)
    {
        fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
        exit(1);
    }

    int sockfd; //文件描述符
    struct sockaddr_in serveraddr; //服务器网络信息结构体
    socklen_t addrlen = sizeof(serveraddr);

    //第一步:创建套接字
    if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    {
        perror("fail to socket");
        exit(1);
    }

    //客户端自己指定自己的ip地址和端口号,一般不需要,系统会自动分配
    #if 0
    struct sockaddr_in clientaddr;
    clientaddr.sin_family = AF_INET;
    clientaddr.sin_addr.s_addr = inet_addr(argv[3]); //客户端的ip地址
    clientaddr.sin_port = htons(atoi(argv[4])); //客户端的端口号
    if(bind(sockfd, (struct sockaddr *)&clientaddr, addrlen) < 0)
    {
        perror("fail to bind");
        exit(1);
    }
    #endif

    //第二步:填充服务器网络信息结构体
    //inet_addr:将点分十进制字符串ip地址转化为整形数据
    //htons:将主机字节序转化为网络字节序
    //atoi:将数字型字符串转化为整形数据
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    serveraddr.sin_port = htons(atoi(argv[2]));

    //第三步:进行通信
    char buf[32] = "";
    while(1)
    {
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = '\0';

        if(sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
        {
            perror("fail to sendto");
            exit(1);
        }

        char text[32] = "";
        if(recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr *)&serveraddr, &addrlen) < 0)
        {
            perror("fail to recvfrom");
            exit(1);
        }
        printf("from server: %s\n", text);
    }
    //第四步:关闭文件描述符
    close(sockfd);

    return 0;
}

4.3 UDP服务器

1、服务器之所以要bind是因为它的本地port需要是固定,而不是随机的

2、服务器也可以主动地给客户端发送数据

3、客户端也可以用bind,这样客户端的本地端口就是固定的了,但一般不这样做

cpp 复制代码
//udp服务器的实现
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>

int main(int argc, char const *argv[])
{
    if(argc < 3)
    {
        fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
        exit(1);
    }

    int sockfd; //文件描述符
    struct sockaddr_in serveraddr; //服务器网络信息结构体
    socklen_t addrlen = sizeof(serveraddr);

    //第一步:创建套接字
    if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    {
        perror("fail to socket");
        exit(1);
    }

    //第二步:填充服务器网络信息结构体
    //inet_addr:将点分十进制字符串ip地址转化为整形数据
    //htons:将主机字节序转化为网络字节序
    //atoi:将数字型字符串转化为整形数据
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    serveraddr.sin_port = htons(atoi(argv[2]));

    //第三步:将套接字与服务器网络信息结构体绑定
    if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
    {
        perror("fail to bind");
        exit(1);
    }

    while(1)
    {
        //第四步:进行通信
        char text[32] = "";
        struct sockaddr_in clientaddr;
        if(recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr *)&clientaddr, &addrlen) < 0)
        {
            perror("fail to recvfrom");
            exit(1);
        }
        printf("[%s - %d]: %s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), text);
        
        strcat(text, " *_*");

        if(sendto(sockfd, text, sizeof(text), 0, (struct sockaddr *)&clientaddr, addrlen) < 0)
        {
            perror("fail to sendto");
            exit(1);
        }
    }

    //第四步:关闭文件描述符
    close(sockfd);

    return 0;
}

执行的结果

相关推荐
syty20208 小时前
flink 伪代码
java·windows·flink
l1t9 小时前
美团龙猫利用expat库实现的保存xml指定范围数据到csv的C程序
xml·c语言·解析器·expat
白鹭9 小时前
MySQL主从复制进阶(GTID复制,半同步复制)
linux·运维·数据库·mysql·集群
叫我Zoe就行9 小时前
MySQL集群——主从复制
linux·数据库·学习·mysql
IDOlaoluo9 小时前
TortoiseGit 2.4.0.0 64位安装教程(附详细步骤和Git配置 附安装包)
windows
TechubNews9 小时前
Webus 与中国国际航空合作实现 XRP 支付
大数据·网络·人工智能·web3·区块链
知行力9 小时前
【GitHub每日速递】不止 TeamViewer 替代!RustDesk 与 PowerToys,Windows 效率神器
windows·github·teamviewer
博语小屋10 小时前
进程控制之进程等待
linux
Dobby_0510 小时前
【Linux】网络安全管理:SELinux 和 防火墙联合使用 | Redhat
linux·运维·云原生·防火墙·selinux
计算机毕设定制辅导-无忧学长10 小时前
MQTT 认证与授权机制实践(二)
网络协议