网络通信原理
交换机
上面所有的口都是等价的
电脑可以连到任意的口上,连上的电脑就都构成了局域网
路由器
WAN 口
LAN 口
要构成局域网的电脑都连到LAN口,WAN 口接 运营商给的网络
交换机和路由器的区别
路由器工作在 网络层
交换机工作在 数据链路层
IP 地址
描述了一个设备在网络上的位置
端口号
描述了一个主机上的哪个应用程序
有了 IP 可以确定主机,但是一个主机上可能有很多程序在使用网络
主机收到网络数据就需要区分出是交给哪个程序使用数据
每个程序在进行网络通信过程中,都需要一个端口号,同一个主机上,程序之间的端口号不能冲突
进行一次网络通信的过程中,涉及到的 IP 和 端口,其实有两个
目的 IP
目的端口
源 IP
源 端口
协议
一种通信过程中的约定,发送方和接收方需要提前商量好数据的格式,才能确保两者之间能够正确进行沟通
分层协议
网络通信的过程中,需要涉及到的细节,非常非常多,如果要有一个协议来完成网络通信,就需要约定好方方面面的内容,非常多的细节,导致这个协议非常复杂
这时候就会进行拆分,将一个复杂而又庞大的协议,拆分成多个功能单一的协议
为了让这些协议能更好的相互配合,引入了协议分层
网络通信协议拆分的多了之后也是分成了很多层
把功能定位相似的协议放到同一层之中
上层协议会调用下层协议的功能
下层协议给上层协议提供服务
只有相邻的层次之间可以进行沟通,不能跨层次调用
协议分层的初心是为了让复杂的一个协议变成更简单的多个协议还有好处
1.上层协议直接使用下层协议即可,不需要了解下层协议的细节
2.某一层协议进行替换之后,对于其它层没有影响
当前网络现状,有很多的协议,并且是按照 “分层” 的结构来组织的
OSI 七层网络协议 实施过程中七层太麻烦就简化成了五层
TCP/ IP 五层网络协议
应用层
程序拿到数据之后,要用来干啥,解决啥问题
传输层
负责关注网络数据包,起点和终点 端到端之间的传输
网络层
负责关注,起点终点之间,要走哪条路
网络链路层
负责两个相邻节点之间的传输
物理层
通信过程中的基础设施
TCP / IP 五层(四层) 模型
不算物理层 物理层是纯硬件的
说是五层,实际上我们能影响的只有应用层,后四层是系统内核 硬件 驱动程序 已经实现好了的
对于一台主机,它的操作系统内核实现了从传输层到物理层的内核,也就是下四层
对于一台路由器,它实现了从网络层到物理层,也就是下三层(工作在 网络层)
对于一台交换机,它实现了从数据链路层到物理层,也就是下两层(工作在 数据链路层)
对于 集线器,它只实现了物理层
事实上,一些交换器也能工作在网络层,有一些路由器功能,一些路由器也能工作在数据链路层,有一些交换机功能
1.应用层
QQ 从消息输入框获取到用户输入的 hello,就要把这个字符串构造成一个应用层的数据包
QQ这样的程序内部就设置了一个应用层协议,应用层数据包就是按照这个应用层协议约定的格式拼接
比如:
应用程序就会调用操作系统提供的 api,把这个数据包交给传输层
2.传输层
传输层就会把上述数据作为一个整体,再构造一个传输层的数据包
传输层涉及到的协议,最主要就是 TCP 和 UDP
假定使用UDP来进行通信,就会构造一个 UDP 的数据包
拼好传输层数据包之后,就要把这个数据包进一步的交给下属,网络层继续进行封装
交给下层指的是,下层协议提供一组 api,上层调用这个 api,并且把刚才构造好的数据通过参数传下去,下层协议就可以来处理这个数据了
对于 UDP 报头来说,承载的最重要的信息就是 源端口 和 目的端口
3.网络层
这里涉及到的最核心的协议,IP协议
网络层 IP 协议,把刚才的传输层的 UDP 数据包作为一个整体,再拼上 IP 协议的报头,构造成一个 IP 数据包
这里包含一些辅助转发的关键信息,此处最关键的信息就是源 IP 和 目的 IP
构造完成完整的 IP 的数据包之后,IP 协议继续调用数据链路层的 api,把数据再交给数据链路层这里的协议进行处理
4.数据链路层
数据链路层这里涉及到的核心协议,以太网
以太网数据帧,以 IP 数据包为一个整体,在这个基础上添加上 帧头 和 帧尾
这样的数据还要继续往下走交给物理层
5.物理层
把上述这样的以太网数据帧,二进制结构,转换成 光信号 / 电信号 / 电磁波 然后进行发送
B 的物理层收到光信号 / 电信号 / 电磁波,就会把这些物理信号转换为 数字信号得到一个 以太网数据帧,进一步的把这个数据交给数据链路层处理
2.数据链路层 以太网
按照以太网数据帧的格式,来解析,取出其中的载荷,再交给上层协议
3.网络层 IP 协议
按照 IP 协议的格式进行解析,取出其中的载荷,再交给上层协议
4.传输层 UDP协议
按照 UDP 协议格式来解析,取出其中的载荷,再交给上层协议
分用的过程,就是封装的逆向过程
经典的交换机来说,就只需要封装分用到 数据链路层即可
经典的路由器来说,就只需要封装分用到 网络层即可
假设只是交换机
交换机会把上述光电信号转换成以太网数据帧二进制数据,交给数据链路
交换机的数据链路层就会对上述数据进行解析
这个解析过程,一方面要取出载荷部分,另一方面,就要解析到帧头中的关键信息
根据帧头的信息决定下一步数据往哪里进行发送,根据这个情况再进一步构造出新的以太网数据帧
把这个新的数据继续通过物理层发送出去
数据链路层对上述数据进行解析,拿到载荷--交给网络层 IP协议
IP 协议又会进一步的对这个数据进行解析,取出载荷
取出的数据,IP 协议重新进行封装
继续交给数据链路层,继续加上帧头帧尾
这个数据再交给物理层,转换成电信号,继续传输
网络编程
有连接
此处的链接是虚拟的,特点就是双方都能认同
无连接则是不管认不认同都能通信
计算机中的“网络连接”,就是通信双方,各自保存对方的信息
连接本质上记录对方的信息
可靠传输 / 不可靠传输
网络上存在的 “异常情况”是非常多的
无论使用什么样的软硬件的技术手段,无法百分百保证网络数据能够从 A 传输到 B
虽然无法确保数据到达对方,至少可以知道,当前这个数据对方是不是收到了
此处谈到的可靠传输,主要指的是发的数据到没到,发送方能够清楚的感知到
面向字节流
字节流和文件中字节流完全一致,网络中传输数据的基本单位就是字节 TCP
面向数据报
每次传输的基本单位是一个数据包,(由一系列字节构成的)特定结构 UDP
全双工
半双工
一个信道,可以双向通信,全双工,只能单向通信,半双工
UDP 和 TCP 两个重要的传输层协议
操作系统提供进行网络编程的 api 也叫做 “socket api”
操作系统有一类文件叫做 socket 文件
它抽象表示了 “网卡” 这样的硬件设备
进行网络通信最核心的硬件设备网卡
通过网卡发送数据,就是写 socket 文件
通过网卡接收数据,就是读 socket 文件
UDP socket api的使用
核心的
1.DatagramSocket
负责对 socket文件读写,也就是借助网卡发送接收数据
2.DatagramPacket
UDP 面向数据报,每次发送接收数据的基本单位,就是一个 UDP 数据报,表示了一个 UDP 数据报
回显服务器
服务器接收客户端的请求,返回响应
服务器
1.创建 DatagramSocket 对象
接下来要操作网卡,操作网卡都是通过 socket 对象来完成的
程序一启动就需要关联上 / 绑定上一个操作系统中的端口号
端口号也是一个整数,用来区分一个主机进行网络通信的程序
一个主机上的一个端口号只能一个进程绑定
一个端口已经被进程1 绑定了,进程2 也想绑定,就会失败,除非 进程1 释放这个端口
反过来,一个进程可以绑定多个端口
端口号 和 socket 对象 是 一 一 对应的
如果一个进程中多个socket 对象自然就能绑定过个端口了
创建对象的时候手动指定一个端口号(在运行一个服务器的时候,通常会手动指定端口)
对于服务器来说,需要不停的收到请求,返回响应,收到请求,返回响应
一个服务器单位时间能处理的请求,能返回的响应越多,那你这个服务器就越厉害
此处 receive 就从网卡能读到一个 UDP 数据报,就被放到了 requestPacket 对象中,其中 UDP 数据报的载荷部分就被放到 requestPacket 内置的 字节数组 中了
另外报头部分,也会被 requestPacket 的其他属性保存
除了 UDP 报头之外,还有其他信息,比如收到的 数据源 IP
通过 requestPacket 还能知道数据从哪里来的(源 IP 源端口)
如果执行到 receive 的时候,此时还没有客户端发来请求就会先阻塞等待
基于字节数组构造出 String,字节数组里面保存的内容也不一定就是二进制数据,也可能是文本数据,把文本数据交给String 来保存,恰到好处,就算是二进制数据,String 也是可以保存的
offset 从字节数组的 0 号位置,开始构造 String
getLength 获取到字节数组中有效数据的长度用这么长来构造 String
要根据请求构造响应,但此处是回显服务器,所以就只是单纯的 return
send 也是需要一个 DatagramPacket 作为参数
构造相应的 packet 对象里面就不是空白的字节数组
直接把 String 里包含的字节数组给拿了过来
在长度这方面得以字节为单位,而不是直接respones.length(),它的单位是字符
我们把 源ip和端口,作为目的端口和ip,此时就可以做到把消息返回给客户端这样的效果
上述代码中,我们可以看到 UDP 是无连接通信,UDP socket 自身不保存对端的 IP 和 端口,而是在每个数据包中有一个,另外的代码中也没有“建立连接”“接收连接”操作
一个socket 即可以发送也可以接收 是全双工
当前的端口号是可以指定任何想要的端口号,但是这个端口号没有被其他进程占用
范围是 大于 1024 小于 65535
代码中手动指定端口号,才能保证短裤是始终固定的
如果不手动指定,依赖系统随机分配,导致服务器在重启之后,端口号可能就变了,客户端就找不到服务器在哪里了
客户端来说,它的端口号就可以是随机的,主要是无法保证手动指定的端口号是空闲的
服务器的端口在我们看来是可以被提前知道哪些是被占用的,而对于客户端,我们是无法知道哪个端口被占用
package Udp;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;/*** Created with IntelliJ IDEA.* Description:* User: 45* Date: 2025-07-16* Time: 17:51*/
public class UdpEchoServer {private DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while(true){//每次循环,就是处理一个请求 - 响应过程//1.读取请求并解析DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(requestPacket);//读到的字节数组,转成 String 方便后续的逻辑处理String request = new String(requestPacket.getData(),0,requestPacket.getLength());//2.根据请求就算响应(对于 回显服务器来说,这一步啥都不用做)String response = process(request);//3.把响应返回到客户端// 构造一个 DatagramPacket 作为响应对象DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);//打印日志System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);}}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}private String process(String request) {return request;}
}
客户端
请求 目的ip 和 目的端口了,请求的源 ip 就是客户端的本机 ip,源端口就是系统分配的空闲端口
第一种,构造的时候指定空白的字节数组即可(搭配receive 使用)
第二种,构造的时候指定有内容的字节数组,并且指定 IP 和 端口(发数据的时候使用)
第三种,构造的时候,指定有内容的字节数组,并且指定 IP 和 端口,IP 端口分开指定的(发数据的时候使用)
package Udp;import java.io.IOException;
import java.net.*;
import java.util.Scanner;/*** Created with IntelliJ IDEA.* Description:* User: 45* Date: 2025-07-17* Time: 15:34*/
public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp;private int serverPort;public UdpEchoClient(String serverIp,int serverPort) throws SocketException {this.serverIp = serverIp;this.serverPort = serverPort;socket = new DatagramSocket();}public void start() throws IOException {Scanner scanner = new Scanner(System.in);System.out.println("客户端启动");while (true) {//1.从控制台读取要发送的请求数据if (!scanner.hasNext()) {break;}String request = scanner.next();//2.构造请求并发送DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort);socket.send(requestPacket);//3.读取服务器的响应DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);//4.把响应显示到控制台上String response = new String(responsePacket.getData(), 0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);client.start();}}
1.服务器启动,启动之后,立即进入 while 循环,执行到 receive,进入阻塞,此时没有任何客户端发来请求
2.客户端启动,启动之后,立即进入 while 循环,执行到 hasNext 这里,进入阻塞,此时用户没有在控制台输入任何内容
3.用户在客户端的控制台输入字符串,按下回车,此时 hasNext 阻塞解除,next 会返回刚在输入的内容
基于用户输入的内容,构造出一个 DatagramPacket 对象,并进行send
send 执行完毕之后,继续执行到 receive 操作,等待服务器返回的响应数据(此时服务器还没返回响应就会阻塞)
4.服务器收到请求之后,就会从 receive 的阻塞中返回
返回之后,就会根据读到的 DatagramPacket 对象,构造String request,通过 process 方法构造一个 String response 再根据 response 构造一个 DatagramPacket 表示响应对象,再通过send 来进行发送给 客户端
执行这个过程,客户端也始终在阻塞等待
5.客户端从 receive 中返回执行,就能够得到服务器返回的响应,并且打印到控制台上,与此同时,服务器进入下一次循环,也要进入到第二次的 receive 阻塞,等待下一次请求。