- 所有已实现的接口:
Closeable,AutoCloseable
Socket 并使用“安全套接字层”(SSL) 或 IETF“传输层安全”(TLS) 协议等协议提供安全套接字。
此类套接字是普通的流套接字,但它们在底层网络传输协议(例如 TCP)上添加了一层安全保护。这些保护包括:
- Integrity Protection。 SSL 防止活动的窃听者修改消息。
- Authentication。在大多数模式下,SSL 提供对等身份验证。服务通常经过身份验证,客户端可能会根据服务的请求进行身份验证。
- Confidentiality (Privacy Protection)。在大多数模式下,SSL 加密在客户端和服务之间发送的数据。这保护了数据的机密性,使被动窃听者不会看到敏感数据,例如财务信息或多种个人信息。
这些类型的保护由“密码套件”指定,它是给定 SSL 连接使用的密码算法的组合。在协商过程中,两个端点必须就在两个环境中都可用的密码套件达成一致。如果没有这样的共同套件,则无法建立 SSL 连接,也无法交换数据。
使用的密码套件是通过称为“握手”的协商过程建立的。这个过程的目标是创建或重新加入一个“会话”,它可以随着时间的推移保护许多连接。握手完成后,您可以使用 getSession 方法访问会话属性。此连接的初始握手可以通过以下三种方式之一启动:
- 调用明确开始握手的
startHandshake,或者 - 任何在此套接字上读取或写入应用程序数据的尝试都会导致隐式握手,或者
- 如果当前没有有效会话,则对
getSession的调用会尝试建立会话,并完成隐式握手。
如果由于任何原因握手失败,SSLSocket 将关闭,并且无法进行进一步的通信。
在管理密码套件时,您需要了解两组密码套件:
- Supported 密码套件:SSL 实现支持的所有套件。此list使用 getSupportedCipherSuites 报告。
- Enabled 密码套件,可能少于支持的全套套件。该组使用setEnabledCipherSuites方法设置,使用getEnabledCipherSuites方法查询。最初,将在代表最低建议配置的新套接字上启用一组默认密码套件。
实现默认要求默认情况下仅启用验证服务和提供机密性的密码套件。只有双方明确同意未经身份验证和/或非私人(未加密)通信才会选择这样的密码套件。
首次创建SSLSocket 时,不会进行任何握手,因此应用程序可能首先设置它们的通信首选项:使用什么密码套件、套接字应处于客户端模式还是服务模式等。但是,安全性始终在以下时间提供应用程序数据通过连接发送。
您可以注册接收握手完成的事件通知。这涉及使用两个额外的类。 HandshakeCompletedEvent 个对象被传递给 HandshakeCompletedListener 个实例,这些实例由该 API 的用户注册。 SSLSocket 由 SSLSocketFactory 创建,或由 accept 从 SSLServerSocket 创建连接。
SSL 套接字必须选择以客户端或服务模式运行。这将决定谁开始握手过程,以及每一方应发送哪些消息。每个连接必须有一个客户端和一个服务,否则握手将无法正常进行。一旦初始握手开始,套接字就不能在客户端和服务模式之间切换,即使在执行重新协商时也是如此。
此类中的方法返回的 ApplicationProtocol String 值在对等方发送的网络字节表示中。字节可以直接比较,也可以转换成UnicodeString格式进行比较。
String networkString = sslSocket.getHandshakeApplicationProtocol();
byte[] bytes = networkString.getBytes(StandardCharsets.ISO_8859_1);
//
// Match using bytes:
//
// "http/1.1" (7-bit ASCII values same in UTF-8)
// MEETEI MAYEK LETTERS "HUK UN I" (Unicode 0xabcd->0xabcf)
//
String HTTP1_1 = "http/1.1";
byte[] HTTP1_1_BYTES = HTTP1_1.getBytes(StandardCharsets.UTF_8);
byte[] HUK_UN_I_BYTES = new byte[] {
(byte) 0xab, (byte) 0xcd,
(byte) 0xab, (byte) 0xce,
(byte) 0xab, (byte) 0xcf};
if ((Arrays.compare(bytes, HTTP1_1_BYTES) == 0 )
|| Arrays.compare(bytes, HUK_UN_I_BYTES) == 0) {
...
}
//
// Alternatively match using string.equals() if we know the ALPN value
// was encoded from a String using a certain character set,
// for example UTF-8 . The ALPN value must first be properly
// decoded to a Unicode String before use.
//
String unicodeString = new String(bytes, StandardCharsets.UTF_8);
if (unicodeString.equals(HTTP1_1)
|| unicodeString.equals("\uabcd\uabce\uabcf")) {
...
}
- API 注意:
-
当不再需要连接时,客户端和服务应用程序应分别关闭各自连接的两端。这可以通过调用
Socket.close()一次性完成,也可以使用Socket.shutdownOutput()/Socket.shutdownInput()单独关闭每一侧,这对于支持半关闭连接的协议版本很有用。请注意,在某些情况下,关闭输入流可能取决于首先关闭对等点的输出流。如果连接未按顺序关闭(例如,
Socket.shutdownInput()在收到对等方的写关闭通知之前被调用),则可能会引发异常以指示发生了错误。一旦
SSLSocket关闭,它就不可重复使用:必须创建一个新的SSLSocket。 - 自从:
- 1.4
- 参见:
-
构造方法总结
构造方法修饰符构造方法描述protected仅由子类使用。protected仅由子类使用。protectedSSLSocket(String host, int port, InetAddress clientAddress, int clientPort) 仅由子类使用。protectedSSLSocket(InetAddress address, int port) 仅由子类使用。protectedSSLSocket(InetAddress address, int port, InetAddress clientAddress, int clientPort) 仅由子类使用。 -
方法总结
修饰符和类型方法描述abstract void注册事件监听以接收 SSL 握手已在此连接上完成的通知。返回为此连接协商的最新应用程序协议值。abstract String[]返回当前启用用于此连接的 SSL 密码套件的名称。abstract String[]返回当前启用用于此连接的协议版本的名称。abstract boolean如果此套接字可以建立新的 SSL 会话,则返回 true。返回在当前正在进行的 SSL/TLS 握手中协商的应用程序协议值。检索在 SSL/TLS/DTLS 握手期间选择应用程序协议值的回调函数。返回在 SSL/TLS 握手期间构造的SSLSession。abstract boolean如果套接字将返回 truerequire客户端身份验证。abstract SSLSession返回此连接使用的 SSL 会话。返回对此 SSLSocket 有效的 SSLParameters。abstract String[]返回可以在此连接上启用的密码套件的名称。abstract String[]返回可以在 SSL 连接上启用的协议的名称。abstract boolean如果套接字设置为在握手时使用客户端模式,则返回 true。abstract boolean如果套接字将返回 truerequest客户端身份验证。abstract void删除先前注册的握手完成监听。abstract voidsetEnabledCipherSuites(String[] suites) 设置为此连接启用的密码套件。abstract voidsetEnabledProtocols(String[] protocols) 设置为此连接启用的协议版本。abstract voidsetEnableSessionCreation(boolean flag) 控制是否可以由此套接字建立新的 SSL 会话。void注册一个回调函数,该函数为 SSL/TLS/DTLS 握手选择应用程序协议值。abstract voidsetNeedClientAuth(boolean need) 将套接字配置为require客户端身份验证。voidsetSSLParameters(SSLParameters params) 将 SSLParameters 应用于此套接字。abstract voidsetUseClientMode(boolean mode) 将套接字配置为在握手时使用客户端(或服务)模式。abstract voidsetWantClientAuth(boolean want) 将套接字配置为request客户端身份验证。abstract void在此连接上启动 SSL 握手。在类 java.net.Socket 中声明的方法
bind, close, connect, connect, getChannel, getInetAddress, getInputStream, getKeepAlive, getLocalAddress, getLocalPort, getLocalSocketAddress, getOOBInline, getOption, getOutputStream, getPort, getReceiveBufferSize, getRemoteSocketAddress, getReuseAddress, getSendBufferSize, getSoLinger, getSoTimeout, getTcpNoDelay, getTrafficClass, isBound, isClosed, isConnected, isInputShutdown, isOutputShutdown, sendUrgentData, setKeepAlive, setOOBInline, setOption, setPerformancePreferences, setReceiveBufferSize, setReuseAddress, setSendBufferSize, setSocketImplFactory, setSoLinger, setSoTimeout, setTcpNoDelay, setTrafficClass, shutdownInput, shutdownOutput, supportedOptions, toString
-
构造方法详细信息
-
SSLSocket
protected SSLSocket()仅由子类使用。构造一个未初始化的、未连接的 TCP 套接字。 -
SSLSocket
仅由子类使用。在指定端口构造到指定主机的 TCP 连接。这充当 SSL 客户端。如果有一个安全管理器,它的
checkConnect方法将以主机地址和port作为参数调用。这可能会导致 SecurityException。- 参数:
host- 要连接的主机的名称,或null作为环回地址。port- 服务端口号- 抛出:
IOException- 如果在创建套接字时发生 I/O 错误SecurityException- 如果安全管理器存在且其checkConnect方法不允许该操作。UnknownHostException- 如果主机未知IllegalArgumentException- 如果端口参数超出有效端口值的指定范围,即介于 0 和 65535 之间(含)。- 参见:
-
SSLSocket
仅由子类使用。在指定的地址和端口处构建到服务的 TCP 连接。这充当 SSL 客户端。如果有一个安全管理器,它的
checkConnect方法将以主机地址和port作为参数调用。这可能会导致 SecurityException。- 参数:
address- 服务的主机port- 它的端口- 抛出:
IOException- 如果在创建套接字时发生 I/O 错误SecurityException- 如果安全管理器存在且其checkConnect方法不允许该操作。IllegalArgumentException- 如果端口参数超出有效端口值的指定范围,即介于 0 和 65535 之间(含)。NullPointerException- 如果address为空。- 参见:
-
SSLSocket
protected SSLSocket(String host, int port, InetAddress clientAddress, int clientPort) throws IOException , UnknownHostException 仅由子类使用。在指定端口构建到指定主机的 SSL 连接,将连接的客户端绑定到给定的地址和端口。这充当 SSL 客户端。如果有一个安全管理器,它的
checkConnect方法将以主机地址和port作为参数调用。这可能会导致 SecurityException。- 参数:
host- 要连接的主机的名称,或null作为环回地址。port- 服务端口号clientAddress- 套接字绑定到的客户端地址,或null用于anyLocal地址。clientPort- 套接字绑定到的客户端端口,或zero用于系统选择的空闲端口。- 抛出:
IOException- 如果在创建套接字时发生 I/O 错误SecurityException- 如果安全管理器存在且其checkConnect方法不允许该操作。UnknownHostException- 如果主机未知IllegalArgumentException- 如果端口参数或 clientPort 参数超出有效端口值的指定范围,即介于 0 和 65535 之间(含)。- 参见:
-
SSLSocket
protected SSLSocket(InetAddress address, int port, InetAddress clientAddress, int clientPort) throws IOException 仅由子类使用。在指定的地址和 TCP 端口构建到服务的 SSL 连接,将连接的客户端绑定到给定的地址和端口。这充当 SSL 客户端。如果有一个安全管理器,它的
checkConnect方法将以主机地址和port作为参数调用。这可能会导致 SecurityException。- 参数:
address- 服务的主机port- 它的端口clientAddress- 套接字绑定到的客户端地址,或null用于anyLocal地址。clientPort- 套接字绑定到的客户端端口,或zero用于系统选择的空闲端口。- 抛出:
IOException- 如果在创建套接字时发生 I/O 错误SecurityException- 如果安全管理器存在且其checkConnect方法不允许该操作。IllegalArgumentException- 如果端口参数或 clientPort 参数超出有效端口值的指定范围,即介于 0 和 65535 之间(含)。NullPointerException- 如果address为空。- 参见:
-
-
方法详情
-
getSupportedCipherSuites
返回可以在此连接上启用的密码套件的名称。通常,默认情况下实际上只会启用其中的一个子集,因为此list可能包含不满足这些默认值的服务质量要求的密码套件。这样的密码套件可能在专门的应用程序中很有用。返回的数组包括来自 Java 安全标准算法名称规范的 JSSE 密码套件名称 部分中的标准密码套件名称列表的密码套件,并且还可能包括提供程序支持的其他密码套件。
- 返回:
- 一组密码套件名称
- 参见:
-
getEnabledCipherSuites
返回当前启用用于此连接的 SSL 密码套件的名称。首次创建 SSLSocket 时,所有启用的密码套件都支持最低服务质量。因此,在某些环境中,此值可能为空。请注意,即使启用了套件,也可能永远不会使用它。如果对等方不支持它,或者它的使用受到限制,或者套件所需的证书(和私钥)不可用,或者启用了匿名套件但需要身份验证,就会发生这种情况。
返回的数组包括来自 Java 安全标准算法名称规范的 JSSE 密码套件名称 部分中的标准密码套件名称列表的密码套件,并且还可能包括提供程序支持的其他密码套件。
- 返回:
- 一组密码套件名称
- 参见:
-
setEnabledCipherSuites
设置为此连接启用的密码套件。suites参数中的每个密码套件必须已由 getSupportedCipherSuites() 列出,否则该方法将失败。成功调用此方法后,仅启用suites参数中列出的套件供使用。请注意,密码套件名称的标准列表可以在 Java 安全标准算法名称规范的 JSSE 密码套件名称 部分找到。提供商可能支持未在此list中找到的密码套件名称,或者可能不使用某个密码套件的推荐名称。
请参阅
getEnabledCipherSuites()以了解有关为什么可能永远不会在连接上使用特定密码套件的更多信息。- 参数:
suites- 要启用的所有密码套件的名称- 抛出:
IllegalArgumentException- 当参数指定的一个或多个密码不受支持时,或者当参数为空时。- 参见:
-
getSupportedProtocols
返回可以在 SSL 连接上启用的协议的名称。- 返回:
- 支持的协议数组
-
getEnabledProtocols
返回当前启用用于此连接的协议版本的名称。请注意,即使启用了协议,也可能永远不会使用它。如果对等方不支持该协议,或者其使用受到限制,或者协议不支持启用的密码套件,则可能会发生这种情况。
- 返回:
- 一系列协议
- 参见:
-
setEnabledProtocols
设置为此连接启用的协议版本。这些协议必须已被
getSupportedProtocols()列为受支持。成功调用此方法后,仅启用protocols参数中列出的协议以供使用。- 参数:
protocols- 要启用的所有协议的名称。- 抛出:
IllegalArgumentException- 当参数指定的一个或多个协议不受支持或协议参数为空时。- 参见:
-
getSession
返回此连接使用的 SSL 会话。这些可以是长期存在的,并且经常对应于某些用户的整个登录会话。会话指定一个特定的密码套件,该密码套件被该会话中的所有连接主动使用,以及会话的客户端和服务的身份。如有必要,此方法将启动初始握手,然后阻塞直到握手建立。
如果在初始握手期间发生错误,此方法将返回一个无效的会话对象,该对象报告一个无效的密码套件“SSL_NULL_WITH_NULL_NULL”。
- 返回:
SSLSession
-
getHandshakeSession
返回在 SSL/TLS 握手期间构造的SSLSession。TLS 协议可以协商使用此类实例时所需的参数,但在
SSLSession已完全初始化并通过getSession可用之前。例如,有效签名算法列表可能会限制在 TrustManager 决策期间可以使用的证书类型,或者可以调整最大 TLS 片段数据包大小以更好地支持网络环境。此方法提供对正在构建的
SSLSession的早期访问。根据握手的进展情况,某些数据可能尚不可用。例如,如果远程服务将发送证书链,但该链尚未处理,则SSLSession的getPeerCertificates方法将抛出 SSLPeerUnverifiedException。一旦处理完该链,getPeerCertificates将返回正确的值。与
getSession()不同,此方法不会启动初始握手,并且在握手完成之前不会阻塞。- 返回:
-
如果此实例当前未进行握手,或者当前握手尚未进行到足以创建基本 SSLSession 的程度,则为 null。否则,此方法返回当前正在协商的
SSLSession。 - 抛出:
UnsupportedOperationException- 如果底层提供者没有实现该操作。- 自从:
- 1.7
- 参见:
-
addHandshakeCompletedListener
注册事件监听以接收 SSL 握手已在此连接上完成的通知。- 参数:
listener- 握手完成事件监听器- 抛出:
IllegalArgumentException- 如果参数为空。- 参见:
-
removeHandshakeCompletedListener
删除先前注册的握手完成监听。- 参数:
listener- 握手完成事件监听器- 抛出:
IllegalArgumentException- 如果监听器未注册,或者参数为空。- 参见:
-
startHandshake
在此连接上启动 SSL 握手。常见原因包括需要使用新的加密密钥、更改密码套件或启动新会话。要强制完全重新验证,可以在开始握手之前使当前会话无效。如果数据已经在连接上发送,它会在握手期间继续流动。握手完成后,将通过事件发出信号。此方法对于连接上的初始握手是同步的,并在协商的握手完成时返回。某些协议可能不支持在现有套接字上进行多次握手,并可能抛出 IOException。
- 抛出:
IOException- 网络级错误- 参见:
-
setUseClientMode
public abstract void setUseClientMode(boolean mode) 将套接字配置为在握手时使用客户端(或服务)模式。必须在任何握手发生之前调用此方法。一旦开始握手,就无法在此套接字的生命周期内重置模式。
服务通常对自己进行身份验证,客户端不需要这样做。
- 参数:
mode- 如果套接字应该在“客户端”模式下开始握手则为真- 抛出:
IllegalArgumentException- 如果在初始握手开始后尝试模式更改。- 参见:
-
getUseClientMode
public abstract boolean getUseClientMode()如果套接字设置为在握手时使用客户端模式,则返回 true。- 返回:
- 如果套接字应该在“客户端”模式下进行握手,则为真
- 参见:
-
setNeedClientAuth
public abstract void setNeedClientAuth(boolean need) 将套接字配置为require客户端身份验证。此选项仅对服务模式下的套接字有用。套接字的客户端身份验证设置是以下之一:
- 需要客户端身份验证
- 请求客户端身份验证
- 不需要客户端身份验证
与
setWantClientAuth(boolean)不同,如果设置了此选项并且客户端选择不提供有关其自身的身份验证信息,协商将停止,连接将断开.调用此方法会覆盖此方法或
setWantClientAuth(boolean)所做的任何先前设置。- 参数:
need- 如果需要客户端身份验证,则设置为 true,如果不需要客户端身份验证,则设置为 false。- 参见:
-
getNeedClientAuth
public abstract boolean getNeedClientAuth()如果套接字将返回 truerequire客户端身份验证。此选项仅对服务模式下的套接字有用。- 返回:
- 如果需要客户端身份验证,则为 true;如果不需要客户端身份验证,则为 false。
- 参见:
-
setWantClientAuth
public abstract void setWantClientAuth(boolean want) 将套接字配置为request客户端身份验证。此选项仅对服务模式下的套接字有用。套接字的客户端身份验证设置是以下之一:
- 需要客户端身份验证
- 请求客户端身份验证
- 不需要客户端身份验证
与
setNeedClientAuth(boolean)不同,如果设置了此选项并且客户端选择不提供有关其自身的身份验证信息,谈判将继续.调用此方法会覆盖此方法或
setNeedClientAuth(boolean)所做的任何先前设置。- 参数:
want- 如果请求客户端身份验证,则设置为 true,如果不需要客户端身份验证,则设置为 false。- 参见:
-
getWantClientAuth
public abstract boolean getWantClientAuth()如果套接字将返回 truerequest客户端身份验证。此选项仅对服务模式下的套接字有用。- 返回:
- 如果请求客户端身份验证,则为 true;如果不需要客户端身份验证,则为 false。
- 参见:
-
setEnableSessionCreation
public abstract void setEnableSessionCreation(boolean flag) 控制是否可以由此套接字建立新的 SSL 会话。如果不允许创建会话,并且没有要恢复的现有会话,则不会成功握手。- 参数:
flag- true 表示可以创建会话;这是默认设置。 false 表示必须恢复现有会话- 参见:
-
getEnableSessionCreation
public abstract boolean getEnableSessionCreation()如果此套接字可以建立新的 SSL 会话,则返回 true。- 返回:
- true 表示可以创建会话;这是默认设置。 false 表示必须恢复现有会话
- 参见:
-
getSSLParameters
返回对此 SSLSocket 有效的 SSLParameters。返回的 SSLParameters的密码套件和协议始终是非空的。- 返回:
- 对此 SSLSocket 有效的 SSLParameters。
- 自从:
- 1.6
-
setSSLParameters
将 SSLParameters 应用于此套接字。这意味着:
- 如果
params.getCipherSuites()不为空,则使用该值调用setEnabledCipherSuites()。 - 如果
params.getProtocols()不为空,则使用该值调用setEnabledProtocols()。 - 如果
params.getNeedClientAuth()或params.getWantClientAuth()返回true,则分别调用setNeedClientAuth(true)和setWantClientAuth(true);否则调用setWantClientAuth(false)。 - 如果
params.getServerNames()不为空,则套接字将使用该值配置其服务名称。 - 如果
params.getSNIMatchers()不为空,则套接字将使用该值配置其 SNI 匹配器。
- 参数:
params- 参数- 抛出:
IllegalArgumentException- 如果 setEnabledCipherSuites() 或 setEnabledProtocols() 调用失败- 自从:
- 1.6
- 如果
-
getApplicationProtocol
返回为此连接协商的最新应用程序协议值。如果底层 SSL/TLS/DTLS 实现支持,应用程序名称协商机制(例如 RFC 7301 应用层协议协商 (ALPN))可以在对等点之间协商应用程序级值。
- 实现要求:
-
此类中的实现抛出
UnsupportedOperationException并且不执行任何其他操作。 - 返回:
-
如果尚未确定应用程序协议是否可用于此连接,则为 null;如果不使用应用程序协议值,则为空
String;如果值已成功协商,则为非空应用程序协议String。 - 抛出:
UnsupportedOperationException- 如果底层提供者没有实现该操作。- 自从:
- 9
-
getHandshakeApplicationProtocol
返回在当前正在进行的 SSL/TLS 握手中协商的应用程序协议值。与
getHandshakeSession()一样,连接可能处于握手过程中。应用程序协议可能可用也可能不可用。- 实现要求:
-
此类中的实现抛出
UnsupportedOperationException并且不执行任何其他操作。 - 返回:
-
如果尚未确定应用程序协议是否可用于此握手,则为 null;如果不使用应用程序协议值,则为空
String;如果值已成功协商,则为非空应用程序协议String。 - 抛出:
UnsupportedOperationException- 如果底层提供者没有实现该操作。- 自从:
- 9
-
setHandshakeApplicationProtocolSelector
public void setHandshakeApplicationProtocolSelector(BiFunction <SSLSocket , List <String >, String > selector) 注册一个回调函数,该函数为 SSL/TLS/DTLS 握手选择应用程序协议值。该函数会覆盖使用SSLParameters.setApplicationProtocols提供的任何值,并且它支持以下类型参数:
例如,以下调用注册了一个回调函数,用于检查 TLS 握手参数并选择一个应用程序协议名称:-
SSLSocket -
该函数的第一个参数允许检查当前的
SSLSocket,包括握手会话和配置设置。 -
List<String> - 该函数的第二个参数列出了 TLS 对等方通告的应用程序协议名称。
-
String -
该函数的结果是应用程序协议名称,或者为 null 以指示不接受任何广告名称。如果返回值为空
String,则将不使用应用程序协议指示。如果返回值为 null(未选择任何值)或者是对等方未通告的值,则底层协议将确定要采取的操作。 (例如,ALPN 将发送“no_application_protocol”警报并终止连接。)
serverSocket.setHandshakeApplicationProtocolSelector( (serverSocket, clientProtocols) -> { SSLSession session = serverSocket.getHandshakeSession(); return chooseApplicationProtocol( serverSocket, clientProtocols, session.getProtocol(), session.getCipherSuite()); });- API 注意:
-
在 TLS 握手开始之前,TLS 服务应用程序应调用此方法。此外,此
SSLSocket应配置与回调函数选择的应用程序协议兼容的参数。例如,启用密码套件的错误选择可能会导致没有合适的应用程序协议。参见SSLParameters。 - 实现要求:
-
此类中的实现抛出
UnsupportedOperationException并且不执行任何其他操作。 - 参数:
selector- 回调函数,或为 null 以注销。- 抛出:
UnsupportedOperationException- 如果底层提供者没有实现该操作。- 自从:
- 9
-
-
getHandshakeApplicationProtocolSelector
检索在 SSL/TLS/DTLS 握手期间选择应用程序协议值的回调函数。有关函数的类型参数,请参阅setHandshakeApplicationProtocolSelector。- 实现要求:
-
此类中的实现抛出
UnsupportedOperationException并且不执行任何其他操作。 - 返回:
- 回调函数,如果未设置则返回 null。
- 抛出:
UnsupportedOperationException- 如果底层提供者没有实现该操作。- 自从:
- 9
-