SE API第九天:初级版:简易聊天室项目——>ChatRoom

java网络编程《聊天室》

  • ---------客户端《Client.java》
  • ---------服务器《Server.java》

1、知识点:

00.网络基本概念:

(1)七层模型:物理型 数据链路层 网络层 传输层(UDP/TCP) 会话层 应用层 表示层
           后四层与Java相关(HTTP,网页相关)(FPT,小范围服务协议)(POP3,邮箱邮件还有SMTP)

(2)IP:在网络中标记主机.IPV4(四组数来表示IP地址,每一组数取值范围0~255 ),后主推IPV6

(3)端口:计算机与外界交互的媒介-----端口号,0~65535

(4)域名:各个网站提供的便于记忆的标记”baidu(二级域名).com(一级域名)”从后往前看//com(商业网站)org(政府网站)edu(教育类网站)

(5)DNS:解析服务器:将域名和IP地址进行对应的

(6)UPD:基于流来使用的,不建立连接,不可靠传输,需要对数据进行封包每个包不超过64K.适用于
       对速度依赖性比较强但是对可靠性比较低的场景

(8)TCP:基于流的,建立连接,经历三次握手,初次客户端发给服务端,请求建立连接/发送数据,二次
       服务端发给客户端,确认可以建立链接/发送数据,第三次客户端发给服务端,再次确认,收
       到信息,准备链接/发送,可靠但传输速率相对较慢,理论上不限制传输数据的大小,适用于
       对可靠性的依赖性更高却对速度依赖性较低的场景------文件传输

01.Socket通信模型:

在这里插入图片描述

02.什么是Socket?

(1)java.net.Socket

java.net.Socket:————>Socket:插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。

【Socket(套接字),用来描述IP地址和端口,是通信链的句柄,应用程序可以通过Socket向网络发送请求或者应答网络请求!
Socket是支持TCP/IP协议的网络通信的基本操作单元,是对网络通信过程中端点的抽象表示。
包含了进行网络通信所必须的五种信息:
1)连接所使用的协议;
2)本地主机的IP地址;
3)本地远程的协议端口;
4)远地主机的IP地址
5)远地进程的协议端口】

1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化
   Socket的过程就是与服务端建立连接的过程。
参数1:服务端的IP地址
参数2:服务端开启的服务端口
我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。

201.Socket提供了两个重要的方法:
   (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
   (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。

02.Socket提供了:
(1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
(2).getInetAddress().getHostAddress:获取远端客户端的地址信息

03.什么是ServerSocket?

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

java.net.ServerSocket:

1java.net.ServerSocketServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
(2)监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。

2ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
   如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
   java.net.BindException:address already in use
   绑定异常:地址被使用了
   
3ServerSocketaccept()方法:——>是一个阻塞方法;
  开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法会立即返回一个
  Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
  注:阻塞方法:调用后,程序就"卡住"不往下执行了。

4、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
   {对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
    java.net.SocketException: connection reset--->服务端无法避免这个异常。}
   (1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容)

2、聊天室10个版本目标:

00.v0:—> 大致步骤 和 相关知识 <—

(1)客户端:

(1)客户端:Socket通信实现步骤解析:
 - Step 1:创建Socket对象
 - Step 2:发起链接,绑定链接地址
 - Step 3:获取自带的输出流,写出数据,禁用输出流
 - Step 4:如果服务端有打回的数据,则需要获取输入流读取数据,禁用输入流
 - Step 5:关闭输入输出流,以及Socket

java.net.Socket:————>Socket:插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
参数1:服务端的IP地址
参数2:服务端开启的服务端口
我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。

201.Socket提供了两个重要的方法:
   (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
   (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
   
02.Socket提供了:
   (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
   (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
   
3String提供方法:equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。

4、创建类ServerHandlerimplements Runnable

(2)服务端:

(2)服务端:ServerSocket通信实现步骤解析:
 - Step 1:创建服务器端套接字对象,并且绑定监听的端口号
 - Step 2:接受连接,获取到一个Socket对象
 - Step 3:获取输入流,读取消息,禁用输入流
 - Step 4:如果需要向客户端打回消息,则需要获取输出流写出数据,禁用输出流
 - Step 5:关闭输入输出流

ServerSocket相当于"总机"
1java.net.ServerSocketServerSocket是运行在服务端上的。其主要有两个作用:
  (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
  (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
  
2ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
   如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
   java.net.BindException:address already in use
   绑定异常:地址被使用了
   
3ServerSocketaccept()方法:
  是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
  会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
  阻塞方法:调用后,程序就"卡住"不往下执行了。
  
4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端

5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端

6、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
   {对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
    java.net.SocketException: connection reset--->服务端无法避免这个异常。}
   (1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容)7、选取合适的锁对象:-------外部类名.this(内部类访问外部类对象)
  (1)写一个同步块————>synchronized (Server.this) { 扩容、放在最后一个元素}
  (2)写一个同步块————>synchronized (Server.this) { for(){if(){缩容、放在最后一个元素}}}
  (3)将【广播消息给所有客户端】中的也放在同步块里:synchronized (Server.this) {打印语句、for}

01. v1:与服务端建立连接

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

java.net.ServerSocket:ServerSocket运行在服务端,作用有两个:
  (1)向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
  (2)监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。

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

(1)客户端:

package apiday.day07.socket.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(){

    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

(2)服务端:

package apiday.day07.socket.v1;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * java网络编程:
 * java.net.Socket:Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,
 *                  并通过它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互
 *
 * java.net.ServerSocket
 * ServerSocket运行在服务端,作用有两个:
 * 1:向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
 * 2:监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。
 *
 * 如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
 *
 */
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();
    }
}

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

Socket提供了两个重要的方法:
OutputStream getOutputStream():
该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。

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

在这里插入图片描述
(1)客户端:

package apiday.day07.socket.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();
    }
}

(2)服务端:

package apiday.day07.socket.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();
    }
}

03. v3:只能收到第一个客户端的消息

(1)客户端:

package apiday.day07.socket.v3;
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);

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

(2)服务端:

package apiday.day07.socket.v3;
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 = 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();
    }
}

04. v4:还是只能接收第一个客户端消息

需要注意的几个点:
(1)当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。
(2)当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。
    
多客户端链接:之前只有第一个连接的客户端可以与服务端说话。
原因:服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了链接并返回了
     Socket,此时可以与其交互。

在这里插入图片描述
添加循环操作后,发现依然无法实现:

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

在这里插入图片描述
(1)客户端:

package apiday.day07.socket.v4;
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);

            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) {
        apiday.day07.socket.v3.Client client = new apiday.day07.socket.v3.Client();
        client.start();
    }
}

(2)服务端:

package apiday.day07.socket.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();
    }
}

05. v5:可接收多客户端发的消息

————>使用多线程实现多客户端连接服务端

server:使用多线程实现多客户端连接服务端

①该线程任务是用一个线程来处理一个客户端的交互:

定义一个ClientHandler类重写run()方法:

②并获取远端客户端的地址信息

在这里插入图片描述

(1)客户端:

