Java TCP/IP协议的Socket如何设置端口复用?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java TCP/IP协议的Socket如何设置端口复用?相关的知识,希望对你有一定的参考价值。

情境如下:
1). 使用new Socket(ServerAddress, ServerPort, ClientAddress, ClientPort);绑定端口建立连接后,
Client端发送“end”信息至Server端,Client端的Socket被close();掉。
2). Server端接收到“end”信息后ServerSocket被close();掉。
3). 接着我使用Client端重新连接Server端,结果报错:
java.net.BindException: bind failed: EADDRINUSE (Address already in use)
貌似是Client端的Socket被close掉后,端口并没有被及时释放,但是过一段时间又可以建立连接。

现在我想要解决的问题是,如何在Client端的Socket被close掉后能够及时释放端口供我使用呢?

你的其中一端的连接没有被及时释放掉的原因是:你没有顺利地进行TCP连接关闭的流程。最近我就因为这个问题头疼了好久,现在终于找到真正的原因和解决办法了!关键点是:在调用close之前先发送一次数据(例如,out.write(0);)。接下来我用通俗的语言来阐述原因。

如果你两端的程序都是在传送完数据后直接调用Socket的close方法断开连接的话,这就会有一个调用close方法谁先谁后的问题,先调用者是主动关闭TCP连接的那一方,而后调用者就是被动关闭TCP连接的那一方。

关闭TCP连接的过程中,双方总共有3个报文需要发送,主动关闭方只需发送1个报文,就是断开请求的报文,被动关闭方要发送2个报文,分别是 回应主动关闭方发来的断开请求报文的第一次确认报文 和 第二次确认报文,第二次的确认报文用来确认已经没有数据需要发送了。

主动关闭方发送了断开请求报文后,就进入了等待被动关闭方发送确认报文的状态,一般情况下,被动关闭方马上就会发送第一次确认报文。主动关闭方收到第一次确认报文后,就会等被动关闭方的第二次确认报文,直到超时,套接字资源被操作系统回收(P.S. 虽然书上不是这么说的,但是实际的OS就是这么处理的,原因下面再说)。此时如果被动关闭方发送了第二次确认报文,整个TCP连接关闭流程就顺利结束了,所有资源就会被OS回收。

重点来了,问题在于,作为被动关闭方的程序中,在主动关闭方等待被动关闭方发送第二次确认报文时,就算执行了Socket的close也不会发送第二次确认报文,只有向主动关闭方发送数据(任意)后,被动关闭方才会发送第二次确认报文,整个流程才能顺利进行,TCP连接的资源才会被释放,下次才能重复使用同个套接字。

我自己实验出来的结果是:在两端的代码中(只有一端发送一端接收),都只使用

out.write(0);
me.close();

进行TCP连接的断开,out是Socket的OutputStream,me是已连接的Socket对象。结果,无论重复多少次运行,都能不间断地顺利运行,每次运行结束后,用资源监视器看TCP连接情况,都看到所使用的连接资源都已经被释放,并没有等待2MSL时间后才被释放,所以实际的OS的处理跟理论上的不一样。所以你的程序要等一段时间后才能再次使用同个端口并不是因为存在2MSL的等待时间,而是因为TCP连接关闭流程没有顺利进行,但是所使用的进程已经退出,OS就会自动帮你回收资源,不过需要等超时后才处理。(P.S. 我测试的OS是Windows)

如果两端的程序断开TCP连接时都是直接开始断开的过程而没有延时的话,谁是主动关闭方就不确定,两端的程序中的断开TCP连接的代码就得有发送数据的部分,如果其中有一方延时了,那延时的一方就有很大的概率是被动关闭方,另一方在调用close前就不需要发送数据了。

还有,就算TCP连接关闭的流程顺利进行,但是,下次使用同个套接字(两端套接字跟之前的相同)前得延时一下,几百毫秒就够了(具体自己调整),因为OS回收套接字资源是需要时间的,关闭TCP连接后就立马创建两端套接字跟所关闭的连接的两端套接字相同的连接也有可能因为仍然被占用而报错。

追问

给你点+32赞!两年前的问题,你还很认真的回答并写了这么多字,有点小感动。谢谢啦!

追答

