欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

网络编程------UDP协议网络程序

程序员文章站 2022-07-08 19:46:52
...

        以下有关IP地址,端口号,网络字节序列相关知识见:网络编程------IP地址,端口号,套接字,网络字节序

        这里要先简单认识UDP协议。

UDP协议

        UDP协议也称为用户数据报协议。它是传输层的一种协议。根据该协议在进行数据传送时,两台主机之间不需要相互连接,直接根据对方的IP地址和端口号进行数据传送。所以不用花费时间去连接两台主机,因此根据该协议进行传输时,速度会相对TCP协议快(关于TCP协议的相关知识见:网络编程------TCP协议网络程序)。但与此同时数据传送错误的概率相对TCP协议会较高。同时,根据该协议数据在传送的基本单位是数据报,即源主机一次发送了多少字节的数据,目的主机一次就应将该数据全部接收。

        因此,根据UDP协议传输的特点为:

(1)它是一种传输层的协议

(2)无连接

(3)不可靠

(4)面向数据报

        下面根据UDP协议来编写一个服务器端和客户端代码来使二者之间进行通信。

        在此之前先认识一些接口函数:

sockaddr结构体

        该结构体用于存储套接字等相关信息。我们知道,由于IP协议的不同,IP地址的格式也不同,其所对应的套接字类型也不同。因此不同的套接字类型对应一个不同的sockaddr结构体,如下图:

网络编程------UDP协议网络程序

        在上述三个结构体中的第一个成员:16位地址类型可以理解为是标识某个套接字类型的。

        上图中的第二个结构体struct sockaddr_in用于存放IPv4类型(IP地址是遵照IPv4协议的)的套接字。它的地址类型(或套接字类型)为AF_INET。该结构体的第二个成员即为该套接字中的16位端口号,第三个成员即为该套接字中的32位的IP地址。第三个成员为8字节填充(这里不讨论)。另外,地址类型为AF_INET6表示某结构体中存放的是IPv6类型的套接字(这里没有具体给出该结构体)。

        上图中的第三个结构体struct sockaddr_un用于存放域间套接字。它的地址类型(套接字类型)为AF_UNIX。这种类型的套接字主要用于同一主机内的两进程通信。

        上图中的第一个结构体struct sockaddr是一种通用的结构体,它可以存放或接收任意类型的套接字类型。比如若该结构体的第一个成员为IPv4类型的标识地址AF_INET,则该结构体存放的是IPv4类型的套接字。若该结构体的第一个成员为AF_INET6,则该结构体中存放的是IPv6类型的套接字。该结构体类似于void*类型,是一种泛性化接口。在以下各接口函数的使用中为保证程序的通用性,统一使用的均是该类型的结构体。对于具体的应用场合在对其进行强制转换即可。

        以下的相关socket API中使用的都是IPv4类型的套接字类型,所以,下面具体介绍struct sockaddr_in结构体:

struct sockaddr_in {
  sa_family_t       sin_family; /* Address family:地址类型(套接字类型)     */
  __be16        sin_port;   /* Port number:16位端口号          */
  struct in_addr    sin_addr;   /* Internet address:封装32位IP地址的结构体     */
  /* Pad to size of `struct sockaddr'. : 8字节填充(这里不讨论)*/
  unsigned char     __pad[__SOCK_SIZE__ - sizeof(short int) -
            sizeof(unsigned short int) - sizeof(struct in_addr)];
};

        上述结构体中嵌套了一个封装32位IP地址的结构体,该结构体定义如下:

struct in_addr {                                                                                                                      
    __be32  s_addr;//32位的IP地址
};

        下面介绍根据UDP协议在进行通信时需要使用的一些接口。

socket API

1. socket函数

        当客户端向服务器端发送请求时,根据冯诺依曼体系结构,数据会在这几个设备之间进行传送:

