第十六章 网络编程多线程(TCP 线程的创建)

Java基础16

  • 第十六章 网络编程多线程(TCP 线程的创建)
    • 16.1 多线程的概念
      • 16.1.1 概述
      • 16.1.2 线程与进程
      • 16.1.3 线程
    • 16.2 线程的创建-继承方式
      • 16.2.1 继承Thread类方式
      • 16.2.2 线程的执行流程
      • 16.2.3 线程内存图
      • 16.2.4 run()方法和start()方法
      • 16.2.5 线程名字的设置和获取
      • 16.2.6 获取运行main方法线程的名字
    • 16.3 线程的创建-实现方式
      • 16.3.1 实现Ruunable接口方式
      • 16.3.2 Thread和Runnable的区别
      • 16.3.3 匿名内部类方式创建线程
    • 16.4 网络编程入门
      • 16.4.1 软件结构
      • 16.4.2 网络通信协议
      • 16.4.3 协议分类
      • 16.4.4 网络编程三要素
        • 协议
        • IP地址
        • IP地址分类
        • 常用命令
        • 特殊的IP地址
        • 端口号
    • 16.5 TCP通信协议
      • 16.5.1 概述
      • 16.5.2 Socket类
        • 构造方法
        • 成员方法
      • 16.5.3 ServerSocket类
        • 构造方法
        • 成员方法
      • 16.5.4 简单的TCP网络程序
      • 16.5.5 简单的TCP通信案例
        • 客户端向服务器发送数据--客户端程序
        • 客户端向服务器发送数据--服务器端程序
        • 客户端向服务器发送数据--客户端程序
        • 客户端向服务器发送数据--服务器端程序
    • 16.6 文件上传案例
      • 16.6.1 文件上传案例
        • 文件上传分析图解
      • 16.6.2 文件上传客户端实现
      • 16.6.3 文件上传单线程服务器实现
  • 总结

第十六章 网络编程多线程(TCP 线程的创建)

16.1 多线程的概念

16.1.1 概述

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决

16.1.2 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

第十六章 网络编程多线程(TCP 线程的创建)_第1张图片

16.1.3 线程

第十六章 网络编程多线程(TCP 线程的创建)_第2张图片
进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

注意:下面内容为了解知识点

  1. 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
  2. Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个
    线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
  3. 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

16.2 线程的创建-继承方式

16.2.1 继承Thread类方式

Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把
    run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

自定义线程类:

public class MyThread extends Thread { 
	/*** 重写run方法,完成该线程执行的逻辑 */ 
	@Override public void run() { 
	for (int i = 0; i < 200; i++) { 
	System.out.println("自定义线程正在执行!"+i); 
	} 
	} 
	}

测试类:

    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();
        for (int i = 0; i < 200; i++) {
            System.out.println("main主线程"+i+" "+Thread.currentThread().getName());
        }
        }

16.2.2 线程的执行流程

第十六章 网络编程多线程(TCP 线程的创建)_第3张图片
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。

通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。

多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

16.2.3 线程内存图

第十六章 网络编程多线程(TCP 线程的创建)_第4张图片
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

16.2.4 run()方法和start()方法

  • run()方法,是线程执行的任务方法,每个线程都会调用run()方法执行,我们将线程要执行的任务代码都写在run()方法中就可以被线程调用执行。
  • start()方法,开启线程,线程调用run()方法。start()方法源代码中会调用本地方法start0()来启动线程:
    private native void start0() ,本地方法都是和操作系统交互的,因此可以看出每次开启一个线程的线程
    都会和操作系统进行交互。
    • 注意:一个线程只能被启动一次!!

16.2.5 线程名字的设置和获取

  • Thread类的方法 String getName() 可以获取到线程的名字
  • Thread类的方法 setName(String name) 设置线程的名字。
  • 通过Thread类的构造方法 Thread(String name) 也可以设置线程的名字。
public class MyThread extends Thread{ 
	public void run(){ 
		System.out.println("线程名字:"+super.getName()); } 
	}

测试类:

public class Demo { 
	public static void main(String[] args) { 
	//创建自定义线程对象 
	MyThread mt = new MyThread(); 
	//设置线程名字 
	mt.setName("旺财"); 
	//开启新线程 
	mt.start(); 
	} 
	}

注意:线程是有默认名字的,如果我们不设置线程的名字,JVM会赋予线程默认名字Thread-0,Thread-1。

16.2.6 获取运行main方法线程的名字

  • Demo类不是Thread的子类,因此不能使用getName()方法获取。
  • Thread类定义了静态方法 static Thread currentThread() 获取到当前正在执行的线程对象。
  • main方法也是被线程调用了,也是具有线程名字的。
public static void main(String[] args){ 
	Thread t = Thread.currentThread(); 	
	System.out.println(t.getName()); 
	}

16.3 线程的创建-实现方式

16.3.1 实现Ruunable接口方式

