计算机网络背景
网络发展
独立模式: 计算机之间相互独立;

网络互联: 多台计算机连接在一起, 完成数据共享

局域网 LAN: 计算机数量更多了, 通过交换机和路由器连接在一起

广域网 WAN: 将远隔千里的计算机都连在一起;

初识协议
• "协议" 是一种约定.
• 打电话约定电话铃响的次数的约定
计算机之间的传输媒介是光信号和电信号. 通过 "频率" 和 "强弱" 来表示 0 和 1 这样的 信息. 要想传递各种不同的信息, 就需要约定好双方的数据格式

只要通信的两台主机, 约定好协议就可以了么?
• 定好协议,但是你用频率表示 01,我用强弱表示 01,就好比我用中国话,你 用葡萄牙语一样,虽然大家可能遵守的一套通信规则,但是语言不同,即是订好了 基本的协议,也是无法正常通信的
所以,完善的协议,需要更多更细致的规定,并让参与的人都要遵守。
• 计算机生产厂商有很多;
• 计算机操作系统,也有很多
• 计算机网络硬件设备, 还是有很多;
• 如何让这些不同厂商之间生产的计算机能够相互顺畅的通信? 就需要有人站出 来, 约定一个共同的标准, 大家都来遵守, 这就是网络协议
协议分层
协议本质也是软件,在设计上为了更好的进行模块化,解耦合,也是被设计成为 层状结构的
软件分层的好处:分层可以实现解耦合,让软件维护的 成本更低

OSI七层模型
• OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放 式系统互联参考模型,是一个逻辑上的定义和规范;
• 把网络从逻辑上分为了 7 层. 每一层都有相关、相对应的物理设备,比如路由 器,交换机;
• OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型 的主机实现数据传输;
• 它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚, 理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可 靠的通讯;
• 但是, 它既复杂又不实用; 所以我们按照 TCP/IP 四层模型来讲
在网络角度,OSI 定的协议 7 层模型其实非常完善,但是在实际操作的过程 中,会话层、表示层是不可能接入到操作系统中的,所以在工程实践中,最终落地是 5 层协议

TCP/IP 五层(或四层)模型
TCP/IP 是一组协议的代名词,它还包括许多协议,组成了 TCP/IP 协议簇. TCP/IP 通讯协议采用了 5 层的层级结构,每一层都呼叫它的下一层所提供的网络来完 成自己的需求
物理层我们考虑的比较少,我们只考虑软件相关的内容. 因此很多时候我们直接称为 TCP/IP 四层模型.

• 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早 期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的 wifi 无线网使用 电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗 干扰性等. 集线器(Hub)工作在物理层.
• 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同 步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就 自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线 LAN 等标准. 交换机 (Switch)工作在数据链路层.
• 网络层: 负责地址管理和路由选择. 例如在 IP 协议中, 通过 IP 地址来标识一台 主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器 (Router)工作在网路层.
• 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据 可靠的从源主机发送到目标主机.
• 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协 议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层
再识协议
为什么要有 TCP/IP 协议?
• 首先,即便是单机,你的计算机内部,其实都是存在协议的,比如:其他设备和 内存通信,会有内存协议。其他设备和磁盘通信,会有磁盘相关的协议,比如: SATA,IDE,SCSI 等。只不过我们感知不到罢了。而且这些协议都在本地主机各自 的硬件中,通信的成本、问题比较少
• 其次,网络通信最大的特点就是主机之间变远了。任何通信特征的变化,一定会 带来新的问题,有问题就得解决问题,所以需要新的协议咯

什么是 TCP/IP 协议?
• TCP/IP 协议的本质是一种解决方案 • TCP/IP 协议能分层,前提是因为问题们本身能分层
简单来说,TCP/IP 协议不是一个单一的协议,而是一个协议族的统称。它定义了电子设备如何连入互联网,以及数据如何在它们之间进行传输的标准。
这个名字来源于其中两个最核心的协议:传输控制协议 (TCP) 和 网际协议 (IP)。
TCP/IP 协议与操作系统的关系(宏观上,怎么实现的)


