UDP通信

1. IP地址和PORT端口号

IP地址:用来在公网中确定唯一的一台主机
PORT端口号:标定特定一台主机上的唯一进程
IP+PORT:标定全网中唯一的一个进程

网络套接字的本质是进程间通信

2. 初识UDP(User Datagram Protocol)协议

UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况下,UDP也无法进行流量控制等避免网络拥塞的行为。此外,传输途中即使出现丢包,UDP也不负责重发。

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

3. 网络字节序

3.1 大端模式和小端模式

字节序是指多字节数据的存储顺序,在设计计算机系统的时候,有两种处理内存中数据的方法:大端格式、小端格式。

  • 小端格式(Little-Endian):将低位字节数据存储在低地址;
  • 大端格式(Big-Endian):将高位字节数据存储在低地址;
    在这里插入图片描述

举例:对于整形 0x12345678,它在大端格式和小端格式的系统中,分别如下图所示的方式存放

在这里插入图片描述

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
在这里插入图片描述

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

4. socketaddr结构

在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数

sockaddr 结构


/* Structure describing a generic socket address.  */
struct sockaddr
{
 __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
 char sa_data[14];           /* Address data.  */
};

sockaddr_in 结构


/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
 __SOCKADDR_COMMON (sin_);
 in_port_t sin_port;         /* Port number.  */
 struct in_addr sin_addr;    /* Internet address.  */
 
 /* Pad to size of `struct sockaddr'.  */
 unsigned char sin_zero[sizeof (struct sockaddr) -
            __SOCKADDR_COMMON_SIZE -
            sizeof (in_port_t) -
            sizeof (struct in_addr)];

  • 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.

in_addr结构

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
 in_addr_t s_addr;
};
  • in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数(4字节)。 值得注意:INADDR_ANY,此时表示主机上的任意IP地址(一般Server端会写这个,这样就不会局限于一个IP地址,同主机上的别的IP地址就无法通行,毕竟IP地址最大的作用是去标定一台主机)

5. UDP使用的socket编程接口

创建 socket() 文件描述符 (UDP, 客户端 + 服务器)

原型:int socket(int domain, int type, int protocol);
返回值: returns file (socket) descriptor if OK, -1 on error.(成功返回文件描述符,失败返回-1)
domain:AF_INET, AF_INET6, AF_UNIX, AF_UNSPEC (address format)

type:SOCK_DGRAM(udp), SOCK_RAW, SOCK_STREAM(tcp), SOCK_SEQPACKET

protocol:IPPROTO_IP, IPPROTO_IPV6, IPPROTO_TCP, IPPROTO_UDP
之所以每一个参数都有多种种类,就是因为设计者想要尽可能减少接口,避免记忆成本的增加,实际上是一种多态的思想。

bind() 绑定端口号 (UDP, 服务器)

原型:int bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
返回值:returns 0 on success, or -1 on error.(成功返回0,失败返回-1)

recvfrom() : UDP类型的数据接收

原型:int recvfrom(int sockfd, void * buf, size_t len, int flags, struct sockaddr * src_addr, int * addrlen);
参数解释:
sockfd – 接收端套接字描述;
buf – 用于接收数据的应用缓冲区地址;
len – 指明缓冲区大小;
flags – 通常为0(数据的接收有的时候是不满足条件的,比如client端并没有给server端发送数据的时候,此时server端应该处于阻塞状态等待);
src_addr – 数据来源端的地址(IP address,Port number)(因为我作为接收端希望知道谁给我发的,发的什么).
fromlen – 作为输入时,fromlen常常设置为sizeof(struct sockaddr)(要知道发送过来的大小).
返回值:成功则返回接收到的字符数,失败则返回-1,错误原因存于errno中

sendto() : UDP类型的数据发送

原型:int sendto(int sockfd, const void * msg, int len, unsigned int flags, const struct sockaddr * dst_addr, int addrlen);
参数同上述基本一致,只是接收改为发送即可。
返回值:成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。

对于这两个函数接口的返回值都是ssize_t类型(表明实际接收的大小,是一个int类型,因为typedef int ssize_t)
size_t是标准C库中定义的,应为unsigned int

在这里插入图片描述
ip地址的转换(从点分十进制转换到32位地址、从32位地址转换到点分十进制)
inet_addr(const char * pIpAddr)

inet_addr()将一个点分十进制的ip地址转换为32位地址,用在bind、send(TCP)、sendto(UDP)等发送报文的接口前

inet_ntoa(struct in_addr stAddr)

inet_ntoa()将一个32位地址ip地址转换为点分十进制地址,用在recv(TCP)、recvfrom(UDP)等接收报文的接口前面

6. UDP通信代码

udpServer.cc

#include"udpServer.hpp"

