2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto

Posted 快乐江湖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto相关的知识,希望对你有一定的参考价值。

一:回顾

前文讲过,套接字分为流式套接字(SOCKET_STREAM)和数据报套接字(SOCKET_DGRAM),他们所采用的协议分别为TCPUDP,相应的对应的Socket编程就是TCP套接字编程UDP套接字编程

相比于UDP而言,TCP保证了数据的可靠传输,所以它比UDP就复杂一点,从下面的流程图中也可以看出来

  • 流式套接字

在这里插入图片描述

  • 数据包套接字

在这里插入图片描述

二:彻底了解套接字和struct socket结构

(1)一切皆文件-文件描述符-套接字描述符

下图是Linux内核中关于socket的数据结构还有后续我们再说编程时的一些API接口。

你可能注意到了一个非常熟悉的地方,struct file* file,这不就是文件吗?是的没错,如果再深入理解一点,其实套接字就是使用文件描述符和其它程序进行通讯的一种方式。我们知道,Linux系统在执行任何I/O的时候,都在和文件描述符打交道,而在Linux下,我们一直反复强调“一切皆文件的思想”,之前说过的屏幕都可以作为文件,那么现在接触的网卡也当然可以做文件
在这里插入图片描述
所以以后再进行网络通讯时,你会利用socket系统调用,它将返回套接字文件描述符,然后你会利用它再通过相应(如下)接口进行通信操作。
既然它是文件描述符,那就意味着你仍然可以使用read()和write()来进行通信,但是“术业有专攻”,网络的事情还是尽量使用它们对应的接口来操作。
在这里插入图片描述

(2)struct socket结构

A:struct socket结构体作用

用户使用socket系统调用编写程序时,通过套接字描述符完成相关操作

int socket(int domain, int type, int protocol);

它对应的就是我们在上面说到的struct socket结构体
在这里插入图片描述

那么内核中为什么要有struct socket这样的结构体呢,以及它有什么作用呢?可以看下面这张图
在这里插入图片描述

所以内核中的进程可以通过该结构体来访问Linux内核中的传输层,网络层和数据链路层,也就是说struct socket是内核中的进程与内核中的网络系统的桥梁

B:struct socket结构体详解

这是一个基本的BSD socket,我们调用socket系统调用创建的各种不同类型的socket,开始创建的都是它,到后面**,各种不同类型的socket在它的基础上进行 各种扩展。struct socket是在虚拟文件系统上被创建出来的,可以把它看成一个文件,**是可以被安全地扩展的。下面是其完整定义:

struct socket {  
    socket_state            state;  
    unsigned long           flags;  
    const struct proto_ops *ops;  
    struct fasync_struct    *fasync_list;  
    struct file             *file;  
    struct sock             *sk;  
    wait_queue_head_t       wait;  
    short                   type;  
};  

1: state用于表示socket所处的状态,是一个枚举变量,其类型定义如下:

该成员只对TCP socket有用,因为只有tcp是面向连接的协议,udp跟raw不需要维护socket状态。

typedef enum {  
    SS_FREE = 0,            //该socket还未分配  
    SS_UNCONNECTED,         //未连向任何socket  
    SS_CONNECTING,          //正在连接过程中  
    SS_CONNECTED,           //已连向一个socket  
    SS_DISCONNECTING        //正在断开连接的过程中  
}socket_state;  

2:ops是协议相关的一组操作集,结构体struct proto_ops的定义如下:

struct proto_ops {  
        int     family;  
        struct module   *owner;  
        int (*release)(struct socket *sock);  
        int (*bind)(struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);  
        int (*connect)(struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);  
        int (*socketpair)(struct socket *sock1, struct socket *sock2);  
        int (*accept)(struct socket *sock,struct socket *newsock, int flags);  
        int (*getname)(struct socket *sock, struct sockaddr *addr,int *sockaddr_len, int peer);  
        unsigned int (*poll)(struct file *file, struct socket *sock,  
                        struct poll_table_struct *wait);  
        int (*ioctl)(struct socket *sock, unsigned int cmd, unsigned long arg);  
        int (*listen)(struct socket *sock, int len);  
        int (*shutdown)(struct socket *sock, int flags);  
        int (*setsockopt)(struct socket *sock, int level,  
                        int optname, char __user *optval, int optlen);  
        int (*getsockopt)(struct socket *sock, int level,  
                        int optname, char __user *optval, int __user *optlen);  
        int (*sendmsg)(struct kiocb *iocb, struct socket *sock,  
                        struct msghdr *m, size_t total_len);  
        int (*recvmsg)(struct kiocb *iocb, struct socket *sock,  
                        struct msghdr *m, size_t total_len, int flags);  
        int (*mmap)(struct file *file, struct socket *sock,struct vm_area_struct * vma);  
        ssize_t (*sendpage)(struct socket *sock, struct page *page,  
                        int offset, size_t size, int flags);  
    };  

其中协议栈总共定义了了三个strcut proto_ops类型的变量,分别myinet_stream_ops, myinet_dgram_ops, myinet_sockraw_ops,分别对应流式套接字,数据报套接字和原生套接字

