Linux 网络编程从入门到进阶 学习指南

Linux 网络编程从入门到进阶 学习指南

网络通信基础

思考一下,如果计算机想要“交朋友”,它们需要怎样互相沟通?正如人们交流需要使用语言一样,计算机通信也必须遵守一套规则 ― 这就是网络协议。

协议确保信息可以在不同的设备和平台之间清晰、准确地传递。要深入理解协议,我们首先要熟悉两个基础的通信模型:OSI 和 TCP/IP 模型。

OSI 模型和 TCP/IP 模型

在网络通信的世界里,OSI(开放式系统互联通信参考模型)和 TCP/IP(传输控制协议/网际协议)模型扮演着基础框架的角色。它们各自描述了网络通信的多个层次和阶段,但以不同的方式来分类和处理数据传输的细节。

  • OSI模型

OSI(Open Systems Interconnection)模型是一个概念性框架,用于描述网络中不同操作层次的功能。由七层组成,从物理硬件的电气信号(物理层),到应用层(如网页浏览器),每一层都有其独特的功能和协议。

  • TCP/IP模型

TCP/IP 模型,则更加贴近实际网络中的运作。Linux 的网络协议栈就是基于该模型实现的。它是基于四层架构,将网络通信过程简化并集中在协议族上,如传输控制协议(TCP)和互联网协议(IP),这两种协议是现代网络通信中最为核心的部分。

基本概念

地址簿:IP地址和MAC地址

想象一下,互联网是一个巨大的数字城市,而每台计算机或网络设备就像是住在这个城市里的居民。

IP地址:数字世界的“家庭住址”

每台设备的 IP 地址就像是它在这个数字城市里的家庭住址。当计算机需要发送信息或访问网络资源时,它会使用目的地设备的 IP 地址来确保信息正确地送达。这个地址有点像是我们现实世界中的邮寄地址,可以根据网络环境的变化而变化(例如,当设备从家庭网络移动到办公室网络时)。

MAC地址:网络中的“身份证”

然后,我们有 MAC 地址,这是网络设备的另一个关键标识。每台设备的 MAC 地址都是独一无二的,类似于每个人的身份证号码。它是在设备制造时就被分配的,并且在大多数情况下,这个地址是固定不变的。MAC 地址在本地网络(如家庭或办公室网络)内起着重要作用,它帮助确保信息被准确地送达到特定设备,就像邮递员需要知道收件人的详细身份信息才能将包裹准确递交。

总结一下 :ip 地址可以让数据包找到目的主机所在的网络,而 MAC 地址确保数据包能准确送到目的主机上。

导航路线:子网掩码和网关 子网掩码:定位网络的“区域地图”

子网掩码可以被视为定位网络内部和外部地址的“区域地图”。就像在一个大城市中,你需要知道哪些街道属于你的社区,哪些通往城市的其他部分。子网掩码帮助计算机确定一个 IP 地址是属于本地网络(即同一个子网)还是位于外部网络。

  • 内部导航:如果目的地IP地址与计算机所在的子网相匹配(根据子网掩码判断),则数据包在本地网络内传送。
  • 外部导航:如果目的地不在本地子网内,计算机知道它需要将数据发送到更远的目的地。

网关:网络间的“中转站”

网关在网络通信中扮演中转站的角色。当你的数据包需要从一个网络(比如你的家庭网络)发送到另一个网络(比如你的工作地点的网络)时,网关是这个旅程的第一站。

路由决策: 网关检查数据包的目的 IP 地址,然后使用它的路由表来决定最佳的路径将数据包发送到目标网络。 总结: 子网掩码和网关共同协作,帮助数据包在复杂的网络结构中找到最有效的路径。子网掩码确定数据包是否在本地网络内,而网关指导跨网络的数据传输。

端口 :确保数据到达正确的“应用程序门牌号” 好了,现在我们的数据包已经知道了去哪里,但它如何确保被正确的程序接收呢?这就是端口登场的时候了。端口号就像是收件人的门牌号,确保数据不只是送到了正确的地址,而且被正确的应用程序接收。

Linux 套接字编程

套接字是什么

在网络编程中,套接字就像是网络世界的通信端口。每一个联网的应用程序,为了能够互发消息,都会使用到这样一个端口。这个端口允许数据从一个程序流向另一个程序。简而言之,套接字是应用程序用来在网络上交流的桥梁。

想象一下,你要用手机给朋友发一条信息。你只需要知道他们的手机号码,这样信息就可以直接发送到他们的手机上。在网络编程中,套接字的作用类似。它使用IP地址 (类似于手机号码) 来确定数据发送的目标位置,而端口号则像是确定信息应该送达到对方手机中的哪个应用程序。这样,套接字(使用 ip 地址和端口)确保了数据能够准确地发送给正在监听那个特定端口的程序。

套接字的工作原理:

套接字的工作原理就像是电话通话的过程。首先,你需要拨打一个号码(即IP地址+端口号)来建立连接。一旦连接建立,电话线(网络连接)就激活了,你的声音(数据)就可以通过它传送。

在这个过程中:

拨号 对应于网络编程中的连接 建立 ,这是通过调用套接字API来完成的,比如 connect( )函数。

通话 对应于 数据传输 ,你可以通过套接字发送 send( ) 和接收 recv( )数据。

挂断 对应于 结束连接 ,完成通信后,你需要关闭套接字 close( )函数,以结束会话并清理资源。

