Java Socket 几个重要的TCP/IP选项解析

http://elf8848.iteye.com/blog/1739598

Socket选择可以指定Socket类发送和接受数据的方式。在JDK1.4中共有8个Socket选择可以设置。这8个选项都定义在java.net.SocketOptions接口中。定义如下:

    public final static int TCP_NODELAY = 0x0001;
    public final static int SO_REUSEADDR = 0x04;
    public final static int SO_LINGER = 0x0080;
    public final static int SO_TIMEOUT = 0x1006;
    public final static int SO_SNDBUF = 0x1001;
    public final static int SO_RCVBUF = 0x1002;
    public final static int SO_KEEPALIVE = 0x0008;
    public final static int SO_OOBINLINE = 0x1003;
    有趣的是,这8个选项除了第一个没在SO前缀外,其他7个选项都以SO作为前缀。其实这个SO就是Socket Option的缩写;因此,在Java中约定所有以SO为前缀的常量都表示Socket选项;当然,也有例外,如TCP_NODELAY.在Socket类中为每一个选项提供了一对get和set方法,分别用来获得和设置这些选项。
    1. TCP_NODELAY

public boolean getTcpNoDelay() throws SocketException
public void setTcpNoDelay(boolean on) throws SocketException
    在默认情况下,客户端向服务器发送数据时,会根据数据包的大小决定是否立即发送。当数据包中的数据很少时,如只有1个字节,而数据包的头却有几十个字节(IP头+TCP头)时,系统会在发送之前先将较小的包合并到软大的包后,一起将数据发送出去。在发送下一个数据包时,系统会等待服务器对前一个数据包的响应,当收到服务器的响应后,再发送下一个数据包,这就是所谓的Nagle算法;在默认情况下,Nagle算法是开启的。
    这种算法虽然可以有效地改善网络传输的效率,但对于网络速度比较慢,而且对实现性的要求比较高的情况下(如游戏、Telnet等),使用这种方式传输数据会使得客户端有明显的停顿现象。因此,最好的解决方案就是需要Nagle算法时就使用它,不需要时就关闭它。而使用setTcpToDelay正好可以满足这个需求。当使用setTcpNoDelay(true)将Nagle算法关闭后,客户端每发送一次数据,无论数据包的大小都会将这些数据发送出去。
    2.  SO_REUSEADDR

public boolean getReuseAddress() throws SocketException          
public void setReuseAddress(boolean on) throws SocketException
错误的说法:
      通过这个选项,可以使多个Socket对象绑定在同一个端口上。
正确的说明是:

     如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用 端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息, 抛出“Address already in use: JVM_Bind”。如果你的服务程序停止后想立即重启,不等60秒,而新套接字依旧 使用同一端口,此时 SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期 望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。


这个参数在Windows平台与Linux平台表现的特点不一样。在Windows平台表现的特点是不正确的, 在Linux平台表现的特点是正确的。
在Windows平台,多个Socket新建立对象可以绑定在同一个端口上,这些新连接是非TIME_WAIT状态的。这样做并没有多大意义。
在Linux平台,只有TCP状态位于 TIME_WAIT ,才可以重用 端口。这才是正确的行为。
publicclass Test {
public static void main(String[] args) {
try {
ServerSocket socket1 = new ServerSocket();
ServerSocket socket2 = new ServerSocket();
socket1.setReuseAddress(true);
socket1.bind(new InetSocketAddress("127.0.0.1", 8899));
System.out.println("socket1.getReuseAddress():"
+ socket1.getReuseAddress());
socket2.setReuseAddress(true);
socket2.bind(new InetSocketAddress("127.0.0.1", 8899));
System.out.println("socket2.getReuseAddress():"
+ socket1.getReuseAddress());
} catch (Exception e) {
e.printStackTrace();
}
}
}

使用SO_REUSEADDR选项时有两点需要注意:
    1.  必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项。因此,要想使用SO_REUSEADDR选项,就不能通过Socket类的构造方法来绑定端口。
    2.  必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用。如在例程4-12中,socket1和socket2都使用了setReuseAddress方法打开了各自的SO_REUSEADDR选项。



在Windows操作系统上运行上面的代码的运行结果如下:
这种结果是不正确的。
socket1.getReuseAddress():true
socket2.getReuseAddress():true
在Linux操作系统上运行上面的代码的运行结果如下:
这种结果是正确的。因为第一个连接不是TIME_WAIT状态的,第二个连接就不能使用8899端口;
只有第一个连接是TIME_WAIT状态的,第二个连接就才能使用8899端口;