3:type是socket的类型,对应的取值如下:

enum sock_type {  
    SOCK_DGRAM = 1,  //数据报套接字
    SOCK_STREAM = 2,  //流式套接字
    SOCK_RAW    = 3,  //原生套接字
    SOCK_RDM    = 4,  
    SOCK_SEQPACKET = 5,  
    SOCK_DCCP   = 6,  
    SOCK_PACKET = 10,  
};  

4:sk是网络层对于socket的表示,用户需要进行传参,让网络层采用TCP还是UDP

三:socket接口(UDP)和sockaddr结构

(1)socket常用API接口1(UDP)

所以我们要完成通讯,就要利用sockt结构体提供给我们的一些接口来实现。由于UDP只负责传送,所以相较于TCP而言它的接口少一点,所以这里只列出UDP中使用到的,但需要注意的是下面的接口对TCP也是通用的,只不过TCP相较于UDP的接口要多一点,还要扩展一点。

本节的模型就是一个服务端接受客户端发送的消息,然后回应客户端

1:创建 一个socket 文件描述符

#include <sys/tyeps.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);

他们三个参数的含义及选用如下
在这里插入图片描述
对于它的返回值其实前面我们已经说过了,本质是一个文件描述符
在这里插入图片描述

2:绑定地址信息

前文说过,IP地址+端口号唯一表示了全网的一个进程。服务端既然想要提供服务,那么必须要让客户端知道怎么找到自己。所以对于服务端我们需要填入ip地址和端口号,以便客户端可以找到自己。 当然客户端一般是不要绑定的,当数据返回给客户端时,具体要用哪一个端口,是操作系统决定的,如果我们人为去绑定,可能导致端口号的冲突

#include <sys/types.h>
#include <sys/socket.h>

int bind(int socket,const struct sockaddr* address,socklen_t addrss_len);

第一个参数很好理解,第二个参数和第三个参数就是我们下面要说到的sockaddr结构

(2)sockaddr结构

所以在绑定时,我们需要将ip和端口号一起封装在某个结构体中,然后传参到bind接口里面。它共有三种类型的结构体

在这里插入图片描述

你可能发现了,bind接口的形参给的是struct sockaddr*的指针,但是为什么这里有三种类型的结构呢。其实这样的设计主要是为了用更少的操作完成更多的事情。

在传参时,这三种结构体的前16位是不相同的,它就是通过这个来区分的,所以只要保证在对齐的情况下,就能用struct sockaddr*来接受不同的结构,这有点像C++中的切片操作

A:struct sockaddr结构

struct sockaddr为许多类型的套接字存储套接字地址信息,其结构如下

struct sockaddr {
  unsigned short sa_family; /* 地址家族, AF_xxx */

  char sa_data[14]; /*14字节协议地址*/
};

这个结构体是一个通用的地址信息结构,它并不是某一个具体的地址信息结构。所以我们不选择它,而选择struct sockaddr_int这种结构

B:struct sockaddr_in

该结构体如下

struct sockaddr_in {

  short int sin_family; /* 通信类型 */

  unsigned short int sin_port; /* 端口 */

  struct in_addr sin_addr; /* Internet 地址 */

  unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/

};

用这样结构可以很轻松的处理套接字地址的基本元素。

现在让我们回到bind接口,看一下它是如何传参的:当我们使用ipv4版本的洗衣,绑定地址信息的时候,需要填充struct sockadd_in结构体来保存服务器的ip和端口号
在这里插入图片描述

四:UDP通信示例

(1)UDP通信

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

  1. 使用函数socket(),生成套接字文件描述符
  2. 通过struct sockaddr_in 结构设置服务器地址和监听端口
  3. 使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定;
  4. 接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;
  5. 向客户端发送数据,使用sendto() 函数向服务器主机发送数据;
  6. 关闭套接字,使用close() 函数释放资源;

UDP客户端流程如下

  1. 使用函数socket(),生成套接字文件描述符
  2. 通过struct sockaddr_in 结构设置服务器地址和监听端口
  3. 向服务器发送数据,sendto() ;
  4. 接收服务器的数据,recvfrom() ;
  5. 关闭套接字,close() ;

需要注意以下几点

  1. 服务器和客户端地址理应是不一样的,这里为了测试使用本地环回地址

  2. ip地址实际是点分十进制,每个部分算作1个字节,但是我们输入时往往是字符串,所以下面的接口可以将字符串转换为正确的IP地址,并且是网络字节序
    在这里插入图片描述

  3. 如果需要将四字节序列转为点分十进制,则用char *inet_ntoa (struct in_addr);

  4. 本地环回:本地环回地址为127.0.0.1,这是一个测试IP。表示数据会完整的走一遍协议,但是是自己发自己收

  5. 对于服务端一般不指定某个ip,因为有可能会有很多个ip,如果指定了ip,服务端只能接受特定ip的数据。所以我们一般把ip设置为一个宏,也即INADDR_ANY表示绑定任意IP

(1)sendto和recvfrom接口

1:UDP发送函数

