java学习第三周(初步学习Java高级知识)

day1-IO流(上)

  • 流概述

  • 流的分类

  • 文件拷贝

  • 缓冲流

  • 转换流

  • 打印流

流概述

在java中,将不同的输入输出源通过流的形式进行相关操作(输入,输出),流是一种抽象描述,在程序中表示数据的一种转移方式

 

流的分类

jdk中提供了各种不同的流用于处理不同的输入输出源,根据流性质划分分为以下类型:

  • 按流向分(站在程序角度考虑

    • 输入流(input)

    • 输出流(output)

  • 按类型分:

    • 字节流(InputStream/OutputStream)

    • 字符流(Reader/Writer)

  • 按功能分:

    • 节点流(低级流:直接跟输入输出源对接)

      • FileInputStream/FileOutputStream/FileReader/FileWriter/PrintStream/PrintWriter...

    • 处理流(高级流:建立在低级流的基础上)

      • 转换流

      • 缓冲流

其中InputStream/OutputStream是所有字节流的顶层父类,是抽象类,类中提供了一系列用于对输入输出源的字节操作;Reader/Writer是所有字符流的顶层父类,是抽象类,类中提供了基于字符的方式操作输入输出源的方法

规律

几乎所有的字节流都是以Stream结尾;几乎所有的字符流都是以Reader/Writer结尾

 

文件拷贝

文件拷贝的原理即:将一个源文件(标准文件)拷贝到指定目录中,通过输入流获取源文件的输入流,再通过获取目标文件的输出流,完成读写过程,如下:

 

public class FileCopy {
    
    /**
     * 完成文件拷贝
     * @param source    源文件
     * @param targetDir 目标目录
     */
    public void copy(File source,File targetDir){
        //根据指定的目录以及源文件名称构建新的file对象
        File target = new File(targetDir,source.getName());
        InputStream is = null;
        OutputStream os = null;
        try {
            //创建源文件的输入流
            is = new FileInputStream(source);
            //创建目标文件的输出流
            os = new FileOutputStream(target);
            //声明字节缓冲区
            byte[] b = new byte[1024];
            //声明临时变量存储每次读取的真实长度
            int len = 0;
            System.out.println("开始拷贝...");
            while((len = is.read(b)) != -1){
                //将读取到源文件的字节信息通过目标文件输出流写入到目标文件
                os.write(b,0,len);
            }
            System.out.println("拷贝完成!");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            try {
                //确保流对象不为空时关闭资源
                if(os != null){
                    os.close();
                }
                if(is != null){
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
    }
    
    public static void main(String[] args) {
        //源文件
        File f1 = new File("D:\\素材\\视频\\larva搞笑虫子\\1.mp4");
        //目标目录
        File f2 = new File("D:\\video"); //d:/video/1.mp4
        //开始拷贝
        new FileCopy().copy(f1, f2);
    }
​
}

注意:

不论是纯文本文件(文本文档,html文件)或是二进制文件(音频,视频,图片,等)都可以通过字节流完成拷贝,但是不允许使用字符流对二进制文件操作,否则,会导致文件格式破坏从而无法正常执行。

缓冲流

由于节点流操作输入输出源的效率相对较低,因此jdk提供了一些用于提高输入输出效率的流(缓冲流),这些流通过装饰器模式(java常用23种设计模式之一),提供对节点流的包装,同时这些高级流也支持mark和reset操作,并且缓冲流中也提供了节点流中没有的方法,比如,BufferedWriter提供newLine()方法用于输出一个换行标记;BufferedReader提供readLine()方法用于读取一行文本内容(以换行标记为结束)。IO包中的缓冲流主要包含以下类型:

  • BufferedInputStream/BufferedOutputStream

  • BufferedReader/BufferedWriter

public class BufferedReaderDemo {
    public static void main(String[] args) throws IOException {
        
        File file = new File("99.txt");
        Reader reader = new FileReader(file);
        //包装节点流,提高读取效率
        BufferedReader br = new BufferedReader(reader);
        String s = "";
        while((s = br.readLine()) != null){
            System.out.println(s);
        }
        br.close();
    }
}

转换流

在实际项目中经常会遇到一些特殊需求,比如,通过网络获取了InputStream对象,以及流中存储的均为字符数据,此时使用字符流(Reader)读取会更为合适,但是由于两种流类型不一致,因此无法直接沟通,所以,jdk提供了转换流用于实现不同类型流之间的转换,转换流主要包含以下两个:

  • InputStreamReader

  • OutputStreamWriter

案例1:

//获取标准的输入流(字节)
InputStream is = System.in;
//将字节流转换为字符流
InputStreamReader isr = new InputStreamReader(is);
//创建一个缓冲字符输入流(字符)
BufferedReader br = new BufferedReader(isr);

案例2:

File f = new File("readme.txt");
​
OutputStream os = new FileOutputStream(f,true);
//将字符流转换为字节流
OutputStreamWriter osw = new OutputStreamWriter(os);
​
BufferedWriter bw = new BufferedWriter(osw);

打印流

打印流是一个较为特殊的流,因为流向方面只提供了输出流,主要用于向目标输出源(文件,控制台,网络)打印输出数据,java中打印流提供了两种:

  • PrintStream 基于字节流的模式输出

  • PrintWriter 基于字符流的模式输出

public class PrintStreamDemo {
    public static void main(String[] args) throws FileNotFoundException {
        
        //获取标准输出流
        PrintStream ps = System.out;
        
        ps.println("打一个");
        ps.print("hello");
        ps.print("world");
        
        //创建File对象
        File f = new File("readme.txt");
        //获取基于目标文件的字节输出流(追加模式)
        OutputStream os = new FileOutputStream(f,true);
        //使用打印流包装节点流
        ps = new PrintStream(os,true);
        ps.println("HelloWorld");
        ps.close();
    }
}

 

作业

  1. 完成一个文件拷贝过程?(将 D:/1.mp3拷贝到 E:/video/1.mp3)

  1. 完成一个目录拷贝(目录中还有子文件,子目录)?-递归

  1. 对一个目录进行扫描,将扫描目录(包括子目录)中的标准文件信息(文件名,大小,创建时间,存储路径)输出到一个以当前系统时间命名的日志文件中(如:20180725165332.log),格式如下:

    HelloWorld.java 2KB 2018-07-22 13:22:32 d:/test/javacode

    softeem宣传片.mp4 400MB 2018-03-25 23:12:55 d:/test/video

    java编码规范.pdf 2.5MB 2016-11-09 09:33:21 d:/test/doc

    JDK1.8.chm 2.5MB 2016-11-09 09:33:21 d:/test/doc/d1

  1. 有如下银行账户交易明细:

    #客户号 姓名 所述机构号 性别 帐号 发生时间 发生额

    000001|刘德华|0000|1|4155990188888888|20060720200005|300.00000201|晓龙|0002|1|4155990199999999|20060720200005|500.00000101|黄晓明|0012|1|4155990100000000|20060720200005|1000.50000101|张东健|0012|1|4155990155555555|20060720200005|600.99000301|梁朝伟|0013|0|4155990111111111|20060722201005|5000.00000001|刘德华|0000|1|4155990188888888|20060725200005|200.00

    其中每一行数据是一条交易明细,每行分6列,列间用 |分隔。#为注释符号。类TransRecord存储一条明细(金额字段数据类型定为BigDecimal)。解析文件数据至 List<TransRecord>

    要求实现以下功能:

    public class TransRecordManager{
        /**
        * 记录数组
        */
        private List<TransRecord> records;
    ​
        /**
        * 加载数据
        * @param in - 数据流
        * @return
        * @throws - 解析过程中IO错误
        */
        public void load(InputStream in) throws IOException;
    ​
        /**
        * 加载数据
        * @param fileName - 包含记录数据的文件名
        * @return
        * @throws - 解析过程中IO错误
        */
        public void load(String fileName) throws IOException;
    ​
        /**
        * 取所有记录
        * @return 所有记录数组或null
        */
        public List<TransRecord> getAll();
    ​
        /**
        * 按客户号查询记录
        * @param customerNumber - 客户号
        * @return 符合条件的记录数组或null
        */
        public List<TransRecord> findByCustomerNumber(String customerNumber);
    ​
        /**
        * 按日期段查询记录
        * @param start - 开始日期
        * @param end - 结束日期
        * @return 符合条件的记录数组或null
        */
        public List<TransRecord> findByDate(String start, String end);
    ​
        /**
        * 取得总金额
        * @return 总金额
        */
        public BigDecimal totalAmount();
    ​
        /**
        * 按金额排序
        * @return 按金额升序排序的结果
        */
        public List<TransRecord> sortByAmount();
    ​
        /**
        * 打印  
        * @param out - 输出流
        */
        public void print(OutputStream out);
    }

 

package com.softeem.example;

import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;

public class FileUtils {

/**
 * 文件拷贝
 * @param source  源文件
 * @param dir 目标目录
 * @throws IOException 
 */
public void fileCopy(File source,File dir) throws IOException{
    //根据目标目录结合源文件名构建新的目标File对象
    File target = new File(dir,source.getName());
    //获取源文件的输入流并包装
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
    //获取目标文件的输出流
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target));
    //定义字节缓冲区
    byte[] b = new byte[1024];
    //记录每次读取的真实字节数
    int len = 0;
    while((len = bis.read(b)) != -1){
        bos.write(b, 0, len);
    }
    bos.close();
    bis.close();
}
​
/**
 * 完成目录拷贝
 * @param sourceDir 源目录
 * @param targetDir 目标目录
 * @throws IOException 
 */
public void dirCopy(File sourceDir,File targetDir) throws IOException{
    targetDir = new File(targetDir,sourceDir.getName());
    if(!targetDir.exists()){
        //创建目录
        targetDir.mkdirs();
    }
    //读取源目录中的子文件
    File[] files = sourceDir.listFiles();
    if(files != null){
        for(File f:files){
            if(f.isFile()){
                //拷贝文件
                fileCopy(f, targetDir);
            }else{
                //递归
                dirCopy(f, targetDir);
            }
        }
    }
}
public static void main(String[] args) throws IOException {
    File dir1 = new File("d:/fileTest");
    File dir2 = new File("d:/targetFile");
    new FileUtils().dirCopy(dir1, dir2);
}

}

 

day3-网络编程(上)

  • 网络概述

  • 网络协议(TCP/IP、UDP)

  • IP与端口

  • C/S、B/S

  • InetAddress类

  • Socket编程

网络概述

概述

把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模宏大、功能强大的网络系统,从而使众多的计算机可以方便地互相传递信息,共享硬件、软件、数据信息等资源。

计算机网络优点

  • 资源共享

  • 分布式计算

网络分类

  • 局域网(LAN,Local Area Network)

  • 都市网(MAN,Metropolis Area Network)

  • 广域网(WAN,Wide Area Network)

网络协议

计算机网络中实现通信必须有一些约定即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准

  • TCP/IP

  • UDP

IP与端口

IP

IP协议(Internet Protocol)是网络上的计算机进行连接的基础,每一台主机在不同网络中都有一个唯一的ip地址,通过该地址主机之间可以完成网路联通实现信息交互;IP地址通常由四个字节(IPV4)构成,由于互联网发展迅速,目前IPV4地址(40多亿)已经接近枯竭,因此出现了更为庞大的ipV6。

端口(Port)

端口号是应用程序在主机中的唯一标识,是当前应用与外部应用通信的通道,其中包含一些数据结构和IO缓冲区,端口的取值范围为0~65535之间;通常端口分为三类

公认端口(Well Known Ports):从0到1023,它们紧密绑定(Binding)一些服务

注册端口(Registered Ports):从1024到49151。它们松散地绑定一些服务

动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上不应为服务分配这些端口

 

C/S、B/S

  • C/S:Client/Server客户端服务器模式

  • B/S:Browser/Server浏览器服务模式

InetAddress类

java中提供了java.net.InetAddress类用于对指定主机的进行操作,该类包含主机信息和ip地址等信息,InetAddress未提供公开的构造器,因此无法直接通过new构建对象,具体使用如下:

public class IpDemo {
​
    public static void main(String[] args) throws UnknownHostException {
        //根据主机名称获取ip对象(包含主机名和ip地址信息)
        InetAddress ip = InetAddress.getByName("DESKTOP-F8QEE0A");
        //获取主机名
        System.out.println(ip.getHostName());
        //获取主机地址
        System.out.println(ip.getHostAddress());
        
        byte[] b = ip.getAddress();
        for (byte c : b) {
            System.out.println(c);
        }
        
        //获取指定主机中的所有ip(包括ipv6)
        InetAddress[] ips = InetAddress.getAllByName("DESKTOP-F8QEE0A");
        for (InetAddress addr : ips) {
            System.out.println("--->"+addr);
        }
        
        b = new byte[]{(byte)192,(byte)168,(byte)4,(byte)254};
        //根据提供的原始ip的字节数组获取ip对象
        ip = InetAddress.getByAddress(b);
        System.out.println(ip);
        
        //本地主机获取
        System.out.println("获取本地主机地址:"+InetAddress.getLocalHost());
        System.out.println(InetAddress.getByName("localhost"));
        System.out.println(InetAddress.getByName("127.0.0.1"));
        //根据域名获取当前域名绑定的ip地址
        System.out.println(InetAddress.getByName("task.softeem.top"));
        System.out.println(InetAddress.getByName("www.baidu.com"));
    }
}

Socket通信

Socket通信包含TCP/IP和UDP协议的通信机制,其中TCP/IP协议为安全可靠协议,需要保证服务器和客户端正常建立连接之后方可通信,java中为TCP/IP协议提供的Socket通信工具主要由java.net.ServerSocket和java.net.Socket完成,常用构造器如下:

  • ServerSocket(int port):提供服务端套接字

  • Socket(String ip,int port):提供客户端套接字

通信原理:

 

以下为一个基于点对点的通信过程:

Server端:

public static void main(String[] args) throws IOException {
    //占据指定端口创建服务
    ServerSocket ss = new ServerSocket(6789);
    //开始监听,一旦有客户端连接,则获取跟此客户端通信的Socket对象
    Socket s = ss.accept();
    //获取基于Socket的输出流并包装为打印流(自动刷新)
    PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
    BufferedReader br = null;
    pw.println("已连接Softeem聊天室,准备跟服务器畅聊吧!!!");
    while(true){
        // 读取从Client端发送过来的消息
        br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String msg = br.readLine();
        System.out.println(s.getInetAddress().getHostAddress()+":"+msg);
        //读取控制台消息发送到Client端
        br = new BufferedReader(new InputStreamReader(System.in));
        msg = br.readLine();
        pw.println(msg);
    }
}

Client端:

public static void main(String[] args) throws UnknownHostException, IOException {
    //连接到指定地址指定端口的服务
    Socket s = new Socket("192.168.4.254",6789);
    BufferedReader br = null;
    //获取基于Socket的输出流并包装为打印流
    PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
    while(true){
        //读取来自Server端的消息
        br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String msg = br.readLine();
        System.out.println(s.getInetAddress().getHostAddress()+":"+msg);
        //读取控制台输入并发送到Server端
        br = new BufferedReader(new InputStreamReader(System.in));
        msg = br.readLine();
        pw.println(msg);
    }
}

 

从服务端传输文件到客户端(一对一)

FTPServer.java:

package com.softeem.net.socket;
​
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
​
public class FTPServer {
​
    public void start(File target) throws IOException{
        //创建一个服务器
        ServerSocket ss = new ServerSocket(5678);
        //开始监听
        Socket  s = ss.accept();
        //获取基于socket的输出流
        PrintStream ps = new PrintStream(s.getOutputStream());
        //获取目标文件的输入流
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(target));
        System.out.println("开始传输...");
        byte[] b = new byte[1024];
        int len = 0;
        while((len = bis.read(b)) != -1){
            ps.write(b, 0, len);
        }
        System.out.println("传输完成!");
        bis.close();
        ps.close();
        ss.close();
    }
    
    public static void main(String[] args) throws IOException {
    
        File target = new File("D:\\素材\\视频\\video.mp4");
        new FTPServer().start(target);
    }
​
}
​

FTPClient.java

package com.softeem.net.socket;
​
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
​
public class FTPClient {
​
    public static void main(String[] args) throws UnknownHostException, IOException {
        
        File f = new File("C:\\Users\\mrchai\\Desktop\\video.mp4");
        
        Socket s = new Socket("192.168.4.254",5678);
        //获取目标文件的输出流
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f));
        //获取socket的输入流
        BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
        byte[] b = new byte[1024];
        int len = 0;
        while((len = bis.read(b)) != -1){
            bos.write(b, 0, len);
        }
        bis.close();
        bos.close();
        s.close();
    }
}

