使用相同的TCP端口进行接受和连接。

6

在评论中指出正确使用SO_REUSEADDR后进行编辑

我想在Java中同时使用相同的端口进行入站和出站连接。

目的是在分布式环境中创建一个节点。但在Tcp中,我需要使用两个不同的端口来接受和发起连接。

// accept incoming connection on one port
ServerSocket.accept()
// connect to remote, the port used will be different from the one used for accepting
Socket.connect()

现在问题是:
- A监听端口a,B监听端口b,C监听端口c。 - 当A连接B(使用Socket.connect()),A和B将保持套接字打开以进行未来的消息传递。 - B仍然不知道A正在侦听的端口,因为从B接收连接的端口与a不同。 - 当C连接B时,B将A的套接字地址提供给C,但该端口是由一个没有accept()方法的Socket()实例绑定的。
当然,A可以告诉B它正在监听的端口,但难道没有直接的方法吗?
如何使此测试通过?
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DualSocketTest {
    ExecutorService service= Executors.newFixedThreadPool(10);
    int echoServerport=8080;
    int localServerport=8090;

    @Test
    public void testConnectivity() throws IOException {
        // create a echo server on  port 8080
        startEcho();

        // create a local Server instance
        ServerSocket localServer=new ServerSocket();

        // set the reuseAddress to true
        localServer.setReuseAddress(true);

        // bind the serverSocket
        localServer.bind(new InetSocketAddress(localServerport));

        // create a socket to connect the echo server using the same port used by localServer
        Socket socket = new Socket();
        socket.setReuseAddress(true);
        // but this will throw SocketBindException
        socket.bind(new InetSocketAddress(localServerport));
        socket.connect(new InetSocketAddress(echoServerport));

        // write hello
        socket.getOutputStream().write("Hello !".getBytes());
        byte[] result=new byte[100];

        // receive hello
        String ans=new String(result,0,socket.getInputStream().read(result));
        System.out.println("Server replied with : "+ans);

        // what was written and what was received must be same.
        assert(ans.equals("Hello !"));

    }
    // start a echo server listening on the specified port
    private void startEcho() throws IOException {
        ServerSocket echoServer=new ServerSocket(echoServerport);
        service.submit(()->{
            try {
                while(!echoServer.isClosed()) {
                    Socket socket = echoServer.accept();
                    System.out.println("connected with :" + socket.getInetAddress().toString() + ":" + socket.getPort());

                    InputStream inputStream = socket.getInputStream();
                    OutputStream outputStream = socket.getOutputStream();

                    service.submit(() -> {
                        while (socket.isConnected()) {
                            try {
                                outputStream.write(inputStream.read());
                            } catch (IOException e) {
                                break;
                            }
                        }
                        System.out.println("The Client has closed connection.");
                    });
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        Thread.yield();

    }
// Write something to the socket.
}

之前使用udp时没有这样的问题。同一个socket支持receive()send()方法。对于udp,共享地址很容易。

  • 当A连接B时,B会保存A的socketAddress
  • 当C连接B时,B会将A的地址发送给C,C会连接到A

但是A无法接受使用与B通信的相同套接字的新连接。你在说什么?如果A正在侦听端口a,那么BC都可以通过端口a连接到A - Kayaman
@Kayaman,我已经编辑了问题。问题是B看到的端口与A正在侦听的端口不同。 - Sunita Khanal
你似乎让这件事情变得不必要地困难了。要么使用标准端口来监听套接字,这样就不需要传递该信息,要么将IP/端口组合传递给新节点。 - Kayaman
我想在Java中使用相同的端口进行入站和出站连接。为什么? - user207421
我很难理解你试图实现什么。你能解释一下为什么不直接让所有机器都监听同一个端口号吗?这样任何一台机器都可以只知道第二台机器的IP地址就能连接到它。这是标准的TCP模型:一方(服务器)有一个静态和众所周知的端口号,客户端每个连接获取一个临时端口号。服务器端的众所周知端口允许两个端建立连接。没有必要关心特定的客户端端口号是什么。 - Gil Hamilton
显示剩余4条评论
2个回答

0

我已经修复了你的测试 - 希望它现在符合你的要求。请查看这里的代码:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Test;

public class DualSocketTest {

    ExecutorService service = Executors.newFixedThreadPool(10);
    int echoServerport = 8080;
    int localServerport = 8080;

    @Test
    public void testConnectivity() throws IOException {
        // create a echo server on  port 8080
        startEcho();

        // create a socket to connect the echo server using the same port used by localServer
        Socket socket = new Socket();
        // but this will throw SocketBindException
        socket.connect(new InetSocketAddress(echoServerport));

        // write hello
        socket.getOutputStream().write("Hello !".getBytes());
        socket.getOutputStream().flush();
        byte[] result = new byte[100];

        // receive hello
        String ans = new String(result, 0, socket.getInputStream().read(result));
        System.out.println("Server replied with : " + ans);

        // what was written and what was received must be same.
        assert (ans.equals("Hello !"));

    }

    // start a echo server listening on the specified port
    private void startEcho() throws IOException {
        ServerSocket echoServer = new ServerSocket(echoServerport);
        service.submit(() -> {
            try {
                while (!echoServer.isClosed()) {
                    Socket socket = echoServer.accept();
                    System.out.println("connected with :" + socket.getInetAddress().toString() + ":" + socket.getPort());

                    InputStream inputStream = socket.getInputStream();
                    OutputStream outputStream = socket.getOutputStream();

                    service.submit(() -> {
                        while (socket.isConnected()) {
                            try {
                                byte[] buffer = new byte[1024];
                                int read = -1;
                                while ((read = inputStream.read(buffer)) != -1) {
                                    outputStream.write(buffer, 0, read);
                                }
                            } catch (IOException e) {
                                break;
                            }
                        }
                        System.out.println("The Client has closed connection.");
                    });
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        Thread.yield();

    }
// Write something to the socket.
}

你的测试运行良好,但它没有达到我想要实现的目标。我想要做的是创建连接套接字,这些套接字与“ServerSocket”共享同一个端口。 - Sunita Khanal
你能告诉我原因吗?使用案例是什么?我认为你可以使用这个代码片段实现任何类型的“节点”架构。 - Halko Karr-Sajtarevic

0

编辑:在Java中不起作用

在绑定之前,应该在套接字上设置SO_REUSEADDR选项。我最初在Python中进行了测试(无法访问Java环境),以下脚本在Windows 10系统上运行时没有出现错误:

import socket

serv = socket.socket()          # set a listening socket
serv.bind(('0.0.0.0',8080))
serv.listen(5)

s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0',8080))
s.connect(('127.0.0.1', 8090))

与另一个进程监听端口8090

不幸的是,在Java中,setReuseAddr javadoc明确指出(强调我的):

在使用bind(SocketAddress)绑定套接字之前启用SO_REUSEADDR允许绑定套接字,即使先前的连接处于超时状态

由于我无法猜测原因,Java在这里更加严格。更奇怪的是,根据这个question,在旧版本的JRE(高达JRE 7U5)中曾经允许它。


以下是原始(错误)帖子:

诀窍是在绑定之前设置SO_REUSEADDR选项。这意味着您需要对ServerSocketSocket都使用无参数构造函数。大致如下:

ServerSocket localServer = new ServerSocket();
localServer.setReuseAddress(true);
localServer.bind(InetSocketAddress(localServerport));
...      // Ok listening...

Socket socket = new Socket();
socket.setReuseAddress(true);
socket.bind(InetSocketAddress(localServerport));
socket.connect(...);

这样,您可以从本地侦听端口连接,以便对等方知道如何在连接关闭后重新连接。

注意:未经测试...


1
由于你实际上并没有尝试过这个,所以发帖的意义是什么? - user207421
我进行了测试,并在 socket.bind() 调用时收到了异常 BindException: Address already in use - Sunita Khanal
@EJP:我可以在Python中测试它是否有效,所以我知道必须在绑定之前调用setReuseAddress。但是我目前没有可用的Java环境。只是希望它能有所帮助... - Serge Ballesta
@Serge Ballesta,您的意思是我想要实现的在Python中是可行的吗?如果您能提供Python代码,那会很有帮助。 - Sunita Khanal
@SunitaKhanal:不幸的是,这是Java中已知的问题。 - Serge Ballesta
1
我认为这与Java vs Python无关。这种使用SO_REUSEADDR将两个套接字绑定到同一个(活动的)端口的方式是特定于Windows的。它在任何其他系统上都不起作用。(而且为什么在Windows上以这种方式实现它是个谜。这是一个愚蠢的想法,具有负面的安全影响,并导致需要第二个标志——SO_EXCLUSIVEADDRUSE——来解决它。我怀疑最初的Winsock实现者没有理解SO_REUSEADDR的意义,它的目的是允许绑定到处于“TIME_WAIT”状态的端口。) - Gil Hamilton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接