Java TCP通訊:Java ServerSocket類和Socket類

2020-07-16 10:04:39
TCP 網路程式是指利用 Socket 編寫的通訊程式。利用 TCP 協定進行通訊的兩個應用程式是有主次之分的,一個是伺服器程式,一個是用戶端程式,兩者的功能和編寫方法不太一樣。其中 ServerSocket 類表示 Socket 伺服器端,Socket 類表示 Socket 用戶端,兩者之間的互動過程如下:
  1. 伺服器端建立一個 ServerSocket(伺服器端通訊端),呼叫 accept() 方法等待用戶端來連線。
  2. 用戶端程式建立一個 Socket,請求與伺服器建立連線。
  3. 伺服器接收客戶的連線請求,同時建立一個新的 Socket 與客戶建立連線,伺服器繼續等待新的請求。

ServerSocket 類

ServerSocket 類是與 Socket 類相對應的用於表示通訊雙方中的伺服器端,用於在伺服器上開一個埠,被動地等待資料(使用 accept() 方法)並建立連線進行資料互動。

伺服器通訊端一次可以與一個通訊端連線,如果多台用戶端同時提出連線請求,伺服器通訊端會將請求連線的用戶端存入佇列中,然後從中取出一個通訊端與伺服器新建的通訊端連線起來。若請求連線大於最大容納數,則多出的連線請求被拒絕;預設的佇列大小是 50。

下面簡單介紹一下 ServerSocket 的構造方法和常用方法。

ServerSocket 的構造方法

ServerSocket 的構造方法如下所示。
  • ServerSocket():無參構造方法。
  • ServerSocket(int port):建立系結到特定埠的伺服器通訊端。
  • ServerSocket(int port,int backlog):使用指定的 backlog 建立伺服器通訊端並將其系結到指定的本地埠。
  • ServerSocket(int port,int backlog,InetAddress bindAddr):使用指定的埠、監聽 backlog 和要係結到原生的 IP 地址建立伺服器。

在上述方法的引數中 port 指的是本地 TCP 埠,backlog 指的是監聽 backlog,bindAddr 指的是要將伺服器系結到的 InetAddress。

建立 ServerSocket 時可能會拋出 IOException 異常,所以要進行異常捕捉。如下所示為使用 8111 埠的 ServerSocket 範例程式碼。
try
{
    ServerSocket serverSocket=new ServerSocket(8111);
}
catch(IOException e)
{
    e.printStackTrace();
}

ServerSocket 的常用方法

ServerSocket 的常用方法如下所示。
  • Server accept():監聽並接收到此通訊端的連線。
  • void bind(SocketAddress endpoint):將 ServerSocket 係結到指定地址(IP 地址和埠號)。
  • void close():關閉此通訊端。
  • InetAddress getInetAddress():返回此伺服器通訊端的本地地址。
  • int getLocalPort():返回此通訊端監聽的埠。
  • SocketAddress getLocalSoclcetAddress():返回此通訊端系結的埠的地址,如果尚未繫結則返回 null。
  • int getReceiveBufferSize():獲取此 ServerSocket 的 SO_RCVBUF 選項的值,該值是從 ServerSocket 接收的通訊端的建議緩衝區大小。

呼叫 accept() 方法會返回一個和用戶端 Socket 物件相連線的 Socket 物件,伺服器端的 Socket 物件使用 getOutputStream() 方法獲得的輸出流將指定用戶端 Socket 物件使用 getInputStream() 方法獲得那個輸入流。同樣,伺服器端的 Socket 物件使用的 getInputStream() 方法獲得的輸入流將指向用戶端 Socket 物件使用的 getOutputStream() 方法獲得的那個輸出流。也就是說,當伺服器向輸出流寫入資訊時,用戶端通過相應的輸入流就能讀取,反之同樣如此,整個過程如圖 1 所示。

圖1 伺服器與用戶端連線示意圖

例 1

了解上面的基礎知識後,下面使用 ServerSocket 類在本機上建立一個使用埠 8888 的伺服器端通訊端,範例程式碼如下所示。
public static void main(String[] args)
{
    try
    {
        //在8888埠建立一個伺服器端通訊端
        ServerSocket serverSocket=new ServerSocket(8888);
        System.out.println("伺服器端Socket建立成功");
        while(true)
        {
            System.out.println("等待用戶端的連線請求");
            //等待用戶端的連線請求
            Socket socket=serverSocket.accept();
            System.out.println("成功建立與用戶端的連線");
        }
    }
    catch(IOException e)
    {
        e.printStackTrace();
    }
}

