【原创】《Linux高级程序设计》杨宗德著 - Linux Socket网络编程基础 - 网络通信基础
TCP/IP协议簇
TCP/IP是一组协议的代名词,包括许多别的协议,组成了TCP/IP协议簇。其中比较重要的有SLIP协议、PPP协议、IP协议、ICMP协议、ARP协议、TCP协议、UDP协议、FTP协议、DNS协议、SMTP协议等。TCP/IP协议并不完全符合OSI的七层参考模型。
(1). 链路层
也称作数据链路层或网络接口层(在第一个图中为网络接口层和硬件层),通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆(或其他任何传输媒介)的物理接口细节。ARP(地址解析协议)和RARP(逆地址解析协议)是某些网络接口(如以太网和令牌环网)使用的特殊协议,用来转换IP层和网络接口层使用的地址。
(2). 网络层
也称作互联网层(在第一个图中为网际层),处理分组在网络中的活动,例如分组的选路。在TCP/IP协议族中,网络层协议包括IP协议(网际协议),ICMP协议(Internet互联网控制报文协议),以及IGMP协议(Internet组管理协议)。
IP是一种网络层协议,提供的是一种不可靠的服务,它只是尽可能快地把分组从源结点送到目的结点,但是并不提供任何可靠性保证。同时被TCP和UDP使用。TCP和UDP的每组数据都通过端系统和每个中间路由器中的IP层在互联网中进行传输。
ICMP是IP协议的附属协议。IP层用它来与其他主机或路由器交换错误报文和其他重要信息。
IGMP是Internet组管理协议。它用来把一个UDP数据报多播到多个主机。
(3). 传输层
主要为两台主机上的应用程序提供端到端的通信。在TCP/IP协议族中,有两个互不相同的传输协议:TCP(传输控制协议)和UDP(用户数据报协议)。
TCP为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。由于运输层提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节。为了提供可靠的服务,TCP采用了超时重传、发送和接收端到端的确认分组等机制。
UDP则为应用层提供一种非常简单的服务。它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端。一个数据报是指从发送方传输到接收方的一个信息单元(例如,发送方指定的一定字节数的信息)。UDP协议任何必需的可靠性必须由应用层来提供。
(4). 应用层
应用层负责处理特定的应用程序细节。
TCP/IP体系结构及各组协议
IPv4协议基础
IP地址表示及分类
IP地址是IP网络中数据传输的依据,它标识了IP网络中的一个连接,一台主机可以有多个IP地址,IP分组中的IP地址在网络传输中将保持不变。下面具体介绍IP地址的三种不同表示格式。
一、点分10进制表示格式
这是我们最常见的表示格式,比如某机的IP地址可能为“202.101.105.66”。事实上,对于Ipv4(IP版本)来说,IP地址是由一个32位的二进制数所构成,但这样一串数字序列无疑是十分冗长并且难以阅读和记忆的。为了方便人们的记忆和使用,就将这串数字序列分成4组,每组8位,并改为用 10进制数进行表示,最后用小原点隔开,于是就演变成了“点分10进制表示格式”。
来看看刚才那个IP地址的具体转化过程:
IP地址:11001010011001010110100101000010
分成4组后:11001010 01100101 01101001 01000010
十进制表示:202 101 105 66 www.2cto.com
点分表示:202.101.105.66
二、网络字节顺序格式(NBO,Network Byte Order)
下面我们来谈谈网络字节顺序格式,它和我们后面将要介绍的主机字节顺序格式一样,都只在进行网络开发中才会遇到。因此,在下面的介绍中,我假设读者对Socket编程知识有一定的基础。在网络传输中,TCP/IP协议在保存IP地址这个32位二进制数时,协议规定采用在低位存储地址中包含数据的高位字节的存储顺序(大头),这种顺序格式就被称为网络字节顺序格式。在实际网络传输时,数据按照每32位二进制数为一组进行传输,由于存储顺序的影响,实际的字节传输顺序是由高位字节到低位字节的传输顺序。 为了使通信的双方都能够理解数据分组所携带的源地址、目的地址以及分组的长度等二进制信息,无论是主机还是路由器,在发送每一个分组以前,都必须将二进制信息转换为TCP/IP标准的网络字节顺序格式。网络字节顺序格式的地址不受主机、路由器类型的影响,它的表示是唯一的。
IP地址划分
地址结构定义
<span style="font-size:18px;">struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
struct sockaddr_in {
short int sin_family; /* Address family AF_INET */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
}; <br>
struct in_addr {
unsigned long s_addr; /* Internet address */
};
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 traffic class & flow info */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* set of interfaces for a scope */
};
struct in6_addr {
uint8_t s6_addr[16]; /* IPv6 address */
};
struct addrinfo{
int ai_flags; /* AI_PASSIVE,AI_CANONNAME,AI_NUMERICHOST */
int ai_family; /* AF_INET,AF_INET6 */
int ai_socktype; /* SOCK_STREAM,SOCK_DGRAM */
int ai_protocol; /* IPPROTO_IP, IPPROTO_IPV4, IPPROTO_IPV6 */
size_t ai_addrlen; /* Length */
char *ai_cannoname; /* */
struct sockaddr *ai_addr; /* struct sockaddr */
struct addrinfo *ai_next; /* pNext */
} </span>
地址转化函数
inet_addr()函数将点分十进制的字符串转换为32位的网络字节顺序的IP信息。
inet_network ()函数将点分十进制的字符串转换为32位的主机字节顺序的IP信息。
<span style="font-size:18px;">#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
//函数原型:
int inet_aton(const char * cp,struct in_addr *inp);
char * inet_ntoa(struct in_addr in);</span>
示例代码
#include<arpa/inet.h>运行结果
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
int main(int argc,char *argv[])
{
in_addr_t net;
struct in_addr net_addr,ret;
char buf[128];
memset(buf,'\0',128);
net=inet_addr("192.168.68.128");
net_addr.s_addr=net;
printf("inet_addr(192.168.68.128)=0x%x\n",inet_addr("192.168.68.128"));
printf("inet_network(192.168.68.128)=0x%x\n",inet_network("192.168.68.128"));
printf("inet_ntoa(net)%s\n",inet_ntoa(net_addr));
inet_aton("192.168.68.128",&ret);
printf("test inet_aton,then inet_ntoa(ret)%s\n",inet_ntoa(ret));
}
$ ./test_add
inet_addr(192.168.68.128)=0x8044a8c0
inet_network(192.168.68.128)=0xc0a84480
inet_ntoa(net)192.168.68.128
test inet_aton,then inet_ntoa(ret)192.168.68.128
IPv6中,使用inet_ntop/inet_pton来转化字符串形式表示的IPv6地址和数字形式表示的IPv6地址。IPv4中也可使用这两个函数。
函数原型:<span style="font-size:18px;">int inet_pton(int af, const char *src, void *dst);
//这个函数转换字符串到网络地址,第一个参数af是地址族,转换后存在dst中 af的值可为AF_INET (代表使用IPv4协议)或AF_INET6(代表作用IPv6协议)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
//这个函数转换网络二进制结构到ASCII类型的地址,参数的作用和上面相同,只是多了一个参数socklen_t cnt,
//他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC</span>
示例代码
#include<arpa/inet.h>运行结果
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
int main(int argc,char *argv[])
{
in_addr_t net;
struct in_addr net_addr,ret;
char buf[128];
inet_pton(AF_INET,"192.168.68.128",&ret);
inet_ntop(AF_INET,&ret,buf,128);
printf("buf=%s\n",buf);
}
$ ./test_inet_pton
buf=192.168.68.128
网络数据封包和拆包过程
数据包接收拆包分类流程
以太网链路层数据帧格式
IP数据包头
TCP包头
UDP包头
字节顺序与大小端问题
网络字节序采用大端模式
大小端解析
端模式出自Jonathan Swift书写的《格列佛游记》一书,这本书根据将鸡蛋敲开的方法不同将所有的人分为两类,从圆头开始将鸡蛋敲开的人被归为Big Endian,从尖头开始将鸡蛋敲开的人被归为Littile Endian。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。
在计算机业Big Endian和Little Endian也几乎引起一场战争。在计算机业界,Endian表示数据在存储器中的存放顺序。
大端:高位存在低地址,低位存在高地址;
小端:高位存在高地址,低位存在低地址;(intel的x86,ARM普遍都是属于小端)
举个例子,从内存地址0x0000开始有以下数据
0x0000 0x12
0x0001 0x34
0x0002 0xab
0x0003 0xcd
如果我们去读取一个地址为0x0000的四个字节变量:
若字节序为big-endian,则读出结果为0x1234abcd;
若字节序位little-endian,则读出结果为0xcdab3412.
如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为:
big-endian little-endian
0x0000 0x12 0xcd
0x0001 0x23 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12
Intelx86系列以及ARM系列CPU都是little-endian的字节序.
示例代码
#include <stdio.h>运行结果
#include <stdlib.h>
union word
{
inta;
charb;
} c;
int checkCPU(void)
{
c.a = 1;
printf("c.b=%d\n",c.b);
return (c.b==1);
}
int main(void)
{
int i;
i= checkCPU();
if(i==0)
printf("this is Big_endian\n");
else if(i==1)
printf("this is Little_endian\n");
return 0;
}
$ ./big_little_endian
c.b=1
this is Little_endian
字节顺序转换函数
在存储主机信息时,IP地址与端口号需要存储为网络的大端模式,因此在为网络地址赋值时需要进行转换,例如,添加端口的代码如下:
<span style="font-size:18px;">struct sockaddr_in s_addr;
s_addr.sin_port = htons(7838);//端口信息赋值</span>
原文链接
http://blog.csdn.net/geng823/article/details/41702265