package apiday.socket.v5;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
 * 聊天室客户端Client:
 * java.net.Socket:————>Socket:插座 套接字
 * Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
 * 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
 *
 * 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
 * 参数1:服务端的IP地址
 * 参数2:服务端开启的服务端口
 * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
 *
 * 2、
 * 01.Socket提供了两个重要的方法:
 *    (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
 *    (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
 *
 * 02.Socket提供了:
 *    (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
 *    (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
 *
 *
 * 4、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
 */
public class Client {
    /*
        java.net.Socket 插座 套接字
        Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
        读写完成数据交换。
     */
    private Socket socket;


    /**
     * 构造方法:用于初始化客户端
     */
    public Client() {
        try {
            /**
             * 实例化Socket时常用的构造方法:Socket(String host,int port)
             * 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
             * 参数1:服务端的IP地址
             * 参数2:服务端开启的服务端口
             * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
             * 该机器上的服务端应用程序从而与之建立连接。
             */
            //本机IP地址的写法可以是“localhost”
            System.out.println("正在连接服务端");
            socket = new Socket("192.168.0.104", 8088);
            System.out.println("与服务端连接了");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            /* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.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();
                //equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
                if ("exit".equalsIgnoreCase(line)) {
                     break;
                }
                pw.println(line);//自动行刷新,调用println()方法
            }

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

(2)服务端:

package apiday.socket.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;
import java.nio.charset.StandardCharsets;
/**
 * 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
 * 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
 *   (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
 *   (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
 *
 * 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
 *    如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
 *    java.net.BindException:address already in use
 *    绑定异常:地址被使用了
 *
 * 3、ServerSocket的accept()方法:
 *   是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
 *   会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
 *   阻塞方法:调用后,程序就"卡住"不往下执行了。
 *
 * 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
 *
 */

public class Server {
    /*
    java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
    (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
    (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
     */
    private ServerSocket serverSocket;
    public Server()  {
        try {
            /*
                ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
                端口建立连接。
                如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
                java.net.BindException:address already in use
                绑定异常:地址被使用了
             */
            System.out.println("正在启动服务端");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            /*
                ServerSocket的accept方法是一个阻塞方法。
                开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
                会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
                相当于是接电话的动作。
                阻塞方法:调用后,程序就"卡住"不往下执行了。
             */
            while (true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互: Handler:处理器
                //①创建线程任务
                ClientHandler clientHandler = new ClientHandler(socket);
                //②创建1条线程执行该任务
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
    private class ClientHandler implements Runnable{
        private Socket socket;
        //记录远端计算机的地址信息:
        private String host;

        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

            /*
            阻塞:
            这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
            当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
            处于阻塞状态,直到对方发送过来一行字符串为止。
            如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
             */
                String line;
                while ((line = br.readLine())!=null){
                    System.out.println(host+"说:"+line);
                }
            } catch (IOException e) {
                //e.printStackTrace();
            } finally {
            }
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

06. v6:客户端发给服务端的消息能在客户端显示

————>初步实现服务端发送消息给客户端:
在这里插入图片描述
(1)客户端:

package apiday.socket.v6;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 聊天室客户端Client:
 * java.net.Socket:————>Socket:插座 套接字
 * Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
 * 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
 *
 * 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
 * 参数1:服务端的IP地址
 * 参数2:服务端开启的服务端口
 * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
 *
 * 2、
 * 01.Socket提供了两个重要的方法:
 *    (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
 *    (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
 *
 * 02.Socket提供了:
 *    (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
 *    (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
 *
 *
 * 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
 */
public class Client {
    /*
        java.net.Socket 插座 套接字
        Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
        读写完成数据交换。
     */
    private Socket socket;


    /**
     * 构造方法:用于初始化客户端
     */
    public Client() {
        try {
            /**
             * 实例化Socket时常用的构造方法:Socket(String host,int port)
             * 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
             * 参数1:服务端的IP地址
             * 参数2:服务端开启的服务端口
             * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
             * 该机器上的服务端应用程序从而与之建立连接。
             */
            //本机IP地址的写法可以是“localhost”
            System.out.println("正在连接服务端");
            socket = new Socket("192.168.0.104", 8088);
            System.out.println("与服务端连接了");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            /* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            BufferedWriter bw = new BufferedWriter(osw);
            PrintWriter pw = new PrintWriter(bw, true);

            //通过socket获取输入流读取服务端发送过来的消息
            InputStream in = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
            BufferedReader br = new BufferedReader(isr);

            //是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String line = scanner.nextLine();
                //equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
                if ("exit".equalsIgnoreCase(line)) {
                     break;
                }
                pw.println(line);//自动行刷新,调用println()方法

                //读取服务端发送过来的一行字符串:
                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();
    }
}

(2)服务端:

package apiday.socket.v6;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.sql.Struct;

/**
 * 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
 * 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
 *   (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
 *   (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
 *
 * 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
 *    如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
 *    java.net.BindException:address already in use
 *    绑定异常:地址被使用了
 *
 * 3、ServerSocket的accept()方法:
 *   是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
 *   会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
 *   阻塞方法:调用后,程序就"卡住"不往下执行了。
 *
 * 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
 *
 */

public class Server {
    /*
    java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
    (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
    (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
     */
    private ServerSocket serverSocket;
    public Server()  {
        try {
            /*
                ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
                端口建立连接。
                如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
                java.net.BindException:address already in use
                绑定异常:地址被使用了
             */
            System.out.println("正在启动服务端");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            /*
                ServerSocket的accept方法是一个阻塞方法。
                开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
                会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
                相当于是接电话的动作。
                阻塞方法:调用后,程序就"卡住"不往下执行了。
             */
            while (true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互: Handler:处理器
                //①创建线程任务
                ClientHandler clientHandler = new ClientHandler(socket);
                //②创建1条线程执行该任务
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
    private class ClientHandler implements Runnable{
        private Socket socket;
        //记录远端计算机的地址信息:
        private String host;

        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于给对方发消息:
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw, true);


            /*
            阻塞:
            这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
            当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
            处于阻塞状态,直到对方发送过来一行字符串为止。
            如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
             */
                String line;
                while ((line = br.readLine())!=null){
                    System.out.println(host+"说:"+line);
                    //将消息发送给客户端
                    pw.println(host+"说:"+line);
                }
            } catch (IOException e) {
                //e.printStackTrace();
            } finally {
            }
        }
    }


    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

07. v7:多个客户端之间收发消息有冲突:

——>服务端转发客户端发送的消息给所有客户端,但多个客户端之间收发消息有冲突:

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

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

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

server:
(1)定义一个PrintWriter[]类型数组 allOut:
    在run()中将该输出流存入共享数组allOut中:
    1)扩容
    2)将pw放到数组最后一个元素

(2)把【将消息发送给客户端】改为————>遍历allOut数组,将消息发送所有给客户端

在这里插入图片描述

(1)客户端:

package apiday.socket.v7;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 聊天室客户端Client:
 * java.net.Socket:————>Socket:插座 套接字
 * Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
 * 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
 *
 * 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
 * 参数1:服务端的IP地址
 * 参数2:服务端开启的服务端口
 * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
 *
 * 2、
 * 01.Socket提供了两个重要的方法:
 *    (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
 *    (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
 *
 * 02.Socket提供了:
 *    (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
 *    (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
 *
 *
 * 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
 */
public class Client {
    /*
        java.net.Socket 插座 套接字
        Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
        读写完成数据交换。
     */
    private Socket socket;


    /**
     * 构造方法:用于初始化客户端
     */
    public Client() {
        try {
            /**
             * 实例化Socket时常用的构造方法:Socket(String host,int port)
             * 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
             * 参数1:服务端的IP地址
             * 参数2:服务端开启的服务端口
             * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
             * 该机器上的服务端应用程序从而与之建立连接。
             */
            //本机IP地址的写法可以是“localhost”
            System.out.println("正在连接服务端");
            socket = new Socket("192.168.0.104", 8088);
            System.out.println("与服务端连接了");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            /* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            BufferedWriter bw = new BufferedWriter(osw);
            PrintWriter pw = new PrintWriter(bw, true);

            //通过socket获取输入流读取服务端发送过来的消息
            InputStream in = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
            BufferedReader br = new BufferedReader(isr);

            //是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String line = scanner.nextLine();
                //equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
                if ("exit".equalsIgnoreCase(line)) {
                     break;
                }
                pw.println(line);//自动行刷新,调用println()方法

                //读取服务端发送过来的一行字符串:
                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();
    }
}

(2)服务端:

package apiday.socket.v7;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
 * 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
 *   (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
 *   (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
 *
 * 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
 *    如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
 *    java.net.BindException:address already in use
 *    绑定异常:地址被使用了
 *
 * 3、ServerSocket的accept()方法:
 *   是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
 *   会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
 *   阻塞方法:调用后,程序就"卡住"不往下执行了。
 *
 * 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
 *
 * 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
 */

public class Server {
    /*
    java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
    (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
    (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
     */
    private ServerSocket serverSocket;
    private PrintWriter[] allOut = {};

    public Server()  {
        try {
            /*
                ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
                端口建立连接。
                如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
                java.net.BindException:address already in use
                绑定异常:地址被使用了
             */
            System.out.println("正在启动服务端");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            /*
                ServerSocket的accept方法是一个阻塞方法。
                开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
                会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
                相当于是接电话的动作。
                阻塞方法:调用后,程序就"卡住"不往下执行了。
             */
            while (true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互: Handler:处理器
                //①创建线程任务
                ClientHandler clientHandler = new ClientHandler(socket);
                //②创建1条线程执行该任务
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
    private class ClientHandler implements Runnable{
        private Socket socket;
        //记录远端计算机的地址信息:
        private String host;

        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于给对方发消息:
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw, true);

                //将该输出流存入共享数组allOut中:
                //(1)扩容:
                allOut = Arrays.copyOf(allOut,allOut.length+1);
                //(2)将pw放到数组最后一个元素:
                allOut[allOut.length-1] = pw;


            /*
            阻塞:
            这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
            当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
            处于阻塞状态,直到对方发送过来一行字符串为止。
            如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
             */
                String line;
                while ((line = br.readLine())!=null){
                    System.out.println(host+"说:"+line);
                    //遍历allOut数组,将消息发送所以给客户端:
                    for(int i=0;i<allOut.length;i++) {
                        allOut[i].println(host + "说:" + line);
                    }
                }
            } catch (IOException e) {
                //e.printStackTrace();
            } finally {
            }
        }
    }


    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

08. v8:客户端解决收发消息的冲突问题:

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

Client:
(1)该线程任务负责处理服务端发送过来的消息:
   创建类ServerHandlerimplements Runnable
(2)将上面的【通过socket获取输入流读取服务端发送过来的消息】代码剪切到run()(3)在上眠的代码中写:循环读取服务端发送过来的每一行字符串:    
(4)将【读取服务端发送过来的一行字符串:】代码删除    
(5)启动用于读取服务端发送过来消息的线程     

(1)客户端:

package apiday.socket.v8;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 聊天室客户端Client:
 * java.net.Socket:————>Socket:插座 套接字
 * Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
 * 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
 *
 * 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
 * 参数1:服务端的IP地址
 * 参数2:服务端开启的服务端口
 * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
 *
 * 2、
 * 01.Socket提供了两个重要的方法:
 *    (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
 *    (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
 *
 * 02.Socket提供了:
 *    (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
 *    (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
 *
 *
 * 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
 *
 * 4、创建类ServerHandler 并implements Runnable
 */
public class Client {
    /*
        java.net.Socket 插座 套接字
        Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
        读写完成数据交换。
     */
    private Socket socket;


    /**
     * 构造方法:用于初始化客户端
     */
    public Client() {
        try {
            /**
             * 实例化Socket时常用的构造方法:Socket(String host,int port)
             * 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
             * 参数1:服务端的IP地址
             * 参数2:服务端开启的服务端口
             * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
             * 该机器上的服务端应用程序从而与之建立连接。
             */
            //本机IP地址的写法可以是“localhost”
            System.out.println("正在连接服务端");
            socket = new Socket("192.168.0.104", 8088);
            System.out.println("与服务端连接了");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            //启动用于读取服务端发送过来消息的线程:
            ServerHandler handler = new ServerHandler();
            Thread t = new Thread(handler);
            t.start();

            /* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.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();
                //equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
                if ("exit".equalsIgnoreCase(line)) {
                     break;
                }
                pw.println(line);//自动行刷新,调用println()方法
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //Socket提供了.close()方法:可以与远端计算机断开连接。
                // 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /** 该线程任务负责处理服务端发送过来的消息 */
    private class ServerHandler implements Runnable{
        public void run() {
            try{
                //通过socket获取输入流读取服务端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //循环读取服务端发送过来的每一行字符串:
                String line;
                while((line=br.readLine())!=null){
                    System.out.println(line);
                }
            }catch (IOException e){

            }
        }
    }


    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

(2)服务端:

package apiday.socket.v8;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
 * 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
 *   (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
 *   (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
 *
 * 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
 *    如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
 *    java.net.BindException:address already in use
 *    绑定异常:地址被使用了
 *
 * 3、ServerSocket的accept()方法:
 *   是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
 *   会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
 *   阻塞方法:调用后,程序就"卡住"不往下执行了。
 *
 * 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
 *
 * 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
 */

public class Server {
    /*
    java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
    (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
    (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
     */
    private ServerSocket serverSocket;
    private PrintWriter[] allOut = {};

    public Server()  {
        try {
            /*
                ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
                端口建立连接。
                如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
                java.net.BindException:address already in use
                绑定异常:地址被使用了
             */
            System.out.println("正在启动服务端");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            /*
                ServerSocket的accept方法是一个阻塞方法。
                开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
                会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
                相当于是接电话的动作。
                阻塞方法:调用后,程序就"卡住"不往下执行了。
             */
            while (true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互: Handler:处理器
                //①创建线程任务
                ClientHandler clientHandler = new ClientHandler(socket);
                //②创建1条线程执行该任务
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
    private class ClientHandler implements Runnable{
        private Socket socket;
        //记录远端计算机的地址信息:
        private String host;

        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于给对方发消息:
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw, true);

                //将该输出流存入共享数组allOut中:
                //(1)扩容:
                allOut = Arrays.copyOf(allOut,allOut.length+1);
                //(2)将pw放到数组最后一个元素:
                allOut[allOut.length-1] = pw;


            /*
            阻塞:
            这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
            当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
            处于阻塞状态,直到对方发送过来一行字符串为止。
            如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
             */
                String line;
                while ((line = br.readLine())!=null){
                    System.out.println(host+"说:"+line);
                    //遍历allOut数组,将消息发送所以给客户端:
                    for(int i=0;i<allOut.length;i++) {
                        allOut[i].println(host + "说:" + line);
                    }
                }
            } catch (IOException e) {
                //e.printStackTrace();
            } finally {
            }
        }
    }


    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

09. v9:服务端完成处理客户端断开连接后的操作

(1)当一个客户端断开连接后,服务端处理该客户端交互的线程ClientHandler应当将通过socket获取
   的输出流从共享数组allOut中删除,
(2)防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端
   (【前提:catch中调用printStackTrace();方法将异常显示在控制台。】若不处理这个问
      题,对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛
      出下面异常: java.net.SocketException: connection reset--->服务端无法避免
      这个异常。).


Server:
(1)在客户端处理程序:private class ClientHandler implements Runnable{。。。。} 中:
    finally{...}中:处理客户端断开连接后的操作:捕获这个异常:socket.close();
(2)在上面ClientHandler中的run():定义:PrintWriter pw = null;
(3)接上面:因为上边初始化了,这里直接赋值就行:把【PrintWriter pw = new PrintWriter(bw, true);】进行赋值
   pw = new PrintWriter(bw, true);
(4)在上面【处理客户端断开连接后的操作】中将pw从数组allOut中删除(数组缩容)(5)run()方法的下边写一个方法:将消息群发给所有客户端:
(6)将上面的【遍历allOut数组,将消息发送所以给客户端:】 复制粘在上面的方法里————>并将打印语句里换为line
(7)将复制的源代码:【遍历allOut数组将消息发送所以给客户端:】改为:sendMessage(host+"说:"+line); (8)在【将pw放到数组最后一个格子里(元素):】 下面添加:通知所有客户端,该用户上线了:
(9)同理:在run()try-catchfinally中:通知所有客户端,该用户下线了:

(1)客户端:

package apiday.socket.v9;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 聊天室客户端Client:
 * java.net.Socket:————>Socket:插座 套接字
 * Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
 * 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
 *
 * 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
 * 参数1:服务端的IP地址
 * 参数2:服务端开启的服务端口
 * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
 *
 * 2、
 * 01.Socket提供了两个重要的方法:
 *    (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
 *    (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
 *
 * 02.Socket提供了:
 *    (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
 *    (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
 *
 *
 * 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
 *
 * 4、创建类ServerHandler 并implements Runnable
 */
public class Client {
    /*
        java.net.Socket 插座 套接字
        Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
        读写完成数据交换。
     */
    private Socket socket;


    /**
     * 构造方法:用于初始化客户端
     */
    public Client() {
        try {
            /**
             * 实例化Socket时常用的构造方法:Socket(String host,int port)
             * 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
             * 参数1:服务端的IP地址
             * 参数2:服务端开启的服务端口
             * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
             * 该机器上的服务端应用程序从而与之建立连接。
             */
            //本机IP地址的写法可以是“localhost”
            System.out.println("正在连接服务端");
            socket = new Socket("192.168.0.104", 8088);
            System.out.println("与服务端连接了");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            //启动用于读取服务端发送过来消息的线程:
            ServerHandler handler = new ServerHandler();
            Thread t = new Thread(handler);
            t.start();

            /* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.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();
                //equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
                if ("exit".equalsIgnoreCase(line)) {
                     break;
                }
                pw.println(line);//自动行刷新,调用println()方法
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //Socket提供了.close()方法:可以与远端计算机断开连接。
                // 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /** 该线程任务负责处理服务端发送过来的消息:服务器处理程序 */
    private class ServerHandler implements Runnable{
        public void run() {
            try{
                //通过socket获取输入流读取服务端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //循环读取服务端发送过来的每一行字符串:
                String line;
                while((line=br.readLine())!=null){
                    System.out.println(line);
                }
            }catch (IOException e){

            }
        }
    }


    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

(2)服务端:

package apiday.socket.v9;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
 * 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
 * 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
 *   (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
 *   (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
 *
 * 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
 *    如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
 *    java.net.BindException:address already in use
 *    绑定异常:地址被使用了
 *
 * 3、ServerSocket的accept()方法:
 *   是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
 *   会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
 *   阻塞方法:调用后,程序就"卡住"不往下执行了。
 *
 * 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
 *
 * 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
 *
 * 6、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
 *    {对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
 *     java.net.SocketException: connection reset--->服务端无法避免这个异常。}
 *    (1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容):
 */

public class Server {
    /*
    java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
    (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
    (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
     */
    private ServerSocket serverSocket;
    //存放所有客户端输出流,用于广播消息:
    private PrintWriter[] allOut = {};

    public Server()  {
        try {
            /*
                ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
                端口建立连接。
                如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
                java.net.BindException:address already in use
                绑定异常:地址被使用了
             */
            System.out.println("正在启动服务端");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            /*
                ServerSocket的accept方法是一个阻塞方法。
                开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
                会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
                相当于是接电话的动作。
                阻塞方法:调用后,程序就"卡住"不往下执行了。
             */
            while (true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互: Handler:处理器
                //①创建线程任务
                ClientHandler clientHandler = new ClientHandler(socket);
                //②创建1条线程执行该任务
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
    private class ClientHandler implements Runnable{
        private Socket socket;
        //记录远端客户端计算机的IP信息:
        private String host;

        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            PrintWriter pw = null;
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于给对方发消息:
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                //将该输出流存入共享数组allOut中:
                //(1)扩容:
                allOut = Arrays.copyOf(allOut,allOut.length+1);
                //(2)将pw放到数组最后一个格子里(元素):
                allOut[allOut.length-1] = pw;
                //通知所有客户端,该用户上线了:
                sendMessage(host + "上线了,当前在线人数:"+allOut.length);
            /*
            阻塞:
            这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
            当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
            处于阻塞状态,直到对方发送过来一行字符串为止。
            如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。

            对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法
            会抛出下面异常: --->服务端无法避免这个异常。
            java.net.SocketException: connection reset

             */
                String line;
                while ((line = br.readLine())!=null){
                    //遍历allOut数组,将消息发送所以给客户端:
                    sendMessage(host+"说:"+line);
                }
            } catch (IOException e) {
                //e.printStackTrace();
            } finally {
                //处理客户端断开连接后的操作:
                //将pw从数组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 line
         */
        private void sendMessage(String line){
            System.out.println(line);
            //遍历allOut数组,将消息发送所以给客户端:
            for(int i=0;i<allOut.length;i++) {
                allOut[i].println(line);
            }
        }
    }


    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

10. v10:服务端解决多线程并发安全问题/选取合适的锁对象——>最终版

为了让能叫消息转发给所有客户端,我们在Server上添加了一个数组类型的属性allOut,并且
供所有线程ClientHandler使用,
这时对数组的操作要考虑并发安全问题 :
(1)this不可以
(2)allOut不可以。大多数情况下可以选择临界资源作为锁对象,但是这里不行。 :
   从上述代码可以看出,锁定allOut并没有限制多个线程(ClientHandler)操作allOut数组,
   还是存在并发安全问题。 
(3)可以选取外部类对象作为锁对象,因为这些内部类ClientHandler都从属于这个外部类对象Server.this 
   还要考虑对数组的不同操作之间的互斥问题,道理同上。因此,对allOut数组的扩容,缩容和
   遍历操作要进行互斥。 

当两个客户端同时上线(橙,绿):
在这里插入图片描述
两个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数组的扩容,缩容和遍历操作要进行互斥。

-------最终代码:
(1)客户端:

package apiday.socket.v10;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 聊天室客户端Client:
 * java.net.Socket:————>Socket:插座 套接字
 * Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
 * 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
 *
 * 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
 * 参数1:服务端的IP地址
 * 参数2:服务端开启的服务端口
 * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
 *
 * 2、
 * 01.Socket提供了两个重要的方法:
 *    (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
 *    (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
 *
 * 02.Socket提供了:
 *    (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
 *    (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
 *
 *
 * 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
 *
 * 4、创建类ServerHandler 并implements Runnable
 */
public class Client {
    /*
        java.net.Socket 插座 套接字
        Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
        读写完成数据交换。
     */
    private Socket socket;


    /**
     * 构造方法:用于初始化客户端
     */
    public Client() {
        try {
            /**
             * 实例化Socket时常用的构造方法:Socket(String host,int port)
             * 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
             * 参数1:服务端的IP地址
             * 参数2:服务端开启的服务端口
             * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
             * 该机器上的服务端应用程序从而与之建立连接。
             */
            //本机IP地址的写法可以是“localhost”
            System.out.println("正在连接服务端");
            socket = new Socket("192.168.0.104", 8088);
            System.out.println("与服务端连接了");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 客户端开始工作的方法
     */
    public void start() {
        try {
            //启动用于读取服务端发送过来消息的线程:
            ServerHandler handler = new ServerHandler();
            Thread t = new Thread(handler);
            t.start();

            /* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.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();
                //equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
                if ("exit".equalsIgnoreCase(line)) {
                     break;
                }
                pw.println(line);//自动行刷新,调用println()方法
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //Socket提供了.close()方法:可以与远端计算机断开连接。
                // 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /** 该线程任务负责处理服务端发送过来的消息:服务器处理程序 */
    private class ServerHandler implements Runnable{
        public void run() {
            try{
                //通过socket获取输入流读取服务端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //循环读取服务端发送过来的每一行字符串:
                String line;
                while((line=br.readLine())!=null){
                    System.out.println(line);
                }
            }catch (IOException e){

            }
        }
    }


    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

(2)服务端:将数组换为集合:

package apiday.socket.v10;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
/**
 * 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
 * 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
 *   (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
 *   (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
 *
 * 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
 *    如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
 *    java.net.BindException:address already in use
 *    绑定异常:地址被使用了
 *
 * 3、ServerSocket的accept()方法:
 *   是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
 *   会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
 *   阻塞方法:调用后,程序就"卡住"不往下执行了。
 *
 * 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
 *
 * 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
 *
 * 6、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
 *    {对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
 *     java.net.SocketException: connection reset--->服务端无法避免这个异常。}
 *    (1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容):
 *
 * 7、选取合适的锁对象:-------外部类名.this(内部类访问外部类对象)
 *   (1)写一个同步块————>synchronized (Server.this) { 扩容、放在最后一个元素}
 *   (2)写一个同步块————>synchronized (Server.this) { for(){if(){缩容、放在最后一个元素}}}
 *   (3)将【广播消息给所有客户端】中的也放在同步块里:synchronized (Server.this) {打印语句、for}
 */

public class Server {
    /*
    java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
    (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
    (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
     */
    private ServerSocket serverSocket;
    //存放所有客户端输出流,用于广播消息:
//    private PrintWriter[] allOut = {};
    /* 将数组换成集合的形式:*/
    private Collection<PrintWriter> allOut = new ArrayDeque<>();




    public Server()  {
        try {
            /*
                ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
                端口建立连接。
                如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
                java.net.BindException:address already in use
                绑定异常:地址被使用了
             */
            System.out.println("正在启动服务端");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            /*
                ServerSocket的accept方法是一个阻塞方法。
                开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
                会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
                相当于是接电话的动作。
                阻塞方法:调用后,程序就"卡住"不往下执行了。
             */
            while (true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互: Handler:处理器
                //①创建线程任务
                ClientHandler clientHandler = new ClientHandler(socket);
                //②创建1条线程执行该任务
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    /** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
    private class ClientHandler implements Runnable{
        private Socket socket;
        //记录远端客户端计算机的IP信息:
        private String host;

        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            PrintWriter pw = null;
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于给对方发消息:
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                //将该输出流存入共享数组allOut中(存放着所有客户输出流):(在下面实现将消息回复给所有客户端)
                /*
                    ①不可以:
                    因为this是ClientHandler。不同线程是不同的ClientHandler任务
                    synchronized (this) {

                    ②不可以:
                    通常情况下,同步监视器对象选取的就是多个线程并发操作的临界资源。这里临界资源
                    就是数组allOut。但是这里之所以不可以是因为同步块中有对该数组扩容的操作,这
                    会导致allOut对象发生了变化,其他需要同步执行该代码块看到的就不再是之前的allOut
                    导致同步失效
                    synchronized (allOut) {
                 */
                synchronized (Server.this) {
                    //(1)扩容:
                    //allOut = Arrays.copyOf(allOut, allOut.length + 1);
                    //(2)将pw放到数组最后一个格子里(元素):
                    //allOut[allOut.length - 1] = pw;

                    allOut.add(pw);
                }
                //通知所有客户端,该用户上线了:
                //sendMessage(host + "上线了,当前在线人数:"+allOut.length);
                sendMessage(host + "上线了,当前在线人数:"+allOut.size());
            /*
            阻塞:
            这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
            当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
            处于阻塞状态,直到对方发送过来一行字符串为止。
            如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。

            对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法
            会抛出下面异常: --->服务端无法避免这个异常。
            java.net.SocketException: connection reset

             */
                String line;
                while ((line = br.readLine())!=null){
                    //遍历allOut数组,将消息发送所以给客户端:
                    sendMessage(host+"说:"+line);
                }
            } catch (IOException e) {
                //e.printStackTrace();
            } finally {
                //处理客户端断开连接后的操作:
                synchronized (Server.this) {
                    //将pw从数组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;
//                        }
//                    }
                    allOut.remove(pw);
                }

                //通知所有客户端,该用户下线了:
//                sendMessage(host + "下线了,当前在线人数:"+allOut.length);
                sendMessage(host + "下线了,当前在线人数:"+allOut.size());

                try {
                    socket.close();//与客户端断开链接
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 广播消息给所有客户端(将消息群发给客户端)
         * @param line
         */
        private void sendMessage(String line){
            synchronized (Server.this) {
                System.out.println(line);
                //遍历allOut数组,将消息发送所以给客户端:
//                for (int i = 0; i < allOut.length; i++) {
//                    allOut[i].println(line);
//                }
                for(PrintWriter pw : allOut){
                    pw.println(line);
                }
            }
        }
    }


    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

11. v11:非阻塞IO(NIO)实现聊天室服务端

在这里插入图片描述

(1)客户端:

package apiday.socket.v11;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
 * 聊天室客户端Client:
 * java.net.Socket:————>Socket:插座 套接字
 * Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
 * 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
 *
 * 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
 * 参数1:服务端的IP地址
 * 参数2:服务端开启的服务端口
 * 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
 *
 * 2、
 * 01.Socket提供了两个重要的方法:
 *    (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
 *    (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
 *
 * 02.Socket提供了:
 *    (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
 *    (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
 *
 *
 * 3、String提供方法:equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
 *
 * 4、创建类ServerHandler 并implements Runnable
 */
public class Client {
    /*
        java.net.Socket 套接字
        封装了TCP协议的通讯细节,使用它可以和远端计算机建立TCP链接,并基于两条流的读写操作
        完成与远端计算机的数据交换。

     */
    private Socket socket;

    /**
     * 构造方法,用于初始化客户端
     */
    public Client(){
        try {
            /*
                实例化Socket时通常需要传入两个参数:
                1:远端计算机的地址信息(IP) 192.168.1.1
                2:远端计算机开启的服务端口
                我们通过地址信息找到网络上的服务端计算机,通过端口链接到该机器上运行的服务端
                应用程序。
                实例化的过程就是与服务端链接的过程,若链接失败会抛出异常!
             */
            System.out.println("正在连接服务端...");
            socket = new Socket("localhost",8088);
            System.out.println("与服务端建立连接!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 客户端开始工作的方法
     */
    public void start(){
        //启动一条线程来处理读取服务端发送过来消息的操作
        ServerHandler handler = new ServerHandler();
        Thread t = new Thread(handler);
        t.setDaemon(true);//如果不再发消息了也就不用再读客户端消息了,所以可以设置为守护线程
        t.start();
        try {
            /*
                通过Socket的方法getOutputStream()可以获取一个字节输出流
                使用这个输出流写出的字节会发送给远端计算机.
             */
            OutputStream out = socket.getOutputStream();
            //转换输出流(高级流),1:负责衔接字符与字节流  2:将写出的字符转换为字节
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            //缓冲字符输出流(高级流),负责块写文本数据提高写出效率
            BufferedWriter bw = new BufferedWriter(osw);
            //PrintWriter(高级流),1:按行写出字符串  2:自动行刷新
            PrintWriter pw = new PrintWriter(bw,true);

            Scanner scanner = new Scanner(System.in);
            System.out.println("请开始输入内容,单独输入exit退出。");
            while(true) {
                String line = scanner.nextLine();
                if("exit".equals(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(){
            try {
                //通过socket获取输入流,用于读取服务端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                String line;
                //循环读取服务端发送过来的每一条消息并输出
                while((line = br.readLine())!=null){
                    System.out.println(line);
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
}

(2)NIO实现聊天室服务端

package apiday.socket.v11;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
 * NIO实现聊天室服务端
 *
 * netty框架:对nio的封装
 */
public class NIOServer {
    public static void main(String[] args) {
        try {
            //存放所有客户端的SocketChannel,用于广播消息
            List<SocketChannel> allChannel = new ArrayList<>();


            //创建总机,ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //ServerSocketChannel模式为阻塞模式,可以将其设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);//传入false设置为非阻塞模式
            //为ServerSocketChannel绑定服务端口,客户端可以通过该端口与我们建立连接
            serverSocketChannel.bind(new InetSocketAddress(8088));
            //以上创建为固定模式,以后都可以用这样的形式创建ServerSocketChannel的非阻塞模式

            /*
                多路选择器的应用
                这个是NIO解决非阻塞的关键API,用于监听所有通道对应的事件,并做出对应的操作。
                我们的线程只要轮询处理多路选择器中待处理的通道事件即可完成所有通道的工作,避免使用大量线程
                处于阻塞来减少不必要的系统开销。
             */
            Selector selector = Selector.open();//使用其静态方法open获取一个多路选择器实例

            /**
             * 让"总机"ServerSocketChannel向多路选择器上注册一个事件,即:accept事件。
             * 原因:原来使用ServerSocket时,一旦主线程调用accept方法就会进入阻塞状态,直到一个客户端连接
             * 否则将无法继续执行其他工作。而现在的操作是让多路选择器关心该事件,避免让线程处于阻塞。
             */
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


            while(true) {
                /*
                    多路选择器的select方法
                    当注册在该选择器上的channel有事件待处理时,此方法会立即返回一个int值,表示有多少个事件待处理
                    若没有任何事件待处理时,此方法会形成阻塞。
                 */
                System.out.println("等待选择器告知是否有事件等待处理...");
                selector.select();
                System.out.println("选择器有事件待处理!!!");
                //通过选择器获取所有待处理的事件
                Set<SelectionKey> keySet = selector.selectedKeys();
                //遍历集合,将所有待处理的事件处理完毕
                for (SelectionKey key : keySet) {
                    //判断该事件是否为可以接受一个客户端连接了(对应的是向多路选择器注册的事件SelectionKey.OP_ACCEPT)
                    if (key.isAcceptable()) {
                        //处理接收客户端连接的操作
                        /*
                            通过SelectionKey的方法channel()获取该事件触发的通道

                            因为只有ServerSocketChannel向多路选择器注册了OP_ACCEPT事件,因此当该事件
                            isAcceptable()返回值为true,则说明该事件一定是由ServerSocketChannel触发的
                            所以我们通过该事件获取触发该事件的通道时,一定获取的是ServerSocketChannel
                         */
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        /*
                            获取的SocketChannel与原来ServerSocket.accept返回的Socket道理一致
                            通过该SocketChannel就可以与连接的客户端进行双向交互数据了
                         */
                        SocketChannel socket = channel.accept();//接受客户端的连接
                        /*
                            非阻塞的ServerSocketChannel就算多路选择器提示有客户端请求可接受了,accept返回时
                            得到的SocketChanel有可能为null
                         */
                        if (socket == null) {
                            continue;//忽略本次事件的后续处理
                        }
                        /*
                            当我们接受了客户端连接后,原来的聊天室服务端我们通过Socket获取输入流读取客户端
                            发送过来消息的操作时如果客户端不发送内容,那么读取操作就会阻塞!
                            对此,我们将当前SocketChannel它的读取消息操作也注册到多路选择器中,这样一来只有
                            当客户端发送过来消息时,多路选择器才会通知我们进行处理。
                         */
                        //将当前SocketChannel设置为非阻塞模式
                        socket.configureBlocking(false);
                        //向多路选择器中注册读取客户端消息的事件
                        socket.register(selector, SelectionKey.OP_READ);
                        //将该SocketChannel存入共享集合,用于将消息广播
                        allChannel.add(socket);
                        System.out.println("一个客户端连接了!当前在线人数:"+allChannel.size() );


                        //该事件是否为某个SocketChannel有消息可以读取了(某个客户端发送过来了消息)
                    }else if(key.isReadable()){
                        try {
                            //通过事件获取触发了该事件的channel
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            //通过SocketChannel读取该客户端发送过来的消息
                            ByteBuffer buffer = ByteBuffer.allocate(1024);

                            //非阻塞状态下,有可能读取数据时未读取到任何字节
                            int len = socketChannel.read(buffer);
                            if (len == 0) {
                                continue;
                            } else if (len == -1) {//若本次读取的长度返回值为-1则表示客户端断开连接了
                                socketChannel.close();//关闭SocketChannel与客户端也断开
                                allChannel.remove(socketChannel);
                                continue;
                            }

                            buffer.flip();//反转后position到limit之间就是本次读取到的数据了
                            byte[] data = new byte[buffer.limit()];
                           /*
                                Buffer的get方法要求我们传入一个字节数组,作用是将当前Buffer中从下标
                                position开始的连续data数组长度的字节量装入该data数组。
                            */
                            buffer.get(data);//调用完毕后,data中保存的就是buffer中本次读取到的所有字节了
                            //将读取的消息转换为字符串(客户端发送过来的消息)
                            String line = new String(data, StandardCharsets.UTF_8);
                            //通过SocketChannel获取客户端地址信息
                            String host = socketChannel.socket().getInetAddress().getHostAddress();
                            System.out.print(host+"说:" + line);

                            //遍历所有的SocketChannel,将该消息发送给所有客户端
                            for(SocketChannel sc : allChannel){
                                buffer.flip();//position:0   limit:buffer中所有之前读取到的字节
                                sc.write(buffer);//position=limit=buffer中所有之前读取到的字节
                            }




                        }catch(IOException e){
                            //读取客户端消息这里若抛出异常,则通常是客户端强行断开连接造成的。
                            key.channel().close();//断开该SocketChannel与客户端断开连接即可
                            allChannel.remove(key.channel());
                        }
                    }
                }

            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引言 1. 前提 2. Java的学习 3. 目标 4. 联机文档 5. 章节 6. 练习 7. 多媒体CD-ROM 8. 源代码 9. 编码样式 10. Java本 11. 课程和培训 12. 错误 13. 封面设计 14. 致谢 第1章 对象入门 1.1 抽象的进步 1.2 对象的接口 1.3 实现方案的隐藏 1.4 方案的重复使用 1.5 继承:重新使用接口 1.5.1 改善基础类 1.5.2 等价和类似关系 1.6 多形对象的互换使用 1.6.1 动态绑定 1.6.2 抽象的基础类和接口 1.7 对象的创建和存在时间 1.7.1 集合与继承器 1.7.2 单根结构 1.7.3 集合库与方便使用集合 1.7.4 清除时的困境:由谁负责清除? 1.8 违例控制:解决错误 1.9 多线程 1.10 永久性 1.11 Java和因特网 1.11.1 什么是Web? 1.11.2 客户端编程 1.11.3 服务器端编程 1.11.4 一个独立的领域:应用程序 1.12 分析和设计 1.12.1 不要迷失 1.12.2 阶段0:拟出一个计划 1.12.3 阶段1:要制作什么? 1.12.4 阶段2:开始构建? 1.12.5 阶段3:正式创建 1.12.6 阶段4:校订 1.12.7 计划的回报 1.13 Java还是C++? 第2章 一切都是对象 2.1 用句柄操纵对象 2.2 必须创建所有对象 2.2.1 保存在什么地方 2.2.2 特殊情况:主类型 2.2.3 Java中的数组 2.3 绝对不要清除对象 2.3.1 作用域 2.3.2 对象的作用域 2.4 新建数据类型:类 2.4.1 字段和方法 2.5 方法、自变量和返回值 2.5.1 自变量列表 2.6 构建Java程序 2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入HTML 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档标记 2.8.7 方法文档标记 2.8.8 文档示例 2.9 编码样式 2.10 总结 2.11 练习 第3章 控制程序流程 3.1 使用Java运算符 3.1.1 优先级 3.1.2 赋值 3.1.3 算术运算符 3.1.4 自动递增和递减 3.1.5 关系运算符 3.1.6 逻辑运算符 3.1.7 按位运算符 3.1.8 移位运算符 3.1.9 三元if-else运算符 3.1.10 逗号运算符 3.1.11 字串运算符+ 3.1.12 运算符常规操作规则 3.1.13 造型运算符 3.1.14 Java没有“sizeof” 3.1.15 复习计算顺序 3.1.16 运算符总结 3.2 执行控制 3.2.1 真和假 3.2.2 if-else 3.2.3 反复 3.2.4 do-while 3.2.5 for 3.2.6 中断和继续 3.2.7 切换 3.3 总结 3.4 练习 第4章 初始化和清除 4.1 由构建器保证初始化 4.2 方法过载 4.2.1 区分过载方法 4.2.2 主类型的过载 4.2.3 返回值过载 4.2.4 默认构建器 4.2.5 this关键字 4.3 清除:收尾和垃圾收集 4.3.1 finalize()用途何在 4.3.2 必须执行清除 4.4 成员初始化 4.4.1 规定初始化 4.4.2 构建器初始化 4.5 数组初始化 4.5.1 多维数组 4.6 总结 4.7 练习 第5章 隐藏实施过程 5.1 包:库单元 5.1.1 创建独一无二的包名 5.1.2 自定义工具库 5.1.3 利用导入改变行为 5.1.4 包的停用 5.2 Java访问指示符 5.2.1 “友好的” 5.2.2 public:接口访问 5.2.3 private:不能接触 5.2.4 protected:“友好的一种” 5.3 接口与实现 5.4 类访问 5.5 总结 5.6 练习 第6章 类再生 6.1 合成的语法 6.2 继承的语法 6.2.1 初始化基础类 6.3 合成与继承的结合 6.3.1 确保正确的清除 6.3.2 名字的隐藏 6.4 到底选择合成还是继承 6.5 protected 6.6 递增开发 6.7 上溯造型 6.7.1 何谓“上溯造型”? 6.8 final关键字 6.8.1 final数据 6.8.2 final方法 6.8.3 final类 6.8.4 final的注意事项 6.9 初始化和类装载 6.9.1 继承初始化 6.10 总结 6.11 练习 第7章 多形性 7.1 上溯造型 7.1.1 为什么要上溯造型 7.2 深入理解 7.2.1 方法调用的绑定 7.2.2 产生正确的行为 7.2.3 扩展性 7.3 覆盖与过载 7.4 抽象类和方法 7.5 接口 7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用外部类对象 7.6.6 从内部类继承 7.6.7 内部类可以覆盖吗? 7.6.8 内部类标识符 7.6.9 为什么要用内部类:控制框架 7.7 构建器和多形性 7.7.1 构建器的调用顺序 7.7.2 继承和finalize() 7.7.3 构建器内部的多形性方法的行为 7.8 通过继承进行设计 7.8.1 纯继承与扩展 7.8.2 下溯造型与运行期类型标识 7.9 总结 7.10 练习 第8章 对象的容纳 8.1 数组 8.1.1 数组和第一类对象 8.1.2 数组的返回 8.2 集合 8.2.1 缺点:类型未知 8.3 枚举器(反复器) 8.4 集合的类型 8.4.1 Vector 8.4.2 BitSet 8.4.3 Stack 8.4.4 Hashtable 8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 8.9 练习 第9章 违例差错控制 9.1 基本违例 9.1.1 违例自变量 9.2 违例的捕获 9.2.1 try块 9.2.2 违例控制器 9.2.3 违例规范 9.2.4 捕获所有违例 9.2.5 重新“掷”出违例 9.3 标准Java违例 9.3.1 RuntimeException的特殊情况 9.4 创建自己的违例 9.5 违例的限制 9.6 用finally清除 9.6.1 用finally做什么 9.6.2 缺点:丢失的违例 9.7 构建器 9.8 违例匹配 9.8.1 违例准则 9.9 总结 9.10 练习 第10章 Java IO系统 10.1 输入和输出 10.1.1 InputStream的类型 10.1.2 OutputStream的类型 10.2 增添属性和有用的接口 10.2.1 通过FilterInputStream从InputStream里读入数据 10.2.2 通过FilterOutputStream向OutputStream里写入数据 10.3 本身的缺陷:RandomAccessFile 10.4 File类 10.4.1 目录列表器 10.4.2 检查与创建目录 10.5 IO流的典型应用 10.5.1 输入流 10.5.2 输出流 10.5.3 快捷文件处理 10.5.4 从标准输入中读取数据 10.5.5 管道数据流 10.6 StreamTokenizer 10.6.1 StringTokenizer 10.7 Java 1.1的IO流 10.7.1 数据的发起与接收 10.7.2 修改数据流的行为 10.7.3 未改变的类 10.7.4 一个例子 10.7.5 重定向标准IO 10.8 压缩 10.8.1 用GZIP进行简单压缩 10.8.2 用Zip进行多文件保存 10.8.3 Java归档(jar)实用程序 10.9 对象串联 10.9.1 寻找类 10.9.2 序列化的控制 10.9.3 利用“持久性” 10.10 总结 10.11 练习 第11章 运行期类型鉴定 11.1 对RTTI的需要 11.1.1 Class对象 11.1.2 造型前的检查 11.2 RTTI语法 11.3 反射:运行期类信息 11.3.1 一个类方法提取器 11.4 总结 11.5 练习 第12章 传递和返回对象 12.1 传递句柄 12.1.1 别名问题 12.2 制作本地副本 12.2.1 按值传递 12.2.2 克隆对象 12.2.3 使类具有克隆能力 12.2.4 成功的克隆 12.2.5 Object.clone()的效果 12.2.6 克隆合成对象 12.2.7 用Vector进行深层复制 12.2.8 通过序列化进行深层复制 12.2.9 使克隆具有更大的深度 12.2.10 为什么有这个奇怪的设计 12.3 克隆的控制 12.3.1 副本构建器 12.4 只读类 12.4.1 创建只读类 12.4.2 “一成不变”的弊端 12.4.3 不变字串 12.4.4 String和StringBuffer类 12.4.5 字串的特殊性 12.5 总结 12.6 练习 第13章 创建窗口和程序片 13.1 为何要用AWT? 13.2 基本程序片 13.2.1 程序片的测试 13.2.2 一个更图形化的例子 13.2.3 框架方法的演示 13.3 制作按钮 13.4 捕获事件 13.5 文本字段 13.6 文本区域 13.7 标签 13.8 复选框 13.9 单选钮 13.10 下拉列表 13.11 列表框 13.11.1 handleEvent() 13.12 布局的控制 13.12.1 FlowLayout 13.12.2 BorderLayout 13.12.3 GridLayout 13.12.4 CardLayout 13.12.5 GridBagLayout 13.13 action的替用品 13.14 程序片的局限 13.14.1 程序片的优点 13.15 视窗化应用 13.15.1 菜单 13.15.2 对话框 13.16 新型AWT 13.16.1 新的事件模型 13.16.2 事件和接收者类型 13.16.3 用Java 1.1 AWT制作窗口和程序片 13.16.4 再探早期示例 13.16.5 动态绑定事件 13.16.6 将商业逻辑与UI逻辑区分开 13.16.7 推荐编码方法 13.17 Java 1.1 UI API 13.17.1 桌面颜色 13.17.2 打印 13.17.3 剪贴板 13.18 可视编程和Beans 13.18.1 什么是Bean 13.18.2 用Introspector提取BeanInfo 13.18.3 一个更复杂的Bean 13.18.4 Bean的封装 13.18.5 更复杂的Bean支持 13.18.6 Bean更多的知识 13.19 Swing入门 13.19.1 Swing有哪些优点 13.19.2 方便的转换 13.19.3 显示框架 13.19.4 工具提示 13.19.5 边框 13.19.6 按钮 13.19.7 按钮组 13.19.8 图标 13.19.9 菜单 13.19.10 弹出式菜单 13.19.11 列表框和组合框 13.19.12 滑杆和进度指示条 13.19.13 树 13.19.14 表格 13.19.15 卡片式对话框 13.19.16 Swing消息框 13.19.17 Swing更多的知识 13.20 总结 13.21 练习 第14章 多线程 14.1 反应灵敏的用户界面 14.1.1 从线程继承 14.1.2 针对用户界面的多线程 14.1.3 用主类合并线程 14.1.4 制作多个线程 14.1.5 Daemon线程 14.2 共享有限的资源 14.2.1 资源访问的错误方法 14.2.2 Java如何共享资源 14.2.3 回顾Java Beans 14.3 堵塞 14.3.1 为何会堵塞 14.3.2 死锁 14.4 优先级 14.4.1 线程组 14.5 回顾runnable 14.5.1 过多的线程 14.6 总结 14.7 练习 第15章 网络编程 15.1 机器的标识 15.1.1 服务器和客户机 15.1.2 端口:机器内独一无二的场所 15.2 套接字 15.2.1 一个简单的服务器和客户机程序 15.3 服务多个客户 15.4 数据报 15.5 一个Web应用 15.5.1 服务器应用 15.5.2 NameSender程序片 15.5.3 15.5.3 要注意的问题 15.6 Java与CGI的沟通 15.6.1 CGI数据的编码 15.6.2 程序片 15.6.3 用C++写的CGI程序 15.6.4 POST的概念 15.7 用JDBC连接数据库 15.7.1 获得学习示例 15.7.2 查找程序的GUI本 15.7.3 JDBC API为何如何复杂 15.8 远程方法 15.8.1 远程接口概念 15.8.2 远程接口的实施 15.8.3 创建根与干 15.8.4 使用远程对象 15.8.5 RMI的替选方案 15.9 总结 15.10 练习 第16章 设计范式 16.1 范式的概念 16.1.1 单子 16.1.2 范式分类 16.2 观察器范式 16.3 模拟垃圾回收站 16.4 改进设计 16.4.1 “制作更多的对象” 16.4.2 用于原型创建的一个范式 16.5 抽象的应用 16.6 多重派遣 16.6.1 实现双重派遣 16.7 访问器范式 16.8 RTTI有害吗 16.9 总结 16.10 练习 第17章 项目 17.1 文字处理 17.1.1 提取代码列表 17.1.2 检查大小写样式 17.2 方法查找工具 17.3 复杂性理论 17.4 总结 17.5 练习 附录A 使用非Java代码 A.1 Java固有接口 A.1.1 调用固有方法 A.1.2 访问JNI函数:JNIEnv自变量 A.1.3 传递和使用Java对象 A.1.4 JNI和Java违例 A.1.5 JNI和线程处理 A.1.6 使用现成代码 A.2 微软的解决方案 A.3 J/Direct A.3.1 @dll.import引导命令 A.3.2 com.ms.win32包 A.3.3 汇集 A.3.4 编写回调函数 A.3.5 其他J/Direct特性 A.4 本原接口(RNI) A.4.1 RNI总结 A.5 Java/COM集成 A.5.1 COM基础 A.5.2 MS Java/COM集成 A.5.3 用Java设计COM服务器 A.5.4 用Java设计COM客户 A.5.5 ActiveX/Beans集成 A.5.6 固有方法与程序片的注意事项 A.6 CORBA A.6.1 CORBA基础 A.6.2 一个例子 A.6.3 Java程序片和CORBA A.6.4 比较CORBA与RMI A.7 总结 附录B 对比C++和Java 附录C Java编程规则 附录D 性能 D.1 基本方法 D.2 寻找瓶颈 D.2.1 安插自己的测试代码 D.2.2 JDK性能评测[2] D.2.3 特殊工具 D.2.4 性能评测的技巧 D.3 提速方法 D.3.1 常规手段 D.3.2 依赖语言的方法 D.3.3 特殊情况 D.4 参考资源 D.4.1 性能工具 D.4.2 Web站点 D.4.3 文章 D.4.4 Java专业书籍 D.4.5 一般书籍 附录E 关于垃圾收集的一些话 附录F 推荐读物
课程目标 1、精通JavaEE平台开发的JAVA软件工程师,能够胜任各种行业的企业级软件开发工作; 2、具备一年以上软件开发经验; 3、熟悉java软件开发流程;具备解决目前主要问题的能力; 4、良好的语言表达、沟通能力、工作责任心和团队意识。 课程针对就业岗位 java软件工程师系统设计师系统分析师 课程目录 [第1节] 1.面向对象开发 [第2节] 2.定义类 [第3节] 3.创建对象 [第4节] 4.类成员之成员方法 [第5节] 5.类成员之成员变量 [第6节] 6.构造方法 [第7节] 7.案例:影片播放机 [第8节] 8.小结 [第9节] 1.2.1为什么需要封装1 [第10节] 1.2.1为什么需要封装2 [第11节] 1.2.2如何实现封装2 [第12节] 1.2.3如何使用包1 [第13节] 1.2.3如何使用包2 [第14节] 1.2.4类成员的访问控制1 [第15节] 1.2.4类成员的访问控制2 [第16节] 1.2.4类成员的访问控制3 [第17节] 1.3.1 如何实现继承1 [第18节] 1.3.1 如何实现继承2 [第19节] 1.3.2 不同权限分配下的继承1 [第20节] 1.3.2 不同权限分配下的继承2 [第21节] 1.3.3 如何实现方法重写1 [第22节] 1.3.3 如何实现方法重写2 [第23节] 1.3.4 继承条件下的构造方法的执行1 [第24节] 1.3.4 继承条件下的构造方法的执行2 [第25节] 1.3.5 如何重写equals()方法1 [第26节] 1.3.5 如何重写equals()方法2 [第27节] 1.3.5 如何重写equals()方法3 [第28节] 1.4.1 未使用多态实现不同宠物看病1 [第29节] 1.4.1 未使用多态实现不同宠物看病2 [第30节] 1.4.2 使用多态实现不同宠物看病1 [第31节] 1.4.2 使用多态实现不同宠物看病2 [第32节] 1.4.3 向下转型1 [第33节] 1.4.3 向下转型2 [第34节] 1.4.4 instanceof运算符的用法1 [第35节] 1.4.4 instanceof运算符的用法2 [第36节] 1.4.5 多态的使用:父类作为形参1 [第37节] 1.4.5 多态的使用:父类作为形参2 [第38节] 1.4.6 多态的使用:父类作为返回值2 [第39节] 1.4.6 多态的使用:父类作为返回值3 [第40节] 1.5.1 汽车租赁运行效果2 [第41节] 1.5.2 汽车租赁框架1 [第42节] 1.5.2 汽车足联框架2 [第43节] 1.5.3 处理空指针异常1 [第44节] 1.5.3 处理空指针异常2 [第45节] 1.6.1 如何定义和使用抽象类和接口1 [第46节] 1.6.1 如何定义和使用抽象类和接口2 [第47节] 1.6.2 使用抽象类的好处1 [第48节] 1.6.2 使用抽象类的好处2 [第49节] 1.6.3 使用接口的好处(一)1 [第50节] 1.6.3 使用接口的好处(一)2 [第51节] 1.6.4 使用接口的好处(二)1 [第52节] 1.6.4 使用接口的好处(二)2 [第53节] 1.6.4 使用接口的好处(二)3 [第54节] 1.7.1 图书销售管理-运行效果2 [第55节] 1.7.2 图书销售管理-用户、角色、权限框架1 [第56节] 1.7.2 图书销售管理-用户、角色、权限框架2 [第57节] 1.7.3 图书销售管理-查询图书、入库1 [第58节] 1.7.3 图书销售管理-查询图书、入库2 [第59节] 1.7.4 完善图书销售管理-查询图书-入库2 [第60节] 1.7.4 完善图书销售管理-查询图书-入库3 [第61节] 1.8.1 异常1 [第62节] 1.8.1 异常2 [第63节] 1.8.2 使用try-cache处理异常1 [第64节] 1.8.2 使用try-cache处理异常2 [第65节] 1.8.3 使用try-catch-finally处理异常1 [第66节] 1.8.3 使用try-catch-finally处理异常2 [第67节] 1.8.4 多个catch语句的使用1 [第68节] 1.8.4 多个catch语句的使用2 [第69节] 1.8.5 throws关键字的使用1 [第70节] 1.8.5 throws关键字的使用2 [第71节] 1.8.6 throw关键字的使用1 [第72节] 1.8.6 throw关键字的使用2 [第73节] 1.8.7 自定义异常的类型1 [第74节] 1.8.7 自定义异常的类型2 [第75节] 1.8.8 异常链的使用1 [第76节] 1.8.8 异常链的使用2 [第77节] 1.8.8 异常链的使用3 [第78节] 1.9.1 封装、继承和多态 [第79节] 1.9.2 异常 [第80节] 1.9.3 内部类串讲 [第81节] 1.10.1职场写作力
写在前面的话 引言 1. 前提 2. Java的学习 3. 目标 4. 联机文档 5. 章节 6. 练习 7. 多媒体 8. 源代码 9. 编码样式 10. Java本 11. 课程和培训 12. 错误 13. 封面设计 14. 致谢 第1章 对象入门 1.1 抽象的进步 1.2 对象的接口 1.3 实现方案的隐藏 1.4 方案的重复使用 1.5 继承:重新使用接口 1.5.1 改善基础类 1.5.2 等价和类似关系 1.6 多形对象的互换使用 1.6.1 动态绑定 1.6.2 抽象的基础类和接口 1.7 对象的创建和存在时间 1.7.1 集合与继承器 1.7.2 单根结构 1.7.3 集合库与方便使用集合 1.7.4 清除时的困境:由谁负责清除? 1.8 违例控制:解决错误 1.9 多线程 1.10 永久性 1.11 Java和因特网 1.11.1 什么是Web? 1.11.2 客户端编程 1.11.3 服务器端编程 1.11.4 一个独立的领域:应用程序 1.12 分析和设计 1.12.1 不要迷失 1.12.2 阶段0:拟出一个计划 1.12.3 阶段1:要制作什么? 1.12.4 阶段2:开始构建? 1.12.5 阶段3:正式创建 1.12.6 阶段4:校订 1.12.7 计划的回报 1.13 Java还是C++? 第2章 一切都是对象 2.1 用句柄操纵对象 2.2 必须创建所有对象 2.2.1 保存在什么地方 2.2.2 特殊情况:主类型 2.2.3 Java中的数组 2.3 绝对不要清除对象 2.3.1 作用域 2.3.2 对象的作用域 2.4 新建数据类型:类 2.4.1 字段和方法 2.5 方法、自变量和返回值 2.5.1 自变量列表 2.6 构建Java程序 2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档标记 2.8.7 方法文档标记 2.8.8 文档示例 2.9 编码样式 2.10 总结 2.11 练习 第3章 控制程序流程 3.1 使用Java运算符 3.1.1 优先级 3.1.2 赋值 3.1.3 算术运算符 3.1.4 自动递增和递减 3.1.5 关系运算符 3.1.6 逻辑运算符 3.1.7 按位运算符 3.1.8 移位运算符 3.1.9 三元if-else运算符 3.1.10 逗号运算符 3.1.11 字串运算符 3.1.12 运算符常规操作规则 3.1.13 造型运算符 3.1.14 Java没有“sizeof” 3.1.15 复习计算顺序 3.1.16 运算符总结 3.2 执行控制 3.2.1 真和假 3.2.3 反复 3.2.6 中断和继续 3.2.7 切换 3.3 总结 3.4 练习 第4章 初始化和清除 4.1 由构建器保证初始化 4.2 方法过载 4.2.1 区分过载方法 4.2.2 主类型的过载 4.2.3 返回值过载 4.2.4 默认构建器 4.2.5 this关键字 4.3 清除:收尾和垃圾收集 4.3.1 finalize()用途何在 4.3.2 必须执行清除 4.4 成员初始化 4.4.1 规定初始化 4.4.2 构建器初始化 4.5 数组初始化 4.5.1 多维数组 4.6 总结 4.7 练习 第5章 隐藏实施过程 5.1 包:库单元 5.1.1 创建独一无二的包名 5.1.2 自定义工具库 5.1.3 利用导入改变行为 5.1.4 包的停用 5.2 Java访问指示符 5.2.1 “友好的” 5.2.2 public:接口访问 5.2.3 private:不能接触 5.2.4 protected:“友好的一种” 5.3 接口与实现 5.4 类访问 5.5 总结 5.6 练习 第6章 类再生 6.1 合成的语法 6.2 继承的语法 6.2.1 初始化基础类 6.3 合成与继承的结合 6.3.1 确保正确的清除 6.3.2 名字的隐藏 6.4 到底选择合成还是继承 6.6 递增开发 6.7 上溯造型 6.7.1 何谓“上溯造型”? 6.8 final关键字 6.8.1 final数据 6.8.2 final方法 6.8.3 final类 6.8.4 final的注意事项 6.9 初始化和类装载 6.9.1 继承初始化 6.10 总结 6.11 练习 第7章 多形性 7.1 上溯造型 7.1.1 为什么要上溯造型 7.2 深入理解 7.2.1 方法调用的绑定 7.2.2 产生正确的行为 7.2.3 扩展性 7.3 覆盖与过载 7.4 抽象类和
写在前面的话 引言 1. 前提 2. Java的学习 3. 目标 4. 联机文档 5. 章节 6. 练习 7. 多媒体CD-ROM 8. 源代码 9. 编码样式 10. Java本 11. 课程和培训 12. 错误 13. 封面设计 14. 致谢 第1章 对象入门 1.1 抽象的进步 1.2 对象的接口 1.3 实现方案的隐藏 1.4 方案的重复使用 1.5 继承:重新使用接口 1.5.1 改善基础类 1.5.2 等价和类似关系 1.6 多形对象的互换使用 1.6.1 动态绑定 1.6.2 抽象的基础类和接口 1.7 对象的创建和存在时间 1.7.1 集合与继承器 1.7.2 单根结构 1.7.3 集合库与方便使用集合 1.7.4 清除时的困境:由谁负责清除? 1.8 违例控制:解决错误 1.9 多线程 1.10 永久性 1.11 Java和因特网 1.11.1 什么是Web? 1.11.2 客户端编程 1.11.3 服务器端编程 1.11.4 一个独立的领域:应用程序 1.12 分析和设计 1.12.1 不要迷失 1.12.2 阶段0:拟出一个计划 1.12.3 阶段1:要制作什么? 1.12.4 阶段2:开始构建? 1.12.5 阶段3:正式创建 1.12.6 阶段4:校订 1.12.7 计划的回报 1.13 Java还是C++? 第2章 一切都是对象 2.1 用句柄操纵对象 2.2 必须创建所有对象 2.2.1 保存在什么地方 2.2.2 特殊情况:主类型 2.2.3 Java中的数组 2.3 绝对不要清除对象 2.3.1 作用域 2.3.2 对象的作用域 2.4 新建数据类型:类 2.4.1 字段和方法 2.5 方法、自变量和返回值 2.5.1 自变量列表 2.6 构建Java程序 2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入HTML 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档标记 2.8.7 方法文档标记 2.8.8 文档示例 2.9 编码样式 2.10 总结 2.11 练习 第3章 控制程序流程 3.1 使用Java运算符 3.1.1 优先级 3.1.2 赋值 3.1.3 算术运算符 3.1.4 自动递增和递减 3.1.5 关系运算符 3.1.6 逻辑运算符 3.1.7 按位运算符 3.1.8 移位运算符 3.1.9 三元if-else运算符 3.1.10 逗号运算符 3.1.11 字串运算符+ 3.1.12 运算符常规操作规则 3.1.13 造型运算符 3.1.14 Java没有“sizeof” 3.1.15 复习计算顺序 3.1.16 运算符总结 3.2 执行控制 3.2.1 真和假 3.2.2 if-else 3.2.3 反复 3.2.4 do-while 3.2.5 for 3.2.6 中断和继续 3.2.7 切换 3.3 总结 3.4 练习 第4章 初始化和清除 4.1 由构建器保证初始化 4.2 方法过载 4.2.1 区分过载方法 4.2.2 主类型的过载 4.2.3 返回值过载 4.2.4 默认构建器 4.2.5 this关键字 4.3 清除:收尾和垃圾收集 4.3.1 finalize()用途何在 4.3.2 必须执行清除 4.4 成员初始化 4.4.1 规定初始化 4.4.2 构建器初始化 4.5 数组初始化 4.5.1 多维数组 4.6 总结 4.7 练习 第5章 隐藏实施过程 5.1 包:库单元 5.1.1 创建独一无二的包名 5.1.2 自定义工具库 5.1.3 利用导入改变行为 5.1.4 包的停用 5.2 Java访问指示符 5.2.1 “友好的” 5.2.2 public:接口访问 5.2.3 private:不能接触 5.2.4 protected:“友好的一种” 5.3 接口与实现 5.4 类访问 5.5 总结 5.6 练习 第6章 类再生 6.1 合成的语法 6.2 继承的语法 6.2.1 初始化基础类 6.3 合成与继承的结合 6.3.1 确保正确的清除 6.3.2 名字的隐藏 6.4 到底选择合成还是继承 6.5 protected 6.6 递增开发 6.7 上溯造型 6.7.1 何谓“上溯造型”? 6.8 final关键字 6.8.1 final数据 6.8.2 final方法 6.8.3 final类 6.8.4 final的注意事项 6.9 初始化和类装载 6.9.1 继承初始化 6.10 总结 6.11 练习 第7章 多形性 7.1 上溯造型 7.1.1 为什么要上溯造型 7.2 深入理解 7.2.1 方法调用的绑定 7.2.2 产生正确的行为 7.2.3 扩展性 7.3 覆盖与过载 7.4 抽象类和方法 7.5 接口 7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用外部类对象 7.6.6 从内部类继承 7.6.7 内部类可以覆盖吗? 7.6.8 内部类标识符 7.6.9 为什么要用内部类:控制框架 7.7 构建器和多形性 7.7.1 构建器的调用顺序 7.7.2 继承和finalize() 7.7.3 构建器内部的多形性方法的行为 7.8 通过继承进行设计 7.8.1 纯继承与扩展 7.8.2 下溯造型与运行期类型标识 7.9 总结 7.10 练习 第8章 对象的容纳 8.1 数组 8.1.1 数组和第一类对象 8.1.2 数组的返回 8.2 集合 8.2.1 缺点:类型未知 8.3 枚举器(反复器) 8.4 集合的类型 8.4.1 Vector 8.4.2 BitSet 8.4.3 Stack 8.4.4 Hashtable 8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 8.9 练习 第9章 违例差错控制 9.1 基本违例 9.1.1 违例自变量 9.2 违例的捕获 9.2.1 try块 9.2.2 违例控制器 9.2.3 违例规范 9.2.4 捕获所有违例 9.2.5 重新“掷”出违例 9.3 标准Java违例 9.3.1 RuntimeException的特殊情况 9.4 创建自己的违例 9.5 违例的限制 9.6 用finally清除 9.6.1 用finally做什么 9.6.2 缺点:丢失的违例 9.7 构建器 9.8 违例匹配 9.8.1 违例准则 9.9 总结 9.10 练习 第10章 Java IO系统 10.1 输入和输出 10.1.1 InputStream的类型 10.1.2 OutputStream的类型 10.2 增添属性和有用的接口 10.2.1 通过FilterInputStream从InputStream里读入数据 10.2.2 通过FilterOutputStream向OutputStream里写入数据 10.3 本身的缺陷:RandomAccessFile 10.4 File类 10.4.1 目录列表器 10.4.2 检查与创建目录 10.5 IO流的典型应用 10.5.1 输入流 10.5.2 输出流 10.5.3 快捷文件处理 10.5.4 从标准输入中读取数据 10.5.5 管道数据流 10.6 StreamTokenizer 10.6.1 StringTokenizer 10.7 Java 1.1的IO流 10.7.1 数据的发起与接收 10.7.2 修改数据流的行为 10.7.3 未改变的类 10.7.4 一个例子 10.7.5 重定向标准IO 10.8 压缩 10.8.1 用GZIP进行简单压缩 10.8.2 用Zip进行多文件保存 10.8.3 Java归档(jar)实用程序 10.9 对象串联 10.9.1 寻找类 10.9.2 序列化的控制 10.9.3 利用“持久性” 10.10 总结 10.11 练习 第11章 运行期类型鉴定 11.1 对RTTI的需要 11.1.1 Class对象 11.1.2 造型前的检查 11.2 RTTI语法 11.3 反射:运行期类信息 11.3.1 一个类方法提取器 11.4 总结 11.5 练习 第12章 传递和返回对象 12.1 传递句柄 12.1.1 别名问题 12.2 制作本地副本 12.2.1 按值传递 12.2.2 克隆对象 12.2.3 使类具有克隆能力 12.2.4 成功的克隆 12.2.5 Object.clone()的效果 12.2.6 克隆合成对象 12.2.7 用Vector进行深层复制 12.2.8 通过序列化进行深层复制 12.2.9 使克隆具有更大的深度 12.2.10 为什么有这个奇怪的设计 12.3 克隆的控制 12.3.1 副本构建器 12.4 只读类 12.4.1 创建只读类 12.4.2 “一成不变”的弊端 12.4.3 不变字串 12.4.4 String和StringBuffer类 12.4.5 字串的特殊性 12.5 总结 12.6 练习 第13章 创建窗口和程序片 13.1 为何要用AWT? 13.2 基本程序片 13.2.1 程序片的测试 13.2.2 一个更图形化的例子 13.2.3 框架方法的演示 13.3 制作按钮 13.4 捕获事件 13.5 文本字段 13.6 文本区域 13.7 标签 13.8 复选框 13.9 单选钮 13.10 下拉列表 13.11 列表框 13.11.1 handleEvent() 13.12 布局的控制 13.12.1 FlowLayout 13.12.2 BorderLayout 13.12.3 GridLayout 13.12.4 CardLayout 13.12.5 GridBagLayout 13.13 action的替用品 13.14 程序片的局限 13.14.1 程序片的优点 13.15 视窗化应用 13.15.1 菜单 13.15.2 对话框 13.16 新型AWT 13.16.1 新的事件模型 13.16.2 事件和接收者类型 13.16.3 用Java 1.1 AWT制作窗口和程序片 13.16.4 再探早期示例 13.16.5 动态绑定事件 13.16.6 将商业逻辑与UI逻辑区分开 13.16.7 推荐编码方法 13.17 Java 1.1 UI API 13.17.1 桌面颜色 13.17.2 打印 13.17.3 剪贴板 13.18 可视编程和Beans 13.18.1 什么是Bean 13.18.2 用Introspector提取BeanInfo 13.18.3 一个更复杂的Bean 13.18.4 Bean的封装 13.18.5 更复杂的Bean支持 13.18.6 Bean更多的知识 13.19 Swing入门 13.19.1 Swing有哪些优点 13.19.2 方便的转换 13.19.3 显示框架 13.19.4 工具提示 13.19.5 边框 13.19.6 按钮 13.19.7 按钮组 13.19.8 图标 13.19.9 菜单 13.19.10 弹出式菜单 13.19.11 列表框和组合框 13.19.12 滑杆和进度指示条 13.19.13 树 13.19.14 表格 13.19.15 卡片式对话框 13.19.16 Swing消息框 13.19.17 Swing更多的知识 13.20 总结 13.21 练习 第14章 多线程 14.1 反应灵敏的用户界面 14.1.1 从线程继承 14.1.2 针对用户界面的多线程 14.1.3 用主类合并线程 14.1.4 制作多个线程 14.1.5 Daemon线程 14.2 共享有限的资源 14.2.1 资源访问的错误方法 14.2.2 Java如何共享资源 14.2.3 回顾Java Beans 14.3 堵塞 14.3.1 为何会堵塞 14.3.2 死锁 14.4 优先级 14.4.1 线程组 14.5 回顾runnable 14.5.1 过多的线程 14.6 总结 14.7 练习 第15章 网络编程 15.1 机器的标识 15.1.1 服务器和客户机 15.1.2 端口:机器内独一无二的场所 15.2 套接字 15.2.1 一个简单的服务器和客户机程序 15.3 服务多个客户 15.4 数据报 15.5 一个Web应用 15.5.1 服务器应用 15.5.2 NameSender程序片 15.5.3 15.5.3 要注意的问题 15.6 Java与CGI的沟通 15.6.1 CGI数据的编码 15.6.2 程序片 15.6.3 用C++写的CGI程序 15.6.4 POST的概念 15.7 用JDBC连接数据库 15.7.1 获得学习示例 15.7.2 查找程序的GUI本 15.7.3 JDBC API为何如何复杂 15.8 远程方法 15.8.1 远程接口概念 15.8.2 远程接口的实施 15.8.3 创建根与干 15.8.4 使用远程对象 15.8.5 RMI的替选方案 15.9 总结 15.10 练习 第16章 设计范式 16.1 范式的概念 16.1.1 单子 16.1.2 范式分类 16.2 观察器范式 16.3 模拟垃圾回收站 16.4 改进设计 16.4.1 “制作更多的对象” 16.4.2 用于原型创建的一个范式 16.5 抽象的应用 16.6 多重派遣 16.6.1 实现双重派遣 16.7 访问器范式 16.8 RTTI有害吗 16.9 总结 16.10 练习 第17章 项目 17.1 文字处理 17.1.1 提取代码列表 17.1.2 检查大小写样式 17.2 方法查找工具 17.3 复杂性理论 17.4 总结 17.5 练习 附录A 使用非Java代码 A.1 Java固有接口 A.1.1 调用固有方法 A.1.2 访问JNI函数:JNIEnv自变量 A.1.3 传递和使用Java对象 A.1.4 JNI和Java违例 A.1.5 JNI和线程处理 A.1.6 使用现成代码 A.2 微软的解决方案 A.3 J/Direct A.3.1 @dll.import引导命令 A.3.2 com.ms.win32包 A.3.3 汇集 A.3.4 编写回调函数 A.3.5 其他J/Direct特性 A.4 本原接口(RNI) A.4.1 RNI总结 A.5 Java/COM集成 A.5.1 COM基础 A.5.2 MS Java/COM集成 A.5.3 用Java设计COM服务器 A.5.4 用Java设计COM客户 A.5.5 ActiveX/Beans集成 A.5.6 固有方法与程序片的注意事项 A.6 CORBA A.6.1 CORBA基础 A.6.2 一个例子 A.6.3 Java程序片和CORBA A.6.4 比较CORBA与RMI A.7 总结 附录B 对比C++和Java 附录C Java编程规则 附录D 性能 D.1 基本方法 D.2 寻找瓶颈 D.2.1 安插自己的测试代码 D.2.2 JDK性能评测[2] D.2.3 特殊工具 D.2.4 性能评测的技巧 D.3 提速方法 D.3.1 常规手段 D.3.2 依赖语言的方法 D.3.3 特殊情况 D.4 参考资源 D.4.1 性能工具 D.4.2 Web站点 D.4.3 文章 D.4.4 Java专业书籍 D.4.5 一般书籍 附录E 关于垃圾收集的一些话 附录F 推荐读物
引言 1. 前提 2. Java的学习 3. 目标 4. 联机文档 5. 章节 6. 练习 7. 多媒体CD-ROM 8. 源代码 9. 编码样式 10. Java本 11. 课程和培训 12. 错误 13. 封面设计 14. 致谢 第1章 对象入门 1.1 抽象的进步 1.2 对象的接口 1.3 实现方案的隐藏 1.4 方案的重复使用 1.5 继承:重新使用接口 1.5.1 改善基础类 1.5.2 等价和类似关系 1.6 多形对象的互换使用 1.6.1 动态绑定 1.6.2 抽象的基础类和接口 1.7 对象的创建和存在时间 1.7.1 集合与继承器 1.7.2 单根结构 1.7.3 集合库与方便使用集合 1.7.4 清除时的困境:由谁负责清除? 1.8 违例控制:解决错误 1.9 多线程 1.10 永久性 1.11 Java和因特网 1.11.1 什么是Web? 1.11.2 客户端编程 1.11.3 服务器端编程 1.11.4 一个独立的领域:应用程序 1.12 分析和设计 1.12.1 不要迷失 1.12.2 阶段0:拟出一个计划 1.12.3 阶段1:要制作什么? 1.12.4 阶段2:开始构建? 1.12.5 阶段3:正式创建 1.12.6 阶段4:校订 1.12.7 计划的回报 1.13 Java还是C++? 第2章 一切都是对象 2.1 用句柄操纵对象 2.2 必须创建所有对象 2.2.1 保存在什么地方 2.2.2 特殊情况:主类型 2.2.3 Java中的数组 2.3 绝对不要清除对象 2.3.1 作用域 2.3.2 对象的作用域 2.4 新建数据类型:类 2.4.1 字段和方法 2.5 方法、自变量和返回值 2.5.1 自变量列表 2.6 构建Java程序 2.6.1 名字的可见性 2.6.2 使用其他组件 2.6.3 static关键字 2.7 我们的第一个Java程序 2.8 注释和嵌入文档 2.8.1 注释文档 2.8.2 具体语法 2.8.3 嵌入HTML 2.8.4 @see:引用其他类 2.8.5 类文档标记 2.8.6 变量文档标记 2.8.7 方法文档标记 2.8.8 文档示例 2.9 编码样式 2.10 总结 2.11 练习 第3章 控制程序流程 3.1 使用Java运算符 3.1.1 优先级 3.1.2 赋值 3.1.3 算术运算符 3.1.4 自动递增和递减 3.1.5 关系运算符 3.1.6 逻辑运算符 3.1.7 按位运算符 3.1.8 移位运算符 3.1.9 三元if-else运算符 3.1.10 逗号运算符 3.1.11 字串运算符+ 3.1.12 运算符常规操作规则 3.1.13 造型运算符 3.1.14 Java没有“sizeof” 3.1.15 复习计算顺序 3.1.16 运算符总结 3.2 执行控制 3.2.1 真和假 3.2.2 if-else 3.2.3 反复 3.2.4 do-while 3.2.5 for 3.2.6 中断和继续 3.2.7 切换 3.3 总结 3.4 练习 第4章 初始化和清除 4.1 由构建器保证初始化 4.2 方法过载 4.2.1 区分过载方法 4.2.2 主类型的过载 4.2.3 返回值过载 4.2.4 默认构建器 4.2.5 this关键字 4.3 清除:收尾和垃圾收集 4.3.1 finalize()用途何在 4.3.2 必须执行清除 4.4 成员初始化 4.4.1 规定初始化 4.4.2 构建器初始化 4.5 数组初始化 4.5.1 多维数组 4.6 总结 4.7 练习 第5章 隐藏实施过程 5.1 包:库单元 5.1.1 创建独一无二的包名 5.1.2 自定义工具库 5.1.3 利用导入改变行为 5.1.4 包的停用 5.2 Java访问指示符 5.2.1 “友好的” 5.2.2 public:接口访问 5.2.3 private:不能接触 5.2.4 protected:“友好的一种” 5.3 接口与实现 5.4 类访问 5.5 总结 5.6 练习 第6章 类再生 6.1 合成的语法 6.2 继承的语法 6.2.1 初始化基础类 6.3 合成与继承的结合 6.3.1 确保正确的清除 6.3.2 名字的隐藏 6.4 到底选择合成还是继承 6.5 protected 6.6 递增开发 6.7 上溯造型 6.7.1 何谓“上溯造型”? 6.8 final关键字 6.8.1 final数据 6.8.2 final方法 6.8.3 final类 6.8.4 final的注意事项 6.9 初始化和类装载 6.9.1 继承初始化 6.10 总结 6.11 练习 第7章 多形性 7.1 上溯造型 7.1.1 为什么要上溯造型 7.2 深入理解 7.2.1 方法调用的绑定 7.2.2 产生正确的行为 7.2.3 扩展性 7.3 覆盖与过载 7.4 抽象类和方法 7.5 接口 7.5.1 Java的“多重继承” 7.5.2 通过继承扩展接口 7.5.3 常数分组 7.5.4 初始化接口中的字段 7.6 内部类 7.6.1 内部类和上溯造型 7.6.2 方法和作用域中的内部类 7.6.3 链接到外部类 7.6.4 static内部类 7.6.5 引用外部类对象 7.6.6 从内部类继承 7.6.7 内部类可以覆盖吗? 7.6.8 内部类标识符 7.6.9 为什么要用内部类:控制框架 7.7 构建器和多形性 7.7.1 构建器的调用顺序 7.7.2 继承和finalize() 7.7.3 构建器内部的多形性方法的行为 7.8 通过继承进行设计 7.8.1 纯继承与扩展 7.8.2 下溯造型与运行期类型标识 7.9 总结 7.10 练习 第8章 对象的容纳 8.1 数组 8.1.1 数组和第一类对象 8.1.2 数组的返回 8.2 集合 8.2.1 缺点:类型未知 8.3 枚举器(反复器) 8.4 集合的类型 8.4.1 Vector 8.4.2 BitSet 8.4.3 Stack 8.4.4 Hashtable 8.4.5 再论枚举器 8.5 排序 8.6 通用集合库 8.7 新集合 8.7.1 使用Collections 8.7.2 使用Lists 8.7.3 使用Sets 8.7.4 使用Maps 8.7.5 决定实施方案 8.7.6 未支持的操作 8.7.7 排序和搜索 8.7.8 实用工具 8.8 总结 8.9 练习 第9章 违例差错控制 9.1 基本违例 9.1.1 违例自变量 9.2 违例的捕获 9.2.1 try块 9.2.2 违例控制器 9.2.3 违例规范 9.2.4 捕获所有违例 9.2.5 重新“掷”出违例 9.3 标准Java违例 9.3.1 RuntimeException的特殊情况 9.4 创建自己的违例 9.5 违例的限制 9.6 用finally清除 9.6.1 用finally做什么 9.6.2 缺点:丢失的违例 9.7 构建器 9.8 违例匹配 9.8.1 违例准则 9.9 总结 9.10 练习 第10章 Java IO系统 10.1 输入和输出 10.1.1 InputStream的类型 10.1.2 OutputStream的类型 10.2 增添属性和有用的接口 10.2.1 通过FilterInputStream从InputStream里读入数据 10.2.2 通过FilterOutputStream向OutputStream里写入数据 10.3 本身的缺陷:RandomAccessFile 10.4 File类 10.4.1 目录列表器 10.4.2 检查与创建目录 10.5 IO流的典型应用 10.5.1 输入流 10.5.2 输出流 10.5.3 快捷文件处理 10.5.4 从标准输入中读取数据 10.5.5 管道数据流 10.6 StreamTokenizer 10.6.1 StringTokenizer 10.7 Java 1.1的IO流 10.7.1 数据的发起与接收 10.7.2 修改数据流的行为 10.7.3 未改变的类 10.7.4 一个例子 10.7.5 重定向标准IO 10.8 压缩 10.8.1 用GZIP进行简单压缩 10.8.2 用Zip进行多文件保存 10.8.3 Java归档(jar)实用程序 10.9 对象串联 10.9.1 寻找类 10.9.2 序列化的控制 10.9.3 利用“持久性” 10.10 总结 10.11 练习 第11章 运行期类型鉴定 11.1 对RTTI的需要 11.1.1 Class对象 11.1.2 造型前的检查 11.2 RTTI语法 11.3 反射:运行期类信息 11.3.1 一个类方法提取器 11.4 总结 11.5 练习 第12章 传递和返回对象 12.1 传递句柄 12.1.1 别名问题 12.2 制作本地副本 12.2.1 按值传递 12.2.2 克隆对象 12.2.3 使类具有克隆能力 12.2.4 成功的克隆 12.2.5 Object.clone()的效果 12.2.6 克隆合成对象 12.2.7 用Vector进行深层复制 12.2.8 通过序列化进行深层复制 12.2.9 使克隆具有更大的深度 12.2.10 为什么有这个奇怪的设计 12.3 克隆的控制 12.3.1 副本构建器 12.4 只读类 12.4.1 创建只读类 12.4.2 “一成不变”的弊端 12.4.3 不变字串 12.4.4 String和StringBuffer类 12.4.5 字串的特殊性 12.5 总结 12.6 练习 第13章 创建窗口和程序片 13.1 为何要用AWT? 13.2 基本程序片 13.2.1 程序片的测试 13.2.2 一个更图形化的例子 13.2.3 框架方法的演示 13.3 制作按钮 13.4 捕获事件 13.5 文本字段 13.6 文本区域 13.7 标签 13.8 复选框 13.9 单选钮 13.10 下拉列表 13.11 列表框 13.11.1 handleEvent() 13.12 布局的控制 13.12.1 FlowLayout 13.12.2 BorderLayout 13.12.3 GridLayout 13.12.4 CardLayout 13.12.5 GridBagLayout 13.13 action的替用品 13.14 程序片的局限 13.14.1 程序片的优点 13.15 视窗化应用 13.15.1 菜单 13.15.2 对话框 13.16 新型AWT 13.16.1 新的事件模型 13.16.2 事件和接收者类型 13.16.3 用Java 1.1 AWT制作窗口和程序片 13.16.4 再探早期示例 13.16.5 动态绑定事件 13.16.6 将商业逻辑与UI逻辑区分开 13.16.7 推荐编码方法 13.17 Java 1.1 UI API 13.17.1 桌面颜色 13.17.2 打印 13.17.3 剪贴板 13.18 可视编程和Beans 13.18.1 什么是Bean 13.18.2 用Introspector提取BeanInfo 13.18.3 一个更复杂的Bean 13.18.4 Bean的封装 13.18.5 更复杂的Bean支持 13.18.6 Bean更多的知识 13.19 Swing入门 13.19.1 Swing有哪些优点 13.19.2 方便的转换 13.19.3 显示框架 13.19.4 工具提示 13.19.5 边框 13.19.6 按钮 13.19.7 按钮组 13.19.8 图标 13.19.9 菜单 13.19.10 弹出式菜单 13.19.11 列表框和组合框 13.19.12 滑杆和进度指示条 13.19.13 树 13.19.14 表格 13.19.15 卡片式对话框 13.19.16 Swing消息框 13.19.17 Swing更多的知识 13.20 总结 13.21 练习 第14章 多线程 14.1 反应灵敏的用户界面 14.1.1 从线程继承 14.1.2 针对用户界面的多线程 14.1.3 用主类合并线程 14.1.4 制作多个线程 14.1.5 Daemon线程 14.2 共享有限的资源 14.2.1 资源访问的错误方法 14.2.2 Java如何共享资源 14.2.3 回顾Java Beans 14.3 堵塞 14.3.1 为何会堵塞 14.3.2 死锁 14.4 优先级 14.4.1 线程组 14.5 回顾runnable 14.5.1 过多的线程 14.6 总结 14.7 练习 第15章 网络编程 15.1 机器的标识 15.1.1 服务器和客户机 15.1.2 端口:机器内独一无二的场所 15.2 套接字 15.2.1 一个简单的服务器和客户机程序 15.3 服务多个客户 15.4 数据报 15.5 一个Web应用 15.5.1 服务器应用 15.5.2 NameSender程序片 15.5.3 15.5.3 要注意的问题 15.6 Java与CGI的沟通 15.6.1 CGI数据的编码 15.6.2 程序片 15.6.3 用C++写的CGI程序 15.6.4 POST的概念 15.7 用JDBC连接数据库 15.7.1 获得学习示例 15.7.2 查找程序的GUI本 15.7.3 JDBC API为何如何复杂 15.8 远程方法 15.8.1 远程接口概念 15.8.2 远程接口的实施 15.8.3 创建根与干 15.8.4 使用远程对象 15.8.5 RMI的替选方案 15.9 总结 15.10 练习 第16章 设计范式 16.1 范式的概念 16.1.1 单子 16.1.2 范式分类 16.2 观察器范式 16.3 模拟垃圾回收站 16.4 改进设计 16.4.1 “制作更多的对象” 16.4.2 用于原型创建的一个范式 16.5 抽象的应用 16.6 多重派遣 16.6.1 实现双重派遣 16.7 访问器范式 16.8 RTTI有害吗 16.9 总结 16.10 练习 第17章 项目 17.1 文字处理 17.1.1 提取代码列表 17.1.2 检查大小写样式 17.2 方法查找工具 17.3 复杂性理论 17.4 总结 17.5 练习 附录A 使用非Java代码 A.1 Java固有接口 A.1.1 调用固有方法 A.1.2 访问JNI函数:JNIEnv自变量 A.1.3 传递和使用Java对象 A.1.4 JNI和Java违例 A.1.5 JNI和线程处理 A.1.6 使用现成代码 A.2 微软的解决方案 A.3 J/Direct A.3.1 @dll.import引导命令 A.3.2 com.ms.win32包 A.3.3 汇集 A.3.4 编写回调函数 A.3.5 其他J/Direct特性 A.4 本原接口(RNI) A.4.1 RNI总结 A.5 Java/COM集成 A.5.1 COM基础 A.5.2 MS Java/COM集成 A.5.3 用Java设计COM服务器 A.5.4 用Java设计COM客户 A.5.5 ActiveX/Beans集成 A.5.6 固有方法与程序片的注意事项 A.6 CORBA A.6.1 CORBA基础 A.6.2 一个例子 A.6.3 Java程序片和CORBA A.6.4 比较CORBA与RMI A.7 总结 附录B 对比C++和Java 附录C Java编程规则 附录D 性能 D.1 基本方法 D.2 寻找瓶颈 D.2.1 安插自己的测试代码 D.2.2 JDK性能评测[2] D.2.3 特殊工具 D.2.4 性能评测的技巧 D.3 提速方法 D.3.1 常规手段 D.3.2 依赖语言的方法 D.3.3 特殊情况 D.4 参考资源 D.4.1 性能工具 D.4.2 Web站点 D.4.3 文章 D.4.4 Java专业书籍 D.4.5 一般书籍 附录E 关于垃圾收集的一些话 附录F 推荐读物
Java WebSocket可以用于实现简易聊天室。下面是一个简单的示例代码: 1. 首先,创建一个WebSocketServer类,继承自javax.websocket.Endpoint,实现onOpen、onMessage、onClose和onError方法。 ```java import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; @ServerEndpoint("/chatRoom") public class WebSocketServer { //在线人数 private static int onlineCount = 0; //存储每个客户端对应的WebSocketServer对象 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>(); //与某个客户端的会话,需要通过它来给客户端发送数据 private Session session; @OnOpen public void onOpen(Session session) { this.session = session; //将WebSocketServer对象加入到webSocketSet中 webSocketSet.add(this); //在线人数加1 addOnlineCount(); System.out.println("有新连接加入!当前在线人数为" + getOnlineCount()); } @OnClose public void onClose() { //将WebSocketServer对象从webSocketSet中移除 webSocketSet.remove(this); //在线人数减1 subOnlineCount(); System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount()); } @OnMessage public void onMessage(String message, Session session) { System.out.println("来自客户端的消息:" + message); //群发消息 for (WebSocketServer webSocketServer : webSocketSet) { try { webSocketServer.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } @OnError public void onError(Session session, Throwable error) { System.out.println("发生错误:" + error.getMessage()); error.printStackTrace(); } public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } } ``` 2. 在web.xml中配置WebSocketServlet。 ```xml <servlet> <servlet-name>WebSocketServlet</servlet-name> <servlet-class>org.apache.tomcat.websocket.server.WsSci</servlet-class> <load-on-startup>1</load-on-startup> </servlet> ``` 3. 在jsp页面中添加WebSocket连接。 ```html var websocket = new WebSocket("ws://" + window.location.host + "/chatRoom"); websocket.onopen = function () { console.log("WebSocket连接成功!"); }; websocket.onmessage = function (event) { console.log("接收到服务器发送的消息:" + event.data); }; websocket.onclose = function () { console.log("WebSocket连接关闭!"); }; websocket.onerror = function () { console.log("WebSocket连接发生错误!"); }; ``` 4. 发送消息。 ```html <button onclick="send()">发送</button> <script> function send() { var message = document.getElementById("message").value; websocket.send(message); } </script> ``` 这样,一个简易聊天室就实现了。多个用户可以在同一页面中实时聊天。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值