Socket:UDP协议小白

UDP(user datagram protocol)用户数据报协议,属于传输层。

首先要搞清楚网络通信的几个层次:

OSI 是 Open System Interconnection 的缩写,译为“开放式系统互联”。
OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
OSI 只是存在于概念和理论上的一种模型,它的缺点是分层太多,增加了网络工作的复杂性,所以没有大规模应用。后来人们对 OSI 进行了简化,合并了一些层,最终只保留了 4 层,从下到上分别是接口层、网络层、传输层和应用层,这就是大名鼎鼎的 TCP/IP 模型

OSI 七层网络模型和 TCP/IP 四层网络模型的对比

我们平常使用的程序(或者说软件)一般都是通过应用层来访问网络的,程序产生的数据会一层一层地往下传输,直到最后的网络接口层,就通过网线发送到互联网上去了。数据每往下走一层,就会被这一层的协议增加一层包装,等到发送到互联网上时,已经比原始数据多了四层包装。整个数据封装的过程就像俄罗斯套娃。

当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。

给数据加包装的过程,实际上就是在数据的头部增加一个标志(一个数据块),表示数据经过了这一层,我已经处理过了。给数据拆包装的过程正好相反,就是去掉数据头部的标志,让它逐渐现出原形。

我们所说的 socket 编程,是站在传输层的基础上,所以可以使用 TCP/UDP 协议,但是不能干「访问网页」这样的事情,因为访问网页所需要的 http 协议位于应用层。

两台计算机进行通信时,必须遵守以下原则:

  • 必须是同一层次进行通信,比如,A 计算机的应用层和 B 计算机的传输层就不能通信,因为它们不在一个层次,数据的拆包会遇到问题。
  • 每一层的功能都必须相同,也就是拥有完全相同的网络模型。如果网络模型都不同,那不就乱套了,谁都不认识谁。
  • 数据只能逐层传输,不能跃层。
  • 每一层可以使用下层提供的服务,并向上层提供服务。

TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,其中 TCP 和 IP 是最常用的两种底层协议,所以把它们统称为“TCP/IP 协议族”。

也就是说,“TCP/IP模型”中所涉及到的协议称为“TCP/IP协议族”。

个人学习的socket 编程是基于 TCP 和 UDP 协议的,它们的层级关系如下图所示:

UDP是面向非连接的协议,它不与对方建立连接,而是直接把数据报发给对方。UDP无需建立类如三次握手的连接,使得通信效率很高。因此UDP适用于一次传输数据量很少、对可靠性要求不高的或对实时性要求高的应用场景。

使用UDP通信的过程

服务端:

      (1)使用函数socket(),生成套接字文件描述符;

      (2)通过struct sockaddr_in 结构设置服务器地址和监听端口;

      (3)使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定;

      (4)接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;

      (5)向客户端发送数据,使用sendto() 函数向服务器主机发送数据;

      (6)关闭套接字,使用close() 函数释放资源;

    客户端:

      (1)使用socket(),生成套接字文件描述符;

      (2)通过struct sockaddr_in 结构设置服务器地址和监听端口;

      (3)向服务器发送数据,sendto() ;

      (4)接收服务器的数据,recvfrom() ;

      (5)关闭套接字,close() ;

(1)socket()函数解读:生成套接字文件描述符

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。

int socket(int af, int type, int protocol);//Linux下//在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字
SOCKET socket(int af, int type, int protocol);//windows下
//两者除了返回值类型不同,其他都是相同的。Windows 不把套接字作为普通文件对待,而是返回 SOCKET 类型的句柄。请看下面的例子: 
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字

1) af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

2) type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)

3) protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?(因为有时TCP和UDP同时满足两种情况,这时候就不知道用哪种了。)

正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

本教程使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议

这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() 函数:

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

这种套接字称为 UDP 套接字。

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字


大家需要记住127.0.0.1,它是一个特殊IP地址,表示本机地址,后面的教程会经常用到。

你也可以使用 PF 前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。

(2)struct sockaddr_in 结构体解读:设置服务器地址和监听端口

上述提到的sockaddr_in是什么?还有一个是sockaddr ?这俩个分别是什么有什么区别

struct sockaddr 和 struct sockaddr_in 这两个结构体用来处理网络通信的地址。

下图是 sockaddr 与 sockaddr_in 的对比(括号中的数字表示所占用的字节数):

sockaddr_in 结构体,sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了:

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};

sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下: 

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};

1) sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。

地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

2) sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。

端口号需要用 htons() 函数转换,为什么呢?

3) sin_addr 是 struct in_addr 结构体类型的变量,下面会详细讲解。

4) sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。

该结构体中提到的另外一个结构体in_addr 结构体,该结构体只包含一个成员,如下所示:

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

in_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换,例如:

unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);

运行结果:
13278343

sockaddr_in 结构体

至于为什么要搞的这么复杂?

为什么要搞这么复杂,结构体中嵌套结构体,而不用 sockaddr_in 的一个成员变量来指明IP地址呢?socket() 函数的第一个参数已经指明了地址类型,为什么在 sockaddr_in 结构体中还要再说明一次呢,这不是啰嗦吗?

这些繁琐的细节确实给初学者带来了一定的障碍,我想,这或许是历史原因吧,后面的接口总要兼容前面的代码。各位读者一定要有耐心,暂时不理解没有关系,根据教程中的代码“照猫画虎”即可,时间久了自然会接受。

总结:

  1. 二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
  2. sockaddr常用于bind、connect、recvfrom、sendto等函数的参数(为社么呢??),指明地址信息,是一种通用的套接字地址。 

bind() 第二个参数的类型为 sockaddr,而代码中却使用 sockaddr_in,然后再强制转换为 sockaddr,这是为什么呢?

因为sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。

  1. sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。

可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址,它的定义如下:

struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6
    in_port_t sin6_port;  //(2)16位端口号
    uint32_t sin6_flowinfo;  //(4)IPv6流信息
    struct in6_addr sin6_addr;  //(4)具体的IPv6地址
    uint32_t sin6_scope_id;  //(4)接口范围ID
};

正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

 

扩展:

两个函数 htons() 和 inet_addr()。

htons()作用是将端口号由主机字节序转换为网络字节序的整数值。(host to net)

inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。

inet_ntoa()作用是将一个sin_addr结构体输出成IP字符串(network to ascii)。比如:

printf("%s",inet_ntoa(mysock.sin_addr));

htonl()作用和htons()一样,不过它针对的是32位的(long),而htons()针对的是两个字节,16位的(short)。

与htonl()和htons()作用相反的两个函数是:ntohl()和ntohs()。 

   

(3)bind()函数解读:绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

下面的代码,将创建的套接字与IP地址 127.0.0.1、端口 1234 绑定:

//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
serv_addr.sin_family = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
serv_addr.sin_port = htons(1234);  //端口

//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

(4)recvfrom() 函数解读:服务器端接收客户端的网络数据

int recvfrom(int s, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen); 

返回值说明:

    成功则返回实际接收到的字符数,失败返回-1,错误原因会存于errno 中。

  参数说明:

    s:          socket描述符;
    buf:       UDP数据报缓存区(包含所接收的数据); 
    len:       缓冲区长度。 
    flags:    调用操作方式(一般设置为0)。 
    from:     指向发送数据的客户端地址信息的结构体(sockaddr_in需类型转换);
    fromlen:指针,指向from结构体长度值。

(5)sendto() 函数解读:向客户端/服务器主机发送数据

int sendto(int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);

返回值说明:

    成功则返回实际传送出去的字符数,失败返回-1,错误原因会存于errno 中。

  参数说明:

    s:      socket描述符;
    buf:  UDP数据报缓存区(包含待发送数据);
    len:   UDP数据报的长度;
    flags:调用方式标志位(一般设置为0);
    to:  指向接收数据的主机地址信息的结构体(sockaddr_in需类型转换);
    tolen:to所指结构体的长度;

示例:

(1)服务器端

//服务器端
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define MAXLINE 4096
#define UDPPORT 8001
#define SERVERIP "192.168.255.129"

using namespace std;

int main(){
    int serverfd;
    unsigned int server_addr_length, client_addr_length;
    char recvline[MAXLINE];
    char sendline[MAXLINE];
    struct sockaddr_in serveraddr , clientaddr;

    // 使用函数socket(),生成套接字文件描述符;
    if( (serverfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){
        perror("socket() error");
        exit(1);
    }

    // 通过struct sockaddr_in 结构设置服务器地址和监听端口;
    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(UDPPORT);
    server_addr_length = sizeof(serveraddr);

    // 使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定;
    if( bind(serverfd, (struct sockaddr *) &serveraddr, server_addr_length) < 0){
        perror("bind() error");
        exit(1);
    }

    // 接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;
    client_addr_length = sizeof(sockaddr_in);
    int recv_length = 0;
    recv_length = recvfrom(serverfd, recvline, sizeof(recvline), 0, (struct sockaddr *) &clientaddr, &client_addr_length);
    cout << "recv_length = "<< recv_length <<endl;
    cout << recvline << endl;

    // 向客户端发送数据,使用sendto() 函数向服务器主机发送数据;
    int send_length = 0;
    sprintf(sendline, "hello client !");
    send_length = sendto(serverfd, sendline, sizeof(sendline), 0, (struct sockaddr *) &clientaddr, client_addr_length);
    if( send_length < 0){
        perror("sendto() error");
        exit(1);
    }
    cout << "send_length = "<< send_length <<endl;

    //关闭套接字,使用close() 函数释放资源;
    close(serverfd);

    return 0;
}

(2)客户端

//客户端
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define MAXLINE 4096
#define UDPPORT 8001
#define SERVERIP "192.168.255.129"

using namespace std;

int main(){
    int confd;
    unsigned int addr_length;
    char recvline[MAXLINE];
    char sendline[MAXLINE];
    struct sockaddr_in serveraddr;

    // 使用socket(),生成套接字文件描述符;
    if( (confd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){
        perror("socket() error");
        exit(1);
    }

    //通过struct sockaddr_in 结构设置服务器地址和监听端口;
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVERIP);
    serveraddr.sin_port = htons(UDPPORT);
    addr_length = sizeof(serveraddr);

    // 向服务器发送数据,sendto() ;
    int send_length = 0;
    sprintf(sendline,"hello server!");
    send_length = sendto(confd, sendline, sizeof(sendline), 0, (struct sockaddr *) &serveraddr, addr_length);
    if(send_length < 0 ){
        perror("sendto() error");
        exit(1);
    }
    cout << "send_length = " << send_length << endl;

    // 接收服务器的数据,recvfrom() ;
    int recv_length = 0;
    recv_length = recvfrom(confd, recvline, sizeof(recvline), 0, (struct sockaddr *) &serveraddr, &addr_length);
    cout << "recv_length = " << recv_length <<endl;
    cout << recvline << endl;

    // 关闭套接字,close() ;
    close(confd);

    return 0;
}

参考:https://blog.csdn.net/qingzhuyuxian/article/details/79736821

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页