zoukankan      html  css  js  c++  java
  • JAVA I/O(四)网络Socket和ServerSocket

     《Thinking in Enterprise Java》中第一章描述了用Socket和Channel的网络编程,核心即为Socket和Channel,本文简单讲述Socket的应用。

    Socket可以认为是两个互联机器终端应用软件的抽象,即对于一个网络连接,两端都有一个Socket,应用可以通过套接字进行交互通信。

    在Java中,创建Socket连接另一台机器,可以从Socket中获取InputStream和OutputStream,将其作为输入输出流,使应用程序与操作本地文件IO类似。存在2个基于流的Socket类:ServerSocket和Socket。

    • ServerSocket用于服务器端,监听客户端连接
    • Socket用于客户端与服务端交互
    • 服务段accept()方法处于阻塞状态,直到有客户端连接,创建一个服务端Socket,与客户端交互

    另外,当创建ServerSocket时,只需要提供一个端口号,IP信息为本机默认信息;创建Socket时,必须提供IP和端口号;由ServerSocket.accept( )创建的不需要,其已包含所有信息。

    1. 简单客户端和服务端

    服务器端:

    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class JabberServer {
    
        public static final int PORT = 8080;
        public static void main(String[] args) throws IOException{
    
            ServerSocket server = new ServerSocket(PORT);
            System.out.println("开始: " + server);
            try {
                Socket socket = server.accept();
                System.out.println("Connection socket: " + socket);
                try {
                    BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    //Output is automatically flushed by PrintWrite
                    PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
                    while(true) {
                        String str = in.readLine();
                        if("END".equals(str))
                            break;
                        System.out.println("Echoing: " + str);
                        out.println(str);
                    }
                } finally {
                    System.out.println("CLosing....");
                    socket.close();
                }
            } finally {
                server.close();
            }
        }
    }

    输出:

    开始: ServerSocket[addr=0.0.0.0/0.0.0.0,localport=8080]

    大致步骤:

    • 创建ServerSocket,绑定端口8080
    • 调accept()方法监听连接,并返回套接字Socket
    • 获取输入流,并通过InputStreamReader转为字符,缓存在BufferdReader中
    • 获取输出流,通过OutputStreamWriter将BufferedWriter中的字符转换为字节,并通过PrintWriter格式化输出,同时自动flush
    • 根据输入流读取的字符,如果是END则结束会话
    • 关闭套接字和ServerSocket

    客户端:

    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.io.PrintWriter;
    import java.net.InetAddress;
    import java.net.Socket;
    
    /**
     * 根据服务器ip和端口/服务器地址等,创建Socket
     * Socket可以获取输入和输出流,默认是使用AbstractPlainSocketImpl类中的SocketInputStream和SocketOutputStream
     *
     */
    public class JabberClient {
    
        public static void main(String[] args) throws Exception{
    
            //服务器端信息,address和8080;后台连接服务器,还会绑定客户端
            InetAddress address = InetAddress.getByName(null);
            System.out.println("address = " + address);
            Socket socket = new Socket(address, 8080);
            
            try {
                System.out.println("Socket = " +socket);
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                
                PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
                
                for(int i = 0; i < 10; i++) {
                    out.println("hello " + i);
                    String str = in.readLine();
                    System.out.println(str);
                }
                out.println("END");
            } finally {
                System.out.println("Closing socket...");
                socket.close();
            }
        }
    }

     大致步骤与服务端相似,不同之处在于创建客户端套接字,需要指定服务端地址和端口号。

    服务器端输出:

    Connection socket: Socket[addr=/127.0.0.1,port=35702,localport=8080]
    Echoing: hello 0
    Echoing: hello 1
    Echoing: hello 2
    Echoing: hello 3
    Echoing: hello 4
    Echoing: hello 5
    Echoing: hello 6
    Echoing: hello 7
    Echoing: hello 8
    Echoing: hello 9
    CLosing....

    客户端输出:

    address = localhost/127.0.0.1
    Socket = Socket[addr=localhost/127.0.0.1,port=8080,localport=35702]
    hello 0
    hello 1
    hello 2
    hello 3
    hello 4
    hello 5
    hello 6
    hello 7
    hello 8
    hello 9
    Closing socket...

     2. 源码分析

    无论是创建ServerSocket还是Socket,都需要与底层实现SocketImpl关联,以实现具体网络交互的逻辑。

    (1)服务端创建ServerSocket

    ServerSocket server = new ServerSocket(PORT);

     具体构造过程如下,构造参数为服务端监听端口:

     /**
         * Creates a server socket, bound to the specified port. A port number
         * of {@code 0} means that the port number is automatically
         * allocated, typically from an ephemeral port range. This port
         * number can then be retrieved by calling {@link #getLocalPort getLocalPort}.
         * <p>
         * The maximum queue length for incoming connection indications (a
         * request to connect) is set to {@code 50}. If a connection
         * indication arrives when the queue is full, the connection is refused.
         * <p>
         * If the application has specified a server socket factory, that
         * factory's {@code createSocketImpl} method is called to create
         * the actual socket implementation. Otherwise a "plain" socket is created.
         * <p>
         * If there is a security manager,
         * its {@code checkListen} method is called
         * with the {@code port} argument
         * as its argument to ensure the operation is allowed.
         * This could result in a SecurityException.
         *
         *
         * @param      port  the port number, or {@code 0} to use a port
         *                   number that is automatically allocated.
         *
         * @exception  IOException  if an I/O error occurs when opening the socket.
         * @exception  SecurityException
         * if a security manager exists and its {@code checkListen}
         * method doesn't allow the operation.
         * @exception  IllegalArgumentException if the port parameter is outside
         *             the specified range of valid port values, which is between
         *             0 and 65535, inclusive.
         *
         * @see        java.net.SocketImpl
         * @see        java.net.SocketImplFactory#createSocketImpl()
         * @see        java.net.ServerSocket#setSocketFactory(java.net.SocketImplFactory)
         * @see        SecurityManager#checkListen
         */
        public ServerSocket(int port) throws IOException {
            this(port, 50, null);
        }
    
    /**
         * Create a server with the specified port, listen backlog, and
         * local IP address to bind to.  The <i>bindAddr</i> argument
         * can be used on a multi-homed host for a ServerSocket that
         * will only accept connect requests to one of its addresses.
         * If <i>bindAddr</i> is null, it will default accepting
         * connections on any/all local addresses.
         * The port must be between 0 and 65535, inclusive.
         * A port number of {@code 0} means that the port number is
         * automatically allocated, typically from an ephemeral port range.
         * This port number can then be retrieved by calling
         * {@link #getLocalPort getLocalPort}.
         *
         * <P>If there is a security manager, this method
         * calls its {@code checkListen} method
         * with the {@code port} argument
         * as its argument to ensure the operation is allowed.
         * This could result in a SecurityException.
         *
         * The {@code backlog} argument is the requested maximum number of
         * pending connections on the socket. Its exact semantics are implementation
         * specific. In particular, an implementation may impose a maximum length
         * or may choose to ignore the parameter altogther. The value provided
         * should be greater than {@code 0}. If it is less than or equal to
         * {@code 0}, then an implementation specific default will be used.
         * <P>
         * @param port  the port number, or {@code 0} to use a port
         *              number that is automatically allocated.
         * @param backlog requested maximum length of the queue of incoming
         *                connections.
         * @param bindAddr the local InetAddress the server will bind to
         *
         * @throws  SecurityException if a security manager exists and
         * its {@code checkListen} method doesn't allow the operation.
         *
         * @throws  IOException if an I/O error occurs when opening the socket.
         * @exception  IllegalArgumentException if the port parameter is outside
         *             the specified range of valid port values, which is between
         *             0 and 65535, inclusive.
         *
         * @see SocketOptions
         * @see SocketImpl
         * @see SecurityManager#checkListen
         * @since   JDK1.1
         */
        public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
            setImpl();
            if (port < 0 || port > 0xFFFF)
                throw new IllegalArgumentException(
                           "Port value out of range: " + port);
            if (backlog < 1)
              backlog = 50;
            try {
                bind(new InetSocketAddress(bindAddr, port), backlog);
            } catch(SecurityException e) {
                close();
                throw e;
            } catch(IOException e) {
                close();
                throw e;
            }
        }

    实际调用的构造方法为ServerSocket(int port, int backlog, InetAddress bindAddr),port表示端口号,backlog表示服务端请求连接队列最大数(默认为50),bindAddr表示服务器要绑定的本地地址(默认为null)。

    • setImpl(),设置系统默认类型SocketImpl,其是服务端和客户端套接字创建、连接、交互等操作的核心
    • bind(new InetSocketAddress(bindAddr, port), backlog),绑定对应的地址和端口,并设置最大连接数(超过连接数,服务器拒绝连接)

    (2)服务端accept

    Socket socket = server.accept();

     服务端会阻塞等待客户端连接,直到有客户端连接,并创建一个服务端Socket,与客户端交互。

    /**
         * 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;
        }

    ① 指定SocketImpl,创建一个非连接的Socket构造方法,如下:

     /**
         * Creates an unconnected Socket with a user-specified
         * SocketImpl.
         * <P>
         * @param impl an instance of a <B>SocketImpl</B>
         * the subclass wishes to use on the Socket.
         *
         * @exception SocketException if there is an error in the underlying protocol,
         * such as a TCP error.
         * @since   JDK1.1
         */
        protected Socket(SocketImpl impl) throws SocketException {
            this.impl = impl;
            if (impl != null) {
                checkOldImpl();
                this.impl.setSocket(this);
            }
        }

     ② implAccept(s)方法

    上一步创建Socket的参数SocketImpl为null,该方法为Socket创建具体的SocketImpl,绑定地址和文件描述符,具体可见源码。

    (3)客户端创建套接字

    Socket socket = new Socket(address, 8080);

    具体创建过程如下,构造参数为InetAddress和port:

    /**
         * Creates a stream socket and connects it to the specified port
         * number at the specified IP address.
         * <p>
         * If the application has specified a socket factory, that factory's
         * {@code createSocketImpl} method is called to create the
         * actual socket implementation. Otherwise a "plain" socket is created.
         * <p>
         * If there is a security manager, its
         * {@code checkConnect} method is called
         * with the host address and {@code port}
         * as its arguments. This could result in a SecurityException.
         *
         * @param      address   the IP address.
         * @param      port      the port number.
         * @exception  IOException  if an I/O error occurs when creating the socket.
         * @exception  SecurityException  if a security manager exists and its
         *             {@code checkConnect} method doesn't allow the operation.
         * @exception  IllegalArgumentException if the port parameter is outside
         *             the specified range of valid port values, which is between
         *             0 and 65535, inclusive.
         * @exception  NullPointerException if {@code address} is null.
         * @see        java.net.Socket#setSocketImplFactory(java.net.SocketImplFactory)
         * @see        java.net.SocketImpl
         * @see        java.net.SocketImplFactory#createSocketImpl()
         * @see        SecurityManager#checkConnect
         */
        public Socket(InetAddress address, int port) throws IOException {
            this(address != null ? new InetSocketAddress(address, port) : null,
                 (SocketAddress) null, true);
        }
    
    private Socket(SocketAddress address, SocketAddress localAddr,
                       boolean stream) throws IOException {
            setImpl();
    
            // backward compatibility
            if (address == null)
                throw new NullPointerException();
    
            try {
                createImpl(stream);
                if (localAddr != null)
                    bind(localAddr);
                connect(address);
            } catch (IOException | IllegalArgumentException | SecurityException e) {
                try {
                    close();
                } catch (IOException ce) {
                    e.addSuppressed(ce);
                }
                throw e;
            }
        }
    • 首先,setImpl(),与服务端相似,设置系统默认类型SocketImpl,其是服务端和客户端套接字创建、连接、交互等操作的核心
    • 其次,createImpl(stream),根据stream布尔值创建socket实现,true时创建基于流的socket(或者面向连接),false时创建无连接UDP套接字
    • 最后,connect(address),连接服务器,连接一直处于阻塞状态,直到连接成功,或者超时或报错等

    (4)SocketImpl类

    SocketImpl类是服务器和客户端连接的核心,源码如下,包含Socket、ServerSocket、文件描述符、IP地址、端口号和套接字连接的本地端口号:

    /**
     * The abstract class {@code SocketImpl} is a common superclass
     * of all classes that actually implement sockets. It is used to
     * create both client and server sockets.
     * <p>
     * A "plain" socket implements these methods exactly as
     * described, without attempting to go through a firewall or proxy.
     *
     * @author  unascribed
     * @since   JDK1.0
     */
    public abstract class SocketImpl implements SocketOptions {
        /**
         * The actual Socket object.
         */
        Socket socket = null;
        ServerSocket serverSocket = null;
    
        /**
         * The file descriptor object for this socket.
         */
        protected FileDescriptor fd;
    
        /**
         * The IP address of the remote end of this socket.
         */
        protected InetAddress address;
    
        /**
         * The port number on the remote host to which this socket is connected.
         */
        protected int port;
    
        /**
         * The local port number to which this socket is connected.
         */
        protected int localport;

     类结构如下:

    (5)套接字输入输出流

    SocketImpl默认子类是AbstractPlainSocketImpl,大部分套接字的创建、连接等操作都通过该类进行。套接字通过获取输入输出流,使应用可以像操作本地I/O流一样,操作网络数据。

    Socket获取输入输出流的方法是getInputStream()和getOutputStream(),底层调AbstractPlainSocketImpl的方法获取,实际流对象为SocketInputStream和SocketOutputStream,具体细节此处不阐述。

    socket.getInputStream()//获取输入流
    socket.getOutputStream()//获取输出流

    AbstractPlainSocketImpl类中包含2个套接字输入输出流属性,如下:

     private SocketInputStream socketInputStream = null;
     private SocketOutputStream socketOutputStream = null;

     他们分别继承自FileInputStream和FileOutputStream,以表示输入输出源。并且他们都不是public类型的,一般不会直接使用。

    class SocketInputStream extends FileInputStream
    class SocketOutputStream extends FileOutputStream 

    3. 总结

    1. 网络连接的核心是套接字Socket,服务端ServerSocket监听连接,创建套接字;客户端创建套接字,绑定服务端ip和端口

    2. SocketImpl类和子类AbstractPlainSocketImpl是服务端和客户端套接字创建、连接、交互的核心

    3. 通过套接字获取输入输出流,应用程序可以与本地I/O流操作一样

  • 相关阅读:
    phpStudy for Linux (lnmp+lamp一键安装包)
    eq,neq,gt,lt等表达式缩写
    lnmp环境的使用
    lnmp环境的搭建
    箭头函数中的this
    Vue中实现路由懒加载及组件懒加载
    Vue项目中实现路由按需加载(路由懒加载)的3中方式:
    判断数据类型的方式以及各自的优缺点
    最近工作中踩的坑
    7种方法实现CSS左侧固定,右侧自适应布局
  • 原文地址:https://www.cnblogs.com/shuimuzhushui/p/10322995.html
Copyright © 2011-2022 走看看