0x10-网络的世界
写在最前方
- 网络编程没有想象之中的难,但是同样一句废话,也没有想象之后那么容易。
- 接下来记录的是对于网络编程的一些教接近底层的东西,也就是称之为系统接口函数的东西,通常叫做系统编程,
- 当然网络编程在非学院派看来,是使用一些成熟的库(这是对于C语言来说,当然很少有人愿意这么做,但个人觉得有了库的C就和其他高级语言更像了)(注:C/C++都没有标准网络库,所以只能使用第三方开发的库,所谓乱世出英雄。C++在 C++17 似乎要有了。), 例如
libev
这一类的。 - 最后,还是先将底层基础打好为妙。
开始首先是万物根源的协议信息
概念
- 最具误导性的当属于
TCP/IP
协议了- 所谓
TCP/IP
协议指的并不是一个协议,往往在生活中听见的术语如:IP地址, TCP连接 等,总会被误导,以为就是一个东西 - 实际上它们都是彼此独立的 协议 ,只不过会相互合作罢了
TCP/IP
说的是一个 协议族 ,也就是说是一堆协议的统称
- 所谓
- 对比 OSI 和 TCP/IP 参考模型:
OSI | TCP/IP |
---|---|
应用层 表示层 会话层 | 应用层 |
传输层 | 传输层 |
网络层 | 网络层 |
链路层 物理层 | 网络接口层 |
- 其中最常接触的
- 位于 网络层 的 IP 协议,大家所熟知的
IP地址
就是由它进行封装并传往下一层 - 位于 传输层 的 TCP/UDP 两个协议, 一个是面向连接(STREAM), 一个是面向数据(DGRAM)的,实际上还有一个但这里不记录。
- 查看自身 网络信息的办法
*nix
: 在 Terminal 中输入ifconfig -a
Windows
: 在 PowerShell 中输入ipconfig
- 位于 网络层 的 IP 协议,大家所熟知的
- 概念模糊的 DNS
- 其实很简单,它的作用就是用来找到域名所对应的 IP地址
- 为什么?因为 IP地址 太难记了!如果你觉得 IPv4 地址还难不倒你,那请你试试 IPv6
- 怎么查看域名对应的 IP地址,当然先不考虑 CDN
*nix
和Windows
都可以通过ping <domain name>
命令进行查询
- MAC地址 和 端口号
- 对于前者,实际上应该是最熟悉不过的,对于网络上的主机而言,每一台主机就有一个专属的 MAC地址
- 后者则是相当于一个房子的门,这个比喻在各大教材中广泛引用,但也的确贴切,假设 IP地址 是房子的地址,那么到了别人家要知道门在哪才行。
一个完整的应用程序传输数据时候 封装 的过程(从右二向左依次封装):
以太网首部 | IP | TCP/UDP | 真实数据 | 尾部 |
---|---|---|---|---|
MAC地址 | IP地址 | TCP或者UDP协议 | 应用程序数据 | 效验码 |
源和目的MAC地址以及 | 及前层协议类型 | 源和目的端口号及前层应用程序首部信息 | 应用软件信息和真正的数据 |
其中端口号实际上就是 应用程序的信息
接收数据时的 拆解 顺序与 封装 正好相反。
其中在传输过程中,作为接收方最开始使用的是 网络接口层/数据链路层 的驱动程序(即操作系统自带或另行安装,总之不用使用的程序员写就对了),来判断这个包是否属于我,判断的依据就是 MAC地址,如果是再判断什么协议
- 在此处的协议可不止 IP协议, 也可能是 ARP协议 等。之后就是就事论事交给相应的处理软件去处理(拆解)就行
- 科普: MAC地址是
48bit
的, 前24bit
由 IEEE 分配, 后24bit
由厂商分配。原则上是唯一的。
MAC地址 和 IP地址
- 既然前方说到 MAC地址 和 IP地址 都能够作为识别另一个主机的唯一标识,但是为什么需要有两个相同功能的东西?
- 是,在一开始,网络很小的情况下,例如我们在同一个局域网中,我们之间需要通信的时候,只需要使用ARP协议,进行广播,向在一个网络中的所有主机发送消息就行,剩下的就让其他主机去判断(通过MAC地址)这个数据是不是发给我的。
- ARP协议 的作用就是在同一个网络中,通过 广播 找出符合自己要求的主机的 MAC地址 ,如果不在同一个网络中,又想知道对方的 MAC地址, 那只能借助把每个网络链接在一起的 网关 来帮助你发送 。 总之进行网络通信时必须知道对方的 IP地址 和 MAC地址
- 但是如果是现在整个互联网呢?不算 IPv6 ,就算 IPv4 也是几十亿的存在,如果我从中国向国外发送信息,广播整个互联网的所有主机,那就炸了!
- 所以我们需要对世界网络进行分区,让大区域包含小区域,就像国家-省-市区… , 很遗憾的是 MAC地址 是跟计算机相关而不是和位置相关的。所以我们有了 IP协议
- IP协议 所附带的产品 IP地址 的作用就在帮助计算机识别自己是否在同一个网络中( 这里省略了子网掩码的作用 )。
实际上,在进行网络编程的时候,以上细节几乎都被隐藏起来,留给我们的只是可供使用的接口。
也许,许多大学计算机基础课程,会讲到 IP地址 有种类,分为 A,B,C…类,老师还介绍了各种类型的地址范围。
但是在现代,这种分类早已经失效,或者说正在逐渐消失,因为当下的 IP 地址的 子网掩码 可以是任意位,并以反斜杠跟在 IP地址后方。
比较现代的 IP地址 表示形式一般如此 1.185.223.1/24 代表着子网掩码是由 24个 从左至右连续的的二进制1 组合而成,其余位为0。称为CIDR分类
夹在中间
事实上有一些实用且挺炫酷的函数,可以先提一下
- 域名 和 IP地址 的互查
gethostbyname
用于域名查找 IP信息及各类信息struct hostent * gethostbyname(const char * hostname)
struct hostent
是存储查找到的各类型信息,后方会有介绍hostname
即要查询的域名
gethostbyaddr
用于IP地址查找 域名及各类信息struct hostent * gethostbyaddr(const char * addr, socklen_t len, int family)
addr
是要查询的 IP地址,之所以是const char *
是因为C语言历史遗留的原因,实际上其类型应为struct in_addr *
(IPv4)len
地址的长度,即 IPv4 为4, IPv6 为16family
即协议的种类, IPv4 为AF_INET
, IPv6 为AF_INET6
struct hostent 的成员 | . | 类型 | . | 解释 |
---|---|---|---|---|
h_name | char * | 官方名称 | ||
h_aliases | char ** | 域名集合,以NULL结尾 | ||
h_addrtype | int | 地址族的类型 AF_INET 或 AF_INET6 | ||
h_length | int | 地址的长度 4 或 16 | ||
h_addr_list | char ** | IP的集合,以NULL结尾, 实际上每个元素的类型为 struct in_addr* |
- 其中第二和最后一个是关注的重点所在,可以在调用函数之后,输出信息
实际上,这并不是一个好的方法,在后方将记录 现代人的我们 该如何做到这些事情,以上只是以前的TCP/IP 编程
只适用于 IPv4
套接字网络编程初始
选择使用 C 语言进行编程
- 在网络编程中,最常实用的两种连接方式
TCP
和UDP
- 最常编程的平台
POSIX 标准->*nix平台标准
和Windows 平台标准
- 实际上,后者也是参考前者进行一些细微的改变(指的是接口)
对比两种不同连接方式的不同地位的创建,使用
TCP服务器 | TCP客户端 | UDP服务器 | UDP客户端 | 注释 |
---|---|---|---|---|
socket() | socket() | socket() | socket() | 创建套接字 |
bind() | bind() | bind() | 绑定所分配IP地址和端口号 | |
listen() | connect() | 客户端则绑定IP地址和端口号,并等待连接;服务器则是等待连接 | ||
accept() | 服务器接受连接 | |||
… | … | sendto/recvfrom() | sendto/recvfrom() | 对于UDP即是连接也是操作 |
close() | close() | close() | close | 双向直接关闭连接 |
shutdown() | shutdown() | shutdown() | shutdown() | 可选择方向的关闭连接,即更加灵活 |
如此对比虽然有一些小瑕疵,但是能够大体上反映出真个网络编程上不同方式的区别
注1: 对于 sendto recvfrom 这两个接口函数,并不一定是只能用在 UDP类型的 套接字上,同样 TCP类型的 套接字也能使用,但是这么做并没有什么意义。
注2: 实际上 UDP 没有所谓的 服务器和和护短,因为本来就是单纯的互相发来发去。客户端端口 一般是随机的
以上是 *nix平台下的标准, Windows下的操作方式和 API有细微不同,但大部分是一致的。
Windows | *nix |
---|---|
socket() | socket() |
bind() | bind() |
connect() | connect() |
listen() | listen() |
accept() | accept() |
closesocket() | close() |
send() | send() |
read() | read() |
sendto() | sendto() |
recvfrom() | recvfrom() |
不仅仅是接口名字相同,参数个数以及功能也是一致,即使有一个例外,其参数以及使用方法也相同。
那岂不是可以直接移植了?
并不!
在 Windows 套接字编程时 , 由于 Windows
将其实现为动态库,所以在使用时需要将其加载进程序。
故而多加了加载操作。
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData /* 这是一个结构体, 传入类型为WSADATA* */
);
int WSACleanup(void);
每当在 Windows 上进行套接字编程时,总要指定某个版本的套接字库:
WSADATA wsaData;
int err_code;
/*
* MAKEWORD()的作用在于将版本号转为指定格式传入
* 当下(2015-10)套接字库的版本号最高是 2.2
*/
err_code = WSAStartup(MAKEWORD(2, 2), &wsaData);
/* TODO Something */
WSACleanup();
这是最基本的在 Windows 上使用 套接字 编程的流程,但是如果本平台的套接字库最高版本并不符合当前要求呢?
那么首先会将套接字版本库尽可能设置到平台的 最高版本 ,可以通过结构体 WSADATA
进行查询
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("Could not find a usable version of Winsock.dll\n");
WSACleanup();
return 1;
}
总体而言,
Windows平台
和*uix平台
的区别在于,前者使用时需要 加载和清除 套接字库 其余逻辑流程一致,毕竟只有统一才能越利于编程世界的发展。