原理图:

 

基于UDP协议的Socket通信

UDP(User Datagram Protocol)用户数据报协议,是一个不可靠的网络传输协议,原理为:将需要发送的数据打成数据报包(DatagramPacket)然后通过网络通道(DatagramSocket)发送,发送过程不能保证数据能及时送达,也不能保证数据报包的接收顺序跟发送顺序一致,但是传输效率相比TCP/IP协议较高。

UDP编程实例:

Sender.java

String msg = "听说女寝被盗?究竟是人性的扭曲,还是道德沦丧!!!";
byte[] b = msg.getBytes();
//将数据打包成数据报包
DatagramPacket dp = new DatagramPacket(
    b, 
    0, 
    b.length, 
    InetAddress.getByName("192.168.4.254"), 
    7788);
//创建网络通道
DatagramSocket ds = new DatagramSocket();
//发送
ds.send(dp);

Receiver.java

DatagramSocket ds = new DatagramSocket(7788);
//声明字节缓冲区
byte[] b = new byte[1024];
DatagramPacket dp = new DatagramPacket(b, 0, b.length);
//将消息接收到数据报包中
ds.receive(dp);
//解析数组中的数据为字符串消息
String s = new String(b,0,dp.getLength());
System.out.println(s);

与飞秋通信

由于飞秋内部协议基于IPMSG协议(飞鸽传书),底层也是UDP,所以通过该协议构建指定格式的消息也可以完成自定义程序实现跟飞秋通信,飞秋消息发送的协议模式为 (版本号:包编号:用户名:主机名:命令字:消息内容),例如"1​:100 :​ tom : toms:32:Hello,tom!​",另外飞秋的默认端口是2425。

