前言
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架構
可以看出,Socket的使用可以基於TCP或者UDP協議。
1.4 TCP協議
定義:Transmission Control Protocol,即傳輸控制協議,是一種傳輸層通信協議
基於TCP的應用層協議有FTP、Telnet、SMTP、HTTP、POP3與DNS。
特點:面向連接、面向字節流、全雙工通信、可靠
面向連接:指的是要使用TCP傳輸數據,必須先建立TCP連接,傳輸完成後釋放連接,就像打電話一樣必須先撥號建立一條連接,打完後掛機釋放連接。
全雙工通信:即一旦建立了TCP連接,通信雙方可以在任何時候都能發送數據。
可靠的:指的是通過TCP連接傳送的數據,無差錯,不丟失,不重複,並且按序到達。
面向字節流:流,指的是流入到進程或從進程流出的字符序列。簡單來說,雖然有時候要傳輸的數據流太大,TCP報文長度有限制,不能一次傳輸完,要把它分爲好幾個數據塊,但是由於可靠性保證,接收方可以按順序接收數據塊然後重新組成分塊之前的數據流,所以TCP看起來就像直接互相傳輸字節流一樣,面向字節流。
TCP建立連接
必須進行
三次握手
:若A要與B進行連接,則必須
第一次握手:建立連接。客戶端發送連接請求報文段,將SYN位置爲1,Sequence Number爲x;然後,客戶端進入SYN_SEND狀態,等待服務器的確認。即A發送信息給B
第二次握手:服務器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認。即B收到連接信息後向A返回確認信息
第三次握手:客戶端收到服務器的(SYN+ACK)報文段,並向服務器發送ACK報文段。即A收到確認信息後再次向B返回確認連接信息
此時,A告訴自己上層連接建立;B收到連接信息後告訴上層連接建立。
TCP三次握手
這樣就完成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釋放連接需要四次揮手?
爲了保證雙方都能通知對方“需要釋放連接”,即在釋放連接後都無法接收或發送消息給對方
需要明確的是: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協議
詳情請看我寫的另外一篇文章你需要了解的HTTP知識都在這裏了!
2. Socket定義
即套接字,是應用層 與 TCP/IP 協議族通信的中間軟件抽象層,表現爲一個封裝了 TCP / IP協議族 的編程接口(API)
示意圖
Socket
不是一種協議,而是一個編程調用接口(API
),屬於傳輸層(主要解決數據如何在網絡中傳輸)即:通過
Socket
,我們才能在Andorid平臺上通過TCP/IP
協議進行開發對用戶來說,只需調用Socket去組織數據,以符合指定的協議,即可通信
成對出現,一對套接字:
Socket ={(IP地址1:PORT端口號),(IP地址2:PORT端口號)}
一個
Socket
實例 唯一代表一個主機上的一個應用程序的通信鏈路
3. 建立Socket連接過程
示意圖
4. 原理
Socket
的使用類型主要有兩種:
流套接字(
streamsocket
) :基於TCP
協議,採用 流的方式 提供可靠的字節流服務數據報套接字(
datagramsocket
):基於UDP
協議,採用 數據報文 提供數據打包發送的服務
具體原理圖如下:
原理圖
5. Socket 與 Http 對比
Socket
屬於傳輸層,因爲TCP / IP
協議屬於傳輸層,解決的是數據如何在網絡中傳輸的問題HTTP
協議 屬於 應用層,解決的是如何包裝數據
由於二者不屬於同一層面,所以本來是沒有可比性的。但隨着發展,默認的Http裏封裝了下面幾層的使用,所以纔會出現Socket
& HTTP
協議的對比:(主要是工作方式的不同):
Http
:採用 請求—響應 方式。
即建立網絡連接後,當 客戶端 向 服務器 發送請求後,服務器端才能向客戶端返回數據。
可理解爲:是客戶端有需要才進行通信
Socket
:採用 服務器主動發送數據 的方式
即建立網絡連接後,服務器可主動發送消息給客戶端,而不需要由客戶端向服務器發送請求
可理解爲:是服務器端有需要才進行通信
6. 使用步驟
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連接
7. 具體實例
實例
Demo
代碼包括:客戶端 & 服務器本文着重講解客戶端,服務器僅採用最簡單的寫法進行展示
7.1 客戶端 實現
步驟1:加入網絡權限
<uses-permission android:name="android.permission.INTERNET" />
步驟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"/>
步驟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();
}
}
});
}
}
7.2 服務器 實現
因本文主要講解客戶端,所以服務器僅僅是爲了配合客戶端展示;
爲了簡化服務器使用,此處採用
Mina
框架
服務器代碼請在
eclipse
平臺運行按照我的步驟一步步實現就可以無腦運行了
步驟1:導入Mina包
請直接移步到百度網盤:下載鏈接(密碼: q73e)
示意圖
步驟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 {
}
}
步驟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();
}
}
}
至此,客戶端 & 服務器的代碼均實現完畢。
7.3 測試結果
點擊
Connect
按鈕: 連接成功
示意圖
輸入發送的消息,點擊
Send
按鈕發送
示意圖
服務器接收到客戶端發送的消息
示意圖
點擊
Receive From Message
按鈕,客戶端 讀取 服務器返回的消息
示意圖
點擊
DisConnect
按鈕,斷開 客戶端 & 服務器的連接
客戶端示意圖
服務器示意圖
7.4 源碼地址