介绍
可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)。
网络通信架构
CS架构: Client客户端 / Server服务端
- 客户端需要程序员开发,需要用户安装
- 服务端需要程序员开发
BS架构: Browser浏览器 / Server服务端
- 用户下载安装浏览器就行
- 服务器需要程序员开发
无论是CS架构,还是BS架构的软件都必须依赖网络编程!
java.net.*包下提供了网络编程的解决方案
通信三要素
IP地址
IP地址/端口/协议在通信中的作用
IP(Internet Protocol): 全称互联网协议地址”,是分配给上网设备的唯一标识。
- 目前,被广泛采用的IP地址形式有两种: IPv4、IPV6。
- IPv4是Internet Protocolversion 4的缩写,它使用32位地址,通常以点分十进制表示。(支持40亿台设备)
- IPv6是Internet Protocol version 6的缩写,它使用128位地址,号称可以为地球上的每一粒沙子编号
- IPv6分成8段,每段每四位编码成一个十六进制位表示,每段之间用冒号(:)分开,称为冒分十六进制
- 目前IPv4和IPv6是混用状态, 大部分设置都要兼容这两种地址形式
IP域名: 用于在互联网上识别和定位网站的人类可读的名称。例如 www.baidu.com
- DNS域名解析(Domain Name System)
- 是互联网中用于将域名转换为对应IP地址的分布式命名系统。
- 它充当了互联网的“电话簿,将易记的域名映射到数字化的IP地址, 使用户可以通过域名来访问网络资源。
- 使用域名访问网络资源时, 请求要经过本机的NDS服务器, 把域名转成IP地址, 使用IP地址进行通信
- 如果本机的DNS服务器找不到该域名, 就会请求宽带运营商的DNS服务器, 获取该域名, 获取成功后缓存到本地
- 如果获取不到域名信息, 解析失败, 就会提示网络错误
IP分类
公网IP: 可以连接互联网的IP地址
内网IP: 只能在组织或机构内部使用的地址
- 192.168. 开头的就是常见的局域网地址,
- 范围在 192.168.0.0 -- 192.168.255.255 , 专门为组织机构内部使用
- 内网IP的作用就是节省IP地址, 并且内网IP的通信效率更高
- 本机IP: 172.0.0.1 / localhost 代表本机IP, 只会寻找当前程序所在的主机
- IP常用命令
- 查看本机IP地址: ipconfig
- 查看物理IP: ipconfig /all
- 检查网络是否连通: ping IP地址
Java中使用 InetAddress 代表IP地址, 提供了如下方法
public class Test {
public static void main(String[] args) {
// 目标: 认识InetAddress获取本机IP对象和对方IP对象
try {
// 1.获取本机IP对象
InetAddress local = InetAddress.getLocalHost();
System.out.println(local); // LAPTOP-E5G8L6BB/192.168.1.1
System.out.println(local.getHostAddress()); // 192.168.1.1
System.out.println(local.getHostName()); // LAPTOP-E5G8L6B
// 2.获取对方IP对象
InetAddress remote = InetAddress.getByName("www.baidu.com");
System.out.println(remote); // www.baidu.com/39.156.66.18
System.out.println(remote.getHostAddress()); // 39.156.66.18
System.out.println(remote.getHostName()); // www.baidu.com
// 3.测试本机与对方是否互通
boolean isReachable = remote.isReachable(3000); // 3秒内能否联通
System.out.println(isReachable); // true
} catch (Exception e) {
e.printStackTrace();
}
}
}
端口
端口用来标记标记设备上运行的应用程序,被规定为一个16位的二进制,范围是 ~65535.
端口分类
- 周知端口: 0-1023, 被预先定义的知名应用占用 (HTTP占用80, FTP占用21)
- 注册端口: 1024-49151, 分配给用户进程或应用程序
- 动态端口: 4952-65535, 一般不固定分配给某进程, 而是动态分配
- 我们开发的程序一般选择注册端口, 一个设备中不能出现端口一样的程序, 否则报错
协议
网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议
- 为了让全球所有上网设备都能互联互通,需要指定一套统一的标准, 就是通信协议
开放式网络互联标准
OSI网络参考模型: 全球网络互联标准。
TCP/IP网络模型: 事实上的国际标准。
传输层的2个通信协议
UDP(User Datagram Protocol): 用户数据报协议
- 特点: 无连接、不可靠通信。
- 不事先建立连接,数据按照包发,一包数据包含: 自己的IP/端口、目的地IP/端口和数据(限制在64KB内)等。
- 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的
- 通信效率高, 适合视频直播/语音通话等场景
TCP(Transmission Control Protocol): 传输控制协议。
- 特点: 面向连接、可靠通信。
- TCP的最终目的: 要保证在不可靠的信道上实现可靠的数据传输。
- TCP主要有三个步骤实现可靠传输: 三次握手建立连接,传输数据进行确认,四次挥手断开连接。
- 通信效率相对不高, 可靠性更高, 适合网页/文件下载/支付等场景
三次握手建立可靠连接: 确保通信的双方收发消息都是没问题的(全双工)
- 客户端向服务端发起连接请求, 服务端确定客户端发送无问题
- 服务端给客户端返回响应, 客户端确定服务端收发无问题
- 客户端向服务端发送确定信息, 建立连接, 服务端确定客户端接受无问题
- 数据传输会进行确定, 以保证数据传输的可靠性
四次挥手断开连接: 确保通信的双方收发消息都已经完成
- 客户端向服务端发送断开请求
- 服务端先返回等待的响应消息, 让客户端等待
- 服务端确认无误后再返回可以断开的消息
- 客户端发送确认断开的消息
UDP通信
快速入门
Java提供了一个java.net.DatagramSocket类来实现UDP通信。
- 特点: 无连接、不可靠通信。
- 不事先建立连接;
- 发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了
- 通信模型: socket对象(通信端) Packet数据包(韭菜盘子) 数据(韭菜)
UDP通信的实现
DatagramSocket: 用于创建客户端、服务端
DatagramPacket: 创建数据包
代码示例
实现一发一收通信
public class UDPClient {
public static void main(String[] args) throws Exception {
System.out.println("===客户端启动===");
// UDP通信一发一收: 客户端
// 1.创建发送端对象(抛韭菜的人)
DatagramSocket socket = new DatagramSocket(); // 随机端口(不接收数据, 无所谓)
// 2.准备要发送的数据(韭菜)
byte[] bytes = "我是客户端, 约你今晚喝啤酒!".getBytes();
/**
* 3.创建数据包对象封装要发送的数据(韭菜盘子)
* 参数一: 要发送的数据
* 参数二: 要发送的数据的长度
* 参数三: 目标主机的IP地址
* 参数四: 服务端程序的端口号
*/
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 8090);
// 4.发送数据, 释放资源
socket.send(packet);
socket.close();
}
}
public class UDPService {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动===");
// UDP通信一发一收: 服务端
// 1.创建接收端对象,注册端口(接韭菜的人)
DatagramSocket socket = new DatagramSocket(8090);
// 2.创建数据包对象封装要接收的数据(韭菜盘子)
byte[] buf = new byte[1024 * 64]; // UDP每包数据最大64KB
DatagramPacket packet = new DatagramPacket(buf, buf.length);
// 3.接收数据, 释放资源
socket.receive(packet);
// 4.查看数据包的数据
int length = packet.getLength(); // 获取接收到的数据长度
String data = new String(buf, 0, length);
System.out.println("服务端接收到数据:" + data);
// 5.查看发送端的信息
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
System.out.println("对方ip:" + ip + "端口:" + port);
// 6.释放资源
socket.close();
}
}
多收多发
使用死循环改造入门程序, 实现多发多收的效果
public class UDPClient {
public static void main(String[] args) throws Exception {
System.out.println("===客户端启动===");
// UDP通信多发多收: 客户端
// 1.创建发送端对象(抛韭菜的人)
DatagramSocket socket = new DatagramSocket(); // 随机端口(不接收数据, 无所谓)
Scanner sc = new Scanner(System.in);
while (true) {
// 2.准备要发送的数据(韭菜)
System.out.println("请说:");
String msg = sc.nextLine(); // 接收整行数据
if (msg.equals("exit")) {
System.out.println("===客户端退出===");
socket.close();
break;
}
// 3.创建数据包对象封装要发送的数据(韭菜盘子)
byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 8090);
// 4.发送数据
socket.send(packet);
}
}
}
public class UDPService {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动===");
// UDP通信多发多收: 服务端
// 1.创建接收端对象,注册端口(接韭菜的人)
DatagramSocket socket = new DatagramSocket(8090);
// 2.创建数据包对象封装要接收的数据(韭菜盘子)
byte[] buf = new byte[1024 * 64]; // UDP每包数据最大64KB
DatagramPacket packet = new DatagramPacket(buf, buf.length);
while (true) {
// 3.接收数据
socket.receive(packet); // 阻塞式方法,没有数据,一直等待
// 4.查看数据包的数据
int length = packet.getLength(); // 获取接收到的数据长度
String data = new String(buf, 0, length);
System.out.println("服务端接收到数据:" + data);
// 5.查看发送端的信息
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
System.out.println("对方ip:" + ip + "端口:" + port);
System.out.println("--------------------------------");
}
}
}
- IEDA实现程序多开
- 运行效果
- UDP的接收端为什么可以接收很多发送端的消息?
- 接收端只负责接收数据包,无所谓是哪个发送端的数据包。
TCP通信
快速入门
通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信
- 特点: 面向连接、可靠通信
- 底层能保证数据成功传给服务端。
Java提供了一个java.net.Socket类来实现TCP通信。
服务端是通过java.net包下的ServerSocket类来实现的
使用Socket和Server完成TCP通信: 单发单收
public class TcpClient {
public static void main(String[] args) throws Exception {
// 目标: 实现TCP通信, 一发一收, 客户端
System.out.println("===客户端启动===");
// 1.创建Socket管道对象, 请求与服务端建立连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从socket管道中得到字节输入流
OutputStream os = socket.getOutputStream();
// 3.把字节输入流包装为特殊流, 注意, 通信中收发数据要保证流的对应
DataOutputStream dos = new DataOutputStream(os);
// 4.发送数据
dos.writeInt(1); // 代表消息是文本类型
dos.writeUTF("你好, 服务端!");
// 5.释放资源
dos.close();
}
}
public class TcpService {
public static void main(String[] args) throws Exception {
// 目标: 实现TCP通信, 一发一收, 服务端
System.out.println("===服务端启动===");
// 1.创建ServerSocket管道对象, 绑定端口号
ServerSocket ss = new ServerSocket(9999);
// 2.监听客户端连接, 阻塞式方法, 客户端连接后, 返回Socket对象
Socket socket = ss.accept();
// 3.获取输入流, 读取客户端发送的数据
InputStream is = socket.getInputStream();
// 4.把输入流包装为特殊流, 因为客户端使用特殊流发送数据
DataInputStream dis = new DataInputStream(is);
// 4.读取数据
int id = dis.readInt();// 消息类型
String msg = dis.readUTF();// 消息内容
System.out.println("id=" + id + ",数据=: " + msg);
// 5.获取客户端的ip和端口
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress() + ",端口= " + socket.getPort());
// 6.释放资源
dis.close();
}
}
使用Socket和Server完成TCP通信: 多发多收
public class TcpClient {
public static void main(String[] args) throws Exception {
// 目标: 实现TCP通信, 多发多收, 客户端
System.out.println("===客户端启动===");
// 1.创建Socket管道对象, 请求与服务端建立连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从socket管道中得到字节输入流
OutputStream os = socket.getOutputStream();
// 3.把字节输入流包装为特殊流, 注意, 通信中收发数据要保证流的对应
DataOutputStream dos = new DataOutputStream(os);
// 4.发送数据
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();
if (msg.equals("exit")) {
System.out.println("===客户端退出===");
socket.close(); // 关闭socket
dos.close(); // 关闭输出流
break;
}
dos.writeUTF(msg);
dos.flush(); // 没有关闭输出流,所以要手动刷新缓冲区
}
}
}
public class TcpService {
public static void main(String[] args) throws Exception {
// 目标: 实现TCP通信, 多发多收, 服务端
System.out.println("===服务端启动===");
// 1.创建ServerSocket管道对象, 绑定端口号
ServerSocket ss = new ServerSocket(9999);
// 2.监听客户端连接, 阻塞式方法, 客户端连接后, 返回Socket对象
Socket socket = ss.accept();
// 3.获取输入流, 读取客户端发送的数据
InputStream is = socket.getInputStream();
// 4.把输入流包装为特殊流, 因为客户端使用特殊流发送数据
DataInputStream dis = new DataInputStream(is);
while (true) {
// 5.读取数据
String msg = dis.readUTF();// 消息内容
System.out.println("数据=: " + msg);
// 6.获取客户端的ip和端口
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress() + ",端口= " + socket.getPort());
System.out.println("==============");
}
}
}
使用Socket和Server完成TCP通信: 服务端可以接收多个客服端消息
- 主线程定义循环负责接收客户端Socket管道连接
- 每接收到一个Socket通信管道后分配一个独立的线程负责处理它
public class TcpClient {
public static void main(String[] args) throws Exception {
// 目标: 实现TCP通信, 多发多收, 客户端
System.out.println("===客户端启动===");
// 1.创建Socket管道对象, 请求与服务端建立连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从socket管道中得到字节输入流
OutputStream os = socket.getOutputStream();
// 3.把字节输入流包装为特殊流, 注意, 通信中收发数据要保证流的对应
DataOutputStream dos = new DataOutputStream(os);
// 4.发送数据
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();
if (msg.equals("exit")) {
System.out.println("===客户端退出===");
socket.close(); // 关闭socket
dos.close(); // 关闭输出流
break;
}
dos.writeUTF(msg);
dos.flush(); // 没有关闭输出流,所以要手动刷新缓冲区
}
}
}
public class TcpService {
public static void main(String[] args) throws Exception {
// 目标: 实现TCP通信, 多发多收, 服务端, 支持多个客户端
System.out.println("===服务端启动===");
// 1.创建ServerSocket管道对象, 绑定端口号
ServerSocket ss = new ServerSocket(9999);
while (true) {
// 2.监听客户端连接, 阻塞式方法, 客户端连接后, 返回Socket对象
Socket socket = ss.accept();
// 8.监听客户端上线
System.out.println("客户端上线了=" + socket.getInetAddress());
// 3.客户端连接后, 创建一个子线程, 专门负责读取数据
new ServerReader(socket).start();
}
}
}
public class ServerReader extends Thread {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 4.获取输入流, 读取客户端发送的数据
InputStream is = socket.getInputStream();
// 5.把输入流包装为特殊流, 因为客户端使用特殊流发送数据
DataInputStream dis = new DataInputStream(is);
while (true) {
// 6.读取数据
String msg = dis.readUTF();// 消息内容
System.out.println("数据=" + msg);
// 7.获取客户端的ip和端口
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress() + ",端口= " + socket.getPort());
System.out.println("==============");
}
} catch (Exception e) {
System.out.println("客户端下线了" + socket.getInetAddress());
e.printStackTrace();
}
}
}
B/S架构的原理
要求从浏览器中访问服务器, 并立即让服务器响应一个网页给浏览器展示
服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据
当我们使用浏览器访问服务, 实际就是建立了一个TCP连接, 服务器返回网页资源给浏览器展示
public class TcpService {
public static void main(String[] args) throws Exception {
// 目标: 了解B/S架构的原理
System.out.println("===服务端启动===");
// 1.创建ServerSocket管道对象, 绑定端口号
ServerSocket ss = new ServerSocket(8080); // 建议使用8080端口
// 创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
while (true) {
// 2.监听客户端连接, 阻塞式方法, 客户端连接后, 返回Socket对象
Socket socket = ss.accept();
// 8.监听客户端上线
System.out.println("客户端上线了=" + socket.getInetAddress());
// 3.客户端连接后, 把客户端管道包装成任务对象, 交给线程池处理
// 注意: 线程对象可以直接当作任务对象使用
// 因为: class Thread implements Runnable
pool.execute(new ServerReader(socket));
}
}
}
public class ServerReader extends Thread {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 4.获取字节输出流, 给浏览器发送数据
OutputStream os = socket.getOutputStream();
// 5.把字节输出流包装为打印流 (方便)
PrintStream ps = new PrintStream(os);
// 6.写数据
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println(); // 必须换一行
ps.println("<html>");
ps.println("<head>");
ps.println("<meta charset='utf-8'>");
ps.println("<title>");
ps.println("网站标题");
ps.println("</title>");
ps.println("</head>");
ps.println("<body>");
ps.println("<h1 style='color:red'>键盘敲烂,月入过万</h1>");
ps.println("</body>");
ps.println("</html>");
// 7.关闭资源
ps.close();
socket.close();
} catch (Exception e) {
System.out.println("客户端下线了" + socket.getInetAddress());
e.printStackTrace();
}
}
}