Android 基于TCP的 Socket 編程實現(結合 okio)

前言

? ? ? ?兩個進程如果要進行通訊最基本的一個前提就是能夠唯一的標識一個進程,在本地進程通訊中我們可以使用 PID 來唯一標識一個進程,但 PID 只在本地是唯一的,網絡中兩個進程 PID 沖突幾率很大,這時我們就需要通過其他手段來唯一標識網絡中的進程了,我們知道 IP 層的 ip 地址可以唯一標示主機,而 TCP 層協議和端口號結合就可以唯一標示主機的一個進程了。

能夠唯一標示網絡中的進程后,它們就可以利用 Socket 進行通信了,什么是 Socket 呢?我們經常把 Socket 翻譯為套接字(為什么翻譯成套接字),Socket 是在應用層和傳輸層之間的一個抽象層,它把 TCP/IP 層復雜的操作抽象為幾個簡單的接口供應用層調用,從而實現進程在網絡中通信。


原理圖

相關類

? ? ? ?這里提到的 Socket 為廣義上的 Socket 編程,它可以基于 TCP 或者 UDP 實現,Java為 Socket 編程封裝了幾個重要的類,如下:

Socket (TCP)

? ? ? Socket 類實現了一個客戶端 Socket,作為兩臺機器通信的終端,默認采用的傳輸層協議為 TCP 可靠傳輸協議。Socket 類除了構造函數返回一個 socket 外,還提供了 connect , getOutputStream, getInputStream 和 close 方法。connect 方法用于請求一個 socket 連接,getOutputStream 用于獲得寫 socket的輸出流,getInputStream 用于獲得讀 socket 的輸入流,close 方法用于關閉一個流。

DatagramSocket (UDP)

? ? ? ?DatagramSocket 類實現了一個發送和接收數據報的 socket,傳輸層協議使用 UDP,不能保證數據報的可靠傳輸。DataGramSocket 主要有 send, receive 和 close 三個方法。send 用于發送一個數據報,Java 提供了 DatagramPacket 對象用來表達一個數據報。receive 用于接收一個數據報,調用該方法后,一直阻塞接收到直到數據報或者超時。close 是關閉一個 socket。

ServerSocket

? ? ? ?ServerSocket 類實現了一個服務器 socket,一個服務器 socke t等待客戶端網絡請求,然后基于這些請求執行操作,并返回給請求者一個結果。ServerSocket 提供了 bind、accept 和 close 三個方法。bind 方法為ServerSocket 綁定一個IP地址和端口,并開始監聽該端口。accept 方法為 ServerSocket 接受請求并返回一個 Socket 對象,accept 方法調用后,將一直阻塞直到有請求到達。close 方法關閉一個 ServerSocket 對象。

SocketAddress

? ? ? ?SocketAddress 提供了一個 socket 地址,不關心傳輸層協議。這是一個虛類,由子類來具體實現功能、綁定傳輸協議。它提供了一個不可變的對象,被 socket 用來綁定、連接或者返回數值。

InetSocketAddress

? ? ? ?InetSocketAddress 實現了IP地址的 SocketAddress,也就是有 IP 地址和端口號表達 Socket 地址。如果不制定具體的 IP 地址和端口號,那么 IP 地址默認為本機地址,端口號隨機選擇一個。

DatagramPacket(UDP)

? ? ? ? DatagramSocket 是面向數據報 socket 通信的一個可選通道。數據報通道不是對網絡數據報 socket 通信的完全抽象。socket通信的控制由DatagramSocket 對象實現。DatagramPacket 需要與 DatagramSocket 配合使用才能完成基于數據報的 socket 通信。

基于TCP的 Socket

? ? ? ?基于 TCP 的 Socket可以實現客戶端—服務器間的雙向實時通信。上面提到的 java.NET包中定義的兩個類 Socket 和 ServerSocket,分別用來實現雙向連接的 client 和 server 端。


通信模型

實現

客戶端連接:demo


android端效果

客戶端發送:消息給服務端


向服務端發數據

服務端代碼:

'''

public class SocketTest {

? ? ? ? ? private static final int PORT =9999;

? ? ? ? ? private List mList =newArrayList();

? ? ? ? ? private ServerSocket server =null;

? ? ? ? ? private ExecutorService mExecutorService =null;

? ? ? ? ? private String receiveMsg;

? ? ? ? ? private String sendMsg;

? ? ? ? ? public static void main(String[] args) {

? ? ? ? ? ? ? ? ? ? ? newSocketTest();

? ? ? ? ? }

? ? ? ? ?public Socket Test() {

? ? ? ? ? ? ? ? ? ?try{

? ? ? ? ? ? ? ? ? ? ? ? ?server =newServerSocket(PORT);

? ? ? ? ? ? ? ? ? ? ? ? ?mExecutorService = Executors.newCachedThreadPool();

? ? ? ? ? ? ? ? ? ? ? ? ?System.out.println("服務器已啟動...");

? ? ? ? ? ? ? ? ? ? ? ? ?Socket client =null;

? ? ? ? ? ? ? ? ? ? ? ? ?while(true) {

? ? ? ? ? ? ? ? ? ? ? ? ?client = server.accept();

? ? ? ? ? ? ? ? ? ? ? ? ?mList.add(client);

? ? ? ? ? ? ? ? ? ? ? ? ?mExecutorService.execute(new Service(client));

? ? ? ? ? ? ? ? ? ? ?}

? ? ? ? ? ? ?}catch(Exception e) {

? ? ? ? ? ? ? ? ?e.printStackTrace();

? ? ? ? ? ? }

? ? ? }

class Service implements Runnable {

private Socket socket;

private BufferedReader in=null;

private PrintWriter printWriter=null;

public Service(Socket socket) {

this.socket = socket;try{

printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter( socket.getOutputStream(),"UTF-8")),true);

in=new BufferedReader(new InputStreamReader(

socket.getInputStream(),"UTF-8"));

printWriter.println("成功連接服務器"+"(服務器發送)");

System.out.println("成功連接服務器");

}catch(IOException e) {

e.printStackTrace();

}

}

@Override

public void run() {

try{

while(true) {

if((receiveMsg =in.readLine())!=null) {

System.out.println("receiveMsg:"+receiveMsg);

if(receiveMsg.equals("0")) {

System.out.println("客戶端請求斷開連接");

printWriter.println("服務端斷開連接"+"(服務器發送)");

mList.remove(socket);

in.close();

socket.close();

break;

}else{

sendMsg ="我已接收:"+ receiveMsg +"(服務器發送)";

printWriter.println(sendMsg);

}

}

}

}catch(Exception e) {

e.printStackTrace();

}

}

}

}

'''

服務端使用線程池實現多客戶端連接,server.accept() 表示等待客戶端連接,當有客戶端連接時新建一個線程去處理,其中涉及到的方法之前都提到過,不再贅述。

客戶端代碼:

'''

public class SocketActivity extends AppCompatActivity{

private EditText mEditText;

private TextView mTextView;

private static final String TAG ="TAG";

private static final String HOST ="192.168.23.1";

private static final int PORT =9999;

private PrintWriter printWriter;

private BufferedReader in;

private ExecutorService mExecutorService =null;

private String receiveMsg;

@Override

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);

setContentView(R.layout.activity_socket);

mEditText = (EditText) findViewById(R.id.editText);

mTextView = (TextView) findViewById(R.id.textView);

mExecutorService = Executors.newCachedThreadPool();

}

public void connect(View view) {

mExecutorService.execute(newconnectService());

}

public void send(View view) {

String sendMsg = mEditText.getText().toString();

mExecutorService.execute(newsendService(sendMsg));

}

public void disconnect(View view) {

mExecutorService.execute(newsendService("0"));

}

private class sendService implements Runnable{

privateString msg;

sendService(String msg) {

this.msg = msg;

}

@Override

public void run() {

printWriter.println(this.msg);

}

}

private class connectService implements Runnable{

@Override

public void run() {try{

Socket socket =newSocket(HOST, PORT);

socket.setSoTimeout(60000);

printWriter =newPrintWriter(new BufferedWriter(new OutputStreamWriter(

socket.getOutputStream(),"UTF-8")),true);

in =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));

receiveMsg();

}catch(Exception e) {

Log.e(TAG, ("connectService:"+ e.getMessage()));

}

}

}

private void receiveMsg() {

try{

while(true) {

if((receiveMsg = in.readLine()) !=null) {

Log.d(TAG,"receiveMsg:"+ receiveMsg);

runOnUiThread(new Runnable() {

@Override

public void run() {

mTextView.setText(receiveMsg +"\n\n"+ mTextView.getText());

}

});

}

}

}catch(IOException e) {

Log.e(TAG,"receiveMsg: ");

e.printStackTrace();

}

}

}

'''

客戶端同樣使用了線程池進行管理,把連接和發送分割為兩個 Runnable 易于調用,當發送 “0” 且服務端收到時關閉連接。

okio 實現

到這里一個簡單的 Socket 通信就完成了,其中對于 Socket 的信息流使用的是 java.io,之前學習 okio 時,了解到 okio 可以替代 java.io,okio是一個由square公司開發的開源庫,它彌補了Java.io和java.nio的不足,能夠更方便快速的讀取、存儲和處理數據(了解更多請點擊Okio源碼分析),下面就嘗試用 okio 替換 java.io。

直接上代碼:

服務端代碼:

'''

public class SocketTest {

private static final int PORT =9999;

private List mList =newArrayList();

private ServerSocket server =null;

private ExecutorService mExecutorService =null;

private String receiveMsg;

private String sendMsg;

public static void main(String[] args) {

newSocketTest();

}

public SocketTest() {

try{

server =new ServerSocket(PORT);

mExecutorService = Executors.newCachedThreadPool();

System.out.println("服務器已啟動...");

Socket client =null;

while(true) {

client = server.accept();

mList.add(client);

mExecutorService.execute(new Service(client));

}

}catch(Exception e) {

e.printStackTrace();

}

}

class Service implements Runnable {

private Socket socket;

private BufferedSink mSink;

private BufferedSource mSource;

public Service(Socket socket) {

this.socket = socket;

try{

mSink = Okio.buffer(Okio.sink(socket));

mSource = Okio.buffer(Okio.source(socket));

sendMsg="成功連接服務器"+"(服務器發送)";

mSink.writeUtf8(sendMsg+"\n");

mSink.flush();

System.out.println("成功連接服務器");

}catch(IOException e) {

e.printStackTrace();

}

}

@Override

public void run() {

try{

while(true) {

for(String receiveMsg; (receiveMsg = mSource

.readUtf8Line()) !=null;) {

System.out.println("receiveMsg:"+ receiveMsg);

if(receiveMsg.equals("0")) {

System.out.println("客戶端請求斷開連接");

mSink.writeUtf8("服務端斷開連接"+"(服務器發送)");

mSink.flush();

mList.remove(socket);

socket.close();

break;

}else{

sendMsg ="我已接收:"+ receiveMsg +"(服務器發送)";

mSink.writeUtf8(sendMsg+"\n");

mSink.flush();

}

}

}

}catch(Exception e) {

e.printStackTrace();

}

}

}

}

'''


客戶端代碼:

'''

public class SocketActivity extends AppCompatActivity{

private EditText mEditText;

private TextView mTextView;

private static final String TAG ="TAG";

private static final String HOST ="192.168.23.1";

private static final int PORT =9999;

private BufferedSink mSink;

private BufferedSource mSource;

private ExecutorService mExecutorService =null;

@Override

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);

setContentView(R.layout.activity_socket);

mEditText = (EditText) findViewById(R.id.editText);

mTextView = (TextView) findViewById(R.id.textView);

mExecutorService = Executors.newCachedThreadPool();

}publicvoidconnect(View view) {

mExecutorService.execute(new connectService());

}

public void send(View view) {

String sendMsg = mEditText.getText().toString();

mExecutorService.execute(new sendService(sendMsg));

}

public void disconnect(View view) {

mExecutorService.execute(new sendService("0"));

}

private class sendService implements Runnable{

private String msg;

sendService(String msg) {

this.msg = msg;

}

@Override

public void run() {

try{

mSink.writeUtf8(this.msg+"\n");

mSink.flush();

}catch(IOException e) {

e.printStackTrace();

}

}

}

private class connectService implements Runnable{

@Override

public void run() {

try{

Socket socket =newSocket(HOST, PORT);

mSink = Okio.buffer(Okio.sink(socket));

mSource = Okio.buffer(Okio.source(socket));

receiveMsg();

}catch(Exception e) {

Log.e(TAG, ("connectService:"+ e.getMessage()));

}

}

}

private void receiveMsg() {

try{

while(true) {

for(String receiveMsg; (receiveMsg = mSource.readUtf8Line()) !=null; ) {

Log.d(TAG,"receiveMsg:"+ receiveMsg);finalString finalReceiveMsg = receiveMsg;

runOnUiThread(new Runnable() {

@Override

public void run() {

mTextView.setText(finalReceiveMsg +"\n\n"+ mTextView.getText());

}

});

}

}

}catch(IOException e) {

Log.e(TAG,"receiveMsg: ");

e.printStackTrace();

}

}

}

'''

這里有一個很坑的地方:

mSink.writeUtf8(this.msg+"\n");

mSink.flush();

起初沒有加 “\n” 時,調用 flush 方法后消息是無法發送成功的,除非調用 sink.close 方法后才會發送成功,但是我們不能每發送一次就 close 掉,對比 printWriter.println 方法,嘗試加上一個換行符,果真發送成功。

總結

android有兩種通信方式,一種是常用的基于 HTTP 協議方式,另一種就是基于 TCP/UDP 協議的 Socket 方式。雖然大部分需求都可通過 HTTP 實現,實現起來也較為簡單,但某些情景下需要使用 Socket 方式,這時永遠不要放棄去使用最佳的工具來解決問題的機會。本文主要通過 Socket 實現了 Android 基于 TCP 協議的通信,后面將 Socket 的輸入輸出流處理由 java.io 替換為 Okio 實現,雖然說 Okio 彌補了Java.io和 java.nio 的不足,能夠更方便快速的讀取、存儲和處理數據,但是實際性能并沒測試過,這里主要是為了復習一下 Okio 的使用,另外就是在Okio源碼分析中沒有涉及到 Socket 的內容,這里正好填補一下知識漏洞。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容