客户端内存->客户端网卡->网络->服务器端网卡->服务器端内存。

        所以要想将数据从内存中写入网卡。而我们知道在Linux中,一切皆文件,要对网卡写入数据,就需要将网卡文件打开,因此可以调用如下接口:

int socket(int domain,int type,int protocol);//头文件<sys.types.h> <sys/socket.h>

        函数功能:相当于打开网卡,创建socket文件。

        参数说明:

domain:该主机的套接字类型标识,即为上述结构体的第一个成员。若为IPv4类型的套接字,则该参数为AF_INET。

type:服务类型。如果根据UDP协议提供服务,则该参数是SOCK_DGRAM。如果是TCP,则为SOCK_STREAM。

protocol:该参数默认为0。

        返回值:成功返回文件描述符,失败返回0。

2. bind函数

        服务器要通过某主机中的某特定进程来提供特定的服务,所以必须为该进程指定一个端口号和IP地址(套接字)来绑定套接字文件。当客户端将指定的套接字传送过来时,服务器就根据该套接字找到对应的进程来处理请求。该函数原型如下:

int bind(int socket,const char* sockaddr* address,socklen_t address_len);//头文件:<sys/types.h> <sys/socket.h>

        函数功能:绑定端口号,将网络信息和文件信息关联起来

        参数:

socket:表示上述socket函数的返回值。

address:表示要绑定的套接字等相关信息,这些信息存放在该结构体变量中

address_len:第二个参数结构体的大小

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

3. recvfrom函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,\
                        struct sockaddr *src_addr, socklen_t *addrlen);//头文件:<sys/types.h> <sys/socket.h>

        函数功能:将文件sockfd从src_addr中接收到的消息写入buf中

        参数:

sockfd:socket函数创建的文件描述符

buf:输出型参数,用于存放接收到的消息

len:buf的长度

flags:状态位,为0表示当sockfd文件为空时,阻塞等待

src_addr:输出型参数,用于存放发送消息的套接字等相关信息

addrlen:src_addr的长度

        返回值:成功返回实际获取的字节数,失败返回-1。

4. sendto函数

 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);//头文件<sys.types.h> <sys/socket.h>

        函数功能:将buf中存放的内容先放入sockfd表示的文件中,通过该文件将内容发送给dest_addr。

        参数:

socket:sockfd函数返回的文件描述符

buf:缓冲区

len:buf的长度

flags:为0表示sockfd文件为满时,阻塞等待

dest_addr:数据发送给该变量指定的套接字

addrlen:dest_addr的长度

        返回值:成功返回实际发送的字节数,失败返回-1。

3. 地址转换函数

        以下程序用到的都是IPv4类型的IP地址,所以下面两个地址转换函数都是基于IPv4类型的。

        我们人识别的IP地址一般是“点分十进制”的字符串类型。在在网络中进行传送时,要将其转换为整数的格式来传输。所以在发送和接收IP地址时,都要进行相应的转换。转换函数如下:

“点分十进制”字符串转换为整型格式:

in_addr_t inet_addr(const char *cp);//头文件为<sys/socket.h> <netinet/in.h> <arpa/inet.h>

cp:表示要转换的字符串

转换后的结果直接由返回值带回,注意返回值的类型。

整型格式转换为“点分十进制”字符串

char *inet_ntoa(struct in_addr in);

in:表示要转换的整型地址(注意它的类型为结构体)

转换后的字符串直接由返回值带回。

        下面来编写简单的UDP网络程序。

UDP服务器

(1)首先,服务器要用socket函数打开一个套接字文件用于接收网络中传来的数据

(2)利用bind函数将上述打开的套接字文件与提供服务的套接字进行绑定

(3)开始接收来自客户端发来的请求

(4)处理请求后,将结果发送给客户端(这里简单处理为将客户端发来的信息在回发过去)

