文章目录
程序就是对象的集合
五. IO流
File类的使用
在高中的时候,我考虑过聊天机器人的技术。我当时想,如果机器人无法记住我们之前的对话,那么我们跟机器人的对话就没有积累,每次都是新的开始。但是程序关掉之后,那些对话全都消失了,有没有什么办法可以记录下来呢。当时的我没有办法解决这个问题
File类:使用File类对象表示一个文件或者文件夹,并对其进行操作。但是,涉及到写入或者读取文件内容的操作时,需要IO流来完成
持久化内存技术
持久化指的就是当程序退出后,其中的数据能够保留下来,供程序再次运行的时候使用
File类的实例化
java.io.File
提供了三种构造器
(1)带参构造器,绝对路径或者相对路径
路径中的每级目录之间用一个路径分隔符隔开
windows系统,DOS系统:使用反斜杠
UNIX系统:使用斜杠/
java支持跨平台,使用时要注意
File类提供了一个静态常量作为分隔符,public static final String separater
File file1 = new File( pathname: "hello.txt");//相对于当前module
File file2 = new File( pathname:"D: \\workspace_idea1\\JavaSenior\\daye8\\he.txt");
File file2 = new File("d:" + File.separator + "atguigu” File.separator + "infd.txt" );
(2)带参构造器,父目录路径,子目录路径
File file3 = new File( parent: "D:\\workspace_idea1", child: "JavaSenior");
(3)带参构造器,File类对象,路径
File file3 = new File( parent: "D:\\workspace_idea1", child: "JavaSenior");
File file4 = new File(file3,child: "hi.txt" );
File类实例化之后,如果直接输出对象,输出的是传入构造器参数的拼接,最终指向文件或者文件夹
并且实例化还只是内存层面,不涉及到持久化内存
File类常用方法
File类的获取功能
public String getAbsolutePath():获取绝对路径
public String getPath():获取路径
public String getName():获取名称
public String getParent():获取上层文件目录路径。若无,返回null
public long length():获取文件长度(即:字节数)。不能获取目录的长度。
public long lastModified():获取最后一次的修改时间,毫秒值
如下两个方法适用于文件目录(文件目录就是文件夹)
public String[] list():获取指定目录下的所有文件或者文件目录的名称数组,带后缀的文件名
public File[] listFiles():获取指定目录下的所有文件或者文件目录的File数组,文件的绝对路径
使用不同的构造器实例化对象,那么上面某些函数的结果就会不一样
(1)实例化传入相对路径,那么getParent()的结果就是null
(2)实例化传入绝对路径,那么getParent()的结果就是去掉绝对路径的最后一个目录
public booleanrenameTo(File dest):把文件重命名为指定的文件路径
File file1 = new File( pathname: "hello.txt" );
File file2 = new File( pathname: "D: llio\\hi.txt" );
boolean renameTo = file1.renameTo(file2);
system.out.println(renameTo);
所谓的重命名,其真实过程是这样的:将f1重命名为f2
(1)首先保证需要file1在硬盘中是存在的,且file2不能在硬盘中存在
(2)创建f2
(3)将f1的内容写到f2里面
(4)删除f1
File类的判断方法
public boolean isDirectory():判断是否是文件目录
public boolean isFile():判断是否是文件
public boolean exists() :判断是否存在
public boolean canRead() :判断是否可读
public boolean canWrite():判断是否可写
public boolean isHidden() :判断是否隐藏
前三个用于判断的方法非常常用
File类的创建功能
在硬盘中创建文件或者文件夹
public boolean createNewFile():创建文件。若文件存在,则不创建,返回false
public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建
File类的删除功能
public boolean delete():删除文件或者文件夹删除
注意事项:
Java中的删除不走回收站。
要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录,文件目录必须是空的
内存结构
当硬盘中真有一个真实的文件或目录存在时,创建File对象时,各个属性会显式赋值。
当硬盘中没有真实的文件或目录对应时,那么创建对象时,除了指定的目录和路径之外,其他的属性都是取成员变量的默认值。
IO流原理及流的分类
背景
I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。
在java程序中,数据以流的形式输入输出
java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
核心
文件输入以字符流方式,那么输出也必须是字符流方式;输入以字节流方式,输出也必须是字节流方式
java IO原理
输入输出是相对概念,我们是站在程序,也是内存的这一侧看待输入输出
流的分类
字节流(8bit)(图片,视频,非文本数据),字符流(16bit)
输入流,输出流
节点流(最底层的流),处理流(修饰底层流的流)
java的IO流共涉及40多个类,但都是基于4个抽象基类
子类都会以其继承的父类名作为后缀
除了第二行是节点流,之后的每一行都是处理流
节点流又叫做文件流
节点流(文件流)
FileReader子类
注意在方法中创建File对象和在main中不同,相对路径的上一级目录不同
public static void main( string[] args) i
File file = new File( pathname: "hello.txt");//相较于当前工程
File file1 = new File( pathname: "day09\\hello.txt" );
System.out.println(file1.getAbsolutePath());
@Test
public void testFileReader(){
File file = new File( pathname: "hello.txt");//相较于当前ModuLe
}
将文件的内容读入内存并输出到控制台
说明点:
(1)read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1。本质上每次使用read(),就会和硬盘交互一次
(2)异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理3.读入的文件一定要存在,否则就会报FiLeNotFoundException。
public void testFileReader() throws IOException {
//1.实例化FiLe类的对象,指明要操作的文件
File file = new File( pathname: "hello.txt");//相较于当前ModuLe
//2.提供具体的流,字符输入节点流
FileReader fr = new FileReader(file);
//3.数据的读入
//read():返回读入的一个字符。如果达到文件末尾,返回-1
int data = fr.read();
while(data != -1){
system.out.print1n((char)data);
data = fr.read();
}
//流的关闭
//JVM对其他物理连接,比如数据库连接、输入流输出流、Socket连接无能为力
fr.close();
}
read()的工作方式和Iterator接口的next()很像,我估计底层也是用了指针
流的关闭是必须要执行的,而此方法选择throws的方式抓住异常,这样就导致流没有关闭。将throws改成try-catch模型
public void testFileReader(){
FileReader fr = null;
try {
File f = new File("src\\hello.txt");
fr = new FileReader(f);
int data;
while((data = fr.read()) != -1){
System.out.print((char)data);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
fr.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这里我自己写的时候出现了一个问题,查了好久。就是节点流引用的创建要放在try外面,因为一旦出现异常,跳出try之后,节点流引用就丢失了,想去关闭节点流会找不到这个引用
加强版的read(),read(char[] cbuf):返回每次读入cbuf数组中的字符的个数。并且将取到的字符存入cbuf数组。如果达到文件末尾,返回-1
char[] cbuf = new char[5];//这里要定好char数组的长度,表示预期每次读入多少个字符
int len;
while((len = fr.read(cbuf)) != -1){
for(int i = 0;i < len;i++) syso(cbuf[i]);
//或者说可以采用char型数组和String数组的转换
String s = new String(cbuf,0,len);//从下标0开始,取len个元素
syso(s);
}
FileWriter子类
//1.提供File类的对象,指明写出到的文件
File file = new File( pathname: "he11o1.txt" );
//2.提供Filewriter的对象,用于数据的写出
Filewriter fw = new Filewriter(file);
//3.写出的操作
fw.write( str: "I have a dream! " );
//4.流资源的关闭
fw.close();
如果没有这个文件,可以先创建文件。File类有用于创建文件的方法,public boolean createNewFile()
但是在这里,FileWriter子类会自己创建还不存在的文件,所以不必担心
FileWriter子类还有一个重载的构造器,多了一个append参数,表示保留文件的原有内容,在后面添加新的内容,而非替换掉旧文件
使用FileReader子类和FileWriter子类实现文本复制
//1.创建File类的对象,指明读入和写出的文件
File srcFile = new File( pathname: "hello.txt" );
File destFile = new File( pathname: "hello2.txt" );
//2.创建输入流和输出流的对象
FileReader fr = new FileReader(srcFile) ;
Filewriter fw = new Filewriter(destFile);
//3.数据的读入和写出操作
char[]cbuf = new char[5];
int len;//记录每次读入到cbuf数组中的字符的个数
while( ( len = fr.read(cbuf)) != -1){
//每次写出Len个字符
fw.write(cbufl off: 8,len) ;
}
//4.关闭输入输出流资源
这四步就是IO流的核心框架
字符流不能处理图片文件
图片文件属于字节数据,都是0101的,字符流是不能用来处理字节数据的。python要打开一张图片倒是非常的简单,opencv就能轻松搞定
节点流有四种,其中两种是字节流,两种是字符流
FileInputStream子类和FileOutputStream子类
//1.造文件
File file = new File(pathname: "hello.txt" );
//2.造流
FileInputstream fis = new FileInputStream(file);
//3.读数据
byte[] buffer = new byte[5];
int len;//记录每次读取的字节的个数
while( ( len = fis.read(buffer)) != -1){
string str = new string( buffer, offset: 0,len);
system.out.print(str);
}
//4.关闭资源
fis.close();
用字节流对象读取文件,那么就造一个字节类型的数组byte[]
程序的结果和使用字符流读取的结果是一样的。按理说,字符是占两个字节,用字节流应该出现乱码。但实际上,英文字母,数字就只用了一个字节来存储。如果文件中加入中文这种一个字符占四个字节的数据,出现乱码就在情理之中了
对于纯文本文件(.txt .java .c .cpp),使用字符流处理
对于非纯文本文件(.jpg .mp3 .mp4 .avi .doc),使用字节流处理
核心步骤和字符流那里是一样的:
1.创建File类的对象,指明读入和写出的文件
2.创建输入流和输出流的对象
3.数据的读入和写出操作
4.关闭流资源
缓冲流(处理流)
BufferInputStream, BufferOutputStream, BufferReader, BufferWriter四个子类
核心:提升文件的读写速率
使用缓冲流加速非文本文件的复制
//1.造文件
File fi = new File("C:\\Users\\JJCale\\Desktop\\mingtian.jpg");
File fo = new File("C:\\Users\\JJCale\\Desktop\\mingtian2.jpg");
//2.造流
FileInputStream fis = new FileInputStream(fi);
FileOutputStream fos = new FileOutputStream(fo);
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
//3.复制
byte[] b = new byte[1024];
int len;
while((len = bis.read(b)) != -1){
bos.write(b, 0, len);
}
System.out.println("复制成功");
//4.关闭流资源
bos.close();
bis.close();
fos.close();
fis.close();
说明:外层流关闭的时候,会把传入的内层流自动关闭
缓冲流加速的原理
其内部提供了一个类变量缓冲区:private static int DEFAULT_BUFFER_SIZE = 8192;
读取的文件数据先存到缓冲区,等到存满了再刷新缓冲区
使用缓冲流复制文本文件
BufferReader子类又多了一个read系列的方法,可以实现按行读取文本文件
/方式二:使用string
string data;
while((data = br.readLine()) != null){
bw.write(data); //data中不包含换行符
bw.newLine();//增加每行的换行符
}
因为readLine方法不包含换行符,所以在输出的时候还需要补上换行符,BufferWriter提供了newLine方法以实现这一点
总结:
转换流
转换流提供了在字节流和字符流之间的转换
也能实现文件以不同编码集的持久化存储
有InputStreamReader和OutputStreamWriter两个子类
从见名生意的角度看,OutputStreamWriter看起来很像是从字节流转换到字符流,但实际上它是从字符流到字节流
这张图有些反直觉的地方。在之前,进行文本文件的复制是不涉及到编码集的。一个文本文件的输入和输出都应该用字符流来处理。输入使用节点流里边的输入字符流FileReader,输出使用节点流里边的输出字符流FileWriter
我感觉这个图可以做得更细致一点,按照标准的四个步骤:
首先,文件在底层是二进制形式,对于纯文本文件,以字符流的方式读取;否则以字节流的方式读取。上图左分支相当于先用字节流读取,再将结果转换为字符流;等价于直接用字符流读取。这也符合对纯文本文件的读取方式
右边分支先将文件设置为数据以字节流方式写入,再将文件设置为字符流方式写入
实现文本文件以不同编码集的复制
//1.造文件、造流
File file1 = new File( pathname: "dbcp.txt" ) ;
File file2 = new File( pathname: "dbcp_gbk.txt" );
FileInputStream fis = new FileInputStream(file1);
Fileoutputstream fos = new Fileoutputstream(file2);
InputstreamReader isr = new InputStceamReader(fis,".utf.8".);
outputstreamwriter osw = new OutputStreamWriter(fos,"gbk");
//2.读写过程
char[ ] cbuf = new char[20];
int len;
while((len = isr.cead(cbuf)) != -1){
osw. write(cbuf,0,len);
}
//3.关闭资源
I
isr.close();
osw.close();
标准输入输出流
数据流
对象流
随机存储文件流
NIO.2中Path,Paths,Files类的使用
网络编程
概述
Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在Java的本机安装系统里,由JVM进行控制。并且Java实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境。
网络编程的目的
直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯
两个主要问题
(1)定位:如何准确地定位网络上一台或多台主机;定位主机上的特定的应用
(2)高效传输:找到主机后如何可靠高效地进行数据传输
网络通信要素
IP和网络端口
网络协议
OSI参考模型(七层,物联网淑慧试用,不用这个),TCP/IP参考模型(四层,国际标准)
计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。
计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量控制,路由控制,如何实现如此复杂的网络协议呢?
通信要素1:IP和网络端口
IP地址
IP地址很抽象,采用域名来定位一台或多台主机
本地回环地址:127.0.0.1,对应的域名localhost。访问本机上的服务器,那就填这个IP地址。比如说在本机上装MySQL数据服务器
InetAddress类
在Java中使用InetAddress类对象代表一台或多台主机
创建主机是通过调用InetAddress类的getByName方法来实现的
InetAddress对象可以通过两个方法getHostName方法,getHostAddress方法分别获取主机域名和主机IP地址
InetAddress i = InetAddress.getByName("192.168.10.14");
getByName方法中的参数是主机名,可以将对象i视作IP地址为192.168.10.14的主机
也可以在getByName方法中填入域名,输出结果除了域名,还有DNS解析出来的IP地址
InetAddress i1 = InetAddress.getByName("www.baidu.com");
本机IP地址使用也很频繁,InetAddress类提供了获取本机地址的静态方法
InetAddress i2 = InetAddress.getLocalHost();
输出i2的结果是,本机自定义主机名+局域网地址(私网地址)DESKTOP-…/192.168.256.256
网络端口
端口号标识正在计算机上运行的进程(程序)
一个16位的整数0~65535
端口分类
公认端口:~1023。被预先定义的服务通信占用(如:HTTP占用端口80,FTP占巾端口21,Telnet占用端口23)
注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。可以改,比如启用两个Tomcat
动态/私有端口:49152~65535。
端口号与IP地址的组合得出一个网络套接字:Socket,一个socket可以定位一台主机上的一个应用。因此网络编程也叫作socket编程。socket,直译就是插座的意思,最先采用这个词的人,觉得网络连接,就像插口和插座的关系
Socket是一个类
通信要素2:网络协议
TCP/IP协议将网络分为四层:物理层+数据链路层,网络层,传输层,应用层(会话层+表示层+应用层)
传输层有两个重要的协议:
(1)传输控制协议TCP(Transmission Control Protocol)
(2)用户数据报协议UDP(User Datagram Protocol)。
TCP/IP协议簇 = 传输层的TCP + 网络层的IP协议(网络互联协议)
TCP协议
(1)使用TCP协议前,须先建立TCP连接,形成传输数据通道
(2)传输前,采用***“三次握手”方式,点对点通信,是可靠的
SYN同步序列编号,ACK确认字符。
简单说一下,seq号是随机的,ACK = 收到的seq + 1
(3)TCP协议进行通信的两个应用进程***:客户端、服务端。
(4)在连接中可进行大数据量的传输
(5)传输完毕,需释放已建立的连接,效率低
我感觉步骤三的ack应该大写,结束连接的时候ACK = 收到的seq
(1)客户端A发送一个FIN,用为关团客户A到服务器B的数据传送。
(2) 服务器B收到这个FIN,它发回一个ACK,确认序号为收到序号加1。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。
类似于打电话,专用线路
UDP协议
(1)将数据、源、目的封装成数据包,不需要建立连接
(2)每个数据报的大小限制在64K内
(3)发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
(4)可以广播发送
(5)发送数据结束时无需释放资源,开销小,速度快
应用:看视频(丢数据无所谓,但不能卡顿),类似于发短信
TCP网络编程
客户端的创建
我之前认为客户端和服务器端都应该是类,使用的时候都应该实例化。那要真是这样,他们的属性和方法应该是什么呢?直觉上这些东西很难固定下来。另外,客户端和服务器还有需要一直在线的需求,如果使用对象,该怎么实现这种需求呢??
1.创建Socket对象,指明服务器端的ip和端口号。
Socket是一个类,这一点没有提到。使用Socket类对象表示客户端。造好客户端之后,就要
尝试连接服务器,看看目标主机上哪个程序的端口号是目标端口号
2.获取一个输出流,用于输出数据。
但是这个输出字节流的创建方式和之前很不一样。先是创建了一个抽象基类的对象,然后通过调
用Socket对象的getOutputStream方法来实例化
3.写出数据的操作
这里通过客户端向服务器发送了字符串,但是输出流用的是字节流,所以对字符串调用了
getBytes方法将字符串改写成字节流的形式(有点稀里糊涂)
4.资源的关闭
我没看出来TCP在哪里,是封装到什么位置了吗?是由Socket对象去实现了TCP连接吗??感觉上是的,Socket对象定位了服务器的位置,但他怎么知道是TCP连接呢???
步骤2还有个反直觉的地方,在IO流一章,我们是将数据输出到文件中,所以节点流需要传入文件,然后再用节点流对象去不停地往文件写入数据。而这里是直接用输出流写数据。注意这里的区别,前者用的是节点流,后者是输出流
//客户端
@Test
public void client() {
Socket socket = null;
OutputStream os = null;
try {
//1.创建Socket对象,指明服务器端的ip和端口号
InetAddress inet = InetAddress.getByName("192.168.14.100");
socket = new Socket(inet,8899);
//2.获取一个输出流,用于输出数据
os = socket.getOutputStream();
//3.写出数据的操作
os.write("你好,我是客户端mm".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.资源的关闭
服务器的创建
1.创建服务器端的ServerSocket,指明自己的端口号。
服务器使用ServerSocket类对象来表示,对客户端只需要说明自己的端口是多少
2.调用accept()表示接收来自于客户端的socket
服务器需要去搜索试图连接自己的客户端,调用accept方法搜索用Socket类对象表示的客户端
3.获取输入流
找到客户端之后,需要接收客户端的输出流。创建输入流,使用抽象基类中的输入字节流
InputStream类。调用客户端的getInputStream方法创建输入流
4.读取输入流中的数据
客户端输出的是字节数组,输入的时候使用处理流里边的访问数组输出字节流
ByteArrayOutputStream类
5.关闭资源
ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
//1.创建服务器端的ServerSocket,指明自己的端口号
ss = new ServerSocket(8899);
//2.调用accept()表示接收来自于客户端的socket
socket = ss.accept();
//3.获取输入流
is = socket.getInputStream();
//不建议这样写,可能会有乱码
// byte[] buffer = new byte[1024];
// int len;
// while((len = is.read(buffer)) != -1){
// String str = new String(buffer,0,len);
// System.out.print(str);
// }
//4.读取输入流中的数据
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[5];
int len;
while((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");
} catch (IOException e) {
e.printStackTrace();
} finally {
整体来看ServerSocket对象服务器只需要搜索Socket对象客户端,建立TCP连接。流的输入输出都是客户端来负责
总结
对于上面的过程我又几个疑问
(1)虽然我知道先启动服务器,再启动客户端,但是两端有很多指令,这些指令的执行顺序是什么????
【1】创建服务器端socket
【2】服务器socket监听连接,等待来自客户端socket的连接请求
【3】创建客户端socket。这个创建就是请求与服务器socket进行连接,不需要显式地发起什么连接
【4】创建流以及服务器和客户端进行数据交换所需要的语句
(2)没有看到TCP???
A:Socket类内部实现了TCP服务,这些是固定的内容,被封装隐藏了
(3)在上例中,客户端里的输出流和服务器端的输入流都是用socket创建,和之前IO流那一章不同,输入是从文件输入,输出是向文件输出,这一点很反直觉???
A:在客户端的编写中,我们看到数据来自于自定义的一个字节数组,socket创建的输出流调用了write方法,将字节数组传了进去,自然而然就会想数据输出到哪里去了。可以这样讲,TCP通信要对socket进行读写,也就是说数据跟随socket
(4)客户端和服务器都有一个while循环,客户端的输出和服务器的输入的执行顺序是什么???
A:回想IO流那一章,在复制文件这一例子中,如果只使用节点流,每输入一次内存,就要读一次硬盘;每输出一次内存,就要写一次硬盘。不是说全读到内存,再一次性写到硬盘。回到上例的情况,数据一开始就在内存,客户端socket每输出一部分数据,服务器socket就要输入一部分数据,不是说客户端socket一次性把数据读完,然后服务器那边一次性输入
事实上,客户端socket读一段数据,就要给服务器发过去,服务器收到后再写到终端上
(5)如果要实现交互需要注意什么???
A:到例题2回答
例题
需求一:客户端发送文件给服务端,服务端将文件保存在本地
输入流需要输入本地的文件,然后客户端将其写入输出流对象
public void client() throws IOException {
//1.
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
//2.
OutputStream os = socket.getOutputStream();
//3.
FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
//4.
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
//5.
fis.close();
os.close();
socket.close();
}
public void server() throws IOException {
//1.
ServerSocket ss = new ServerSocket(9090);
//2.
Socket socket = ss.accept();
//3.
InputStream is = socket.getInputStream();
//4.
FileOutputStream fos = new FileOutputStream(new File("beauty1.jpg"));
//5.
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
//6.
fos.close();
is.close();
socket.close();
ss.close();
这里再总结一下,在IO流那一章,对于复制文件这一需求,数据的输入和输出分别由输入流和输出流对象来负责,数据的来源是传入输入流对象的文件。然后,输入流读取一部分数据,这部分数据用变量buffer来接收,输出流就向输出文件写入读到的那部分数据。输出流的数据来源是输入流,输入流的数据来源是文件
在这里,对于发送数据这个需求,客户端造了一个输出流来写数据,数据由客户端提供。那么这个数据写到哪里去了呢???至少是跟着客户端走的吧,可能,可能是存到客户端的输出流???!然后在服务器端这一边,客户端又造了一个输入流,那么这个输入流的数据来源呢???来自于客户端的输出流。然后单独又造了一个输出流,由这个输出流输出客户端的输入流读到的数据。输出到哪里去呢,没有输出文件作为去向啊???只能说存到输出流了
对于发送文件这个需求,综上,其实就是数据的初始来源发生了变化,去向也发生了变化,最终写到文件里了
需求二:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端,并关闭相应的连接。
需求分析:服务器也可以创建输出流??并不是!如前所说,由客户端来管理输入流输出流,socket读写
public void client(){
//1.
Socket socket = null;
//2.
OutputStream os = null;
//3.
FileInputStream fis = null;
InputStream is = null;
ByteArrayOutputStream bos = null;
try {
socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
os = socket.getOutputStream();
fis = new FileInputStream(new File("C:\\Users\\JJCale\\Desktop\\123.mp4"));
//4.
byte[] buffer = new byte[1024];
int len;
System.out.println("客户端开始输出");
while((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
Date d = new Date();
System.out.println("客户端输出了一句********" + d.toString());
}
System.out.println("客户端全部发完了,看到我就没有被阻塞");
is = socket.getInputStream();
System.out.println("客户端准备好接收来自服务器的信息");
bos = new ByteArrayOutputStream();
byte[] bufferr = new byte[20];
int len1;
System.out.println("马上开始接收");
while((len1 = is.read(bufferr)) != -1){
System.out.println("完成一次读取");
bos.write(bufferr, 0 , len1);
}
System.out.println(bos.toString());
public void server(){
//1.
ServerSocket ss = null;
//2.
Socket socket = null;
//3.
InputStream is = null;
//4.
FileOutputStream fos = null;
OutputStream os = null;
try {
ss = new ServerSocket(9090);
System.out.println("搜索客户端");
socket = ss.accept();
is = socket.getInputStream();
fos = new FileOutputStream(new File("C:\\Users\\JJCale\\Desktop\\133.mp4"));
//5.
byte[] buffer = new byte[1024];
int len;
System.out.println("服务器开始输出");
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
Date d = new Date();
System.out.println("服务器收到了一句" + d.toString());
}
System.out.println("服务器全部收完了,看到我就没有被阻塞");
os = socket.getOutputStream();
os.write("服务器已经收到了".getBytes());
测试一:在例题一的基础上,无论是客户端还是服务器,while循环之后,都是可以打印语句的
测试二:在例题二,加上服务器向客户端发送数据,客户端和服务器都阻塞在while循环
通过debug,测试一的执行顺序是要等到客户端socket创建的输出流释放之后才执行服务器端while循环之后的打印指令。我之前其实一直没搞清楚客户端,服务器中每条指令的执行顺序,但我又认为很重要
事实上,有了前面debug的结果,测试二就比较好解释了,我有理由认为,如果客户端socket创建的输出流这一资源不释放,那么,服务器是无法像客户端传输数据的
上面这一说法接近答案了,但并不准确。要解释测试二,需要一个新的知识
TCP的连接与关闭
TCP 是双向的,比如客户端到服务器端的方向,指的是客户端通过套接字接口,向服务器端发送 TCP 报文;而服务器端到客户端方向则是另一个传输方向。在绝大多数情况下,TCP 连接都是先关闭一个方向,此时另外一个方向还是可以正常进行数据传输。
是否可以这样认为,客户端socket创建的输出流规定了TCP连接的方向是从客户端到服务器??!!
如果上述猜想成立,那么为什么是由socket调用shutdown方法,而不是客户端socket创建的输出流调用close方法??
A:关闭连接的方法有两种:
(1)close方法:IO流对象可以调用他,表示释放资源;socket对象也可以调用他,表示TCP两个方向的数据流彻底关掉tcp
socket对象调用close往往是在最后。close方法的缺陷在于不能关闭TCP连接的一个方向
close 函数如何关闭两个方向的数据流呢?
在输入方向, 系统内核会将该套接字设置为不可读,任何读操作都会返回异常;
在输出方向, 系统内核 尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文, 接下来如果再对该套接字进行写操作会返回异常。
(2)shutdown方法:IO流对象不能调用这个方法,socket对象能调用。java提供了两个很清晰的方法shutdownInput(),
shutdownOutput()分别关闭TCP连接的一个方向
补充:socket的底层设计原理
这个部分等到java web去解决
浏览器访问Tomcat服务器
之前听说过tomcat,觉得这个名字好奇怪
客户端:(1)自定义,比如说最简单地写了个函数,可以当做服务器。(2)浏览器
服务器:(1)自定义。比如把自己的计算机当作服务器(2)Tomcat服务器
UDP网络编程
概述
类DatagramSocket和 DatagramPacket实现了基于UDP协议网络程序。
UDP数据报通过数据报套接字DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。
A:TCP通信就不同,服务器监听网络,客户端去找目标IP地址和端口号。似乎客户端的IP地址和端口号没有显示,但是建立TCP连接的时候,通信两端的IP地址和端口号都确定下来了。UDP不建立连接,所以发出的数据需要定位发送端和接收端,就像快递一样
UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样。
UDP通信不像TCP通信,要求发送数据的双方扮演不同的角色,客户端和服务器,客户端负责进行服务的请求,而服务器进行请求的响应。只是将两者区分为发送者和接受者
例子
//发送端
@Test
public void sender() throws IOException {
DatagramSocket socket = new DatagramSocket();
String str = "我是UDP方式发送的导弹";
byte[] data = str.getBytes();
InetAddress inet = InetAddress.getLocalHost();
DatagramPacket packet = new DatagramPacket(data,0,data.length,inet,9090);
socket.send(packet);
socket.close();
}
//接收端
@Test
public void receiver() throws IOException {
DatagramSocket socket = new DatagramSocket(9090);
//buffer用来接收数据,实例化DatagramPacket对象的时候也要用
byte[] buffer = new byte[100];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
//接收数据,将数据存到DatagramPacket对象,实际上就是存到上面的buffer中
socket.receive(packet);
System.out.println(new String(packet.getData(),0,packet.getLength()));
socket.close();
}
感觉没有体现出DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号,没有看到发送者的IP地址和端口号,和TCP通信一样
URL编程
URL(Uniform Resource Locator):统一资源定位符,它表示Internet上某一资源的地址.也就是种子
java提供了一个URL类来进行URL编程
从服务器中下载资源
HttpURLConnection urlConnection = null;
InputStream is = null;
FileOutputStream fos = null;
try {
URL url = new URL("http://localhost:8080/examples/beauty.jpg");
//这两步用来连接服务器,其实和TCP,UDP通信非常类似
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.connect();
is = urlConnection.getInputStream();
fos = new FileOutputStream("day10\\beauty3.jpg");
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("下载完成");
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(urlConnection != null){
urlConnection.disconnect();
}
}