问题:主机 B 能识别 data,并且准确提取 a=10,b=20,c=30 吗? 回答:答案是肯定的!因为双方都有同样的结构体类型 struct protocol。也就是说, 用同样的代码实现协议,用同样的自定义数据类型,天然就具有"共识",能够识别 对方发来的数据,这不就是约定吗?
关于协议的朴素理解:所谓协议,就是通信双方都认识的结构化的数据类型 因为协议栈是分层的,所以,每层都有双方都有协议,同层之间,互相可以认识对 方的协议
网络传输基本流程
局域网网络传输流程图
局域网(以太网为例)通信原理
两台主机在同一个局域网,是能够直接通信的 ,原理类似上课
每台主机在局域网上,要有唯一的标识来保证主机的唯一性:mac 地址
认识 MAC 地址
• MAC 地址用来识别数据链路层中相连的节点;
• 长度为 48 位, 及 6 个字节. 一般用 16 进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)
• 在网卡出厂时就确定了, 不能修改. mac 地址通常是唯一的(虚拟机中的 mac 地 址不是真实的 mac 地址, 可能会冲突; 也有些网卡支持用户配置 mac 地址)

• 以太网中,任何时刻,只允许一台机器向网络中发送数据
• 如果有多台同时发送,会发生数据干扰,我们称之为数据碰撞
• 所有发送数据的主机要进行碰撞检测和碰撞避免
• 没有交换机的情况下,一个以太网就是一个碰撞域
• 局域网通信的过程中,主机对收到的报文确认是否是发给自己的,是通过目标 mac 地址判定
初步明白了局域网通信原理,再来看同一个网段内的两台主机进行发送消息的过程

而其中每层都有协议,所以当我进行进行上述传输流程的时候,要进行封装和解包

下面我们明确一下概念
• 报头部分,就是对应协议层的结构体字段,我们一般叫做报头
• 除了报头,剩下的叫做有效载荷
• 故,报文 = 报头 + 有效载

不同层的完整报文的叫法:
• 不同的协议层对数据包有不同的称谓。
在传输层叫做段(segment),在网络层叫做 数据报 (datagram),在链路层叫做帧(frame).
• 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部 (header),称为封装(Encapsulation).
• 首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长, 上层协议是 什么等信息.
• 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 "上层协议字段" 将数据交给对应的上层协议处理.
总结:
在网络传输的过程中,数据不是直接发送给对方主机的,而是先要自定向下将数据交 付给下层协议,最后由底层发送,然后由对方主机的底层来进行接受,在自底向上进 行向上交付


数据包封装和分用
数据封装的过程:

数据分用的过程:

我们学习任何协议,都要先宏观上建立这样的认识:
-
要学习的协议,是如何做到解包的?只有明确了解包,封包也就能理解
-
要学习的协议,是如何做到将自己的有效载荷,交付给上层协议的?
跨网络传输流程图
网络中的地址管理 - 认识 IP 地址
IP 协议有两个版本, IPv4 和 IPv6.。提到 IP 协议, 没有特殊说明的, 默认都是指 IPv4
• IP 地址是在 IP 协议中, 用来标识网络中不同主机的地址;
• 对于 IPv4 来说, IP 地址是一个 4 字节, 32 位的整数;
• 我们通常也使用 "点分十进制" 的字符串表示 IP 地址, 例如 192.168.0.1 ; 用点 分割的每一个数字表示一个字节, 范围是 0 - 255
跨网段的主机的数据传输. 数据从一台计算机到另一台计算机传输过程中要经过一个或 多个路由器.

首先理解一下 IP 地址的意义 • 为什么要去目标主机,先要走路由器? • 目的 IP 的意义

