Java基础——多线程 & 网络编程

一、多线程

1. 进程 & 线程

1.1 进程

  • 进程:正在运行的应用长须
  • 是系统进行资源分配和调用的独立单位
  • 每一个进程都有它自己的内存空间和系统资源

1.2. 线程

  • 线程:是进程中的单个顺序控制流,是一条执行路径
  • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
  • 多线程:一个进程如果有多条执行路径,则称为多线程程序

2. 实现多线程<两种方式>

2.1 继承Thread类<方式一>

2.1.1 方法
方法名说明
void run()在线程开启后,此方法将被调用执行
void start()使此线程开始执行,Java虚拟机会调用run方法()
2.1.2 实现步骤
  • 定义一个类MyThread 继承 Thread类
  • 在MyThread类中重写 run() 方法
    • run() 是用来封装被线程执行的代码
    • 如果直接调用,相当于普通方法的调用
  • 创建MyThread类的对象
  • 启动线程 start()
    • 启动线程,然后由JVM调用此线程的run()方法
2.1.3 代码示例
  • MyThread类
package com.wang;
//定义一个类MyThread继承 Thread类
public class MyThread extends Thread{
    //重写run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
  • MyThreadDemo类
public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();
/*  区别run() 与 start()
        run()方法的调用并没有启动此线程
        my1.run();
        my2.run();
*/
        //void start () 导致此线程开始执行;Java虚拟机调用此线程的run()方法
        my1.start();
        my2.start();
    }
}

2.2 设置 & 获取线程名称

2.2.1 相关方法
方法名说明
void setName(String name)将此线程的名称更改为等于参数name
String getName()返回此线程的名称
Thread currentThread()返回对当前正在执行的线程对象的引用
2.2.2 代码示例
  • 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);
        }
    }
}
  • MyThreadDemo
public class MyThreadDemo {
    public static void main(String[] args) {
/*      //无参构造方法
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.setName("高铁");
        my2.setName("飞机");*/
/*        //带参构造方法
        MyThread my1 = new MyThread("高铁");
        MyThread my2 = new MyThread("飞机");

        my1.start();
        my2.start();*/

        //静态方法 currentThread 返回对当前正在执行的线程对象的引用
        //Thread.currentThread().getName() 返回当前线程名称
        System.out.println(Thread.currentThread().getName());  //main
    }
}

2.3 线程优先级

2.3.1 线程调度
  • 分时调度模型:所有线程 轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一 个,优先级高的线程获取的 CPU 时间片相对多一些
    • 注意:并不是优先级最高就一定能获取到CPU时间片
    • Java使用的就是 抢占式调度模型
    • 具有随机性
      • 假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也 就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一 定的
2.3.2 相关方法
方法名说明
inal int getPriority()返回此线程的优先级
final void setPriority(int newPriority)更改此线程的优先级 线程默认优先级是5;线程优先级的范围 是:1-10
2.3.3 代码示例
  • ThreadPriority类
public class ThreadPriority extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}
  • ThreadPriorityDemo类
