Java IO 高级特性总结

Java IO系列

Java IO 基础

Java IO 高级

Java NIO


序列化与反序列化

定义

把对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为对象的过程称为对象的反序列化。对象与字节序列(流)之间的相互转换流通,就构成了Java 的序列化与反序列化。

对象的序列化主要有两种用途:

1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

2. 在网络上传送对象的字节序列。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

java API

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

只有实现了SerializableExternalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。

对象序列化包括如下步骤:

  • 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  • 通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:

  • 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  • 通过对象输入流的readObject()方法读取对象。

实例

public class Employee implements Serializable {
	//序列化ID
    private static final long serialVersionUID = 202006121010007L;
    private String name;
    private Integer age;
    private String gender;
    private String mobile;
    private String address;
    private BigDecimal salary;
    private String department;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public BigDecimal getSalary() {
        return salary;
    }

    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                ", mobile='" + mobile + '\'' +
                ", address='" + address + '\'' +
                ", salary=" + salary +
                ", department='" + department + '\'' +
                '}';
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file/employee.txt"));
        Employee employee = new Employee();
        employee.setName("孔子");
        employee.setMobile("12345678901");
        employee.setGender("男");
        employee.setAddress("江苏南京");
        employee.setAge(18);
        employee.setDepartment("顾问");
        employee.setSalary(new BigDecimal(3000));
        oos.writeObject(employee);
        oos.flush();
        oos.close();

        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file/employee.txt"));
        Employee employee1 = (Employee) ois.readObject();
        ois.close();
        System.out.println(employee1);
    }
}

序列化Employee 成功后在E盘生成了一个employee.txt文件,而反序列化Employee 是读取E盘的employee.txt后生成了一个Employee 对象。

serialVersionUID的作用

s​e​r​i​a​l​V​e​r​s​i​o​n​U​I​D​:​ ​字​面​意​思​上​是​序​列​化​的​版​本​号​,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。

实现Serializable接口的类如果类中没有添加serialVersionUID,那么就会出现如下的警告提示
在这里插入图片描述
采用Add default serial version ID这种方式生成的serialVersionUID是1L,例如:

private static final long serialVersionUID = 1L;

采用Add generated serial version ID这种方式生成的serialVersionUID是根据类名,接口名,方法和属性等来生成的,例如:

private static final long serialVersionUID = 202006121010007L;

那么serialVersionUID(序列化版本号)到底有什么用呢?

序列化必须保证serialVersionUID前后一致性,原因在于在序列化之后添加一个字段和方法时,如果serialVersionUID前后不一致,再次反序列化就会抛出异常,它会认为前后序列化的对象不一致。

serialVersionUID如何取值?

serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。

显式地定义serialVersionUID有两种用途:

  1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
  2. 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

Properties类读取

定义

Properties(Java.util.Properties),该类主要用于读取Java的配置文件,不同的编程语言有自己所支持的配置文件,配置文件中很多变量是经常改变的,为了方便用户的配置,能让用户够脱离程序本身去修改相关的变量设置。就像在Java中,其配置文件常为.properties文件,是以键值对的形式进行参数配置的。

Java API

java中提供了配置文件的操作类Properties类(java.util.Properties):
public class Properties extends Hashtable可见Properties类继承了Hashtable,而HashTable又实现了Map接口,所以可对 Properties 对象应用 put 和 putAll 方法。但不建议使用这两个方法,因为它们允许调用者插入其键或值不是 String 的项。相反,应该使用 setProperty 方法。如果在“不安全”的 Properties 对象(即包含非 String 的键或值)上调用 store 或 save 方法,则该调用将失败。
在这里插入图片描述
常用的方法