public static void main(String[] args) throws IOException {
    String msg = "听说女寝被盗?究竟是人性的扭曲,还是道德沦丧!!!";
    msg = "1:100:mrchai:血饮狂刀:32:"+msg;
    byte[] b = msg.getBytes();
    //将数据打包成数据报包
    DatagramPacket dp = new DatagramPacket(
        b, 
        0, 
        b.length, 
        InetAddress.getByName("192.168.4.255"), 
        2425);
​
    //创建网络通道
    DatagramSocket ds = new DatagramSocket();
    //发送
    ds.send(dp);
}

 

练习:

  1. 完成一个点对点的聊天工具,要求客户端能够实现聊天记录的保存,根据日期,每天生成一个,比如:20180727.log ,格式如下(推荐UDP)

    【2018-07-27 15:32:33】小明 :今天天气真好啊!

    【2018-07-27 15:33:33】我:你为何如此优秀??

  2. 完成一个文件服务器,服务端对外公开一个目录,目录中包含N个标准文件,要求客户端能够任意下载目录中的指定文件(提示:Map集合,对象序列化)

 

day4-多线程(上)

  • 线程与进程

  • 线程概述

  • 线程创建

  • 守护线程

  • 多线程编程(文件服务器)

线程和进程

  • 进程:是操作系统中正在运行的一个任务

  • 线程:是进程中的一条执行路径

