> 文章列表 > Java阶段一Day20

Java阶段一Day20

Java阶段一Day20

Java阶段一Day20

文章目录

  • Java阶段一Day20
    • 异常
      • throw
      • throws
      • 检测异常和非检测异常
      • 常见的RuntimeException
      • 常见的catch中的方法(ExceptionAPI)
      • 自定义异常
    • 网络
      • C/S 和 B/S
      • TCP 和 UDP
      • 三次握手
      • 四次挥⼿
  • 教师总结
    • 新单词
    • 异常处理
      • throw关键字
      • throws关键字
      • throws的重写规则
      • 异常分类
        • Java异常可以分为可检测异常,非检测异常:
        • 常见的RuntimeException子类
      • 异常API
      • 自定义异常
      • 总结:
          • java异常处理机制:
    • 网络编程
      • 网络应用模型
      • 可靠传输与不可靠传输
      • Socket与ServerSocket
          • 如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
      • 服务端建立连接案例
      • 客户端与服务端完成第一次通讯(发送一行字符串)
          • Socket提供了两个重要的方法:
        • 原理图
      • 实现客户端循环发消息给服务端
        • 需要注意的几个点:
      • TCP进行连接时的三次握手
      • TCP断开连接时的四次挥手
      • 多客户端链接

异常

throw

当我们使用 throw 主动对外抛出一个异常时,除了RuntimeException以外的其他异常都需要在方法上 显示的声明 throws 来对外告知要抛出该异常

throws

当调用一个含有 throws 声明异常抛出的方法时,编译器要求必须处理该异常。处理方式有两种,遵循责任制原则,可以在这句代码所在方法继续throws向上抛出,也可以使用 try-catch主动捕获并处理该异常

永远不要在main方法上声明throws,这是种不负责的表现,虚拟机发现异常会直接kill进程

当子类重写超类含有throws声明异常抛出的方法时,对throws的重写规则:

  • 允许原样重写方法
  • 允许不再抛任何异常
  • 允许抛出一部分异常
  • 允许抛超类方法声明抛出异常的子类型异常
  • 不允许抛出超类方法抛出异常的超类型异常(不允许越抛越大)
  • 不允许抛出额外异常(超类方法没有声明且不存在继承关系)

检测异常和非检测异常

  • 可检测异常:可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则,不捕捉这个异常,编译器就通不过,不允许编译
  • 非检测异常:非检测异常不遵循处理或者声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已经解决了这样一个异常
  • RuntimeException类 属于非检测异常,因为普通JVM操作引起的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。因此它们不受编译器检查与处理或声明规则的限制

常见的RuntimeException

  • IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数
  • NullPointerException:当应用程序试图在需要对象的地方使用 null 时,抛出该异常
  • ArraylndexOutOfBoundsException:当使用的数组下标超出数组允许范围时,抛出该异常
  • ClassCastException:当试图将则象强制转换为不是实例的子类时,抛出该异常
  • NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常

常见的catch中的方法(ExceptionAPI)

  • printStackTrace:Throwable中定义了一个方法可以输出错误信息,用来跟踪异常事件发生时执行堆栈的内容。该方法定义为:void printStackTrace()
  • getMessage:获取字符消息,提示用户端,日志输出因为什么原因报错,一般在记录日志时使用

自定义异常

  • 通常用来声明那些在项目中满足语法但是不满足业务场景的问题
  • 自定义异常需要做一下几件事:
    1. 类名要做到见名知义
    2. 继承Exception(直接或间接继承都可以)
    3. 提供超类异常提供的所有构造器

网络

C/S 和 B/S

  • C/S
  • B/S

TCP 和 UDP

比较 TCP UDP
是否面向链接
是否可靠
是否有状态
传输效率
首部开销 20~60 8
传输形式 字节流 数据报文段
是否支持广播和多播

