如何使用TCP/IP開發網路程式
摘要:進行TCP協議網路程式的編寫,關鍵在於ServerSocket套接字的熟練使用,TCP通訊中所有的資訊傳輸都是依託ServerSocket類的輸入輸出流進行的。
本文分享自華為雲社群《Java利用TCP協議實現客戶端與伺服器通訊【附通訊原始碼】》,作者:灰小猿。
TCP協議概念
我們知道TCP是可靠而非安全的網路協議。它可以保證資料在從一端送至另一端的時候可以準確的送達,並且抵達的資料的排列順序和送出時的順序是相同的。因此在進行TCP協議通訊的時候,我們首先應該保證客戶端和伺服器之間的連線通暢。
而TCP協議程式的編寫,仍然是依靠套接字Socket類來實現的,並且利用TCP協議進行通訊的兩個程式之間是有主次之分的,即一個是伺服器的程式,另一個是客戶端的程式。因此兩者的功能和編寫上也略有不同。如下圖是伺服器與客戶端之間進行通訊的示意圖:
以上就是在TCP協議中客戶端與伺服器建立連線的過程示意圖。而在這其中起到關鍵作用的就是伺服器端套接字ServerSocket和客戶端套接字Socket。通過這兩個套接字來建立伺服器和客戶端,從而利用其中的函式進行資料的通訊。
在ServerSocket類中有很多需要注意的地方,接下來大灰狼和大家分享一下ServerSocket類的具體用法:
ServerSocket類
ServerSocket類存在於http://Java.net包中,表示伺服器端的套接字,在使用時需要首先匯入這個類,我們也知道ServerSocket類的主要功能就是通過指定的埠等待來自於網路中客戶端的請求並且進行連線。
值得注意的是:
ServerSocket類的構造方法通常會丟擲IOException異常,具體有以下幾種形式:
- ServerSocket():建立非繫結伺服器套接字
- ServerSocket(inr port):建立繫結到特定埠的伺服器套接字
- ServerSocket(int port, int backlog):利用指定的backlog建立伺服器套接字,並將其繫結到指定的伺服器埠上,
- ServerSocket(int port, int backlog, InetAddress bindAddress):使用指定的埠,偵聽backlog和要繫結到本地的IP地址建立伺服器。這種情況適用於計算機上有多個網絡卡和多個IP地址的情況,使用者可以明確的規定ServerSocket在哪塊網絡卡或哪個IP地址上等待使用者的連線請求。
以下是ServerSocket類中一些常用的方法:
瞭解了ServerSocket類的基本方法之後,就是如何進行客戶端和伺服器進行連線的問題了。
在伺服器端我們可以呼叫ServerSocket類的accpet()方法與請求連線的客戶機建立連線,這時會返回一個和客戶端相連線的Socket物件,這個時候其實已經連線成功了,使用getInetAddress()方法就可以獲取到進行請求的客戶機的IP地址。
對於如何進行客戶端和伺服器端資料的通訊,就要用到資料的輸入流和輸出流了,伺服器端的Socket物件使用getOutputStream()方法獲取到的輸出流,將指向客戶端的Socket物件使用getInputStream()方法獲取到的輸入流。由此就實現在伺服器向客戶端傳送資料的一個過程,同樣的道理,客戶端端的Socket物件使用getOutputStream()方法獲取到的輸出流,將指向伺服器端的Socket物件使用getInputStream()方法獲取到的輸入流。從而實現由客戶端向伺服器傳送資料的過程。
注意:accpet()方法會阻塞執行緒的繼續執行,如果在對應的介面沒有收到客戶端的呼叫,則程式會停留在此處,直到獲取到客戶端的呼叫才會繼續向下執行,但是如果伺服器沒有收到來自客戶端的呼叫請求,並且accpet()方法沒有發生阻塞,那麼通常情況下就是程式出了問題,一般來說可能是使用了一個已經被其他程式佔用了的埠號,導致ServerSocket沒有繫結成功!遇到這種情況可以嘗試更換新的埠號。
瞭解了TCP協議的通訊過程,接下來就是進行TCP通訊程式的書寫啦!
在網路通訊中,如果只要求客戶機向伺服器傳送資訊,不要求伺服器向客戶端反饋資訊的行為稱為“單向通訊”,要求客戶機和伺服器雙方互相通訊的過程稱為“雙向通訊”,雙向通訊只不過是比單向通訊多了一個伺服器向客戶端傳送訊息的過程,
接下來分別是伺服器端和客戶端程式的編寫:
伺服器端程式
package server_1; 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 MyTcp { private ServerSocket server; //設定伺服器套接字 private Socket client; //設定客戶端套接字 //連線客戶端函式 void getServer() { try { server = new ServerSocket(1100); //建立伺服器 埠為1100 System.out.println("伺服器建立成功!正在等待連線......"); client = server.accept(); //呼叫伺服器函式對客戶端進行連線 System.out.println("客戶端連線成功!ip為:" + client.getInetAddress()); //返回客戶端IP getClientMessage(); //呼叫資訊傳輸和接收函式 } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } void getClientMessage() { try { while (true) { InputStream is = client.getInputStream(); //獲取到客戶端的輸入流 byte[] b = new byte[1024]; //定義位元組陣列 int len = is.read(b); //由於資訊的傳輸是以二進位制的形式,所以要以二進位制的形式進行資料的讀取 String data = new String(b, 0,len); System.out.println("客戶端發來訊息:" + data); //定義傳送給客戶端的輸出流 OutputStream put = client.getOutputStream(); String putText = "我已經收到!歡迎你!"; put.write(putText.getBytes()); //將輸出流資訊以二進位制的形式進行寫入 } } catch (Exception e) { // TODO: handle exception } try { //判斷客戶端位元組流不是空,則關閉客戶端 if (server != null) { server.close(); } } catch (Exception e) { // TODO: handle exception } } public static void main(String[] args) { // TODO Auto-generated method stub MyTcp myTcp = new MyTcp(); //呼叫該類生成物件 myTcp.getServer(); //呼叫方法 } }
客戶端程式
package client_1; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; public class MyClient { private Socket client; //定義客戶端套接字 //建立客戶端函式 void getClient() { try { client = new Socket("127.0.0.1", 1100); //建立客戶端,使用的IP為127.0.0.1,埠和伺服器一樣為1100 System.out.println("客戶端建立成功!"); setClientMessage(); //呼叫客戶端資訊寫入函式 } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //定義客戶端資訊寫入函式 void setClientMessage() { try { OutputStream pt = client.getOutputStream(); //建立客戶端資訊輸出流 String printText = "伺服器你好!我是客戶端!"; pt.write(printText.getBytes()); //以二進位制的形式將資訊進行輸出 InputStream input = client.getInputStream(); //建立客戶端資訊輸入流 byte [] b = new byte[1024]; //定義位元組陣列 int len = input.read(b); //讀取接收的二進位制資訊流 String data = new String(b, 0,len); System.out.println("收到伺服器訊息:" + data); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { //如果客戶端資訊流不為空,則說明客戶端已經建立連線,關閉客戶端 if (client != null) { client.close(); } } catch (Exception e) { // TODO: handle exception } } public static void main(String[] args) { // TODO Auto-generated method stub //生成客戶端類物件 MyClient myClient = new MyClient(); myClient.getClient(); } }
同時要注意:在客戶端和伺服器搭建成功之後,應該先開啟伺服器等待連線,再開啟客戶端進行連線,同樣在進行關閉時,應該先關閉客戶端,再關閉伺服器。
以上面程式為例:
開啟伺服器等待客戶端連線
開啟客戶端與伺服器連線成功,並且實現雙向通訊:
注意:當一臺機器上安裝了多個網路應用程式時,很可能指定的埠已經被佔用,甚至還可能遇到之前執行很好的程式突然卡住的情況,這種情況很可能是埠被別的程式佔用了,這時可以執行netstat-help來活的幫助,可以使用命令netstat-an來檢視該程式所使用的埠。