实现多线程

进程与线程

  1. 进程:是正在运行的程序

    • 是系统进行资源分配和调用的独立单位
    • 每一个进程都有它自己的内存空间和系统资源
  2. 线程:是进程中的单个顺序控制流,是一条执行路径

    • 单线程:一个进程如果只有一条执行路径,则称之为单线程程序,例如记事本
    • 多线程:一个进程如果有多条执行路径,则称之为多线程程序,例如扫雷

多线程的实现方式

方法一:继承Thread

  • 定义一个类MyThread继承Thread
  • MyThread类中重写run()方法
  • 创建MyThread类的对象
  • 启动线程

两个注意点:

  • 为什么要重写run()方法?

    • 因为run()是用来封装被线程执行的代码
  • run()方法和start()方法的区别?

    • run():封装线程执行的代码,直接调用,相当于普通方法的调用
    • start():启动线程,然后由JVM调用此线程的run()方法

MyThreadDemo

  public static void main(String[] args) {
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();
    //void start():导致此线程开始执行,java虚拟机调用此线程的run方法
    my1.start();
    my2.start();
  }

MyThread

public class MyThread extends Thread {

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

设置和获取线程的名称

Thread类中设置和获取线程名称的方法:

  • void setName(String name):将此线程的名称更改为等于参数name
  • String getName():返回此线程的名称
  • 通过构造方法也可以设置线程名称

如何获取main()方法所在的线程名称?

  • public static Thread currentThread():返回当前正在执行的线程对象的引用

MyThread

public class MyThread extends Thread {

  public MyThread() {
  }

  public MyThread(String name) {
    super(name);
  }

  @Override
  public void run() {
    for (int i = 0; i < 100; i++) {
      System.out.println(getName() + ":" + i);
    }
  }
}
/*    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

        public final String getName() {
        return name;
    }

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

        public Thread(String name) {
        init(null, null, name, 0);
    }
*/

MyThreadDemo

  public static void main(String[] args) {
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();
    //void start():导致此线程开始执行,java虚拟机调用此线程的run方法
    my1.setName("YuKi");
    my2.setName("XP");
    MyThread my3 = new MyThread("XianKe");
    my1.start();
    my2.start();
    my3.start();
    //static Thread currentThread():返回对当前正在执行的线程对象的引用
    System.out.println(Thread.currentThread().getName());
  }

线程调度

线程有两种调度模型

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些

Java使用的是抢占式调度模型

假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令,所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

Thread类中设置和获取线程优先级的方法:

  • public final int getPriority():返回此线程的优先级
  • public final void setPriority(int newPriority):更改此线程的优先级

线程默认优先级是5,线程优先级的范围是:1-10,线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果

ThreadPriority

public class ThreadPriority extends Thread {

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

ThreadPriorityDemo

  public static void main(String[] args) {
    ThreadPriority tp1 = new ThreadPriority();
    ThreadPriority tp2 = new ThreadPriority();
    ThreadPriority tp3 = new ThreadPriority();
    tp1.setName("train");
    tp2.setName("plain");
    tp3.setName("ship");
    //public final int getPriority():返回此线程的优先级
    System.out.println(tp1.getPriority());
    System.out.println(tp2.getPriority());
    System.out.println(tp3.getPriority());
    //public final void setPriority(int newPriority):更改此线程的优先级
    System.out.println(Thread.MIN_PRIORITY);
    System.out.println(Thread.MAX_PRIORITY);
    System.out.println(Thread.NORM_PRIORITY);
    tp1.setPriority(5);
    tp2.setPriority(10);
    tp3.setPriority(1);
    System.out.println("-----");
    tp1.start();
    tp2.start();
    tp3.start();
  }

线程控制

方法名说明
static void sleep(long millis)使当前正在执行的线程停留(暂停执行)指定的毫秒数
void join()等待这个线程死亡
void setDeamon(boolean on)将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

ThreadSleep

public class ThreadSleep extends Thread {