谢谢!那时候在网上到处都找不到满意的回答,所以我想把我那时候悟出来的方法和道理都记录下来让别人知道真相,于是就顺便写在相关问题的回答上了,写的时候还是很激动的。。。我是在刚刚实验成功后写的,就当作是我的实验体会吧!

参考技术A 服务端的原因,检查你的服务端代码是不是有什么限制 参考技术B 释放net试试追问

什么意思,怎么释放?我这个没用到web,客户端是android应用。

追答

做一个假设的例程 可能要套用代码库 我记得有一个是可以直接把服务端送到客户入口的代码 你可以去看一下

本回答被提问者采纳

Java TCP/IP SocketUDP Socket(含代码)

UDP的Java支持

    UDP协议提供的服务不同于TCP协议的端到端服务,它是面向非连接的,属不可靠协议,UDP套接字在使用前不需要进行连接。实际上,UDP协议只实现了两个功能:

    1)在IP协议的基础上添加了端口;

    2)对传输过程中可能产生的数据错误进行了检测,并抛弃已经损坏的数据。

 

    Java通过DatagramPacket类和DatagramSocket类来使用UDP套接字,客户端和服务器端都通过DatagramSocket的send()方法和receive()方法来发送和接收数据,用DatagramPacket来包装需要发送或者接收到的数据。发送信息时,Java创建一个包含待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket实例的send()方法;接收信息时,Java程序首先创建一个DatagramPacket实例,该实例预先分配了一些空间,并将接收到的信息存放在该空间中,然后把该实例作为参数传递给DatagramSocket实例的receive()方法。在创建DatagramPacket实例时,要注意:如果该实例用来包装待接收的数据,则不指定数据来源的远程主机和端口,只需指定一个缓存数据的byte数组即可(在调用receive()方法接收到数据后,源地址和端口等信息会自动包含在DatagramPacket实例中),而如果该实例用来包装待发送的数据,则要指定要发送到的目的主机和端口。

 

 

UDP的通信建立的步骤

 

    UDP客户端首先向被动等待联系的服务器发送一个数据报文。一个典型的UDP客户端要经过下面三步操作:

    1、创建一个DatagramSocket实例,可以有选择地对本地地址和端口号进行设置,如果设置了端口号,则客户端会在该端口号上监听从服务器端发送来的数据;

    2、使用DatagramSocket实例的send()和receive()方法来发送和接收DatagramPacket实例,进行通信;

    3、通信完成后,调用DatagramSocket实例的close()方法来关闭该套接字。

 

   由于UDP是无连接的,因此UDP服务端不需要等待客户端的请求以建立连接。另外,UDP服务器为所有通信使用同一套接字,这点与TCP服务器不同,TCP服务器则为每个成功返回的accept()方法创建一个新的套接字。一个典型的UDP服务端要经过下面三步操作:

    1、创建一个DatagramSocket实例,指定本地端口号,并可以有选择地指定本地地址,此时,服务器已经准备好从任何客户端接收数据报文;

    2、使用DatagramSocket实例的receive()方法接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了客户端的地址,这样就知道了回复信息应该发送到什么地方;

    3、使用DatagramSocket实例的send()方法向服务器端返回DatagramPacket实例。

 

UDP Socket Demo

 

     这里有一点需要注意:

UDP程序在receive()方法处阻塞,直到收到一个数据报文或等待超时。由于UDP协议是不可靠协议,如果数据报在传输过程中发生丢失,那么程序将会一直阻塞在receive()方法处,这样客户端将永远都接收不到服务器端发送回来的数据,但是又没有任何提示。为了避免这个问题,我们在客户端使用DatagramSocket类的setSoTimeout()方法来制定receive()方法的最长阻塞时间,并指定重发数据报的次数,如果每次阻塞都超时,并且重发次数达到了设置的上限,则关闭客户端。

下面给出一个客户端服务端UDP通信的Demo(没有用多线程),该客户端在本地9000端口监听接收到的数据,并将字符串"Hello UDPserver"发送到本地服务器的3000端口,服务端在本地3000端口监听接收到的数据,如果接收到数据,则返回字符串"Hello UDPclient"到该客户端的9000端口。在客户端,由于程序可能会一直阻塞在receive()方法处,因此这里我们在客户端用DatagramSocket实例的setSoTimeout()方法来指定receive()的最长阻塞时间,并设置重发数据的次数,如果最终依然没有接收到从服务端发送回来的数据,我们就关闭客户端。

 

