文章目录
-
- 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不就能标识唯一的进程了?
技术上确实可以,但有几个问题:
-
跨主机的PID没有意义。主机A上的PID 1234和主机B上的PID 1234是两个完全不同的进程。PID是操作系统内部的概念,不能跨主机使用。
-
让网络和进程管理强耦合。如果用PID标识网络进程,那么网络子系统就要和进程管理子系统紧密绑定,这违背了模块化设计的原则。
-
PID是动态分配的。你今天启动浏览器,PID可能是1234;明天启动,PID可能变成5678。但HTTP服务总是用80端口,这样客户端才知道去哪里找服务器。
所以,实际设计中引入了一个新的概念:端口号(Port)。
1.3 端口号的作用
端口号是传输层协议的内容,它用来在网络层面标识主机上的唯一进程。
端口号是一个16位的整数,范围是0-65535。当数据到达主机后:
- 网络层根据IP地址找到目标主机
- 传输层根据端口号找到目标进程
- 操作系统把数据交给对应的进程处理
现在我们可以用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 容易混淆的点
-
端口号和进程ID的区别:端口号是网络层面的概念,可以跨主机使用;进程ID是操作系统内部的概念,只在本地有效。端口号和进程ID可以不一样,一个进程可以绑定多个端口。
-
Socket的多重含义:Socket既可以指IP+Port这个概念,也可以指Socket API中的文件描述符,还可以指Socket编程这个技术领域。具体含义要根据上下文判断。
-
TCP和UDP不是互斥的:一个服务器可以同时提供TCP和UDP服务,只要使用不同的Socket。比如DNS服务通常同时监听TCP 53和UDP 53。
-
字节序转换的时机 :发送数据时,在调用
bind/connect之前就要转换好IP地址和端口号。接收数据时,从accept/recvfrom得到的地址信息也是网络字节序,使用前要转换。忘记转换是新手常犯的错误。 -
为什么要强制转换sockaddr :这不是设计缺陷,而是C语言实现多态的方式。系统调用需要一个通用的地址类型,通过类型转换和
sa_family字段来识别具体的地址结构。 -
端口号的网络字节序:端口号虽然只有16位,但也要转换字节序。你填8080,在小端机器上会被理解成41021。这是很隐蔽的bug,调试时要注意。
💬 总结:这一篇把Socket编程的预备知识讲清楚了。端口号、Socket、TCP/UDP、网络字节序,这些是网络编程的基础概念。理解了这些,后面学习具体的Socket编程时,你就知道为什么要这样写代码,每个参数的含义是什么。下一篇我们会开始实际编写网络程序,从UDP Echo服务器开始,逐步掌握Socket编程的技巧。
👍 点赞、收藏与分享:如果这篇文章帮你理清了Socket编程的基础概念,请点赞收藏!下一篇会有完整的代码示例,敬请期待!