前言
API知识点:IO输入输出流、网络编程、多线程、多线程的并发安全问题的解决
IO输入输出流
前面讲到的变量、基本类型、对象等,它们在系统中存储的数据都是在内存中暂存的数据,当一个程序结束时,这些暂存数据也会被销毁。如果要永久地保留这些数据,就需要将它们保存在电脑的磁盘文件中。
Java的I/O机制可以将保存在磁盘文件中的数据读取出来,也可以将数据删除或写入磁盘文件(文件不限于文本文件、Excle表格、二进制文件等)。
流的概念:
“流”是个抽象概念,它是指不同设备间数据传输内容的抽象。当需要从一个数据源读取或是向一个目标写入数据时,就可以使用流。数据源可以是文件、内存、网络连接等,流就是这些数据在传输过程中的抽象概念,也可以理解为一个有序列的数据。
流的划分:
按传输类型分为:输入流和输出流。
输入流是指从一个数据源读取数据对象。输出流是指向一个目的地传输数据对象。
Java I/O,即Java Input or Output,是指Java中对流处理的方式。操作的流可以是文件、网络请求数据、压缩包、Excle文档等。java.io包中提供了专门表示输入/输出流的类,如字节输入流InputStream类、字节输出流OutputStream类、字符输入流Reader类、字符输出流Writer类。
网络编程
java.net.Socket(客户端)
Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,并通过 它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互
java.net.ServerSocket(服务端)
ServerSocket运行在服务端,作用有两个:
1:向系统(服务端操作系统)申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接
2:监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。
如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。
用户打电话到总机,总机分配一个电话使得服务端与你沟通。
多线程
线程:程序中一个单一的顺序执行流程。代码一句一句的有先后顺序的执行。
多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。
多线程改变了代码的执行方式,从原来的单一顺序执行流程变为多个执行流程"同时"执行。 可以让多个代码片段的执行互不打扰。
并发:
线程之间是并发执行的,并非真正意义上的同时运行。
多个线程实际运行是走走停停的。线程调度程序会将CPU运行时间划分为若干个时间片段并
尽可能均匀的分配给每个线程,拿到时间片的线程被CPU执行这段时间。当超时后线程调度
程序会再次分配一个时间片段给一个线程使得CPU执行它。如此反复。由于CPU执行时间在
纳秒级别,我们感觉不到切换线程运行的过程。所以微观上走走停停,宏观上感觉一起运行
的现象成为并发运行!
用途:
当出现多个代码片段执行顺序有冲突时,希望它们各干各的时就应当放在不同线程上"同时"运行
一个线程可以运行,但是多个线程可以更快时,可以使用多线程运行。
多线程并发安全问题
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪.
临界资源:操作该资源的全过程同时只能被单个线程完成.
BIO实现多人聊天室
分为服务端、客户端
搭建服务端、客户端,并将客户端与服务端建立连接
服务端代码:
package socket; import java.io.IOException; import java.net.Socket; /** * 聊天室客户端 */ public class Client { /* java.net.Socket 套接字 Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接, 并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成 与远端计算机的数据交互工作。 我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克 风(输出流),通过它们就可以与对方交流了。 */ private Socket socket; /** * 构造方法,用来初始化客户端 */ public Client(){ try { System.out.println("正在链接服务端..."); /* 实例化Socket时要传入两个参数 参数1:服务端的地址信息 可以是IP地址,如果链接本机可以写"localhost" 参数2:服务端开启的服务端口 我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上 的服务端应用程序。 实例化的过程就是链接的过程,如果链接失败会抛出异常: java.net.ConnectException: Connection refused: connect */ 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(); } }
客户端代码
package socket; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * 聊天室服务端 */ public class Server { /** * 运行在服务端的ServerSocket主要完成两个工作: * 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接 * 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket * 就可以和该客户端交互了 * * 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个 * 电话使得服务端与你沟通。 */ private ServerSocket serverSocket; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); /* 实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他 应用程序占用的端口相同,否则会抛出异常: java.net.BindException:address already in use 端口是一个数字,取值范围:0-65535之间。 6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。 */ serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { System.out.println("等待客户端链接..."); /* ServerSocket提供了接受客户端链接的方法: Socket accept() 这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端 的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例 通过这个Socket就可以与客户端进行交互了。 可以理解为此操作是接电话,电话没响时就一直等。 */ Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(); server.start(); } }
客户端与服务端完成第一次通讯(发送一行字符串)
Socket提供了两个重要的方法:
OutputStream getOutputStream()
该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
InputStream getInputStream()
通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
客户端代码:
package socket; import java.io.*; import java.net.Socket; /** * 聊天室客户端 */ public class Client { private Socket socket; /** * 构造方法,用来初始化客户端 */ public Client(){ try { System.out.println("正在链接服务端..."); socket = new Socket("localhost",8088); System.out.println("与服务端建立链接!"); } catch (IOException e) { e.printStackTrace(); } } /** * 客户端开始工作的方法 */ public void start(){ try { /* Socket提供了一个方法: OutputStream getOutputStream() 该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。 */ //低级流,将字节通过网络发送给对方 OutputStream out = socket.getOutputStream(); //高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节 OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); //高级流,负责块写文本数据加速 BufferedWriter bw = new BufferedWriter(osw); //高级流,负责按行写出字符串,自动行刷新 PrintWriter pw = new PrintWriter(bw,true); pw.println("你好服务端!"); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Client client = new Client(); client.start(); } }
服务端代码:
package socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * 聊天室服务端 */ public class Server { private ServerSocket serverSocket; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { System.out.println("等待客户端链接..."); Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); /* Socket提供的方法: InputStream getInputStream() 获取的字节输入流读取的是对方计算机发送过来的字节 */ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in,"UTF-8"); BufferedReader br = new BufferedReader(isr); String message = br.readLine(); System.out.println("客户端说:"+message); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(); server.start(); } }
实现客户端循环发消息给服务端
客户端代码:
package socket; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * 聊天室客户端 */ public class Client { private Socket socket; /** * 构造方法,用来初始化客户端 */ public Client(){ try { System.out.println("正在链接服务端..."); socket = new Socket("localhost",8088); System.out.println("与服务端建立链接!"); } catch (IOException e) { e.printStackTrace(); } } /** * 客户端开始工作的方法 */ public void start(){ try { OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); BufferedWriter bw = new BufferedWriter(osw); PrintWriter pw = new PrintWriter(bw,true); Scanner scanner = new Scanner(System.in); while(true) { String line = scanner.nextLine(); if("exit".equalsIgnoreCase(line)){ break; } pw.println(line); } } catch (IOException e) { e.printStackTrace(); } finally { try { /* 通讯完毕后调用socket的close方法。 该方法会给对方发送断开信号。 */ socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { Client client = new Client(); client.start(); } }
服务端代码:
package socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * 聊天室服务端 */ public class Server { private ServerSocket serverSocket; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { System.out.println("等待客户端链接..."); Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in,"UTF-8"); BufferedReader br = new BufferedReader(isr); String message = null; while((message = br.readLine())!=null) { System.out.println("客户端说:" + message); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(); server.start(); } }
需要注意的几个点:
1:当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。
2:当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。
多客户端链接
之前只有第一个连接的客户端可以与服务端说话。
原因:
服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了链接并返回了Socket,此时可以与其交互。
而第二个客户端建立链接时,由于服务端没有再次调用accept,因此无法与其交互。
服务端代码:
package socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * 聊天室服务端 */ public class Server { private ServerSocket serverSocket; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { while(true) { System.out.println("等待客户端链接..."); Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); String message = null; while ((message = br.readLine()) != null) { System.out.println("客户端说:" + message); } } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(); server.start(); } }
添加循环操作后,发现依然无法实现。
原因在于:
外层的while循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了里层循环不结束,外层循环则无法进入第二次操作。
使用多线程实现多客户端连接服务端
流程图
服务端代码改造:
package socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * 聊天室服务端 */ public class Server { private ServerSocket serverSocket; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { while(true) { System.out.println("等待客户端链接..."); Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); //启动一个线程与该客户端交互 ClientHandler clientHandler = new ClientHandler(socket); Thread t = new Thread(clientHandler); t.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; } public void run(){ try{ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); String message = null; while ((message = br.readLine()) != null) { System.out.println("客户端说:" + message); } }catch(IOException e){ e.printStackTrace(); } } } }
实现服务端发送消息给客户端
在服务端通过Socket获取输出流,客户端获取输入流,实现服务端将消息发送给客户端.
这里让服务端直接将客户端发送过来的消息再回复给客户端来进行测试.
服务端代码:
package socket; import java.io.*; import java.net.ServerSocket; import java.net.Socket; /** * 聊天室服务端 */ public class Server { private ServerSocket serverSocket; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { while(true) { System.out.println("等待客户端链接..."); Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); //启动一个线程与该客户端交互 ClientHandler clientHandler = new ClientHandler(socket); Thread t = new Thread(clientHandler); t.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; private String host;//记录客户端的IP地址信息 public ClientHandler(Socket socket){ this.socket = socket; //通过socket获取远端计算机地址信息 host = socket.getInetAddress().getHostAddress(); } public void run(){ try{ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); BufferedWriter bw = new BufferedWriter(osw); PrintWriter pw = new PrintWriter(bw,true); String message = null; while ((message = br.readLine()) != null) { System.out.println(host + "说:" + message); //将消息回复给客户端 pw.println(host + "说:" + message); } }catch(IOException e){ e.printStackTrace(); } } } }
客户端代码:
package socket; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * 聊天室客户端 */ public class Client { private Socket socket; /** * 构造方法,用来初始化客户端 */ public Client(){ try { System.out.println("正在链接服务端..."); socket = new Socket("localhost",8088); System.out.println("与服务端建立链接!"); } catch (IOException e) { e.printStackTrace(); } } /** * 客户端开始工作的方法 */ public void start(){ try { OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); BufferedWriter bw = new BufferedWriter(osw); PrintWriter pw = new PrintWriter(bw,true); //通过socket获取输入流读取服务端发送过来的消息 InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in,"UTF-8"); BufferedReader br = new BufferedReader(isr); Scanner scanner = new Scanner(System.in); while(true) { String line = scanner.nextLine(); if("exit".equalsIgnoreCase(line)){ break; } pw.println(line); line = br.readLine(); System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } finally { try { /* 通讯完毕后调用socket的close方法。 该方法会给对方发送断开信号。 */ socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { Client client = new Client(); client.start(); } }
服务端转发消息给所有客户端
当一个客户端发送一个消息后,服务端收到后如何转发给所有客户端.
问题:例如红色的线程一收到客户端消息后如何获取到橙色的线程二中的输出流?得不到就无法将消息转发给橙色的客户端(进一步延伸就是无法转发给所有其他客户端)
解决:内部类可以访问外部类的成员,因此在Server类上定义一个数组allOut可以被所有内部类ClientHandler实例访问.从而将这些ClientHandler实例之间想互访的数据存放在这个数组中达到共享数据的目的.对此只需要将所有ClientHandler中的输出流都存入到数组allOut中就可以达到互访输出流转发消息的目的了.
服务端代码:
package socket; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Arrays; /** * 聊天室服务端 */ public class Server { private ServerSocket serverSocket; /* 存放所有客户端输出流,用于广播消息 */ private PrintWriter[] allOut = {}; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { while(true) { System.out.println("等待客户端链接..."); Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); //启动一个线程与该客户端交互 ClientHandler clientHandler = new ClientHandler(socket); Thread t = new Thread(clientHandler); t.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; private String host;//记录客户端的IP地址信息 public ClientHandler(Socket socket){ this.socket = socket; //通过socket获取远端计算机地址信息 host = socket.getInetAddress().getHostAddress(); } public void run(){ try{ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); BufferedWriter bw = new BufferedWriter(osw); PrintWriter pw = new PrintWriter(bw,true); //将该输出流存入共享数组allOut中 //1、对allOut数组扩容 allOut = Arrays.copyOf(allOut,allOut.length+1); //2、将输出流存入数组最后一个位置 allOut[allOut.length-1] = pw; String message = null; while ((message = br.readLine()) != null) { System.out.println(host + "说:" + message); //将消息回复给所有客户端 for(int i=0;i<allOut.length;i++) { allOut[i].println(host + "说:" + message); } } }catch(IOException e){ e.printStackTrace(); } } } }
客户端解决收发消息的冲突问题
由于客户端start方法中循环进行的操作顺序是先通过控制台输入一句话后将其发送给服务端,然后再读取服务端发送回来的一句话.这导致如果客户端不输入内容就无法收到服务端发送过来的其他信息(其他客户端的聊天内容).因此要将客户端中接收消息的工作移动到一个单独的线程上执行,才能保证收发消息互不打扰.
客户端代码:
package socket; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * 聊天室客户端 */ public class Client { private Socket socket; /** * 构造方法,用来初始化客户端 */ public Client(){ try { System.out.println("正在链接服务端..."); socket = new Socket("localhost",8088); System.out.println("与服务端建立链接!"); } catch (IOException e) { e.printStackTrace(); } } /** * 客户端开始工作的方法 */ public void start(){ try { //启动读取服务端发送过来消息的线程 ServerHandler handler = new ServerHandler(); Thread t = new Thread(handler); t.setDaemon(true); t.start(); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); BufferedWriter bw = new BufferedWriter(osw); PrintWriter pw = new PrintWriter(bw,true); Scanner scanner = new Scanner(System.in); while(true) { String line = scanner.nextLine(); if("exit".equalsIgnoreCase(line)){ break; } pw.println(line); } } catch (IOException e) { e.printStackTrace(); } finally { try { /* 通讯完毕后调用socket的close方法。 该方法会给对方发送断开信号。 */ socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { Client client = new Client(); client.start(); } /** * 该线程负责接收服务端发送过来的消息 */ private class ServerHandler implements Runnable{ public void run(){ //通过socket获取输入流读取服务端发送过来的消息 try { InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in,"UTF-8"); BufferedReader br = new BufferedReader(isr); String line; //循环读取服务端发送过来的每一行字符串 while((line = br.readLine())!=null){ System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } } }
服务端完成处理客户端断开连接后的操作
当一个客户端断开连接后,服务端处理该客户端交互的线程ClientHandler应当将通过socket获取的输出流从共享数组allOut中删除,防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端.
服务端代码:
package socket; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Arrays; /** * 聊天室服务端 */ public class Server { private ServerSocket serverSocket; /* 存放所有客户端输出流,用于广播消息 */ private PrintWriter[] allOut = {}; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { while(true) { System.out.println("等待客户端链接..."); Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); //启动一个线程与该客户端交互 ClientHandler clientHandler = new ClientHandler(socket); Thread t = new Thread(clientHandler); t.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; private String host;//记录客户端的IP地址信息 public ClientHandler(Socket socket){ this.socket = socket; //通过socket获取远端计算机地址信息 host = socket.getInetAddress().getHostAddress(); } public void run(){ PrintWriter pw = null; try{ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); BufferedWriter bw = new BufferedWriter(osw); pw = new PrintWriter(bw,true); allOut = Arrays.copyOf(allOut, allOut.length + 1); allOut[allOut.length - 1] = pw; //通知所有客户端该用户上线了 sendMessage(host + "上线了,当前在线人数:"+allOut.length); String message = null; while ((message = br.readLine()) != null) { System.out.println(host + "说:" + message); //将消息回复给所有客户端 sendMessage(host + "说:" + message); } }catch(IOException e){ e.printStackTrace(); }finally{ //处理客户端断开链接的操作 //将当前客户端的输出流从allOut中删除(数组缩容) for(int i=0;i<allOut.length;i++){ if(allOut[i]==pw){ allOut[i] = allOut[allOut.length-1]; allOut = Arrays.copyOf(allOut,allOut.length-1); break; } } sendMessage(host+"下线了,当前在线人数:"+allOut.length); try { socket.close();//与客户端断开链接 } catch (IOException e) { e.printStackTrace(); } } } /** * 广播消息给所有客户端 * @param message */ private void sendMessage(String message){ for(int i=0;i<allOut.length;i++) { allOut[i].println(message); } } } }
服务端解决多线程并发安全问题
为了让能叫消息转发给所有客户端,我们 在Server上添加了一个数组类型的属性allOut,并且共所有线程ClientHandler使用,这时对数组的操作要考虑并发安全问题
当两个客户端同时上线(橙,绿)
两个ClientHandler启动后都会对数组扩容,将自身的输出流存入数组
此时ClientHandler(橙)先拿到CPU时间,进行数组扩容
扩容后发生CPU切换,ClientHandler(绿)拿到时间
此时ClientHandler(绿)进行数组扩容
ClientHandler(绿)扩容后,将输出流存入数组最后一个位置
线程切换回ClientHandler(橙)
ClientHandler(橙)将输出流存入数组最后一个位置,此时会将ClientHandler(绿)存入的输入流覆盖。出现了并发安全问题!!
选取合适的锁对象
this不可以
allOut不可以。大多数情况下可以选择临界资源作为锁对象,但是这里不行。
ClientHandler(橙)锁定allOut
ClientHandler(橙)扩容allOut
由于数组是定长的,扩容实际是创建新数组,因此扩容后赋值给allOut时,ClientHandler(橙)之前锁定的对象就被GC回收了!而新扩容的数组并没有锁。
若此时发生线程切换,ClientHandler(绿)锁定allOut时,发现该allOut没有锁,因此可以锁定,并执行synchronized内部代码
ClientHandler(绿)也可以进行数组扩容,那么它之前锁定的数组也被GC回收了!
从上述代码可以看出,锁定allOut并没有限制多个线程(ClientHandler)操作allOut数组,还是存在并发安全问题。
可以选取外部类对象作为锁对象,因为这些内部类ClientHandler都从属于这个外部类对象Server.this
还要考虑对数组的不同操作之间的互斥问题,道理同上。因此,对allOut数组的扩容,缩容和遍历操作要进行互斥。
最终代码
服务端代码:
package socket; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Arrays; /** * 聊天室服务端 */ public class Server { /** * 运行在服务端的ServerSocket主要完成两个工作: * 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接 * 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket * 就可以和该客户端交互了 * * 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个 * 电话使得服务端与你沟通。 */ private ServerSocket serverSocket; /* 存放所有客户端输出流,用于广播消息 */ private PrintWriter[] allOut = {}; /** * 服务端构造方法,用来初始化 */ public Server(){ try { System.out.println("正在启动服务端..."); /* 实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他 应用程序占用的端口相同,否则会抛出异常: java.net.BindException:address already in use 端口是一个数字,取值范围:0-65535之间。 6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。 */ serverSocket = new ServerSocket(8088); System.out.println("服务端启动完毕!"); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start(){ try { while(true) { System.out.println("等待客户端链接..."); /* ServerSocket提供了接受客户端链接的方法: Socket accept() 这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端 的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例 通过这个Socket就可以与客户端进行交互了。 可以理解为此操作是接电话,电话没响时就一直等。 */ Socket socket = serverSocket.accept(); System.out.println("一个客户端链接了!"); //启动一个线程与该客户端交互 ClientHandler clientHandler = new ClientHandler(socket); Thread t = new Thread(clientHandler); t.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; private String host;//记录客户端的IP地址信息 public ClientHandler(Socket socket){ this.socket = socket; //通过socket获取远端计算机地址信息 host = socket.getInetAddress().getHostAddress(); } public void run(){ PrintWriter pw = null; try{ /* Socket提供的方法: InputStream getInputStream() 获取的字节输入流读取的是对方计算机发送过来的字节 */ InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in, "UTF-8"); BufferedReader br = new BufferedReader(isr); OutputStream out = socket.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); BufferedWriter bw = new BufferedWriter(osw); pw = new PrintWriter(bw,true); //将该输出流存入共享数组allOut中 // synchronized (this) {//不行,因为这个是ClientHandler实例 // synchronized (allOut) {//不行,下面操作会扩容,allOut对象会变 synchronized (Server.this) {//外部类对象可以 //1对allOut数组扩容 allOut = Arrays.copyOf(allOut, allOut.length + 1); //2将输出流存入数组最后一个位置 allOut[allOut.length - 1] = pw; } //通知所有客户端该用户上线了 sendMessage(host + "上线了,当前在线人数:"+allOut.length); String message = null; while ((message = br.readLine()) != null) { System.out.println(host + "说:" + message); //将消息回复给所有客户端 sendMessage(host + "说:" + message); } }catch(IOException e){ e.printStackTrace(); }finally{ //处理客户端断开链接的操作 //将当前客户端的输出流从allOut中删除(数组缩容) synchronized (Server.this) { for (int i = 0; i < allOut.length; i++) { if (allOut[i] == pw) { allOut[i] = allOut[allOut.length - 1]; allOut = Arrays.copyOf(allOut, allOut.length - 1); break; } } } sendMessage(host+"下线了,当前在线人数:"+allOut.length); try { socket.close();//与客户端断开链接 } catch (IOException e) { e.printStackTrace(); } } } /** * 广播消息给所有客户端 * @param message */ private void sendMessage(String message){ synchronized (Server.this) { for (int i = 0; i < allOut.length; i++) { allOut[i].println(message); } } } } }
客户端代码:
package socket; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * 聊天室客户端 */ public class Client { /* java.net.Socket 套接字 Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接, 并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成 与远端计算机的数据交互工作。 我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克 风(输出流),通过它们就可以与对方交流了。 */ private Socket socket; /** * 构造方法,用来初始化客户端 */ public Client(){ try { System.out.println("正在链接服务端..."); /* 实例化Socket时要传入两个参数 参数1:服务端的地址信息 可以是IP地址,如果链接本机可以写"localhost" 参数2:服务端开启的服务端口 我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上 的服务端应用程序。 实例化的过程就是链接的过程,如果链接失败会抛出异常: java.net.ConnectException: Connection refused: connect */ socket = new Socket("localhost",8088); System.out.println("与服务端建立链接!"); } catch (IOException e) { e.printStackTrace(); } } /** * 客户端开始工作的方法 */ public void start(){ try { //启动读取服务端发送过来消息的线程 ServerHandler handler = new ServerHandler(); Thread t = new Thread(handler); t.setDaemon(true); t.start(); /* Socket提供了一个方法: OutputStream getOutputStream() 该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。 */ //低级流,将字节通过网络发送给对方 OutputStream out = socket.getOutputStream(); //高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节 OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8"); //高级流,负责块写文本数据加速 BufferedWriter bw = new BufferedWriter(osw); //高级流,负责按行写出字符串,自动行刷新 PrintWriter pw = new PrintWriter(bw,true); Scanner scanner = new Scanner(System.in); while(true) { String line = scanner.nextLine(); if("exit".equalsIgnoreCase(line)){ break; } pw.println(line); } } catch (IOException e) { e.printStackTrace(); } finally { try { /* 通讯完毕后调用socket的close方法。 该方法会给对方发送断开信号。 */ socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { Client client = new Client(); client.start(); } /** * 该线程负责接收服务端发送过来的消息 */ private class ServerHandler implements Runnable{ public void run(){ //通过socket获取输入流读取服务端发送过来的消息 try { InputStream in = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(in,"UTF-8"); BufferedReader br = new BufferedReader(isr); String line; //循环读取服务端发送过来的每一行字符串 while((line = br.readLine())!=null){ System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } } }