在前一章节中,我们讲到RMI框架的底层就是ServerSocket和Socket之间的通信。如果到此就停止脚步,难免会让人惋惜。为了不留遗憾,我决定继续停留此处,慢慢欣赏这多彩的世界。
在上节我们看到连接RMI服务端的客户端程序,最终调用了TCPDirectSocketFactory类的createSocket()方法。该类的具体代码如下所示:
public class TCPDirectSocketFactory extends RMISocketFactory {
public Socket createSocket(String host, int port) throws IOException
{
return new Socket(host, port);
}
public ServerSocket createServerSocket(int port) throws IOException
{
return new ServerSocket(port);
}
}
看到new Socket(host, port);,不知道你是什么状态,反正我飘了,想立马跑出出租屋,在大街上发疯。但在理智的监督下,我没得做出如此出格的事情。于是继续向下看,下述代码来源于TCPEndpoint# newSocket():
try {
socket.setTcpNoDelay(true);
} catch (Exception e) {
// if we fail to set this, ignore and proceed anyway
}
// fix 4187495: explicitly set SO_KEEPALIVE to prevent client hangs
try {
socket.setKeepAlive(true);
} catch (Exception e) {
// ignore and proceed
}
这是什么?它们有啥作用?老天,我居然不知道,等等后面好像还有类似的东西,以下代码来自TCPChannel#createConnection():
int originalSoTimeout = 0;
try {
originalSoTimeout = sock.getSoTimeout();
sock.setSoTimeout(handshakeTimeout);
} catch (Exception e) {
// if we fail to set this, ignore and proceed anyway
}
// 中间删除了一些代码,详情请见源码
try {
/*
* If socket factory had set a non-zero timeout on its
* own, then restore it instead of using the property-
* configured value.
*/
sock.setSoTimeout((originalSoTimeout != 0 ?
originalSoTimeout :
responseTimeout));
} catch (Exception e) {
// if we fail to set this, ignore and proceed anyway
}
好了好了,先到这里吧,已经无法继续了,后面的代码已经把我绕晕了。不过还是先总结一下:
- Socket对象的创建。创建Socket的方式有很多,常用的构造方法有以下几个:
- Socket()
- Socket(InetAddress address, int port) throws UnknownHostException,IOException
- Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
- Socket(String host, int port) throws UnknownHostException,IOException
- Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
- Socket(Proxy proxy)
这些方法中,除了第一个无参构造方法外,其他构造方法都会试图与服务器之间建立一个连接,一旦连接成功,就会返回一个 Socket 对象,否则抛出异常。不过这里有一点需要我们注意一下,这些尝试与服务器之间建立连接的构造方法,默认情况下,会一直等待下去,直到连接成功,或者出现异常。但我们要清楚,如果底层网络不稳定,通过构造方法创建连接的动作会一直等待下去,我想这不是大家想看到的吧!那有没有办法让他不要一直等待下去呢?方法肯定是有的。就是第一个无参构造方法和connect()方法结合。具体代码如下所示:
Socket socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", 1099), 60000);
上述代码表示创建一个客户端到服务器之间的连接,如果在一分钟之内出现某种异常,则抛出该异常,如果在一分钟后既没有连接成功,也没有出现异常,那么就抛出 SocketTimeout-Exception异常。如果在一分钟内连接成功,则 connect() 方法正常返回。由此我们可以知道上述代码表示在创建连接时会等待一分钟,从而避免一直等待下去。注意:这里的60000单位是毫秒。
- Socket中相关信息的获取。在一个 Socket 对象中同时包含了远程服务器的 IP 地址和端口信息,以及客户本地的 IP 地址和端口信息。此外,从 Socket 对象中还可以获得输出流和输入流,分别用于向服务器发送数据,以及接收从服务器端发来的数据。
- getInetAddress() // 获得远程被连接进程的IP地址
- getPort() // 获得远程被连接进程的端口
- getLocalAddress() // 获得本地的IP地址
- getLocalPort() // 获得本地的端口
- getInputStream() // 获得输入流,如果Socket还没有连接,或者已经关团,或者已经通过shutdownInput()方法关闭输入流,那么此方法会抛出IOException
- getOutputStream() // 获得输出流,如果Socket还没有连接,或者已经关闭,或者已经通过shutdownOutput()方法关闭输出流,那么此方法会抛出 IOException
- Socket相关设置选项
- TCP_NODELAY:表示立即发送数据。在默认情况下发送数据采用 Negale 算法,发送方发送的数据不会立刻被发出,而是先放在缓冲区内,等缓冲区满了再发出。发送完一批数据后,会等待接收方对这批数据的回应,然后发送下一批数据。此算法法适用于发送方需要发送大批量数据并且接收方会及时做出回应的场合,这种算法通过减少传输数据的次数来提高通信效率。如果发送方持续地发送小批量的数据。并且接收方不一定会立即发送响应数据,那么 Negale 算法会使发送方运行得很慢,对于GU程序,比如网络游戏程序(服务器需要实时跟踪客户端鼠标的移动),这个问题尤其突出。TCP_NODEALY 的默认值为 false,表示采用 Negale 算法,如果调用 setTcpNoDelay(true) 方法,就会关闭 Socket 的缓冲,确保数据被及时发送:if(!socket.getTcpNoDelay()) { socket.setTcpNoDelay(true); }
- SO_RESUSEADDR:表示是否允许重用 Socket 所绑定的本地地址。当接收方通过 Socket 的 close() 方法关闭 Socket 时,如果网络上还有发送到这个 Socket 的数据,那么底层的 Socket 不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,再释放端口。Socket 接收到延迟数据后,不会对这些数据做任何处理。Socket 接收延迟数据的目的是,确保这些数据不会被其他碰巧绑定到同样端口的新进程接收到。客户程序一般采用随机端口,因此出现两个客户程序绑定到同样端口的可能性不大。许多服务器程序都使用固定的端口。当服务器程序被关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一台主机上重启服务器程序,由于端口已经被占用,使得服务感程序无法绑定到该端口,导致启动失败。为了确保当一个进程关闭了 Socket 后,即便它还没释放端口,同一台主机上的其他进要也可以立刻重用该端口,可以调用 Socke 的 setResuseAddress(ture) 方法:if(!socket.getResuseAddress()) { socket.setResuseAddress(true); }。值得注意的是 socket.setResuseAddress(true) 方法必须在 Socket 还没有被绑定到一个本地端口之前调用,否则执行无效
- SO_TIMEOUT:表示接收数据时的等待超时时间。当通过 Socket 的输入流读数据时,如果还没有数据,就会等待。Socket 类的SO_TIMEOUT 选项用于设定接收数据的等待超时时间,单位为 ms,它的默认值为0,表示会无限等待,永远不会超时。以下代码把接收数据的等待超时时间设为三分钟:if(socket.getTimeout() == 0) { socket.setTimeout(60000 * 3); };Socket 的 setTimeout() 方法必须在接收数据之前执行才有效
- SO_LINGER:表示与执行 Socket 的 close() 方法时,是否立即关闭底层的 Socket。在默认情况下执行 Socket 的 close() 方法,该方法会立即返回,但底层的 Socket 实际上并不立即关闭,它会延迟一段时间,直到发送完所有剩余的数据,才会真正关闭 Socket。如果执行以下方法:socket.setSoLinger(true,0);。那么执行 Socket 的 close() 方法,该方法也会立即返回而且底层的 Socket 也会立即关闭,所有未发送完的数据被丢弃。如果执行以下方法:socke-t.setSoLinger(true,3600);。那么执行 Socket 的 close() 方法,该方法不会立即返回,而是进入阻塞状态,同时,底层的 Socket 会尝试发送剩余的数据。只有满足以下两个条件之一,close() 方法才返回:1)底层的 Socket 已经发送完所有的剩余数据;2)尽管底层的 Socket 还没有发送完所有的剩余数据,但己经阻塞了 3600s,此时 close() 也会返回,未发送的数据被丢弃
- SO_RCVBUF:表示接收数据的缓冲区的大小。一般说来,传输大的连续的数据块,比如基于 HTTP 或 FTP 的通信,可以使用较大的缓冲区,这可以减少传输数据的次数,提高传输数据的效率。而对于交互式的通信方式,比如 Telnet 和网络游戏,则应该采用小的缓冲区,确保小批量的数据能及时发送给对方
- SO_SNDBUF:表示发送数据的缓冲区的大小
- SO_KEEPALIVE:表示对于长时间处于空闲状态的 Socket,是否要自动把它关闭。当 SO_KEEPALIVE 选项为 tue 时,表示底层的 TCP 实现会监视该连接是否有效连接,是否处于空闲状态,即连接的两端没有互相传送数据超过了 2 小时,本地的 TCP 实现发送一个数据包给远程的 Socket,如果远程 Socke 没有返回响应,TCP 实现就会持续尝试发送 11 分钟,直到接收到响应为止。如果在 12 分钟内未收到响应,TCP 实现就会自动关闭本地 Socket,断开连接。SO_KEEPALIVE 选项的默认值为 false,表示 TCP 不会监视连接是否有效,不活动的客户端可能会永久存在下去,而不会注意到服务器已经崩溃