在整个通信过程中,套接字保证了数据从一个程序准确地传送到另一个程序,无论这两个程序是在同一台计算机上还是跨越了广阔的互联网。

在 Linux 中,套接字其实就是一系列的编程接口,Linux 提供了很多特定的 API 来创建和使用套接字,接下来,让我们学习如何使用 Linux 套接字 api 来编写各种网络服务程序。

套接字类型

在 Linux 中,有三种套接字类型, 前两种是重点掌握的, 第三种了解即可。

TCP套接字 (SOCK_STREAM):

  • 这是一种可靠的套接字连接,保证数据传输的完整性和顺序。
  • 必须先建立连接,才能传输数据。
  • 常用于需要准确数据传输的应用,如网页浏览和文件传输。

UDP套接字 (SOCK_DGRAM):

  • 不需要建立连接,但是数据传输可能会丢失,没有先后顺序。
  • 适用于视频流和在线游戏,这些应用可以容忍一定的数据丢失。

原始套接字 (SOCK_RAW):

  • 允许直接对较低层次的协议如 IP 或 ICMP 进行访问和操作,它绕过了 TCP 和 UDP 的处理。
  • 开发者可以使用原始套接字来构建自定义的协议或直接处理来自网络的数据包。
  • 通常用于需要进行网络诊断或网络安全应用,如自定义的ping实现,或者网络嗅探器。

选择哪种类型取决于你的应用需求―是否需要可靠传输(TCP),还是速度更快但可能丢失数据也没关系(UDP)。

选择使用原始套接字通常意味着你需要对网络协议有深入的理解,因为你将直接与网络层面的数据交互。这比处理 TCP 和 UDP 套接字更复杂,通常只在特殊情况下使用,例如网络工具的开发或定制协议的实现。

套接字常用 API

接下来,看下常用的套接字 API:

1
2
3
4
5
6
7
8
9
socket()                    : 创建套接字
bind()                      : 绑定套接字到本地地址
listen()                    : 监听网络连接
accept()                    : 接受网络连接
connect()                   : 连接到远程主机
send(), recv()              : 发送和接收数据(面向连接的套接字)
sendto(), recvfrom()        : 发送和接收数据(无连接的套接字)
close(), shutdown()         : 关闭套接字
getsockopt(), setsockopt()  : 获取和设置套接字选项
套接字地址结构以及地址转换 API
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
sockaddr 是一个通用的套接字地址结构,它通常与特定的地址族结构(如 sockaddr_in )一起使用。
 这是因为多数套接字函数,如 bind(), connect(), 和 accept(),需要使用指向 sockaddr 结构的指针的参数。
*/
struct sockaddr {
   sa_family_t     sa_family;      /* Address family */
   char            sa_data[];      /* Socket address */
};

// 套接字地址结构(适用于IPv4网络通信的地址结构)
struct sockaddr_in 
{
    sa_family_t    sin_family; # address family: AF_INET 
    in_port_t      sin_port;   # port in network byte order 
    struct in_addr sin_addr;   # ip address 
};

struct in_addr
{
     uint32_t       s_addr;    # address in network byte order 
};

/* 
网络地址转换函数 (用于将IP地址在可打印的格式和二进制结构之间转换)
将点分十进制的IP地址(如"192.168.1.1")转换成网络字节顺序的二进制形式
inet_pton()   
将网络字节顺序的二进制IP地址转换为点分十进制字符串格式
inet_ntop()   
*/

# demo 示例:
#define INET_ADDRSTRLEN 16;

char str[INET_ADDRSTRLEN];
struct in_addr ipv4addr;
inet_pton(AF_INET, "192.168.1.1", &ipv4addr);
inet_ntop(AF_INET, &ipv4addr, str, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", str);
完整 demo 示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define INET_ADDRSTRLEN 16

int main(void)
{
	char str[INET_ADDRSTRLEN];
	struct in_addr ipv4addr;
	inet_pton(AF_INET, "192.168.1.1", &ipv4addr);
	inet_ntop(AF_INET, &ipv4addr, str, INET_ADDRSTRLEN);
	printf("The IPv4 address is: %s\n", str);

	return 0;
}
字节序转换 API

在网络编程中,字节序(也称为端序)指的是数值在内存中保存的顺序。不同的计算机体系结构可能会采用不同的字节序来表示数据。最常见的两种字节序是大端字节序(Big-Endian)和小端字节序(Little-Endian)。在网络通信中,为了确保数据在不同的系统间正确传输和解释,定义了一个统一的字节序,即:网络字节序,它采用大端字节序。

由于主机字节序与网络字节序可能不同,因此在发送数据前,发送方需要将其主机字节序的数值转换为网络字节序;接收方收到数据后,需要将网络字节序的数值转换回主机字节序。

Linux 提供了一组 API 来处理字节序的转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 将无符号长整型数/无符号短整型数从主机字节顺序转换为网络字节顺序。
htonl() 和 htons()  
// 将一个无符号长整型数/无符号短整型数从网络字节顺序转换为主机字节顺序。
ntohl() 和 ntohs()  

/*
为了方便记忆,大家可以这样理解:h 代表 host(主机),n 代表 network(网络),
l 代表 long(四字节:代表ip),s 代表 short(两字节:代表端口) 。

以 htons() 举例,host to network short 即:将端口从主机字节序转成网络字节序。
*/

注意:htonl 和 ntohl 一般处理的是 IP 地址,而 htons 和 ntohs 一般处理的是端口。

Licensed under CC BY-NC-SA 4.0