第十六章 网络编程多线程(TCP 线程的创建)
16.1 多线程的概念
16.1.1 概述
我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?
要解决上述问题,咱们得使用多进程或者多线程来解决
16.1.2 线程与进程
16.1.3 线程
进程与线程的区别
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
- 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
注意:下面内容为了解知识点
- 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
- Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个
线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。 - 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
16.2 线程的创建-继承方式
16.2.1 继承Thread类方式
Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把
run()方法称为线程执行体。 - 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
自定义线程类:
public class MyThread extends Thread {
@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 线程的执行流程
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。
多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
16.2.3 线程内存图
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
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方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的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类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
16.3.3 匿名内部类方式创建线程
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
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 软件结构
16.4.2 网络通信协议
- 网络通信协议:通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了
统一规定,通信双方必须同时遵守,最终完成数据交换。 - TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求
16.4.3 协议分类
通信的协议还是比较复杂的, java.net 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。
java.net 包中提供了两种常见的网络协议的支持:
- TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,
在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
- 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,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 ,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
常用命令
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)。
两端通信时步骤:
- 服务端程序,需要事先启动,等待客户端的连接。
- 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
在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通信分析图解
- 【服务端】启动,创建ServerSocket对象,等待连接。
- 【客户端】启动,创建Socket对象,请求连接。
- 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
- 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
- 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
- 【服务端】Socket对象,获取OutputStream,向客户端回写数据。
- 【客户端】Scoket对象,获取InputStream,解析回写数据。
- 【客户端】释放资源,断开连接。
16.5.5 简单的TCP通信案例
客户端向服务器发送数据–客户端程序
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("客户端 发送数据");
Socket client = new Socket("localhost", 6666);
OutputStream os = client.getOutputStream();
os.write("你好么? tcp ,我来了".getBytes());
os.close();
client.close();
}
}
客户端向服务器发送数据–服务器端程序
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器已经启动");
Socket accept = serverSocket.accept();
InputStream in = accept.getInputStream();
byte [] buff = new byte[1024];
int len = in.read(buff);
System.out.println(new String(buff,0,len));
in.close();
out.close();
accept.close();
}
客户端向服务器发送数据–客户端程序
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 6666);
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));
outputStream.close();
inputStream.close();
socket.close();
}
}
客户端向服务器发送数据–服务器端程序
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器已经启动");
Socket accept = serverSocket.accept();
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());
in.close();
out.close();
accept.close();
}
}
16.6 文件上传案例
16.6.1 文件上传案例
文件上传分析图解
- 【客户端】输入流,从硬盘读取文件数据到程序中。
- 【客户端】输出流,写出文件数据到服务端。
- 【服务端】输入流,读取文件数据到服务端程序。
- 【服务端】输出流,写出文件数据到服务器硬盘中。
- 【服务端】获取输出流,回写数据。
- 【客户端】获取输入流,解析回写数据。
16.6.2 文件上传客户端实现
public class Client {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("filedemo/1.jpeg");
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)
{
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();
}
}
}
总结
文件上传案例可以自己先做一遍再看答案哦~
|