内容概述
- 网络编程三要素
- TCP通信
- 文件上传
- 模拟B/S
- 式
- 多例设计模式
- 工厂设计模式
- [ ] 能够描述网络编程的三要素及其对应作用
- [ ] 能够说出UDP协议的特点
- [ ] 能够说出TCP协议的特点
- [ ] 能够说出TCP协议构建服务端和客户端的类
- [ ] 能够使用TCP协议实现客户端和服务端字符串数据传输
- [ ] 能够理解TCP协议下文件上传案例
- [ ] 能够理解TCP协议下实现Web服务端案例
- [ ] 能够使用Junit进行程序测试
- [ ] 能够使用断言进行逻辑结果测试
- [ ] 能够说出单例设计模式的好处
- [ ] 能够说出多例模式的好处
- [ ] 能够使用简单工厂模式编写java程序
第一章 网络编程入门
1.1软件结构
- C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。
1.2 网络通信协议
-
**网络通信协议:**通信协议是计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。
-
TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
1.3 协议分类
通信的协议还是比较复杂的,java.net
包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。
java.net
包中提供了两种常见的网络协议的支持:
- TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
- 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。服务器你死了吗?
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。我活着啊!!
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。我知道了!!
- 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
- UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
1.4 网络编程三要素
协议
- **协议:**计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。
IP地址
- IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。
**IP地址分类 **
-
IPv4:是一个32位的二进制数,通常被分为4个字节,表示成
a.b.c.d
的形式,例如192.168.65.100
。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。 -
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。
为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
常用命令
- 查看本机IP地址,在控制台输入:
ipconfig
- 检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 220.181.57.216
ping www.baidu.com
特殊的IP地址
- 本机IP地址:
127.0.0.1
、localhost
。
端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。
位于网络中一台计算机可以通过IP地址去访问另一台计算机,并通过端口号访问目标计算机中的某个进程。一个网络进程至少会有一个端口号。
- **端口号:用整数表示,它的取值范围是065535**。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
利用协议
+IP地址
+端口号
三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
在Java中,可以使用java.net.InetAddress类来表示一个IP地址:
/**
InetAddress类概述
* 一个该类的对象就代表一个IP地址对象。
InetAddress类成员方法
* static InetAddress getLocalHost()
* 获得本地主机IP地址对象
* static InetAddress getByName(String host)
* 根据IP地址字符串或主机名获得对应的IP地址对象
* String getHostName();获得主机名
* String getHostAddress();获得IP地址字符串
*/
public class InetAddressDemo01 {
public static void main(String[] args) throws Exception {
// 获得本地主机IP地址对象
InetAddress inet01 = InetAddress.getLocalHost();
// pkxingdeMacBook-Pro.local/10.211.55.2
// 主机名/ip地址字符串
System.out.println(inet01);
// 根据IP地址字符串或主机名获得对应的IP地址对象
// InetAddress inet02 = InetAddress.getByName("192.168.73.97");
InetAddress inet02 = InetAddress.getByName("baidu.com");
System.out.println(inet02);
// 获得主机名
String hostName = inet01.getHostName();
System.out.println(hostName);
// 获得IP地址字符串
String hostAddress = inet01.getHostAddress();
System.out.println(hostName);
System.out.println(hostAddress);
}
}
1.5 IP地址类InetAddress
InetAddress 此类表示Internet协议(IP)地址,是构建UDP和TCP协议的低级协议
InetAddress
静态方法:
public static InetAddress getLocalHost()
获得本地主机IP地址对象
public static InetAddress getByName(String host)
根据IP地址字符串或主机名获得对应的IP地址对象,也可以通过网址例如:www.baidu.com
非静态方法:
String getHostName(): 获得主机名
String getHostAddress(): 获得IP地址字符串
【代码实践】
- 获取本机IP地址对象
- 根据IP地址字符串或主机名获得对应的IP地址对象
- 根据网址获取IP地址对象
public class Demo01 {
public static void main(String[] args) throws UnknownHostException {
//InetAddress
//1. 获取本机IP地址对象
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("localHost = " + localHost);//lyc-surfacebook/192.168.147.72
//获取主机名
String hostName = localHost.getHostName();
System.out.println("hostName = " + hostName);//lyc-surfacebook
//获取IP地址
String hostAddress = localHost.getHostAddress();
System.out.println("hostAddress = " + hostAddress);//192.168.147.72
//2. 根据IP地址字符串或主机名获得对应的IP地址对象
InetAddress ip2 = InetAddress.getByName("lyc-surfacebook");
System.out.println("ip2 = " + ip2);//lyc-surfacebook/192.168.147.72
InetAddress ip3 = InetAddress.getByName("192.168.147.72");
System.out.println("ip3 = " + ip3);
//3. 根据网址获取IP地址对象
InetAddress ip4 = InetAddress.getByName("www.baidu.com");
System.out.println("ip4 = " + ip4);//www.baidu.com/112.80.248.76
}
}
第二章 TCP通信程序
1 概述
TCP通信是严格区分客户端与服务器端的,在通信时,必须先由客户端去连接服务器端才能实现通信,服务器端不可以主动连接客户端,并且服务器端程序需要事先启动,等待客户端的连接。
在JDK中提供了两个类用于实现TCP程序,一个是ServerSocket
类,用于表示服务器端,一个是Socket
类,用于表示客户端。
通信时,首先创建代表服务器端的ServerSocket对象,该对象相当于开启一个服务,并等待客户端的连接,然后创建代表客户端的Socket对象向服务器端发出连接请求,服务器端响应请求,两者建立连接开始通信。
2 Socket类
Socket
类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。
构造方法
Socket(String host, int port) 创建一个流套接字并将其连接到指定主机上的指定端口号
参数说明:
host 表示服务器端的主机名,也可以是服务器端的IP地址,只不过是String类型的
port 表示服务器端的端口
注意:客户端套接字,一旦创建,就会尝试与目标IP和端口的服务端建立连接通道。
【代码实践】
建立客户端,连接本电脑的服务端程序,服务端程序端口为:8888
public class Demo01 {
public static void main(String[] args) throws IOException {
//建立客户端,连接本电脑的服务端程序,服务端程序端口为:8888
Socket socket = new Socket("127.0.0.1", 8888);
System.out.println("socket = " + socket);
}
}
因为服务端还没有构建,此时不存在,连接会出错:
常用方法
InputStream getInputStream();获得字节输入流对象,用来获取服务端发送的数据
OutputStream getOutputStream(); 获得字节输出流对象,用来给服务端发送数据
void shutdownOutput():禁用此套接字的输出流,任何先前写出的数据将被发送,随后终止输出流。
void close():关闭此套接字。
3 ServerSocket类
说明:ServerSocket类用来描述服务器端的套接字,创建完这个类的对象之后,那么就会等待客户端来访问,客户端有请求之后,服务器端有可能会有返回结果,有可能也会没有,具体由我们书写的代码来决定。
构造方法
ServerSocket(int port) 创建服务端对象,绑定一个端口号
参数说明:
port就是端口号
说明:创建服务器端套接字对象时必须指定端口,只有这样,客户端访问服务器端的时候才能根据指定的端口来访问。
常用方法
Socket accept() 侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
【代码实践】
构建服务端,绑定端口8888,并监控客户端的连接。
public class Demo01 {
public static void main(String[] args) throws IOException {
//ServerSocket 服务端
//1.创建服务端对象,绑定端口
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
//2.调用accept方法等待客户端的连接
System.out.println("等待客户端连接:");
Socket socket = serverSocket.accept();//阻塞,等待客户端连接
System.out.println("客户端已连接:" + socket);
}
}
}
4 TCP通信案例
代码实现客户端发送一条信息给服务端,服务端回发一条信息给客户端
通讯流程
- 创建服务器端并启动
- 创建客户端并启动【指定服务端的地址和端口】
- 服务端accept 等待接受客户端的连接
- 客户端获取网络的输出流,将数据写到网络中发送给服务端
- 服务端获取网络的输入流,将客户端发送的数据读取
- 服务端获取网络的输出流,将数据发送给客户端的
- 客户端获取网络的输入流,将服务端发送的数据读取
- 释放资源
当实现了客户端和服务端后,要先开启服务端程序。
客户端开发
客户端向服务器发送数据,并接收服务端回发的数据
开发步骤
- 创建Socket对象并指定服务端的IP和端口
- 获取网络输出流:getOutputStream
- 使用网络输出流给服务端发送一个字符串
- 当发送完数据后,告诉服务端,发送完毕:shutdownOutput()
- 获取网络输入流,读取服务端发送的数据
- 释放资源
/*
客户端: 给服务端发送一个字符串,并接收服务端回发的字符串
*/
public class Client {
public static void main(String[] args) throws IOException {
//1. 创建Socket对象并指定服务端的IP和端口
Socket socket = new Socket("127.0.0.1", 8888);
//2. 获取网络输出流:getOutputStream
OutputStream netOut = socket.getOutputStream();
//3. 使用网络输出流给服务端发送一个字符串
netOut.write("在吗,约不约!".getBytes());
//4. 当发送完数据后,告诉服务端,发送完毕:shutdownOutput()
socket.shutdownOutput();
//5. 获取网络输入流,读取服务端发送的数据:getInputStream()
InputStream netIn = socket.getInputStream();
int len;
byte[] buf = new byte[1024];
while ((len = netIn.read(buf)) != -1) {
System.out.println(new String(buf, 0, len));
}
//6. 释放资源
socket.close();
}
}
服务端开发
服务端接收客户端的连接,并获取客户端的数据,然后回发一条数据给客户端。
开发步骤
- 创建服务端ServerSocket对象,绑定端口号
- 调用accept方法,等待客户端连接。
- 当accept解阻塞,意味着有客户端连接,得到一个Socket对象
- 获取网络输入流,读取客户端发送的数据
- 获取网络输出流,写数据给客户端
- 告诉客户端,写数据完毕
- 释放资源
/*
服务端:接收客户端的信息,并回发信息给客户端
*/
public class Server {
public static void main(String[] args) throws IOException {
//1. 创建服务端ServerSocket对象,绑定端口号
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
//2. 调用accept方法,等待客户端连接。
System.out.println("等待客户端连接~~");
Socket socket = serverSocket.accept();
//3. 当accept解阻塞,意味着有客户端连接,得到一个Socket对象
System.out.println("客户端连接成功:" + socket);
//4. 获取网络输入流,读取客户端发送的数据
InputStream netIn = socket.getInputStream();
int len;
byte[] buf = new byte[1024];
while ((len = netIn.read(buf)) != -1) {
System.out.println(new String(buf, 0, len));
}
//5. 获取网络输出流,写数据给客户端
OutputStream netOut = socket.getOutputStream();
netOut.write("不约,没空,我要学习Java,等我毕业找到工作,再约!!".getBytes());
//6. 告诉客户端,写数据完毕
socket.shutdownOutput();
//7. 释放资源
socket.close();
//serverSocket.close(); //如果循环服务,服务器是不能关的
}
}
}
注意:
socket.shutdownOutput();
//该方法的执行可以让对方任务数据发送完毕,如果是循环读取会得到-1
第三章 综合案例
3.1 文件上传案例
件上传分析图解
需求:实现客户端将一个图片文件,上传到文件服务端。服务端接收到图片数据保存到本地。
客户端开发
读取本地文件,发送给服务端
开发步骤:
- 创建Socket对象指定服务端的IP和端口号
- 创建本地的输入流,关联要上传的文件
- 获取网络的输出流,用来发送文件字节数据到网络中
- 边读(读取本地文件)边写(使用网络输出流发送数据)
- 调用shutdownOutput方法告诉服务端,数据传输完毕
- 获取网络输入流,读取服务端发送的数据
- 释放客户端资源和本地流资源
public class Client {
public static void main(String[] args) throws IOException {
//1. 创建Socket对象指定服务端的IP和端口号
Socket socket = new Socket("127.0.0.1", 8888);
//2. 创建本地的输入流,关联要上传的文件
FileInputStream localIn = new FileInputStream("day12/client/美女.jpg");
//3. 获取网络的输出流,用来发送文件字节数据到网络中
OutputStream netOut = socket.getOutputStream();
//4. 边读(读取本地文件)边写(使用网络输出流发送数据)
int len;
byte[] buf = new byte[1024];
while ((len = localIn.read(buf)) != -1) {//本地流读取文件字节
netOut.write(buf, 0, len);//将读取的字节,写到网络中
}
//5. 调用shutdownOutput方法告诉服务端,数据传输完毕
socket.shutdownOutput();// 对方while循环读取的时候,就会得到-1标记
//6. 获取网络输入流,读取服务端发送的数据
InputStream netIn = socket.getInputStream();
while ((len = netIn.read(buf)) != -1) {
//System.out.println((char)len);
//System.out.println(buf,0,len);
String msg = new String(buf, 0, len);//解码
System.out.println("收到服务端的回信:" + msg);
}
//7. 释放客户端资源和本地流资源
socket.close();
localIn.close();
}
}
服务端开发
接收客户端的连接,并接收客户端上传的数据。完成之后回发一个信息
开发步骤:
- 创建服务端对象,绑定端口
- 接受客户端连接:accept方法
- 如果连接成功得到一个Socket对象,可以与客户端进行通讯
- 先网络的输入流,用来读取客户端上传的文件信息
- 创建一个本地的输出流,用来保存客户端上传的文件数据到服务端的文件
- 边读(读取网络中数据),边写(保存到本地硬盘)
- 获取网络的输出流,用来发送信息给客户端
- 告诉对方发完:shutdownOutput
- 释放资源
/*
图片上传服务端:
接收客户端的连接,并接收客户端上传的数据。完成之后回发一个信息
*/
public class Server {
public static void main(String[] args) throws IOException {
//1. 创建服务端对象,绑定端口
ServerSocket serverSocket = new ServerSocket(8888);
//2. 接受客户端连接:accept方法
System.out.println("等待客户端的连接~~");
Socket socket = serverSocket.accept();
//3. 如果连接成功得到一个Socket对象,可以与客户端进行通讯
System.out.println("客户端连接成功:" + socket);
//4. 先获取网络的输入流,用来读取客户端上传的文件信息
InputStream netIn = socket.getInputStream();
//5. 创建一个本地的输出流,用来保存客户端上传的文件数据到服务端的文件
FileOutputStream localOut = new FileOutputStream("day12/server/美女.jpg");
//6. 边读(读取网络中数据),边写(保存到本地硬盘)
int len;
byte[] buf = new byte[1024];
while ((len = netIn.read(buf)) != -1) {
localOut.write(buf, 0, len);
}
//7. 获取网络的输出流,用来发送信息给客户端
OutputStream netOut = socket.getOutputStream();
netOut.write("恭喜,文件上传成功!!".getBytes());
//8. 告诉对方发完:shutdownOutput
socket.shutdownOutput();
//9. 释放资源
socket.close();
localOut.close();
//服务端
serverSocket.close();
}
}
案例优化
以上案例存在的问题:
- 不能持续服务【采用线程池的方式优化】
- 文件会被覆盖
/*
服务端的优化:
1.循环服务
2.让文件不会覆盖
*/
public class ServerBetter2 {
public static void main(String[] args) throws IOException {
//线程池处理上传任务
//1.先创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
//2.将文件上传的业务,封装成为一个任务,提交给线程池
//1. 创建服务端对象,绑定端口
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
//2. 接受客户端连接:accept方法
System.out.println("等待客户端的连接~~");
Socket socket = serverSocket.accept();
//创建上传任务,将任务提交给线程池
threadPool.submit(new UploadTask(socket));
//服务端
//serverSocket.close();
}
}
}
class UploadTask implements Runnable {
private Socket socket;
public UploadTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
FileOutputStream localOut = null;
try {
//3. 如果连接成功得到一个Socket对象,可以与客户端进行通讯
System.out.println("客户端连接成功:" + socket);
//4. 先获取网络的输入流,用来读取客户端上传的文件信息
InputStream netIn = socket.getInputStream();
//5. 创建一个本地的输出流,用来保存客户端上传的文件数据到服务端的文件
long t = System.currentTimeMillis();
localOut = new FileOutputStream("day12/server/美女" + t + ".jpg");
//6. 边读(读取网络中数据),边写(保存到本地硬盘)
int len;
byte[] buf = new byte[1024];
while ((len = netIn.read(buf)) != -1) {
localOut.write(buf, 0, len);
}
//7. 获取网络的输出流,用来发送信息给客户端
OutputStream netOut = socket.getOutputStream();
netOut.write("恭喜,文件上传成功!!".getBytes());
//8. 告诉对方发完:shutdownOutput
socket.shutdownOutput();
//9. 释放资源
// socket.close();
//localOut.close();
} catch (Exception e) {
System.out.println("上传出现问题:" + e.getMessage());
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (localOut != null)
localOut.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.2 模拟服务器(扩展)
模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。
案例分析
我们要完成的任务:构建服务端程序,监听浏览器的请求。浏览器需要什么资源,我们服务端就给什么资源(文件)。
-
准备页面数据,web文件夹。
将今天下发的资料文件夹中的web整个文件夹复制到今天的模块中去
-
我们模拟服务器端,ServerSocket类监听端口,使用浏览器访问。浏览器在访问服务端的时候,会发送http协议数据给服务端。我们可以在里面解析出浏览器想要的文件。
服务端获取浏览器的请求数据: GET /day12/web/index.html HTTP/1.1 Host: 127.0.0.1:8888 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1
以上是协议数据,我们现在所要关心的是第一行
GET /day12/web/index.html HTTP/1.1 第一行有三部分组成,使用空格分隔 1.GET : 表示HTTP协议的一种请求方式,POST 2./day12/web/index.html :表示浏览器需要的资源【我们需要解析的】 3.HTTP/1.1:是协议的版本
我们要在程序中实现,把协议的第一行中的中间表示资源的路径解析出来。
解析如下:
//创建服务端对象,绑定端口8888 ServerSocket serverSocket = new ServerSocket(8888); //等待浏览器的连接 System.out.println("等待浏览器的访问:"); Socket socket = serverSocket.accept(); //获取浏览器传输的数据 //需要第一行:GET /day12/web/index.html HTTP/1.1的 中间路径字符串 InputStream netIn = socket.getInputStream(); //1.先把字节输入流转换为字符流 InputStreamReader isr = new InputStreamReader(netIn); //2.将字符流包装成为高效流 BufferedReader br = new BufferedReader(isr); //3.读取第一行,解析中间的资源路径 String firstLine = br.readLine(); //浏览器需要的文件路径 String path = firstLine.split(" ")[1].substring(1); System.out.println("浏览器要:" + path);
-
将浏览器所需的资源,通过网络输出流输出到网络中,写出数据时,先写协议,浏览器才能够认识。
netOut.write("HTTP/1.1 200 OK\r\n".getBytes()); netOut.write("Content-Type:text/html\r\n".getBytes()); netOut.write("\r\n".getBytes()); //第三行空行
案例实现
普通版
public class BsServer {
public static void main(String[] args) throws IOException {
//创建服务端对象,绑定端口8888
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
//等待浏览器的连接
System.out.println("等待浏览器的访问:");
Socket socket = serverSocket.accept();
//获取浏览器传输的数据
//需要第一行:GET /day12/web/index.html HTTP/1.1的 中间路径字符串
InputStream netIn = socket.getInputStream();
//1.先把字节输入流转换为字符流
InputStreamReader isr = new InputStreamReader(netIn);
//2.将字符流包装成为高效流
BufferedReader br = new BufferedReader(isr);
//3.读取第一行,解析中间的资源路径
String firstLine = br.readLine();
//浏览器需要的文件路径
String path = firstLine.split(" ")[1].substring(1);
System.out.println("浏览器要:" + path);
//将文件发送给浏览器
//1.获取网络输出流,用来发送数据给浏览器
OutputStream netOut = socket.getOutputStream();
//2.创建一个本地输入流,关联浏览器需要的文件
FileInputStream localIn = new FileInputStream(path);
//3.边读,边写
//给浏览器发送内容数据之前,需要给浏览器发送协议数据
netOut.write("HTTP/1.1 200 OK\r\n".getBytes());
netOut.write("Content-Type:text/html\r\n".getBytes());
netOut.write("\r\n".getBytes()); //第三行空行
int len;
byte[] buf = new byte[1024];
while ((len = localIn.read(buf)) != -1) {
netOut.write(buf, 0, len);
}
//4.告诉浏览器,写完了
socket.shutdownOutput();
//5.释放资源
socket.close();
//serverSocket.close(); //服务端不能关,需要重复服务
}
}
}
优化版
使用了线程池技术
public class BsServerBetter {
public static void main(String[] args) throws IOException {
//线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
//创建服务端对象,绑定端口8888
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
//等待浏览器的连接
System.out.println("等待浏览器的访问:");
Socket socket = serverSocket.accept();
threadPool.submit(new Task(socket));
//serverSocket.close(); //服务端不能关,需要重复服务
}
}
}
class Task implements Runnable {
private Socket socket;
public Task(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
FileInputStream localIn = null;
try {
//获取浏览器传输的数据
//需要第一行:GET /day12/web/index.html HTTP/1.1的 中间路径字符串
InputStream netIn = socket.getInputStream();
//1.先把字节输入流转换为字符流
InputStreamReader isr = new InputStreamReader(netIn);
//2.将字符流包装成为高效流
BufferedReader br = new BufferedReader(isr);
//3.读取第一行,解析中间的资源路径
String firstLine = br.readLine();
//浏览器需要的文件路径
String path = firstLine.split(" ")[1].substring(1);
System.out.println("浏览器要:" + path);
//将文件发送给浏览器
//1.获取网络输出流,用来发送数据给浏览器
OutputStream netOut = socket.getOutputStream();
//2.创建一个本地输入流,关联浏览器需要的文件
localIn = new FileInputStream(path);
//3.边读,边写
//给浏览器发送内容数据之前,需要给浏览器发送协议数据
netOut.write("HTTP/1.1 200 OK\r\n".getBytes());
netOut.write("Content-Type:text/html\r\n".getBytes());
netOut.write("\r\n".getBytes()); //第三行空行
int len;
byte[] buf = new byte[1024];
while ((len = localIn.read(buf)) != -1) {
netOut.write(buf, 0, len);
}
//4.告诉浏览器,写完了
socket.shutdownOutput();
} catch (Exception e) {
System.out.println("请求出错:" + e.getMessage());
}finally {
//5.释放资源
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (localIn!=null) {
localIn.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
通过上述代码我们可以书写代码模拟服务端,其实在企业真实开发过程中我们是很少自己书写代码模拟服务器的,我们使用的是第三方服务器公司给我们提供好的服务器,
我们现在企业开发中比较主流的服务器有Tomcat、JBoss、Oracle WebLogic 等。其中最为流行的服务器就是Apache 公司的Tomcat服务器。
总结
案例的总体思想,开发一个ServerSocket服务端,用来接收浏览器的请求。我们要解析浏览器的请求资源,浏览器要什么就给什么。
- 浏览器发送请求,发送的是协议数据。而浏览器所需的数据就在协议中,所以先要把协议中的资源解析出来
- 把浏览器所需的文件发送出去,发送之前需要发送三行协议
第四章 Junit单元测试
1 单元测试概述
-
Junit是什么?
Junit是Java语言编写的第三方单元测试框架(工具类),加载到我们自己的工程或者模块后,可以直接使用这些框架中定义好的类型。
-
单元测试概念
单元:单元就是人为规定的最小的被测功能模块,在Java中,一个类就是一个单元。
单元测试:对某个类中的定义的方法进行功能测试或业务逻辑测试。
-
Junit单元测试框架的作用
用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性。能够让方法独立运行起来。
在工作中完成代码开发后就可以进行单元测试,以达到使程序稳定,健壮的效果。
2 Junit使用步骤
-
编写业务类,在业务类中编写业务方法。比如增删改查的方法【被测试的代码】
-
编写测试类,在测试类中编写测试方法,在测试方法中编写测试代码来测试。【测试操作代码】
测试类
-
命名规范:以Test开头,以业务类类名结尾,使用驼峰命名法每一个单词首字母大写。
比如业务类类名:ProductDao,那么测试类类名就应该叫:TestProductDao
测试方法
-
命名规范:以test开头,以业务方法名结尾
比如业务方法名为:save,那么测试方法名就应该叫:testSave
-
方法格式
-
必须是public修饰的,没有返回值,没有参数(如果不按规则写,测试方法执行不起来)
-
必须使注解@Test修饰(这个注解来自Junit测试框架)
【固定格式】
@Test public void testXxx(){ 写测试的代码 }
初次使用时,需要将将Junit导入工程==【Alt+Enter快捷键】==,会有两个版本一个Junit4和Junit5。我们常用使用Junit4版本进行测试,选择Junit4版本。
} -
-
-
如何运行测试方法
选中方法名 --> 右键 --> Run ‘测试方法名’ 运行选中的测试方法
选中测试类类名 --> 右键 --> Run ‘测试类类名’ 运行测试类中所有测试方法
选中模块名 --> 右键 --> Run ‘All Tests’ 运行模块中的所有测试类的所有测试方法 -
如何查看测试结果
绿色:表示测试通过
红色:表示测试失败,有问题
【代码实践】
写一个Calculator类,实现加减乘除的功能。然后使用测试类对功能进行测试。
public class Calculator {
public int add(int a, int b) {
System.out.println(1/0);
return a + b;
}
}
public class TestCalculator {
@Test
public void testAdd() {
// System.out.println("Hello Test~~");
Calculator c = new Calculator();
int add = c.add(10, 10);
System.out.println("add = " + add);
}
@Test
public void test1() {
System.out.println("test1~~~");
}
}
- 把Junit4导入到模块
- IDEA导入,在写入方法后直接Alt+Enter
- 直接导入jar包
- 写测试类
- 写测试方法
- 测试方法必须是无参,无返回值,的公共方法。
- 使用@Test进行注解
3 Junit中其他注解使用
//Junit4常用注解(Junit4.xxxx版本)
@Before:用来修饰方法,该方法会在每一个测试方法执行之前执行一次。
@After:用来修饰方法,该方法会在每一个测试方法执行之后执行一次。
//以上两个注解,修饰非静态方法,每一个test方法执行,都会导致执行
@BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前执行一次。
@AfterClass:用来静态修饰方法,该方法会在所有测试方法之后执行一次。
//以上两个注解,修饰静态方法,不管有多少个测试方法,只会执行一次
//Junit5常用注解(Junit5.xxxx版本)
@BeforeEach:用来修饰方法,该方法会在每一个测试方法执行之前执行一次。
@AfterEach:用来修饰方法,该方法会在每一个测试方法执行之后执行一次。
@BeforeAll:用来静态修饰方法,该方法会在所有测试方法之前执行一次。
@AfterAll:用来静态修饰方法,该方法会在所有测试方法之后执行一次。
【示例代码】
public class TestCalculator2 {
@BeforeClass
public static void beforeClassTest() {
System.out.println("beforeClass方法执行");
}
@AfterClass
public static void afterClassTest() {
System.out.println("afterClass方法执行");
}
Calculator c;
@After
public void afterTest() {
System.out.println("after方法执行");
c = null;
}
@Before
public void beforeTest() {
System.out.println("before方法执行");
c = new Calculator();
}
@Test
public void testAdd() {
// System.out.println("Hello Test~~");
int add = c.add(10, 10);
System.out.println("add = " + add);
}
@Test
public void testSub() {
System.out.println(c.sub(10, 10));
}
}
最常用的最核心的注解:@Test
4 断言
断言作用
预先判断某个条件一定成立,如果条件不成立,则直接报错。
断言代码
//第一个参数表示期望值
//第二个参数表示实际值
//如果实际值和期望值相同,说明结果正确就测试通过,如果不相同,说明结果是错误的,就会报错
Assert.assertEquals( 期望值, 实际值);
//例如:
int result = add(100,200);
Assert.assertEquals(300, result);
代码实践
@Test
public void testMultiply() {
Calculator cal = new Calculator();
int multiply = cal.multiply(2, 2);
System.out.println("multiply = " + multiply);
//断言
Assert.assertEquals(4,multiply);
int multiply1 = cal.multiply(2, 3);
System.out.println("multiply1 = " + multiply1);
//断言
Assert.assertEquals(6,multiply1);
}
第五章 设计模式
一、 单例设计模式
正常情况下一个类可以创建多个对象
public static void main(String[] args) {
// 正常情况下一个类可以创建多个对象
Person p1 = new Person();
Person p2 = new Person();
Person p3 = new Person();
}
定义一个皇帝类,该类就应该只能存在一个对象。
1 单例设计模式的作用
单例模式,是一种常用的软件设计模式。通过单例模式可以保证系统中,应用该模式的这个类只有一个实例。即一个类只有一个对象实例。
2 单例设计模式实现步骤
- 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
- 在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型的成员变量。
- 定义一个静态方法返回这个唯一对象。
3 单例设计模式的类型
根据实例化对象的时机单例设计模式又分为以下两种:
- 饿汉单例设计模式
- 懒汉单例设计模式
4 饿汉单例设计模式
饿汉单例设计模式就是使用类的时候已经将对象创建完毕,不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故被称为“饿汉模式”。
代码如下:
public class Singleton {
// 1.将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
private Singleton() {}
// 2.在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型的成员变量。
private static final Singleton instance = new Singleton();
// 3.定义一个静态方法返回这个唯一对象。
public static Singleton getInstance() {
return instance;
}
}
需求:定义一个皇帝类,要求对象只能存在一个。
public class Demo01 {
public static void main(String[] args) {
//new King("亚历山大二世");
//获取皇帝对象
King k1 = King.getInstance();
System.out.println("k1 = " + k1);
for (int i = 0; i < 10; i++) {
System.out.println("King.getInstance() = " + King.getInstance());
}
}
}
//定义一个皇帝类,要求对象只能存在一个。
class King {
String name;
//1. 私有化构造方法
private King(String name) {
this.name = name;
}
//2.提供一个静态字段,用来保存一个对象
private static King king = new King("亚历山大二世");
//3.提供一个公共的静态方法,用来获取该字段
public static King getInstance() {
return king;
}
}
5 懒汉单例设计模式
懒汉单例设计模式就是调用getInstance()方法时实例才被创建,先不急着实例化出对象,等要用的时候才实例化出对象。不着急,故称为“懒汉模式”。
代码如下:
public class Singleton {
// 2.在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型的成员变量。
private static Singleton instance;
// 1.将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
private Singleton() {}
// 3.定义一个静态方法返回这个唯一对象。要用的时候才例化出对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
注意:懒汉单例设计模式在多线程环境下可能会实例化出多个对象,不能保证单例的状态,所以加上关键字:synchronized,保证其同步安全。
需求:使用懒汉单例,实现皇帝类的单例模式
public class Demo02 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
HuangDi hd = HuangDi.getInstance();
System.out.println("hd = " + hd);
}
}
}
//懒汉单例
class HuangDi {
String name;
// 1. 私有化构造方法
private HuangDi(String name) {
this.name = name;
}
//2. 静态字段定义,不赋值【不着急】
private static HuangDi huangDi;
//3.定义方法获取单例
public static synchronized HuangDi getInstance() {
if (huangDi == null) {
huangDi = new HuangDi("秦始皇");
}
return huangDi;
}
}
6 小结
单例模式可以保证系统中一个类只有一个对象实例。
实现单例模式的步骤:
- 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
- 在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型的成员变量。
- 定义一个静态方法返回这个唯一对象。
二、多例设计模式
1.多例设计模式的作用
多例模式,是一种常用的软件设计模式。通过多例模式可以保证系统中,应用该模式的类有固定数量的实例。多例类要自我创建并管理自己的实例,还要向外界提供获取本类实例的方法。
使用场景:线程池
线程池 = Executors.newFixedThreadPool(3);
2.实现步骤
1.创建一个类, 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
2.在类中定义该类被创建对象的总数量
3.在类中定义存放类实例的list集合
4.在类中提供静态代码块,在静态代码块中创建类的实例
5.提供获取类实例的静态方法
3.实现代码
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Multition {
// 定义该类被创建的总数量
private static final int maxCount = 3;
// 定义存放类实例的list集合
private static List instanceList = new ArrayList();
// 构造方法私有化,不允许外界创建本类对象
private Multition() {
}
static {
// 创建本类的多个实例,并存放到list集合中
for (int i = 0; i < maxCount; i++) {
Multition multition = new Multition();
instanceList.add(multition);
}
}
// 给外界提供一个获取类对象的方法
public static Multition getMultition(){
Random random = new Random();
// 生成一个随机数
int i = random.nextInt(maxCount);
// 从list集合中随机取出一个进行使用
return (Multition)instanceList.get(i);
}
}
4.测试结果
public static void main(String[] args) {
// 编写一个循环从中获取类对象
for (int i = 0; i < 10; i++) {
Multition multition = Multition.getMultition();
System.out.println(multition);
}
}
需求:
定义一个类,用来描述公司的公用车。固定只能有3辆车。使用多例模式进行设计该类。
- 定义静态字段,用来设定实例个数
- 定义静态的集合,用来保存多例
- 私有化构造方法
- 静态代码块中进行对多例初始化
- 提供静态方法将某个实例返回
public class Demo01 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Car car = Car.getInstance();
System.out.println("car = " + car);
}
}
}
//定义一个类,用来描述公司的车。固定只能有3辆车。使用多例模式进行设计该类。
class Car {
//1. 定义静态字段,用来设定实例个数
//2. 定义静态的集合,用来保存多例
//3. 私有化构造方法
private static int count = 3;
private static ArrayList<Car> carList = new ArrayList<>();
private Car() {
}
//静态代码块
//4. 静态代码块中进行对多例初始化
static {
for (int i = 0; i < count; i++) {
carList.add(new Car());
}
}
//5. 提供静态方法将某个实例返回
public static Car getInstance() {
int index = new Random().nextInt(count);
Car car = carList.get(index);
return car;
}
}
car = _12多例设计模式.Car@14ae5a5
car = _12多例设计模式.Car@7f31245a
car = _12多例设计模式.Car@14ae5a5
car = _12多例设计模式.Car@14ae5a5
car = _12多例设计模式.Car@7f31245a
car = _12多例设计模式.Car@14ae5a5
car = _12多例设计模式.Car@14ae5a5
car = _12多例设计模式.Car@6d6f6e28
car = _12多例设计模式.Car@14ae5a5
car = _12多例设计模式.Car@6d6f6e28
5.小结
多例模式可以保证系统中一个类有固定个数的实例, 在实现需求的基础上, 能够提高实例的复用性.
实现多例模式的步骤:
- 创建一个类, 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
- 在类中定义该类被创建的总数量
- 在类中定义存放类实例的list集合
- 在类中提供静态代码块,在静态代码块中创建类的实例
- 提供获取类实例的静态方法
枚举,也是一种多例模式。
三、 工厂设计模式
1概述
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。之前我们创建类对象时, 都是使用new 对象的形式创建, 除new 对象方式以外, 工厂模式也可以创建对象.
2作用
解决类与类之间的耦合问题
3实现步骤
- 编写一个Car接口, 提供run方法
- 编写一个Falali类实现Car接口,重写run方法
- 编写一个Benchi类实现Car接口
- 提供一个CarFactory(汽车工厂),用于生产汽车对象
- 定义CarFactoryTest测试汽车工厂
4实现代码
1.编写一个Car接口, 提供run方法
public interface Car {
public void run();
}
2.编写一个Falali类实现Car接口,重写run方法
public class Falali implements Car {
@Override
public void run() {
System.out.println("法拉利以每小时500公里的速度在奔跑.....");
}
}
3.编写一个Benchi类实现Car接口
public class Benchi implements Car {
@Override
public void run() {
System.out.println("奔驰汽车以每秒1米的速度在挪动.....");
}
}
4.提供一个CarFactory(汽车工厂),用于生产汽车对象
public class CarFactory {
/**
* @param id : 车的标识
* benchi : 代表需要创建Benchi类对象
* falali : 代表需要创建Falali类对象
* 如果传入的车标识不正确,代表当前工厂生成不了当前车对象,则返回null
* @return
*/
public Car createCar(String id){
if("falali".equals(id)){
return new Falali();
}else if("benchi".equals(id)){
return new Benchi();
}
return null;
}
}
5.定义CarFactoryTest测试汽车工厂
public class CarFactoryTest {
public static void main(String[] args) {
CarFactory carFactory = new CarFactory();
Car benchi = carFactory.createCar("benchi");
benchi.run();
Car falali = carFactory.createCar("falali");
falali.run();
}
}
5小结
工厂模式的存在可以改变创建类的方式,解决类与类之间的耦合.
实现步骤:
- 编写一个Car接口, 提供run方法
- 编写一个Falali类实现Car接口,重写run方法
- 编写一个Benchi类实现Car接口
- 提供一个CarFactory(汽车工厂),用于生产汽车对象
)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。之前我们创建类对象时, 都是使用new 对象的形式创建, 除new 对象方式以外, 工厂模式也可以创建对象.