通八洲科技

深入理解Socket API与TCP协议:网络编程基础与字节序解析 包头seo优化哪家专业

日期:2026-01-15 18:37 / 作者:网络

[]

[]

在 TCP/IP 协议里,“IP 地址加上 TCP 或者 UDP 端口号”能够唯一地标识网络通讯里的一个进程,“IP 地址与端口号相结合”就被称作。

在 TCP 协议里,建立连接的两个进程各自都有一个标识。这两个进程组成的 pair 能够唯一地标识一个连接。“插座”本身就有其特定含义,所以被用来描述网络连接的一对一关系。

TCP/IP 协议最早是在 BSD UNIX 上得以实现的。为 TCP/IP 协议所设计的应用层编程接口被称作。

[]

二、网络字节序

我们已知,内存中的多字节数据对于内存地址存在大端和小端的区别。磁盘文件中的多字节数据对于文件中的偏移地址也有大端小端之分。网络数据流同样具备大端小端之分。那么,怎样去定义网络数据流的地址呢?发送主机一般会把发送缓冲区里的数据依据内存地址从低到高的次序进行发出。接收主机则把从网络上接收到的字节依次存放在接收缓冲区中,同样是按照内存地址从低到高的顺序来保存。所以,对于网络数据流的地址要这样规定:先发出的那些数据对应的是低地址,而后发出的数据对应的是高地址。

TCP/IP 协议规定,网络数据流的字节序应采用大端字节序,也就是说在这种字节序中,低地址对应的字节是高字节。四十五节的 UDP 段格式中,地址 0 到 1 为 16 位的源端口号。若此端口号是 1000(0x3e8),那么地址 0 是 0x03,地址 1 是 0xe8,即先发送 0x03,接着发送 0xe8。在发送主机的缓冲区中,这 16 位也应是低地址存 0x03,高地址存 0xe8。如果发送主机是小端字节序,那么这 16 位会被这样解释,而非 1000。所以,发送主机在把 1000 填到发送缓冲区之前,需要进行字节序的转换。同样地,若接收主机是小端字节序,当接到 16 位的源端口号时,也需要做字节序的转换。而如果主机是大端字节序,无论是发送还是接收,都不需要进行转换。同样地,对于 32 位的 IP 地址,需要考虑网络字节序以及主机字节序方面的问题。

为让网络程序具备可移植性,让相同的 C 代码在大端计算机和小端计算机上编译后都能正常运转,能够调用以下这些库函数来进行网络字节序与主机字节序的转换。

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'> <pre class="prettyprint"><code class=" hljs actionscript">#<span class="hljs-preprocessor"><span class="hljs-keyword">include</span> <a style='color:#0000CC;font-size:15px;' rpa/inet.h> 定义了一个名为 htonl 的函数,该函数的参数是一个 uint32_t 类型的 hostlong,函数的返回值也是 uint32_t 类型。</span> 这个函数名为 htons,它接收一个 16 位的主机短整型数值作为参数,然后进行相应的网络字节序转换操作,最终返回转换后的网络字节序的 16 位数值。 定义了一个名为 ntohl 的函数,该函数接收一个 uint32_t 类型的参数 netlong,并返回一个 uint32_t 类型的值。 定义一个函数 ntohs,它接收一个无符号 16 位整数类型的参数 netshort,函数返回值也为无符号 16 位整数类型。 h 表示host n 表示network l 表示<span class="hljs-number">32</span>位长整数 s 表示<span class="hljs-number">16</span>位短整数。 如果主机的字节序是小端的,那么这些函数会对参数进行相应的大小端转换之后再返回。如果主机的字节序是大端的,这些函数就不会进行转换,而是将参数原原本本地返回。</code></pre></p>

三、IP地址转换函数

早期使用的是一下函数:

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'> <pre class="prettyprint"><code class=" hljs vala"><span class="hljs-preprocessor">#include <sys/socket.h></span> <span class="hljs-preprocessor">#include <netinet/in.h></span> <span class="hljs-preprocessor">#include <a style='color:#0000CC;font-size:15px;' rpa/inet.h></span> <span class="hljs-keyword">int</span> inet_aton(<span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *cp, <span class="hljs-keyword">struct</span> in_addr *inp); in_addr_t inet_addr(<span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *cp); <span class="hljs-keyword">char</span> *inet_ntoa(<span class="hljs-keyword">struct</span> in_addr in);

只能处理IPv4的ip地址 不可重入函数 注意参数是<span class="hljs-keyword">struct</span> in_addr</code></pre></p>

现在多使用的是:

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'> <pre class="prettyprint"><code class=" hljs vala"><span class="hljs-preprocessor">#include <a style='color:#0000CC;font-size:15px;' rpa/inet.h></span> <span class="hljs-keyword">int</span> inet_pton(<span class="hljs-keyword">int</span> af, <span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *src, <span class="hljs-keyword">void</span> *dst);  <span class="hljs-comment">//将字符串转换为网络地址</span> <span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span> *inet_ntop(<span class="hljs-keyword">int</span> af, <span class="hljs-keyword">const</span> <span class="hljs-keyword">void</span> *src, <span class="hljs-keyword">char</span> *dst, socklen_t size);  <span class="hljs-comment">//将网络地址转换为字符串</span> 支持IPv4和IPv6 可重入函数</code></pre></p>

