Linux 网络套接字编程

1.网络通信的本质

我们平时说两台设备在通信,原理上听起来像是机器和机器在对话,但本质根本不是这样。

日常生活中用手机刷抖音,点开抖音这个软件,它一旦启动,就会被加载到内存里,在操作系统中变成一个进程。

你刚开始刷视频时,本地是没有视频数据的,所以抖音这个进程就会通过系统里的网络协议栈,向远方的抖音服务器发送请求。

而另一边的抖音服务器,看起来是一台强大的机器,但真正干活的同样是运行在内存里的服务进程。

服务器进程会一直循环等待、被动接收外面发来的请求,收到后再处理、返回数据。

所以整个流程其实是:

你这边的进程 → 通过网络协议栈发请求 → 对方服务器的进程接收 → 再返回数据。

也就是说,网络通信的本质,其实就是跨设备的进程间通信。

网络通信,本质就是进程间通信。

2.端口号

比如你在自己电脑上打开微信,想给别人发消息。

流程很简单:你的微信先把数据发给微信服务器,服务器再帮你转发给对方,最后还给你一个"发送成功"的反馈。数据的传输:你的微信客户端 → 微信服务端 → 服务端再把结果传回你的客户端。

之前我们已经知道,IP地址是用来标识一台主机的。数据打包时,会带上源IP(你的机器)和目的IP(服务器机器),依靠IP,数据就能准确送到目标主机。

但问题来了:

服务器收到数据后,知道是发给自己的。可一台服务器上同时跑着很多应用------微信服务端、抖音服务端、网易云服务端...... 传输层拿到数据后,怎么知道该把数据交给哪一个应用?

这时候,端口号就出现了。端口号是传输层里的概念,占2字节(16位),是一个整数。它的作用非常明确:标识同一台主机里,具体是哪个网络进程在通信。 使用方式就是:把端口号和进程绑定。

服务器在启动时,早就把服务进程和固定端口号绑好了。当数据到达传输层,解包后会看到包头里有源端口、目的端口。只要目的端口和服务器上微信服务绑定的端口一致,传输层就知道:这个数据是给微信服务的,于是把内容上交给应用层的微信服务进程。

这样,微信服务端就成功拿到了你发的数据。

那服务器处理完后,怎么把结果传回你的微信呢?

很简单:你发过去的报文里,已经带上了你的IP + 你的微信进程绑定的端口。你的微信启动后就是一个进程,只要进行网络通信,系统就会给它分配端口,方便对方回复。

服务器拿到你的IP和端口后:

  1. 把自己的IP当源IP
  2. 自己的端口当源端口
  3. 你的IP当目的IP
  4. 你的端口当目的端口

再通过协议栈发回给你,你的机器收到后,同样根据端口号,把数据交给微信客户端。

所以我们可以总结:

IP地址:在整个互联网里,唯一标识一台主机。

端口号:在一台主机里,唯一标识一个网络进程。

合在一起:

IP + 端口 = 互联网上唯一的一个网络进程
而socket(套接字),本质就是封装了IP和端口,只要两边都有(客户端IP:端口、服务端IP:端口),就能完成稳定的网络通信。

系统里不是已经有PID(进程号)可以唯一标识进程了吗?为什么还要专门搞一个端口号?

原因主要有两点:

  1. 不是所有进程都需要联网,但所有进程都必须有PID。端口号只给需要网络通信的进程用,更精准。
  2. PID是操作系统内核层面的概念,属于系统模块。如果网络协议直接依赖PID,那系统一旦改了PID规则,整个网络模块都要改。网络和操作系统是两个独立模块,我们希望它们低耦合、互不干扰。所以网络层专门设计了自己的"端口号"来标识进程,不管操作系统怎么变,网络层都不受影响。

这也就是端口号存在的意义。

一个进程可以绑定多个端口号,一个端口号不能被多个进程绑定。

3.UDP协议和TCP协议

TCP 协议

TCP(传输控制协议)是一种面向连接、可靠且基于字节流的传输层协议。

特点:

  1. (1)传输层协议
  2. (2)建立连接
  3. (3)可靠传输
  4. (4)面向字节流

使用 TCP 进行通信时,双方必须先建立连接。只有连接成功建立,数据才能开始传输。它保证了三件事:

  1. 数据一定能送到对方主机
  2. 数据不会丢失
  3. 数据会按顺序整齐到达

TCP 是基于字节流传输的,也就是说,数据会被拆成一个个字节,再逐个发送给对方。因为它的安全性和可靠性非常高,所以常用于支付、银行系统等对数据准确性要求极高的场景。

但它的代价是 效率较低。为了保证可靠,TCP 需要三次握手建立连接,四次挥手断开连接,还要做确认机制、超时重传、乱序修复等等。这些操作都会消耗系统资源,也会拖慢传输速度。

UDP 协议

UDP(用户数据报协议)则是另一种风格,它是一个无连接、不可靠、面向数据报的协议。

特点:

  1. (1)传输层协议
  2. (2)无连接
  3. (3)不可靠传输
  4. (4)面向数据报

使用 UDP 时,双方不需要建立连接,发送方把数据准备好就可以直接发出去,不等待对方的状态,也不做任何检查。一旦数据发出,本地就不再保留,因此它速度很快、延迟低。

缺点是:它不保证数据一定到达,也不保证顺序是否正确。丢包、乱序、重复,它都不会处理。