线程概述

线程是程序中一条执行路径

线程的状态

  • 创建

  • 就绪

  • 运行

  • 阻塞

  • 死亡

一个线程创建后一旦执行start方法,则该线程进入就绪态(准备运行),就绪态的线程当得到CPU分配时间片之后进入到运行态(反之进入阻塞),处于运行态的线程由于失去时间片而进入阻塞态,当处于阻塞态的线程得到时间片,此时进入就绪态(阻塞态的线程无法直接运行),当线程执行完毕(run方法执行结束),则被GC销毁。

 

 

线程创建

  • 继承Thread类

  • 实现Runnable接口

线程创建方法一(继承Thread类):

public class SaleTicket extends Thread{
​
    public void run(){
        for(int i = 0;i<10;i++){
            System.out.println(Thread.currentThread()+"买票:"+i);
        }
    }
    
    public static void main(String[] args) {
        //创建一个线程对象
        SaleTicket st = new SaleTicket();
        //启动线程
        st.start(); //st.run() 不能叫线程启动,只能叫方法调用
        
        SaleTicket st2 = new SaleTicket();
        st2.start();
        
    }
}

线程创建方法二(实现Runnable接口):

public class RunnableDemo implements Runnable{
​
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程"+Thread.currentThread()+"执行"+i);
        }
    }
    
    public static void main(String[] args) {
        RunnableDemo rd = new RunnableDemo();
        Thread t1 = new Thread(rd);
​
        Thread t2 = new Thread(rd);
        
        t1.start();
        t2.start();
    }
}

