引言
在计算机网络中,跨网段的主机之间进行文件传输,本质上是一个跨越不同网络层、经过路由转发、最终在应用层完成数据交换的过程。本文将从网络分层的角度,梳理跨网段文件传输的完整流程。
封装
下图展示了跨网段文件传输从应用层封装到数据链路层封装,再经路由器转发、逐层解封装的完整流程:
从图中可以看出,数据在源主机侧逐层向下封装,经过路由器逐跳转发,再在目标主机侧逐层向上解封装,最终在应用层还原出原始文件。
跨网段主机的文件传输,是应用层数据经过逐层封装、路由转发、逐层解封装,最终在目标主机还原的完整过程。理解这一过程,有助于排查网络故障和优化传输性能。
不同的协议层对数据包有不同的称谓,在 传输层 叫做 段(segment),在 网络层 叫做 数据报(datagram),在 链路层 叫做 帧(frame)。
应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部 (header),称为封装(Encapsulation)。
首部信息中包含了一些类似于首部有多长,载荷 (payload)有多长,上层协议是什么等信息。
数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的"上层协议字段"将数据交给对应的上层协议处理。

网络编程
在了解网络通信传输的过程
那以编码方式实现网络通信,就是网络编程。
在进行一次网络数据传输时
发送端 与 接收端
发送端
数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端
数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
请求 与 响应
一般地 网络资源获取时 涉及到两次⽹络数据传输 即 先请求数据的发送 再响应数据的发送
客⼾端 与 服务端
服务端
在常⻅的⽹络数据传输场景下,把提供服务的⼀⽅进程,称为服务端,可以提供对外服务
客⼾端
获取服务的⼀⽅进程,称为客⼾端
Socket套接字
定义
Socket 套接字是由系统提供、用于 网络通信 的技术,是基于 TCP/IP 协议 的网络通信基本操作单元。基于 Socket 套接字的网络程序开发,就是 网络编程。
分类
字主要针对传输层协议划分为
原始套接字
原始套接字⽤于⾃定义传输层协议,⽤于读写内核没有处理的IP协议数据。
流套接字
使⽤传输层 TCP 协议
• 有连接
• 可靠传输
• ⾯向字节流
• 有接收缓冲区,也有发送缓冲区
• ⼤⼩不限
数据报套接字
使⽤ 传输层UDP协议
• ⽆连接
• 不可靠传输
• ⾯向数据报
• 有接收缓冲区,⽆发送缓冲区
• ⼤⼩受限:⼀次最多传输64k
流套接字与数据报套接字对比
| 对比维度 | 流套接字 | 数据报套接字 |
|---|---|---|
| 传输层协议 | TCP | UDP |
| 连接方式 | 有连接 | 无连接 |
| 可靠性 | 可靠传输 | 不可靠传输 |
| 数据形式 | 面向字节流 | 面向数据报 |
| 缓冲区 | 有接收缓冲区,也有发送缓冲区 | 有接收缓冲区,无发送缓冲区 |
| 数据大小 | 大小不限 | 大小受限,一次最多传输 64k |
简单来说,流套接字 基于 TCP ,提供可靠、有序、面向字节流的传输,适合对数据完整性要求高的场景;数据报套接字 基于UDP,提供无连接、不可靠的传输,但开销更小、实时性更好,适合对延迟敏感或允许少量丢包的场景。
TCP
TCP 常见方法签名
基于 TCP 的流套接字编程,服务端与客户端常用方法签名如下:
java
// 服务端
ServerSocket serverSocket = new ServerSocket(int port);
Socket socket = serverSocket.accept();
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
socket.close();
serverSocket.close();
// 客户端
Socket socket = new Socket(String host, int port);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
socket.close();
ServerSocket
ServerSocket 是 Java 中用于 TCP 服务端的 套接字类 ,负责在指定端口上监听客户端的连接请求。它本身不直接传输数据,而是通过 accept() 方法等待并接受客户端连接,返回一个用于实际读写数据的 Socket 对象。简单来说,ServerSocket 就是服务端"开门迎客"的入口。
|-----------------------------|------------------------------------------------------------|
| ⽅法签名 | ⽅法说明 |
| ServerSocket(int port ) | 创建⼀个服务端流套接字 Socket,并绑定到指定端⼝ |
| Socket accept() | 开始监听并接受客户端的连接请求,返回一个与该客户端通信的 Socket 对象;该方法会阻塞,直到有客户端连接到达 |
| void close() | 关闭此套接字 |
Socket
Socket 是 Java 中用于 TCP 客户端的 套接字类 ,负责与指定 IP 和端口上的服务端建立连接。它直接承载数据的读写,连接建立后,通过 getOutputStream() 发送数据、getInputStream() 接收数据。简单来说,Socket 就是客户端与服务端之间"通话"的通道。
|--------------------------------------|---------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host, int port) | 创建一个客户端流套接字 Socket,并与对应 IP 的主机上、对应端口的进程建立连接 |
| InetAddress getInetAddress() | 返回此 Socket 连接到的远程主机的 IP 地址 |
| InputStream getInputStream() | 返回此 Socket 的输入流,用于读取服务端发来的数据 |
| OutputStream getOutputStream() | 返回此 Socket 的输出流,用于向服务端发送数据 |
| void close() | 关闭此 Socket,释放占用的连接资源 |
UDP
UDP 常见方法签名
基于 UDP 的数据报套接字编程,发送端与接收端常用方法签名如下:
java
// 发送端
DatagramSocket socket = new DatagramSocket();
byte[] data = "hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("host"), port);
socket.send(packet);
socket.close();
// 接收端
DatagramSocket socket = new DatagramSocket(int port);
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength());
socket.close();
可以看出,TCP 编程需要先建立连接(accept/connect),再通过输入输出流读写数据;UDP 编程无需建立连接,直接通过 DatagramPacket 封装数据报进行发送和接收。
一次完整的 UDP 数据报发送与接收流程如下:
- 发送端准备数据:将要发送的内容转换为字节数组,并指定目标主机的 IP 地址和端口号。
- 发送端封装数据报 :通过
DatagramPacket把字节数组、长度、目标 IP 和端口封装成一个数据报包裹。 - 发送端发送 :调用
DatagramSocket.send(packet),将数据报交给系统,由 UDP 协议直接发往目标主机,无需事先建立连接。 - 接收端准备接收 :接收端先创建绑定到指定端口的
DatagramSocket,并准备好一个空的DatagramPacket(指定缓冲区)用于接收数据。 - 接收端接收 :调用
DatagramSocket.receive(packet),系统把收到的数据填入这个空包裹中,该方法会阻塞直到数据到达。 - 接收端解析数据:从包裹中取出字节数组,按需还原为字符串或其他格式,完成一次数据报传输。
DatagramSocket
DatagramSocket.receive(packet) 是 UDP 接收端的 核心方法,它的作用可以概括为三点:
- 阻塞等待数据:调用后当前线程会一直阻塞,直到有数据报到达绑定的端口,才会继续向下执行。
- 填充数据报包裹 :系统把收到的数据写入传入的
DatagramPacket缓冲区中,同时更新该包裹的长度信息,便于后续读取。 - 完成一次接收 :方法返回后,即可通过
packet.getData()和packet.getLength()取出实际收到的字节数据。
send 负责把数据 "发出去", receive 负责把数据" 接进来"。接收端必须先准备好一个空的 DatagramPacket 作为容器,再调用 receive 等待数据填入,整个过程无需与发送端建立连接。
整个过程无需连接,发送端只管发出,接收端只管接收,双方通过 IP 和端口完成寻址,因此开销小、实时性好,但不保证数据一定到达、也不保证到达顺序。
简单来说,DatagramPacket 就是 UDP 传输中用来装数据的一个"数据报包裹"。
发送时,把要发的字节数组、长度、目标 IP 和端口一起装进包裹里,交给 DatagramSocket 发出去;接收时,先准备好一个空包裹(指定缓冲区),再通过 DatagramSocket 接收,数据就会被装进这个包裹里,最后从包裹中取出字节数组还原成字符串。
TCP 就像打电话:先拨号建立连接,接通后双方再一句一句地通话,说完挂断;
UDP就像发短信:不用提前打招呼,直接把消息发出去就行,对方能不能收到、按什么顺序收到,都不保证。所以 TCP 更可靠但稍慢,UDP更快但可能丢消息。
回显服务器
回显服务器(Echo Server)是一种最简单的网络服务程序:客户端把数据发送给服务器,服务器收到后原封不动地把数据返回给客户端,因此称为"回显"。
回显服务器常用于验证网络通信链路是否正常、测试 Socket 编程的基本流程
其核心工作流程如下:
- 服务端启动 :创建
ServerSocket并绑定端口,调用accept()等待客户端连接。 - 客户端连接 :客户端创建
Socket连接服务端,建立 TCP 连接。 - 发送数据:客户端通过输出流把数据发送给服务端。
- 回显数据:服务端从输入流读取数据,再通过输出流原样写回客户端。
- 接收数据:客户端从输入流读取服务端返回的数据,完成一次回显。
回显服务器就是"你发什么,我回什么",它不处理业务逻辑 ,只负责验证数据能否在两端之间正确往返。
简单来说,回显服务器就像一个"回声":客户端喊一句,服务器原样喊回来。它本身不做任何加工,只是把收到的数据原封不动地返回,用来确认网络通路是否畅通、两端能否正常收发数据。
下面给出一段最简单的回显服务器代码,帮助理解它的工作方式:
java
// 服务端:收到什么就原样返回什么
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept(); // 等待客户端连接
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buffer = new byte[1024];
int len = in.read(buffer); // 读取客户端发来的数据
out.write(buffer, 0, len); // 原样写回客户端
socket.close();
serverSocket.close();
这段代码的核心只有两步:先用 in.read(buffer) 读取客户端发来的数据,再用 out.write(buffer, 0, len) 把读到的内容原样写回,从而实现"你发什么,我回什么"。
一个服务器程序的典型处理流程通常包含以下三个步骤:
- 读取请求并解析:接收客户端发来的请求数据,并解析出其中的方法、路径、参数等信息。
- 根据请求计算响应 :依据解析得到的请求内容,执行相应的业务逻辑,生成对应的响应数据。(但在回显服务器从而省去了这一步)
- 返回响应:将计算得到的响应数据发送回客户端,完成一次请求-响应交互。