如上述程式碼所示,在成功建立 8888 埠的伺服器端通訊端之後,如果沒有用戶端的連線請求,則 accept() 方法為空,所以不會輸出“成功建立與用戶端的連線”,執行結果如下所示。
伺服器端S.ocket創違成功
等待用戶端的連線請求

Socket 類

Socket 類表示通訊雙方中的用戶端,用於呼叫遠端機器上的一個埠,主動向伺服器端傳送資料(當連線建立後也能接收資料)。下面簡單介紹一下 Socket 類的構造方法和常用方法。

Socket 的構造方法

Socket 的構造方法如下所示。
  • Socket():無參構造方法。
  • Socket(InetAddress address,int port):建立一個流通訊端並將其連線到指定 IP 地址的指定埠。
  • Soclcet(InetAddress address,int port,InetAddress localAddr,int localPort):建立一個通訊端並將其連線到指定遠端地址上的指定遠端埠。
  • Socket(String host,int port):建立一個流通訊端並將其連線到指定主機上的指定埠。
  • Socket(String host,int port,InetAddress localAddr,int localPort):建立一個通訊端並將其連線到指定遠端地址上的指定遠端埠。Socket 會通過呼叫 bind() 函數來系結提供的本地地址及埠。

在上述方法的引數中,address 指的是遠端地址,port 指的是遠端埠,localAddr 指的是要將通訊端系結到的本地地址,localPort 指的是要將通訊端系結到的本地埠。

Socket 的常用方法

Socket 的常用方法如下所示。
  • void bind(SocketAddress bindpoint):將通訊端系結到本地地址。
  • void close():關閉此通訊端。
  • void connect(SocketAddress endpoint):將此通訊端連線到伺服器。
  • InetAddress getInetAddress():返回通訊端的連線地址。
  • InetAddress getLocalAddress():獲取通訊端系結的本地地址。
  • InputStream getInputStream():返回此通訊端的輸入流。
  • OutputStream getOutputStream():返回此通訊端的輸出流。
  • SocketAddress getLocalSocketAddress():返回此通訊端系結的端點地址,如果尚未繫結則返回 null。
  • SocketAddress getRemoteSocketAddress():返回此通訊端的連線的端點地址,如果尚未連線則返回 null。
  • int getLoacalPort():返回此通訊端系結的本地埠。
  • intgetPort():返回此通訊端連線的遠端埠。

例 2

編寫 TCP 程式,包括一個用戶端和一個伺服器端。要求伺服器端等待接收用戶端傳送的內容,然後將接收到的內容輸出到控制台並做出反饋。

(1) 建立一個類作為用戶端,首先在 main() 方法中定義一個 Socket 物件、一個 OutputStream 物件和一個 InputStream 物件並完成初始化。接著定義伺服器端的 IP 地址和埠號,程式碼如下所示。
public static void main(String[] args)
{
    Socket socket=null;
    OutputStream out=null;
    InputStream in=null;
    String serverIP="127.0.0.1";    //伺服器端 IP 地址
    int port=5000;    //伺服器端埠號
}

(2) 建立與伺服器端的連線並將資料傳送到伺服器端,程式碼如下所示。
socket=new Socket(serverIP,port);    //建立連線
out=socket.getOutputStream();    //傳送資料
out.write("我是用戶端資料 ".getBytes());

(3) 從輸入流中讀出伺服器的反餽資訊並輸出到控制台,程式碼如下所示。
byte[] b=new byte[1024];
in=socket.getInputStream();
int len=in.read(b);
System.out.println(" 伺服器端的反饋為:"+new String(b,0,len));

(4) 關閉輸入/輸出流以及 Socket 物件,程式碼如下所示。
in.close();
out.close();
socket.close();

(5) 建立一個類作為伺服器端,編寫 main() 方法,建立 ServerSocket、Socket、InputStream、OutputStream 以及埠號並初始化,程式碼如下所示。
ServerSocket ServerSocket=null;
Socket socket=null;
InputStream in=null;
OutputStream out=null;
int port=5000;

(6) 開啟伺服器並接收用戶端傳送的資料,程式碼如下所示。
ServerSocket=new ServerSocket(port);    //建立伺服器通訊端
System.out.println("伺服器開啟,等待連線。。。");
socket=ServerSocket.accept();    //獲得連線
//接收用戶端傳送的內容
in=socket.getInputStream();
byte[] b=new byte[1024];
int len=in.read(b);
System.out.println("用戶端傳送的內容為:"+new String(b,0,len));

(7) 使用輸出流物件將資訊反饋給用戶端,程式碼如下所示。
out=socket.getOutputStream();
out.write("我是伺服器端".getBytes());

(8) 關閉輸入/輸出流、Socket 物件以及 ServerSocket 物件,程式碼如下所示。
in.close();
out.close();
ServerSocket.close();
socket.close();

