草庐IT

socket(套接字)实现udp通信

_End丶断弦 2024-02-16 原文

udp通信

储备知识

源ip地址和目的ip地址

我们先来看个例子:

如果当女儿国国王问你上一站从何而来,下一站去往何处?唐僧就会说我上一站从XXX来下一站到XXX。唐僧总是有2套说辞。源ip地址就像是唐僧的上一站,目的ip就是下一站的地址。

源ip地址:就是发送数据包的那个电脑的IP地址。
目的ip地址: 就是想要发送到的那个电脑的IP地址。

端口号

那我们有了ip地址就能通信了吗?例如QQ发消息,我们有了ip地址能够把信息发给对方的机器上,但是我们还需要有一个其他的标识来区分出这个数据交给哪个程序来进行解析。
下面来简单认识一下端口号:

  • 端口号是是一个2字节16位的整数
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
  • 一个端口号只能被一个进程占用

ip来标识主机,端口号标识进程,ip+端口号就可以标识全网的唯一进程,我们就可以知道数据要交给哪个程序解析了。一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。

源端口号和目的端口号

和上面的源ip地址、目的ip地址一样的,传输层协议的数据段中有2个端口号,源端口号:数据是谁发的,目的端口号:要发给谁

udp协议:先简单认识一下,后面有详细的讲解

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

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

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

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

下面有一批接口可以使用:

#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htonl(uint16_t hostshort);
uint32_t htonl(uint32_t netlong);
uint16_t htonl(uint16_t netshort);

详细解释:

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

udp使用的接口

先来认识udp的一套接口:

1.创建socket

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

参数说明:
domain:协议域又称协议家族,协议族决定了socket的地址类型,我们使用ipv4进行通信,使用AF_INET
type:套接字类别,有流式套接字和数据报套接字,upd使用的是SOCK_DGRAM
protocol:协议指定与套接字一起使用的特定协议。默认使用0即可。

返回值:

成功则返回socket文件描述符,错误返回-1.

为什么返回文件描述符?

Linux中说一切皆文件,为了表示和区分已经打开的文件,Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
所以网络连接也是一个文件,也要有文件描述符,所以它的返回值是文件描述符。

2.bind(绑定函数)

函数原型:

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

参数说明:
socket:需要绑定的socket
addr:存放了服务端用于通信的地址和端口。
addrlen:表示addr结构体的大小。

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

3.recvfrom(接收)

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr*src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:接收的socket
  • buf:接收的数据放在哪
  • len:接收多大的数据
  • flags:需不需要阻塞,默认填0
  • src_addr:发送数据的客户端地址信息的结构体
  • addrlen:指向结构体长度值
  • 注意后两个参数是输出参数,其中addrlen既是输入又是输出参数,即值-结果参数,需要在调用时,指明src_addr的长度。另外,如果不关心数据发送端的地址,可以将后两者均设置为NULL。

返回值:如果正确接收返回接收到的字节数,失败返回-1.

4.sendto(发送数据)

函数原型:

ssize_t sendto(int sockfd, const void *buf, size_t len,int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  • sockfd:socket文件描述字
  • buf:指明一个存放应用程序要发送数据的缓冲区
  • len:指明buf的长度
  • flag:一般设置为0
  • dest_addr:表示目的机的地址和端口号信息
  • addrlen:常被赋值为sizeof(struct sockaddr)

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。上面的接口都是使用sockaddr结构,先来看一下它的结构:

它们的接口使用的都是struct sockaddr,是为了使用一套接口就可以完成通信。但是我们在使用的时候使用的是第2个,因为我们实现通信就要传ip地址和端口号,哪个8字节填充是为了内存对齐,不用管它。那怎么区别它们呢?

只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好
处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

sockaddr_in结构


下面的填充的不用管。

in_addr结构


in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

简单的udp通信

知道了上面的接口,下面我们使用这些接口写一个简单的udp通信。

服务器端:

我们让服务器一直读客户端发来的信息,并给服务器返回。整体的逻辑:先创建套接字,绑定,接收客户端信息,给客户端返回信息。

udpServer.hpp:

  1 #pragma once                                                                                                                        
  2 #include<iostream>     
  3 #include<cstdio>       
  4 #include<string>  
  5 #include<unistd.h>
  6 #include<sys/socket.h>
  7 #include<stdlib.h>    
  8 #include<sys/types.h> 
  9 #include<netinet/in.h>
 10 #include<arpa/inet.h> 
 11                       
 12 class udpServer       
 13 {                    
 14   private:     
 15     std::string ip;
 16     int port;      
 17     int sock;      
 18   public:          
 19     udpServer(std::string _ip="127.0.0.1",int _port = 8088)
 20       :ip(_ip)                                             
 21       ,port(_port)                                         
 22   {}                                                       
 23     void initServer()
 24     {                
 25        sock = socket(AF_INET,SOCK_DGRAM,0);
 26        std::cout<<"sock:"<<sock<<std::endl;
 27        struct sockaddr_in local;           
 28        local.sin_family = AF_INET;         
 29        local.sin_port = htons(port);//主机序列转成网络序列
 30        local.sin_addr.s_addr = inet_addr(ip.c_str());     
 31                                                           
 32        //开始绑定                                         
 33        //绑定失败就直接退出                          
 34        if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
 35        {                                                        
 36          std::cerr<<"bind error"<<std::endl;   
  37          exit(1);
 38        
 39        }
 40     }
 41     void start()
 42     {
 43       char msg[64];
 44          for(;;)
 45          {
 46            msg[0] = '\0';
 47            //从远端接收
 48             struct sockaddr_in end_point;
 49             socklen_t len = sizeof(end_point);
 50             ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
 51             if(s > 0 )
 52             {
 53               msg[s] = '\0';
 54               std::cout<<"client:"<<msg<<std::endl;
 55               std::string echo_string = msg;
 56               echo_string += "[srever echo]";
 57               sendto(sock,echo_string.c_str(),echo_string.size(),0,
 58                     (struct sockaddr*)&end_point,len);
 59             }
 60          }
 61     }
 62     ~udpServer()
 63     {
 64        close(sock);
 65     }
 66 };
   

udpServer.cc

  1 #include"udpServer.hpp"
  2 
  3 int main()
  4 {
  5   udpServer *us = new udpServer();
  6   us->initServer();
  7   us->start();
  8   delete us;                                                                                                                        
  9   return 0;
 10 }

客户端发消息给服务器端,服务器端返回echo server

udpClient,hpp

  1 #pragma once                                                                                                                        
  2 #include<iostream>
  3 #include<cstdio>
  4 #include<unistd.h>
  5 #include<sys/socket.h>
  6 #include<stdlib.h>
  7 #include<sys/types.h>
  8 #include<arpa/inet.h>
  9 #include<string>
 10 #include<netinet/in.h>
 11 class udpClient
 12 {
 13   private:
 14     std::string ip;
 15     int port;
 16     int sock;
 17   public:
 18     udpClient(std::string _ip="127.0.0.1",int _port = 8088)
 19       :ip(_ip)
 20       ,port(_port)
 21   {}
 22     void initClient()
 23     {
 24        sock = socket(AF_INET,SOCK_DGRAM,0);
 25        std::cout<<"sock:"<<sock<<std::endl;
 26 
 27     }
 28     void start()
 29     {
 30 
 31       std::string msg;
 32       struct sockaddr_in peer;
 33       peer.sin_family = AF_INET;
 34       peer.sin_port = htons(port);
 35       peer.sin_addr.s_addr = inet_addr(ip.c_str());
 36          for(;;)
 37          {
 38            std::cout<<"please enter:";
 39            std::cin>>msg;
 40            if(msg == "quit")
 41            {
 42              break;
 43            }
 44             sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
 45             char echo[128];
 46             ssize_t s = recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);
 47             if(s > 0)
 48             {
 49               echo[s] = 0;
 50               std::cout<<"server:"<<echo<<std::endl;
 51             }
 52          }
 53     }
 54     ~udpClient()
 55     {
 56        close(sock);
 57     }
 58 };