三次握手

  • 刚开始时:客户端closed状态,服务端listen状态
  • 第一次握手:客户端给服务器发送SYN报⽂,并指明客户端的初始化序列ISN(C)此时客户端处于SYN_Send状态
  • 第⼆次握⼿:服务器收到客户端传送的SYN报⽂后,会以⾃⼰的SYN报⽂作为回应,并且指定⾃⼰的初始化序列ISN(S),同时会把客户端的ISN+1作为ACK的值,表示⾃⼰已经收到了客户端的SYN,此服务端处于SYN_REVD状态
  • 第三次握⼿:客户端收到了服务端的SYN后,同时把服务端的ISN+1作为ACK报⽂,表示已经收到了服务器的SYN,此时客户端处于establised服务器收到客户端的ACK后,也处于establised,双⽅建⽴连接确认双⽅的发送接受能⼒正常指定⾃⼰的初始化序列号,为后⾯的可靠传输做准备

四次挥⼿

  • 刚开始处在establised,假如客户端先发起关闭请求
  • 第⼀次挥手:客户端发送⼀个FIN报⽂,报⽂中指定⼀个序列号,此时客户端处于CLOSED_WAIT1
  • 第⼆次挥手:服务端收到FIN报⽂,将报⽂中的序列号+1,作为ACK报⽂发送给客户端,表明⾃⼰收到CLOSE_WAIT2
  • 第三次挥手:如果服务端也想断开链接,同样发送⼀个FIN报⽂给客户端,指定⼀个序列号,此时服务端LAST_ACK
  • 第四次挥手:客户端收到FIN,同样发送ACK报⽂,客户端TIME_WAIT,过⼀阵确保服务端收到ACK报⽂,才会进⼊CLOSED服务端收到ACK,处于关闭状态,CLOSED
  • TIME_WAIT确保服务端收到了ACK报⽂,如果没收到,服务端会重新发送FIN给客户端,客户端搜
  • 到两次FIN就知道之前的ACK报⽂丢失了,然后再次发送ACK等待时间⾄少⼀个报⽂的来回时间,设置⼀个时间等待计时器,过了计时器没收到FIN报⽂,代表成功接收ACK,则进⼊CLOSED

教师总结

新单词

  • Client-客户端
  • Server-服务端
  • Socket-计算机中翻译为套接字,日常为插座
  • illegal-非法的

异常处理

throw关键字

throw用来对外主动抛出一个异常,通常下面两种情况我们主动对外抛出异常:

  • 当程序遇到一个满足语法,但是不满足业务要求时,可以抛出一个异常告知调用者。
  • 程序执行遇到一个异常,但是该异常不应当在当前代码片段被解决时可以抛出给调用者。