其中,它不仅能够转换 IPv4 的相关内容,还能够转换 IPv6 的相关内容,正因如此,其函数接口为 void *。

四、数据结构

很多网络编程函数诞生于 IPv4 协议之前,当时使用的是结构体。为了实现向前兼容,现在这些函数退化成了(void *)的作用,即传递一个地址给函数。而这个函数具体是哪种类型,由地址族来确定。之后,函数内部会再将其强制类型转化为所需的地址类型。

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'> <pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">struct</span> sockaddr { sa_family_t sa_family;  <span class="hljs-comment">地址族,其格式为 AF_xxx 。</span> <span class="hljs-keyword">char</span> sa_data[<span class="hljs-number">14</span>]; <span class="hljs-comment">协议地址为 14 字节。</span> }; <span class="hljs-keyword">struct</span> sockaddr_in { 内核的地址族类型为 sin_family;sin_family 是一个特定的地址族类型;该地址族类型被用于内核相关的操作中;它在网络编程等领域有重要作用。<span class="hljs-comment">/* Address family */</span> __be16 sin_port;<span class="hljs-comment">/* Port number 端口号*/</span> <span class="hljs-keyword">struct</span> in_addr sin_addr; <span class="hljs-comment">/* Internet address IP地址*/</span> <span class="hljs-comment">将其填充至 `struct sockaddr` 的大小。</span> unsigned <span class="hljs-keyword">char</span> __pad[__SOCK_SIZE__ - <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">short</span> <span class="hljs-keyword">int</span>) - <span class="hljs-keyword">sizeof</span>(unsigned <span class="hljs-keyword">short</span> <span class="hljs-keyword">int</span>) - <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">struct</span> in_addr)]; }; <span class="hljs-comment">/* Internet address. */</span> <span class="hljs-keyword">struct</span> in_addr { __be32 s_addr; <span class="hljs-comment">// 32位地址</span> };

<span class="hljs-keyword">struct</span> sockaddr_in6 { unsigned <span class="hljs-keyword">short</span> <span class="hljs-keyword">int</span> sin6_family;<span class="hljs-comment">/* AF_INET6 */</span> __be16 sin6_port;  <span class="hljs-comment">关于运输层的端口号</span> __be32 sin6_flowinfo; <span class="hljs-comment">IPv6 流信息的控制属性。</span> <span class="hljs-keyword">struct</span> in6_addr sin6_addr;  <span class="hljs-comment">/* IPv6 addressip地址128位*/</span> __u32 sin6_scope_id;  <span class="hljs-comment">范围标识(在 RFC2553 中是新的)</span> }; <span class="hljs-keyword">struct</span> in6_addr { union {   __u8u6_addr8[<span class="hljs-number">16</span>]; <span class="hljs-comment">// 可以使用16个8位的地址</span>   __be16 u6_addr16[<span class="hljs-number">8</span>]; <span class="hljs-comment">// 8个16位地址</span>   __be32 u6_addr32[<span class="hljs-number">4</span>]; <span class="hljs-comment">// 4个32位地址</span> } in6_u; <span class="hljs-preprocessor">#<span class="hljs-keyword">define</span> s6_addr in6_u.u6_addr8</span> <span class="hljs-preprocessor">#<span class="hljs-keyword">define</span> s6_addr16 in6_u.u6_addr16</span> <span class="hljs-preprocessor">#<span class="hljs-keyword">define</span> s6_addr32 in6_u.u6_addr32</span> }; <span class="hljs-preprocessor">#<span class="hljs-keyword">define</span> UNIX_PATH_MAX 108</span> <span class="hljs-keyword">struct</span> sockaddr_un { sun_family 属于 __kernel_sa_family_t 类型。<span class="hljs-comment">/* AF_UNIX */</span> <span class="hljs-keyword">char</span> sun_path[UNIX_PATH_MAX];<span class="hljs-comment">/* pathname */</span> };</code></pre></p>

IPv4 的地址格式在 /in.h 中被定义,它用结构体来表示,其中包含 16 位端口号以及 32 位 IP 地址。IPv6 的地址格式也在 /in.h 中被定义,它用结构体来表示,此结构体包含 16 位端口号、128 位 IP 地址和一些控制字段。UNIX 的地址格式在 sys/un.h 中被定义,用 sock-结构体来表示。各种地址结构体的开头是相同的。其前 16 位表示整个结构体的长度,不过并非所有 UNIX 的实现都有长度字段,像 Linux 就没有。而后 16 位表示地址类型。IPv4 的地址类型定义为常数,IPv6 的地址类型定义为常数,Unix 的地址类型也定义为常数。这样,只要能够获取到某种结构体的首地址,并且无需知晓具体属于哪种类型的结构体,就能够依据地址类型字段来确定结构体中的内容。API 能够接受各种类型的结构体指针作为参数,像 bind 等函数就是如此。这些函数的参数需设计为 void 类型,这样就能接受各种类型的指针。然而,sock API 的实现早于 ANSI C 标准化,在那时候还不存在 void 类型,所以这些函数的参数都用 *类型来表示,在传递参数之前需要进行强制类型转换,例如:

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'> <pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">struct</span> sockaddr_in servaddr; <span class="hljs-comment">/* initialize servaddr */</span> bind(listen_fd, (<span class="hljs-keyword">struct</span> sockaddr *)&servaddr, <span class="hljs-keyword">sizeof</span>(servaddr));</code></pre></p>