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