Java网络IO

网络编程

基本知识

1.概述

基本概念

javaweb:网页编程 B/S

网络编程:TCP/IP C/S

基本要素

  • IP和端口号
  • 网络通信和协议:TCP/UDP

TCP/IP四层

在这里插入图片描述

2.Mac上的网络操作

# 3306替换成需要grep的端口号
netstat -an | grep 3306
# 通过list open file命令可以查看到当前打开文件,在linux中所有事物都是以文件形式存在,包括网络连接及硬件设备
# -i参数表示网络链接,:80指明端口号,该命令会同时列出PID,方便kill
su lsof -i:80
# 查看所有进程监听的端口
sudo lsof -i -P | grep -i "listen"

3.TCP通信

//服务端
public class TCPServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket socket = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            serverSocket = new ServerSocket(9999);//服务端socket,9999为socket监听的端口号
            socket = serverSocket.accept();//服务端接收客户端请求,并为客户端分配一个socket
            is = socket.getInputStream();//读取socket中的流
            baos = new ByteArrayOutputStream();//为避免中文乱码,所以使用数组流,统一输出
            int len;
            byte[] b = new byte[1024];
            while ((len = is.read(b)) > 0) {
                baos.write(b, 0, len);
            }
            System.out.println(baos.toString());
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
                if (is != null)
                    is.close();
                if (socket != null)
                    socket.close();
                if (serverSocket != null)
                    serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