getProperty(String key)在此属性列表中搜索具有指定键的属性。如果在此属性列表中找不到该键,则会检查默认属性列表及其默认值(递归)。如果未找到该属性,则该方法返回默认值参数。
list(PrintStream out)将此属性列表打印到指定的输出流。此方法对于调试很有用。
load(InputStream inStream)从输入字节流中读取属性列表(键和元素对)。输入流采用加载(Reader)中指定的简单的面向行的格式,并假定使用ISO 8859-1字符编码;即每个字节是一个Latin1字符。不在Latin1中的字符和某些特殊字符在使用Unicode转义符的键和元素中表示。 此方法返回后,指定的流仍保持打开状态。
setProperty(String key, String value)调用 Hashtable 的方法 put 。他通过调用基类的put方法来设置 键值对。
store(OutputStream out, String comments)将此Properties表中的此属性列表(键和元素对)以适合使用load(InputStream)方法加载到Properties表的格式写入输出流。 此Properties方法不会写出此Properties表的defaults表中的属性(如果有)。
storeToXML(OutputStream os, String comment, String encoding)使用指定的编码发出表示此表中包含的所有属性的XML文档。
clear()清除此哈希表,使其不包含任何键。
stringPropertyNames()返回此属性列表中的一组键,其中键及其对应的值是字符串,如果尚未从主属性列表中找到相同名称的键,则包括默认属性列表中的不同键。键或键不是String类型的属性将被省略。
getProperty(String key)在此属性列表中搜索具有指定键的属性。如果在此属性列表中找不到该键,则会检查默认属性列表及其默认值(递归)。如果未找到该属性,则该方法返回默认值参数。
list(PrintStream out)将此属性列表打印到指定的输出流。此方法对于调试很有用。
load(InputStream inStream)从输入字节流中读取属性列表(键和元素对)。输入流采用加载(Reader)中指定的简单的面向行的格式,并假定使用ISO 8859-1字符编码;即每个字节是一个Latin1字符。不在Latin1中的字符和某些特殊字符在使用Unicode转义符的键和元素中表示。 此方法返回后,指定的流仍保持打开状态。
setProperty(String key, String value)调用 Hashtable 的方法 put 。他通过调用基类的put方法来设置 键值对。
store(OutputStream out, String comments)将此Properties表中的此属性列表(键和元素对)以适合使用load(InputStream)方法加载到Properties表的格式写入输出流。 此Properties方法不会写出此Properties表的defaults表中的属性(如果有)。
storeToXML(OutputStream os, String comment, String encoding)使用指定的编码发出表示此表中包含的所有属性的XML文档。
clear()清除此哈希表,使其不包含任何键。
stringPropertyNames()返回此属性列表中的一组键,其中键及其对应的值是字符串,如果尚未从主属性列表中找到相同名称的键,则包括默认属性列表中的不同键。键或键不是String类型的属性将被省略。

实例

1. 为properties对象添加属性和获取值

public class Test {
    public static void main(String[] args) {
        Properties pro=new Properties();
        //设置值
        pro.setProperty("driver", "com.mysql.jdbc.Driver");
        pro.setProperty("url", "jdbc:mysql//192.168.152.140:3306/myshops");
        pro.setProperty("user", "root");
        pro.setProperty("password", "hubert");
        
        //获取值:
        //1、getProperty(String key)方法  通过键获取值
        String str= pro.getProperty("driver");
        System.out.println(str);
        
        //2、getProperty(String key, String defaultValue)重载方法
        //当properties对象中没有所指定的键值时,显示给定的默认值
        String str2=pro.getProperty("driver", "没有该值");
        String str3=pro.getProperty("haha", "没有该值");
        System.out.println(str2);
        System.out.println(str3);
    }
}

结果:
com.mysql.jdbc.Driver
com.mysql.jdbc.Driver
没有该值

2. 以properties配置文件格式写入到硬盘中的某个文件夹

public class Test {
    public static void main(String[] args) throws Exception {
        Properties pro=new Properties();
        //设置值
        pro.setProperty("driver", "com.mysql.jdbc.Driver");
        pro.setProperty("url", "jdbc:mysql://192.168.152.140:3306/myshops");
        pro.setProperty("user", "root");
        pro.setProperty("password", "hubert");
        //1.通过字节流的形式
        //store(OutputStream out, String comments)
        //outputStream:字节输出流   comments:配置文件说明
        pro.store(new FileOutputStream(new File("resources/jdbc.properties")), "数据库配置文件");

        //2.通过字符流的形式
        //store(Writer writer, String comments)
        //writer:字符输出流   comments:配置文件说明
        pro.store(new FileWriter("resources/jdbc.properties"),  "数据库配置文件");
    }
}

结果:

#\u6570\u636E\u5E93\u914D\u7F6E\u6587\u4EF6
#Wed Oct 21 12:06:54 CST 2020
user=root
password=hubert
url=jdbc\:mysql\://192.168.152.140\:3306/myshops
driver=com.mysql.jdbc.Driver

3. 以XML配置文件格式写入到硬盘中的某个文件夹

public class Test {
    public static void main(String[] args) throws Exception {
        Properties pro=new Properties();
        //设置值
        pro.setProperty("driver", "com.mysql.jdbc.Driver");
        pro.setProperty("url", "jdbc:mysql://192.168.152.140:3306/myshops");
        pro.setProperty("user", "root");
        pro.setProperty("password", "hubert");
        //1.不指定编码  默认为:UTF-8
        //storeToXML(OutputStream os, String comment)
        //因为XML不是文本文件,所以只能用字节流,为不能用字符流
        pro.storeToXML(new FileOutputStream("resources/jdbc.xml"), "数据库配置文件");

        //1.不指定编码
        //storeToXML(OutputStream os, String comment)
        //因为XML不是文本文件,所以只能用字节流,为不能用字符流
        pro.storeToXML(new FileOutputStream("resources/jdbc2.xml"), "数据库配置文件", "GBK");
    }
}

运行效果:

// jdbc.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>数据库配置文件</comment>
<entry key="user">root</entry>
<entry key="password">hubert</entry>
<entry key="url">jdbc:mysql://192.168.152.140:3306/myshops</entry>
<entry key="driver">com.mysql.jdbc.Driver</entry>
</properties>

// jdbc2.xml
<?xml version="1.0" encoding="GBK" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>数据库配置文件</comment>
<entry key="user">root</entry>
<entry key="password">hubert</entry>
<entry key="url">jdbc:mysql://192.168.152.140:3306/myshops</entry>
<entry key="driver">com.mysql.jdbc.Driver</entry>
</properties>

5. 加载和读取配置文件(以properties文件为例)

public void loadAndReadFile() throws Exception {
    Properties pro=new Properties();
    //通过字节输入流
    //load(InputStream inStream)
    pro.load(new FileInputStream("resources/jdbc.properties"));
    //通过类加载器 获取当前类路径
    //类路径是指      / bin路径
    pro.load(this.getClass().getResourceAsStream("/jdbc.properties"));
    pro.load(this.getClass().getClassLoader().getResourceAsStream("jdbc.properties"));

    //也可以使用当前上下文的类加载器,不用“/”
    pro.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("jdbc.properties"));
    //通过字符输入流
    //load(Reader reader)
    pro.load(new FileReader("resources/jdbc.properties"));
    System.out.println(pro.get("driver"));
}

public Connection getConnection() throws Exception{
    Properties info=new Properties();
    info.load(this.getClass().getClassLoader().getResourceAsStream("jdbc.properties"));
    String  driver=info.getProperty("driver");
    String jdbcUrl=info.getProperty("jdbcUrl");
    String user=info.getProperty("user");
    String password=info .getProperty("password");
    Class.forName(driver);
    Connection connection= DriverManager.getConnection(jdbcUrl,user,password);
    return connection;
}

通过读取配置文件的形式可以实现代码的深层次解耦,在java编程中,配置文件的重要性更是不言而喻。java编程有一条不成文的规定就是:“约定大于配置,配置大于代码”意思就是能用约定的就不去配置,能用配置文件搞定的就不去写代码,真正牛逼的攻城狮(工程师)不是写代码,而是写配置。java的一些开源框架,如:Struts、Struts2、Hibernate、Spring、MyBatis等都大量的使用配置文件,我们在学习和工作中,都应该将“约定大于配置,配置大于代码”的思想运用到项目中,实现代码的解耦,体现出你比别人的高明之处,才能比别人优秀。

RandomAccessFile

简介

RandomAccessFile是Java输入/输出流体系中功能最丰富的文件内容访问类,既可以读取文件内容,也可以向文件输出数据。与普通的输入/输出流不同的是,RandomAccessFile支持跳到文件任意位置读写数据,RandomAccessFile对象包含一个记录指针,用以标识当前读写处的位置,当程序创建一个新的RandomAccessFile对象时,该对象的文件记录指针对于文件头(也就是0处),当读写n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由移动该记录指针。

