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等网络字节序转换,强调代码封装与关键步骤实现。

相关推荐
AlfredZhao4 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户97183563346610 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪12 小时前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux
网络研究院1 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智1 天前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest1 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
不会C语言的男孩1 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言