1. 目的 IP 的意义:唯一的身份标识
图中的 目的 IP(172.168.2.2) 代表了**"你最终想去哪里"**。
-
全球唯一:它就像是你在互联网世界里的"门牌号"。不管你在哪个城市、哪栋楼(不管你的物理位置在哪),这个 IP 地址都能唯一定位到你这台电脑。
-
逻辑地址:它是工作在网络层的(图中圈出的部分)。它不关心具体的路线,只负责告诉网络:"我是谁,我要找谁"。
-
跨网段通信 :图中用户 A 的 IP 是
192.168.2.2,用户 B 是172.168.2.2。这两个 IP 属于不同的网段(可以理解为两个不同的小区)。IP 地址的存在,使得系统知道这是一个跨网段的请求,从而触发了"找路由器"的机制。
2. 为什么要先走路由器?
要回答这个问题,得看图中的那个提示框:"发现不是发给本网段主机的报文,就推送给路由器"。
简单来说:因为目标主机"不在同一个房间",你需要一个"管家"帮你递送。
-
局域网的限制:
你的电脑(用户 A)发出的数据,首先会通过数据链路层寻找目标。数据链路层使用的是 MAC 地址(物理地址),它的寻址范围非常有限,通常只能在同一个局域网内(比如同一个办公室、校园网)直达。
-
路由器的角色(网关):
当你发送数据给
172.168.2.2时,你的电脑一查发现:"咦?这个 IP 不在我的本地列表里(不是 192.168.2.x 网段)"。这时候,电脑就知道:"我自己直接发不过去,必须交给路由器。"
因此,路由器充当了"跨网段快递员"的角色。你的数据先发给路由器(图中左侧线路),路由器再根据它的地图(路由表),把数据转发给下一个路由器,直到抵达用户 B 所在的网络。
3. 总结流程
-
应用层:用户 A 产生数据(Data)。
-
网络层 :加上 IP 头(源 IP:192.168.2.2,目的 IP:172.168.2.2)。系统发现目的 IP 不在本地,于是将数据交给默认网关(即路由器 192.168.2.1)。
-
数据链路层:将数据封装成帧,此时需要找路由器的 MAC 地址(macA),而不是找目的主机的 MAC 地址。
-
传输:数据到达路由器,路由器"拆包"看一眼目的 IP,重新"打包"发给下一段路,最终到达用户 B。
然后结合封装与解包,体现路由器解包和重新封装的特点

