Linux网络:使用UDP实现网络通信(网络套接字的创建&&绑定)

文章目录

      • [1. UDP网络程序的服务端](#1. UDP网络程序的服务端)
        • [1.1 封装一个UdpServer类](#1.1 封装一个UdpServer类)
        • [1.2 获取socket套接字](#1.2 获取socket套接字)
        • [1.3 绑定套接字](#1.3 绑定套接字)
  • 序:在上一章中,先介绍网络基础,然后阐述以太网通信原理、局域网通信的数据封装分用及跨网络靠路由器通信,指出IP和Mac地址作用。接着聚焦网络套接字,说明端口号用于标识主机网络进程,而本章将从UDP协议的角度去理解网络通信的原理。

传输层提供的协议有TCP和UDP两种,提供数据收发的服务,TCP叫做传输控制协议,有链接(确保通信信道的可靠性,收发两端能正常交流),有可靠性(传输层),只有成功建立链接才能进行通信,面向字节流,UDP叫做用户数据报协议,无连接(不需要确保通信信道的可靠性,直接发),不可靠性,面向数据报(发出去的数据有明显的边界,意思就是要发数据就发完整的数据,要么就不发)
其中的udp的不可靠性,并不意味着就是不好了,这是一个中性词,不可靠意味着简单,方便,而要维持可靠性,就必然需要花费一定的成本和资源,所以可靠性的负担也是要被考虑到的地方,所以对于选择tcp还是udp进行通信,还是要因地制宜!!!

H代表主机,n表示网络,l表示long四字节,s表示short两字节
第一个是32位的,主机转网络序列。
第二个是16位的,主机转网络序列。
第三个是32位的,网络转主机序列。
第四个是16位的,网络转主机序列。

网络套接字

套接字编程的种类:

  1. 域间套接字编程--->同一个机器内
  2. 原始套接字编程--->网络工具(抓包... )
  3. 网络套接字编程--->用户间的网络通信

想将三个网络接口统一抽象化--->参数的类型必须是统一的

1. UDP网络程序的服务端

1.1 封装一个UdpServer类

要想启动服务端,服务端至少要一个构造,一个析构,一个初始化和一个运行的接口!!!

复制代码
class UdpServer{
public:
    UdpServer()
    {}

    void Init()
    {}

    void Run(func_t func)
    {}

    ~UdpServer()
    {}
private:

};
1.2 获取socket套接字

想要使用UDP套接字,就必须先获取一个套接字,要用到下面的一个函数来获取套接字

复制代码
需要包含下面两个库
#include<sys/types.h>
#include<sys/socket.h>

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

该函数的第一个参数表示创建的套接字的域(IPv4还是IPv6,在udp场景中使用AF_INET或者PF_INET选项(IPv4)),不同场景要创建不同种类的套接字

第二个参数表示当前socket对应的类型,例如SOCK_STREM表示的就是流式套接字,SOCK_DGRAM表示用户数据报套接字,显然,之前提到的UDP是面向用户数据报的,TCP是面向字节流的,对于不同的场景填充不同的套接字类型,所以在UDP的场景下我们选择SOCK_DGRAM的套接字类型

第三个参数表示协议类型,当前默认不用考虑,直接填0
返回值RETURN_VALUE,如果成功了套接字被返回,失败了-1被返回,错误码被设置!这说明该返回值就是一个文件,所以打开一个套接字就是打开一个文件(一切皆文件),类比文件描述符,要想进行网络上的通信就必须经过"网络文件描述符"

复制代码
class UdpServer{
public:
    void Init()
    {
	    //1.创建udp socket
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        //用的网络协议IPv4,SOCK_DGRAM数据报(无连接,不稳定)
        //因为#define AF_INET PF_INET,所以用PF_INET也是一样的

        if(_sockfd < 0) 
        {
	        //使用日志系统对错误信息进行打印
            log(Fatal,"socket create error, _sockfd: %d", _sockfd);
            exit(SOCKET_ERR);
        }
        log(Info,"socket create success, _sockfd: %d", _sockfd);
    }
private:
	//添加私有成员
	int _sockfd; //网络文件描述符
};
1.3 绑定套接字

绑定套接字要用到bind函数:

第一个参数:要绑定的套接字(sockfd)
第二个参数:一个结构体,该结构体是本文一开始讲到的通用接口,所有的套接字接口都是struct sockaddr,其参数传递的时候都是struct sockaddr * ,但是我们现在使用的是网络通信,所以我们使用的是struct sockaddr_in这样的结构,要传参数的时候直接强转就行了。

第三个参数:第二个参数的结构体的长度

在绑定的时候,必须要知道当前服务的端口号和IP地址,IP确定唯一的一台主机,端口号确定该主机上对应的一个服务

复制代码
uint16_t defaultport = 5070;
std::string defaultip = "0.0.0.0";

class UdpServer{
public:
	UdpServer(const uint16_t &port = defaultport,const std::string &ip = defaultip)
    :_port(port)
    ,_ip(ip)
    {}
    
private:
	//添加私有成员
	std::string _ip; //IP地址, 0.0.0.0 任意地址绑定
    uint16_t _port;  //表示服务器进程的端口号
};

虽然bind的第二个参数要传的是struct sockaddr,但我们实际要用的是struct sockaddr_in,但是又由于网络接口统一抽象化,所以只需要将struct sockaddr_in填充完后再强转就行了。

复制代码
#include<netinet/in.h> //要使用该结构体所要包含的头文件
#include<arpa/inet.h>  //主机网络序列转换所需要用到的头文件

初始化struct sockaddr_in结构体

struct sockaddr_in结构体的各个参数介绍:

其中的sin_zero表示这个结构体的填充字段,没啥用,就是占位符。其中的sin,s表示socket,in表示inet。

sin_addr表示IP地址,但是,如果直接将服务器的IP地址传给sin_addr的话,是不行的,因为该参数还是一个结构体,这个结构体里面的in_addr_t才是能传递参数的网络序列,所以要想将IP地址传入进去,第一步就是要将string类型的IP地址转化为uint32_t的类型,第二步是将uint32_t转化为网络序列

问题一:如何将一个字符串的IP地址转化为一个整数?又怎么转换回来?

使用inet_addr函数,可以直接将字符串转化为4字节整数,并且转化为网络序列。
sin_port表示该服务器的端口号,该参数的类型是in_port_t,而该类型就是uint16_t,但是到这一步还是不可以直接将服务器的端口号传进去,需要注意的是,端口号需要保证是网络字节序列,因为端口号是要给对方发送的,所以还需要将主机序列转化为网络序列,使用函数htons

sin_family表示要表明自己的结构体所对应的类型,也就是套接字的域,因为我们使用的UDP的套接字的域是IPv4,所以这里直接把AF_INET传过去就行

问题二:但是我们并没有在struct sockaddr_in的里面看到famliy啊?


我们可以看到该结构体中有一个像这样的宏,传一个_sin进去。

**该宏定义是意思就是用sa_fanily_t sa_prefix##family代替前面__SOCKADDR_COMMON(sin_ ) **

问题三:其中的"##"是什么意思?

可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称为记号粘合
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
这里我们想想,写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。

比如

复制代码
int int_max(int x, int y)
{
	return x>y?x:y;
}

float float_max(float x, float y)
{
	return x>yx:y;
}

但是这样写起来太繁琐了,现在我们这样写代码试试:

复制代码
//宏定义
#define GENERIC_MAX(type)        \
type type##_max(type x, type y)  \
{                                \
	return (x>y?x:y);            \
}

所以这块宏定义就是在sin_后面加了一个family变成了sin_family来表示该参数,这也就是sin_family的由来

复制代码
class UdpServer{
public:
    void Init()
    {
	    //1.创建udp socket
        //...
        //2.绑定bind socket
        //2.1 准备数据
        struct sockaddr_in local;
        //memset(&local,0,sizeof(local));
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        //local.sin_addr.s_addr = inet_addr(_ip.c_str());
        //1.string -> uint32_t 2. uint32_t必须是网络序列的
        //如果IP要被网络使用,IP也必须是网络序列的
        local.sin_addr.s_addr=INADDR_ANY;//(INADDR_ANY就是000000000全零)
        
        //2.2 开始绑定 
        if(bind(_sockfd, (const struct sockaddr*)&local, sizeof(local)) < 0)
        {
	        log(Fatal,"bind error,errno: %d ,strerror: %s", 
	        errno, strerror(errno));
	        exit(BIND_ERR);
        }

        log(Info,"bind success, _sockfd: %d",_sockfd);
    }
private:
	int _sockfd; //网络文件描述符
	std::string _ip; //IP地址, 0.0.0.0 任意地址绑定
    uint16_t _port;  //表示服务器进程的端口号
};

总结:

本文主要介绍了传输层的TCP与UDP协议特点:TCP可靠、面向连接、字节流;UDP不可靠、无连接、面向数据报。重点讲解UDP服务端编程:创建socket(AF_INET/SOCK_DGRAM)、绑定IP和端口(使用struct sockaddr_in,调用bind),涉及htons、inet_addr等网络字节序转换,强调代码封装与关键步骤实现。

相关推荐
weixin_749949903 小时前
当没办法实现从win复制东西到Linux虚拟机时的解决办法
linux·运维·服务器
时空自由民.3 小时前
SC3336 rgb sensor linux
linux·运维·服务器
東雪蓮☆3 小时前
Ansible 自动化运维:集中化管理服务器实战指南
linux·运维·自动化·ansible
Cyan_RA93 小时前
Linux 虚拟机软件 VMware Workstation Pro 安装CentOS的相关说明和操作
linux·运维·服务器·centos·vmware·vmtools
半桔3 小时前
【网络编程】UDP 编程实战:从套接字到聊天室多场景项目构建
linux·网络·c++·网络协议·udp
草莓熊Lotso3 小时前
《算法闯关指南:优选算法--滑动窗口》--14找到字符串中所有字母异位词
java·linux·开发语言·c++·算法·java-ee
IT成长日记4 小时前
【LVS入门宝典】LVS NAT模式实战指南:ip_forward、iptables与SNAT、DNAT规则配置详解
linux·运维·tcp/ip·负载均衡·lvs·nat
---学无止境---4 小时前
九、内核数据结构之list
linux·数据结构·list
error:(4 小时前
【Linux命令从入门到精通系列指南】apt 命令详解:Debian/Ubuntu 系统包管理的现代利器
linux·ubuntu·debian