package exception;/* 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}public void setAge(int age) throws Exception {if(age<0||age>100){//使用throw对外抛出一个异常throw new RuntimeException("年龄不合法!");}this.age = age;}
}
package exception;/* throw关键字,用来对外主动抛出一个异常。* 通常下面两种情况我们主动对外抛出异常:* 1:当程序遇到一个满足语法,但是不满足业务要求时,可以抛出一个异常告知调用者。* 2:程序执行遇到一个异常,但是该异常不应当在当前代码片段被解决时可以抛出给调用者。*/
public class ThrowDemo {public static void main(String[] args) {Person p = new Person();p.setAge(10000);//符合语法,但是不符合业务逻辑要求。System.out.println("此人年龄:"+p.getAge());}
}

throws关键字

throws用来在声明方法时同时声明该方法可能抛出的异常,用于通知调用方添加处理该异常的手段。

当一个方法中使用throw抛出一个非RuntimeException的异常时,就要在该方法上使用throws声明这个异常的抛出。此时调用该方法的代码就必须处理这个异常,否则编译不通过。

package exception;/* 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}/* 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常*/public void setAge(int age) throws Exception {if(age<0||age>100){//使用throw对外抛出一个异常
//            throw new RuntimeException("年龄不合法!");//除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常throw new Exception("年龄不合法!");}this.age = age;}
}

当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须处理这个异常,否则编译不通过。 处理手段有两种:

  • 使用try-catch捕获并处理这个异常
  • 在当前方法(本案例就是main方法)上继续使用throws声明该异常的抛出给调用者解决。 具体选取那种取决于异常处理的责任问题。
package exception;/* throw关键字,用于主动对外抛出一个异常*/
public class ThrowDemo {public static void main(String[] args){System.out.println("程序开始了...");try {Person p = new Person();/*当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须添加处理异常的手段,否则编译不通过.而处理手段有两种1:使用try-catch捕获并处理异常2:在当前方法上继续使用throws声明该异常的抛出具体用哪种取决于异常处理的责任问题*/p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求System.out.println("此人年龄:"+p.getAge()+"岁");} catch (Exception e) {e.printStackTrace();}System.out.println("程序结束了...");}
}

注意,永远不应当在main方法上使用throws!!

throws的重写规则

当一个子类重写超类含有throws声明异常抛出的方法时,针对throws的重写规则

  • 允许不再抛出任何异常
  • 允许仅抛出部分异常
  • 允许抛出超类方法声明抛出异常的子类型异常
  • 不允许抛出额外异常(超类方法没有声明的且不存在继承关系的)
  • 不允许抛出超类方法抛出异常的超类型异常

子类方法的几种重写分别解开注释就可以观察编译器是否允许该种重写方式

package exception;import java.awt.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;/* 当子类重写超类含有throws声明异常抛出的方法时,对throws的重写规则*/
public class ThrowsDemo {public void dosome()throws IOException, AWTException {}
}
class SubClass extends ThrowsDemo{
//    public void dosome()throws IOException, AWTException {
//    }//允许不再抛出任何异常
//    public void dosome(){
//    }//允许仅抛出部分异常
//    public void dosome()throws IOException{
//    }//允许抛出超类方法声明抛出异常的子类型异常
//    public void dosome()throws FileNotFoundException {
//    }//不允许抛出额外异常(超类方法没有声明的且不存在继承关系的)
//    public void dosome()throws SQLException {
//    }//不允许抛出超类方法抛出异常的超类型异常
//    public void dosome()throws Exception {
//    }
}

异常分类

Java异常可以分为可检测异常,非检测异常:

  • 可检测异常:可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则,不捕捉这个异常,编译器就通不过,不允许编译
  • 非检测异常:非检测异常不遵循处理或者声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已经解决了这样一个异常
  • RuntimeException 类属于非检测异常,因为普通JVM操作引起的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。因此它们不受编译器检查与处理或声明规则的限制。实际上RuntimeException及其子类型表达的都是因为程序漏洞(BUG),即:逻辑不严谨等原因导致的。这类异常都是通过修复代码可完全避免的异常,因此不应当由异常处理机制来处理

常见的RuntimeException子类

  • IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数
  • NullPointerException:当应用程序试图在需要对象的地方使用 null 时,抛出该异常
  • ArrayIndexOutOfBoundsException:当使用的数组下标超出数组允许范围时,抛出该异常
  • ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常
  • NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。

异常API

Exception上有一些常用的方法

  • void printStackTrace():用于输出当前异常的堆栈跟踪信息

    便于程序员找到错误根源,分析错误原因制定B计划

  • String getMessage():用于获取当前错误消息

    多用于提示给用户或记录日志使用

package exception;/* 异常常用方法*/
public class ExceptionAPIDemo {public static void main(String[] args) {System.out.println("程序开始了");try {String str = "abc";System.out.println(Integer.parseInt(str));} catch (NumberFormatException e) {//输出错误堆栈跟踪信息便于程序员找到错误根源,分析错误原因制定B计划e.printStackTrace();//输出错误信息//错误消息多用于提示给用户或记录日志使用String message = e.getMessage();//获取错误消息System.out.println(message);}System.out.println("程序结束了");}
}

自定义异常

自定义异常通常用来定义那些业务上的异常问题。

定义自定义异常需要注意以下问题:

  • 异常的类名要做到见名知义
  • 需要是Exception的子类
  • 提供超类异常提供的所有种类构造器

package exception;/* 非法的年龄异常 自定义异常通常用来说明业务上的错误.* 自定义异常要注意以下问题:* 1:定义的类名要做到见名知义* 2:必须是Exception的子类* 3:提供Exception所定义的所有构造方法*/
public class IllegalAgeException extends Exception{public IllegalAgeException() {}public IllegalAgeException(String message) {super(message);}public IllegalAgeException(String message, Throwable cause) {super(message, cause);}public IllegalAgeException(Throwable cause) {super(cause);}public IllegalAgeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}
package exception;/* 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}/* 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常*/public void setAge(int age) throws IllegalAgeException {if(age<0||age>100){//使用throw对外抛出一个异常
//            throw new RuntimeException("年龄不合法!");//除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常
//            throw new Exception("年龄不合法!");//抛出自定义异常throw new IllegalAgeException("年龄超范围:"+age);}this.age = age;}
}
package exception;/* throw关键字,用于主动对外抛出一个异常*/
public class ThrowDemo {public static void main(String[] args){System.out.println("程序开始了...");try {Person p = new Person();/*当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须添加处理异常的手段,否则编译不通过.而处理手段有两种1:使用try-catch捕获并处理异常2:在当前方法上继续使用throws声明该异常的抛出具体用哪种取决于异常处理的责任问题*/p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求System.out.println("此人年龄:"+p.getAge()+"岁");} catch (IllegalAgeException e) {e.printStackTrace();}System.out.println("程序结束了...");}
}

总结:

java异常处理机制:
  • 异常处理机制是用来处理那些可能存在的异常,但是无法通过修改逻辑完全规避的场景。
  • 而如果通过修改逻辑可以规避的异常是bug,不应当用异常处理机制在运行期间解决!应当在编码时及时修正

try语句块用来包含可能出错的代码片段

catch用来捕获并处理对应的异常,可以定义多个,也可以合并多个异常在一个catch中。

finally是异常的最后一块,只要程序执行到try中则必走。一般用于释放资源这类操作。

throw用于主动对外抛出异常。要么是满足语法不满足业务主动抛出异常,要么就是实际发生了异常但是不应当在当前代码片段被解决是抛出。具体情况要结合实际业务分析。

throws用于在方法声明时声明该异常的抛出,使得调用者必须处理该异常。

网络编程

网络应用模型

  • C/S:这里的C是指Client,即客户端。而S是指Server,即服务端。网络上的的应用本质上就是两台计算机上的软件进行交互。而客户端和服务端就是对应的两个应用程序,即客户端应用程序和服务端应用程序

  • B/S:这里的B是Browser,即浏览器,而S是指Server。浏览器是一个通用的客户端,可以与不同的服务端进行交互。但是本质上B/S还是C/S结构,只不过浏览器是一个通用的客户端而已。

可靠传输与不可靠传输

TCP协议与UDP协议都是传输协议,客户端程序与服务端程序基于这些协议完成网络间的数据交互。

  • TCP是可靠传输协议,是面向连接的协议,保证数据传输中的可靠性和完整性。

    TCP保证可靠传输,但是传输效率低,占用带宽高。

  • UDP是不可靠传输协议,不保证数据传输的完整性。

    UDP不保证可靠传输,但是传输速度块,占用带宽小。

Socket与ServerSocket

  • java.net.Socket

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

  • java.net.ServerSocket

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

    • 向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
    • 监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。

Java阶段一Day20

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

    Java阶段一Day20

与服务端建立连接案例

package socket;import java.io.IOException;
import java.net.Socket;/* 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/* 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/* 客户端开始工作的方法*/public void start(){}public static void main(String[] args) {Client client = new Client();client.start();}
}
package socket;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/* 聊天室服务端*/
public class Server {/* 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/* 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/* 服务端开始工作的方法*/public void start(){try {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}

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

Socket提供了两个重要的方法:
  • OutputStream getOutputStream()

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

  • InputStream getInputStream()

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

原理图

Java阶段一Day20

package socket;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();}
}
package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/* 聊天室服务端*/
public class Server {/* 运行在服务端的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();}
}

实现客户端循环发消息给服务端

package socket;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/* 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/* 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/* 客户端开始工作的方法*/public void start(){try {/*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();}
}
package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/* 聊天室服务端*/
public class Server {/* 运行在服务端的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();}
}

需要注意的几个点:

  • 当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。
  • 当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。

TCP进行连接时的三次握手

Java阶段一Day20

TCP断开连接时的四次挥手

Java阶段一Day20

多客户端链接

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

原因:

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

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

Java阶段一Day20

package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/* 聊天室服务端*/
public class Server {/* 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket*   就可以和该客户端交互了 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/* 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/* 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);String message = null;while ((message = br.readLine()) != null) {System.out.println("客户端说:" + message);}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}

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

原因在于:

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

Java阶段一Day20