android Socket實(shí)現(xiàn)簡單聊天功能以及文件傳輸
干程序是一件枯燥重復(fù)的事,每當(dāng)感到內(nèi)心浮躁的時(shí)候,我就會(huì)找小說來看。我從小就喜愛看武俠小說,一直有著武俠夢(mèng)。從金庸,古龍,梁羽生系列到鳳歌(昆侖),孫曉(英雄志)以及蕭鼎的(誅仙)讓我領(lǐng)略著不一樣的江湖。
如果你有好看的武俠系列小說,給我留言哦。題外話就扯這么多了,接著還是上技術(shù)。
看看今天實(shí)現(xiàn)的功能效果圖:
可以這里使用多臺(tái)手機(jī)進(jìn)行通訊,我采用的服務(wù)器發(fā)送消息。
是不是只有發(fā)送消息,有些顯得太單調(diào)了。好,在發(fā)送消息的基礎(chǔ)上增加文件傳輸。后期會(huì)增加視頻,音頻的傳輸,增加表情包。那一起來看看圖文消息的效果圖,帶領(lǐng)大家一起來實(shí)現(xiàn)通訊的簡易聊天功能。
需要解決的難點(diǎn):
如何判斷socket接收的數(shù)據(jù)是字符串還是流?
如果你已是一名老司機(jī),還請(qǐng)留言給出寶貴意見。帶著這個(gè)疑問我們接著往下看。
Socket概述
Socket我們稱之為"套接字",用于消息通知系統(tǒng)(如:激光推送),時(shí)事通訊系統(tǒng)(如:環(huán)信)等等。用于描述IP地址和端口,是一個(gè)通信鏈的句柄。網(wǎng)絡(luò)上的兩個(gè)程序通過一個(gè)雙向的通訊連接實(shí)現(xiàn)數(shù)據(jù)的交換,這個(gè)雙向鏈路的一端稱為一個(gè)Socket,一個(gè)Socket由一個(gè)IP地址和一個(gè)端口號(hào)唯一確定(如:ServerSocket)。應(yīng)用程序通常通過"套接字"向網(wǎng)絡(luò)發(fā)出請(qǐng)求或者應(yīng)答網(wǎng)絡(luò)請(qǐng)求。Socket是TCP/IP協(xié)議的一個(gè)十分流行的編程界面,但是,Socket所支持的協(xié)議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯(lián)系的。在Java環(huán)境下,Socket編程主要是指基于TCP/IP協(xié)議的網(wǎng)絡(luò)編程。
java.net包下有兩個(gè)類:Socket和ServerSocket,基于TCP協(xié)議。
本文針對(duì)Socket和ServerSocket作主要講解。
socket連接
建立Socket連接至少需要一對(duì)套接字,其中一個(gè)運(yùn)行于客戶端,稱為ClientSocket ,另一個(gè)運(yùn)行于服務(wù)器端,稱為ServerSocket。
套接字之間的連接過程分為三個(gè)步驟:服務(wù)器監(jiān)聽,客戶端請(qǐng)求,連接確認(rèn)。步驟如下:
- 服務(wù)器監(jiān)聽:服務(wù)器端套接字并不定位具體的客戶端套接字,而是處于等待連接的狀態(tài),實(shí)時(shí)監(jiān)控網(wǎng)絡(luò)狀態(tài),等待客戶端的連接請(qǐng)求
- 客戶端請(qǐng)求:指客戶端的套接字提出連接請(qǐng)求,要連接的目標(biāo)是服務(wù)器端的套接字。為此,客戶端的套接字必須首先描述它要連接的服務(wù)器的套接字,指出服務(wù)器端套接字的地址和端口號(hào),然后就向服務(wù)器端套接字提出連接請(qǐng)求。
- 連接確認(rèn):當(dāng)服務(wù)器端套接字監(jiān)聽到或者說接收到客戶端套接字的連接請(qǐng)求時(shí),就響應(yīng)客戶端套接字的請(qǐng)求,建立一個(gè)新的線程,把服務(wù)器端套接字的描述發(fā)給客戶端,一旦客戶端確認(rèn)了此描述,雙方就正式建立連接。而服務(wù)器端套接字繼續(xù)處于監(jiān)聽狀態(tài),繼續(xù)接收其他客戶端套接字的連接請(qǐng)求。
JDK Socket
在java.net包下有兩個(gè)類:Socket和ServerSocket。ServerSocket用于服務(wù)器端,Socket是建立網(wǎng)絡(luò)連接時(shí)使用的。在連接成功時(shí),應(yīng)用程序兩端都會(huì)產(chǎn)生一個(gè)Socket實(shí)例,操作這個(gè)實(shí)例,完成所需的會(huì)話。對(duì)于一個(gè)網(wǎng)絡(luò)連接來說,套接字是平等的,并沒有差別,不因?yàn)樵诜?wù)器端或在客戶端而產(chǎn)生不同級(jí)別。不管是Socket還是ServerSocket它們的工作都是通過SocketImpl類及其子類完成的。接著了解下Socket和ServerSocket的構(gòu)造方法。
Socket
Socket的構(gòu)造方法:
Socket(InetAddress address,int port); //創(chuàng)建一個(gè)流套接字并將其連接到指定 IP 地址的指定端口號(hào) Socket(String host,int port); //創(chuàng)建一個(gè)流套接字并將其連接到指定主機(jī)上的指定端口號(hào) Socket(InetAddress address,int port, InetAddress localAddr,int localPort); //創(chuàng)建一個(gè)套接字并將其連接到指定遠(yuǎn)程地址上的指定遠(yuǎn)程端口 Socket(String host,int port, InetAddress localAddr,int localPort); //創(chuàng)建一個(gè)套接字并將其連接到指定遠(yuǎn)程主機(jī)上的指定遠(yuǎn)程端口 Socket(SocketImpl impl); //使用用戶指定的 SocketImpl 創(chuàng)建一個(gè)未連接 Socket
參數(shù)含義:
- address 雙向連接中另一方的IP地址
- port 端口號(hào)
- localPort 本地主機(jī)端口號(hào)
- localAddr 本地機(jī)器地址
- impl 是socket的父類,既可以用來創(chuàng)建serverSocket又可以用來創(chuàng)建Socket
注意:我們?cè)谶x取端口號(hào)的時(shí)候需要特別注意,每一個(gè)端口提供一種特定的服務(wù),只有給出正確的端口,才能獲得相應(yīng)的服務(wù)。0~1023的端口號(hào)為系統(tǒng)所保留,例如http服務(wù)的端口號(hào)為80,telnet服務(wù)的端口號(hào)為21,ftp服務(wù)的端口號(hào)為23。本文選取的端口號(hào)為30003
Socket的幾個(gè)重要方法:
public InputStream getInputStream(); //方法獲得網(wǎng)絡(luò)連接輸入,同時(shí)返回一個(gè)IutputStream對(duì)象實(shí)例 public OutputStream getOutputStream(); //方法連接的另一端將得到輸入,同時(shí)返回一個(gè)OutputStream對(duì)象實(shí)例 public Socket accept(); //用于產(chǎn)生"阻塞",直到接受到一個(gè)連接,并且返回一個(gè)客戶端的Socket對(duì)象實(shí)例。
對(duì)流的操作,操作完記得處理和關(guān)閉。以及對(duì)流異常的處理。
ServerSocket
ServerSocket的構(gòu)造方法:
ServerSocket(int port); //創(chuàng)建綁定到特定端口的服務(wù)器套接字 ServerSocket(int port,int backlog); //利用指定的 backlog 創(chuàng)建服務(wù)器套接字并將其綁定到指定的本地端口號(hào) ServerSocket(int port,int backlog, InetAddress bindAddr); //使用指定的端口、偵聽 backlog 和要綁定到的本地 IP地址創(chuàng)建服務(wù)器
接著我們一起來看看案例。
發(fā)送和接收消息
首先來實(shí)現(xiàn)一個(gè)簡單的案例,服務(wù)器端一直監(jiān)聽某個(gè)端口,等待客戶端連接請(qǐng)求。客戶端根據(jù)IP地址和端口號(hào)連接服務(wù)器端,接著客服端通過控制臺(tái)向服務(wù)端發(fā)送消息,服務(wù)端接收到消息并且展示出來。下面來看看具體實(shí)現(xiàn)的步驟:
ClientSocket客服端:
try { Socket socket = new Socket("173.1.1.121", 30004); //獲取控制臺(tái)輸入的內(nèi)容 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); System.out.print("請(qǐng)輸入發(fā)送的字符串:"); String str = bufferedReader.readLine(); //給服務(wù)端發(fā)送消息 PrintWriter printWriter = new PrintWriter(socket.getOutputStream()); printWriter.write(str + "\r\n"); printWriter.flush(); //關(guān)閉資源 bufferedReader.close(); printWriter.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); }
Socket兩個(gè)參數(shù)分別是IP地址和端口號(hào),可以通過以下代碼獲取IP地址:
InetAddress ia = null; try { ia = ia.getLocalHost(); String localname = ia.getHostName(); String localip = ia.getHostAddress(); System.out.println("本機(jī)名稱是:" + localname); System.out.println("本機(jī)的ip是 :" + localip); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }
注意:關(guān)閉多余的網(wǎng)絡(luò)適配,只保留當(dāng)前的網(wǎng)絡(luò)連接。關(guān)閉防火墻,安全軟件。不然可能導(dǎo)致連接不上。
ServerSocket服務(wù)端:
try { mServerSocket = new java.net.ServerSocket(30004); Socket socket = mServerSocket.accept(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String content = null; while ((content=bufferedReader.readLine() )!= null) { System.out.println("接收到客服端發(fā)來的消息:" +content); } //關(guān)閉連接 bufferedReader.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); }
服務(wù)端和客服端的端口號(hào)必須保持一致。下面我們運(yùn)行兩個(gè) java 程序看看效果:
可能有些童鞋還不知道,怎么在AndroidStudio下面運(yùn)行java程序,請(qǐng)看下面截圖:
傳輸文件
Socket只能通過流去讀取消息,傳輸文件需要解決文章開始提出的問題, 如何判斷socket接收的數(shù)據(jù)是字符串還是流?
定義協(xié)議
為了保證接收到的數(shù)據(jù)類型統(tǒng)一(數(shù)據(jù)是字符串還是流),需要定義協(xié)議。定義協(xié)議的方式有很多種:
- 發(fā)送一個(gè)握手信號(hào)。 根據(jù)握手信號(hào)來確定發(fā)送的是字符串還是流
- 定義了Header(頭)和Body(實(shí)體),頭是固定大小的,用來告訴接收者數(shù)據(jù)的格式、用途、長度等信息,接收者根據(jù)Header來接受Body。
- 自定義協(xié)議
我這里采用的自定義協(xié)議,原理跟前面兩種類似。我傳輸?shù)氖荍SON數(shù)據(jù),根據(jù)字段標(biāo)識(shí)傳輸?shù)氖亲址€是流,接收者根據(jù)標(biāo)識(shí)去解析數(shù)據(jù)即可。
協(xié)議的實(shí)體類(Transmission):
//文件名稱 public String fileName; //文件長度 public long fileLength; //傳輸類型 public int transmissionType; //傳輸內(nèi)容 public String content; //傳輸?shù)拈L度 public long transLength; //發(fā)送還是接受類型 1發(fā)送 2接收 public int itemType = 1; //0 文本 1 圖片 public int showType;
根據(jù)字段transmissionType去標(biāo)識(shí)傳輸(序列化)或接收(反序列化)的類型。傳輸?shù)倪^程中始終都是以JSON的格式存在的。傳輸文件時(shí)需要把流轉(zhuǎn)換成字符串(方式很多種我用的是Base64加密與解密)。
客戶端(ClientThread)
客戶端發(fā)送文件的代碼如下:
/** * 文件路徑 * * @param filePath */ private void sendFile(String filePath) { FileInputStream fis = null; File file = new File(filePath); try { mSendHandler.sendEmptyMessage(Constants.PROGRESS); fis = new FileInputStream(file); Transmission trans = new Transmission(); trans.transmissionType = Constants.TRANSFER_FILE; trans.fileName = file.getName(); trans.fileLength = file.length(); trans.transLength = 0; byte[] bytes = new byte[1024]; int length = 0; while ((length = fis.read(bytes, 0, bytes.length)) != -1) { trans.transLength += length; trans.content = Base64Utils.encode(bytes); mPrintWriter.write(mGson.toJson(trans) + "\r\n"); mPrintWriter.flush(); //更新進(jìn)度 Message message = new Message(); message.what = Constants.PROGRESS; message.obj = 100 * trans.transLength / trans.fileLength; mSendHandler.sendMessage(message); } fis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); if (fis != null) { try { fis.close(); } catch (IOException e1) { e1.printStackTrace(); } } } catch (IOException e) { e.printStackTrace(); mPrintWriter.close(); } }
文章結(jié)尾處我會(huì)附上源碼。
trans.content = Base64Utils.encode(bytes); mPrintWriter.write(mGson.toJson(trans) + "\r\n"); mPrintWriter.flush();
把字節(jié)流轉(zhuǎn)換成字符串傳輸Base64Utils.encode(bytes),接收方把字符串解析成字節(jié)流并寫入文件。
注意:在Android程序中運(yùn)行,記得添加網(wǎng)絡(luò)文件讀寫的權(quán)限。
服務(wù)端(ServerThread)
服務(wù)端接收文件的代碼如下:
long fileLength = trans.fileLength; long transLength = trans.transLength; if (mCreateFile) { mCreateFile = false; fos = new FileOutputStream(new File("d:/" + trans.fileName)); } byte[] b = Base64Utils.decode(trans.content.getBytes()); fos.write(b, 0, b.length); System.out.println("接收文件進(jìn)度" + 100 * transLength / fileLength + "%..."); if (transLength == fileLength) { mCreateFile = true; fos.flush(); fos.close(); }
服務(wù)端接收到文件,并存儲(chǔ)到了d盤。注意文件傳輸結(jié)束后關(guān)閉流。
源碼地址:SocketDemo_jb51.rar
下載源碼后,請(qǐng)先替換Constants類中HOST地址,然后運(yùn)行MyServer的java程序,最后運(yùn)行MainActivity的Android程序。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 如何正確實(shí)現(xiàn)Android啟動(dòng)屏畫面的方法(避免白屏)
- Android編程之簡單啟動(dòng)畫面實(shí)現(xiàn)方法
- Android簡單實(shí)現(xiàn)啟動(dòng)畫面的方法
- Android編程中調(diào)用Camera時(shí)預(yù)覽畫面有旋轉(zhuǎn)問題的解決方法
- Android啟動(dòng)畫面的實(shí)現(xiàn)方法
- Android開機(jī)畫面的具體修改方法
- 詳解Android——藍(lán)牙技術(shù) 帶你實(shí)現(xiàn)終端間數(shù)據(jù)傳輸
- Android實(shí)時(shí)獲取攝像頭畫面?zhèn)鬏斨罰C端思路詳解
相關(guān)文章
Android解析相同接口返回不同格式j(luò)son數(shù)據(jù)的方法
這篇文章主要介紹了Android解析相同接口返回不同格式j(luò)son數(shù)據(jù)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08Android NestedScrolling嵌套滾動(dòng)的示例代碼
這篇文章主要介紹了Android NestedScrolling嵌套滾動(dòng)的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05Android仿淘寶view滑動(dòng)至屏幕頂部會(huì)一直停留在頂部的位置
這篇文章主要介紹了Android仿淘寶view滑動(dòng)至屏幕頂部會(huì)一直停留在頂部的位置的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11Android中button實(shí)現(xiàn)onclicklistener事件的兩種方式
本文介紹下Android中button實(shí)現(xiàn)onclicklistener事件的兩種方法,感興趣的朋友可以參考下2013-04-04