美文网首页
2020-02-06-Java-Socket通信

2020-02-06-Java-Socket通信

作者: 耿望 | 来源:发表于2020-02-06 16:37 被阅读0次

    Socket是什么

    socket起源于Unix,可以理解成一个文件,可以执行“读,写,打开,关闭”等操作,实际上是对TCP/IP协议的封装,太复杂,这篇简单看下Java层的实现。
    Java的Socket默认是TCP协议,会建立一个稳定的长连接。

    Socket的生命周期

    Socket的构造过程像是工厂方法模式,Socket是一个抽象产品,具体产品是在SocketImpl类实现的。不同工厂可以根据需求实现不一样的具体产品,比如SocksSocketImpl,DualStackPlainSocketImpl等都是SocketImpl的子类。

        /**
         * Sets impl to the system-default type of SocketImpl.
         * @since 1.4
         */
        void setImpl() {
            if (factory != null) {
                impl = factory.createSocketImpl();
                checkOldImpl();
            } else {
                // No need to do a checkOldImpl() here, we know it's an up to date
                // SocketImpl!
                impl = new SocksSocketImpl();
            }
            if (impl != null)
                impl.setSocket(this);
        }
    
    Java Socket通信.png

    1. create()

        /**
         * Creates a socket with a boolean that specifies whether this
         * is a stream socket (true) or an unconnected UDP socket (false).
         */
        protected synchronized void create(boolean stream) throws IOException {
            this.stream = stream;
            if (!stream) {
                ResourceManager.beforeUdpCreate();
                // only create the fd after we know we will be able to create the socket
                fd = new FileDescriptor();
                try {
                    socketCreate(false);
                } catch (IOException ioe) {
                    ResourceManager.afterUdpClose();
                    fd = null;
                    throw ioe;
                }
            } else {
                fd = new FileDescriptor();
                socketCreate(true);
            }
            if (socket != null)
                socket.setCreated();
            if (serverSocket != null)
                serverSocket.setCreated();
        }
    

    create()方法创建文件描述符(FileDescriptor),后续可以对这个socket进行读写操作。
    传入的参数boolean stream用来区分协议,true标识TCP协议,false标识UDP协议。
    文件描述符只能在本机标识这个socket,要进行网络通信,还需要进一步执行bind()。

    2. bind()

        /**
         * Binds the socket to a local address.
         * <P>
         * If the address is {@code null}, then the system will pick up
         * an ephemeral port and a valid local address to bind the socket.
         *
         * @param   bindpoint the {@code SocketAddress} to bind to
         * @throws  IOException if the bind operation fails, or if the socket
         *                     is already bound.
         * @throws  IllegalArgumentException if bindpoint is a
         *          SocketAddress subclass not supported by this socket
         * @throws  SecurityException  if a security manager exists and its
         *          {@code checkListen} method doesn't allow the bind
         *          to the local port.
         *
         * @since   1.4
         * @see #isBound
         */
        public void bind(SocketAddress bindpoint) throws IOException {
            if (isClosed())
                throw new SocketException("Socket is closed");
            if (!oldImpl && isBound())
                throw new SocketException("Already bound");
    
            if (bindpoint != null && (!(bindpoint instanceof InetSocketAddress)))
                throw new IllegalArgumentException("Unsupported address type");
            InetSocketAddress epoint = (InetSocketAddress) bindpoint;
            if (epoint != null && epoint.isUnresolved())
                throw new SocketException("Unresolved address");
            if (epoint == null) {
                epoint = new InetSocketAddress(0);
            }
            InetAddress addr = epoint.getAddress();
            int port = epoint.getPort();
            checkAddress (addr, "bind");
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkListen(port);
            }
            getImpl().bind (addr, port);
            bound = true;
        }
    

    bind()方法传入一个SocketAddress类型,给这个socket绑定一个地址,不考虑协议的情况下,需要IP地址+端口才能唯一标识一个socket。
    在没有指定地址的情况下,系统会暂时分配一个空闲的端口给socket进行连接。
    bind()方法是在Impl类实现的,可以看下socketBind()方法,具体实现应该是在native层。

    3. connect()

    如果是客户端,下一步会执行connet()方法尝试与服务端连接。

        /**
         * Connects this socket to the server with a specified timeout value.
         * A timeout of zero is interpreted as an infinite timeout. The connection
         * will then block until established or an error occurs.
         *
         * @param   endpoint the {@code SocketAddress}
         * @param   timeout  the timeout value to be used in milliseconds.
         * @throws  IOException if an error occurs during the connection
         * @throws  SocketTimeoutException if timeout expires before connecting
         * @throws  java.nio.channels.IllegalBlockingModeException
         *          if this socket has an associated channel,
         *          and the channel is in non-blocking mode
         * @throws  IllegalArgumentException if endpoint is null or is a
         *          SocketAddress subclass not supported by this socket
         * @since 1.4
         * @spec JSR-51
         */
        public void connect(SocketAddress endpoint, int timeout) throws IOException {
            if (endpoint == null)
                throw new IllegalArgumentException("connect: The address can't be null");
    
            if (timeout < 0)
              throw new IllegalArgumentException("connect: timeout can't be negative");
    
            if (isClosed())
                throw new SocketException("Socket is closed");
    
            if (!oldImpl && isConnected())
                throw new SocketException("already connected");
    
            if (!(endpoint instanceof InetSocketAddress))
                throw new IllegalArgumentException("Unsupported address type");
    
            InetSocketAddress epoint = (InetSocketAddress) endpoint;
            InetAddress addr = epoint.getAddress ();
            int port = epoint.getPort();
            checkAddress(addr, "connect");
    
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                if (epoint.isUnresolved())
                    security.checkConnect(epoint.getHostName(), port);
                else
                    security.checkConnect(addr.getHostAddress(), port);
            }
            if (!created)
                createImpl(true);
            if (!oldImpl)
                impl.connect(epoint, timeout);
            else if (timeout == 0) {
                if (epoint.isUnresolved())
                    impl.connect(addr.getHostName(), port);
                else
                    impl.connect(addr, port);
            } else
                throw new UnsupportedOperationException("SocketImpl.connect(addr, timeout)");
            connected = true;
            /*
             * If the socket was not bound before the connect, it is now because
             * the kernel will have picked an ephemeral port & a local address
             */
            bound = true;
        }
    
    

    connect()方法传入两个参数,SocketAddress代表目标地址,timeout代表超时时间,单位是毫秒,默认是0。
    这个地址要跟上一个bind()方法区分,bind传入的是本地要绑定的地址,connect()传入的是要连接的服务端地址。具体实现也是在native层。

    4. listen()

    如果是服务端,下一步是调用listen()方法,进入被动监听,等待连接请求的过程。

        /**
         * Sets the maximum queue length for incoming connection indications
         * (a request to connect) to the {@code count} argument. If a
         * connection indication arrives when the queue is full, the
         * connection is refused.
         *
         * @param      backlog   the maximum length of the queue.
         * @exception  IOException  if an I/O error occurs when creating the queue.
         */
        protected abstract void listen(int backlog) throws IOException;
    

    传入的参数backlog表示请求队列的最大长度。这部分也是native代码,具体可以看下socketListen()方法。
    下一步需要对客户端的请求进行接收和处理。

    5. accept()

    accept()方法对客户端请求进行接收,具体实现在Impl类,具体可以看下socketAccept()里面的native调用。

        /**
         * Listens for a connection to be made to this socket and accepts
         * it. The method blocks until a connection is made.
         *
         * <p>A new Socket {@code s} is created and, if there
         * is a security manager,
         * the security manager's {@code checkAccept} method is called
         * with {@code s.getInetAddress().getHostAddress()} and
         * {@code s.getPort()}
         * as its arguments to ensure the operation is allowed.
         * This could result in a SecurityException.
         *
         * @exception  IOException  if an I/O error occurs when waiting for a
         *               connection.
         * @exception  SecurityException  if a security manager exists and its
         *             {@code checkAccept} method doesn't allow the operation.
         * @exception  SocketTimeoutException if a timeout was previously set with setSoTimeout and
         *             the timeout has been reached.
         * @exception  java.nio.channels.IllegalBlockingModeException
         *             if this socket has an associated channel, the channel is in
         *             non-blocking mode, and there is no connection ready to be
         *             accepted
         *
         * @return the new Socket
         * @see SecurityManager#checkAccept
         * @revised 1.4
         * @spec JSR-51
         */
        public Socket accept() throws IOException {
            if (isClosed())
                throw new SocketException("Socket is closed");
            if (!isBound())
                throw new SocketException("Socket is not bound yet");
            Socket s = new Socket((SocketImpl) null);
            implAccept(s);
            return s;
        }
    

    在没有请求的情况下,线程会阻塞等待,有客户端请求才会唤醒。

    6. close()

    当发生异常或者通信结束,调用close()方法关闭socket。

    protected void close() throws IOException {
            synchronized(fdLock) {
                if (fd != null) {
                    if (!stream) {
                        ResourceManager.afterUdpClose();
                    }
                    if (fdUseCount == 0) {
                        if (closePending) {
                            return;
                        }
                        closePending = true;
                        /*
                         * We close the FileDescriptor in two-steps - first the
                         * "pre-close" which closes the socket but doesn't
                         * release the underlying file descriptor. This operation
                         * may be lengthy due to untransmitted data and a long
                         * linger interval. Once the pre-close is done we do the
                         * actual socket to release the fd.
                         */
                        try {
                            socketPreClose();
                        } finally {
                            socketClose();
                        }
                        fd = null;
                        return;
                    } else {
                        /*
                         * If a thread has acquired the fd and a close
                         * isn't pending then use a deferred close.
                         * Also decrement fdUseCount to signal the last
                         * thread that releases the fd to close it.
                         */
                        if (!closePending) {
                            closePending = true;
                            fdUseCount--;
                            socketPreClose();
                        }
                    }
                }
            }
        }
    

    相关文章

      网友评论

          本文标题:2020-02-06-Java-Socket通信

          本文链接:https://www.haomeiwen.com/subject/qrryxhtx.html