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