正因为不做任何"可靠工作",UDP 的效率非常高,适合实时场景。比如视频通话、直播、游戏、网络监控、日志传输等都经常用 UDP。

4.网络字节序

在网络通信里,有一个必须解决的"硬件歧义"问题:不同主机在内存里存储多字节数据的顺序是不一样的。

一边是大端机(高位在前),一边是小端机(低位在前)。如果两边直接按各自的内存格式发数据,接收方解读出来就会是乱码。即使数据包里带了"我是大端/小端"的标识,也没用,因为接收方不知道怎么反解析,看不懂对方的字节流格式。所以,网络协议直接做了统一规定:所有数据在网络上传输时,必须统一成大端字节序。 这就叫网络字节序。

什么是大端与小端

大端:数据的高位字节放在内存的低地址,像"正常读数"一样,从大到小排列。

小端:数据的低位字节放在内存的低地址,更接近硬件的"直白存储"方式。

记忆方式只记一个就行:

小小小(低位 → 低地址) = 小端。

剩下的情况,统统是大端。

网络传输规则

TCP/IP 协议规定:

  1. 发送方从缓冲区低地址到高地址依次发送字节。

  2. 接收方从网络收到的字节,也是按低地址到高地址的顺序存进缓冲区。

  3. 所以网络字节流的顺序 = 大端字节序。

无论你本地是大端还是小端,发数据前都必须转换成网络字节序(大端),收数据时再根据需要转回本地字节序(大端或者小端)。

主机字节序与网络字节序的转换

在 socket 编程里,系统提供了专门的转换函数,命名非常直观:

h = host(主机字节序)

n = network(网络字节序)

s = short(16 位,例如端口号)

l = long(32 位,例如 IP 地址)

函数如下:

当你在小端机上调用 htonl 时,它会自动把你的小端数据转换成大端,再发出去;

如果在大端机上调用,它则直接返回原数据。

5.套接字

套接字在网络编程里不是单一类型,而是根据使用场景分成了三类。它们虽然功能不同,但系统为了方便开发者,统一了一套接口,使得在三种套接字上都可以用 socket 、 bind 等函数来操作。

三种主要的套接字类型

  1. 域间套接字(AF_UNIX / AF_LOCAL)

这种套接字只用于 同一台主机内 的进程通信。它不需要网络协议栈,而是通过一个文件路径来标识,两个进程只要看到同一个"文件"就能通信。

  1. 原始套接字(Raw Socket)

原始套接字权限更高,可以绕过传输层,直接访问到 网络层、数据链路层。常用于抓包、协议分析、网络诊断工具等。

  1. 网络套接字(AF_INET / AF_INET6)

这是最常用的,用于 跨主机网络通信。基于 IP 地址和端口号,实现 TCP/UDP 网络编程。

为什么三套套接字要共用一套 API?

如果三套套接字分别用三套完全不同的接口,学习成本会很高,代码也不统一。

因此操作系统设计者把三类套接字统一到一套通用接口中,也就是三个套接字的操作都可以使用socket来操作。

socket 参数

创建套接字时,必须指定一个 协议族(Domain),告诉系统你想用哪类套接字:

如果是本机通信: AF_UNIX 或 AF_LOCAL

如果是网络通信: AF_INET (IPv4)或 AF_INET6 (IPv6)

第二个参数指定类型:

UDP: SOCK_DGRAM

TCP: SOCK_STREAM

第三个参数一般填 0。

创建成功后返回一个 文件描述符。

这也符合 Linux "一切皆文件" 的设计------套接字本质上也是一个文件对象。

6.bind (兼容三种结构体)

网络套接字要绑定 IP 和端口。

域间套接字要绑定文件路径。

原始套接字又有别的结构。

但是bind 的接口必须统一,所以系统做了两件事:

(1)使用通用结构体 struct sockaddr 作为统一接口,虽然它是"通用容器",但内部真正的数据取决于协议族。

(2)用具体结构体填充内容,再强制转换成 struct sockaddr*

比如:

cpp 复制代码
网络套接字用: struct sockaddr_in 
域间套接字用: struct sockaddr_un 

它们前 16 个字节都存放"家族信息"(如 AF_INET、AF_UNIX)。

这样内核在 bind 内部就能判断:"你传进来的是哪类套接字,我应该进入哪套处理逻辑"。

内核内部的逻辑类似:

cpp 复制代码
if (addr->family == AF_INET) {
// 处理网络套接字
} else if (addr->family == AF_UNIX) {
// 处理域间套接字
}
相关推荐
熬夜有啥好3 小时前
Linux软件编程——TCP并发服务器
运维·服务器
开开心心_Every3 小时前
PDF密码移除工具,解除打印编辑复制权限免费
linux·运维·服务器·pdf·web3·ocr·共识算法
卓律涤3 小时前
【工作篇】 Dell机架式服务器,采用RAID 5,怎么部署win系统
运维·服务器·单片机·嵌入式硬件·深度学习·程序人生·安全
生活很暖很治愈3 小时前
Linux——UDP编程&通信
linux·服务器·c++·ubuntu
就不掉头发3 小时前
Linux与数据库
linux·运维·数据库
皮皮哎哟3 小时前
linux网络编程:UDP
网络·udp·socket·sendto·udp包头
失途老马3 小时前
EdgeRouter PPPoE IPv6 完整配置指南(从 0 到通)
网络·飞牛os
2401_858936883 小时前
深入浅出 TCP 通信:从基础到并发服务器实现
服务器·网络·tcp/ip