//客户端
public class TCPClient {
    public static void main(String[] args) {
        Socket socket = null;
        OutputStream os = null;
        try {
            InetAddress address = InetAddress.getByName("127.0.0.1");//通过IP获取InetAddress对象
            int port = 9999;//请求服务端的9999端口,需要和服务端的保持一致
            socket = new Socket(address, port);//确定IP地址和端口
            os = socket.getOutputStream();//获取socket的输出流
            os.write("Hello World!".getBytes());//往输出流中写入字符串,要转化为Bytes,因为OutputStream是字节流
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4.UDP通信

UDP没有服务端和客户端的明显区别,一个端既可以是服务端也可以是客户端

//发送方
public class UDPClient {
    public static void main(String[] args) throws Exception {
        try (DatagramSocket socket = new DatagramSocket();
             BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            while (true) {
                String msg = br.readLine();
                DatagramPacket packet = new DatagramPacket(msg.getBytes(), msg.getBytes().length, InetAddress.getByName("localhost"), 8888);//数据包
                socket.send(packet);//发送数据包
            }
        }
    }
}

//接收方
public class UDPServer {
    public static void main(String[] args) throws Exception {
        try (DatagramSocket socket = new DatagramSocket(8888)) {
            while (true) {
                byte[] b = new byte[1024];
                DatagramPacket packet = new DatagramPacket(b, b.length);//数据包
                socket.receive(packet);//接收数据包
                System.out.println(new String(packet.getData()));
            }
        }
    }
}

5.URL

统一资源定位符

//最基本的使用和下载url资源
public class URLDemo {
    public static void main(String[] args) throws IOException {
        URL url = new URL("https://i0.hdslb.com/bfs/article/2f8e577f45e973548a7f5e86b9f67ae2e9ae2471.png@1320w_876h.webp");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        InputStream is = connection.getInputStream();
        FileOutputStream fos = new FileOutputStream("NetInformation.webp");
        byte[] b = new byte[1024];
        int len;
        while ((len = is.read(b)) > 0) {
            fos.write(b, 0, len);
        }
        fos.close();
        is.close();
    }
}

深入理解

疑问

服务器程序为什么能够在同一个端口处理多个客户端请求,即服务端和客户端通信模型是什么样的?

网络通信通过{本地ip,本地端口,目的ip,目的端口,协议}这个五元组来唯一区别一个连接,也就是说当客户端发送请求到服务端的指定端口时,服务端通过目的ip和目的端口来判断由哪个进程处理这个请求,这里的目的端口也是我们在客户端指定的端口,再由指定程序通过本地ip和本地端口判断哪个客户端发来请求,而这里的本地端口其实对于客户端来说是隐式的,因为我们并没有指定,是系统自动为我们分配的(也是可以查看的),再有,需要注意,服务端会为客户端分配端口,accept就是这个用处,方便服务端与客户端通信。

总结来说,就是客户端请求连接,服务端接收,并为客户端分配端口,同时服务端继续监听对应端口,而通信的任务则交给刚才分配的端口,而它们能够分辨对方的依据就是上面说的五元组

线程

运行线程

1.派生Thread

public class DigestThread extends Thread {
    private String filename;

    public DigestThread(String filename) {
        super();
        this.filename = filename;
    }

    @Override
    public void run() {
        MessageDigest sha = null;
        try {
            //选用SHA-256的加密摘要
            sha = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        //带资源的try,处理流,对文件流做加密处理(和缓冲流一样都是装饰者)
        try (DigestInputStream din = new DigestInputStream(new FileInputStream(filename), sha)) {
            while (din.read() != -1) ;
            //获取加密摘要
            byte[] digest = sha.digest();
            StringBuilder str = new StringBuilder(filename);
            str.append(":");
            //加密摘要转成16进制格式
            str.append(DatatypeConverter.printHexBinary(digest));
            System.out.println(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        String[] files = new String[]{"in.txt", "out.txt"};
        for (String file : files) {
            //派生出新线程
            new DigestThread(file).start();
        }
    }
}

2.实现Runnable接口

//run方法都是一样的,主要区别在继承了Runnable接口
public class DigestThread implements Runnable {
  	.....
    public static void main(String[] args) {
        String[] files = new String[]{"in.txt", "out.txt"};
        for (String file : files) {
            //派生出新线程
          	//Thread构造函数接收一个Runnable对象
            new Thread(new DigestThread(file)).start();
        }
    }
}

从线程返回信息

无法预知线程的执行顺序是从线程返回信息的一个主要原因

学术一点说就是竞争条件的原因

竞争条件指多个线程或者进程在读写一个共享数据时结果依赖于它们执行的相对时间的情形

更深一点就是因为线程的一些操作并非“原子操作”,通俗的讲就是当一个线程在进行数据操作的时候,另一个可能也在对这个数据进行操作,从而导致了不可预测的数据变化

回调

线程告诉主程序何时结束,通过调用主类的中一个方法来实现,这被称为回调

回调的优点是不会浪费CPU时间,同时更加灵活,多个对象对线程计算结果感兴趣,可以将自己加入到线程类的列表中进行注册,并定义一个接口,由这些感兴趣的类去实现对应的接口,并声明为回调方法

其实一些监听事件就是通过回调机制,比如某个组件的鼠标点击事件,一些观察者对象对这个事件感兴趣,就去注册,并在事件完成之后回调特定的方法,完成鼠标响应操作,这个就是观察者模式

//自定义回调的写法,也算是对观察者模式的一次复习了
public class DigestThread implements Runnable {
    private String filename;

    InstanceCallbackDigestUserInterface callback;

    public DigestThread(String filename, InstanceCallbackDigestUserInterface callback) {
        super();
        this.filename = filename;
        this.callback = callback;
    }

    @Override
    public void run() {
        MessageDigest sha = null;
        try {
            //选用SHA-256的加密摘要
            sha = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        //带资源的try,处理流,对文件流做加密处理(和缓冲流一样都是装饰者)
        try (DigestInputStream din = new DigestInputStream(new FileInputStream(filename), sha)) {
            while (din.read() != -1) ;
            //获取加密摘要
            byte[] digest = sha.digest();
            callback.receiveDigest(digest);//观察者模式的核心,回调方法
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class InstanceCallbackDigestUserInterface {

    private byte[] digest;
    private String filename;


    public InstanceCallbackDigestUserInterface(String filename) {
        this.filename = filename;
    }

  	//回调的方法
    void receiveDigest(byte[] digest) {
        this.digest = digest;
        StringBuilder str = new StringBuilder(filename);
        str.append(":");
        //加密摘要转成16进制格式
        str.append(DatatypeConverter.printHexBinary(digest));
        System.out.println(str);
    }

    public void calculateDigest() {
        DigestThread cb = new DigestThread(filename, this);
        new Thread(cb).start();
    }


    public static void main(String[] args) {
        String[] files = new String[]{"in.txt", "out.txt"};
        for (String file : files) {
            InstanceCallbackDigestUserInterface d = new InstanceCallbackDigestUserInterface(file);
            d.calculateDigest();
        }
    }
}

Future&Callable&Executor

通过隐藏细节更容易处理回调,向ExecutorService提交Callable任务,对于每个Callable任务会得到一个Future结果,最后可以向Future请求得到任务的结果

需要注意的是:结果如果没有准备好,轮询线程会阻塞,直到结果准备就绪,这样的好处是创建不同线程,然后按我们需要的顺序同步执行

//使用Future&Callable&Executor找最大值
public class FindMaxTask implements Callable<Integer> {
    private int start;
    private int end;
    private int[] arr;

    public FindMaxTask(int start, int end, int[] arr) {
        this.start = start;
        this.end = end;
        this.arr = arr;
    }

    @Override
    public Integer call() throws Exception {
        return Arrays.stream(arr).reduce(Math::max).orElse(arr[start]);
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int[] arr = new int[]{1, 2, 3, 4, 5, 6, -1, 1};
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<Integer> future1 = pool.submit(new FindMaxTask(0, arr.length / 2, arr));
        Future<Integer> future2 = pool.submit(new FindMaxTask(arr.length / 2 + 1, arr.length, arr));
        System.out.println(Math.max(future1.get(), future2.get()));
    }
}

同步

1.同步块

不同步的代码

public class SynchronizedTest implements Runnable {
    public static int cnt = 0;

    @Override
    public void run() {
        System.out.println(cnt++);
        System.out.println(cnt++);
        System.out.println(cnt++);
        System.out.println(cnt++);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++)
            new Thread(new SynchronizedTest()).start();
      	//0 3 4 5 2 6 7 8 1 9 10 11
      	//每次的结果可能都不一样
    }
}

添加了同步

public void run() {
  			//对System.out同步
        synchronized (System.out) {
            System.out.println(cnt++);
            System.out.println(cnt++);
            System.out.println(cnt++);
            System.out.println(cnt++);
        }
}

这里的同步是添加给了System.out对象,也就是说当前线程独占了System.out对象,必须得等这个线程执行完同步块中System.out才可以被释放

当我换了一种方式写同步块的时候问题就出现了

public class SynchronizedTest implements Runnable {
    public static int cnt = 0;

    @Override
    public void run() {
        synchronized (this) {//小知识点,这个写法相当于在方法返回类型前加synchronized——同步当前对象,同步块是整个方法
            System.out.println(Thread.currentThread().getName() + ":" + cnt++);
            System.out.println(Thread.currentThread().getName() + ":" + cnt++);
            System.out.println(Thread.currentThread().getName() + ":" + cnt++);
            System.out.println(Thread.currentThread().getName() + ":" + cnt++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();
        for (int i = 0; i < 3; i++)
            new Thread(new SynchronizedTest()).start();
    }
}
/*每次可能都不一样
Thread-0:0
Thread-1:1
Thread-0:2
Thread-1:3
Thread-0:4
Thread-1:5
Thread-0:6
Thread-1:7
Thread-2:8
Thread-2:9
Thread-2:10
Thread-2:11
*/

我预测的的结果是先执行线程1,后2,再3

但结果好像不太对,查了博客后才发现自己对同步块同步的目标理解有偏差

如果同步当前对象synchronized (this),其实独占的是当前对象,而我的写法中每一次循环都是new SynchronizedTest(),所以每一个线程相当于有一个私有的SynchronizedTest对象,也就不存在什么独占不独占的问题了,所以在理解了同步对象之后,修改了一下代码,结果就符合预期了

public static void main(String[] args) {
    SynchronizedTest test = new SynchronizedTest();
    for (int i = 0; i < 3; i++)
      new Thread(test).start();
}
/*
Thread-0:1
Thread-0:2
Thread-0:3
Thread-2:4
Thread-2:5
Thread-2:6
Thread-2:7
Thread-1:8
Thread-1:9
Thread-1:10
Thread-1:11
*/

只需要把每次new一个对象改成先new一个对象,再放入到Thread中,使得这3个线程共享的是一个对象即可达到对同步块的预期,当然了,我们也可以不这么修改,就直接把把同步对象改成同步类,这样每个线程就独占了一个类了,也可以达到预期,不过还是前者会好一点。总之把握一个核心点就是:同步谁,对应的线程就独占谁(图片辅助理解)

synchronized 同步对象概念

2.同步的代替方法

  • 不可变类型
  • 使用局部变量,局部变量不存在同步问题
  • 非线程安全的类用作为线程安全类的私有属性
    • 只要以线程安全的方式访问非安全类

如果需要作为一个原子连续地完成两个操作,中间不能有中断,就需要同步

尽管每个方法调用确实是原子的,可以保证安全,但是如果没有明确的同步,这个操作序列并不一定安全

线程调度

1.优先级

java中优先级0~10,级别越高,优先级越高,默认优先级5级

UINX中优先级越大,进程获得的CPU时间越少

2.抢占

为了让其他线程有机会运行,一个线程有8种方式可以暂停或指示它准备暂停

  • 对I/O阻塞
    • 停下来等待它没有的资源时,就会发生阻塞
  • 对同步对象阻塞
  • 礼让:使其他相同优先级的线程有机会运行
public class YieldTest implements Runnable {

    public static int cnt = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread() + ":" + cnt++);
            Thread.yield();//用于线程放弃
        }
    }

    public static void main(String[] args) {
        new Thread(new YieldTest()).start();
        new Thread(new YieldTest()).start();
    }
}
//0 1 0 1交替输出
  • 休眠:不止相同优先级,还会给较低优先级的线程运行的机会
  • 连接另一个线程
    • 一个线程需要另一个线程的结果,java提供了join方法,主要用来同步,不过现在可以使用Executor和Future取代
public class JoinTest implements Runnable {

    private String name;

    public JoinTest(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(name);
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new JoinTest("陈恒"));
        Thread thread2 = new Thread(new JoinTest("横陈"));
      
      	//这段话的意思其实是main主线程需要等待thread1执行完才可以开始thread2
        thread1.start();
        try {
            thread1.join();//不传参数表示无线等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}
//join的实现原理
//join方法是通过调用线程的wait方法来达到同步的目的的。例如,A线程中调用了B线程的join方法,则相当于A线程调用了B线程的wait方法,在调用了B线程的wait方法后,A线程就会进入阻塞状态
public final synchronized void join(long millis)
    throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
      throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
      while (isAlive()) {
        wait(0);
      }
    } else {
      while (isAlive()) {
        long delay = millis - now;
        if (delay <= 0) {
          break;
        }
        wait(delay);
        now = System.currentTimeMillis() - base;
      }
    }
}

//其中wait是原生的
public final native void wait(long timeout) throws InterruptedException;

核心原理:线程A会调用线程B的join,相当于线程A调用了线程B的wait方法,当线程B执行完毕,B会调用notifyAll方法唤醒A,从而达到同步的目的

  • 等待一个对象

    • 线程会等待一个它锁定的对象,在等待的时候,他会释放这个对象的锁并暂停,直到得到其他线程的通知/时间到期/被中断
  • 结束

    • 标志法结束(处在wait和sleep中无法使用标志法结束)
    • Interrupt中断
    public class JoinTest implements Runnable {
    
        private String name;
    
        public JoinTest(String name) {
            this.name = name;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100000000; i++) {
                System.out.println(name);
                if (Thread.currentThread().isInterrupted())
                    break;
            }
        }
    
        public static void main(String[] args) {
            Thread thread = new Thread(new JoinTest("陈恒"));
            thread.start();
            thread.interrupt();//线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定
        }
    }
    
    • stop结束(已被废弃)

    为什么弃用stop:

    1. 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。

    2. 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

    3. 例如,存在一个对象 u 持有 ID 和 NAME 两个字段,假如写入线程在写对象的过程中,只完成了对 ID 的赋值,但没来得及为 NAME 赋值,就被 stop() 导致锁被释放,那么当读取线程得到锁之后再去读取对象 u 的 ID 和 Name 时,就会出现数据不一致的问题,如下图:

      img

  • 被高优先级线程抢占

3.线程状态图

img

==看到张图的时候有那么一点问题,join的源码中是调用wait的,wait既然是释放锁的,那么join不应该也是释放锁的嘛?==

参考文献

java 线程方法join的简单总结

Java终止线程的三种方式

线程的状态图详解

Internet地址

1.InetAddress类

public class InetAddressTest {
    public static void main(String[] args) {
        try {
            InetAddress address = InetAddress.getByName("www.baidu.com");//根据主机名查询
            System.out.println(address.getHostName());//主机名
            System.out.println(address.getHostAddress());//IP
          	//和C不一样,java没有无符号这种基本数据类型,大于127的字节会被当作负数,所以想得到常规的点分十进制,需要自己转化成int
          	System.out.println(Arrays.toString(address.getAddress()));//IP byte数组
            System.out.println(address);//主机名+IP
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}
/*
www.baidu.com
36.152.44.96
[36, -104, 44, 96]
www.baidu.com/36.152.44.96
*/

2.Object类

//对于这两个方法,InetAddress里重载了,主要针对IP地址,和其他类有点不太一样,注意一下
public boolean equals(Object obj) {
        return (obj != null) && (obj instanceof Inet4Address) &&
            (((InetAddress)obj).holder().getAddress() == holder().getAddress());
}
public int hashCode() {
        return holder().getAddress();
}

3.小练习(有bug)

//解析web日志,但getByName这个方法有点问题,无法通过ip反查出域名,还有待研究。。。
public class LookupTask implements Callable<String> {
    private String line;

    public LookupTask(String line) {
        this.line = line;
    }

    @Override
    public String call() throws Exception {
        int index = line.indexOf(' ');
        String ipAddress = line.substring(0, index);
        String result = line.substring(index);
        String hostName = InetAddress.getByName(ipAddress).getCanonicalHostName();
        System.out.println(hostName);
        return hostName + result;
    }
}
public class PoolWebLog {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(4);
        Queue<LogEntry> logQueue = new LinkedList<>();
        try (FileReader fr = new FileReader("in.txt");
             BufferedReader bis = new BufferedReader(fr)) {
            String str = null;
            while ((str = bis.readLine()) != null) {
                LogEntry entry = new LogEntry(str, pool.submit(new LookupTask(str)));
                logQueue.add(entry);
            }
            for (LogEntry log : logQueue)
                System.out.println(log.future.get());
            pool.shutdown();

        } catch (IOException | ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class LogEntry {
        String original;
        Future<String> future;

        public LogEntry(String original, Future<String> future) {
            this.original = original;
            this.future = future;
        }
    }
}

URL&URI

1.基本概念

URI 是统一资源标识符,而 URL 是统一资源定位符

URI主要用于标识一个网络资源,类比于现实世界中的身份证,强调给资源命名

URL主要用给网络资源定位,类比于现实世界的地址,强调给资源定位

大多数情况下给一个网络资源命名和定位太麻烦,所以干脆就把地址既当作地址,又当作资源标记用,这个地址就是URL

# Java  IO 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×