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

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

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

UDP协议

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

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

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

(2)无连接

(3)不可靠

(4)面向数据报

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

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

sockaddr结构体

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


        在上述三个结构体中的第一个成员: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:客户端主机存放客户端代码的路径

        结果如下:

服务器端:


客户端:




猜你喜欢

转载自blog.csdn.net/sandmm112/article/details/80246249