Java代码  收藏代码
socket1.getReuseAddress():true 
java.net.BindException: Address already in use 
    at java.net.PlainSocketImpl.socketBind(Native Method) 
    at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:383) 
    at java.net.ServerSocket.bind(ServerSocket.java:328) 
    at java.net.ServerSocket.bind(ServerSocket.java:286) 
    at com.Test.main(Test.java:15) 





    3.  SO_LINGER

public int getSoLinger() throws SocketException
public void setSoLinger(boolean on, int linger) throws SocketException
    这个Socket选项可以影响close方法的行为。在默认情况下,当调用close方法后,将立即返回;如果这时仍然有未被送出的数据包,那么这些数据包将被丢弃。如果将linger参数设为一个正整数n时(n的值最大是65,535),在调用close方法后,将最多被阻塞n秒。在这n秒内,系统将尽量将未送出的数据包发送出去;如果超过了n秒,如果还有未发送的数据包,这些数据包将全部被丢弃;而close方法会立即返回。如果将linger设为0,和关闭SO_LINGER选项的作用是一样的。
    如果底层的Socket实现不支持SO_LINGER都会抛出SocketException例外。当给linger参数传递负数值时,setSoLinger还会抛出一个IllegalArgumentException例外。可以通过getSoLinger方法得到延迟关闭的时间,如果返回-1,则表明SO_LINGER是关闭的。例如,下面的代码将延迟关闭的时间设为1分钟:

if(socket.getSoLinger() == -1) socket.setSoLinger(true, 60);
    4.  SO_TIMEOUT

public int getSoTimeout() throws SocketException
public void setSoTimeout(int timeout) throws SocketException
    这个Socket选项在前面已经讨论过。可以通过这个选项来设置读取数据超时。当输入流的read方法被阻塞时,如果设置timeout(timeout的单位是毫秒),那么系统在等待了timeout毫秒后会抛出一个InterruptedIOException例外。在抛出例外后,输入流并未关闭,你可以继续通过read方法读取数据。
    如果将timeout设为0,就意味着read将会无限等待下去,直到服务端程序关闭这个Socket.这也是timeout的默认值。如下面的语句将读取数据超时设为30秒:

socket1.setSoTimeout(30 * 1000);
    当底层的Socket实现不支持SO_TIMEOUT选项时,这两个方法将抛出SocketException例外。不能将timeout设为负数,否则setSoTimeout方法将抛出IllegalArgumentException例外。
    5.  SO_SNDBUF

public int getSendBufferSize() throws SocketException
public void setSendBufferSize(int size) throws SocketException
    在默认情况下,输出流的发送缓冲区是8096个字节(8K)。这个值是Java所建议的输出缓冲区的大小。如果这个默认值不能满足要求,可以用setSendBufferSize方法来重新设置缓冲区的大小。但最好不要将输出缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。
    如果底层的Socket实现不支持SO_SENDBUF选项,这两个方法将会抛出SocketException例外。必须将size设为正整数,否则setSendBufferedSize方法将抛出IllegalArgumentException例外。
    6.  SO_RCVBUF

public int getReceiveBufferSize() throws SocketException
public void setReceiveBufferSize(int size) throws SocketException
    在默认情况下,输入流的接收缓冲区是8096个字节(8K)。这个值是Java所建议的输入缓冲区的大小。如果这个默认值不能满足要求,可以用setReceiveBufferSize方法来重新设置缓冲区的大小。但最好不要将输入缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。
    如果底层的Socket实现不支持SO_RCVBUF选项,这两个方法将会抛出SocketException例外。必须将size设为正整数,否则setReceiveBufferSize方法将抛出IllegalArgumentException例外。

    7.  SO_KEEPALIVE

public boolean getKeepAlive() throws SocketException
public void setKeepAlive(boolean on) throws SocketException
    如果将这个Socket选项打开,客户端Socket每隔段的时间(大约两个小时)就会利用空闲的连接向服务器发送一个数据包。这个数据包并没有其它的作用,只是为了检测一下服务器是否仍处于活动状态。如果服务器未响应这个数据包,在大约11分钟后,客户端Socket再发送一个数据包,如果在12分钟内,服务器还没响应,那么客户端Socket将关闭。如果将Socket选项关闭,客户端Socket在服务器无效的情况下可能会长时间不会关闭。SO_KEEPALIVE选项在默认情况下是关闭的,可以使用如下的语句将这个SO_KEEPALIVE选项打开:

socket1.setKeepAlive(true);
    8.  SO_OOBINLINE