public class ThreadPriorityDemo {
    public static void main(String[] args) {

        ThreadPriority t1 = new ThreadPriority();
        ThreadPriority t2 = new ThreadPriority();
        ThreadPriority t3 = new ThreadPriority();
        //设置线程名称
        t1.setName("汽车");
        t2.setName("高铁");
        t3.setName("飞机");
        //获取线程优先级
        System.out.println(t1.getPriority()); // 默认的优先级为5
        System.out.println(Thread.MAX_PRIORITY);//最高优先级 10
        System.out.println(Thread.MIN_PRIORITY);//最低优先级 1
        //设置线程优先级
        //t1.setPriority(99); //IllegalArgumentException 表示一种方法已经通过了非法或不正确的参数
        t1.setPriority(10);
        //线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果
        System.out.println(t1.getPriority()); 

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

2.4 线程控制

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

2.5 线程的生命周期

image-20210824204027604

2.6 实现Runnable接口<方式二>

2.6.1 实现步骤
  • 定义一个类 MyRunnable 实现 Runnable接口
  • 在 MyRunnable类中重写 run() 方法
  • 创建MyRunnable类的对象
  • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
  • 启动线程
2.6.2 Thread的构造方法
方法名说明
Thread(Runnable target)分配一个新的Thread对象
Thread(Runnable target, String name)分配一个新的Thread对象
2.6.3 代码示例
  • MyRunnable类
//继承Runnable接口
public class MyRunnable implements Runnable{
    //重写run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
  • MyRunnableDemo类
public class MyRunnableDemo {
    public static void main(String[] args) {
        //创建MyRunnable类的对象
        MyRunnable my = new MyRunnable();
        //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
        Thread t1 = new Thread(my,"飞机");
        Thread t2 = new Thread(my,"汽车");
        //启动线程
        t1.start();
        t2.start();
    }
}
2.6.4 优势
  • 避免了Java单继承的局限性
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想

3. 线程同步

3.1 数据安全问题

3.1.1 问题出现的条件
  • 是多线程环境
  • 有共享数据
  • 有多条语句操作共享数据
3.1.2 解决问题的基本思想
  • 让程序没有安全问题
    • 三个条件中缺少一个条件,就不会出现数据安全问题

3.2 同步代码块

3.2.1 基本格式
synchronized (任意对象){
    多条语句操作共享数据的代码
}
  • synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
3.2.2 优点 & 缺点
  • 优点:解决了 多线程 的 数据安全问题

  • 缺点:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的 运行效率

3.3 同步方法

3.3.1 基本格式
  • 同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {
    方法体;
}
  • 静态同步方法
修饰符 static synchronized 返回值类型 方法名(方法参数) {
	方法体;
}
3.3.2 注意事项
  • 同步方法的锁(任意对象)为 this
  • 同步静态方法的锁为(任意对象) 类名.class

3.4 线程安全的类

  • StringBuffer

    • 线程安全,可变的字符序列

    • 从版本JDK 5开始,被StringBuilder 替代。通常应该使用StringBuilder类,因为它支持所有相同的操 作,但它更快,因为它不执行同步

  • Vector

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

    • 该类实现了一个哈希表,它将键映射到值。 任何非null对象都可以用作键或者值
    • 从Java 2平台v1.2开始,该类进行了改进,实现了Map接口,使其成为Java Collections Framework的成 员。与新的集合实现不同,Hashtable被同步。 如果不需要线程安全的实现,建议使用HashMap代替 Hashtable
  • Collections工具类下的方法

    • 返回由指定列表支持的同步(线程安全)列表。

    • List<String> list = Collections.synchronizedList(new ArrayList<String>());
      

image-20210825085938894

3.5 Lock锁

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

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

3.5.2 构造方法
方法名说明
ReentrantLock()创建一个ReentrantLock的实例
3.5.3 加锁 & 解锁
方法名说明
void lock()获得锁
void unlock()释放锁
3.5.4 代码示例
  • SellTicket类
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//这里结合 售票案例说明
/*
假设有一百张票 同时在三个售票窗口出售
*/
public class SellTicket implements Runnable{
    //定义一百张票
    private int tickets = 100;
    //构造Lock 创建一个ReenTrantLock的实例
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try {
                //上锁
                lock.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            }finally {
                //释放锁
                lock.unlock();
            }
        }
    }
}
  • SellTicketDemo类
public class 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();
    }
}
3.5.5 注意事项
  • 获得锁 与 释放锁 结合try - finally使用,主要是防止在上锁后操作过程中出现异常,导致释放锁无法被执行

4. 生产者消费者模式

4.1 概述

  • 生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻

  • 所谓生产者消费者问题,实际上主要是包含了两类线程:

    • 一类是生产者线程用于生产数据
    • 一类是消费者线程用于消费数据
  • 为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

    • 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
    • 消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

    image-20210825092834819

4.2 相关方法

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

