【Linux】网络编程基础(三):Socket编程预备知识

文章目录

    • Socket编程入门:端口号与网络字节序
    • 一、为什么需要端口号
      • [1.1 数据到达主机不是终点](#1.1 数据到达主机不是终点)
      • [1.2 进程ID为什么不够用](#1.2 进程ID为什么不够用)
      • [1.3 端口号的作用](#1.3 端口号的作用)
    • 二、端口号的分类
      • [2.1 端口号的范围划分](#2.1 端口号的范围划分)
      • [2.2 服务器端口和客户端端口](#2.2 服务器端口和客户端端口)
      • [2.3 一个端口只能被一个进程占用](#2.3 一个端口只能被一个进程占用)
    • 三、理解Socket
      • [3.1 Socket的定义](#3.1 Socket的定义)
      • [3.2 Socket通信的四元组](#3.2 Socket通信的四元组)
      • [3.3 网络通信就是进程间通信](#3.3 网络通信就是进程间通信)
    • 四、传输层协议初识
      • [4.1 为什么有TCP和UDP两种协议](#4.1 为什么有TCP和UDP两种协议)
      • [4.2 TCP协议的特点](#4.2 TCP协议的特点)
      • [4.3 UDP协议的特点](#4.3 UDP协议的特点)
      • [4.4 如何选择TCP还是UDP](#4.4 如何选择TCP还是UDP)
    • 五、网络字节序
      • [5.1 什么是字节序](#5.1 什么是字节序)
      • [5.2 为什么网络通信需要统一字节序](#5.2 为什么网络通信需要统一字节序)
      • [5.3 主机字节序和网络字节序的转换](#5.3 主机字节序和网络字节序的转换)
      • [5.4 什么时候需要转换](#5.4 什么时候需要转换)
    • 六、Socket编程接口预览
      • [6.1 Socket API是什么](#6.1 Socket API是什么)
      • [6.2 常见的Socket函数](#6.2 常见的Socket函数)
        • [6.2.1 创建Socket](#6.2.1 创建Socket)
        • [6.2.2 绑定端口](#6.2.2 绑定端口)
        • [6.2.3 监听连接(TCP服务器)](#6.2.3 监听连接(TCP服务器))
        • [6.2.4 接受连接(TCP服务器)](#6.2.4 接受连接(TCP服务器))
        • [6.2.5 发起连接(TCP客户端)](#6.2.5 发起连接(TCP客户端))
        • [6.2.6 发送和接收数据](#6.2.6 发送和接收数据)
      • [6.3 sockaddr结构体](#6.3 sockaddr结构体)
        • [6.3.1 通用地址结构](#6.3.1 通用地址结构)
        • [6.3.2 IPv4地址结构](#6.3.2 IPv4地址结构)
      • [6.4 为什么要这样设计](#6.4 为什么要这样设计)
    • 七、本篇总结
      • [7.1 核心要点](#7.1 核心要点)
      • [7.2 容易混淆的点](#7.2 容易混淆的点)

Socket编程入门:端口号与网络字节序

💬 开篇:前两篇讲清楚了协议分层和数据传输流程,但数据到达主机后,怎么知道该交给哪个进程?这就是端口号的作用。这一篇会带你理解端口号的本质,认识TCP和UDP的初步差异,以及为什么要有网络字节序。最后预览Socket API,为后面的网络编程打基础。理解了这些概念,你就能把网络通信和进程通信联系起来。

👍 点赞、收藏与分享:这篇会讲清楚IP+Port如何标识进程,以及Socket编程的基础概念。如果对你有帮助,请点赞收藏!

🚀 循序渐进:从端口号到Socket,从TCP到UDP,从字节序到API,一步步理解网络编程的入口。


一、为什么需要端口号

1.1 数据到达主机不是终点

上一篇我们讲了数据怎么通过IP地址找到目标主机。但数据到达主机后,还要面对一个问题:主机上同时运行着很多进程,这个数据包该交给谁?

你在用浏览器访问网页,同时QQ也在后台运行,迅雷在下载文件。当网卡收到数据时,操作系统怎么知道这个数据包是给浏览器的,还是给QQ的,还是给迅雷的?

如果只有IP地址,我们只能找到主机,找不到具体的进程。所以需要在网络层面标识进程的唯一性。

1.2 进程ID为什么不够用

你可能会想:进程不是有PID吗?用PID不就能标识唯一的进程了?

技术上确实可以,但有几个问题:

  1. 跨主机的PID没有意义。主机A上的PID 1234和主机B上的PID 1234是两个完全不同的进程。PID是操作系统内部的概念,不能跨主机使用。

  2. 让网络和进程管理强耦合。如果用PID标识网络进程,那么网络子系统就要和进程管理子系统紧密绑定,这违背了模块化设计的原则。

  3. PID是动态分配的。你今天启动浏览器,PID可能是1234;明天启动,PID可能变成5678。但HTTP服务总是用80端口,这样客户端才知道去哪里找服务器。

所以,实际设计中引入了一个新的概念:端口号(Port)。

1.3 端口号的作用

端口号是传输层协议的内容,它用来在网络层面标识主机上的唯一进程。

端口号是一个16位的整数,范围是0-65535。当数据到达主机后:

  1. 网络层根据IP地址找到目标主机
  2. 传输层根据端口号找到目标进程
  3. 操作系统把数据交给对应的进程处理

现在我们可以用IP地址+端口号 来唯一标识网络中的一个组合叫做Socket(套接字)。


二、端口号的分类

2.1 端口号的范围划分

端口号虽然可以是0-65535中的任意值,但实际使用时有约定俗成的划分:

知名端口(Well-Known Ports):0--1023(由 IANA 维护分配)

  • 20/21:FTP

  • 22:SSH

  • 23:Telnet

  • 25:SMTP

  • 80:HTTP

  • 443:HTTPS

这些端口号是固定的,服务器启动时会绑定对应的端口。客户端知道要访问HTTP服务,就去连接80端口。

注册端口号(Registered Ports):1024-49151

这个范围的端口号可以注册给特定应用使用,但不是强制的。比如:

  • 8080:常用于HTTP测试服务器
  • 3000:Node.js开发服务器的默认端口
  • 5000:Flask开发服务器的默认端口

动态端口号(Dynamic Ports):49152-65535

这个范围的端口号由操作系统动态分配给客户端程序。当你的浏览器要访问某个网站时,操作系统会从这个范围里随机分配一个端口号给浏览器使用。

2.2 服务器端口和客户端端口

通常情况下:

  • 服务器:绑定知名端口号(如HTTP服务器绑定80)。这样客户端才知道去哪里连接。
  • 客户端:使用动态端口号。操作系统自动分配,客户端程序不需要关心具体是哪个端口。

举个例子:你用浏览器访问百度:

bash 复制代码
浏览器(客户端):
IP: 192.168.1.100
Port: 54321 (操作系统动态分配)

百度服务器:
IP: 220.181.38.148
Port: 80 (固定的HTTP端口)

浏览器向220.181.38.148:80发起连接,操作系统自动给浏览器分配了一个端口号54321。服务器收到请求后,会向192.168.1.100:54321发送响应。

2.3 一个端口只能被一个进程占用

这是一个重要的规则:同一时刻,一个端口号只能被一个进程绑定。

如果你启动了一个HTTP服务器,绑定了80端口,然后又想启动另一个HTTP服务器,也想绑定80端口,操作系统会拒绝,提示"端口已被占用"(Address already in use)。

这就像门牌号,一个门牌号只能对应一户人家。你不能让两户人家都用同一个门牌号,邮递员会搞不清楚该送给谁。

但反过来,一个进程可以绑定多个端口。比如一个服务器程序,可以同时监听80端口(HTTP)和443端口(HTTPS)。

注:端口号在 TCP 和 UDP 是分别管理的,因此同一个数字端口可以分别用于 TCP 与 UDP。某些系统/配置下也支持端口复用(如SO_REUSEADDR/SO_REUSEPORT),但入门阶段先按"同一协议下通常只能一个监听者绑定"理解即可。


三、理解Socket

3.1 Socket的定义

Socket这个词有多重含义,容易混淆。这里我们先理解它的基本概念。

socket(端点概念):IP:Port(有时还要加上协议 TCP/UDP)

socket(编程对象):内核里的通信对象,用户态用 文件描述符 fd 表示

connection(连接):TCP 用四元组唯一标识一条连接

一个Socket可以唯一标识网络中的一个进程。比如:

  • 192.168.1.100:8080:这是一个Socket,标识192.168.1.100这台主机上使用8080端口的进程
  • 220.181.38.148:80:这是另一个Socket,标识百度服务器上的HTTP服务进程

3.2 Socket通信的四元组

网络通信本质上是两个进程之间的通信。要建立一个网络连接,需要知道两端的Socket信息:

bash 复制代码
{源IP, 源端口, 目的IP, 目的端口}

这四个信息可以唯一标识一个网络连接。比如:

bash 复制代码
浏览器 → 百度服务器:
{192.168.1.100, 54321, 220.181.38.148, 80}

操作系统通过这四元组来区分不同的网络连接。你可以同时打开多个浏览器标签,每个标签都有自己的连接,操作系统会给每个连接分配不同的源端口号。

注意,在 TCP 内部,一条连接通常由四元组唯一标识:{srcIP, srcPort, dstIP, dstPort}。
如果把 TCP/UDP 等协议一起考虑,更严格的标识是五元组:{srcIP, srcPort, dstIP, dstPort, protocol}。

3.3 网络通信就是进程间通信

到这里,我们可以得出一个重要结论:网络通信的本质就是进程间通信

只不过,这里的进程可能在不同的主机上,甚至在地球的两端。但从抽象层面看,和本地的进程间通信(管道、共享内存、消息队列)没有本质区别,都是进程之间交换数据。

Socket就是网络版的进程间通信机制。操作系统提供了Socket API,让我们能够像操作文件一样操作网络连接。


四、传输层协议初识

4.1 为什么有TCP和UDP两种协议

传输层有两个最常用的协议:TCP和UDP。为什么需要两个?因为不同的应用场景有不同的需求。

有些应用对可靠性要求很高,比如网页浏览、文件下载,数据不能丢。有些应用对实时性要求很高,比如视频通话、在线游戏,宁可丢几个数据包,也不能卡顿。

TCP和UDP就是针对这两类需求设计的。

4.2 TCP协议的特点

TCP(Transmission Control Protocol,传输控制协议)的主要特点:

1. 面向连接

通信前要先建立连接(三次握手),通信结束后要关闭连接(四次挥手)。就像打电话,要先拨号接通,说完话要挂断。

2. 可靠传输

TCP提供可靠、有序传输:丢包会重传、乱序会重排、重复会去重(应用层看到的是有序字节流)

3. 面向字节流

TCP把数据看成连续的字节流,没有边界。你发送"Hello"和"World"两次,接收方可能收到"HelloWorld",也可能收到"Hel"和"loWorld"。应用层需要自己处理消息边界。

4. 有流量控制和拥塞控制

TCP会根据网络状况和接收方的处理能力,动态调整发送速度,避免网络拥堵。

适用场景:文件传输、网页浏览、邮件发送等对可靠性要求高的场景。

4.3 UDP协议的特点

UDP(User Datagram Protocol,用户数据报协议)的主要特点:

1. 无连接

通信前不需要建立连接,想发就发。就像寄信,写完地址直接扔邮筒,不管对方在不在家。

2. 不可靠传输

UDP不保证数据一定能到达,也不保证按序到达。数据包丢了就丢了,不会重传。

3. 面向数据报

UDP是以数据报为单位传输的,有明确的边界。你发送"Hello"和"World"两次,接收方就收到两个独立的数据报,不会粘在一起。

4. 没有流量控制和拥塞控制

UDP以最快的速度发送数据,不管网络是否拥堵。

5. 开销小,速度快

UDP的报头只有8个字节,TCP的报头有20个字节。UDP不需要维护连接状态,所以开销更小。

适用场景:视频直播、在线游戏、DNS查询等对实时性要求高、可以容忍少量丢包的场景。

4.4 如何选择TCP还是UDP

选择的关键在于:你的应用能不能容忍数据丢失?

  • 不能容忍丢失:用TCP。比如文件下载,丢了一个字节,整个文件就坏了。
  • 可以容忍丢失:用UDP。比如视频直播,丢几帧画面不影响观看,但如果用TCP等待重传,就会卡顿。

还有一个考虑因素是性能。UDP更轻量,如果你的应用场景是大量的短消息(比如游戏中的位置更新),UDP的性能会更好。

但这不意味着UDP就比TCP好。TCP的可靠性是花了很多代价换来的,这些机制(超时重传、流量控制、拥塞控制)是经过几十年验证的成熟技术。大部分应用都用TCP,只有特定场景才用UDP。


五、网络字节序

5.1 什么是字节序

字节序(Byte Order)是指多字节数据在内存中的存储顺序。比如一个32位整数0x12345678,在内存中可以有两种存放方式:

大端字节序(Big-Endian):高位字节存在低地址

bash 复制代码
地址    0x00  0x01  0x02  0x03
内容    0x12  0x34  0x56  0x78
高位                低位

小端字节序(Little-Endian):低位字节存在低地址

bash 复制代码
地址    0x00  0x01  0x02  0x03
内容    0x78  0x56  0x34  0x12
低位                高位

不同的CPU架构使用不同的字节序:

  • x86、x86-64(Intel、AMD):小端
  • ARM(大部分模式):小端
  • PowerPC、SPARC:大端
  • MIPS:可配置

5.2 为什么网络通信需要统一字节序

假设主机A(小端)要给主机B(大端)发送整数0x12345678

  • 主机A按小端存储:78 56 34 12
  • 主机A发送数据:先发78,再发56...
  • 主机B按大端接收:认为第一个字节是高位,得到0x78563412

结果完全错了!

为了避免这种混乱,TCP/IP协议规定:网络传输统一使用大端字节序,也叫网络字节序(Network Byte Order)。

5.3 主机字节序和网络字节序的转换

发送数据时:

  • 如果主机是大端,直接发送,不需要转换
  • 如果主机是小端,需要先转换成大端,再发送

接收数据时:

  • 如果主机是大端,直接使用,不需要转换
  • 如果主机是小端,需要先转换成小端,再使用

但你不需要自己判断主机是大端还是小端,系统提供了转换函数:

c 复制代码
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);   // 主机序 → 网络序 (32位)
uint16_t htons(uint16_t hostshort);  // 主机序 → 网络序 (16位)
uint32_t ntohl(uint32_t netlong);    // 网络序 → 主机序 (32位)
uint16_t ntohs(uint16_t netshort);   // 网络序 → 主机序 (16位)

函数名很好记:

  • h:host(主机)
  • n:network(网络)
  • l:long(32位)
  • s:short(16位)

所以htonl就是"host to network long",把32位整数从主机字节序转换为网络字节序。

5.4 什么时候需要转换

不是所有数据都需要转换,只有多字节的整数类型才需要:

需要转换的

  • IP地址(32位整数)
  • 端口号(16位整数)
  • 协议报头中的长度、序列号等字段

不需要转换的

  • 字符串(字符串是字节数组,没有字节序问题)
  • 单字节数据(只有一个字节,不存在高低位顺序)

实际编程中,处理IP地址和端口号时一定要记得转换。比如:

c 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);              // 端口号要转换
addr.sin_addr.s_addr = inet_addr("192.168.1.1");  // IP地址也要转换

如果忘了转换,在小端机器上,端口号8080会被理解成41021(字节序颠倒了),导致连接失败。


六、Socket编程接口预览

6.1 Socket API是什么

Socket API是操作系统提供的网络编程接口。它是一层抽象,让应用程序不需要关心底层网络协议的细节,就能进行网络通信。

Socket API最初是在BSD Unix中设计的,后来成为网络编程的标准接口。Linux、Windows、macOS等操作系统都实现了Socket API,虽然细节略有差异,但核心函数是一致的。

6.2 常见的Socket函数

这里先预览一下,具体用法后面会详细讲解。

6.2.1 创建Socket
c 复制代码
int socket(int domain, int type, int protocol);
  • domain:地址族,通常是AF_INET(IPv4)或AF_INET6(IPv6)
  • type:传输类型,SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)
  • protocol:具体协议,通常填0,让系统自动选择

返回值是一个文件描述符,后续操作都通过这个描述符进行。

6.2.2 绑定端口
c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

服务器需要绑定一个端口号,这样客户端才知道去哪里连接。bind函数把Socket绑定到指定的IP地址和端口号。

6.2.3 监听连接(TCP服务器)
c 复制代码
int listen(int sockfd, int backlog);

TCP服务器调用listen开始监听端口,等待客户端连接。backlog参数指定连接队列的长度。

6.2.4 接受连接(TCP服务器)
c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept() 会从监听 socket 上取出一个已完成握手的连接,并返回新的 socket fd,之后服务器用这个新 fd 与该客户端通信;原来的监听 socket 继续用于接收新的连接。

6.2.5 发起连接(TCP客户端)
c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

TCP客户端调用connect连接到服务器。这个函数会触发TCP的三次握手过程。

6.2.6 发送和接收数据
c 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

// 或者用通用的read/write函数
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

对于UDP,使用:

c 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

Server(TCP):socket → bind → listen → accept → recv/send → close

Client(TCP):socket → connect → send/recv → close

UDP:socket → bind(可选) → sendto/recvfrom → close

6.3 sockaddr结构体

Socket API使用struct sockaddr来表示地址信息。但这是一个通用结构,实际使用时需要转换成具体的地址结构。

6.3.1 通用地址结构
c 复制代码
struct sockaddr {
    sa_family_t sa_family;  // 地址族,AF_INET或AF_INET6
    char sa_data[14];       // 地址数据
};
6.3.2 IPv4地址结构
c 复制代码
struct sockaddr_in {
    sa_family_t sin_family;     // 地址族,AF_INET
    in_port_t sin_port;         // 端口号(网络字节序)
    struct in_addr sin_addr;    // IP地址
    unsigned char sin_zero[8];  // 填充字节,保证和sockaddr一样大
};

struct in_addr {
    uint32_t s_addr;  // IP地址(网络字节序)
};

实际编程中,我们使用sockaddr_in,然后强制转换成sockaddr *传给系统调用:

c 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("192.168.1.1");

bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

6.4 为什么要这样设计

你可能会奇怪,为什么不直接用sockaddr_in,还要转换成sockaddr

原因是Socket API设计时要支持多种地址类型(IPv4、IPv6、Unix域套接字等)。sockaddr是一个通用的抽象类型,具体的地址类型通过sa_family字段来区分。

这样设计的好处是:Socket API的函数原型是统一的,不管你用IPv4还是IPv6,函数接口都一样,只是传入的结构体类型不同。这种设计思想在C语言中很常见,通过类型转换实现多态。


七、本篇总结

7.1 核心要点

端口号的作用

  • 端口号在传输层标识主机上的唯一进程
  • 16位整数,范围0-65535
  • 知名端口号(0-1023)预留给常用服务
  • 动态端口号(49152-65535)由操作系统分配给客户端
  • 一个端口只能被一个进程占用

Socket的概念

  • Socket = IP地址 + 端口号
  • 可以唯一标识网络中的一个进程
  • 网络通信的四元组:{源IP, 源端口, 目的IP, 目的端口}
  • 网络通信本质是进程间通信

TCP vs UDP

  • TCP:面向连接、可靠传输、面向字节流、有流量控制
  • UDP:无连接、不可靠传输、面向数据报、开销小速度快
  • 选择标准:能否容忍数据丢失

网络字节序

  • 网络传输统一使用大端字节序
  • 主机可能是大端或小端
  • 发送前要把主机字节序转换为网络字节序
  • 接收后要把网络字节序转换为主机字节序
  • 使用htonl/htons/ntohl/ntohs函数进行转换
  • 只有多字节整数需要转换,字符串和单字节数据不需要

Socket API

  • 操作系统提供的网络编程接口
  • 核心函数:socket、bind、listen、accept、connect、send/recv
  • sockaddr是通用地址结构,sockaddr_in是IPv4地址结构
  • 使用时需要强制类型转换

7.2 容易混淆的点

  1. 端口号和进程ID的区别:端口号是网络层面的概念,可以跨主机使用;进程ID是操作系统内部的概念,只在本地有效。端口号和进程ID可以不一样,一个进程可以绑定多个端口。

  2. Socket的多重含义:Socket既可以指IP+Port这个概念,也可以指Socket API中的文件描述符,还可以指Socket编程这个技术领域。具体含义要根据上下文判断。

  3. TCP和UDP不是互斥的:一个服务器可以同时提供TCP和UDP服务,只要使用不同的Socket。比如DNS服务通常同时监听TCP 53和UDP 53。

  4. 字节序转换的时机 :发送数据时,在调用bind/connect之前就要转换好IP地址和端口号。接收数据时,从accept/recvfrom得到的地址信息也是网络字节序,使用前要转换。忘记转换是新手常犯的错误。

  5. 为什么要强制转换sockaddr :这不是设计缺陷,而是C语言实现多态的方式。系统调用需要一个通用的地址类型,通过类型转换和sa_family字段来识别具体的地址结构。

  6. 端口号的网络字节序:端口号虽然只有16位,但也要转换字节序。你填8080,在小端机器上会被理解成41021。这是很隐蔽的bug,调试时要注意。


💬 总结:这一篇把Socket编程的预备知识讲清楚了。端口号、Socket、TCP/UDP、网络字节序,这些是网络编程的基础概念。理解了这些,后面学习具体的Socket编程时,你就知道为什么要这样写代码,每个参数的含义是什么。下一篇我们会开始实际编写网络程序,从UDP Echo服务器开始,逐步掌握Socket编程的技巧。
👍 点赞、收藏与分享:如果这篇文章帮你理清了Socket编程的基础概念,请点赞收藏!下一篇会有完整的代码示例,敬请期待!

相关推荐
阿猿收手吧!1 小时前
【C++】volatile与线程安全:核心区别解析
java·c++·安全
德迅云安全—珍珍2 小时前
低配服务器性能不够用怎么去优化?
运维·服务器
-dzk-2 小时前
【代码随想录】LC 707.设计链表
数据结构·c++·算法·链表
酣大智2 小时前
DHCP中继配置实验
运维·网络·网络协议·tcp/ip·华为
倔强菜鸟2 小时前
2026.2.2--Jenkins的基本使用
java·运维·jenkins
笑锝没心没肺2 小时前
Linux Audit 系统配置介绍
linux·运维·服务器
小义_2 小时前
【RH134知识点问答题】第6章 管理 SELinux 安全性
linux·网络·云原生·rhel
魏波.2 小时前
主流 Linux 发行版有哪些?
linux
txinyu的博客2 小时前
解析muduo源码之 Buffer.h & Buffer.cc
c++