文件与流
什么是文件?
文件可认为是相关记录或放在一起的数据的集合
JAVA程序如何访问文件属性?
java.io.File 该类用于描述文件系统中的一个文件或目录
1.可以访问文件或目录的属性信息
2.可以访问文件或目录中的所有子文件
3.操作文件或目录 但不能访问文件数据
File file =new File("文件所在路径");
相对路径:
./表示当前路径
../表示上一级路径
其中当前路径:默认情况下,java.io 包中的类总是根据当前用户目录来分析相对路径名。
此目录由系统属性 user.dir 指定,通常是 Java 虚拟机的调用目录。”
绝对路径:
绝对路径名是完整的路径名,不需要任何其他信息就可以定位自身表示的文件
(1)文件检测相关方法
boolean isDirectory():判断File对象是不是目录
boolean isFile():判断File对象是不是文件
boolean exists():判断File对象对应的文件或目录是不是存在
(2)文件操作的相关方法
boolean createNewFile():路径名指定的文件不存在时,创建一个新的空文件
boolean delete():删除File对象对应的文件或目录
(3)目录操作的相关方法
boolean mkdir():单层创建空文件夹
boolean mkdirs():多层创建文件夹
File[] listFiles():返回File对象表示的路径下的所有文件对象数组
(4)访问文件相关方法
String getName():获得文件或目录的名字
String getAbsolutePath():获得文件目录的绝对路径
String getParent():获得对象对应的目录的父级目录
long lastModified():获得文件或目录的最后修改时间
long length() :获得文件内容的长度
File类 创建目录
public class MyFile {
public static void main(String[] args) {
//创建一个目录
File file1 = new File("d:\\test");
//判断对象是不是目录
System.out.println("是目录吗?"+file1.isDirectory());
//判断对象是不是文件
System.out.println("是文件吗?"+file1.isFile());
//获得目录名
System.out.println("名称:"+file1.getName());
//获得相对路径
System.out.println("相对路径:"+file1.getPath());
//获得绝对路径
System.out.println("绝对路径:"+file1.getAbsolutePath());
//最后修改时间
System.out.println("修改时间:"+file1.lastModified());
//文件大小
System.out.println("文件大小:"+file1.length());
}
}
程序运行结果
是目录吗?false
是文件吗?false
名称:test
相对路径:d:\test
绝对路径:d:\test
修改时间:0
文件大小:0
file1对象是目录啊,怎么在判断“是不是目录”时输出了false呢?
这是因为只是创建了代表他是目录的对象,还没有真正的创建,这时候要用到mkdirs()方法,如下:
public class MyFile {
public static void main(String[] args) {
//创建一个目录
File file1 = new File("d:\\test\\test");
file1.mkdirs();//创建了多级目录
//判断对象是不是目录
System.out.println("是目录吗?"+file1.isDirectory());
}
}
创建文件
public class MyFile {
public static void main(String[] args) {
//创建一个文件对象
File file1 = new File("d:\\a.txt");
try {
file1.createNewFile();//创建真正的文件
} catch (IOException e) {
e.printStackTrace();
}
//判断对象是不是文件
System.out.println("是文件吗?"+file1.isFile());
}
}
文件是存放在文件夹下的,所以应该先创建文件夹,后创建文件,如下:
public class MyFile {
public static void main(String[] args) {
//代表一个文件夹对象,单层的创建
File f3 = new File("d:/test");
File f4 = new File("d:/test/a.txt");
f3.mkdir();//先创建文件夹,才能在文件夹下创建文件
try {
f4.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
//代表一个文件夹对象,多层的创建
File f5= new File("d:/test/test/test");
File f6 = new File("d:/test/test/test/a.txt");
f5.mkdirs();//多层创建
try {
f6.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件的逐层读取
File的listFiles()只能列出当前文件夹下的文件及目录,
那么其子目录下的文件及目录该如何获取呢?解决的办法有很多,在这运用递归解决.
//逐层获取文件及目录
public class TestFindFile {
public static void openAll(File f) {// 递归的实现
File[] arr = f.listFiles();// 先列出当前文件夹下的文件及目录
for (File ff : arr) {
if (ff.isDirectory()) {// 列出的东西是目录吗
System.out.println(ff.getName());
openAll(ff);// 是就继续获得子文件夹,执行操作
} else {
// 不是就把文件名输出
System.out.println(ff.getName());
}
}
}
public static void main(String[] args) {
File file = new File("d:/test");// 创建目录对象
openAll(file);// 打开目录下的所有文件及文件夹
}
}
IO的概念
Java的IO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,
在Java中把不同的输入/输出源抽象表述为"流"。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。
即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
流有输入和输出,输入时是流从数据源流向程序。输出时是流从程序传向数据源,而数据源可以是内存,文件,网络或程序等。
IO流的抽象基类
根据流的流向以及操作的数据单元不同,将流分为了四种类型,每种类型对应一种抽象基类。
这四种抽象基类分别为:InputStream,Reader,OutputStream以及Writer。
四种基类下,对应不同的实现类,具有不同的特性。在这些实现类中,又可以分为节点流和处理流。
下面就是整个由着四大基类支撑下,整个IO流的框架图。
FileInputStream
文件字节输入流,是一个低级流。可以从指定文件中读取字节
FileInputStream fis =new FileInputStream();
构造器通常有两种形式
1.new FileInputStream(File file);
2.new FileInputStream(String fileName);
常用方法:
int read( )
int read(byte[] b)
int read(byte[] b,int off,int len)
void close( )
单个字节读取文件
public static void main(String[] args) throws IOException {
// write your code here
File file = new File("d:/我的青春谁做主.txt");
FileInputStream fileInputStream = new FileInputStream(file);
byte[] buffer = new byte[100];//保存从磁盘读到的字节
int index = 0;
int content = fileInputStream.read();//读文件中的一个字节
while (content != -1)//文件中还有内容,没有读完
{
buffer[index] = (byte)content;
index++;
//读文件中的下一个字节
content = fileInputStream.read();
}
//此时buffer数组中读到了文件的所有字节
String string = new String(buffer,0, index);
System.out.println(string);
}
批量读取
public static void main(String[] args) throws IOException {
// write your code here
File file = new File("d:/我的青春谁做主.txt");
FileInputStream fileInputStream = new FileInputStream(file);
byte[] buffer = new byte[SIZE];//保存从磁盘读到的字节
int len = fileInputStream.read(buffer);//第一次读文件中的100个字节
while (len != -1)
{
String string = new String(buffer,0, len);
System.out.println(string);
//读下一批字节
len = fileInputStream.read(buffer);
}
}
向文件中写入文本
public class FileOutputStreamTest {
public static void main(String[] args) {
FileOutputStream fos=null;
try {
String str ="好好学习Java";
byte[] words = str.getBytes();
fos = new FileOutputStream("D:\\myDoc\\hello.txt");
fos.write(words, 0, words.length);
System.out.println("hello文件已更新!");
}catch (IOException obj) {
System.out.println("创建文件时出错!");
}finally{
try{
if(fos!=null)
fos.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileOutputStream
文件字节输出流,是一个低级流。用于向文件中写出字节。
FileOutputStream fos =new FileOutputStream();
FileOutputStream (File file)
FileOutputStream(String name)
FileOutputStream(String name,boolean append)
1、前两种构造方法在向文件写数据时将覆盖文件中原有的内容
2、创建FileOutputStream实例时,如果相应的文件并不存在,则会自动创建一个空的文件
3、该构造方法会传入两个参数,一个为文件名 ,另一个是boolean值,为true时
则具有追加写入的操作功能,写入的内容会被追加到文件的末尾
常用方法:
void write(int c)
void write(byte[] buf)
void write(byte[] b,int off,int len)
void close( )
文件复制
文件“我的青春谁做主.txt”位于D盘根目录下,要求将此文件的内容复制到
C:\myFile\my Prime.txt中
实现思路
创建文件“D:\我的青春谁做主.txt”并自行输入内容
创建C:\myFile的目录。
创建输入流FileInputStream对象,负责对D:\我的青春谁做主.txt文件的读取。
创建输出流FileOutputStream对象,负责将文件内容写入到C:\myFile\my Prime.txt中。
创建中转站数组words,存放每次读取的内容。
通过循环实现文件读写。
关闭输入流、输出流
public static void main(String[] args) throws IOException {
//创建E:\myFile的目录。
File folder = new File("E:\\myFile");
if (!folder.exists())//判断目录是否存在
{
folder.mkdirs();//如果不存在,创建该目录
}
//创建输入流
File fileInput = new File("d:/b.rar");
FileInputStream fileInputStream = new FileInputStream(fileInput);
//创建输出流
FileOutputStream fileOutputStream = new FileOutputStream("E:\\myFile\\c.rar");
//创建中转站数组buffer,存放每次读取的内容
byte[] buffer = new byte[1000];
//从fileInputStream(d:/我的青春谁做主.txt)中读100个字节到buffer中
int length = fileInputStream.read(buffer);
int count = 0;
while (length != -1) //这次读到了至少一个字节
{
System.out.println(length);
//将buffer中内容写入到输出流(E:\myFile\myPrime.txt)
fileOutputStream.write(buffer,0,length);
//继续从输入流中读取下一批字节
length = fileInputStream.read(buffer);
count++;
}
System.out.println(count);
fileInputStream.close();
fileOutputStream.close();
}
缓冲流 是一对高级流 可以提高读写效率
FileInputStream fis = new FileInputStream(fileInput);
BufferedInputStream bis =new BufferedInputStream(fis);
FileOutputStream fos = new FileOutputStream("E:\\myFile\\c.rar");
BufferedOutputStream bos=new BufferedOutputStream(fos);
int d=-1;
while( (d=bis.read()) !=-1){
bos.write(d);
bos.flush();
}
bis.close();
bos.close();
缓冲流内部维护了一个缓冲区,当调用read()读取一个字节时
实际上缓冲流会让fis对象读取一组字节并存入到自身内部的字节数组中
然后将第一个字节返回 再次调用read()时 缓冲流会直接将数组中第二个字节返回
以此类推
实际上还是通过提高每次读取的数据量来减少读取的次数
从而提高读取的效率
通过缓冲输出流写出的字节并不会立刻被写入文件
会先存入其内部的字节数组 直到该数组存满了
才会一次性写出所有的数据
flush() 该方法可以强制将缓冲区内已有的数据一次性写出
这样可以提高及时性
字节流与字符流
字节流和字符流和用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同。
字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。
本质其实就是基于字节流读取时,去查了指定的码表。字节流和字符流的区别:
(1)读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
(2)处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。
(3)字符流虽然是以字符为单位,但是底层实际上还是以字节形式读写 所有字符流具备字符与字节相互转换的能力
只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。
Java网络编程
计算机网络概述
网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输。
按照计算机网络的定义,通过一定的物理设备将处于不同位置的计算机连接起来组成的网络,
这个网络中包含的设备有:计算机、路由器、交换机等等。
其实从软件编程的角度来说,对于物理设备的理解不需要很深刻,
就像你打电话时不需要很熟悉通信网络的底层实现是一样的,
但是当深入到网络编程的底层时,这些基础知识是必须要补的。
路由器和交换机组成了核心的计算机网络,计算机只是这个网络上的节点以及控制等,通过光纤、网线等连接将设备连接起来,从而形成了一张巨大的计算机网络。
网络最主要的优势在于共享:共享设备和数据,现在共享设备最常见的是打印机,一个公司一般一个打印机即可,共享数据就是将大量的数据存储在一组机器中,其它的计算机通过网络访问这些数据,例如网站、银行服务器等等。
如果需要了解更多的网络硬件基础知识,可以阅读《计算机网络》教材,对于基础进行强化,这个在基础学习阶段不是必须的,但是如果想在网络编程领域有所造诣,则是一个必须的基本功。
对于网络编程来说,最主要的是计算机和计算机之间的通信,这样首要的问题就是如何找到网络上的计算机呢?这就需要了解IP地址的概念。
为了能够方便的识别网络上的每个设备,网络中的每个设备都会有一个唯一的数字标识,这个就是IP地址。在计算机网络中,现在命名IP地址的规定是IPv4协议,该协议规定每个IP地址由4个0-255之间的数字组成,例如10.0.120.34。每个接入网络的计算机都拥有唯一的IP地址,这个IP地址可能是固定的,例如网络上各种各样的服务器,也可以是动态的,例如使用ADSL拨号上网的宽带用户,无论以何种方式获得或是否是固定的,每个计算机在联网以后都拥有一个唯一的合法IP地址,就像每个手机号码一样。
但是由于IP地址不容易记忆,所以为了方便记忆,有创造了另外一个概念——域名(Domain Name),例如sohu.com等。一个IP地址可以对应多个域名,一个域名只能对应一个IP地址。域名的概念可以类比手机中的通讯簿,由于手机号码不方便记忆,所以添加一个姓名标识号码,在实际拨打电话时可以选择该姓名,然后拨打即可。
在网络中传输的数据,全部是以IP地址作为地址标识,所以在实际传输数据以前需要将域名转换为IP地址,实现这种功能的服务器称之为DNS服务器,也就是通俗的说法叫做域名解析。例如当用户在浏览器输入域名时,浏览器首先请求DNS服务器,将域名转换为IP地址,然后将转换后的IP地址反馈给浏览器,然后再进行实际的数据传输。
当DNS服务器正常工作时,使用IP地址或域名都可以很方便的找到计算机网络中的某个设备,例如服务器计算机。当DNS不正常工作时,只能通过IP地址访问该设备。所以IP地址的使用要比域名通用一些。
IP地址和域名很好的解决了在网络中找到一个计算机的问题,但是为了让一个计算机可以同时运行多个网络程序,就引入了另外一个概念——端口(port)。
在介绍端口的概念以前,首先来看一个例子,一般一个公司前台会有一个电话,每个员工会有一个分机,这样如果需要找到这个员工的话,需要首先拨打前台总机,然后转该分机号即可。这样减少了公司的开销,也方便了每个员工。在该示例中前台总机的电话号码就相当于IP地址,而每个员工的分机号就相当于端口。
有了端口的概念以后,在同一个计算机中每个程序对应唯一的端口,这样一个计算机上就可以通过端口区分发送给每个端口的数据了,换句话说,也就是一个计算机上可以并发运行多个网络程序,而不会在互相之间产生干扰。
在硬件上规定,端口的号码必须位于0-65535之间,每个端口唯一的对应一个网络程序,一个网络程序可以使用多个端口。这样一个网络程序运行在一台计算上时,不管是客户端还是服务器,都是至少占用一个端口进行网络通讯。在接收数据时,首先发送给对应的计算机,然后计算机根据端口把数据转发给对应的程序。
有了IP地址和端口的概念以后,在进行网络通讯交换时,就可以通过IP地址查找到该台计算机,然后通过端口标识这台计算机上的一个唯一的程序。这样就可以进行网络数据的交换了。
网络编程概述
按照前面的介绍,网络编程就是两个或多个设备之间的数据交换,其实更具体的说,
网络编程就是两个或多个程序之间的数据交换,和普通的单机程序相比,
网络程序最大的不同就是需要交换数据的程序运行在不同的计算机上,这样就造成了数据交换的复杂。
虽然通过IP地址和端口可以找到网络上运行的一个程序,但是如果需要进行网络编程,则还需要了解网络通讯的过程
网络通讯基于“请求-响应”模型。为了理解这个模型,先来看一个例子,
经常看电视的人肯定见过审讯的场面吧,一般是这样的:
警察:姓名
嫌疑犯:XXX
警察:性别
嫌疑犯:男
警察:年龄
嫌疑犯:29
……
在这个例子中,警察问一句,嫌疑犯回答一句,如果警察不问,则嫌疑犯保持沉默。
这种一问一答的形式就是网络中的“请求-响应”模型。也就是通讯的一端发送数据,
另外一端反馈数据,网络通讯都基于该模型。
在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,
而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。
一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
由此,网络编程中的两种程序就分别是客户端和服务器端,
例如QQ程序,每个QQ用户安装的都是QQ客户端程序,而QQ服务器端程序则运行在腾讯公司的机房中,
为大量的QQ用户提供服务。这种网络编程的结构被称作客户端/服务器结构,也叫做Client/Server结构,简称C/S结构。
使用C/S结 构的程序,在开发时需要分别开发客户端和服务器端,这种结构的优势在于由于客户端是专门开发的,
所以根据需要实现各种效果,专业点说就是表现力丰富,而服务器端也需要专门进行开发。但是这种结构也存在着很多不足,
例如通用性差,几乎不能通用等,也就是说一种程序的客户端只能和对应的服务器端通讯,
而不能和 其它服务器端通讯,在实际维护时,也需要维护专门的客户端和服务器端,维护的压力比较大。
其实在运行很多程序时,没有必要使用专用的客户端,而需要使用通用的客户端
例如浏览器,使用浏览器作为客户端的结构被称作浏览器/服务器结构,
也叫做Browser/Server结构,简称为B/S结构。
使用B/S结构的程序,在开发时只需要开发服务器端即可,这种结构的优势在于开发的压力比较小,
不需要维护客户端。但是这种结构也存在着很多不足,例如浏览器的限制比较大,表现力不强,无法进行系统级操作等。
总之C/S结构和B/S结构是现在网络编程中常见的两种结构,B/S结构其实也就是一种特殊的C/S结构。
网络通信方式
在现有的网络中,网络通讯的方式主要有两种:
1、 TCP(传输控制协议)方式
2、 UDP(用户数据报协议)方式
在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,
然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。
而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,
如果发送失败则客户端无法获得。
这两种传输方式都是实际的网络编程中进行使用,重要的数据一般使用TCP方式进行数据传输
而大量的非核心数据则都通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据的传递。
由于TCP需要建立专用的虚拟连接以及确认传输是否正确,
所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。
Java网络编程技术
在Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.Net.Socket类代表客户端连接,
以java.net.ServerSocket类代表服务器端连接。在进行网络编程时,底层网络通讯的细节已经实现了比较高的封装,
所以在程序员实际编程时,只需要指定IP地址和端口号码就可以建立连接了。
正是由于这种高度的封装,一方面简化了Java语言网络编程的难度,
另外也使得使用Java语言进行网络编程时无法深入到网络的底层,所以使用Java语言进行网络底层系统编程很困难,
具体点说,Java语言无法实现底层的网络嗅探以及获得IP包结构等信息。
但是由于Java语言的网络编程比较简单,所以还是获得了广泛的使用。
在客户端网络编程中,首先需要建立连接,在Java API中以java.net.Socket类的对象代表网络连接,
所以建立客户端网络连接,也就是创建Socket类型的对象,该对象代表网络连接,示例如下:
Socket socket1 = new Socket(“192.168.1.103”,10000);
Socket socket2 = new Socket(“www.sohu.com”,80);
上面的代码中,socket1实现的是连接到IP地址是192.168.1.103的计算机的10000号端口,
而socket2实现的是连接到域名是www.sohu.com的计算机的80号端口,至于底层网络如何实现建立连接,
对于程序员来说是完全透明的。如果建立连接时,本机网络不通,或服务器端程序未开启,则会抛出异常。
连接一旦建立,则完成了客户端编程的第一步,紧接着的步骤就是按照“请求-响应”模型进行网络数据交换,
在Java语言中,数据传输功能由Java IO实现,也就是说只需要从连接中获得输入流和输出流即可,
然后将需要发送的数据写入连接对象的输出流中,在发送完成以后从输入流中读取数据即可。示例代码如下:
OutputStream os = socket1.getOutputStream(); //获得输出流
InputStream is = socket1.getInputStream(); //获得输入流
最后当数据交换完成以后,关闭网络连接,释放网络连接占用的系统端口和内存等资源,完成网络操作,示例代码如下:
socket1.close();
客户端
下面是一个简单的网络客户端程序示例,该程序的作用是向服务器端发送一个字符串“Hello”,
并将服务器端的反馈显示到控制台,数据交换只进行一次,当数据交换进行完成以后关闭网络连接,
程序结束。实现的代码如下:
package tcp;
import java.io.*;
import java.net.*;
/**
* 简单的Socket客户端
* 功能为:发送字符串“Hello”到服务器端,并打印出服务器端的反馈
*/
public class SimpleSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
//服务器端IP地址
String serverIP = "127.0.0.1";
//服务器端端口号
int port = 10000;
//发送内容
String data = "Hello";
try {
//建立连接
socket = new Socket(serverIP,port);
//发送数据
os = socket.getOutputStream();
os.write(data.getBytes());
//接收数据
is = socket.getInputStream();
byte[] b = new byte[1024];
int n = is.read(b);
//输出反馈数据
System.out.println("服务器反馈:" + new String(b,0,n));
} catch (Exception e) {
e.printStackTrace(); //打印异常信息
}finally{
try {
//关闭流和连接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
在该示例代码中建立了一个连接到IP地址为127.0.0.1,端口号码为10000的TCP类型的网络连接,然后获得连接的输出流对象,
将需要发送的字符串“Hello”转换为byte数组写入到输出流中,由系统自动完成将输出流中的数据发送出去,
如果需要强制发送,可以调用输出流对象中的flush方法实现。在数据发送出去以后,
从连接对象的输入流中读取服务器端的反馈信息,
读取时可以使用IO中的各种读取方法进行读取,这里使用最简单的方法进行读取,
从输入流中读取到的内容就是服务器端的反馈,并将读取到的内容在客户端的控制台进行输出,
最后依次关闭打开的流对象和网络连接对象。
服务端
在服务器端程序编程中,由于服务器端实现的是被动等待连接,所以服务器端编程的第一个步骤是监听端口,
也就是监听是否有客户端连接到达。实现服务器端监听的代码为:
ServerSocket ss = new ServerSocket(10000);
该代码实现的功能是监听当前计算机的10000号端口,
如果在执行该代码时,10000号端口已经被别的程序占用,那么将抛出异常。否则将实现监听。
服务器端编程的第二个步骤是获得连接。该步骤的作用是当有客户端连接到达时,
建立一个和客户端连接对应的Socket连 接对象,从而释放客户端连接对于服务器端端口的占用。
Socket socket = ss.accept();
最后,在服务器端通信完成以后,关闭服务器端连接。实现的代码为:
ss.close();
package tcp;
import java.io.*;
import java.net.*;
/**
* echo服务器
* 功能:将客户端发送的内容反馈给客户端
*/
public class SimpleSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//监听端口号
int port = 10000;
try {
//建立连接
serverSocket = new ServerSocket(port);
//获得连接
socket = serverSocket.accept();
//接收客户端发送内容
is = socket.getInputStream();
byte[] b = new byte[1024];
int n = is.read(b);
//输出
System.out.println("客户端发送内容为:" + new String(b,0,n));
//向客户端发送反馈内容
os = socket.getOutputStream();
os.write(b, 0, n);
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//关闭流和连接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
在该示例代码中建立了一个监听当前计算机10000号端口的服务器端Socket连接,然后获得客户端发送过来的连接,
如果有连接到达时,读取连接中发送过来的内容,并将发送的内容在控制台进行输出,
输出完成以后将客户端发送的内容再反馈给客户端。最后关闭流和连接对象,结束程序。
如何复用Socket连接?
在前面的示例中,客户端中建立了一次连接,只发送一次数据就关闭了,
这就相当于拨打电话时,电话打通了只对话一次就关闭了,
其实更加常用的应该是拨通一次电话以后多次对话,这就是复用客户端连接
解决:建立连接以后,将数据交换的逻辑写到一个循环中就可以了。这样只要循环不结束则连接就不会被关闭
package tcp;
import java.io.*;
import java.net.*;
/**
* 复用连接的echo服务器
* 功能:将客户端发送的内容反馈给客户端
*/
public class MulSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//监听端口号
int port = 10000;
try {
//建立连接
serverSocket = new ServerSocket(port);
System.out.println("服务器已启动:");
//获得连接
socket = serverSocket.accept();
//初始化流
is = socket.getInputStream();
os = socket.getOutputStream();
byte[] b = new byte[1024];
for(int i = 0;i < 3;i++){
int n = is.read(b);
//输出
System.out.println("客户端发送内容为:" + new String(b,0,n));
//向客户端发送反馈内容
os.write(b, 0, n);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//关闭流和连接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
如何使服务器端支持多个客户端同时工作?
前面介绍的服务器端程序,只是实现了概念上的服务器端,离实际的服务器端程序结构距离还很遥远,
如果需要让服务器端能够实际使用,那么最需要解决的问题就是——如何支持多个客户端同时工作。
一个服务器端一般都需要同时为多个客户端提供通讯,如果需要同时支持多个客户端,则必须使用前面介绍的线程的概念
简单来说,也就是当服务器端接收到一个连接时,启动一个专门的线程处理和该客户端的通讯。
按照这个思路改写的服务端示例程序将由两个部分组成,MulThreadSocketServer类实现服务器端控制,
实现接收客户端连接,然后开启专门的逻辑线程处理该连接,LogicThread类实现对于一个客户端连接的逻辑处理,
将处理的逻辑放置在该类的run方法中。该示例的代码实现为:
package tcp;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 支持多客户端的服务器端实现
*/
public class MulThreadSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
//监听端口号
int port = 10000;
try {
//建立连接
serverSocket = new ServerSocket(port);
System.out.println("服务器已启动:");
while(true){
//获得连接
socket = serverSocket.accept();
//启动线程
new LogicThread(socket);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//关闭连接
serverSocket.close();
}catch(Exception e){}
}
}
}
在该示例代码中,实现了一个while形式的死循环,由于accept方法是阻塞方法,所以当客户端连接未到达时,
将阻塞该程序的执行,当客户端到达时接收该连接,并启动一个新的LogicThread线程处理该连接,
然后按照循环的执行流程,继续等待下一个客户端连接。这样当任何一个客户端连接到达时,
都开启一个专门的线程处理,通过多个线程支持多个客户端同时处理。
下面再看一下LogicThread线程类的源代码实现:
package tcp;
import java.io.*;
import java.net.*;
/**
* 服务器端逻辑线程
*/
public class LogicThread extends Thread {
Socket socket;
InputStream is;
OutputStream os;
public LogicThread(Socket socket){
this.socket = socket;
start(); //启动线程
}
public void run(){
try{
//初始化流
os = socket.getOutputStream();
is = socket.getInputStream();
for(int i = 0;i < 3;i++){
byte[] b = new byte[1024];
//读取数据
int n = is.read(b);
//反馈数据
os.write(b);
}
}catch(Exception e){
e.printStackTrace();
}finally{
close();
}
}
/**
* 关闭流和连接
*/
private void close(){
try{
//关闭流和连接
os.close();
is.close();
socket.close();
}catch(Exception e){}
}
}
多线程
理解程序、进程、线程的概念
程序可以理解为静态的代码
进程可以理解为执行中的程序
线程可以理解为进程的进一步细分,程序的一条执行路径
使用多线程的优点
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
实现多线程的方式
继续Thread类
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name=name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
}
}
程序启动运行main时候,java虚拟机启动一个进程,
主线程main在main()调用时候被创建。
随着调用Thread1的两个对象的start方法,另外两个线程也启动了,
这样,整个应用就在多线程下运行。
以下是关系到线程运行状态的几个方法:
1)start方法
start()用来启动一个线程,当调用start方法后,
系统才会开启一个新的线程来执行用户定义的子任务,
在这个过程中,会为相应的线程分配需要的资源。
2)run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,
当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。
注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
实现Runnable接口
用Runnable也是非常常见的一种,我们只需要重写run方法即可。下面也来看个实例
class Thread2 implements Runnable{
private String name;
public Thread2(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
new Thread(new Thread2("C")).start();
new Thread(new Thread2("D")).start();
}
}
Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。
run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。
Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象
然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。
因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,
最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
Thread和Runnable的区别
//模拟火车站售票窗口,开启三个窗口售票,总票数为100张
//存在线程的安全问题
class Window extends Thread {
int ticket = 100;
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售票,票号为:"+ ticket--);
} else {
break;
}
}
}
}
public class TestWindow {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window implements Runnable {
int ticket = 100;//要将全局变量声明为静态,不然每个对象都有这个属性,会卖出300张票
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:"+ ticket--);
} else {
break;
}
}
}
}
public class Main {
//模拟火车站售票窗口,开启三个窗口售票,总票数为100张
//存在线程的安全问题
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1, "t1");
Thread t2 = new Thread(w1, "t2");
Thread t3 = new Thread(w1, "t3");
t1.start();
t2.start();
t3.start();
}
}
线程的生命周期
新建(NEW):新创建了一个线程对象。
可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程调度
1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。
millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。
sleep()平台移植性好。
3、线程等待:Object类中的wait()方法,导致当前的线程等待,
直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。
4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,
把执行机会让给相同或者更高优先级的线程。
5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,
则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
sleep()和yield()的区别
sleep()和yield()的区别):sleep()使当前线程进入停滞状态,
所以执行sleep()的线程在指定的时间内肯定不会被执行;
yield()只是使当前线程重新回到可执行状态,
所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,
这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,
但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:
先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,
否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
线程的同步
1、线程安全问题存在的原因:
由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。
2、如何解决线程安全问题
必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。
3、java如何实现线程安全:线程的同步机制
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码块(即为操作共享数据的代码)
}
1、共享数据:多个线程共同操作的同一个数据(变量)
2、同步监视器:由任何一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁
注:在实现Runnable接口的方式中,考虑同步的话,可以使用this来充当锁。但是在继承的方式中,慎用this
class Window2 implements Runnable {
int ticket = 1000;// 共享数据
public void run() {
while (true) {
synchronized (this) {//this表示当前对象,本题中即为w
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "售票,票号为:" + ticket--);
}
}
}
}
}
public class TestWindow2 {
public static void main(String[] args) {
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
方式二:同步方法
将操作共享数据的方法声明为synchronized. 即此方法为同步方法,能够保证当其中一个线程执行此方法时,其他线程在外等待直至此线程执行完此方法。
同步方法的锁:this(不用显式的写)
class Window4 implements Runnable {
int ticket = 1000;// 共享数据
public void run() {
while (true) {
show();
}
}
public synchronized void show() {
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为:"
+ ticket--);
}
}
}
public class TestWindow4 {
public static void main(String[] args) {
Window4 w = new Window4();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
释放锁的操作
当前线程的同步方法、同步代码块执行结束
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程的死锁问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,
都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
解决方法:专门的算法、原则;尽量减少同步资源的定义
/死锁的问题:处理线程同步时容易出现。
//不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
//写代码时,要避免死锁!
public class TestDeadLock {
static StringBuilder sb1 = new StringBuilder();
static StringBuilder sb2 = new StringBuilder();
public static void main(String[] args) {
new Thread() {
public void run() {
synchronized (sb1) {
try {
Thread.currentThread().sleep(10);//问题放大
} catch (InterruptedException e) {
e.printStackTrace();
}
sb1.append("A");
synchronized (sb2) {
sb2.append("B");
System.out.println(sb1);
System.out.println(sb2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (sb2) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sb1.append("C");
synchronized (sb1) {
sb2.append("D");
System.out.println(sb1);
System.out.println(sb2);
}
}
}
}.start();
}
}
线程通信
wait() 与 notify() 和 notifyAll()
wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,
而当前线程排队等候再次对资源的访问
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,
以便其他正在等待此锁的线程可以得到同步锁并运行,
只有其他线程调用了notify方法(notify并不释放锁,
只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,
但不是马上得到锁,因为锁还在别人手里,别人还没释放),
调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,
程序如果可以再次得到锁,就可以继续向下运行。
1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
2)当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞,
这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,
这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁
3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,
如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;
(notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)
4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程,
唤醒的线程获得锁的概率是随机的,取决于cpu调度
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,
店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;
如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
分析:
1、是否涉及到多线程的问题?是!生产者、消费者
2、是否涉及到共享数据?有!考虑线程安全问题
3、此共享数据是谁?产品的数量
4、是否涉及到线程的通信呢?存在生产者与消费者的通信
class Clerk{//店员
int product;
public synchronized void addProduct(){//生产产品
if(product >= 20){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else{
product++;
System.out.println(Thread.currentThread().getName() + ":生产了第" + product + "个产品");
notifyAll();
}
}
public synchronized void consumeProduct(){//消费产品
if(product <= 0){
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else{
System.out.println(Thread.currentThread().getName() + ":消费了第" + product + "个产品");
product--;
notifyAll();
}
}
}
class Producer implements Runnable{//生产者
Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
public void run(){
System.out.println("生产者开始生产产品");
while(true){
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Consumer implements Runnable{//消费者
Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
public void run(){
System.out.println("消费者消费产品");
while(true){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class TestProduceConsume {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Consumer c1 = new Consumer(clerk);
Thread t1 = new Thread(p1);//一个生产者的线程
Thread t3 = new Thread(p1);
Thread t2 = new Thread(c1);//一个消费者的线程
t1.setName("生产者1");
t2.setName("消费者1");
t3.setName("生产者2");
t1.start();
t2.start();
t3.start();
}
}