参考连接: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 如何判断本机的字节序
通过代码检测主机的字节序:
#include <stdio.h>
int main()
{
union
{
short value;
char bytes[sizeof(short)];
} 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);
uint32_t htonl(uint32_t hostlong);
uint32_t ntohl(uint32_t netlong);
使用示例:
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
unsigned short a = 0x0102;
unsigned short b = htons(a);
printf("a : 0x%x\n", a);
printf("b : 0x%x\n", b);
printf("*******************************\n");
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");
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");
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)];
}
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;
in_port_t sin_port;
struct in_addr sin_addr;
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;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_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 inet_aton(cosnt char *cp,struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
下面是更新的函数也可以完成上面3个函数同样的功能,并且同时适用IPv4地址和IPv6地址,建议使用下面的两个函数。
#include <arpa/inet.h>
int inet_pton(int af,const char *src,void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
示例:
#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
|