隐藏

java网络编程-聊天室

发布:2023/4/7 17:23:13作者:管理员 来源:本站 浏览次数:564



目录


V01


       # 聊天室客户端(V1)


       # 聊天室服务端(V1)


V02


        # 聊天室客户端(V2)


       # 聊天室服务端(V2)


V03


V04


        # 聊天室客户端(V4)


        # 聊天室服务端(V4)


V05


       # 聊天室客户端(V5)


       # 聊天室服务端(V5)


V06


       # 聊天室客户端(V6)


       # 聊天室服务端(V6)


V07


       # 聊天室客户端(V7)


       # 聊天室服务端(V7)


V08


       # 聊天室客户端(V8)


       # 聊天室服务端(V8)


V09


       # 聊天室客户端(V9)


       # 聊天室服务端(V9)



V01



   ### java网络编程


   #### java.net.Socket

   Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,并通过 它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互


   #### java.net.ServerSocket

   ServerSocket运行在服务端,作用有两个:


   1:向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。

   2:监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。


   ##### 如果把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。


   ##### 与服务端建立连接案例:


       # 聊天室客户端(V1)


   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(){

   }

   /**

    * main方法,先走

    */

   public static void main(String[] args) {

   Client client = new Client();

   client.start();

   }

   }


       # 聊天室服务端(V1)


   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();

   }

   }



V02


   #### 客户端与服务端完成第一次通讯(发送一行字符串)


   ##### Socket提供了两个重要的方法:


   OutputStream getOutputStream()


   该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。


   InputStream getInputStream()


   通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。


        # 聊天室客户端(V2)


   import java.io.*;

   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(){

   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();

   }

   }


       # 聊天室服务端(V2)


   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 {

   /**

    * 运行在服务端的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("一个客户端链接了!");

   /*

    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();

   }

   }



V03


   上一版本实现了,客户端与服务端完成一次通讯(发送一行字符串)

   #### 该版本实现客户端循环发消息给服务端


   在干版本中其实就是在发送/接收时加入一个while死循环,实现服务端不断接收客户端发送过来的消息,在客户端输入"exit"且不区分大小写时,结束客户端运行


   但是,问题,在客户端结束运行后,服务端就会报异常 : SocketException 套接字异常


   需要注意的几个点:


   1:当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。


   2:当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。


较为简单,恕不展示



V04


   多客户端链接


   之前只有第一个连接的客户端可以与服务端说话。


   原因:


   服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了链接并返回了Socket,此时可以与其交互。


   而第二个客户端建立链接时,由于服务端没有再次调用accept,因此无法与其交互。


        # 聊天室客户端(V4)


   /**

    * 客户端与上一版本相同没有改变

    */


        # 聊天室服务端(V4)


   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 {

   /**

    * 运行在服务端的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 {

   while(true) {

   System.out.println("等待客户端链接...");

   /*

    ServerSocket提供了接受客户端链接的方法:

    Socket accept()

    这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端

    的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例

    通过这个Socket就可以与客户端进行交互了。

    可以理解为此操作是接电话,电话没响时就一直等。

    */

   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 = 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{} 死循环还是不行


   原因在于 :


   添加循环操作后,发现依然无法实现。


   原因在于:


   外层的while循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了里层循环不结束,外层循环则无法进入第二次操作。



   外层循环走一次,内层循环走全部。


   只有内层循环结束,外层循环才可以进入下一次循环



V05


   在此版本需要实现多个客户端连接时,服务端都可以建立连接


   在此需要用到线程方法


       # 聊天室客户端(V5)


   /**

    * 客户端与上一版本相同没有改变

    */


       # 聊天室服务端(V5)


   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 {

   /**

    * 运行在服务端的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 {

   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;

   public ClientHandler(Socket socket){

   this.socket = socket;

   }

   public void run(){

   try{

   /*

    Socket提供的方法:

    InputStream getInputStream()

    获取的字节输入流读取的是对方计算机发送过来的字节

    */

   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();

   }

   }

   }

   }



V06


   #### 实现服务端发送消息给客户端


   在服务端通过Socket获取输出流,客户端获取输入流,实现服务端将消息发送给客户端.


   这里让服务端直接将客户端发送过来的消息再回复给客户端来进行测试.


       # 聊天室客户端(V6)


   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 {

   /*

    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);

   //通过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();

   }

   }


       # 聊天室服务端(V6)


   import java.io.*;

   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 {

   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(){

   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);

   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();

   }

   }

   }

   }



V07


上一版本实现到服务端将消息转发给单一客户端


该版本实现将消息转发给所有客户端


   #### 服务端转发消息给所有客户端


   当一个客户端发送一个消息后,服务端收到后如何转发给所有客户端.


   问题:例如红色的线程一收到客户端消息后如何获取到橙色的线程二中的输出流?得不到就无法将消息转发给橙色的客户端(进一步延伸就是无法转发给所有其他客户端)


   解决:内部类可以访问外部类的成员,因此在Server类上定义一个数组allOut可以被所有内部类ClientHandler实例访问.从而将这些ClientHandler实例之间想互访的数据存放在这个数组中达到共享数据的目的.对此只需要将所有ClientHandler中的输出流都存入到数组allOut中就可以达到互访输出流转发消息的目的了.


       # 聊天室客户端(V7)


   #### 客户端解决收发消息的冲突问题


   由于客户端start方法中循环进行的操作顺序是先通过控制台输入一句话后将其发送给服务端,然后再读取服务端发送回来的一句话.这导致如果客户端不输入内容就无法收到服务端发送过来的其他信息(其他客户端的聊天内容).因此要将客户端中接收消息的工作移动到一个单独的线程上执行,才能保证收发消息互不打扰.


   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();

   }

   }

   }

   }


       # 聊天室服务端(V7)


   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(){

   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);

   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();

   }

   }

   }

   }



V08


   #### 服务端完成处理客户端断开连接后的操作


   当一个客户端断开连接后,服务端处理该客户端交互的线程ClientHandler应当将通过socket获取的输出流从共享数组allOut中删除,防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端.


       # 聊天室客户端(V8)


   /**

    * 客户端与上一版本相同没有改变

    */


       # 聊天室服务端(V8)


   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中

   //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中删除(数组缩容)

   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);

   }

   }

   }

   }



V09


在以上版本中基本实现客户端发送消息,服务端接收,并且群发给所有客户端,


但是会出现并发安全问题


如果两个客户端同时上线


   两个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数组的扩容,缩容和遍历操作要进行互斥。


       # 聊天室客户端(V9)


   /**

    * 客户端与上一版本相同没有改变

    */


       # 聊天室服务端(V9)


   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);

   }

   }

   }

   }

   }




结束聊天室项目.感谢欣赏 ! ! !