使用Runnable接口可以将CPU,代码和数据分开,形成清晰的模型;还可以从其他类继承;保持程序风格的一致性。

直接继承Thread类不能再从其他类继承;编写简单,可以直接操纵线程,无需使用Thread.currentThread()。

线程中断

线程的中断通常包含三种方式

  • 调用stop()(已过时):会引发SecurityException

  • interrupt() :不推荐

  • 通过标记判断是否达到结束条件

public class ThreadDemo extends Thread{
​
    private boolean isStop;
    
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                if(!isStop){                    
                    sleep(500);
                    System.out.println("子线程:"+i);
                    if(i == 5){
                        //终止线程
//                  interrupt();
                        isStop = true;
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        
​
        new ThreadDemo().start();
        
        for (int i = 10; i < 20; i++) {
            Thread.sleep(500);
            System.out.println("主线程:"+i);
        }
    }

守护线程

守护线程也叫后台线程,主要作为是为其他线程提供服务器,如果其他线程一旦结束,即便守护线程自身任务还未完毕,也会随之结束;守护线程一般常见于GC,程序在运行过程中会不断产生对象,当这些对象使用完毕之后就会被GC(线程)回收,当程序运行一旦结束,GC也会随之结束。

public class DeamonDemo extends Thread{
​
    private boolean isStop;
    
    public DeamonDemo(String name) {
        super(name);
    }
    
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                if(!isStop){
                    sleep(500);
                    System.out.println(this.getName()+"--->"+i);
                    if(i == 5 && "线程1".equals(this.getName())){
                        isStop = true;
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        DeamonDemo d1 = new DeamonDemo("线程1");
        DeamonDemo d3 = new DeamonDemo("守护线程");
        
        //设置当前线程为守护线程
        d3.setDaemon(true);
        d1.start();
        d3.start();
    }
}

多线程编程之-文件服务器

提供文件传输服务的线程类(FileServer.java)

public class FileServer extends Thread{
​
    private File file;
    private Socket s;
​
    public FileServer(File file,Socket s) {
        super();
        this.file = file;
        this.s = s;
    }
    
    @Override
    public void run() {
        System.out.println("已连接客户端:"+s.getInetAddress()+",准备传输...");
        BufferedOutputStream bos = null;
        BufferedInputStream bis = null;
        try {
            //获取目标文件的输入流
            bis = new BufferedInputStream(new FileInputStream(file));
            //获取socket的输出流
            bos = new BufferedOutputStream(s.getOutputStream());
            byte[] b = new byte[1024];
            int len = 0;
            while((len=bis.read(b)) != -1){
                bos.write(b, 0, len);
            }
            System.out.println("传输完成:"+s.getInetAddress());
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                if(bis != null){
                    bis.close();
                }
                if(bos != null){
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
    }
}

文件服务器类(FtpServer.java)

public class FTPServer{
​
    public static void main(String[] args) throws IOException {
        File target = new File("D:\\素材\\视频\\demo.mp4");
        ServerSocket ss = new ServerSocket(9999);
        System.out.println("服务器启动");
        while(true){
            Socket s = ss.accept();
            System.out.println(s);
            new FileServer(target,s).start();
        }
    }
}

文件传输客户端程序(FtpClient.java)

public class FTPClient {
​
    public static void main(String[] args) throws UnknownHostException, IOException {
        
        File f = new File("C:\\Users\\mrchai\\Desktop\\video.mp4");
        
        Socket s = new Socket("192.168.4.254",9999);
        //获取目标文件的输出流
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f));
        //获取socket的输入流
        BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
        byte[] b = new byte[1024];
        int len = 0;
        while((len = bis.read(b)) != -1){
            bos.write(b, 0, len);
        }
        bis.close();
        bos.close();
        s.close();
    }
}

多线程文件服务器原理图:

 

作业

  1. 将一个500M的文件,用4个线程搬运到另外一个文件目录里去。

  2. 使用一个线程进行文件拷贝,另一个子线程返回当前拷贝的进度 (守护线程)

day5-多线程(下)

  • 线程并发

  • 死锁

  • 线程间通信

    • synchronized

    • wait&notify

  • 生产者消费者问题

  • 并发编程(java.util.concurrent)

线程并发

多线程应用中如果涉及到多个线程操作共享变量,极有可能出现线程并发导致数据不安全问题,例如银行账户取钱问题:

​ 有一个银行账户,还有余额1100元,现在A通过银行卡从中取1000元,而同时另外一个人B通过存折也从这个账户中取1000元。取钱之前,要首先进行判断:如果账户中的余额大于要取的金额,则可以执行取款操作,否则,将拒绝取款。

​ 我们假定有两个线程来分别从银行卡和存折进行取款操作,当A线程执行完判断语句后,获得了当前账户中的余额数(1000元),因为余额大于取款金额,所以准备执行取钱操作(从账户中减去1000元),但此时它被线程B打断,然后,线程B根据余额,从中取出1000元,然后,将账户里面的余额减去1000元,然后,返回执行线程A的动作,这个线程将从上次中断的地方开始执行:也就是说,它将不再判断账户中的余额,而是直接将上次中断之前获得的余额减去1000。此时,经过两次的取款操作,账户中的余额为100元,从账面上来看,银行支出了1000元,但实际上,银行支出了2000元。

 

通过编程,重现以上问题:

账户类:Account.java

public class Account {
​
    private String num; //账号
    private double cash; //余额
    
    public Account(String num,double cash) {
        this.num = num;
        this.cash = cash;
    }
​
    public String getNum() {
        return num;
    }
​
    public void setNum(String num) {
        this.num = num;
    }
​
    public double getCash() {
        return cash;
    }
​
    public void setCash(double cash) {
        this.cash = cash;
    }
}

取款线程类:

/**
 * 用于完成取款操作的线程类
 * @author mrchai
 */
public class AccountManager implements Runnable{
​
    private Account account;    //需要被取款的账户
    private double money;   //需要取走金额
​
    public AccountManager(Account account, double money) {
        super();
        this.account = account;
        this.money = money;
    }
​
    @Override
    public void run() {
            //判断账户中的余额是否足够
            if(account.getCash() >= money){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //减少账户的余额
                account.setCash(account.getCash() - money);
                System.out.println(Thread.currentThread()+"取款成功,余额:"+account.getCash());
            }else{
                System.out.println(Thread.currentThread()+"取款失败,余额不足!");
            }
    }

测试类Client.java

public class Client {
​
    public static void main(String[] args) {
        
        //创建账户类,余额10000
        Account a = new Account("0001", 10000);
        //创建Runnable对象,每次取款10000
        AccountManager am = new AccountManager(a, 10000);
        
        Thread t1 = new Thread(am);
        Thread t2 = new Thread(am); 
        //启动两个取钱线程
        t1.start();
        t2.start();
    }
}

结果

Thread[Thread-0,5,main]取款成功,余额:0.0
Thread[Thread-1,5,main]取款成功,余额:0.0

根据结果显示,如果时间点恰到好处(两个线程同时进入查询,发现余额足够),两个线程都能成功取钱,这对银行就不公平(损失10000);

Synchronized

以上问题就是线程并发共享变量时引起的数据结果不一致的问题;针对以上的问题,可以通过同步块或者同步方法来解决,通过synchronized可以实现对指定对象或者方法锁定,一旦锁定,则当前线程结束对该对象或该方法使用前其他线程无法进行操作,从而确保数据同步。

  • 同步块:使用synchronized语句块锁定指定对象

  • 同步方法:在方法返回类型前使用synchronized关键字修饰

同步块

synchronized (account) {
    //判断账户中的余额是否足够
    if(account.getCash() >= money){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //减少账户的余额
        account.setCash(account.getCash() - money);
        System.out.println(Thread.currentThread()+"取款成功,余额:"+account.getCash());
    }else{
        System.out.println(Thread.currentThread()+"取款失败,余额不足!");
    }
}

同步方法

public synchronized void getMoney(){
    if(account.getCash() >= money){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //减少账户的余额
        account.setCash(account.getCash() - money);
        System.out.println(Thread.currentThread()+"取款成功,余额:"+account.getCash());
    }else{
        System.out.println(Thread.currentThread()+"取款失败,余额不足!");
    }
}

线程并发实例-多线程放号器:

思考:银行业务办理时需要先排号,如果银行有多台取号机,如何保证每台放号机放出的号码不出现重复,并且断点之后还能从上一次最后的取号位置继续放号,如下:

放号机A:1 3 7 8 9....放号机B:2 4 5 6 10....

实现原理如下:

public class SendCode implements Runnable{
​
    //声明整数变量存储当前号码
    private int num;
    //声明文件读写工具用于记录最后一次出现的号码
    private RandomAccessFile raf;
​
    {
        try {
            //读取指定文件中的第一个号码(记录的最后一次存储的号码)
            raf = new RandomAccessFile("code.txt", "rw");
            raf.seek(0);
            num = raf.readInt();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch(EOFException e){
            //如果未读取到(第一次放号),则从0还是放号
            System.out.println("无号码记录,重新开始!");
            num = 0;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
​
    @Override
    public void run() {
        //循环放号
        while(true){
            try {
                //锁定当前线程对象,确保以下代码在同一时间只能由一个线程执行
                synchronized(this){
                    Thread.sleep(500);
                    num++;
                    System.out.println(Thread.currentThread().getName()+"取得号码:"+num);
                    //设置指针到文件开头处
                    raf.seek(0);
                    //记录当前已放出的号码
                    raf.writeInt(num);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
​
    public static void main(String[] args) {
​
        SendCode sc = new SendCode();
        //创建四个线程模拟四台放号机,共享相同的Runnable对象
        Thread t1 = new Thread(sc,"rose");
        Thread t2 = new Thread(sc,"jack");
        Thread t3 = new Thread(sc,"tom");
        Thread t4 = new Thread(sc,"jarry");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }   
}

死锁

在多线程并发操作时,如果使线程同步,则极有可能造成死锁,死锁主要是有多个线程在等待对方释放所占有的资源时引起,死锁无法解决只能尽量避免,死锁案例:

public class DeathLock implements Runnable{
​
    Object o1 = new Object();
    Object o2 = new Object();
    
    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            synchronized (o1) {
                System.out.println("t1已经锁定o1");
                synchronized (o2) {
                    System.out.println("T1锁定O2执行完毕");
                }
            }
        }else{
            synchronized (o2) {
                System.out.println("t2已经锁定o2");
                synchronized (o1) {
                    System.out.println("T2锁定o1执行完毕");
                }
            }
        }
    }
    
    public static void main(String[] args) {
        
        DeathLock  dl = new DeathLock();
        
        Thread t1 = new Thread(dl,"t1");
        Thread t2 = new Thread(dl,"t2");
        
        t1.start();
        t2.start(); 
    }
}

生产者消费者问题(wait,notify,notifyAll)

 

sleep和wait区别 ?

sleep方法是Thread中的静态方法,使用时会导致线程休眠一段时间,到时间之后会自动恢复,sleep方法的使用无需拥有对象监视器

wait方法是来自Object中的方法,wait方法一旦执行除非调用notify或notifyAll才能恢复执行,否则会一直阻塞,wait方法必须保证当前线程拥有对象见识才能执行,否则会出现java.lang.IllegalMonitorStateException异常

day6-网络编程(下)

  • UDP数据广播

  • Http协议概述

  • URL&HttpURLConnection类

  • JSON数据格式

  • URLEncoder&URLDecoder

UDP数据广播

​ 在通用的以太网(Ethernet)构架下,计算机于计算机之间的数据交换都是通过交换机来完成的。如果一份数据需要被传送给多个接收者,在使用TCP/IP连接的情况下,数据发送者需要向交换机发送N 个同样的拷贝,而交换机则负责将这N 个拷 贝分发给所有的接收者;

​ 在使用UDP 数据广播的情况下,数据发送者只需要向交换机发送一个拷贝,交换机负责将这个信息制作N 个拷贝发送给所有的机器。在这种情况下,使用TCP/IP连接会大大的增加网络的负担。在一个普通局域网络中,可以认为由于网络状况较差而造成数据丢失的可能性比较小,而利用UDP 数据广播进行 数据交换能够大幅度减轻网络的负担 。

UDP数据广播核心类:

  • MulticastSocket

  • DatagramPacket

广播发送

public class MulticastDemo {
​
    public static void main(String[] args) throws IOException {
        
        String msg = "本周五下午举办第一次班级过活动,望广大童鞋积极参加,相互转告!!!!";
        //创建组播地址对象
        InetAddress ip = InetAddress.getByName("228.5.6.7");
        //创建数据广播通道对象
        MulticastSocket ms = new MulticastSocket();
        //将组播地址加入网络通道中
        ms.joinGroup(ip);
        
        //将消息内容打包成数据报包
        DatagramPacket dp = new DatagramPacket(msg.getBytes(),0, msg.getBytes().length, ip, 6789);
        //发送数据广播
        ms.send(dp);
        ms.close();
    }
}

接收广播

public class MulticastDemo_Receiver {
​
    public static void main(String[] args) throws IOException {
​
        InetAddress ip = InetAddress.getByName("228.5.6.7");
        MulticastSocket ms = new MulticastSocket(6789);
        //将组播地址加入网络通道中
        ms.joinGroup(ip);
​
        byte[] b = new byte[1024];
        //将消息内容打包成数据报包
        DatagramPacket dp = new DatagramPacket(b,b.length);
        ms.receive(dp);
        String s = new String(dp.getData(),0,dp.getLength());
        System.out.println(s);
    }
}

Http协议

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。1960年美国人Ted Nelson构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了HTTP超文本传输协议标准架构的发展根基。Ted Nelson组织协调万维网协会(World Wide Web Consortium)和互联网工程工作小组(Internet Engineering Task Force )共同合作研究,最终发布了一系列的RFC,其中著名的RFC 2616定义了HTTP 1.1。

 

 

JSON数据格式

JSON:JavaScript Object Notation,是一种基于js的一种跨语言平台的数据交换格式,是轻量级(区别于XML),JSON包含JSON对象以及JSON数组

语法规则:

  • JSON对象: {“属性名”:"属性值"}

    • 复杂:{"属性名":[{},{}]}

  • JSON数组

    • 简单数组:["元素1,","元素2"...]

    • 复杂数组:[{"属性名":"属性值"},{"属性名":"属性值"}]

例如:java类如下

public class User{
    private int uid;
    private String name;
    private String sex;
    //setter/getter
}

表示为json数据

//JSON对象
{"uid":"1","name","张三","sex":"男"}
​
//JSON数组
[
    {"uid":"1","name","张三","sex":"男"},
    {"uid":"2","name","李四","sex":"女"}
]

JSON插件

  • Json-lib

  • Gson

  • Jackson

  • FastJSON - alibaba

 

 

 

 

 

 

 

 

作业

  1. 完成一个基于UDP协议的群聊天室,要求能完成消息的任意发送,并且能够保存聊天记录到文件中,要求消息显示格式为: ip地址[系统时间]:消息内容

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值