(9) 執行伺服器端程式程式碼,執行結果如下所示。
伺服器開啟,等待連線。。。

(10) 為了使程式的結果更加清晰,在步驟 (2) 的程式碼最後加入一句程式碼“Thread.sleep(1000);”。接著執行用戶端程式程式碼,剛開始會出現如下所示的執行結果。
伺服器開啟,等待連線。。。
用戶端傳送的內容為:我是用戶端資料

緊接著又會出現如下所示的執行結果。
用戶端的反饋為:我是伺服器端

用戶端與伺服器端的簡單通訊

在了解 TCP 通訊中 ServerSocket 類和 Socket 類的簡單應用之後,本節將編寫一個案例實現用戶端向伺服器傳送資訊,伺服器讀取用戶端傳送的資訊,並將讀取的資料寫入到資料流中。

首先來看一下用戶端的程式碼,如下所示:
public class SocketDemo
{
    public static void main(String[] args)
    {
        Socket socket=null;
        PrintWriter out=null;
        BufferedReader in=null;
        String serverIP="127.0.0.1";    //伺服器端ip地址
        int port=5000;    //伺服器端埠號
        try
        {
            socket=new Socket(serverIP,port);
            in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out=new PrintWriter(socket.getOutputStream(),true);
            while(true)
            {
                int number=(int)(Math.random()*10)+1;
                System.out.println("用戶端正在傳送的內容為:"+number);
                out.println(number);
                Thread.sleep(2000);
            }
        }
        catch(IOException | InterruptedException e)
        {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        }
    }
}

如上述程式碼所示,用戶端程式碼主要是使用 Socket 連線 IP 為 127.0.0.1(本機)的 5000 埠。在建立連線之後將隨機生成的數位使用 PrintWriter 類輸出到通訊端。休眠 2 秒後,再次傳送亂數,如此迴圈。

再來看一個伺服器端的程式碼,如下所示:
package ch16;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketDemoServer1
{
    public static void main(String[] args)
    {
        ServerSocket serverSocket=null;
        Socket clientSocket=null;
        BufferedReader in=null;
        int port=5000;
        String str=null;
        try
        {
            serverSocket=new ServerSocket(port);    //建立伺服器通訊端
            System.out.println("伺服器開啟,等待連線。。。");
            clientSocket=serverSocket.accept();// 獲得連結
            //接收用戶端傳送的內容
            in=new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            while(true)
            {
                str=in.readLine();
                System.out.println("用戶端傳送的內容為:"+str);
                Thread.sleep(2000);
            }
        }
        catch(IOException | InterruptedException e)
        {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        }
    }
}

如上述程式碼所示,伺服器端與用戶端程式碼類似,首先使用 ServerSocket 在 IP為127.0.0.1(本機)的 5000 埠建立通訊端監聽。在 accept() 方法接收到用戶端的 Socket 範例之後呼叫 BufferedReader 類的 readLine() 方法,從通訊端中讀取一行作為資料,再將它輸出到控制後休眠 2 秒。

要執行本案例,必須先執行伺服器端程式,然後執行用戶端程式。用戶端每隔 2 秒向伺服器傳送一個數位,如下所示。
用戶端正在傳送的內容為:10
用戶端正在傳送的內容為:5
用戶端正在傳送的內容為:10
用戶端正在傳送的內容為:4
用戶端正在傳送的內容為:3

伺服器端會將用戶端傳送的資料輸出到控制台,如下所示。
伺服器幵啟,等待連線。。。
用戶端傳送的內容為:7
用戶端傳送的內容為:2
用戶端傳送的內容為:10
用戶端傳送的內容為:5
用戶端傳送的內容為:10
......

傳輸物件資料

經過前面的學習,掌握了如何在伺服器開始一個埠監聽通訊端,以及如何在用戶端連線伺服器,傳送簡單的數位。本次案例將實現如何在用戶端傳送一個物件到伺服器端,伺服器如何解析物件中的資料。

例 3

第一步是建立用於儲存資料的類。這裡使用的 User 類是一個普通的類,包含 name 和 password 兩個成員。由於需要序列化這個物件以便在網路上傳輸,所以需要實現 java. io.Serializable 接 P。

User 類的程式碼如下:
package ch16;
public class User implements java.io.Serializable
{
    private String name;
    private String password;
    public User(String name,String password)
    {
        this.name=name;
        this.password=password;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name=name;
    }
    public String getPassword()
    {
        return password;
    }
    public void setPassword(String password)
    {
        this.password=password;
    }
}

接下來編寫伺服器端的程式碼。伺服器的作用是接收用戶端傳送過來的資料,將資料轉換成 User 物件並輸出成員資訊,然後對 User 物件進行修改再輸出給用戶端。

