Socket 编程基础

文章目录

  • [1. 对齐颗粒度](#1. 对齐颗粒度)
    • [① 人与计算机的交互方式](#① 人与计算机的交互方式)
    • [② 网络的真正目的](#② 网络的真正目的)
    • [③ 人上网的两种行为(仅此两种)](#③ 人上网的两种行为(仅此两种))
    • [④ 网络通信的本质](#④ 网络通信的本质)
    • 一个问题
  • [2. 端口号(Port)](#2. 端口号(Port))
    • [2.1 对齐颗粒度](#2.1 对齐颗粒度)
    • [2.2. 核心结论](#2.2. 核心结论)
    • [2.3 为什么不用 pid?](#2.3 为什么不用 pid?)
    • [2.4 端口号范围划分](#2.4 端口号范围划分)
    • [2.5 源端口 & 目的端口](#2.5 源端口 & 目的端口)
  • [3. Socket](#3. Socket)
    • [3.1 深入理解 Socket](#3.1 深入理解 Socket)
  • [4. 初识 TCP 与 UDP](#4. 初识 TCP 与 UDP)
    • [4.1 TCP(Transmission Control Protocol)](#4.1 TCP(Transmission Control Protocol))
    • [4.2 UDP(User Datagram Protocol)](#4.2 UDP(User Datagram Protocol))
    • [4.3 网络字节序(大小端)](#4.3 网络字节序(大小端))
      • [4.3.1 一个结论](#4.3.1 一个结论)
      • [4.3.2 一些问题](#4.3.2 一些问题)
      • [4.3.3 字节序转换函数(必须配套使用)](#4.3.3 字节序转换函数(必须配套使用))
  • [5. Socket 编程接口](#5. Socket 编程接口)
    • [5.1 Socket 地址结构](#5.1 Socket 地址结构)
    • [5.2 Socket 核心接口(按流程)](#5.2 Socket 核心接口(按流程))
      • [① socket() ------ 创建通信端点](#① socket() —— 创建通信端点)
      • [② bind() ------ 绑定地址](#② bind() —— 绑定地址)
      • [③ listen() ------ 监听连接(TCP)](#③ listen() —— 监听连接(TCP))
      • [④ accept() ------ 接受连接(TCP)](#④ accept() —— 接受连接(TCP))
      • [⑤ connect() ------ 发起连接(TCP 客户端)](#⑤ connect() —— 发起连接(TCP 客户端))
      • [⑥ 数据收发](#⑥ 数据收发)
      • [⑦ 关闭连接](#⑦ 关闭连接)
    • [5.3 sockaddr 结构关系](#5.3 sockaddr 结构关系)

1. 对齐颗粒度

① 人与计算机的交互方式

  • 人和计算机进行交互的唯一方式是:启动进程
  • 进程是"人"在操作系统中的代表
  • 只要把数据交给进程,就等价于把数据交给了人

② 网络的真正目的

  • 将数据传输到主机 不是目的
  • 数据到达主机后,再交给主机内的目标进程,才是最终目的

③ 人上网的两种行为(仅此两种)

  1. 从远端服务器获得数据(下载
  2. 将本地数据上传到远端服务器(上传

之所以只能有这两种行为,是由 冯诺依曼体系结构 决定的:

  • 上网的实际执行者是 进程
  • 进程与网卡之间只能进行 IO 操作
  • 所以:
    👉 用户上网,本质上只能进行 IO 行为

④ 网络通信的本质

  • 网络通信并不是"主机和主机通信"
  • 而是:两个不同主机上的进程在进行数据交互

📌 结论:网络通信的本质是 ------ 进程间通信(IPC)

前提条件只有一个:

通信双方必须"看到同一份资源"

在网络中,这个"资源"就是 ------ 网络本身


一个问题

系统中存在大量进程,数据从网络到达主机后,如何准确交给目标进程?

答案:引入端口号(Port)


2. 端口号(Port)

2.1 对齐颗粒度

  • 端口号是传输层协议中的内容
  • 端口号是一个 16 位(2 字节)整数
  • 用于标识:当前数据应交给哪一个进程处理
  • IP 地址 + 端口号可唯一标识网络上的 某一台主机的某一个进程
  • 一个端口号只能被一个进程占用

2.2. 核心结论

端口号用于唯一标识系统中的一个网络进程


2.3 为什么不用 pid?

Q:pid 不是也可以唯一区分一个进程吗?为什么网络通信不用 pid?

A:

  • 不是所有的进程都要进行网络通信,所以不是所有的进程都有端口号。
  • 从技术角度来讲,直接使用pid也行,但是pid是一个系统的概念,如果进程pid发生改变那么网络也要被迫发生改变。网络不想和系统产生太强的耦合度,引入了端口号。所以端口号的本质是为了解耦

📌 端口号的本质:解耦


2.4 端口号范围划分

范围 含义
0 ~ 1023 知名端口(HTTP、FTP、SSH 等)
1024 ~ 65535 OS 动态分配端口(客户端常用)

2.5 源端口 & 目的端口

  • TCP / UDP 报文中包含:

    • 源端口
    • 目的端口
  • 用于描述:

    "数据从哪里来,要到哪里去"


3. Socket

  • 网络通信的本质是:

    👉 全网内两个唯一进程之间的进程间通信

  • 使用:

    • IP:标识主机
    • Port:标识进程

我们将 IP + Port 称为:

👉 Socket(套接字)


3.1 深入理解 Socket

  • IP → 唯一标识一台主机 。 Port → 唯一标识该主机上的一个网络进程
  • IP + Port → 唯一标识互联网中的一个进程

通信时,用 四元组 标识通信双方:

复制代码
{ srcIP, srcPort, dstIP, dstPort }

📌 结论再强化一次:

  • 网络通信的本质 = 进程间通信
  • Socket = IP + Port

4. 初识 TCP 与 UDP

4.1 TCP(Transmission Control Protocol)

  • 传输层协议
  • 面向连接
  • 可靠传输
  • 面向字节流

适合对数据完整性、顺序性要求高的场景

例如:文件传输


4.2 UDP(User Datagram Protocol)

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

适合追求实时性、可容忍丢包的场景

例如:音视频、在线游戏
⚠️ 这里的"可靠 / 不可靠"是 中性描述


4.3 网络字节序(大小端)

4.3.1 一个结论

内存地址有大小端之分 ,网络数据流也有,网络 中默认凡是发送到网络中的数据必须是大端传输

因此 :如果机器是小端系统,需要先进行转换否则就忽略,再传输或者接收网络中的数据。

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/P/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/P/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

4.3.2 一些问题

Q:市面上绝大多数消费级和服务器级电脑(无论品牌),只要用 Intel/AMD x86/x86-64 或默认小端的 ARM/RISC-V CPU,均采用小端存储,这是行业主流标准。但为什么网络采用大端传输?

A:是因为:早期网络设备以大端为主。

Q:那为什么后来设备改为了小端?

A:因为小端符合CPU处理逻辑。

比如说:CPU 执行加减乘除等算术运算时,先处理低位字节(比如计算 1234,先算 34,再算 12),小端将低位字节存在内存低地址,CPU 可以直接从低地址读取低位字节开始运算,无需额外的字节移位 / 重组操作。而大端是高位字节存在低地址,CPU 需要先跳过高位、找到低位再运算,多了一步硬件层面的处理,效率略低(早期处理器算力弱,这个差异尤为明显)。


4.3.3 字节序转换函数(必须配套使用)

  • htons() / htonl():主机 → 网络
  • ntohs() / ntohl():网络 → 主机
  • inet_addr():字符串 IP → 网络字节序整数
  • inet_ntoa():网络字节序整数 → 字符串 IP

5. Socket 编程接口

5.1 Socket 地址结构

c 复制代码
struct sockaddr_in {
    sa_family_t    sin_family;   // AF_INET
    in_port_t      sin_port;     // 端口号(网络字节序)
    struct in_addr sin_addr;     // IP(网络字节序)
    char           sin_zero[8];  // 填充
};

struct sockaddr; // 通用地址结构

5.2 Socket 核心接口(按流程)

① socket() ------ 创建通信端点

c 复制代码
int socket(int domain, int type, int protocol);

② bind() ------ 绑定地址

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

③ listen() ------ 监听连接(TCP)

c 复制代码
int listen(int sockfd, int backlog);

④ accept() ------ 接受连接(TCP)

c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

⑤ connect() ------ 发起连接(TCP 客户端)

c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

⑥ 数据收发

  • TCP:send() / recv()
  • UDP:sendto() / recvfrom()

⑦ 关闭连接

  • close():基础关闭
  • shutdown():半关闭 / 强制关闭

5.3 sockaddr 结构关系

开始监听 socket (TCP, 服务器): int listen(int sockfd, int backlog);

接收请求 (TCP, 服务器): int accept(int sockfd, struct sockaddr* address, socklen_t* address_len);

建立连接 (TCP, 客户端): int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

五个接口中,三个都有参数:struct sockaddr address*,该参数主要使用其中两个:

• 网络通信 → strcut sockaddr_in *

• 本地通信 → struct sockaddr_un*

注1:两者均为地址类型

注2:三者构成继承与多态的关系,其中sockaddr是基类,sockaddr_in 和 sockaddr_un 是派生类。实际编写函数时,需要将派生类的类型强转为基类的类型

  • sockaddr:基类
  • sockaddr_in / sockaddr_un:派生类
  • 接口统一使用 sockaddr*
    👉 实际使用时需 强转

相关推荐
陌上花开缓缓归以1 小时前
Linux 5.4内核版本内核宏梳理
linux·网络·github
女王大人万岁2 小时前
Go标准库 io与os库详解
服务器·开发语言·后端·golang
Madison-No72 小时前
【Linux】文件操作&&重定向原理
android·linux·运维
若风的雨2 小时前
安全与验证模块设计方案
linux·安全
Eiceblue3 小时前
.NET框架下Windows、Linux、Mac环境C#打印PDF全指南
linux·windows·.net
试试勇气3 小时前
Linux学习笔记(十三)--文件系统
linux·笔记·学习
yingdonglan3 小时前
鸿蒙跨端Flutter学习——GridView高级功能
linux·运维·windows
2301_803554523 小时前
阻塞,非阻塞,同步,异步以及linux上的5种IO模型阻塞,非阻塞,信号驱动,异步,IO复用
java·服务器·网络
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [drivers][clk]clk
linux·笔记·学习