//使用说明书
void Usage(std::string proc)
{
  std::cout<<"Usage: "<<proc<<" local_port"<<std::endl;
}
int main(int argc,char *argv[])
{
  if(argc != 2){
    Usage(argv[0]);
    exit(1);
  }
  udpServer* up = new udpServer(atoi(argv[1]));//此时所定义出来的Server对象在堆上,堆的空间大,所以推荐以后都这样写
  up->initServer();
  up->start();

  delete up;//在结束的时候,还需要主动的调用,来析构掉所创建出来的类
}

  1. 这里使用到了命令行参数形式,来进行程序的启动。能够更加灵活进行IP地址的更改和端口号的修改之后Client端和Server端依旧可以通信。

在这里插入图片描述

udpServer.hpp

#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>
class udpServer{
  private:
    //std::string ip;Server端的IP地址可以省略,那么在写sockaddr_in结构体的时候,就需要将地址设定为INADDR_ANY,表示主机的任意一个IP地址
    int port;//对于端口号当然也可以定义为short类型,2字节
    int sock;
  public:
    //udpServer(std::string _ip = "127.0.0.1",int _port = 8080)
      //:ip(_ip),port(_port)
    udpServer(int _port = 8080)
     :port(_port)
    {}

    void initServer()
    {
      //第一步应该先创建套接字
      sock = socket(AF_INET,SOCK_DGRAM,0);//第一个参数-协议家族中的哪一个协议 第二个参数①面向字节流TCP ②用户数据报 UDP 第三个参数设置为0,使用默认的行为

      struct sockaddr_in local;//这个是一个临时变量,在用户栈上所定义出来变量,要想把它写入到操作系统中,还需要一步调用bing()接口
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
      //local.sin_addr.s_addr = inet_addr(ip.c_str()); //ip是一个STL的容器,所以要把它转换为c语言形式
      local.sin_addr.s_addr = htonl(INADDR_IN);//INADDR_IN实际上是一个宏,也就是0,那就是一个int类型的整数,需要转换成为网络字节序使用htonl()接口,当然也可以直接写INADDR_IN

      if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
        std::cerr<< "bind error"<<std::endl;
        exit(1); //终止进程
      }
    }

    void start()
    {
      char msg[64]; //接收的缓冲区
      //对于服务器来说,我不希望他终止,我要她一直运行下去,所以需要一个死循环
      for(;;){
        msg[0] = '\0';//对缓冲区进行初始化
        //ssize_t 是一个int类型,是一个有符号的
        struct sockaddr_in end_point; //表示对端
        socklen_t len = sizeof(end_point);
        ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
        if(s > 0){
        //我还希望知道谁给我发的,所以需要获取到它的IP地址和端口号
          char buf[16];
          sprintf(buf,"%d",ntohs(end_point.sin_port));//end_point.sin_port是一个int类型的整数,此时需要把它转换为字符串,所以需要sprintf把一个整数转成字符串

          std::string cli = inet_ntoa(end_point.sin_addr);//把网络的4字节转换为点分十进制形式的字符串
          cli += ":";
          cli += buf;

          msg[s] = '\0';
          std::cout<< cli <<"#"<< msg <<std::endl;

          //我希望接收以后在Server端打印出来,返回都去的数值再加上一定的标识,说明Server接收到了的应答
          std::string echo_string = msg;
          echo_string += " [Server echo!] ";
          sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);//参数都是要以C语言形式进行传参的
        }
      }
    }
    ~udpServer()
    {
      close(sock);
    }
};

扩展

  1. 识别恶意程序:对于Server端来说,是知道谁给我发送消息的,因为此时我可以拿到他的ip+port,正常情况下,一个普通用户对于Server端的访问次数肯定次数是有限的,比如一分钟20次,但是当有一个ip在短时间内进行了上百万次的访问的时候,说明这是一个恶意的攻击程序,所以当此时Server端就可以采取相应的措施,不再让这个Client端进行访问。
  2. 群聊: 对于Server端来说,是知道谁给我发送消息的,所以我把这些已经出现的ip+port信息都放在一个vector中,当其中有一个人发消息之后,我就遍历整个vector,把消息发送给每一个,那么这样就构建了一个群聊
  3. xshell(原理):首先Server端接收到你所发送来的信息①进行命令分析②fork(创建子进程)③exec(进行程序替换)④dup2(重定向)1,socket

udpClient.cc

#include"udpClient.hpp"

void Usage(std::string proc)
{
  std::cout<<"Usage: "<<proc<<"server_ip server_port"<<std::endl;
}

int main(int argc,char *argv[])
{
  if(argc != 3){
    Usage(argv[0]);
    exit(1);
  }
  udpClient uc(argv[1],atoi(argv[2]));//此时所定义出来的Client对象在栈上,但是我们知道,栈的空间是有限的,比较小,所以不推荐这种方式,但是此时代码还比较少,所以可以采用这种方式。
  uc.initClient();
  uc.start();

  return 0;
}

在这里插入图片描述

udpClient.hpp

#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>
class udpClient{
  private:
    std::string ip;
    int port;//对于端口号当然也可以定义为short类型,2字节
    int sock;
  public:
    //因为客户端需要链接服务器,所以这里应该需要的是Server端的ip和port
    udpClient(std::string _ip = "127.0.0.1",int _port = 8080)
      :ip(_ip),port(_port)
    {}