4.3 案例

  • 奶箱类(Box):即共享区域,定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
  public class Box {
      //定义一个成员变量
      private int milk;
      //定义一个成员变量,表示奶箱的状态
      private boolean state = false;
  
      public synchronized void put(int milk){
          //如果有牛奶,等待消费
          if (state){
              try {
                  wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          //如果没有牛奶 就生产牛奶
          this.milk = milk;
          System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");
  
          //生产完毕后,修改奶箱状态
          state = true;
          //唤醒其他等待的线程
          notifyAll();
      }
  
      public synchronized void get() {
          //如果没有牛奶 就等待
          if (!state){
              try {
                  wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          //如果有牛奶,就获取牛奶
          System.out.println("用户拿到第" + this.milk + "瓶奶");
          state = false;
          //唤醒其他等待的线程
          notifyAll();
      }
  }
  • 生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
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):实现Runnable接口,重写run()方法,调用获取牛奶的操作
public class Customer implements Runnable{
    private Box b;
    public Customer(Box b) {
         this.b = b;
    }

    @Override
    public void run() {
        while (true){
            b.get();
        }
    }
}
  • 测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下

    • 创建奶箱对象,这是共享数据区域
    • 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
    • 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
    • 创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
    • 启动线程
public class 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();
    }
}

二、网络编程

1. 计算机网络

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

2. 网络编程

2.1 概述

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

2.2 三要素

IP地址端口协议
要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数 据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区 分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序 了。也就是应用程序的标识通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定 的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则 被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守 才能完成数据交换。常见的协议有UDP协议和TCP协议

3. IP地址

3.1 概述

  • 网络中设备的 唯一标识

3.2 分类

  • IPV4
    • 是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每 个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是“11000000 10101000 00000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP地址经常被写成十进制 的形式,中间使用符号“.”分隔不同的字节。于是,上面的IP地址可以表示为“192.168.1.66”。IP地址的这 种表示法叫做“点分十进制表示法”,这显然比1和0容易记忆得多
  • IPV6
    • 由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发 紧张。为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16位一组,分成8组十六进制数,这样就解决了网络地址资源数量不够的问题

3.3 DOS常用命令

命令功能
ipconfig查看本机IP地址
ping IP地址检查网络是否连通

3.4 特殊IP地址

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

4. InetAddress类

4.1 概述

  • 表示Internet协议(IP)地址

4.2 相关方法

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

4.3 代码示例

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressDemo {
    public static void main(String[] args) throws UnknownHostException {
        InetAddress byName = InetAddress.getByName("localhost");
        System.out.println(byName); //localhost/127.0.0.1

        String hostName = byName.getHostName();
        String hostAddress = byName.getHostAddress();

        System.out.println("ip:" + hostAddress); //ip:127.0.0.1
        System.out.println("主机名:" + hostName); //主机名:localhost
    }
}

5. 端口

5.1 概述

  • 设备上 应用程序 的唯一标识

5.2 端口号

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

6. 协议

6.1 概述

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

6.2 UDP协议

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

6.3 TCP协议

  • 传输控制协议 (Transmission Control Protocol)
  • TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数 据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由 客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”
  • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠
    • 第一次握手,客户端向服务器端发出连接请求,等待服务器确认
    • 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求
    • 第三次握手,客户端再次向服务器端发送确认信息,确认连接
  • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性, TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等
  • 四次挥手:终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开

7. UDP通信程序

7.1 概述

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

7.2 发送数据

7.2.1 构造方法
方法名说明
DatagramSocket()创建数据报套接字并将其绑定到本机地址上的任 何可用端口
DatagramPacket(byte[] buf,int len,InetAddress add,int port)创建数据包,发送长度为len的数据包到指定主机 的指定端口
7.2.2 相关方法
方法名说明
void send(DatagramPacket p)发送数据报包
void close()关闭数据报套接字
void receive(DatagramPacket p)从此套接字接受数据报包
7.2.3 步骤
  • 创建发送端Socket对象(DatagramSocket)
    • DatagramSocket()
  • 创建数据,并把数据打包
    • DatagramPacket(byte[] buf, int length, InetAddress address, int port)
  • 调用DatagramSocket对象的方法发送数据
    • void send(DatagramPacket p)
  • 关闭发送端
    • void close()
7.2.4 代码示例
import java.io.IOException;
import java.net.*;
//UDP发送数据
public class SendDemo {
    public static void main(String[] args) throws IOException {
        //创建发送端的Socket对象
        DatagramSocket ds = new DatagramSocket();

        //创建数据,并把数据打包
        byte[] bys = "hello,udp,我来了".getBytes();
        int length = bys.length;
        InetAddress address = InetAddress.getByName("localhost");
        int port = 9999;
        DatagramPacket dp = new DatagramPacket(bys,length,address,port);
        //DatagramPacket dp = new DatagramPacket(bys,bys.length,InetAddress.getByName("localhost"),9999);
        //调用对象的方法发送数据
        ds.send(dp);
        //关闭发送端
        ds.close();
    }
}

7.3 接收数据

7.3.1 构造方法
方法名说明
DatagramPacket(byte[] buf, int len)创建一个DatagramPacket用于接收长度为len的数据包
7.3.2 相关方法
方法名说明
byte[] getData()返回数据缓冲区
int getLength()返回要发送的数据的长度或接收的数据的长度
7.3.3 步骤
  • 创建接收端Socket对象(DatagramSocket)
    • DatagramSocket(int port)
  • 创建一个数据包,用于接收数据
    • DatagramPacket(byte[] buf, int length)
  • 调用DatagramSocket对象的方法接收数据
    • void receive(DatagramPacket p)
  • 解析数据包,并把数据在控制台显示
    • byte[] getData()
    • int getLength()
  • 关闭接收端
    • void close()
7.3.4 代码示例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ReceivDemo {
    public static void main(String[] args) throws IOException {
        //创建接收端Socket对象
        DatagramSocket ds = new DatagramSocket(9999);
        //创建数据包,用于接收数据
        byte[] bys = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bys,bys.length);
        //调用方法接受数据
        ds.receive(dp);
        //解析数据包,并在数据台显示
        //返回数据缓冲区
        byte[] dates = dp.getData();
        //接受数据的长度
        int len = dp.getLength();
        String dateString = new String(dates,0,len);
        System.out.println("数据是:" + dateString);
        //System.out.println("数据是:" + new String(dates,0,dp.getLength()));
        //关闭接收端
        ds.close();

    }
}

