专栏简介: JavaEE从入门到进阶
题目来源: leetcode,牛客,剑指offer.
创作目标: 记录学习JavaEE学习历程
希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
1. 网络编程基础
1.1 为什么需要网络编程?
1.2 什么是网络编程?
1.3 网络编程中的基本概念
2. Socket套接字
2.1 Java 数据报套接字模型
2.2 Java 流套接字通信模型
2.3 Socket 编程注意事项
3.UDP 数据报套接字编程
3.1 DatagramSocket API
3.2 DatagramPacket API
3.3 InetSocketAddress API
示例一: 回显服务器(echo server)
示例二: 查词典服务器
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();
}
}
此时如果再启用回显服务 , 就会造成端口冲突(一个端口只能绑定一个进程).