JAVA I/O(四)網絡Socket和ServerSocket

 《Thinking in Enterprise Java》中第一章描述了用Socket和Channel的網絡編程,核心即爲Socket和Channel,本文簡單講述Socket的應用。java

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]

大體步驟:app

  • 建立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();
        }
    }
}

 大體步驟與服務端類似,不一樣之處在於建立客戶端套接字,須要指定服務端地址和端口號。less

服務器端輸出:socket

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....

客戶端輸出:ide

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流操做同樣

相關文章
相關標籤/搜索