0x10-网络的世界

写在最前方

  • 网络编程没有想象之中的难,但是同样一句废话,也没有想象之后那么容易。
  • 接下来记录的是对于网络编程的一些教接近底层的东西,也就是称之为系统接口函数的东西,通常叫做系统编程,
  • 当然网络编程在非学院派看来,是使用一些成熟的库(这是对于C语言来说,当然很少有人愿意这么做,但个人觉得有了库的C就和其他高级语言更像了)(注:C/C都没有标准网络库,所以只能使用第三方开发的库,所谓乱世出英雄。C在 C++17 似乎要有了。), 例如libev这一类的。
  • 最后,还是先将底层基础打好为妙。

开始首先是万物根源的协议信息

概念

  • 最具误导性的当属于 TCP/IP 协议了
    • 所谓 TCP/IP 协议指的并不是一个协议,往往在生活中听见的术语如:IP地址TCP连接 等,总会被误导,以为就是一个东西
    • 实际上它们都是彼此独立的 协议 ,只不过会相互合作罢了
    • TCP/IP说的是一个 协议族 ,也就是说是一堆协议的统称
  • 对比 OSITCP/IP 参考模型:
OSI TCP/IP
应用层 表示层 会话层 应用层
传输层 传输层
网络层 网络层
链路层 物理层 网络接口层
  • 其中最常接触的
    • 位于 网络层IP 协议,大家所熟知的 IP地址 就是由它进行封装并传往下一层
    • 位于 传输层TCP/UDP 两个协议, 一个是面向连接(STREAM), 一个是面向数据(DGRAM)的,实际上还有一个但这里不记录。
    • 查看自身 网络信息的办法
      • *nix: 在 Terminal 中输入 ifconfig -a
      • Windows: 在 PowerShell 中输入 ipconfig
  • 概念模糊的 DNS
    • 其实很简单,它的作用就是用来找到域名所对应的 IP地址
    • 为什么?因为 IP地址 太难记了!如果你觉得 IPv4 地址还难不倒你,那请你试试 IPv6
    • 怎么查看域名对应的 IP地址,当然先不考虑 CDN
      • *nixWindows 都可以通过 ping <domain name> 命令进行查询
  • MAC地址端口号
    • 对于前者,实际上应该是最熟悉不过的,对于网络上的主机而言,每一台主机就有一个专属的 MAC地址
    • 后者则是相当于一个房子的门,这个比喻在各大教材中广泛引用,但也的确贴切,假设 IP地址 是房子的地址,那么到了别人家要知道门在哪才行。

一个完整的应用程序传输数据时候 封装 的过程(从右二向左依次封装):

以太网首部 IP TCP/UDP 真实数据 尾部
MAC地址 IP地址 TCP或者UDP协议 应用程序数据 效验码
源和目的MAC地址以及 及前层协议类型 源和目的端口号及前层应用程序首部信息 应用软件信息和真正的数据

其中端口号实际上就是 应用程序的信息

接收数据时的 拆解 顺序与 封装 正好相反。

  • 其中在传输过程中,作为接收方最开始使用的是 网络接口层/数据链路层 的驱动程序(即操作系统自带或另行安装,总之不用使用的程序员写就对了),来判断这个包是否属于我,判断的依据就是 MAC地址,如果是再判断什么协议

    • 在此处的协议可不止 IP协议, 也可能是 ARP协议 等。之后就是就事论事交给相应的处理软件去处理(拆解)就行
    • 科普: MAC地址是 48bit 的, 前24bitIEEE 分配, 后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 为16
        • family 即协议的种类, IPv4AF_INET, IPv6AF_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 语言进行编程

  • 在网络编程中,最常实用的两种连接方式 TCPUDP
  • 最常编程的平台 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平台 的区别在于,前者使用时需要 加载和清除 套接字库 其余逻辑流程一致,毕竟只有统一才能越利于编程世界的发展。


书籍推荐