#include <sys/types.h>
#include <sys/socket.h>
int sendto(
int sockfd,//套接字描述符
const void* buf,//要发什么东西
size_t len,//期望发多长
,int flags,//阻塞还是非阻塞,一般设置为0
const struct sockaddr* dest_addr,//指向服务器的struct_in结构体(注意强转)
socklen_t addrlen//指的是上面结构体的长度
)

2:UDP接受函数

#include <sys/tyeps.h>
#include <sys/socket.h>
ssize_t recvfrom(
int sockfd,//套接字描述符
void* buf,//读取到放在哪?
size_t len,//期望读取多长
int flags.//没有数据读的时候挂起,默认设置为0表示阻塞等待
struct sockaddr* src_addr,
socklen_t* addrlen//保存那个客户端发给你的(这两个如果你不关心是谁发给你的,设置为null即可。)

(2)代码

udpServer.h

#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;

class udpServer
{
private:
  int _port;//端口号
  int _sock;//套接字描述符

public:
  udpServer(int port=8080):_port(port)
  {

  }

  void initServer()//初始化服务器
  {
    _sock=socket(AF_INET,SOCK_DGRAM,0);//
    cout<<_sock<<endl;
    struct sockaddr_in local;//创建sockaddr_in结构
    //填充
    local.sin_family=AF_INET;//IPV4协议
    local.sin_port=htons(_port);//主机字节序转为网路字节序
    local.sin_addr.s_addr=INADDR_ANY;//绑定任意IP

    //绑定
    if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        cerr << "绑定失败" <<endl;
        exit(1);
    }
  }
  void startServer()
  {
    char msg[64];
    for(;;)//服务器永不停机
    {
      msg[0]='\\0';//清空缓冲区
      struct sockaddr_in end_point;//客户端的信息
      socklen_t len=sizeof(end_point);//输出和输入型参数,recvfrom和sendto都要用
      ssize_t ret=recvfrom(_sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);//接受,并把客户端的信息保存在结构体当中

      if(ret > 0)
      {
        char bu[16];
        sprintf(bu,"%d",ntohs(end_point.sin_port));//把网络字节序转为主机字节序,端口号
        string cli=inet_ntoa(end_point.sin_addr);//把客户端的ip转为点分十进制
        cli+=":";
        cli+=bu;

        msg[ret]='\\0';
        cout<<"服务端接受到消息:来自->"<<cli<<" "<< msg << endl;

        string respond="服务端回消息";
        sendto(_sock,respond.c_str(),respond.size(),0,(struct sockaddr*)&end_point,len);//服务器应答
      }

    }
   }
  ~udpServer()
  {
    close(_sock);
  }
};

udpServer.cpp

#include "udpServer.h"

int main(int argc,char* argv[])
{
  if(argc!=2)//判断是否传入端口号
  {
    cout<<"端口号未传入"<<endl;
    exit(1);
  }

  udpServer* ss=new udpServer(atoi(argv[1]));
  ss->initServer();
  ss->startServer();
}

udpClient.h

#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;

class udpClient
{
private:
  string _ip;//
  int _port;//客户端要保存服务器的IP和端口
  int _sock;//套接字描述符

public:
  udpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port)
  {
      //连接服务器,本地环回测试
  }

  void initClient()//初始化服务器
  {
    _sock=socket(AF_INET,SOCK_DGRAM,0);//

    //客户端不需要绑定
  }
  void startClient()
  {
    string msg;//接受用户输入
    //发送给服务器
    struct sockaddr_in peer;
    peer.sin_family=AF_INET;
    peer.sin_port=htons(_port);
    peer.sin_addr.s_addr=inet_addr(_ip.c_str());

    for(;;)
    {
    
      cout<<"【请输入:】";
      cin>>msg;
      if(msg=="quit")
        break;//如果用户输入退出,下线


    sendto(_sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));//客户端给服务端发送消息

    //服务器接受消息会返回信息
    char echo[128];
    ssize_t ret=recvfrom(_sock,echo,sizeof(echo)-1,0,nullptr,nullptr);//不关心服务器的地址

    if(ret > 0)
    {
      echo[ret]='\\0';
      cout<<"客户端受到回应"<< echo<< endl;
    }
    
  }
  }
  
  ~udpClient()
  {
    close(_sock);
  }

};

udpClient.cpp

#include "udpClient.h"

int main(int argc,char* argv[])
{
  if(argc!=3)
  {
    cout<<"服务器地址没有传入"<<endl;
    exit(1);
  }

  udpClient uu(argv[1],atoi(argv[2]));
  uu.initClient();//初始化客户端
  uu.startClient();//启动客户端
}

(3)效果

使用netstat nlup可以查看网络进程信息

在这里插入图片描述
1:本地环回测试
在这里插入图片描述
2:局域网IP
在这里插入图片描述
3:公网IP
在这里插入图片描述

以上是关于2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto的主要内容,如果未能解决你的问题,请参考以下文章

UDP之socket编程

专题七.网络编程之套接字SocketTCP和UDP通信实例

2-4:套接字(Socket)编程之TCP通信

Python经典面试题之网络编程与并发34问

Linux之socket套接字编程20160704

UDP和Socket通信步骤