网络编程
一、网络编程入门
1、软件结构
- C/S 结构:全称为 Client/Server 结构,是指客户端和服务器结构,常见的有 QQ、百度网盘等软件
- B/S 结构:全称为 Browser/Server 结构,是指浏览器和服务器结构。
两种结构各有优势,但无论哪一种结构都离不开网络的支持。
网络编程,就是在一定的协议下,实现两台计算机通信。
2、网络通信协议
- 网络通信协议:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机进行连接和通信时需要遵守一定的规则,这就好比在道路上行驶的汽车需要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为 网络通信协议。它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
- TCP/IP 协议:传输控制协议/因特网互连协议(Transmission Control Protocol/Internet Protocol),是 Internet 最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了 4 层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求:
上图中,TCP/IP 协议中的四层分别是:
- 应用层:
- 主要负责应用程序的协议,例如 HTTP 协议、FTP 协议等等
- 传输层:
- 主要负责网络程序之间的通信,在进行网络通信时,可以采用 TCP 协议,也可以采用 UDP 协议
- 网络层:
- 网络层是整个 TCP/IP 协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络
- 链路层:
- 定义物理传输通道,通常是对某些网络设备的驱动协议,例如针对光纤、网线提供的驱动
3、协议分类
java.net
包中包含的类和接口,提供底层次的通信细节。
java.net
中提供了对两种常见的网络协议的支持:
- UDP:用户数据报协议(User Datagram Protocol)。UDP 是 无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,而是直接发送数据,同样,接收端在收到数据时,也不会向发送端反馈是否收到了数据。
- 由于使用 UDP 协议消耗资源少,通信效率高,所以通常会用于音频、视频和普通数据的传输例如视频会议都使用 UDP 协议,因为这种情况下偶尔丢失一两个数据包,也不会对接收结果产生较大影响。
- 但是在使用 UDP 协议传输数据时,由于 UDP 的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用 UDP 协议
- 特点:数据报大小被限制在 64KB 之内,超过这个范围就不能发送
数据报(Datagram):网络传输的基本单位
TCP:传输控制协议(Transmission Control Protocol)。TCP 协议是 面向连接 的通信协议,即传输数据之前,在发送端和接收端之间建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
在 TCP 连接中必须要明确客户端与服务器端,由客户端向服务端发送请求连接,每次连接的创建都需要经过 “三次握手”
- 三次握手:TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手:客户端向服务器端发出连接请求
- 第二次握手:服务器端向客户端回送一个响应,通知客户端自己收到了请求
- 第三次握手:客户端再次向服务器端发送确认信息,确认连接。
- 三次握手:TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
4、网络编程三要素
概述:
- 协议
- IP 地址
- 端口号
协议
- 协议:计算机网络通信必须遵守的规则
IP 地址
- IP 地址:指互联网协议地址(Internet Protocol Address)。俗称 ip,IP 地址用来给一个网络中的计算机设备作为唯一的编号。
IP地址分类:
- IPv4:是一个 32 位的二进制数,通常被分为 4 个字节,表示成
a.b.c.d
的形式,例如:192.168.1.1
。其中 a、b、c、d 都是 0 - 255 之间的十进制整数,最多可以表示 42 亿个 IP 地址。 - IPv6:由于互联网蓬勃发展,IP 地址的需求量愈来愈大,为了扩大地址空间,拟通过 Ipv6 重新定义地址空间,采用 128 位地址长度,每 16 个字节一组,分为 8 组十六进制数,表示成
ABCD:EF01:2356:6789:ABCD:EF01:2345:6789
。
常用命令
- 查看本机 IP 地址
ipconfig # Windows
- 检查网络是否连通
# ping 空格 IP 地址
ping 192.168.1.1
- 特殊的 IP:本机 IP : 127.0.0.1、localhost
端口号
网络的通信,本质上是两个进程(应用程序)之间的通信。每台计算机有很多进程,那么在网络通信时,如何区分这些进程呢?
如果说 IP 地址 可以唯一标识网络中的设备,那么 端口号 就可以唯一标识这台设备中的进程了
- 端口号:用两个字节的表示的整数,它的取值范围是 0 - 65535
- 0 - 1023 之间的端口用于一些知名的网络服务和应用
- 普通的应用程序应该使用 1024 以上的端口号。如果端口号被另外一个服务或则和应用占用,会导致当前程序启动失败
利用 协议
+ IP 地址
+ 端口号
三元组合,就可以标识网络中的进程,那么进程间的通信就可以利用这个标识与其他进程进行交互。
二、TCP 通信程序
1、概述
TCP 通信能够实现两台计算机之间的数据交互,通信的两端,要严格区分为 客户端(Client)与 服务端(Server)。
两端通信时的步骤:
- 服务端程序,需要事先启动,等待客户端的连接
- 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端
Java 中,提供了两个类用于实现 TCP 通信程序:
- 客户端:
java.net.Socket
类表示,创建 Socket 对象,向服务端发送请求,服务端响应请求,两者建立连接开始通信 - 服务端:
java.net.ServerSocket
类表示,创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接
2、Socket 类
Socket
类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。
注意:套接字的实际工作是有 SocketImpl
类的实例来执行。应用程序通过更改创建套接字实现的套接字工厂,可以配置自己创建适合本地防火墙的套接字。
常用构造方法
Socket(String host, int port)
:创建流套接字并将其连接到指定主机上的指定端口号。 如果指定的 host 是 null,则相当于指定地址为本机回送地址
Tip:回送地址(127.x.x.x)是本机回送地址(Loopback Address),主要用于网络软件测试及本机进程之间的通信。
3、ServerSocket 类
这个类实现了服务器套接字。服务器套接字等待通过网络进入的请求。它根据该请求执行一些操作,然后可能将结果返回给请求者。
服务器套接字的实际工作由SocketImpl
类的实例执行。 应用程序可以更改创建套接字实现的套接字工厂,以配置自己创建适合本地防火墙的套接字。
4、编写测试代码
注意:本例中我们以发送消息为例子,所以为了方便显示文本信息,使用了适配器获得字符流显示。
(1)客户端
客户端:Client
/**
* TCP 通信的客户端: 向服务器发送连接请求,向服务器发送数据,读取服务器回写的数据
*
* 表示客户端:
* java.net.Scoket: 此类实现客户端套接字。套接字是两台机器间通信的端点
* 套接字:包含了 IP 地址和端口号的网络单位
*
* 常用方法:
* getInputStream() 返回此套接字的输入流
* getOutputStream() 返回此套接字的输出流
* close() 关闭此套接字
*
* 使用步骤:
* 1. 创建客户端 Socket 对象,构造方法绑定服务器的 Ip 地址和端口号
* 2. 使用 Socket 对象的方法 getOutputStream 获取网络字节输出流 OutputStream 对象
* 3. 使用网络字节输出流 OutputStream 对象方法 write, 给服务器发送数据
* 4. 使用 Socket 对象的方法 getInputStream 获取网络字节输入流 InputStream 对象
* 5. 使用网络字节输入流 InputStream 对象方法 read,读取服务器回写的数据
* 6. 释放资源 (Socket)
* 注意:
* 1. 客户端和服务器端进行交互必须使用 Socket 中提供的网络流,不能使用自己创建的流对象
* 2. 当我们创建完客户端对象 Socket 的时候,它就会请求服务器和服务器经过三次握手建立连接通路
* 如果服务器没有启动,就会抛出异常
* ConnectException: Connection refused
* 如果服务器已经启动,就可以正常传输数据
*/
public class TCPClient {
public static void main(String[] args) {
Socket socket = null;
InputStreamReader isr = null;
try {
// 1. 创建客户端对象 Socket, 绑定要和服务器上的哪个端点通信(Ip:端口号)
socket = new Socket("127.0.0.1", 8888);
// 2. 获取网络字节输出流
OutputStream os = socket.getOutputStream();
// 3. 使用 write 向服务器发送数据
os.write("你好,服务器。".getBytes());
// 4. 获取网络字节输入流获取服务器回写的数据
InputStream is = socket.getInputStream();
isr = new InputStreamReader(is);
char[] chars = new char[100];
isr.read(chars);
System.out.println("客户端接收到服务器端发送的数据: " + String.valueOf(chars));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 5. 释放资源
if (isr != null) {
isr.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2)服务器端
服务器端:Server
/**
* TCP 通信的服务器端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据
* java.net.ServerSocket
*
* 构造方法:
* ServerSocket(int port) : 创建绑定到特定端口的服务器套接字
*
* 服务器端必须明确一件事情,必须知道是哪个客户端请求的服务器:
* 需要使用 accept() 方法获取到客户端对象 Socket:
* Socket accept() 侦听要连接到此套接字并接受它。
*
* 服务器实现步骤:
* 1. 创建 ServerSocket 对象,需要指定端口号
* 2. 使用对象方法 accept 获取请求的客户端对象 Socket
* 3. 使用获取到的客户端 Sokcet 对象获取相应的网络输入/输出流
* 4. 通过网络输入/输出流对客户端数据进行读写操作
* 5. 释放资源 (Socket、ServerSocket)
*/
public class TCPServer {
public static void main(String[] args) {
Socket clientScoket = null;
ServerSocket serverSocket = null;
InputStreamReader isr = null;
try {
// 1. 注意这里绑定的端口号是客户端指定的服务器端口号
serverSocket = new ServerSocket(8888);
// 2. 获取到客户端 Socekt 对象
clientScoket = serverSocket.accept();
// 3. 获取网络字节输入流,获取客户端发送的数据
InputStream is = clientScoket.getInputStream();
isr = new InputStreamReader(is);
char[] chars = new char[100];
int read = isr.read(chars);
if (read > 0) { // 说明服务器接收到客户端发送的数据,就显示出来并回写数据
System.out.println("服务器端接收到客户端发送的数据: " + String.valueOf(chars));
// 4. 获取网络字节输出流,向客户端回写数据
OutputStream os = clientScoket.getOutputStream();
os.write("你好,客户端。".getBytes());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5. 释放资源
try {
if (isr != null) {
isr.close();
}
if (clientScoket != null) {
clientScoket.close();
}
if (serverSocket != null) {
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
5、运行
上面的例子中我们指定了端口号 8888
先来看看未启动服务器端时该端口的监听情况:
启动服务器进程:
最终结果
三、文件上传
1、流程分析
- 【客户端】输入流:从硬盘中读取文件数据到程序中
- 【客户端】输出流:从内存中写出文件数据到网络流
- 【服务端】输入流:从网络流中读取文件数据到服务器端程序中
- 【服务端】输出流:从内存中写出文件数据到服务器本地硬盘
2、基本实现
(1)客户端
/**
* 文件上传案例之客户端程序
*/
public class Client {
public static void main(String[] args) {
// 准备工作
FileInputStream fis = null;
Socket clientSocket = null;
try {
// 创建输入流对象,从硬盘中读取文件数据
fis = new FileInputStream("src\\main\\java\\com\\naivekyo\\Java_Net\\FileUpload\\client_picture.jpg");
// 创建客户端 Socket 对象, 测试使用:本机 9999 端口
clientSocket = new Socket("127.0.0.1", 9999);
System.out.println("开始上传");
// 获取网络输出和输入流用于上传数据和接收服务端消息
OutputStream os = clientSocket.getOutputStream();
InputStream is = clientSocket.getInputStream();
// 上传方式一:使用 read() 方法一个一个字节的来读,效率极低,但是不会阻塞
// int read;
// while ((read = fis.read()) != -1) {
// System.out.println("客户端发送一个字节:" + read);
// os.write(read);
// }
// 上传方式二:构建一个缓冲数组,效率比上面的高,但是会导致文件大小不一致
// int len = -1;
// byte[] buf = new byte[1024];
// while ((len = fis.read(buf)) != -1) {
// System.out.println("客户端发送一个字节数组的大小: " + len);
// os.write(buf);
// }
// 上传方式三:推荐,使用缓存数组同时使用特定方法做处理
System.out.println("上传中...");
int len = -1;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
/**
* 解决:上传完文件后,给服务器传过去一个网络流结束的标志
* void shutdownOutput()
* 禁用此套接字的输出流
* 对于 TCP套接字,任何先前写入的数据将被发送,随后是 TCP的正常连接终止序列。
*/
clientSocket.shutdownOutput();
// 上传成功后,清空缓存区
Arrays.fill(bytes, (byte) 0);
// 得到服务端传递回来的消息
while ((len = is.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len));
}
clientSocket.shutdownInput();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (clientSocket != null) {
clientSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2)服务端
/**
* 文件上传案例之服务器端
*/
public class Server {
public static void main(String[] args) {
// 准备工作
FileOutputStream fos = null;
ServerSocket serverSocket = null;
Socket clientSocket = null;
try {
// 创建对象
fos = new FileOutputStream("src\\main\\java\\com\\naivekyo\\Java_Net\\FileUpload\\server_picture.jpg");
serverSocket = new ServerSocket(9999); // 服务器指定监听 9999 端口
clientSocket = serverSocket.accept();
// 获取网络输入流,取出数据并写出到服务器端磁盘
InputStream is = clientSocket.getInputStream();
// 获取网络输出流,告诉客户端已经处理成功了
OutputStream os = clientSocket.getOutputStream();
// 接收方式一:一个字节一个字节的读写
// int read;
// while ((read = is.read()) != -1) {
// System.out.println("服务端接收一个字节:" + read);
// fos.write(read);
// }
// 接收方式二:一个数组一个数组的读,同样一个数组一个数组的写,会导致文件大小不一致问题
// int len = -1;
// byte[] bytes = new byte[1024];
// while ((len = is.read(bytes)) != -1) {
// System.out.println("服务端接收一个字节数组的大小: " + len);
// fos.write(bytes);
// }
// 接收方式三:一个数组一个数组的读,每次写入读取的总字节
int len = -1;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
System.out.println("服务端从网络输入流中一次得到的字节数: " + len);
fos.write(bytes, 0, len);
}
// 结束这次网络输入流的连接
clientSocket.shutdownInput();
// 清空缓冲区
Arrays.fill(bytes, (byte) 0);
// 读取完毕,告诉客户端已经处理成功了
os = clientSocket.getOutputStream();
os.write("服务端: 文件接收完毕!".getBytes());
clientSocket.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
if (clientSocket != null) {
serverSocket.close();
}
if (serverSocket != null) {
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、效率及阻塞问题
阻塞问题
我们现在使用的有两套 IO 流:
- Java 的 BIO
- Socket 的 BIO
它们都是 同步且阻塞 的 IO,而阻塞型 IO 主要阻塞在两个地方:
- 调用
InputStream.read
(或者OutputStream.write
)方法是阻塞的,它会一直等到数据到来(或者超时)才会返回 - 调用
ServerSocket.accept
方法时,服务端会一直阻塞到有客户端连接才会返回
案例中出现的阻塞
对于普通的文件流来说:一般都是将文件加载到内存中,或从内存中把数据写入到文件
- read
- write
上面两个方法会阻塞当前线程,除非可以读/写到数据,一般都可以正常结束(文件末尾 -1 标志)。
重点在于通过 Socket 获取的网络读/写字节流,它们和普通的字节输入/输出流不一样,结束的标记是 TCP 释放连接时使用的 TCP 终止序列。这段终止序列有时候需要我们来指定,比如:
- 一段代码,上面使用了网络输出流,下面又准备使用网络输入流
- 案例场景:使用网络输出流向服务器端发送数据,然后使用网络输入流获取服务器端返回的信息
- 那么两者中间我们一定要先结束掉输出流,为输出流结束 TCP 连接
使用 Socket 的方法:
shutdownOutput()
shutdownInput()
对于服务器端也是如此(tip:上面第五步其实可以不用加)
使用类似如下代码测试是否关闭连接:
System.out.println(clientSocket.isOutputShutdown() ? "网络输出流已结束" : "OuputStream -1");
System.out.println(clientSocket.isInputShutdown() ? "网络输入流已结束" : "InputStream -1");
效率问题
(1)read()和 write()
对代码片段做一些处理:
在上述的例子中,我们是这样读取数据并上传的:
int read;
while ((read = fis.read()) != -1) {
System.out.println("客户端发送一个字节:" + read);
os.write(read);
}
服务端是这样接收数据的:
int read;
while ((read = is.read()) != -1) {
System.out.println("服务端接收一个字节:" + read);
fos.write(read);
}
这种情况下流程是这样的:
- 客户端从文件流中读取一个字节
- 将该字节发送给 Socket 的网络流
- 服务端从网络流中取出一个字节的数据
- 服务端将该字节写入到文件流
客户端和服务端采用 TCP 协议传输一个一个的字节,效率极其低下,但是传输不会出现问题。
(2)read(byte[] b) 和 write(byte[] b)
参数说明:
- byte[] b:读写数据的缓存区
一个字节一个字节的读取数据,效率太慢,但是如果我们使用 read(byte[] b)
这个重载的方法又会怎么样呢?
// 客户端发送数据
int len = -1;
byte[] buf = new byte[1024];
while ((len = fis.read(buf)) != -1) {
System.out.println("客户端发送一个字节数组的大小: " + len);
os.write(buf);
}
// 服务端接收数据
int len = -1;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
System.out.println("服务端接收一个字节数组的大小: " + len);
fos.write(bytes);
}
如果这样干了,就会出现这种情况:
- 客户端最后一次从文件流中读取到字节数是 137(举个例子)
- 但是服务端最后一次从网络输入流中读取到的字节数是 1024 (因为我们这里用的缓冲区的大小就是 1024 字节)
最终的结果是服务端保存的文件比客户端硬盘上的文件大。
原因在于:
- 缓冲数组只有刚开始的时候里面才是空的
- 第一次向缓冲数组写入数据是 1024 个字节,把数组填满了
- 此后每一次向缓冲区存入的字节数不一定都是 1024
- 但是发送给服务端的一定是 1024 个字节
解决方法:见下文
(3)write(byte[] b, int off, int len)
参数说明:
- byte[] b:读取数据的缓冲区
- int off:目标数组 b 的起始偏移量:准确来说是每次写出数据的时候从数组的哪个下标开始
- int len:要写入的字节数
该方法结合 read(byte[] b)
可以实现每次从文件流中读取多少数据就将多少字节传给网络流,最终服务器端可以将文件完整的保存下来:
// 客户端
int len = -1;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
System.out.println("客户端发送到网络输出流的字节数: " + len);
os.write(bytes, 0, len);
}
// 服务端
int len = -1;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
System.out.println("服务端从网络输入流中一次得到的字节数: " + len);
fos.write(bytes, 0, len);
}
这样一来,客户端和服务端上的文件大小就一样了。
4、优化分析
(1)文件名称写死的问题
服务端,保存文件的名称如果写死,最终会导致服务器硬盘,只保留一个文件,建议使用系统时间来优化,保证文件名唯一:
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);
(2)循环接收问题
服务端,保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环来改进,可以不断的接收不同用户的文件:
// 每次接收新的连接,创建一个 Socket
while(true) {
Socket socket = serverSocket.accept();
......
}
(3)效率问题
服务端,在接收大文件时,可能耗费的时间比较长,此时不能接收其他用户上传,所以,使用多线程技术优化:
while(true) {
Socket socket = serverSocket.accept();
new Thread(() -> {
// 接收文件的方法
})
}
(4)优化后的代码
优化后的代码还是有一定的缺点:
- 不能检测文件类型,现在写死了上传的是 jpg 图片
- 没有使用缓冲流去进一步优化性能
客户端
/**
* 优化客户端上传
*/
public class EnhanceClient {
public static void main(String[] args) {
// 要上传的文件路径
String file = "src\\main\\java\\com\\naivekyo\\Java_Net\\FileUploadImporve\\enhance_client_picture.jpg";
// 测试上传 10 个文件
for (int i = 0; i < 10; i++) {
upload(file);
}
}
private static void upload(String fileName) {
System.out.println("文件上传线程 id: " +
Thread.currentThread().getId() + " --- 线程名: " +
Thread.currentThread().getName() + "开始执行");
// 准备工作
Socket socket = null;
FileInputStream fis = null;
// 创建对象
try {
socket = new Socket("127.0.0.1", 10000);
fis = new FileInputStream(fileName);
// 获取网络输出流
OutputStream os = socket.getOutputStream();
int len = -1;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
// 上传完毕,关闭网络输出流,断开 TCP 连接
socket.shutdownOutput();
// 清空缓存数组
Arrays.fill(bytes, (byte) 0);
// 获取网络输入流以获取服务器端返回的消息
InputStream is = socket.getInputStream();
while ((len = is.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, len));
}
System.out.println();
// 接收完毕,关闭网络输入流
socket.shutdownInput();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源
try {
if (fis != null)
fis.close();
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端
/**
* 优化服务端接收
*
* 属于监控线程,一直开启
*/
@SuppressWarnings("all")
public class EnhanceServer {
public static void main(String[] args) throws IOException {
// 开启一个线程池,用于监控是否有客户端请求上传文件
ExecutorService threadPool = Executors.newFixedThreadPool(7);
ServerSocket serverSocket = null;
serverSocket = new ServerSocket(10000);
System.out.println("服务器启动,开始监控文件上传服务端口: " + serverSocket.getLocalPort());
while (true) {
System.out.println("当前监控线程 id: " + Thread.currentThread().getId() + " --- " +
"当前监控线程 name: " + Thread.currentThread().getName());
System.out.println("等待连接...");
// 当前线程捕捉到客户端发送的一个请求
// 注意这里 accept 方法阻塞的整个主线程
final Socket socket = serverSocket.accept();
System.out.println("客户端地址: " + socket.getInetAddress().getHostAddress() + " 连接成功!");
// 开启一个子线程去处理,然后继续循环,继续阻塞
threadPool.execute(() -> handler(socket));
}
}
// 开启子线程处理上传服务
private static void handler(Socket socket) {
System.out.println("当前子线程: id -> " + Thread.currentThread().getId() +
" name -> " + Thread.currentThread().getName());
// 准备工作
FileOutputStream fos = null;
// 开始
// 创建一个文件夹用于保存上传的所有文件
File file = new File("src\\main\\java\\com\\naivekyo\\Java_Net\\FileUploadImporve\\server_save_path");
if (!file.exists()) {
file.mkdir();
}
// 构建文件路径
String fileName = file.getPath() + "\\file_" + System.currentTimeMillis() + ".jpg";
try {
fos = new FileOutputStream(fileName);
// 获取网络输入流
InputStream is = socket.getInputStream();
// 开始存储文件
int len = -1;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
// 保存完成,关闭网络输入流、刷新缓冲数组、向客户端返回成功的信息
socket.shutdownInput();
Arrays.fill(bytes, (byte) 0);
// 获取网络输出流,返回成功信息
OutputStream os = socket.getOutputStream();
os.write("上传成功!".getBytes());
// 关闭网络输出流
socket.shutdownOutput();
System.out.println("当前子线程: id -> " + Thread.currentThread().getId() +
" name -> " + Thread.currentThread().getName() + " 处理完成!");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (fos != null)
fos.close();
if (socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
四、模拟 B/S 服务器
模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。
先做一个模拟服务器端,监听 8080 端口:
/**
* 模拟服务器端
* 使用浏览器请求该模拟服务器
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
// 监听本机 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
// 使用网络输入流,获取请求的相关信息
InputStream is = socket.getInputStream();
int len = -1;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len));
}
}
}
1、服务端代码
/**
* 模拟服务器端
* 使用浏览器请求该模拟服务器
*/
@SuppressWarnings("all")
public class TCPServer {
// 测试路径: src/main/java/com/naivekyo/Java_Net/BS/index.html
public static void main(String[] args) throws IOException {
// 监听本机 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
// 线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
while (true) {
// 阻塞线程直到收到客户端请求
final Socket socket = serverSocket.accept();
/*
注意,我们的 html 中有图片,而且默认网页有一个 favicon,我们也需要准备,所以会发起至少三次请求
第一次返回 html
第二次返回 favicon
第三次返回 图片
使用循环 + 多线程实现比较方便
*/
// 处理请求
pool.execute(() -> {
try {
handler(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
private static void handler(Socket socket) throws IOException {
// 使用网络输入流,获取请求的相关信息
InputStream is = socket.getInputStream();
// 通过转换流获取浏览器的请求信息
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 读取客户端请求的第一行数据,获取其中请求的资源路径
// GET /src/main/java/com/naivekyo/Java_Net/BS/index.html HTTP/1.1
String line = br.readLine();
String[] arr = line.split(" ");
String htmlPath = arr[1].substring(1); // src/main/java/com/naivekyo/Java_Net/BS/index.html
String url = htmlPath.replace("/", "\\");
// 创建本地字节输入流
FileInputStream fis = new FileInputStream(url);
// 获取网络输出流
OutputStream os = socket.getOutputStream();
// 写入 Http 协议响应头,这是固定写法
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
// 必须写入空行,否则浏览器不解析
os.write("\r\n".getBytes());
// 读取请求的 html 文件
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
// 释放资源
fis.close();
br.close();
socket.close();
}
}
2、效果
五、Java 网络爬虫
package com.naivekyo.network;
import sun.net.www.protocol.https.HttpsURLConnectionImpl;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author NaiveKyo
* @version 1.0
* @description: java 网络爬虫
* @since 2022/3/16 10:33
*/
public class Spider {
public static void main(String[] args) {
// 爬取凤凰网首页的超链接信息
String urls = "https://www.ifeng.com";
String filePath = System.getProperty("user.dir") + "\\sortutil\\src\\com\\naivekyo\\network\\spider.txt";
URL url = null;
HttpsURLConnectionImpl httpsCi = null;
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
FileOutputStream fos = null;
OutputStreamWriter osw = null;
BufferedWriter bw = null;
try {
url = new URL(urls);
httpsCi = (HttpsURLConnectionImpl) url.openConnection();
// 设置网络连接属性
httpsCi.setConnectTimeout(2000);
httpsCi.setReadTimeout(2000);
httpsCi.setRequestMethod("GET");
// 获取数据
httpsCi.connect();
is = httpsCi.getInputStream();
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
fos = new FileOutputStream(new File(filePath), true);
osw = new OutputStreamWriter(fos);
bw = new BufferedWriter(osw);
// 通过正则表达式匹配网页中的超链接地址, 并保存
String pat = "https://\\w+\\.\\w+\\.[A-Za-z]+";
String str = null;
while ((str = br.readLine()) != null) {
Pattern compile = Pattern.compile(pat);
Matcher matcher = compile.matcher(str);
while (matcher.find()) {
bw.write(matcher.group());
bw.newLine();
}
}
System.out.println("爬取完毕!");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流
try {
if (bw != null)
bw.close();
if (osw != null)
osw.close();
if (fos != null)
fos.close();
if (br != null)
br.close();
if (isr != null)
isr.close();
if (is != null)
is.close();
if (httpsCi != null)
httpsCi.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}