1. 用户 A 端:封装(Encapsulation)
当数据从用户 A 的应用层产生并向下传递时,每一层都会为其添加特定的头部信息,就像寄快递时不断添加包装和面单:
-
网络层(加 IP 头) :系统为数据包打上源 IP(
192.168.2.2)和目的 IP(172.168.2.2)。此时系统发现目的 IP 不在本地网段,于是通过 ARP 协议查找到达网关(路由器)的 MAC 地址(macLeft)。 -
数据链路层(加 MAC 头) :在数据帧的头部填入源 MAC(
macA)和目的 MAC(macLeft)。 -
发送:网卡将最终的二进制数据帧通过物理线路发送出去。
2. 路由器端:解包(De-encapsulation)
路由器接收到数据帧后,开始进行反向操作以读取地址信息:
-
去 MAC 头 :数据链路层剥离外层的数据帧,读取里面的 目的 MAC 地址 (
macLeft)。确认是发给自己的,于是将数据交给网络层。 -
读 IP 头 :网络层剥离 IP 头,读取里面的 目的 IP 地址 (
172.168.2.2)。 -
核心决策(路由选择):路由器查询自身的路由表,发现目的地在另一个网段,决定通过右侧接口转发出去。此时,内层携带原始数据和 IP 地址的数据包被提取出来,准备进行下一步。
3. 路由器端:重新封装(Re-encapsulation)
为了将数据包送到下一站,路由器必须为其换上"新衣服":
-
换 MAC 头 :数据链路层根据路由决策,将源 MAC 地址替换为路由器右侧接口的 MAC(
macRight),目的 MAC 地址替换为用户 B 的 MAC(macB)。 -
保留 IP 头 :关键点在于,内层的 IP 头(源 IP 和目的 IP)在整个过程中完全没有被修改。这保证了数据在复杂的网络中流转时,始终能明确最终的通信目标。
4. 用户 B 端:解包(交付)
用户 B 接收到数据帧后,重复常规的解包流程:
-
剥离 MAC 头,检查目的 MAC 是否为自己。
-
剥离 IP 头,检查目的 IP 是否为自己。
-
确认无误后,将最内层的"数据(data: 你好)"向上交付给传输层和应用层进行处理。
对比 IP 地址和 Mac 地址的区别
• IP 地址在整个路由过程中,一直不变(目前,我们只能这样说明,后面在修正)
• Mac 地址一直在变
• 目的 IP 是一种长远目标,Mac 是下一阶段目标,目的 IP 是路径选择的重要依 据,mac 地址是局域网转发的重要依据
Socket 编程预备
1. 理解源 IP 地址和目的 IP 地
IP 在网络中,用来标识主机的唯一性
思考一个问题:数据传输到主机不是目的。因为数据是给人用 的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览? 但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过 启动的 qq,迅雷,浏览器。
而启动的 qq,迅雷,浏览器都是进程 。换句话说,进程是人在系统中的代表,只要把 数据给进程,人就相当于就拿到了数据。 所以:数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程, 才是目的。 但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标 进程?这就要在网络的背景下,在系统中,标识主机的唯一性。

2. 认识端口号
端口号(port)是传输层协议的内容.
• 端口号是一个 2 字节 16 位的整数;
• 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来 处理;
• IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程;
• 一个端口号只能被一个进程占用

