专栏简介: JavaEE从入门到进阶
题目来源: leetcode,牛客,剑指offer.
创作目标: 记录学习JavaEE学习历程
希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
1. 网络编程基础
1.1 为什么需要网络编程?
用户在浏览器中 , 打开在线视频网站 , 如腾讯视频 , 实际是通过网络获取网络上的一个视频资源.
与打开本地视频文件类似 , 只不过视频资源来源是网络. 相比本地视频来说 , 网络提供了更为丰富的网络资源.
所谓的网络资源其实就是网络中可以获取的各种数据资源.
而所有的网络资源都是通过网络编程来进行数据传输的.
1.2 什么是网络编程?
网络编程指的是 , 网络上的主机通过不同的进程(端口) , 以编程的方式实现网络通信(网络数据传输)
其实 , 网络传输只要满足不同进程就行 , 因此即使是同一主机 , 只要是不同进程就可以进行网络传输.
对于开发人员来说 , 在条件有限的情况下 , 都是在同一主机上进行网络编程.
但是 , 我们一定要明确 , 我们的目的是提供网络上不同主机 , 基于网络来传输数据资源.
- 进程A , 编程来获取网络资源
- 进程B , 编程来提供网络资源
1.3 网络编程中的基本概念
发送端和接收端
再一次网络数据传输时:
发送端: 数据的发送方进程 , 发送端主机即网络通信中的源主机.
接收端: 数据的接收方进程 , 接收端主机即网络通信中的目的主机.
收发端: 发送端和接收端两端 , 简称收发端.
Tips: 发送端和接收端是相对的 , 只是一次网络数据传输产生数据流向后的概念.
请求和相应
一般来说 , 获取一个网络资源 , 涉及到两次网络数据传输:
- 第一次: 请求数据的发送
- 第二次: 响应数据的发送
例如在快餐店点一份炸鸡:
先要发起请求: 点一份炸鸡 , 之后快餐店提供对应的响应: 提供一份炸鸡.
客户端和服务器
服务端: 在常见的网络数据传输场景下 , 把提供服务的一方进程 , 称为服务器 , 可以提供对外服务.
客户端: 获取服务的一方进程 , 称为客户端.
对于服务来说 , 一般是提供:
- 客户端获取服务资源.
- 客户端保存资源在服务器.
常见的客户端服务端模型
1. 客户端先发送请求到服务器.
2. 服务端根据请求数据 , 执行响应的业务处理.
3. 服务端返回响应 ,发送业务的处理结果.
4. 客户端根据响应数据 , 展示处理结果(展示获取的资源 , 或提示保存资源的处理结果).
2. Socket套接字
概念:
Socket套接字 , 是由系统提供用于网络通信的技术 , 是基于 TCP/IP 协议的网络通信的基本操作单元. 基于 Socket 套接字的网络程序开发就是网络编程.
分类:
Socket 套接字主要针对传输层协议划分为如下三类:
流套接字:
使用传输层 TCP 协议 , 即 Transmission Control Protocol (传输控制协议) , 传输层协议.
TCP 的特点:
- 有连接
- 可靠传输
- 面向字节流
- 有接收缓冲区 , 也有发送缓冲区.(全双工)
- 大小不限
对于字节流来说 , 可以简单的理解为 , 传输数据是基于 IO 流 , 流式数据的特征就是 IO 没有关闭的情况下 , 是无边界的数据 , 可以多次发送 , 也可以分开多次接收.
数据报套接字:
使用传输层 UDP 协议 , 即User Dategram Protocol (用户数据报协议) , 传输层协议.
UDP 的特点:
- 无连接
- 不可靠传输
- 面向数据报
- 有接收缓冲区 , 也有发送缓冲区(全双工)
- 大小不限
对于数据报来说 , 传输数据是一块一块的 , 发送一块100个字节的数据 , 必须一次发送 , 接收也必须一次接收 100 个字节 , 而不能分 100 次 , 每次接收一个字节.
原始套接字
原始套接字用于自定义传输层协议 , 用于读写内核没有处理的 IP 协议数据.
2.1 Java 数据报套接字模型
对应 UDP 数据报来说 , 具有无连接 , 面向数据报的特征 , 即每次都是没有建立连接 , 并且一次发送全部数据报 , 一次接收全部数据报.
java 总使用 UDP 协议通信 , 主要基于 DatagramSocket 类来创建数据报套接字 , 并使用 DatagramPacket 来发送或接收 UDP 数据报. 对应发送及接收数据报的流程如下:
2.2 Socket 编程注意事项
- 1. 客户端和服务端: 开发时 , 经常是基于一个主机开启两个进程作为客户端和服务端 , 但 真实的场景一般都是不同主机.
- 2. 注意目的IP和目的端口号 , 标识了一次数据传输时要发送数据的终点主机和进程.
- 3. Socket 编程我们是使用流套接字和数据报套接字 , 基于传输层的TCP或UDP协议 , 但 应用层协议 , 也需要考虑 , 后续会介绍如何设计应用层协议.
- 4. 如果一个进程 A 绑定一个端口 , 再启动一个进程 B 绑定该端口 , 就会报错 , 这种情况 也叫做端口被占用.
3.UDP 数据报套接字编程
3.1 DatagramSocket API
DatagramSocket 是 UDP 套接字 , 用于发送和接收数据报.
DatagramSocket 构造方法:
方法签名 | 方法说明 |
DatagramSocket() | 创建一个 UDP 数据报的套接字 Socket,绑定本机任意一个随机端口(常用于客户端) |
DatagramSocket(int prot) | 创建一个 UDP 数据报的套接字 Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket 方法:
方法签名 | 方法说明 |
void receice(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报 , 该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报(直接发送 , 不会阻塞等待) |
void close() | 关闭次数据报套接字 |
3.2 DatagramPacket API
DatagramPacket 是UDP Socket 发送和接收的数据报.
DatagramPacket 构造方法:
方法签名 | 方法说明 |
DatagramPacket(byte[] buf,int length) | 构造一个DatagramPack 用来接收数据报 , 接收的数据报存在字节数组(第一个参数buf)中 , 指定长度(第二个参数 length) |
DatagramPacket(byte[] buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket 用来发送数据 , 发送的数据为字节数组 , 从0到指定长度 , address指定的是主机的IP和端口号 |
DatagramPacket 方法:
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中 , 获取发送端主机IP地址;或从发送的数据报中 , 获取接收端主机IP地址. |
int getPort() | 从接收的数据报中 , 获取发送端主机端口号;或从发送的数据报中 , 获取接收端主机端口号. |
byte[] getData() | 获取数据报中的数据 |
构造UDP发送的数据报时 , 需要传入 SocketAddress , 该对象可使用 InetSocketAddress 来创建.
3.3 InetSocketAddress API
InetSocketAddress (SocketAddress 的子类) 构造方法:
方法签名 | 方法说明 |
InetSocketAddress(InetAddress addr , int port) | 创建一个Socket地址 , 包含IP地址和端口号 |
示例一: 回显服务器(echo server)
一般服务器执行操作: 收到请求 , 根据请求计算响应 , 返回响应
echo server 省略了其中的 "根据请求计算响应" , 请求是啥就返回啥.(该服务器没有实际的业务 , 只是展示了 socket api 基本用法)
public class UdpEchoServer {
//网络编程本质上是操作网卡
//但网卡不方便直接操作 , 在操作系统内核中 , 使用一种特殊的叫做"socket"这样的文件来抽象表示网卡
//因此进行网络通信势必有一个 socket 对象
private DatagramSocket socket = null;
//对于服务器来说,创建socket对象的同时,要给它绑定一个端口号
//服务器是网络传输中,被动的一方,如果是操作系统的随机分配的端口,此时客户端就不知道这个端口是啥了,也就无法通信了
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器!");
//服务器为多个客户端服务
while(true){
//只要有客户端,就可以提供服务
//1.读取客户端发来的请求
//receive 方法的参数是一个输出形参数,需要先构造好空白的DatagramPacket对象,交给receive进行填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//此时这个 DatagramPacket 是一个特殊的对象,不方便直接进行处理,可以把这里包含的时刻拿出来构造成一个字符串.
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应,此处是回显服务器,请求与响应相同
String response = process(request);
//3.把响应数据写回到客户端,send的参数也是 DatagramPacket,需要把这个对象构造好
//此处的响应对象不能是空的字节数组构造的而是要响应数组构造
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印一下当前请求响应的处理中间结果
System.out.printf("[%s:%d]req: %s; resp: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//这个方法表示 根据请求计算响应
public String process(String request){
return request;
}
public static void main(String[] args) throws SocketException {
//端口号码在 1024-65535 之间随机选择.
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort = 0;
//一次通信需要有两个 ip 和两个端口
//客户端的是 ip 127.0.0.1(环回 ip) 已知
//客户端的端口号是系统随机分配的
//服务器 ip 和端口号也要告诉客户端 , 才能顺利把消息发送给服务器.
public UdpEchoClient(String serverIp , int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scan = new Scanner(System.in);
while(true){
//1. 从控制台读取要发送的数据
System.out.println(">");
String request = scan.next();
if(request.equals("exit")){
System.out.println("good bye");
break;
}
//2. 构成 Udp 请求并发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes() , request.getBytes().length,
InetAddress.getByName(serverIp) , serverPort);
socket.send(requestPacket);
//3. 读取服务器的 Udp 响应并解析.
DatagramPacket respondPacket = new DatagramPacket(new byte[4096] , 4096);
socket.receive(respondPacket);
String respond = new String(respondPacket.getData() , 0 , respondPacket.getLength());
//4. 把解析好的结果显示出来
System.out.println(respond);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
示例二: 查词典服务器
//对于UdpDictSever 主要代码与回显服务器一致
//主要是 "根据请求计算响应" 这个步骤不一样
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
// 给字典设置内容
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
dict.put("mouse", "老鼠");
// 这里可以无限的设置
}
@Override
public String process(String request) {
//查词典和过程
return dict.getOrDefault(request, "当前单词没有查到结果");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
此时如果再启用回显服务 , 就会造成端口冲突(一个端口只能绑定一个进程).