public boolean getOOBInline() throws SocketException
public void setOOBInline(boolean on) throws SocketException
    如果这个Socket选项打开,可以通过Socket类的sendUrgentData方法向服务器发送一个单字节的数据。这个单字节数据并不经过输出缓冲区,而是立即发出。虽然在客户端并不是使用OutputStream向服务器发送数据,但在服务端程序中这个单字节的数据是和其它的普通数据混在一起的。因此,在服务端程序中并不知道由客户端发过来的数据是由OutputStream还是由sendUrgentData发过来的。下面是sendUrgentData方法的声明:

public void sendUrgentData(int data) throws IOException
    虽然sendUrgentData的参数data是int类型,但只有这个int类型的低字节被发送,其它的三个字节被忽略。下面的代码演示了如何使用SO_OOBINLINE选项来发送单字节数据。

package mynet;

import java.net.*;
import java.io.*;

class Server
{
    public static void main(String[] args) throws Exception
    {
        ServerSocket serverSocket = new ServerSocket(1234);
        System.out.println("服务器已经启动,端口号:1234");
        while (true)
        {
            Socket socket = serverSocket.accept();
            socket.setOOBInline(true);
            InputStream in = socket.getInputStream();
            InputStreamReader inReader = new InputStreamReader(in);
            BufferedReader bReader = new BufferedReader(inReader);
            System.out.println(bReader.readLine());
            System.out.println(bReader.readLine());
            socket.close();
        }
    }
}
public class Client
{
    public static void main(String[] args) throws Exception
    {
        Socket socket = new Socket("127.0.0.1", 1234);
        socket.setOOBInline(true);
        OutputStream out = socket.getOutputStream();
        OutputStreamWriter outWriter = new OutputStreamWriter(out);
        outWriter.write(67);              // 向服务器发送字符"C"
        outWriter.write("hello world\r\n");
        socket.sendUrgentData(65);        // 向服务器发送字符"A"
        socket.sendUrgentData(322);        // 向服务器发送字符"B"
        outWriter.flush();
        socket.sendUrgentData(214);       // 向服务器发送汉字”中”
        socket.sendUrgentData(208);
        socket.sendUrgentData(185);       // 向服务器发送汉字”国”
        socket.sendUrgentData(250);
        socket.close();
    }
}
    由于运行上面的代码需要一个服务器类,因此,在加了一个类名为Server的服务器类,关于服务端套接字的使用方法将会在后面的文章中详细讨论。在类Server类中只使用了ServerSocket类的accept方法接收客户端的请求。并从客户端传来的数据中读取两行字符串,并显示在控制台上。

    测试
    由于本例使用了127.0.0.1,因Server和Client类必须在同一台机器上运行。
    运行Server

java mynet.Server
    运行Client

java mynet.Client
    在服务端控制台的输出结果

服务器已经启动,端口号:1234
ABChello world
中国
    在ClienT类中使用了sendUrgentData方法向服务器发送了字符'A'(65)和'B'(66)。但发送'B'时实际发送的是322,由于sendUrgentData只发送整型数的低字节。因此,实际发送的是66.十进制整型322的二进制形式如图1所示。

图1  十进制整型322的二进制形式
    从图1可以看出,虽然322分布在了两个字节上,但它的低字节仍然是66.
    在Client类中使用flush将缓冲区中的数据发送到服务器。我们可以从输出结果发现一个问题,在Client类中先后向服务器发送了'C'、"hello world"r"n"、'A'、'B'.而在服务端程序的控制台上显示的却是ABChello world.这种现象说明使用sendUrgentData方法发送数据后,系统会立即将这些数据发送出去;而使用write发送数据,必须要使用flush方法才会真正发送数据。
    在Client类中向服务器发送"中国"字符串。由于"中"是由214和208两个字节组成的;而"国"是由185和250两个字节组成的;因此,可分别发送这四个字节来传送"中国"字符串。
    注意:在使用setOOBInline方法打开SO_OOBINLINE选项时要注意是必须在客户端和服务端程序同时使用setOOBInline方法打开这个选项,否则无法命名用sendUrgentData来发送数据。



1. SO_LINGER/ SO_REUSEADDR
    TCP正常的关闭过程如下(四次握手过程):