端口号范围划分
• 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的 端口号都是固定的.
• 1024 - 65535: **操作系统动态分配的端口号.**客户端程序的端口号, 就是由操作 系统从这个范围分配的
理解 "端口号" 和 "进程 ID"
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程 ; 此处我们的端口号也 是唯一表示一个进程. 那么这两者之间是怎样的关系?
这是问题触及了网络编程和操作系统设计的核心。我们可以用一个经典的比喻来理解:
端口号是"服务窗口"或"电话号码",而进程ID是"窗口后的办事员"或"接电话的客服"。
核心关系:分工与解耦
-
端口号(Port) :网络通信的"门户"
-
作用 :它是一个16位的数字 ,用于标识主机上的一个网络服务 。当数据从网络到达你的电脑时,操作系统根据目标端口号 来决定将数据交给哪个进程中的哪个服务来处理。
-
类比(10086例子) :
10086是中国移动的客服热线号码(一个著名的端口)。这个号码是公开、固定、众所周知的。任何想联系移动客服的人,都会拨打这个号码。 -
特点:
-
相对稳定:知名服务使用固定端口(如HTTP: 80, HTTPS: 443)。
-
对外公开:是网络协议的一部分,用于远程寻址。
-
资源标识:是网络栈(传输层)管理的资源。
-
-
-
进程ID(PID) :系统内部的"身份证"
-
作用 :它是操作系统内核分配的一个数字 ,用于在系统内部唯一标识和管理一个正在运行的进程实例。它用于进程调度、内存分配、信号发送等。
-
类比(10086例子):在10086热线背后,可能有成千上万个客服人员(进程)。每个客服都有一个内部工号(PID)。工号是内部管理用的,对外部客户没有意义。客户不关心是谁接的电话,只关心电话能接通到正确的服务部门。
-
特点:
-
动态变化:进程每次启动时,PID通常都会改变。
-
内部私有:只在单机操作系统内部有效,不具备网络意义。
-
管理标识:是操作系统进程管理模块管理的资源。
-
-
为什么不用PID直接进行网络通信?(设计哲学)
为了解耦。
如果直接用PID作为网络地址,会导致:
-
强耦合:网络协议将完全依赖特定操作系统的进程管理机制,破坏了网络层的独立性。
-
不稳定性:PID是动态的。一个服务重启后,PID就变了,所有远程客户端都需要重新获取新的PID才能连接,这不可行。
-
缺乏抽象:一个进程可能提供多种网络服务(如一个Web服务器同时提供HTTP和HTTPS)。一个PID无法区分这些不同的服务"入口"。
两者如何协同工作?
当你的电脑收到一个数据包(比如发往 你的IP:80)时,操作系统的网络协议栈和进程管理模块会这样协作:
-
网络层:根据IP地址确认数据包是发给本机的。
-
传输层 :查看TCP/UDP头中的目标端口号(例如80)。
-
系统协作 :操作系统内核中维护着一张 "端口号-进程"映射表 。它通过查表,找到监听80端口的那个进程的PID(比如,找到了Nginx服务器的进程)。
-
交付数据:内核将数据包负载(应用层数据)放入该进程对应的缓冲区,并唤醒该进程进行处理。
总结一下关系:
-
端口号 是网络通信的公共地址,用于从网络定位到主机上的服务。
-
进程ID 是操作系统内部的私有句柄,用于管理系统资源。
-
一个进程 (一个办事员)可以打开多个端口(负责多个服务窗口)。
-
一个端口 在任一时刻只能被一个进程监听(一个窗口后只能坐一个办事员),否则会发生冲突。
-
操作系统内核充当"调度中心",维护着端口到进程 的映射关系,完美地将网络世界 的寻址(端口)与系统内部的管理(PID)解耦开来。
理解源端口号和目的端口号
传输层协议(TCP 和 UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的,要发给谁";
理解 socket
• 综上,IP 地址用来标识互联网中唯一的一台主机,port 用来标识该主机上唯一的 一个网络进程
• IP+Port 就能表示互联网中唯一的一个进程
• 所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp, srcPort,dstIp,dstPort}这样的 4 元组就能标识互联网中唯二的两个进程
• 所以,网络通信的本质,也是进程间通信
• 我们把ip+port 叫做套接字 socke
3. 传输层的典型代表
传输层是属于内核 的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信

认识 TCP 协议
此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论 TCP 的一些细节问题.
• 传输层协议
• 有连接
• 可靠传输
• 面向字节流
认识 UDP 协议
此处我们也是对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后 面再详细讨论.
• 传输层协议
• 无连接
• 不可靠传输
• 面向数据
4. 网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的 多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之 分. 那么如何定义网络数据流的地址呢?
• 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
• 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从 低到高的顺序保存;
• 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高 地址.
• TCP/IP 协议规定,网络数据流应采用大端字节序 ,即低地址高字节.
• 不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来 发送/接收数据;

• 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即 可;
-
为什么要转换?
-
如果你用的是小端机(比如常见的 x86 电脑),它在内存里存
0x1234abcd的时候,0xcd是在0x0000地址的。 -
但是网络规定必须先发
0x12。 -
所以小端机在发送前,必须先在 CPU 内部把数据"倒过来"转成大端序,然后再发出去。这就是所谓的"主机字节序转网络字节序"。
-
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运 行,可以调用以下库函数做网络字节序和主机字节序的转换。