7.4 案例

  • 需求
    • UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束
    • UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
  • 发送端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class SendTest {
    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){
            if ("886".equals(line)) {
                break;
            }
            byte[] bys = line.getBytes();
            DatagramPacket dp = new DatagramPacket(bys,bys.length, InetAddress.getByName("localhost"),9999);

            ds.send(dp);
        }
        ds.close();
    }
}
  • 接收端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class ReceiveTest {
    public static void main(String[] args) throws IOException {
        DatagramSocket ds = new DatagramSocket(9999);
        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()));
        }
    }
}

8. TCP通信程序

8.1 概述

  • Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过 Socket产生IO流来进行网络通信
  • Java为客户端提供了Socket类,为服务器端提供了ServerSocket类

8.2 发送数据

8.2.1 构造方法
方法名说明
Socket(InetAddress address,int port)创建流套接字并将其连接到指定IP指定端口号
Socket(String host, int port)创建流套接字并将其连接到指定主机上的指定端口号
8.2.2 相关方法
方法名说明
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流
8.2.3 步骤
  • 创建客户端的Socket对象(Socket)
    • Socket(String host, int port)
  • 获取输出流、写数据
    • OutputStream getOutputStream()
  • 释放资源
    • void close()
8.2.4 代码示例
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
//客户端
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        //创建客户端的Socket对象
        //Socket s = new Socket(InetAddress.getByName("localhost"), 9999);
        Socket s = new Socket("localhost", 9999);

        //获取输出流,写数据
        OutputStream os = s.getOutputStream();
        os.write("hello,tcp,我来了".getBytes());

        s.close();
    }
}

8.3 接收数据