客户端代码如下:

 

package zyb.org.UDP;  
  
import java.io.IOException;  
import java.io.InterruptedIOException;  
import java.net.DatagramPacket;  
import java.net.DatagramSocket;  
import java.net.InetAddress;  
  
public class UDPClient {  
    private static final int TIMEOUT = 5000;  //设置接收数据的超时时间  
    private static final int MAXNUM = 5;      //设置重发数据的最多次数  
    public static void main(String args[])throws IOException{  
        String str_send = "Hello UDPserver";  
        byte[] buf = new byte[1024];  
        //客户端在9000端口监听接收到的数据  
        DatagramSocket ds = new DatagramSocket(9000);  
        InetAddress loc = InetAddress.getLocalHost();  
        //定义用来发送数据的DatagramPacket实例  
        DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),loc,3000);  
        //定义用来接收数据的DatagramPacket实例  
        DatagramPacket dp_receive = new DatagramPacket(buf, 1024);  
        //数据发向本地3000端口  
        ds.setSoTimeout(TIMEOUT);              //设置接收数据时阻塞的最长时间  
        int tries = 0;                         //重发数据的次数  
        boolean receivedResponse = false;     //是否接收到数据的标志位  
        //直到接收到数据,或者重发次数达到预定值,则退出循环  
        while(!receivedResponse && tries<MAXNUM){  
            //发送数据  
            ds.send(dp_send);  
            try{  
                //接收从服务端发送回来的数据  
                ds.receive(dp_receive);  
                //如果接收到的数据不是来自目标地址,则抛出异常  
                if(!dp_receive.getAddress().equals(loc)){  
                    throw new IOException("Received packet from an umknown source");  
                }  
                //如果接收到数据。则将receivedResponse标志位改为true,从而退出循环  
                receivedResponse = true;  
            }catch(InterruptedIOException e){  
                //如果接收数据时阻塞超时,重发并减少一次重发的次数  
                tries += 1;  
                System.out.println("Time out," + (MAXNUM - tries) + " more tries..." );  
            }  
        }  
        if(receivedResponse){  
            //如果收到数据,则打印出来  
            System.out.println("client received data from server:");  
            String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) +   
                    " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort();  
            System.out.println(str_receive);  
            //由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数,  
            //所以这里要将dp_receive的内部消息长度重新置为1024  
            dp_receive.setLength(1024);     
        }else{  
            //如果重发MAXNUM次数据后,仍未获得服务器发送回来的数据,则打印如下信息  
            System.out.println("No response -- give up.");  
        }  
        ds.close();  
    }    
}   

服务端代码如下:

 

package zyb.org.UDP;  
  
import java.io.IOException;  
import java.net.DatagramPacket;  
import java.net.DatagramSocket;  
  
public class UDPServer {   
    public static void main(String[] args)throws IOException{  
        String str_send = "Hello UDPclient";  
        byte[] buf = new byte[1024];  
        //服务端在3000端口监听接收到的数据  
        DatagramSocket ds = new DatagramSocket(3000);  
        //接收从客户端发送过来的数据  
        DatagramPacket dp_receive = new DatagramPacket(buf, 1024);  
        System.out.println("server is on,waiting for client to send data......");  
        boolean f = true;  
        while(f){  
            //服务器端接收来自客户端的数据  
            ds.receive(dp_receive);  
            System.out.println("server received data from client:");  
            String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) +   
                    " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort();  
            System.out.println(str_receive);  
            //数据发动到客户端的3000端口  
            DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),dp_receive.getAddress(),9000);  
            ds.send(dp_send);  
            //由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数,  
            //所以这里要将dp_receive的内部消息长度重新置为1024  
            dp_receive.setLength(1024);  
        }  
        ds.close();  
    }  
}  

如果服务器端没有运行,则receive()会失败,此时运行结果如下图所示:

技术分享图片

 

    如果服务器端先运行,而客户端还没有运行,则服务端运行结果如下图所示:

技术分享图片

    此时,如果客户端运行,将向服务端发送数据,并接受从服务端发送回来的数据,此时运行结果如下图所示:

技术分享图片

技术分享图片

   

   