  @Override
  public void run() {
    for (int i = 0; i < 100; i++) {
      System.out.println(getName() + ":" + i);
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

ThreadSleepDemo

  public static void main(String[] args) {
    ThreadSleep ts1 = new ThreadSleep();
    ThreadSleep ts2 = new ThreadSleep();
    ThreadSleep ts3 = new ThreadSleep();
    ts1.setName("YuKi");
    ts2.setName("XianKe");
    ts3.setName("XP");
    ts1.start();
    ts2.start();
    ts3.start();
  }

ThreadJoin

public class ThreadJoin extends Thread {

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

ThreadJoinDemo

  public static void main(String[] args) {
    ThreadJoin tj1 = new ThreadJoin();
    ThreadJoin tj2 = new ThreadJoin();
    ThreadJoin tj3 = new ThreadJoin();
    tj1.setName("YuKi");
    tj2.setName("XianKe");
    tj3.setName("XP");
    tj1.start();
    try {
      tj1.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    tj2.start();
    tj3.start();
  }

ThreadDaemon

public class ThreadDaemon extends Thread {

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

ThreadDaemonDemo

  public static void main(String[] args) {
    ThreadDaemon td1 = new ThreadDaemon();
    ThreadDaemon td2 = new ThreadDaemon();
    td1.setName("YuKi");
    td2.setName("XianKe");
    //设置主线程为XP
    Thread.currentThread().setName("XP");
    //设置守护线程
    td1.setDaemon(true);
    td2.setDaemon(true);
    td1.start();
    td2.start();
    for (int i = 0; i < 10; i++) {
      System.out.println(Thread.currentThread().getName() + ":" + i);
    }
  }

线程生命周期

graph LR
    A[创建线程对象]
    B[有执行资格没有执行权]
    C[有执行资格有执行权]
    D[没有执行资格没有执行权]
    E[线程死亡变成垃圾]
    A-->|"start()"|B
    B-->|抢到CPU的执行权|C
    C-->|其他线程抢走CPU的执行权|B
    C-->|"sleep()等其他阻塞方法"|D
    D-->|"sleep()方法时间到了或者阻塞结束"|B
    C-->|"run()结束或者stop()"|E

多线程的实现方式

方法二:实现Runnable接口

  • 定义一个类MyRunnable实现Runnable接口
  • MyRunnable类中重写run()方法
  • 创建MyRunnable类的对象
  • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
  • 启动线程

多线程的实现方案有两种

  • 继承Thread
  • 实现Runnable接口

相比继承Thread类,实现Runnable接口的好处

  • 避免了Java单继承的局限性
  • 适合多个相同的代码去处理同一个资源的情况,把线程和程序的代码,数据有效的分离,较好的体现了面向对象的设计思想

MyRunnable

public class MyRunnable implements Runnable {

  @Override
  public void run() {
    for (int i = 0; i < 100; i++) {
      //注意这里不能直接使用getName()方法,因为它实现的是Runnable接口
      System.out.println(Thread.currentThread().getName() + ":" + i);
    }
  }
}

MyRunnableDemo

  public static void main(String[] args) {
    //创建Myrunnable类的对象
    MyRunnable my = new MyRunnable();
    //创建Thread类的对象,把Myrunnable对象作为构造方法的参数
    Thread t1 = new Thread(my, "YuKi");
    Thread t2 = new Thread(my, "XianKe");
    //启动线程
    t1.start();
    t2.start();
  }

线程同步

案例:卖票

需求:某电影院目前正在上映电影,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

思路:

  1. 定义一个类SellTicket接口,里面定义一个成员变量:private int tickets = 100;
  2. SellTicket类中重写run()方法实现卖票,代码步骤如下

    • 判断票数大于0,就卖票,并告知是哪个窗口卖的
    • 卖了票之后,总票数减1
    • 票没有了,也有可能有人来问,所以这里用死循环让卖票的动作一直执行
  3. 定义一个测试类SellTicketDemo,里面有main()方法,代码步骤如下

    • 创建SellTicket类的对象
    • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
    • 启动线程

SellTicket

public class SellTicket implements Runnable {

  private int tickets = 100;

  @Override
  public void run() {
    while (true) {
      if (tickets > 0) {
        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "票");
        tickets--;
      }
    }
  }
}

SellTicketDemo

  public static void main(String[] args) {
    SellTicket st = new SellTicket();
    Thread t1 = new Thread(st, "1号窗口");
    Thread t2 = new Thread(st, "2号窗口");
    Thread t3 = new Thread(st, "3号窗口");
    t1.start();
    t2.start();
    t3.start();
  }

卖票案例的思考

我们先前写的代码看上去是没有什么问题的,但当我们仔细观察输出的时候,能发现其中有一些问题,所以接下来我们去修改卖票程序里的出票时间,每一次出票的时间为100毫秒,用sleep()方法
SellTicket

public class SellTicket implements Runnable {

  private int tickets = 100;

  @Override
  public void run() {
    while (true) {
      if (tickets > 0) {
        try {
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "票");
        tickets--;
      }
    }
  }
}

卖票出现了问题

  • 相同的票出现了好几次
  • 出现了票数为负数的票

出问题的原因

  • 线程执行的随机性导致的

卖票案例数据安全问题的解决

为什么会出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

如何解决多线程安全问题?

  • 基本思想:让程序没有安全问题的环境

实现方法

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能用一个线程执行即可
  • Java提供了同步代码块的方式来解决

同步代码块

锁多条语句操作共享数据,可以使用同步代码块实现

  • 格式:
  synchronized (任意对象){
    多条语句操作共享数据的代码
  }
  • synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

SellTicket

public class SellTicket implements Runnable {

  private int tickets = 100;
  //因为是任意对象,但不能有多个,所以我们new一个唯一且只能一次用一下的obj
  private Object obj = new Object();

  @Override
  public void run() {
    while (true) {
      synchronized (obj) {
        if (tickets > 0) {
          //因为电脑CPU性能太高的原因,如果使用sleep加锁,就算我把时间调到了1000
          //每一次都还是1号窗口抢占到资源,所以这里我把sleep注释了,各位根据自我条件可以放开sleep
//          try {
//            Thread.sleep(100);
//          } catch (InterruptedException e) {
//            e.printStackTrace();
//          }
          System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "票");
          tickets--;
        }
      }
    }
  }
}

同步的好处与弊端

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

同步方法

同步方法:就是把synchronized关键字加到方法上

  • 格式:

        修饰符synchronized返回值类型 方法名(方法参数) {
        }

同步方法的锁对象是什么?

  • this
public class SellTicket implements Runnable {

  private int tickets = 100;
  //因为是任意对象,但不能有多个,所以我们new一个唯一且只能一次用一下的obj
  private Object obj = new Object();
  private int x = 0;

  @Override
  public void run() {
    while (true) {
      if (x % 2 == 0) {
        //注意我这里将obj改为了this
        synchronized (this) {
          if (tickets > 0) {
            //因为电脑CPU性能太高的原因,如果使用sleep加锁,就算我把时间调到了1000
            //每一次都还是1号窗口抢占到资源,所以这里我把sleep注释了,各位根据自我条件可以放开sleep
//          try {
//            Thread.sleep(100);
//          } catch (InterruptedException e) {
//            e.printStackTrace();
//          }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "票");
            tickets--;
          }
        }
      } else {
        Ticket();
      }
    }
  }

  private synchronized void Ticket() {
    if (tickets > 0) {
      //因为电脑CPU性能太高的原因,如果使用sleep加锁,就算我把时间调到了1000
      //每一次都还是1号窗口抢占到资源,所以这里我把sleep注释了,各位根据自我条件可以放开sleep
//          try {
//            Thread.sleep(100);
//          } catch (InterruptedException e) {
//            e.printStackTrace();
//          }
      System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "票");
      tickets--;
    }
  }
}

同步静态方法:就是把synchronized关键字加到静态方法上

  • 格式:
  修饰符 static synchronized 返回值类型 方法名(方法参数){
  }

同步静态方法的锁对象是什么?

  • 类名.class
public class SellTicket implements Runnable {

  private static int tickets = 100;
  //因为是任意对象,但不能有多个,所以我们new一个唯一且只能一次用一下的obj
  private Object obj = new Object();
  private int x = 0;

  @Override
  public void run() {
    while (true) {
      if (x % 2 == 0) {
        //这里运用反射,获取到了SellTicket的class对象
        synchronized (SellTicket.class) {
          if (tickets > 0) {
            //因为电脑CPU性能太高的原因,如果使用sleep加锁,就算我把时间调到了1000
            //每一次都还是1号窗口抢占到资源,所以这里我把sleep注释了,各位根据自我条件可以放开sleep
//          try {
//            Thread.sleep(100);
//          } catch (InterruptedException e) {
//            e.printStackTrace();
//          }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "票");
            tickets--;
          }
        }
      } else {
        Ticket();
      }
    }
  }

  private static synchronized void Ticket() {
    if (tickets > 0) {
      //因为电脑CPU性能太高的原因,如果使用sleep加锁,就算我把时间调到了1000
      //每一次都还是1号窗口抢占到资源,所以这里我把sleep注释了,各位根据自我条件可以放开sleep
//          try {
//            Thread.sleep(100);
//          } catch (InterruptedException e) {
//            e.printStackTrace();
//          }
      System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "票");
      tickets--;
    }
  }
}

线程安全的类

StringBuffer

  • 线程安全,可变的字符序列
  • 从版本JDK5开始,被StringBuilder替代,通常应该使用StringBuilder类,因为它支持所有相同的操作,但它更快,因为它不执行同步

Vector

  • 从Java2平台V1.2开始,该类改进了List接口,使其成为Java collections Framework的成员,与新的集合实现不同,Vector被同步,如果不需要线程安全的实现,建议使用ArrayList代替Vector

Hashtable

  • 该类实现了一个哈希表,它将键映射到值,任何非null对象都可以用作键或者值
  • 从Java2平台V1.2开始,该类进行了改进,实现了Map接口,使其成为Java collections Framework的成员,与新的集合实现不同,Hashtable被同步,如果不需要线程安全的实现,建议使用HashMap代替Hashtable
  public static void main(String[] args) {
    StringBuffer sb1 = new StringBuffer();
    StringBuilder sb2 = new StringBuilder();
    Vector<String> v1 = new Vector<>();
    ArrayList<String> array = new ArrayList<>();
    Hashtable<String, String> ht = new Hashtable<>();
    HashMap<String, String> hm = new HashMap<>();
    //public static <T> List<T> synchronizedList(List<T> list):返回由指定列表支持的同步(线程安全)列表
    List<String> list = Collections.synchronizedList(new ArrayList<String>());
  }

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock实现提供比使用synchronized()方法和语句可以获得更广泛的锁定操作

Lcok中提供了获得锁和释放锁的方法

  • void lock():获得锁
  • void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

  • ReentrantLock():创建一个ReentrantLock的实例

SellTicket

public class SellTicket2 implements Runnable {

  private int tickets = 100;
  private Lock lock = new ReentrantLock();

  @Override
  public void run() {
    while (true) {
      lock.lock();
      try {
        if (tickets > 0) {
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
          tickets--;
        }
      } finally {
        lock.unlock();
      }
    }
  }
}

生产者消费者

生产者消费者模式概述

生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻,所谓生产者消费者问题,实际上主要是包含了两类线程

  • 一类是生产者线程用于生产数据
  • 二类是消费者线程用于消费数据
  • 为了解藕生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
  • 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
  • 消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中,Object类的等待和唤醒方法

方法名说明
void wait()导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法
void notify()唤醒正在等待对象监视器的单个线程
void notifyAll()唤醒正在等待对象监视器的所有线程

生产者消费者案例

生产者消费者案例中包含的类:

  • 奶箱(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
  • 生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
  • 消费类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
  • 测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下

    1. 创建奶箱对象,这里共享数据区域
    2. 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
    3. 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
    4. 创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
    5. 启动线程

Box

public class Box {

  private int milk;
  private boolean state = false;

  public synchronized void put(int milk) {
    //如果有牛奶,则等待
    if (state) {
      try {
        wait();
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
    //如果没有牛奶,则生产牛奶
    this.milk = milk;
    System.out.println("送奶工将第" + this.milk + "瓶奶送入箱子内");
    //生产完毕后,修改牛奶状态
    state = true;
    //唤醒其他线程
    notify();
  }

  public synchronized void get() {
    if (state == false) {
      try {
        wait();
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
    //如果有牛奶,则消费牛奶
    System.out.println("用户拿到第" + this.milk + "瓶奶");
    //消费完毕之后,改变奶箱状态
    state = false;
    notify();
  }
}

Producer

public class Producer implements Runnable {

  private Box b;

  public Producer(Box b) {
    this.b = b;
  }

  @Override
  public void run() {
    for (int i = 1; i <= 5; i++) {
      b.put(i);
    }
  }

Customer

public class Customer implements Runnable{
private Box b;
  public Customer(Box b) {
    this.b=b;
  }

  @Override
  public void run() {
    while (true) {
      b.get();
    }
  }
}

BoxDemo

  public static void main(String[] args) {
    Box b = new Box();
    Producer p = new Producer(b);
    Customer c = new Customer(b);
    Thread t1 = new Thread(p);
    Thread t2 = new Thread(c);
    t1.start();
    t2.start();
  }

网络编程

网络编程入门

网络编程概述

计算机网络:是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统

网络编程:在网络通信协议下,实现网络互连的不同计算机上运行的程序间可以进行数据交换

网络编程三要素

ip地址

  • 要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接受数据的计算机和识别发送的计算机,而IP地址就是这个标识号,也就是设备的标识

端口

  • 网络的通信,本质上是两个应用程序的通信,每台计算机都有很多的应用程序,那么在网络通信中,如何区分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了,也就是应用程序的标识

协议

  • 通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样,在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式,传输速率,传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换,常见的协议有UDP协议和TCP协议

IP地址

常用命令

  • ipconfig:查看本机IP地址
  • ping IP地址:查看网络是否连通

特殊IP地址

  • 127.0.0.1:是回送地址,可以代表本机地址,一般用来测试使用

InetAddress的使用

为了方便我们对IP地址的获取和操作,Java提供了一个类InetAddress供我们使用

InetAddress:此类表示Internet协议(IP)地址

方法名说明
static InetAddress getByName(String host)确定主机名称的IP地址,主机名称可以是机器名称,也可以是IP名称
String getHostName()获取此IP地址的主机名
String getHostAddress()返回文本显示中的IP地址字符串

InetAddressDemo

  public static void main(String[] args) throws UnknownHostException {
    InetAddress address = InetAddress.getByName("YQHP-YuKis-MacBook-Pro.local");
    //这里其实个人更建议使用注释的方法进行查询,相当于不需要知道主机名或者IP就能查询出来
//    InetAddress localHost = InetAddress.getLocalHost();
    String name = address.getHostName();
    String ip = address.getHostAddress();
//    String name = localHost.getHostName();
//    String ip = localHost.getHostAddress();
    System.out.println("主机名" + name);
    System.out.println("IP地址" + ip);
  }

端口

端口:设备上应用程序的唯一标识

端口号:用两个字节表示的整数,它的取值范围是0~65535,其中0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号,如果端口号被另外一个服务或应用占用,会导致当前程序启动失败

协议

协议:计算机网络中,连接和通信的规则被称之为网络通信协议

UDP协议

  • 用户数据报协议(User Datagram Protocol)
  • UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接,简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据,由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频,视频和普通数据的传输
  • 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响,但是在使用UDP协议传输数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议

TCP协议

  • 传输控制协议(Transmission Control Protocol)
  • TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输,在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过三次握手
  • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务端之间的三次交互,以保证连接的可靠

    1. 第一次握手:客户端向服务端发出连接请求,等待服务器确认
    2. 第二次握手:服务端向客户端回送一个响应,通知客户端收到了连接请求
    3. 第三次握手:客户端再次向服务器端发送确认信息,确认连接
  • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了,由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如上传文件,下载文件,浏览网页等

UDP通信程序

UDP通信原理

UDP协议是一种不可靠的网络协议,它在通信的两端各建立一个Socket对象,但是这两个Socket只能发送,接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器端的概念,Java提供了DatagramSocket类作为基于UDP协议的Socket

UDP发送数据

发送数据的步骤

  1. 创建发送端的Socket对象(DatagramSocket)

    DatagramSocket()
  2. 创建数据,并把数据打包

    DatagramPacket(byte buf[], int length, InetAddress address, int port)
  3. 调用DatagramSocket对象的方法发送数据

    void send(DatagramPacket p)
  4. 关闭发送端

    void close()

UdpSendDemo

  public static void main(String[] args) throws IOException {
    //创建发送端对象
    DatagramSocket ds = new DatagramSocket();
    //创建数据并打包
    //public DatagramPacket(byte buf[], int length,InetAddress address, int port)
    byte[] bys = "Hello i'm YHQP-YuKi".getBytes();
    DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getLocalHost(), 10086);
    //调用发送方法
    ds.send(dp);
    //关闭发送端
    ds.close();
  }

UDP接收数据

接收数据的步骤

  1. 创建接收端的Socket对象(DatagramSocket)

    DatagramSocket(int port)
  2. 创建一个数据包,用于接收数据

    DatagramPacket(byte buf[], int length)
  3. 调用DatagramSocket对象的方法接收数据

    synchronized void receive(DatagramPacket p)
  4. 解析数据包,并把数据输出在控制台上

        byte[] getData()
      int getLength()
  5. 关闭接收端

    void close()

UdpReceiveDemo

  public static void main(String[] args) throws IOException {
    //DatagramSocket(int port)构造数据报套接字并将其绑定到本地主机的指定端口
    DatagramSocket ds = new DatagramSocket(10086);
    //创建一个数据包,用于接收
    //DatagramPacket(byte buf[], int length)构造一个DatagramPacket用于接收长度为length的数据包
    byte[] bys = new byte[1024];
    DatagramPacket dp = new DatagramPacket(bys, bys.length);
    //调用方法,收集数据
    ds.receive(dp);
    //解析数据包,并把数据输出在控制台上
//    byte[] d = dp.getData();
    //int getLength()返回要发送的数据的长度或接收到的数据的长度
//    int len = dp.getLength();
//    String datas = new String(d, 0, len);
//    System.out.println("数据是:" + datas);
    System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
    //关闭接收端
    ds.close();
  }

UDP通信程序练习

按照下面的要求实现程序

  • UDP发送数据:数据来自键盘录入,直到输入的数据是9527,发送数据结束
  • UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收

UdpSendDemo2

  public static void main(String[] args) throws IOException {
    //创建对象
    DatagramSocket ds = new DatagramSocket();
    //自己封装键盘录入数据
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    String line;
    while ((line = br.readLine()) != null) {
      //如果为9527则退出循环
      if (line.equals("9527")) {
        break;
      } else {
        //创建数据并打包
        byte[] bys = line.getBytes();
        DatagramPacket dp = new DatagramPacket(bys, bys.length,
            InetAddress.getLocalHost(), 10086);
        //调用发送数据方法
        ds.send(dp);
      }
    }
    //关闭发送端
    ds.close();
  }

UdpReceiveDemo2

  public static void main(String[] args) throws IOException {
    //创建接收对象
    DatagramSocket ds = new DatagramSocket(10086);
    while (true) {
      //创建一个数据包用于接收数据
      byte[] bys = new byte[1024];
      DatagramPacket dp = new DatagramPacket(bys, bys.length);
      //调用方法接收数据
      ds.receive(dp);
      //解析数据,并把数据输出在控制台上
      System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
    }
  }

TCP通信程序

TCP通信原理

TCP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket对象,从而在通信的两端形成网络虚拟链路,一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信,Java对基于TCP协议的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信,Java为客户端提供了Socket类,为服务器端提供了ServerSocket类

TCP发送数据

发送数据的步骤

  1. 创建客户端的Socket对象(Socket)

    Socket(String host, int port)
  2. 获取输出流,写数据

    OutputStream getOutputStream()
  3. 释放资源

    void close()

ClientDemo

  public static void main(String[] args) throws IOException {
    //创建客户端的对象
    Socket s = new Socket(InetAddress.getLocalHost(), 10000);
    //获取输出流,写数据
    //OutputStream getOutputStream()
    OutputStream os = s.getOutputStream();
    os.write("I am YQHP-YuKi".getBytes());
    //释放资源
    s.close();
  }

TCP接收数据

接收数据的步骤

  1. 创建服务器端的Socket对象(ServerSocket)

    ServerSocket(int port)
  2. 监听客户端链接,返回一个Socket对象

    Socket accept()
  3. 获取输入流,读数据,并把数据显示在控制台

    InputStream getInputStream()
  4. 释放资源

    void close()

ServerDemo

  public static void main(String[] args) throws IOException {
    //创建服务器端的对象
    //ServerSocket(int port)
    ServerSocket ss = new ServerSocket(10000);
    //监听客户端的连接
    //Socket accept()
    Socket s = ss.accept();
    //获取输入流,读数据,并把数据显示在控制台上
    InputStream is = s.getInputStream();
    //读数据
    byte[] bys = new byte[1024];
    int len;
    while ((len = is.read(bys)) != -1) {
      System.out.println("数据是:" + new String(bys, 0, len));
    }
    //释放资源
    s.close();
    ss.close();
  }

TCP通信程序练习

练习1

  • 客户端:发送数据,接收服务器反馈
  • 服务端;接收数据,给出反馈

ClientDemo2

  public static void main(String[] args) throws IOException {
    //创建客户端的对象
    Socket s = new Socket(InetAddress.getLocalHost(), 10000);
    //获取输出流
    OutputStream os = s.getOutputStream();
    os.write("Hello TCP i'm coming".getBytes());
    //客户端也有相应的变化,在发送完消息时,调用关闭输出流方法,然后打开输出流,等候服务端的消息
    //后续只能接收数据
    s.shutdownOutput();
    //接收服务器反馈
    InputStream is = s.getInputStream();
    byte[] bys = new byte[1024];
    int len;
    while ((len = is.read(bys)) != -1) {
      System.out.println("客户端:" + new String(bys, 0, len));
    }
    //释放资源
    s.close();
  }

ServerDemo2

  public static void main(String[] args) throws IOException {
    //创建服务器端的对象
    ServerSocket ss = new ServerSocket(10000);
    //监听客户端的连接
    Socket s = ss.accept();
    //获取输入流,读取数据
    InputStream is = s.getInputStream();
    byte[] bys = new byte[1024];
    int len;
    while ((len = is.read(bys)) != -1) {
      System.out.println("服务器:" + new String(bys, 0, len));
    }
    //给出反馈
    OutputStream os = s.getOutputStream();
    os.write("Copy that captain!".getBytes());
    //释放资源
    ss.close();
  }

练习2

  • 客户端:数据来自键盘录入,直到输入的数据是9527,发送数据结束
  • 服务端:接收到的数据在控制台输出

ClientDemo3

  public static void main(String[] args) throws IOException {
    //创建客户端对象
    Socket s = new Socket(InetAddress.getLocalHost(), 10000);
    //数据来自键盘,直到输入的数据是9527,发送数据结束
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    //创建键盘录入流,封装输出流对象
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
    String line;
    while ((line = br.readLine()) != null) {
      if (line.equals("9527")) {
        break;
      } else {
        //获取输出流对象
        bw.write(line);
        bw.newLine();
        bw.flush();
      }
    }
    //释放资源
    br.close();
    s.close();
  }

ServerDemo3

  public static void main(String[] args) throws IOException {
    //创建服务器端的对象
    ServerSocket ss = new ServerSocket(10000);
    //监听客户端的连接,返回对应的Socket对象
    Socket s = ss.accept();
    //获取输入流
    BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
    String line;
    while ((line = br.readLine()) != null) {
      System.out.println(line);
    }
    //释放资源
    ss.close();
  }

练习3

  • 客户端:数据来自于键盘录入,直到输入的数据是9527,发送数据结束
  • 服务端:接收到的数据写入文本文件

ServerDemo4

  public static void main(String[] args) throws IOException {
    //创建服务器Socket对象
    ServerSocket ss = new ServerSocket(10000);
    //监听客户端连接,返回一个对应的Socker对象
    Socket s = ss.accept();
    //接收数据
    BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
    //把数据写入文本文件操作
    BufferedWriter bw = new BufferedWriter(new FileWriter("UPDATE/fos.txt"));
    String line;
    while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine();
      bw.flush();
    }
    bw.close();
    ss.close();
  }

练习4

  • 客户端:数据来自于文本文件
  • 服务端:接收到的数据写入文本文件

ClientDemo5

  public static void main(String[] args) throws IOException {
    //创建客户端的连接对象
    Socket s = new Socket(InetAddress.getLocalHost(), 10000);
    //封装文本文件的数据
    BufferedReader br = new BufferedReader(new FileReader("UPDATE/information.txt"));
    //封装输出流写数据
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
    //读取文件,并发送出去
    String line;
    while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine();
      bw.flush();
    }
    //释放资源
    br.close();
    s.close();
  }

练习5

  • 客户端:数据来自于文件,接收服务器反馈
  • 服务端:接收到的数据写入文本文件,给出反馈
  • 出现问题:程序一直等待
  • 原因:读数据的方法是阻塞式的
  • 解决方法:自定义结束标记,使用shutdownOutput()方法

readline()阻塞原因有两种:

  1. 没有\r\n换行符
  2. 不知道流什么时候结束

解决分别通过加换行符和关闭流,所以在使用Socket通信的时候一定要注意readline()和read()的阻塞问题

ClientDemo6

  public static void main(String[] args) throws IOException {
    //创建客户端Socket对象
    Socket s = new Socket(InetAddress.getLocalHost(), 10000);
    //封装文本文件的数据
    BufferedReader br = new BufferedReader(new FileReader("UPDATE/information.txt"));
    //封装输出流写数据 
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
    String line;
    while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine();
      bw.flush();
    }
    //自定义结束标记
    s.shutdownOutput();
    //接收反馈
    BufferedReader brCilent = new BufferedReader(new InputStreamReader(s.getInputStream()));
    System.out.println("服务器的反馈:" + brCilent.readLine());
    //释放资源
    br.close();
    s.close();
  }

ServerDemo6

  public static void main(String[] args) throws IOException {
    //创建服务器ServerSocket对象
    ServerSocket ss = new ServerSocket(10000);
    //监听客户端,并返回一个对应的Socket对象
    Socket s = ss.accept();
    //接收数据
    BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
    //把数据写入文本文件
    BufferedWriter bw = new BufferedWriter(
        new FileWriter("UPDATE/fos.txt"));
    String line;
    //一直在等待读取数据,并不知道客户端那边已经写完了
    while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine();
      bw.flush();
    }
    //给出反馈
    BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
    bwServer.write("File is ok");
    bwServer.newLine();
    bwServer.flush();
    //释放资源
    bw.close();
    ss.close();
  }

练习6

  • 客户端:数据来自于文本文件,接收服务器反馈
  • 服务端:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程

ServerDemo7

  public static void main(String[] args) throws IOException {
    //创建服务端Socket对象
    ServerSocket ss = new ServerSocket(10000);
    while (true) {
      //监听客户端链接
      Socket s = ss.accept();
      //为每一个客户端开启一个线程
      new Thread(new ServerThread(s)).start();
    }
  }

ServerThread

public class ServerThread implements Runnable {

  private Socket s;

  public ServerThread(Socket s) {
    this.s = s;
  }

  @Override
  public void run() {
    //接收数据,写入文本文件
    try {
      BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
      //BufferedWriter bw = new BufferedWriter(new FileWriter("UPDATE/fos.txt"));
      //解决名称冲突问题,不然每次都会覆盖掉上一个同名的文件
      int count = 0;
      File file = new File("UPDATE/fos[" + count + "].txt");
      while (file.exists()) {
        count++;
        file = new File("UPDATE/fos[" + count + "].txt");
      }
      BufferedWriter bw = new BufferedWriter(new FileWriter(file));
      String line;
      while ((line = br.readLine()) != null) {
        bw.write(line);
        bw.newLine();
        bw.flush();
      }
      //给出反馈
      BufferedWriter bwServer = new BufferedWriter(
          new OutputStreamWriter(s.getOutputStream()));
      bwServer.write("Files are OK");
      bwServer.newLine();
      bwServer.flush();
      //释放资源
      s.close();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}
Last modification:April 20, 2022
If you think my article is useful to you, please feel free to appreciate