8.3.1 构造方法
方法名说明
ServletSocket(int port)创建绑定到指定端口的服务器套接字
8.3.2 相关方法
方法名说明
Socket accept()监听要连接到此的套接字并接受它
8.3.3 步骤
  • 创建服务器端的Socket对象(ServerSocket)
    • ServerSocket(int port)
  • 监听客户端连接,返回一个Socket对象
    • Socket accept()
  • 获取输入流,读数据,并把数据显示在控制台
    • InputStream getInputStream()
  • 释放资源
    • void close()
8.3.4 代码示例
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
//服务器端
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(9999);

        Socket s = ss.accept();

        InputStream is = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String data = new String(bys,0,len);
        System.out.println(data);
        //后用先释放
        s.close();
        ss.close();
    }
}

8.4 注意事项

  • 要先启动服务器端,在启动客户端,否则会出现异常

8.5 案例

8.5.1 案例 1
8.5.1.1 需求
  • 客户端:发送数据,接收服务器反馈
  • 服务器:接收数据,给出反馈
8.5.1.2 代码
  • 客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
//客户端
public class ClientTest {
    public static void main(String[] args) throws IOException {
        //创建客户端对象 Socket
        Socket s = new Socket("localhost", 9999);
        //获取输出流,写数据
        OutputStream os = s.getOutputStream();
        os.write("这是第一个案例".getBytes());
        //接收服务器反馈
        InputStream is = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String data = new String(bys,0,len);
        System.out.println(data);
        //释放资源
        is.close();
        os.close();
        s.close();
    }
}
  • 服务器
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerTest {
    public static void main(String[] args) throws IOException {
        //创建服务器对象 ServerSocket
        ServerSocket ss = new ServerSocket(9999);
		//监听客户端连接,返回一个Socket对象
        Socket s = ss.accept();
        //获取输入流
        InputStream is = s.getInputStream();
        //读数据 并把数据显示在控制台
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String data = new String(bys,0,len);
        System.out.println(data);
        //给出反馈
        OutputStream os = s.getOutputStream();
        os.write("数据已经收到".getBytes());

        ss.close();
    }
}
8.5.2 案例 2
8.5.2.1 需求
  • 客户端:数据来自键盘录入,知道输入的数据是886,发送数据结束
  • 服务器:接收到的数据在控制台输出
8.5.2.2 代码
  • 客户端
import java.io.*;
import java.net.Socket;
//客户端
public class ClientTest {
    public static void main(String[] args) throws IOException {
        //创建Socket对象
        Socket s = new Socket("localhost", 9999);
        //创建输入流对象 数据来自键盘录入 System.in
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        //封装输出流对象
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
        String line;
        while ((line = br.readLine())!=null){
            //如果输入的数据是"886" 就跳出while循环 
            if ("886".equals(line)){
                break;
            }
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        //释放资源
        bw.close();
        br.close();
        s.close();
    }
}
  • 服务器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
//服务器
public class ServerTest {
    public static void main(String[] args) throws IOException {
        //创建 ServerSocket 对象
        ServerSocket ss = new ServerSocket(9999);
        //监听客户端连接,返回一个对应的Socket对象
        Socket s = ss.accept();
        //InputStream is = s.getInputStream();
        //获取输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String line;
        while ((line = br.readLine())!=null){
            System.out.println(line);
        }
        //释放资源
        br.close();
        s.close();
        ss.close();
    }
}
8.5.3 案例 3
8.5.3.1 需求
  • 客户端:数据来自键盘录入,知道输入的数据是886,发送数据结束
  • 服务器:接收数据,将接收到的数据写入文本文档
8.5.3.2 代码
  • 客户端
import java.io.*;
import java.net.Socket;
//客户端
public class ClientTest {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("localhost", 9999);

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
        String line;
        while ((line = br.readLine())!=null){
            if ("886".equals(line)){
                break;
            }
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        s.close();
    }
}
  • 服务器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerTest {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(9999);

        Socket s = ss.accept();
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        //把数据写入文本文件
        BufferedWriter bw = new BufferedWriter(new FileWriter("demo\\s.txt"));

        String line;
        while ((line = br.readLine())!=null){
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        bw.close();
        br.close();
        ss.close();
    }
}
8.5.4 案例 4
8.5.4.1 需求
  • 客户端:数据来自于文本文件
  • 服务器:接收到的数据写入文本文件
8.5.4.2 代码
  • 客户端
import java.io.*;
import java.net.Socket;

public class ClientTest {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("localhost", 9999);

