C/S:客户端/服务器端,所有网络应用都是基于客户端服务器端进行的,Java写的是服务端,客户端是一个软件,服务端也是一个软件,两个软件之间交互;(只能连接对应的服务器)
B/S:浏览器/服务器端,它本质上也是C/S,只不过它的C是一个浏览器而已;(连接任意服务器都可以、百度、淘宝、京东等)
聊天室:两个程序A和B,一个是客户端、一个是服务端:谁发起连接,谁是客户端;谁接收连接,谁是服务端。
想连在一起需要网络,目的是为了传输数据,发送数据的过程中时需要遵守一些协议的:
TCP协议:可靠传输协议,发送数据后需要对方确认,对方确认后才能继续发送数据,再需要对方确认,100%所有数据发送过去,比较像打电话,需要接电话才能听见。虽然说数据完全可靠,但效率低下;
UDP:不可靠传输协议,只管发,不管对方能否接收到,有点像大广播,播了就播了,不管你听没听见。虽然不能保证数据100%都发送成功,但是效率比较高。
但是我们学习的时候学TCP,因为日后面对的客户端都是B(浏览器),而浏览器和客户端之间采用的协议是Http协议,Http协议要求地下客户端服务端传输时必须建立在可靠传输协议TCP上。
网络编程
什么是Socket编程
什么是Socket编程
-
java.net.Socket
是Java
提供的用于进行网络编程的API; -
Socket
编程可以让软件与软件之间以计算机作为载体,以网络作为信息流动的通道进行远端数据传递,达到交流的目的。
什么是计算机网络
- 计算机网络是指将具有独立功能的多台计算机通过通信线路连接起来,实现数据共享和交流的计算机系统。
什么是Socket
Socket
(套接字):网络通信的标准API、进行可靠的网络通讯。
Socket通讯
IP地址(楼)
端口(门)
IP+端口
客户端与服务端(C/S)
Socket通讯
ServerSocket
多客户端支持
如何接受多客户端
-
ServerSocket
提供方法:-
Socket accept()
:用于监听服务端口,一旦一个客户端连接即返回一个Socket
; -
多次调用该方法可以感知多个客户端的连接。
-
建立连接
建立连接
-
Server.java
package socket; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * 服务端 */ public class Server { /** * java.net.ServerSocket: * 运行在服务端,主要作用两个: * 1.向系统申请服务端口,客户端通过该端口与服务端建立连接 * 2.监听服务端口,一旦有客户端连接了,就会立即创建一个Socket对象与客户端进行交互 * 如果将Socket比喻为"电话",那ServerSocket就相当于客户中心的"总机" */ private ServerSocket serverSocket; public Server(){ try { System.out.println("正在启动服务器"); /** * 创建ServerSocket对象时,会申请一个端口 * 如果该端口被其它程序占用,会抛出异常: * java.net.BindException: Address already in use: * 解决办法: * 1.更换端口号 * 2.杀死占用该端口的进行(通常由于服务端启动两次导致) */ serverSocket = new ServerSocket(8088); System.out.println("服务器启动完毕"); } catch (IOException e) { e.printStackTrace(); } } /** * 该方法用于启动程序的执行 */ public void start(){ try { System.out.println("等待客户端连接..."); /* accept(): 用于接收客户端连接,并返回一个Scoket对象与所连接的客户端进行交互 该方法是一个阻塞方法,调用后程序会"卡住",直接一个客户端连接为止 */ Socket socket = serverSocket.accept(); System.out.println("一个客户端连接了!"); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(); server.start(); } }
-
Client.java
package socket; import java.io.IOException; import java.net.Socket; /** * 客户端 */ public class Client { /* java.net.Socket:套接字 它封装了TCP协议的通信细节,使用它可以和服务器建立连接并进行交互 */ private Socket socket; public Client(){ try { System.out.println("正在连接服务端..."); /* 实例化Socket对象,就是与远端计算机建立连接的过程 需要传入两个对数: 1.服务端的IP地址,用于找到网络上对方的计算机 2.服务端口,用于找到服务端计算机上的应用程序 查看IP操作: Windows: win+r,输入cmd,回车,输入ipconfig,IPv4就是IP地址 Mac:触控板上五指向中间抓,选择"终端"程序打开,输入/sbin/ifconfig查看IP地址 */ socket = new Socket("localhost",8088); System.out.println("服务端连接成功!"); } catch (IOException e) { e.printStackTrace(); } } /** * 该方法用于启动客户端程序的执行 */ public void start(){ } public static void main(String[] args) { Client client = new Client(); client.start(); } }
-
注意在尝试连接本地时要提前打开服务端
Server.java
,再连接自己,之后就可以通过其他人给出的端口号及IP地址连接其他人。
客户端发送文本数据
- 利用
IO
只是中的流连接,使用客户端的Socket
获取输出流进行流连接,利用PrintWriter
来实现发送一行字符串的操作。
Server.java
package day18;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* 服务端
*/
public class Server {
/**
* java.net.ServerSocket:
* 运行在服务端,主要作用有两个:
* 1.像系统申请服务端口,客户端通过该端口与服务端建立连接
* 2.监听服务端口,一旦有客户链接了,就会立即创建一个Socket对象与客户端进行交互
* 如果将Socket比喻为“电话”,那ServerSocket就相当于客户中心的“总机”。
* 解决方法:
* 1.更换端口号;
* 2.杀死占用该端口的进行(通常由于服务端启动了两次导致)
*/
private ServerSocket serverSocket;
public Server(){
try {
System.out.println("正在启动服务端......");
serverSocket = new ServerSocket(8888);
System.out.println("服务端启动完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 启动服务端方法:
public void start(){
try {
while (true){
System.out.println("服务端正在等待客户端连接......");
/**
* accept():
* 用于接收客户端连接,并返回一个Socket对象与所连接的客户端进行交互
* 该方法是一个阻塞方法,调用后程序会“卡住”,直到一个客户端连接为止
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
// 接收客户端发送过来的消息
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String message;
while ((message = bufferedReader.readLine()) != null) { // 读取客户端发送过来的消息
System.out.println("客户端说:" + message);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
Client.java
package day18;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 客户端
*/
public class Client {
/**
* java.net.Socket:套接字
* 它封装了TCP协议的通信细节,使用它可以和服务器建立连接并进行交互
* 可以理解为是电话
*/
private Socket socket;
public Client() {
// 实例化Socket对象,就是与远端计算机建立连接的过程
// 需要传递两个对数:
// 1.远端计算机的IP地址,用于找到网络上对方的计算机
// 2.远端计算机的端口号,用于找到对方计算机中的应用程序
try {
System.out.println("正在连接服务端......");
/**
* 如何查找本机IP地址:
* Windows:Win+R,输入cmd,回车,输入ipconfig,回车
* Mac:触控板上五指向中间抓,选择"终端"程序打开,输入/sbin/ifconfig查看IP地址
*/
socket = new Socket("172.40.140.102", 8088);
System.out.println("连接成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 该方法用于启动客户端程序的执行
public void start() {
// 通过Socket获取输出流用于向服务端发送消息
try {
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
PrintWriter printWriter = new PrintWriter(bufferedWriter,true); // 自动行刷新
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("exit".equalsIgnoreCase(line)) {
break;
}
printWriter.println(line); // 发送消息给服务端
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
三次握手与四次挥手
客户端断开连接的操作
- 我们在上方代码中输入“
exit
”后,服务器会出现一大堆红字;
什么是三次握手四次挥手
-
异常出现的原因:
-
TCP
协议是面向连续的可靠传输协议,双方建立连接及断开连接需要双向确认后才可以进行 -
当任意一方在没有进行确认断开操作就擅自结束,另一方就会出现该异常。
-
-
三次握手与四次挥手:
-
客户端与服务端建立
TCP
连接时需要进行三次握手,以确保双方可以建立可靠连接; -
任意一方需要断开
TCP
连接时需要进行四次挥手,以确保双方可靠断开。
-
三次握手(Three-way Handshake)
-
第一次握手(SYN):
-
客户端发送一个带有
SYN
(同步)标志的数据包到服务器,并指定客户端的初始化序列号(ISN
); -
客户端进入
SYN_SENT
状态,以等待服务器的确认。
-
-
第二次握手(SYN-ACK):
-
服务器收到客户端的
SYN
数据包后,会发送一个带有SYN
和ACK
(确认)标志的数据包到客户端,确认客户端的请求,并指定服务器的初始化序列号(ISN
)。 -
服务器进入
SYN_RCVD
状态;
-
-
第三次握手(ACK):
-
客户端收到服务器的
SYN-ACK
数据包后,会发送一个带有ACK
标志的数据包给服务器,表示确认连线请求; -
服务器收到客户端的
ACK
后,双方都进入ESTABLISHED
状态,建立起可靠的连接。
-
四次挥手(Four-way Wavehake)
-
Socket
提供方法:void close()
:用于和对方断开TCP
连接,底层会进行四次挥手操作。
-
客户端发送
FIN
报文:客户端首先向服务器发送一个带有FIN
标志位的保温,表示客户端不再发送数据,并请求关闭连接。此时客户端进入“FIN_WAIT_1
”状态; -
服务器发送
ACK
报文:服务器收到客户端的FIN
报文后,会发送一个确认保温,即ACK
报文,告知客户端已经收到关闭请求。此时服务器进入“CLOSE_WAIT
”状态,等待确认关闭; -
服务器发送
FIN
报文:服务器在发送ACK
报文后,会继续检查是否还有剩余的数据需要发送。如果有,服务器会继续发送数据;如果没有,服务器会发送一个带有FIN
标志位的报文给客户端,表示服务器也不在发送数据了。此时服务及进入“LAST_ACK
”状态; -
客户端发送
ACK
报文:客户端收到服务器的FIN
报文后,会发送一个确认报文,即ACK
报文,告知服务器已经收到关闭请求,并进入“TIME_WAIT
”状态。再“TIME_WAIT
”状态下,客户端等待一段时间(通常为2倍的最大报文段生存时间,即MSL
),以确保服务器已经收到了ACK
报文; -
服务器收到
ACK
报文:服务器收到客户端的ACK
报文后,确认客户端已经收到了服务器的关闭请求,此时服务器进入“CLOSED
”状态,完成连接的关闭。
package day18;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 客户端
*/
public class Client {
/**
* java.net.Socket:套接字
* 它封装了TCP协议的通信细节,使用它可以和服务器建立连接并进行交互
* 可以理解为是电话
*/
private Socket socket;
public Client() {
// 实例化Socket对象,就是与远端计算机建立连接的过程
// 需要传递两个对数:
// 1.远端计算机的IP地址,用于找到网络上对方的计算机
// 2.远端计算机的端口号,用于找到对方计算机中的应用程序
try {
System.out.println("正在连接服务端......");
/**
* 如何查找本机IP地址:
* Windows:Win+R,输入cmd,回车,输入ipconfig,回车
* Mac:触控板上五指向中间抓,选择"终端"程序打开,输入/sbin/ifconfig查看IP地址
*/
socket = new Socket("localhost", 8888);
System.out.println("连接成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 该方法用于启动客户端程序的执行
public void start() {
// 通过Socket获取输出流用于向服务端发送消息
try {
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
PrintWriter printWriter = new PrintWriter(bufferedWriter,true); // 自动行刷新
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("exit".equalsIgnoreCase(line)) {
break;
}
printWriter.println(line); // 发送消息给服务端
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close(); // 进行四次挥手
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
多线程
概念
什么是线程与多线程
-
线程是一个单一的顺序执行流程;
-
多线程是多个单一顺序执行流程一起执行。
顺序执行与并发执行
-
顺序执行:程序从上至下一句一句执行;
-
并发执行:多个线程一起执行称为“并发执行”。
使用场景
-
互不干扰:多线程通常用于在一个程序中需要同时处理多个互不干扰的任务;
-
多线程更快:单线程可以完成,但是多线程更快。
单线程和多线程比喻
-
可以把单线程看作只有一个灶台,一次只能做一道菜;或看作电路中的串联;
-
多线程则是有很多灶台,一次能做很多菜;或看作电路中的并联。
CPU与多线程
CPU 的多线程操作通过时间片轮转(Time Slicing
)来实现。虽然一个 CPU 在某一时刻只能执行一个线程,但它可以在多个线程之间快速切换,使得每个线程都能获得一定的执行时间。这种快速的切换让用户感觉多个线程是同时运行的。
线程的创建
第一种创建线程形式
-
继承
Thread
来定义一个线程:- 重写
run
方法,在其中定义该线程的任务代码。
- 重写
package day18;
/**
* 线程的第一种创建方式:继承Thread类
* 更方便地使用匿名内部类创建线程、结构简单
* 缺点:Java是单继承的,所以不能再继承其他类
* 意味着继承了Thread就无法再继承其他类了,不利于复用
* 定义线程的同时定义了线程的任务,导致线程和任务绑定在了一起,不利于复用
*/
public class ThreadDemo01 {
public static void main(String[] args) {
Thread thread01 = new MyThread01();
Thread thread02 = new MyThread02();
// 启动线程要调用star(),而不是直接调用run()方法
thread01.start();
thread02.start();
}
}
class MyThread01 extends Thread {
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Hi!" + i);
}
}
}
class MyThread02 extends Thread {
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("ByeBye!" + i);
}
}
}
第二种创建线程的方式
-
启动线程:
-
实例化线程任务;
-
实例化线程,同时将任务通过现成的构造器传递给线程;
-
调用线程的
start()
方法。
-
package day18;
/**
* 第二种创建线程的方式:实现Runnable接口
*/
public class ThreadDemo02 {
public static void main(String[] args) {
// 创建线程任务
Runnable runnable01 = new MyRunnable01();
Runnable runnable02 = new MyRunnable02();
// 创建线程
Thread thread01 = new Thread(runnable01);
Thread thread02 = new Thread(runnable02);
// 启动线程
thread01.start();
thread02.start();
}
}
class MyRunnable01 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println("Hello!" + i);
}
}
}
class MyRunnable02 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println("Hi!" + i);
}
}
}
使用匿名内部类实现线程的两种创建方式
package day18;
/**
* 使用匿名内部类方式实现线程的两种创建方式
*/
public class ThreadDemo03 {
public static void main(String[] args) {
//第一种创建方式:继承Thread并重写run方法
Thread thread01 = new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程01:" + i);
}
}
};
thread01.start();
//第二种创建方式:实现Runnable接口并重写run方法
Thread thread02 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("线程02:" + i);
}
});
thread02.start();
}
}
并发解决聊天室不能多用户使用问题
package day18;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* 服务端
*/
public class Server {
/**
* java.net.ServerSocket:
* 运行在服务端,主要作用有两个:
* 1.像系统申请服务端口,客户端通过该端口与服务端建立连接
* 2.监听服务端口,一旦有客户链接了,就会立即创建一个Socket对象与客户端进行交互
* 如果将Socket比喻为“电话”,那ServerSocket就相当于客户中心的“总机”。
* 解决方法:
* 1.更换端口号;
* 2.杀死占用该端口的进行(通常由于服务端启动了两次导致)
*/
private ServerSocket serverSocket;
public Server(){
try {
System.out.println("正在启动服务端......");
serverSocket = new ServerSocket(8888);
System.out.println("服务端启动完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 启动服务端方法:
/**
* accept():
* 用于接收客户端连接,并返回一个Socket对象与所连接的客户端进行交互
* 该方法是一个阻塞方法,调用后程序会“卡住”,直到一个客户端连接为止
*/
public void start(){
try {
while (true){
System.out.println("服务端正在等待客户端连接......");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
// 启动单独的线程来处理与客户端的通信
ClientHandler clientHandler = new ClientHandler(socket);
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 该线程任务用于负责与指定的客户端通信
*/
private class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 接收客户端发送过来的消息
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String message;
while ((message = bufferedReader.readLine()) != null) { // 读取客户端发送过来的消息
System.out.println("客户端说:" + message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
主线程
-
Java程序的任何代码都是靠线程进行,
main
方法也是; -
程序启动后,
JVM
创建一条线程执行main
方法,这条线程我们叫“主线程main”;
什么是主线程
-
Thread
提供的静态方法:-
static Thread currentThread()
; -
该方法用于返回执行他的线程;
-
查看线程的相关信息
-
Thread
提供了获取线程信息的相关方法:-
long getId()
:返回该线程的标识符; -
String getName()
:返回该线程的名称; -
int getPriority()
:返回线程的优先级; -
boolean isAlive()
:检测线程是否处于活跃状态; -
boolean isDeamon()
:测试线程是否为守护线程; -
boolean isInterrupted()
:测试线程是否已经中断;
-
package day18;
import static java.lang.Thread.currentThread;
/**
* 线程信息
*/
public class ThreadInfoDemo {
public static void main(String[] args) {
Thread main = currentThread(); // 获取主线程
System.out.println(main);
doSome(); // 主线程调用doSome方法
}
public static void doSome() {
Thread t = currentThread(); // 获取的是doSome方法的线程
System.out.println("运行doSome方法的线程:" + t);
System.out.println("运行doSome方法的线程名称:" + t.getName());
System.out.println("运行doSome方法的线程优先级:" + t.getPriority());
System.out.println("运行doSome方法的线程是否是守护线程:" + t.isDaemon());
System.out.println("运行doSome方法的线程是否是活动线程:" + t.isAlive());
System.out.println("运行doSome方法的线程是否是中断状态:" + t.isInterrupted());
System.out.println("运行doSome方法的线程标识符:" + t.getId());
}
}