返回介绍

网络的世界

发布于 2024-10-04 12:28:35 字数 27194 浏览 0 评论 0 收藏 0

0x10-网络的世界

写在最前方

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

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

概念

  • 最具误导性的当属于 TCP/IP 协议了
    • 所谓 TCP/IP 协议指的并不是一个协议,往往在生活中听见的术语如:IP地址TCP连接 等,总会被误导,以为就是一个东西
    • 实际上它们都是彼此独立的 协议 ,只不过会相互合作罢了
    • TCP/IP说的是一个 协议族 ,也就是说是一堆协议的统称
  • 对比 OSITCP/IP 参考模型:
OSITCP/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地址 是房子的地址,那么到了别人家要知道门在哪才行。

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

以太网首部IPTCP/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_namechar *官方名称
h_aliaseschar **域名集合,以NULL结尾
h_addrtypeint地址族的类型 AF_INET 或 AF_INET6
h_lengthint地址的长度 4 或 16
h_addr_listchar **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平台 的区别在于,前者使用时需要 加载和清除 套接字库 其余逻辑流程一致,毕竟只有统一才能越利于编程世界的发展。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文