        BufferedReader br = new BufferedReader(new FileReader("demo\\src\\com\\wang\\tcptest\\ClientTest.java"));

        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();
    }
}
  • 服务器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerTest {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(9999);

        Socket s = ss.accept();
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));

        BufferedWriter bw = new BufferedWriter(new FileWriter("demo\\s.txt"));
        String line;
        while ((line = br.readLine())!= null){
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        bw.close();
        ss.close();
    }
}
8.5.5 案例 5
8.5.5.1 需求
  • 客户端:数据来自于文本文件,接收服务器反馈
  • 服务器:接收到的数据写入文本文件,给出反馈
8.5.5.2 代码
  • 客户端
import java.io.*;
import java.net.Socket;

public class ClientTest {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("localhost",9999);

        BufferedReader br = new BufferedReader(new FileReader("demo\\src\\com\\wang\\tcpdemo\\ClientDemo.java"));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

        String line;
        while ((line = br.readLine())!=null){
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        //shutdownOutput方法 告诉服务器端 数据写入完成
        s.shutdownOutput();
        BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String data = brClient.readLine();
        System.out.println(data);


        //s.close();
    }
}
  • 服务器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerTest {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(9999);

        Socket s = ss.accept();
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        BufferedWriter bw = new BufferedWriter(new FileWriter("demo\\ss.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("文件上传成功");
        bwServer.newLine();
        bwServer.flush();
        //ss.close();
    }
}
8.5.6 案例 6
8.5.6.1 需求
  • 客户端:数据来自于文本文件,接收服务器反馈
  • 服务器:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程
  • 多线程类:在run()方法中读取客户端发送的数据,并且用计数器给文件名编号防止文件重名,接受结束后给客户端反馈信息
8.5.6.2 代码
  • 客户端
import java.io.*;
import java.net.Socket;

public class ClientTest {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("localhost", 9999);

        BufferedReader br = new BufferedReader(new FileReader("demo\\src\\com\\wang\\tcptest\\ClientTest.java"));
        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 bwServer = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String data = bwServer.readLine();
        System.out.println(data);

        bw.close();
        bwServer.close();
        s.close();
    }
}
  • 服务器
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerTest {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(9999);
        while(true){
            Socket s = ss.accept();

            new Thread(new ServerThread(s)).start();
        }
    }
}
  • 多线程类
import java.io.*;
import java.net.Socket;
//d
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()));

            int count = 0;
            File file = new File("demo\\sss["+count+"].txt");
            while (file.exists()){
                count++;
                file = new File("demo\\sss["+count+"].txt");
            }

            BufferedWriter bw = new BufferedWriter(new FileWriter(file));
            String line;
            while ((line = br.readLine())!= null){
                bw.write(line);
                bw.newLine();
                bw.flush();
            }
            BufferedWriter bwClient = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            bwClient.write("文件上传成功");
            bwClient.newLine();
            bwClient.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
8.5.7 总结
  • 问题
    • 在案例 5 中,出现了程序一直等待的问题,,服务器端一直在等待接收数据,客户端一直在等待服务器端的反馈数据
  • 原因
    • 读数据的方法是阻塞式的
  • 解决
    • 自定义结束标记;
      • 在客户端定义一个标记,表示客户端完成了写数据
      • 如果服务器端收到886这个标记,就开始读数据
      • 缺点:如果文本中有这个标记,会导致标记后的内容丢失
    • 使用shutdownOutput()方法
  • 扩展方法
方法名说明
shutdownOutput()禁用此套接字的输出流。
shutdownInput()将此套接字的输入流放置在“流的末尾”。

参考视频:黑马程序员全套Java教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值