Linux网络编程实战:从字节序到UDP协议栈的深度解析与开发指南
网路通信的三大要素:协议,端口和IP
知识点1【字节序】
多字节在主机中的存放数据
把多字节看成一个整体存储的顺序。
为什么我们在文件中没有这个概念呢?
因为文件是字节流(流指针),流是以一个字节为操作单位,并不是多字节
字节序分为两种:大端和小端
大端格式:高字节数据存放在低地址
小端格式:低字节数据存放在低地址
记忆方式:只看低地址,看是存储的高字节数据还是低字节数据
小(低)对小 高大上
大小端是系统决定的,我们不能更改。
Linux 都是小端的。
案例1:判断当前系统是大端还是小端
判断思路:判断低地址存放的是 低字节数据还是高字节数据
代码演示
代码运行结果:
数组是 不管是什么系统,第0个元素是低地址
知识点2【网络字节序和主机字节序】
1、如果计算机没有考虑字节序的问题,容易导致传输的数据错误
这种错误会现在异构计算机上:计算机的架构不同。小端架构和大端架构
小端主机和大端数据通信时,又有字节序的不同,导致数据错误,如下图
2、解决上述问题的方法
方法引入
我们各地都有各自的方言,为了让全国都能正常沟通,国家为此规定了普通话,将方言转换为普通话,就都能正常沟通了,普通话就是一个标准。
下图只是说明问题,并非实际过程
方法
网络 里的 数据必须是大端
因此上述的实际过程。
主机A(大端格式)传输数据,传到网络,发现是大端字节序,无需数据格式转换
主机B(小端格式)接受数据,知道网络传输的数据格式是大端格式,会进行类型格式的转换,转换为小端字节序
3、主机字节序和网络字节序
引入
那么问题又来了,主机不知道自己是什么字节序,那该如何判读是否需要数据转换呢?
因此 这里 引入和 网络字节序(net) 和主机字节序(host)
转换的过程,有需要引入两个函数
hton,ntoh:这两个函数都会进行 主机字节序 的 判断,根据判断结果决定需不需要转换。
总结
发送数据:需要将 主机字节序 转换为 网络字节序 hton
接收数据:需要将 网络字节序 转换为 主机字节序 ntoh
知识点2【字节序的转换函数】
1、主机字节序 转 网络字节序(发送)
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//转IP
uint16_t htons(uint16_t hostshort);//转端口号
函数介绍 只介绍htonl htons类似
-
函数介绍
功能:
将 32 位主机字节序数据转换成网络字节序数据
参数:
hostint32:待转换的 32 位主机字节序数据
返回值:
成功:返回网络字节序的值
重复
端口号无符号短整型
IP地址 32位
mac地址 48位
2、网络字节序 转 主机字节序(接收)
#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);//转ip
uint16_t ntohs(uint16_t netshort);//转端口号
-
函数介绍
功能:
将 32 位网络字节序数据转换成主机字节序数据
参数:
uint32_t: unsigned int
netint32:待转换的 32 位网络字节序数据
返回值:
成功:返回主机字节序的值
案例 htonl
代码演示
#include <stdio.h>
#include <arpa/inet.h> int main(int argc, char const *argv[])
{uint32_t num = 0x04030201;printf("num = %d\\n",num);printf("htonl(num) = %#x\\n",htonl(num));printf("htonl(num) = %d\\n",htonl(num));return 0;
}
代码运行结果
知识点3【地址转换函数】
这里的地址指的是IP地址
“10.9.11.5” 这种IP是 点分十进制串 (用户识别的IP地址)IPv4
“fe80::578a:738a:f506:f37a”这种IP是 冒分十六进制串 IPv6
但是:计算机 是用 32位无符号数据 存储的IP地址
大家判断一下 上面我写的这个字符串多少个字节
10个字节
那么 点分十进制串 最大的字节数是多少呢?
“192.168.100.101”
是16个字节(算’\0’)
因此我们以后 存储 点分十进制串 要用16个字节来存储
1、发送数据
需要将 点分十进制数串 转换为 32位无符号数
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
-
函数介绍
功能:
将点分十进制串 转换为 无符号数网络IP
参数:
af: (address family)
IPv4的点分十进制串→AF_INET
IPv6的冒分十六进制串→AF_INET6
src:输入参数,指向以空字符结尾的字符串,表示 IPv4 或 IPv6 地址。
点分十进制串 和 冒分十六进制串
dst: 输出参数,指向一个缓冲区,用于存储转换后的二进制数据。
返回值:
- 1:成功转换。
- 0:输入地址无效(例如,IPv4 字符串包含字母)。
- -1:发生错误(如
af
参数不合法),可通过errno
获取具体错误
代码演示
#include <stdio.h>
#include <arpa/inet.h>int main(int argc, char const *argv[])
{//实现IP 点分十进制串 向 32位IP地址(IPv4)的转换//我们假设IP地址为:10.11.1.5char *str_ip = "10.11.1.5";u_int32_t addr = 0;inet_pton(AF_INET,str_ip,&addr);printf("addr = %u\\n",addr);//现在我们按照字符遍历一下unsigned char *p = (unsigned char *)&addr;printf("%d.%d.%d.%d\\n",*p,*(p + 1),*(p + 2),*(p + 3));return 0;
}
代码运行结果
这里我自己实现了一个 pton的函数,大家看一下,有助于大家更好地理解pton
自己实现的pton
int my_pton(char *str_ip,int *addr)
{//先进性数据提取char ch_p[4];int int_p[4];sscanf(str_ip,"%d.%d.%d.%d",&int_p[0],&int_p[1],&int_p[2],&int_p[3]);//这里的数据高位(第一个%d) 存储到了&int_p[0]中,而数组下标小的在低地址,与我们想要的相反,因此我们下面要转换过来//将数据提取到字符数组当中for (size_t i = 0; i < 4; i++){if(int_p[i] > 255 || int_p[i] < 0){return 0;}ch_p[i] = (unsigned char)int_p[i];}*addr = ch_p[0] << 24 | ch_p[1] << 16 | ch_p[2] << 8 | ch_p[3];//这里不需要强转,因为会自动类型转换//现在的顺序对了,但是此时是小端存储,但是pton的结果 我们分析出 是大端存储,因此我们需要 转换为大端存储*addr = htonl(*addr);return 1;
}
代码运行结果
2、接收数据
将32为无符号数据 转换成 点分十进制数串
-
函数介绍
#include <arpa/inet.h> const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
功能:
将 二进制格式的 IP 地址 转换为 可读字符串
参数:
af:Address Family
src:输入参数,指向二进制格式的 IP 地址。
dst:输出参数,指向用于存储结果字符串的缓冲区。
size:缓冲区
dst
的字节长度。需足够大以防止溢出。返回值:
- 成功:返回指向
dst
的指针(即转换后的字符串地址)。 - 失败:返回
NULL
,并设置errno
表示具体错误。
- 成功:返回指向
知识点4【UDP编程概述】
1、socket
网络通信要解决的问题 是不同主机进程间的通信
三要素
1、IP地址
2、PORT
3、协议
socket:网络程序编程开发接口的统称
socket作用
提供不同主机上的进程之间的通信
socket特点
1、socket也称”套接字“
2、是一个特殊的文件描述符(套接字),代表一个通信管道的一个端点
3、类似对文件的操作一样,可以使用read,write,close等函数对socket套接字进行网络数据的读取和发送等操作
4、得到套接字(描述符)的方法调用socket()
5、socket可读可写,且有端口和IP
注意
不要直接成socket是文件描述符,它是一种特殊的文件描述符,它有专属名称是套接字
2、UDP的编程流
bind 绑定函数,服务器是被动的,服务器给用户一个固定的端口,而这个固定的端口的提供 需要bind绑定函数来实现,可以通过端口找到该服务器。一旦绑定了这个端口,这个端口号其他进程就不可以再使用。
绑定 客户端也是可以有的,一旦绑定,发送消息,其他客户端就知道 是本端口发送的,其他客户端也可以通过这个端口找到该客户端
客户端:创建套接字,收,发,关闭套接字
服务器:创建,绑定,收,发,关闭(图中没有,记得加上)
关闭防火墙交流
背一下:
hton:将 主机字节序 转换为 网络字节序
ntoh:将 网络字节序 转换为 主机字节序
pton:字符串转换 为 大端的二进制数据
ntop:大端二进制数据 转换为 字符串
3、socket函数创建一个套接字(通信的端点)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int family,int type,int protocol);
功能:
创建一个用于网络通信的 socket 套接字(描述符)
参数:
family:协议族(AF_INET、AF_INET6、PF_PACKET 等)
type:套接字类(SOCK_STREAM、SOCK_DGRAM、SOCK_RAW 等)
protocol:协议类别(0、IPPROTO_TCP、IPPROTO_UDP 等
返回值:
套接字
特点:
创建套接字时,系统不会分配端口
代码演示
代码运行结果
4、IPv4地址结构体
**作用:**定义 地址结构体变量
#include <netinet/in.h>
struct in_addr
{in_addr_t s_addr;//4 字节
};struct sockaddr_in//IPv4地址 结构体
{sa_family_t sin_family;//2 字节in_port_t sin_port;//2 字节struct in_addr sin_addr;//4 字节char sin_zero[8]//8 字节
};//IPv6地址结构体 :sockaddr_in6
sin_zero 全部需要补零。作用是将结构体补充成16个字节
我们定义结构体后,建议先把结构体清零
5、通用地址结构体
对传递给函数的参数进行类型转换 不做赋值操作
struct sockaddr
{sa_family_t sa_family; // 2 字节char sa_data[14] //14 字节
};
这里解释一下,为什么要这个结构体,由family可以看出,我们可以选择IPv4 也可以选择IPv6,IPv4和IPv6是两种协议,我们在设计sendto函数的时候,由于它们所需的地址结构体是不同的,但是我们又想用同一个sendto函数。
我们就定义了一个通用地址结构体,通用结构体是16个字节,它只有前两个字节有效也就是family成员,代码中会对family就行判断,确定是IPv4 还是IPv6,然后函数内部进行结构体类型强转。就可以实现一个sendto函数完成IPv4和IPv6的通用。
综上在这里 通用地址结构体的作用类似于:**void ***
大家可以在下面sendto介绍中看一下,to 的类型是什么,还有recvform中from的的类型是什么。
6、sendto发送UDP消息
TCP消息是用send发送的。
套接字,内容,大小,0,目标,大小
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
功能:
向 to 结构体指针中指定的 ip,发送 UDP 数据
参数:
sockfd:套接字
buf:发送数据缓冲区
nbytes: 发送数据缓冲区的大小
flags:一般为 0
行为标志,网络数据比较复杂,紧急指针等需要特殊处理,这里我们写0就行
to:指向目的主机地址结构体的指针
addrlen:to 所指向内容的长度
返回值:
成功:发送数据的字符数
失败: -1
注意:
通过 to 和 addrlen 确定目的地址
可以发送 0 长度的 UDP 数据包
代码演示
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, char const *argv[])
{//创建一个 套接字int sockfd = 0;sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){perror("socket");return 0;}printf("sockfd = %d\\n",sockfd);//遍历一下sockfd//sendto //输入数据printf("请输入数据:\\n");char buf[64] = "";fgets(buf,sizeof(buf),stdin);buf[strlen(buf) - 1] = 0;//地址结构体创建struct sockaddr_in addr;memset(&addr,0,sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(8080);inet_pton(AF_INET,"192.168.136.1",&addr.sin_addr.s_addr);//sendto 发送数据int ret = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)(&addr),sizeof(addr));if(ret < 0){perror("sendto");return 0;}return 0;
}
代码运行结果
在上面运行结果我们看到,发送端口的端口号是随机的
这个端口是如何来的呢?
是又第一次sendto随机分配的,记住只有第一次send 才会分配,比如我们在程序中多次发送数据,在第一次sendto之后,端口号将不改变,我们将上面的代码加上循环
可以看到端口号没有改变。但是 结束该进程,开启一个新进程后,端口号会改变
但实际使用过程中,总不能端口号一直在改变,因此我们下面需要学习一下bind()函数设置固定的端口
7、bind给套接字绑定固定的 IP和端口
绑定的位置应在 socket()之后,在sendto()/recvfrom()之前
绑定函数只能绑定 本地机的IP和端口
int bind(int sockfd, const struct sockaddr *myaddr,socklen_t addrlen);
功能:
将本地协议地址 与 sockfd 绑定
参数:
sockfd: socket 套接字
myaddr: 指向特定协议的地址结构指针
addrlen:该地址结构的长度
返回值:
成功:返回 0
失败:其他
代码演示
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{//创建socketint sockfd = 0;sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){perror("socket");return 0;}printf("sockfd = %d\\n",sockfd);//固定端口 bind 套接字 地址结构体 大小struct sockaddr_in my_addr;memset(&my_addr,0,sizeof(my_addr));my_addr.sin_family = AF_INET;my_addr.sin_port = htons(8000);my_addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY==0,通配地址,让协议栈取本地机去找IPint ret = bind(sockfd,(struct sockaddr *)&my_addr,sizeof(my_addr));if(ret != 0){perror("bind");return 0;}//发送消息 套接字 大小 flags 地址结构体 大小//键盘获取输入char buf[64] = "";fgets(buf,sizeof(buf),stdin);buf[strlen(buf)-1] = 0;//地址结构体的配置struct sockaddr_in addr;memset(&addr,0,sizeof(addr));addr.sin_family = AF_INET;addr.sin_port = htons(7000);inet_pton(AF_INET,"192.168.136.1",&addr.sin_addr.s_addr);sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&addr,sizeof(addr));//关闭套接字描述符close(sockfd);return 0;
}
代码运行结果
可以看到即使是两个进程,端口号也是不变的
8、recvfrom 接收数据(带阻塞)
如果负责收数据,记得绑定固定的端口及IP
ssize_t recvfrom(int sockfd, void *buf,size_t nbytes,int flags,struct sockaddr *from,socklen_t *addrlen);
功能:
接收 UDP 数据,并将源地址信息保存在 from 指向的结构中
参数:
sockfd:套接字
buf:接收数据缓冲区
nbytes:接收数据缓冲区的大小
flags:套接字标志(常为 0)
from:源地址结构体指针,用来保存数据的来源**(可以为NULL)**
addrlen: from 的长度**(可以是NULL)**
和送礼一样,谁送的不重要,重要的是礼物
注意:
通过 from 和 addrlen 参数存放数据来源信息
from 和 addrlen 可以为 NULL, 表示不保存数据来源
返回值:
成功:接收到的字符数
失败: -1
注意:
这个返回值 我们可以利用返回值 判断数据是否收完。
接收到的数据如果小于收到的内容nybte,则表示收完了,停止接收数据。
有可能接收图片(RGB),因此我们存的话需要用unsigned char接收。
又由于以太网最大传输单元(MTU)是1500个,因此接受数组的个数1500即可
代码演示
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>int main(int argc, char const *argv[])
{//创建一个 socketint sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){perror("socket");return 0;}printf("sockfd = %d\n",sockfd);//bind 固定端口于IP 必须要进行的操作 fd,地址结构体,大小struct sockaddr_in my_addr;memset(&my_addr,0,sizeof(my_addr));my_addr.sin_family = AF_INET;my_addr.sin_port = htons(6000);my_addr.sin_addr.s_addr = htonl(INADDR_ANY);int ret = bind(sockfd,(struct sockaddr *)&my_addr,sizeof(my_addr));if(ret != 0){perror("bind");return 0;} //接收数据while(1){unsigned char buf[1500] = "";struct sockaddr_in addr;memset(&addr,0,sizeof(addr));int addr_len = sizeof(addr);int ret = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&addr,&addr_len);if(ret < 0){perror("recvfrom");return 0;}//数据存储在buf中,数据的长度为ret//IP来源于addr.sin_addr.s_addr,需要处理//端口号为addr.sin_port,需要进行字节序转换//IP处理char str_add[16] = "";inet_ntop(AF_INET,&addr.sin_addr.s_addr,str_add,sizeof(str_add));//端口字节序转换int port = ntohs(addr.sin_port);//遍历一下printf("从IP:%s 端口号:%hu 接收到的数据为:%s,数据长度为%d\n",str_add,port,buf,ret);}//关闭 套接字描述符close(sockfd);return 0;
}
代码运行结果
代码书写过程中的错误
在printf的时候 没有\n,这里和同学一起看了很久,最后在持续发送中,发现只有当缓冲区满的时候,才会输出到终端,才发现这个问题。
这里回忆一下这个知识点:
缓冲区的刷新方式
1、强制刷新
2、行刷新
3、满刷新
4、关闭刷新
重点内容:
1、端口号输出的时候,需要将网络字节序 转换为 主机字节序输出。
2、并且IP也是需要转换为字符串输出的
结束
代码重在练习!
代码重在练习!
代码重在练习!
今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!