描述一个网络中各个协议层的常用方法是OSI模型,下图给出它与网际协议族的近似映射
OSI模型的底下两层是随系统提供的设备驱动程序和网络硬件
网络层由IPv4和IPv6这两个协议处理
可以选择的传输层有TCP和UDP。如上图,TCP和UDP之间留有间隙,表明网络应用可以绕过传输层直接使用IPv4或IPv6
OSI模型的顶上三层被合并为一层,称为应用层。这就是Web客户(浏览器)、Telnet客户、Web服务器、FTP服务器和其他我们在使用的网络应用所在的层。对于网际协议,OSI模型的顶上三层协议几乎没有区别
TCP/IP通常被认为是一个四层协议系统,每一层负责不同的功能:
TCP(传输控制协议)
和``UDP(用户数据报协议)```当应用程序用TCP传送数据是时,数据被送入协议栈
中,然后逐个通过每一层直到被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些首部信息(有时还要增加尾部信息),该过程如下图:
TCP传给IP的数据单元称为TCP报文段或TCP段(TCP segment)
。IP传给网络接口层的数据单元称为IP数据报(IP datagram)
。通过以太网传输的比特流称作网帧(Frame)
上图,网帧头和网帧尾下面标注的数字是典型以太网帧首部的字节长度
以太网数据帧的物理特性是其长度必须在46~1500字节之间
更准确地说,上图中IP和网络接口层之间传送的数据单元应该时分组(packet)。分组既可以是一个IP数据报,也可以是IP数据报的一个片(fragment)。
UDP数据与TCP数据基本一致。唯一的不同是UDP传给IP的信息单元称作UDP数据报(UDP datagram),而且UDP的首部长度为8字节
由于TCP、UDP、ICMP和IGMP都要向IP传送数据,因此IP必须在生成的IP首部中存入一个长度为8bit的数值,称作协议域
。1表示为ICMP协议,2表示为IGMP协议,6表示为TCP协议,17表示为UDP协议
许多应用程序都可以使用TCP或UDP来传送数据。运输层协议在生成报文首部时要存入一个应用程序的标识符。TCP和UDP都用一个16bit端口号来表示不同的应用程序。TCP和UDP把原端口号和目的端口号分别存入报文首部中
网络接口分别要发送和接收IP、ARP和RARP数据,一次也必须在以太网的网帧首部中加入某种形式的标识,以指明生成数据的网络层协议。一次,以太网的网帧首部也有一个16bit的帧类型域
以太网帧采用48bit(6字节)的目的地址和源地址。这是硬件地址(MAC地址)
ARP和RARP协议对32bit(4字节)的IP地址和48bit(6字节)的MAC地址进行映射
以太网的2bit类型字段定义了后续数据的类型
类型字段后就是数据
4bitCRC字段用于帧内后续字节擦错的循环冗余码检验(检验和
)(它也被称为FCS或帧检验序列)
当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议盒都要去检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称为分用(Demultiplexing)
,该过程如下图:
TCP和UDP采用16bit的端口号来识别应用程序
服务器一般都是通过知名端口号
来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP服务器的UDP端口号都是69。任何TCP/IP实现锁提供的服务都用知名的1~1023之间的端口号。
客户端通常对它所使用的端口号并不关心,只需保证该端口号在本机上是唯一的就可以了。客户端口号又称为临时端口号(即,存在时间很短暂)。这是因为它通常只是在用户运行该客户程序时才存在,而服务器则要主机开着,其服务就运行
大多数TCP/IP实现给临时端口分配1024~5000之间的端口号。大于5000的端口号为其他服务器预留的(Internet上并不常用的服务)
IP数据报的格式如下图。普通的IP首部长为20个字节,除非含有选项字段
上图IP首部中,最高位在左边,记为0bit,最低位在右边,记为31bit
4个字节的32bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端(big endian)字节序
。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。其他形式存储二进制整数的机器,如小端(little endian)格式,则必须在传输数据之前把首部转换成网络字节序
4位版本:目前的协议版本号是4,因此IP又是也称为IPv4
4位首部长度:首部长度指的是首部占32bit字的数目(4个字节的段的数目)。由于它是一个4bit字段,因此首部最长位60个字节(60B = 15(4位能表示的最大数) * 4B)。普通IP数据报(没有任何选择项)字段的值是5(即,首部长度 :5 * 4B = 20B)
8位服务类型(TOS):服务类型(TOS)字段包括一个3bit的优先权子字段(现在已被忽略),4bit的TOS子字段和1bit未用位但必须置0。4bit的TOS分别代表:最小时延、最大吞吐量、最高可靠性和最小费用。4bit中只能置其中1bit。如果所有4bit均为0,那么就意味着一般服务
16位总长度(字节数):总长度字段是指整个IP数据报的长度,以字节为单位。利用首部长度字段和总长度字段,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长16bit,所以IP数据报最长可达65535字节
16位标识:标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1
8位生存时间(TTL):TTL(time to live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为32或64),一旦经过一个处理它的路由器,它的值就减去1。当该字段的值为0时,数据报就被丢弃,并发送ICMP报文通知源主机
8位协议:TCP、UDP、ICMP和IGMP都要向IP传送数据,因此IP必须在生成的IP首部中加入协议域,以表明数据属于哪一层。在8位协议中:1表示ICMP协议,2表示IGMP协议,6表示TCP协议,17表示UDP协议
16位首部检验和:首部检验和字段是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。
16位源端、目的端端口号:每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TC连接
有时,一个IP地址和一个端口号称为一个插口(socket)。插口对(socket pair)可唯一确定互联网络中每个TCP连接的双方
32位序号:序号用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,择TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达2^32 - 1后又从0开始
4位首部长度:首部长度给出首部中32bit字的数目(即,4B数据段的数目)。需要这个值是因为任选字段的长度是可变的。这个字段占4bit,因此TCP最多有60B的首部。当咩有任选字段,正常的长度是20B
保留6位:在TCP首部中有6个标志bit。他们中的多个课同时被设置为1,一下是6个标志的简介:
URG - 紧急指针有效
ACK - 确认序号有效
PSH - 接收方应该尽快将这个报文段交给应用层
RST - 重键连接
SYN - 同步序号用来发起一个连接
FIN - 发端完成发送任务
16位窗口大小:TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端期望接收的字节。窗口大小是一个16bit字段,因此窗口大小最大为65535字节
网络中常用的设备:
Hub集线器 - 将电信号放大、分流,属于物理层交换
交换机 - 交换网帧,属于链路层交换
路由器 - 交换IP包,属于网络层交换
IPv4地址长32bit。Internet地址并不采用平面形式的地址空间。IP地址具有一定的结构,五类不同的互联网地址格式如下:
网络号 - 决定主机所属的网络
主机号 - 决定主机在网络中的编号
这些32bit的地址通常写成四个十进制的数,其中每个整数对应一个字节。这种表示方法称作"点分十进制表示法"。各类IPv4地址的范围:
有三类IP地址:
子网掩码:用于找出IP地址中的网络号
子网掩码书写形式:192.168.1.130/25
等价于192.168.1.130/255.255.255.128
IP地址与子网掩码作与操作,就可获取IP地址的网络号
参考一
参考二
参考
arp -a:查看ARP表
route(ubuntu):查看路由表
netstat - nr(mac os):查看路由表
ping 目标IP地址/网址:查看本机与目标地址/网址的连接情况
ifconfig:查看内网IP
curl ifconfig.me:查看外网IP
建立一个TCP连接时会发生下述情形
TCP建立一个连接需要3个分节,终止一个连续则需要4个分节:
socket - create an endpoint for communication
指定期望的通行协议类型(例如,使用IPv4的TCP、使用IPv6的UDP...)
所需头文件
#include /* See NOTES */
#include
函数原型
int socket(int domain, int type, int protocol);
参数
domain - 指定网络层协议族
AF_INET:使用IPv4协议
AF_INET6:使用IPv6协议
type - 指定套接字类型
SOCK_STREAM:使用字节流套接字(即,适用于TCP)
SOCK_DGRAM:使用数据报套接字(即,适用于UDP使用)
protocol - 指定传输层与套接字一起使用的协议
IPPROTO_CP:使用TCP传输协议
IPPROTO_UDP:使用UDP传输协议
IPPROTO_SCTP:使用SCTP传输协议
返回值
成功,返回新的套接字描述符
失败,返回 -1,errno被设置
bind - bind a name to a socket
将通讯描述符与服务器地址空间绑定
把一个本地协议地址赋予一个套接字。对于国际网协议,协议地址是32位的IPv4地址或是128位的IPv6地址与16位的TCP或者UDP端口号的组合
所需头文件
#include /* See NOTES */
#include
函数原型
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数
sockfd - socket(2)的返回值
addr - 指定一个指向特定于协议的地址结构的指针,地址结构定义如下:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
sa_family可选值:
AF_INET - IPv4
AF_INET6 - IPv6
addrlen - addr指定的地址结构的长度
返回值
成功,返回 0
失败,返回 -1
listen - listen for connections on a socket
所需头文件
#include /* See NOTES */
#include
函数原型
int listen(int sockfd, int backlog);
参数
sockfd - socket(2)返回值
backlog - 最大的未决连接数
返回值
成功,返回 0
失败,返回 -1,errno被设置
connect - initiate a connection on a socket
所需头文件
#include /* See NOTES */
#include
函数原型
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数
sockfd - socket(2)的返回值
addr - 服务器的地址空间
addrlen - addr指向地址空间大小
返回值
成功,返回 0
失败,返回 -1,errno被返回
accept - accept a connection on a socket
所需头文件
#include /* See NOTES */
#include
函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
sockdf - socket(2)返回值
addr - 客户端的地址空间填充addr指定的空间里
如果addr指定为NULL,那么addrlen也需要指定为NULL
addrlen - 客户端地址空间的尺寸
返回值
成功,返回连接描述符(使用这个连接描述符和客户端进行通讯)
失败,返回 -1,errno被设置
htonl, htons, ntohl, ntohs - convert values between host and network byte order
主机字节序与网络字节序相互转化
所需头文件
#include
函数原型
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
参数:
hostlong - 32位主机字节序数据
hostshort - 16位主机字节序数据
netlong - 32位网络字节序数据
netshort - 16位网络字节序数据
返回值
返回目标字节序的数据
netinet/in.h - Internet Protocol family
定义互联网协议族
所需头文件
#include
描述:
当<netinet/in.h>被包括,下面的类型是通过typedef定义的:
in_port_t - 用于定义一个精确为16位的无符号整数类型(定义端口号)
in_addr_t - 用于定义一个精确为32位的无符号整数类型(定义IP地址)
<netinet/in.h>头文件定义了in_addr结构体数据类型,其中至少包含以下成员:
struct in_addr{
in_addr_t s_addr;
};
<netinet/in.h>定义了sockaddr_in结构体数据类型,其中至少包含以下成员:
struct sockaddr_in{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
sockaddr_in结构体数据类型用于存储Internet协议族的地址。此类型的值必须转换为struct sockaddr结构体数据类型,以便与本文档中定义的套接字接口一起使用
<netinet/in.h>定义了sa_family_t数据类型,并在<sys/socket.h>中对其进行描述
<netinet/in.h>定义了以下宏作为getsockopt()和setsockopt()的level参数的值:
IPPROTO_IP - 虚拟IP
IPPROTO_ICMP - Control message protocol
IPPROTO_TCP - TCP
IPPROTO_UDP - UDP
<netinet/in.h>定义了以下宏作为connect()、sendmsg()和sendto()的目标地址:
INADDR_ANY - 主机地址
INADDR_BROADCAST - 广播地址
inet_pton - convert IPv4 and IPv6 addresses from text to binary form
该函数将字符串src转换为af地址族中的网络地址结构,然后将网络地址结构复制到dst。af参数必须是AF_INET或AF_INET6。dst是按网络字节顺序写的
所需头文件
#include
函数原型
int inet_pton(int af, const char *src, void *dst);
参数
af - 指定地址协议族类型
AF_INET - IPv4
AF_INET6 - IPv6
src - src指向一个字符串,该字符串包含一个点十进制格式的IPv4网络地址,“ddd.ddd.ddd.ddd”,其中ddd是一个范围为0到255的三位数。地址被转换为struct in_addr类型并复制到dst,它必须是sizeof(struct in_addr)字节长
dst - 指向已转换为网络字节序的IP地址(以struct in_addr类型数据表示)的指针
返回值
返回 -1,表示af参数没有指定有效的地址协议族,errno被设置
返回 0,表示src参数表示的地址不符合af指定的地址协议族规定
返回 1,表示成功
inet_ntop - convert IPv4 and IPv6 addresses from binary to text form
所需头文件
#include
函数原型
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
此函数将af地址家族中的网络地址结构src转换为字符串。转换后的字符串被复制到dst指向的缓冲区中,该缓冲区必须是非空指针。调用者在参数size中指定此缓冲区中可用的字节数
参数
af - 指定地址协议族类型
AF_INET - 将src指向的struct in_addr(按网络字节顺序)类型数据转换成点分十进制格式的IPv4网络地址“ddd.ddd.ddd.ddd”。缓冲区dst必须至少有INET_ADDRSTRLEN字节长。
AF_INET6 - 将src指向的struct in6_addr(按网络字节顺序)类型数据转换成最合适的IPv6网络地址格式的表示。缓冲区dst必须至少有INET6_ADDRSTRLEN字节长。
src - 指定要转换的网络地址,IPv4对应structt in_addr,IPv6对应struct in6_addr
dst - 转换后的文本形式IP地址
size - 设置存放文本形式IP地址的空间大小,对IPv4至少指定为INET_ADDRSTRLEN,对IPv6至少指定为INET6_ADDRSTRLEN
返回值
成功,返回指向dst空间的首地址
失败,返回NULL,errno被设置
需求:
MyServer.c 编译链接后生成可执行文件MyServer
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void){
int count;
char IP[128];
int sockfd,acptfd;
pid_t pid;
struct sockaddr_in server_sockaddr,client_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(5017);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
socklen_t len = sizeof(client_sockaddr);
sockfd = socket(PF_INET,SOCK_STREAM,0);
if(-1 == sockfd){
perror("socket");
return 2;
}
if(-1 == bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))){
perror("bind");
return 3;
}
if(-1 == listen(sockfd,10)){
perror("listen");
return 4;
}
while(1){
acptfd = accept(sockfd,(struct sockaddr *)&client_sockaddr,&len);
if(-1 == acptfd){
perror("accept");
return 5;
}
else{
pid = fork();
if(-1 == pid){
perror("fork");
return 6;
}
else if(0 == pid){
printf("%s connected...\n",inet_ntop(AF_INET,&client_sockaddr.sin_addr,IP,128));
while(1){
int i = 0;
char writeBuf[128] = {0};
char readBuf[128] = {0};
count = read(acptfd,readBuf,128);
while('\n' != readBuf[i]){
writeBuf[i] = toupper(readBuf[i]);
i++;
}
if(0 == strcmp(readBuf,"QUIT\n"))
break;
write(acptfd,writeBuf,count);
}
close(acptfd);
exit(0);
}
else{
waitpid(-1,NULL,WNOHANG);
}
}
}
if(-1 == close(sockfd)){
perror("close");
return 7;
}
return 0;
}
MyClient.c 编译链接后生成可执行文件MyClient
#include
#include
#include
#include
#include
#include
int main(int argc,char **argv){
int count;
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(5017);
if(-1 == inet_pton(AF_INET,argv[1],&server_sockaddr.sin_addr)){
perror("inet_pton");
return 1;
}
int sockfd = socket(PF_INET,SOCK_STREAM,0);
if(-1 == sockfd){
perror("socket");
return 2;
}
if(-1 == connect(sockfd,(struct sockaddr*)&server_sockaddr,sizeof(server_sockaddr))){
perror("connect");
return 3;
}
else{
printf("connected...\n");
}
while(1){
char readBuf[128] = {0};
char writeBuf[128] = {0};
printf("input strings or quit with \"quit\": ");
fgets(writeBuf,128,stdin);
write(sockfd,writeBuf,strlen(writeBuf) + 1);
if(0 == strcmp(writeBuf,"quit\n"))
break;
count = read(sockfd,readBuf,128);
write(1,readBuf,count);
printf("\n");
}
if(-1 == close(sockfd)){
perror("close");
return 4;
}
else{
printf("client process closed...\n");
}
return 0;
}
编译后生成MyServer、MyClient,开启MyServer后,在本地和网内使用客户端测试:
使用本地回环测试结果:
Desktop Linraffe$ MyClient 127.0.0.1
connected...
input strings or quit with "quit": giraffe
GIRAFFE
input strings or quit with "quit": quit
client process closed...
使用本地IP测试结果、使用网段内其他主机测试结果与本地回环相同
MyServer执行结果:
Desktop Linraffe$ MyServer
127.0.0.1 connected...
xxx.xxx.xxx.5 connected...
xxx.xxx.xxx.6 connected...
使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质差异,其原因在于两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。
下图给出了典型的UDP客户/服务器程序的函数调用。客户不与服务器建立连接,而是只管使用sendto(2)函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。服务器不接受来自客户的连接,而是只管调用recvfrom(2)函数,等待来自某个客户的数据到达。recvfrom(2)将与所接收的数据报一起返回客户的协议地址,因此服务器可以把响应发送给正确的客户
recvfrom - receive a message from a socket
所需头文件
#include
#include
函数原型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数
sockfd - socket(2)返回值
buf - 指向要接收数据的缓冲区地址
len - 要接收的字节数
flags - 0
src_addr - 指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构(指向发送者的协议地址空间,如果指定为NULL,addrlen也要指定为NULL)
addrlen - 指向存放将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构中填写的字节数的地址(指向src_addr变量空间的长度)
返回值
成功,返回接收到的字节数
失败,返回 -1,errno被设置
sendto - send a message on a socket
所需头文件
#include
#include
函数原型
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数
sockfd - socket(2)返回值
buf - 指向要发送数据的缓冲区地址
len - 指定要发送的字节数
flags - 0
dest_addr - 指定要发送的目标地址
addrlen - 指定目标地址长度
返回值
成功,返回发送的字节数
失败,返回 -1,errno被设置
MyServer.c 编译生成可执行文件MyServer
#include
#include
#include
#include
#include
#include
#include
#include
int main(void){
char IP[128];
struct sockaddr_in server_sockaddr,client_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(5017);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
socklen_t len = sizeof(client_sockaddr);
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(-1 == sockfd){
perror("socket");
return 1;
}
if(-1 == bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))){
perror("bind");
return 2;
}
while(1){
int count;
int i = 0;
char *recvBuf = (char *)malloc(sizeof(char) * 128);
char *sendBuf = (char *)malloc(sizeof(char) * 128);
count = recvfrom(sockfd,recvBuf,128,0,(struct sockaddr *)&client_sockaddr,&len);
if(-1 == count){
perror("recvfrom");
return 3;
}
else{
printf("receive from:%s\n",inet_ntop(AF_INET,&client_sockaddr.sin_addr,IP,128));
while(recvBuf[i]){
sendBuf[i] = toupper(recvBuf[i]);
i++;
}
}
if(-1 == sendto(sockfd,sendBuf,count,0,(struct sockaddr *)&client_sockaddr,sizeof(client_sockaddr))){
perror("sendto");
return 4;
}
free(recvBuf);
free(sendBuf);
}
if(-1 == close(sockfd)){
perror("close");
return 5;
}
return 0;
}
MyClient.c 编译生成可执行文件MyClient
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char **argv){
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(5017);
char IP[128];
if(-1 == inet_pton(AF_INET,argv[1],&server_sockaddr.sin_addr)){
perror("inet_pton");
return 1;
}
if(-1 == sockfd){
perror("socket");
return 2;
}
while(1){
int count;
int i = 0;
char *recvBuf = (char *)malloc(sizeof(char) * 128);
char *sendBuf = (char *)malloc(sizeof(char) * 128);
printf("input a string or input a \"quit\" to quit:");
fgets(sendBuf,128,stdin);
if(0 == strcmp(sendBuf,"quit\n"))
break;
if(-1 == sendto(sockfd,sendBuf,strlen(sendBuf) + 1,0,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))){
perror("sendto");
return 3;
}
count = recvfrom(sockfd,recvBuf,128,0,NULL,NULL);
if(-1 == count){
perror("recvfrom");
return 4;
}
else{
write(1,recvBuf,count);
}
free(recvBuf);
free(sendBuf);
}
if(-1 == close(sockfd)){
perror("close");
return 5;
}
return 0;
}
在本地测试客户端:
Desktop Linraffe$ MyClient 127.0.0.1
input a string or input a "quit" to quit:giraffe
GIRAFFE
在内网测试客户端:
Linraffe@ubuntu:~/Desktop$ ./MyClient xxx.xxx.xxx.4
input a string or input a "quit" to quit:unicorn
UNICORN
服务器执行结果:
Desktop Linraffe$ MyServer
receive from:127.0.0.1
receive from:xxx.xxx.xxx.5