udpClient.cc

  1 #include"udpClient.hpp"
  2 
  3 int main()
  4 {
  5   udpClient uc;
  6   uc.initClient();
  7   uc.start();                                                                                                                       
  8   return 0;
  9 }

下面开始通信


我们可以看到通信成功了,服务器给我们返回echo server。

优化服务器

12 class udpServer
 13 {
 14   private:
 15    // std::string ip;
 16     int port;
 17     int sock;
 18   public:
 19     udpServer(int _port = 8088)
 20       //:ip(_ip)
 21       :port(_port)
 22   {}
 23     void initServer()
 24     {
 25        sock = socket(AF_INET,SOCK_DGRAM,0);
 26        std::cout<<"sock:"<<sock<<std::endl;
 27        struct sockaddr_in local;
 28        local.sin_family = AF_INET;
 29        local.sin_port = htons(port);//主机序列转成网络序列
 30        //local.sin_addr.s_addr = inet_addr(ip.c_str());
 31        local.sin_addr.s_addr = INADDR_ANY;

将sin.addr设为INADDR_ANY;

运行服务器:
用命令:netstat -nlup查看udp协议相关的统计数据,一般用于检验本机各端口的网络连接情况


转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。

客户端和服务器端都加上命令行参数

服务器端:

  1 #include"udpServer.hpp"
  2 
  3 void Usage(std::string proc)
  4 {
  5   std::cout<<"Usage: "<<proc<<"port"<<std::endl;
  6 }
  7 int main(int argc,char* argv[])
  8 {
  9   if(argc != 2)
 10   {
 11     Usage(argv[0]);
 12     exit(1);
 13   }
 14   udpServer *us = new udpServer(atoi(argv[1]));                                                                                     
 15   us->initServer();
 16   us->start();
 17   delete us;
 18   return 0;
 19 }

客户端:

  1 #include"udpServer.hpp"
  2 
  3 void Usage(std::string proc)
  4 {
  5   std::cout<<"Usage: "<<proc<<"port"<<std::endl;
  6 }
  7 int main(int argc,char* argv[])
  8 {
  9   if(argc != 2)
 10   {
 11     Usage(argv[0]);
 12     exit(1);
 13   }
 14   udpServer *us = new udpServer(atoi(argv[1]));                                                                                     
 15   us->initServer();
 16   us->start();
 17   delete us;
 18   return 0;
 19 }

效果演示:


可以绑定127.0.0.1本地环回网,和ifconfig查看的ip都可以进行通信。

小结:

客户端不需要绑定。为什么呢?

  • 客户端自己bind容易冲突,如果有多个客户端进行bind,我们自己不知道哪个端口号被bind,肯定会有客户端启动失败。
  • 客户端需要唯一性,但是不需要明确是哪个端口号,所以客户端由操作系统自己选择绑定,因为系统知道哪个端口被bind,哪个端口没被bind。

本地环回:

通常用来进行网络通信代码的本地测试,一般跑通,本地环境以及代码基本没有问题。

也可以远程通信,代码链接,点击直达
有客户端代码就可以和博主远程通信了。

有关socket(套接字)实现udp通信的更多相关文章

  1. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  2. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  5. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  6. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  9. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  10. ruby - 是否可以在不实际发送或读取数据的情况下查明 ruby​​ 套接字是否处于 ESTABLISHED 或 CLOSE_WAIT 状态? - 2

    s=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)s.connect(Socket.pack_sockaddr_in('port','hostname'))ssl=OpenSSL::SSL::SSLSocket.new(s,sslcert)ssl.connect从这里开始,如果ssl连接和底层套接字仍然是ESTABLISHED,或者它是否在默认值7200之后进入CLOSE_WAIT,我想检查一个线程几秒钟甚至更糟的是在实际上不需要.write()或.read()的情况下关闭。是用select()、IO.select()还是其他方法完成

随机推荐