    void initClient()
    {
      //第一步应该先创建套接字
      sock = socket(AF_INET,SOCK_DGRAM,0);//第一个参数-协议家族中的哪一个协议 第二个参数①面向字节流TCP ②用户数据报 UDP 第三个参数设置为0,使用默认的行为

      //客户端是不需要绑定,交由操作系统来帮我们完成这一步
    }

    void start()
    {
      std::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());//把点分十进制的IP地址转换为网络的4字节,但是这个接口只能接受C语言格式的字符串,现在你的ip地址是一个string类型,所以还需要转换
      for(;;){
        std::cout<< "Please Enter# ";
        std::cin>>msg;


        //对于Client来说,应该是要先给Server端发送数据,然后在接收
        sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));

        char echo[128];
        ssize_t s = recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);
        if(s > 0){
          echo[s] = '\0';
          std::cout<<"Server# "<<echo<<std::endl;
        }
      }
    }
    ~udpClient()
    {
      close(sock);
    }
};
  1. udpClient端为什么不写bind()?答:其实是由操作系统帮我们完成了这一步,比如说:对于一个Client来说,你的机子上可能同时打开着多个程序(进程),那么你要是显示的去绑定端口号的话,有可能会发生冲突,导致当有一个进程也想要绑定正在被占用的端口号的时候,就会导致进程挂掉,也就是访问不了。对于这些端口号我们虽然不是很清楚谁被占了,谁还是空闲的,但是操作系统是非常清楚的,所以交给操作系统来完成这一步。
  2. IP地址:127.0.0.1是本地环回通常用来进行网络通信代码的本地测试,一般本地环境能够通过的话,那么代码基本上是没有问题的。

Makefile

FLAG=-std=c++11

.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
	g++ -o $@ $^ $(FLAG)
udpServer:udpServer.cc
	g++ -o $@ $^ $(FLAG)
.PHONY:clean
clean:
	rm -f udpClient udpServer 

在这里插入图片描述

  • ifconfig可以查看关于这台主机的所有相关的ip地址,由于Server端的ip地址所写的是INADDR_ANY,所以可以接收主机相关的所有IP地址发送过来的消息。

在这里插入图片描述

6.1 UDP—英译汉

#include<iostream>
#include<string>
#include<map>
#include<cstdio>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
class udpServer{
  private:
    int port;//对于端口号当然也可以定义为short类型,2字节
    int sock;
    std::map<std::string,std::string> dict; //字典
  public:
    udpServer(int _port = 8080)
      :port(_port)
    {
      dict.insert(std::pair<std::string,std::string>("string","字符串"));
      dict.insert(std::pair<std::string,std::string>("sort","排序"));
      dict.insert(std::pair<std::string,std::string>("student","学生"));
    }

    void initServer()
    {
      //第一步应该先创建套接字
      sock = socket(AF_INET,SOCK_DGRAM,0);//第一个参数-协议家族中的哪一个协议 第二个参数①面向字节流TCP ②用户数据报 UDP 第三个参数设置为0,使用默认的行为

      struct sockaddr_in local;//这个是一个临时变量,在用户栈上所定义出来变量,要想把它写入到操作系统中,还需要一步
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
     // local.sin_addr.s_addr = inet_addr(ip.c_str()); //ip是一个STL的容器,所以要把它转换为c语言形式
      local.sin_addr.s_addr = INADDR_ANY; //ip是一个STL的容器,所以要把它转换为c语言形式

      if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
        std::cerr<< "bind error"<<std::endl;
        exit(1); //终止进程
      }
    }

    void start()
    {
      char msg[64]; //接收的缓冲区
      //对于服务器来说,我不希望他终止,我要她一直运行下去,所以需要一个死循环
      for(;;){

      
        msg[0] = '\0';
        //ssize_t 是一个int类型,是一个有符号的
        struct sockaddr_in end_point; //表示对端
        socklen_t len = sizeof(end_point);
        ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
        if(s > 0){
          char buf[16];
          sprintf(buf,"%d",ntohs(end_point.sin_port));

          std::string cli = inet_ntoa(end_point.sin_addr);
          cli += ":";
          cli += buf;

          msg[s] = '\0';
          std::cout<< cli <<" # "<< msg <<std::endl;

          std::string echo = "unknow";
          //std::map<std::string,std::string>::iterator it = dict.find(msg);
          auto it = dict.find(msg);
          while(it != dict.end()){
            //说明找到了
            echo = dict[msg];//通过K值返回V值
            break;
          }
          sendto(sock,echo.c_str(),echo.size(),0,(struct sockaddr*)&end_point,len);
        }
      }
    }
    ~udpServer()
    {
      close(sock);
    }
};

在这里插入图片描述

借鉴blog转自:

  1. socket网络编程接口链接: link.
  2. sockaddr和sockaddr_in结构体详解链接: link.
  3. 网络字节序和IP地址链接: link.
  4. IP地址转换链接: link.
  • 13
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值