Socket
的使用在 Android
網絡編程中很是重要Socket
及 其使用方法
計算機網絡分爲五層:物理層、數據鏈路層、網絡層、運輸層、應用層java
其中:android
端口號規定爲16位,即容許一個IP主機有2的16次方65535個不一樣的端口。其中:git
0~1023:分配給系統的端口號github
> 咱們不能夠亂用
1024~49151:登記端口號,主要是讓第三方應用使用apache
> 可是必須在IANA(互聯網數字分配機構)按照規定手續登記,
在Socket使用時,能夠用1024~65535的端口號編程
做用:充分利用兩端硬件環境的優點,將任務合理分配到Client端和Server端來實現,下降了系統的通信開銷。服務器
> Socket正是使用這種結構創建鏈接的,一個套接字接客戶端,一個套接字接服務器。
如圖:網絡
能夠看出,Socket的使用能夠基於TCP或者UDP協議。session
定義:Transmission Control Protocol,即傳輸控制協議,是一種傳輸層通訊協議app
基於TCP的應用層協議有FTP、Telnet、SMTP、HTTP、POP3與DNS。
特色:面向鏈接、面向字節流、全雙工通訊、可靠
TCP創建鏈接
必須進行三次握手:若A要與B進行鏈接,則必須
第三次握手:客戶端收到服務器的(SYN+ACK)報文段,並向服務器發送ACK報文段。即A收到確認信息後再次向B返回確認鏈接信息
> 此時,A告訴本身上層鏈接創建;B收到鏈接信息後告訴上層鏈接創建。
這樣就完成TCP三次握手 = 一條TCP鏈接創建完成 = 能夠開始發送數據
- 三次握手期間任何一次未收到對面回覆都要重發。
- 最後一個確認報文段發送完畢之後,客戶端和服務器端都進入ESTABLISHED狀態。
答:防止服務器端由於接收了早已失效的鏈接請求報文從而一直等待客戶端請求,從而浪費資源
- 「已失效的鏈接請求報文段」的產生在這樣一種狀況下:Client發出的第一個鏈接請求報文段並無丟失,而是在某個網絡結點長時間的滯留了,以至延誤到鏈接釋放之後的某個時間纔到達server。
- 這是一個早已失效的報文段。但Server收到此失效的鏈接請求報文段後,就誤認爲是Client再次發出的一個新的鏈接請求。
- 因而就向Client發出確認報文段,贊成創建鏈接。
- 假設不採用「三次握手」:只要Server發出確認,新的鏈接就創建了。
- 因爲如今Client並無發出創建鏈接的請求,所以不會向Server發送數據。
- 但Server卻覺得新的運輸鏈接已經創建,並一直等待Client發來數據。>- 這樣,Server的資源就白白浪費掉了。
採用「三次握手」的辦法能夠防止上述現象發生:
TCP釋放鏈接
TCP釋放鏈接須要四次揮手過程,如今假設A主動釋放鏈接:(數據傳輸結束後,通訊的雙方均可釋放鏈接)
第四次揮手:A收到B發送的信息後向B發送確認釋放信息:我贊成你的釋放鏈接請求
> B收到確認信息後就會正式關閉鏈接; > A等待2MSL後依然沒有收到回覆,則證實B端已正常關閉,因而A關閉鏈接
爲了保證雙方都能通知對方「須要釋放鏈接」,即在釋放鏈接後都沒法接收或發送消息給對方
當主機1發出「釋放鏈接請求」(FIN報文段)時,只是表示主機1已經沒有數據要發送 / 數據已經所有發送完畢;
> 可是,這個時候主機1仍是能夠接受來自主機2的數據。
當主機2返回「確認釋放鏈接」信息(ACK報文段)時,表示它已經知道主機1沒有數據發送了
> 但此時主機2仍是能夠發送數據給主機1
當主機2也發送了FIN報文段時,即告訴主機1我也沒有數據要發送了
> 此時,主機1和2已經沒法進行通訊:主機1沒法發送數據給主機2,主機2也沒法發送數據給主機1,此時,TCP的鏈接纔算釋放
定義:User Datagram Protocol,即用戶數據報協議,是一種傳輸層通訊協議。
基於UDP的應用層協議有TFTP、SNMP與DNS。
特色:無鏈接的、不可靠的、面向報文、沒有擁塞控制
詳情請看我寫的另一篇文章你須要瞭解的HTTP知識都在這裏了!
即套接字,是一個對 TCP / IP協議進行封裝 的編程調用接口(API)
> 1. 即經過`Socket`,咱們才能在Andorid平臺上經過 `TCP/IP`協議進行開發 > 2. `Socket`不是一種協議,而是一個編程調用接口(`API`),屬於傳輸層(主要解決數據如何在網絡中傳輸)
Socket ={(IP地址1:PORT端口號),(IP地址2:PORT端口號)}
Socket
的使用類型主要有兩種:
streamsocket
) :基於 TCP
協議,採用 流的方式 提供可靠的字節流服務datagramsocket
):基於 UDP
協議,採用 數據報文 提供數據打包發送的服務具體原理圖以下:
Socket
屬於傳輸層,由於 TCP / IP
協議屬於傳輸層,解決的是數據如何在網絡中傳輸的問題 HTTP
協議 屬於 應用層,解決的是如何包裝數據 因爲兩者不屬於同一層面,因此原本是沒有可比性的。但隨着發展,默認的Http裏封裝了下面幾層的使用,因此纔會出現Socket
& HTTP
協議的對比:(主要是工做方式的不一樣):
Http
:採用 請求—響應 方式。
> 1. 即創建網絡鏈接後,當 客戶端 向 服務器 發送請求後,服務器端才能向客戶端返回數據。 > 2. 可理解爲:**是客戶端有須要才進行通訊**
Socket
:採用 服務器主動發送數據 的方式
> 1. 即創建網絡鏈接後,服務器可主動發送消息給客戶端,而不須要由客戶端向服務器發送請求 > 2. 可理解爲:**是服務器端有須要才進行通訊**
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鏈接
Demo
代碼包括:客戶端 & 服務器步驟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(); } } }); } }
爲了簡化服務器使用,此處採用Mina
框架
> 1. 服務器代碼請在`eclipse`平臺運行 > 2. 按照個人步驟一步步實現就能夠無腦運行了
步驟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(); } } }
至此,客戶端 & 服務器的代碼均實現完畢。
Connect
按鈕: 鏈接成功
Send
按鈕發送
Receive From Message
按鈕,客戶端 讀取 服務器返回的消息
DisConnect
按鈕,斷開 客戶端 & 服務器的鏈接