Socket/ServerSocket 选项

时间:2021-08-26 08:44:03
在网络编程中,Socket/ServerSocket有一些选项用来自定义一些行为,现在分享一下。
 
Socket/ServerSocket 选项
 

Socket选项

1.TCP_NODELAY

在Socket发送数据时,默认情况下,数据会先进入缓冲区,等缓冲区满了再发送出去,意图是为了通过减少传输数据的次数,以此来提高通信效率

但是,对于一些需要即时发送,即时响应的场景并不合适,比如网络游戏。客户端因为采用的默认行为,会等到缓冲区满了后才发送数据,服务端的响应相应也会变慢,导致整个游戏运行起来不流畅。

这时,就要开启TCP_NODELAY,关闭默认行为,实时发送实时响应。

 

2.SO_TIMEOUT

在读取数据时,常常因为种种原因,导致读方法阻塞,为了保证阻塞时间可控,可以设置SO_TIMEOUT选项,设置读数据时的最大等待时间,如果阻塞时间超过设置的时间,

则会抛出【Exception in thread "main" java.net.SocketTimeoutException: Read timed out】。示例如下。

客户端代码:阻塞6000ms后才发送数据

public class Main {

    public static void main(String[] args) throws InterruptedException, IOException {

        Socket socket = new Socket("127.0.0.1", 8899);
System.out.println("客户端启动...");
OutputStream out = socket.getOutputStream();
Thread.sleep(6000); //阻塞6000ms后才发送数据
out.write("This is for everyone".getBytes());
}
}

服务端代码:通过SO_TIMEOUT选项,设置读阻塞时长最大为5000ms,由于客户端再6000ms后才会出数据,所以会抛出SocketTimeoutException异常。

public class SimpleServer {

    public static void main(String[] args) throws IOException, InterruptedException {

        ServerSocket serverSocket = new ServerSocket(8899, 2);
System.out.println("服务端启动...");
while (true) {
Socket socket = serverSocket.accept();
System.out.println("连接成功:" + socket);
soTimeout(socket);
}
} private static void soTimeout(Socket socket) throws IOException {
socket.setSoTimeout(5000); //设置最大阻塞时间为5000ms
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = 0;
while ((len = in.read(buf)) >= 0) { //因为阻塞时间超过5000ms,所以这里抛出【Exception in thread "main" java.net.SocketTimeoutException: Read timed out】异常
System.out.println("read,len=" + len + ",str=" + new String(buf, 0, len));
}
}
}

3.SO_REUSEADDR

对于Socket来说,当通过Socket.close()关闭时,底层的Socket为了把数据发送完或者接受完,不会立即关闭而是会等待一段时间,确保完成自己的使命。

但这会带来一个问题,由于Socket的端口号不能共享,一旦一个端口号被占用,之后再用这个端口号尝试建立新连接的话会报端口冲突的异常。

为了确保一个进程关闭Socket后,同一主机的其他进程还能使用这个端口,可以设置SO_REUSEADDR选项。

4.SO_LINGER

SO_LINGER选项用来控制Socket关闭时的行为,上面已经简单讲过,默认情况下,调用Socket.close()方法后,close()方法会立即返回,但是底层的Socket会等待数据发送完成后再关闭。

当设置Socket.setSoLinger(true, 0)选项时,close()方法也会立即返回,底层的Socket也会立即关闭,未发送完的数据会抛弃掉。

当设置Socket.setSoLinger(true, n)选项时,close()方法会阻塞,直到满足下列条件中的一个才会返回。

  1. n秒后,即使底层Socket还未发送完,也强制关闭底层Socket,并返回close()方法。

  2. 底层Socket数据全部发送完(全部送到缓冲区)。

示例如下。

服务端代码:没什么特别的,就是从Socket中读出数据,重点在客户端

public class SimpleServer {