几个需要注意的地方

    1、UDP套接字和TCP套接字的一个微小但重要的差别:UDP协议保留了消息的边界信息。

    DatagramSocket的每一次receive()调用最多只能接收调用一次send()方法所发送的数据,而且,不同的receive()方法调用绝对不会返回同一个send()方法所发送的额数据。

当在TCP套接字的输出流上调用write()方法返回后,所有调用者都知道数据已经被复制到一个传输缓存区中,实际上此时数据可能已经被发送,也有可能还没有被传送,而UDP协议没有提供从网络错误中恢复的机制,因此,并不对可能需要重传的数据进行缓存。这就意味着,当send()方法调用返回时,消息已经被发送到了底层的传输信道中。

 

2、UDP数据报文所能负载的最多数据,亦及一次传送的最大数据为65507个字节

当消息从网络中到达后,其所包含的数据被TCP的read()方法或UDP的receive()方法返回前,数据存储在一个先进先出的接收数据队列中。对于已经建立连接的TCP套接字来说,所有已接受但还未传送的字节都看作是一个连续的字节序列。然而,对于UDP套接字来说,接收到的数据可能来自不同的发送者,一个UDP套接字所接受的数据存放在一个消息队列中,每个消息都关联了其源地址信息,每次receive()调用只返回一条消息。如果receive()方法在一个缓存区大小为n的DatagramPacket实例中调用,而接受队里中的第一条消息的长度大于n,则receive()方法只返回这条消息的钱n个字节,超出部分会被自动放弃,而且对接收程序没有任何消息丢失的提示!

出于这个原因,接受者应该提供一个有足够大的缓存空间的DatagramPacket实例,以完整地存放调用receive()方法时应用程序协议所允许的最大长度的消息。一个DatagramPacket实例中所允许传输的最大数据量为65507个字节,也即是UDP数据报文所能负载的最多数据。因此,可以用一个65600字节左右的缓存数组来接受数据。

 

3、DatagramPacket的内部消息长度值在接收数据后会发生改变,变为实际接收到的数据的长度值。

每一个DatagramPacket实例都包含一个内部消息长度值,其初始值为byte缓存数组的长度值,而该实例一旦接受到消息,这个长度值便会变为接收到的消息的实际长度值,这一点可以用DatagramPacket类的getLength()方法来测试。如果一个应用程序使用同一个DatagramPacket实例多次调用receive()方法,每次调用前就必须显式地将其内部消息长度重置为缓存区的实际长度,以免接受的数据发生丢失(见上面客户端代码第53行,服务端代码第29行)。

以上面的程序为例,若在服务端的receiver()后加入如下代码:System.out.println(dp_receive.getLength());则得到的输出结果为:15,即接收到的字符串数据“Hello UDPserver”的长度。

 

4、DatagramPacket的getData()方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和长度信息。

由于DatagramPacket的getData()方法总是返回缓冲数组的原始大小,即刚开始创建缓冲数组时指定的大小,在上面程序中,该长度为1024,因此如果我们要获取接收到的数据,就必须截取getData()方法返回的数组中只含接收到的数据的那一部分。

在Java1.6之后,我们可以使用Arrays.copyOfRange()方法来实现,只需一步便可实现以上功能:

byte[] destbuf = Arrays.copyOfRange(dp_receive.getData(),dp_receive.getOffset(),

dp_receive.getOffset() + dp_receive.getLength());

当然,如果要将接收到的字节数组转换为字符串的话,也可以采用本程序中直接new一个String对象的方法(见上面客户端代码第48行,服务端代码第21行):

new String(dp_receive.getData(),dp_receive.getOffset(),

dp_receive.getOffset() + dp_receive.getLength());

 

以上几个比较重要的知识点,笔者均已做过测试。

 转自:http://blog.csdn.net/ns_code/article/details/14128987

以上是关于Java TCP/IP协议的Socket如何设置端口复用?的主要内容,如果未能解决你的问题,请参考以下文章

loadrunner 怎么测试tcp/ip 协议的GPS,是用socket协议录制吗

TCP/ip 的三次握手 和 socket 是啥关系?

Java TCP/IP Socket构建和解析自定义协议消息(含代码)

Java TCP/IP SocketTCP Socket(含代码)

JAVA基础知识|Socket

TCP-IP协议详解(3) IP/ARP/RIP/BGP协议