<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[whowin - 开源和分享是技术发展的源泉和动力]]></title><description><![CDATA[一个从业30多年的退休程序员，主要从事嵌入式软件开发。]]></description><link>https://whowin.cn</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 17:22:04 GMT</lastBuildDate><atom:link href="https://whowin.cn/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[双向链表及如何使用GLib的GList实现双向链表]]></title><description><![CDATA[双向链表是一种比单向链表更为灵活的数据结构，与单向链表相比可以有更多的应用场景，本文讨论双向链表的基本概念及实现方法，并着重介绍使用GLib的GList实现单向链表的方法及步骤，本文给出了多个实际范例源代码，旨在帮助学习基于GLib编程的读者较快地掌握GList的使用方法，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；本文适合初学者阅读。

1 双向链表及其实现

在文章《单向链表以及如何使用GLib中的GSList实现单向链表》中，介绍了单向链表以及基于 G...]]></description><link>https://whowin.cn/130004-doubly-linked-lists-in-glib</link><guid isPermaLink="true">https://whowin.cn/130004-doubly-linked-lists-in-glib</guid><category><![CDATA[GList]]></category><category><![CDATA[双向链表]]></category><category><![CDATA[GLib]]></category><category><![CDATA[#DoublyLinkedList]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Tue, 29 Oct 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730609737712/18e1916a-e72f-4343-a0b7-b420b78ef79f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>双向链表是一种比单向链表更为灵活的数据结构，与单向链表相比可以有更多的应用场景，本文讨论双向链表的基本概念及实现方法，并着重介绍使用GLib的GList实现单向链表的方法及步骤，本文给出了多个实际范例源代码，旨在帮助学习基于GLib编程的读者较快地掌握GList的使用方法，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；本文适合初学者阅读。</p>
</blockquote>
<h2 id="heading-1">1 双向链表及其实现</h2>
<ul>
<li>在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472406">《单向链表以及如何使用GLib中的GSList实现单向链表》</a>中，介绍了单向链表以及基于 GLib 实现单向链表的方法，建议阅读本文前先阅读这篇文章；</li>
<li>在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472383">《使用GLib进行C语言编程的实例》</a>中，简单介绍了 GLib，建议阅读本文前先阅读这篇文章；</li>
<li>双向链表(Doubly Linked List)是一种链式数据结构，每个节点包含三个主要部分：<ol>
<li>数据部分：存储节点的数据</li>
<li>前向指针：指向链表中的下一个节点</li>
<li>后向指针：指向链表中的上一个节点</li>
</ol>
</li>
<li>可以看出，和单向链表相比较，双向链表多了一个指向前一个节点的指针</li>
<li>双向链表的基本特性<ol>
<li>双向性：与单向链表不同，双向链表允许从两个方向遍历，可以从头节点向尾节点遍历，也可以从尾节点向头节点遍历；</li>
<li>动态大小：双向链表的大小可以动态增长或缩小，不需要提前定义大小；</li>
<li>节点插入和删除：在双向链表中，插入和删除节点操作相对简单，因为每个节点都有指向前后节点的指针；</li>
</ol>
</li>
<li>双向链表的节点结构：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Node</span> {</span>
      <span class="hljs-keyword">int</span> data;               <span class="hljs-comment">// 数据部分</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Node</span> *<span class="hljs-title">next</span>;</span>      <span class="hljs-comment">// 指向下一个节点的指针</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Node</span> *<span class="hljs-title">prev</span>;</span>      <span class="hljs-comment">// 指向前一个节点的指针</span>
  };
</code></pre>
</li>
<li>双向链表的基本操作<ol>
<li>插入节点：可以在链表的开头、结尾或任意位置插入节点；</li>
<li>删除节点：可以删除链表中的任意节点，操作相对简单，因每个节点都知道其前一个和后一个节点；</li>
<li>遍历链表：可以从头到尾遍历链表(正向遍历)或从尾到头遍历链表(反向遍历)；</li>
</ol>
</li>
<li><p>与单向链表相比，双向链表有以下特点：</p>
<ol>
<li>由于数据结构中增加了后向指针，使链表可以双向遍历，而单向链表仅能单向遍历；</li>
<li>通过后向指针可以直接访问前一个节点，与单向链表相比，可以简化节点删除操作的复杂度；</li>
<li>在插入节点时，比单向链表更快捷更灵活；</li>
<li>与单向链表相比，由于增加了后向指针，内存开销增加；</li>
<li>与单向链表相比，双向链表需要操作两个指针，其操作和维护的复杂度要高一些；</li>
</ol>
</li>
<li><p>总的来说，‌双向链表比单向链表更加灵活，‌适用场景也要多一些。；</p>
</li>
<li>下面程序是一个简单的双向链表的 C 语言标准库实现，<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130004/dllist-c.c">dllist-c.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li>编译：<code>gcc -Wall -g dllist-c.c -o dllist-c</code></li>
<li>运行：<code>./dllist-c</code></li>
<li>该程序实现了双向链表的插入、删除以及正向遍历；</li>
<li>该程序首先建立一个双向链表，并在链表中加入 4 个节点，数据分别为：1、2、3、5，然后显示整个链表；</li>
<li>在第 3 个节点(数据为 3，索引号为 2)的后面插入节点，数据为 4，然后显示整个链表；</li>
<li>将第 3 个节点(数据为 3，索引号为 2)删除，然后显示整个链表；</li>
<li>最后释放整个链表；</li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/130004/screenshot-of-dllist-c.png" alt="screenshot of dllist-c" /></p>
</li>
</ul>
<h2 id="heading-2-glib-glist">2 GLib 中双向链表结构 GList</h2>
<ul>
<li><a target="_blank" href="https://docs.gtk.org/glib/index.html">GLib API version 2.0 手册</a> (<strong>点击查看手册</strong>)</li>
<li><a target="_blank" href="https://docs.gtk.org/glib/struct.List.html">GLib API 手册中 GList 部分</a>  (<strong>点击查看手册</strong>)</li>
<li>在 GLib 中，‌双向链表是通过 GList 结构体实现的，GList 是一个简单的双向链表结构，‌用于存储各种类型的数据；</li>
<li>GSList 定义如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">GList</span> {</span>
      gpointer data;
      GList *next;
      GList *prev;
  }
</code></pre>
</li>
<li>data 为双向链表的数据指针，可以指向任何类型或结构的数据；</li>
<li>next 为指向该双向链表当前节点的下一个节点的指针；</li>
<li>prev 为指向该双向链表当前节点的前一个节点的指针；</li>
<li><p>GLib 为双向链表结构 GList 的操作提供了大量的函数，本文仅就其中的一部分函数进行简单介绍；</p>
</li>
<li><p><strong>添加、插入新节点</strong></p>
<ul>
<li><code>g_list_append()</code> 在双向链表的最后添加一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_slist_append</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, gpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>返回指向双向链表的起始指针；</li>
<li>说明：在双向链表的最后添加节点，必须要遍历整个链表才能找到链表的尾部，这种做法效率很低，通常的做法是使用 <code>g_list_prepend()</code> 在链表的起始位置添加节点，当所有节点添加完毕后，再使用 <code>g_list_reverse()</code> 将整个链表反转；</li>
</ul>
</li>
<li><code>g_list_prepend()</code> 在双向链表的最前面添加一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_prepend</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, gpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>返回指向双向链表的指针，在双向链表的开头添加一个节点，双向链表的指针是肯定会变化的；</li>
</ul>
</li>
<li><code>g_list_insert()</code> 在双向链表的中间插入一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_insert</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, gpointer data, gint position)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>position - 插入节点的位置，如果是负数或者超过了该双向链表的节点的数量，新节点将插到双向链表的最后；</li>
<li>返回该双向链表的起始指针；</li>
</ul>
</li>
<li><code>g_list_insert_before()</code> 在包含指定数据的节点之前插入一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_insert_before</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, GSList *sibling, gpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>sibling - 指向一个节点的指针，将在这个节点前插入新节点</li>
<li>返回该双向链表的起始指针；</li>
</ul>
</li>
</ul>
</li>
<li><strong>删除节点</strong><ul>
<li><code>g_list_remove_link()</code> 从双向链表中删除一个节点，但并不释放该节点占用的内存<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_remove_link</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, GList *llink_)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>llink_ - 指向双向链表中一个节点的指针，该节点将被删除；</li>
<li>返回该双向链表的起始指针；</li>
<li>该函数并不释放被删除的节点内存，被删除的节点的 next 和 prev 指针将指向 NULL，所以可以认为被删除的节点变成了一个只有一个节点的新的双向链表；</li>
</ul>
</li>
<li><code>g_list_delete_link()</code> 从双向链表中删除一个节点，并释放该节点占用的内存；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_delete_link</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, GList *link_)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>link_ - 指向双向链表中一个节点的指针，该节点将被删除；</li>
<li>返回该双向链表的起始指针；</li>
<li>该函数与 <code>g_list_remove_link()</code> 的唯一区别是该函数在删除节点后释放了被删除节点占用的内存；</li>
</ul>
</li>
<li><code>g_list_remove()</code> 从双向链表中删除指定数据的一个节点，如果链表中有指定数据的节点有多个，将只删除第一个；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_remove</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>data - 指向要删除节点的数据</li>
<li>返回该双向链表的起始指针；</li>
</ul>
</li>
<li><code>g_list_remove_all()</code> 从双向链表中删除指定数据的所有节点；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_remove_all</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>data - 指向要删除节点的数据</li>
<li>返回该双向链表的起始指针；</li>
</ul>
</li>
</ul>
</li>
<li><strong>遍历链表</strong><ul>
<li><code>g_list_foreach()</code> 遍历双向链表，每个节点都会调用一个指定函数；<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">g_list_foreach</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, GFunc func, gpointer user_data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>func - 一个指向函数的指针，遍历到双向链表的每个节点时，都会调用这个函数；</li>
<li>GFunc 的定义如下：<pre><code class="lang-C"><span class="hljs-keyword">void</span> (* GFunc) (gpointer data, gpointer user_data)
</code></pre>
</li>
<li>GFunc 的定义表明，传递给 func 的参数有两个，一个是 data - 指向当前节点的节点数据指针，另一个就是指向自定义参数 user_data 的指针</li>
<li>user_data - 指针指向调用 func 时传递的用户参数；</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>查找节点</strong></p>
<ul>
<li><code>g_list_find()</code> 查找链表中包含给定数据的节点；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_find</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针</li>
<li>data - 指向要查找节点的数据</li>
<li>返回在双向链表中找到的节点的指针，如果没有找到相应节点，返回 NULL;</li>
</ul>
</li>
<li><code>g_list_index()</code> 获取包含给定数据的节点的位置(从 0 开始)；<pre><code class="lang-C">  <span class="hljs-function">gint <span class="hljs-title">g_list_index</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>data - 指向要查找节点的数据；</li>
<li>返回数据为 data 的节点在双向链表中的位置(从 0 开始)，如果没找到相应节点，则返回 -1；</li>
</ul>
</li>
<li><code>g_list_position()</code> 获取给定节点在链表中的位置(从 0 开始)；<pre><code class="lang-C">  <span class="hljs-function">gint <span class="hljs-title">g_list_position</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, GList *llink)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>llink - 指向双向链表中的一个节点的指针；</li>
<li>返回 llink 指向的节点在双向链表中的位置(从 0 开始)，如果没找到相应节点，则返回 -1；</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>释放链表</strong></p>
<ul>
<li><code>g_list_free()</code> 释放链表使用的所有内存，该函数不会释放节点中动态分配的内存；<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">g_list_free</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>该函数仅释放 GList 占用的内存，并不释放双向链表中各个节点动态申请的内存，如果链表中有动态申请内存，考虑使用 <code>g_list_free_full()</code> 或手动释放内存；</li>
</ul>
</li>
<li><code>g_list_free_full()</code> 释放链表使用的所有内存，并对每个节点的数据调用指定的销毁函数<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">g_list_free_full</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>, GDestroyNotify free_func)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>free_func - 销毁函数，对双向链表中的每个节点数据将调用该函数，可用于释放节点中动态分配的内存；</li>
<li>GDestroyNotify 的定义如下：<pre><code class="lang-C"><span class="hljs-keyword">void</span> (* GDestroyNotify) (gpointer data)
</code></pre>
</li>
<li>所以在调用 free_func 时会将指向节点数据的指针传递给该函数；</li>
</ul>
</li>
</ul>
</li>
<li><strong>其它</strong><ul>
<li><code>g_list_length()</code> 获取双向链表的长度；<pre><code class="lang-C">  <span class="hljs-function">guint <span class="hljs-title">g_list_length</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>返回双向链表中节点的数量。</li>
</ul>
</li>
<li><code>g_list_last()</code> 获取双向链表的最后一个节点；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_last</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>返回双向链表的最后一个节点的指针，如果双向链表没有节点，则返回 NULL；</li>
</ul>
</li>
<li><code>g_list_concat()</code>  连接两个双向链表；<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_concat</span><span class="hljs-params">(GList *list1, GList *list2)</span></span>
</code></pre>
<ul>
<li>list1 - 指向第 1 个双向链表的指针；</li>
<li>list2 - 指向准备连接到第 1 个双向链表后面的双向链表的指针；</li>
<li>返回连接好的双向链表的指针，</li>
</ul>
</li>
<li><code>g_list_reverse()</code> 反转整个双向链表<pre><code class="lang-C">  <span class="hljs-function">GList *<span class="hljs-title">g_list_reverse</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>)</span></span>
</code></pre>
<ul>
<li>list - 指向双向链表的指针；</li>
<li>返回该双向链表的起始指针；</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-3-glist">3 如何使用 GList 实现双向链表</h2>
<ul>
<li>文章的一开始有一个使用标准 C 语言函数库的双向链表的实例，使用 GLib 的 GList 操作双向链表要容易得多；</li>
<li>下面程序是使用 C 语言，基于 GLib 实现的双向链表，<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130004/dllist-glib.c">dllist-glib.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li>该程序实现的功能与文章开头的程序 <a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130004/dllist-c.c">dllist-c.c</a> 完全一样，但程序看上去要简洁很多，我们不妨把源程序列在这里</li>
<li><p>该程序与文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472406">《单向链表以及如何使用GLib中的GSList实现单向链表》</a>中使用 GLib 实现单向链表的程序非常相似</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">print_node</span><span class="hljs-params">(gpointer data, gpointer user_data)</span> </span>{
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%d -&gt; "</span>, GPOINTER_TO_INT(data));
  }
  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">print_list</span><span class="hljs-params">(GList *<span class="hljs-built_in">list</span>)</span> </span>{
      g_list_foreach(<span class="hljs-built_in">list</span>, &amp;print_node, <span class="hljs-literal">NULL</span>);
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"NULL\n"</span>);
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      GList *<span class="hljs-built_in">list</span> = <span class="hljs-literal">NULL</span>;

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Append 4 nodes, the data are 1, 2, 3, 5.\n"</span>);
      <span class="hljs-built_in">list</span> = g_list_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">1</span>));
      <span class="hljs-built_in">list</span> = g_list_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">2</span>));
      <span class="hljs-built_in">list</span> = g_list_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">3</span>));
      <span class="hljs-built_in">list</span> = g_list_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">5</span>));
      print_list(<span class="hljs-built_in">list</span>);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Insert a new node after node with the data 3.\n"</span>);
      <span class="hljs-built_in">list</span> = g_list_insert(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">4</span>), <span class="hljs-number">3</span>);
      print_list(<span class="hljs-built_in">list</span>);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Remove node with the data 3.\n"</span>);
      <span class="hljs-built_in">list</span> = g_list_remove(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">3</span>));
      print_list(<span class="hljs-built_in">list</span>);

      <span class="hljs-comment">// Free the list</span>
      g_list_free(<span class="hljs-built_in">list</span>);

      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li>该程序中涉及到的两个宏：<code>GINT_TO_POINTER(value)</code> 和 <code>GPOINTER_TO_INT(p)</code>，在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472406">《单向链表以及如何使用GLib中的GSList实现单向链表》</a>中有比较详细的介绍；</li>
<li><p>编译：</p>
<pre><code class="lang-bash">  gcc -Wall -g dllist-glib.c -o dllist-glib `pkg-config --cflags --libs glib-2.0`
</code></pre>
</li>
<li><p>其中，<code>pkg-config --cflags --libs glib-2.0</code> 的含义在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472383">《使用GLib进行C语言编程的实例》</a>中做过介绍；</p>
</li>
<li>运行：<code>./dllist-glib</code></li>
<li>该程序实现了双向链表的插入、删除、遍历；</li>
<li><code>print_list()</code> 中使用 <code>g_list_foreach()</code> 对链表进行遍历，对链表中的每个节点数据，将调用函数 <code>print_node()</code>；</li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/130004/screenshot-of-dllist-glib.png" alt="screenshot of dllist-glib" /></p>
</li>
</ul>
<h2 id="heading-4">4 双向链表的应用场景</h2>
<ul>
<li><p>双向链表是一种数据结构，它的每个节点包含对前一个节点和后一个节点的引用；这种结构在许多应用场景中非常有用，以下是一些常见的应用场景：</p>
</li>
<li><p>浏览器历史记录：</p>
<blockquote>
<p>双向链表可以用来实现浏览器的“后退”和“前进”按钮，用户可以在历史记录中前后移动当前指针；</p>
</blockquote>
</li>
<li><p>音乐播放器：</p>
<blockquote>
<p>在音乐播放器中，双向链表可以用于管理播放列表，允许用户在歌曲之间前后切换；</p>
</blockquote>
</li>
<li><p>文本编辑器：</p>
<blockquote>
<p>在实现撤销和重做功能时，双向链表可用于存储编辑历史，方便在不同操作间切换；</p>
</blockquote>
</li>
<li><p>LRU缓存：</p>
<blockquote>
<p>在实现最近最少使用(LRU)缓存时，双向链表可以高效地维护访问顺序，以便快速找到和删除最少使用的项；</p>
</blockquote>
</li>
<li><p>操作系统中的进程调度：</p>
<blockquote>
<p>在某些调度算法中，双向链表可用于管理就绪队列，使得进程可以方便地添加和移除；</p>
</blockquote>
</li>
<li><p>图形界面中的组件布局：</p>
<blockquote>
<p>在某些图形用户界面(GUI)框架中，双向链表用于管理组件的顺序和关系，使得组件之间的插入和删除变得灵活；</p>
</blockquote>
</li>
<li><p>实现栈和队列：</p>
<blockquote>
<p>双向链表可以作为基础结构来实现栈和队列，提供灵活的插入和删除操作。</p>
</blockquote>
</li>
</ul>
<h2 id="heading-5-glib-glist">5 基于 GLib 的 GList 模拟终端命令的历史记录</h2>
<ul>
<li>当我们在 Linux 终端上输入命令时，终端应用程序会记录你输入的命令并形成历史记录，可以使用 <code>history</code> 命令来查看这个历史记录；</li>
<li>在终端上也可以使用上、下箭头键来翻看曾经输入过的前一个或者后一个历史命令，这个命令历史记录给使用终端带来了一定的便利；</li>
<li>本实例模拟了终端输入命令并使用双向链表生成命令的历史记录，按上下箭头键可以查看上一条或下一条命令；</li>
<li>源程序 <a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130004/dllist-glib.c">cmd-history.c</a>(<strong>点击文件名下载源程序</strong>) 基于 GLib 的 GList 模拟了终端历史记录；</li>
<li>该程序首先建立了一个双向链表队列，然后模拟输入命令，链表中的每个节点存储一条命令，命令输入完成后显示最后一条命令，然后按上下箭头键可以从链表中取出上一条命令或者下一条命令并显示在屏幕上；</li>
<li>很显然，使用单向链表实现命令历史记录是不方便的，但使用双向链表就很方便；</li>
<li><p>编译：</p>
<pre><code class="lang-bash">  gcc -Wall -g cmd-history.c -o cmd-history `pkg-config --cflags --libs glib-2.0`
</code></pre>
</li>
<li><p>其中，<code>pkg-config --cflags --libs glib-2.0</code> 的含义在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472383">《使用GLib进行C语言编程的实例》</a>中做过介绍；</p>
</li>
<li>运行：<code>./cmd-history</code></li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/130004/screenshot-of-cmd-history.png" alt="screenshot of cmd-history" /></p>
</li>
<li><p>该程序涉及到终端的操作，使用了结构 <code>struct termios</code>、函数 <code>tcgetattr()</code> 和 <code>tcsetattr()</code>，这些并不在 C 标准库 libc 中，需要启用 GNU 扩展库，所以在程序的开始有 <code>#define _GNU_SOURCE</code></p>
</li>
<li>有关终端操作的相关数据结构、宏定义以及相关函数，并不在本文的讨论之内，请自行参考其它资料；</li>
<li>该程序中还涉及到了使用 ESC 转义符对终端屏幕进行清屏操作，有关 ESC 转义符的含义，请参考另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128767730">《ANSI的ESC转义序列》</a></li>
<li>该程序中还涉及到了从键盘缓冲区读取上、下箭头键的方法，上箭头键返回的编码为 <code>ESC [ A</code>，下箭头键返回的编码为 <code>ESC [ B</code>，这里说明一下有助于读者更快地读懂程序。</li>
</ul>
<hr />
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[C程序员应该知道的最好的8个c编程框架]]></title><description><![CDATA[C 编程框架是开发人员必不可少的工具，编程框架可以为构建强大且性能优异的应用程序提供结构化的基础，本文将对 8 个最佳 C 编程框架和库做出简要的介绍，如果您正在寻找适合初学者的 C 编程框架或旨在进行 C 编程框架比较，相信本文可以给您一定的帮助。

顶级 C 编程框架 – 概述

本文将介绍以下 8 个 C 语言编程框架：





序号框架名称主要特点易于集成下载链接



1GTK全面的小部件集，跨平台支持中等的下载

2Qt跨平台支持，集成开发环境中等的下载

3CMocka轻量级，模...]]></description><link>https://whowin.cn/130005-best-c-programming-frameworks-you-should-know</link><guid isPermaLink="true">https://whowin.cn/130005-best-c-programming-frameworks-you-should-know</guid><category><![CDATA[CMocka]]></category><category><![CDATA[GTK]]></category><category><![CDATA[Qt]]></category><category><![CDATA[libevent]]></category><category><![CDATA[GLib]]></category><category><![CDATA[libuv]]></category><category><![CDATA[ncurses]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Sat, 19 Oct 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730609877554/4ffc5bbe-b217-4974-af3e-ee447472f02b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>C 编程框架是开发人员必不可少的工具，编程框架可以为构建强大且性能优异的应用程序提供结构化的基础，本文将对 8 个最佳 C 编程框架和库做出简要的介绍，如果您正在寻找适合初学者的 C 编程框架或旨在进行 C 编程框架比较，相信本文可以给您一定的帮助。</p>
</blockquote>
<h2 id="heading-c">顶级 C 编程框架 – 概述</h2>
<ul>
<li>本文将介绍以下 8 个 C 语言编程框架：</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>序号</td><td>框架名称</td><td>主要特点</td><td>易于集成</td><td>下载链接</td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>GTK</td><td>全面的小部件集，跨平台支持</td><td>中等的</td><td><a target="_blank" href="https://www.gtk.org/">下载</a></td></tr>
<tr>
<td>2</td><td>Qt</td><td>跨平台支持，集成开发环境</td><td>中等的</td><td><a target="_blank" href="https://www.qt.io/product/framework">下载</a></td></tr>
<tr>
<td>3</td><td>CMocka</td><td>轻量级，模拟支持</td><td>简单的</td><td><a target="_blank" href="https://cmocka.org/">下载</a></td></tr>
<tr>
<td>4</td><td>libevent</td><td>事件驱动架构，异步 I/O 支持</td><td>中等的</td><td><a target="_blank" href="https://libevent.org/doc/">下载</a></td></tr>
<tr>
<td>5</td><td>APR(Apache Portable Runtime)</td><td>跨平台可移植性、线程和内存管理</td><td>中等的</td><td><a target="_blank" href="https://apr.apache.org/">下载</a></td></tr>
<tr>
<td>6</td><td>GLib</td><td>数据结构和实用程序、事件循环和线程支持</td><td>中等的</td><td><a target="_blank" href="https://docs.gtk.org/glib/">下载</a></td></tr>
<tr>
<td>7</td><td>libuv</td><td>异步 I/O、事件循环支持</td><td>中等的</td><td><a target="_blank" href="https://libuv.org/">下载</a></td></tr>
<tr>
<td>8</td><td>ncurses</td><td>独立于终端的界面、窗口和屏幕管理</td><td>中等的</td><td><a target="_blank" href="https://github.com/chavamee/ncframe">下载</a></td></tr>
</tbody>
</table>
</div><h2 id="heading-c-1">最流行的 C 编程框架</h2>
<h3 id="heading-1-gtk">1. GTK</h3>
<ul>
<li><p>GTK 是一个用于创建图形用户界面的开源工具包，主要用于 Linux 环境，因其在开发桌面应用程序方面的灵活性和易用性而闻名；</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>广泛应用于 Linux 桌面环境；</p>
</li>
<li><p>强有力的社区支持；</p>
</li>
<li><p>适用于基于 GNOME 的应用程序；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>全面的小部件集</p>
</li>
<li><p>跨平台支持</p>
</li>
<li><p>可主题化的用户界面</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：</strong>中等</p>
</li>
<li><p><strong>性能：</strong>性能良好，功能丰富</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容 Linux、Windows 和 macOS；</p>
</li>
<li><p>与 GNOME 桌面环境良好集成；</p>
</li>
</ul>
</li>
<li><p><strong>集成难度：</strong>中等</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>Linux 桌面应用程序</p>
</li>
<li><p>跨平台 GUI 开发</p>
</li>
<li><p>GNOME 应用程序</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在 Linux 开发和开源项目开发领域有较大需求；</p>
</li>
<li><p>开发 GNOME 桌面应用程序项目的开发人员通常使用 GTK。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.gtk.org/">立即下载</a></p>
</li>
</ul>
<h3 id="heading-2-qt">2. Qt</h3>
<ul>
<li><p>Qt 是一个强大的跨平台应用程序框架，用于开发具有原生应用的外观和操控感觉的应用程序，它支持大多数的平台，包括桌面、移动和嵌入式系统。</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>适用于跨平台开发；</p>
</li>
<li><p>强大的商业和社区支持；</p>
</li>
<li><p>广泛应用于商业和开源项目；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>跨平台支持</p>
</li>
<li><p>集成开发环境(Qt Creator)</p>
</li>
<li><p>高性能图形</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：</strong>中等</p>
</li>
<li><p><strong>性能：</strong>高性能，优化的图形渲染</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容 Windows、macOS、Linux 和嵌入式系统</p>
</li>
<li><p>支持与各种第三方库集成</p>
</li>
</ul>
</li>
<li><p><strong>集成难度：</strong>中等</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>跨平台应用程序</p>
</li>
<li><p>移动和嵌入式系统</p>
</li>
<li><p>高性能桌面应用程序</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在跨平台开发领域有较大需求；</p>
</li>
<li><p>科技公司和嵌入式系统开发商使用 Qt 比较多。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.qt.io/product/framework">立即下载</a></p>
</li>
</ul>
<h3 id="heading-3cmocka">3.CMocka</h3>
<ul>
<li><p>CMocka 是一个轻量级的 C 语言单元测试框架，旨在易于使用和跨平台移植，它提供了一个用于编写测试的简单 API 并支持模拟测试；</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>在 C 编程社区中很受欢迎；</p>
</li>
<li><p>广泛用于测试 C 代码库；</p>
</li>
<li><p>强有力的社区支持；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>轻巧便携</p>
</li>
<li><p>模拟支持</p>
</li>
<li><p>简单的 API</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：</strong>简单</p>
</li>
<li><p><strong>性能：</strong>以最小的开销实现高性能</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容各种操作系统</p>
</li>
<li><p>与 CI/CD 管道集成</p>
</li>
</ul>
</li>
<li><p><strong>集成简易性：</strong>简单</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>C 代码库的单元测试</p>
</li>
<li><p>嵌入式系统测试</p>
</li>
<li><p>测试驱动开发</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在 C 代码测试和嵌入式系统测试领域有较大需求；</p>
</li>
<li><p>高质量软件开发工程师和测试工程师使用比较多。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://cmocka.org/">立即下载</a></p>
</li>
</ul>
<h3 id="heading-4-libevent">4. libevent</h3>
<ul>
<li><p>libevent 是一个事件通知库，它提供了当文件描述符上发生特定事件时执行回调函数的机制，它广泛用于网络编程和构建事件驱动的应用程序；</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>在网络编程中很流行；</p>
</li>
<li><p>强有力的社区支持；</p>
</li>
<li><p>在性能至关重要的应用程序中被广泛采用；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>事件驱动架构</p>
</li>
<li><p>跨平台支持</p>
</li>
<li><p>异步 I/O 支持</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：</strong>中等</p>
</li>
<li><p><strong>性能：</strong>高性能、低延迟</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容类Unix系统和Windows</p>
</li>
<li><p>与各种网络库集成</p>
</li>
</ul>
</li>
<li><p><strong>集成难度：</strong>中等</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>网络服务器和网络服务</p>
</li>
<li><p>事件驱动编程</p>
</li>
<li><p>高性能 I/O 应用程序</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在网络编程和服务器端开发领域有较大需求；</p>
</li>
<li><p>供构建高性能网络服务的公司使用。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://libevent.org/doc/">立即下载</a></p>
</li>
</ul>
<h3 id="heading-5-aprapache-portable-runtime">5. APR(Apache Portable Runtime)</h3>
<ul>
<li><p>Apache Portable Runtime(APR)是一个库，它提供了一组 API，旨在允许程序无需修改即可在不同的操作系统上运行，通常用于 Apache HTTP Server 和相关项目；</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>在 Apache 项目中广泛使用；</p>
</li>
<li><p>强大的社区和企业支持；</p>
</li>
<li><p>适用于跨平台开发；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>跨平台可移植性</p>
</li>
<li><p>线程和内存管理</p>
</li>
<li><p>网络和文件 I/O</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：*</strong>中等*</p>
</li>
<li><p><strong>性能：</strong>性能良好，具有跨平台可移植性</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容 Windows、macOS、Linux 和类 Unix 系统</p>
</li>
<li><p>与 Apache 项目和其他 C 库集成</p>
</li>
</ul>
</li>
<li><p><strong>集成难度：</strong>中等</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>Web 服务器和网络服务</p>
</li>
<li><p>跨平台应用程序开发</p>
</li>
<li><p>Apache HTTP 服务器及相关项目</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在网络服务器和网络编程方面有较大需求；</p>
</li>
<li><p>从事 Apache 项目和跨平台应用程序的开发人员使用较多。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://apr.apache.org/">立即下载</a></p>
</li>
</ul>
<h3 id="heading-6-glib">6. GLib</h3>
<ul>
<li><p>GLib 是一个低级核心库，是 GTK 和 GNOME 等项目的基础，它为事件循环、线程和对象系统提供数据结构、实用程序和接口。</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>广泛用于 GNOME 和 GTK 项目；</p>
</li>
<li><p>强有力的社区支持；</p>
</li>
<li><p>在 Linux 桌面开发中很受欢迎；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>数据结构和实用程序</p>
</li>
<li><p>事件循环和线程支持</p>
</li>
<li><p>对象系统(GObject)</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：</strong>中等</p>
</li>
<li><p><strong>性能：</strong>高性能，高效的内存管理</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容类 Unix 系统和 Windows</p>
</li>
<li><p>与 GTK 和 GNOME 项目集成</p>
</li>
</ul>
</li>
<li><p><strong>集成难度：</strong>中等</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>GNOME 和 GTK 应用程序</p>
</li>
<li><p>Linux 桌面环境</p>
</li>
<li><p>跨平台实用程序</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在 GNOME 和 Linux 开发方面需求量很大；</p>
</li>
<li><p>开发桌面应用程序和实用程序的开发人员使用较多。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://docs.gtk.org/glib/">立即下载</a></p>
</li>
</ul>
<h3 id="heading-7-libuv">7. libuv</h3>
<ul>
<li><p>libuv 是一个多平台支持库，专注于异步 I/O，它主要用于 Node.js，但在 C 编程中也可用于处理异步任务和事件驱动编程。</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>在 Node.js 生态系统中很受欢迎；</p>
</li>
<li><p>强有力的社区支持；</p>
</li>
<li><p>广泛应用于事件驱动和异步编程；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>异步 I/O</p>
</li>
<li><p>事件循环支持</p>
</li>
<li><p>跨平台兼容性</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：</strong>中等</p>
</li>
<li><p><strong>性能：</strong>高性能，低延迟 I/O 操作</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容类 Unix 系统和 Windows</p>
</li>
<li><p>与 Node.js 和其他事件驱动的应用程序集成</p>
</li>
</ul>
</li>
<li><p><strong>集成难度：</strong>中等</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>事件驱动的应用程序</p>
</li>
<li><p>异步 I/O 操作</p>
</li>
<li><p>实时服务</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在事件驱动编程和实时应用方面有较大需求；</p>
</li>
<li><p>从事 Node.js 和 C/C++ 项目的开发人员使用较多。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://libuv.org/">立即下载</a></p>
</li>
</ul>
<h3 id="heading-8-ncurses">8. ncurses</h3>
<ul>
<li><p>ncurses 是一个编程库，它提供了以独立于终端的方式构建基于文本的用户界面的 API，它广泛用于创建需要用户界面的控制台应用程序和工具。</p>
</li>
<li><p><strong>受欢迎程度：</strong></p>
<ul>
<li><p>在类 Unix 系统中很流行；</p>
</li>
<li><p>强有力的社区支持；</p>
</li>
<li><p>广泛应用于基于终端的应用程序；</p>
</li>
</ul>
</li>
<li><p><strong>主要特点：</strong></p>
<ul>
<li><p>独立于终端的接口</p>
</li>
<li><p>窗口和屏幕管理</p>
</li>
<li><p>键盘和鼠标输入处理</p>
</li>
</ul>
</li>
<li><p><strong>学习曲线：</strong>中等</p>
</li>
<li><p><strong>性能：</strong>性能良好，开销低</p>
</li>
<li><p><strong>兼容性：</strong></p>
<ul>
<li><p>兼容类 Unix 系统和 Windows</p>
</li>
<li><p>适用于各种终端环境</p>
</li>
</ul>
</li>
<li><p><strong>集成难度：</strong>中等</p>
</li>
<li><p><strong>用例和行业采用：</strong></p>
<ul>
<li><p>基于文本的用户界面</p>
</li>
<li><p>控制台应用程序</p>
</li>
<li><p>系统管理工具</p>
</li>
</ul>
</li>
<li><p><strong>就业市场需求：</strong></p>
<ul>
<li><p>在系统管理和控制台应用程序开发方面有较大的需求；</p>
</li>
<li><p>构建基于终端的用户界面的开发人员使用较多。</p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://github.com/chavamee/ncframe">立即下载</a></p>
</li>
</ul>
<h2 id="heading-5bi46keb6zeu6aky">常见问题</h2>
<ol>
<li><p>2024 年最佳 C 编程框架有哪些？</p>
<ul>
<li><p>2024 年最佳使用的 C 编程框架是：</p>
<ul>
<li><p>GTK</p>
</li>
<li><p>Qt</p>
</li>
<li><p>CMocka</p>
</li>
<li><p>libevent</p>
</li>
<li><p>APR(Apache Portable Runtime)</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>在 C 编程框架中我应该寻找哪些关键特性？</p>
<ul>
<li>要寻找的关键特性包括性能、可移植性、易于集成和全面的文档。</li>
</ul>
</li>
<li><p>哪些 C 编程框架对于初学者来说最容易学习？</p>
<ul>
<li>GTK 和 CMocka 是初学者最容易学习的 C 编程框架。</li>
</ul>
</li>
<li><p>不同 C 编程框架的常见用例是什么？</p>
<ul>
<li>常见用例包括 GUI 开发(GTK、Qt)、网络编程(libevent)、单元测试(CMocka)和跨平台应用程序开发(APR)。</li>
</ul>
</li>
<li><p>有哪些可用于快速应用程序开发的轻量级 C 编程框架？</p>
<ul>
<li>一些用于快速应用程序开发的轻量级 C 编程框架是 CMocka、libevent 和 APR。</li>
</ul>
</li>
<li><p>顶级公司使用哪些 C 编程框架？</p>
<ul>
<li>顶级公司使用 Qt 和 GTK 等框架来构建桌面应用程序，并使用 libevent 来提供高性能网络服务。</li>
</ul>
</li>
<li><p>就业市场对 C 编程框架相关技能的需求如何？</p>
<ul>
<li>对 C 编程框架相关技能的需求很强烈，特别是在嵌入式系统、网络编程和系统级开发方面。</li>
</ul>
</li>
</ol>
<hr />
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>
]]></content:encoded></item><item><title><![CDATA[单向链表以及如何使用GLib中的GSList实现单向链表]]></title><description><![CDATA[单向链表是一种基础的数据结构，也是一种简单而灵活的数据结构，本文讨论单向链表的基本概念及实现方法，并着重介绍使用GLib的GSList实现单向链表的方法及步骤，本文给出了多个实际范例源代码，旨在帮助学习基于GLib编程的读者较快地掌握GSList的使用方法，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；本文适合初学者阅读。

1 单向链表及其实现

在文章《使用GLib进行C语言编程的实例》中，简单介绍了 GLib，建议阅读本文前先阅读这篇文章；
单向链表是一...]]></description><link>https://whowin.cn/130003-singly-linked-lists-in-glib</link><guid isPermaLink="true">https://whowin.cn/130003-singly-linked-lists-in-glib</guid><category><![CDATA[单向链表]]></category><category><![CDATA[Linux]]></category><category><![CDATA[GLib]]></category><category><![CDATA[singly linked list in c]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Mon, 19 Aug 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729379316378/7daf96cc-5f97-4f32-8959-30c59855cfbb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>单向链表是一种基础的数据结构，也是一种简单而灵活的数据结构，本文讨论单向链表的基本概念及实现方法，并着重介绍使用GLib的GSList实现单向链表的方法及步骤，本文给出了多个实际范例源代码，旨在帮助学习基于GLib编程的读者较快地掌握GSList的使用方法，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；本文适合初学者阅读。</p>
</blockquote>
<h2 id="heading-1">1 单向链表及其实现</h2>
<ul>
<li>在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472383">《使用GLib进行C语言编程的实例》</a>中，简单介绍了 GLib，建议阅读本文前先阅读这篇文章；</li>
<li>单向链表是一种基础的数据结构，‌它由一系列节点组成，‌每个节点都包含数据部分和指向下一个节点的指针；</li>
<li>这种链表的特点是数据只能在一个方向上流动，‌即从头节点开始，‌通过每个节点的指针依次访问后续节点，‌直到链表的末尾；</li>
<li>在单向链表中，‌头节点是链表的起始点，‌它存储了链表的第一个数据元素以及指向下一个节点的指针；</li>
<li>随后的每个节点都存储了自己的数据和一个指向下一个节点的指针，‌这样形成了链表的结构；</li>
<li>链表的最后一个节点，‌也就是尾节点，‌它的指向下一个节点的指针指向 NULL，‌表示链表的结束。‌</li>
<li><p>单向链表的主要操作包括：‌</p>
<ol>
<li>插入节点：‌可以在链表的头部、‌尾部或中间某个位置插入新的节点。‌</li>
<li>删除节点：‌可以删除链表中的任意节点，‌并通过调整指针来保持链表的完整性。‌</li>
<li>遍历链表：‌通过从头节点开始，‌依次访问每个节点，‌直到到达链表的末尾。‌</li>
<li>查找节点：‌根据特定的条件或值，‌在链表中查找节点。‌</li>
</ol>
</li>
<li><p>单向链表的优势在于其动态的内存分配和高效的插入与删除操作，‌特别是在链表中间或头部插入和删除节点时，‌只需调整指针，‌无需移动其他数据；</p>
</li>
<li>然而，‌单向链表的访问效率较低，‌因为访问特定位置的元素需要从头节点开始遍历。‌</li>
<li>总的来说，‌单向链表是一种简单而灵活的数据结构，‌适用于需要频繁插入和删除操作，‌但访问操作相对较少的场景。‌</li>
<li>下面程序是一个简单的单向链表的 C 语言标准库实现，<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130003/sllist-c.c">sllist-c.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li>编译：<code>gcc -Wall -g sllist-c.c -o sllist-c</code></li>
<li>运行：<code>./sllist-c</code></li>
<li>该程序实现了单向链表的插入、删除、遍历和查找；</li>
<li>该程序首先建立一个单向链表，并在链表中加入 4 个节点，数据分别为：1、2、3、5，然后显示整个链表；</li>
<li>在第 2 个节点(数据为 3，索引号为 2)的后面插入节点，数据为 4，然后显示整个链表；</li>
<li>将第 2 个节点(数据为 3，索引号为 2)删除，然后显示整个链表；</li>
<li>最后释放整个链表；</li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/130003/screenshot-of-sllist-c.png" alt="screenshot of sllist-c" /></p>
</li>
</ul>
<h2 id="heading-2-glib-gslist">2 GLib 中单向链表结构 GSList</h2>
<ul>
<li><a target="_blank" href="https://docs.gtk.org/glib/index.html">GLib API version 2.0 手册</a> (<strong>点击查看手册</strong>)</li>
<li><a target="_blank" href="https://docs.gtk.org/glib/struct.SList.html">GLib API 手册中 GSList 部分</a>  (<strong>点击查看手册</strong>)</li>
<li>在 GLib 中，‌单向链表是通过GSList结构体实现的。‌GSList是一个简单的单向链表结构，‌用于存储各种类型的数据；</li>
<li>GSList 定义如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">GSList</span> {</span>
      gpointer data;
      GSList *next;
  }
</code></pre>
</li>
<li>data 为单向链表的数据指针，可以指向任何类型或结构的数据；</li>
<li>next 为指向该单向链表下一个节点的指针；</li>
<li><p>GLib 为单向链表结构 GSList 的操作提供了大量的函数，本文仅就其中的一部分函数进行介绍；</p>
</li>
<li><p><strong>添加、插入新节点</strong></p>
<ul>
<li><code>g_slist_append()</code> 在单向链表的最后添加一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_append</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, gpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>返回指向单向链表的起始指针；</li>
</ul>
</li>
<li><code>g_slist_prepend()</code> 在单向链表的最前面添加一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_prepend</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, gpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>返回指向单向链表的指针，在单向链表的开头添加一个节点，单向链表的指针是肯定会变化的；</li>
<li>返回该单向链表的起始指针；</li>
</ul>
</li>
<li><code>g_slist_insert()</code> 在单向链表的中间插入一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_insert</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, gpointer data, gint position)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>position - 插入节点的位置，如果是负数或者超过了该单向链表的节点的数量，新节点将插到单向链表的最后；</li>
<li>返回该单向链表的起始指针；</li>
</ul>
</li>
<li><code>g_slist_insert_before()</code> 在包含指定数据的节点之前插入一个新节点；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_insert_before</span><span class="hljs-params">(GSList *slist, GSList *sibling, gpointer data)</span></span>
</code></pre>
<ul>
<li>slist - 指向单向链表的指针</li>
<li>data - 指向添加节点的数据</li>
<li>sibling - 指向一个节点的指针，将在这个节点前插入新节点</li>
<li>返回该单向链表的起始指针；</li>
</ul>
</li>
</ul>
</li>
<li><strong>删除节点</strong><ul>
<li><code>g_slist_remove_link()</code> 从单向链表中删除一个节点，但并不释放该节点占用的内存<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_remove_link</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, GSList *link_)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>link_ - 指向单向链表中一个节点的指针，该节点将被删除；</li>
<li>返回该单向链表的起始指针；</li>
<li>该函数并不释放被删除的节点内存，被删除的节点的 next 指针将指向 NULL，所以可以认为被删除的节点变成了一个只有一个节点的新的单向链表；</li>
</ul>
</li>
<li><code>g_slist_delete_link()</code> 从单向链表中删除一个节点，并释放该节点占用的内存；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_delete_link</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, GSList *link_)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>link_ - 指向单向链表中一个节点的指针，该节点将被删除；</li>
<li>返回该单向链表的起始指针；</li>
<li>该函数与 <code>g_slist_remove_link()</code> 的唯一区别是该函数在删除节点后释放了被删除节点占用的内存；</li>
</ul>
</li>
<li><code>g_slist_remove()</code> 从单向链表中删除指定数据的一个节点，如果链表中有指定数据的节点有多个，将只删除第一个；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_remove</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针</li>
<li>data - 指向要删除节点的数据</li>
<li>返回该单向链表的起始指针；</li>
</ul>
</li>
<li><code>g_slist_remove_all()</code> 从单向链表中删除指定数据的所有节点；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_remove_all</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针</li>
<li>data - 指向要删除节点的数据</li>
<li>返回该单向链表的起始指针；</li>
</ul>
</li>
</ul>
</li>
<li><strong>遍历链表</strong><ul>
<li><code>g_slist_foreach()</code> 遍历单向链表，每个节点都会调用一个指定函数；<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">g_slist_foreach</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, GFunc func, gpointer user_data)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针</li>
<li>func - 一个指向函数的指针，遍历到单向链表的每个节点时，都会调用这个函数；</li>
<li>GFunc 的定义如下：<pre><code class="lang-C"><span class="hljs-keyword">void</span> (* GFunc) (gpointer data, gpointer user_data)
</code></pre>
</li>
<li>GFunc 的定义表明，传递给 func 的参数有两个，一个是 data - 指向当前节点的节点数据指针，另一个就是指向自定义参数 user_data 的指针</li>
<li>user_data - 指针指向调用 func 时传递的用户参数；</li>
</ul>
</li>
</ul>
</li>
<li><strong>查找节点</strong><ul>
<li><code>g_slist_find()</code> 查找链表中包含给定数据的节点；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_find</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针</li>
<li>data - 指向要查找节点的数据</li>
<li>返回在单向链表中找到的节点的指针，如果没有找到相应节点，返回 NULL;</li>
</ul>
</li>
<li><code>g_slist_index()</code> 获取包含给定数据的节点的位置(从 0 开始)；<pre><code class="lang-C">  <span class="hljs-function">gint <span class="hljs-title">g_slist_index</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, gconstpointer data)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>data - 指向要查找节点的数据；</li>
<li>返回数据为 data 的节点在单向链表中的位置(从 0 开始)，如果没找到相应节点，则返回 -1；</li>
</ul>
</li>
<li><code>g_slist_position()</code> 获取给定节点在链表中的位置(从 0 开始)；<pre><code class="lang-C">  <span class="hljs-function">gint <span class="hljs-title">g_slist_position</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, GSList *llink)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>llink - 指向单向链表中的一个节点的指针；</li>
<li>返回 llink 指向的节点在单向链表中的位置(从 0 开始)，如果没找到相应节点，则返回 -1；</li>
<li></li>
</ul>
</li>
</ul>
</li>
<li><strong>释放链表</strong><ul>
<li><code>g_slist_free()</code> 释放链表使用的所有内存，该函数不会释放节点中动态分配的内存；<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">g_slist_free</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>该函数仅释放 GSList 占用的内存，并不释放单向链表中各个节点动态申请的内存，如果链表中有动态申请内存，考虑使用 <code>g_slist_free_full()</code> 或手动释放内存；</li>
</ul>
</li>
<li><code>g_slist_free_full()</code> 释放链表使用的所有内存，并对每个节点的数据调用指定的销毁函数<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">g_slist_free_full</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>, GDestroyNotify free_func)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>free_func - 销毁函数，对单向链表中的每个节点数据将调用该函数，可用于释放节点中动态分配的内存；</li>
<li>GDestroyNotify 的定义如下：<pre><code class="lang-C"><span class="hljs-keyword">void</span> (* GDestroyNotify) (gpointer data)
</code></pre>
</li>
<li>所以在调用 free_func 时会将指向节点数据的指针传递给该函数；</li>
</ul>
</li>
</ul>
</li>
<li><strong>其它</strong><ul>
<li><code>g_slist_length()</code> 获取单向链表的长度；<pre><code class="lang-C">  <span class="hljs-function">guint <span class="hljs-title">g_slist_length</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>返回单向链表中节点的数量。</li>
</ul>
</li>
<li><code>g_slist_last()</code> 获取单向链表的最后一个节点；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_last</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>)</span></span>
</code></pre>
<ul>
<li>list - 指向单向链表的指针；</li>
<li>返回单向链表的最后一个节点的指针，如果单向链表没有节点，则返回 NULL；</li>
</ul>
</li>
<li><code>g_slist_concat()</code>  连接两个单向链表；<pre><code class="lang-C">  <span class="hljs-function">GSList *<span class="hljs-title">g_slist_concat</span><span class="hljs-params">(GSList *list1, GSList *list2)</span></span>
</code></pre>
<ul>
<li>list1 - 指向第 1 个单向链表的指针；</li>
<li>list2 - 指向准备连接到第 1 个单向链表后面的单向链表的指针；</li>
<li>返回连接好的单向链表的指针，</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-3-gslist">3 如何使用 GSList 实现单向链表</h2>
<ul>
<li>文章的一开始有一个使用标准 C 语言函数库的单向链表的实例，使用 GLib 的 GSList 操作单向链表要容易得多；</li>
<li>下面程序是使用 C 语言，基于 GLib 实现的单向链表，<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130003/sllist-glib.c">sllist-glib.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li><p>该程序实现的功能与文章开头的程序 <a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130003/sllist-c.c">sllist-c.c</a> 完全一样，但程序看上去要简洁很多，我们不妨把源程序列在这里</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">print_node</span><span class="hljs-params">(gpointer data, gpointer user_data)</span> </span>{
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%d -&gt; "</span>, GPOINTER_TO_INT(data));
  }

  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">print_list</span><span class="hljs-params">(GSList *<span class="hljs-built_in">list</span>)</span> </span>{
      g_slist_foreach(<span class="hljs-built_in">list</span>, &amp;print_node, <span class="hljs-literal">NULL</span>);
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"NULL\n"</span>);
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      GSList *<span class="hljs-built_in">list</span> = <span class="hljs-literal">NULL</span>;

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Append 4 nodes, the data are 1, 2, 3, 5.\n"</span>);
      <span class="hljs-built_in">list</span> = g_slist_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">1</span>));
      <span class="hljs-built_in">list</span> = g_slist_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">2</span>));
      <span class="hljs-built_in">list</span> = g_slist_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">3</span>));
      <span class="hljs-built_in">list</span> = g_slist_append(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">5</span>));
      print_list(<span class="hljs-built_in">list</span>);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Insert a new node after node with the data 3.\n"</span>);
      <span class="hljs-built_in">list</span> = g_slist_insert(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">4</span>), <span class="hljs-number">3</span>);
      print_list(<span class="hljs-built_in">list</span>);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Remove node with the data 3.\n"</span>);
      <span class="hljs-built_in">list</span> = g_slist_remove(<span class="hljs-built_in">list</span>, GINT_TO_POINTER(<span class="hljs-number">3</span>));
      print_list(<span class="hljs-built_in">list</span>);

      <span class="hljs-comment">// Free the list</span>
      g_slist_free(<span class="hljs-built_in">list</span>);

      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li><p>编译：</p>
<pre><code class="lang-bash">  gcc -Wall -g sllist-glib.c -o sllist-glib `pkg-config --cflags --libs glib-2.0`
</code></pre>
</li>
<li><p>其中，<code>pkg-config --cflags --libs glib-2.0</code> 的含义在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472383">《使用GLib进行C语言编程的实例》</a>中做过介绍；</p>
</li>
<li>运行：<code>./sllist-glib</code></li>
<li>该程序实现了单向链表的插入、删除、遍历和查找；</li>
<li><code>print_list()</code> 中使用 <code>g_slist_foreach()</code> 对链表进行遍历，对链表中的每个节点数据，将调用函数 <code>print_node()</code>；</li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/130003/screenshot-of-sllist-glib.png" alt="screenshot of sllist-glib" /></p>
</li>
</ul>
<h2 id="heading-4">4 单向链表的应用场景</h2>
<ul>
<li><p>单向链表是一种基础的数据结构，具有节点之间按顺序相连的特性，在特定场景下非常有用，以下是一些典型的应用场景：</p>
<ol>
<li><p><strong>动态数据集</strong></p>
<blockquote>
<p>当数据量不确定且频繁增删时，单向链表比数组更适用，它可以方便地在任意位置插入或删除节点，而不需要像数组那样移动大量元素；</p>
</blockquote>
</li>
<li><p><strong>队列和栈的实现</strong></p>
<blockquote>
<p>单向链表常用于实现队列(FIFO)和栈(LIFO)，因为它支持高效的插入和删除操作，尤其在头部或尾部进行操作时性能更好；</p>
</blockquote>
</li>
<li><p><strong>浏览历史记录或撤销操作</strong></p>
<blockquote>
<p>在一些应用程序中，如浏览器的历史记录，单向链表可以用来保存用户的浏览路径或操作步骤，方便逐步返回或撤销；</p>
</blockquote>
</li>
<li><p><strong>分配器管理内存块</strong></p>
<blockquote>
<p>操作系统的内存管理器中，单向链表经常被用于管理空闲的内存块(free lists)，通过链表可以快速地找到可用的内存块；</p>
</blockquote>
</li>
</ol>
</li>
<li><p>由于单向链表的简单结构，它在上述场景下既灵活又高效，特别是当增删操作频繁时。</p>
</li>
</ul>
<h2 id="heading-5-glib-gslist-fifo">5 基于 GLib 的 GSList 实现的 FIFO 队列</h2>
<ul>
<li>FIFO(First Input First Output)队列，也就是先进先出队列，是一种简单的机制，操作一个 FIFO 队列需要队列的头指针和尾指针；</li>
<li>当向 FIFO 队列中加入数据时，数据添加到队列的尾指针处，当从队列中取出数据时，要从队列的头指针处取；</li>
<li>FIFO 队列的重要参数是队列的最大长度，当队列中数据的数量达到队列的最大长度时，则不能再向队列中添加数据；</li>
<li>FIFO 队列的两个重要判断就是判断队列为空(队列中没有数据)或者队列已满(数据数量达到最大长度)；</li>
<li>源程序 <a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130003/sllist-glib.c">queue-glib.c</a>(<strong>点击文件名下载源程序</strong>) 基于 GLib 的 GSList 实现了一个简单的 FIFO 队列；</li>
<li><p>该程序实现了 FIFO 队列的两个基本操作：入队操作和出队操作，基于 GLib 使得程序相当的简单；</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>

  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> QUEUE_MAX_LEN           10</span>
  GSList *queue_head, *queue_tail;        <span class="hljs-comment">// head and tail pointers of the queue</span>
  guint32 queue_max_len;                  <span class="hljs-comment">// Max. length of the queue</span>

  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">queue_init</span><span class="hljs-params">(<span class="hljs-keyword">int</span> maxn)</span> </span>{
      queue_head = queue_tail = <span class="hljs-literal">NULL</span>;
      queue_max_len = maxn;
  }

  <span class="hljs-function">gboolean <span class="hljs-title">queue_put</span><span class="hljs-params">(gpointer data)</span> </span>{
      guint queue_len = g_slist_length(queue_head);           <span class="hljs-comment">// length of the queue</span>
      <span class="hljs-keyword">if</span> (queue_len &gt;= queue_max_len) {
          <span class="hljs-comment">// the queue is full</span>
          <span class="hljs-keyword">return</span> FALSE;
      } <span class="hljs-keyword">else</span> {
          queue_head = g_slist_append(queue_head, data);      <span class="hljs-comment">// append a node with data to the queue</span>
          queue_tail = g_slist_last(queue_head);              <span class="hljs-comment">// get the pointer of last node</span>
      }
      <span class="hljs-keyword">return</span> TRUE;
  }

  <span class="hljs-function">gpointer <span class="hljs-title">queue_get</span><span class="hljs-params">()</span> </span>{
      guint queue_len = g_slist_length(queue_head);           <span class="hljs-comment">// length of the queue</span>
      <span class="hljs-keyword">if</span> (queue_len == <span class="hljs-number">0</span>) {
          <span class="hljs-comment">// the queue is empty</span>
          <span class="hljs-keyword">return</span> <span class="hljs-literal">NULL</span>;
      }
      gpointer queue_data = queue_tail-&gt;data;                 <span class="hljs-comment">// data pointer of the last node</span>
      queue_head = g_slist_delete_link(queue_head, queue_tail);   <span class="hljs-comment">// delete the last node</span>
      <span class="hljs-keyword">if</span> (queue_head == <span class="hljs-literal">NULL</span>) {
          queue_tail = queue_head;                            <span class="hljs-comment">// the queue is empty</span>
      } <span class="hljs-keyword">else</span> {
          queue_tail = g_slist_last(queue_head);              <span class="hljs-comment">// get the pointer of last node</span>
      }
      <span class="hljs-keyword">return</span> queue_data;
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">int</span> argc, <span class="hljs-keyword">char</span> **argv)</span> </span>{
      guint64 len;
      <span class="hljs-keyword">if</span> (argc &gt;= <span class="hljs-number">2</span>) {
          len = g_ascii_strtoll(argv[<span class="hljs-number">1</span>], <span class="hljs-literal">NULL</span>, <span class="hljs-number">10</span>);           <span class="hljs-comment">// Convert string to int</span>
          <span class="hljs-keyword">if</span> (len &lt;= <span class="hljs-number">0</span> || len &gt; (QUEUE_MAX_LEN * <span class="hljs-number">10</span>)) {
              len = QUEUE_MAX_LEN;
          }
      } <span class="hljs-keyword">else</span> {
          len = QUEUE_MAX_LEN;
      }

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Max. length of the queue is %ld.\n"</span>, len);
      queue_init(len);            <span class="hljs-comment">// Initialize the queue</span>

      guint16 i;
      <span class="hljs-comment">// append some data to the queue</span>
      <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; (queue_max_len &lt;&lt; <span class="hljs-number">1</span>); ++i) {
          <span class="hljs-keyword">if</span> (queue_put(GINT_TO_POINTER(i + <span class="hljs-number">1</span>))) {
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Put data %d into the queue.\n"</span>, i + <span class="hljs-number">1</span>);
          } <span class="hljs-keyword">else</span> {
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"The queue is full.\n"</span>);
              <span class="hljs-keyword">break</span>;
          }
      }
      <span class="hljs-comment">// get some data from the queue</span>
      <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; (queue_max_len &lt;&lt; <span class="hljs-number">1</span>); ++i) {
          gpointer queue_data = queue_get();
          <span class="hljs-keyword">if</span> (queue_data != <span class="hljs-literal">NULL</span>) {
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Get data %d from the queue.\n"</span>, GPOINTER_TO_INT(queue_data));
          } <span class="hljs-keyword">else</span> {
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"The queue is empty.\n"</span>);
              <span class="hljs-keyword">break</span>;
          }
      }

      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li>可以看出，用 GLib 实现的 FIFO 队列非常简洁；</li>
<li><p>编译：</p>
<pre><code class="lang-bash">  gcc -Wall -g queue-glib.c -o queue-glib `pkg-config --cflags --libs glib-2.0`
</code></pre>
</li>
<li><p>其中，<code>pkg-config --cflags --libs glib-2.0</code> 的含义在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/142472383">《使用GLib进行C语言编程的实例》</a>中做过介绍；</p>
</li>
<li>运行：<code>./queue-glib 8</code></li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/130003/screenshot-of-queue-glib.png" alt="screenshot of queue-glib" /></p>
</li>
<li><p>该程序并不完整，如果实际运用，至少要加一个互斥锁，以保证 FIFO 队列的线程安全；</p>
</li>
<li>使用 GLib 的 GSList 实现的 FIFO 队列，其中的数据并不需要是相同的数据类型，因为队列中存储的数据的指针，这一点在某些应用场景下会带来一些方便，但也会增加开销，而且在数据使用完成后有可能需要释放额外申请的内存空间。</li>
</ul>
<hr />
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[使用GLib进行C语言编程的实例]]></title><description><![CDATA[本文将讨论使用GLib进行编程的基本步骤，GLib是一个跨平台的，用C语言编写的3个底层库(以前是5个)的集合，GLib提供了多种高级的数据结构，如内存块、双向和单向链表、哈希表等，GLib还实现了线程相关的函数、多线程编程以及相关的工具，例如原始变量访问、互斥锁、异步队列等，GLib主要由GNOME开发；本文是使用GLib编程的入门文章，旨在通过实例帮助希望学习GLib编程的读者较快地入门，本文将给出多个使用GLib库编程范例的源代码，本文程序在 ubuntu 20.04 下编译测试完成，gc...]]></description><link>https://whowin.cn/130002-start-programming-with-glib</link><guid isPermaLink="true">https://whowin.cn/130002-start-programming-with-glib</guid><category><![CDATA[C]]></category><category><![CDATA[GLib]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Fri, 09 Aug 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729372674262/54b84f19-5498-44c5-9ca0-d735b5c9ce74.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>本文将讨论使用GLib进行编程的基本步骤，GLib是一个跨平台的，用C语言编写的3个底层库(以前是5个)的集合，GLib提供了多种高级的数据结构，如内存块、双向和单向链表、哈希表等，GLib还实现了线程相关的函数、多线程编程以及相关的工具，例如原始变量访问、互斥锁、异步队列等，GLib主要由GNOME开发；本文是使用GLib编程的入门文章，旨在通过实例帮助希望学习GLib编程的读者较快地入门，本文将给出多个使用GLib库编程范例的源代码，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；本文适合初学者阅读。</p>
</blockquote>
<h2 id="heading-1">1 前言</h2>
<ul>
<li>GLib 与 glibc 不是一个东西，glibc 是 GNU 实现的一套标准 C 的库函数，而 GLib 是 GTK+ 的一套函数库，如果非要扯上点关系，GLib 依赖于 glibc，不过 Linux 下几乎所有的应用程序都是依赖于 glibc 的；</li>
<li>GLib 最初是 GTK+ 项目(现名为 GTK)的一部分；在发布 GTK+ 版本 2 时，该项目的开发人员决定将非图形用户界面(GUI)的代码从 GTK+ 中分离出来，作为一个单独的库(GLib)发布，以使不需要使用 GUI 的开发人员可以使用这些功能，而无需依赖完整的 GUI 库，这就产生了 GLib 库；</li>
<li>GLib 是一个跨平台库，使用 GLib 编写的应用程序无需进行重大修改即可移植到不同的操作系统上，所以 GLib 不仅可以用在 Linux 下，也可以在 Windows 下使用；</li>
<li>GLib 仍然在不断地开发中，截止到 2024 年 7 月，GNOME 已经发布了 GLib 2.9版。</li>
<li>GLib 包由五个库组成：<ul>
<li>GObject</li>
<li>GLib</li>
<li>GModule</li>
<li>GThread</li>
<li>GIO</li>
</ul>
</li>
<li>这 5 个库全部合并在一个库里，称为 GLib；目前在源代码中，还保留着三个目录：GLib、GObject 和 GIO，GModule、GThread 已经放在 GLib 中了，所以现在通常认为 GLib 是 3 个底层库的集合；</li>
<li>C 语言有一些令程序员头疼的数据类型，比如指针、字符串(以nul为结束符)，GLib 拥有一系列自身的数据类型，较好地解决了这个问题；</li>
<li>GLib 的设计很多都是面向对象的，所有可以使用面向对象的概念进行 C 语言编程；</li>
<li><a target="_blank" href="https://docs.gtk.org/glib/index.html">GLib API version 2.0</a> (<strong>点击查看 API 手册</strong>)</li>
</ul>
<h2 id="heading-2-glib">2 如何将一个程序按 GLib 的方式改写</h2>
<ul>
<li>先使用标准 C 语言按照题目要求编写一个简单的程序，这个题目的原型出自 <a target="_blank" href="https://adventofcode.com/2019/day/1">Advent of Code - 2019</a></li>
<li>题目：宇宙飞船飞回地球需要多少燃料？飞船所需的燃料与飞船的质量有直接的关系，计算方式为：飞船质量 ÷ 3，结果向下取整，再减 2，若结果小于 0，则为 0；<ul>
<li>如果飞船质量为 12，除以 3 为 4，再减 2 则结果为 2；</li>
<li>如果飞船质量为 14，除以 3 向下取整为 4，再减 2 其结果为 2；</li>
<li>如果飞船质量为 1969，则所需燃料为 654；</li>
</ul>
</li>
<li>这个问题的难点在于当我们计算出所需燃料后，实际上飞船的总质量已经改变，变成了飞船质量 + 燃料质量，需要为增加的燃料再补充适当的燃料，所以这实际上是一个递归计算；</li>
<li><p>按照标准 C 语言编写的源程序：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130002/puzzle-2019.c">puzzle-2019.c</a>(<strong>点击文件名下载源程序</strong>)</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdlib.h&gt;</span></span>

  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;math.h&gt;</span></span>

  <span class="hljs-comment">// Calculate the fuel required</span>
  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">calculate_fuel</span><span class="hljs-params">(<span class="hljs-keyword">int</span> weight)</span> </span>{
      <span class="hljs-keyword">int</span> additional_weight = fmax(weight / <span class="hljs-number">3</span> - <span class="hljs-number">2</span>, <span class="hljs-number">0</span>);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Weight: %d\tAdditional weight(Fuel required): %d\n"</span>, weight, additional_weight);
      <span class="hljs-keyword">if</span> (additional_weight &gt; <span class="hljs-number">0</span>) {
          additional_weight += calculate_fuel(additional_weight);
      }

      <span class="hljs-keyword">return</span> additional_weight;
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">int</span> argc, <span class="hljs-keyword">char</span> *argv[])</span> </span>{
      <span class="hljs-keyword">if</span> (argc &lt; <span class="hljs-number">2</span>) {
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"USAGE: %s &lt;mass&gt;\n"</span>, argv[<span class="hljs-number">0</span>]);
          <span class="hljs-built_in">exit</span>(EXIT_FAILURE);
      }

      <span class="hljs-keyword">int</span> mass = atoi(argv[<span class="hljs-number">1</span>]);
      <span class="hljs-keyword">if</span> (mass &lt;= <span class="hljs-number">0</span>) {
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"The mass can not be zero.\n"</span>);
          <span class="hljs-built_in">exit</span>(EXIT_FAILURE);
      }

      <span class="hljs-keyword">int</span> fuel_required = calculate_fuel(mass);
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Total fuel reqired: %d\n"</span>, fuel_required);

      <span class="hljs-keyword">return</span> EXIT_SUCCESS;
  }
</code></pre>
</li>
<li>编译：<code>gcc -Wall -g puzzle-2019.c -o puzzle-2019 -lm</code></li>
<li>因为该程序使用了 fmax()，所以需要连接数学函数库，也就是 <code>-lm</code>；</li>
<li><p>运行：<code>./puzzle-2019 2024</code> (后面跟的参数为飞船质量)</p>
<p>  <img src="https://blog.whowin.net/images/130002/screenshot-of-puzzle2019.png" alt="Screenshot of puzzle-2019" /></p>
</li>
<li><p>把这个程序使用基于 GLib 的方式改写，源程序为：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/130002/puzzle-2019-glib.c">puzzle-2019-glib.c</a>(<strong>点击文件名下载源程序</strong>)</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>

  <span class="hljs-comment">// Calculate the fuel required</span>
  <span class="hljs-function">gint <span class="hljs-title">calculate_fuel</span><span class="hljs-params">(gint weight, GString *message)</span> </span>{
      gint additional_weight = MAX(weight / <span class="hljs-number">3</span> - <span class="hljs-number">2</span>, <span class="hljs-number">0</span>);
      g_autoptr(GString) temp_str = g_string_new(<span class="hljs-literal">NULL</span>);

      g_string_printf(temp_str, <span class="hljs-string">"Weight: %d\tAdditional weight(Fuel required): %d\n"</span>, weight, additional_weight);
      g_string_append(message, temp_str-&gt;str);

      <span class="hljs-keyword">if</span> (additional_weight &gt; <span class="hljs-number">0</span>) {
          additional_weight += calculate_fuel(additional_weight, message);
      }

      <span class="hljs-keyword">return</span> additional_weight;
  }

  <span class="hljs-function">gint <span class="hljs-title">main</span><span class="hljs-params">(gint argc, gchar *argv[])</span> </span>{
      <span class="hljs-keyword">if</span> (argc &lt; <span class="hljs-number">2</span>) {
          g_print(<span class="hljs-string">"USAGE: %s &lt;mass&gt;\n"</span>, argv[<span class="hljs-number">0</span>]);
          <span class="hljs-built_in">exit</span>(EXIT_FAILURE);
      }

      gint mass = g_ascii_strtoll(argv[<span class="hljs-number">1</span>], <span class="hljs-literal">NULL</span>, <span class="hljs-number">10</span>);         <span class="hljs-comment">// Convert an ASCII string to a number</span>
      <span class="hljs-keyword">if</span> (mass &lt;= <span class="hljs-number">0</span>) {
          g_print(<span class="hljs-string">"The mass can not be zero.\n"</span>);
          <span class="hljs-built_in">exit</span>(EXIT_FAILURE);
      }

      GString *message = g_string_new(<span class="hljs-literal">NULL</span>);                  <span class="hljs-comment">// Create a string object</span>
      gint fuel_required = calculate_fuel(mass, message);
      g_print(<span class="hljs-string">"%sTotal fuel required: %d\n"</span>, message-&gt;str, fuel_required);
      g_string_free(message, TRUE);                           <span class="hljs-comment">// Release the string object</span>

      <span class="hljs-keyword">return</span> EXIT_SUCCESS;
  }
</code></pre>
</li>
<li>将这个程序转换成基于 GLib 的程序首先要增加头文件 glib.h<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>
</code></pre>
</li>
<li>原程序中使用 <code>fmax()</code> 函数求两个数的最大值，这里可以使用 <code>MAX(a, b)</code> 来代替，这样，原来的头文件 <code>#include &lt;math.h&gt;</code> 可以去掉，编译时的 <code>-lm</code> 也就不需要了；</li>
<li>后面的修改主要是修改数据类型，GLib 定义了一系列的基本数据类型，一些是明确带有类型长度的，比如：gint8、gint16、guint32、gint64，分别为 8 位整数、16 位整数、32 位无符号整数和 64 位整数；</li>
<li>还有一些数据类型可以直接替换标准 C 中的类型，比如：gint 替换 int，guint 替换 <code>unsigned int</code> 等；</li>
<li>C 语言的字符串是以一个 nul 结尾的字节数组，这其实是有隐患的，比如如果不小心将字符串结尾处的 nul 覆盖，将可能导致灾难性的后果，而且这个字符串在使用上也不方便，比如不知道字符串的长度，添加字符时可能超过定义的数组长度等，为此，GLib 定义了一个新的字符串类型 GString；<ul>
<li>这其实就是一个字符串对象，GString 是一个结构，其定义为：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> _<span class="hljs-title">GString</span> {</span>
      gchar  *str;            <span class="hljs-comment">// 以 nul 结尾的字符串</span>
      gsize len;              <span class="hljs-comment">// 字符串的长度</span>
      gsize allocated_len;    <span class="hljs-comment">// 为字符串 str 实际申请的内存长度</span>
  };
</code></pre>
</li>
<li>GLib 提供了一系列初始化和操作这个字符串对象的方法</li>
<li><code>g_string_new()</code> 用于初始化一个字符串对象，<code>g_string_append()</code> 用于在字符串后面添加一个新字符串，<code>g_string_insert()</code> 在一个字符串中插入一个新字符串，等等；</li>
<li>在对字符串对象进行操作时，用于存放字符串 str 的内存空间会被重新分配，不会导致内存溢出的问题；</li>
<li>在使用完字符串对象之后，要使用 <code>g_string_free()</code> 将字符串对象释放掉；</li>
<li>这个程序中，在主程序中建立了字符串对象 message，在子程序 <code>calculate_fuel()</code> 中，使用 <code>g_string_append()</code> 向 message 中添加了内容，然后在主程序中显示了 message 的内容，最后使用 <code>g_string_free()</code> 释放了字符串对象 message；</li>
<li>在 <code>calculate_fuel()</code> 中，建立了一个字符串对象 temp_str，用于生成一个临时字符串，并将其追加到字符串对象 message 的后边，该字符串对象的建立与 message 略有不同，并且也没有使用 <code>g_string_free()</code> 去释放，这一点下面介绍；</li>
</ul>
</li>
<li><p>GLib 提供了内存自动管理功能</p>
<ul>
<li>GLib 有一个类型 g_autoptr，它可以自动释放所引用对象的内存：系统跟踪对该对象的所有引用，如果删除最后一个引用或者函数退出，则释放内存；</li>
<li>程序中在 <code>calculate_fuel()</code> 中，在建立字符串对象 temp_str 时，使用了 <code>g_autoptr()</code>，使 temp_str 对象成为一个可以自动释放内存的对象，当函数 <code>calculate_fuel()</code> 退出时，temp_str 所占用的内存被自动释放；</li>
</ul>
</li>
<li><p>程序中所有字符串对象的操作都不是必需的，仅用于演示 GLib 的一些特性；</p>
</li>
<li>程序中还使用 <code>g_ascii_strtoll()</code> 取代了 <code>atoi()</code> 函数将一个字符串变量转换成数字，<code>g_ascii_strtoll()</code> 与 <code>strtol()</code> 基本相同；</li>
<li>程序中还使用 GLib 的 <code>g_print()</code> 替换了 <code>printf()</code>，这两个函数基本是相同的功能；</li>
<li>编译(下面有关于编译的一些说明)：<pre><code>  gcc -Wall -g puzzle<span class="hljs-number">-2019</span>-glib.c -o puzzle<span class="hljs-number">-2019</span>-glib <span class="hljs-string">`pkg-config --cflags --libs glib-2.0`</span>
</code></pre></li>
<li>运行：<code>./puzzle-2019-glib 2024</code></li>
</ul>
<h2 id="heading-3-glib">3 安装和编译基于 GLib 的程序</h2>
<ul>
<li>在 Ubuntu 下检查是否安装了 GLib：<pre><code class="lang-bash">  dpkg -l | grep libglib2.0-dev
</code></pre>
</li>
<li>如果没有安装，可按照下面方法安装：<pre><code class="lang-bash">  sudo apt update
  sudo apt install libglib2.0-dev
</code></pre>
</li>
<li><p>还可以编一个小程序显示一下 GLib 的版本号</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">int</span> argc, <span class="hljs-keyword">char</span> *argv[])</span> </span>{
      g_print(<span class="hljs-string">"Glib version: %d.%d.%d\n"</span>, GLIB_MAJOR_VERSION, GLIB_MINOR_VERSION, GLIB_MICRO_VERSION);
      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li>把这段程序命名为 glib-ver.c，用下面的方式进行编译：<pre><code class="lang-bash">  gcc glib-ver.c -o glib-ver `pkg-config --cflags --libs glib-2.0`
</code></pre>
</li>
<li>GLib 支持 <code>pkg-config</code>，亦即可以使用 <code>pkg-config</code> 导出其编译环境，可以在命令行下单独运行 <code>pkg-config --cflags --libs glib-2.0</code> 看看可以得到什么结果；<pre><code class="lang-bash">  pkg-config --cflags --libs glib-2.0
</code></pre>
</li>
<li>这个命令又可以分为两个部分，一部分是编译(compile)所需的环境，另一部分是连接(link)所需的环境，可以在命令行分别运行下列两个命令看看会得到什么结果；<pre><code class="lang-bash">  pkg-config --cflags glib-2.0
  pkg-config --libs glib-2.0
</code></pre>
</li>
<li>Glib 对 pkg-config 的支持使得编译基于 Glib 的应用程序变得比较简单，在编译时只要加上下面的命令即可； <pre><code class="lang-bash">  `pkg-config --cflags --libs glib-2.0`
</code></pre>
</li>
</ul>
<h2 id="heading-4-glib-gstring">4 基于 GLib 的 GString 的一些基本用法</h2>
<ul>
<li><p>前面已经对 GString 做了简单的介绍，其基本使用方法非常简单，下面程序演示了如何在 GString 中添加、删除、插入以及截断字符串，与标准 C 库中的字符串不同，GString 会自动管理内存，在对 GString 操作时不必关心内存的重新分配问题；</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">int</span> argc, <span class="hljs-keyword">char</span> *argv[])</span> </span>{
      <span class="hljs-comment">// 创建一个新的 GString</span>
      GString *gstring = g_string_new(<span class="hljs-string">""</span>);

      <span class="hljs-comment">// 追加字符串到 GString</span>
      g_string_append(gstring, <span class="hljs-string">"Hello, "</span>);
      g_string_append(gstring, <span class="hljs-string">"GString!"</span>);

      <span class="hljs-comment">// 打印 GString 的内容</span>
      g_print(<span class="hljs-string">"Initial GString content: %s\n"</span>, gstring-&gt;str);

      <span class="hljs-comment">// 在 GString 的指定位置插入字符串</span>
      g_string_insert(gstring, <span class="hljs-number">7</span>, <span class="hljs-string">"GLib "</span>);

      <span class="hljs-comment">// 再次打印 GString 的内容</span>
      g_print(<span class="hljs-string">"GString content after insert: %s\n"</span>, gstring-&gt;str);

      <span class="hljs-comment">// 替换 GString 中的子串</span>
      g_string_erase(gstring, <span class="hljs-number">7</span>, <span class="hljs-number">5</span>);          <span class="hljs-comment">// 删除 "GLib "</span>
      g_string_insert(gstring, <span class="hljs-number">7</span>, <span class="hljs-string">"World "</span>);  <span class="hljs-comment">// 在指定位置插入“World ”</span>

      <span class="hljs-comment">// 再次打印 GString 的内容</span>
      g_print(<span class="hljs-string">"GString content after replace: %s\n"</span>, gstring-&gt;str);

      <span class="hljs-comment">// 截断 GString 到指定长度</span>
      g_string_truncate(gstring, <span class="hljs-number">12</span>);

      <span class="hljs-comment">// 打印截断后的 GString 内容</span>
      g_print(<span class="hljs-string">"GString content after truncate: %s\n"</span>, gstring-&gt;str);

      <span class="hljs-comment">// 释放 GString 及其内容</span>
      g_string_free(gstring, TRUE);

      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li>GString 在使用完后必需使用 g_string_free() 进行释放，g_string_free() 的定义如下：<pre><code class="lang-C">  <span class="hljs-function">gchar *<span class="hljs-title">g_string_free</span><span class="hljs-params">(GString *<span class="hljs-built_in">string</span>, gboolean free_segment)</span></span>
</code></pre>
<ul>
<li>GString 的数据结构在前面已经介绍过了；</li>
<li>当 free_segment 为 TRUE 时，这个函数不仅会释放 GString 结构本身，也会释放掉 GString 中的 str 占用的内存，否则该函数仅释放 GString 结构本身，而不释放其中的字符串 str；</li>
</ul>
</li>
<li><p>下面的这段程序演示了在释放了 GString 后如何继续使用原 GString 中以 nul 结尾的字符串；</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;glib.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">(<span class="hljs-keyword">int</span> argc, <span class="hljs-keyword">char</span> *argv[])</span> </span>{
      <span class="hljs-comment">// 创建一个新的 GString</span>
      GString *gstring = g_string_new(<span class="hljs-string">""</span>);

      <span class="hljs-comment">// 追加字符串到 GString</span>
      g_string_append(gstring, <span class="hljs-string">"Hello World!"</span>);
      g_print(<span class="hljs-string">"Initial GString content: %s\n"</span>, gstring-&gt;str);

      <span class="hljs-comment">// 取出 GString 中字符串的指针</span>
      gchar *p = gstring-&gt;str;
      <span class="hljs-comment">// 以 FALSE 的方式释放 GString</span>
      g_string_free(gstring, FALSE);

      <span class="hljs-comment">// 释放 GString 后再次显示原 GString 中的字符串</span>
      g_print(<span class="hljs-string">"String after releasing GString: %s\n"</span>, p);
      <span class="hljs-keyword">return</span> EXIT_SUCCESS;
  }
</code></pre>
</li>
</ul>
<h2 id="heading-5">5 结束语</h2>
<ul>
<li>此篇文章仅仅是 GLib 的介绍文章，远没有涉及 GLib 的重要部分，GLib 能做的远不止像本文介绍的那样去替代 C 标准库中的函数；</li>
<li>GLib 的 GObject 可以让程序员使用 C 进行面向对象的编程，听起来有点天方夜谭的感觉；</li>
<li>C 语言中最令人头疼的无疑是指针，内存指针的操作失误会使 C 语言的程序崩溃，最近波及全球的微软蓝屏事件据初步报道就和 C 语言的内存指针相关，GLib 提供了一些宏来辅助指针的操作，同时，GLib 还提供了一系列内存管理的函数和宏，使得内存管理和指针应用更加安全；</li>
<li>GLib 提供了一套丰富的类型系统，能够有效地减少编程错误，提高代码的可读性和可维护性，GLib 还提供了多种丰富的数据结构，如链表(单向链表、双向链表)、哈希表、动态数组等，这些数据结构能够高效地存储和管理数据，提升程序的性能；</li>
<li>GLib 还提供了许多实用的功能支持，如事件循环、线程操作、动态链接库的操作、出错处理和日志等，这些功能使得基于 GLib 开发的应用程序能够更加方便地处理并发事件、管理资源、处理错误等，提高了程序的健壮性和稳定性；</li>
<li>GLib 良好的可移植性也是广受赞誉的，基于 GLib 编写的应用程序可以轻松地在 Linux、Unix 以及 Windows 下运行，如果你要编写跨平台的应用程序，可以选择基于 GLib 编程。</li>
<li>在以后得文章中奖尽可能地介绍更多的基于 GLib 编程的方法和范例。</li>
</ul>
<hr />
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>
]]></content:encoded></item><item><title><![CDATA[Linux下使用libiw进行无线信号扫描的实例]]></title><description><![CDATA[打开电脑连接wifi是一件很平常的事情，但这些事情通常都是操作系统下的wifi管理程序替我们完成的，如何在程序中扫描wifi信号其实资料并不多，前面已经有两篇文章介绍了如何使用ioctl()扫描wifi信号，但其实在Linux下有一个简单的库对这些ioctl()的操作进行了封装，这个库就是libiw，使用libiw可以简化编程，本文介绍了如果使用libiw对wifi信号进行扫描的基本方法，本文将给出完整的源代码，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；尽...]]></description><link>https://whowin.cn/180026-wifi-scanner-with-libiw</link><guid isPermaLink="true">https://whowin.cn/180026-wifi-scanner-with-libiw</guid><category><![CDATA[libiw]]></category><category><![CDATA[Linux]]></category><category><![CDATA[网络编程]]></category><category><![CDATA[802.11]]></category><category><![CDATA[wifi]]></category><category><![CDATA[无线网络]]></category><category><![CDATA[ioctl]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Thu, 04 Jul 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729272282445/b1212d50-7d41-4a94-9beb-3ff1d4381370.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>打开电脑连接wifi是一件很平常的事情，但这些事情通常都是操作系统下的wifi管理程序替我们完成的，如何在程序中扫描wifi信号其实资料并不多，前面已经有两篇文章介绍了如何使用ioctl()扫描wifi信号，但其实在Linux下有一个简单的库对这些ioctl()的操作进行了封装，这个库就是libiw，使用libiw可以简化编程，本文介绍了如果使用libiw对wifi信号进行扫描的基本方法，本文将给出完整的源代码，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；尽管本文内容主要涉及无线网络，但读者并不需要对 802.11 标准有所了解。</p>
</blockquote>
<h2 id="heading-1">1 前言</h2>
<ul>
<li>前面已经有两篇文章介绍了如何扫描 wifi 信号，<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的实例(一)》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/137711398">《使用ioctl扫描wifi信号获取信号属性的实例(二)》</a>，这两篇文章均是使用 <code>ioctl()</code> 完成的 wifi 信号扫描；</li>
<li>本文介绍使用 libiw 库进行 wifi 信号扫描的方法，比较前两篇文章中介绍的方法，编程上更加简单；</li>
<li>实际上使用 libiw 扫描 wifi 信号，本质上还是使用 <code>ioctl()</code>；</li>
<li>在大多数以 Linux 内核为基础的操作系统中，都是包含 WE(Wireless Extensions) 的，WE 实际就是一组在用户空间操作无线网卡驱动程序的一组 API，库 libiw 是对 WE 的一个封装；</li>
<li>尽管库 libiw 可以给 wifi 编程带来一定的便利，但其实这是一个已经过时的库，这个库的最后更新日期是 2009 年，尽管如此，现在的绝大多数无线网卡驱动程序仍然支持 WE，所以我们仍然可以使用 libiw 进行 wifi 编程；</li>
<li>一些常用的 wifi 工具软件是使用 WE 实现的，比如：iwlist、iwconfig 等，由此也可以看出 WE 在 wifi 编程中仍然占有很重要的位置；</li>
</ul>
<h2 id="heading-2-libiw">2 安装 libiw</h2>
<ul>
<li>libiw 包含在开源项目 <a target="_blank" href="https://github.com/HewlettPackard/wireless-tools">Wireless Tools</a> 中，可以自行编译 libiw 库或者使用 apt 安装</li>
<li>在 ubuntu 上使用 apt 安装 libiw<pre><code class="lang-bash">  sudo apt update
  sudo apt install libiw-dev
</code></pre>
</li>
<li><p>自行编译 libiw</p>
<ul>
<li>克隆项目：<pre><code>  git clone https:<span class="hljs-comment">//github.com/HewlettPackard/wireless-tools</span>
</code></pre></li>
<li>编译动态库 libiw.so.29<pre><code class="lang-bash">  gcc -Os -W -Wall -Wstrict-prototypes -Wmissing-prototypes -Wshadow -Wpointer-arith -Wcast-qual -Winline -I. -MMD -fPIC -c -o iwlib.so iwlib.c
  gcc -shared -o libiw.so.29 -Wl,-soname,libiw.so.29  -lm -lc iwlib.so
</code></pre>
</li>
<li>编译静态库 libiw.a<pre><code>  rm -f libiw.a
  ar cru libiw.a iwlib.so
  ranlib libiw.a
</code></pre></li>
</ul>
</li>
<li><p>要说明的是，使用 apt 安装的 libiw，其动态链接库为 <code>libiw.so.30</code>，但是使用这个项目的开源版本编译出来的动态链接库为 <code>libiw.so.29</code>，版本略有不同，<code>libiw.so.30</code> 更新于 2009 年，而开源项目的源代码更新于 2007 年，二者还是略有差别的；</p>
</li>
</ul>
<h2 id="heading-3-wifi-libiw">3 wifi扫描涉及的 libiw 函数和数据结构</h2>
<ul>
<li>使用 libiw 编写 wifi 扫描程序，比起使用 <code>ioctl()</code> 要容易一些，但可以获得的信息远不如使用 <code>ioct()</code> 直接扫描 wifi 获得的信息多；</li>
<li><p>以下一些 libiw 中的函数和数据结构在本文的实例程序中会使用到，这些函数的更详细的说明，也可以查看另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/140196003">《libiw中的函数说明》</a>；</p>
</li>
<li><p><code>int iw_sockets_open(void)</code></p>
<blockquote>
<p>这个函数逐个尝试使用不同的协议族建立 socket，直至成功或者全部失败，尝试的顺序为：<code>AF_INET、AF_IPX、AF_AX25、AF_APPLETALK</code>，绝大多数情况下可以使用 <code>AF_INET</code> 建立 socket，成功返回 socket，失败则返回 -1；</p>
</blockquote>
</li>
<li><p><code>void iw_enum_devices(int skfd, iw_enum_handler fn, char *args[], int count)</code></p>
<blockquote>
<p>这个函数会列举出系统中的所有网络接口，每找到一个网络接口就会调用一次函数 <code>fn()</code>，<code>args[]</code> 是传给 <code>fn()</code> 的参数数组，count 是参数的数量，本文利用这个函数在所有网络接口中找到无线网络接口，然后对无线网络接口进行扫描；</p>
<p>其查找无线网络接口的原理是：当 <code>iw_enum_devices()</code> 函数找到一个网络接口时会调用 <code>fn()</code>，在函数 <code>fn()</code> 辨别该网络接口是否为无线网络接口；</p>
</blockquote>
</li>
<li><p><code>int iw_get_range_info(int skfd, const char *ifname, iwrange *range)</code></p>
<blockquote>
<p>使用这个函数主要是为了得到当前系统 WE(Wireless Extensions) 的版本号，在调用 iw_scan() 对无线网络接口进行扫描时，需要 WE 的版本号作为参数，这是因为不同版本的 WE 在进行扫描时方法略有差异；</p>
</blockquote>
</li>
<li><p><code>int iw_scan(int skfd, char *ifname, int we_version, wireless_scan_head *context)</code></p>
<blockquote>
<p>对无线网络接口进行扫描，ifname 为网络接口名称，扫描结果放在 context 中，context 是 <code>struct wireless_scan_head</code> 的指针，里面存放了扫描结果链表的首指针 result，<code>struct wireless_scan_head</code> 定义在 iwlib.h 中，其具体结构如下：</p>
</blockquote>
<pre><code class="lang-C">  <span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_scan_head</span>
  {</span>
      wireless_scan   *result;    <span class="hljs-comment">/* Result of the scan */</span>
      <span class="hljs-keyword">int</span>             retry;      <span class="hljs-comment">/* Retry level */</span>
  } wireless_scan_head;
</code></pre>
<blockquote>
<p>其中的 wireless_scan 就是 <code>struct wireless_scan</code>，同样定义在 iwlib.h 中，其具体定义如下：</p>
</blockquote>
<pre><code class="lang-C">  <span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_scan</span>
  {</span>
      <span class="hljs-comment">/* Linked list */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_scan</span> *<span class="hljs-title">next</span>;</span>

      <span class="hljs-comment">/* Cell identifiaction */</span>
      <span class="hljs-keyword">int</span>         has_ap_addr;
      sockaddr    ap_addr;        <span class="hljs-comment">/* Access point address */</span>

      <span class="hljs-comment">/* Other information */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_config</span>  <span class="hljs-title">b</span>;</span>  <span class="hljs-comment">/* Basic information */</span>
      iwstats stats;              <span class="hljs-comment">/* Signal strength */</span>
      <span class="hljs-keyword">int</span>     has_stats;
      iwparam maxbitrate;         <span class="hljs-comment">/* Max bit rate in bps */</span>
      <span class="hljs-keyword">int</span>     has_maxbitrate;
  } wireless_scan;
</code></pre>
<blockquote>
<p>这是一个结构链表，next 指向链表的下一项，链表中的每一项表示一个扫描到的信号，在 struct wireless_scan 中，当 has_ap_addr 不为 0 时，ap_addr 中存放着该信号对应的 AP 的 MAC 地址；当 has_maxbitrate 不为 0 时，maxbitrate 中存放着该信号所支持的最大传输速率；当 has_stats 不为 0 时，stats 中存放着信号强度；信号的 SSID、工作频率等，存放在 b 中，b 是一个 struct wireless_scan 结构，定义在 iwlib.h 中，其具体定义如下：</p>
</blockquote>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_config</span>
  {</span>
      <span class="hljs-keyword">char</span>    name[IFNAMSIZ + <span class="hljs-number">1</span>]; <span class="hljs-comment">/* Wireless/protocol name */</span>
      <span class="hljs-keyword">int</span>     has_nwid;
      iwparam nwid;               <span class="hljs-comment">/* Network ID */</span>
      <span class="hljs-keyword">int</span>     has_freq;
      <span class="hljs-keyword">double</span>  freq;               <span class="hljs-comment">/* Frequency/channel */</span>
      <span class="hljs-keyword">int</span>     freq_flags;
      <span class="hljs-keyword">int</span>     has_key;
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span>   key[IW_ENCODING_TOKEN_MAX]; <span class="hljs-comment">/* Encoding key used */</span>
      <span class="hljs-keyword">int</span>     key_size;           <span class="hljs-comment">/* Number of bytes */</span>
      <span class="hljs-keyword">int</span>     key_flags;          <span class="hljs-comment">/* Various flags */</span>
      <span class="hljs-keyword">int</span>     has_essid;
      <span class="hljs-keyword">int</span>     essid_on;
      <span class="hljs-keyword">char</span>    essid[IW_ESSID_MAX_SIZE + <span class="hljs-number">2</span>];   <span class="hljs-comment">/* ESSID (extended network) */</span>
      <span class="hljs-keyword">int</span>     essid_len;
      <span class="hljs-keyword">int</span>     has_mode;
      <span class="hljs-keyword">int</span>     mode;               <span class="hljs-comment">/* Operation mode */</span>
  }
</code></pre>
<blockquote>
<p>实际上这里面的一些项很多 WE 都已经不再支持，本例中我们会用到以下字段：name、freq、essid，其它字段很多 WE 并不返回数据；</p>
</blockquote>
</li>
<li><p><code>void iw_sockets_close(int skfd)</code></p>
<blockquote>
<p>这个函数将关闭一个使用 iw_sockets_open() 打开的 socket，其实这个函数和 close() 无异，所以你可以直接用 close(skfd) 关闭 socket。</p>
</blockquote>
</li>
</ul>
<h2 id="heading-4-libiw-wifi">4 使用 libiw 扫描 wifi 信号的基本思路</h2>
<ul>
<li><p><strong>扫描 wifi 信号的基本流程</strong></p>
<ol>
<li>使用 <code>iw_sockets_open()</code> 建立一个socket</li>
<li>使用 <code>iw_enum_devices()</code> 枚举所有的网络设备，并从中找到无线网卡设备名称</li>
<li>使用 <code>iw_get_range_ino()</code> 获取 WE 的版本号</li>
<li>使用 <code>iw_scan()</code> 启动 wifi 信号扫描并获取扫描结果</li>
<li>使用 <code>iw_sockets_close()</code> 关闭使用 <code>iw_sockets_open()</code> 打开的 socket</li>
</ol>
</li>
<li><p><strong>使用 iw_scan() 获取扫描结果的问题</strong></p>
<ul>
<li>与使用 <code>ioctl()</code> 进行 wifi 信号扫描(请参考<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的实例(一)》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/137711398">《使用ioctl扫描wifi信号获取信号属性的实例(二)》</a>)相比，使用 libiw 扫描 wifi 信号得到的信息其实要少一些，这也是这种方法的最主要的不足；</li>
<li>这主要是因为 <code>struct wireless_scan</code> 这个结构可以容纳的信息十分有限</li>
<li><code>iw_scan()</code> 需要 4 个参数(见上节关于该函数的说明)，其中第 4 个参数是 <code>wireless_scan_head *context</code>；</li>
<li>wireless_scan_head 和其中的 wireless_scan 的数据结构在上节已经做了详细的介绍，扫描到的 wifi 信号的数据实际存放在一个 wireless_scan 结构链表中；</li>
<li>受结构所限，很多信息都不得不减少，比如一般一个信号可以支持很多连接速率，但是该结构中只给出一个 maxbitrate 字段，所以无法所有信号支持的传输速率</li>
<li>在比如一个信号通常可以支持多个不同的信道，每个信道为不同的工作频率，使用 libiw 扫描信号只能得到其列表中的最后一个频率值；</li>
<li>这些缺失的数据在使用 ioctl() 进行信号扫描时都是可以得到的。</li>
</ul>
</li>
</ul>
<h2 id="heading-5-libiw-wifi">5 使用 libiw 扫描 wifi 信号的实例</h2>
<ul>
<li>完整的源代码，文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180026/iw-scanner.c">iw-scanner.c</a>(<strong>点击文件名下载源程序</strong>)；</li>
<li>这个程序比起在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的实例(一)》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/137711398">《使用ioctl扫描wifi信号获取信号属性的实例(二)》</a>的程序要简单得多；</li>
<li><p>简述一下程序的基本流程：</p>
<ul>
<li>使用 <code>iw_sockets_open()</code> 为内核打开一个 socket；</li>
<li>使用 <code>iw_enum_devices()</code> 枚举所有的网络接口，在调用 <code>iw_enum_devices()</code> 设置了一个回调函数 <code>iw_ifname()</code>，并将一个字符缓冲区的指针 ifname 传给该回调函数；</li>
<li>当 <code>iw_enum_devices()</code> 发现一个网络接口时，便会调用 <code>iw_ifname()</code>，<code>iw_ifname()</code> 通过一个 ioctl() 的调用来判断该接口是否为无线网络接口，如果是，则将其接口名称复制到参数 ifname 中，并返回 1，否则返回 0；</li>
<li>执行完 <code>iw_enum_devices()</code> 如果 ifname 中没有接口名称，则表示当前系统中么有无线网络接口，直接退出程序，如果 ifname 中已有网络接口名称，则使用该接口准备开始进行 wifi 信号扫描；</li>
<li>使用 <code>iw_get_range_info()</code> 获取该无线网络接口的 range 信息，实际上只是需要获得 WE 的版本号，以便在下面使用；</li>
<li>使用 libiw 的 <code>iw_scan()</code> 启动 wifi 信号扫描非常容易，<code>iw_scan()</code> 需要四个参数，第一个是用 <code>iw_sockets_open()</code> 打开的 socket，第二个是我们在上面获取的无线网络接口的名称，第三个是 WE 的版本号，第四个是用于存储扫描结果链表的头指针；</li>
<li><code>iw_scan()</code> 的执行是需要 root 权限的，所以这个程序要以 sudo 的方式运行，另外 <code>iw_scan()</code> 的执行也需要一定时间；</li>
<li>获取扫描结果，可以通过检查扫描结果链表的头指针来判断是否有扫描结果，如果这个指针不为空则表示有扫描结果；</li>
<li>接下来就是遍历整个扫描结果链表，并将其中的信息显示出来，比较文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的实例(一)》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/137711398">《使用ioctl扫描wifi信号获取信号属性的实例(二)》</a>中的扫描结果，这里获得的信息比较有限；</li>
<li>最后使用 <code>iw_sockets_close()</code> 关闭打开的 socket。</li>
</ul>
</li>
<li><p>程序中有些不明确的概念或做法，请参考文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的实例(一)》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/137711398">《使用ioctl扫描wifi信号获取信号属性的实例(二)》</a></p>
</li>
<li>编译：<code>gcc -Wall iw-scanner.c -o iw_scanner -liw</code></li>
<li>运行：<code>sudo ./iw-scanner</code></li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/180026/screenshot-of-iwscanner.png" alt="Screenshot of iw-scanner" /></p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>




]]></content:encoded></item><item><title><![CDATA[libiw中的函数说明]]></title><description><![CDATA[打开电脑连接wifi是一件很平常的事情，但这些事情通常都是操作系统下的wifi管理程序替我们完成的，如何在程序中连接指定的wifi其实很少有资料介绍，在网络专栏的文章中，有两篇是关于wfi编程的文章，其中对无线网卡的操作都是通过ioctl()完成的，显得有些繁琐和晦涩，但其实WE(Wireless Extensions)有一个简单的库libiw，这个库的实现也是使用ioctl()，但是经过封装后，会使wifi编程变得容易一些，本文为一篇资料类的文章，主要描述libiw中API的调用方法。

1 ...]]></description><link>https://whowin.cn/180027-libiw-functions</link><guid isPermaLink="true">https://whowin.cn/180027-libiw-functions</guid><category><![CDATA[Linux]]></category><category><![CDATA[网络编程]]></category><category><![CDATA[wifi]]></category><category><![CDATA[无线网络]]></category><category><![CDATA[libiw]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Fri, 12 Apr 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729273333178/99508342-20fc-4ba9-9848-deead667dd5b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>打开电脑连接wifi是一件很平常的事情，但这些事情通常都是操作系统下的wifi管理程序替我们完成的，如何在程序中连接指定的wifi其实很少有资料介绍，在网络专栏的文章中，有两篇是关于wfi编程的文章，其中对无线网卡的操作都是通过ioctl()完成的，显得有些繁琐和晦涩，但其实WE(Wireless Extensions)有一个简单的库libiw，这个库的实现也是使用ioctl()，但是经过封装后，会使wifi编程变得容易一些，本文为一篇资料类的文章，主要描述libiw中API的调用方法。</p>
</blockquote>
<h2 id="heading-1">1 前言</h2>
<ul>
<li>在大多数以 Linux 内核为基础的操作系统中，都是包含 WE(Wireless Extensions) 的，WE 实际就是一组在用户空间操作无线网卡驱动程序的一组 API，库 libiw 是对 WE 的一个封装；</li>
<li>尽管库 libiw 可以给 wifi 编程带来一定的便利，但其实这是一个已经过时的库，这个库的最后更新日期是 2009 年，尽管如此，现在的绝大多数无线网卡驱动程序仍然支持 WE，所以我们仍然可以使用 libiw 进行 wifi 编程；</li>
<li>一些常用的 wifi 工具软件是使用 WE 实现的，比如：iwlist、iwconfig 等，由此也可以看出 WE 在 wifi 编程中仍然占有很重要的位置；</li>
<li>WE 的基本实现是使用 ioctl()，前面的两篇文章 <a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的实例(一)》</a> 和 <a target="_blank" href="https://blog.csdn.net/whowin/article/details/137711398">《使用ioctl扫描wifi信号获取信号属性的实例(二)》</a> 都是使用 ioctl() 实现的；</li>
<li>libiw 是对 WE 的一个封装，使编程者不必直接面对 ioctl() 中复杂的调用方法和返回数据，可以给编程带来一些便利；</li>
<li>之所以要写这样一份资料类的文章，是因为完整的 libiw 中 API 调用方法的资料几乎没有，libiw API 的使用方法几乎只能通过阅读源码来学习，很不方便；</li>
<li><p>本文是通过阅读 libiw 的源码 <code>iwlib.c</code> 和 <code>iwlib.h</code> 以及一些 wifi 工具软件的源码总结而成；</p>
</li>
<li><p>项目 <a target="_blank" href="https://github.com/HewlettPackard/wireless-tools">Wireless Tools</a> 中的 <code>iwlib.c</code> 和 <code>iwlib.h</code> 组成了 libiw</p>
</li>
<li>克隆项目：<pre><code>  git clone https:<span class="hljs-comment">//github.com/HewlettPackard/wireless-tools</span>
</code></pre></li>
<li>编译静态库 libiw.so.29<pre><code class="lang-bash">  gcc -Os -W -Wall -Wstrict-prototypes -Wmissing-prototypes -Wshadow -Wpointer-arith -Wcast-qual -Winline -I. -MMD -fPIC -c -o iwlib.so iwlib.c
  gcc -shared -o libiw.so.29 -Wl,-soname,libiw.so.29  -lm -lc iwlib.so
</code></pre>
</li>
<li><p>编译静态库 libiw.a</p>
<pre><code>  rm -f libiw.a
  ar cru libiw.a iwlib.so
  ranlib libiw.a
</code></pre></li>
<li><p>在 ubuntu 上，要使用 libiw 需要单独安装，安装方法非常容易：</p>
<pre><code class="lang-bash">  sudo apt update
  sudo apt install libiw-dev
</code></pre>
</li>
<li><p>要说明的是，使用 apt 安装的 libiw，看到的动态链接库为 <code>libiw.so.30</code>，但是使用这个项目的开源版本编译出来的动态链接库为 <code>libiw.so.29</code>，版本略有不同，从 <code>iwlib.h</code> 看，<code>libiw.so.30</code> 更新于 2009 年，而开源项目的源代码更新于 2007 年，二者还是有一些差距的；</p>
</li>
<li>从 <code>libiw.h</code> 看，<code>libiw.so.30</code> 中包含了一些 <code>libiw.so.29</code> 中没有的函数，但总体看，区别不是很大。</li>
</ul>
<h2 id="heading-2-52">2. 库函数列表(共计 52 个)</h2>
<ul>
<li><strong>SOCKET SUBROUTINES(2 个)</strong><ol>
<li><code>int iw_sockets_open(void);</code></li>
<li><code>void iw_enum_devices(int skfd, iw_enum_handler fn, char *args[], int count);</code></li>
</ol>
</li>
<li><strong>WIRELESS SUBROUTINES(6个)</strong><ol>
<li><code>int iw_get_kernel_we_version(void);</code></li>
<li><code>int iw_print_version_info(const char *toolname);</code></li>
<li><code>int iw_get_range_info(int skfd, const char *ifname, iwrange *range);</code></li>
<li><code>int iw_get_priv_info(int skfd, const char *ifname, iwprivargs **    ppriv);</code></li>
<li><code>int iw_get_basic_config(int skfd, const char *ifname, wireless_config *info);</code></li>
<li><code>int iw_set_basic_config(int skfd, const char *ifname, wireless_config *info);</code></li>
</ol>
</li>
<li><strong>PROTOCOL SUBROUTINES(1个)</strong><ol>
<li><code>int iw_protocol_compare(const char *protocol1, const char *protocol2);</code></li>
</ol>
</li>
<li><strong>ESSID SUBROUTINES(2个)</strong><ol>
<li><code>void iw_essid_escape(char *dest, const char *src, const int slen);</code></li>
<li><code>int iw_essid_unescape(char *dest, const char *src);</code></li>
</ol>
</li>
<li><strong>FREQUENCY SUBROUTINES(7个)</strong><ol>
<li><code>void iw_float2freq(double in, iwfreq *out);</code></li>
<li><code>double iw_freq2float(const iwfreq *in);</code></li>
<li><code>void iw_print_freq_value(char *buffer, int buflen, double freq);</code></li>
<li><code>void    iw_print_freq(char *buffer, int buflen, double freq, int channel, int freq_flags);</code></li>
<li><code>int iw_freq_to_channel(double freq, const struct iw_range *range);</code></li>
<li><code>int iw_channel_to_freq(int channel, double *pfreq, const struct iw_range *range);</code></li>
<li><code>void iw_print_bitrate(char *buffer, int buflen, int bitrate);</code></li>
</ol>
</li>
<li><strong>POWER SUBROUTINES(3个)</strong><ol>
<li><code>int iw_dbm2mwatt(int in);</code></li>
<li><code>int iw_mwatt2dbm(int in);</code></li>
<li><code>void iw_print_txpower(char *buffer, int buflen, struct iw_param *txpower);</code></li>
</ol>
</li>
<li><strong>STATISTICS SUBROUTINES(2个)</strong><ol>
<li><code>int iw_get_stats(int skfd, const char *ifname, iwstats *stats, const iwrange *range, int has_range);</code></li>
<li><code>void iw_print_stats(char *buffer, int buflen, const iwqual *qual, const iwrange *range, int has_range);</code></li>
</ol>
</li>
<li><strong>ENCODING SUBROUTINES(3个)</strong><ol>
<li><code>void iw_print_key(char *buffer, int buflen, const unsigned char *key, int key_size, int key_flags);</code></li>
<li><code>int iw_in_key(const char *input, unsigned char *key);</code></li>
<li><code>int iw_in_key_full(int skfd, const char *ifname, const char *input, unsigned char *key, __u16 *flags);</code></li>
</ol>
</li>
<li><strong>POWER MANAGEMENT SUBROUTINES(2个)</strong><ol>
<li><code>void iw_print_pm_value(char *buffer, int buflen, int value, int flags, int we_version);</code></li>
<li><code>void iw_print_pm_mode(char *buffer, int buflen, int flags);</code></li>
</ol>
</li>
<li><strong>RETRY LIMIT/LIFETIME SUBROUTINES(1个)</strong><ol>
<li><code>void iw_print_retry_value(char *buffer, int buflen, int value, int flags, int we_version);</code></li>
</ol>
</li>
<li><strong>TIME SUBROUTINES(1个)</strong><ol>
<li><code>void iw_print_timeval(char *buffer, int buflen, const struct timeval *time, const struct timezone *tz);</code></li>
</ol>
</li>
<li><strong>ADDRESS SUBROUTINES(9个)</strong><ol>
<li><code>int iw_check_mac_addr_type(int skfd, const char *ifname);</code></li>
<li><code>int iw_check_if_addr_type(int skfd, const char *ifname);</code></li>
<li><code>char *iw_mac_ntop(const unsigned char *mac, int maclen, char *buf, int buflen);</code></li>
<li><code>void iw_ether_ntop(const struct ether_addr *eth, char *buf);</code></li>
<li><code>char *iw_sawap_ntop(const struct sockaddr *sap, char *buf);</code></li>
<li><code>int iw_mac_aton(const char *orig, unsigned char *mac, int macmax);</code></li>
<li><code>int iw_ether_aton(const char *bufp, struct ether_addr *eth);</code></li>
<li><code>int iw_in_inet(char *bufp, struct sockaddr *sap);</code></li>
<li><code>int iw_in_addr(int skfd, const char *ifname, char *bufp, struct sockaddr *sap);</code></li>
</ol>
</li>
<li><p><strong>MISC SUBROUTINES(1个)</strong></p>
<ol>
<li><code>int iw_get_priv_size(int args);</code></li>
</ol>
</li>
<li><p><strong>EVENT SUBROUTINES(2个)</strong></p>
<ol>
<li><code>void iw_init_event_stream(struct stream_descr *stream, char *data, int len);</code></li>
<li><code>int iw_extract_event_stream(struct stream_descr *stream, struct iw_event *iwe, int we_version);</code></li>
</ol>
</li>
<li><strong>SCANNING SUBROUTINES(2个)</strong><ol>
<li><code>int iw_process_scan(int skfd, char *ifname, int we_version, wireless_scan_head *context);</code></li>
<li><code>int iw_scan(int skfd, char *ifname, int we_version, wireless_scan_head *context);</code></li>
</ol>
</li>
<li><strong>内联函数(8个)</strong><ol>
<li><code>static inline int iw_set_ext(int skfd, const char *ifname, int request, struct iwreq *pwrq);</code></li>
<li><code>static inline int iw_get_ext(int skfd, const char *ifname, int request, struct iwreq *pwrq);</code></li>
<li><code>static inline void iw_sockets_close(int skfd);</code></li>
<li><code>static inline char *iw_saether_ntop(const struct sockaddr *sap, char* bufp);</code></li>
<li><code>static inline int iw_saether_aton(const char *bufp, struct sockaddr *sap);</code></li>
<li><code>static inline void iw_broad_ether(struct sockaddr *sap);</code></li>
<li><code>static inline void iw_null_ether(struct sockaddr *sap);</code></li>
<li><code>static inline int iw_ether_cmp(const struct ether_addr* eth1, const struct ether_addr* eth2);</code></li>
</ol>
</li>
</ul>
<h2 id="heading-3-socket-subroutines2">3 SOCKET SUBROUTINES(2个函数)</h2>
<ul>
<li><code>int iw_sockets_open(void);</code><ul>
<li>功能：打开一个 socket</li>
<li>返回：成功时，返回一个打开的 socket；失败时，返回 <code>-1</code></li>
<li>说明：按顺序尝试使用不同的协议族打开 socket，直至成功或全部失败，协议族顺序为：<code>AF_INET、AF_IPX、AF_AX25、AF_APPLETALK</code>；99% 的情况下可以使用 AF_INET 协议族打开 socket。</li>
</ul>
</li>
<li><code>void iw_enum_devices(int skfd, iw_enum_handler fn, char *args[], int count);</code><ul>
<li>功能：枚举所有无线接口设备</li>
<li>参数：<ul>
<li>skfd 为一个使用 <code>iw_sockets_open()</code> 打开的 socket</li>
<li>fn 为一个函数的指针，每找到一个无线接口，都会调用 fn，iw_enum_handler 的定义如下：<pre><code>  typedef int (*iw_enum_handler)(int skfd, char *ifname, char *args[], int count);
</code></pre><blockquote>
<p>由此可见 fn() 返回的是一个 int，其调用参数有 4 个，skfd 为一个打开的 socket，ifname 为接口名称，args[] 为传递给 fn() 的自定义参数，count 为 args[] 中的参数数量；</p>
</blockquote>
</li>
<li>argc[] 和 count 为传递给 fn() 的自定义参数，count 表示 args[] 参数的数量；</li>
</ul>
</li>
<li>可以使用 fn() 打印接口信息等；</li>
<li>说明：这个函数的实现是首先读取文件 <code>/proc/net/wireless</code> 获取无线网络接口名称，然后调用 <em>fn()，并将获得的设备接口名称自动填到 </em>fn() 第二个参数 ifname 上。</li>
</ul>
</li>
</ul>
<h2 id="heading-4-wireless-subroutines6">4 WIRELESS SUBROUTINES(6个函数)</h2>
<ul>
<li><code>int iw_get_kernel_we_version(void);</code><ul>
<li>功能：获取内核中 WE(Wireless Extension) 的版本号</li>
<li>返回：版本号</li>
<li>说明：这个函数的实现是从文件 <code>/proc/net/wireless</code> 中提取出 WE 的版本号。</li>
</ul>
</li>
<li><code>int iw_print_version_info(const char *toolname);</code><ul>
<li>功能：打印 Wireless Tools 所使用的 WE 的版本号，Wireless Tools 的版本号，Wireless Tools 的版本号其实就是常数 WT_VERSION，以及接口支持的 WE 版本号</li>
<li>参数：toolsname - Wireless Tools 名称，从源码看，这个参数可以是任意字符串，或者为 NULL</li>
<li>返回：成功返回 0，如果打开 socket 失败则返回 -1</li>
</ul>
</li>
<li><code>int iw_get_range_info(int skfd, const char *ifname, iwrange *range);</code><ul>
<li>功能：从驱动程序中获取 range 信息</li>
<li>参数：<ul>
<li>skfd 为一个使用 iw_sockets_open() 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>iwrange 就是 <code>struct iw_range</code>，获取的信息将放在 range 指向的 <code>struct iw_range</code> 中，<code>struct iw_range</code> 定义在 <code>wireless.h</code> 中；</li>
</ul>
</li>
<li>返回：成功返回 0，range 中存放获取的信息，失败则返回 -1</li>
<li>说明：该函数使用 <code>ioctl()</code> 的 SIOCGIWRANGE 命令获取 range 信息，<code>struct iw_range</code> 结构比较庞大，不同的驱动程序可以返回的信息也不同。</li>
</ul>
</li>
<li><code>int iw_get_priv_info(int skfd, const char *ifname, iwprivargs **ppriv);</code><ul>
<li>功能：获取有关驱动程序支持哪些私有 ioctl 的信息</li>
<li>参数：<ul>
<li>skfd 为一个使用 <code>iw_sockets_open()</code> 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>iwprivargs 就是 <code>struct iw_priv_args</code>，该结构定义在 wireless.h 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_priv_args</span> {</span>
      __u32       cmd;            <span class="hljs-comment">/* Number of the ioctl to issue */</span>
      __u16       set_args;       <span class="hljs-comment">/* Type and number of args */</span>
      __u16       get_args;       <span class="hljs-comment">/* Type and number of args */</span>
      <span class="hljs-keyword">char</span>        name[IFNAMSIZ]; <span class="hljs-comment">/* Name of the extension */</span>
  };
</code></pre>
</li>
<li>ppriv 为一个数组指针，成功返回后将存放该接口支持的 ioctl</li>
</ul>
</li>
<li>返回：调用成功返回支持的 ioctl 的数量，失败返回 -1；</li>
<li>说明：该函数会动态为 ppriv 参数申请内存，所以使用完毕一定要记得 <code>free()</code>，源码中提示，即便返回的数据长度为 0，仍然要调用 <code>free(*ppriv)</code>。</li>
</ul>
</li>
<li><code>int iw_get_basic_config(int skfd, const char *ifname, wireless_config *info);</code><ul>
<li>功能：从设备驱动程序获取必要的(基本的)无线配置</li>
<li>参数：<ul>
<li>skfd 为一个使用 iw_sockets_open() 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>wireless_config 就是 <code>struct wireless_config</code>，定义如下：<pre><code class="lang-C">  <span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_config</span> {</span>
      <span class="hljs-keyword">char</span>        name[IFNAMSIZ + <span class="hljs-number">1</span>];     <span class="hljs-comment">/* Wireless/protocol name */</span>
      <span class="hljs-keyword">int</span>         has_nwid;
      iwparam     nwid;                   <span class="hljs-comment">/* Network ID */</span>
      <span class="hljs-keyword">int</span>         has_freq;
      <span class="hljs-keyword">double</span>      freq;                   <span class="hljs-comment">/* Frequency/channel */</span>
      <span class="hljs-keyword">int</span>         freq_flags;
      <span class="hljs-keyword">int</span>         has_key;
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span>    key[IW_ENCODING_TOKEN_MAX];    <span class="hljs-comment">/* Encoding key used */</span>
      <span class="hljs-keyword">int</span>         key_size;               <span class="hljs-comment">/* Number of bytes */</span>
      <span class="hljs-keyword">int</span>         key_flags;              <span class="hljs-comment">/* Various flags */</span>
      <span class="hljs-keyword">int</span>         has_essid;
      <span class="hljs-keyword">int</span>         essid_on;
      <span class="hljs-keyword">char</span>        essid[IW_ESSID_MAX_SIZE + <span class="hljs-number">1</span>];       <span class="hljs-comment">/* ESSID (extended network) */</span>
      <span class="hljs-keyword">int</span>         has_mode;
      <span class="hljs-keyword">int</span>         mode;                   <span class="hljs-comment">/* Operation mode */</span>
  } wireless_config;
</code></pre>
</li>
<li>info 是一个指向 <code>struct wireless_config</code> 的指针，调用成功将存放获得的无线配置</li>
</ul>
</li>
<li>返回：成功返回 0，info 中存放着获得的无线配置，失败返回 -1</li>
<li>说明：该函数会调用 ioctl() 的如下命令已获得无线配置信息：SIOCGIWNAME、SIOCGIWNWID、SIOCGIWFREQ、SIOCGIWENCODE、SIOCGIWESSID、SIOCGIWMODE</li>
</ul>
</li>
<li><code>int iw_set_basic_config(int skfd, const char *ifname, wireless_config *info);</code><ul>
<li>功能：在设备驱动程序中设置必要的(基本的)无线配置</li>
<li>参数：<ul>
<li>skfd 为一个使用 <code>iw_sockets_open()</code>打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>info 说明同上，为一个 <code>struct wireless_config</code> 结构指针；</li>
</ul>
</li>
<li>返回：全部成功返回 0，有一项或多项失败返回 -1；</li>
<li>说明：该函数会调用 <code>ioctl()</code> 的如下命令已设置无线配置信息：SIOCSGIWNAME、SIOCSIWMODE、SIOCSIWFREQ、SIOCSIWENCODE、SIOCSIWNWID、SIOCSIWESSID</li>
</ul>
</li>
</ul>
<h2 id="heading-5-protocol-subroutines1">5 PROTOCOL SUBROUTINES(1个函数)</h2>
<ul>
<li><code>int iw_protocol_compare(const char *protocol1, const char *protocol2);</code><ul>
<li>功能：比较协议标识符</li>
<li>参数：<ul>
<li>protocol1 为第一个协议(字符串)</li>
<li>protecol2 为第二个协议(字符串)</li>
</ul>
</li>
<li>返回：如果协议兼容则返回 1，否则返回 0</li>
<li>说明：如果 protocol1 和 protocol2 完全一样，返回 1；如果 protocol1 和 protocol2 的起始字符串都是 "IEEE 802.11"，则 protocol1 和 protocol2 后面字符串中只要含有 "Dbg" 之中任一个字符(protocol1 和 protocol2 中可以不一样)，则返回 1，或者 protocol1 和 protocol2 中均含有 "a"(5g)，也将返回 1；其它情况返回 0。</li>
</ul>
</li>
</ul>
<h2 id="heading-6-essid-subroutines2">6 ESSID SUBROUTINES(2个函数)</h2>
<ul>
<li><code>void iw_essid_escape(char *dest, const char *src, const int slen);</code></li>
<li><code>int iw_essid_unescape(char *dest, const char *src);</code></li>
<li>上面两个函数在 libiw.so.29 中没有，仅存在在 libiw.so.30 中。</li>
</ul>
<h2 id="heading-7-frequency-subroutines7">7 FREQUENCY SUBROUTINES(7个函数)</h2>
<ul>
<li><p><code>void iw_float2freq(double in, iwfreq *out);</code></p>
<ul>
<li>功能：将浮点数转换为频率，内核是不好处理浮点数的，所以需要将一个浮点数转换成一种内部格式(<code>struct iw_freq</code>)传递给内核</li>
<li>参数：<ul>
<li>in 为浮点数频率</li>
<li>iwfreq 就是 <code>struct iw_freq</code>，是一个内核可以接受的频率格式，定义如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_freq</span> {</span>
      __s32 m;      <span class="hljs-comment">/* Mantissa */</span>
      __s16 e;      <span class="hljs-comment">/* Exponent */</span>
      __u8  i;      <span class="hljs-comment">/* List index (when in range struct) */</span>
      __u8  flags;  <span class="hljs-comment">/* Flags (fixed/auto) */</span>
  };
</code></pre>
</li>
<li>in = m x 10<sup>e</sup></li>
</ul>
</li>
<li>返回：转换结果存放在 out 中；</li>
</ul>
</li>
<li><p><code>double iw_freq2float(const iwfreq *in);</code></p>
<ul>
<li>功能：将内部格式(<code>struct iw_freq</code>)表示的频率转换为浮点数</li>
<li>参数：<ul>
<li>in 为使用 <code>struct iw_freq</code> 表示的频率</li>
</ul>
</li>
<li>返回：返回以浮点数表示的频率</li>
</ul>
</li>
<li><p><code>void iw_print_freq_value(char *buffer, int buflen, double freq);</code></p>
<ul>
<li>功能：将频率值转换成适当单位(GHz、MHz、kHz)的字符串</li>
<li>参数：<ul>
<li>buffer 存放转换完成的字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>freq 频率值</li>
</ul>
</li>
<li>返回：转换好的字符串存放在 buffer 中</li>
</ul>
</li>
<li><p><code>void iw_print_freq(char *buffer, int buflen, double freq, int channel, int freq_flags);</code></p>
<ul>
<li>功能：将频率值转换成适当单位(GHz、MHz、kHz)的字符串，并分离出工作信道(channel)</li>
<li>说明：当使用 ioctl() 获取工作频率时，如果返回值 <code>&lt; 1000</code> 则该值为工作信道(channel)，否则为工作频率；当 <code>freq &lt; 1000</code> 时，该函数转换成类似 <code>Channel:5</code> 这样的字符串，否则，转换成类似 <code>Frequency:5.265 GHz</code> 这样的字符串；</li>
<li>参数：<ul>
<li>buffer 存放转换完成的字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>freq 频率值</li>
<li>当 <code>channel &gt;= 0</code> 时，转换的字符串类似 <code>Frequency:5.265 GHz (Channel 3)</code> 这样的字符串；</li>
<li>当 <code>freq_flags = 1</code> 时，转换出的字符串类似 <code>Frequency=5.265 GHz</code>，否则为 <code>Frequency:5.265 GHz</code></li>
</ul>
</li>
<li>返回：转换好的字符串存放在 buffer 中</li>
</ul>
</li>
<li><p><code>int iw_freq_to_channel(double freq, const struct iw_range *range);</code></p>
<ul>
<li>功能：得到工作频率对应的工作信道(channel)</li>
<li>参数：<ul>
<li>freq 为工作频率值</li>
<li>range 从接口驱动程序中获取的 range</li>
</ul>
</li>
<li>返回：<code>&gt;=0</code> 时表示工作频率所在的工作信道，<code>-2</code> 表示在 range 中没有找到指定的频率 freq，<code>-1</code> 表示 <code>freq &lt; 1000</code></li>
<li>说明：<code>struct iw_range</code> 定义在 wireless.h 中，其中有一个 <code>struct iw_freq</code> 的结构数组，该函数将从这个结构数组(<code>struct iw_freq).freq</code>)中查找与 freq 一致的频率，找到后返回 <code>(struct iw_freq).i</code>，否则返回 -2，当参数 <code>freq &lt; 1000</code> 时，返回 -1</li>
</ul>
</li>
<li><p><code>int iw_channel_to_freq(int channel, double *pfreq, const struct iw_range *range);</code></p>
<ul>
<li>功能：将工作信道转换成工作频率</li>
<li>参数：<ul>
<li>channel 为工作信道</li>
<li>pfreq 为存放频率的指针，转换好的频率将存放在这里</li>
<li>range 从接口驱动程序中获取的 range</li>
</ul>
</li>
<li>返回：转换成功返回 channel，否则返回 <code>&lt;0</code> 的值</li>
<li>说明：参考前面 <code>struct iw_freq</code> 的定义，<code>struct iw_range</code> 定义在 wireless.h 中，从 range 中的 <code>struct iw_freq</code> 结构数组中的 <code>(struct iw_freq).i</code> 中查找与 channel 一致的项，并将其对应的 <code>(struct iw_freq).freq</code> 存放到 pfreq 中；</li>
</ul>
</li>
<li><p><code>void iw_print_bitrate(char *buffer, int buflen, int bitrate);</code></p>
<ul>
<li>功能：将传输速率(bitrate)以适当的单位(<code>Gb/s、Mb/s、kb/s</code>)表示的字符串</li>
<li>参数：<ul>
<li>buffer 为存放转换结果字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>bitrate 为要转换的传输速率</li>
</ul>
</li>
<li>返回：转换好的字符串存放在 buffer 中</li>
<li>说明：当速率 &gt;10<sup>9</sup> 时，单位为 <code>Gb/s</code>，当速率 &gt;10<sup>6</sup> 时，单位为 <code>Mb/s</code>，当速率 &gt;1000 时，单位为 <code>kb/s</code> </li>
</ul>
</li>
</ul>
<h2 id="heading-8-power-subroutines3">8 POWER SUBROUTINES(3个函数)</h2>
<ul>
<li><p><code>int iw_dbm2mwatt(int in);</code></p>
<ul>
<li>功能：将 dBm 值转换为毫瓦值</li>
<li>参数：<ul>
<li>in 为功率的 dBm 值</li>
</ul>
</li>
<li>返回：转换后的毫瓦值</li>
</ul>
</li>
<li><p><code>int iw_mwatt2dbm(int in);</code></p>
<ul>
<li>功能：将毫瓦值转换为 dBm 值</li>
<li>参数：<ul>
<li>in 为以毫瓦表示的功率值</li>
</ul>
</li>
<li>返回：转换后的 dBm 值</li>
</ul>
</li>
<li><p><code>void iw_print_txpower(char *buffer, int buflen, struct iw_param *txpower);</code></p>
<ul>
<li>功能：txpower 表示发射功率，本函数对 txpower 进行适当的转换，转换结果以字符串输出</li>
<li>参数：<ul>
<li>buffer 为存放转换结果字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li><code>struct iw_param</code> 时 WE 下的一个通用结构，定义在 wireless.h 中，具体如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span> {</span>
      __s32       value;      <span class="hljs-comment">/* The value of the parameter itself */</span>
      __u8        fixed;      <span class="hljs-comment">/* Hardware should not use auto select */</span>
      __u8        disabled;   <span class="hljs-comment">/* Disable the feature */</span>
      __u16       flags;      <span class="hljs-comment">/* Various specifc flags (if any) */</span>
  };
</code></pre>
</li>
<li><code>txpower-&gt;value</code> 为发射功率值，当 <code>txpower-&gt;flags</code> 的 bit 0 为 1 时，value 的单位为毫瓦(否则为 dBm)，当 bit 1 为 1 时表示 value 中的值是一个相对值，没有具体单位</li>
</ul>
</li>
<li>返回：转换结果存放在 buffer 中</li>
</ul>
</li>
</ul>
<h2 id="heading-9-statistics-subroutines2">9 STATISTICS SUBROUTINES(2个函数)</h2>
<ul>
<li><p><code>int iw_get_stats(int skfd, const char *ifname, iwstats *stats, const iwrange *range, int has_range);</code></p>
<ul>
<li>功能：读取文件 <code>/proc/net/wireless</code> 获取最新的统计数据</li>
<li>参数：<ul>
<li>skfd 为一个使用 <code>iw_sockets_open()</code> 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>iwstats 就是 <code>struct iw_statistics</code>，定义在 wireless.h 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_statistics</span> {</span>
      __u16               status;     <span class="hljs-comment">/* Status - device dependent for now */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_quality</span>   <span class="hljs-title">qual</span>;</span>       <span class="hljs-comment">/* Quality of the link (instant/mean/max) */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_discarded</span> <span class="hljs-title">discard</span>;</span>    <span class="hljs-comment">/* Packet discarded counts */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_missed</span>    <span class="hljs-title">miss</span>;</span>       <span class="hljs-comment">/* Packet missed counts */</span>
  };
</code></pre>
</li>
<li>其中：<code>struct iw_quality</code>、<code>struct iw_discarded</code> 和 <code>struct iw_missed</code> 均定义在 wireless.h 中</li>
<li>从文件 <code>/proc/net/wireless</code> 读出的统计信息存放在 stats 指向的 <code>struct iw_statistics</code> 中</li>
<li>has_range 为 1 表示 range 参数存在，range 参数仅用于比较 WE 的版本号，因为 WE version 11 以后才有 <code>/proc/net/wireless</code> 文件</li>
</ul>
</li>
<li>返回：调用成功返回 0，stats 中存放有统计数据，调用失败返回 -1</li>
</ul>
</li>
<li><p><code>void iw_print_stats(char *buffer, int buflen, const iwqual *qual, const iwrange *range, int has_range);</code></p>
<ul>
<li>功能：获取连接统计数据，包括信号质量、信号强度和信号噪音，以字符串形式输出</li>
<li>参数：<ul>
<li>buffer 为存放转换结果字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>iwqual 就是 <code>struct iw_quality</code>，定义在 wireless.h 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_quality</span> {</span>
      __u8    qual;       <span class="hljs-comment">/* link quality (%retries, SNR, %missed beacons or better...) */</span>
      __u8    level;      <span class="hljs-comment">/* signal level (dBm) */</span>
      __u8    noise;      <span class="hljs-comment">/* noise level (dBm) */</span>
      __u8    updated;    <span class="hljs-comment">/* Flags to know if updated */</span>
  };
</code></pre>
</li>
<li>关于信号质量以及相关数据结构的说明，请参考文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/137711398">《使用ioctl扫描wifi信号获取信号属性的实例(二)》</a></li>
<li>has_range 为 1 表示 range 参数存在，该函数中将使用 <code>range-&gt;max_qual.qual</code>、<code>range-&gt;max_qual.level</code> 和 <code>range-&gt;max_qual.noise</code></li>
</ul>
</li>
<li>返回：信号质量、信号强度和信号噪音，将转换成字符串存放在 buffer 中</li>
</ul>
</li>
</ul>
<h2 id="heading-10-encoding-subroutines3">10 ENCODING SUBROUTINES(3个函数)</h2>
<ul>
<li><p><code>void iw_print_key(char *buffer, int buflen, const unsigned char *key, int key_size, int key_flags);</code></p>
<ul>
<li>功能：按照一个特定的格式显示经过编码后的秘钥</li>
<li>参数：<ul>
<li>buffer 为存放转换结果字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>key 为经过编码的秘钥</li>
<li>key_size 为 key 的长度</li>
<li>key_flags 为秘钥的标志，当 bit 8 为 1 时，表示没有秘钥</li>
</ul>
</li>
<li>返回：秘钥转换成特定格式的字符串存放在 buffer中</li>
</ul>
</li>
<li><p><code>int iw_in_key(const char *input, unsigned char *key);</code></p>
<ul>
<li>功能：从命令行解析密钥</li>
<li>参数：<ul>
<li>input 为密码字符串</li>
<li>key 为转换为以 16 进制表示的秘钥</li>
</ul>
</li>
<li>返回：&gt;0 时为秘钥的长度，=0 表示没有秘钥，</li>
<li>说明：<ul>
<li>input 为一个字符串，当没有任何标志时，应该是用字符串表达的 16 进制数，比如："01:2A:0C:E3:..."，转换完存放在 key 中是一个 8 位无符号整数数组，比如：<code>{0X01, 0X2A, 0X0C, 0XE3, ...}</code></li>
<li>当 input 起始字符为 "s:" 时，表示 input 中为一个由 ASCII 字符组成的字符串，比如："Abc123..."，转换完放在 key 中的 8 位无符号整数数组为：<code>{0X41, 0X62, 0X63, 0X31, 0X32, 0X33, ...}</code></li>
</ul>
</li>
</ul>
</li>
<li><p><code>int iw_in_key_full(int skfd, const char *ifname, const char *input, unsigned char *key, __u16 *flags);</code></p>
<ul>
<li>功能：从命令行解析密钥，当 input 起始字符为 "l:" 时，被认为是登录的 <code>username:password</code>，否则调用 <code>iw_in_key()</code></li>
<li>参数：<ul>
<li>skfd 为一个使用 iw_sockets_open() 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>input 为密码字符串</li>
<li>key 为转换为以 16 进制表示的秘钥</li>
<li>flags</li>
</ul>
</li>
<li>说明：skfd 和 ifname 用于获取 range，然后根据 WE 的版本号及 <code>range.encoding_login_index</code> 设置 flags</li>
</ul>
</li>
</ul>
<h2 id="heading-11-power-management-subroutines2">11 POWER MANAGEMENT SUBROUTINES(2个函数)</h2>
<ul>
<li><p><code>void iw_print_pm_value(char *buffer, int buflen, int value, int flags, int we_version);</code></p>
<ul>
<li>功能：将电源管理值 value 根据 flags 的提示转换成适当的字符串存入 buffer 中</li>
<li>参数：<ul>
<li>buffer 为转换后存放字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>value 为电源管理值(power management value)</li>
<li>flags 一些标志电源管理值属性的标志，bit0-</li>
<li>we_version 为 WE 的版本号 </li>
</ul>
</li>
<li>返回：buffer 中存放转换后的字符串</li>
<li>说明：</li>
</ul>
</li>
<li><p><code>void iw_print_pm_mode(char *buffer, int buflen, int flags);</code></p>
<ul>
<li>功能：将 flags 中的电源管理方式转换成字符串存放在 buffer 中</li>
<li>参数：<ul>
<li>buffer 为转换后存放字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>flags 为电源管理方式标志，bit8-11 为电源管理方式，bit8-Unicast only received，bit9-Multicast only received，bit10-All packets received，bit11-Force sending，bit12-Repeat multicasts</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-12-retry-limitlifetime-subroutines1">12 RETRY LIMIT/LIFETIME SUBROUTINES(1个函数)</h2>
<ul>
<li><code>void iw_print_retry_value(char *buffer, int buflen, int value, int flags, int we_version);</code><ul>
<li>功能：将重试值 value 根据 flags 的提示转换成适当的字符串存入 buffer 中</li>
<li>参数：<ul>
<li>buffer 为转换后存放字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>value 为重试值(retry value)</li>
<li>flags 一些属性的标志</li>
<li>we_version 为 WE 的版本号 </li>
</ul>
</li>
<li>返回：buffer 中存放转换后的字符串</li>
</ul>
</li>
</ul>
<h2 id="heading-13-time-subroutines1">13 TIME SUBROUTINES(1个函数)</h2>
<ul>
<li><code>void iw_print_timeval(char *buffer, int buflen, const struct timeval *time, const struct timezone *tz);</code><ul>
<li>功能：将 time 中的时间戳转换成字符串存放在 buffer 中</li>
<li>参数：<ul>
<li>buffer 为转换后存放字符串的缓冲区</li>
<li>buflen 为 buffer 的长度</li>
<li>time 为时间戳</li>
<li>tz 为时间戳所在时区</li>
</ul>
</li>
<li>返回：buffer 中存放转换后的字符串</li>
</ul>
</li>
</ul>
<h2 id="heading-14-address-subroutines9">14 ADDRESS SUBROUTINES(9个函数)</h2>
<ul>
<li><p><code>int iw_check_mac_addr_type(int skfd, const char *ifname);</code></p>
<ul>
<li>功能：检查网络接口是否支持正确 MAC 地址类型</li>
<li>参数：<ul>
<li>skfd 为一个使用 iw_sockets_open() 打开的 socket</li>
<li>ifname 为网络接口名称</li>
</ul>
</li>
<li>返回：0 表示支持 MAC 地址，-1 表示不支持 MAC 地址</li>
<li>说明：通过 <code>ioctl()</code> 的 SIOCGIFHWADDR 命令获取 HW 的地址类型，应该为 ARPHRD_ETHER 或者 <code>ARPHRD_IEEE80211</code></li>
</ul>
</li>
<li><p><code>int iw_check_if_addr_type(int skfd, const char *ifname);</code></p>
<ul>
<li>功能：检查网络接口是否支持正确的接口地址类型(INET)</li>
<li>参数：<ul>
<li>skfd 为一个使用 iw_sockets_open() 打开的 socket</li>
<li>ifname 为网络接口名称</li>
</ul>
</li>
<li>返回：0 表示支持接口地址类型，-1 表示不支持接口地址类型</li>
<li>说明：通过 ioctl() 的 SIOCGIFADDR 命令获取接口地址类型，应该为 AF_INET</li>
</ul>
</li>
<li><p><code>char *iw_mac_ntop(const unsigned char *mac, int maclen, char *buf, int buflen);</code></p>
<ul>
<li>功能：以可读格式显示任意长度的 MAC 地址</li>
<li>参数：<ul>
<li>mac 为 16 进制表示的 MAC 地址</li>
<li>maclen 为 mac 的长度</li>
<li>buf 为 MAC 地址转换为字符串后的存储区</li>
<li>buflen 为 buf 的有效长度</li>
</ul>
</li>
<li>返回：成功返回 buf 指针，失败返回 NULL</li>
</ul>
</li>
<li><p><code>void iw_ether_ntop(const struct ether_addr *eth, char *buf);</code></p>
<ul>
<li>功能：以可读格式显示以太网地址(实际就是 MAC 地址)</li>
<li>参数：<ul>
<li><code>struct ether_addr</code> 定义在 ethernet.h 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ether_addr</span> {</span>
      <span class="hljs-keyword">uint8_t</span> ether_addr_octet[ETH_ALEN];
  } __attribute__ ((__packed__));
</code></pre>
</li>
<li>其实就是一个 8 位无符号整数的数组，数组有 6 个(因为 ETH_ALEN 为 6)元素</li>
<li>buf 转换后以字符串表示的以太网地址存放在这里</li>
</ul>
</li>
<li>返回：buf 中存放着已经转换好的字符串</li>
<li>说明：<code>iw_mac_ntop()</code> 把一个任意长度的 MAC 地址转换成字符串，其输入参数为一个 <code>unsigned char</code> 数组，<code>iw_ether_ntop()</code> 将一个长度为 6 个字符的 <code>unsigned char</code> 数组转换为字符串，输入参数为 <code>struct ether_addr</code>，两个函数本质上非常相似。</li>
</ul>
</li>
<li><p><code>char *iw_sawap_ntop(const struct sockaddr *sap, char *buf);</code></p>
<ul>
<li>功能：以可读格式显示无线接入点 socket 地址</li>
<li>参数：<ul>
<li><code>struct sockaddr</code> 定义在文件 socket.h 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span> {</span>
      <span class="hljs-keyword">sa_family_t</span> sa_family;    <span class="hljs-comment">/* Common data: address family and length.  */</span>
      <span class="hljs-keyword">char</span> sa_data[<span class="hljs-number">14</span>];        <span class="hljs-comment">/* Address data.  */</span>
  };
</code></pre>
</li>
<li>buf 转换后以字符串表示的以太网地址存放在这里</li>
</ul>
</li>
<li>返回：返回 buf 指针，buf 中存储着转换结果</li>
<li>说明：实际上就是使用 <code>iw_ether_ntop()</code> 将 socket.sa_data 中的 MAC 地址转换成字符串，这个函数仍然是把 MAC 地址转换为字符串，不过输入参数是 <code>struct sockaddr</code> 而已。</li>
</ul>
</li>
</ul>
<blockquote>
<p><code>iw_mac_ntop()</code>、<code>iw_ether_ntop()</code>、<code>iw_sawap_ntop()</code> 都是把 MAC 地址转换成易读的字符串形式，但各自的输入参数不同，<code>iw_mac_ntop()</code> 的输入参数是一个任意长度的二进制数组；<code>iw_ether_ntop()</code> 的输入参数是 <code>struct ether_addr</code>；iw_sawap_ntop() 的输入参数是 <code>struct sockaddr</code></p>
</blockquote>
<ul>
<li><p><code>int iw_mac_aton(const char *orig, unsigned char *mac, int macmax);</code></p>
<ul>
<li>功能：将任意长度的字符串表达的 MAC 地址，转换成二进制</li>
<li>参数：<ul>
<li>orig 为以字符串格式表达的一个 MAC 地址</li>
<li>mac 为 MAC 地址转换为二进制后存放在这里</li>
<li>macmax 为 mac 的最大长度</li>
</ul>
</li>
<li>返回：成功则返回转换后 MAC 地址长度，失败则返回 0</li>
</ul>
</li>
<li><p><code>int iw_ether_aton(const char *orig, struct ether_addr *eth);</code></p>
<ul>
<li>功能：将一个以字符串形式表达的以太网地址转换成二进制</li>
<li>参数：<ul>
<li>orig 为以字符串格式表达的一个以太网地址</li>
<li><code>struct ether_addr</code> 在前面有介绍，转换好的二进制以太网地址存放在 eth 中</li>
</ul>
</li>
<li>返回：成功则返回以太网地址的长度，失败返回 0</li>
<li>说明：<code>iw_mac_aton()</code> 和 <code>iw_ether_aton()</code> 都是把一个字符串表达的 MAC 地址转换成二进制形式，<code>iw_mac_aton()</code> 的输出参数是一个 <code>unsigned char</code> 数组，<code>iw_ether_aton()</code> 的输出参数是 <code>struct ether_addr</code>，<code>iw_mac_aton()</code> 是任意长度，<code>iw_ether_aton()</code> 是 6 个字符长度。</li>
</ul>
</li>
</ul>
<blockquote>
<p>iw_mac_aton()、iw_ether_aton()、</p>
</blockquote>
<ul>
<li><p><code>int iw_in_inet(char *bufp, struct sockaddr *sap);</code></p>
<ul>
<li>功能：将一个以字符串表达的互联网地址(域名)转换成二进制(DNS 名称、IP 地址等)</li>
<li>参数：<ul>
<li>bufp 为一个以字符串表达的互联网地址，可以是域名、IP 地址</li>
<li>sap 存放转换结果的结构，bufp 中如果不是官方域名，将改为官方域名</li>
</ul>
</li>
<li>返回：成功返回 0，失败返回 -1</li>
<li>说明：该函数会使用 gethostbyname() 将域名(主机名)进行解析</li>
</ul>
</li>
<li><p><code>int iw_in_addr(int skfd, const char *ifname, char *bufp, struct sockaddr *sap);</code></p>
<ul>
<li>功能：将一个以字符串表达的地址转换成二进制</li>
<li>参数：<ul>
<li>skfd 为一个使用 iw_sockets_open() 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>bufp 为一个用字符串格式表达的地址</li>
<li>sap 转换为二进制后存放在这个结构中</li>
</ul>
</li>
<li>返回：成功返回 0，失败返回 -1</li>
<li>说明：bufp 中既可以是 MAC 地址，也可以是 IP 地址，该函数实际上就是用 MAC 地址生成一个 <code>struct sockaddr</code></li>
</ul>
</li>
</ul>
<h2 id="heading-15-misc-subroutines1">15 MISC SUBROUTINES(1个函数)</h2>
<ul>
<li><code>int iw_get_priv_size(int args);</code><ul>
<li>功能：返回私有参数以字节为单位的最大长度</li>
<li>参数：args 私有参数的标志，bit0-10 表示参数的数量，bit12-14 表示参数的类型</li>
<li>返回：返回私有参数以字节为单位的最大长度</li>
</ul>
</li>
</ul>
<h2 id="heading-16-event-subroutines2">16 EVENT SUBROUTINES(2个函数)</h2>
<ul>
<li><p><code>void iw_init_event_stream(struct stream_descr *stream, char *data, int len);</code></p>
<ul>
<li>功能：初始化结构体stream_descr，以便我们可以从事件流中提取单个事件</li>
<li>参数：<ul>
<li><code>struct stream_descr</code> 定义在 iwlib.h 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">stream_descr</span> {</span>
      <span class="hljs-keyword">char</span> *end;          <span class="hljs-comment">/* End of the stream */</span>
      <span class="hljs-keyword">char</span> *current;      <span class="hljs-comment">/* Current event in stream of events */</span>
      <span class="hljs-keyword">char</span> *value;        <span class="hljs-comment">/* Current value in event */</span>
  } stream_descr;
</code></pre>
</li>
<li>stream 为需要初始化的结构指针</li>
<li>data 为事件流数据的起始指针</li>
<li>len 为事件流数据的长度</li>
</ul>
</li>
<li>返回：stream-&gt;current=data，stream-&gt;end=(data+len)</li>
</ul>
</li>
<li><p><code>int iw_extract_event_stream(struct stream_descr *stream, struct iw_event *iwe, int we_version);</code></p>
<ul>
<li>功能：从事件流中提取下一个事件</li>
<li>参数：<ul>
<li>stream 为经过 <code>iw_init_event_stream()</code> 初始化的结构</li>
<li><code>struct iw_event</code> 定义在 <code>wireless.h</code> 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> {</span>
      __u16   len;            <span class="hljs-comment">/* Real lenght of this stuff */</span>
      __u16   cmd;            <span class="hljs-comment">/* Wireless IOCTL */</span>
      <span class="hljs-keyword">union</span> iwreq_data u;     <span class="hljs-comment">/* IOCTL fixed payload */</span>
  };
</code></pre>
</li>
<li>iwe 将用于存储当前要提取的事件</li>
<li>we_version 为 WE 的版本号</li>
</ul>
</li>
<li>返回：成功返回 1，此时 iwe 中为当前事件，失败返回 -1，返回 2 表示跳过了当前事件</li>
</ul>
</li>
</ul>
<h2 id="heading-7-scanning-subroutines2">7 SCANNING SUBROUTINES(2个函数)</h2>
<ul>
<li><p><code>int iw_process_scan(int skfd, char *ifname, int we_version, wireless_scan_head *context);</code></p>
<ul>
<li>功能：启动无线信号扫描程序并处理结果</li>
<li><p>参数：</p>
<ul>
<li>skfd 为一个使用 iw_sockets_open() 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>we_version 为 WE 的版本号</li>
<li>wireless_scan_head 就是 <code>struct wireless_scan_head</code>，定义在 <code>iwlib.h</code> 中，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_scan_head</span> {</span>
      wireless_scan *result;      <span class="hljs-comment">/* Result of the scan */</span>
      <span class="hljs-keyword">int</span>            retry;       <span class="hljs-comment">/* Retry level */</span>
  } wireless_scan_head;
</code></pre>
</li>
<li>context 中将存储获取的扫描结果的首指针，以及获取扫描结果重试的次数</li>
<li><p><code>wireless_scan</code> 就是 <code>struct wireless_scan</code>，定义在 <code>iwlib.h</code> 中，如下：</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_scan</span>
  {</span>
      <span class="hljs-comment">/* Linked list */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">wireless_scan</span> *<span class="hljs-title">next</span>;</span>

      <span class="hljs-comment">/* Cell identifiaction */</span>
      <span class="hljs-keyword">int</span>         has_ap_addr;
      sockaddr    ap_addr;        <span class="hljs-comment">/* Access point address */</span>

      <span class="hljs-comment">/* Other information */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span>  <span class="hljs-title">wireless_config</span>    <span class="hljs-title">b</span>;</span>  <span class="hljs-comment">/* Basic information */</span>
      iwstats stats;              <span class="hljs-comment">/* Signal strength */</span>
      <span class="hljs-keyword">int</span>     has_stats;
      iwparam maxbitrate;         <span class="hljs-comment">/* Max bit rate in bps */</span>
      <span class="hljs-keyword">int</span>     has_maxbitrate;
  }
</code></pre>
</li>
<li>扫描结果存放在 <code>struct wireless_scan</code> 中，并且以链表形式存储多个多个无线信号的扫描结果；</li>
<li><code>struct wireless_scan</code> 中的 <code>struct wireless_config</code> 在前面有介绍；</li>
</ul>
</li>
<li>返回：成功则返回 0</li>
</ul>
</li>
<li><p><code>int iw_scan(int skfd, char *ifname, int we_version, wireless_scan_head *context);</code></p>
<ul>
<li>功能：对指定接口执行无线信号扫描</li>
<li>参数：<ul>
<li>skfd 为一个使用 <code>iw_sockets_open()</code> 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>we_version 为 WE 的版本号</li>
<li>context 中将存储获取的扫描结果的首指针，以及获取扫描结果重试的次数</li>
</ul>
</li>
<li>返回：成功则返回 0，失败返回 -1</li>
</ul>
</li>
</ul>
<h2 id="heading-8-8">8 内联函数(8个函数)</h2>
<ul>
<li>这 8 个内联函数(inline)定义在 iwlib.h 中；</li>
<li><code>static inline int iw_set_ext(int skfd, const char *ifname, int request, struct iwreq *pwrq);</code><ul>
<li>功能：将无线网卡的参数通过调用 ioctl() 设置到驱动程序中</li>
</ul>
</li>
<li><code>static inline int iw_get_ext(int skfd, const char *ifname, int request, struct iwreq *pwrq);</code><ul>
<li>功能：通过调用 ioctl() 从无线网卡驱动程序中获取无线参数</li>
<li>参数：<ul>
<li>skfd 为一个使用 <code>iw_sockets_open()</code> 打开的 socket</li>
<li>ifname 为网络接口名称</li>
<li>request 为调用 ioctl() 时的命令代码，定义在 wireless.h 中</li>
<li>pwrq 为一个 <code>struct iwreq</code> 的结构指针，不同的 request，其填写方法也不一样</li>
</ul>
</li>
<li>返回：返回 ioctl() 调用的返回值，0 表示调用成功，-1 表示出现错误，errno 中为错误代码</li>
</ul>
</li>
</ul>
<blockquote>
<p>说明：上面这两个内联函数其实是一样的，都是调用指定的 <code>ioctl()</code>，WE 支持的 ioctl 调用方式为：<code>int ioctl(int fd, unsigned long request, struct iwreq *pwrq)</code>，其 request 通常有两种，一种是 SIOCS<em>，另一种是 SIOCG</em>，均定义在 wireless.h 中，按照惯例，当 request 为 SIOCS<em> 时，使用 <code>iw_set_ext()</code>，当 request 为 SIOCG</em> 时，使用 iw_get_ext()。</p>
</blockquote>
<ul>
<li><p><code>static inline void iw_sockets_close(int skfd);</code></p>
<ul>
<li>功能：关闭一个打开的 socket</li>
<li>参数：<ul>
<li>skfd 为一个使用 <code>iw_sockets_open()</code> 打开的 socket</li>
</ul>
</li>
<li>返回：无</li>
<li>说明：实际调用 <code>close(skfd)</code></li>
</ul>
</li>
<li><p><code>static inline char *iw_saether_ntop(const struct sockaddr *sap, char* bufp);</code></p>
<ul>
<li>功能：将 struct sockaddr 中的 MAC 地址转换成可读格式的字符串</li>
<li>参数：<ul>
<li>sap 是一个 struct sockaddr 的结构指针，这个结构在 socket 编程中常会用到</li>
<li>bufp 一个字符缓冲区指针，转换好的字符串存放在这里</li>
</ul>
</li>
<li>返回：转换完成的字符串指针，也就是 bufp</li>
<li>说明：bufp 的长度务必大于等于 18个字符，否则会内存溢出，该函数实际是调用了 <code>iw_ether_ntop()</code></li>
</ul>
</li>
<li><p><code>static inline int iw_saether_aton(const char *bufp, struct sockaddr *sap);</code></p>
<ul>
<li>功能：将一个以字符串表示的 MAC 地址转换成二进制格式</li>
<li>参数：<ul>
<li>bufp 指向 MAC 地址字符串的指针</li>
<li>sap 指向结构 struct sockaddr 的指针，转换结果将放到该结构的 sa_data 字段中</li>
</ul>
</li>
<li>返回：成功则返回 MAC 地址的长度，失败则返回 0</li>
<li>说明：该函数实际调用 <code>iw_ether_aton()</code> 函数</li>
</ul>
</li>
<li><p><code>static inline void iw_broad_ether(struct sockaddr *sap);</code></p>
<ul>
<li>功能：创建一个以太网广播地址</li>
<li>参数：<ul>
<li>sap 指向结构 struct sockaddr 的指针</li>
</ul>
</li>
<li>返回：无，建立的广播地址放在 sap 的 sa_data 字段中</li>
<li>说明：广播地址就是：FF:FF:FF:FF:FF:FF，该函数实际就是在 sap 的 sa_data 中填上 6 个 0xff</li>
</ul>
</li>
<li><p><code>static inline void iw_null_ether(struct sockaddr *sap);</code></p>
<ul>
<li>功能：建立一个以太网空地址</li>
<li>参数：<ul>
<li>sap 指向结构 struct sockaddr 的指针</li>
</ul>
</li>
<li>返回：无，建立的广播地址放在 sap 的 sa_data 字段中</li>
<li>说明：空地址就是：00:00:00:00:00:00，该函数实际就是在 sap 的 sa_data 中填上 6 个 0</li>
</ul>
</li>
<li><p><code>static inline int iw_ether_cmp(const struct ether_addr* eth1, const struct ether_addr* eth2);</code></p>
<ul>
<li>功能：比较两个以太网地址</li>
<li>参数：<ul>
<li>eth1 指向结构 struct ether_addr 的指针</li>
<li>eth2 指向结构 struct ether_addr 的指针</li>
</ul>
</li>
<li>返回：<code>eth1 &lt; eth2</code> 则小于 0，<code>eth1 &gt; eth2</code> 则大于 0，否则等于 0</li>
<li>说明：该函数使用 memcmp() 对两个以太网地址的二进制数据进行比较，其意义不甚清楚，其结果的意义似乎也不明确</li>
</ul>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[使用ioctl扫描wifi信号获取信号属性的实例(二)]]></title><description><![CDATA[使用工具软件扫描 wifi 信号是一件很平常的事情，在知晓 wifi 密码的前提下，通常我们会尽可能地连接信号质量比较好的 wifi 信号，但是如何通过编程来扫描 wifi 信号并获得这些信号的属性(比如信号强度等)，却鲜有文章提及，本文在前面博文的基础上通过实例向读者介绍如何通过编程扫描 wifi 信号，并获得信号的一系列的属性，本文给出了完整的源代码，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；阅读本文并不需要对 IEEE802.11 协议有所了解，但本...]]></description><link>https://whowin.cn/180025-another-wifi-scanner-example</link><guid isPermaLink="true">https://whowin.cn/180025-another-wifi-scanner-example</guid><category><![CDATA[802.11]]></category><category><![CDATA[无线网络]]></category><category><![CDATA[Linux]]></category><category><![CDATA[网络编程]]></category><category><![CDATA[wifi]]></category><category><![CDATA[ioctl]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Wed, 10 Apr 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729260186738/1babd407-5df4-4c11-bbc3-9b1de7332475.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>使用工具软件扫描 wifi 信号是一件很平常的事情，在知晓 wifi 密码的前提下，通常我们会尽可能地连接信号质量比较好的 wifi 信号，但是如何通过编程来扫描 wifi 信号并获得这些信号的属性(比如信号强度等)，却鲜有文章提及，本文在前面博文的基础上通过实例向读者介绍如何通过编程扫描 wifi 信号，并获得信号的一系列的属性，本文给出了完整的源代码，本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；阅读本文并不需要对 IEEE802.11 协议有所了解，但本文实例中大量涉及链表和指针，所以本文可能不适合初学者阅读。</p>
</blockquote>
<h2 id="heading-1">1 前言</h2>
<ul>
<li>在<a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a>专栏里写过一篇wifi信号扫描的文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>，与该文相比本文所附带的实例将可以获取更多的 wifi 属性；</li>
<li>在阅读本文前，请阅读<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>，并请理解范例中的程序，该文中所涉及的概念以及数据结构，本文将不再做介绍；</li>
<li>在<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>中，我们使用 <code>ioctl()</code> 启动了 wifi 信号的扫描，并获取了 wifi 信号的 SSID、MAC地址、工作频率和工作信道，但有一些重要的信号属性并没有获得，比如：信号强度、信号质量、信号噪音以及加密方式等，本文将讨论如何获取这些属性；</li>
<li>本文提供的实例的基本框架与文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>中的基本一致；</li>
<li>本文所提供的实例中并不需要第三方库的支持，所以不需要安装任何其它支持软件和库。</li>
</ul>
<h2 id="heading-2">2 遍历网络设备列表</h2>
<ul>
<li>在对无线网卡操作之前，首先要找到无线网卡的设备名，在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>中，使用 <code>getifaddrs()</code> 找到所有的网络接口，然后再用 <code>ioctl()</code> 的 SIOCGIWNAME 命令从中找到无线网卡；</li>
<li>其实我们可以从 <code>/proc/net/dev</code> 中找到所有的网络接口，而不必使用 <code>getifaddrs()</code>；</li>
<li>就本文而言，需要知道的就是无线网卡的设备名，我们也可以从文件 <code>/proc/net/wireless</code> 文件中直接获得，这种方法更加简单一些，先来看一下这个文件中有什么内容：<pre><code>  $ cat /proc/net/wireless 
  Inter-| sta-|   Quality        |   Discarded packets               | Missed | WE
  face  | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon | <span class="hljs-number">22</span>
  <span class="hljs-attr">wlp1s0</span>: <span class="hljs-number">0000</span>   <span class="hljs-number">70.</span> <span class="hljs-number">-256.</span>  <span class="hljs-number">-256</span>     <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">1</span>        <span class="hljs-number">0</span>
  $
</code></pre></li>
<li>可以看到，这个文件的前两行是标题，从第三行起开始是无线网卡的信息，其中接口名称后面紧跟着 ": "；</li>
<li><p>这台电脑上只有一个无线网卡，其接口名称为：wlp1s0，下面程序片段从 <code>/proc/net/wireless</code> 中提取出无线接口的名称：</p>
<pre><code class="lang-C">  FILE *fh;
  <span class="hljs-keyword">char</span> buff[<span class="hljs-number">1024</span>];

  fh = fopen(<span class="hljs-string">"/proc/net/wireless"</span>, <span class="hljs-string">"r"</span>);
  <span class="hljs-keyword">if</span> (fh != <span class="hljs-literal">NULL</span>) {
      <span class="hljs-comment">// Skip 2 lines of header</span>
      fgets(buff, <span class="hljs-keyword">sizeof</span>(buff), fh);
      fgets(buff, <span class="hljs-keyword">sizeof</span>(buff), fh);
      <span class="hljs-comment">// Read each device line</span>
      <span class="hljs-keyword">while</span> (fgets(buff, <span class="hljs-keyword">sizeof</span>(buff), fh)) {
          <span class="hljs-keyword">char</span> name[IFNAMSIZ + <span class="hljs-number">1</span>];

          <span class="hljs-comment">// Skip empty lines.</span>
          <span class="hljs-keyword">if</span> ((buff[<span class="hljs-number">0</span>] == <span class="hljs-string">'\0'</span>) || (buff[<span class="hljs-number">1</span>] == <span class="hljs-string">'\0'</span>)) <span class="hljs-keyword">continue</span>;
          <span class="hljs-comment">// Extract interface name</span>
          <span class="hljs-keyword">char</span> *p = buff;
          <span class="hljs-comment">// Skip leading spaces</span>
          <span class="hljs-keyword">while</span> (<span class="hljs-built_in">isspace</span>(*p)) p++;
          <span class="hljs-keyword">char</span> *end;
          end = <span class="hljs-built_in">strstr</span>(buf, <span class="hljs-string">": "</span>);
          <span class="hljs-comment">// Not found</span>
          <span class="hljs-keyword">if</span> (end == <span class="hljs-literal">NULL</span>) <span class="hljs-keyword">continue</span>;
          <span class="hljs-comment">// Copy</span>
          <span class="hljs-built_in">memcpy</span>(name, p, (end - p));
          name[end - p] = <span class="hljs-string">'\0'</span>;
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"The wireless interface name is %s\n"</span>, name);
      }
      fclose(fh);
  } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Can't open file /proc/net/wireless\n"</span>);
  }
</code></pre>
</li>
</ul>
<h2 id="heading-3">3 信号质量、信号强度、信号噪音的获取</h2>
<ul>
<li><p>通过阅读文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>，应该可以了解如何使用 <code>ioctl()</code> 启动 wifi 信号的扫描并获得扫描结果，在此简单回顾一下：</p>
<ul>
<li><p><code>struct iwreq</code> 定义，其中 <code>struct iwreq_data</code> 见下面定义；</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> {</span>
      <span class="hljs-keyword">union</span>
      {
          <span class="hljs-keyword">char</span>  ifrn_name[IFNAMSIZ];  <span class="hljs-comment">/* if name, e.g. "eth0" */</span>
      } ifr_ifrn;

      <span class="hljs-comment">/* Data part (defined just above) */</span>
      <span class="hljs-keyword">union</span> iwreq_data  u;
  };
</code></pre>
</li>
<li><code>ioctl()</code> 的调用方式：<code>int ioctl(int socket, unsigned long request, struct iwreq *wrq)</code></li>
<li>启动 wifi 扫描时，<code>request=SIOCSIWSCAN</code>，<code>wrq-&gt;ifr_name</code> 设置为无线接口名称，<code>wrq-&gt;u.data.pointer=NULL</code>，<code>wrq-&gt;u.data.flags=0</code>，<code>wrq-&gt;u.data.length=0</code>，然后调用 <code>ioctl()</code>;</li>
<li>获取扫描结果时，<code>request=SIOCGIWSCAN</code>，<code>wrq-&gt;u.data.pointer=接收数据缓冲区指针</code>，<code>wrq-&gt;u.data.length=接收缓冲区的长度</code>，<code>wrq-&gt;u.data.flags=0</code>，然后调用 <code>ioctl()</code>，调用时，<code>wrq-&gt;ifr_name</code> 也是要设置为无线接口名称的，只是因为在启动扫描时已经设置过，所以这里通常不需要再设置；</li>
<li>如果返回的扫描结果数据比较大，设置的接收缓冲区不够用，<code>ioctl()</code> 将返回 -1，errno 为 E2BIG，此时应该重新为缓冲区分配内存并再次调用 <code>ioctl()</code> 获取扫描结果；</li>
<li>如果在使用 <code>ioctl()</code> 获取扫描结果时，扫描还没有完成，<code>ioctl()</code> 将返回 -1，errno 为 EAGAIN，此时应该等待一会再次调用 <code>ioctl()</code> 获取扫描结果；</li>
<li>正常获取扫描结果时，<code>wreq-&gt;u.data.falgs</code> 将被设为 1(调用时为 0)，<code>wreq-&gt;u.data.length</code> 中为返回数据的实际长度，返回的数据被存放在 <code>wreq-&gt;u.data.pointer</code> 指向的数据缓冲区中；</li>
</ul>
</li>
<li><p>返回的扫描结果是一个数据流(stream)，其中包含着许多的事件(event)，每个 event 包含着一个属性，返回的扫描结果数据符合 <code>struct iw_event</code>，每个 event 数据也符合 <code>struct iw_event</code>，这个结构定义在 <code>wireless.h</code> 中：</p>
<pre><code class="lang-C">  <span class="hljs-keyword">union</span> iwreq_data {
      <span class="hljs-comment">/* Config - generic */</span>
      <span class="hljs-keyword">char</span>                name[IFNAMSIZ];
      <span class="hljs-comment">/* Name : used to verify the presence of  wireless extensions.
       * Name of the protocol/provider... */</span>

      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_point</span>     <span class="hljs-title">essid</span>;</span>      <span class="hljs-comment">/* Extended network name */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">nwid</span>;</span>       <span class="hljs-comment">/* network id (or domain - the cell) */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_freq</span>      <span class="hljs-title">freq</span>;</span>       <span class="hljs-comment">/* frequency or channel :
                                       * 0-1000 = channel
                                       * &gt; 1000 = frequency in Hz */</span>

      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">sens</span>;</span>       <span class="hljs-comment">/* signal level threshold */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">bitrate</span>;</span>    <span class="hljs-comment">/* default bit rate */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">txpower</span>;</span>    <span class="hljs-comment">/* default transmit power */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">rts</span>;</span>        <span class="hljs-comment">/* RTS threshold threshold */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">frag</span>;</span>       <span class="hljs-comment">/* Fragmentation threshold */</span>
      __u32               mode;       <span class="hljs-comment">/* Operation mode */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">retry</span>;</span>      <span class="hljs-comment">/* Retry limits &amp; lifetime */</span>

      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_point</span>     <span class="hljs-title">encoding</span>;</span>   <span class="hljs-comment">/* Encoding stuff : tokens */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">power</span>;</span>      <span class="hljs-comment">/* PM duration/timeout */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_quality</span>   <span class="hljs-title">qual</span>;</span>       <span class="hljs-comment">/* Quality part of statistics */</span>

      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span>     <span class="hljs-title">ap_addr</span>;</span>    <span class="hljs-comment">/* Access point address */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span>     <span class="hljs-title">addr</span>;</span>       <span class="hljs-comment">/* Destination address (hw/mac) */</span>

      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>     <span class="hljs-title">param</span>;</span>      <span class="hljs-comment">/* Other small parameters */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_point</span>     <span class="hljs-title">data</span>;</span>       <span class="hljs-comment">/* Other large parameters */</span>
  };
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> {</span>
      __u16               len;        <span class="hljs-comment">/* Real length of this stuff */</span>
      __u16               cmd;        <span class="hljs-comment">/* Wireless IOCTL */</span>
      <span class="hljs-keyword">union</span> iwreq_data    u;          <span class="hljs-comment">/* IOCTL fixed payload */</span>
  }
</code></pre>
</li>
<li><code>struct iw_event</code> 的前两个字段，len 表明了这个 event 的数据长度，cmd 表明了这个 event 的类别，不同的 event，字段 u 中对应的数据结构也不相同；</li>
<li>在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>中，我们只解析了三个 cmd，SIOCGIWAP(MAC地址)、SIOCGIWESSID(SSID) 和 SIOCGIWFREQ(Frequence 和 Channel)：<ul>
<li>当 cmd 为 SIOCGIWAP 时，字段 u 对应的数据结构为 <code>struct sockaddr</code>，MAC 地址存放在 <code>u.addr.sa_data</code> 的前 6 个字节中；</li>
<li>当 cmd 为 SIOCGIWESSID 时，字段 u 对应的数据结构为 <code>struct iw_essid</code>，这个是自己定义的，在上面 union 中并没有列出，这个定义可以使问题更加简单一些；</li>
<li>当 cmd 为 SIOCGIWFREQ 时，字段 u 对应的数据结构为 <code>struct iw_freq freq</code>，当计算出的频率大于 1000 时，则结果为 wifi 信号的工作频率，否则为该信号的工作信道；</li>
</ul>
</li>
<li><p>当 cmd 为 IWEVQUAL，获得的信息为统计数据的信号质量部分(Quality part of statistics)，这部分数据中包括信号质量、信号强度、信号噪音等信息；</p>
<ul>
<li>此时字段 u 对应的数据结构为 <code>struct iw_quality</code>，在 <code>wireless.h</code> 中定义，如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_quality</span> {</span>
      __u8        qual;       <span class="hljs-comment">/* link quality */</span>
      __u8        level;      <span class="hljs-comment">/* signal level (dBm) */</span>
      __u8        noise;      <span class="hljs-comment">/* noise level (dBm) */</span>
      __u8        updated;    <span class="hljs-comment">/* Flags to know if updated */</span>
  };
</code></pre>
</li>
<li><p>qual 字段为信号连接质量，先看一下使用无线工具 <code>sudo iwlist [wifname] scan</code> 扫描信号看到的信号连接质量是什么样子的，[wifname] 是无线网络接口的名称，在我的电脑上是 <code>wlp1s0</code>，不同电脑可能会不一样；</p>
<p>  <img src="https://blog.whowin.net/images/180025/screenshot-of-iwlist-scan.png" alt="Screenshot of wifi scanning" /></p>
</li>
<li><p>图中红线所示部分就是信号连接质量，其表达方式为 <code>47/70</code>，这是什么含义呢？</p>
</li>
<li>WE(Wireless Extension) 假设信号范围为 <code>-110dBm ~~ -40dBm</code>，信号质量的值为信号强度 +110 得出的，这样信号质量值的范围为 <code>0~~70</code>，<code>47/70</code> 的 47 表示信号连接质量值为 47，70 标志信号质量值最大为 70；</li>
<li>level 字段为信号的强度，其单位为 dBm(分贝毫瓦)，通常用于表示无线电信号的功率，如上所述，正常情况下，<code>level + 110 = qual</code></li>
<li>noise 字段为信号背景噪音的强度，这个字段在我的电脑上并不支持，如何判断是否支持，请看下面对 updated 字段的介绍；</li>
<li><p>updated 字段是一个位掩码(bit mask)，在 <code>wireless.h</code> 中定义了该字段每个 bit 表达的含义，如下：</p>
<pre><code class="lang-C">  <span class="hljs-comment">/* Statistics flags (bitmask in updated) */</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_QUAL_UPDATED    0x01    <span class="hljs-comment">/* Value was updated since last read */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_LEVEL_UPDATED   0x02</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_NOISE_UPDATED   0x04</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_ALL_UPDATED     0x07</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_DBM             0x08    <span class="hljs-comment">/* Level + Noise are dBm */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_QUAL_INVALID    0x10    <span class="hljs-comment">/* Driver doesn't provide value */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_LEVEL_INVALID   0x20</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_NOISE_INVALID   0x40</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_RCPI            0x80    <span class="hljs-comment">/* Level + Noise are 802.11k RCPI */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_QUAL_ALL_INVALID     0x70</span>
</code></pre>
</li>
<li>如果网卡驱动程序不支持 quality、level 或者 noise，则 IW_QUAL_QUAL_INVALID、IW_QUAL_LEVEL_INVALID 或者 IW_QUAL_NOISE_INVALID 对应的 bit 就会被置 1；</li>
<li>如果自上次读取 quality、level 或者 noise 后，数据已经被网卡驱动程序再次更新，则 IW_QUAL_QUAL_UPDATED、IW_QUAL_LEVEL_UPDATED 或者 IW_QUAL_NOISE_UPDATED 对应的 bit 会被置 1；</li>
</ul>
</li>
</ul>
<h2 id="heading-4">4 无线信号的工作方式</h2>
<ul>
<li>wireless.h 中定义了 8 中无线信号的工作方式：<pre><code class="lang-C">  <span class="hljs-comment">/* Modes of operation */</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_AUTO        0   <span class="hljs-comment">/* Let the driver decides */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_ADHOC       1   <span class="hljs-comment">/* Single cell network */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_INFRA       2   <span class="hljs-comment">/* Multi cell network, roaming, ... */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_MASTER      3   <span class="hljs-comment">/* Synchronisation master or Access Point */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_REPEAT      4   <span class="hljs-comment">/* Wireless Repeater (forwarder) */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_SECOND      5   <span class="hljs-comment">/* Secondary master/repeater (backup) */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_MONITOR     6   <span class="hljs-comment">/* Passive monitor (listen only) */</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IW_MODE_MESH        7   <span class="hljs-comment">/* Mesh (IEEE 802.11s) network */</span></span>
</code></pre>
</li>
<li>我们扫描到的信号，大多数应该是 Master；</li>
<li>当使用 <code>ioctl()</code> 扫描无线信号时，返回的 <code>struct iw_event</code> 中当 cmd 字段为 SIOCGIWMODE，该事件为工作方式；</li>
<li>当 cmd 为 SIOCGIWMODE时，<code>struct iw_event</code> 中的 <code>u.mode</code> 为该无线信号的工作方式；</li>
</ul>
<h2 id="heading-5">5 无线信号支持的传输速率</h2>
<ul>
<li>当使用 <code>ioctl()</code> 扫描无线信号时，返回的 <code>struct iw_event</code> 中当 cmd 字段为 SIOCGIWRATE 时，该事件中的数据为信号支持的传输速率；</li>
<li>一个信号支持的传输速率通常有很多种，所以这个数据通常也是有很多组的，下面是一组实际的数据(16进制数)：<pre><code>  <span class="hljs-number">0000</span>:   <span class="hljs-number">28</span> <span class="hljs-number">00</span> <span class="hljs-number">21</span> <span class="hljs-number">8</span>B <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">80</span> <span class="hljs-number">8</span>D <span class="hljs-number">5</span>B <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> 
  <span class="hljs-number">0010</span>:   <span class="hljs-number">00</span> <span class="hljs-number">1</span>B B7 <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">36</span> <span class="hljs-number">6</span>E <span class="hljs-number">01</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> 
  <span class="hljs-number">0020</span>:   <span class="hljs-number">00</span> <span class="hljs-number">6</span>C DC <span class="hljs-number">02</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span> <span class="hljs-number">00</span>
</code></pre></li>
<li>按照 <code>struct iw_event</code> 的定义，前两个字节是这个 event 的长度，为 0x0028，也就是 40 个字节，后面两个字节 0x8B21 是 cmd 字段，0x8b21 也就是 SIOCGIWRATE(见 <code>wireless.h</code> 中的定义)，所以这个 event 的数据是传输速率；</li>
<li>当收到的是传输速率时，<code>struct iw_event</code> 中的 <code>u.bitrate</code> 为对应的传输率的数据结构(见第 3 节关于 <code>union iwreq_data</code> 的介绍)，<code>u.bitrate</code> 是一个 <code>struct iw_param</code>，其定义如下(见 wireless.h)：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span> {</span>
      __s32   value;      <span class="hljs-comment">/* The value of the parameter itself */</span>
      __u8    fixed;      <span class="hljs-comment">/* Hardware should not use auto select */</span>
      __u8    disabled;   <span class="hljs-comment">/* Disable the feature */</span>
      __u16   flags;      <span class="hljs-comment">/* Various specifc flags (if any) */</span>
  };
</code></pre>
</li>
<li>根据其定义，其中的 <code>u.bitrate.value</code> 字段即为传输速率；</li>
<li>如上数据，一个 wifi 信号通常都是支持多种传输速率的，这时可以将数据部分定义成一个 <code>struct iw_param</code> 的结构数组，并通过 event 的长度和 <code>struct iw_param</code> 的长度计算得出这个 event 中有多少组传输速率的数据，如下：<pre><code class="lang-C">  ......
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> *<span class="hljs-title">evp</span> = <span class="hljs-title">data</span>;</span>
  <span class="hljs-keyword">int</span> rate_count = (evp.len - IW_EV_LCP_LEN) / <span class="hljs-keyword">sizeof</span>(struct iw_param);
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span> *<span class="hljs-title">rates</span> = &amp;<span class="hljs-title">evp</span>-&gt;<span class="hljs-title">u</span>.<span class="hljs-title">bitrate</span>;</span>
  <span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; rate_count; ++i) {
      ......
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Bit rate: %d Mb/s\n"</span>, rates[i].value / <span class="hljs-number">1000000</span>);
  }
</code></pre>
</li>
<li>其中 IW_EV_LCP_LEN 为 <code>struct iw_event</code> 中结构头(len 和 cmd 字段)长度(包含为对齐而填充的空字符)，请见文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>中的相关解释；</li>
<li>当一个 WiFi 信号支持的传输速率比较多时，可能会收到两个 SIOCGIWRATE 事件(也许会有多个，但我没有遇到过)，每个事件中的速率是不一样的，所以都需要处理，不能忽略任何一个事件；</li>
</ul>
<h2 id="heading-6-beacon">6 beacon 相关信息</h2>
<ul>
<li>当使用 <code>ioctl()</code> 扫描无线信号时，返回的 <code>struct iw_event</code> 中当 cmd 字段为 IWEVCUSTOM 时，该事件在 <code>wireless.h</code> 中定义为 "Driver specific ascii string"，意为：驱动程序特定的 ASCII 字符串；</li>
<li>AP 要周期性地在 wifi 上广播 beacon 帧，用于在网络上宣告一个 wifi 信号的存在，之所以可以扫描到 wifi 信号就是因为收到了 beacon 帧；</li>
<li>beacon 帧并不是本文要讨论的问题，本文不会展开讨论；</li>
<li>回到 WiFi 信号的扫描主题上，<code>wireless.h</code> 中并没有定义一个事件可以收到有关 beacon 帧的信息，但是我们在事件 IWEVCUSTOM 中看到了 beacon 信息；</li>
<li>IWEVCUSTOM 事件中的这个字符串的结构与 essid 是一样的，所以可以用相同的方法提取，可以参考文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>；</li>
<li><p>有意思的是起初并不知道这个字符串中会有什么内容，但把收到的内容显示出来后，发现是类似下面的内容：</p>
<p>  <img src="https://blog.whowin.net/images/180025/screenshot-of-driver-string.png" alt="Screenshot of driver specific ascii string" /></p>
</li>
<li><p>图中红线所示就是收到的字符串，原来这个字符串中藏着与 beacon 帧有关的信息，Last beacon 是最后收到的 beacon 帧时间，TSF 是 Timing Synchronization Function 的缩写，是 beacon 帧中的一个字段，用于 AP 和 Station 之间同步时间；</p>
</li>
<li>这个事件与 essid 还是有所不同，一个 WiFi 信号只有一个 essid，所以 SIOCGIWESSID 事件只会收到一次，但是 IWEVCUSTOM 有可能收到多次，而每次收到的字符串是不相同的，所以接收 IWEVCUSTOM 事件比接收 essid 还是要麻烦一些，在本文实例源程序中这部分有详细的中文注释；</li>
</ul>
<h2 id="heading-7-information-elements">7 Information Elements</h2>
<ul>
<li>当使用 <code>ioctl()</code> 扫描无线信号时，返回的 <code>struct iw_event</code> 中当 cmd 字段为 IWEVGENIE 时，该事件在 <code>wireless.h</code> 中定义为 "Generic IE (WPA, RSN, WMM, ..)"，意为通用 IE(Information Elements)；</li>
<li>IE 可以提供非常多的信息，本文中仅就 SSID 和速率信息进行示范性的解析；</li>
<li>首先，这个事件在大多数情况下都是提供多个 IE，所以事件通常比较长，而且长度并不固定；</li>
<li><p>我们先来看一个实际收到的 IE 数据</p>
<p>  <img src="https://blog.whowin.net/images/180025/screenshot-of-IE-data.png" alt="Real IE data" /></p>
</li>
<li><p>数据与普通事件一样，符合 <code>struct iw_event</code> 结构，前两个字节是事件数据的长度，0x00D2(十进制 210)字节，第 3、4 字节为事件类型，0x8C05(IWEVGENIE) 表示这个事件中是 IE 数据；</p>
</li>
<li>前四个字节组成了 <code>struct iw_event</code> 的头信息，紧跟着的 4 个字节为按 64 位对齐填充的字符，第 9、10 字节为实际 IE 数据所占的长度 ie_length，第 11-16 字节是对齐填充字节；</li>
<li>所以从第 17 个字节(第 2 行)开始才是真正的 IE 数据，正是因为这个原因，通常 ie_length(第 9、10 字节) 比事件的总长度(第 1、2 字节)小 16(0x10)，在这组实际数据中，事件长度为 0x00D2(十进制 210)，而 IE 数据的长度为 0x00C2(十进制 194)，要注意的是，事件长度是包含 <code>struct iw_event</code> 头信息的 8 个字节，而 IE 数据长度是不包括头信息的长度(仅为 IE 数据长度)；</li>
<li>IEEE 标准 <code>802.11-2007</code> 文档中定义了 IE 的结构，该文档的下载地址如下：<ul>
<li><a target="_blank" href="https://people.iith.ac.in/tbr/teaching/docs/802.11-2007.pdf">Wireless LAN Medium Access Control (MAC) and Physical Layer (PHY) Specifications</a></li>
</ul>
</li>
<li><p>该标准的 <code>7.3.2 Information elements</code> 定义了 IE 的结构：</p>
<p>  <img src="https://blog.whowin.net/images/180025/screenshot-of-IE-structure.png" alt="IE Structure" /></p>
</li>
<li><p>每个 IE 符合 <code>type-length-value</code> 格式，即：第 1 个字节表示 IE 类型 Element ID，第 2 个字节表示数据的长度 Length，后面若干字节为实际数据 Information，数据长度为 Length；</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ieee80211_ie</span> {</span>
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> eid;
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> len;
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> data[<span class="hljs-number">0</span>];
  };
</code></pre>
</li>
<li>当收到一个 IE 数据的事件时，可以考虑如下定义：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event_ie</span> {</span>
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">short</span> len;
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">short</span> cmd;
  <span class="hljs-meta">#<span class="hljs-meta-keyword">ifdef</span> __x86_64__           <span class="hljs-comment">// 64位系统按8字节对齐</span></span>
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">short</span> __attribute((aligned(<span class="hljs-number">8</span>)))ie_len;
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ieee80211_ie</span> __<span class="hljs-title">attribute</span>((<span class="hljs-title">aligned</span>(8)))<span class="hljs-title">ie</span>[0];</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">else</span>                       <span class="hljs-comment">// 32位系统按4字节对齐</span></span>
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">short</span> __attribute((aligned(<span class="hljs-number">4</span>)))ie_len;
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ieee80211_ie</span> __<span class="hljs-title">attribute</span>((<span class="hljs-title">aligned</span>(4)))<span class="hljs-title">ie</span>[0];</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">endif</span></span>
  };
</code></pre>
</li>
<li>字段 len 和 cmd 与 <code>struct iw_event</code> 是一致的，ie_len 字段将得到 ie 这个字段所对应的数据的总长度，<code>struct ieee80211_ie</code> 则定义了每个 IE 的结构，具体有多少个 IE 则需要在遍历 IE 时根据 ie_len 字段的值做出判断；</li>
<li><p><code>struct ieee80211_ie</code> 中的 eid(Element ID) 字段定义了这个 IE 的类型，这些类型在 <code>802.11-2007</code> 的文档中第 100 页有定义，这里摘录其中的一部分：</p>
<p>  <img src="https://blog.whowin.net/images/180025/screenshot-of-80211-2007-1.png" alt="Parts of IE type definition" /></p>
</li>
<li><p>从上面定义可以看到，当 Element ID 为 0 时，其信息内容为 SSID，以上面的实际数据为例，数据的第二行，也就是第 1 个 IE 的数据为：</p>
<pre><code class="lang-plain">  00 07 31 35 2D 31 31 30 31
</code></pre>
<ul>
<li>按照 IE 的格式定义，Element ID 为 0，表示其信息为 SSID，数据长度 Length 为 7，所以后面的 7 个字节 <code>31 35 2D 31 31 30 31</code> 为实际数据；</li>
<li>查 ASCII 表，这个数据其实就是 "15-1101"，这就是这个信号的 SSID</li>
</ul>
</li>
<li>IE 提供了一种非常灵活的传递信息的方法，内容非常丰富，在本文所载实例中仅就其中的几个进行了解析； </li>
<li>还要简单介绍一下所支持传输速率的 IE，因为在实例中解析了这个 IE;</li>
<li><p>根据 Element ID 的定义，当 Element ID 为 1 时，IE 的数据为该信号所支持的传输速率，其结构在 <code>802.11-2007</code> 文档的第 102 页有介绍：</p>
<p>  <img src="https://blog.whowin.net/images/180025/screenshot-of-rates.png" alt="Supported Rates" /></p>
</li>
<li><p>根据文档，一个 IE 中最多描述 8 个传输速率，单位为 500 kb/s，每个速率占用 1 个字节，其最高位(bit 7)有其它意义，(bit 0 - 6) 表示速率值，所以在计算时要将 bit 7 过滤掉；</p>
</li>
<li>IE 信息会有空信息，也就是长度字段为 0，这种 IE 通常只有 2 个字节，没有意义，在实际解析中要过滤掉，比如：
  <code>00 00</code></li>
</ul>
<h2 id="heading-8">8 实例</h2>
<ul>
<li>完整的源代码，文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180025/wifi-new-scanner.c">wifi-new-scanner.c</a>(<strong>点击文件名下载源程序</strong>)，请务必使用 UTF-8 字符集，否则源程序中的中文注释为乱码；</li>
<li>阅读这个源码最好先阅读文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>，并搞懂其中的源代码；</li>
<li>简述一下程序的基本流程：<ul>
<li>通过读取文件 <code>/proc/net/wireless</code> 获取无线网络接口的列表(在文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>中使用的是另一种方法)；</li>
<li>使用 <code>ioctl()</code> 向无线网络接口发出 wifi 信号扫描指令；</li>
<li>使用 <code>ioctl()</code> 获取扫描结果，根据返回的结果生成事件链表；</li>
<li>从事件链表中分析每一个事件，从中解析出每个信号的属性，生成 ap 链表；</li>
<li>如果有 IE 事件，还要在 AP 链表中生成 IE 链表；</li>
<li>从 AP 链表中解析出信息并显示出来；</li>
</ul>
</li>
<li>比较<a target="_blank" href="https://blog.csdn.net/whowin/article/details/131504380">《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》</a>中的实例，本文除了解析出 MAC 地址、ESSID、工作频率、工作信道外，还可以解析出信号质量、工作模式、信号支持的传输速率、驱动程序字符串以及 Information Elements；</li>
<li>源程序中有比较详细的注释请自行参考；</li>
<li>其中比较复杂的是 IE 的解析，IE 的内容极其丰富，作为示范，本例仅解析了 SSID 和速率，需要更多信息的读者可以查阅 <a target="_blank" href="https://people.iith.ac.in/tbr/teaching/docs/802.11-2007.pdf">802.11-2007</a> 文档；</li>
<li>编译：<code>gcc -Wall wifi-new-scanner.c -o wifi-new-scanner -lm</code></li>
<li>运行：<code>sudo ./wifi-new-scanner</code></li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/180025/wifi-new-scanner.gif" alt="GIF of running wifi-new0scanner" /></p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[使用epoll()进行socket编程处理多客户连接的TCP服务器实例]]></title><description><![CDATA[在网络编程中，当需要使用单线程处理多客户端的连接时，常使用select()或者poll()来处理，但是当并发数量非常大时，select()和poll()的性能并不好，epoll()的性能大大好于select()和poll()，在编写大并发的服务器软件时，epoll()应该是首选的方案，本文介绍epoll()在网络编程中的使用方法，本文提供了一个具体的实例，并附有完整的源代码，本文实例在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0。

1 基本概念

在『网络编程专栏』中...]]></description><link>https://whowin.cn/180024-using-epoll-in-socket-programming</link><guid isPermaLink="true">https://whowin.cn/180024-using-epoll-in-socket-programming</guid><category><![CDATA[Linux]]></category><category><![CDATA[网络编程]]></category><category><![CDATA[epoll]]></category><category><![CDATA[socket]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Sat, 09 Mar 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729259439302/3b269da6-c63d-41a6-8610-07c72ceada4d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>在网络编程中，当需要使用单线程处理多客户端的连接时，常使用select()或者poll()来处理，但是当并发数量非常大时，select()和poll()的性能并不好，epoll()的性能大大好于select()和poll()，在编写大并发的服务器软件时，epoll()应该是首选的方案，本文介绍epoll()在网络编程中的使用方法，本文提供了一个具体的实例，并附有完整的源代码，本文实例在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0。</p>
</blockquote>
<h2 id="heading-1">1 基本概念</h2>
<ul>
<li>在<a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a>中，有两篇文章都涉及到了使用 <code>select()</code> 处理多个 <code>socket</code> 连接：<ul>
<li><a target="_blank" href="https://blog.csdn.net/whowin/article/details/129410476">《使用select实现的UDP/TCP组合服务器》</a></li>
<li><a target="_blank" href="https://blog.csdn.net/whowin/article/details/129685842">《TCP服务器如何使用select处理多客户连接》</a></li>
</ul>
</li>
<li>在<a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a>中，有一篇文章都涉及到了使用 <code>poll()</code> 处理多个 <code>socket</code> 连接：<ul>
<li><a target="_blank" href="https://blog.csdn.net/whowin/article/details/136454503">《使用poll()代替select()处理多客户连接的TCP服务器实例》</a></li>
</ul>
</li>
<li><code>poll()</code> 和 <code>select()</code> 的编程方法非常相似，但 epoll 有较大区别；</li>
<li>epoll 完成与 <code>poll()</code> 相似的工作：监视多个文件描述符看它们是否可以进行 I/O 操作；</li>
<li>epoll 的核心概念就是 epoll 实例，从用户空间的角度看，一个 epll 实例就是内核中的一个数据结构，可以被看成是下列两个列表的容器；<ul>
<li><strong>Interest List</strong>：(有时也被称为 epoll set)进程向 epoll 登记的需要被监视的文件描述符集；</li>
<li><strong>Ready List</strong>：可以无阻塞地进行 I/O 操作的文件描述符集，Ready List 是 Interest List 的一个子集，内核实时地把可以进行 I/O 操作的文件描述符从 Interest List 填充到 Ready List；</li>
</ul>
</li>
<li>使用 epoll 的过程就是将要监视的文件描述符向 epoll 登记进入 Interest List，然后从 Ready List 中处理那些可以进行 I/O 操作的文件描述符；</li>
<li>使用 epoll 有三个基本的函数，后面会详细介绍这三个函数的使用方法：<ul>
<li><code>epoll_create1()</code> - 用于建立一个 epoll 实例；</li>
<li><code>epoll_ctl()</code> - 用于向 epoll 实例的 Interest List 中添加要监视的文件描述符，或者修改/删除 Interest List 中的文件描述符；</li>
<li><code>epoll_wait()</code> - 用于监视已经登记的文件描述符集，当有一个或多个被监视的文件描述符可以进行 I/O 操作时返回；</li>
</ul>
</li>
<li>在调用 <code>epoll_wait()</code> 后，有两种触发方式可以使 <code>epoll_wait()</code> 返回，边沿触发(Edge-Triggered)和电平触发(Level-Triggered)，这两个词是从电子电路中引申过来的，熟悉电子电路的或者做嵌入式编程的读者应该对此有些了解；</li>
<li>可以用一个例子来说明这两种触发方式的不同，假设在下列条件下，看看边沿触发和电平触发有什么不同：<ol>
<li>将一个管道(pipe)读出端的文件描述符 rfd 登记到 epoll 实例上进行监视；</li>
<li>在管道的写入端写入 2kb 的数据；</li>
<li>调用 <code>epoll_wait()</code> 会返回文件描述符 rfd，表示在 rfd 上有数据可以读取；</li>
<li>从 rfd 中读取 1kb 的数据；</li>
<li>再次调用 <code>epoll_wait()</code>；</li>
</ol>
</li>
<li>当使用电平触发(Level-Triggered)方式时，只要 rfd 中仍然还有数据没有读出，<code>epoll_wait()</code> 就会被触发返回，由于写入了 2kb 数据但只读出了 1kb，所以在第 5 步时，<code>epoll_wait()</code> 会返回 rfd 有数据可读；</li>
<li>当使用边沿触发(Edge-Triggered)方式时，只有当 rfd 从没有数据可读变为有数据可读时才会触发 <code>epoll_wait()</code> 返回，虽然读缓冲区中仍有 1kb 的数据没有被读出，但在第 5 步时 <code>epoll_wait()</code> 是不会返回的；</li>
<li>当使用电平触发方式时，epoll 实际上只是一个运行的比较快的 <code>poll()</code>，可以在任何使用 <code>poll()</code> 的地方使用电平触发方式的 epoll，epoll 真正的意义在于其边缘触发方式；</li>
<li>由于边沿触发方式的特点，<code>epoll_wait()</code> 被触发后必须将读缓冲区的数据全部读出，否则可能会有数据丢失，所以当使用边沿触发方式时，通常需要将文件描述符设置成非阻塞方式，然后循环读取，直至出现 EAGAIN 错误代码为止，如下<pre><code class="lang-C">  ......
  <span class="hljs-keyword">int</span> done = <span class="hljs-number">0</span>;               <span class="hljs-comment">// not done</span>
  <span class="hljs-keyword">int</span> nbytes = <span class="hljs-number">0</span>;             <span class="hljs-comment">// how many bytes to read</span>
  <span class="hljs-keyword">do</span> {
      nbytes = recv(fd, buffer, <span class="hljs-keyword">sizeof</span>(buffer), <span class="hljs-number">0</span>);
      <span class="hljs-keyword">if</span> (nbytes &gt; <span class="hljs-number">0</span>) {
          buffer[nbytes] = <span class="hljs-string">'\0'</span>;
          ...
          <span class="hljs-keyword">continue</span>;
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (rc == <span class="hljs-number">0</span>) {
          <span class="hljs-comment">// the socket discinnected</span>
          <span class="hljs-keyword">break</span>;
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (errno == EINTR) {
          <span class="hljs-comment">// if errno==EINTR, it means socket is not closed, just because some network errors happened</span>
          <span class="hljs-keyword">continue</span>;
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (errno == EAGAIN) {
          done = <span class="hljs-number">1</span>;
          <span class="hljs-keyword">break</span>;
      } <span class="hljs-keyword">else</span> {
          perror(<span class="hljs-string">"recv() failed"</span>);
          <span class="hljs-keyword">break</span>;
      }
  } <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>);
  ......
</code></pre>
</li>
<li>本文后面的实例中将演示边沿触发方式的具体编程方法；</li>
</ul>
<h2 id="heading-2-epoll">2 epoll 的基本使用方法</h2>
<ul>
<li>前面提到过了使用 epoll 的三个基本函数，本节将着重介绍这些函数的使用方法及相关的数据结构；</li>
<li><p><strong>epoll_create1()</strong> - 创建一个 epoll 实例</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/epoll.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">epoll_create</span><span class="hljs-params">(<span class="hljs-keyword">int</span> size)</span></span>;
  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">epoll_create1</span><span class="hljs-params">(<span class="hljs-keyword">int</span> flags)</span></span>;
</code></pre>
<ul>
<li>这两个函数都是创建一个 epoll 实例，在 epoll_create() 中的参数 size 表示在这个 epoll 实例上所管理的最大描述符的数量，但从 Linux 2.6.8 以后，这个参数已经无效，但参数 size 必须是一个大于 0 的整数；</li>
<li>实际上通常都是使用 epoll_create1() 来建立一个 epoll 实例，参数 flags 通常设为 0；</li>
<li>调用成功，函数返回 epoll 实例的文件描述符，调用失败时返回 -1，errno 中是错误代码；</li>
</ul>
</li>
<li><p><strong>epoll_ctl()</strong> - epoll 文件描述符的控制接口</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/epoll.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">epoll_ctl</span><span class="hljs-params">(<span class="hljs-keyword">int</span> epfd, <span class="hljs-keyword">int</span> op, <span class="hljs-keyword">int</span> fd, struct epoll_event *event)</span></span>;
</code></pre>
<ul>
<li>这个函数用于向 epoll 实例的 Interest List 中添加、修改和删除文件描述符，具体操作取决于参数 op；</li>
<li><strong>epfd</strong> 为 epoll_create1() 返回的 epoll 实例的文件描述符</li>
<li>当 op 为 <strong>EPOLL_CTL_ADD</strong> 时，表示要添加一个文件描述符 fd 进入 epoll 实例的 Interest List 中；</li>
<li>当 op 为 <strong>EPOLL_CTL_MOD</strong> 时，表示要修改一个已经在 Interest List 中的文件描述符 fd；</li>
<li>当 op 为 <strong>EPOLL_CTL_DEL</strong> 时，表示要将一个已经在 Interest List 中的文件描述符 fd 从 Intersst List 中删除；</li>
<li>参数 <strong>fd</strong> 为想要操作的文件描述符；</li>
<li>参数 <strong>event</strong> 在添加和修改时是有意义的，在删除时可以设置为 NULL；</li>
<li><p><strong>struct epoll_event</strong> 的定义如下：</p>
<pre><code class="lang-C">  <span class="hljs-keyword">typedef</span> <span class="hljs-keyword">union</span> epoll_data {
      <span class="hljs-keyword">void</span>        *ptr;
      <span class="hljs-keyword">int</span>          fd;
      <span class="hljs-keyword">uint32_t</span>     u32;
      <span class="hljs-keyword">uint64_t</span>     u64;
  } <span class="hljs-keyword">epoll_data_t</span>;

  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">epoll_event</span> {</span>
      <span class="hljs-keyword">uint32_t</span>     events;      <span class="hljs-comment">/* Epoll events */</span>
      <span class="hljs-keyword">epoll_data_t</span> data;        <span class="hljs-comment">/* User data variable */</span>
  };
</code></pre>
</li>
<li>struct epoll_event 中的 events 是一个位掩码，由以下零个或多个可用事件类型组合而成(这里仅列出常用的几个)：<ul>
<li><strong>EPOLLIN</strong>：相应的文件描述符上有数据可读；</li>
<li><strong>EPOLLOUT</strong>：相应的文件描述符上可以进行写操作；</li>
<li><strong>EPOLLET</strong>：使用边沿触发方式；</li>
</ul>
</li>
<li><p>下面代码将一个文件描述符 fd 加入到 epoll 实例 epfd 的 Interest List 中，使用边沿触发方式，当可以进行读操作时触发 epoll_wait() 返回：</p>
<pre><code class="lang-C">  ......
  <span class="hljs-keyword">int</span> epfd = epoll_create1(<span class="hljs-number">0</span>);
  ...
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">epoll_event</span> <span class="hljs-title">event</span>;</span>

  <span class="hljs-built_in">memset</span>(&amp;event, <span class="hljs-number">0</span> , <span class="hljs-keyword">sizeof</span>(struct epoll_event));
  <span class="hljs-comment">// Set up the structure epoll_event</span>
  event.data.fd = fd;
  event.events = EPOLLIN | EPOLLET;
  <span class="hljs-comment">// Add a new descriptor to the interest list</span>
  <span class="hljs-keyword">if</span> (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &amp;event) == <span class="hljs-number">-1</span>) {
      perror(<span class="hljs-string">"EPOLL_CTL_ADD failed"</span>);
  }
  ......
</code></pre>
</li>
<li>该函数调用成功时返回 0，失败时返回 -1，errno 中为错误代码；</li>
</ul>
</li>
<li><p><strong>epoll_wait()</strong> - 等待 epoll 文件描述符上的 I/O 事件</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/epoll.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">epoll_wait</span><span class="hljs-params">(<span class="hljs-keyword">int</span> epfd, struct epoll_event *events, <span class="hljs-keyword">int</span> maxevents, <span class="hljs-keyword">int</span> timeout)</span></span>;
</code></pre>
<ul>
<li>调用 <code>epoll_wait()</code> 后，当 epoll 实例中被监视的文件描述符有事件产生或者超时时间 timeout 到，该函数将返回；</li>
<li>参数 <strong>epfd</strong> 为使用 <code>epoll_create1()</code> 返回的 epoll 实例的文件描述符；</li>
<li>参数 <strong>events</strong> 中将返回所有有事件产生的 fd，<code>events-&gt;data.fd</code> 为产生事件的文件描述符，<code>events-&gt;events</code> 为实际产生的事件(位掩码)；</li>
<li>参数 <strong>maxevent</strong> 为返回事件的最大值，必须大于 0，<code>epoll_wait()</code> 在参数 events 中返回的事件不会大于 maxevents；</li>
<li>参数 <strong>timeout</strong> 为超时时间，单位为毫秒，<code>epoll_wait()</code> 等待 timeout 时长后不论是否有事件产生都会返回，将 timeout 设为 -1，<code>epoll_wait()</code> 将一直等待直至有事件产生，将 timeout 设为 0，<code>epoll_sait()</code> 将立即返回，不论是否有事件产生；</li>
<li><code>epoll_wait()</code> 调用成功时，返回一个正整数，表示在参数 events 中有多少个事件；</li>
<li><code>epoll_wait()</code> 因超时返回时，将返回 0；</li>
<li><code>epoll_wait()</code> 调用失败将返回 -1，errno 中为错误代码；</li>
<li><code>epoll_wait()</code> 可以被信号打断，此时，错误代码为 EINTR，通常情况下如果 errno 为 EINTR 时可以重新调用 <code>epoll_wait()</code>；</li>
</ul>
</li>
</ul>
<h2 id="heading-3-epoll-socket">3 epoll 进行 socket 编程的基本步骤</h2>
<ul>
<li>尽管 epoll 监视的事件是文件描述符的事件，但通常不会用在普通文件(指文件系统下的文件)，一个普通文件将永远处于可读或者可写的状态，epoll 更多地是用在 socket 编程上；</li>
<li><p>epoll 进行 socket 编程的基本步骤：</p>
<ol>
<li>使用 <code>socket()</code> 建立需要侦听的 socket；</li>
<li>使用 <code>setsockopt()</code> 设置 socket 为可重复使用；</li>
<li>使用 <code>ioctl()</code> 设置 socket 为非阻塞；</li>
<li>使用 <code>bind()</code> 绑定服务器的地址和端口；</li>
<li>使用 <code>listen()</code> 开始侦听端口；</li>
<li>以上步骤和使用 <code>select()/poll()</code> 编程时是一致的；</li>
<li>使用 <code>epoll_create1()</code> 构建一个 epoll 实例 epfd； </li>
<li>构建一个结构 <code>struct epoll_event ev</code>，将服务器侦听 socket 加入到加入到结构中，并设置 EPOLLIN 事件及边沿触发方式(EPOLLET)；</li>
<li>使用 <code>epoll_ctl()</code> 的 <code>EPOLL_CTL_ADD</code> 方法将侦听 socket 加入到 epoll 实例 epfd 的 Interest List 中；</li>
<li><p>启动 <code>epoll_wait()</code>；</p>
<ul>
<li>返回 0 表示调用超时，可以重新启动 <code>epoll_wait()</code>；</li>
<li>返回 <code>&lt;0</code> 表示 <code>epoll_wait()</code> 出错，errno 中为错误代码；</li>
<li><p>返回 <code>&gt;0</code> 表示有需要处理的 socket，进行处理；</p>
<blockquote>
<p>要处理的 socket 通常又分为两种，一种是正在侦听的 socket，如果有 <code>EPOLLIN</code> 事件表示有客户端发出了连接请求，使用 <code>accept()</code> 接受连接将产生一个新的 socket，这个新的 socket 要按照步骤 7、8 的方法加入到 epoll 实例的 Interest List 中，以便在 epoll 中可以被监视，因为我们使用的边沿触发方式，所以还要记得使用 <code>ioctl()</code> 将这个新的 socket 设置成非阻塞；</p>
<p>另一类 socket 就是已经和服务器建立连接的一个或多个客户端的 socket，这类 socket 有 EPOLLIN 事件产生可能是有数据发送回来，也可能是因为连接中断，在调用 <code>recv()</code> 从 socket 中接收数据时，如果返回值 <code>&gt;0</code> 表示确实有数据发送回来，要做出相应处理，如果返回值为 0 则表示这个连接已经中断，此时只需将该 socket 关闭即可，理论上说，当一个 socket 被关闭后，epoll 会自动地将该 socket 从 Interest List 中删除，所以通常我们不需要显式地使用 epoll_ctl() 的 EPOLL_CTL_DEL 方法从 epoll 实例的 Interest List 中删除这个 socket；</p>
</blockquote>
</li>
</ul>
</li>
<li><p>回到步骤 9，再次启动 <code>epoll_wait()</code>；</p>
</li>
</ol>
</li>
</ul>
<h2 id="heading-4-epoll-tcp">4 实例：一个使用 epoll() 的 TCP 服务器</h2>
<ul>
<li><strong>源程序</strong>：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180024/epoll-server.c">epoll-server.c</a>(<strong>点击文件名下载源程序，建议使用UTF-8字符集</strong>)演示了使用 epoll 完成的一个 TCP 服务器；</li>
<li>编译：<code>gcc -Wall -g epoll-server.c -o epoll-server</code></li>
<li>运行：<code>./epoll-server</code></li>
<li>该程序是一个多进程程序，程序会建立一个服务端进程和若干个(默认为 3 个，由宏 MAX_CONNECTIONS 控制)客户端进程；</li>
<li>服务端进程侦听在端口 8888 上，等待客户端进程的连接；</li>
<li>启动 <code>epoll_wait()</code> 监视 socket；</li>
<li>服务端在接受客户端请求后，将新连接的 socket 加入到 epoll 实例中，并向客户端发送一条欢迎信息；</li>
<li>客户端在连接建立以后向服务端发送一条信息，服务端在收到客户端信息后会将该信息原封不动地发送回客户端；</li>
<li>客户端判断收到的信息与自己发出的信息一样后，主动关闭连接，然后退出进程；</li>
<li>服务端发现连接中断后，关闭该 socket，并使用 epoll_ctl() 的 EPOLL_CTL_DEL 方法从 epoll 中删除该失效 socket(这一步可以没有)，然后继续启动 <code>epoll_wait()</code> 监视 socket；</li>
<li>服务进程中拦截了 SIGINT 信号，这个信号可以使用 <code>ctrl + c</code> 产生，服务进程在收到这个信号后将退出进程；</li>
<li>主进程监视客户端进程的退出，当所有客户端进程都已退出后，向服务端进程发送 SIGINT 信号，使服务端进程退出，整个程序运行结束；</li>
<li>该程序的客户端进程的程序与文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/136454503">《使用poll()代替select()处理多客户连接的TCP服务器实例》</a>中的客户端程序完全一样；</li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/180024/screenshot-of-epoll-server.png" alt="Screenshot of epoll-server" /></p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[使用poll()代替select()处理多客户连接的TCP服务器实例]]></title><description><![CDATA[在网络编程中，使用 select() 处理多客户端的连接是非常常用的方法，select() 是一个非常古老的方法，在大量连接下会显得效率不高，而且其对描述符的数值还有一些限制，Linux内核从 2.1.13 版以后提供了 poll() 替代 select()，本文介绍 poll() 在网络编程中的使用方法，并着重介绍 poll() 在编程行与 select() 的区别，旨在帮助熟悉 select() 编程的程序员可以很容易地使用 poll() 编程，本文提供了一个具体的实例，并附有完整的源代码，...]]></description><link>https://whowin.cn/180021-using-poll-instead-of-select</link><guid isPermaLink="true">https://whowin.cn/180021-using-poll-instead-of-select</guid><category><![CDATA[Linux]]></category><category><![CDATA[网络编程]]></category><category><![CDATA[poll]]></category><category><![CDATA[select]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Mon, 26 Feb 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729226751555/41834f6e-5cb9-46fe-acd9-6316b69d6941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>在网络编程中，使用 select() 处理多客户端的连接是非常常用的方法，select() 是一个非常古老的方法，在大量连接下会显得效率不高，而且其对描述符的数值还有一些限制，Linux内核从 2.1.13 版以后提供了 poll() 替代 select()，本文介绍 poll() 在网络编程中的使用方法，并着重介绍 poll() 在编程行与 select() 的区别，旨在帮助熟悉 select() 编程的程序员可以很容易地使用 poll() 编程，本文提供了一个具体的实例，并附有完整的源代码，本文实例在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0。</p>
</blockquote>
<h2 id="heading-1">1 前言</h2>
<ul>
<li>在<a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a>中，有两篇文章都涉及到了使用 <code>select()</code> 处理多个 <code>socket</code> 连接：<ul>
<li><a target="_blank" href="https://blog.csdn.net/whowin/article/details/129410476">《使用select实现的UDP/TCP组合服务器》</a></li>
<li><a target="_blank" href="https://blog.csdn.net/whowin/article/details/129685842">《TCP服务器如何使用select处理多客户连接》</a></li>
</ul>
</li>
<li>Linux 上的另一个系统调用 <code>poll()</code>，可以和 <code>select()</code> 完成相同的工作，而且通常认为 <code>poll()</code> 的效率要高于 <code>select()</code>；</li>
<li>似乎 <code>select()</code> 要比 <code>poll()</code> 更普及一些，这可能是因为在 Linux 内核 2.0 版以前是不支持 <code>poll()</code> 的，只有 <code>select()</code>，直到 <code>2.1.13</code> 版后才开始既支持 <code>select()</code> 也支持 <code>poll()</code>；</li>
<li>另一个导致 <code>select()</code> 更加普及的原因可能是大多数的应用程序需要同时处理的 <code>I/O</code> 数量并不是很多，使得对性能的要求不高，其实在大量的 <code>I/O</code> 处理上，<code>poll()</code> 和 <code>select()</code> 在性能上的差别还是挺大的；</li>
<li>本文假定读者已经对 socket 编程和 <code>select()</code> 函数有基本的理解，有关这方面的知识请自行参考前面提到的两篇文章，本文不再讨论；</li>
<li><code>select()</code> 和 <code>poll()</code> 并不适用于普通文件(指文件系统上的文件)，一个普通文件将永远处于可读或者可写的状态，不管是使用 <code>select()</code> 还是 <code>poll()</code>，都会一直返回；</li>
<li>通常情况下，<code>select()</code> 和 <code>poll()</code> 用于 <code>socket</code>、管道等，尽管我们在 <code>D-Bus</code> 的文章中也使用了 <code>select()</code>，但实际上 <code>D-Bus</code> 使用的是 <code>socket</code> 或者管道，<code>D-Bus</code> 只是将其抽象化了；</li>
<li><code>select()</code> 和 <code>poll()</code> 实际上完成的功能非常近似，使用上的差异也不大；</li>
<li>本文讨论的重点是使用 <code>poll()</code> 替代以前使用 <code>select()</code> 的程序，会介绍 <code>poll()</code> 的使用方法，以及 <code>poll()</code> 和 <code>select()</code> 在编程上的区别，并最终实现一个使用 <code>poll()</code> 的 TCP 服务器；</li>
<li>尽管普遍认为 <code>epoll()</code> 在处理多连接方面表现更加优异，但 <code>epoll()</code> 的编程方式与 <code>select()</code> 和 <code>poll()</code> 有较大区别，所以本文不会讨论 <code>epoll()</code> 相关的编程方法；</li>
</ul>
<h2 id="heading-2-poll">2 poll() 的基本使用方法</h2>
<ul>
<li>poll() 函数的输入参数中有一个结构数组，结构中包含有一个描述符字段，poll() 函数等待在这些描述符上，当结构数组中的任何一个或多个描述符上有事件发生时，该函数将返回；</li>
<li><p>poll() 函数的定义如下：</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;poll.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">poll</span><span class="hljs-params">(struct pollfd *fds, <span class="hljs-keyword">nfds_t</span> nfds, <span class="hljs-keyword">int</span> timeout)</span></span>;
</code></pre>
</li>
<li><p><code>struct pollfd</code> 的定义</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">pollfd</span>
  {</span>
      <span class="hljs-keyword">int</span> fd;                 <span class="hljs-comment">/* File descriptor to poll.  */</span>
      <span class="hljs-keyword">short</span> <span class="hljs-keyword">int</span> events;       <span class="hljs-comment">/* Types of events poller cares about.  */</span>
      <span class="hljs-keyword">short</span> <span class="hljs-keyword">int</span> revents;      <span class="hljs-comment">/* Types of events that actually occurred.  */</span>
  };
</code></pre>
</li>
<li>在调用 <code>poll()</code> 之前，要先初始化 <code>fds</code>；<ul>
<li><code>fds</code> 中的 fd 是要监视的文件描述符，可以是文件、<code>socket</code>、管道、设备等，比较常用的是在一个 TCP 服务器上监视侦听的 <code>socket</code> 以及与多个客户端之间的连接 <code>socket</code>；</li>
<li><code>fds</code> 中的 events 是要在这个描述符上监视哪些事件，可监视的事件主要有：<ul>
<li><code>POLLIN</code> - 当 socket 上有数据可读时，触发该事件；</li>
<li><code>POLLOUT</code> - 当向 socket 上写入数据不会产生阻塞时，触发该事件；</li>
<li><code>POLLPRI</code> - 当 socket 上有紧急的数据需要读取时，触发该事件；</li>
<li><code>POLLERR</code> - 当 socket 上产生一个异步错误时，触发该事件；</li>
<li><code>POLLHUP</code> - 当 socket 连接已经断开时，触发该事件，该事件仅在输出时有效；</li>
<li><code>POLLNVAL</code> - 无效的请求，通常表示描述符没有打开，触发该事件；</li>
<li>可以使用 "或" 操作在一个描述符上同时监视多个事件，最常用的监视事件为 <code>PULLIN</code> 和 <code>POLLOUT</code>，同时监视这两个事件时可以表达为 <code>POLLIN | POLLOUT</code>；</li>
</ul>
</li>
<li><code>revents</code> 是实际发生的事件，当 <code>poll()</code> 返回时会填充该字段，程序通过这个字段可以判断在这个描述符上发生了什么事件； </li>
</ul>
</li>
<li><code>poll()</code> 函数的第二个参数 <code>nfds</code> 是 fds 数组中有效的条目的数量，比如结构数组的 fds 可以容纳的条目最大为 100 个，但只有前 10 个是我们准备监视的描述符，则 <code>nfds</code> 应该为 10；</li>
<li><code>poll()</code> 函数的第三个参数 <code>timeout</code> 是超时时间，调用 <code>poll()</code> 会进入阻塞，等待被监视的描述符产生事件，如果设置了 <code>timeout</code> 参数，则阻塞 <code>timeout</code> 时间后，即便没有产生事件，<code>poll()</code> 函数也会返回；</li>
<li><code>timeout</code> 的时间单位为毫秒；<ul>
<li>将 <code>timeout</code> 设为 -1 表示永久等待，直至有事件产生；</li>
<li>将 <code>timeout</code> 设为 0，<code>poll()</code> 将立即返回，不管是否有事件产生；</li>
</ul>
</li>
<li><p><code>poll()</code> 的返回值有三种情况：</p>
<ul>
<li>当有事件产生时，<code>poll()</code> 返回一个大于 0 的正整数，表示有多少个文件描述符上有事件产生，程序需要遍历 fds 数组，查看结构中的 revent 字段来判断那个文件描述符上产生了哪些事件；</li>
<li>当没有事件产生，<code>poll()</code> 仅仅是因为超时返回时，<code>poll()</code> 返回 0；</li>
<li>当出现错误时，<code>poll()</code> 返回 -1，此时，errno 中存放有错误代码，详情可以查看在线手册 <code>man 2 poll</code>；</li>
</ul>
</li>
<li><p><code>poll()</code> 检测到 <code>socket</code> 有数据可读时，如果读出的数据长度为 0 时，认为该 <code>socket</code> 连接已经断开；</p>
</li>
<li>poll() 的返回值有四种可能：<ul>
<li><strong>0</strong>：表示调用超时；</li>
<li><strong>-1</strong>：表示调用失败，errno 中为错误代码；</li>
<li><strong>1</strong>：表示在被监听的描述符中，只有一个描述符上有事件产生，等待处理；</li>
<li><strong>1+</strong>：表示在被监听的描述符中，有多个描述符上有等待处理的事件，返回值为等待处理的描述符的数量；</li>
</ul>
</li>
</ul>
<h2 id="heading-3-poll-select">3 poll() 和 select() 在编程上的区别</h2>
<ul>
<li>通常情况下，poll() 的代码会比 select() 简单一些；</li>
<li>先看一下 select() 编程的代码：<pre><code class="lang-C">  fd_set fds;
  FD_ZERO(fd_set);
  FD_SET(<span class="hljs-number">500</span>, &amp;fds);
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">tv</span>;</span>
  tv.tv_sec = <span class="hljs-number">5</span>;
  tv.tv_usec = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">if</span> (select(<span class="hljs-number">501</span>, &amp;fds, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>, &amp;tv)) {
      <span class="hljs-keyword">if</span> (FD_ISSET(<span class="hljs-number">500</span>, &amp;fds)) {
          ... 
      }
  }
</code></pre>
</li>
<li>再看一下完成同样工作的 poll() 编程的代码：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">pollfd</span> <span class="hljs-title">pfd</span>[10];</span>
  pfd[<span class="hljs-number">0</span>].fd = <span class="hljs-number">500</span>;
  pfd[<span class="hljs-number">0</span>].events = POLLIN;
  <span class="hljs-keyword">if</span> (poll(&amp;pfd, <span class="hljs-number">1</span>, <span class="hljs-number">5000</span>)) {
      <span class="hljs-keyword">if</span> (pfd.revents &amp; POLLIN) {
          ... 
      }
  }
</code></pre>
</li>
<li>看上去显然使用 <code>poll()</code> 编程要简单一些；</li>
<li><code>select()</code> 在标识文件描述符时使用的位掩码，<code>bit 0</code> 表示描述符为 0，<code>bit 1</code> 表示描述符为 1，以此类推，当表示某个描述符的 bit 为 1 时，表示这个描述符需要被监视，如果我们仅需要监视数值为 500 的文件描述符，此时，<code>bit 0~499</code> 为 0，<code>bit 500</code> 为 1；</li>
<li>所以，在使用 <code>select()</code> 时，即便我们只需要监视描述符为 500 的 socket，实际上，仍要检查描述符 <code>0~499</code> 的位掩码，当所要监视的描述符值比较大时，运行效率肯定会受到影响；</li>
<li>在使用 <code>select()</code> 时，使用三个描述符集来监视读、写和意外事件，当一个描述符既需要监视读又需要监视写时，是需要在两个描述符集中设置相应的描述符的；</li>
<li>另外，使用 <code>select()</code> 监视描述符时，对描述符的最大值是有限制的，在 Linux 下允许的描述符的最大值为 1024，这一点显然也是 <code>select()</code> 的麻烦之处，当要监视的描述符的值大于 1024 时，将无法使用 <code>select()</code>；</li>
<li>使用 <code>poll()</code> 则不需要设置描述符集，而是需要为每一个需要监视的描述符建立一个结构：<code>struct pollfd</code>，这个结构中的 fd 字段指定要监视的描述符，events 字段可以设置多个要监视的事件，如果对一个描述符既要监视其"读"事件，也要监视其"写"事件，只需在这个 events 字段上设置两个事件即可，像下面这段代码<pre><code class="lang-C">  ......
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">pollfd</span> *<span class="hljs-title">fds</span>;</span>
  ...
  fds[i].fd = fd;
  fds[i].events = POLLIN | POLLOUT;
  ......
</code></pre>
</li>
<li>由此可见，使用 <code>poll()</code> 监视描述符不会有最大值不能超过 1024 的限制，在监视效率上也会比 <code>select()</code> 要高一些；</li>
<li><p>在 <code>select()</code> 编程中，通常每次调用 <code>select()</code> 之前需要重新设置描述符集，像下面代码，这段代码中 client_socket 数组中存放着连接到服务器的客户端的 socket，listen_socket 为服务器上正在侦听的 socket：</p>
<pre><code class="lang-C">  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {
      FD_ZERO(&amp;readfds);
      FD_SET(listening_socket, &amp;readfds);
      max_fd = listening_socket;

      <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span> ; i &lt; MAX_CLIENTS; i++) {
          <span class="hljs-keyword">if</span> (client_socket[i] &gt; <span class="hljs-number">0</span>) {
              FD_SET(client_socket[i], &amp;readfds);
          }
          <span class="hljs-keyword">if</span> (client_socket[i] &gt; max_fd) max_fd = client_socket[i];
      }
      rc = select(max_fd + <span class="hljs-number">1</span>, &amp;readfds, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>);
      ......
  }
</code></pre>
</li>
<li>但其实 <code>poll()</code> 编程中也有类似的困扰，<code>poll()</code> 使用一个结构数组 <code>struct pollfd *</code> 来标识需要监视的描述符及其事件，但当一个客户连接中止时，尽管可以将结构数组中的 fd 字段置为 0，但 <code>poll()</code> 并不会自动地跳过 fd 字段为 0 的数组项，所以我们在启动 <code>poll()</code> 之前仍然有必要重新整理整个结构数组，以保证其中没有已经不再使用的描述符，像下面代码段，fds_size 为结构数组 fds 中的有效元素数，从中删除所有的 fd 字段为 0 的元素：<pre><code class="lang-C">  ......
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">pollfd</span> *<span class="hljs-title">fds</span>;</span>
  ...
  <span class="hljs-keyword">while</span> ((fds_size &gt; <span class="hljs-number">0</span>) &amp;&amp; (fds[fds_size - <span class="hljs-number">1</span>].fd == <span class="hljs-number">0</span>)) fds_size--;
  <span class="hljs-keyword">if</span> (fds_size &gt; <span class="hljs-number">1</span>) {
      <span class="hljs-keyword">int</span> i = fds_size - <span class="hljs-number">1</span>;
      <span class="hljs-keyword">while</span> (i &gt; <span class="hljs-number">0</span>) {
          <span class="hljs-keyword">if</span> (fds[i - <span class="hljs-number">1</span>].fd == <span class="hljs-number">0</span>) {
              fds[i - <span class="hljs-number">1</span>].fd = fds[fds_size - <span class="hljs-number">1</span>].fd;
              fds[i - <span class="hljs-number">1</span>].events = fds[fds_size - <span class="hljs-number">1</span>].events;
              fds[i - <span class="hljs-number">1</span>].revents = <span class="hljs-number">0</span>;
              fds[fds_size - <span class="hljs-number">1</span>].fd = <span class="hljs-number">0</span>;
              <span class="hljs-keyword">while</span> ((fds_size &gt; <span class="hljs-number">0</span>) &amp;&amp; (fds[fds_size - <span class="hljs-number">1</span>].fd == <span class="hljs-number">0</span>)) fds_size--;
              <span class="hljs-keyword">if</span> (fds_size &lt; <span class="hljs-number">2</span>) <span class="hljs-keyword">break</span>;
          }
          i--;
      }
  }
  ......
</code></pre>
</li>
<li>在 <code>poll()</code> 编程中，当有事件产生时，需要遍历整个结构数组，检查每个数组结构中的 revents 字段，找到需要处理的描述符及其事件，这一点和 <code>select()</code> 编程类似，<code>select()</code> 返回后，需要检查所有被监控的描述符，以找到哪个描述符需要处理；</li>
</ul>
<h2 id="heading-4-poll">4 poll() 编程的基本步骤</h2>
<ol>
<li>使用 <code>socket()</code> 建立需要侦听的 socket；</li>
<li>使用 <code>setsockopt()</code> 设置 socket 为可重复使用；</li>
<li>使用 <code>ioctl()</code> 设置 socket 为非阻塞；</li>
<li>使用 <code>bind()</code> 绑定服务器的地址和端口；</li>
<li>使用 <code>listen()</code> 开始侦听端口；</li>
<li>以上步骤和使用 <code>select()</code> 编程时一致的；</li>
<li>初始化结构数组 <code>struct pollfd *</code>，将服务器侦听 socket 加入到数组中；</li>
<li><p>启动 <code>poll()</code>；</p>
<ul>
<li>返回 0 表示调用超时，可以重新启动 <code>poll()</code>；</li>
<li>返回 <code>&lt;0</code> 表示 <code>poll()</code> 出错，errno 中为错误代码；</li>
<li><p>返回 <code>&gt;0</code> 表示有需要处理的 socket，进行处理；</p>
<blockquote>
<p>要处理的 socket 通常又分为两种，一种是正在侦听的 socket，如果有 <code>POLLIN</code> 事件表示有客户端发出了连接请求，使用 <code>accept()</code> 接受连接将产生一个新的 socket，这个新的 socket 要加入到结构数组 <code>struct pollfd *</code> 中，以便在 <code>poll()</code> 中可以被监视；另一类 socket 就是已经和服务器建立连接的一个或多个客户端的 socket，这类 socket 有 POLLIN 事件产生可能是有数据发送回来，也可能是因为连接中断，在调用 <code>recv()</code> 从 socket 中接收数据时，如果返回值 <code>&gt;0</code> 表示确实有数据发送回来，要做出相应处理，如果返回值为 0 则表示这个连接已经中断，此时应该及时将该 socket 从结构数组 <code>struct pollfd *</code> 中清除，避免在调用 <code>poll()</code> 时给 <code>poll()</code> 增加额外负担；</p>
</blockquote>
</li>
</ul>
</li>
<li><p>整理结构数组 <code>struct pollfd *</code>，然后再次启动 <code>poll()</code>；</p>
<blockquote>
<p>在处理 poll() 函数的返回结果时，当有新的客户端连接请求被接受时，会有新的 socket 需要添加到结构数组 <code>struct pollfd *</code> 中，当有客户端 socket 连接中断时，需要将这个已经失效的 socket 从结构数组 <code>struct pollfd *</code> 中删除，所以在处理完 poll() 的返回结果后需要对结构数组 <code>struct pollfd *</code> 进行整理，将新的 socket 添加进来，将失效的 socket 删除；不能简单地把结构数组 <code>struct pollfd *</code> 中的 fd 字段或者 events 字段置 0 来表示一个失效的 socket，poll() 并不会自动跳过这样标识的结构数组项；</p>
</blockquote>
</li>
</ol>
<h2 id="heading-5-poll-tcp">5 实例：一个使用 poll() 的 TCP 服务器</h2>
<ul>
<li><strong>源程序</strong>：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180021/poll-server.c">poll-server.c</a>(<strong>点击文件名下载源程序，建议使用UTF-8字符集</strong>)演示了使用 poll() 完成的一个 TCP 服务器；</li>
<li>编译：<code>gcc -Wall -g poll-server.c -o poll-server</code></li>
<li>运行：<code>./poll-server</code></li>
<li>该程序是一个多进程程序，程序会建立一个服务端进程和若干个(默认为 3 个，由宏 MAX_CONNECTIONS 控制)客户端进程；</li>
<li>服务端进程侦听在端口 8888 上，等待客户端进程的连接；</li>
<li>启动 <code>poll()</code> 监视 socket；</li>
<li>服务端在接受客户端请求后，将新连接的 socket 加入到结构数组中，并向客户端发送一条欢迎信息；</li>
<li>客户端在连接建立以后向服务端发送一条信息，服务端在收到客户端信息后会将该信息原封不动地发送回客户端；</li>
<li>客户端判断收到的信息与自己发出的信息一样后，主动关闭连接，然后退出进程；</li>
<li>服务端发现连接中断后，将从结构数组中删除该失效 socket，然后继续启动 <code>poll()</code> 监视 socket；</li>
<li>服务进程中拦截了 SIGINT 信号，这个信号可以使用 <code>ctrl + c</code> 产生，服务进程在收到这个信号后将退出进程；</li>
<li>主进程监视客户端进程的退出，当所有客户端进程都已退出后，向服务端进程发送 SIGINT 信号，使服务端进程退出，整个程序运行结束。</li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/180021/screenshot-of-poll-server.png" alt="Screenshot of poll-server" /></p>
</li>
</ul>
<h2 id="heading-6">6 结论</h2>
<ul>
<li><code>poll()</code> 和 <code>select()</code> 编程有很多相似的地方，除了 <code>select()</code> 使用描述符集而 <code>poll()</code> 使用结构数组外，其它方面非常相似，对熟悉 <code>select()</code> 编程的程序员而言，使用 <code>poll()</code> 替换 <code>select()</code> 编写服务端程序并不困难；</li>
<li>在调用 <code>select()</code> 之前，需要初始化描述符集，在调用 <code>poll()</code> 之前，需要初始化结构数组；</li>
<li>在 <code>select()</code> 返回后，需要使用 <code>FD_ISSET()</code> 遍历描述符集以判断哪个描述符有事件产生，在 <code>poll()</code> 返回后，需要遍历结构数组中的 revents 字段，以判断哪个描述符有事件产生；</li>
<li>在接受连接请求、接收、发送数据，判断错误以及连接是否中断方面，使用 <code>select()</code> 和 <code>poll()</code> 是一样的；</li>
<li>在处理完 select() 的返回后，需要重新初始化描述符集才可以再次调用 <code>select()</code>，在处理完 <code>poll()</code> 的返回后，需要重新整理结构数组后才可以再次调用 <code>poll()</code>。</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>


]]></content:encoded></item><item><title><![CDATA[使用signal中止阻塞的socket函数的应用实例]]></title><description><![CDATA[在 socket 编程中，有一些函数是阻塞的，为了使程序高效运行，有一些办法可以把这些阻塞函数变成非阻塞的，本文介绍一种使用定时器信号中断阻塞函数的方法，同时介绍了一些信号处理和定时器设置的编程方法，本文附有完整实例的源代码，本文实例在 Ubuntu 20.04 上编译测试通过，gcc版本号为：9.4.0；本文不适合 Linux 编程的初学者阅读。

1 前言

在 socket 编程中，阻塞还是不阻塞是经常要考虑的问题，accept()、recv() 等一些函数都是阻塞函数，阻塞函数有时会给程...]]></description><link>https://whowin.cn/180023-using-signals-with-blocking-socket-apis</link><guid isPermaLink="true">https://whowin.cn/180023-using-signals-with-blocking-socket-apis</guid><category><![CDATA[accept]]></category><category><![CDATA[Linux]]></category><category><![CDATA[网络编程]]></category><category><![CDATA[signals]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Wed, 24 Jan 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729258732167/103b7ae2-9c9a-4e4f-89f3-c267a9daa2c7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>在 socket 编程中，有一些函数是阻塞的，为了使程序高效运行，有一些办法可以把这些阻塞函数变成非阻塞的，本文介绍一种使用定时器信号中断阻塞函数的方法，同时介绍了一些信号处理和定时器设置的编程方法，本文附有完整实例的源代码，本文实例在 Ubuntu 20.04 上编译测试通过，gcc版本号为：9.4.0；本文不适合 Linux 编程的初学者阅读。</p>
</blockquote>
<h2 id="heading-1">1 前言</h2>
<ul>
<li>在 socket 编程中，阻塞还是不阻塞是经常要考虑的问题，<code>accept()</code>、<code>recv()</code> 等一些函数都是阻塞函数，阻塞函数有时会给程序带来麻烦；</li>
<li>使用 <code>select()</code> 或者 <code>poll()</code> 监视 <code>socket</code> 描述符可以有效地避免诸如 <code>accept()</code>、<code>recv()</code> 等函数的阻塞带来的麻烦；</li>
<li>下面这段代码是使用 select() 避免阻塞的示例：<pre><code class="lang-C">  <span class="hljs-keyword">int</span> sockfd = socket(AF_INET, SOCK_STREAM , <span class="hljs-number">0</span>);
  ......
  fd_set fds;
  FD_ZERO(fd_set);
  FD_SET(sockfd, &amp;fds);
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">tv</span>;</span>
  tv.tv_sec = <span class="hljs-number">5</span>;
  tv.tv_usec = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">if</span> (select(sockfd + <span class="hljs-number">1</span>, &amp;fds, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>, &amp;tv)) {
      <span class="hljs-keyword">if</span> (FD_ISSET(sockfd, &amp;fds)) {
          ......
      }
  }
</code></pre>
</li>
<li>下面这段代码是使用 poll() 避免阻塞的示例：<pre><code class="lang-C">  <span class="hljs-keyword">int</span> sockfd = socket(AF_INET, SOCK_STREAM , <span class="hljs-number">0</span>);
  ......
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">pollfd</span> <span class="hljs-title">pfd</span>;</span>
  pfd.fd = sockfd;
  pfd.events = POLLIN;
  <span class="hljs-keyword">if</span> (poll(&amp;pfd, <span class="hljs-number">1</span>, <span class="hljs-number">5000</span>)) {
      <span class="hljs-keyword">if</span> (pfd.revents &amp; POLLIN) {
          ...... 
      }
  }
</code></pre>
</li>
<li>使用 <code>ioctl()</code> 将一个 socket 设置为非阻塞模式也是解决 socket 函数阻塞的方法之一；</li>
<li>下面代码使用 ioctl() 将 socket 设置为非阻塞模式：<pre><code class="lang-C">  <span class="hljs-keyword">int</span> sockfd = socket(AF_INET, SOCK_STREAM , <span class="hljs-number">0</span>);
  <span class="hljs-keyword">int</span> on = <span class="hljs-number">1</span>;
  ioctl(sockfd, FIONBIO, (<span class="hljs-keyword">char</span> *)&amp;on);
  ......
</code></pre>
</li>
<li>下面这段代码使用 fcntl() 将 socket 设置为非阻塞模式，与 ioctl() 是等效的：<pre><code class="lang-C">  <span class="hljs-keyword">int</span> sockfd = socket(AF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);
  <span class="hljs-keyword">int</span> flags = fcntl(sockfd, F_GETFL, <span class="hljs-number">0</span>);
  fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
  ......
</code></pre>
</li>
<li>如果将 <code>socket</code> 设置为非阻塞模式，<code>socket</code> 阻塞函数将立即返回，给出一个错误代码 <code>EAGAIN</code>，所以代码要写成下面这样：<pre><code class="lang-C">  <span class="hljs-keyword">int</span> sockfd = socket(AF_INET, SOCK_STREAM , <span class="hljs-number">0</span>);
  <span class="hljs-keyword">int</span> on = <span class="hljs-number">1</span>;
  ioctl(sockfd, FIONBIO, (<span class="hljs-keyword">char</span> *)&amp;on);
  ......
  <span class="hljs-keyword">int</span> rc = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">do</span> {
      rc = accept(sockfd, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>);
      usleep(<span class="hljs-number">100</span> * <span class="hljs-number">1000</span>);             <span class="hljs-comment">// sleep 100 ms</span>
  } <span class="hljs-keyword">while</span> (rc == EAGAIN || rc == EINTR);
  ......
</code></pre>
</li>
<li>本文讨论使用信号(signal)避免 socket 阻塞函数产生阻塞的方法。</li>
</ul>
<h2 id="heading-2-signal-socket">2 使用 signal 中止 socket 阻塞函数</h2>
<ul>
<li>实际上 socket 阻塞函数除了在非阻塞模式下会立即返回外，一旦当前进程收到信号(任何信号)时也会返回；<ul>
<li>在非阻塞模式下，socket 阻塞函数返回值为 -1 时，其 errno=EAGAIN;</li>
<li>因为收到信号而中止的 socket 阻塞函数返回值为 -1， errno=EINTR；</li>
</ul>
</li>
<li>基于此，可以设置一个定时器，Linux 的定时器会发出一个 SIGALRM 信号，该信号显然可以中止 socket 阻塞函数的阻塞状态；</li>
<li>设置定时器通常有两种方法，一种是使用 <code>alarm()</code>，另一种是使用 <code>setitimer()</code>；</li>
<li>下面代码使用 setitimer() 设置一个 5 秒的定时器：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">itimerval</span> <span class="hljs-title">new_value</span>;</span>
  new_value.it_value.tv_sec = <span class="hljs-number">5</span>;
  new_value.it_value.tv_usec = <span class="hljs-number">0</span>;
  new_value.it_interval.tv_sec = <span class="hljs-number">5</span>;
  new_value.it_interval.tv_usec = <span class="hljs-number">0</span>;
  setitimer(ITIMER_REAL, &amp;new_value, <span class="hljs-literal">NULL</span>);
</code></pre>
</li>
<li>有关 <code>setitimer()</code> 的详细信息，可以查看在线手册 <code>man setitimer</code>，这里仅做简单介绍；</li>
<li><p><code>setitimer()</code> 的定义：</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/time.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">setitimer</span><span class="hljs-params">(<span class="hljs-keyword">int</span> which, <span class="hljs-keyword">const</span> struct itimerval *new_value, struct itimerval *old_value)</span></span>;
</code></pre>
</li>
<li><p>其中 <code>struct itimeval</code> 的定义如下：</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">itimerval</span> {</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">it_interval</span>;</span> <span class="hljs-comment">/* Interval for periodic timer */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">it_value</span>;</span>    <span class="hljs-comment">/* Time until next expiration */</span>
  };

  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> {</span>
      <span class="hljs-keyword">time_t</span>      tv_sec;         <span class="hljs-comment">/* seconds */</span>
      <span class="hljs-keyword">suseconds_t</span> tv_usec;        <span class="hljs-comment">/* microseconds */</span>
  };
</code></pre>
</li>
<li>调用 <code>setitimer()</code> 时，参数 which 表示计时方式，有三个可选值：<ul>
<li><strong>ITIMER_REAL</strong>：以实际时钟计时，计时器时间到产生 SIGALRM 信号；</li>
<li><strong>ITIMER_VIRTUAL</strong>：以进程消耗的用户模式下 CPU 时间计时，计时器时间到产生一个 SIGVTALRM 信号；</li>
<li><strong>ITIMER_PROF</strong>：以进程消耗的总 CPU 时间计时，计时器到时时产生一个 SIGPROF 信号；</li>
</ul>
</li>
<li>调用 <code>setitimer()</code> 时，参数 <code>new_value</code> 用于设置定时器时间：<ul>
<li><code>new_value.it_value</code> 中有两个字段，如果两个字段均为 0，表示取消定时器，如果两个字段中有一个不为 0，则认为是设置了一个时间间隔；</li>
<li><code>new_value.it_interval</code> 用于指定计时器的新间隔，当 <code>new_value.it_interval</code> 中的两个字段均为 0 时，表示这个计时器是单次的，其中有一个字段不为 0，则将被作为一个新的时间间隔在下次被指定；</li>
</ul>
</li>
<li>调用 <code>setitimer()</code> 时，参数 <code>old_value</code> 用于返回之前的设置值(实际就是 <code>getitime()</code> 返回的值)，可以设置为 NULL；</li>
<li><p>函数 <code>setitimer()</code> 调用成功时返回 0，失败时返回 -1，<code>errno</code> 中为错误代码；</p>
</li>
<li><p><code>alarm()</code> 的使用比较简单，定义如下：</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;unistd.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span> <span class="hljs-title">alarm</span><span class="hljs-params">(<span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span> seconds)</span></span>;
</code></pre>
</li>
<li><code>alarm()</code> 设置的时间到时，将产生一个 SIGALRM 信号，<code>alarm()</code> 是一个单次的定时器，所以使用 <code>alarm()</code> 设置的定时器只会响应一次，如果需要重复定时，可以在 SIGALRM 信号处理程序中再次执行 <code>alarm()</code> 重新设置定时；</li>
<li><code>alarm()</code> 和 <code>setitimer()</code> 使用的是同一个定时器，所以，这两个函数相互间会互相影响，建议在同一个进程中，应避免使用两种方法设置定时器；</li>
<li><code>alarm()</code> 在设置定时器时只能设置到秒的精度，而且只能使用实际时钟，相比较而言，<code>setitimer()</code> 可以设置精度更高的定时器，而且计时方式也比较多样，但复杂度略高；</li>
<li>不管是 <code>alarm()</code> 还是 <code>setitimer()</code>，在计时时间到时都是发出一个信号，所以编写信号处理程序是使用定时器时必须要做的工作，需要使用 <code>signal()</code> 设置信号处理程序；</li>
<li><p>下面程序设置了 SIGALRM 信号的信号处理程序：</p>
<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">signal_handler</span><span class="hljs-params">(<span class="hljs-keyword">int</span> sig)</span> </span>{
      signal(sig, signal_handler);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Catch the signal: %d\n"</span>,sig);
      ......
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      ......
      signal(SIGALRM, signal_handler);
      ......
  }
</code></pre>
</li>
<li>signal() 函数设置的信号处理程序在信号产生后会被重置为默认处理程序，如果需要下次产生信号时继续使用当前处理程序，需要在信号处理程序中执行 signal() 重新设置，就像上面程序演示的那样；</li>
<li><p>下面这段程序使用 alarm() 设置了一个 5 秒的定时器，每 5 秒会产生一个 SIGALRM 信号：</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> _POSIX_SOURCE</span>
  ......
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;unistd.h&gt;</span></span>
  ......
  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">signal_handler</span><span class="hljs-params">(<span class="hljs-keyword">int</span> sig)</span> </span>{
      signal(sig, signal_handler);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Catch the signal: %d\n"</span>,sig);
      ......
      alarm(<span class="hljs-number">5</span>);
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      ......
      signal(SIGALRM, signal_handler);
      alarm(<span class="hljs-number">5</span>);

      <span class="hljs-keyword">while</span> (loop) {
          ......
      }
      ......
  }
</code></pre>
</li>
<li><p>下面这段代码使用 setitimer() 设置了一个 5 秒的定时器，每 5 秒会产生一个 SIGALRM 信号：</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> _POSIX_SOURCE</span>
  ......
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/time.h&gt;</span></span>
  ......
  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">signal_handler</span><span class="hljs-params">(<span class="hljs-keyword">int</span> sig)</span> </span>{
      signal(sig, signal_handler);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Catch the signal: %d\n"</span>,sig);
      ......
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      ......
      signal(SIGALRM, signal_handler);

      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">itimerval</span> <span class="hljs-title">new_value</span>;</span>
      new_value.it_value.tv_sec = <span class="hljs-number">5</span>;
      new_value.it_value.tv_usec = <span class="hljs-number">0</span>;
      new_value.it_interval.tv_sec = <span class="hljs-number">5</span>;
      new_value.it_interval.tv_usec = <span class="hljs-number">0</span>;
      setitimer(ITIMER_REAL, &amp;new_value, <span class="hljs-literal">NULL</span>);

      <span class="hljs-keyword">while</span> (loop) {
          ......
      }
      ......
  }
</code></pre>
</li>
<li>除了编程上的区别外，还要注意 <code>alarm()</code> 需要的头文件是 <code>&lt;unistd.h&gt;</code>，而 <code>setitimer()</code> 需要的头文件是 <code>&lt;sys/time.h&gt;</code>；</li>
<li>关于系统调用中的阻塞函数在进程收到信号后会被中止的相关信息可以参考在线手册 <code>man 7 signal</code>，其中 <code>&lt;Interruption of system calls and library functions by signal handlers&gt;</code> 一节中详细介绍了那些阻塞函数可以被信号中止；</li>
<li>另外，阻塞函数被信号中止的功能是 POSIX 标准中的一部分，并不是 libc 默认支持的，所以在程序的开头要加上 <code>#include _POSIX_SOURCE</code>。</li>
</ul>
<h2 id="heading-3">3 范例</h2>
<ul>
<li><strong>源程序</strong>：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180023/nonblock-signal.c">nonblock-signal.c</a>(<strong>点击文件名下载源程序，建议使用UTF-8字符集</strong>)演示了使用信号使 socket 编程里的阻塞函数 <code>accept()</code> 每隔 5 秒钟中止一次的过程；</li>
<li>该范例不仅仅是处理了 SIGALRM 信号，还处理了 SIGINT 和 SIGQUIT 信号，旨在说明不仅仅是定时器产生的 SIGALRM 信号会中止阻塞函数，任何信号都会使阻塞函数中止；</li>
<li>SIGQUIT 信号可以使用键盘 <code>ctrl + \</code> 产生，SIGINT 信号就是 <code>ctrl + c</code>；</li>
<li>为了程序可以正常退出，程序对 SIGINT 信号做了计数，当按下 <code>ctrl + c</code> 四次时，程序会正常退出；</li>
<li>因为一个 socket 阻塞函数可以被任意信号打断，被打断的函数会返回一个 EINTR 错误，所以在进行 socket 编程时，一定要处理 EINTR；</li>
<li>程序使用 常量 <code>_ALARM_FUNC</code> 控制采用哪种方式设置定时器，当常量 <code>_ALARM_FUNC</code> 已定义时，使用 <code>alarm()</code> 设置定时器，否则使用 <code>setitimer()</code> 设置定时器；</li>
<li>编译：<code>gcc -Wall -g nonblock-signal.c -o nonblock-signal</code></li>
<li>运行：<code>./nonblock-signal</code></li>
<li><p>运行截图：</p>
<p>  <img src="https://blog.whowin.net/images/180023/nonblock-signal.gif" alt="GIF of running nonblock-signal" /></p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[在ubuntu上的18个非常实用的命令行工具软件]]></title><description><![CDATA[使用Ubuntu的过程中，在终端上使用命令行工具是非常常见的事情，熟练地掌握命令行工具是使用ubuntu必不可少的技能，即便是Ubuntu的初学者，通常也很熟悉诸如ls、rm、cp等一些文件操作工具，当浏览/bin目录时，你会发现Ubuntu还有许多工具软件，本文将向读者简单介绍18个在Ubuntu上使用的命令行工具软件，本文不会详细介绍每个命令的用法，有对某个命令感兴趣的读者可以自行查找更详细的资料或者使用man在线手册，本文非常适合初学者阅读。


1 find - 在文件系统中查找所需的文...]]></description><link>https://whowin.cn/100010-19-useful-tools-in-ubuntu</link><guid isPermaLink="true">https://whowin.cn/100010-19-useful-tools-in-ubuntu</guid><category><![CDATA[Linux]]></category><category><![CDATA[Ubuntu]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Wed, 10 Jan 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729301512423/c4a30ea9-fa36-47c9-8123-bf1559843d81.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>使用Ubuntu的过程中，在终端上使用命令行工具是非常常见的事情，熟练地掌握命令行工具是使用ubuntu必不可少的技能，即便是Ubuntu的初学者，通常也很熟悉诸如<code>ls</code>、<code>rm</code>、<code>cp</code>等一些文件操作工具，当浏览<code>/bin</code>目录时，你会发现Ubuntu还有许多工具软件，本文将向读者简单介绍18个在Ubuntu上使用的命令行工具软件，本文不会详细介绍每个命令的用法，有对某个命令感兴趣的读者可以自行查找更详细的资料或者使用man在线手册，本文非常适合初学者阅读。</p>
</blockquote>
<hr />
<h2 id="heading-1-find">1 <code>find</code> - 在文件系统中查找所需的文件</h2>
<ul>
<li>在当前目录及其子目录下查找 C 语言的源程序：<pre><code>  $ find . -name *.c
</code></pre></li>
<li>在 <code>/var</code> 目录及其子目录下查找文件大小大于 <code>100kb</code> 的日志文件：<pre><code>  $ sudo find /<span class="hljs-keyword">var</span> -name <span class="hljs-string">"*.log"</span> -size +<span class="hljs-number">100</span>k
</code></pre></li>
<li>在当前目录及其子目录下查找大于 2M 的临时文件，如果其所属用户不是 <code>developer</code>，则将其删除：<pre><code>  $ find . -name <span class="hljs-string">"*tmp"</span> -size +<span class="hljs-number">2</span>M ! -user developer -exec rm {} \;
</code></pre></li>
<li>要求在删除文件前确认：<pre><code>  $ find . -name <span class="hljs-string">"*tmp"</span> -size +<span class="hljs-number">2</span>M ! -user developer -ok rm {} \;
</code></pre></li>
</ul>
<hr />
<h2 id="heading-2-grep">2 <code>grep</code> - 在文件中查找字符串</h2>
<ul>
<li>在当前目录下的文本文件中，查找含有 "hello" 字符串的文件：<pre><code>  $ grep <span class="hljs-string">"hello"</span> *.txt
</code></pre></li>
<li>在当前目录及其子目录下的所有文件中，查找含有 "hello" 字符串的文件：<pre><code>  $ grep -r <span class="hljs-string">"hello"</span> ./
</code></pre></li>
<li>在当前目录及其子目录下的 C 语言源代码文件中，查找含有 "hello" 字符串的文件：<pre><code>  $ grep -r <span class="hljs-string">"hello"</span> ./ --include=<span class="hljs-string">"*.c"</span>
</code></pre></li>
<li>使用正则表达式查找文件中的字符串：<pre><code>  $ grep -r <span class="hljs-string">"uid-[0-9]*"</span> ./ --include=<span class="hljs-string">"*.c"</span>
</code></pre></li>
</ul>
<hr />
<h2 id="heading-3-cut">3 <code>cut</code> - 打印文件中指定的字段(列)</h2>
<ul>
<li>打印文件 <code>/etc/fstab</code> 文件中每行的前 12 个字符<pre><code>  $ cut -c1<span class="hljs-number">-12</span> /etc/fstab
</code></pre></li>
<li>打印文件 <code>/etc/passwd</code> 中第 1、6、7 字段的内容，字段分隔符为 ":"<pre><code>  $ cut -d: -f1,<span class="hljs-number">6</span>,<span class="hljs-number">7</span> /etc/passwd
</code></pre></li>
</ul>
<hr />
<h2 id="heading-4-bc">4 <code>bc</code> - 命令行上的计算器</h2>
<ul>
<li>这是一个命令行下的交互式计算器，可以使用变量，还可以使用数学函数，<code>quit</code> 命令退出交互；<pre><code>  $ bc -l
  bc <span class="hljs-number">1.07</span><span class="hljs-number">.1</span>
  Copyright <span class="hljs-number">1991</span><span class="hljs-number">-1994</span>, <span class="hljs-number">1997</span>, <span class="hljs-number">1998</span>, <span class="hljs-number">2000</span>, <span class="hljs-number">2004</span>, <span class="hljs-number">2006</span>, <span class="hljs-number">2008</span>, <span class="hljs-number">2012</span><span class="hljs-number">-2017</span> Free Software Foundation, Inc.
  This is free software <span class="hljs-keyword">with</span> ABSOLUTELY NO WARRANTY.
  For details type <span class="hljs-string">`warranty'. 
  102+240*3.5
  942.0
  a=102
  b=240
  c=3.5
  a+b*c
  942.0
  b/c
  68.57142857142857142857
  sqrt(a)
  10.09950493836207795336
  quit
  $</span>
</code></pre></li>
<li>也可以在脚本中使用：<pre><code>  $ x=<span class="hljs-number">100</span>
  $ y=<span class="hljs-number">20</span>
  $ echo <span class="hljs-string">"$x/$y"</span> | bc -l
  <span class="hljs-number">5.00000000000000000000</span>
  $ echo <span class="hljs-string">"$x/$y"</span> | bc
  <span class="hljs-number">5</span>
  $
</code></pre></li>
</ul>
<hr />
<h2 id="heading-5-comm">5 <code>comm</code> - 比较两个已排序的文件</h2>
<ul>
<li>为了演示这个命令，先制作两个简单的文本文件：<pre><code>  $ echo -e <span class="hljs-string">"avi\ndani\nrina\nzina"</span>&gt;student1
  $ echo -e <span class="hljs-string">"avi\ndina\nmeni\nzina"</span>&gt;student2
</code></pre></li>
<li>用 comm 比较这两个文件：<pre><code>  $ cat student1
  avi
  dani
  rina
  zina
  $ cat student2
  avi
  dina
  meni
  zina
  $ comm student1 student2
                  avi
  dani
          dina
          meni
  rina
                  zina
  $
</code></pre></li>
<li>第 1 列是第 1 个文件的内容，第 2 列是第 2 个文件内容，第 3 列是两个文件中相同的内容；</li>
</ul>
<hr />
<h2 id="heading-6-diff">6 <code>diff</code> - 逐行比较两个文件</h2>
<ul>
<li>使用上面使用过的文件 student1 和 student2 进行比较：<pre><code>  $ diff student1 student2
  <span class="hljs-number">2</span>,<span class="hljs-number">3</span>c2,<span class="hljs-number">3</span>
  &lt; dani
  &lt; rina
  ---
  &gt; dina
  &gt; meni
  $
</code></pre></li>
<li>其中 <code>2,3c2,3</code> 的含义是：将第 1 个文件中的第 2、3 行改为第 2 个文件的第 2、3 行，两个文件就一样了；</li>
</ul>
<hr />
<h2 id="heading-7-df">7 <code>df</code> - 文件系统的磁盘空间使用情况</h2>
<pre><code>    $ df
    Filesystem     <span class="hljs-number">1</span>K-blocks    Used Available Use% Mounted on
    udev              <span class="hljs-number">211908</span>       <span class="hljs-number">0</span>    <span class="hljs-number">211908</span>   <span class="hljs-number">0</span>% /dev
    tmpfs              <span class="hljs-number">48496</span>     <span class="hljs-number">712</span>     <span class="hljs-number">47784</span>   <span class="hljs-number">2</span>% /run
    /dev/vda1        <span class="hljs-number">6105676</span> <span class="hljs-number">4262704</span>   <span class="hljs-number">1522648</span>  <span class="hljs-number">74</span>% /
    tmpfs             <span class="hljs-number">242468</span>       <span class="hljs-number">0</span>    <span class="hljs-number">242468</span>   <span class="hljs-number">0</span>% <span class="hljs-regexp">/dev/</span>shm
    tmpfs               <span class="hljs-number">5120</span>       <span class="hljs-number">0</span>      <span class="hljs-number">5120</span>   <span class="hljs-number">0</span>% <span class="hljs-regexp">/run/</span>lock
    tmpfs             <span class="hljs-number">242468</span>       <span class="hljs-number">0</span>    <span class="hljs-number">242468</span>   <span class="hljs-number">0</span>% <span class="hljs-regexp">/sys/</span>fs/cgroup
    tmpfs              <span class="hljs-number">48492</span>       <span class="hljs-number">0</span>     <span class="hljs-number">48492</span>   <span class="hljs-number">0</span>% <span class="hljs-regexp">/run/u</span>ser/<span class="hljs-number">1000</span>
    $
</code></pre><hr />
<h2 id="heading-8-du">8 <code>du</code> - 估算文件系统的磁盘使用情况</h2>
<ul>
<li>使用这个命令通常都要加上参数 <code>-h</code>，这样显示的结果才比较清楚：<pre><code>  $ du -h
  <span class="hljs-number">4.0</span>K    ./.config/procps
  <span class="hljs-number">8.0</span>K    ./.config
  <span class="hljs-number">28</span>K     ./.mitmproxy
  <span class="hljs-number">4.0</span>K    ./.cache
  <span class="hljs-number">22</span>M     ./frp
  <span class="hljs-number">51</span>M     .
  $
</code></pre></li>
<li>可以不显示子目录，仅显示汇总结果：<pre><code>  $ du -h -s
  <span class="hljs-number">51</span>M     .
  $
</code></pre></li>
<li>还可以指定子目录的深度，比如：<pre><code>  $ du -h -d <span class="hljs-number">1</span>
  <span class="hljs-number">8.0</span>K    ./.config
  <span class="hljs-number">28</span>K        ./.mitmproxy
  <span class="hljs-number">4.0</span>K    ./.cache
  <span class="hljs-number">22</span>M     ./frp
  <span class="hljs-number">51</span>M     .
  $
</code></pre></li>
<li>与前面的 <code>du -h</code> 相比，这次没有显示目录 <code>./.config/procps</code>，这是因为 <code>-d 1</code> 指定了目录深度只有 1 层。</li>
</ul>
<hr />
<h2 id="heading-9-file">9 <code>file</code> - 查看文件类型</h2>
<pre><code>$ file mytest.c
mytest.c: C source, ASCII text, <span class="hljs-keyword">with</span> CRLF line terminators
$
</code></pre><pre><code>$ file /bin/cp
/bin/cp: ELF <span class="hljs-number">64</span>-bit LSB shared object, x86<span class="hljs-number">-64</span>, version <span class="hljs-number">1</span> (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86<span class="hljs-number">-64.</span>so<span class="hljs-number">.2</span>, BuildID[sha1]=<span class="hljs-number">421e1</span>abd8faf1cb290df755a558377c5d7def3b1, <span class="hljs-keyword">for</span> GNU/Linux <span class="hljs-number">3.2</span><span class="hljs-number">.0</span>, stripped
$
</code></pre><pre><code>$ file /lib64/ld-linux-x86<span class="hljs-number">-64.</span>so<span class="hljs-number">.2</span>
/lib64/ld-linux-x86<span class="hljs-number">-64.</span>so<span class="hljs-number">.2</span>: symbolic link to /lib/x86_64-linux-gnu/ld<span class="hljs-number">-2.31</span>.so
$
</code></pre><hr />
<h2 id="heading-10-fold">10 <code>fold</code> - 打印文件内容时，在指定宽度位置添加"换行"</h2>
<ul>
<li>为演示 <code>fold</code> 命令，先制作一个文件：<pre><code>  $ echo <span class="hljs-string">"id-2093384 id-8984773 id-8725536 id-9828835 id-6455351 id-9873773 "</span>&gt;data1
  $
</code></pre></li>
<li>执行下面命令看一下打印效果：<pre><code>  $ cat data1
  id<span class="hljs-number">-2093384</span> id<span class="hljs-number">-8984773</span> id<span class="hljs-number">-8725536</span> id<span class="hljs-number">-9828835</span> id<span class="hljs-number">-6455351</span> id<span class="hljs-number">-9873773</span> 
  $ fold -w <span class="hljs-number">11</span> data1
  id<span class="hljs-number">-2093384</span> 
  id<span class="hljs-number">-8984773</span> 
  id<span class="hljs-number">-8725536</span> 
  id<span class="hljs-number">-9828835</span> 
  id<span class="hljs-number">-6455351</span> 
  id<span class="hljs-number">-9873773</span> 
  $
</code></pre></li>
</ul>
<hr />
<h2 id="heading-11-headtail">11 <code>head/tail</code> - 显示文件的第一行/最后一行</h2>
<ul>
<li>显示 <code>/etc/passwd</code> 文件的最前面的 2 行：<pre><code>  $ head <span class="hljs-number">-2</span> /etc/passwd
  <span class="hljs-attr">root</span>:x:<span class="hljs-number">0</span>:<span class="hljs-number">0</span>:root:<span class="hljs-regexp">/root:/</span>bin/bash
  <span class="hljs-attr">daemon</span>:x:<span class="hljs-number">1</span>:<span class="hljs-number">1</span>:daemon:<span class="hljs-regexp">/usr/</span>sbin:<span class="hljs-regexp">/usr/</span>sbin/nologin
  $
</code></pre></li>
<li>显示 <code>/etc/passwd</code> 文件的最后 2 行：<pre><code>  $ tail <span class="hljs-number">-2</span> /etc/passwd
  <span class="hljs-attr">snap_daemon</span>:x:<span class="hljs-number">584788</span>:<span class="hljs-number">584788</span>::<span class="hljs-regexp">/nonexistent:/u</span>sr/bin/<span class="hljs-literal">false</span>
  <span class="hljs-attr">glances</span>:x:<span class="hljs-number">128</span>:<span class="hljs-number">135</span>::<span class="hljs-regexp">/var/</span>lib/glances:<span class="hljs-regexp">/usr/</span>sbin/nologin
  $
</code></pre></li>
<li>显示 <code>/etc/passwd</code> 文件的前 50 个字符：<pre><code>  $ head -c <span class="hljs-number">50</span> /etc/passwd
  <span class="hljs-attr">root</span>:x:<span class="hljs-number">0</span>:<span class="hljs-number">0</span>:root:<span class="hljs-regexp">/root:/</span>bin/bash
  <span class="hljs-attr">daemon</span>:x:<span class="hljs-number">1</span>:<span class="hljs-number">1</span>:daemo$
</code></pre></li>
</ul>
<hr />
<h2 id="heading-12-join">12 <code>join</code> - 将两个具有公共字段的文件合并在一起</h2>
<ul>
<li>为了演示 <code>join</code> 命令，先制作 2 个文件：<pre><code>  $ echo -e <span class="hljs-string">"avi haifa\ndani aco\nrina tel aviv\nzina ny"</span>&gt;s1
  $ echo -e <span class="hljs-string">"avi 1002\ndani 2000\nrina 3000\nzina 4255"</span>&gt;s2
  $
</code></pre></li>
<li>显示这两个文件<pre><code>  $ cat s1
  avi haifa
  dani aco
  rina tel aviv
  zina ny
  $ cat s2
  avi <span class="hljs-number">1002</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  zina <span class="hljs-number">4255</span>
  $
</code></pre></li>
<li>可以看到这两个文件的第一列是一样的，这就是这两个文件的公共字段，这样的文件就可以使用 <code>join</code> 合并：<pre><code>  $ join -j1 <span class="hljs-number">1</span> -j2 <span class="hljs-number">1</span> s1 s2
  avi haifa <span class="hljs-number">1002</span>
  dani aco <span class="hljs-number">2000</span>
  rina tel aviv <span class="hljs-number">3000</span>
  zina ny <span class="hljs-number">4255</span>
  $
</code></pre></li>
<li>其中 <code>-j1 1</code> 表示第一个文件的第一个字段，<code>-j2 1</code> 表示第二个文件的第一个字段。</li>
</ul>
<hr />
<h2 id="heading-13-od">13 <code>od</code> - 以各种不同的格式显示文件</h2>
<ul>
<li>先制作一个文件<pre><code>  $ echo -e <span class="hljs-string">"hello"</span>&gt;a1
</code></pre></li>
<li>正常显示文件<pre><code>  $ cat a1
  hello
  $
</code></pre></li>
<li>使用 <code>od</code> 按八进制和字符显示文件<pre><code>  $ od -bc a1
  <span class="hljs-number">0000000</span> <span class="hljs-number">150</span> <span class="hljs-number">145</span> <span class="hljs-number">154</span> <span class="hljs-number">154</span> <span class="hljs-number">157</span> <span class="hljs-number">012</span>
            h   e   l   l   o  \n
  <span class="hljs-number">0000006</span>
  $
</code></pre></li>
<li>按 16 进制字和字符显示文件<pre><code>  $ od -xc a1
  <span class="hljs-number">0000000</span>    <span class="hljs-number">6568</span>    <span class="hljs-number">6</span>c6c    <span class="hljs-number">0</span>a6f
            h   e   l   l   o  \n
  <span class="hljs-number">0000006</span>
  $
</code></pre></li>
<li>按 16 进制字节和字符显示文件<pre><code>  $ od -t x1c a1
  <span class="hljs-number">0000000</span>  <span class="hljs-number">68</span>  <span class="hljs-number">65</span>  <span class="hljs-number">6</span>c  <span class="hljs-number">6</span>c  <span class="hljs-number">6</span>f  <span class="hljs-number">0</span>a
            h   e   l   l   o  \n
  <span class="hljs-number">0000006</span>
  $
</code></pre></li>
</ul>
<hr />
<h2 id="heading-14-paste">14 <code>paste</code> - 合并文件行</h2>
<ul>
<li>制作一个新文件 s3<pre><code>  $ echo -e <span class="hljs-string">"20003 france\n09388 uk\n20019 italy\n98377 spain"</span>&gt;s3
</code></pre></li>
<li>使用 <code>paste</code> 命令将前面用过的文件 s2 和 s3 文件合并<pre><code>  $ cat s2
  avi <span class="hljs-number">1002</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  zina <span class="hljs-number">4255</span>
  $ cat s3
  <span class="hljs-number">20003</span> france
  <span class="hljs-number">09388</span> uk
  <span class="hljs-number">20019</span> italy
  <span class="hljs-number">98377</span> spain
  $ paste s2 s3
  avi <span class="hljs-number">1002</span>    <span class="hljs-number">20003</span> france
  dani <span class="hljs-number">2000</span>    <span class="hljs-number">09388</span> uk
  rina <span class="hljs-number">3000</span>    <span class="hljs-number">20019</span> italy
  zina <span class="hljs-number">4255</span>    <span class="hljs-number">98377</span> spain
  $
</code></pre></li>
</ul>
<hr />
<h2 id="heading-15-sort">15 <code>sort</code> - 对文件进行排序</h2>
<ul>
<li>使用 s3 文件演示 <code>sort</code> 的作用：<pre><code>  $ cat s3
  <span class="hljs-number">20003</span> france
  <span class="hljs-number">09388</span> uk
  <span class="hljs-number">20019</span> italy
  <span class="hljs-number">98377</span> spain
  $ sort s3
  <span class="hljs-number">09388</span> uk
  <span class="hljs-number">20003</span> france
  <span class="hljs-number">20019</span> italy
  <span class="hljs-number">98377</span> spain
  $
</code></pre></li>
</ul>
<hr />
<h2 id="heading-16-uniq">16 <code>uniq</code> - 从一个已排序的文件中删除重复的行</h2>
<ul>
<li>以前面用过的 s2 文件演示 <code>uniq</code> 命令<pre><code>  $ echo -e <span class="hljs-string">"dani 2000\nrina 3000"</span>&gt;&gt;s2
  $ cat s2
  avi <span class="hljs-number">1002</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  zina <span class="hljs-number">4255</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  $ sort s2&gt;s4
  $ cat s4
  avi <span class="hljs-number">1002</span>
  dani <span class="hljs-number">2000</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  rina <span class="hljs-number">3000</span>
  zina <span class="hljs-number">4255</span>
  $ uniq s4
  avi <span class="hljs-number">1002</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  zina <span class="hljs-number">4255</span>
  $
</code></pre></li>
</ul>
<hr />
<h2 id="heading-17-split">17 <code>split</code> - 将文件分割为多个部分</h2>
<ul>
<li>以 s2 文件为例，下面 <code>split</code> 命令的意思是：将 s2 文件按照 2 行一个文件分割为若干个文件，新文件名称以 <code>gen_</code> 开头<pre><code>  $ cat s2
  avi <span class="hljs-number">1002</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  zina <span class="hljs-number">4255</span>
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  $ split <span class="hljs-number">-2</span> s2 gen_
  $ cat gen_aa
  avi <span class="hljs-number">1002</span>
  dani <span class="hljs-number">2000</span>
  $ cat gen_ab
  rina <span class="hljs-number">3000</span>
  zina <span class="hljs-number">4255</span>
  $ cat gen_ac
  dani <span class="hljs-number">2000</span>
  rina <span class="hljs-number">3000</span>
  $
</code></pre></li>
<li>因为 s2 文件有 6 行，所以被分割成了 3 个文件，文件名分别是：<code>gen_aa</code>、<code>gen_ab</code> 和 <code>gen_ac</code></li>
</ul>
<hr />
<h2 id="heading-18-wc">18 <code>wc</code> - 打印文件中的字符数、单词数和行数</h2>
<ul>
<li>默认情况下，打印出文件的行数、单词数和字符数：<pre><code>  $ wc /etc/passwd
    <span class="hljs-number">51</span>   <span class="hljs-number">90</span> <span class="hljs-number">3070</span> /etc/passwd
</code></pre></li>
<li>其中：<code>51</code> 是文件行数，<code>90</code> 是文件中的单词数，<code>3070</code> 是该文件的字符数；</li>
<li>可以使用参数 <code>-l</code> 表示行数，<code>-w</code> 表示单词数，<code>-c</code> 表示字符数；<pre><code>  $ wc -l /etc/passwd
  <span class="hljs-number">51</span> /etc/passwd
  $ wc -c /etc/passwd
  <span class="hljs-number">3070</span> /etc/passwd
  $ wc -w /etc/passwd
  <span class="hljs-number">90</span> /etc/passwd
</code></pre></li>
<li>还可以这样用：<pre><code>  $ ps aux | wc -l
  <span class="hljs-number">381</span>
</code></pre></li>
<li>表示 <code>ps aux</code> 这个命令的输出有 381 行。</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<h2 id="heading-httpsblogcsdnnetwhowincategory12404164html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12404164.html">『进程间通信专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>
]]></content:encoded></item><item><title><![CDATA[在ubuntu上检查内存使用情况的九种方法]]></title><description><![CDATA[在 Ubuntu 中，可以通过 GUI(图形用户界面)和命令行使用多种方法来监视系统的内存使用情况，监视 Ubuntu 服务器上的内存使用情况并不复杂；了解已使用和可用的内存量对于故障排除和优化服务器性能至关重要，因为内存对系统 I/O 速度至关重要，定期监控内存使用情况有助于诊断潜在的系统问题和优化服务器性能，还可以帮助使用者确定是否需要扩充内存；本文将简要描述在 Ubuntu 上使用命令和工具监视内存使用情况的各种方法。


方法 1：使用 free 命令查看内存

free 命令显示系统中...]]></description><link>https://whowin.cn/100027-how-to-check-ram-usage-in-ubuntu</link><guid isPermaLink="true">https://whowin.cn/100027-how-to-check-ram-usage-in-ubuntu</guid><category><![CDATA[memory check]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Ubuntu]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Mon, 08 Jan 2024 16:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729312876916/bee3908c-85d2-46d6-8924-10e8ce0eb289.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>在 Ubuntu 中，可以通过 GUI(图形用户界面)和命令行使用多种方法来监视系统的内存使用情况，监视 Ubuntu 服务器上的内存使用情况并不复杂；了解已使用和可用的内存量对于故障排除和优化服务器性能至关重要，因为内存对系统 I/O 速度至关重要，定期监控内存使用情况有助于诊断潜在的系统问题和优化服务器性能，还可以帮助使用者确定是否需要扩充内存；本文将简要描述在 Ubuntu 上使用命令和工具监视内存使用情况的各种方法。</p>
</blockquote>
<hr />
<h2 id="heading-1-free">方法 1：使用 <code>free</code> 命令查看内存</h2>
<ul>
<li><code>free</code> 命令显示系统中可用和已使用的内存量，要使用此命令，请打开终端并键入以下命令：<pre><code>  free
</code></pre></li>
<li><p>输出将显示内存总量、已用内存、可用内存和共享内存，输出还将显示缓冲区和缓存内存；</p>
<pre><code>  whowin@vm448813:~$ free
                total        used        free      shared  buff/cache   available
  <span class="hljs-attr">Mem</span>:         <span class="hljs-number">484936</span>      <span class="hljs-number">108308</span>       <span class="hljs-number">10336</span>         <span class="hljs-number">248</span>      <span class="hljs-number">366292</span>      <span class="hljs-number">366360</span>
  <span class="hljs-attr">Swap</span>:        <span class="hljs-number">239696</span>       <span class="hljs-number">25588</span>      <span class="hljs-number">214108</span>
</code></pre></li>
</ul>
<hr />
<h2 id="heading-2-top">方法 2：使用 <code>top</code> 命令查看内存</h2>
<ul>
<li><code>top</code> 命令显示系统进程及其资源使用情况，包括内存使用情况，在终端上键入 <code>top</code> 即可启动该命令；<pre><code>  top
</code></pre></li>
<li><p>输出将显示系统上运行的进程列表，包括它们的 PID、用户、CPU 使用情况和内存使用情况，内存使用情况(MiB Mem)和交换内存使用情况(MiB Swap)的单位为 MiB 或者 KiB，1 MiB 为 1024<sup>2</sup> bytes，1 KiB 为 1024 bytes；</p>
<pre><code>  top - <span class="hljs-number">23</span>:<span class="hljs-number">07</span>:<span class="hljs-number">33</span> up <span class="hljs-number">144</span> days, <span class="hljs-number">11</span>:<span class="hljs-number">01</span>,  <span class="hljs-number">1</span> user,  load average: <span class="hljs-number">0.20</span>, <span class="hljs-number">0.07</span>, <span class="hljs-number">0.01</span>
  <span class="hljs-attr">Tasks</span>:  <span class="hljs-number">85</span> total,   <span class="hljs-number">1</span> running,  <span class="hljs-number">84</span> sleeping,   <span class="hljs-number">0</span> stopped,   <span class="hljs-number">0</span> zombie
  %Cpu(s):  <span class="hljs-number">4.3</span> us,  <span class="hljs-number">3.7</span> sy,  <span class="hljs-number">0.0</span> ni, <span class="hljs-number">87.7</span> id,  <span class="hljs-number">0.0</span> wa,  <span class="hljs-number">0.0</span> hi,  <span class="hljs-number">1.7</span> si,  <span class="hljs-number">2.7</span> st
  MiB Mem :    <span class="hljs-number">473.6</span> total,     <span class="hljs-number">32.7</span> free,     <span class="hljs-number">92.4</span> used,    <span class="hljs-number">348.5</span> buff/cache
  MiB Swap:    <span class="hljs-number">234.1</span> total,    <span class="hljs-number">209.1</span> free,     <span class="hljs-number">25.0</span> used.    <span class="hljs-number">370.6</span> avail Mem 

      PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
       <span class="hljs-number">10</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.7</span>   <span class="hljs-number">0.0</span> <span class="hljs-number">871</span>:<span class="hljs-number">36.89</span> rcu_sched
        <span class="hljs-number">1</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>  <span class="hljs-number">168804</span>   <span class="hljs-number">6408</span>   <span class="hljs-number">4172</span> S   <span class="hljs-number">0.3</span>   <span class="hljs-number">1.3</span>  <span class="hljs-number">27</span>:<span class="hljs-number">35.83</span> systemd
        <span class="hljs-number">9</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.3</span>   <span class="hljs-number">0.0</span> <span class="hljs-number">178</span>:<span class="hljs-number">24.79</span> ksoftirqd/<span class="hljs-number">0</span>
    <span class="hljs-number">15485</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>    <span class="hljs-number">6448</span>    <span class="hljs-number">996</span>    <span class="hljs-number">780</span> S   <span class="hljs-number">0.3</span>   <span class="hljs-number">0.2</span> <span class="hljs-number">242</span>:<span class="hljs-number">24.27</span> qemu-ga
    <span class="hljs-number">20887</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>   <span class="hljs-number">12184</span>   <span class="hljs-number">2264</span>   <span class="hljs-number">2092</span> S   <span class="hljs-number">0.3</span>   <span class="hljs-number">0.5</span>  <span class="hljs-number">32</span>:<span class="hljs-number">42.65</span> sshd
    <span class="hljs-number">21836</span> root      <span class="hljs-number">19</span>  <span class="hljs-number">-1</span>  <span class="hljs-number">195616</span> <span class="hljs-number">114896</span> <span class="hljs-number">114128</span> S   <span class="hljs-number">0.3</span>  <span class="hljs-number">23.7</span> <span class="hljs-number">109</span>:<span class="hljs-number">38.57</span> systemd-journal
  <span class="hljs-number">1877591</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.3</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">21.95</span> kworker/<span class="hljs-number">0</span>:<span class="hljs-number">1</span>-events
        <span class="hljs-number">2</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">09.50</span> kthreadd
        <span class="hljs-number">3</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> rcu_gp
        <span class="hljs-number">4</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> rcu_par_gp
        <span class="hljs-number">6</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> kworker/<span class="hljs-number">0</span>:<span class="hljs-number">0</span>H-kblockd
        <span class="hljs-number">8</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> mm_percpu_wq
       <span class="hljs-number">11</span> root      rt   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">1</span>:<span class="hljs-number">00.36</span> migration/<span class="hljs-number">0</span>
       <span class="hljs-number">12</span> root     <span class="hljs-number">-51</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> idle_inject/<span class="hljs-number">0</span>
       <span class="hljs-number">14</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> cpuhp/<span class="hljs-number">0</span>
       <span class="hljs-number">15</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> kdevtmpfs
       <span class="hljs-number">16</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> netns
       <span class="hljs-number">17</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> rcu_tasks_kthre
       <span class="hljs-number">18</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> kauditd
       <span class="hljs-number">19</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">37.21</span> khungtaskd
       <span class="hljs-number">20</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> oom_reaper
       <span class="hljs-number">21</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.12</span> writeback
       <span class="hljs-number">22</span> root      <span class="hljs-number">20</span>   <span class="hljs-number">0</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">01.04</span> kcompactd0
       <span class="hljs-number">23</span> root      <span class="hljs-number">25</span>   <span class="hljs-number">5</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> S   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> ksmd
       <span class="hljs-number">69</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> kintegrityd
       <span class="hljs-number">70</span> root       <span class="hljs-number">0</span> <span class="hljs-number">-20</span>       <span class="hljs-number">0</span>      <span class="hljs-number">0</span>      <span class="hljs-number">0</span> I   <span class="hljs-number">0.0</span>   <span class="hljs-number">0.0</span>   <span class="hljs-number">0</span>:<span class="hljs-number">00.00</span> kblockd
</code></pre></li>
</ul>
<hr />
<h2 id="heading-3-htop">方法 3：使用 <code>htop</code> 命令查看内存</h2>
<ul>
<li><code>htop</code> 命令是 <code>top</code> 命令的增强版本，它以更加人性化的方式显示系统进程及其资源使用情况；</li>
<li>使用如下命令安装 <code>htop</code>：<pre><code>  sudo apt install htop
</code></pre></li>
<li>安装好后在终端上键入 <code>htop</code> 即可启动；</li>
<li><p>输出将显示系统上运行的进程列表，包括它们的 PID、用户、CPU 使用情况和内存使用情况；</p>
<p>  <img src="https://blog.whowin.net/images/100027/screenshot-of-htop.png" alt="Screenshot of htop" /></p>
</li>
</ul>
<hr />
<h2 id="heading-4-vmstat">方法 4. 使用 <code>vmstat</code> 命令查看内存</h2>
<ul>
<li><code>vmstat</code> 是一个报告虚拟内存统计信息的工具，它提供有关进程、内存、分页、块 I/O、陷阱和 CPU 活动的信息；</li>
<li>在终端上键入 <code>vmstat</code> 即可使用；</li>
<li>查看 "swpd"(已使用的交换区)和 "free"(可用内存)列，了解有关内存使用情况的详细信息；<pre><code>  whowin@vm448813:~$ vmstat
  procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
   r  b   swpd   free   buff  cache   si   so    bi    bo   <span class="hljs-keyword">in</span>   cs us sy id wa st
   <span class="hljs-number">0</span>  <span class="hljs-number">0</span>  <span class="hljs-number">25844</span>  <span class="hljs-number">18916</span>  <span class="hljs-number">69112</span> <span class="hljs-number">302020</span>    <span class="hljs-number">0</span>    <span class="hljs-number">0</span>     <span class="hljs-number">2</span>    <span class="hljs-number">14</span>    <span class="hljs-number">1</span>    <span class="hljs-number">3</span>  <span class="hljs-number">0</span>  <span class="hljs-number">1</span> <span class="hljs-number">96</span>  <span class="hljs-number">0</span>  <span class="hljs-number">3</span>
</code></pre></li>
</ul>
<hr />
<h2 id="heading-5-procmeminfo">方法 5. 通过文件 <code>/proc/meminfo</code> 查看内存</h2>
<ul>
<li><code>/proc/meminfo</code> 文件包含有关系统内存使用情况的详细信息，使用下面命令查看其内容：<pre><code>  cat /proc/meminfo
</code></pre></li>
<li>该命令显示系统内存的详细分类，包括总内存、可用内存、已用内存和其他与内存相关的统计信息；<pre><code>  whowin@whowin-ThinkPad-T14s:~$ cat /proc/meminfo
  <span class="hljs-attr">MemTotal</span>:       <span class="hljs-number">15090452</span> kB
  <span class="hljs-attr">MemFree</span>:         <span class="hljs-number">7713808</span> kB
  <span class="hljs-attr">MemAvailable</span>:   <span class="hljs-number">11334744</span> kB
  <span class="hljs-attr">Buffers</span>:          <span class="hljs-number">155260</span> kB
  <span class="hljs-attr">Cached</span>:          <span class="hljs-number">3976592</span> kB
  <span class="hljs-attr">SwapCached</span>:            <span class="hljs-number">0</span> kB
  <span class="hljs-attr">Active</span>:          <span class="hljs-number">1509760</span> kB
  <span class="hljs-attr">Inactive</span>:        <span class="hljs-number">5128712</span> kB
  Active(anon):       <span class="hljs-number">3864</span> kB
  Inactive(anon):  <span class="hljs-number">2842564</span> kB
  Active(file):    <span class="hljs-number">1505896</span> kB
  Inactive(file):  <span class="hljs-number">2286148</span> kB
  <span class="hljs-attr">Unevictable</span>:           <span class="hljs-number">0</span> kB
  <span class="hljs-attr">Mlocked</span>:               <span class="hljs-number">0</span> kB
  <span class="hljs-attr">SwapTotal</span>:       <span class="hljs-number">2097148</span> kB
  <span class="hljs-attr">SwapFree</span>:        <span class="hljs-number">2097148</span> kB
  <span class="hljs-attr">Dirty</span>:                <span class="hljs-number">16</span> kB
  <span class="hljs-attr">Writeback</span>:             <span class="hljs-number">0</span> kB
  <span class="hljs-attr">AnonPages</span>:       <span class="hljs-number">2506628</span> kB
  <span class="hljs-attr">Mapped</span>:          <span class="hljs-number">1368216</span> kB
  <span class="hljs-attr">Shmem</span>:            <span class="hljs-number">339912</span> kB
  <span class="hljs-attr">KReclaimable</span>:     <span class="hljs-number">163640</span> kB
  <span class="hljs-attr">Slab</span>:             <span class="hljs-number">349152</span> kB
  <span class="hljs-attr">SReclaimable</span>:     <span class="hljs-number">163640</span> kB
  <span class="hljs-attr">SUnreclaim</span>:       <span class="hljs-number">185512</span> kB
  <span class="hljs-attr">KernelStack</span>:       <span class="hljs-number">22048</span> kB
  <span class="hljs-attr">PageTables</span>:        <span class="hljs-number">52072</span> kB
  <span class="hljs-attr">NFS_Unstable</span>:          <span class="hljs-number">0</span> kB
  <span class="hljs-attr">Bounce</span>:                <span class="hljs-number">0</span> kB
  <span class="hljs-attr">WritebackTmp</span>:          <span class="hljs-number">0</span> kB
  <span class="hljs-attr">CommitLimit</span>:     <span class="hljs-number">9642372</span> kB
  <span class="hljs-attr">Committed_AS</span>:   <span class="hljs-number">14348500</span> kB
  <span class="hljs-attr">VmallocTotal</span>:   <span class="hljs-number">34359738367</span> kB
  <span class="hljs-attr">VmallocUsed</span>:       <span class="hljs-number">59772</span> kB
  <span class="hljs-attr">VmallocChunk</span>:          <span class="hljs-number">0</span> kB
  <span class="hljs-attr">Percpu</span>:            <span class="hljs-number">17856</span> kB
  <span class="hljs-attr">HardwareCorrupted</span>:     <span class="hljs-number">0</span> kB
  <span class="hljs-attr">AnonHugePages</span>:         <span class="hljs-number">0</span> kB
  <span class="hljs-attr">ShmemHugePages</span>:        <span class="hljs-number">0</span> kB
  <span class="hljs-attr">ShmemPmdMapped</span>:        <span class="hljs-number">0</span> kB
  <span class="hljs-attr">FileHugePages</span>:         <span class="hljs-number">0</span> kB
  <span class="hljs-attr">FilePmdMapped</span>:         <span class="hljs-number">0</span> kB
  <span class="hljs-attr">HugePages_Total</span>:       <span class="hljs-number">0</span>
  <span class="hljs-attr">HugePages_Free</span>:        <span class="hljs-number">0</span>
  <span class="hljs-attr">HugePages_Rsvd</span>:        <span class="hljs-number">0</span>
  <span class="hljs-attr">HugePages_Surp</span>:        <span class="hljs-number">0</span>
  <span class="hljs-attr">Hugepagesize</span>:       <span class="hljs-number">2048</span> kB
  <span class="hljs-attr">Hugetlb</span>:               <span class="hljs-number">0</span> kB
  <span class="hljs-attr">DirectMap4k</span>:      <span class="hljs-number">545488</span> kB
  <span class="hljs-attr">DirectMap2M</span>:     <span class="hljs-number">9693184</span> kB
  <span class="hljs-attr">DirectMap1G</span>:     <span class="hljs-number">5242880</span> kB
</code></pre></li>
</ul>
<hr />
<h2 id="heading-6">方法 6：使用系统监视器查看内存</h2>
<ul>
<li>系统监视器是一个图形工具，显示系统进程和资源使用情况；</li>
<li><p>在桌面上点击 "应用程序" 菜单，再单击 "工具"，然后选择 "系统监视器"；</p>
<p>  <img src="https://blog.whowin.net/images/100027/screenshot-of-application-menu.png" alt="Screenshot of application and utilities" /></p>
<p>  <img src="https://blog.whowin.net/images/100027/screenshot-of-system-monitor.png" alt="Screenshot of system monitor" /></p>
</li>
<li><p>在系统监视器中，点击 "资源" 选项卡，可以看到内存使用情况和其他资源使用信息；</p>
<p>  <img src="https://blog.whowin.net/images/100027/screenshot-of-resources.png" alt="screenshot-of-resources" /></p>
</li>
</ul>
<hr />
<h2 id="heading-7-glances">方法 7. 使用 <code>glances</code> 查看内存</h2>
<ul>
<li><code>glances</code> 是一种先进的系统监控工具，可提供各种系统资源(包括内存)的全面信息；</li>
<li>用下面命令安装 <code>glances</code>：<pre><code>  sudo apt install glances
</code></pre></li>
<li><p>在终端上键入 <code>glances</code> 即可启动；</p>
<p>  <img src="https://blog.whowin.net/images/100027/sreenshot-of-glances.png" alt="Screenshot of glances" /></p>
</li>
</ul>
<hr />
<h2 id="heading-8-nmon">方法 8. 使用 <code>nmon</code> 查看内存</h2>
<ul>
<li><code>nmon</code> 是另一个系统监视工具，它提供有关各种系统资源(包括内存)的信息；</li>
<li>使用下面命令安装 <code>nmon</code>：<pre><code>  sudo apt install nmon
</code></pre></li>
<li><p>安装完成后，在终端键入 <code>nmon</code> 即可启动；</p>
<p>  <img src="https://blog.whowin.net/images/100027/screenshot-of-nmon-1.png" alt="Screenshot of nmon" /></p>
</li>
<li><p>启动会，按 "m" 可查看内存使用情况；</p>
<p>  <img src="https://blog.whowin.net/images/100027/screenshot-of-nmon-2.png" alt="Screenshot of nmon" /></p>
</li>
</ul>
<hr />
<h2 id="heading-9-smem">方法 9. 使用 <code>smem</code> 查看内存</h2>
<ul>
<li><code>smem</code> 提供内存使用情况报告，它能够更准确地表示应用程序和进程正在使用的物理内存； </li>
<li>使用下面命令安装 <code>smem</code>：<pre><code>  sudo apt install smem
</code></pre></li>
<li>运行 <code>smem</code>：<pre><code>  whowin@whowin-ThinkPad-T14s:~$ smem
    PID User     Command                         Swap      USS      PSS      RSS 
   <span class="hljs-number">2383</span> whowin   /usr/bin/fcitx-dbus-watcher        <span class="hljs-number">0</span>      <span class="hljs-number">224</span>      <span class="hljs-number">246</span>     <span class="hljs-number">1412</span> 
   <span class="hljs-number">2436</span> whowin   /usr/libexec/gnome-session-        <span class="hljs-number">0</span>      <span class="hljs-number">480</span>      <span class="hljs-number">528</span>     <span class="hljs-number">5300</span> 
   <span class="hljs-number">2623</span> whowin   /usr/libexec/gsd-screensave        <span class="hljs-number">0</span>      <span class="hljs-number">728</span>      <span class="hljs-number">789</span>     <span class="hljs-number">6424</span> 
   <span class="hljs-number">2535</span> whowin   /usr/libexec/xdg-permission        <span class="hljs-number">0</span>      <span class="hljs-number">744</span>      <span class="hljs-number">801</span>     <span class="hljs-number">6388</span> 
   <span class="hljs-number">2379</span> whowin   /usr/bin/dbus-daemon --sysl        <span class="hljs-number">0</span>      <span class="hljs-number">684</span>      <span class="hljs-number">802</span>     <span class="hljs-number">3796</span> 
   <span class="hljs-number">2262</span> whowin   /usr/lib/gdm3/gdm-x-session        <span class="hljs-number">0</span>      <span class="hljs-number">760</span>      <span class="hljs-number">815</span>     <span class="hljs-number">6616</span> 
   <span class="hljs-number">2410</span> whowin   /usr/bin/dbus-daemon --conf        <span class="hljs-number">0</span>      <span class="hljs-number">692</span>      <span class="hljs-number">818</span>     <span class="hljs-number">4512</span> 
   <span class="hljs-number">2601</span> whowin   /usr/libexec/gsd-a11y-setti        <span class="hljs-number">0</span>      <span class="hljs-number">772</span>      <span class="hljs-number">848</span>     <span class="hljs-number">7052</span> 
   <span class="hljs-number">3532</span> whowin   /snap/chromium/<span class="hljs-number">2724</span>/usr/lib        <span class="hljs-number">0</span>      <span class="hljs-number">304</span>      <span class="hljs-number">867</span>     <span class="hljs-number">3096</span> 
  ......
</code></pre></li>
<li>USS(Unique Set Size)：唯一集大小，即进程独自占用物理内存，只计算进程独自占用的物理内存大小，不包含任何共享的部分；</li>
<li>PSS(Proportion Set Size)：比例集大小，使用某个共享内存的所有进程均分该共享内存的大小，加上该进程独自占用的内存(USS)，即为比例集的大小；</li>
<li>RSS(Resident Set Size)：驻留集大小，即进程所使用的非交换区的物理内存的大小，该进程独占内存(USS)，加上该进程使用的共享内存大小(非均分共享内存)，即为驻留集大小。</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<h2 id="heading-httpsblogcsdnnetwhowincategory12404164html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12404164.html">『进程间通信专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>
]]></content:encoded></item><item><title><![CDATA[使用ioctl扫描wifi信号获取信号属性的实例(一)]]></title><description><![CDATA[使用 wifi 是一件再平常不过的是事情，有很多 wifi 工具可以帮助你扫描附近的 wifi 信号，测试信号强度等，但如何通过编程来操作 wifi 却鲜有文章涉及；本文立足实践，不使用任何第三方库，仅使用 ioctl 扫描附近的 wifi 信号，并获取这些 AP 的 ESSID、MAC 地址、占用信道和工作频率，本文将给出完整的源程序，今后还会写一些文章讨论如果编程获取 wifi 信号的其它属性(比如：信号强度、加密方式等)的方法，敬请关注；本文程序在 ubuntu 20.04 下编译测试完成...]]></description><link>https://whowin.cn/180022-how-to-scan-wifi-signal</link><guid isPermaLink="true">https://whowin.cn/180022-how-to-scan-wifi-signal</guid><category><![CDATA[networking]]></category><category><![CDATA[wireless network]]></category><category><![CDATA[ioctl]]></category><category><![CDATA[网络编程]]></category><category><![CDATA[wifi scanning]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Mon, 26 Jun 2023 12:07:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688299765591/362887c4-6f09-4ab9-b841-20224f7ee067.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>使用 wifi 是一件再平常不过的是事情，有很多 wifi 工具可以帮助你扫描附近的 wifi 信号，测试信号强度等，但如何通过编程来操作 wifi 却鲜有文章涉及；本文立足实践，不使用任何第三方库，仅使用 ioctl 扫描附近的 wifi 信号，并获取这些 AP 的 ESSID、MAC 地址、占用信道和工作频率，本文将给出完整的源程序，今后还会写一些文章讨论如果编程获取 wifi 信号的其它属性(比如：信号强度、加密方式等)的方法，敬请关注；本文程序在 ubuntu 20.04 下编译测试完成，gcc 版本号 9.4.0；阅读本文并不需要对 IEEE802.11 协议有所了解。</p>
</blockquote>
<h2 id="heading-1">1 前言</h2>
<ul>
<li>目前的无线网络都是采用 <code>IEEE802.11</code> 协议，<code>802.11</code> 是一个协议簇，目前无线网络最常用的是 <code>802.11n</code>，理论最高速度高达 <code>600Mbit/s</code></li>
<li>WIFI 是 <code>802.11</code> 规范的一种具体实现；</li>
<li>本文的目标是使用 C 语言在 ubuntu 下编写出一个扫描 WIFI 信号的程序，电脑上至少要有一片无线网卡才能扫描附近的 WIFI 信号；</li>
<li>扫描 WIFI 信号显然是要操作无线网卡才能实现，通常情况下无线网卡的驱动程序是在内核空间的，用户空间的应用程序是无法直接控制驱动程序的；</li>
<li>为了能够从用户空间控制无线网卡的驱动程序，我们在用户空间编写的程序需要使用 IPC 通信与内核进程进行通信；</li>
<li>实现 IPC 进程间通信的方式有很多，本文采用的是 <code>ioctl</code>，但还有其它方式，比如 <code>netlink</code> 等；</li>
<li>本文采用的 <code>ioctl</code> 方法是基于 <strong>Wireless Extensions</strong>(简称 <strong>WE</strong> 或 WEXT)的，WE 是一组通用 API，可以控制无线网卡驱动程序向用户空间进程传送 wifi 的配置和统计信息；</li>
<li>2006年，出现了 <code>cfg80211</code> 和 <code>nl80211</code>，其目标是取代 WE，<code>cfg80211</code> 和 <code>nl80211</code> 不再使用 <code>ioctl</code> 与无线网卡驱动程序进行通信，而是采用 <code>netlink</code>；</li>
<li>有些无线网络工具是使用 <code>cfg80211</code> 和 <code>nl80211的</code>，像 <code>iw、hostapd</code> 或 <code>wpa_supplicant</code> 程序，它们需要使用 <code>netlink</code> 库(如 <code>libnl</code> 或 <code>libnl-tiny</code>)和 <code>netlink</code> 头文件 <code>nl80211.h</code>；</li>
<li>使用 WE 的另一个好处就是不需要依赖其它库(比如 <code>libnl</code>)，只要有标准 C 语言库即可实现，像无线网络工具 <code>iwlist</code> 等使用的就是 WE</li>
<li>实际上，不管是 WE 还是 <code>cfg80211</code> 和 <code>nl80211</code>，都鲜有资料和范例，本文介绍了 WE 的使用，后续文章可能会介绍 <code>cfg80211</code> 和 <code>nl80211</code> 的使用；</li>
<li>尽管前面多次提到 802.11 协议，但阅读本文并不需要对该协议有所了解，但需要有一定的 C 语言基础，范例中大量使用了单向链表和系统调用 <code>ioctl()</code>，读者需要对这些知识有足够的了解；</li>
<li>本文旨在向读者介绍如何使用 ioctl() 对 wifi 信号进行扫描并获取扫描结果，在处理扫描结果上仅处理了三类数据，以便搭建起一个大致的框架，后续文章会着重介绍对扫描结果的处理。</li>
</ul>
<h2 id="heading-2-ioctlwifi">2 使用ioctl进行wifi信号扫描的基本原理</h2>
<h3 id="heading-21-we-api">2.1 WE API</h3>
<ul>
<li><strong>WE</strong>(Wireless Extensions) 定义了一系列关于无线网络接口的系统调用，使用 <code>ioctl()</code> 实现，这些系统调用实现了用户空间的应用程序与内核中的无线网络接口驱动程序之间的通信；</li>
<li>这些系统调用定义在头文件 <code>/usr/include/linux/wireless.h</code>，调用 <code>ioctl()</code> 的基本方法如下：<pre><code class="lang-C">  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">ioctl</span><span class="hljs-params">(<span class="hljs-keyword">int</span> socket, <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> request, struct iwreq *wrq)</span></span>;
</code></pre>
</li>
<li>其中的 request 在 <code>wireless.h</code> 中定义，以 SIOC 开头的宏定义；<code>struct iwreq</code> 同样在 <code>wireless.h</code> 中定义，所有 WE 中的调用均使用这个结构的指针作为 <code>ioctl()</code> 的第三个参数；</li>
<li><p>下面一段代码可以获得无线网络接口 <code>wlp3s0</code> 当前连接的 WIFI 信号的 ESSID，将其中的 <code>wlp3s0</code> 改成你的电脑上的无线网卡的设备名就可以编译运行了</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;string.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;unistd.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/types.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/ioctl.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/socket.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;linux/wireless.h&gt;</span></span>

  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> IF_NAME         <span class="hljs-meta-string">"wlp3s0"</span></span>
  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> <span class="hljs-title">wrq</span>;</span>
      <span class="hljs-keyword">char</span> essid[IW_ESSID_MAX_SIZE + <span class="hljs-number">1</span>] = {<span class="hljs-number">0</span>};

      <span class="hljs-keyword">int</span> sock = socket(AF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);
      <span class="hljs-built_in">memset</span>(&amp;wrq, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(struct iwreq));
      <span class="hljs-built_in">strncpy</span>(wrq.ifr_name, IF_NAME, IFNAMSIZ);
      ioctl(sock, SIOCGIWNAME, &amp;wrq);
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Protocol: %s\n"</span>, wrq.u.name);
      wrq.u.data.pointer = essid;
      ioctl(sock, SIOCGIWESSID, &amp;wrq);
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"ESSID is %s\n"</span>, (<span class="hljs-keyword">char</span> *)wrq.u.essid.pointer);
      close(sock);
      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li>这段代码调用了两次 <code>ioctl()</code>，第一次的指令是 <code>SIOCGIWNAME</code>，获取了无线网卡的协议，第二次的指令是 <code>SIOCGIWESSID</code>，获取了无线网卡连接的 wifi 信号的 <code>ESSID</code>；</li>
<li>对 WE 的很多指令而言，在执行 <code>ioctl()</code> 之前，需要先调用一下 <strong>SIOCGIWNAME</strong>，这个调用比较简单，只需要设置一下接口名称，调用成功会返回协议名称，可以用来检验是否为无线网卡，有线接口的设备名在调用这个 <code>ioctl()</code> 时会出错；</li>
<li>这段程序没有任何错误处理，如果要实际应用一定要补充一些代码；</li>
<li>编译：<code>gcc -Wall wifi-essid.c -o wifi-essid</code></li>
<li>运行：<code>./wifi-essid</code></li>
</ul>
<h3 id="heading-22-wifi">2.2 启动 wifi 信号扫描</h3>
<ul>
<li>在头文件 <code>wireless.h</code> 中定义的众多指令中，有一个 <strong>SIOCSIWSCAN</strong> 可以使用指定的无线网卡扫描附近的 AP(Access Point)，然后使用 <strong>SIOCGIWSCAN</strong> 获取扫描结果；</li>
<li>在使用 <code>SIOCSIWSCAN</code> 启动扫描之前，不需要先调用 <code>SIOCGIWNAME</code></li>
<li><p>下面这段代码会在无线网卡 <code>wlp3s</code> 上启动 AP 扫描</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> <span class="hljs-title">wrq</span>;</span>
  <span class="hljs-built_in">memset</span>(&amp;wrq, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(struct iwreq));

  <span class="hljs-built_in">strncpy</span>(wrq.ifr_name, ifname, IFNAMSIZ);
  wrq.u.data.pointer = <span class="hljs-literal">NULL</span>;
  wrq.u.data.flags = <span class="hljs-number">0</span>;
  wrq.u.data.length = <span class="hljs-number">0</span>;
  ioctl(sockfd, SIOCSIWSCAN, &amp;wrq);
</code></pre>
</li>
<li>在启动 <code>SIOCSIWSCAN</code> 之前，要初始化 <code>struct iwreq</code> 中的四个字段，参考上面程序。</li>
</ul>
<h3 id="heading-23-wifi">2.3 获取 wifi 信号的扫描结果</h3>
<ul>
<li>使用头文件 <code>wireless.h</code> 中的 <strong>SIOCGIWSCAN</strong> 可以获取 wifi 信号的扫描结果</li>
<li>在启动 wifi 信号扫描后，并不能立即返回结果，要等待几秒后再发出 <code>SIOCGIWSCAN</code> 获取扫描结果，等待的时间主要取决于当前的系统和驱动程序，所以在调用 <code>ioctl()</code> 获取扫描结果时，要监视 <strong>errno</strong>，如果 <code>error == EAGAIN</code>，则需要 sleep 一下后再次调用 <code>ioctl()</code></li>
<li><p>下面这段程序演示了获取扫描结果的过程</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> <span class="hljs-title">wrq</span>;</span>

  GET_AGAIN:
  wrq.u.data.pointer = buffer;
  wrq.u.data.length = buflen;
  wrq.u.data.flags = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">if</span> (ioctl(sockfd, SIOCGIWSCAN, &amp;wrq) == <span class="hljs-number">-1</span>) {
      <span class="hljs-keyword">if</span> (errno == EAGAIN) {
          sleep(<span class="hljs-number">2</span>);
          <span class="hljs-keyword">goto</span> GET_AGAIN;
      }
  }
</code></pre>
</li>
<li>在发出指令 <code>SIOCGIWSCAN</code> 之前，需要初始化 <code>struct iwreq</code> 中的三个字段，参考上面程序，<code>buffer</code> 是存放返回结果的内存缓冲区，<code>buflen</code> 是 <code>buffer</code> 的长度；</li>
<li>扫描结果的数据需要多大的内存空间，在调用 <code>SIOCGIWSCAN</code> 之前并不知道，所以在调用 <code>ioctl()</code> 时可能会因为 <code>buffer</code> 不够大而失败，这时我们不得不重新为 <code>buffer</code> 申请一块更大的内存并再次调用 <code>ioctl()</code>；</li>
<li><p>下面这段代码演示了获取扫描结果的全过程</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> <span class="hljs-title">wrq</span>;</span>
  <span class="hljs-keyword">char</span> *buffer = <span class="hljs-literal">NULL</span>;
  <span class="hljs-keyword">uint32_t</span> buflen = IW_SCAN_MAX_DATA;
  <span class="hljs-keyword">int</span> counter = <span class="hljs-number">0</span>;

  REALLOC_MEM:
  <span class="hljs-keyword">if</span> (buffer) {
      <span class="hljs-built_in">free</span>(buffer);
      <span class="hljs-built_in">exit</span>(<span class="hljs-number">-1</span>);
  }
  buflen = IW_SCAN_MAX_DATA * (counter + <span class="hljs-number">1</span>);
  buffer = (<span class="hljs-keyword">char</span> *)<span class="hljs-built_in">malloc</span>(buflen);
  <span class="hljs-keyword">if</span> (buffer == <span class="hljs-literal">NULL</span>) {
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Can't allocate enough memory for scanning result.\n"</span>);
      <span class="hljs-built_in">exit</span>(<span class="hljs-number">-1</span>);
  }

  GET_AGAIN:
  wrq.u.data.pointer = buffer;
  wrq.u.data.length = buflen;
  wrq.u.data.flags = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">if</span> (ioctl(sockfd, SIOCGIWSCAN, &amp;wrq) == <span class="hljs-number">-1</span>) {
      <span class="hljs-keyword">if</span> (errno == EAGAIN) {
          sleep(<span class="hljs-number">2</span>);
          <span class="hljs-keyword">goto</span> GET_AGAIN;
      }
      <span class="hljs-keyword">if</span> (errno == E2BIG) {
          counter++;
          <span class="hljs-keyword">goto</span> REALLOC_MEM;
      }
      <span class="hljs-keyword">if</span> (buffer) {
          <span class="hljs-built_in">free</span>(buffer);
      }
      <span class="hljs-built_in">exit</span>(<span class="hljs-number">-1</span>);
  }
  <span class="hljs-comment">/* TODO */</span>
</code></pre>
</li>
<li>当 <code>errno == E2BIG</code> 表示 buffer 不够大；<code>IW_SCAN_MAX_DATA</code> 是头文件 <code>wireless.h</code> 中定义的一个常数，在我的版本下是 4096；</li>
</ul>
<h3 id="heading-24">2.4 扫描结果的数据格式</h3>
<ul>
<li>首先，扫描结果是一个数据流(stream)，所谓数据流，就是收到的数据是各种不同结构的数据连接在一起的连续字节序列，中间并不会有分隔符，这些数据需要自行进行解析、分割；</li>
<li>在收到的数据中，包含有扫描到的所有 wifi 信号的各种属性，比如：ESSID、MAC、工作频率、占用信道等等，如果不能正确解析，将导致混乱；</li>
<li>下面所展示的 <code>struct、union</code> 等如无特别说明，均在 <code>wireless.h</code> 中定义；</li>
<li><p>我们先来看一下前面经常提到的 <code>struct iwreq</code>，WE 中每次发起 <code>ioctl()</code> 都会用到这个结构，调用前设置参数，调用后返回数据，均使用这个结构；</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> {</span>
      <span class="hljs-keyword">union</span>
      {
          <span class="hljs-keyword">char</span>  ifrn_name[IFNAMSIZ];  <span class="hljs-comment">/* if name, e.g. "eth0" */</span>
      } ifr_ifrn;

      <span class="hljs-comment">/* Data part (defined just above) */</span>
      <span class="hljs-keyword">union</span> iwreq_data  u;
  };
</code></pre>
</li>
<li>在头文件 <code>/usr/include/linux/if.h</code>，有一个宏定义，使得我们可以较为方便地访问 <code>struct iwreq</code> 中的 <code>ifrn_name</code> 字段；<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ifr_name    ifr_ifrn.ifrn_name;</span>
</code></pre>
</li>
<li>下面这段代码使用这个宏定义去访问 <code>struct iwreq</code> 中的 <code>ifrn_name</code> 字段；<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> <span class="hljs-title">wrq</span>;</span>
  <span class="hljs-built_in">strncpy</span>(wrq.ifr_name, IF_NAME, IFNAMSIZ);
</code></pre>
</li>
<li>因为 <code>if.h</code> 中的这个宏定义，上面代码中的 <code>wrq.ifr_name</code> 实际访问的是 <code>wrq.ifr_ifrn.ifrn_name</code>，在前面的代码中，也曾有过这种用法，如果你当时有疑问的话，现在应该清楚了；</li>
<li><p><code>struct iwreq</code> 的第二个字段是 <code>union iwreq_data</code>，这个 union 的定义如下(中间省略了一些本例用不上的定义)：</p>
<pre><code class="lang-C">  <span class="hljs-keyword">union</span> iwreq_data {
    <span class="hljs-comment">/* Config - generic */</span>
    <span class="hljs-keyword">char</span>    name[IFNAMSIZ];
            <span class="hljs-comment">/* Name : used to verify the presence of  wireless extensions.
             * Name of the protocol/provider... */</span>

    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_point</span>  <span class="hljs-title">essid</span>;</span>   <span class="hljs-comment">/* Extended network name */</span>
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>  <span class="hljs-title">nwid</span>;</span>    <span class="hljs-comment">/* network id (or domain - the cell) */</span>
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_freq</span>   <span class="hljs-title">freq</span>;</span>    <span class="hljs-comment">/* frequency or channel :
                               * 0-1000 = channel
                               * &gt; 1000 = frequency in Hz */</span>
      ......
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span>  <span class="hljs-title">ap_addr</span>;</span> <span class="hljs-comment">/* Access point address */</span>
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span>  <span class="hljs-title">addr</span>;</span>    <span class="hljs-comment">/* Destination address (hw/mac) */</span>

    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_param</span>  <span class="hljs-title">param</span>;</span>   <span class="hljs-comment">/* Other small parameters */</span>
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_point</span>  <span class="hljs-title">data</span>;</span>    <span class="hljs-comment">/* Other large parameters */</span>
  };
</code></pre>
</li>
<li>我们在前面的代码中多次用到的 <code>wrq.u.data</code>，按照上面的定义，是一个 <code>struct iw_point</code>，这个结构的定义如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_point</span> {</span>
      <span class="hljs-keyword">void</span>  *pointer;   <span class="hljs-comment">/* Pointer to the data  (in user space) */</span>
      __u16 length;     <span class="hljs-comment">/* number of fields or size in bytes */</span>
      __u16 flags;      <span class="hljs-comment">/* Optional params */</span>
  };
</code></pre>
</li>
<li>在调用 <code>ioctl()</code> 获取扫描结果前，我们把存储返回数据的指针放在了 <code>struct iw_point</code> 的 <code>pointer</code> 中，把 <code>length</code> 设置为缓冲区的长度，把 <code>flags</code> 设置为0；</li>
<li>当这个 <code>ioctl()</code> 调用成功后，<code>struct iw_point</code> 中的 <code>flags</code> 被设置为 1，<code>length</code> 返回数据的实际长度，当然数据的指针还在 <code>pointer</code> 中；</li>
<li><p>下面这段代码简单回顾一下到现在为止我们在这一节的成果：</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> <span class="hljs-title">wrq</span>;</span>

  wrq.u.data.pointer = buffer;
  wrq.u.data.length = buflen;
  wrq.u.data.flags = <span class="hljs-number">0</span>;
  ioctl(socket, SIOCGIWSCAN, &amp;wrq);
  <span class="hljs-comment">/* 
     wrq.u.data.flags   由初始值0变为1
     wrq.u.data.pointer 扫描结果数据指针
     wrq.u.data.length  扫描结果数据的实际长度
  */</span>
</code></pre>
</li>
<li>获得了返回数据的实际长度，我们就可以遍历数据，而不至于产生越界等不可预知的错误；</li>
<li>前面说过，wifi 信号扫描结果返回的是一个数据流(stream)，这些数据的首指针就是 <code>wrq.u.data.pointer</code>，通常称这个数据流为 event stream，数据流中包含着很多 wifi 信号的属性，每个属性被称为一个 event；</li>
<li>这个 <code>event stream</code> 中每个 <code>event</code> 符合 <code>struct iw_event</code>，定义如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> {</span>
      __u16        len;            <span class="hljs-comment">/* Real length of this stuff */</span>
      __u16        cmd;            <span class="hljs-comment">/* Wireless IOCTL */</span>
      <span class="hljs-keyword">union</span> iwreq_data    u;        <span class="hljs-comment">/* IOCTL fixed payload */</span>
  };
</code></pre>
</li>
<li>先来看一下实际收到的数据(<code>wrq.u.data.pointer</code>指向的数据)是什么样子：<pre><code class="lang-plain">  18 00 15 8B 00 00 00 00 01 00 DC FE 18 68 73 80 
  00 00 00 00 00 00 00 00 10 00 05 8B 00 00 00 00 
  9D 00 00 00 00 00 00 00 10 00 05 8B 00 00 00 00 
  99 16 00 00 06 00 00 00 17 00 1B 8B 00 00 00 00 
  07 00 01 00 00 00 00 00 31 35 2D 31 31 30 31
</code></pre>
</li>
<li>用 <code>struct iw_event</code> 去对应这个数据，那么，<code>len</code> 是 0x0018(十进制24)，表示这个 event 数据的总长度，所以可以确定这个 event 的数据如下：<pre><code class="lang-plain">  18 00 15 8B 00 00 00 00 01 00 DC FE 18 68 73 80 
  00 00 00 00 00 00 00 00
</code></pre>
</li>
<li>这是第 1 个 event(简称为 event_1)，后面的数据是另一个 event，仍然可以用 <code>struct iw_event</code> 去对应，以此类推，还可以再分割出三个 event；</li>
<li>第 2 个 event(event_2)，长度是0X0010(十进制16)：<pre><code class="lang-plain">  10 00 05 8B 00 00 00 00 9D 00 00 00 00 00 00 00
</code></pre>
</li>
<li>第 3 个 event(event_3)，长度是0X0010(十进制16)：<pre><code class="lang-plain">  10 00 05 8B 00 00 00 00 99 16 00 00 06 00 00 00
</code></pre>
</li>
<li>第 4 个 event(event_4)，长度是0X0017(十进制23)：<pre><code class="lang-plain">  17 00 1B 8B 00 00 00 00 07 00 01 00 00 00 00 00 
  31 35 2D 31 31 30 31
</code></pre>
</li>
<li>在 event_1 中，<code>struct iw_enent</code> 中的 <code>cmd</code> 在这个 event 中是 0X8B15，这个值决定着 <code>struct iw_event</code> 中的 <code>union iwreq_data</code> 如何取值；</li>
<li>前面说到过 <code>WE API</code> 定义了一组与无线网卡驱动程序交互的指令，定义在头文件 <code>wireless.h</code> 中，以 SIOC 开头的宏定义，这些指令代码适用于 <code>struct iw_event</code> 中的 <code>cmd</code>；</li>
<li>从 <code>wireless.h</code> 中可以查到 0X8B15 的指令宏定义是 <code>SIOCGIWAP</code>，含义是 <strong>获取AP的MAC地址</strong>，可以把指令为 <code>SIOCGIWAP</code> 的 event 称为 <code>SIOCGIWAP event</code>；</li>
<li>按照这个方法，可以把 event_2、event_3 和 event_4 的指令宏定义查出来：<ul>
<li>event_2：指令代码是 0X8B05，宏定义为：<code>SIOCGIWFREQ</code>，含义为：获取 AP 的工作信道/工作频率；</li>
<li>event_3：指令代码是 0X8B05，宏定义为：<code>SIOCGIWFREQ</code>，含义为：获取 AP 的工作信道/工作频率；</li>
<li>event_4：指令代码是 0X8B1B，宏定义为：<code>SIOCGIWESSID</code>，含义为：获取 AP 的 ESSID；</li>
</ul>
</li>
<li><p>这里面有两个 <code>SIOCGIWFREQ event</code>，一个返回的是占用的信道，另一个返回的工作频率；</p>
</li>
<li><p>再回到 <code>struct iw_event</code> 上来，我们已经搞清楚了其中的 len 和 cmd 两个字段，还有一个字段是 <code>union wreq_data u</code>；</p>
</li>
<li><p>从 <code>union wreq_data</code> 的定义(前面介绍过)中可以看到，这个 union 可以有很多种选择，本文的范例中仅处理了 <code>SIOCGIWAP、SIOCGIWFREQ、SIOCGIWESSID</code> 三个指令，仅以这三个指令为例做出说明；</p>
<ul>
<li>当指令为 <code>SIOCGIWAP</code> 时，<code>union wreq_data `` 应选择``struct sockaddr ap_addr</code>；</li>
<li>当指令为 <code>SIOCGIWFREQ</code> 时，<code>union wreq_data u</code> 应选择 <code>struct iw_freq freq</code>；</li>
<li><p>当指令为 <code>SIOCGIWESSID</code> 时，相对复杂一些，并不能选择 <code>struct iw_point essid</code>(在用指令 <code>SIOCGIWESSID</code> 获取 ESSID 时要选择这个结构)，建议自定义一个结构使问题变得简单一点；</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_essid</span> {</span>
      <span class="hljs-keyword">uint16_t</span> len;
      <span class="hljs-keyword">uint16_t</span> flags;
      <span class="hljs-keyword">char</span> __attribute__((aligned(<span class="hljs-number">8</span>)))essid;
  };

  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> *<span class="hljs-title">essid_evt</span> = ...;</span>   <span class="hljs-comment">/* 指向 SIOCGIWESSID event 数据 */</span>
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_essid</span> *<span class="hljs-title">essid_p</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">iw_essid</span> *)&amp;(<span class="hljs-title">essid_evt</span>-&gt;<span class="hljs-title">u</span>.<span class="hljs-title">data</span>);</span>

  <span class="hljs-comment">/* 
     essid_p-&gt;len     为 essid 的长度
     &amp;essid_p-&gt;essid  指向 essid 字符串
     essid_p-&gt;flags   为 1
  */</span>
</code></pre>
</li>
</ul>
</li>
<li><p>下面这段程序可以打印出这段 event_1 中的 AP 的 MAC 地址：</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;linux/wireless.h&gt;</span></span>

  <span class="hljs-keyword">uint8_t</span> data[] = {<span class="hljs-number">0x18</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x15</span>,<span class="hljs-number">0x8B</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x01</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0xDC</span>,<span class="hljs-number">0xFE</span>,<span class="hljs-number">0x18</span>,<span class="hljs-number">0x68</span>,<span class="hljs-number">0x73</span>,<span class="hljs-number">0x80</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>};

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> *<span class="hljs-title">wevt</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">iw_event</span> *)<span class="hljs-title">data</span>;</span>

      <span class="hljs-keyword">if</span> (wevt-&gt;cmd == SIOCGIWAP){
          <span class="hljs-keyword">uint8_t</span> *mac = (<span class="hljs-keyword">uint8_t</span> *)wevt-&gt;u.ap_addr.sa_data;
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"MAC: "</span>);
          <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">6</span>; ++i) {
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%02X"</span>, mac[i]);
              <span class="hljs-keyword">if</span> (i &lt; <span class="hljs-number">5</span>) <span class="hljs-built_in">putchar</span>(<span class="hljs-string">':'</span>);
          }
          <span class="hljs-built_in">puts</span>(<span class="hljs-string">""</span>);
      }
      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li>event_2 和 event_3 都是 <code>SIOCGIWFREQ event</code>，在 <code>wireless.h</code> 中有说明，当计算出来的频率大于 1000(Hz) 时，其值为 AP 的工作频率，否则为 AP 占用的信道，所以，event_2 和 event_3 一个返回的是频率，另一个返回的是信道；</li>
<li>前面说过，<code>SIOCGIWFREQ event</code> 返回数据使用 <code>struct iw_freq</code>，这个结构的定义如下：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_freq</span> {</span>
      __s32       m;      <span class="hljs-comment">/* Mantissa */</span>
      __s16       e;      <span class="hljs-comment">/* Exponent */</span>
      __u8        i;      <span class="hljs-comment">/* List index (when in range struct) */</span>
      __u8        flags;  <span class="hljs-comment">/* Flags (fixed/auto) */</span>
  };
</code></pre>
<ul>
<li>字段 flags 为 0 时，表示工作频率是由驱动程序自动选择的；为 1 时表示工作频率为固定设置值；</li>
<li>字段 i 在本例中没有意义；</li>
<li>频率由 m 和 e 两个字段计算得到，其中：e 为底数为 10 的指数，m 为尾数，frequency = m x 10<sup>e</sup> </li>
</ul>
</li>
<li><p>下面这段程序可以处理 event_2 和 event_3，打印出 AP 占用的信道号和工作频率：</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;math.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;linux/wireless.h&gt;</span></span>

  <span class="hljs-keyword">uint8_t</span> data[] = {<span class="hljs-number">0x10</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x05</span>,<span class="hljs-number">0x8B</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x9D</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,
                    <span class="hljs-number">0x10</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x05</span>,<span class="hljs-number">0x8B</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x99</span>,<span class="hljs-number">0x16</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x06</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>};

  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">channel_or_frequency</span><span class="hljs-params">(struct iw_event *wevt)</span> </span>{
      <span class="hljs-keyword">if</span> (wevt-&gt;cmd == SIOCGIWFREQ){
          <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_freq</span> *<span class="hljs-title">ap_freq</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">iw_freq</span> *)&amp;(<span class="hljs-title">wevt</span>-&gt;<span class="hljs-title">u</span>.<span class="hljs-title">freq</span>);</span>
          <span class="hljs-keyword">double</span> freq = (<span class="hljs-keyword">double</span>)ap_freq-&gt;m * <span class="hljs-built_in">pow</span>(<span class="hljs-number">10</span>, ap_freq-&gt;e);
          <span class="hljs-keyword">if</span> (freq &gt; <span class="hljs-number">1000</span>) {
              <span class="hljs-comment">// ap的工作频率</span>
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Frequency: %.3f\n"</span>, (<span class="hljs-keyword">float</span>)freq / (<span class="hljs-number">1e9</span>));
          } <span class="hljs-keyword">else</span> {
              <span class="hljs-comment">// AP的channel</span>
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Channel: %d\n"</span>, (<span class="hljs-keyword">int</span>)freq);
          }
      }
  }
  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> *<span class="hljs-title">wevt</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">iw_event</span> *)<span class="hljs-title">data</span>;</span>
      channel_or_frequency(wevt);
      wevt = (struct iw_event *)(data + wevt-&gt;len);
      channel_or_frequency(wevt);
      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
<blockquote>
<p>这段程序因为使用了数学函数 pow()，所以用 gcc 编译时要带上参数 <strong>-lm</strong></p>
</blockquote>
</li>
<li><p>event_4 中的 essid 在前面已经基本说清楚了，下面这段代码会打印出 event_4 中的 essid</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;linux/wireless.h&gt;</span></span>

  <span class="hljs-keyword">uint8_t</span> data[] = {<span class="hljs-number">0x17</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x1B</span>,<span class="hljs-number">0x8B</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x07</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x01</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0x31</span>,<span class="hljs-number">0x35</span>,<span class="hljs-number">0x2D</span>,<span class="hljs-number">0x31</span>,<span class="hljs-number">0x31</span>,<span class="hljs-number">0x30</span>,<span class="hljs-number">0x31</span>};

  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_essid</span> {</span>
      <span class="hljs-keyword">uint16_t</span> len;
      <span class="hljs-keyword">uint16_t</span> flags;
      <span class="hljs-keyword">char</span> __attribute__((aligned(<span class="hljs-number">8</span>)))essid;
  };

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> *<span class="hljs-title">wevt</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">iw_event</span> *)<span class="hljs-title">data</span>;</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_essid</span> *<span class="hljs-title">essid_p</span>;</span>

      <span class="hljs-keyword">if</span> (wevt-&gt;cmd == SIOCGIWESSID){
          essid_p = (struct iw_essid *)&amp;wevt-&gt;u.data;
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Len: %d\tflags: %d\n"</span>, essid_p-&gt;len, essid_p-&gt;flags);
          <span class="hljs-keyword">char</span> *p = &amp;essid_p-&gt;essid;
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"ESSID: "</span>);
          <span class="hljs-keyword">int</span> i;
          <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; essid_p-&gt;len; ++i) {
              <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%c"</span>, p[i]);
          }
          <span class="hljs-built_in">puts</span>(<span class="hljs-string">""</span>);
      }
      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li><p>这里简要介绍一下 SSID 的概念，经常说的 ESSID 和 SSID 其实是一个东西；</p>
<ul>
<li>Basic Service Set 简称 BSS，指的是一个 WAP(Wireless Access Point) 所覆盖(服务)的区域，BSSID 指这个 BSS 的标识，为一个 6-bytes(48-bits)的 ID，实际就是这个 WAP 的 MAC 地址；</li>
<li>Extended Service Set 简称 ESS，指的是多个 WAP 共同覆盖(服务)的区域，ESSID 是这个 ESS 的标识，是一个 32个字符长度的字符串(ASCII码)，这些 WAP 各自拥有不同 BSSID 但使用相同的 ESSID；</li>
<li>ESSID 常常简称为 SSID。</li>
</ul>
</li>
<li><p>本文范例中仅处理四个 wifi 信号的属性：MAC、Channel、Frequency、ESSID，涉及三个指令代码：SIOCGIWAP、SIOCGIWFREQ、SIOCGIWESSID；</p>
</li>
<li>本文涉及的相关数据结构及调用方法至此已经介绍完毕。</li>
</ul>
<h2 id="heading-3-wifi">3 wifi信号扫描的步骤和方法</h2>
<h3 id="heading-31-wifi">3.1 wifi信号扫描的基本步骤</h3>
<ol>
<li>获取本机所有的网络接口</li>
<li>从所有的网络接口中找到无线网络接口</li>
<li>向无线网络接口发出wifi信号扫描指令</li>
<li>等待扫描结果的返回</li>
<li>分析返回结果，解析出所有的 event，并生成 event 链表 </li>
<li>遍历 event 链表并从中提取出 wifi 信号的属性</li>
<li>将 wifi 信号的属性显示在屏幕上</li>
</ol>
<h3 id="heading-32-wifi">3.2 wifi信号扫描的基本方法</h3>
<ul>
<li>本例中，大量的信息的长度和数量都是未知的：<ol>
<li>本机网络接口的数量</li>
<li>本机无线网络接口数量</li>
<li>wifi信号扫描后返回的结果的长度</li>
<li>返回结果中有多少个 event</li>
<li>扫描到了多少个wifi信号</li>
</ol>
</li>
<li>为此，本例中大量使用的单向链表结构，主要有下面四个单向链表：<ul>
<li>本机网络接口链表 - <code>struct ifaddrs</code><blockquote>
<p>调用 getifaddrs() 生成该链表</p>
</blockquote>
</li>
<li>本机无线网络接口链表 - <code>struct wifs_chain</code><blockquote>
<p>扫描本机网络接口链表，找出其中的无线网络接口，生成本机无线网络接口链表，当本机只有一片无线网卡时，通常这个链表中只有一项；如果没有找到无线网络接口，应该终止程序运行</p>
</blockquote>
</li>
<li>扫描返回结果的 event 链表 - <code>struct events_chain</code><blockquote>
<p>向无线网卡发出扫描指令 <code>SIOCSIWSCAN</code> 后，使用 <code>SIOCGIWSCAN</code> 指令获取扫描结果，分析扫描结果生成 <code>event</code> 链表</p>
</blockquote>
</li>
<li>无线 AP(Access Point) 链表 - <code>struct aps_chain</code><blockquote>
<p>遍历 event 链表，提取出各个 AP 的属性，生成无线 AP 链表</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h3 id="heading-33">3.3 如何获取本机的所有网络接口</h3>
<ul>
<li>使用 <code>getifaddrs()</code> 可以非常容易地获取全部网络接口</li>
<li>可以通过在线手册 <code>man getifaddrs</code> 了解详细的关于 <code>getifaddrs</code> 函数的信息；</li>
<li><code>getifaddrs</code> 函数会创建一个本地网络接口的结构链表，该结构链表定义在 <code>struct ifaddrs</code> 中(头文件 <code>ifaddrs.h</code>)；</li>
<li>关于 <code>ifaddrs</code> 结构有很多文章介绍，本文仅简单介绍一下与本文密切相关的内容，下面是 <code>struct ifaddrs</code> 的定义<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ifaddrs</span> {</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ifaddrs</span>  *<span class="hljs-title">ifa_next</span>;</span>          <span class="hljs-comment">/* Next item in list */</span>
      <span class="hljs-keyword">char</span>            *ifa_name;          <span class="hljs-comment">/* Name of interface */</span>
      <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span>     ifa_flags;         <span class="hljs-comment">/* Flags from SIOCGIFFLAGS */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span> *<span class="hljs-title">ifa_addr</span>;</span>          <span class="hljs-comment">/* Address of interface */</span>
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span> *<span class="hljs-title">ifa_netmask</span>;</span>       <span class="hljs-comment">/* Netmask of interface */</span>
      <span class="hljs-keyword">union</span> {
          <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span> *<span class="hljs-title">ifu_broadaddr</span>;</span> <span class="hljs-comment">/* Broadcast address of interface */</span>
          <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr</span> *<span class="hljs-title">ifu_dstaddr</span>;</span>   <span class="hljs-comment">/* Point-to-point destination address */</span>
      } ifa_ifu;
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span>              ifa_broadaddr ifa_ifu.ifu_broadaddr</span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span>              ifa_dstaddr   ifa_ifu.ifu_dstaddr</span>
      <span class="hljs-keyword">void</span>            *ifa_data;          <span class="hljs-comment">/* Address-specific data */</span>
  };
</code></pre>
</li>
<li><code>ifa_next</code> 是结构链表的后向指针，指向链表的下一项，当前项为最后一项时，该指针为 NULL；</li>
<li>本例中，我们的目标是找到这些网络接口中的无线网络接口，实际上我们仅需要 <code>ifa_name</code> 这个字段，也就是接口名称；</li>
<li><p>下面是获取全部网络接口的代码片段：</p>
<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ifaddrs</span> *<span class="hljs-title">ifs_start_pointer</span> = <span class="hljs-title">NULL</span>;</span>

  <span class="hljs-keyword">if</span> (getifaddrs(&amp;ifs_start_pointer) == <span class="hljs-number">-1</span>) {
      perror(<span class="hljs-string">"can't get local address\n"</span>);
      <span class="hljs-built_in">exit</span>(<span class="hljs-number">-1</span>);
  }
</code></pre>
<h3 id="heading-34">3.4 如何判断网络接口是无线网络接口</h3>
</li>
<li>头文件 <code>wireless.h</code> 中定义了一个 <code>SIOCGIWNAME</code> 指令，使用 <code>ioctl()</code> 调用该指令时只需设置接口名称，如果该接口是无线网络接口，<code>ioctl()</code> 执行成功并返回该接口使用的协议，否则，执行失败；</li>
<li>当我们生成了网络接口链表后，只需遍历该链表，并依此调用 <code>SIOCGIWNAME</code> 指令，便可找到所有的无线网络接口，并生成无线网络接口链表；</li>
<li><p>下面代码检查网络接口是否为无线接口，其中 <code>if_name</code> 为网络接口名称：</p>
<pre><code class="lang-C">  <span class="hljs-keyword">int</span> sock;
  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iwreq</span> <span class="hljs-title">wreq</span>;</span>

  <span class="hljs-built_in">memset</span>(&amp;wreq, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(wreq));
  <span class="hljs-built_in">strncpy</span>(wreq.ifr_name, if_name, IFNAMSIZ);      <span class="hljs-comment">// 接口名称</span>
  sock = socket(AF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);
  <span class="hljs-keyword">if</span> (ioctl(sock, SIOCGIWNAME, &amp;wreq) == <span class="hljs-number">0</span>) {
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"\nThe [%s] is a wireless interface. The protocol is %s\n"</span>, if_name, wreq.u.name);
  } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"\nThe [%s] is a wireless interface.\n"</span>, if_name);
  }
  close(sock);
</code></pre>
<h3 id="heading-35-wifi">3.5 wifi信号扫描有关的其它技术要点</h3>
</li>
<li>本文第 2 节已经对 wifi 信号的扫描原理做了详尽的描述，请参考 [<strong>2 使用ioctl进行wifi信号扫描的基本原理</strong>]；</li>
<li>[<strong>2.2 启动 wifi 信号扫描</strong>] - 详细描述了启动 wifi 信号扫描的方法；</li>
<li>[<strong>2.3 获取 wifi 信号的扫描结果</strong>] - 详细描述了获取 wifi 信号扫描结果的方法；</li>
<li>[<strong>2.4 扫描结果的数据格式</strong>] - 详细描述了如何从扫描结果中提取出 event，以及如何从 event 提取出 wifi 信号属性的方法；</li>
<li><p>下面这张图对 wifi 信号扫描的过程做了简单的回顾：</p>
<p>  <img src="https://blog.whowin.net/images/180022/SIOCGIWSCAN.png" alt="wifi signals scanning" /></p>
</li>
</ul>
<h3 id="heading-36-memory-alignment">3.6 关于内存对齐(memory alignment)</h3>
<ul>
<li>编写应用程序的程序员可能很少关心内存对齐问题，绝大多数情况下，内存对齐对应用程序的影响也不大，但内存对齐问题对本文有重要的影响；</li>
<li>我们用前面介绍过的 <code>struct iw_event</code> 来说明内存对齐对这个结构的影响：<pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> {</span>
      __u16        len;            <span class="hljs-comment">/* Real length of this stuff */</span>
      __u16        cmd;            <span class="hljs-comment">/* Wireless IOCTL */</span>
      <span class="hljs-keyword">union</span> iwreq_data    u;        <span class="hljs-comment">/* IOCTL fixed payload */</span>
  };
</code></pre>
</li>
<li>不使用 <code>sizeof()</code> 函数，你能够猜到系统会为这个结构分配多少内存吗？</li>
<li>首先，对于 union 而言，系统会选择其中最大的一个结构为其分配内存，<code>union iwreq_data</code> 中最大字段的长度是16个字节，所以系统会为其分配 16 字节内存，加上 len 和 cmd 两个字段共 4 个字节，似乎系统应该为这个结构分配 20 个字节；</li>
<li>但是，如果用 <code>sizeof(struct iw_event)</code> 计算这个结构的大小，给出的结果是 24，那么多出来的 4 个字节在哪里呢？</li>
<li>这 4 个字节用于内存对齐了，我的 ubuntu 系统是 64 位(数据总线是 64 位)的，内存当然是按照 8 字节对齐的(32 位系统是按 4 字节对齐)，len 和 cmd 两个字段共用前 8 个字节中的前 4 个字节，后 4 个字节空着用于内存对齐，然后从第 9 个字节开始为 <code>union iwreq_data</code> 分配 16个字节的内存，这样算下来刚好是 24 个字节；</li>
<li><p>下面这段程序可以很直观地看到内存分配的实际情况</p>
<pre><code class="lang-C">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;string.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;linux/wireless.h&gt;</span></span>

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_event</span> <span class="hljs-title">wevt</span>;</span>

      wevt.cmd = <span class="hljs-number">0x8b15</span>;
      wevt.len = <span class="hljs-number">20</span>;
      <span class="hljs-built_in">strcpy</span>(wevt.u.name, <span class="hljs-string">"struct iw_event"</span>);

      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"sizeof(struct iw_event): %ld\n"</span>, <span class="hljs-keyword">sizeof</span>(struct iw_event));
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"pointer of len: %p\n"</span>, &amp;wevt.len);
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"pointer of cmd: %p\n"</span>, &amp;wevt.cmd);
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"pointer of u.name: %p\n"</span>, &amp;wevt.u);

      <span class="hljs-keyword">uint8_t</span> *p = (<span class="hljs-keyword">uint8_t</span> *)&amp;wevt;
      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-keyword">sizeof</span>(struct iw_event); ++i) {
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%02x "</span>, p[i]);
      }
      <span class="hljs-built_in">puts</span>(<span class="hljs-string">""</span>);

      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
</li>
<li><p>这段程序的运行截图</p>
<p>  <img src="https://blog.whowin.net/images/180022/screenshot-of-iwevent-test.png" alt="Screenshot of iwevent test" /></p>
<ul>
<li>首先可以看到系统确实为 <code>struct iw_event</code> 分配了 24 字节的内存，而不是 20 字节；</li>
<li>字段 len 的地址是 <code>~f160</code>，字段 cmd 的地址是 <code>~f162</code>，因为 len 的数据类型是 __u16，占用 2 个字节；</li>
<li>cmd 字段的类型也是 __u16，按理也应该占用 2 个字节，但字段 u 的地址却是 <code>~f168</code>，而不是 <code>~f164</code>，这其中多出的 4 个字节就是为了内存对齐；</li>
<li>最后我们打印出了这个结构的所有数据，红线所示的 4 个字节就是为了内存对齐而填充的；</li>
</ul>
</li>
<li><p>那么，为什么不在 cmd 字段后面为 <code>union iwreq_data</code> 分配内存呢？这是因为 64 位的系统每次从内存读 8 个字节，如果按照 8 字节对齐分配内存，读取 <code>union iwreq_data</code> 需要读两次，否则就需要读三次，速度降低 50%，当然也可以强制不按 8 字节对齐，节省了内存但会损失性能；</p>
</li>
<li>系统为这个结构按内存对齐规则分配内存后，是否还能和实际的数据流对应呢？我们看看 event_1 的数据：<pre><code class="lang-plain">  18 00 15 8B 00 00 00 00 01 00 DC FE 18 68 73 80 
  00 00 00 00 00 00 00 00
</code></pre>
</li>
<li>0x0018 对应 len 字段，0x8B15 对应 cmd 字段，后面的 4 个字节刚好和 <code>struct iw_event</code> 中的 4 个用于对齐的字节一致，然后应该是 <code>struct sockaddr</code>，0x0001 是 <code>struct sockaddr</code> 中的 <code>sa_family</code>，再后面的 14 个字节是 <code>struct sockaddr</code> 中的 <code>sa_data</code> 字段，对应的非常好；</li>
<li>为了提取出 AP 的 ESSID，我们在前面自定义了一个结构 <code>struct iw_essid</code><pre><code class="lang-C">  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">iw_essid</span> {</span>
      <span class="hljs-keyword">uint16_t</span> len;
      <span class="hljs-keyword">uint16_t</span> flags;
      <span class="hljs-keyword">char</span> __attribute((aligned(<span class="hljs-number">8</span>)))essid;
  }
</code></pre>
<ul>
<li>这里用了 <code>__attribute((aligned(8)))</code>，其作用也是为了 essid 字段能够按照 8 字节对齐的方式分配内存，因为 essid 字段是 char 型，仅占 1 个字节，与 len 和 flags 合起来也不超过 8 个字节，所以会紧跟着 flags 分配内存，这样会和实际的数据流不一致，所以这里必须要使用 <code>__attribute((aligned(8)))</code>；</li>
</ul>
</li>
<li>如果希望更多地了解有关内存对齐的相关信息，请自行搜索相关文章。</li>
</ul>
<h2 id="heading-4-wifi">4 完整的 wifi 信号扫描程序</h2>
<ul>
<li>完整的源代码，文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180022/wifi-scanner.c">wifi-scanner.c</a>(<strong>点击文件名下载源程序</strong>)，请务必使用 UTF-8 字符集，否则源程序中的中文注释为乱码；</li>
<li>编译，因为在计算工作频率时使用了函数 pow()，所以编译时要加上 <code>-lm</code> 选项，意即连接数学函数库；<pre><code class="lang-bash">  gcc -Wall wifi-scanner.c wifi-scanner -lm
</code></pre>
</li>
<li>运行，本程序的运行需要 root 权限，这主要是因为操作 wifi 需要 root 权限；<pre><code class="lang-bash">  sudo ./wifi-scanner
</code></pre>
</li>
<li><p>运行截图</p>
<p>  <img src="https://blog.whowin.net/images/180022/screenshot-of-wifi-scanner.png" alt="Screenshot of wifi-scanner" /></p>
</li>
</ul>
<h2 id="heading-5">5 后记</h2>
<ul>
<li>本文仅处理了很少的几个 wifi 信号的属性，wifi 信号的属性还有很多，比如：信号强度、加密方式、安全协议等，读者可以试着扩展该程序，获取更多的 wifi 信号属性；</li>
<li>本文范例中的很多地方都是有修改空间的，比如在获取无线网络接口上，我们可以直接从 proc 文件系统上获得，读取 <code>/proc/net/wireless</code> 文件即可，proc 文件系统也是 IPC 的一种方式；</li>
<li>本文范例使用了 <strong>Wireless Extensions</strong> 的 API，非常遗憾的是，几乎找不到这方面的完整资料，学习其调用方法的唯一办法是认真阅读头文件 <code>wireless.h</code> 和学习一些使用 WE 的源代码，下面是有关的两个链接：<ul>
<li>相对完整的 <strong>Wireless Extensions</strong> 资料：<a target="_blank" href="https://www.hpl.hp.com/personal/Jean_Tourrilhes/Linux/Tools.html">Wireless Tools for Linux</a></li>
<li>官方发布的使用 WE 的无线工具源代码：<a target="_blank" href="https://github.com/HewlettPackard/wireless-tools">Wireless Tools for Linux</a></li>
</ul>
</li>
<li>扫描 wifi 信号除了本文介绍的 ioctl() 方法外还有一些其它方法，希望今后有机会介绍其它方法；</li>
<li>对 wifi 信号的操作，扫描仅仅是一个基本的操作，还有其它操作，比如：连接 wifi、共享 wifi、配置wifi等；</li>
<li>实际上，对 wifi 进行编程的文章和资料并不多，希望这篇文章能给你带来一些启发和帮助。</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[用c语言实现的一个dns客户端]]></title><description><![CDATA[DNS可以帮助我们把域名映射到一个IP地址上，或者查询一个IP地址下有那些域名，使用域名访问一个网站或者服务器是一件很平常的事情，很少有人关心域名变成IP地址的实际过程，本文将使用C语言实现一个基本的DNS解析器，通过与DNS服务器的通信完成将一个域名转换成IP地址的过程，本文将提供完整的源程序；阅读本文需要有一定的网络编程基础，熟悉基本的socket编程并对DNS有一些了解，本文对网络编程的初学者难度较大。

1. 目标

本文要实现一个DNS的客户端解析器(DNS resolver)，意即通...]]></description><link>https://whowin.cn/180019-dns-client-in-c</link><guid isPermaLink="true">https://whowin.cn/180019-dns-client-in-c</guid><category><![CDATA[dns resolver]]></category><category><![CDATA[networking]]></category><category><![CDATA[C]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Tue, 28 Mar 2023 06:22:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680243643203/ff634542-0bc3-484b-9fb2-647e74a98277.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>DNS可以帮助我们把域名映射到一个IP地址上，或者查询一个IP地址下有那些域名，使用域名访问一个网站或者服务器是一件很平常的事情，很少有人关心域名变成IP地址的实际过程，本文将使用C语言实现一个基本的DNS解析器，通过与DNS服务器的通信完成将一个域名转换成IP地址的过程，本文将提供完整的源程序；阅读本文需要有一定的网络编程基础，熟悉基本的socket编程并对DNS有一些了解，本文对网络编程的初学者难度较大。</p>
</blockquote>
<h2 id="heading-1">1. 目标</h2>
<ul>
<li>本文要实现一个DNS的客户端解析器(DNS resolver)，意即通过直接与DNS服务器通讯，将一个域名转换成其所对应的IP地址；</li>
<li>对DNS客户端解析器的要求：<ol>
<li>命令行接受用户输入的域名</li>
<li>向DNS服务器发出查询请求，并将查询结果显示在屏幕上</li>
<li>仅查询域名的A记录(QTYPE=HOST，QCLASS=IN)，后面会讨论相关细节</li>
<li>如果查询结果有多条记录，要求显示所有查询结果</li>
<li>如果查询的域名为别名(Alias)，要求显示其实际域名(Canonical Name)</li>
<li>仅查询IPv4地址。</li>
</ol>
</li>
<li>在C语言编程中，当需要将一个域名转换成IP地址时，通常是使用getaddrbyname()或者getaddrinfo()函数，这两个函数会使用系统设定的DNS服务器，本文实现的DNS客户端将使用自己定义的DNS服务器；</li>
<li>一个DNS的客户端无非就是按照一定的格式向DNS服务器发送一个报文，然后接收来自DNS服务器的响应，并解析收到的信息，从而获得结果。</li>
</ul>
<h2 id="heading-2-dns">2. DNS协议</h2>
<ul>
<li>要编写一个DNS客户端程序，了解DNS协议是必须的，本节将仅对我们有用的有关DNS协议中的内容加以说明；看协议是枯燥的，也可以先不看，到后面需要时再回来查阅；</li>
<li><p>DNS协议的主要内容包含在下面两个文件中</p>
<ul>
<li><a target="_blank" href="https://www.ietf.org/rfc/rfc1034.txt">rfc 1034: Domain Concepts and Facilities</a></li>
<li><a target="_blank" href="https://www.ietf.org/rfc/rfc1035.txt">rfc 1035: Domain Implementation and Specification</a></li>
</ul>
</li>
<li><p>rfc 1035中对一些参数的最大值做了限制</p>
<ul>
<li>labels - 最多63个字符</li>
<li>names - 最多255字符</li>
<li>TTL - 32bit有符号数字，只能是正数</li>
<li>UDP messages - 最多512个字符</li>
</ul>
</li>
<li><p>这些限制告诉我们：</p>
<ol>
<li>一个域名最长为255个字符，以'.'分开的各个部分，每部分最多63个字符</li>
<li>使用UDP与DNS通信时，每个报文的长度不能超过512个字节</li>
<li>TTL(Time To Live)，指查询到的一个DNS信息的生命周期，过了这个时间，这条信息即为作废，应该重新查询；</li>
</ol>
</li>
<li><p>在DNS的各种文章中，会经常看到<strong>RR</strong>的表述，这个是Resource Record的缩写，从DNS服务器返回的各种信息，都会存储在RR中；</p>
</li>
<li>RR其实就是一个符合某种格式的数据结构，RR是DNS协议中非常重要的一个结构，rfc 1035(3.2)中，对<strong>RR</strong>做了定义：<pre><code class="lang-plaintext">                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                                               |
/                                               /
/                      NAME                     /
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      TYPE                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     CLASS                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      TTL                      |
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                   RDLENGTH                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/                     RDATA                     /
/                                               /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
</code></pre>
</li>
<li><p>其中</p>
<ul>
<li>NAME - 拥有者名称，意即该资源记录RR所属的节点名称；实际上就是个域名，这个字段的长度是可变的，下面会详细说明其记录方式；</li>
<li><p>TYPE - RR的类别，占2个字节；常用的TYPE如下，更多信息请查阅<a target="_blank" href="https://www.ietf.org/rfc/rfc1035.txt">rfc 1035</a>第3.2.2；本文中，会用到TYPE=A和TYPE=CNAME两种；</p>
<pre><code>TYPE  VALUE   MEANING
-----------------------------
A       <span class="hljs-number">1</span>     主机地址
CNAME   <span class="hljs-number">5</span>     别名的正式名称
MX      <span class="hljs-number">15</span>    邮件交换
TXT     <span class="hljs-number">16</span>    文本信息
</code></pre></li>
<li><p>CLASS - RR的适用的网络类别；CLASS的值常用的只有一个，即CLASS=IN，其值为1，表示互联网(Internet)</p>
</li>
<li>TTL - 32bit的正整数表示该RR可以被缓存的时长，以秒为单位；该值为0时表示该RR只能用于当前事务，不能被缓存；</li>
<li>RDLENGTH - 16bit无符号整数；该值表示RDATA字段的长度(字节数)；</li>
<li>RDATA - 用于描述资源的可变长度字串；其格式取决于TYPE和CLASS字段的值，比如当TYPE=A时，RDATA中是一个32bit的IP地址。</li>
</ul>
</li>
<li><p>在<a target="_blank" href="https://www.ietf.org/rfc/rfc1035.txt">rfc 1035</a>的第4章定义了DNS协议的消息格式，向DNS服务器发送的查询报文以及DNS服务器的返回报文都符合这个格式；</p>
<pre><code class="lang-plaintext">+---------------------+
|        Header       |
+---------------------+
|       Question      | the question for the name server
+---------------------+
|        Answer       | RRs answering the question
+---------------------+
|      Authority      | RRs pointing toward an authority
+---------------------+
|      Additional     | RRs holding additional information
+---------------------+
</code></pre>
</li>
<li>在这个报文格式中，不管是查询请求报文还是应答报文，都会有一个报头(Header)、在查询请求报文中，显然不需要有Answer、Authority和Additional三部分；</li>
<li>Question部分有自己的格式，Answer、Authority和Additional这三部分的格式是一样的，下面将就这三种格式(Header、Question、Answer)做个简要说明；</li>
<li><p>如果觉着这部分枯燥也可以先跳过，等看不懂程序时才回来查阅；</p>
</li>
<li><p><strong>Header的格式</strong></p>
<ul>
<li>Header部分占了12个字节<pre><code class="lang-plaintext">                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
</code></pre>
</li>
<li><p>其中</p>
<ul>
<li>ID - 随机标识，16bit长，随便填一个数即可；</li>
<li>QR - Query Response；0表示该报文为查询报文，1表示该报文为应答报文；</li>
<li>Opcode - Operation Code；表示报文的查询类型，该值由查询的发起方设置，并复制到应答报文中；0表示一个标准查询(QUERY)，1表示一个反向查询(IQUERY)，2表示查询服务器状态(STATUS)，3-15备用；</li>
<li>AA - Authoritative Answer；权威答案，该位仅在响应报文中有效，该位为1表示当前名称服务器对所查询的域名具有权威性；</li>
<li>TC - TrunCation；该位为1表示该报文不完整；由于该报文的长度过长，超过了传输通道允许的最大长度，该报文被截断；</li>
<li>RD - Recursion Desired；该位为1表示要求DNS服务器做递归查询；如果在查询报文中设置了RD，在应答报文中将复制该位；</li>
<li>RA - Recursion Available；递归可用，在应答报文中将该位置1表示名称服务器支持递归查询，将该位清0表示不支持递归查询(因为协议规定递归查询功能是可选的)；</li>
<li>Z  - Reserved；备用</li>
<li><p>RCODE - Response code；响应码，在相应报文中设置，按照 <a target="_blank" href="https://www.ietf.org/rfc/rfc1035.txt">rfc 1035</a> 的定义，其值的含义如下：</p>
<p>|值|含义|
|:--:|:----|
|0|没有错误|
|1|格式错误 - 名称服务器无法解释查询报文|
|2|服务器故障 - 由于服务器故障，无法处理此查询|
|3|名称错误 - 仅对来自权威名称服务器的响应有意义，表示查询的域名不存在|
|4|功能未实现 - 名称服务器不支持所请求的查询类型|
|5|拒绝 - 名称服务器出于政策原因拒绝执行指定的操作|
|6-15|备用|</p>
</li>
<li><p>QDCOUNT - 无符号16位整数，表示该报文中 <strong>QUESTION部分</strong> 有多少条查询请求；</p>
</li>
<li>ANCOUNT - 无符号16位整数，表示该报文中 <strong>ANSWER部分</strong> 有多少条RR(Resource Record)；</li>
<li>NSCOUNT - 无符号16位整数，表示该报文中 <strong>Authority部分</strong> 有多少条有权威性的RR(Resource Record)</li>
<li>ARCOUNT - 无符号16位整数，表示该报文中 <strong>Additional部分</strong> 有多少条附加的RR(Resource Record)</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Question的格式</strong></p>
<ul>
<li>Question部分是可变长度的<pre><code class="lang-plaintext">                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                                               |
/                     QNAME                     /
/                                               /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QTYPE                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QCLASS                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
</code></pre>
</li>
<li>其中QNAME是可变长度的，QTYPE和QCLASS各占2个字节，16bits</li>
<li>QNAME - 域名；以标签(label)方式表示的域名；每个标签的第一个字节表示这个标签的长度，后面跟着与该长度相同的字符，多个标签组成一个域名，标签的最后填充1个字节的0，表示标签结束；举例来说，www.baidu.com将用如下方式表达(以16进制表示)：<pre><code class="lang-plaintext">length------length------------length------end 
03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00
   w  w  w     b  a  i  d  u     c  o  m
</code></pre>
</li>
<li>要注意的是，这个字段的长度(字节数)可能是奇数，不需要填充以保证4字节或2字节对齐；</li>
<li>QTYPE - 2字节，表示查询类型；在前面介绍RR时曾介绍了RR中的TYPE字段，所有TYPE的值这里都使用，本文用到的有：QTYPE=1(A记录)，QTYPE=5(CNAME记录)</li>
<li>QCLASS - 2字节，表示网络类别，常用的值只有1个，即CALSS=1(Internet - 表示互联网)</li>
</ul>
</li>
<li><p><strong>Answer的格式</strong></p>
<ul>
<li>Answer、Authority、Additional的格式是一样的，就是前面介绍的RR的格式，只是这里可能放着多个RR，其具体数量由Header中的ANCOUNT、AUCOUNT和ARCOUNT确定；</li>
<li>我们把RR的的格式在这里再展示一下：<pre><code class="lang-plaintext">                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                                               |
/                                               /
/                      NAME                     /
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      TYPE                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     CLASS                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      TTL                      |
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                   RDLENGTH                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/                     RDATA                     /
/                                               /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
</code></pre>
</li>
<li>要说明的是，为了减小报文的长度，其NAME部分常常采用一种压缩方案(Compression Scheme)；因为在服务器发回的应答报文中，会包含查询请求报文中的QUESTION字段，这个字段中已经存放了域名，如果在ANSWER中再存放一次域名，显然是重复的，所以通常采用压缩方案；</li>
<li>比如我们查询 baidu.com 的A记录，在发出DNS请求的报文中，会包含 baidu.com 这个域名，在应答报文中，会把请求报文中的 question 部分复制过来，那么在 answer 部分的 RR 中的 baidu.com 与 question 中的 baidu.com 就是重复的，这时候，在 anwser 部分的 baidu.com 就会采用压缩方案存储；</li>
<li>采用标签方式存储域名时，第一个字符用于表示这个标签的长度，DNS协议规定，当这个长度字节的最高两位为0时，后面的6位表示这个标签的长度，当这个字节的最高两位全为1时，这个字节的后面6位连同下一个字节一起表示一个从报文首字节开始的偏移，指向一个域名；</li>
<li>前面说过，一个域名的总长度最大为255个字符，以'.'分开的各个部分，每部分的最大长度为63个字符，<strong>为什么会限制为63个字符呢</strong>？因为在DNS协议里规定只有6 bit用于表示域名每部分的长度，所以每部分的长度最大只能是 2<sup>6</sup> - 1 = 63</li>
<li>我们用一个实际的例子来说明，这个例子是查询域名 baidu.com 的 A 记录时，实际返回的数据：<pre><code><span class="hljs-number">0000</span>:   <span class="hljs-number">0xf1</span>  <span class="hljs-number">0x01</span>  <span class="hljs-number">0x81</span>  <span class="hljs-number">0x80</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x01</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x02</span>  
<span class="hljs-number">0008</span>:   <span class="hljs-number">0x00</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x05</span>  <span class="hljs-number">0x62</span>  <span class="hljs-number">0x61</span>  <span class="hljs-number">0x69</span>  
<span class="hljs-number">0010</span>:   <span class="hljs-number">0x64</span>  <span class="hljs-number">0x75</span>  <span class="hljs-number">0x03</span>  <span class="hljs-number">0x63</span>  <span class="hljs-number">0x6f</span>  <span class="hljs-number">0x6d</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x00</span>  
<span class="hljs-number">0018</span>:   <span class="hljs-number">0x01</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x01</span>  <span class="hljs-number">0xc0</span>  <span class="hljs-number">0x0c</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x01</span>  <span class="hljs-number">0x00</span>  
<span class="hljs-number">0020</span>:   <span class="hljs-number">0x01</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x01</span>  <span class="hljs-number">0x72</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x04</span>  <span class="hljs-number">0x6e</span>  
<span class="hljs-number">0028</span>:   <span class="hljs-number">0xf2</span>  <span class="hljs-number">0x44</span>  <span class="hljs-number">0x42</span>  <span class="hljs-number">0xc0</span>  <span class="hljs-number">0x0c</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x01</span>  <span class="hljs-number">0x00</span>  
<span class="hljs-number">0030</span>:   <span class="hljs-number">0x01</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x01</span>  <span class="hljs-number">0x72</span>  <span class="hljs-number">0x00</span>  <span class="hljs-number">0x04</span>  <span class="hljs-number">0x27</span>  
<span class="hljs-number">0038</span>:   <span class="hljs-number">0x9c</span>  <span class="hljs-number">0x42</span>  <span class="hljs-number">0x0a</span>
</code></pre></li>
<li>最左边的一列是在内存中的偏移地址，在 question 部分的域名 baidu.com 出现在偏移地址为 0x000c 的位置，我们把它单独拿出来看：<pre><code><span class="hljs-number">000</span>c:   <span class="hljs-number">0x05</span>  <span class="hljs-number">0x62</span>  <span class="hljs-number">0x61</span>  <span class="hljs-number">0x69</span>  <span class="hljs-number">0x64</span>  <span class="hljs-number">0x75</span>  <span class="hljs-number">0x03</span>  <span class="hljs-number">0x63</span>  <span class="hljs-number">0x6f</span>  <span class="hljs-number">0x6d</span>  <span class="hljs-number">0x00</span>
</code></pre></li>
<li>这样可能还不直观，我们换一种更加直观的方式：<pre><code><span class="hljs-number">000</span>c:   <span class="hljs-number">0x05</span>  <span class="hljs-string">'b'</span>  <span class="hljs-string">'a'</span>  <span class="hljs-string">'i'</span>  <span class="hljs-string">'d'</span>  <span class="hljs-string">'u'</span>  <span class="hljs-number">0x03</span>  <span class="hljs-string">'c'</span>  <span class="hljs-string">'o'</span>  <span class="hljs-string">'m'</span>  <span class="hljs-number">0x00</span>
</code></pre></li>
<li>这是一种标准的标签方式记录的域名；</li>
<li>这个回应报文中的 answer 有两个，第一个从偏移地址 0x001b 开始，answer 的开始是 NAME 字段，采用了压缩方案存储：<pre><code><span class="hljs-number">001</span>b:   <span class="hljs-number">0xc0</span>  <span class="hljs-number">0x0c</span>
</code></pre></li>
<li>按照标签方式，第一个字节应该表示这个标签的长度，但是这个字节 0xc0 的最高两位全为1，所以这里采用的是压缩方案，这个字节的后6位与下个字节一起组成一个偏移，下一个字节是 0x0c，所以偏移应该是：((0xc0 &amp; 0x3f) &lt;&lt; 8) + 0x0c = 0x000c，偏移地址 0x000c 处显示的是什么呢？正是 question 部分用标签方式存储的域名 baidu.com</li>
<li>这个例子只是为了直观的说明阳索方案是如何表达一个域名的，实际应用中可能比这个复杂一些，比如可能前面使用标签方式，但最后是一个压缩方案的指针等。</li>
</ul>
</li>
</ul>
<h2 id="heading-3-dns">3. 实现一个DNS客户端</h2>
<ul>
<li>我们要实现的这个DNS客户端，仅实现域名 A 记录和 CNAME 记录的查询，这也是最常见的两种DNS记录；</li>
<li>通常 DNS 客户端使用 UDP 实现，DNS 协议规定的端口号是 53；</li>
<li><p>实现一个DNS客户端的步骤：</p>
<ol>
<li>建立一个UDP socket</li>
<li>设置socket接收超时，避免在接收应答消息时阻塞<pre><code class="lang-C"> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">timeout</span>;</span>
 timeout.tv_sec  = <span class="hljs-number">5</span>;
 timeout.tv_usec = <span class="hljs-number">0</span>;
 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &amp;timeout, <span class="hljs-keyword">sizeof</span>(timeout));
</code></pre>
</li>
<li><p><strong>构建DNS request报文</strong></p>
<ul>
<li>DNS request报文由(报头 + 域名 + QTYPE + QCLASS)组成，报头的长度为12字节，QTYPE和QCLASS的长度均为2字节，域名为不定长度</li>
<li>一个域名在 request 中占用的字节数为(域名字符串长度 + 2)；域名中每个'.'的位置要换成标签的长度，域名的第一个标签需要增加1字节的长度字节，域名的结束需要填充一个0；</li>
<li>通过以上计算可以得出一个request的长度，合理地分配内存空间<pre><code class="lang-C"><span class="hljs-keyword">int</span> dns_name_len = <span class="hljs-built_in">strlen</span>(domain_name) + <span class="hljs-number">2</span>;
<span class="hljs-keyword">int</span> dns_request_len = <span class="hljs-number">12</span> + dns_name_len + <span class="hljs-number">2</span> + <span class="hljs-number">2</span>;
<span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> *dns_request = <span class="hljs-built_in">malloc</span>(dns_request_len);
<span class="hljs-built_in">memset</span>(dns_request, <span class="hljs-number">0</span>, dns_request_len);
</code></pre>
</li>
<li>将一个域名用标签方式存储，这是构建request最复杂的部分，<code>struct dnshdr</code> 定义了DNS报头结构，详见源程序<pre><code class="lang-C"><span class="hljs-keyword">uint8_t</span> *dns_name = dns_request + <span class="hljs-keyword">sizeof</span>(struct dnshdr);
<span class="hljs-keyword">char</span> *p = (<span class="hljs-keyword">char</span> *)dns_name;
<span class="hljs-built_in">strcpy</span>(p + <span class="hljs-number">1</span>, domain_name);
<span class="hljs-keyword">char</span> *pdot;
<span class="hljs-keyword">while</span> ((pdot = index(p + <span class="hljs-number">1</span>, <span class="hljs-string">'.'</span>)) != <span class="hljs-literal">NULL</span>) {
    *pdot = <span class="hljs-number">0</span>;
    *p = <span class="hljs-built_in">strlen</span>(p + <span class="hljs-number">1</span>);
    p = pdot;
}
*p = <span class="hljs-built_in">strlen</span>(p + <span class="hljs-number">1</span>);
</code></pre>
</li>
<li><p>填写request中的QTYPE和QCLASS</p>
<pre><code class="lang-C"><span class="hljs-keyword">uint16_t</span> *qtype = (<span class="hljs-keyword">uint16_t</span> *)(dns_name + dns_name_len);
<span class="hljs-keyword">uint16_t</span> *qclass = (<span class="hljs-keyword">uint16_t</span> *)(dns_name + dns_name_len + <span class="hljs-number">2</span>);

*qtype = htons(<span class="hljs-number">1</span>);
*qclass = htons(<span class="hljs-number">1</span>);
</code></pre>
</li>
<li><p>填写DNS报头，ID填任何数字都可以，flags中RD=1表示要求服务器进行递归查询，其他字段均为0，qu_count=1表示有一个查询，要注意的是，request中的存储都必须是网络字节序，所以要使用htons()转换一下</p>
<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">dnshdr</span> {</span>
    <span class="hljs-keyword">uint16_t</span> id;
    <span class="hljs-keyword">uint16_t</span> flags;
    <span class="hljs-keyword">uint16_t</span> qu_count;      <span class="hljs-comment">// Number of questions</span>
    <span class="hljs-keyword">uint16_t</span> an_count;      <span class="hljs-comment">// Number of answer rr</span>
    <span class="hljs-keyword">uint16_t</span> au_count;      <span class="hljs-comment">// Number of authority rr</span>
    <span class="hljs-keyword">uint16_t</span> ad_count;      <span class="hljs-comment">// Number of additional rr</span>
};

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">dnshdr</span> *<span class="hljs-title">dns_header</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">dnshdr</span> *)<span class="hljs-title">dns_request</span>;</span>
dns_header-&gt;id = htons(<span class="hljs-number">0x1234</span>);
dns_header-&gt;flags = htons(<span class="hljs-number">0x0100</span>);
dns_header-&gt;qu_count = htons(<span class="hljs-number">1</span>);
dns_header-&gt;an_count = <span class="hljs-number">0</span>;
dns_header-&gt;au_count = <span class="hljs-number">0</span>;
dns_header-&gt;ad_count = <span class="hljs-number">0</span>;
</code></pre>
</li>
</ul>
</li>
<li><p><strong>向DNS服务器发送DNS request报文</strong></p>
<pre><code class="lang-C"> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">dns_addr</span>;</span>
 <span class="hljs-built_in">memset</span>(&amp;dns_addr, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(struct sockaddr_in));
 dns_addr.sin_family = AF_INET;
 dns_addr.sin_port = htons(<span class="hljs-number">53</span>);
 dns_addr.sin_addr.s_addr = inet_addr(<span class="hljs-string">"114.114.114.114"</span>);

 sendto(sockfd, dns_request, dns_request_len, <span class="hljs-number">0</span>, (struct sockaddr *)&amp;dns_addr, <span class="hljs-keyword">sizeof</span>(struct sockaddr));
</code></pre>
</li>
<li><p><strong>接收来自DNS服务器的应答报文</strong></p>
<pre><code class="lang-C"> <span class="hljs-keyword">uint8_t</span> *buf = <span class="hljs-built_in">malloc</span>(<span class="hljs-number">512</span>);
 <span class="hljs-built_in">memset</span>(buf, <span class="hljs-number">0</span>, <span class="hljs-number">512</span>);
 <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">addr</span>;</span>
 <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">int</span> addr_len = <span class="hljs-keyword">sizeof</span>(struct sockaddr_in);
 recvfrom(sockfd, buf, <span class="hljs-number">512</span>, <span class="hljs-number">0</span>, (struct sockaddr *)&amp;addr, &amp;addr_len);
</code></pre>
<ul>
<li>DNS协议规定，DNS数据包的长度不超过512字节，所以这里仅给接收缓冲区分配512个字节</li>
</ul>
</li>
<li><p><strong>解析DNS服务器的应答报文</strong></p>
<ul>
<li>从DNS报头获取answer部分的RR数量<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">dnshdr</span> *<span class="hljs-title">dns_hdr</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">dnshdr</span> *)<span class="hljs-title">buf</span>;</span>
<span class="hljs-keyword">uint16_t</span> ancount = ntohs(dns_hdr-&gt;an_count);
</code></pre>
</li>
<li>找到answer部分的起始位置，在应答报文中，要跳过question部分才是answer部分，但question部分不是固定长度的，所以要费点周折；<pre><code class="lang-C"><span class="hljs-keyword">uint8_t</span> *p_question = buf + <span class="hljs-keyword">sizeof</span>(struct dnshdr);
<span class="hljs-keyword">uint8_t</span> *p = p_question;
<span class="hljs-keyword">while</span> (*p &gt; <span class="hljs-number">0</span>) {
    p += *p;
    p++;
}
p++;
<span class="hljs-keyword">uint8_t</span> *p_answer = p + <span class="hljs-number">2</span> + <span class="hljs-number">2</span>;
</code></pre>
</li>
<li>p_question指向question部分的开头，经过一个循环找到QTYPE字段的位置，再加上QTYPE和QCLASS的长度就找到了answer的起始位置；</li>
<li>answer中的内容其实就是一个一个的RR，前面我们已经从包头中获得了RR的数量，这里循环就好了</li>
<li>前面介绍过RR的结构，一个RR是由(NAME + TYPE + CLASS + TTL + RDLENGTH + RDATA)组成，其中CLASS永远为IN，TTL是设置DNS cache时用的，这两项我们可以不用管；</li>
<li>TYPE决定着RDATA的格式和内容，我们只解析A记录和CNAME记录，所以如果TYPE为其他类型，我们可以放弃</li>
<li>如果是A记录(TYPE=1)，则RDATA中是一个32位的IP地址，占4个字节，NAME中存放着其主域名(不是别名)；</li>
<li>如果是CNAME记录(TYPE=5)，则NAME中存放的是一个别名，RDATA中存放着这个别名的主域名，此时RDATA中的数据采用标签方式或压缩方案存储域名；</li>
<li><p>在源程序中有一个函数parse_name()专门用于将RR中的name或者RDATA中的name转换成我们可以读懂的域名格式</p>
<pre><code class="lang-C"><span class="hljs-keyword">char</span> *owner_name;
<span class="hljs-keyword">char</span> *cname;
<span class="hljs-keyword">int</span> name_len = <span class="hljs-number">0</span>;
<span class="hljs-keyword">uint16_t</span> ans_type;
<span class="hljs-keyword">uint16_t</span> rdlength;
<span class="hljs-keyword">uint8_t</span> *p_rdata;

owner_name = <span class="hljs-built_in">malloc</span>(<span class="hljs-number">256</span>);
<span class="hljs-keyword">int</span> i;
<span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; ancount; ++i) {
    <span class="hljs-built_in">memset</span>(owner_name, <span class="hljs-number">0</span>, <span class="hljs-number">256</span>);
    name_len = parse_name(buf, p_answer, owner_name, <span class="hljs-number">256</span>);
    ans_type = ntohs(*(<span class="hljs-keyword">int16_t</span> *)(p_answer + name_len));
    rdlength = ntohs(*(<span class="hljs-keyword">int16_t</span> *)(p_answer + name_len + <span class="hljs-number">2</span> + <span class="hljs-number">2</span> + <span class="hljs-number">4</span>));

    p_rdata = p_answer + name_len + <span class="hljs-number">2</span> + <span class="hljs-number">2</span> + <span class="hljs-number">4</span> + <span class="hljs-number">2</span>;
    <span class="hljs-keyword">if</span> (ans_type == TYPE_HOST) {
        <span class="hljs-comment">// host ip in rdata. ip points to rdata.</span>
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"The owner name: %s\n"</span>, owner_name);
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"ip: %d.%d.%d.%d\n"</span>, p_rdata[<span class="hljs-number">0</span>], p_rdata[<span class="hljs-number">1</span>], p_rdata[<span class="hljs-number">2</span>], p_rdata[<span class="hljs-number">3</span>]);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (ans_type == TYPE_CNAME) {
        <span class="hljs-comment">// canonical name in rdata</span>
        cname = <span class="hljs-built_in">malloc</span>(<span class="hljs-number">256</span>);
        <span class="hljs-built_in">memset</span>(cname, <span class="hljs-number">0</span>, <span class="hljs-number">256</span>);
        parse_name(buf, p_rdata, cname, <span class="hljs-number">256</span>);
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"The alias name: %s\n"</span>, owner_name);
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"The canonical name: %s\n"</span>, cname);
        <span class="hljs-built_in">free</span>(cname);
    }
    <span class="hljs-comment">// point to next answer</span>
    p_answer = p_answer + name_len + QTYPE_LEN + QCLASS_LEN + TTL_LEN + RDLENGTH_LEN + rdlength;
}
<span class="hljs-built_in">free</span>(owner_name);
</code></pre>
</li>
<li>owner_name是RR中NAME字段中的域名，cname是当TYPE=CNAME时，RDATA中的域名</li>
<li>parse_name()是解析name的函数，其返回值为该name在报文中占用的字节数，p_answer是指向answer部分开头的指针，所以p_answer加上parse_name()的返回值就是RR中TYPE字段的位置，再加上TYPE、CLASS和TTL的长度，就是RDLENGTH字段的位置，再加上RDLENGTH的长度，就是RDATA的位置，p_data指针就是这样得到的；</li>
</ul>
</li>
</ol>
</li>
<li><p>完整源程序的文件名为：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180019/dns-client.c">dns-client.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li>编译：<code>`gcc -Wall dns-client.c -o dns-client</code></li>
<li><p>运行：<code>./dns-client baidu.com</code></p>
<ul>
<li><p>查询这个域名通常会返回2条A记录</p>
<p><img src="https://blog.whowin.net/images/180019/screenshot-dns-baidu.com.png" alt="Screenshot of baidu.com" /></p>
</li>
</ul>
</li>
<li><p>运行：<code>./dns-test www.baidu.com</code></p>
<ul>
<li><p>www.baidu.com其实是一个别名，所以这个查询会得到一个CNAME记录和2个A记录</p>
<p><img src="https://blog.whowin.net/images/180019/screenshot-dns-www-baidu-com.png" alt="Screenshot of www.baidu.com" /></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-4">4. 后记</h2>
<ul>
<li>在日常的网络活动中，有时会遇到DNS污染的困扰，比如在浏览器中输入域名，却无法到达我们期望到达的网站，这有时就是因为我们收到的DNS回应并不是来自一个合法的DNS服务器；当遇到此类问题时，本文的程序可以帮助你判断是否存在DNS污染；</li>
<li>如果一个网站没有完成备案，则使用域名访问时也无法到达该网站，这其实也是在DNS上做的文章；</li>
<li>本文仅对DNS的A记录和CNAME记录做了解析，其实常用DNS记录还有MX、TXT等；</li>
<li>DNS的反向查询指的是使用IP地址查询其对应的域名；</li>
<li>DNS协议的在rfc 1035中定义的反向查询(Inverse Queries)，但是该功能是可选的(Optional)，也就是说DNS服务器可以不具备反向查询的功能，所以使用这个功能可能无法达到预期结果；</li>
<li>但是DNS协议在rfc 1035中定义了一个域 <strong>IN-ADDR.ARPA</strong>，利用这个域的查询可以达到和反向查询类似的功能，所以在实际应用中，如果需要做DNS反向查询，通常都是采用 IN-ADDR.PARA 域来完成，更详细的信息请查阅 <a target="_blank" href="https://www.ietf.org/rfc/rfc1035.txt">rfc 1035</a> 第3.5节。</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[使用SOCK_DGRAM类型的socket实现的ping程序]]></title><description><![CDATA[SOCK_DGRAM类型的socket常用于UDP通信，本文将尝试把这种socket用于ICMP协议，并完成一个简单的ping程序。使用ping去测试某个主机是否可用可能是一件很平常的事，尽管ping非常普通，但是编写一个实现ping功能的程序却并不是那么简单，因为ping使用的ICMP协议并不是一个应用层协议，在网上看到的实现ping的例子大多使用raw socket去实现，不仅增加了解析IP报头的麻烦，而且还需要有root权限才能运行，本文简要介绍ICMP协议，并给出一个使用普通的常用于UD...]]></description><link>https://whowin.cn/180020-implement-ping-program-with-sock-dgram</link><guid isPermaLink="true">https://whowin.cn/180020-implement-ping-program-with-sock-dgram</guid><category><![CDATA[ping]]></category><category><![CDATA[networking]]></category><category><![CDATA[SOCK_DGRAM]]></category><category><![CDATA[icmp]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Thu, 16 Mar 2023 06:37:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679034930522/aab44e2f-7215-4ee5-afcc-b0a296f1aa68.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>SOCK_DGRAM类型的socket常用于UDP通信，本文将尝试把这种socket用于ICMP协议，并完成一个简单的ping程序。使用ping去测试某个主机是否可用可能是一件很平常的事，尽管ping非常普通，但是编写一个实现ping功能的程序却并不是那么简单，因为ping使用的ICMP协议并不是一个应用层协议，在网上看到的实现ping的例子大多使用raw socket去实现，不仅增加了解析IP报头的麻烦，而且还需要有root权限才能运行，本文简要介绍ICMP协议，并给出一个使用普通的常用于UDP通信的socket实现ping的实例，本文将提供完整的源程序，本文的程序在 Ubuntu 20.04 下测试通过，gcc 版本号 9.4.0；阅读本文需要熟悉socket编程，对初学者而言，本文有一定的难度。</p>
</blockquote>
<h2 id="heading-1">1. 前言</h2>
<ul>
<li>ICMP协议和UDP一样，都是面向无连接的；</li>
<li>发送一个ICMP数据包和发送一个UDP数据包非常类似，对UDP而言是构建一个UDP报头然后和数据一起发出去，对ICMP而言就是构建一个ICMP报头然后和数据一起发出去；</li>
<li>创建一个socket时，常用的socket类型有三种：SOCK_STREAM、SOCK_DGRAM和SOCK_RAW，SOCK_STREAM常用于TCP通信，SOCK_DGRAM常用于UDP通信，SOCK_RAW用于接收和发送原始数据包；</li>
<li>其实socket的种类也不止这三种，这些socket类型定义在头文件中，但除了常用的这三个外，其它的基本都还没有实现，大多是因为缺少标准的协议支持，还有的是已经淘汰的socket类型，比如SOCK_PACKET；</li>
<li><p>可以用下面的代码测试在你的操作系统下，是否支持某个的socket类型，以SOCK_RDM为例：</p>
<pre><code class="lang-C"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/socket.h&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
  <span class="hljs-keyword">int</span> sock_fd = socket(AF_INET, SOCK_RDM, <span class="hljs-number">0</span> );
  perror(<span class="hljs-string">"socket: "</span>);
  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"sock_fd: %d\n"</span>, sock_fd);
  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
</li>
<li><p>运行结果截图</p>
<p><img src="https://blog.whowin.net/images/180020/screenshot-for-testing-sock-rdm.png" alt="Screenshot for testing SOCK_RDM" /></p>
</li>
<li><p>SOCK_STREAM这种socket类型显然不适合用在ping程序上，因为这种socket是面向连接的，使用之前要先建立连接，但是如果可以建立了连接就完全没有必要使用ping去测试目的主机是否可用了，SOCK_RAW过于复杂而且必须要有root权限才能运行，我们放弃不用，所以最终我们使用SOCK_DGRAM来尝试发送ICMP数据包以实现一个ping程序；</p>
</li>
</ul>
<h2 id="heading-2-icmp">2. ICMP协议</h2>
<ul>
<li>经常编写网络程序的程序员应该都很熟悉IP协议，IP协议没有任何内在机制来发送错误和控制消息，也就是说如果网络通信出现问题，IP协议本身是无法得知原因的，所以需要ICMP协议来帮助IP协议来完成这件事；</li>
<li>ICMP(Internet Control Message Protocol)协议是互联网协议族中的一个支持协议，用于在IP协议中发送控制信息，报告网络通信中可能发生的问题；</li>
<li><p>通常认为ICMP有两个主要作用：</p>
<ol>
<li><p>报告错误</p>
<blockquote>
<p>当两个设备通过互联网连接时，ICMP会生成错误信息并将该信息发回到发送设备上，以防发送数据未到达其预期目的地；例如：一个数据包的长度大于某个路由器所能接收的最大长度，路由器将丢弃该数据包并将 ICMP 消息发送回数据的发送设备。</p>
</blockquote>
</li>
<li><p>执行网络诊断</p>
<blockquote>
<p>常用的终端实用程序 <code>traceroute</code> 和 <code>ping</code> 都是使用<strong>ICMP</strong>协议运行；<code>traceroute</code> 实用程序用于显示两个互联网设备之间的路由路径；路由路径是请求数据到达目的地之前必须经过的路由器的实际物理路径；一个路由器与另一个路由器之间的路径称为"跃点"，路由跟踪还会报告经过每个跃点所需的时间；这对于确定网络延迟来源是非常有用的。</p>
</blockquote>
</li>
</ol>
</li>
<li><p>ICMP协议常被归为网络层协议，但是ICMP报文是被包装在一个IP报文中的，把ICMP、IP同归为网络层似乎也不是那么合适；</p>
</li>
<li>但是ICMP与常用的传输层协议TCP和UDP也有明显的不同，因为它通常不会用于在两个或多个计算机系统之间传输数据，所以把ICMP称为传输层协议似乎也不大合适，所以，也有人说ICMP协议是IP协议的附属协议或者说ICMP是介于网络层和传输层之间的中间层协议；</li>
<li>ICMP是那一层的协议其实并不重要，重要的是我们可以理解和使用它；实际上在设计ICMP协议时并没有考虑OSI的网络模型，所以才造成了这种含混的现象，不过这并不影响它的使用；</li>
<li>和发送UDP报文相比，发送ICMP报文仅仅是两个协议的报头不一样而已，所以我们先了解ICMP报头结构，如果需要了解以太网报头、IP报头和UDP报头，可以参考文章：<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128766145">《Linux下如何在数据链路层接收原始数据包》</a>；</li>
<li><p>ICMP报文的结构：</p>
<p><img src="https://blog.whowin.net/images/180019/structure-of-icmp-packet.png" alt="Structure of ICMP packet" /></p>
</li>
<li><p>ICMP报头，定义在头文件中</p>
<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">icmphdr</span>
{</span>
  <span class="hljs-keyword">uint8_t</span> type;        <span class="hljs-comment">/* message type */</span>
  <span class="hljs-keyword">uint8_t</span> code;        <span class="hljs-comment">/* type sub-code */</span>
  <span class="hljs-keyword">uint16_t</span> checksum;
  <span class="hljs-keyword">union</span>
  {
    <span class="hljs-class"><span class="hljs-keyword">struct</span>
    {</span>
      <span class="hljs-keyword">uint16_t</span> id;
      <span class="hljs-keyword">uint16_t</span> sequence;
    } echo;               <span class="hljs-comment">/* echo datagram */</span>
    <span class="hljs-keyword">uint32_t</span> gateway;     <span class="hljs-comment">/* gateway address */</span>
    <span class="hljs-class"><span class="hljs-keyword">struct</span>
    {</span>
      <span class="hljs-keyword">uint16_t</span> __glibc_reserved;
      <span class="hljs-keyword">uint16_t</span> mtu;
    } frag;               <span class="hljs-comment">/* path mtu discovery */</span>
  } un;
};
</code></pre>
<ul>
<li>type：ICMP报文类型，定义在头文件中：<pre><code class="lang-C"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_ECHOREPLY      0     <span class="hljs-comment">/* Echo Reply             */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_DEST_UNREACH   3     <span class="hljs-comment">/* Destination Unreachable*/</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_SOURCE_QUENCH  4     <span class="hljs-comment">/* Source Quench          */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_REDIRECT       5     <span class="hljs-comment">/* Redirect (change route)*/</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_ECHO           8     <span class="hljs-comment">/* Echo Request           */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_TIME_EXCEEDED  11    <span class="hljs-comment">/* Time Exceeded          */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_PARAMETERPROB  12    <span class="hljs-comment">/* Parameter Problem      */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_TIMESTAMP      13    <span class="hljs-comment">/* Timestamp Request      */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_TIMESTAMPREPLY 14    <span class="hljs-comment">/* Timestamp Reply        */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_INFO_REQUEST   15    <span class="hljs-comment">/* Information Request    */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_INFO_REPLY     16    <span class="hljs-comment">/* Information Reply      */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_ADDRESS        17    <span class="hljs-comment">/* Address Mask Request   */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_ADDRESSREPLY   18    <span class="hljs-comment">/* Address Mask Reply     */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> NR_ICMP_TYPES       18</span>
</code></pre>
</li>
<li>本文只关心ICMP_ECHO和ICMP_ECHOREPLY两种报文类型，ping的实现实际上就是发送ICMP_ECHO报文，然后等待目的主机响应ICMP_ECHOREPLY报文，当然出现错误时可能会收到其他类型的报文，但本文的实例中将不予处理；</li>
<li>code：子类型编码，在我们要用到的这两种类型的ICMP报文中，code都是为0的，也就是用不上这个字段，但有些报文类型是要用到code的，比如ICMP_DEST_UNREACH类型的报文，表示目标不可达，这个报文中的code字段的值将说明目标不可达的原因；</li>
<li>checksum：ICMP报文的检查和，计算这个检查和时要包括ICMP报头和ICMP数据，其计算方法与internet checksum的计算方法一致，可以参考文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128766194">《如何计算UDP头的checksum》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128846658">《如何计算IP报头的checksum》</a></li>
<li>由于我们仅关心ICMP_ECHO和ICMP_ECHOREPLY这两类报文，所以在ICMP报头的union中，我们使用echo这个结构：<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span>
  {</span>
    <span class="hljs-keyword">uint16_t</span> id;
    <span class="hljs-keyword">uint16_t</span> sequence;
  } echo;               <span class="hljs-comment">/* echo datagram */</span>
</code></pre>
<ul>
<li>id：一个唯一的ID，用于标识收到的报文是由当前进程发出，一般可以使用进程ID作为这个标识，也可以是其他的任意唯一标识；</li>
<li>sequence：icmp数据包的序列号；也就是一个编号，通常使用一个递增的序号，发的第一个报文为1，第二个报文为2，......</li>
</ul>
</li>
</ul>
</li>
<li>ICMP报头结构就是这么几个字段，但是比起UDP的报头还是要复杂一些，比如UDP报头中虽然也有checksum字段，但这个字段在IPv4中不是强制的，可以不填，但是ICMP报头中的checksum字段是必须要计算的，计算的不对报文将无法送达。 </li>
</ul>
<h2 id="heading-2-ping">2. ping程序的工作机制</h2>
<ul>
<li><code>ping</code> 程序的工作原理很像声纳回声定位，执行<code>ping</code>程序的主机向目标主机发送一个 ICMP_ECHO 请求数据包，然后目标主机返回一个 ICMP_ECHOREPLY 数据包；</li>
<li><code>ping</code> 程序是一个基本的Internet工具，可以验证一个 IP 或者主机名称所指向的主机是否存在并可以响应请求；</li>
<li><code>ping</code> 程序通过打开一个 socket 来发送 ICMP_ECHO 请求数据包，然后等待目标主机的响应，如果数据包顺利到达目标主机，而且主机也为可用状态，主机的<strong>内核</strong>将返回一个 ICMP_ECHOREPLY 数据包，如果出现错误，主机或者其他相关的网络设备将返回一个 ICMP 的错误信息数据包；</li>
<li>要注意的是 ICMP_ECHOREPLY 是由内核发出的，与任何应用层的程序无关；</li>
<li>IP报头中有一个TTL(Time To Live)值，该值决定了路由器的最大跳数(就是报文经过的路由器最大数量，一般定为64)，当经过的路由器超过这个数量时，路由器将丢弃该数据包；</li>
<li>如果数据包没有到达，那么发送方将收到一个错误信息，错误信息有以下类型：<ol>
<li>传输过程中 TTL 过期</li>
<li>目标主机不可达</li>
<li>请求超时(即没有收到回复)</li>
<li>其它原因</li>
</ol>
</li>
<li>由此可见，一个ping程序可能会遇到以下类型的ICMP数据包(参考ICMP报头结构)：<ol>
<li>ICMP_ECHO请求 - type=ICMP_ECHO(8)，code=0</li>
<li>ICMP_ECHOREPLY应答 - type=ICMP_ECHOREPLY(0)，code=0</li>
<li>TTL 过期 - type=ICMP_TIME_EXCEEDED(11)，code=0</li>
<li>目标主机不可达 - type=ICMP_DEST_UNREACH(3)，code=0-5</li>
<li>没有收到回复 - 接收超时</li>
</ol>
</li>
<li>当我们发出一个 ICMP ECHO 请求后，我们收到的回应并不一定来自目标主机，比如我们收到了一个type=3的ICMP数据包，也就是目标不可达的错误，当code=2时表示主机不可达，此时主机是不可能发出消息的，这个ICMP数据包一般会由目标主机的 gateway 发出；</li>
<li>ICMP协议是一种面向无连接的协议，发送数据包之前无需像TCP一样先建立连接；</li>
</ul>
<h2 id="heading-3-ping">3. ping的简单实现</h2>
<ul>
<li>本文并不寻求实现 Linux 下目前存在的 ping 的所有功能，那样过于复杂，本文仅实现ping的基本功能；</li>
<li><p>功能列表</p>
<ol>
<li>接收 IP 或者主机名作为输入，并可以自动识别</li>
<li>对主机名执行 DNS lookup，将其转换为 IP</li>
<li>ctrl+c可以随时终止程序，并在终止时显示数据统计报告</li>
<li>按照一定的时间间隔向目标主机发送ICMP_ECHO报文，并等待目标主机回应ICMP_ECHOREPLY报文</li>
<li>接收超时则认为丢失报文</li>
<li>如果收到的报文不是ICMP_ECHOREPLY，则认为报文丢失</li>
</ol>
</li>
<li><p>实现步骤</p>
<ol>
<li><p><strong>检查命令行输入的参数</strong></p>
<blockquote>
<p>判断命令行的输入是IP地址还是主机名，如果是主机名，要对主机名执行DNS lookup将其转换为IP地址</p>
</blockquote>
<pre><code class="lang-C"> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">hostent</span> *<span class="hljs-title">host</span>;</span>
 <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">in_addr</span> <span class="hljs-title">ipaddr</span>;</span>

 <span class="hljs-keyword">if</span> ((host = gethostbyname(hostname)) == <span class="hljs-literal">NULL</span>) {
     <span class="hljs-comment">// fail to DNS lookup</span>
     herror(<span class="hljs-string">"gethostbyname()"</span>);
     <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
 }
 ipaddr.s_addr = *(<span class="hljs-keyword">in_addr_t</span> *)host-&gt;h_addr_list[<span class="hljs-number">0</span>];
</code></pre>
</li>
<li><p><strong>获取ICMP协议的编号</strong></p>
<blockquote>
<p>执行getprotobyname()获取ICMP的协议号是为了下面建立socket时有一个正确的协议号，其实不用这么麻烦，直接用宏IPPROTO_ICMP也是完全可以的；</p>
</blockquote>
<pre><code class="lang-C"> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">protoent</span> *<span class="hljs-title">protocol</span>;</span>
 protocol = getprotobyname(<span class="hljs-string">"icmp"</span>)
</code></pre>
</li>
<li><p><strong>建立socket</strong></p>
<blockquote>
<p>如果上面没有执行getprotobyname()获取协议号，这里的协议号直接用IPPROTO_ICMP没有问题；</p>
</blockquote>
<pre><code class="lang-C"> <span class="hljs-keyword">int</span> sockfd = socket(AF_INET, SOCK_DGRAM, protocol-&gt;p_proto)
</code></pre>
</li>
<li><p><strong>设置接收超时，以避免接收时阻塞</strong></p>
<blockquote>
<p>设置接收超时是为了防止接收ICMP_ECHOREPLY时出现阻塞导致程序无法继续运行；</p>
</blockquote>
<pre><code class="lang-C"> <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RECV_TIMEOUT    5</span>

 <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">timeout</span>;</span>
 timeout.tv_sec  = RECV_TIMEOUT;
 timeout.tv_usec = <span class="hljs-number">0</span>;
 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &amp;timeout, <span class="hljs-keyword">sizeof</span>(timeout));
</code></pre>
</li>
<li><p><strong>设置IP层的TTL(Time To Live)</strong></p>
<blockquote>
<p>TTL其实就是报文最多经过多少路由器；TTL是个非常重要的参数，如果没有TTL，数据包有可能在互联网上无限循环，数据包每经过一个路由器时，TTL都会减1，当TTL为0时，路由器将丢弃该数据包；系统中会有一个默认的TTL值，一般这个默认值为64，查看默认的TTL值可以用命令 <code>cat /proc/sys/net/ipv4/ip_default_ttl</code> 或者 <code>sudo sysctl -a|grep ip_default_ttl</code>，TTL最大可以为255，所以实际上不设置TTL也不会有什么问题；</p>
</blockquote>
<pre><code class="lang-C"> <span class="hljs-keyword">int</span> ttl_val = <span class="hljs-number">64</span>;

 setsockopt(sockfd, SOL_IP, IP_TTL, &amp;ttl_val, <span class="hljs-keyword">sizeof</span>(ttl_val));
</code></pre>
</li>
<li><p><strong>接管信号量SIGINT的处理程序</strong></p>
<blockquote>
<p>当按下ctrl+c时，将产生信号SIGINT，接管该信号意味着接管ctrl+c的处理程序；ping程序只能使用ctrl+c退出，所以接管该信号是必要的</p>
</blockquote>
<pre><code class="lang-C"> signal(SIGINT, sigint);
</code></pre>
</li>
<li><p><strong>记录开始时间</strong></p>
<blockquote>
<p>记录开始时间是为了退出时打印统计数据，统计数据中有一项是总的耗时时间，需要用到这个开始时间；所有时间均以时间戳的形式记录，计时的精度要达到0.00ms以上；</p>
</blockquote>
<pre><code class="lang-C"> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">start_time</span>;</span>
 gettimeofday(&amp;start_time, <span class="hljs-literal">NULL</span>);
</code></pre>
</li>
<li><p><strong>构建一个ICMP报文</strong></p>
<ul>
<li><p>一个ICMP报文就是一个ICMP报头+数据，通常一个ping数据包的数据部分为56个字节，ICMP报头为8个字节，一起为64个字节，加上IP报头20个字节，一共为84个字节，但是，IPv4报文的最大长度可以达到65535字节，所以理论上ICMP报文可以很长，不一定非要64个字节，按照ICMP协议规定，ICMP_ECHOREPLY报文会把ICMP_ECHO请求报文中的数据全部返回回来；</p>
</li>
<li><p>一个ICMP报文最小长度是多少呢？IEEE 802.3标准中定义了一个以太网帧最小为64字节，这里面包含了以太网报头的14字节和帧结尾的4字节的CRC，这些占了18个字节，IP报头占用20字节，剩下留给ICMP报文的为：64 - 18 -20 = 26字节，ICMP报头占8字节，所以ICMP报文数据为18字节，其实ICMP报文长度还可以小，但没有任何意义，因为数据帧还是要填充到64字节发出去；</p>
</li>
<li><p>关于ICMP报头也没什么好说的，type=ICMP_ECHO，返回来的ICMP数据包中type=ICMP_ECHOREPLY；code=0；checksum在计算前一定要先填0，这是计算checksum要求的；sequence一般是一个序号，从0或者1开始都没有关系，每发出一个数据包，sequence+1就好了；id可以填任意唯一标识，通常使用当前进程ID；</p>
</li>
<li><p>ICMP报文的数据部分，我们首先填了一个发送时的时间戳，ICMP_ECHO报文的数据部分会在ICMP_ECHOREPLY报文中完全返回来，所以这个时间戳会出现在收到的ICMP_ECHOREPLY报文中，我们会在收到ICMP_ECHOREPLY报文时利用这个时间戳计算icmp报文的往返时间；后面的数据我们填充上了字符'0'，完全可以什么都不填，让后面的数据为随机数据；</p>
<pre><code class="lang-C"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ICMP_DATA_SIZE      (64 - sizeof(struct icmphdr));</span>

<span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> send_buf[<span class="hljs-number">512</span>];
<span class="hljs-keyword">int</span> pack_size = <span class="hljs-keyword">sizeof</span>(struct icmphdr) + ICMP_DATA_SIZE;
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">icmphdr</span> *<span class="hljs-title">icmp_hdr</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">icmphdr</span> *)<span class="hljs-title">send_buf</span>;</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> *<span class="hljs-title">tval</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">timevar</span> *)(<span class="hljs-title">send_buf</span> + <span class="hljs-title">sizeof</span>(<span class="hljs-title">struct</span> <span class="hljs-title">icmphdr</span>));</span>
<span class="hljs-keyword">char</span> *icmp_data = (<span class="hljs-keyword">char</span> *)(send_buf + <span class="hljs-keyword">sizeof</span>(struct icmphdr) + <span class="hljs-keyword">sizeof</span>(struct timeval));

icmp_hdr-&gt;type = ICMP_ECHO;              <span class="hljs-comment">// ICMP_ECHO packet</span>
icmp_hdr-&gt;code = <span class="hljs-number">0</span>;                      <span class="hljs-comment">// code=0</span>
icmp_hdr-&gt;checksum = <span class="hljs-number">0</span>;                  <span class="hljs-comment">// checksum will be calculated later</span>
icmp_hdr-&gt;un.echo.sequence = pack_no;    <span class="hljs-comment">// serial no</span>
icmp_hdr-&gt;un.echo.id = getpid();         <span class="hljs-comment">// process id</span>

gettimeofday(tval, <span class="hljs-literal">NULL</span>);                <span class="hljs-comment">// fill a sending timestamp into data</span>
<span class="hljs-keyword">int</span> i = pack_size - <span class="hljs-keyword">sizeof</span>(struct timeval);
<span class="hljs-built_in">memset</span>(icmp_data, <span class="hljs-string">'0'</span>, i);               <span class="hljs-comment">// fill '0' into rest place of send_buf</span>
<span class="hljs-comment">// checksum</span>
icmp_hdr-&gt;checksum = checksum((<span class="hljs-keyword">uint16_t</span> *)ping_p, pack_size);
</code></pre>
</li>
</ul>
</li>
<li><p><strong>计算ICMP报文的checksum</strong></p>
<blockquote>
<p>构建ICMP报文的最后一步就是计算checksum，这里仅给出程序，需要了解计算方法的可以参考文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128766194">《如何计算UDP头的checksum》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128846658">《如何计算IP报头的checksum》</a></p>
</blockquote>
<pre><code class="lang-C"> <span class="hljs-function"><span class="hljs-keyword">uint16_t</span> <span class="hljs-title">checksum</span><span class="hljs-params">(<span class="hljs-keyword">uint16_t</span> *addr, <span class="hljs-keyword">int</span> len)</span> </span>{
     <span class="hljs-keyword">register</span> <span class="hljs-keyword">long</span> sum = <span class="hljs-number">0</span>;
     <span class="hljs-keyword">uint16_t</span> *w = addr;
     <span class="hljs-keyword">uint16_t</span> check_sum = <span class="hljs-number">0</span>;
     <span class="hljs-keyword">int</span> nleft = len;

     <span class="hljs-keyword">while</span> (nleft &gt; <span class="hljs-number">1</span>) {
         sum += *w++;
         nleft -= <span class="hljs-number">2</span>;
     }
     <span class="hljs-comment">// Add left-over byte, if any</span>
     <span class="hljs-keyword">if</span> (nleft == <span class="hljs-number">1</span>) {
         check_sum = *(<span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> *)w;
         sum += check_sum;
     }
     <span class="hljs-comment">// Add carries</span>
     <span class="hljs-keyword">while</span> (sum &gt;&gt; <span class="hljs-number">16</span>)
         sum = (sum &amp; <span class="hljs-number">0xffff</span>) + (sum &gt;&gt; <span class="hljs-number">16</span>);

     check_sum = ~(<span class="hljs-keyword">uint16_t</span>)sum;     <span class="hljs-comment">// one's complement</span>
     <span class="hljs-keyword">return</span> check_sum;
 }
</code></pre>
</li>
<li><p><strong>发送ICMP_ECHO报文</strong></p>
<blockquote>
<p>像发送一个UDP报文那样，我们使用sendto()发送ICMP报文，ipaddr是在一开始执行DNS lookup时得到的目标IP地址，端口号port在这里设置为0，但实际上填上多少都没关系，比如1025，接收端会完全忽略掉这个值；</p>
</blockquote>
<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">dest_addr</span>;</span>
d_addr.sin_family = AF_INET;
d_addr.sin_port = htons(<span class="hljs-number">0</span>);
dest_addr.sin_addr.s_addr = ipaddr.s_addr;

sendto(sockfd, send_buf, packet_size, <span class="hljs-number">0</span>, &amp;dest_addr, <span class="hljs-keyword">sizeof</span>(struct sockaddr_in));
</code></pre>
</li>
<li><p><strong>接收返回的ICMP_ECHOREPLY报文</strong></p>
<blockquote>
<p>像接收一个UDP报文一样，我们使用recvfrom()接收报文，在第4步时已经设置了接收超时，所以这里的recvfrom()不会阻塞很久，如果产生超时，我们就认为数据包丢失；我们这里设置的接收缓冲区大小是固定的，如果你要用这个程序发送很大的ICMP_ECHO包时，小心返回的ICMP_ECHOREPLY可能无法完整接收；</p>
</blockquote>
<pre><code class="lang-C"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RECV_BUF_SIZE       1024</span>
<span class="hljs-keyword">char</span> recv_buf[RECV_BUF_SIZE];
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">from</span>;</span>
<span class="hljs-keyword">int</span> from_len = <span class="hljs-keyword">sizeof</span>(struct sockaddr_in);

recvfrom(sockfd, recv_buf, RECV_BUF_SIZE, <span class="hljs-number">0</span>, (struct sockaddr *)&amp;from, &amp;from_len);
</code></pre>
</li>
<li><p><strong>检查收到的报文的checksum</strong></p>
<blockquote>
<p>在计算收到的报文的checksum之前，要先检查这个报文是不是一个ICMP_ECHOREPLY报文，本例不处理其它ICMP报文；其次要检查ICMP报头中的ID是否为当前进程的ID(在发送ICMP_ECHO报文时设置的)，第三要通过ICMP报头中的sequence判断该报文是否为重复报文(就是已经收到过相同sequence的报文)；</p>
<p>用前面的checksum()运算一个带有checksum字段的ICMP报文，其结果应该为0，否则就是报文有问题。</p>
</blockquote>
</li>
<li><p><strong>计算报文的往返时间，填写统计数据</strong></p>
<blockquote>
<p>在发送ICMP_ECHO报文时，我们在数据包的数据部分填写了一个发送时的时间戳，为了统计数据需要，我们需要记录下面几个数据，每组icmp报文的往返时间之和<strong>sum_time</strong>、每组往返时间平方之和<strong>qsum_time</strong>、最小往返时间<strong>min_time</strong>和最大往返时间<strong>max_time</strong>，当然还要记录发出的icmp报文的数量<strong>nsend</strong>和收到的icmp报文的数量<strong>nreceived</strong>，至于这些数据的应用，我们在后面会提到；</p>
</blockquote>
<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> *<span class="hljs-title">send_time_p</span> = (<span class="hljs-title">struct</span> <span class="hljs-title">timeval</span> *)(<span class="hljs-title">recv_buf</span> + <span class="hljs-title">sizeof</span>(<span class="hljs-title">struct</span> <span class="hljs-title">icmphdr</span>));</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">timeval</span> <span class="hljs-title">recv_time</span>;</span>
gettimeofday(&amp;recv_time, <span class="hljs-literal">NULL</span>);             <span class="hljs-comment">// Receive time</span>

<span class="hljs-keyword">float</span> interval = (recv_time.tv_sec - send_time_p-&gt;tv_sec) * <span class="hljs-number">1000.00</span> + ((recv_time.tv_usec - send_time_p-&gt;tv_usec) * <span class="hljs-number">1.00</span>) / <span class="hljs-number">1000</span>;

sum_time += interval;
qsum_time += (interval * interval);
<span class="hljs-keyword">if</span> (interval &lt; min_time) min_time = interval;
<span class="hljs-keyword">if</span> (interval &gt; max_time) max_time = interval;
</code></pre>
</li>
<li><p><strong>返回步骤8，发送下一个报文</strong></p>
<blockquote>
<p>当然要先打印出当前icmp报文的状况后再返回步骤8，开始发送下一个报文；</p>
</blockquote>
</li>
<li><p><strong>ctrl+c处理程序</strong></p>
<blockquote>
<p>前面我们拦截了ctrl+c的信号，这个信号的处理非常简单，只要使发送-接收icmp报文的循环结束即可，实际上就是在步骤14时不要再返回步骤8，而是直接打印统计结果然后退出程序；本例我们使用了一个公共变量ping_loop来控制循环，ping_loop=true时循环继续，否则循环停止；</p>
</blockquote>
<pre><code class="lang-C"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sigint</span><span class="hljs-params">(<span class="hljs-keyword">int</span> signum)</span> </span>{
    ping_loop = <span class="hljs-literal">false</span>;
}
</code></pre>
</li>
<li><p><strong>打印统计数据</strong></p>
<ul>
<li>我们先看一下Linux(Ubuntu)提供的ping程序的统计结果输出的截屏</li>
</ul>
<p><img src="https://blog.whowin.net/images/180020/screenshot-of-ping-statistics.png" alt="screetshot of ping statistics" /></p>
<ul>
<li>其统计数据有</li>
<li>发送的ICMP_ECHO报文数量：nsend</li>
<li>接收到的ICMP_ECHOREPLY报文数量：nreceived</li>
<li>丢失的ICMP_ECHOREPLY报文数量：nsend - nreceived</li>
<li>最小往返时间(min)：min_time</li>
<li>最大往返时间(max)：max_time</li>
<li><p>平均往返时间(avg)：$\large {\sum_{1}^{n}rtt \over n} = {{sum_time} \over nreceived} $ </p>
<blockquote>
<p>rtt是Round Trip Time的意思，意即数据包的往返时间</p>
</blockquote>
</li>
<li><p>平均偏差(mdev)</p>
<blockquote>
<p>mdev是Mean Deviation的意思，它表示这些ICMP的往返时间rtt偏离平均值的程度，一般认为这个值越大，网络的稳定性越差，这个值的计算公式为：</p>
</blockquote>
<p>$$
{\sqrt{{\sum{x_i^2} \over n} - ({\sum{x_i} \over n})^2}} = {\sqrt{{qsum\_time \over nreceived} - {({sum\_time \over nreceived})^2}}} = {\sqrt{{qsum\_time \over nreceived} - avg^2}}
$$</p></li>
<li>下面是统计数据的主要代码<pre><code class="lang-C"><span class="hljs-keyword">float</span> avg = <span class="hljs-number">0.0</span>, mdev = <span class="hljs-number">0.0</span>;
<span class="hljs-keyword">if</span> (nreceived) {
  avg = (sum_time * <span class="hljs-number">1.00</span>) / nreceived;
  mdev = <span class="hljs-built_in">sqrt</span>(((qsum_time * <span class="hljs-number">1.00</span>) / nreceived) - (avg * avg));
}
</code></pre>
</li>
</ul>
</li>
</ol>
</li>
<li>完整源代码，文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180020/ping-dgram.c">ping-dgram.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li>编译，因为在统计部分使用了数学函数，所以编译时要加上 <code>-lm</code> 选项，意即连接数学函数库<pre><code class="lang-bash">gcc -Wall ping-dgram.c -o ping-dgram -lm
</code></pre>
</li>
<li>运行：<code>./ping-dgram baidu.com</code></li>
<li><p>运行截图</p>
<p><img src="https://blog.whowin.net/images/180020/screenshot-of-ping-dgram.png" alt="screenshot of ping-dgram" /></p>
</li>
</ul>
<h2 id="heading-4">4. 后记</h2>
<ul>
<li>ping的输出中有一项是TTL，文中多次提到了这个值的意义，本例输出的这个值可能并不准确，这个值的准确值是放在IP报头中的，但本例使用的方法是读取不到IP报头的，所以无法取得准确的TTL值，这应该是使用SOCK_DGRAM类型的socket编写ping程序的一个小缺陷，在本例中我们使用了初始化socket时设置的TTL值进行了显示；</li>
<li>代码中对重复报文做了判断，其判断本身也并不是很准确，正常情况下是不应该收到重复的ICMP_ECHOREPLY报文的，但根据我的经验，除了在局域网中有重复IP的情况以外，多发生在网络中有多个并行路由器的情况下，有时是因为某台机器开启了某些有路由功能的进程，比如网络中有一台运行openWrt的机器，很可能上面就会运行有一些操作路由的进程，但是更详细的产生重复ICMP报文的情况并不十分清楚，因为这种情况并不多见，也不太容易捕捉到；</li>
<li>本例中仅仅处理了ICMP_ECHOREPLY报文，但实际上一个ICMP_ECHO报文发出后，只有在一切正常的情况下才返回ICMP_ECHOREPLY报文，如果出现错误，会返回其他类型的报文，比如：ICMP_DEST_UNREACH报文，表示报文没有到达目的地，code中将标识没有到达的原因，其实这类报文更有意义，如果读者有兴趣，可以在本例的基础上进行扩展，写出更完美的ping程序；</li>
<li>文中提到，一个ICMP报文可以很长，如果想用本例中的程序去测试比较长的ICMP报文，请注意本例的接收缓冲区的大小是一个固定值，请进行调整，避免缓冲区溢出或接收不到完整数据包的情况发生。</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>


]]></content:encoded></item><item><title><![CDATA[使用tun虚拟网络接口建立IP隧道的实例]]></title><description><![CDATA[通常的socket编程，面对的都是物理网卡，Linux下其实很容易创建虚拟网卡；本文简单介绍一下Linux虚拟网卡的概念，并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡，最终实现一个从客户端到服务器的简单的IP隧道，希望本文能对理解虚拟网卡和IP隧道有所帮助，本文将提供完整的源程序；阅读本文需要具备在Linux下使用C语言进行IPv4下socket编程的基本能力，本文对网络编程的初学者难度较大。

1. Linux下的虚拟网卡TUN/TAP

TUN和TAP是Linuxn内核的虚...]]></description><link>https://whowin.cn/180018-tun-example-for-setting-up-ip-tunnel</link><guid isPermaLink="true">https://whowin.cn/180018-tun-example-for-setting-up-ip-tunnel</guid><category><![CDATA[networking]]></category><category><![CDATA[tunnel]]></category><category><![CDATA[tun]]></category><category><![CDATA[ip tunnel]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Tue, 07 Mar 2023 13:56:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678283619269/8e593ac5-ba95-45cf-b1cd-1f34b560d143.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>通常的socket编程，面对的都是物理网卡，Linux下其实很容易创建虚拟网卡；本文简单介绍一下Linux虚拟网卡的概念，并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡，最终实现一个从客户端到服务器的简单的IP隧道，希望本文能对理解虚拟网卡和IP隧道有所帮助，本文将提供完整的源程序；阅读本文需要具备在Linux下使用C语言进行IPv4下socket编程的基本能力，本文对网络编程的初学者难度较大。</p>
</blockquote>
<h2 id="heading-1-linuxtuntap">1. Linux下的虚拟网卡TUN/TAP</h2>
<ul>
<li>TUN和TAP是Linuxn内核的虚拟网络设备，不同于普通靠硬件网络适配器实现的设备，这些虚拟的网络设备全部用软件实现，并可以向运行于Linux上的应用软件提供与硬件的网络设备完全相同的功能；</li>
<li>TAP等同于一个以太网设备，它操作OSI模型的第二层(数据链路层)数据包，通常我们所使用的网络就是以太网数据帧，所以要使用TAP设备，就需要自己构建以太网报头、IP报头、TCP/UDP报头；</li>
<li>TUN模拟了网络层设备，操作第三层(网络层)数据包，通常我们使用的TCP/UDP报文在网络层使用的IP协议，所以使用TUN设备，需要自己构建IP报头和TCP/UDP报头，比TAP设备少构建一个以太网报头；</li>
<li>Linux通过TUN/TAP设备向绑定该设备的用户空间的应用程序发送数据；同样，用户空间的应用程序也可以像操作硬件网络设备那样，通过TUN/TAP设备发送数据；在后面这种情况下，TUN/TAP设备向Linux的网络协议栈提交数据包，从而模拟从外部接收数据的过程；</li>
</ul>
<h2 id="heading-2-tun">2. 构建一个TUN设备</h2>
<ul>
<li>上一节的描述显然过于枯燥，可能会对初次接触虚拟网卡的读者感到困惑，不知所云，本节将实际建立一个tun设备，帮助你走出困惑；</li>
<li><p>构建一个基本的tun设备，只需要两个步骤</p>
<ol>
<li><p><strong>编写一个程序，至少完成三个任务</strong></p>
<ul>
<li>以可读写模式打开设备文件 <code>/dev/net/tun</code><pre><code class="lang-C"><span class="hljs-keyword">int</span> fd;
fd = open(<span class="hljs-string">"/dev/net/tun"</span>, O_RDWR));
</code></pre>
</li>
<li><p>向Linux内核注册一个tun设备名称，本例中为tun0</p>
<blockquote>
<p>(struct ifreq)定义在头文件中，在我的很多文章中都有介绍，比如文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128891255">《如何使用raw socket发送UDP报文》</a>，如果需要，可以参考；</p>
</blockquote>
<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ifreq</span> <span class="hljs-title">ifr</span>;</span>
<span class="hljs-built_in">memset</span>(&amp;ifr, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
<span class="hljs-built_in">strcpy</span>(ifr.ifr_name, <span class="hljs-string">"tun0"</span>);

ioctl(fd, TUNSETIFF, (<span class="hljs-keyword">void</span> *)&amp;ifr);
</code></pre>
</li>
<li><p>编写处理tun0接收/发送数据的程序</p>
<pre><code class="lang-C"><span class="hljs-keyword">char</span> buffer[BUFSIZE];

<span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {
  read(fd, buffer, BUFSIZE);
  <span class="hljs-comment">// todo</span>
}
</code></pre>
</li>
</ul>
</li>
<li><strong>为设备分配IP地址</strong>(本例中为tun0分配的IP为10.0.0.1)<pre><code class="lang-bash"> sudo ifconfig tun0 10.0.0.1 netmask 255.255.255.0 up
</code></pre>
</li>
</ol>
</li>
<li>把上面的代码片段组合在一起，就可以完成一个tun设备的建立，文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/tun-01.c">tun_01.c</a>(<strong>点击文件名下载源文件</strong>)</li>
<li>这段程序在进入循环前增加了 <code>system("ifconfig tun0 10.0.0.1/24 up")</code>，为tun0分配了IP地址10.0.0.1，所以运行完后就不需要再为这个设备分配IP了；</li>
<li>编译：<code>gcc -Wall tun-01.c -o tun-01</code></li>
<li>该程序需要root权限运行，主要是因为其中使用了ioctl，运行：<code>sudo ./tun-01</code></li>
<li><p>运行该程序，会构建一个tun设备，打开一个新的终端，使用 <code>ifconfig</code> 将可以看到系统中多了一个虚拟网络接口tun0，使用 <code>route -n</code> 查看路由也会看到增加了一条关于tun0设备的路由</p>
<p><img src="https://blog.whowin.net/images/180018/screenshot-of-setting-up-tun0.png" alt="screenshot of setting up tun0 device" /></p>
<center><b>图1：构建一个tun设备后</b></center>

<hr />
</li>
<li><p>尽管建立起了虚拟网卡tun0，但因为程序过于简单，所以这样建立的设备什么事情都做不了，必须完善程序，才能让这个设备真正地发挥作用；</p>
</li>
<li>tun设备是一个第三层(网络层)的设备，在这个设备上只能收到IP报头，收不到以太网报头，所以Linux索性没有为tun设备分配MAC地址；</li>
<li>后面将以本节的程序为基础，不断改进，最终写出一个简单的IP隧道的程序。</li>
</ul>
<h2 id="heading-3-tun">3. 使用tun设备的基本数据流向</h2>
<ul>
<li>设备建立起来以后，程序员关心的是我们如何从这个设备上收发报文，如何处理这些报文；</li>
<li>对于一个物理网络接口而言，接口一端连接着网络协议栈，另一端连接着物理网络；而对于一个虚拟网络接口而言，接口的一端仍然连接着网络协议栈，但是另一端连接着一个应用程序，也就是我们前面下载的那个程序(<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/tun-01.c">tun-01.c</a>)，我们把这个程序称为 <strong>application-tun</strong>；</li>
<li>可以和一个物理网络接口比较来说明虚拟网络接口的数据流向，在物理接口上要发送到物理网络上去的报文，相对于虚拟接口将被发送到应用程序 <strong>application-tun</strong> 上；</li>
<li>当我们使用socket发送报文时，报文被提交给Linux的网络协议栈，协议栈为报文封装各个协议层的报头，并根据路由表将报文交给相应设备的驱动程序，比如enp0s3的驱动程序，然后由驱动程序将报文发送到物理网络上(物理设备)，或者发送给应用程序 application-tun(虚拟设备)；</li>
<li>在上一节中，我们使用 <code>route -n</code> 已经看到了关于tun0设备的路由：<pre><code class="lang-plaintext">内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0        192.168.2.3     0.0.0.0        UG   100    0     0  enp0s3
10.0.0.0       0.0.0.0         255.255.255.0  U    0      0     0  tun0
169.254.0.0    0.0.0.0         255.255.0.0    U    1000   0     0  enp0s3
192.168.2.0    0.0.0.0         255.255.255.0  U    100    0     0  enp0s3
</code></pre>
</li>
<li>路由表明，当目的IP地址为10.0.0.x时，报文将被送到虚拟设备tun0的驱动程序上去，该设备绑定的IP为10.0.0.1；</li>
<li>还有一条路由，当目的IP地址为192.168.2.x时，报文将被送到物理设备enp0s3的驱动程序上去，该设备绑定的IP为192.168.2.114；</li>
<li>这两条路由比较相似，区别是一个是物理设备enp0s3，另一个是虚拟设备tun0，我们拿这两条路由进行对比说明数据流向；</li>
<li><p><strong>发送报文到物理/虚拟接口绑定的IP地址上</strong></p>
<ul>
<li>当我们发送一个UDP报文到 <strong>192.168.2.114:5678</strong>(也就是本机物理设备enp0s3的IP)时，根据路由，报文被送给enp0s3的驱动程序，驱动程序并不会把这个报文发送到物理网络上，因为enp0s3的驱动程序已经是这个报文最终的目的地，所以enp0s3的驱动程序会将这个报文发到一个正在监听192.168.2.114:5678的用户程序上，如果我们没有编写这个程序，报文将被丢弃，这样我们就收不到这个报文；</li>
<li>当我们发送一个UDP报文到 <strong>10.0.0.1:5678</strong>(也就是本机虚拟设备tun0的IP)时，根据路由，报文被送给tun0的驱动程序，驱动程序并不会把这个报文发送到 application-tun 上，因为tun0的驱动程序已经是这个报文的最终目的地，所以tun0的驱动程序会将这个报文发到一个正在监听10.0.0.1:5678的用户程序上，和物理设备一样，如果我们没有编写这个程序，报文将被丢弃，我们收不到这个报文；</li>
</ul>
<p><img src="https://blog.whowin.net/images/180018/send-data-tun-local.png" alt="send data to tun's IP" /></p>
<center><b>图2：发送报文到tun0的IP上</b></center>

</li>
</ul>
<hr />
<ul>
<li><p><strong>发送报文到符合路由的其他IP地址上</strong></p>
<ul>
<li>当我们发送一个UDP报文到 <strong>192.168.2.112:5678</strong> 时，根据路由报文会被送给enp0s3的驱动程序，驱动程序会把这个报文发送到物理网络上；</li>
<li>当我们发送一个UDP报文到 <strong>10.0.0.2:5678</strong> 时，根据路由报文会被送给报文被送给tun0的驱动程序，驱动程序会把这个报文发送到应用程序 <strong>application-tun</strong> 上；</li>
</ul>
<p><img src="https://blog.whowin.net/images/180018/send-data-tun-remote.png" alt="send data to tun's route" /></p>
<center><b>图3：发送报文到符合tun0路由的其他IP上</b></center>

</li>
</ul>
<hr />
<ul>
<li><p><strong>对上述说明可以做一个简单的测试</strong></p>
<ul>
<li>打开终端，运行前面的程序：tun-01<pre><code class="lang-bash">sudo ./tun-01
</code></pre>
</li>
<li>打开另一个终端，使用下面命令分别向 <strong>10.0.0.1:5678</strong> 发送数据，在运行 tun-01 的终端上并不会显示收到数据；<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"hello"</span> &gt; /dev/udp/10.0.0.1/5678
</code></pre>
</li>
<li>使用下面命令分别向 <strong>10.0.0.2:5678</strong> 发送数据，在运行 tun-01 的终端上会显示收到数据；<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"hello"</span> &gt; /dev/udp/10.0.0.2/5678
</code></pre>
</li>
</ul>
</li>
<li><p><strong>源IP地址的选择</strong></p>
<ul>
<li>当我们在电脑系统上运行 <code>sudo ./tun-01</code> 时，我们的系统就有了两个IP地址，一个是物理网卡的，IP为192.168.2.114，另一个是虚拟网卡的，IP为10.0.0.1；</li>
<li>当我们在做上面的测试时，我们用 <code>echo ......</code> 命令向 10.0.0.1 和 10.0.0.2 发送了UDP消息，发送时我们并没有指定源IP地址，那么发出的消息的源IP地址是什么呢？192.168.2.114 还是 10.0.0.1？</li>
<li>我们把前面那个程序 tun_01.c 改一下，一是增加一些错误判断，使这个程序更加完善一些，另外我们增加一个显示IPv4报头的功能，这样我们就可以看到IP头中的源IP地址了；</li>
<li>改好的程序文件名为：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/tun-02.c">tun-02.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li>编译：<code>gcc -Wall tun-02.c -o tun-02</code></li>
<li><p>下面我们做个测试，向 10.0.0.2:5678 发送一条UDP消息，我们看看源IP地址是什么？</p>
<ul>
<li>打开一个终端，运行tun-02<pre><code class="lang-bash">sudo ./tun-02
</code></pre>
</li>
<li>打开另一个终端，向10.0.0.2发送消息<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"hello"</span> &gt; /dev/udp/10.0.0.2/5678
</code></pre>
</li>
<li><p>在运行tun-02的终端上显示出源IP地址为10.0.0.1</p>
<p><img src="https://blog.whowin.net/images/180018/screenshot-for-source-ip.png" alt="screenshot for source IP" /></p>
<center><b>图4：Linux在多网卡环境下选择源IP</b></center>

</li>
</ul>
</li>
</ul>
<hr />
<ul>
<li>当使用sendmsg()发送数据时，是可以显式地指定源IP地址的；</li>
<li><p>路由表中有一个src字段，当没有指定源IP地址时，将使用选定路由的src字段作为源IP地址，使用 <code>ip route</code> 可以看到src字段</p>
<p><img src="https://blog.whowin.net/images/180018/screenshot-of-ip-route.png" alt="screenshot of 'ip route'" /></p>
<center><b>图5：ip route命令显示路由表中src字段</b></center>

</li>
</ul>
<hr />
<ul>
<li>如果选定的路由没有src字段，Linux会搜寻选定路由的网络接口上所有绑定的IP，对IPv6将选择第一个搜寻到的地址，对IPv4则尽量选择与目标IP在同一网段的IP地址；</li>
</ul>
</li>
</ul>
<h2 id="heading-4-tunip">4. 使用tun设备搭建一个简单的IP隧道</h2>
<ul>
<li>tun实际上是tunnel的前面三个字母，tun设备注定和隧道是有关系的，tun设备也的确常用来构建一个IP隧道；</li>
<li>IP报文其实是指：IP报头 + TCP/UDP报头 + 数据</li>
<li>所谓IP隧道是指把一个IP报文作为数据再封装一个TCP头和IP头，所以整个报文变成：IP报头 + TCP报头 + (IP报头 + TCP/UDP报头 + 数据)</li>
<li>至于IP隧道的意义、应用场景之类的，本文不予讨论，可以自己去百个度或者谷个歌查一下，本文将致力于做一个简单的IP隧道；</li>
<li><p>先看一张示意图</p>
<p><img src="https://blog.whowin.net/images/180018/simple-ip-tunnel.png" alt="Diagram Simple IP tunnel" /></p>
<center><b>图6：简单的IP隧道示意图</b></center>
</li>
<li><p>有两台电脑，Computer A和Computer B</p>
<ul>
<li>Computer A：<ol>
<li>物理网卡为enp0s3，绑定IP：192.168.2.112</li>
<li>虚拟网卡为tun0，绑定IP：10.0.0.2</li>
</ol>
</li>
<li>Computer B：<ol>
<li>物理网卡为enp0s3，绑定IP：192.168.2.114</li>
<li>虚拟网卡为tun0，绑定IP：10.0.0.1</li>
</ol>
</li>
</ul>
</li>
<li>Computer A和Computer B的路由表一样，如下：<pre><code class="lang-plaintext">内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0        192.168.2.3     0.0.0.0        UG   100    0     0  enp0s3
10.0.0.0       0.0.0.0         255.255.255.0  U    0      0     0  tun0
169.254.0.0    0.0.0.0         255.255.0.0    U    1000   0     0  enp0s3
192.168.2.0    0.0.0.0         255.255.255.0  U    100    0     0  enp0s3
</code></pre>
</li>
<li>Computer A的应用程序app A向10.0.0.1:1234发送报文，Computer B的应用程序app D侦听在10.0.0.1:1234上；</li>
<li>目标很简单，computer A的app A直接向10.0.0.1:1234发送报文，computer B的app D能够正常收到收到，就像在一个局域网上一样；</li>
<li>首先要明确的，物理局域网的网段是192.168.2.x，所以向10.0.0.1发送报文并不会被送到物理局域网上，按照路由，这条报文会被送到tun0的驱动程序上去，因为10.0.0.1并不是computer A的虚拟网卡tun0绑定的IP，所以驱动程序会把这个报文送到application-tun上，所以如果我们不做处理，这个报文根本无法到达目的地；</li>
<li>如何处理这个报文使其发送到computer B的app D上去呢？通常的方法就是在computer A和computer B的物理网卡之间建立一条IP隧道；</li>
<li>当computer A启动applition-tun时，主动发起向computer B的连接，端口号定为5678，computer B在启动applition-tun时，主动侦听在端口5678上，并等待computer A的连接请求，一旦连接建立，这个隧道就建好了；</li>
<li>computer A的application-tun收到发往10.0.0.1的报文时，要在整个IP报文上再包装上一个IP报头+TCP报头，TCP报头中指定目的端口号为5678，IP报头中指定目的IP为192.168.2.114，源IP为192.168.2.112，然后把这个新报文从建立的隧道中发出；</li>
<li><p>computer B上侦听在5678端口上的应用程序app C会收到这个报文，app C去掉IP报头和TCP报头，把数据部分作为一个完整的报文重新从socket发出，这个报文的内容正是computer A发出的原始报文，computer B的内核协议栈根据路由会将该报文发给tun0的驱动程序，驱动程序会将这个报文送到正在侦听1234端口的app D上；</p>
</li>
<li><p>在客户端(computer A)需要编写一个程序，程序文件名：app-client.c，这个程序应遵循以下处理流程：</p>
<ol>
<li>打开 /dev/net/tun 文件，返回tun_fd，在内核注册虚拟设备 tun0；</li>
<li>创建socket，sock_fd，在这个 sock_fd 上连接服务器端(computer B)的5678端口，建立IP隧道；</li>
<li>使用select检查tun_fd和sock_fd，并分别处理在这两个 fd 上收到的数据；</li>
<li>在tun_fd上收到数据的处理流程<ul>
<li>将收到的包括IP报头在内的报文作为数据从sock_fd上发出</li>
</ul>
</li>
<li>在sock_fd上收到数据的处理流程<ul>
<li>把收到的数据作为一个IP报文显示报头及内容</li>
</ul>
</li>
</ol>
</li>
<li><p>在服务器端(computer B)编写一个程序，文件名为：app-server.c，这个程序应遵循以下处理流程：</p>
<ol>
<li>打开 /dev/net/tun 文件，返回tun_fd，在内核注册虚拟设备 tun0；</li>
<li>创建socket，fd为sock_fd，在这个 sock_fd 上侦听5678端口，等待客户端连接以建立IP隧道；</li>
<li>接受客户端的连接请求，为新连接创建socket，fd为net_fd</li>
<li>使用select检查tun_fd和net_fd，并分别处理在这两个 fd 上收到的数据；</li>
<li>在tun_fd上收到数据的处理流程<ul>
<li>将收到的包括IP报头在内的报文作为数据从net_fd上发出</li>
</ul>
</li>
<li>在net_fd上收到数据的处理流程<ul>
<li>把收到的数据(不包括IP报头和TCP报头)作为带有IP报头的报文发到tun_fd上</li>
</ul>
</li>
</ol>
</li>
<li><p>客户端程序：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/app-client.c">app-client.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li>客户端程序编译：<code>gcc -Wall app-client.c -o app-client</code></li>
<li>服务器端程序：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180018/app-server.c">app-server.c</a>(<strong>点击文件名下载源程序</strong>)</li>
<li>服务器端程序编译：<code>gcc -Wall app-server.c -o app-server</code></li>
<li>为了运行方便，也可以将这两个程序写成守护进程，将程序中注释掉的 <code>daemon(0, 0)</code> 放开即可；</li>
<li>请根据实际情况调整程序中的宏定义，SERVER_IP和TUN_IP；</li>
<li>在服务器端注意防火墙设置，打开5678端口或者关闭防火墙；</li>
<li><p>这两个程序的运行均需要root权限。</p>
</li>
<li><p>客户端程序测试</p>
<ul>
<li>需要打开三个终端窗口；</li>
<li>首先将程序中的SERVER_IP改为本机的IP地址，然后重新编译；</li>
<li>打开第一个终端，运行 <code>nc -l 5678</code>，这个命令将监听本机的5678端口；</li>
<li>打开第二个终端，运行客户端程序：<code>sudo ./app-client</code>，应该显示"Connected to server ..."</li>
<li>打开第三个终端，运行 <code>echo "hello" &gt; /dev/udp/10.0.0.1/1234</code>，这个命令将向10.0.0.1的1234端口发送一个UDP报文，报文的数据部分为"hello"</li>
<li>此时在第二个终端上应该显示"Received data from tun"，在第一个终端上收到一些乱码，但其中有"hello"字符串，乱码是因为我们收到的数据包括IP报头和UDP报头，这两部分是二进制的数据；</li>
<li>如果你看到的和上面的描述一致，那么你的客户端程序基本没有问题；</li>
<li><p>下面是截屏</p>
<p><img src="https://blog.whowin.net/images/180018/screenshot-of-1st-termianl-for-client.png" alt="screenshot of 1st terminal for client test" /></p>
<center><b>图7：测试客户端程序时的第一个终端</b></center>


</li>
</ul>
</li>
</ul>
<p>    <img src="https://blog.whowin.net/images/180018/screenshot-of-2nd-terminal-for-client.png" alt="screenshot of 2nd terminal for client test" /></p>
    <center><b>图8：测试客户端程序时的第二个终端</b></center>

<ul>
<li>服务器端程序无需独立测试；</li>
<li><p>IP隧道测试</p>
<ul>
<li>需要两台机器，一台做客户端，另一台做服务器端</li>
<li>再次强调，请根据实际情况调整程序中的宏定义，SERVER_IP和TUN_IP，并重新编译程序；</li>
<li>在服务器端注意防火墙设置，打开5678端口或者关闭防火墙；</li>
<li><p>在服务器端和客户端均需要打开两个终端，下面是测试方法示意图</p>
<p><img src="https://blog.whowin.net/images/180018/diagram-for-testing.png" alt="diagram for testing" /></p>
<center><b>图9：测试示意图</b></center>
</li>
<li><p>在服务器第一个终端上启动服务器端程序 <code>sudo ./app-server</code></p>
</li>
<li>在服务器第二个终端上执行命令 <code>nc -luk 1234</code>，这个命令将一直监听在UDP的1234端口上；</li>
<li>在客户端第一个终端上启动客户端程序 <code>sudo ./app-client</code></li>
<li>在客户端第二个终端上执行命令 <code>echo "hello" &gt; /dev/udp/10.0.0.1/1234</code>，这个命令将向10.0.0.1(服务器端的tun0绑定的IP)的UDP端口1234发送一条消息</li>
<li>客户端第二个终端上向10.0.0.1:1234发送了一个UDP消息，内容是：hello</li>
<li>最终在服务器端的第二个终端上收到了这个信息</li>
<li><p>下面是运行截图</p>
<p><img src="https://blog.whowin.net/images/180018/screenshot-server-1st-terminal-for-testing.png" alt="screenshot server 1st terminal for testing" /></p>
<center><b>图10：服务器端第一个终端</b></center>

<p><img src="https://blog.whowin.net/images/180018/screenshot-client-1st-terminal-for-testing.png" alt="screenshot client 1st terminal for testing" /></p>
<center><b>图11：客户端第一个终端</b></center>

<p><img src="https://blog.whowin.net/images/180018/screenshot-server-2nd-terminal-for-testing.png" alt="screenshot server 2nd terminal for testing" /></p>
<center><b>图12：服务器端第二个终端</b></center>

<p><img src="https://blog.whowin.net/images/180018/screenshot-client-2nd-terminal-for-testing.png" alt="screenshot client 2nd terminal for testing" /></p>
<center><b>图13：客户端第二个终端</b></center>


</li>
</ul>
</li>
</ul>
<h2 id="heading-5">5. 后记</h2>
<ul>
<li>我们实现了一个简单的IP隧道，在这个IP隧道，我们传送一个UDP报文，我们传了一个UDP报文而不是一个TCP报文是为了省去connect()的麻烦；</li>
<li>这样一个IP隧道并不局限在局域网中，通过互联网一样可以建立一个IP隧道；</li>
<li>我们的这个服务器端的程序仅处理了一个客户端的连接，如果我们允许多个客户端接入并建立多条IP隧道，如果连接的多个客户端的tun都绑定在同一个网段上，那么通过服务器显然是可以像局域网一样相互通信的，好像多个终端在一个局域网里一样，是不是有点像<img src="https://blog.whowin.net/images/180018/vpn.png" alt />，实际上很多<img src="https://blog.whowin.net/images/180018/vpn.png" alt />就是使用IP隧道实现的；</li>
<li>IP隧道还可以用于很多场合，如果你的防火墙不允许某些协议通过，那么你可以通过一个防火墙允许的端口与服务器建立一个IP隧道，然后在这个IP隧道里跑那个不被防火墙允许的协议，就像我们在IP隧道里跑UDP协议一样；</li>
<li>建立隧道也不一定非得使用TCP/IP协议，比如可以使用ICMP协议建立一个ICMP隧道，当你的电脑只能ping通你的服务器，其它的所有协议都无法通过防火墙的情况下，使用ICMP协议建立一个ICMP隧道，然后可以在这个隧道里跑其它协议；</li>
<li>虚拟网络接口的用途很多，现在的虚拟机、容器等大多使用了虚拟网络接口，希望这篇文章可以让你对虚拟网络接口有个初步的认识。</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>


]]></content:encoded></item><item><title><![CDATA[使用sntp协议从时间服务器同步时间]]></title><description><![CDATA[在互联网上校准时间，是几乎连接在互联网上的每台计算机都要去做的事情，而且很多是在后台完成的，并不需要人工干预；互联网上有很多时间服务器可以发布精确的时间，计算机客户端使用NTP(Network Time Protocol)协议与这些时间服务器进行时间同步，使本机得到精确时间，本文简要描述了NTP协议的原理，对NTP协议的时间同步精度做了简要分析，并具体实现了SNTP(Simple Network Time Protocol)下的客户端，本文附有完整的C语言SNTP客户端的源程序。阅读本文只需掌握...]]></description><link>https://whowin.cn/180017-sync-time-from-time-server-using-sntp</link><guid isPermaLink="true">https://whowin.cn/180017-sync-time-from-time-server-using-sntp</guid><category><![CDATA[NTP ]]></category><category><![CDATA[sntp]]></category><category><![CDATA[networking]]></category><category><![CDATA[C]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Mon, 13 Feb 2023 03:15:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676981394405/d587f20d-6a4d-4eca-8bf0-12821268df10.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>在互联网上校准时间，是几乎连接在互联网上的每台计算机都要去做的事情，而且很多是在后台完成的，并不需要人工干预；互联网上有很多时间服务器可以发布精确的时间，计算机客户端使用NTP(Network Time Protocol)协议与这些时间服务器进行时间同步，使本机得到精确时间，本文简要描述了NTP协议的原理，对NTP协议的时间同步精度做了简要分析，并具体实现了SNTP(Simple Network Time Protocol)下的客户端，本文附有完整的C语言SNTP客户端的源程序。阅读本文只需掌握基本的socket编程即可，本文对网络编程的初学者难度不大。</p>
</blockquote>
<h2 id="heading-1-ntpsntp">1. NTP协议和SNTP协议</h2>
<ul>
<li>SNTP协议使用与NTP协议同样的报文结构和格式，所以仅就从服务器进行时间同步而言，在服务器端看NTP和SNTP没有什么区别，使用SNTP协议的客户端可以从任何一台符合NTP协议的时间服务器上进行时间同步；</li>
<li>NTP和SNTP协议的区别在于错误检测和时间校准的算法上，这主要体现在客户端的软件上；</li>
<li>SNTP客户端程序向一台NTP时间服务器发出时间数据包，接收来自服务器的回应，并据此计算本机的时间偏差，从而校准本机时间；</li>
<li>NTP协议的算法比SNTP复杂得多，NTP通常使用多个时间服务器校验时间，该算法使用多种方法来确定这些获取的时间值是否准确，包括模糊因子和识别与其他时间服务器不一致的时间服务器，然后加速或减慢系统时钟的<strong>漂移率</strong>，使系统时间可以做到<ol>
<li>系统的时间总是正确的；</li>
<li>在初始校正时间后，系统时间不会再有任何时间跳跃。</li>
</ol>
</li>
<li>与NTP客户端不同，SNTP客户端通常只使用一个时间服务器来计算时间，然后将系统时间跳转到计算的时间；为了防止时间服务器出现不可用的情况，SNTP客户端可以有一个或多个备份时间服务器，但不会同时使用多个时间服务器来计算时间；</li>
<li>SNTP客户端通常按照一个固定的间隔时间去访问时间服务器，在间隔期间则不对系统时间做任何调整，所以，当SNTP访问时间服务器校准时间时，往往会产生时间跳跃；</li>
<li>我们可以用一个更形象的例子来说明SNTP客户端，我们把墙上的挂钟当做时间服务器，把我们戴的手表当做客户端<ul>
<li>当我们的手表为SNTP客户端时，我们每隔一小时看一下挂钟，并使用挂钟来校准手表；</li>
<li>当手表12:00时，挂钟的时间为11:59:57秒，手表快了3秒钟，所以把手表调慢3秒钟；</li>
<li>在接下来的1个小时里，不会对手表做任何调整，当手表1:00时，挂钟的时间为12:59:57秒，所以我们再次把手表调慢3秒钟；</li>
<li>从手表时间的准确度来说，刚刚校准完时间时是最准确的，然后准确度逐渐变差，在再次调整时间前，其准确度是最差的；</li>
</ul>
</li>
<li>如果这样的情况能够满足你对时间的要求，那就可以使用SNTP协议去校准时间，否则就要考虑使用NTP客户端；</li>
<li>NTP客户端计算"手表"的时间变化方向和速率，以此为基础来补偿"手表"上的时间漂移，对"手表"进行实时调整，使"手表"一直保持准确；</li>
<li>实际上，对大多数PC而言，SNTP都是可以满足要求的，windows的内建程序w32time采用的就是SNTP协议。</li>
</ul>
<h2 id="heading-2-ntp">2. NTP时间同步的基本原理</h2>
<ul>
<li>NTP的原理是通过一个时间消息包的传送计算出客户端和服务器端的时间偏差，从而校准客户端的时间；</li>
<li>NTP和SNTP客户端均使用UDP向时间服务器发送消息，IANA为NTP分配的端口号为123，也就是说，NTP/SNTP客户端需要向时间服务器的123端口发送一个符合格式(下一节介绍消息格式)的UDP消息，客户端的接收端口号没有规定；</li>
<li>时间服务器是Server，客户端是Client，同步过程如下进行：<ol>
<li>Client向Server发送一个消息包，记录发出消息包时的时间戳 <strong>T<sub>1</sub></strong>(以Client系统时间为准)</li>
<li>Server收到消息包立即记录时间戳 <strong>T<sub>2</sub></strong>(以Server系统时间为准)</li>
<li>Server向Client返回一个消息包，返回消息包时记录时间戳 <strong>T<sub>3</sub></strong>(以Server系统时间为准)</li>
<li>Client收到Server返回的消息包，此时记录时间戳 <strong>T<sub>4</sub></strong>(以Client系统时间为基准</li>
</ol>
</li>
<li><p>过程如下图所示：</p>
<p><img src="https://blog.whowin.net/images/180017/process-of-time-synchronization.png" alt="Time Synchronization" /></p>
</li>
</ul>
<ul>
<li>T<sub>4</sub> 和 T<sub>1</sub> 是以Client的时间标准记录的时间戳，其差 T<sub>4</sub> - T<sub>1</sub> 表示整个消息传递过程所花费的总时间；</li>
<li>T<sub>3</sub> 和 T<sub>2</sub> 是以Server的时间标准记录的时间戳，其差 T<sub>3</sub> - T<sub>2</sub> 表明消息传递过程在Server停留的时间；</li>
<li>那么 (T<sub>4</sub> - T<sub>1</sub>) - (T<sub>3</sub> - T<sub>2</sub>) 应该就是信息包的往返时间(总时间-在Server停留的时间)；</li>
<li><p><strong>如果假定信息包从Client到Server和从Server到Client所用的时间一样</strong>，那么，从Client到Server或者从Server到Client信息包的传送时间d为：</p>
<p>$$
\large \\ \\ d = {(T_4 - T_1) - (T_3 - T_2) \over 2}
$$</p></li>
<li><p>假定Client相对于Server机的时间误差是 <strong>t</strong>(t = 服务器时间戳 - 客户端时间戳)，则有下列等式：</p>
<p>$$
\begin{cases}
T_2 = T_1 + t + d \newline
T_4 = T_3 - t + d
\end{cases}
$$</p></li>
<li><p>从以上三个等式组成一个方程式：</p>
<p>$$
\begin{cases}
\large d = {(T_4 - T_1) - (T_3 - T_2) \over 2} \newline
\normalsize T_2 = T_1 + t + d \newline
T_4 = T_3 - t + d
\end{cases}
$$</p></li>
<li><p>可以解出Client机的时间误差 <strong>t</strong> 为：</p>
<p>$$
\large t = {(T_2 - T_1) + (T_3 - T_4) \over 2}
$$</p></li>
<li><p>如果一时没有转过来，可以自己在纸上画个图，再细细地琢磨一下，应该没有问题。</p>
</li>
</ul>
<h2 id="heading-3-ntpsntp">3. NTP和SNTP协议参考</h2>
<ul>
<li>这里列出这两个协议的原件下载地址，有兴趣的读者可以认真读一下；</li>
<li>NTP最新版目前是NTP v4(RFC5905)，v5还没有正是推出，这里列出NTP v4和v3(RFC1305)的链接，还有一个NTP v3的pdf文档的下载地址；</li>
<li>SNTP目前的版本是v4(RFC4330)，其实这个版本也是很久以前的，2006年的，SNTP一直没有新的版本。</li>
<li><a target="_blank" href="https://www.rfc-editor.org/rfc/rfc5905">Network Time Protocol Version 4</a></li>
<li><a target="_blank" href="https://www.rfc-editor.org/rfc/rfc1305">Network Time Protocol (Version 3)</a></li>
<li><a target="_blank" href="https://www.rfc-editor.org/rfc/rfc1305.pdf">Network Time Protocol (Version 3) pdf</a></li>
<li><a target="_blank" href="https://www.rfc-editor.org/rfc/rfc4330">Simple Network Time Protocol (SNTP) Version 4</a></li>
</ul>
<h2 id="heading-4-sntpntp">4. SNTP(NTP)协议的消息结构</h2>
<ul>
<li>下面这张图取自SNTP协议，很直观地显示出NTP消息包的结构，要注意的是，所有字段应该都是网络字节序(big endian)</li>
<li><p>NTP时间同步过程中，客户端发送的消息包结构与时间服务器返回的消息包结构是完全一样的，时间服务器会将其中的一些字段改写，然后发回；</p>

</li>
</ul>
<pre><code class="lang-plaintext">                       1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |LI | VN  |Mode |    Stratum    |     Poll      |   Precision   |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                          Root Delay                           |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                       Root Dispersion                         |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                     Reference Identifier                      |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  |                   Reference Timestamp (64)                    |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  |                   Originate Timestamp (64)                    |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  |                    Receive Timestamp (64)                     |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  |                    Transmit Timestamp (64)                    |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                 Key Identifier (optional) (32)                |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  |                                                               |
  |                 Message Digest (optional) (128)               |
  |                                                               |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
</code></pre>
<ul>
<li><p><strong>LI(Leap Indicator)</strong></p>
<ul>
<li>闰秒指示，2 bits，bit 0和bit 1</li>
<li>表示是否警告在当天的最后一分钟插入/删除一个闰秒；通常填0表示不需要警告。</li>
<li>这个值只在服务器端有意义，SNTP协议第4节中对LI可能的取值做了说明</li>
</ul>
</li>
<li><p><strong>VN(Version Number)</strong></p>
<ul>
<li>NTP/SNTP版本号，3 bits</li>
<li>这里只要填一个服务器支持的版本即可，目前最高的版本为4，填4，估计填3也不会有问题，因为绝大多数的时间服务器应该仍然支持NTP v3，以保证一些古老的客户端仍可以运行；</li>
</ul>
</li>
<li><p><strong>Mode</strong></p>
<ul>
<li>工作模式，3 bits，其取值含义如下<pre><code class="lang-plaintext">Mode  Meaning
------------------------------------
 0    reserved
 1    symmetric active
 2    symmetric passive
 3    client
 4    server
 5    broadcast
 6    reserved for NTP control message
 7    reserved for private use
</code></pre>
</li>
<li>当访问单台时间服务器同步时间时，仅取值3和4有意义，客户端发送消息时将这个字段填3，表示发送消息者是一个客户端；服务器回复消息时将这个字段改为4，表示回复消息的是服务器；</li>
</ul>
</li>
<li><p><strong>Stratum</strong></p>
<ul>
<li>表示当前时间服务器在NTP网络体系中所处的层，是一个8位无符号整数，其值通常为0-15；该字段仅在NTP服务器消息中有意义；</li>
<li>NTP 的网络体系是分层(stratum)结构，Stratum-0层设备(包括原子钟和gps钟)是最精确的，但不能通过网络向客户端授时；0级设备通常仅作为1级时间服务器的参考时钟(或同步源)；</li>
<li>Stratum-1层设备是可以通过网络授时的最准确的ntp时间源，1层设备通常通过0层参考时钟同步时间；</li>
<li>Stratum-2层设备通过网络连接从一级设备同步时间；由于网络抖动和延迟，二级服务器的时间准确度不如一级服务器；从第二层时间源同步的NTP客户端将是Stratum-3设备；</li>
<li>以此类推，层级越高，其时间的精确度和可靠度越低；</li>
<li>NTP协议不允许客户端接受来自Stratum-15以上设备的时间，因此Stratum-15是最低的NTP层；</li>
</ul>
</li>
<li><p><strong>Poll Interval</strong></p>
<ul>
<li>表示连续时间消息之间的最大间隔，以2的指数表示(比如4则间隔时间为 2<sup>4</sup>)，单位为秒，此值为一个8位无符号整数；</li>
<li>该字段仅在SNTP服务器消息中有意义，其值范围为4(16秒)到17(131,072秒——大约36小时)；</li>
</ul>
</li>
<li><p><strong>Precision</strong></p>
<ul>
<li>时间服务器的系统时钟精度，以2的指数表示(比如-10则精度 2<sup>-10</sup>)，单位为秒，此值为一个8位有符号整数；</li>
<li>此字段仅在服务器消息中有意义，其中的值范围从-6(主频时钟)到-20(某些工作站中的微秒时钟)；</li>
</ul>
</li>
<li><p><strong>Root Delay</strong></p>
<ul>
<li>表示时间服务器到主参考源的总往返延迟，以秒为单位，是一个32位有符号的定点数，小数点在第bit 15和bit 16之间；</li>
<li>该变量可以取正值，也可以取负值，取决于相对时间和频率偏移量，该字段仅在服务器消息中有意义，其值范围从几毫秒的负值到几百毫秒的正值；</li>
</ul>
</li>
<li><p><strong>Root Dispersion</strong></p>
<ul>
<li>表示相对于主要时钟参考源的的最大误差，单位为秒，是一个32位有符号的定点数，小数点在第bit 15和bit 16之间；</li>
<li>其值只能是大于0的正数；</li>
</ul>
</li>
<li><p><strong>Reference Identifier</strong></p>
<ul>
<li>用于标识特定的时间参考源，32位串；此字段仅在服务器消息中有意义；</li>
<li>对于Stratum-0和Statum-1，该字段的值为一个四字节的ASCII字符串，左对齐并以0填充到32位；</li>
<li>对于IPv4的Stratum-2时间服务器，该字段的值为时间同步源的32位IPv4地址(IP地址)；</li>
</ul>
</li>
<li><p><strong>Reference Timestamp</strong></p>
<ul>
<li>本地时钟最后一次设置或修正时的时间，64位时间戳格式。</li>
</ul>
</li>
<li><p><strong>Originate Timestamp</strong></p>
<ul>
<li>前面原理部分说到的 T<sub>1</sub>，也就是消息包从客户端发出时，客户端系统时间戳，由客户端程序填写，64位时间戳格式；</li>
</ul>
</li>
<li><p><strong>Receive Timestamp</strong></p>
<ul>
<li>前面原理部分说到的 T<sub>2</sub>，也就是服务器收到客户端消息包时，服务器端系统时间戳，由服务器端程序填写，64位时间戳格式；</li>
</ul>
</li>
<li><p><strong>Transmit Timestamp</strong></p>
<ul>
<li>前面原理部分说到的 T<sub>3</sub>，也就是服务器把数据包返回客户端时，服务器端系统时间戳，由服务器端程序填写，64位时间戳格式；</li>
</ul>
</li>
<li><p><strong>Authenticator</strong></p>
<ul>
<li>可选项，一般不填；</li>
<li>当采用NTP认证方案时，"Key Identifier"和"Message Digest"字段包含了<a target="_blank" href="https://www.rfc-editor.org/rfc/rfc1305">《RFC 1305》</a>附录C中定义的MAC(Message authentication code)信息。</li>
</ul>
</li>
<li><p>关于<strong>64位时间戳</strong></p>
<ul>
<li>前面多次提到<strong>64位时间戳</strong>，在NTP/SNTP协议中，对此有专门的定义</li>
<li>NTP时间戳表示为 64 位无符号定点数，以秒为单位，相对于1900年1月1日的0时；</li>
<li>整数部分在前32位，小数部分在后32位；在小数部分，没有意义的低位，通常要设置为0；</li>
<li>有关小数部分(Fraction Part)，其单位既不是毫秒(millisecond)，也不是微秒(microsecond)，<strong>其单位为 1/2<sup>32</sup> 秒</strong>，这一点非常重要，但却鲜有文章说明，如果不知道这一点，UNIX时间戳和NTP时间戳之间的转换就搞不明白；</li>
<li><p>下图摘自SNTP协议</p>

<pre><code class="lang-plaintext">                     1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           Seconds                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Seconds Fraction (0-padded)                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="heading-5-sntp">5. SNTP时间同步的精度分析</h2>
<ul>
<li>有很多文章中都提到，NTP时间同步的精度可以达到50ms以内，本节将讨论这个精度是如何计算得出的；</li>
<li>要注意我们讲网络时间同步的精度并不包括时间服务器本身的时间精度，只是客户端与服务器端同步时间后，客户端与服务器端相对时间误差；</li>
<li>本文第2节介绍NTP时间同步的基本原理时，曾经列出过方程式，当时有一个重要的假设为：<strong>假定信息包从Client到Server和从Server到Client所用的时间一样</strong>，这次我们去掉这个假设，重新列出方程式；</li>
<li>假定消息包从Client到Server的时长为 d<sub>1</sub>，从Server到Client的时长为 d<sub>2</sub>，d<sub>2</sub> 与 d<sub>1</sub> 的差为  δ，其它定义同前，则有下列等式：<p>$$
\begin{cases}
\delta = d_2 - d_1 \newline
T_2 = T_1 + t + d_1 \newline
T_4 = T_3 - t + d_2 \newline
\end{cases}
$$</p></li>
<li>(将1式带入3式)<p>$$
\begin{cases}
t = T_2 - T_1 - d_1 \newline
t = T_3 - T_4 + \delta + d_1 \newline
\end{cases}
$$</p></li>
<li><p>两式相加，得出</p>
<p>$$
\large t = {{(T_2 - T_1) + (T_3 - T_4)} \over 2} + {\delta \over 2}
$$</p></li>
<li><p>这个结果是精确的，没有任何假设，理论时间同步的精度为 0；</p>
</li>
<li>当 δ 为 <strong>0</strong> 时，相当于 <strong>假定信息包从Client到Server和从Server到Client所用的时间一样</strong>，得出的结果和第 2 节的结果是一样的；</li>
<li>由此可见，误差由 δ 产生，而 δ 的最大值为 d<sub>2</sub> 或者最小值为 -d<sub>1</sub>，假定Client到Server的最大时延为100ms，则 δ 的最大值为 100ms，则根据上式，其时间精度的最大误差为 δ/2，即 50ms</li>
<li>由上面的计算可以得知，NTP协议进行时间同步的精度误差主要来自数据包从Client到Server和从Server到Client的时间不一样，这个差异越大，其误差越大；</li>
<li>SNTP使用UDP协议发送时间信息包，UDP又是一种无连接的协议，从Client到Server和从Server到Client的路由很可能是不一样的，这无形中会使时间同步的精度变差；</li>
<li>由上面的分析可以看出：<strong>找到一个时延比较小的时间服务器可以有效地提高时间同步的精度</strong>。</li>
</ul>
<h2 id="heading-4sntp">4、SNTP客户端实例</h2>
<ul>
<li>说起来一大堆，但实现起来其实并不像说的那么复杂。</li>
<li>SNTP协议允许使用单播(unicast)、广播(broadcast)和多播(manycast)模式，通常我们只能使用单播模式，广播和多播模式的时间服务器只存在于某个子网中，为有限的用户服务；互联网上并不存在实际的广播或多播模式的时间服务器；</li>
<li>根据<a target="_blank" href="https://www.rfc-editor.org/rfc/rfc5905">《SNTP 协议》</a>第5节"<strong>SNTP Client Operations</strong>"的说明，使用单播模式进行时间同步时，向时间服务器发送的请求数据包中，除了第一个字节以外，其他字段都可以设为0，也可以将Originate Timestamp(T<sub>1</sub>)填在Transmit Timestamp(T<sub>4</sub>)这个字段上，时间服务器会将Transmit Timestamp(T<sub>4</sub>)字段的内容搬移到Originate Timestamp(T<sub>1</sub>)上，然后填上正确的Transmit Timestamp(T<sub>4</sub>)；</li>
<li>下面的例子中就是按照SNTP协议的说法去做的，在发送的请求包中，填了LI、VN、MODE三个字段，并把T<sub>1</sub>(Originate Timestamp)填在了T<sub>4</sub>(Transmit Timestamp)上，从时间服务器返回的数据看，完全印证了SNTP协议中的说法；</li>
<li>要注意的是，ntp数据包中的各个字段都是网络字节序(big endian)，而我们使用的电脑都是主机字节序(little endian)，所以相互之间要做转换；</li>
<li>再次强调一下，NTP时间戳的fraction字段的单位是 <strong>1/2<sup>32</sup> 秒</strong>；</li>
<li><p>源程序文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180017/sntp-client.c">sntp-client.c</a>(<strong>点击文件名下载源文件</strong>)</p>
</li>
<li><p>编译：<code>gcc -Wall sntp-client.c -o sntp-client -lm</code>，因为其中使用了数学函数，所以编译时要加上 "-lm"</p>
</li>
<li>运行：<code>./sntp-client ntp.aliyun.com</code></li>
<li><p>运行截图：</p>
<p><img src="https://blog.whowin.net/images/180017/screenshot-of-sntp-client.png" alt="screenshot of sntp_client" /></p>
</li>
<li><p>在源程序中，列出了几个时间服务器，可以通过百度或者谷歌找更多的时间服务器进行尝试。</p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>

]]></content:encoded></item><item><title><![CDATA[简单的路由表查找程序]]></title><description><![CDATA[在Linux操作系统中，内核中有一个路由表，它包含若干条路由记录，这些记录由子网IP、子网掩码、网关IP和接口名等组成，这些信息用于将数据包转发到其他子网或者连接到互联网；本文介绍了当需要转发数据包时，Linux内核查找路由表的基本算法，并用程序模拟了这个过程，附有完整的源代码。本文对网络编程的初学者难度不大。
当我们在Linux系统下发送一个报文时，Linux需要确定路由，也就是将这个报文转发到哪个网络接口下的哪个设备上去，一个连接在网络上的Linux系统至少有两个网络接口，一个是网卡(有线或...]]></description><link>https://whowin.cn/180016-longest-prefix-match</link><guid isPermaLink="true">https://whowin.cn/180016-longest-prefix-match</guid><category><![CDATA[Linux]]></category><category><![CDATA[routing]]></category><category><![CDATA[longest prefix match]]></category><category><![CDATA[fib_lookup]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Fri, 10 Feb 2023 06:44:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676270511435/976361c8-3799-4985-b9ad-151f05f1b972.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>在Linux操作系统中，内核中有一个路由表，它包含若干条路由记录，这些记录由子网IP、子网掩码、网关IP和接口名等组成，这些信息用于将数据包转发到其他子网或者连接到互联网；本文介绍了当需要转发数据包时，Linux内核查找路由表的基本算法，并用程序模拟了这个过程，附有完整的源代码。本文对网络编程的初学者难度不大。</p>
<p>当我们在Linux系统下发送一个报文时，Linux需要确定路由，也就是将这个报文转发到哪个网络接口下的哪个设备上去，一个连接在网络上的Linux系统至少有两个网络接口，一个是网卡(有线或者无线网卡)，一个loopback，Linux从报文中的IP报头中获得目的IP地址，以这个目的IP地址为依据从系统内部的路由表中找到一条最适合的路由，然后将报文转发到这个路由上，在查找路由表的过程中会使用一个叫做 <strong>最长前缀匹配(Longest Prefix Match)</strong> 的算法来确定路由；本文将简要介绍Linux系统中的路由表、路由策略以及路由决策的过程，介绍<strong>最长前缀匹配(Longest Prefix Match)</strong>算法，并提供一个完整的源代码来模拟这个算法在路由查找中的应用。</p>
</blockquote>
<h2 id="heading-1">1. 路由表</h2>
<ol>
<li><p><strong>Linux系统的路由表</strong></p>
<ul>
<li>使用命令 <code>cat /etc/iproute2/rt_tables</code> 看一下当前系统中有那些路由表；</li>
<li><p>下面是在我的机器上的执行结果</p>
<p><img src="https://blog.whowin.net/images/180016/screenshot-cat-rt_tables.png" alt="routing tables" /></p>
</li>
</ul>
<hr />
<ul>
<li>每个路由表除了有个名称外还有一个ID号，就是上面显示的 255、254、253 和 0；</li>
<li>可以使用命令 <code>ip route show table &lt;table id&gt;/&lt;table name&gt;</code> 显示一个路由表；</li>
<li>通常，一台机器上至少有四个路由表<ol>
<li>0号路由表(unspec)，由系统保留</li>
<li>253号表(default)，默认路由表</li>
<li>254号表(main)，主路由表，这个是主要使用的，用户可以设置</li>
<li>255号表(local)，本地路由表，存储本地接口地址、广播地址、NAT地址，由系统维护，用户不能更改。</li>
</ol>
</li>
<li><p>尽管有4个路由表，但有的表可以是空的，比如在我的机器上，默认路由表就是空的</p>
<p><img src="https://blog.whowin.net/images/180016/screenshot-default-routing-table.png" alt="default routing table" /></p>
</li>
</ul>
<hr />
<ul>
<li>我的另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/129409890">《linux下使用netlink获取gateway的IP地址》</a>中，程序中使用netlink获取了内核路由表，在程序中解析内核发回的信息时，使用了宏 <strong>RT_TABLE_MAIN</strong> 就是路由表的ID号，RT_TABLE_MAIN 显然指的是主路由表(main)，所以它的实际值是 254；</li>
</ul>
</li>
<li><p><strong>Linux系统的路由策略</strong></p>
<ul>
<li>Linux系统有一个路由策略数据库，Routing Policy Database，简称 <strong>RPDB</strong></li>
<li>使用命令 <code>ip rule list</code> 或者命令 <code>ip rule show</code> 可以显示当前系统中的RPDB；</li>
<li><p>下面是在我的机器上的执行结果</p>
<p><img src="https://blog.whowin.net/images/180016/screenshot-routing-policy.png" alt="routing policy" /></p>
</li>
</ul>
<hr />
<ul>
<li>可以看到，我的系统RPDB中有三条路由规则，这也是Linux启动时设置的默认规则，前面的数字表示优先级，0 是最高优先级<ol>
<li>rule 0：查询local路由表(ID 255)，查找与目的地址匹配的路由，rule 0非常特殊，不能被删除或者覆盖；</li>
<li>rule 32766：查询main路由表(ID 254)，该表是最常用的表，通常main路由表中有一个默认路由，如果没有更具体的路由，将匹配这个默认路由，系统管理员可以删除或者使用另外的规则覆盖这条规则；</li>
<li>rule 32767：查询default路由表(ID 253)，该表目前是一个空表，为今后的路由处理保留，前面的策略没有匹配到的数据包，系统使用这个策略进行处理，这个规则也可以删除。</li>
</ol>
</li>
<li>由此可以看到，local路由表的优先级要高于main路由表。</li>
</ul>
</li>
</ol>
<h2 id="heading-3">3. 路由查找过程</h2>
<ul>
<li>基于前面对 RPDB 和多路由表的简单介绍，可以简单了解内核查找路由的过程；</li>
<li>在需要确定一个目的IP地址的路由时，Linux内核首先在路由缓存(routing cache)中查找，路由缓存是一个哈希表，用于快速查询最近使用过的路由，如果在路由缓存中找到路由，则使用该路由转发报文；</li>
<li>如果在路由缓存中没有找到路由，Linux内核将开始按照优先级遍历RPDB中的策略，对于RPDB中的每个匹配项，内核将使用<strong>最长前缀匹配算法</strong>在指定的路由表中查找目的IP地址的匹配路由，如果找不到匹配的路由，内核将转到RPDB中的下一个规则，直到找到匹配项，或者查找失败；</li>
</ul>
<h2 id="heading-4-longest-prefix-match">4. 最长前缀匹配(Longest Prefix Match)算法</h2>
<ul>
<li><p>命令 <code>route -n</code> 可以用比较整齐的方式显示主路由表(main)</p>
<p><img src="https://blog.whowin.net/images/180016/screenshot-of-main-routing-table.png" alt="main routing table" /></p>
</li>
</ul>
<hr />
<ul>
<li>可以看到一个路由项有：子网IP、网关、子网掩码、网络接口等，当给定一个目的IP，比如为：192.168.2.114，与路由表中第3和第4条都匹配，那么如何确定匹配路由呢？</li>
<li>这个<strong>最长前缀匹配</strong>的算法就是解决这个匹配问题的，这个算法说的是，当遇到上面这种问题(即目的IP匹配多条路由)时，选择子网掩码最长的那个匹配项，上面这个问题，第3条路由的子网掩码为：255.255.0.0，长度为16位(255位8个二进制的1)，第4条路由的子网掩码为255.255.255.0，长度为24位，所以应该匹配路由表中的第4项；</li>
<li><p>我们把上面的例子说的再仔细一点</p>
<p><img src="https://blog.whowin.net/images/180016/example.png" alt="Example" /></p>
</li>
</ul>
<hr />
<ul>
<li>尽管目的地址192.168.2.114也与第1条路由192.168.0.0匹配，但只匹配了16位(图示蓝色部分)，而与第2条路由匹配了24位(图示红色部分)，按照<strong>最长前缀匹配</strong>规则，应与第2条路由匹配；</li>
<li>所谓匹配指的是IP地址与路由的子网掩码做"与"操作后，与原来IP仍然相同的位数，192.168.2.114与255.255.0.0做"与"操作后成为192.168.0.0，只有前面的192.168与原IP地址相同，我们称之为匹配了16位。</li>
</ul>
<h2 id="heading-5">5. 最长前缀匹配的具体实现</h2>
<ul>
<li>尽管这个算法很简单，但在具体实现中并不会真的去拿目的IP与和路由表中的每条路由去比较，看看匹配多少位，然后取一条位数最多的路由，这样做显得太笨了一点；</li>
<li>下面这段程序对最长前缀匹配做了一个简单的实现，文件routing.txt将用于模拟路由表，程序会把这个文件读入并生成一个路由表，我们可以按照格式编辑这个文件以使其可以模拟我们希望看到的路由情况；</li>
<li>文件routing.txt格式说明<ol>
<li>第一行为表头，说明每一列的含义</li>
<li>每行一条路由，每条路由只取：接口名称、子网、网关和子网掩码四个字段，字段间使用','分隔，各字段允许前后有空格</li>
</ol>
</li>
<li><p>routing.txt内容</p>
<pre><code class="lang-plaintext">ifname   destination   gateway        netmask
eth1,    192.168.0.0,  0.0.0.0,       255.255.0.0
eth2,    0.0.0.0,      192.168.2.3,   0.0.0.0
eth3,    169.254.0.0,  0.0.0.0,       255.255.0.0
eth4,    169.254.3.0,  0.0.0.0,       255.255.255.0
eth5,    192.168.0.0,  0.0.0.0,       255.255.255.0
</code></pre>
<ul>
<li>在路由表中，gateway为0.0.0.0表示不需要经过网关；destination为0.0.0.0表示任意IP地址；</li>
<li>当netmask为0.0.0.0而destination也为0.0.0.0时，任何IP地址与netmask做"与"操作后都将与destination匹配。</li>
</ul>
<p><img src="https://blog.whowin.net/images/180016/content-of-routing-txt.png" alt="routing.txt" /></p>
</li>
</ul>
<hr />
<ul>
<li><p>下面源程序中，最长前缀匹配的执行步骤</p>
<ol>
<li>读取文件routing.txt作为路由表，并为路由表建立路由链表，链表的每个节点表示一条路由；</li>
<li>路由读入内存时，将所有IP和子网掩码转换成32bit整数(网络字节序，big-endian)存放；</li>
<li>将路由按照子网IP(主机字节序，little-endian)进行逆排序(数值大的排在前面)，如果两个子网IP相同，则子网掩码(主机字节序，little-endian)大的排在前面；</li>
<li>从命令行读入要查找路由的目的IP地址，并将其转换成32bit整数(网络字节序，big-endian)</li>
<li>从路由链表开头开始遍历链表，将目的IP地址(32bit整数)，与路由的子网掩码(32bit整数)进行与操作，如果结果与该路由的子网IP相同，则认为已经匹配到路由，程序结束。</li>
</ol>
</li>
<li><p>使用链表存放路由是因为我们并不知道路由表中有多少条路由，不好分配内存，采用链表可以读到一条路由，分配一块内存，不会因为内存分配太少导致无法把全部路由读入内存，而且遍历一个链表也很方便；</p>
</li>
<li>在链表中存放的32bit的IP都是以网络字节序存储的，这是因为当使用inet_addr()将一个"数字、点表示法"的IP地址转换成32bit整数时，结果就是网络字节序的；</li>
<li><p>程序中对链表做了逆排序，可以大大提高查找速度；</p>
<ol>
<li>通常情况下，在Linux的主路由表中都有一条默认网关的路由，一般子网和掩码都是0.0.0.0，加上网关IP，这个路由是和所有IP地址匹配的，进行逆排序后，这条路由将排在链表的最后，当所有路由均无法匹配时，将很自然地匹配最后一条路由，不需要在遍历路由链表时因为会遇到这条路由而进行更多的判断；</li>
<li><strong>最长前缀匹配</strong>指出，当有多条路由匹配时，应匹配子网掩码最长的路由，所以我们在进行排序时，如果遇到两条路由的子网IP相同，将把子网掩码大的(按主机字节序比较，little-endian)排在前面，这样做的好处在于，程序匹配到的第一条路由一定是子网掩码最长的路由，无需再做判断；</li>
</ol>
<blockquote>
<p>试想这样两条路由，<code>192.168.0.0/255.255.0.0</code> 和 <code>192.168.0.0/255.255.255.0</code>，这两条路由只有子网掩码不同，第1条路由表示 <code>192.168.*.*</code> 的走这条路由，第2条路由表示 <code>192.168.0.*</code> 的走这条路由，当目的IP为 <code>192.168.0.10</code> 时，应该是要走第2条路由的，经过我们的排序后，遍历链表时最先匹配的路由一定是第2条路由，这个例子大致说明了链表排序的意义。</p>
</blockquote>
</li>
<li><p>源代码，文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180016/rt-lookup.c">rt-lookup.c</a>(<strong>点击文件名下载源文件</strong>)</p>
</li>
<li><p>实际的路由表要比我们这个复杂的多，路由决策的因素也不仅仅是所谓的'最长前缀匹配'。 </p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>


]]></content:encoded></item><item><title><![CDATA[使用raw socket发送magic packet]]></title><description><![CDATA[Magic Packet是进行网络唤醒的数据包，将这个数据包以广播的形式发到局域网上，与数据包中所关联的MAC相同的电脑就会被唤醒开机，通常我们都是使用UDP报文的形式来发送这个数据包，但实际上在进行网络唤醒的时候，只要报文中包含Magic Packet应该就可以唤醒相关的计算机，与IP协议、UDP协议没有任何关系，本文将试图抛开网络层(IP层)和传输层(TCP/UDP层)，直接在数据链路层发出Magic Packet，并成功实现网络唤醒，本文将提供完整的源代码。阅读本文需要有较好的网络编程基础...]]></description><link>https://whowin.cn/180015-send-magic-packet-via-raw-socket</link><guid isPermaLink="true">https://whowin.cn/180015-send-magic-packet-via-raw-socket</guid><category><![CDATA[Linux]]></category><category><![CDATA[raw socket]]></category><category><![CDATA[magic packet]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Thu, 09 Feb 2023 04:37:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676003731651/a0ddcaf6-aedc-4098-9863-c306da37e83d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Magic Packet是进行网络唤醒的数据包，将这个数据包以广播的形式发到局域网上，与数据包中所关联的MAC相同的电脑就会被唤醒开机，通常我们都是使用UDP报文的形式来发送这个数据包，但实际上在进行网络唤醒的时候，只要报文中包含Magic Packet应该就可以唤醒相关的计算机，与IP协议、UDP协议没有任何关系，本文将试图抛开网络层(IP层)和传输层(TCP/UDP层)，直接在数据链路层发出Magic Packet，并成功实现网络唤醒，本文将提供完整的源代码。阅读本文需要有较好的网络编程基础，本文对网络编程的初学者有一定难度。</p>
</blockquote>
<h2 id="heading-1-magic-packet">1. Magic Packet</h2>
<ul>
<li>我比较喜欢把这个数据包称作"网络唤醒包"；有很多地方把它翻译成"魔术包"或者"魔法数据包"，我个人觉着太过表面，无法表达其实际的含义；本文将这个数据包称为"网络唤醒包"或者"Magic Packet"，二者具有完全相同的含义；</li>
<li>以前写过一篇与嵌入式相关的文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/126502495">《远程开机：一个简单的嵌入式项目开发》</a>，在嵌入式环境下使用Magic Packet进行远程开机的小项目，有兴趣的读者可以参考；</li>
<li><code>Magic Packet</code> 就是一个指定格式的数据包，其格式为：6 个 <strong>0xff</strong>，然后16组需要被网络唤醒的电脑的 <strong>MAC</strong> 地址，比如需要被唤醒的电脑的 MAC 为：<code>00:e0:2b:69:00:03</code>，则 <code>Magic Packet</code> 为(16进制表述)：<pre><code class="lang-plaintext">  ff ff ff ff ff ff 
  00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 
  00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 
  00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 
  00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03
</code></pre>
</li>
<li>关于 <em>Magic Packet</em> 的更多的信息请参考<ul>
<li><a target="_blank" href="https://zh.wikipedia.org/zh-cn/%E7%B6%B2%E8%B7%AF%E5%96%9A%E9%86%92">wikipidia-网络唤醒</a></li>
<li><a target="_blank" href="https://en.wikipedia.org/wiki/Wake-on-LAN">wikipedia-Wake-On-Lan</a></li>
</ul>
</li>
<li>对Magic Packet也没有什么好解释的，理论上只要一个报文中存在这个Magic Packet，那么有网络唤醒功能的NIC都会相应，What is NIC? NIC就是网卡，Network Interface Controller；</li>
<li>但是大多数的 802.11 的无线网卡是收不到 Magic Packet 的，这一点在<a target="_blank" href="https://en.wikipedia.org/wiki/Wake-on-LAN">wikipedia-Wake-On-Lan</a>上有明确的说明；所以不要尝试在无线网卡上做网络唤醒，但是有一个叫做<strong>WoWLAN</strong>的标准是专门支持无线网卡的网络唤醒的，以后有时间的时候试一下；</li>
<li>通常发送Magic Packet是使用UDP广播的方式发，这方面的文章很多，有兴趣的读者可以百个度或者谷个歌去找一下，我的另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/126502495">《远程开机：一个简单的嵌入式项目开发》</a>也是用这种方式发的，这里就不赘述了；</li>
</ul>
<h2 id="heading-2">2. 相关技术要点</h2>
<blockquote>
<p>以下要点将会在本文的范例程序中用到，在此需要简单回顾以下。</p>
</blockquote>
<ul>
<li><p><strong>TCP/IP的五层网络模型</strong>(OSI 七层架构的简化版)</p>
<ol>
<li>应用层</li>
<li>传输层(TCP/UDP)</li>
<li>网络层(IP)</li>
<li>数据链路层(Ethernet)</li>
<li>物理层</li>
</ol>
</li>
<li><p><strong>基于TCP/IP的数据报文的结构</strong></p>
<ul>
<li>一个基于TCP/IP的完整报文结构为：以太网报头 + IP报头 + TCP/UDP报头 + data，如下图：</li>
</ul>
<p><img src="https://blog.whowin.net/images/180002/sending_data_from_app_with_socket.png" alt="Packet structure based on TCP/IP" /></p>
<hr />
</li>
<li><p><strong>以太网报头</strong></p>
<ul>
<li>以太网报头定义在头文件中：<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ethhdr</span> {</span>
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span>  h_dest[ETH_ALEN];    <span class="hljs-comment">/* destination eth addr  */</span>
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span>  h_source[ETH_ALEN];  <span class="hljs-comment">/* source ether addr  */</span>
    __be16         h_proto;             <span class="hljs-comment">/* packet type ID field  */</span>
} __attribute__((packed));
</code></pre>
</li>
<li>h_dest字段为目的MAC地址，h_source字段为源MAC地址；</li>
<li>h_proto表示当前数据包在网络层使用的协议，Linux支持的协议在头文件中定义；通常在网络层使用的IP协议，这个字段的值是0x0800(ETH_P_IP)；</li>
<li>但是本文中的 <strong>h_proto</strong> 字段要填写 <strong>0x842</strong>，很遗憾这个协议在头文件中没有定义，也基本找不到相关资料；</li>
</ul>
</li>
<li><p><strong>raw socket</strong></p>
<ul>
<li>可以参考我的另两篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128766145">《Linux下如何在数据链路层接收原始数据包》</a>和<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128891255">《如何使用raw socket发送UDP报文》</a>，这里仅做一个简单回顾；</li>
<li>打开一个raw socket<pre><code class="lang-C"><span class="hljs-keyword">int</span> sock_raw;
sock_raw = socket(AF_PACKET, SOCK_RAW, <span class="hljs-number">0</span>);
</code></pre>
</li>
<li>第三个参数还可以有其它选择，这个参数往往会对socket的接收产生影响，本文并不接收任何信息，所以对本文来说无关紧要。</li>
</ul>
</li>
<li><p><strong>struct sockaddr_ll</strong></p>
<ul>
<li>这个结构在中定义，有关该结构的详细说明请参考其它文章，本文仅就相关字段做出说明；</li>
<li>这个结构与 IPv4 socket 编程中的结构(struct sockaddr_in)的作用类似，是用在raw socket上的一个地址结构，烦请自行理解，其中'll'表示Low Level<pre><code class="lang-C"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_ll</span> {</span>
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">short</span>  sll_family;
    __be16          sll_protocol;
    <span class="hljs-keyword">int</span>             sll_ifindex;
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">short</span>  sll_hatype;     <span class="hljs-comment">// Hardware Address Type</span>
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span>   sll_pkttype;    <span class="hljs-comment">// Packet Type</span>
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span>   sll_halen;      <span class="hljs-comment">// Hardware Address Length</span>
    <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span>   sll_addr[<span class="hljs-number">8</span>];    <span class="hljs-comment">// Address(Hardware Address)</span>
};
</code></pre>
</li>
<li>sll_family为协议族，和建立raw socket是使用的协议族要一致，所以肯定是AF_PACKET；</li>
<li>sll_protocol是标准的以太网协议类型，定义在头文件中，通常情况下应该 ETH_P_IP(0x800) 表示IP协议，本文要填 <strong>0x842</strong>；</li>
<li>sll_ifindex是网络接口的索引号，我们可以根据接口名称使用ioctl获得；</li>
<li>sll_halen是硬件地址(MAC)的长度，ha是Hardware Address的意思，填常数 ETH_ALEN(定义在头文件中)；</li>
<li>sll_addr是目的MAC地址</li>
<li>实际上，在发送数据时，由于sll_family和sll_protocol都是和socket中一样的，所以大多数情况下都可以不填，只要填sll_ifindex、sll_halen和sll_addr即可，但是本文的例子中，如果使用bind()绑定地址，应该尽量完整地填写，否则执行bind()是会出错。</li>
</ul>
</li>
</ul>
<h2 id="heading-3-magic-packet">3. 在数据链路层发送Magic Packet</h2>
<ul>
<li>先说一下目标，通常使用UDP发送Magic Packet的报文结构为：以太网报头 + IP报头 + UDP报头 + Magic Packet，我们的目标是：<strong>以太网报头 + Magic Packet</strong></li>
<li><p>先看源程序，文件名为：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180015/magic-packet.c">magic-packet.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li><p>报文是以广播的形式发出去的，发送广播时，以太网报头的目的MAC要全部填写0xff，(struct sockaddr_ll)中的sll_addr也要全部填写0xff；</p>
</li>
<li>尽管我们知道目的MAC，但是这个报文必须以广播报文发出，因为此时被唤醒的机器并没有开机，所以无法通过arp获知填写的MAC地址是否在局域网内，所以如果在以太网报头的h_dest中填上了要被唤醒的机器的MAC地址，报文是无法送达的；</li>
<li>关于使用ioctl获取接口索引号(Interface Index)和接口MAC的问题，请自行查找资料，或者参考文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/128891255">《如何使用raw socket发送UDP报文》</a>，这篇文章中有部分内容涉及到这个问题；</li>
<li>程序中写好了用send()或者用sendto()发送报文的代码，使用一个常量USING_SENDTO来控制，当USING_SENDTO为1时，将使用sendto()发送报文，否则使用send()发送报文；</li>
<li>如果使用send()发送报文，需要使用bind()绑定目的地址；</li>
<li>具体实践中，如果使用sendto()发送报文，(struct sockaddr_ll)中只需填写sll_ifindex即可，这个也许和运行环境有关，请自行测试；理论上说，只要编译通过，运行没有出错，就表示这个Magic Packet发送了出去，应该就可以起作用；</li>
<li>源程序中有详细的注释，其它也没有什么更多解释的。</li>
<li><strong>编译</strong>：<code>gcc -Wall magic-packet.c -o magic-packet</code></li>
<li><strong>运行</strong>：<code>sudo ./magic-packet enp0s3 00:e0:2b:68:00:03</code></li>
<li>由于使用了raw socket，所以必须以<strong>root</strong>权限运行；</li>
<li>如果你填写的ifname和mac都正确的话，mac所对应的电脑应该被远程唤醒；</li>
<li>这个程序并不好调试，因为能够在数据链路层侦听的工具不多，加上使用的协议号为0x842，使得大多数工具都无法使用，<strong>wireshark应该是可以的</strong>，而且<a target="_blank" href="https://wiki.wireshark.org/WakeOnLAN">wireshark的wiki</a>上说，它有专门针对0x842号协议的侦听，不过我没有试过，有感兴趣的读者可以试一下；</li>
<li>远程唤醒是需要硬件支持的，主板和网卡都要支持，但是目前绝大多数有线网卡都应该是支持的，但是可能要在BIOS和其它地方做一些设置，请自行搜索相关资料，并参考我的另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/126502495">《远程开机：一个简单的嵌入式项目开发》</a></li>
<li><p>运行结果截图：</p>
<p><img src="https://blog.whowin.net/images/180015/screenshot-magic-packet.png" alt="Screenshot magic_packet" /></p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>


]]></content:encoded></item><item><title><![CDATA[从proc文件系统中获取gateway的IP地址]]></title><description><![CDATA[在linux的命令行下获取当前网络环境的gateway的IP并不是一件难事，常用的命令有ip route或者route -n，route -n是通过读取proc文件系统下的文件来从内核获取路由表的，但ip route是通过netlink来获取的路由表；本文将讨论如何编写程序从proc文件系统中获取路由表，并从路由表中获取gateway的IP地址，文章最后给出了完整的源程序，本文对初学者基本没有难度。

1. 为什么要获取网关的IP地址

以前写过一些与raw socket有关的文章，在使用raw...]]></description><link>https://whowin.cn/180008-get-gateway-ip-from-proc-filesys</link><guid isPermaLink="true">https://whowin.cn/180008-get-gateway-ip-from-proc-filesys</guid><category><![CDATA[Linux]]></category><category><![CDATA[gateway]]></category><category><![CDATA[proc filesystem]]></category><dc:creator><![CDATA[whowin]]></dc:creator><pubDate>Sun, 05 Feb 2023 05:59:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1675835859925/de0ea0c2-3533-45dc-8b81-178681bbf8d8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>在linux的命令行下获取当前网络环境的gateway的IP并不是一件难事，常用的命令有<code>ip route</code>或者<code>route -n</code>，<code>route -n</code>是通过读取proc文件系统下的文件来从内核获取路由表的，但<code>ip route</code>是通过netlink来获取的路由表；本文将讨论如何编写程序从proc文件系统中获取路由表，并从路由表中获取gateway的IP地址，文章最后给出了完整的源程序，本文对初学者基本没有难度。</p>
</blockquote>
<h2 id="heading-1-ip">1. 为什么要获取网关的IP地址</h2>
<blockquote>
<p>以前写过一些与raw socket有关的文章，在使用raw socket发送报文的时候，有时是需要自己构建以太网报头的，以太网报头中是要填写源地址和目的地址的MAC地址的，源地址的MAC地址就是本机的MAC地址，可以使用ioctl()获得，但是目的地址的MAC地址就不好办了，如果是局域网内，我们可以通过arp获取目的地址的MAC地址，因为我们是知道目的地址的IP的，但是如果目的地址在局域网外，我们就要在以太网报头中填写gateway的MAC地址，这时候我们就一定要知道<strong>gateway的IP地址</strong>，然后通过arp cache获取gateway的MAC地址；</p>
<p>获得了gateway的IP地址以后，可以很容易地从本地arp cache中获得MAC地址的，因为gateway的MAC地址一定会在本地arp cache中，有关如何在操作arp cache，可以参考我的另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/129791465">《如何用C语言操作arp cache》</a></p>
</blockquote>
<h2 id="heading-2-procgatewayip">2. 从proc文件系统中获取gateway的IP</h2>
<ul>
<li>Linux下的proc文件系统是一个虚拟文件系统，所谓虚拟文件系统指的是它并不是一个存放在硬盘上的目录，而是内核建立的用于让用户空间了解内核运行状态或者调试的运行时的文件系统；</li>
<li><p>内核的路由表存放在 <code>/proc/net/route</code> 文件下，在终端上用命令<code>cat /proc/net/route</code>可以很容易地打印出路由表；下面是在我的机器上看到的结果：</p>
<p><img src="https://blog.whowin.net/images/180008/cat-proc-net-route.png" alt="routing table" /></p>
</li>
</ul>
<hr />
<ul>
<li><strong>/proc/net/route文件的结构</strong><ul>
<li>第一行为表头，显示每一列的名称，从第二行起为路由数据，每行代表一条路由；</li>
<li>每列之间的分隔符为TAB(\t)；</li>
<li>第1列：Iface，为接口名称(Interface Name);</li>
<li>第2列：Destination，为目标网段或者目标主机的IP，以字符串表达的一个16进制的32位(4字节)数字(比如0X0103A8C0将表示为字符串"0103A8C0")，把这个字符串按照16进制转换成一个32位数字，则表达着一个IP地址；</li>
<li>第3列：Gateway，为gateway的IP，与第2列的表达形式一样；</li>
<li>第4列：Flags，为一个标志，每一位代表一个标志，定义在linux/route.h中，如下：<pre><code class="lang-C"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_UP          0x0001        <span class="hljs-comment">/* route usable                 */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_GATEWAY     0x0002        <span class="hljs-comment">/* destination is a gateway     */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_HOST        0x0004        <span class="hljs-comment">/* host entry (net otherwise)   */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_REINSTATE   0x0008        <span class="hljs-comment">/* reinstate route after tmout  */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_DYNAMIC     0x0010        <span class="hljs-comment">/* created dyn. (by redirect)   */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_MODIFIED    0x0020        <span class="hljs-comment">/* modified dyn. (by redirect)  */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_MTU         0x0040        <span class="hljs-comment">/* specific MTU for this route  */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_MSS         RTF_MTU       <span class="hljs-comment">/* Compatibility :-(            */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_WINDOW      0x0080        <span class="hljs-comment">/* per route window clamping    */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_IRTT        0x0100        <span class="hljs-comment">/* Initial round trip time      */</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> RTF_REJECT      0x0200        <span class="hljs-comment">/* Reject route                 */</span></span>
</code></pre>
</li>
<li>看着挺多，但其实常用的很少，就是前三个，<strong>RTF_UP</strong>表示该路由可用，<strong>RTF_GATEWAY</strong>表示该路由为一个网关，组合在一起就是3，表示一个可用的网关；</li>
<li>第5列：RefCnt，为该路由被引用的次数，Linux内核中没有使用这个；</li>
<li>第6列：Use，该路由被查找的次数；</li>
<li>第7列：Metric，到目的地的"距离"，通常以"跳数"表示，所谓"跳数"可以理解为要经过的网关的数量，实际数字并不准确；</li>
<li>第8列：Mask，网络掩码；</li>
<li>第9列：MTU，路由上可以传送的最大数据包；</li>
<li>第10列：Window，TCP窗口的大小，只在AX.25网络上使用</li>
<li>第11列：IRTT，通过这条路由的初始往返时间，只在AX.25网络上使用；</li>
<li>可以使用在线手册 <code>man route</code> 了解更详细的信息；</li>
<li>当Destination为"00000000"时，表示任意目的地址；</li>
<li>当Gateway为"00000000"时，表示不需要经过网关，比如本地局域网中的目的地址，Gateway字段应该是"00000000"。</li>
</ul>
</li>
</ul>
<h2 id="heading-3-ip-routeroute-n">3. ip route和route -n是如何获取路由表的</h2>
<ul>
<li><p>使用命令 <code>ip route</code> 可以获得路由表，下面是运行截图：</p>
<p><img src="https://blog.whowin.net/images/180008/screenshot-of-ip-route.png" alt="screenshot of executing ip route" /></p>
</li>
</ul>
<hr />
<ul>
<li><code>ip route</code> 是使用netlink获取路由表的，我们可以使用strace命令跟踪 <code>ip route</code> 的执行，然后在输出中查找 <strong>/proc/net/route</strong> 和<strong>RTM_GETROUTE</strong>，从而确定这个命令是如何获得路由表的</li>
<li><p>如果不清楚宏<strong>RTM_GETROUTE</strong>的含义，请参考另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/129409890">《linux下使用netlink获取gateway的IP地址》</a></p>
<pre><code class="lang-bash">strace -o ip.txt ip route
grep RTM_GET_ROUTE ip.txt
grep /proc/net/route ip.txt
</code></pre>
<ul>
<li><code>-o</code> 选项表示跟踪输出结果写入文件ip.txt中，然后从文件ip.txt中分别查找 <code>RTM_GETROUTE</code> 和 <code>/proc/net/route</code></li>
<li><p>下面是运行结果的截图</p>
<p><img src="https://blog.whowin.net/images/180008/screenshot-of-tracing-ip-route.png" alt="trace ip route" /></p>
</li>
</ul>
<hr />
<ul>
<li>可以很清晰第看到，在执行 <code>ip route</code> 的过程中，使用RTM_GETROUTE作为参数向socket上发送了一条获取路由表的信息，而找不到关于文件 <code>/proc/net/route</code> 的任何信息，由此可以肯定 <code>ip route</code> 是使用netlink获取的路由表；</li>
<li>有关使用netlink获取gateway IP的方法，可以参考另一篇文章<a target="_blank" href="https://blog.csdn.net/whowin/article/details/129409890">《linux下使用netlink获取gateway的IP地址》</a></li>
</ul>
</li>
<li><p>使用命令<code>route -n</code>也是可以获得路由表的，下面是运行截图：</p>
<p><img src="https://blog.whowin.net/images/180008/screenshot-of-route-n.png" alt="screenshot of route -n" /></p>
</li>
</ul>
<hr />
<ul>
<li><p><code>route -n</code> 是通过读取proc文件系统中的文件 <code>/proc/net/route</code> 来获取路由表的，我们用同样的方法来跟踪一下 <code>route -n</code> 的运行情况：</p>
<pre><code class="lang-bash">strace -o route.txt route -n
grep RTM_GET_ROUTE route.txt
grep /proc/net/route route.txt
</code></pre>
<ul>
<li><p>下面是运行结果截图</p>
<p><img src="https://blog.whowin.net/images/180008/screenshot-of-tracing-route-n.png" alt="trace route -n" /></p>
</li>
</ul>
</li>
</ul>
<hr />
<ul>
<li>跟踪结果告诉我们在执行 <code>route -n</code> 时，文件 <code>/proc/net/route</code> 被读取，但是却没有找到使用 RTM_GETROUTE 从内核获取路由表的迹象，由此可以断定 <code>route -n</code> 是通过proc文件系统获取的路由表。</li>
</ul>
<h2 id="heading-4-procgatewayip">4. 从proc文件系统中获取gateway的IP地址</h2>
<ul>
<li>内核将路由表放在proc文件系统下，文件名为：/proc/net/route</li>
<li><p>读取文件<code>/proc/net/route</code>即可获得路由表，从中找到gateway这一行就可以了，按以下步骤：</p>
<ol>
<li><p>打开文件/proc/net/route</p>
<pre><code class="lang-C"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> ROUTING_TABLE   <span class="hljs-meta-string">"/proc/net/route"</span></span>

FILE *fp = fopen(ROUTING_TABLE, <span class="hljs-string">"r"</span>);
</code></pre>
</li>
<li><p>读取一行不做任何处理，跳过路由表的表头行；</p>
<pre><code class="lang-C"><span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> BUF_SIZE        128</span>

<span class="hljs-keyword">char</span> line[BUF_SIZE];
fgets(line, BUF_SIZE, fp);
</code></pre>
</li>
<li><p>每次读取一行，检查其是否为gateway的记录，如果是，将gateway字段转换成32位的IP地址再转换成字符串；</p>
<pre><code class="lang-C"><span class="hljs-keyword">char</span> *ifname, *dest_ip, *gw_ip;
<span class="hljs-keyword">while</span> (fgets(line, BUF_SIZE, fp)) {
   ifname  = strtok(line , <span class="hljs-string">"\t"</span>);
   dest_ip = strtok(<span class="hljs-literal">NULL</span> , <span class="hljs-string">"\t"</span>);
   gw_ip   = strtok(<span class="hljs-literal">NULL</span> , <span class="hljs-string">"\t"</span>);

   <span class="hljs-keyword">if</span> (ifname != <span class="hljs-literal">NULL</span> &amp;&amp; dest_ip != <span class="hljs-literal">NULL</span>) {
       <span class="hljs-keyword">if</span> (<span class="hljs-built_in">strcmp</span>(dest_ip, <span class="hljs-string">"00000000"</span>) == <span class="hljs-number">0</span>) {
           <span class="hljs-keyword">if</span> (gw_ip) {
               <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">in_addr</span> <span class="hljs-title">addr</span>;</span>
               <span class="hljs-built_in">sscanf</span>(gw_ip, <span class="hljs-string">"%x"</span>, &amp;addr.s_addr);
               <span class="hljs-built_in">strcpy</span>(gw, inet_ntoa(addr));
           }
           <span class="hljs-keyword">break</span>;
       }
   }
}
</code></pre>
</li>
<li>关闭文件<pre><code class="lang-C">fclose(fp);
</code></pre>
</li>
</ol>
</li>
<li><p>下面是源代码，文件名：<a target="_blank" href="https://gitee.com/whowin/whowin/blob/blog/sourcecodes/180008/get-gateway-proc.c">get-gateway-proc.c</a>(<strong>点击文件名下载源程序</strong>)</p>
</li>
<li><p>这段程序也没什么好解释的，唯一要说明的地方是gateway路由的确定，这里是以目的地址为"00000000"作为判断，前面讨论过，目的地址为"00000000"的含义为任意目的地址；</p>
</li>
<li>编译：<code>gcc -Wall get-gateway-proc.c -o get-gateway-proc</code></li>
<li>运行：<code>./get-gateway-proc</code></li>
<li><p>下面是运行截图</p>
<p><img src="https://blog.whowin.net/images/180008/screenshot-get-gateway-proc.png" alt="screenshot of executing get-gateway-proc" /></p>
</li>
</ul>
<h2 id="heading-httpsblogcsdnnetwhowincategory12180345html"><strong>欢迎订阅 <a target="_blank" href="https://blog.csdn.net/whowin/category_12180345.html">『网络编程专栏』</a></strong></h2>
<hr />
<p><strong>欢迎访问我的博客：https://whowin.cn</strong></p>
<p><strong>email: hengch@163.com</strong></p>
<p><img src="https://blog.whowin.net/images/qrcode/sponsor-qrcode.png" alt="donation" /></p>


]]></content:encoded></item></channel></rss>