伺服器端 MyServer 類的實現程式碼如下:
package ch16;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer
{
    public static void main(String[] args) throws IOException
    {
        // 監聽10000埠
        ServerSocket server=new ServerSocket(10000);
        while(true)
        {
            //接收用戶端的連線
            Socket socket=server.accept();
            //呼叫用戶端的資料處理方法
            invoke(socket);
        }
    }
    private static void invoke(final Socket socket) throws IOException
    {
        //開啟一個新執行緒
        new Thread(new Runnable()
        {
            public void run()
            {
                //建立輸入流物件
                ObjectInputStream is=null;
                //建立輸出流物件
                ObjectOutputStream os=null;
                try
                {
                    is=new ObjectInputStream(socket.getInputStream());
                    os=new ObjectOutputStream(socket.getOutputStream());
                    //讀取一個物件
                    Object obj = is.readObject();
                    //將物件轉換為User型別
                    User user=(User) obj;
                    //在伺服器端輸出name成員和password成員資訊
                    System.out.println("user: "+user.getName()+"/"+user.getPassword());
                    //修改當前物件的name成員資料
                    user.setName(user.getName()+"_new");
                    //修改當前物件的password物件資料
                    user.setPassword(user.getPassword()+"_new");
                    //將修改後的物件輸出給用戶端
                    os.writeObject(user);
                    os.flush();
                }
                catch(IOException|ClassNotFoundException ex)
                {
                    ex.printStackTrace();
                }
                finally
                {
                    try
                    {
                        //關閉輸入流
                        is.close();
                        //關閉輸出流
                        os.close();
                        //關閉用戶端
                        socket.close();
                    }
                    catch(Exception ex){}
                }
            }
        }).start();
    }
}

如上述程式碼所示,在伺服器端分別使用 ObjectInputStream 和 ObjectOutputStream 來接收和傳送 socket 中的 InputStream 和OutputStream,然後轉換成 User 物件。

用戶端需要連線伺服器,接收伺服器輸出的資料並解析,同時需要建立 User 物件並行給伺服器。用戶端 MyClient 類的實現程式碼如下:
package ch16;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
public class MyClient
{
    public static void main(String[] args) throws Exception
    {
        //迴圈100次
        for(int i=0;i<100;i++)
        {
            //建立用戶端Socket
            Socket socket=null;
            //建立輸入流
            ObjectOutputStream os=null;
            //建立輸出流
            ObjectInputStream is=null
            try
            {
                //連線伺服器
                socket=new Socket("localhost",10000);
                //接收輸出流中的資料
                os=new ObjectOutputStream(socket.getOutputStream());
                //建立一個User物件
                User user=new User("user_"+i,"password_"+i);
                //將User物件寫入輸出流
                os.writeObject(user);
                os.flush();
                //接收輸入流中的資料
                is=new ObjectInputStream(socket.getInputStream());
                //讀取輸入流中的資料
                Object obj=is.readObject();
                //如果資料不空則轉換成User物件,然後輸出成員資訊
                if(obj!=null)
                {
                    user=(User) obj;
                    System.out.println("user: "+user.getName()+"/"+user.getPassword());
                }
            }
            catch(IOException ex)
            {
                ex.printStackTrace();
            }
            finally
            {
                try
                {
                    //關閉輸入流
                    is.close();
                    //關閉輸出流
                    os.close();
                    //關閉用戶端
                    socket.close();
                }
                catch(Exception ex) {}
            }
        }
    }
}

仔細觀察上述程式碼可以發現,用戶端與伺服器端的程式碼類似,同樣使用 ObjectOutputStream 和 ObjectInputStream 來處理資料。

先執行伺服器端程式 MyServer,再執行用戶端程式 MyClient。此時將在用戶端看到下所示的輸出。
user:user_86_nevj/password_86_new
user:user_87_new/password_87_new
user:user_88_new/password_88_new
user:user_89_new/password_89_new
user:user_90_new/password_90_new
user:user_91_new/password_91_new
user:user_92_new/password_92_new
user:user_93_new/password_93_new
user:user_94_new/password_94_new
user:user_95_new/password_95_new
user:user_96_new/password_96_new
user:user_97_new/password_97_new
user:user_98_new/password_98_new
user:user_99_new/password_99_new

伺服器端的輸出如下所示。
user:user_86/password_86
user:user_87/password_87
user:user_88/password_88
user:user_89/password_89
user:user_90/password_90
user:user_91/password_91
user:user_92/password_92
user:user_93/password_93
user:user_94/password_94
user:user_95/password_95
user:user_96/password_96
user:user_97/password_97
user:user_98/password_98
user:user_99/password_99