Android:這是一份很詳細的Socket使用攻略
前言
Socket
的使用在Android
網路程式設計中非常重要- 今天我將帶大家全面瞭解
Socket
及 其使用方法
目錄
1.網路基礎
1.1 計算機網路分層
計算機網路分為五層:物理層、資料鏈路層、網路層、運輸層、應用層
其中:
- 網路層:負責根據IP找到目的地址的主機
- 運輸層:通過埠把資料傳到目的主機的目的程序,來實現程序與程序之間的通訊
1.2 埠號(PORT)
埠號規定為16位,即允許一個IP主機有2的16次方65535個不同的埠。其中:
- 0~1023:分配給系統的埠號
我們不可以亂用
1024~49151:登記埠號,主要是讓第三方應用使用
但是必須在IANA(網際網路數字分配機構)按照規定手續登記,
49152~65535:短暫埠號,是留給客戶程序選擇暫時使用,一個程序使用完就可以供其他程序使用。
在Socket使用時,可以用1024~65535的埠號
1.3 C/S結構
- 定義:即客戶端/伺服器結構,是軟體系統體系結構
- 作用:充分利用兩端硬體環境的優勢,將任務合理分配到Client端和Server端來實現,降低了系統的通訊開銷。
Socket正是使用這種結構建立連線的,一個套接字接客戶端,一個套接字接伺服器。
如圖:
可以看出,Socket的使用可以基於TCP或者UDP協議。
1.4 TCP協議
- 定義:Transmission Control Protocol,即傳輸控制協議,是一種傳輸層通訊協議
基於TCP的應用層協議有FTP、Telnet、SMTP、HTTP、POP3與DNS。
特點:面向連線、面向位元組流、全雙工通訊、可靠
面向連線:指的是要使用TCP傳輸資料,必須先建立TCP連線,傳輸完成後釋放連線,就像打電話一樣必須先撥號建立一條連線,打完後掛機釋放連線。
全雙工通訊:即一旦建立了TCP連線,通訊雙方可以在任何時候都能傳送資料。
可靠的:指的是通過TCP連線傳送的資料,無差錯,不丟失,不重複,並且按序到達。
面向位元組流:流,指的是流入到程序或從程序流出的字元序列。簡單來說,雖然有時候要傳輸的資料流太大,TCP報文長度有限制,不能一次傳輸完,要把它分為好幾個資料塊,但是由於可靠性保證,接收方可以按順序接收資料塊然後重新組成分塊之前的資料流,所以TCP看起來就像直接互相傳輸位元組流一樣,面向位元組流。
TCP建立連線
必須進行三次握手- 第一次握手:建立連線。客戶端傳送連線請求報文段,將SYN位置為1,Sequence Number為x;然後,客戶端進入SYN_SEND狀態,等待伺服器的確認。即A傳送資訊給B
- 第二次握手:伺服器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認。即B收到連線資訊後向A返回確認資訊
- 第三次握手:客戶端收到伺服器的(SYN+ACK)報文段,並向伺服器傳送ACK報文段。即A收到確認資訊後再次向B返回確認連線資訊
此時,A告訴自己上層連線建立;B收到連線資訊後告訴上層連線建立。
這樣就完成TCP三次握手 = 一條TCP連線建立完成 = 可以開始傳送資料
- 三次握手期間任何一次未收到對面回覆都要重發。
- 最後一個確認報文段傳送完畢以後,客戶端和伺服器端都進入ESTABLISHED狀態。
為什麼TCP建立連線需要三次握手?
答:防止伺服器端因為接收了早已失效的連線請求報文從而一直等待客戶端請求,從而浪費資源
- “已失效的連線請求報文段”的產生在這樣一種情況下:Client發出的第一個連線請求報文段並沒有丟失,而是在某個網路結點長時間的滯留了,以致延誤到連線釋放以後的某個時間才到達server。
- 這是一個早已失效的報文段。但Server收到此失效的連線請求報文段後,就誤認為是Client再次發出的一個新的連線請求。
- 於是就向Client發出確認報文段,同意建立連線。
- 假設不採用“三次握手”:只要Server發出確認,新的連線就建立了。
- 由於現在Client並沒有發出建立連線的請求,因此不會向Server傳送資料。
- 但Server卻以為新的運輸連線已經建立,並一直等待Client發來資料。>- 這樣,Server的資源就白白浪費掉了。
採用“三次握手”的辦法可以防止上述現象發生:
- Client不會向Server的確認發出確認
- Server由於收不到確認,就知道Client並沒有要求建立連線
所以Server不會等待Client傳送資料,資源就沒有被浪費
TCP釋放連線
TCP釋放連線需要四次揮手過程,現在假設A主動釋放連線:(資料傳輸結束後,通訊的雙方都可釋放連線)- 第一次揮手:A傳送釋放資訊到B;(發出去之後,A->B傳送資料這條路徑就斷了)
第二次揮手:B收到A的釋放資訊之後,回覆確認釋放的資訊:我同意你的釋放連線請求
第三次揮手:B傳送“請求釋放連線“資訊給A
第四次揮手:A收到B傳送的資訊後向B傳送確認釋放資訊:我同意你的釋放連線請求
B收到確認資訊後就會正式關閉連線;
A等待2MSL後依然沒有收到回覆,則證明B端已正常關閉,於是A關閉連線
為什麼TCP釋放連線需要四次揮手?
為了保證雙方都能通知對方“需要釋放連線”,即在釋放連線後都無法接收或傳送訊息給對方
- 需要明確的是:TCP是全雙工模式,這意味著是雙向都可以傳送、接收的
- 釋放連線的定義是:雙方都無法接收或傳送訊息給對方,是雙向的
- 當主機1發出“釋放連線請求”(FIN報文段)時,只是表示主機1已經沒有資料要傳送 / 資料已經全部發送完畢;
但是,這個時候主機1還是可以接受來自主機2的資料。
- 當主機2返回“確認釋放連線”資訊(ACK報文段)時,表示它已經知道主機1沒有資料傳送了
但此時主機2還是可以傳送資料給主機1 - 當主機2也傳送了FIN報文段時,即告訴主機1我也沒有資料要傳送了
此時,主機1和2已經無法進行通訊:主機1無法傳送資料給主機2,主機2也無法傳送資料給主機1,此時,TCP的連線才算釋放
1.5 UDP協議
定義:User Datagram Protocol,即使用者資料報協議,是一種傳輸層通訊協議。
基於UDP的應用層協議有TFTP、SNMP與DNS。
特點:無連線的、不可靠的、面向報文、沒有擁塞控制
無連線的:和TCP要建立連線不同,UDP傳輸資料不需要建立連線,就像寫信,在信封寫上收信人名稱、地址就可以交給郵局傳送了,至於能不能送到,就要看郵局的送信能力和送信過程的困難程度了。
不可靠的:因為UDP發出去的資料包發出去就不管了,不管它會不會到達,所以很可能會出現丟包現象,使傳輸的資料出錯。
面向報文:資料報文,就相當於一個數據包,應用層交給UDP多大的資料包,UDP就照樣傳送,不會像TCP那樣拆分。
- 沒有擁塞控制:擁塞,是指到達通訊子網中某一部分的分組數量過多,使得該部分網路來不及處理,以致引起這部分乃至整個網路效能下降的現象,嚴重時甚至會導致網路通訊業務陷入停頓,即出現死鎖現象,就像交通堵塞一樣。TCP建立連線後如果傳送的資料因為通道質量的原因不能到達目的地,它會不斷重發,有可能導致越來越塞,所以需要一個複雜的原理來控制擁塞。而UDP就沒有這個煩惱,發出去就不管了。
應用場景
很多的實時應用(如IP電話、實時視訊會議、某些多人同時線上遊戲等)要求源主機以很定的速率傳送資料,並且允許在網路發生擁塞時候丟失一些資料,但是要求不能有太大的延時,UDP就剛好適合這種要求。所以說,只有不適合的技術,沒有真正沒用的技術。
1.6 HTTP協議
2. Socket定義
即套接字,是一個對 TCP / IP協議進行封裝 的程式設計呼叫介面(API)
- 即通過
Socket
,我們才能在Andorid平臺上通過TCP/IP
協議進行開發 Socket
不是一種協議,而是一個程式設計呼叫介面(API
),屬於傳輸層(主要解決資料如何在網路中傳輸)
- 即通過
成對出現,一對套接字:
Socket ={(IP地址1:PORT埠號),(IP地址2:PORT埠號)}
- 1
3. 原理
Socket
的使用型別主要有兩種:
- 流套接字(
streamsocket
) :基於TCP
協議,採用 流的方式 提供可靠的位元組流服務 - 資料報套接字(
datagramsocket
):基於UDP
協議,採用 資料報文 提供資料打包傳送的服務
具體原理圖如下:
4. Socket 與 Http 對比
Socket
屬於傳輸層,因為TCP / IP
協議屬於傳輸層,解決的是資料如何在網路中傳輸的問題HTTP
協議 屬於 應用層,解決的是如何包裝資料
由於二者不屬於同一層面,所以本來是沒有可比性的。但隨著發展,預設的Http裡封裝了下面幾層的使用,所以才會出現Socket
& HTTP
協議的對比:(主要是工作方式的不同):
Http
:採用 請求—響應 方式。- 即建立網路連線後,當 客戶端 向 伺服器 傳送請求後,伺服器端才能向客戶端返回資料。
- 可理解為:是客戶端有需要才進行通訊
Socket
:採用 伺服器主動傳送資料 的方式- 即建立網路連線後,伺服器可主動傳送訊息給客戶端,而不需要由客戶端向伺服器傳送請求
- 可理解為:是伺服器端有需要才進行通訊
5. 使用步驟
Socket
可基於TCP
或者UDP
協議,但TCP更加常用- 所以下面的使用步驟 & 例項的
Socket
將基於TCP
協議
// 步驟1:建立客戶端 & 伺服器的連線
// 建立Socket物件 & 指定服務端的IP及埠號
Socket socket = new Socket("192.168.1.32", 1989);
// 判斷客戶端和伺服器是否連線成功
socket.isConnected());
// 步驟2:客戶端 & 伺服器 通訊
// 通訊包括:客戶端 接收伺服器的資料 & 傳送資料 到 伺服器
<-- 操作1:接收伺服器的資料 -->
// 步驟1:建立輸入流物件InputStream
InputStream is = socket.getInputStream()
// 步驟2:建立輸入流讀取器物件 並傳入輸入流物件
// 該物件作用:獲取伺服器返回的資料
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
// 步驟3:通過輸入流讀取器物件 接收伺服器傳送過來的資料
br.readLine();
<-- 操作2:傳送資料 到 伺服器 -->
// 步驟1:從Socket 獲得輸出流物件OutputStream
// 該物件作用:傳送資料
OutputStream outputStream = socket.getOutputStream();
// 步驟2:寫入需要傳送的資料到輸出流物件中
outputStream.write(("Carson_Ho"+"\n").getBytes("utf-8"));
// 特別注意:資料的結尾加上換行符才可讓伺服器端的readline()停止阻塞
// 步驟3:傳送資料到服務端
outputStream.flush();
// 步驟3:斷開客戶端 & 伺服器 連線
os.close();
// 斷開 客戶端傳送到伺服器 的連線,即關閉輸出流物件OutputStream
br.close();
// 斷開 伺服器傳送到客戶端 的連線,即關閉輸入流讀取器物件BufferedReader
socket.close();
// 最終關閉整個Socket連線
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
6. 具體例項
- 例項
Demo
程式碼包括:客戶端 & 伺服器 - 本文著重講解客戶端,伺服器僅採用最簡單的寫法進行展示
6.1 客戶端 實現
步驟1:加入網路許可權
<uses-permission android:name="android.permission.INTERNET" />
- 1
步驟2:主佈局介面設定
包括建立Socket連線、客戶端 & 伺服器通訊的按鈕
<Button
android:id="@+id/connect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="connect" />
<Button
android:id="@+id/disconnect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="disconnect" />
<TextView
android:id="@+id/receive_message"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/Receive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Receive from message" />
<EditText
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="send"/>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
步驟3:建立Socket連線、客戶端 & 伺服器通訊
具體請看註釋
MainActivity.java
package scut.carson_ho.socket_carson;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
/**
* 主 變數
*/
// 主執行緒Handler
// 用於將從伺服器獲取的訊息顯示出來
private Handler mMainHandler;
// Socket變數
private Socket socket;
// 執行緒池
// 為了方便展示,此處直接採用執行緒池進行執行緒管理,而沒有一個個開執行緒
private ExecutorService mThreadPool;
/**
* 接收伺服器訊息 變數
*/
// 輸入流物件
InputStream is;
// 輸入流讀取器物件
InputStreamReader isr ;
BufferedReader br ;
// 接收伺服器傳送過來的訊息
String response;
/**
* 傳送訊息到伺服器 變數
*/
// 輸出流物件
OutputStream outputStream;
/**
* 按鈕 變數
*/
// 連線 斷開連線 傳送資料到伺服器 的按鈕變數
private Button btnConnect, btnDisconnect, btnSend;
// 顯示接收伺服器訊息 按鈕
private TextView Receive,receive_message;
// 輸入需要傳送的訊息 輸入框
private EditText mEdit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* 初始化操作
*/
// 初始化所有按鈕
btnConnect = (Button) findViewById(R.id.connect);
btnDisconnect = (Button) findViewById(R.id.disconnect);
btnSend = (Button) findViewById(R.id.send);
mEdit = (EditText) findViewById(R.id.edit);
receive_message = (TextView) findViewById(R.id.receive_message);
Receive = (Button) findViewById(R.id.Receive);
// 初始化執行緒池
mThreadPool = Executors.newCachedThreadPool();
// 例項化主執行緒,用於更新接收過來的訊息
mMainHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
receive_message.setText(response);
break;
}
}
};
/**
* 建立客戶端 & 伺服器的連線
*/
btnConnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 利用執行緒池直接開啟一個執行緒 & 執行該執行緒
mThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 建立Socket物件 & 指定服務端的IP 及 埠號
socket = new Socket("192.168.1.172", 8989);
// 判斷客戶端和伺服器是否連線成功
System.out.println(socket.isConnected());
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
});
/**
* 接收 伺服器訊息
*/
Receive.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 利用執行緒池直接開啟一個執行緒 & 執行該執行緒
mThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 步驟1:建立輸入流物件InputStream
is = socket.getInputStream();
// 步驟2:建立輸入流讀取器物件 並傳入輸入流物件
// 該物件作用:獲取伺服器返回的資料
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
// 步驟3:通過輸入流讀取器物件 接收伺服器傳送過來的資料
response = br.readLine();
// 步驟4:通知主執行緒,將接收的訊息顯示到介面
Message msg = Message.obtain();
msg.what = 0;
mMainHandler.sendMessage(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
});
/**
* 傳送訊息 給 伺服器
*/
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 利用執行緒池直接開啟一個執行緒 & 執行該執行緒
mThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 步驟1:從Socket 獲得輸出流物件OutputStream
// 該物件作用:傳送資料
outputStream = socket.getOutputStream();
// 步驟2:寫入需要傳送的資料到輸出流物件中
outputStream.write((mEdit.getText().toString()+"\n").getBytes("utf-8"));
// 特別注意:資料的結尾加上換行符才可讓伺服器端的readline()停止阻塞
// 步驟3:傳送資料到服務端
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
});
/**
* 斷開客戶端 & 伺服器的連線
*/
btnDisconnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
// 斷開 客戶端傳送到伺服器 的連線,即關閉輸出流物件OutputStream
outputStream.close();
// 斷開 伺服器傳送到客戶端 的連線,即關閉輸入流讀取器物件BufferedReader
br.close();
// 最終關閉整個Socket連線
socket.close();
// 判斷客戶端和伺服器是否已經斷開連線
System.out.println(socket.isConnected());
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
6.2 伺服器 實現
- 因本文主要講解客戶端,所以伺服器僅僅是為了配合客戶端展示;
- 為了簡化伺服器使用,此處採用
Mina
框架
- 伺服器程式碼請在
eclipse
平臺執行- 按照我的步驟一步步實現就可以無腦運行了
步驟1:匯入Mina
包
步驟2:建立伺服器執行緒
TestHandler.java
package mina;
// 匯入包
public class TestHandler extends IoHandlerAdapter {
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
System.out.println("exceptionCaught: " + cause);
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
System.out.println("recieve : " + (String) message);
session.write("hello I am server");
}
@Override
public void messageSent(IoSession session, Object message) throws Exception {
}
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("sessionClosed");
}
@Override
public void sessionOpened(IoSession session) throws Exception {
System.out.println("sessionOpen");
}
@Override
public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
步驟3:建立伺服器主程式碼
TestHandler.java
package mina;
import java.io.IOException;
import java.net.InetSocketAddress;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class TestServer {
public static void main(String[] args) {
NioSocketAcceptor acceptor = null;
try {
acceptor = new NioSocketAcceptor();
acceptor.setHandler(new TestHandler());
acceptor.getFilterChain().addLast("mFilter", new ProtocolCodecFilter(new TextLineCodecFactory()));
acceptor.setReuseAddress(true);
acceptor.bind(new InetSocketAddress(8989));
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
至此,客戶端 & 伺服器的程式碼均實現完畢。
6.3 測試結果
- 點選
Connect
按鈕: 連線成功
- 輸入傳送的訊息,點選
Send
按鈕傳送
- 伺服器接收到客戶端傳送的訊息
- 點選
Receive From Message
按鈕,客戶端 讀取 伺服器返回的訊息
- 點選
DisConnect
按鈕,斷開 客戶端 & 伺服器的連線
6.4 原始碼地址
7. 總結
- 相信大家已經非常瞭解關於Socket的使用
- 下面我將繼續對
Android
的網路程式設計進行講解,有興趣可以繼續關注Carson_Ho的安卓開發筆記