🔥作者主页 :小林同学的学习笔录
🔥mysql专栏 :小林同学的专栏
目录
[1. 网络编程](#1. 网络编程)
[1.1 概述](#1.1 概述)
[1.2 网络编程的三要素](#1.2 网络编程的三要素)
[1.2.1 IP地址](#1.2.1 IP地址)
[1.2.2 InetAddress](#1.2.2 InetAddress)
[1.2.3 端口和协议](#1.2.3 端口和协议)
[1.3 UDP协议](#1.3 UDP协议)
[1.3.1 UDP发送数据](#1.3.1 UDP发送数据)
[1.3.2 UDP接收数据](#1.3.2 UDP接收数据)
[1.4 TCP协议](#1.4 TCP协议)
[1.4.1 TCP协议实例](#1.4.1 TCP协议实例)
[1.4.2 三次握手,四次挥手](#1.4.2 三次握手,四次挥手)
1. 网络编程
1.1 概述
概述:在网络通信协议下,不同计算机上运行的程序,进行数据的传输
java.net包中可以看见常见的网络应用程序API
常见的软件架构:
C/S:Client / Server 客户端 / 服务器
- 需要用户下载并安装客户端程序,在远程有一个服务器程序
- 优缺点:
- 画面可以做的比较精美,用户体验好(不需要网络传输,数据来源于安装包)
- 需要开发客户端,也需要开发服务端
- 用户需要下载和更新的时候太麻烦
B/S:Browser / Server 浏览器 / 服务器
- 只需要一个浏览器,通过访问不同的网址实现操作程序,客户访问不同的服务器
- 优缺点:
- 不需要开发客户端,只需要页面 + 服务器
- 用户不需要下载,打开浏览器就能使用
- 如果应用过大,用户体验将受到影响(因为数据进行网络传输效率比较低)
1.2 网络编程的三要素
IP:设备在网络中的地址,是唯一标识
端口号:应用程序在设备中的唯一标识
协议:数据在网络中的传输规则,常见的协议有UDP,TCP,HTTP,HTTPS,FTP
1.2.1 IP地址
IP地址分为两大类:
-
IPv4:是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是"11000000 10101000 00000001 01000010",这么长的地址,处理起来也太费劲了。为了方便使用,IP地址经常被写成十进制的形式,中间使用符号"."分隔不同的字节。于是,上面的IP地址可以表示为"192.168.1.66"。IP地址的这种表示法叫做"点分十进制表示法",这显然比1和0容易记忆得多,最多有2^32次方个IP
-
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,每一组用分号隔开,这样就解决了网络地址资源数量不够的问题,最多有2^128次方个IP、
DOS常用命令:
-
ipconfig:查看本机IP地址
-
ping IP地址:检查网络与目标主机是否能连通
特殊IP地址:
- 127.0.0.1:是回送地址,可以代表本机地址LocalHost,一般用来测试使用
疑问:假设192.168.1.100是我的电脑IP,那么这个IP跟127.0.0.1是一样吗?
不一样,192.168.1.100和127.0.0.1不是一样的IP地址。192.168.1.100是局域网内的私有IP地址,用于在局域网中标识设备。而127.0.0.1是本地回环地址,用于在同一台设备内部进行通信。当你的计算机尝试连接127.0.0.1时,它实际上是在尝试与自己通信,而不是与网络上的其他设备通信。
疑问:公网地址(万维网使用)和私有地址(局域网使用)的区别?公网地址和私有地址之间的主要区别在于它们的可访问性和范围。公网地址是全球唯一的IP地址,用于在互联网上唯一标识设备和进行通信。私有地址则是在局域网内使用的地址,不会在互联网上进行路由,因此不能直接从互联网上访问。私有地址用于在局域网内部进行通信,而通过路由器进行网络地址转换(NAT),可以允许多个设备共享单个公网IP地址来访问互联网。
1.2.2 InetAddress
InetAddress
是 Java 编程语言中用于表示 IP 地址的类。它提供了一种将 IP 地址和主机名相互转换的方式。通过 InetAddress
类,可以实现网络通信中的主机名解析、IP 地址解析等功能。
成员方法:
1.2.3 端口和协议
-
端口
- 设备上应用程序的唯一标识
-
端口号
- 用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
-
协议
- 计算机网络中,连接和通信的规则被称为网络通信协议
-
UDP协议
-
用户数据报协议(User Datagram Protocol)
-
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
-
由于使用UDP协议消耗系统资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输
-
速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据
-
例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议
-
-
TCP协议
-
传输控制协议 (Transmission Control Protocol)
-
TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过"三次握手"
-
速度慢,没有大小限制,数据安全
-
三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠
第一次握手,客户端向服务器端发出连接请求,等待服务器确认
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求
第三次握手,客户端再次向服务器端发送确认信息,确认连接
-
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等
-
1.3 UDP协议
1.3.1 UDP发送数据
构造方法:
成员方法:
发送数据的步骤
-
创建发送端的Socket对象(DatagramSocket)
-
创建数据,并把数据打包(DatagramPacket)
-
调用DatagramSocket对象的方法发送数据(send)
-
关闭发送端
代码演示:
java
public class Send {
public static void main(String[] args) throws IOException {
/**
* 创建发送端的Socket对象(DatagramSocket)
* 创建数据,并把数据打包(DatagramPacket)
* 调用DatagramSocket对象的方法发送数据(send)
* 关闭发送端
*/
//这里参数如果没有端口号,系统会自动分配一个端口号
DatagramSocket socket = new DatagramSocket(10000);
String str = "龙颜大怒666";
byte[] bytes = new byte[1024];
bytes = str.getBytes();
DatagramPacket packet = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("127.0.0.1"),10086);
socket.send(packet);
socket.close();
}
}
1.3.2 UDP接收数据
构造方法:
成员方法:
接收数据的步骤
-
创建接收端的Socket对象(DatagramSocket)
-
创建一个数据包,用于接收数据(DatagramPacket)
-
调用DatagramSocket对象的方法接收数据(receive)
-
解析数据包,并把数据在控制台显示
-
关闭接收端
代码演示:
java
public class Receive {
public static void main(String[] args) throws IOException {
/**
* 创建接收端的Socket对象(DatagramSocket)
* 创建一个数据包,用于接收数据(DatagramPacket)
* 调用DatagramSocket对象的方法接收数据(receive)
* 解析数据包,并把数据在控制台显示
* 关闭接收端
*/
//注意:这里的端口号一定要与发送端中的数据包端口一致,不然会收不到数据
DatagramSocket socket = new DatagramSocket(10086);
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes,bytes.length);
//这个方法会一直等待发送端发送信息过来,直到拿到数据才会取消阻塞
socket.receive(packet);
byte[] data = packet.getData();
InetAddress address = packet.getAddress();
int port = packet.getPort();
System.out.println("数据:" + new String(data,0,packet.getLength()) + " 主机IP:" + address + " 端口号:" + port);
socket.close();
}
}
输出结果:
数据:龙颜大怒666 主机IP:/127.0.0.1 端口号:10000
1.4 TCP协议
1.4.1 TCP协议实例
代码演示:
java
public class Client {
public static void main(String[] args) throws IOException {
//创建socket对象
//细节:在创建对象同时会连接服务端
//如果连接不上代码会报错
Socket socket = new Socket("127.0.0.1",10000);
//创建socket的输出流通道
OutputStream outputStream = socket.getOutputStream();
//写入数据
outputStream.write("你好呀".getBytes());
//关闭资源
socket.close();
outputStream.close();
}
}
java
public class Server {
public static void main(String[] args) throws IOException {
//这里的端口号要跟客户端的Socket保持一致
ServerSocket serverSocket = new ServerSocket(10000);
//这里会阻塞等待客户端发送信息
Socket socKet = serverSocket.accept();
//通过socket获取输入流通道
InputStream inputStream = socKet.getInputStream();
//解决中文乱码问题
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
int len;
while ((len = bufferedReader.read()) != -1) {
System.out.print((char) len);
}
//关闭资源
serverSocket.close();
socKet.close();
}
}
1.4.2 三次握手,四次挥手
三次握手:为了确保连接的建立
四次挥手:确保连接断开,且数据处理完毕
1.5 综合练习
1.5.1 多发多送
java
public class Test01 {
public static void main(String[] args) throws IOException {
/**
* 客户端:多次发送数据
* 服务端:接收多次数据,并打印
*/
Socket socket = new Socket("127.0.0.1",10002);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入你要发送的信息:");
String str = scanner.nextLine();
//用户输入886表示退出
if("886".equals(str)){
break;
}
outputStream.write(str.getBytes());
}
//关闭资源
outputStream.close();
socket.close();
}
}
public class TestServer01 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(10002);
Socket socket = serverSocket.accept();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
int len;
while((len = bufferedReader.read()) != -1){
System.out.print((char)len);
}
//关闭资源
socket.close();
serverSocket.close();
}
}
1.5.2 接收并反馈
java
public class Test02 {
public static void main(String[] args) throws IOException {
/**
* 客户端:发送一条数据,接收服务端反馈的消息并打印
* 服务端:接收数据并打印,再给客户端反馈信息
*/
Socket socket = new Socket(InetAddress.getLocalHost(),10003);
Scanner scanner = new Scanner(System.in);
OutputStream os = socket.getOutputStream();
System.out.println("请输入要发给服务端的信息:");
String str = scanner.nextLine();
os.write(str.getBytes());
//细节:这里需要一个结束标记,服务端那边读取才知道结束
socket.shutdownOutput();
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int len;
while ((len = isr.read()) != -1){
System.out.print((char)len);
}
socket.close();
}
}
public class TestServer02 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(10003);
Socket socket = serverSocket.accept();
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int len;
//细节:
//read方法从连接通道读取数据
//但是需要一个结束标记循环才会停止
//否则,程序就会一直停在read方法这里,等待读取下面的数据
while ((len = isr.read()) != -1){
System.out.print((char)len);
}
OutputStream os = socket.getOutputStream();
os.write("收到客户端的信息".getBytes());
socket.close();
serverSocket.close();
}
}
1.5.3 上传练习
java
public class Test03 {
public static void main(String[] args) throws IOException {
/**
* 案例需求:
* 客户端:数据来自于本地文件,接收服务器反馈
* 服务器:接收到的数据写入本地文件,给出反馈
*/
Socket socket = new Socket("127.0.0.1", 10000);
byte[] bytes = new byte[1024];
int len;
//这种方式效率比较低
//FileInputStream fis = new FileInputStream("D:\\img\\0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg");
//OutputStream os = socket.getOutputStream();
//缓存流可以降低磁盘IO次数
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("source/0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//发送数据
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//设置结束标记
socket.shutdownOutput();
//接收数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
while ((len = isr.read()) != -1){
System.out.print((char)len);
}
socket.close();
}
}
public class TestServer03 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(10000);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
//效率低
//FileOutputStream fos = new FileOutputStream(new File("D:\\test\\a.jpg"));
//这里需要解决文件名重复问题,导致原先的文件会被后面的覆盖,用UUID来解决
//System.out.println(UUID.randomUUID());
//72e165ae-98ad-4cd4-80e9-c9f86b910461,我们一般看到的效果是没有"-"的,需要处理一下
String name = UUID.randomUUID().toString().replace("-", "");
//BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\a.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\" + name + ".jpg"));
//边读边写
while((len = is.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//发送数据
OutputStream os = socket.getOutputStream();
os.write("收到数据啦".getBytes());
socket.close();
serverSocket.close();
}
}
1.5.4 服务器改写成多线程,以及线程优化
服务器只能处理一个客户端请求,接收完一个图片之后,服务器就关闭了。
优化方案一:使用循环
弊端:第一个用户正在上传数据,第二个用户就来访问了,此时第二个用户是无法成功上传的。
所以,使用多线程改进
优化方案二:使用循环 + 多线程
每来一个用户,就开启多线程处理
java
public class Test04 {
public static void main(String[] args) throws IOException {
/**
* 案例需求:
* 客户端:数据来自于本地文件,接收服务器反馈
* 服务器:接收到的数据写入本地文件,给出反馈
*/
Socket socket = new Socket("127.0.0.1", 10000);
byte[] bytes = new byte[1024];
int len;
//这种方式效率比较低
//FileInputStream fis = new FileInputStream("D:\\img\\0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg");
//OutputStream os = socket.getOutputStream();
//缓存流可以降低磁盘IO次数
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("source/0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//发送数据
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//设置结束标记
socket.shutdownOutput();
//接收数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
while ((len = isr.read()) != -1){
System.out.print((char)len);
}
socket.close();
}
}
public class TestServer04 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(10000);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
3,//核心线程数量
16,//线程池总大小
60,//空闲时间
TimeUnit.SECONDS,//空闲时间(单位)
new ArrayBlockingQueue<>(2),//队列
Executors.defaultThreadFactory(),//线程工厂,让线程池如何创建线程对象
new ThreadPoolExecutor.AbortPolicy()//阻塞队列
);
while (true) {
//等待客户端连接
Socket socket = serverSocket.accept();
//一个用户对应一条线程
//new Thread(new MyRunnable(socket)).start();
//线程池进行优化
poolExecutor.submit(new MyRunnable(socket));
}
}
}
public class MyRunnable implements Runnable {
private Socket socket;
public MyRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
//效率低
//FileOutputStream fos = new FileOutputStream(new File("D:\\test\\a.jpg"));
//这里需要解决文件名重复问题,导致原先的文件会被后面的覆盖,用UUID来解决
//System.out.println(UUID.randomUUID());
//72e165ae-98ad-4cd4-80e9-c9f86b910461,我们一般看到的效果是没有"-"的,需要处理一下
String name = UUID.randomUUID().toString().replace("-", "");
//BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\a.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\" + name + ".jpg"));
//边读边写
while ((len = is.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
//发送数据
OutputStream os = socket.getOutputStream();
os.write("上传成功".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}