由于RandomAccessFile可以自由访问文件的任意位置,所以如果需要访问文件的部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择

RandomAccessFile不属于InputStream和OutputStream类系的。实际上,除了实现DataInput和DataOutput接口之外(DataInputStream和DataOutputStream也实现了这两个接口),它和这两个类系毫不相干,甚至不使用InputStream和OutputStream类中已经存在的任何功能;它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是从零开始写的。这可能是因为RandomAccessFile能在文件里面前后移动,所以它的行为与其它的I/O类有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。

与OutputStream、Writer等输出流不同的是,RandomAccessFile允许自由定义文件记录指针,RandomAccessFile可以不从开始的地方开始输出,因此RandomAccessFile可以向已存在的文件后追加内容。如果程序需要向已存在的文件后追加内容,则应该使用RandomAccessFile。

RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。

RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。

常用方法

1. RandomAccessFile的构造函数

RandomAccessFile类有两个构造函数,其实这两个构造函数基本相同,只不过是指定文件的形式不同——一个需要使用String参数来指定文件名,一个使用File参数来指定文件本身。除此之外,创建RandomAccessFile对象时还需要指定一个mode参数,该参数指定RandomAccessFile的访问模式,一共有4种模式。

  • r:以只读方式打开指定文件。如果试图对该RandomAccessFile指定的文件执行写入方法则会抛出IOException
  • rw:以读取、写入方式打开指定文件。如果该文件不存在,则尝试创建文件
  • rws:以读取、写入方式打开指定文件。相对于rw模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备,默认情形下(rw模式下),是使用buffer的,只有cache满的或者使用RandomAccessFile.close()关闭流的时候儿才真正的写到文件
  • rwd:与rws类似,只是仅对文件的内容同步更新到磁盘,而不修改文件的元数据

2. RandomAccessFile的重要方法

andomAccessFile既可以读文件,也可以写文件,所以类似于InputStream的read()方法,以及类似于OutputStream的write()方法,RandomAccessFile都具备。除此之外,RandomAccessFile具备两个特有的方法,来支持其随机访问的特性。

RandomAccessFile对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件指针记录位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会后移n个字节。除此之外,RandomAccessFile还可以自由移动该记录指针。下面就是RandomAccessFile具有的两个特殊方法,来操作记录指针,实现随机访问:

  • long getFilePointer():返回文件记录指针的当前位置
  • void seek(long pos):将文件记录指针定位到pos位置

实例

多线程并发将mylog.log中的文件读取后写入到out.log中去。

public class LogReader {
    static class FileWrite implements Runnable{
        private RandomAccessFile in ;
        private RandomAccessFile out;
        private long start ;
        private long end;

        public FileWrite(String  in, String  out, long start, long end) {
            try {
                this.in = new RandomAccessFile(in,"rw");
                this.out = new RandomAccessFile(out,"rw");
            } catch (Exception e) {
                e.printStackTrace();
            }
            this.start = start;
            this.end = end;
        }
        