(FIN_WAIT_1) A       ---FIN--->       B(CLOSE_WAIT)
(FIN_WAIT_2) A       <--ACK--       B(CLOSE_WAIT)
  (TIME_WAIT)A        <--FIN----       B(LAST_ACK)
  (TIME_WAIT)A        ---ACK->       B(CLOSED)
    Ø  A端首先发送一个FIN请求给B端,要求关闭,发送后A段的TCP状态变更为FIN_WAIT_1,接收到FIN请求后B端的TCP状态变更为CLOSE_WAIT
    Ø  B接收到ACK请求后,B回一个ACK给A端,确认接收到的FIN请求,接收到ACK请求后,A端的TCP状态变更为为FIN_WAIT_2。
    Ø  B端再发送一个FIN请求给A端,与连接过程的3次握手过程不一样,这个FIN请求之所以并不是与上一个请求一起发送,之所以如此处理,是因为TCP是双通道的,允许在发送ACK请求后,并不马上发FIN请求,即只关闭A到B端的数据流,仍然允许B端到A端的数据流。这个ACK请求发送之后,B端的TCP状态变更为LAST_ACK,A端的状态变更为TIME_WAIT。
    Ø  A端接收到B端的FIN请求后,再回B端一个ACK信息,对上一个FIN请求进行确认,到此时B端状态变更为CLOSED,Socket可以关闭。
   除了如上正常的关闭(优雅关闭)之外,TCP还提供了另外一种非优雅的关闭方式RST(Reset)
   (CLOSED) A         ---RST-->      B (CLOSED)
   Ø  A端发送RST状态之后,TCP进入CLOSED状态,B端接收到RST后,也即可进入CLOSED状态。
    在第一种关闭方式上(优雅关闭),非常遗憾,A端在最后发送一个ACK请求后,并不能马上将该Socket回收,因为A并不能确定B一定能够接收到这个ACK请求,因此A端必须对这个Socket维持TIME_WAIT状态2MSL(MSL=Max Segment Lifetime,取决于操作系统和TCP实现,该值为30秒、60秒或2分钟)。如果A端是客户端,这并不会成为问题,但如果A端是服务端,那就很危险了,如果连接的Socket非常多,而又维持如此多的TIME_WAIT状态的话,那么有可能会将Socket耗尽(报Too Many Open File)。
    服务端为了解决这个TIME_WAIT问题,可选择的方式有三种:
    Ø  保证由客户端主动发起关闭(即做为B端)
    Ø  关闭的时候使用RST的方式
    Ø  对处于TIME_WAIT状态的TCP允许重用
     一般我们当然最好是选择第一种方式,实在没有办法的时候,我们可以使用SO_LINGER选择第二种方式,使用SO_REUSEADDR选择第三种方式
Java代码   收藏代码
public void setSoLinger(boolean on, int linger) throws SocketException 
public void setReuseAddress(boolean on) throws SocketException  
    第一个on表示是否使用SO_LINGER选项,linger(以秒为单位)表示在发RST之前会等待多久,因为一旦发送RST,还在缓冲区中还没有发送出去的数据就会直接丢弃
2.TCP_NODELAY
     对于交互型的应用(譬如telnet),经常存在的情况是客户端和服务端之间需要频繁地进行一些小数据交换,譬如telnet可能每敲一个键盘都需要将数据发送到服务端。为了避免这种情况会产生大量小数据包,提出了Nagle算法。Nagle算法要求每次在发送端最后只有一个未被确认的包,因此上一个包发送出去还没有接收到响应之前,要求发送的包回先放在缓冲区,接收到响应之后,会将缓冲区中的包合并成一个包发送出去(可以看到,响应回地越快,发送出去的数据也会越快)。
     需要注意的是,由Nagle算法要求只能有一个未被确认的包,因此窗口参数会失效,在大数据量传送的情况下会使网络吞吐量下降,因此对于大数据量的交互,应该关闭Nagle算法,Nagle算法比较适合小数据量频繁交换的情景。我们可以使用TCP_NODELAY关闭Nagle算法。
Java代码   收藏代码
public void setTcpNoDelay(boolean on) throws SocketException 
3.SO_KEEPALIVE
     在一个TCP连接建立之后,我们会很奇怪地发现,默认情况下,如果一端异常退出(譬如网络中断后一端退出,使地关闭请求另一端无法接收到),TCP的另一端并不能获得这种情况,仍然会保持一个半关闭的连接,对于服务端,大量半关闭的连接将会是非常致命的。SO_KEEPALIVE提供了一种手段让TCP的一端(通常服务提供者端)可以检测到这种情况。如果我们设置了SO_KEEPALIVE,TCP在距离上一次TCP包交互2个小时(取决于操作系统和TCP实现,规范建议不低于2小时)后,会发送一个探测包给另一端,如果接收不到响应,则在75秒后重新发送,连续10次仍然没有响应,则认为对方已经关闭,系统会将该连接关闭。一般情况下,如果对方已经关闭,则对方的TCP层会回RST响应回来,这种情况下,同样会将连接关闭。
Java代码   收藏代码
public void setKeepAlive(boolean on) throws SocketException

你可能感兴趣的:(Java Socket 几个重要的TCP/IP选项解析)