Android程序間通訊 - Socket使用(TCP、UDP)
在使用Socket實現程序間通訊前,先對網路協議相關知識進行簡單回顧。
網路分層
一般情況會將網路分為5層:
- 應用層 常見協議:HTTP、FTP、POP3等
- 傳輸層 常見協議:TCP、UDP
- 網路層 常見協議:IP
- 資料鏈路層
- 物理層
TCP、UDP
- TCP:面向連線的、可靠的流協議,提供可靠的通訊傳輸。
- 所謂流,就是指不間斷的資料結構,你可以把它想象成排水管道中的水流。當應用程式採用TCP傳送訊息時,雖然可以保證傳送的順序,但還是猶如沒有任何間隔的資料流傳送給接收端。
- 有順序控制、丟包重發機制
- UDP:面向無連線的,具有不可靠性的資料報協議。(讓廣播和細節控制交給應用的通訊傳輸)
- 無順序控制、丟包重發機制
TCP用於在傳輸層有必要實現可靠傳輸的情況,由於它是面向連線並具有“順序控制”、重發控制等機制;而UDP則主要用於那些對高速傳輸和實時性有較高要求的通訊或廣播通訊。
因此TCP和UDP應該根據應用的目的按需使用,沒有絕對優缺點。
TCP三次握手與四次揮手
使用TCP協議的連線建立與斷開,正常過程下至少需要傳送7個包才能完成,就是我們常說的三次握手,四次揮手。
標誌位Flags、序號
- 序列號 Sequeuece number(seq): 資料包本身的序列號,初始序列號是隨機的。
- 確認號 Acknowledgment number(ack): 在接收端,用來通知傳送端資料成功接收
- 標誌位,標誌位只有為 1 的時候才有效
- SYN(synchronize):表示在連線建立時用來同步序號。
- ACK:TCP協議規定,只有ACK=1時有效,也規定連線建立後所有傳送的報文的ACK必須為1.
- FIN(finish):用來釋放一個連線。當FIN=1時,表明此報文段的傳送方的資料已經發送完畢,並要求釋放連線。
-
三次握手
三次握手:是指建立一個TCP連線時需要客戶端和服務端總共傳送3個包確認連線的建立。在Socket程式設計中,這一個過程由客戶端執行connect來觸發。
- 第一次握手:客戶端向服務端傳送請求報文;即SYN=1,ACK=0,seq=x。
- 第二次握手:服務端收到客戶端的請求報文,服務端會確認應答,告訴客戶端已經收到請求了;即SYN=1,ACK=1,seq=y,ack=x+1;
- 第三次握手:客戶端收到服務端的確認應答後,再次向服務端進行確認應答,建立完整的連線;即ACK=1,seq=x+1,ack=y+1
為什麼要進行三次握手呢,或兩次確認??
下面使用Wireshark抓包工具體驗下三次握手的過程
紅色框內就是一個TCP建立連線的過程
- 53324 —>80:嘿,哥們,我想訪問你的web資源,能不能把你的80埠開啟
- 80 —> 53324:可以啊,我已經把80埠打開了,為了保證我們的資料能可靠傳輸,你那邊也需要把53324埠開啟;
- 53324 —> 80:沒問題,我已經把53324埠打開了,儘管的傳送資料過來吧。
下面看看在三次握手的標誌位的變化
1、53324 —>80
2、80 —> 53324
3、53324 —> 80
四次揮手
四次揮手:即終止TCP連線,就是斷開一個TCP連線時,需要客戶端和服務端總共傳送4個包以確認連線的斷開。在Socket程式設計中,這一工程由客戶端或服務端任意一方執行close來觸發。
由於 TCP 連線是全雙工的,因此每個方向都必須單獨進行關閉。這一原則是當一方資料傳送完成,傳送一個標誌位為 FIN 的報文來終止這一方向的連線,收到標誌位為 FIN 的報文意味著這一方向上不會再收到資料了。但是在 TCP 連線上仍然能夠傳送資料,直到這一方向也傳送了 FIN 。傳送 FIN 的一方執行主動關閉,另一方則執行被動關閉。
- 第一次揮手:客戶端傳送一個FIN=1,用來關閉客戶端到伺服器端的資料傳送,客戶端進入FIN_WAIT_1狀態。意思是說”我客戶端沒有資料要發給你了”,但是如果你伺服器端還有資料沒有傳送完成,則不必急著關閉連線,可以繼續傳送資料。
- 第二次揮手:伺服器端收到FIN後,先發送ack=u+1,告訴客戶端,你的請求我收到了,但是我還沒準備好,請繼續你等我的訊息。這個時候客戶端就進入FIN_WAIT_2 狀態,繼續等待伺服器端的FIN報文。
- 第三次揮手:當伺服器端確定資料已傳送完成,則向客戶端傳送FIN=1報文,告訴客戶端,好了,我這邊資料發完了,準備好關閉連線了。伺服器端進入LAST_ACK狀態。
- 第四次揮手:客戶端收到FIN=1報文後,就知道可以關閉連線了,但是他還是不相信網路,怕伺服器端不知道要關閉,所以傳送ack=w+1後進入TIME_WAIT狀態,如果Server端沒有收到ACK則可以重傳。伺服器端收到ACK後,就知道可以斷開連線了。客戶端等待了2MSL後依然沒有收到回覆,則證明伺服器端已正常關閉,那好,我客戶端也可以關閉連線了。最終完成了四次握手。
Socket
socket:
- 位於傳輸層,是網路上的兩個程式通過一個
雙向的通訊連線
實現資料的交換的一種程序通訊方式之一。 - 成對出現,一對套接字
Socket socket = new Socket("localhost", 8888);
localhost:IP地址
8888:埠號
即
IP地址 -- 埠號成對出現
socket本質是程式設計介面(API),對TCP/IP的封裝,TCP/IP也要提供了網路開發所用的介面;HTTP是轎車,提供了封裝或顯示資料的具體形式;socket是發動機,提供了網路通訊的能力。
典型的應用就是C/S結構:
從圖可知,socket的使用是基於TCP或UDP協議。
Socket java簡單實現
我們知道socket是基於TCP
或UDP
協議實現的,下面以TCP
協議為例實現,因為TCP
更加常用些。
使用步驟
- 客戶端
- 建立
socket
物件,指定成對的服務端IP地址和埠號 - 通過
socket
獲取輸出流,寫入資料發給服務端 - 通過
socket
獲取輸入流,接受服務端的傳送的資料 - 關閉資源
close
- 建立
- 服務端(與服務端類似)
- 建立
ServerSocket
物件,並指定埠號,其埠號必須與客服端一致 - 通過
ServerSocket
物件,獲取客戶端的socket
例項(ServerSocket.accept方法
) - 通過
socket
獲取輸入流,接受客戶端發來的訊息 - 通過
socket
獲取輸出流,寫入資料向客戶端傳送資料作為迴應 - 關閉資源
close
- 建立
具體例項
客戶端Client
public class Client {
private static final String TAG = "Client";
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
//IO操作不能放在主執行緒執行
connectServer();
}
}).start();
}
private static void connectServer() {
try {
//1、建立客戶端socket,指定服務端地址和埠
Socket socket = new Socket("localhost", 8888);
boolean connected = socket.isConnected(); //檢查客戶端與服務端是否連線成功
System.out.println(connected?"連線成功":"連線失敗,請重試!");
//2、獲取輸出流,向伺服器傳送訊息
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream),true);
writer.write("第一次來到廣州\n");
writer.flush();
//3、獲取輸入流,並讀取服務端的響應資訊
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String info=reader.readLine();
System.out.println("客戶端收到服務端迴應:"+info);
//4、關閉資源
outputStream.close();
writer.close();
inputStream.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服務端Server
public class Server {
private static final String TAG = "Server";
public static void main(String[] args) {
try {
//1、建立ServerSocket物件,指定與客戶端一樣的埠號
ServerSocket serverSocket = new ServerSocket(8888);
//2、獲取Socket例項
final Socket socket = serverSocket.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
//3、獲取輸入流,接受客戶端發來的訊息
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader reader = new BufferedReader(inputStreamReader);
String info=reader.readLine();
System.out.println("服務端收到客戶端的資訊: " +info);
//4、獲取輸出流,向客戶端傳送訊息迴應
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream);
writer.write("羊城歡迎你!"+"\n");
writer.flush();
//4、關閉IO資源
inputStream.close();
reader.close();
outputStream.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:
使用write方法
寫入資料,字串必須在中間或者末尾新增轉義符\r
或者\n
,否則在使用readLine
方法讀取資料時,會一直阻塞從而讀取失敗。如下:
writer.write("第一次來到廣州\n");
執行結果
首先執行服務端,再接著執行客戶端
服務端Server:
客戶端Client
Socket Android使用
既然scoket能夠實現兩個程式間的資訊傳輸,很明顯在Android下是一種IPC方式。
下面以一個簡單跨程序聊天程式為例,功能點能夠自動回覆。
實現流程
- 建立一個遠端Service服務,在其建立TCP服務(
服務端
) - 在介面上(Activity、Fragment等)連線TCP服務(
客戶端
) - 在Mainfest中宣告網路許可權及註冊Service
具體實現
服務端:
public class TCPServerService extends IntentService {
private static final String[] defaultMessages = {
"你好啊,嘻嘻",
"看了你相片,你好帥哦,很喜歡你這樣的",
"我是江西人,你呢?",
"你在哪裡工作?"};
private int index = 0;
private boolean isServiceDestroy = false;
//需注意,必須傳入引數
public TCPServerService() {
super("TCP");
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
try {
//1、監聽本地埠號
ServerSocket serverSocket = new ServerSocket(8954);
Socket socket = serverSocket.accept();
//2、獲取輸入流,接受使用者發來的訊息(Activity)
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
//3、獲取輸出流,向客戶端(Activity)回覆訊息
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));
//4、通過迴圈不斷讀取客戶端發來的訊息 ,併發送
while (!isServiceDestroy) {
String readLine = reader.readLine();
if (!TextUtils.isEmpty(readLine)) {
String sendMag = index < defaultMessages.length ? defaultMessages[index] : "已離線";
SystemClock.sleep(500); //延遲傳送
writer.println(sendMag+"\r"); // `\r或\n`必須要有,否則會影響客戶端接受訊息
writer.flush(); //重新整理流
index++;
}
}
//關閉流
inputStream.close();
reader.close();
outputStream.close();
writer.close();
socket.close();
//需關閉,否則再次連線時,會報埠號已被使用
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onDestroy() {
super.onDestroy();
isServiceDestroy = true;
Log.d(TAG, "onDestroy: ");
}
private static final String TAG = "TCPServerService";
}
客戶端:
public class SocketActivity extends AppCompatActivity {
private TextView mTvChatContent;
private EditText mEtSendContent;
private Intent mIntent;
private static final int CONNECT_SERVER_SUCCESS = 0; //與服務端連線成功
private static final int MESSAGE_RECEIVE_SUCCESS = 1; //接受到服務端的訊息
private static final int MESSAGE_SEND_SUCCESS=2; //訊息傳送
@SuppressLint("all")
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case CONNECT_SERVER_SUCCESS:
//與服務端連線成功
mTvChatContent.setText("與聊天室連線成功\n");
break;
case MESSAGE_RECEIVE_SUCCESS:
String msgContent = mTvChatContent.getText().toString();
mTvChatContent.setText(msgContent+msg.obj.toString()+"\n");
break;
case MESSAGE_SEND_SUCCESS:
mEtSendContent.setText("");
mTvChatContent.setText(mTvChatContent.getText().toString()+msg.obj.toString()+"\n");
break;
}
return false;
}
});
private PrintWriter mPrintWriter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_socket);
mTvChatContent = findViewById(R.id.tv_chat_content);
mEtSendContent = findViewById(R.id.et_send_content);
//啟動服務
mIntent = new Intent(this, TCPServerService.class);
startService(mIntent);
new Thread(new Runnable() {
@Override
public void run() {
//連線服務端,實現通訊互動
//IO操作必須放在子執行緒執行
connectTCPServer();
}
}).start();
}
private Socket mSocket=null;
private void connectTCPServer() {
//通過迴圈來判斷Socket是否有被建立,若沒有則會每隔1s嘗試建立,目的是保證客戶端與服務端能夠連線
while (mSocket == null) {
try {
//建立Socket物件,指定IP地址和埠號
mSocket = new Socket("localhost", 8954);
mPrintWriter = new PrintWriter(new OutputStreamWriter(mSocket.getOutputStream()),true);
if (mSocket.isConnected()) //判斷是否連線成功
mHandler.sendEmptyMessage(CONNECT_SERVER_SUCCESS);
} catch (IOException e) {
e.printStackTrace();
//設計休眠機制,每次重試的間隔時間為1s
SystemClock.sleep(1000);
}
}
//通過迴圈來,不斷的接受服務端發來的訊息
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
while (!SocketActivity.this.isFinishing()){ //當Activity銷燬後將不接受
String msg = reader.readLine();
if (!TextUtils.isEmpty(msg)){
//發訊息通知更新UI
mHandler.obtainMessage(MESSAGE_RECEIVE_SUCCESS,msg).sendToTarget();
}
}
//關閉流
mPrintWriter.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressLint("SetTextI18n")
public void onClick(View view) {
switch (view.getId()) {
case R.id.but_send:
//必須開啟子執行緒,不能在UI執行緒操作網路
new Thread(new Runnable() {
@Override
public void run() {
String msg = mEtSendContent.getText().toString();
if (mPrintWriter!=null && !TextUtils.isEmpty(msg)){
mPrintWriter.println(msg+"\n");
//此處可以不用重新整理流的方法,因為在建立mPrintWriter物件時,在其構造方法中設定了自動重新整理快取
// mPrintWriter.flush();
//通知更新UI
mHandler.obtainMessage(MESSAGE_SEND_SUCCESS,msg).sendToTarget();
}
}
}).start();
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//關閉輸入流和連線
if (mSocket!=null){
try {
mSocket.shutdownInput();
mSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//停止後臺服務
stopService(mIntent);
}
private static final String TAG = "TCPServerService";
}
Mainfest:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<service android:name=".socket.TCPServerService"
android:exported="true"
android:process=":socket"/>
執行結果:
注意事項:
1、在使用IntentService時,必須在重寫其構造方法並指定執行緒名,否則報錯has no zero argument constructor
如下:
public TCPServerService( ) {
super("TCP");
}
2、連線服務端和傳送訊息必須放在子執行緒
中執行,否則會報NetworkOnMainThreadException
3、在writer.println()
或writer.write()
方法中傳入的字串必須要有\r
或\n
轉義符,同時需重新整理流flush()
,否則會影響訊息及時性
4、服務端在停止之前必須關閉ServerSocket
,呼叫close()
即可,否則再次連線時,會報埠號已被使用
錯誤,java.net.BindException: bind failed: EADDRINUSE (Address already in use)
5、客戶端Socket與服務端ServerSocket的埠號port必須一致
6、在Mainfest中必須宣告網路許可權,否則連線失敗,提示沒有許可權socket failed: EACCES (Permission denied)
以上幾點是在開發中容易入坑的地方。
參考
- 安卓開發藝術探索
- https://blog.csdn.net/carson_ho/article/details/53366856
- https://www.jianshu.com/p/9f3e879a4c9c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation
- https://www.cnblogs.com/edisonchou/p/5987827.html
- https://www.cnblogs.com/liyiran/p/9102791.html
- https://blog.csdn.net/oney139/article/details/8103223
- https://baike.baidu.com/item/socket/281150?fr=aladdin