        @Override
        public void run() {
            try {
                in.seek(start);
                out.seek(start);
                int read = 0;
                byte[] buf = new byte[1024*1024];
                while (start<end && (read=in.read(buf))!=-1){
                    start += read;
                    out.write(buf,0,read);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                if(in != null || out !=null){
                    try {
                        out.close();
                        in.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        String sourcePath = "log/mylog.log";
        String outPath= "log/out.log";
        final int exes = 6;
        ExecutorService esf = Executors.newFixedThreadPool(exes);
        long fLength = new File(sourcePath).length();
        long length = (long) Math.ceil(fLength*1.0 / exes);
        long evPartion = length>fLength*1.0 / exes ? length -1 : length;
        long time1 = System.currentTimeMillis();
        int i ;
        for ( i= 0; i <exes ; i++) {
            esf.submit(new FileWrite(sourcePath,outPath,i*evPartion,(i+1)*evPartion));
        }
        esf.shutdown();
        while (true){
            if (esf.isTerminated()){
               break;
            }
        }
        long time2 = System.currentTimeMillis();
        System.out.println("耗时"+(time2-time1)+"ms");
    }
}

网络流

网络编程是每个开发人员工具箱中的核心部分,我们在学习了诸多Java的知识后,也将步入几个大的方向,Java网络编程就是其中之一。

如今强调网络的程序不比涉及网络的更多。除了经典的应用程序,如电子邮件、Web浏览器和远程登陆外,大多数主要的应用程序都有某种程度的内质网络功能。比如我们最常使用的IDE(Eclipse/IDEA)与源代码存储库(GitHub等等)进行通信;再比如Word,可以从URL打开文件;又或者是我们玩的众多联机游戏,玩家实时相互对战等等。Java是第一个从一开始就为网络应用而设计的编程语言,最早的两个实用Java应用的程序之一就是Web浏览器,随着Internet的不断发展,Java成为了唯一适合构建下一代网络应用程序的语言。(节选自Java NetWork Programming——Elliotte Rusty Harold著)

Client/Server

为了实现两台计算机的通信,必须要用一个网络线路连接两台计算机。服务器(Server)是指提供信息的计算机或程序,客户机(Client)是指请求信息的计算机或程序,而网络用于连接服务器与客户机,实现两者相互通信。

下面我们看一个简单的通信例子。

如下的Server程序是一个服务器端应用程序,使用 Socket 来监听一个指定的端口。

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        int port = 9999;

        System.out.println("-----------服务器启动-----------");

        ServerSocket server = new ServerSocket(port);
        Socket socket = server.accept();
        Reader reader = new InputStreamReader(socket.getInputStream());
        char chars[] = new char[1024];
        int len;
        StringBuilder builder = new StringBuilder();
        while ((len=reader.read(chars)) != -1) {
           builder.append(new String(chars, 0, len));
        }
        System.out.println("收到来自客户端的信息: " + builder);
        reader.close();
        socket.close();
        server.close();
    }
}

如下的Client是一个客户端程序,该程序通过 socket 连接到服务器并发送一个请求,然后等待一个响应。

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.util.Scanner;

public class Client {

    public static void main(String[] args) throws IOException {
        String host = "127.0.0.1";
        int port = 9999;

        System.out.println("-----------客户端启动-----------");

        Socket client = new Socket(host, port);
        Writer writer = new OutputStreamWriter(client.getOutputStream());
        Scanner in = new Scanner(System.in);
        writer.write(in.nextLine());
        writer.flush();
        writer.close();
        client.close();
        in.close();
    }
}

先启动服务器,运行结果如下:

在这里插入图片描述

再运行客户端,并输入要发送的信息,如下:

在这里插入图片描述

此时Server就接收到了Client发送的消息,如下:

在这里插入图片描述

这就是一个简单的套接字通信了,具体分析见下方TCP。

TCP

TCP网络程序设计是指利用Socket类编写通信程序,具体实例参照上例。

套接字使用TCP提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。

两台计算机间使用套接字建立TCP连接步骤如下:

  • 服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。

  • 服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。

  • 服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。

  • Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。

  • 在服务器端,accept() 方法返回服务器上一个新的 socket 引用,该 socket 连接到客户端的 socket。

连接建立后,通过使用 I/O 流在进行通信,每一个socket都有一个输出流和一个输入流,客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。

1. InetAddress
java.net包中的InetAddress类是与IP地址相关的类,利用该类可以获取IP地址、主机地址等信息。

InetAddress ip = InetAddress.getLocalHost();
String localName = ip.getHostName();    //获取本地主机名
String localIp = ip.getHostAddress();    //获取本地主机的ip地址

2. ServerSocket
 
java.net包中的ServetSocket类用于表示服务器套接字,其主要功能是等待来自网络上的“请求”,它可以通过指定的端口来等待连接的套接字(如上方实例中的9999)。

而服务器套接字一次可以与一个套接字连接,如果多台客户机同时提出连接请求,服务器套接字会将请求连接的客户机存入队列中,然后从中取出一个套接字,与服务器新建的套接字连接起来。若请求连接数大于最大容纳数,则多出的连接请求被拒绝。

ServerSocket的具体方法可参照API,这里只对accept()方法进行一个说明。调用accept()方法将返回一个与客户端Socket对象相连的Socket对象,服务器端的Socket对象使用getOutputStream()方法获得的输出流对象,将指向客户端Socket对象使用getInputStream()方法获得的输入流对象,反之亦然。

需要注意的是,accept()方法会阻塞线程的继续执行,直到接受客户的呼叫,如果没有客户呼叫服务器,那么下面的代码将不会执行。

UDP

用户数据包协议(UDP)是网络信息传输的另一种形式,基于UDP的通信与基于TCP的通信不同,UDP的信息传递更快,但不提供可靠的保证。

1. DatagramPacket

java.net包中的DatagramPacket类用来表示数据包,构造方法如下:

DatagramPacket(byte[] buf, int length)
DatagramPacket(byte[] buf, int length, InetAddress address, int port)

上述构造方法中,第一种指定了数据包的内存空间和大小,第二种不仅指定了数据包的内存空间和大小,并且指定了数据包的目标地址和端口。

2. DatagramSocket
 
java.net包中的DatagramSocket类用于表示发送和接受数据包的套接字,构造方法如下:

DatagramSocket()
DatagramSocket(int port)
DatagramSocket(int port, InetAddress addr)

上述构造方法中,第一种创建数据包套接字并将其绑定到本地主机上任何可用的端口,第二种创建数据包套接字并将其绑定到本地主机上的指定端口,创建数据包套接字并将其绑定到指定的本机地址。

实例

客户端向服务器发送名字,服务器记录并写入文件中。

服务器:

public class Server {

    private static HashSet<String> set = null;
    private static HashSet<String> inSet = null;
    static {
        set = new HashSet();
        inSet = new HashSet<>();
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("files/names.txt"));
            String name = null;
            while (null!=(name=br.readLine())){
                set.add(name);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            Release.close(br);
        }
    }

    public static boolean contains(String name){
        return set.contains(name);
    }

    public static boolean isIn(String name){
        return inSet.contains(name);
    }

    public static void in(String name){
        inSet.add(name);
    }


    public static void main(String[] args) throws IOException {
        String ip = "192.168.1.144";
        int port = 9000;
        InetSocketAddress isa = new InetSocketAddress(InetAddress.getByName(ip),port);
        ServerSocket server = new ServerSocket();
        server.bind(isa,30);

        ExecutorService es = Executors.newCachedThreadPool();

        while (true){
            Socket client = server.accept();
            es.submit(new Runnable() {
                private final Socket _client = client;
                @Override
                public void run() {
                    DataInputStream dis = null;
                    BufferedOutputStream bos = null;
                    try {
                        dis = new DataInputStream(new BufferedInputStream(_client.getInputStream()));
                        String fileName = dis.readUTF();
                        long length = dis.readLong();
                        InetSocketAddress rsa = (InetSocketAddress) _client.getRemoteSocketAddress();
                        System.out.println(
                                "receive file:"+fileName+
                                        " size:"+length+
                                        " fromIP:"+rsa.getAddress().getHostAddress()+
                                        " fromPort"+rsa.getPort());

                        bos = new BufferedOutputStream(new FileOutputStream("copy/upload/" + fileName));
                        byte[] bs = new byte[8192];
                        int total = 0;
                        int len = -1;
                        while ((total+=(len=dis.read(bs)))<length){
                            bos.write(bs,0,len);
                        }
                        bos.flush();
                        System.out.println("receive file:"+fileName+
                                " size:"+length+
                                " fromIP:"+rsa.getAddress().getHostAddress()+
                                " fromPort"+rsa.getPort() +"finished...");
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    } finally {
                        Release.close(bos,dis,_client);
                    }
                }
            });
        }
    }
}

客户端

public class Client {
    public static void main(String[] args) throws IOException {
        String ip = "192.168.1.144";
        int port = 9000;
        InetSocketAddress isa = new InetSocketAddress(InetAddress.getByName(ip),port);
        Socket client = new Socket();
        client.connect(isa,5000);

        OutputStream oos = client.getOutputStream();
        String name = "张三";
        byte[] bs = name.getBytes(Charset.forName("UTF-8"));
        oos.write(bs);
        oos.flush();
        Release.close(oos,client);

    }
}

IO 常用汇总图片

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值