• 这些函数名很好记,h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位 短整数。
• 例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地 址转换后准备发送。
• 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
• 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
5. socket 编程接口
socket 常见 API
cpp
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
1. socket() - 创建套接字
cpp
int socket(int domain, int type, int protocol);
| 参数名 | 类型 | 详细描述 |
|---|---|---|
| domain | int | 指定通信的协议族(Protocol Family),即地址族(Address Family)。决定了socket的地址类型,如IPv4、IPv6等。 |
| 常见取值: • AF_INET :IPv4协议 • AF_INET6 :IPv6协议 • AF_UNIX 或 AF_LOCAL :本地通信(进程间通信) • AF_PACKET:底层数据包接口,用于直接访问网络层 | ||
| type | int | 指定socket类型,决定了通信的语义(如面向连接、无连接)。 |
| 常见取值: • SOCK_STREAM :面向连接的流套接字,提供可靠、双向、基于字节流的通信。对应TCP协议。 • SOCK_DGRAM :无连接的数据报套接字,提供不可靠、不保证顺序、固定最大长度的数据报通信。对应UDP协议。 • SOCK_RAW:原始套接字,允许程序直接访问底层协议(如IP、ICMP)。需要特权。 | ||
| protocol | int | 指定socket使用的具体协议。通常设置为0,表示根据domain和type选择默认协议。 |
| 常见取值: • 0 :自动选择。对于AF_INET和SOCK_STREAM,默认是TCP;对于AF_INET和SOCK_DGRAM,默认是UDP。 • IPPROTO_TCP :TCP协议,通常用于SOCK_STREAM。 • IPPROTO_UDP:UDP协议,通常用于SOCK_DGRAM。 |
返回值:
-
成功:返回一个非负整数,即套接字描述符(socket descriptor),类似于文件描述符。
-
失败:返回-1,并设置errno。
2. bind() - 绑定地址
cpp
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
| 参数 | 类型 | 详细说明 |
|---|---|---|
| socket | int | socket()函数返回的套接字描述符 |
| address | const struct sockaddr* | 指向地址结构体的指针,包含要绑定的IP地址和端口号。这个指针需要被强制转换为struct sockaddr*类型,无论实际使用的是什么地址结构 |
| address_len | socklen_t | 地址结构体的长度,以字节为单位。通常使用sizeof()运算符获取 |
常见地址设置:
-
INADDR_ANY: 绑定到所有网络接口(0.0.0.0) -
htonl(INADDR_LOOPBACK): 绑定到环回地址(127.0.0.1) -
inet_addr("192.168.1.100"): 绑定到指定IP地址
返回值:
-
成功:返回0。
-
失败:返回-1,并设置errno。
注意:服务器程序通常需要调用bind()来绑定一个众所周知的端口,而客户端可以不调用bind(),由系统自动分配端口。
3. listen() - 监听连接
cpp
int listen(int socket, int backlog);
| 参数名 | 类型 | 详细描述 |
|---|---|---|
| socket | int | 要设置为监听状态的套接字描述符。这个套接字必须已经通过bind()绑定了一个地址。 |
| backlog | int | 指定连接队列的最大长度。当有多个客户端连接请求到达时,如果服务器正在处理,则这些请求会在队列中排队等待。backlog参数决定了队列中最多可以有多少个等待接受的连接。注意,在Linux 2.2之后,这个参数指的是已完成三次握手但还未被accept取走的连接数(即ESTABLISHED状态)。未完成连接队列(SYN_RCVD状态)的最大长度由系统参数控制。 |
返回值:
-
成功:返回0。
-
失败:返回-1,并设置errno。
注意:listen()只用于面向连接的套接字(如SOCK_STREAM),并且通常由服务器调用。
backlog参数深入解释:
内核为每个监听套接字维护两个队列:
-
未完成连接队列(SYN队列):
-
存放收到SYN但未完成三次握手的连接
-
状态为SYN_RCVD
-
-
已完成连接队列(Accept队列):
-
存放已完成三次握手但未被accept()取走的连接
-
状态为ESTABLISHED
-
backlog参数指定的是这个队列的大小
-
backlog取值建议:
-
Linux 2.2之前:backlog是未完成+已完成队列的总大小
-
Linux 2.2之后:backlog只是已完成队列的大小
-
常用值:5, 10, 128, 511
-
可以通过
/proc/sys/net/core/somaxconn调整系统级最大值
4. accept() - 接受连接
cpp
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
| 参数名 | 类型 | 详细描述 |
|---|---|---|
| socket | int | 监听套接字描述符,即已经调用了listen()的套接字。 |
| address | struct sockaddr* | 用来返回客户端的地址信息。如果不需要客户端的地址,可以设置为NULL。 |
| address_len | socklen_t* | 这是一个值-结果参数(value-result argument)。调用时,指向一个整数,表示address指向的缓冲区的长度;返回时,这个整数会被设置为客户端地址的实际长度。如果地址的实际长度超过提供的缓冲区长度,则地址会被截断。 |
返回值:
-
成功:返回一个新的套接字描述符,这个新套接字代表与客户端的连接。原监听套接字继续用于接受其他连接。
-
失败:返回-1,并设置errno。
注意:accept()会阻塞,直到有连接请求到达。如果不想阻塞,可以将监听套接字设置为非阻塞模式。
函数行为详解:
-
accept()从已完成连接队列中取出第一个连接
-
如果队列为空,accept()会阻塞(默认情况下),直到有连接可用
-
返回一个新的套接字描述符,专门用于与该客户端通信
-
原监听套接字继续监听新的连接请求
5. connect() - 发起连接
cpp
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| 参数名 | 类型 | 详细描述 |
|---|---|---|
| sockfd | int | 客户端套接字描述符,由socket()创建。 |
| addr | const struct sockaddr* | 指向服务器地址结构体的指针,包含了服务器的IP地址和端口号。 |
| addrlen | socklen_t | 服务器地址结构体的长度,通常为sizeof(struct sockaddr_in)。 |
返回值:
-
成功:返回0,表示连接已建立。
-
失败:返回-1,并设置errno。
常见错误返回值
| 错误码 | 含义 | 可能原因 |
|---|---|---|
| EACCES | 权限被拒绝 | 绑定到保留端口(0-1023)但无root权限 |
| EADDRINUSE | 地址已被使用 | 端口已被其他进程占用 |
| EADDRNOTAVAIL | 地址不可用 | 指定的IP地址不属于本机 |
| ECONNREFUSED | 连接被拒绝 | 目标服务器未运行或拒绝连接 |
| ETIMEDOUT | 连接超时 | 服务器不响应 |
| EINPROGRESS | 连接进行中 | 套接字是非阻塞的,连接需要时间 |
注意:
-
对于TCP套接字,connect()会触发三次握手过程,阻塞直到握手成功或失败。
-
对于UDP套接字,connect()不会真正建立连接,而是记录目标地址,这样后续可以使用send()和recv(),而不必每次都指定地址。同时,只会接收来自该地址的数据报。
函数行为详解:
对于TCP套接字:
-
尝试与指定服务器建立TCP连接
-
触发TCP三次握手过程
-
默认情况下阻塞直到连接成功或失败
-
成功返回0,失败返回-1
对于UDP套接字:
-
不实际建立连接(无握手过程)
-
只是设置默认的目标地址
-
之后可以使用send()/recv()代替sendto()/recvfrom()
-
只接收来自该地址的数据报
sockaddr 结构
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及 后面要讲的 UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同

• IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构 体表示,包括 16 位地址类型, 16 位端口号和 32 位 IP 地址.
• IPv4、IPv6 地址类型分别定义为常数 AF_INET、AF_INET6. 这样,只要取得某 种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可 以根据地址类型字段确定结构体中的内容.
• socket API 可以都用 struct sockaddr *类型表示, 在使用的时候需要强制转化成 sockaddr_in; 这样的好处是程序的通用性, 可以接收 IPv4, IPv6, 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数;
sockaddr 结构

sockaddr_in 结构

虽然 socket api 的接口是 sockaddr, 但是我们真正在基于 IPv4 编程时, 使用的数据结 构是 sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP 地址
in_addr 结构

in_addr 用来表示一个 IPv4 的 IP 地址. 其实就是一个 32 位的整数;