(5)服务器端不断的重复(3)~(4)的工作。

        我们将以下提供服务的进程端口号设置为8080(可自己指定),该程序所在的主机IP地址由ifconfig查找,如:192.168.3.95。在程序中要对该套接字进行绑定,因此要将IP地址和端口号以命令行参数的形式传入。

        代码如下:

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

//该程序运行时的格式为:./a.out 192.168.3.95 8080
int main(int argc,char* argv[])
{
    if(argc != 3)//如果传入的参数不为3,则返回用法说明
    {
        printf("Usage:%s [ip][port]\n",argv[0]);
        return 1;
    }

    //打开套接字文件:套接字类型为IPv4类型,服务类型为UDP
    int sock = socket(AF_INET,SOCK_DGRAM,0);
    if(sock < 0)//打开失败
    {   
        printf("socket error\n");
        return 2;
    }   

    //将上述套接字文件和命令行指定的套接字进行绑定
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = inet_addr(argv[1]);
    local.sin_port = htons(atoi(argv[2]));
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定失败
    {   
        printf("bind error\n");
        return 3;
    }   
    //开始接受和发送数据
    char buf[128];
    struct sockaddr_in client;
    while(1)
    {
        socklen_t len = sizeof(client);//每次接受到得客户端可能不同,所以,要在循环中求长度

        //接收来自客户端的请求
        ssize_t s = recvfrom(sock,buf,sizeof(buf) - 1,0,(struct sockaddr*)&client,&len);
        if(s > 0)
        {
            buf[s] = 0;
            printf("%s %d say# %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
        }

        //处理请求并将结果发给客户端,这里请求的处理是原样发给客户端
        sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,len);

    }
    return 0;
}             

UDP客户端

(1)首先客户端也要打开一个套接字文件来接收和发送信息

(2)因为客户端不需要固定的端口号,因此不需要调用bind进行绑定。客户端的端口号由内核自动分配。

(3)向服务器端发送请求

(4)接收服务器端发回的请求处理结果并将其打印出来。

        客户端在发送请求时,首先要知道服务器端的套接字,才能将请求送到,所以该信息通过命令行参数的格式传入。

        代码如下:

#include<stdio.h>                                                                                                                     
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
        
//程序执行的格式为:./a.out 192.168.3.95 8080
int main(int argc,char* argv[])
{       
    if(argc != 3)//如果传入的参数不为3,打印用法说明
    {   
        printf("Usage:%s [ip][port]\n",argv[0]);
        return 1;
    }       
        
    //创建套接字文件
    int sock = socket(AF_INET,SOCK_DGRAM,0);
    if(sock < 0)//创建失败
    {   
        printf("sock error\n");
        return 2;
    }   
        
    //根据命令行参数来确定服务器端的套接字
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));
    socklen_t len = sizeof(server);
    //开始发送和接受数据
    char buf[128];
    struct sockaddr_in peer;
    while(1)
    {
        socklen_t len1 = sizeof(peer);
        printf("please enter#");
        fflush(stdout);
        buf[0] = 0;
        ssize_t s = read(0,buf,sizeof(buf) - 1);
        if(s > 0)
        {
            buf[s - 1] = 0;
            //发送请求
            sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,len);
        }
        else if(s < 0)
        {
            break;
        }

        //接受服务器端传回的结果
        ssize_t ss = recvfrom(sock,buf,sizeof(buf) - 1,0,(struct sockaddr*)&peer,&len1);
        if(ss > 0)
        {
            buf[ss] = 0;
            printf("server# %s\n",buf);
        }
    }

    //当不再请求时,关闭套接字文件                                                                                                    
    close(sock);
    return 0;
}

        可以通过scp命令将客户端代码远程拷贝到另一主机,然后在两台主机之间进行通信。

scp 本地客户端代码所在的路径 客户端用户名@客户端主机的ip:客户端主机存放客户端代码的路径

        结果如下:

服务器端:

网络编程------UDP协议网络程序

客户端:

网络编程------UDP协议网络程序