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

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

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方法,分别用来得到和设置这些选项。java

PS:算法

  1. netty的有差选项说明:http://docs.jboss.org/netty/3.2/api/org/jboss/netty/channel/socket/SocketChannelConfig.html编程

  2. oracle的说明 http://docs.oracle.com/javase/6/docs/technotes/guides/net/socketOpt.htmlapi

1. TCP_NODELAY

public boolean getTcpNoDelay() throws SocketExceptionpublic 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

错误的说法:oracle

  经过这个选项,可使多个Socket对象绑定在同一个端口上。

正确的说明是:socket

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

这个参数在Windows平台与Linux平台表现的特色不同。在Windows平台表现的特色是不正确的, 在Linux平台表现的特色是正确的。ide

在Windows平台,多个Socket新创建对象能够绑定在同一个端口上,这些新链接是非TIME_WAIT状态的。这样作并无多大意义。

在Linux平台,只有TCP状态位于 TIME_WAIT ,才能够重用 端口。这才是正确的行为。

public class 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():truesocket2.getReuseAddress():true

在Linux操做系统上运行上面的代码的运行结果以下:

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)

这种结果是正确的。由于第一个链接不是TIME_WAIT状态的,第二个链接就不能使用8899端口;

只有第一个链接是TIME_WAIT状态的,第二个链接就才能使用8899端口;

3.  SO_LINGER

public int getSoLinger() throws SocketExceptionpublic 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 SocketExceptionpublic 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 SocketExceptionpublic void setSendBufferSize(int size) throws SocketException

在默认状况下,输出流的发送缓冲区是8096个字节(8K)。这个值是Java所建议的输出缓冲区的大小。若是这个默认值不能知足要求,能够用 setSendBufferSize方法来从新设置缓冲区的大小。但最好不要将输出缓冲区设得过小,不然会致使传输数据过于频繁,从而下降网络传输的效 率。

若是底层的Socket实现不支持SO_SENDBUF选项,这两个方法将会抛出SocketException例外。必须将size设为正整数,不然setSendBufferedSize方法将抛出IllegalArgumentException例外。

6.  SO_RCVBUF

public int getReceiveBufferSize() throws SocketExceptionpublic void setReceiveBufferSize(int size) throws SocketException

在默认状况下,输入流的接收缓冲区是8096个字节(8K)。这个值是Java所建议的输入缓冲区的大小。若是这个默认值不能知足要求,能够用 setReceiveBufferSize方法来从新设置缓冲区的大小。但最好不要将输入缓冲区设得过小,不然会致使传输数据过于频繁,从而下降网络传输 的效率。

若是底层的Socket实现不支持SO_RCVBUF选项,这两个方法将会抛出SocketException例外。必须将size设为正整数,不然setReceiveBufferSize方法将抛出IllegalArgumentException例外。

7.  SO_KEEPALIVE

public boolean getKeepAlive() throws SocketExceptionpublic 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

在服务端控制台的输出结果

服务器已经启动,端口号:1234ABChello world中国

在ClienT类中使用了sendUrgentData方法向服务器发送了字符'A'(65)和'B'(66)。但发送'B'时实际发送的是 322,因为sendUrgentData只发送整型数的低字节。所以,实际发送的是66.十进制整型322的二进制形式如图1所示。

从图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. http://elf8848.iteye.com/blog/1739598

  2. 网络编程之 keepalive http://blog.csdn.net/historyasamirror/article/details/5526486

  3. 在Nettyk,connectionTimeoutMillis选项只能是client使用,server不能使用 http://stackoverflow.com/questions/3234433/what-is-the-meaning-of-child-connecttimeoutmillis-in-nettys-configuration

  4. Netty中ReadTimeoutException的写法:http://stackoverflow.com/questions/3726696/setting-socket-timeout-on-netty-channel

相关文章
相关标签/搜索