    public static void main(String[] args) throws IOException, InterruptedException {

        ServerSocket serverSocket = new ServerSocket(8899, 2);
System.out.println("服务端启动...");
while (true) {
Socket socket = serverSocket.accept();
System.out.println("连接成功:" + socket);
soLinger(socket);
}
} // 没什么特别的,就是从Socket中读出数据,重点在客户端
private static void soLinger(Socket socket) throws InterruptedException, IOException { InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = 0;
while ((len = in.read(buf)) >= 0) {
System.out.println("read,len=" + len + ",str=" + new String(buf, 0, len));
}
}
}

客户端代码:首先是不设置SO_LINGER选项,已默认的方式运行。之后#1,close()方法立即返回。#2,close()方法阻塞了4ms。

public class Main {

    public static void main(String[] args) throws InterruptedException, IOException {

        Socket socket = new Socket("127.0.0.1", 8899);
//socket.setSoLinger(true, 0); #1
//socket.setSoLinger(true, 10); #2
System.out.println("客户端启动...");
OutputStream out = socket.getOutputStream();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 500000; i++) {
sb.append(i);
}
out.write(sb.toString().getBytes());
long start = System.currentTimeMillis();
socket.close();
long end = System.currentTimeMillis();
System.out.println("close:" + (end - start)); //#1:0 #2:4
}
}

默认情况下,close()方法立即返回,Socket底层在发送完数据后关闭,服务端正常运行。

当把#1的注释给删掉,close()方法立即返回,Socket底层也会立即关闭,因为客户端数据没有完全发出,所以服务端运行时抛出了【Exception in thread "main" java.net.SocketException: Connection reset】异常

当吧#2的注释给删掉,close()方法阻塞了4ms才返回,Socket底层在发送完数据后关闭,服务端正常运行。

5.SO_RCVBUF,SO_SNDBUF

设置输入,输出缓冲区的大小。

一般情况下,对于数据量大,传输频率低的场景,适合大的缓冲区,减少传输数据的次数,提高传输效率,比如FTP、HTTP等。

对于数据量小,传输频率高,对实时性要求高的场景,适合设置小的缓冲区。

6.SO_KEEPALIVE

检测TCP连接的有效性。大概机制是,2小时后双方没有过交互一直处于空闲状态,则会发送一个请求,如果请求没有响应,则会认为对方已经关闭,这样本地的Socket也会自动关闭。

ServerSocket选项

1.SO_TIMEOUT

ServerSocket.accept()方法是个阻塞方法,会一直阻塞到有一个TCP连接成功建立,SO_TIMEOUT选项则可以设置最大的阻塞时间,超过这个时间还没有建立TCP连接的话则抛出异常。

注意和Socket的SO_TIMEOUT选项区别开,Socket的SO_TIMEOUT选项是设置读操作的最大阻塞时间,ServerSocket的SO_TIMEOUT选项是设置accept()操作的最大阻塞时间。

服务端代码:5000ms后任然没有与某个客户端成功建立连接,所以会抛出SocketTimeoutException异常。

public class SimpleServer {

    public static void main(String[] args) throws IOException, InterruptedException {

        ServerSocket serverSocket = new ServerSocket(8899, 2);
serverSocket.setSoTimeout(5000);
System.out.println("服务端启动...");
while (true) {
Socket socket = serverSocket.accept(); ////SO_TIMEOUT选项为5000ms,超过这段时间后,抛出【Exception in thread "main" java.net.SocketTimeoutException: Accept timed out】
System.out.println("连接成功:" + socket);
}
}
}

2.SO_REUSEADDR

同Socket的SO_REUSEADDR选项类似,为了保证一个进程关闭ServerSocket后,即使它还没有释放端口,同一主机的其他进程还能使用这个端口,可以设置SO_REUSEADDR解决这个问题。

对于Socket来讲,一般Socket的端口号都是系统随机分配的,碰巧碰到同一端口的情况比较少。但是对于ServerSocket来讲,往往用的都是固定端口号,所以同一端口的情况就不能忽视了。

3.SO_RCVBUF

设置ServerSocket输入缓冲区的大小。

引用

1.《Java网络编程精解》(孙卫琴)