采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{ 
	public void run() { 
	for (int i = 0; i < 20; i++) { 
	System.out.println(Thread.currentThread().getName()+" "+i); 
	} 
	} 
	}
public class Demo { 
	public static void main(String[] args) { 
	//创建自定义类对象 线程任务对象 
	MyRunnable mr = new MyRunnable(); 
	//创建线程对象 
	Thread t = new Thread(mr); 
	t.start(); 
	for (int i = 0; i < 20; i++) 
	{ 
	System.out.println("main " + i); 
	} 
	} 
	}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程
编程的基础。

16.3.2 Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

16.3.3 匿名内部类方式创建线程

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:

public class NoNameInnerClassThread { 
	public static void main(String[] args) {
	// new Runnable(){ 
	// public void run(){ 
	// for (int i = 0; i < 20; i++) { 
	// System.out.println("热巴:"+i); 
	// } 
	// } 
	// }; 
	//---这个整体 相当于new MyRunnable() 
	Runnable r = new Runnable(){ 
		public void run(){ 
		for (int i = 0; i < 20; i++) 
		{ 
		System.out.println("热巴:"+i); } 
		} 
		};
		new Thread(r).start(); 
		for (int i = 0; i < 20; i++) { 
		System.out.println("娜扎:"+i); 
		} 
		} 
		}

16.4 网络编程入门

16.4.1 软件结构

  • C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
    第十六章 网络编程多线程(TCP 线程的创建)_第5张图片

  • B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等
    第十六章 网络编程多线程(TCP 线程的创建)_第6张图片
    两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。

16.4.2 网络通信协议

  • 网络通信协议:通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了
    统一规定,通信双方必须同时遵守,最终完成数据交换。
  • TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求

第十六章 网络编程多线程(TCP 线程的创建)_第7张图片

16.4.3 协议分类

通信的协议还是比较复杂的, java.net 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,
    在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
    • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
      • 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
      • 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
      • 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示
        第十六章 网络编程多线程(TCP 线程的创建)_第8张图片

完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

  • UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
    每次发送的数据最大为64kb。

16.4.4 网络编程三要素

协议
  • 协议:计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。
IP地址
  • IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。
IP地址分类
  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成 a.b.c.d 的形式,例如 192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。
  • 为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成 ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 ,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
常用命令
  • 查看本机IP地址,在控制台输入:
ipconfig
  • 检查网络是否连通,在控制台输入:
ping 空格 IP地址 
ping 220.181.57.216 
ping www.baidu.com
特殊的IP地址
  • 本机IP地址: 127.0.0.1 、 localhost 。
端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。

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

利用 协议 + IP地址 + 端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

16.5 TCP通信协议

16.5.1 概述

TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。

两端通信时步骤:

  1. 服务端程序,需要事先启动,等待客户端的连接。
  2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。

在Java中,提供了两个类用于实现TCP通信程序:

  • 客户端: java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  • 服务端: java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端
    的连接。

16.5.2 Socket类

Socket 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点.

构造方法
  • public Socket(String host, int port) :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。
Socket client = new Socket("127.0.0.1", 6666);
成员方法
  • public InputStream getInputStream() : 返回此套接字的输入流。

    • 如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。
    • 关闭生成的InputStream也将关闭相关的Socket。
  • public OutputStream getOutputStream() : 返回此套接字的输出流。

    • 如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。
    • 关闭生成的OutputStream也将关闭相关的Socket。
  • public void close() :关闭此套接字。

    • 一旦一个socket被关闭,它不可再使用。
    • 关闭此socket也将关闭相关的InputStream和OutputStream 。
  • public void shutdownOutput() : 禁用此套接字的输出流。

    • 任何先前写出的数据将被发送,随后终止输出流。

16.5.3 ServerSocket类

ServerSocket 类:这个类实现了服务器套接字,该对象等待通过网络的请求。

构造方法
  • public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
ServerSocket server = new ServerSocket(6666);
成员方法
  • public Socket accept() :侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

16.5.4 简单的TCP网络程序

TCP通信分析图解

  1. 【服务端】启动,创建ServerSocket对象,等待连接。
  2. 【客户端】启动,创建Socket对象,请求连接。
  3. 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
  4. 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
  5. 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
  6. 【服务端】Socket对象,获取OutputStream,向客户端回写数据。
  7. 【客户端】Scoket对象,获取InputStream,解析回写数据。
  8. 【客户端】释放资源,断开连接。

第十六章 网络编程多线程(TCP 线程的创建)_第9张图片

16.5.5 简单的TCP通信案例

客户端向服务器发送数据–客户端程序
public class ClientTCP { 
	public static void main(String[] args) throws Exception { 
	System.out.println("客户端 发送数据"); 
	// 1.创建 Socket ( ip , port ) , 确定连接到哪里. 
	Socket client = new Socket("localhost", 6666); 
	// 2.获取流对象 . 输出流 
	OutputStream os = client.getOutputStream(); 
	// 3.写出数据. 
	os.write("你好么? tcp ,我来了".getBytes()); 
	// 4. 关闭资源 . 
	os.close(); 
	client.close(); 
	} 
	}
