文章目录
网络原理与网络编程初
网络的发展
单机阶段 => 局域网阶段 => 广域网阶段 => 移动互联网阶段
局域网 : 把一些设备通过 交换机 / 路由器 连接起来
广域网 : 把更多的局域网也互相连接 , 当网络规模足够大时
交换机 和路由器 : 组网过程中的重要设备
ip 地址 : 描述一个主机在互联网上的具体位置
端口号 : 区分一个主机上的应用程序
协议
协议就是 “约定” , 发送方约定了他发数据是什么样的格式 , 接收方也得理解这个格式 , 两边对上号才能够进行正确的通信
网络传输本质上都是通过 网线 / 光纤 / 无线 电信号或者光信号来进行传输
网线 : 传输了一系列的高电平和低电平
光纤 : 传输的是光信号
在网络通信中 , 需要约定的协议 , 其实是非常复杂的 , 面对复杂环境 , 就需要复杂协议 , 一个协议太复杂了 , 就可以拆分成多个协议 . 协议是拆分出很多 , 存在有些小的协议 , 作用或者定位是类似的 . 就可以针对这些小协议 , 进行 “分类” , 同时在针对这些不同的类别 , 进行分层.
分层 : 相当于是约定了 层级 和 层级 之间的调用关系 , 要求上层协议调用下层协议 , 下层协议给上层提供支持 , 不能跨层调用
协议分层的好处 :
- 分层之后就可以做到 , 层次之间 , 耦合程度比较低 , 上层协议不必了解下层的细节 , 下层也不必了解上层的细节
- 方便对某一层的协议进行替换
TCP/IP 五层网络模型
应用层 : 管住传输来的数据 , 是干什么用的
传输层 : 不考虑中间路径 , 只关注起点和终点
比如从家到学校 , 只关心到家和到学校 , 不关心你是做高铁还是飞机
网络层 : 主要负责两个遥远的节点之间 , 路径规划
家在西安 , 学校在北京 , 西安到北京有很多路可以走 , 在众多路径中找一个合适的路径
数据链路层 : 主要关注的是两个相邻节点之间的传输
西安 -> 河南 高铁
河南 -> 北京 飞机
物理层 : 网络通信的基础设施 , 网线 , 光纤, 网络接口
应用层 , 对应程序员写的应用程序
下面的四层 , 则是操作系统内部已经封装好的 , 因此写代码进行网络编程 , 主要工作还是围绕应用层展开的
封装和分用
在协议分层的背景下 , 数据如何通过网络传输 ?
封装和分用
发送方发送数据 , 要把数据从上到下 , 依次交给对应的层次的协议 , 进行封装
接收方收到数据 , 要把数据从下到上 , 依次交给对应的层次的协议 , 进行解析
以 QQ 发送消息为例 , 大概了解下 封装和分用的过程
发送方
A向 B 发送信息 在编辑框输入一条消息 “我爱Java”
-
应用层 (QQ应用程序) 拿到上述用户数据 , 进行封装 , 封装成应用层数据包
数据包 本质就是字符串拼接
-
传输层拿到上述数据
应用层要调用传输层提供的 api ,来处理这个数据 , 传输层有很多协议 , 最典型的是 TCP 和 UDP (此处以UDP为例) UDP针对于上述数据包再进行封装
一个典型的数据报都是通过 报头 + 载荷 的方式构成的
此处 , UDP协议再给应用层数据报加个UDP报头 , 就是为了再贴一层标签 , 从而在标签上填写必要的属性 . 传输层协议 最关键的属性就是源端口和目的端口了
-
传输层到网络层
UDP 数据报 , 已经有了 , 接下来就是要把这个数据报交给网络层协议 , 网络层最常见的协议 , 就是IP协议
上述 UDP 数据报 , 到达网络层 , 还需要进一步的封装, 添加上IP协议报头
源 IP 和目的IP 就描述了这次传输中 ,最初的起点和最终的终点
-
网络层交给数据链路层
最典型的协议 , 叫做以太网 (数据链路层 + 物理层)
mac 地址 , 也叫做物理地址
也是描述一个主机 , 在网络上的位置
它的功能和 IP 很相似 , 但是 当下就把这两个地址分别作用于不同的用途
IP 用来 进行网络层的路径规划
mac 用来进行描述数据链路层 , 两个即将进行传输的相邻节点
-
数据链路层就要把上述以太网数据帧交给物理层了
物理层要把上述 0101 的二进制数据转换为光信号/电信号/电磁波信号 , 进行传输
### 接收方
上述过程操作系统已经帮我们封装好了
接收过程和上述过程,刚好相反
发送 , 从上到下 , 依次封装 , 新增报头
接收 , 从下到上 , 一次分用 , 去掉报头
忽略中间的转发过程 , 只考虑B的电脑接收到这个消息的情况
-
物理层 ,网卡 ,收到高低电平二进制数据
就会对这里的信号进行解析 , 还原成 0101 这样的二进制序列
-
从物理层交给数据链路层
此时就把上述 010 这系列数据当做一个以太网数据帧 (此处是从以太网线 , 收到的数据, 就是要交给以太网协议来 处理)
)]把 帧头去掉 , 帧尾去掉 ,取出中间的载荷 , 再往上交给网络层
以太网数据帧帧头中有一个消息类型 , 根据这个类型就知道了网络层ip 协议了
-
网络层
此时就由网络层的ip协议进行解析数据报 ,也是去掉 ip报头
最重要的还是取出载荷, 交给更上层的传输协议
IP数据报 , 报头中也有一个字段, 标识当前传输层用的是哪个协议
-
传输层
此处是由UDP来解析处理 , 还是去掉报头, 取出载荷 , 把数据交给应用层
借助端口号来区分具体的应用程序 , 每个要接受网络数据的程序都需要关联上一个端口号
-
应用层
由QQ 这个程序 , 进行解析应用层数据报 , 取出系列字段 , 放到程序的界面中
总结
发送方 , 层层分装 , 包装快递
接收方 , 层层分用 , 拆快速
真实的网络环境中 , 数据传输中间可能要经历很多节点进行转发
我的电脑(从应用层封装到物理层) ==>交换机 (交换机会进行分用 , 从物理层分用到数据链路层 ,交换机针对这个数据重新封装 , 从数据链路层封装发哦物理层 , 把数据继续转发) ==> 路由器(路由器收到的数据 , 会从物理层用到网络层 , 根据当前得到的目的 IP 进行下一阶段的寻路操作.(IP协议是在一遍传输的过程中 , 一遍规划路径) , 把数据包重新封装 , 从网络层封装到物理层(此时经过数据链路层的时候 , 也会调整mac地址)) ==> 目标电脑 (一直达到这个目标主机 , 才会完成从物理层分用到应用层这个过程).
每次消息的传输 , 都会涉及封装分用
套接字(socket)
程序员写网络程序 , 主要编写的应用层代码 . 真正要发送这个数据 , 需要上层协议调用下层协议. 应用层要调用传输层
传输层给应用层提供一组 api 统称为 socket api
系统给我们提供的 socket api 主要是两组 :
- 基于 UDP 的 api
- 基于 TCP 的 api
TCP 和 UDP 这俩协议差别很大 , 提供的api 也差异很大
这两个协议各自的特点:
UDP :
- 无连接 使用 UDP 通信的双方 , 不需要刻意保存对端的相关信息
- 不可靠传输 消息发送 , 不关注结果
- 面向数据报 以一个UDP 数据报为基本单位
- 全双工 一条路径 , 双向通信
TCP:
- 有连接 使用TCP通信的双方 , 则需要刻意保存对方的相关信息
- 可靠传输 发了并不能100%到达对方 , 尽可能的传输过去
- 面向字节流 以字节为传输的基本单位 , 读写方式非常灵活
- 全双工 一条路径 , 双向通信
使用这两个 socket 的时候 , 就不必考虑单向通信的问题
全双工 : 双向通信 打电话
半双工 : 单向通信 对讲机
UDP的 api
DatagramSocket API
DatagramSocket 是UDP Socket . 用于发送和接收 UDP数据报
Datagram 就是"数据报" , Socket 书名这个对象是一个 socket 对象(相当于 对应到系统中一个特殊文件(socket文件) socket 文件并非对应到硬盘上的某个数据存储区域 , 而是对应到 ,网卡这个硬件设备)
所以 , 要想进行网络通信 , 就需要有 socket 文件这样的对象 , 借助这个 socket 文件对象 , 才能够间接的操作网卡
往这个socket 对象中写数据 , 相当于通过网卡发送消息
从这个socket 对象中读数据 , 相当于通过网卡接收消息
DatagramSocket 构造方法:
方法名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字 Socket .绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket .绑定到本机指定的端口(一般用于服务器) |
此处的 Socket 对象可能被客户端/服务器都使用 . 服务器这边的socket 往往要关联一个具体的端口号(必须不变) , 客户端这边则不需要 手动指定 , 系统自动分配即可 (不要求)
DatagramSocket 方法:
方法名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收到数据报(如果没有接收到数据报 , 该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待 , 直接发送) |
void close() | 关闭此数据报套接字 |
socket 也是文件 , 文件用完了就需要关闭 , 否则就会出现文件资源泄露问题
DatagramPacket API
DatagramPacket 是 UDP Socket 发送和接收的数据报
DatagramPacket 构造方法
方法名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf,int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数 length) |
DatagramPacket(byte[] buf , int offset,int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节 数组(第一个参数buf)中,从0到指定长度(第二个参数 length)。address指定目的主机的IP和端口号 |
DatagramPacket(byte[] buf,int length) :这个版本不需要设置地址进去 , 通常用来接收信息
DatagramPacket(byte[] buf , int offset,int length,SocketAddress address) : 这个版本 需要显式的设置地址进去 , 通常要用来发送消息
DatagramPacket 方法:
方法名 | 方法说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中, 获取发送端主机的IP地址 ; 或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中 , 获取发送端主机的端口号 ; 或从发送的数据报中 , 获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
基于 UDP socket 简单的客户端 服务器程序
回显服务器(echo sever)
客户端发了个请求 , 服务器返回一个一模一样的响应
一个服务器 , 主要要做三个核心工作:
- 读取请求分析并解析
- 根据请求计算响应(省略)
- 把响应返回到客户端
服务器:
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
// 需要先定义一个 socket 对象.
// 通过网络通信, 必须要使用 socket 对象.
private DatagramSocket socket = null;
// 绑定一个端口, 不一定能成功!!
// 如果某个端口已经被别的进程占用了, 此时这里的绑定操作就会出错.
// 同一个主机上, 一个端口, 同一时刻, 只能被一个进程绑定.
public UdpEchoServer(int port) throws SocketException {
// 构造 socket 的同时, 指定要关联/绑定的端口.
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. 把响应结果写回到客户端
// 根据 response 字符串, 构造一个 DatagramPacket .
// 和请求 packet 不同, 此处构造响应的时候, 需要指定这个包要发给谁.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
// requestPacket 是从客户端这里收来的. getSocketAddress 就会得到客户端的 ip 和 端口
requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
// 这个方法希望是根据请求计算响应.
// 由于咱们写的是个 回显 程序. 请求是啥, 响应就是啥!!
// 如果后续写个别的服务器, 不再回显了, 而是有具体的业务了, 就可以修改 process 方法,
// 根据需要来重新构造响应.
// 之所以单独列成一个方法, 就是想让同学们知道, 这是一个服务器中的关键环节!!!
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
如果客户端 发来的是数据是 “老板 来碗面” , 此时这个数据就会以二进制的形式躺在 requestPacket 中的字节数组中 , 把这个字节数组拿出来 , 重新构造一个 String , 这个String 的内容就是 “老板 来碗面”
客户端:
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
// 客户端启动, 需要知道服务器在哪里!!
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
// 对于客户端来说, 不需要显示关联端口.
// 不代表没有端口, 而是系统自动分配了个空闲的端口.
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
// 通过这个客户端可以多次和服务器进行交互.
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 先从控制台, 读取一个字符串过来
// 先打印一个提示符, 提示用户要输入内容
System.out.print("-> ");
String request = scanner.next();
// 2. 把字符串构造成 UDP packet, 并进行发送.
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 显示出来.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
// UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
UdpEchoClient udpEchoClient = new UdpEchoClient("42.192.83.143", 9090);
udpEchoClient.start();
}
}
这个构造 , 也是吧数据构造成 DatagramPacket ,一方面需要 String 中的 getBytes 数组 , 另一方面 , 需要指定服务器的ip 和 端口 . 此处不是通过 InetAddress 直接构造了 ,而是分开设置
服务器返回响应的时候, 是直接从 packet 里面取出的 InetAddress 对象
总结
-
服务器先启动 , 执行到 receive 进行阻塞
-
客户端运行之后 , 从控制台读取数据, 并进行send
-
客户端这边 , send 之后 , 继续往下走 , 走到 receive 读取响应 , 会阻塞等待
服务器这边 , 就从 receive 返回 , 读到请求数据(客户端发来的) , 往下走到 process 生成响应 ,再往下走到 send , 并且打印日志
-
客户端这边真正收到服务器send 回来的数据后 , 就会解除阻塞 , 执行下面的打印操作
服务器这边进入下一轮循环 , 再次阻塞在 receive , 等待客户端下一个请求
-
客户端继续进入下一轮循环 , 阻塞在 Scanner.next 这里 , 等待用户输入新的数据
虽然上述流程 , 初步看起来比较复杂 , 但是所有的服务器客户端程序基本上都是这样的