1. 网络基础
1.1 局域网
通过路由器,将多个电脑连接到一起,构成了局域网(交换机,是对路由器的端口进行扩展的)
1.2 广域网
将多个局域网连接到一起,构成了更加庞大的网络,覆盖范围也更大
局域网和广域网是相对而言的,没有明确的界限
1.3 IP 地址
描述一台设备在网络上的位置。使用一个四字节的数字来表示,点分十进制
1.4 端口号
区分一个主机上不同的应用程序(2个字节)。一个端口号只能被一个程序绑定,但一个程序可以绑定多个端口
范围:0 - 65535
- 0 一般不使用
- 1-1023 范围的端口号系统留作特殊用途,其他程序不能占用(22:ssh;23:telnet;80:http;443:https)
网络上本质是通过 光 / 电 信号来传输数据的
1.5 协议
通信双方按照什么样的规则来进行通信
五元组:源 IP,源端口,目的 IP,目的端口,协议类型
1.6 协议分层
网络通信场景比较复杂,很多问题都需要通过协议来解决,如果只有一个大的协议来解决所有问题,这个协议就会非常复杂,不利于学习、理解、维护。将大的协议拆分成多个小的协议,每个协议负责一小块,这样做的话,由于网络协议太复杂,拆分出来的小的协议太多,也不好管理和维护,此时就需要对协议进行分层。
按照协议的 定位/作用 分类,并约定不同层次之间的调用关系,“上层协议调用下层协议,下层协议给上层协议提供服务”,此时即使协议比较多,也可以顺利完成任务
优势
- 协议分层之后,上下层彼此之间就进行了封装(使用上层协议,不必过多关注下层,使用下层,也不必过多关注上层)
- 每层协议都可以根据需要灵活替换
1.7 网络模型
1.7.1 OSI 七层模型
从下到上分别为 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
分层名称 | 功能 |
---|---|
应用层 | 针对特定应用的协议 |
表示层 | 设备固有数据格式和网络标准数据格式的转换 |
会话层 | 通信管理。负责建立和断开通信连接(数据流动的逻辑通路)。管理传输层以下的分层 |
传输层 | 管理两个节点之间的数据传输。负责可靠传输(确保数据被可靠地传送到目标地址) |
网络层 | 地址管理与路由选择 |
数据链路层 | 互连设备之间传送和识别数据帧 |
物理层 | 以 “0”、“1”代表电压的高低、灯光的闪灭。界定连接器和网线的规格。 |
1.7.2 TCP/IP 五层模型
分层名称 | 功能 | 实现 |
---|---|---|
应用层 | 应用程序如何使用这个数据 | 应用程序 |
传输层 | 关注起点和终点 | 操作系统的内核 |
网络层 | 进行路径规划 | 操作系统的内核 |
数据链路层 | 两个相邻节点之间的数据传输 | 驱动程序 + 硬件 |
物理层 | 描述网络通信的硬件设备(网线、光纤的规格等)。将数据转为 “0”、“1” 信号,通过 光信号/电信号进行传输。对于接收到的数据,将光信号/电信号转换成二进制数据得到以太网数据报交给数据链路层 | 驱动程序 + 硬件 |
对于一台主机,操作系统内核实现了从传输层到物理层的内容,即 TCP/IP 五层模型的下四层
对于一台路由器,实现了从网络层到物理层,即 TCP/IP 五层模型的下三层
对于一台交换机,实现了从数据链路层到物理层,即 TCP/IP 五层模型的下两层
对于集线器,只实现了物理层
2. 网络编程
网络编程,需要使用操作系统提供的一组 API 来完成编程。可以认为是应用层和传输层之间交互的方式,成为 Socket API,可以完成不同主机、不同系统之间的网络通信。
传输层的协议主要是 TCP 和 UDP,这两个协议的特性差别很大,操作系统针对这两个协议分别提供了两组 API
2.1 TCP 和 UDP 的区别
-
TCP 是有连接的,UDP 是无连接的
有连接:建立连接的双方,各自保存了对方的信息
-
TCP 要通信的话,就得先和对方建立连接,然后才能进行通信
-
UDP 要通信,无需建立连接,直接发送数据即可。不需要征得对方的同意,也不会保存对方的信息。应用层调用 UDP 的 API 的时候,将对方信息传过去就行。
-
-
TCP 是可靠传输,UDP 是不可靠传输
可靠传输:发送方发送数据,是否到达接收方,发送方能够感知到,因此发送方可以在发送失败时采取相应的措施(如重传)
TCP 内置了可靠传输机制,UDP 没有内置可靠传输机制
虽然 TCP 是可靠传输,但是并非就更好,因为可靠传输的代价就是机制会更复杂、传输效率会降低
-
TCP 是面向字节流的,UDP 是面向数据报的
数据报(Datagram)
数据包(Packet)
数据帧(Frame)
数据段(Segment)
-
TCP 和 UDP 都是全双工通信
全双工:可以同时进行双向通信
半双工:可以进行双向通信,但同一时间只有一个方向可以进行通信
一根网线中有 8 根线,4个一组,每组都可以独自完成通信,2 组可以实现,当某一根线坏掉时,剩下的也能继续工作。
2.2 UDP的 Socket API
2.2.1 DatagramSocket
Socket 本质上是一个特殊的文件,将网卡这个设备抽象成了文件,往 Socket 中写数据就相当于是发送数据,往 Socket 中读数据,就相当于接收数据。在 Java 中使用 DatagramSocket 来表示系统内部的 Socket 文件
2.2.1.1 构造方法
方法签名 | 解释 |
---|---|
DatagramSocket() | 创建一个 UDP 数据报套接字,绑定到本机任意一个随机端口 |
DatagramSocket(int port) | 创建一个 UDP 数据报套接字,绑定到本机指定的端口 |
2.2.1.2 主要方法
方法签名 | 解释 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)。参数 p 是一个输出型参数,该方法会将接收到的数据填充到 p 中 |
void send(Datagram Packet p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
void close() | 关闭此数据包套接字 |
客户端和服务器通信,两者都需要创建 Socket 对象,但服务器的 Socket 需要显式指定端口号,客户端的 Socket 一般不显式指定,由客户端操作系统自动分配
服务器端手动指定端口号,是因为开发者清楚该服务器上有什么程序,可以使用哪些端口号,是可控的
而客户端程序在用户电脑上,如果手动指定端口号,有可能这个端口号已经被用户端电脑上的其他程序占用了,就会重现端口冲突,而系统自动分配的话,分配到的肯定是空闲端口
2.2.2 DatagramPacket
用来作为接收和发送的数据报
2.2.2.1 构造方法
签名 | 解释 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket来接收数据报,接收到的数据存储于字节数组 buf 中,length 表示接收指定长度的数据(最多接收的数据的字节个数) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket来发送数据报,发送的数据为字节数组 buf 中,从 0 到指定长度length。address 用于指定目的主机 IP 和 端口号 |
2.2.2.2 主要方法
签名 | 解释 |
---|---|
SocketAddress getSocketAddress() | 从数据报中获取发送端主机 IP 地址或接收端主机 IP 地址 |
int getPort() | 获取发送端主机或接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
2.2.3 回显服务器
服务器端
package udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
this.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 request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应 (实际的业务逻辑)
String response = process(request);
// 3. 将响应写回客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印日志
System.out.printf("[%s:%d] req=%s, resp=%s", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
为什么这里不需要调用 close() 关闭文件
- 这个 socket 是整个进程运行过程中一直都需要用到的
- 当 socket 不需要使用的时候,意味着进程就结束了,进程的结束,PCB 也被销毁了,文件描述符表也被销毁了,因此不存在文件资源泄露
客