参考连接:https://www.nowcoder.com/study/live/504/2/16.
【Linux】网络编程一:网络结构模式、MAC/IP/端口、网络模型、协议及网络通信过程简单介绍
【Linux】网络编程二:socket简介、字节序、socket地址及地址转换API
【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写
文章目录
六, 网络通信
6.1 Socket介绍
Socket
,套接字,是对网络中不同主机上的应用程序之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,套接字提供了应用层程序利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用程序,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议进行交互的接口。
socket
可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑概念。它是网络环境中进程间通信的API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连的进程,通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的socket
中,该socket
通过与网卡(NIC)相连的传输介质将这段信息送到另外一台主机的socket
中,使对方能够接收到这段信息。
socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制。
在Linux环境下,socket用于表示进程间网络通信的特殊文件类型。本质是内核借助缓冲区形成的伪文件。既然是文件,就可以使用文件描述符引用套接字。与管道类似,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致,区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字通信分为两部分:
- 服务器端:客户端主动向服务器发送连接,服务器被动接受连接,服务器一般不会主动发送连接。
- 客户端:主动向服务器发起连接。
socket是一套通信的接口,Linux、Windows都有套接字socket,但有差别。
6.2 字节序
6.2.1 字节序简介
现代CPU的累加器一次能装载至少4字节(32位机),即一个整数。这个4个字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序的问题。
在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字)应该以什么样的顺序进行传递。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
字节序,就是字节的顺序,是大于一个字节类型的数据在内存中的存放顺序。
字节序分为大端字节序Big-Endian
和小端字节序Little-Endian
。
-
大端字节序是指一个整数的最高位字节(2331bit)存储在内存的低地址处。低位字节(07bit)存储在内存的高地址处;采用这种机制的处理器有IBM3700系列、PDP-10系列、Mortolora位处理器和绝大多数的RISC处理器。
-
小端字节序则是指整数的最高位字节存储在内存的高地址处,低位字节存储在内存的低地址处。采用这种机制的处理器有PDP-11、VAX、Intel系列位处理器和一些网络通信设备。
示例:存储0x1234ABCD到内存2000H开始的四个字节中
Big-Endian存储,从2000H开始,依次为12H 34H ABH CDH;
Little-Endian存储,从2000H开始,依次为CDH ABH 34H 12H;
地址 | 大端存储的数据 | 小端存储的数据 |
---|---|---|
2000H | 12H | CDH |
2001H | 34H | ABH |
2002H | ABH | 34H |
2003H | CDH | 12H |
大部分计算机采用小端字节序。
6.2.2 如何判断本机的字节序
通过代码检测主机的字节序:
/**
* @file byteorder.c
* @author Zoya (2314902703@qq.com)
* @brief 通过代码检测当前主机的字节序
* @version 0.1
* @date 2022-10-08
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
int main()
{
union
{
short value; // 2字节
char bytes[sizeof(short)]; // 2字节数组
} test;
test.value = 0x0102;
if (0x01 == test.bytes[0] && 0x02 == test.bytes[1])
{
printf("大端字节序\n");
}
else if (0x02 == test.bytes[0] && 0x01 == test.bytes[1])
{
printf("小端字节序\n");
}
else
{
printf("未知\n");
}
return 0;
}
运行程序,本机测试得到结果:
小端字节序
6.2.3 字节序转换函数
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释。解决的方式是:发送端总是把发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收的数据进行转换。
网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节序采用大端排序方式。
BSD Socket提供了封装好的转换接口,包括:
- 从主机字节序到网络字节序的转换函数:
htons
、htonl
; - 从网络字节序到主机字节序的转换函数:
ntohs
、ntohl
;
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 -> 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 -> 主机字节序
// 转换IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 -> 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络字节序 -> 主机字节序
使用示例:
/**
* @file transbyte.c
* @author zoya (2314902703@qqn.com)
* @brief
* @version 0.1
* @date 2022-10-09
*
* 网络通信时,发送端需要将主机字节序转换成网络字节序(大端)
* 另外一端收到数据后根据情况将网络字节序转换
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
// host to net prot
unsigned short a = 0x0102;
unsigned short b = htons(a); // host port to net port
printf("a : 0x%x\n", a);
printf("b : 0x%x\n", b);
printf("*******************************\n");
// host to net long ip
unsigned char buf[4] = {192, 168, 1, 100};
unsigned int num = *(int *)buf;
unsigned int sum = htonl(num);
unsigned char *p = (char *)∑
printf("%d %d %d %d \n", *p, *(p + 1), *(p + 2), *(p + 3));
printf("*******************************\n");
// net to host port
unsigned short netport = 0x0201;
unsigned short hostport = ntohs(netport);
printf("net port : 0x%x\n", netport);
printf("host port : 0x%x\n", hostport);
printf("*******************************\n");
// net to host ip
unsigned char netbuf[4] = {1, 1, 168, 192};
unsigned int netnum = *(int *)netbuf;
unsigned int hostnum = ntohl(netnum);
unsigned char *phostnum = (unsigned char *)&hostnum;
printf("%d %d %d %d\n",
*phostnum, *(phostnum + 1), *(phostnum + 2), *(phostnum + 3));
return 0;
}
运行结果:
a : 0x102
b : 0x201
*******************************
100 1 168 192
*******************************
net port : 0x201
host port : 0x102
*******************************
192 168 1 1
6.3 socket地址
socket地址是一个结构体,封装了端口号和ip等信息。
客户端要访问服务器,要知道服务器的ip和port。
socket地址分为:通用socket地址、专用socket地址。
6.3.1 通用socket地址
socket网络编程接口中表示socket地址的是结构体sockaddr
,其定义如下:
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
typedef unsigned short int sa_family_t;
参数sa_family
成员是地址族类型(sa_family_t
)的变量,地址族类型通常于协议族类型对应,常见的协议族(protocol family
,也称domain
)和对应的地址族如下所示,PF_*
和AF_*
可以混用:
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX |
AF_UNIX |
UNIX本地域协议族 |
PF_INET |
AF_INET |
TCP/IPv4协议族 |
PF_INET6 |
AF_INET6 |
TCP/IPv6协议族 |
参数sa_data
用于存放socket地址值,不同协议族的地址值具有不同的含义和长度:
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX |
sa_data 表示文件的路径名,长度可达到108字节 |
PF_INET |
sa_data 表示16bit端口号和32bit IPv4地址,共6字节 |
PF_INET6 |
sa_data 表示16bit端口号,32bit流表示,128bit IPv6地址,32bit范围ID,共26字节 |
14字节的sa_data不能容纳多数协议族的地址值,因此,Linux定义了新的通用的socket地址结构体socketaddr_storage
,该结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family; // 协议族
unsiged long int __ss_align; // 用来做内存对齐的
char __ss_padding[128 - sizeof(__ss_align)]; // 存储socket地址信息
}
typedef unsigned short int sa_family_t;
6.3.2 专用socket地址
很多网络编程函数诞生早于IPv4协议,当时使用的是struct sockaddr
结构体,为了向前兼容,现在的sockaddr退化成了(void*)的作用,传递一个地址给函数,至于这个函数是sockaddr_in
还是sockaddr_in6
,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
结构体类型 | 成员 | ||||
---|---|---|---|---|---|
struct sockaddr |
16位地址类型 | 14字节地址数据 | |||
struct sockaddr_in |
16位地址类型:AF_INET | 16位端口号 | 32位IP地址 | 8字节填充 | |
struct sockaddr_un |
16位地址类型:AF_UNIX/AF_LOCAL | 108字节路径名 | |||
struct sockaddr_in6 |
16位地址类型:AF_INET6 | 16位端口号 | 32位 flow label | 128位IP地址 | 32位 scope ID |
TCP/IP协议族有sockaddr_in
和sockaddr_in6
两个专用的socket地址结构体,它们分别用于IPv4和IPv6:
#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; // __SOCKADDR_COMMON(sin_),地址族协议
in_port_t sin_port; // port number 端口号,2字节
struct in_addr sin_addr; // internet address IP地址 4字节
unsigned char sin_zero[sizoef(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)]; // 填充部分
}
struct in_addr
{
in_addr_t s_addr;
}
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; // Transport layer port
uint32_t sin6_flowinfo; // IPv6 flow information
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // IPv6 scope-id
}
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SZIE (sizeof(unsigned short int))
所有专用socket
地址以及sockaddr_storage
类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr
(强制转换),因为所有的socket接口API使用的地址参数类型都是sockaddr
。
6.4 IP地址转换
IP地址转换就是将字符串ip转换为整数或者主机与网络字节序转换。通常用可读性好的字符串表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。
下面3个函数可用于点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); // 转换成int类型整数(网络字节序)
int inet_aton(cosnt char *cp,struct in_addr *inp); // address to network; cp:点分十进制字符串; inp: 保存转换后的IP(网络字节序); 返回值:1表示成功,0-表示非法
char *inet_ntoa(struct in_addr in); // 网络字节序的整数转换为字符串
下面是更新的函数也可以完成上面3个函数同样的功能,并且同时适用IPv4地址和IPv6地址,建议使用下面的两个函数。
#include <arpa/inet.h>
// p表示点分十进制的IP字符串,n表示网络字节序的整数
int inet_pton(int af,const char *src,void *dst); // 从点分十进制字符串类型IP 转换为 网络字节序地址,
// af是使用的地址协议族,AF_INET或AF_INET6;
// 返回值:1表示成功,0表示非法的参数,-1表示错误
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 将网络字节序的整数 转换为 点分十进制的字符串;
// src -> dst,结果保存在dst中;
// size:指定dst可用的字节数;
// 返回值:返回转换后的字符串,与dst是用一个值(dst必须设置为非空指针)
示例:
/**
* @file iptrans.c
* @author zoya (2314902703@qq.com)
* @brief IP地址转换函数
* @version 0.1
* @date 2022-10-09
*
* @copyright Copyright (c) 2022
*
* inet_pton() // 字符串 转换为 网络IP
* inet_ntop() // 网络IP 转换为 字符串
*/
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
// 将点分十进制的字符串 转换为 网络字节序的整数
char buf[] = "192.168.1.100";
unsigned int netnum = 0;
inet_pton(AF_INET, buf, &netnum);
printf("ip : %s \n", buf);
unsigned char *p = (unsigned char *)&netnum;
printf("netnum %d : %d %d %d %d\n", netnum, *p, *(p + 1), *(p + 2), *(p + 3));
// 将网络字节序的整数 转换 为点分十进制的字符串
netnum += 1;
char pbuf[16] = "";
const char *str = inet_ntop(AF_INET, (void *)&netnum, pbuf, 16);
printf("netnum %d : %d %d %d %d\n", netnum, *p, *(p + 1), *(p + 2), *(p + 3));
printf("ip : %s, %s\n", pbuf, str);
return 0;
}
运行结果:
ip : 192.168.1.100
netnum 1677830336 : 192 168 1 100
netnum 1677830337 : 193 168 1 100
ip : 193.168.1.100, 193.168.1.100
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/46062.html