客户端向服务器发送数据–服务器端程序
 public static void main(String[] args) throws IOException {
        //创建一个服务端serverSocket对象,指定服务器端口号
        ServerSocket serverSocket = new ServerSocket(6666);
        //开启服务器,等待客户端来连接,当有客户端连接时,得到连接客户端Socket对象
        System.out.println("服务器已经启动");
        Socket accept = serverSocket.accept();
        //通过客户端Socket对象,获取客户端发来的数据
        InputStream in = accept.getInputStream();
        byte [] buff = new byte[1024];
        //读取客户端发来的数据
        int len = in.read(buff);
        System.out.println(new String(buff,0,len));
        //关闭Socket对象
        in.close();
        out.close();
        accept.close();

    }
客户端向服务器发送数据–客户端程序
public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建一个客户端Socket对象,指定要连接的服务器地址
        Socket socket = new Socket("127.0.0.1", 6666);
        //2.获取当前Socket对象的输出流,发送数据给服务器
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello,server".getBytes());
        //接收服务器发来的数据
        InputStream inputStream = socket.getInputStream();
        byte[] b = new byte[1024];
        int read = inputStream.read(b);
        System.out.println(new String(b,0,read));
        //3.关闭客户端的Socket对象
        outputStream.close();
        inputStream.close();
        socket.close();
    }
}
客户端向服务器发送数据–服务器端程序
public class Server {
    public static void main(String[] args) throws IOException {
        //创建一个服务端serverSocket对象,指定服务器端口号
        ServerSocket serverSocket = new ServerSocket(6666);
        //开启服务器,等待客户端来连接,当有客户端连接时,得到连接客户端Socket对象
        System.out.println("服务器已经启动");
        Socket accept = serverSocket.accept();
        //通过客户端Socket对象,获取客户端发来的数据
        InputStream in = accept.getInputStream();
        byte [] buff = new byte[1024];
        //读取客户端发来的数据
        int len = in.read(buff);
        System.out.println(new String(buff,0,len));
        //返回数据给客户端
        OutputStream out = accept.getOutputStream();
        out.write("好的,你已经成功连接到我了".getBytes());
        //关闭Socket对象
        in.close();
        out.close();
        accept.close();

    }
}

16.6 文件上传案例

16.6.1 文件上传案例

文件上传分析图解
  1. 【客户端】输入流,从硬盘读取文件数据到程序中。
  2. 【客户端】输出流,写出文件数据到服务端。
  3. 【服务端】输入流,读取文件数据到服务端程序。
  4. 【服务端】输出流,写出文件数据到服务器硬盘中。
  5. 【服务端】获取输出流,回写数据。
  6. 【客户端】获取输入流,解析回写数据。

第十六章 网络编程多线程(TCP 线程的创建)_第10张图片

16.6.2 文件上传客户端实现

public class Client {
    public static void main(String[] args) throws IOException {
        //首先用字节输入流获取到要上传的文件
        FileInputStream fis = new FileInputStream("filedemo/1.jpeg");
        //定义客户端socket接收字节流数据
        Socket socket = new Socket("127.0.0.1",6666);
        OutputStream out = socket.getOutputStream();
        byte [] bytes = new byte[1024];
        int read;
        while((read = fis.read(bytes))!=-1)
        {
            //通过Socket输出流发送给服务端
            out.write(bytes,0,read);
        }
        socket.shutdownOutput();
        System.out.println("文件发送完毕");
        InputStream in = socket.getInputStream();
        byte [] by = new byte[1024];
        int read1 = in.read(by);
        System.out.println(new String(by,0,read1));
        fis.close();
        in.close();
        socket.close();
        out.close();
    }
}

16.6.3 文件上传单线程服务器实现

public class Server {
    public static void main(String[] args) throws IOException {
        //服务端接收客户端传入的数据
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器已经启动");
        while (true)
        {
            Socket socket = serverSocket.accept();
           //开启一个新线程与客户端数据交互
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        InputStream in = socket.getInputStream();
                        //定义字节输出流输出到指定文件
                        FileOutputStream fos = new FileOutputStream("img/"+System.currentTimeMillis()+".jpeg");
                        byte [] bytes = new byte[1024];
                        int len;
                        while ((len=in.read(bytes))!=-1)
                        {
                            fos.write(bytes,0,len);
                        }
                        OutputStream opt = socket.getOutputStream();
                        opt.write("文件上传成功".getBytes());
                        fos.close();
                        opt.close();
                        in.close();
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

     }
}

总结

文件上传案例可以自己先做一遍再看答案哦~

你可能感兴趣的:(java基础学习,javase,java,maven,intellij-idea)