Android與服務器的通訊方式主要有兩種:html
二者的最大差別在於:java
Http
鏈接使用的是「請求-響應方式」,即在請求時創建鏈接通道,當客戶端向服務器發送請求後,服務端才能向客戶端返回數據。android
Socket
通訊則是在雙方創建鏈接後,能夠直接進行數據的傳輸,在鏈接時可實現信息的主動推送,而不須要每次由客戶端向服務器發送請求。數組
那麼,什麼是socket?bash
socket又稱套接字,在程序內部提供了與外界通訊的端口,即端口通訊。服務器
經過創建socket
鏈接,可爲通訊雙方的數據傳輸提供通道。socket的主要特色有數據丟失率低,使用簡單且易於移植。markdown
socket
是一種抽象層,應用程序經過它來發送和接受數據,使用Socket能夠將應用程序添加到網絡中,與處於同一網絡中的其餘應用程序進行通訊。網絡
簡單來講,Socket提供了程序內部與外界通訊的端口併爲通訊雙方提供數據傳輸通道。app
根據不一樣的底層協議,Socket的實現是多樣化的。在這主要介紹TCP/IP協議簇當中主要的Socket類型爲流套接字(streamsocket
)和數據報套接字(datagramsocket
)。socket
流套接字將TCP
做爲其端對端協議,提供了一個可信賴的字節流服務。
數據報嵌套字使用UDP
協議,提供數據打包發送數據。
服務端首先聲明一個ServerSocket
對象而且指定端口號,而後調用Serversocket的accept()
方法接受客戶端的數據。
accept()方法在沒有數據進行接受時處於堵塞狀態。(Socket socket = serversocket.accept()
),一旦接受數據,經過inputstream
讀取接受的數據。
客戶端建立一個Socket對象,執行服務器端的ip地址和端口號(Socket socket = new Socket("172.168.10.108", 8080);
),經過inputstream
讀取數據,獲取服務器發出的數據(OutputStream outputstream = socket.getOutputStream();
),最後將要發送的數據寫入到outputstream
便可進行TCP協議的socket數據傳輸。
服務器端首先建立一個DatagramSocket
對象,而且指定監聽端口。接下來建立一個空的DatagramSocket對象用於接收數據(byte data[] = new byte[1024]; DatagramSocket packet = new DatagramSocket(data, data.length);
),使用DatagramSocket
的receive()
方法接受客戶端發送的數據,receive()
與serversocket
的accept()
方法相似,在沒有數據進行接受時處於堵塞狀態。
客戶端也建立個DatagramSocket
對象,而且指定監聽的端口。接下來建立一個InetAddress
對象,這個對象相似於一個網絡的發送地址(InetAddress serveraddress = InetAddress.getByName("172.168.1.120")
)。定義要發送的一個字符串,建立一個DatagramPacket
對象,並指定要將該數據包發送到網絡對應的那個地址和端口號,最後使用DatagramSocket的對象的send()
發送數據。
(String str = "hello"; byte data[] = str.getByte(); DatagramPacket packet = new DatagramPacket(data, data.length, serveraddress, 4567); socket.send(packet);
)
protected void connectServerWithTCPSocket() { Socket socket; try {// 建立一個Socket對象,並指定服務端的IP及端口號 socket = new Socket("192.168.1.32", 1989); // 建立一個InputStream用戶讀取要發送的文件。 InputStream inputStream = new FileInputStream("e://a.txt"); // 獲取Socket的OutputStream對象用於發送數據。 OutputStream outputStream = socket.getOutputStream(); // 建立一個byte類型的buffer字節數組,用於存放讀取的本地文件 byte buffer[] = new byte[4 * 1024]; int temp = 0; // 循環讀取文件 while ((temp = inputStream.read(buffer)) != -1) { // 把數據寫入到OuputStream對象中 outputStream.write(buffer, 0, temp); } // 發送讀取的數據到服務端 outputStream.flush(); /** 或建立一個報文,使用BufferedWriter寫入,看你的需求 **/ // String socketData = "[2143213;21343fjks;213]"; // BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( // socket.getOutputStream())); // writer.write(socketData.replace("\n", " ") + "\n"); // writer.flush(); /************************************************/ } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } 複製代碼
public void ServerReceviedByTcp() { // 聲明一個ServerSocket對象 ServerSocket serverSocket = null; try { // 建立一個ServerSocket對象,並讓這個Socket在1989端口監聽 serverSocket = new ServerSocket(1989); // 調用ServerSocket的accept()方法,接受客戶端所發送的請求, // 若是客戶端沒有發送數據,那麼該線程就停滯不繼續 Socket socket = serverSocket.accept(); // 從Socket當中獲得InputStream對象 InputStream inputStream = socket.getInputStream(); byte buffer[] = new byte[1024 * 4]; int temp = 0; // 從InputStream當中讀取客戶端所發送的數據 while ((temp = inputStream.read(buffer)) != -1) { System.out.println(new String(buffer, 0, temp)); } serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } 複製代碼
protected void connectServerWithUDPSocket() { DatagramSocket socket; try { //建立DatagramSocket對象並指定一個端口號,注意,若是客戶端須要接收服務器的返回數據, //還須要使用這個端口號來receive,因此必定要記住 socket = new DatagramSocket(1985); //使用InetAddress(Inet4Address).getByName把IP地址轉換爲網絡地址 InetAddress serverAddress = InetAddress.getByName("192.168.1.32"); //Inet4Address serverAddress = (Inet4Address) Inet4Address.getByName("192.168.1.32"); String str = "[2143213;21343fjks;213]";//設置要發送的報文 byte data[] = str.getBytes();//把字符串str字符串轉換爲字節數組 //建立一個DatagramPacket對象,用於發送數據。 //參數一:要發送的數據 參數二:數據的長度 //參數三:服務端的網絡地址 參數四:服務器端端口號 DatagramPacket packet = new DatagramPacket(data, data.length ,serverAddress ,10025); socket.send(packet);//把數據發送到服務端。 } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } 複製代碼
public void ReceiveServerSocketData() { DatagramSocket socket; try { //實例化的端口號要和發送時的socket一致,不然收不到data socket = new DatagramSocket(1985); byte data[] = new byte[4 * 1024]; //參數一:要接受的data 參數二:data的長度 DatagramPacket packet = new DatagramPacket(data, data.length); socket.receive(packet); //把接收到的data轉換爲String字符串 String result = new String(packet.getData(), packet.getOffset(), packet.getLength()); socket.close();//不使用了記得要關閉 System.out.println("the number of reveived Socket is :" + flag + "udpData:" + result); } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } 複製代碼
public void ServerReceviedByUdp(){ //建立一個DatagramSocket對象,並指定監聽端口。(UDP使用DatagramSocket) DatagramSocket socket; try { socket = new DatagramSocket(10025); //建立一個byte類型的數組,用於存放接收到得數據 byte data[] = new byte[4*1024]; //建立一個DatagramPacket對象,並指定DatagramPacket對象的大小 DatagramPacket packet = new DatagramPacket(data,data.length); //讀取接收到得數據 socket.receive(packet); //把客戶端發送的數據轉換爲字符串。 //使用三個參數的String方法。參數一:數據包 參數二:起始位置 參數三:數據包長 String result = new String(packet.getData(),packet.getOffset() ,packet.getLength()); } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } 複製代碼
使用UDP方式,android端和服務器端接收能夠看出,其實android端和服務器端的發送和接受截然不同,只要端口號正確,相互通訊就沒有問題,TCP
使用的是流的方式發送,UDP
是以包的形式發送。
查看這部分代碼主要是爲了查看accept()
底層源碼實現阻塞等待的原理。
public Socket accept() throws IOException { if (isClosed()) throw new SocketException("Socket is closed"); if (!isBound()) throw new SocketException("Socket is not bound yet"); Socket s = new Socket((SocketImpl) null); implAccept(s); return s; } 複製代碼
在accept()
方法中調用implAccept()
方法
protected final void implAccept(Socket s) throws IOException { SocketImpl si = null; try { if (s.impl == null) s.setImpl(); else { s.impl.reset(); } si = s.impl; s.impl = null; si.address = new InetAddress(); si.fd = new FileDescriptor(); //核心代碼 getImpl().accept(si); ...... } catch (IOException e) { ...... } s.impl = si; s.postAccept(); } 複製代碼
然後調用PlainSocketImpl
類中的accept()
方法
protected synchronized void accept(SocketImpl s) throws IOException { if (s instanceof PlainSocketImpl) { // pass in the real impl not the wrapper. SocketImpl delegate = ((PlainSocketImpl)s).impl; delegate.address = new InetAddress(); delegate.fd = new FileDescriptor(); // 對應代碼 impl.accept(delegate); // set fd to delegate's fd to be compatible with older releases s.fd = delegate.fd; } else { // 對應代碼 impl.accept(s); } } 複製代碼
此處再調用抽象類abstracPlainSocketImpl
類中的accept()
方法
protected void accept(SocketImpl s) throws IOException {
acquireFD();
try {
socketAccept(s);
} finally {
releaseFD();
}
}
複製代碼
其中acquireFD()
方法的代碼以下:
/* * "Acquires" and returns the FileDescriptor for this impl * * A corresponding releaseFD is required to "release" the * FileDescriptor. */ //「獲取」並返回這個impl的文件描述符須要一個相應的releaseFD來「釋放」文件描述符。 FileDescriptor acquireFD() { synchronized (fdLock) { fdUseCount++; return fd; } } 複製代碼
然後再執行socketAccept()
方法
void socketAccept(SocketImpl s) throws IOException { int nativefd = checkAndReturnNativeFD(); if (s == null) throw new NullPointerException("socket is null"); int newfd = -1; InetSocketAddress[] isaa = new InetSocketAddress[1]; //等待阻塞代碼 if (timeout <= 0) { newfd = accept0(nativefd, isaa); } else { configureBlocking(nativefd, false); try { waitForNewConnection(nativefd, timeout); newfd = accept0(nativefd, isaa); if (newfd != -1) { configureBlocking(newfd, true); } } finally { configureBlocking(nativefd, true); } } /* Update (SocketImpl)s' fd '*/ fdAccess.set(s.fd, newfd); /* Update socketImpls remote port, address and localport */ InetSocketAddress isa = isaa[0]; s.port = isa.getPort(); s.address = isa.getAddress(); s.localport = localport; } 複製代碼
該部分即爲無請求時的阻塞代碼塊,逐一查看accetp0()
方法、configureBlocking()
方法等發現這些代碼使用native代碼實現,提升效率。
於是具體沒有找到accept()方法的阻塞機理
我的感受,configureBlocking()方法時等待阻塞的調用的方法,而accept0()方法是響應請求的方法。
(該部分純屬我的猜想,多是錯誤的,於是但願你們指教)
flush()
方法很簡單,就是刷新此輸出流並強制寫出任何已緩衝的輸出字節
/** * Flushes this output stream and forces any buffered output bytes * to be written out. The general contract of <code>flush</code> is * that calling it is an indication that, if any bytes previously * written have been buffered by the implementation of the output * stream, such bytes should immediately be written to their * intended destination. * <p> * If the intended destination of this stream is an abstraction provided by * the underlying operating system, for example a file, then flushing the * stream guarantees only that bytes previously written to the stream are * passed to the operating system for writing; it does not guarantee that * they are actually written to a physical device such as a disk drive. * <p> * The <code>flush</code> method of <code>OutputStream</code> does nothing. * * @exception IOException if an I/O error occurs. */ public void flush() throws IOException { } 複製代碼
最後附上Java測試socket,查看其底層源碼實現機制,由於一直嘗試網絡請求沒成功,只能debug一步一步查看底層實現機制。
感興趣能夠本身嘗試下,查看具體實現原理。
import java.io.*; import java.net.*; public class socketTest { public static void main(String[] args) throws UnknownHostException, IOException { Socket socket = setSocket(); if(socket == null) System.out.println("socket is null"); InputStream inputStream = new FileInputStream("d://haha.txt"); OutputStream outputStream = socket.getOutputStream(); byte buffer[] = new byte[4 * 1024]; int temp = 0; while((temp = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, temp); } outputStream.flush(); listener(); } public static Socket setSocket() throws IOException{ String ip = "127.0.0.5"; int port = 8080; Socket socket = new Socket(); //設置最長等待時間 socket.setSoTimeout(8000); //進行鏈接請求 socket.connect(new InetSocketAddress(ip, port)); return socket; } //服務端監聽方法 public static void listener() throws IOException{ //正常狀況下,此處的"9999"和客戶端中的端口號port須要相同,才能完成網絡請求, //由於測試不成功,可是又想查看實現原理,於是才這樣作!!! ServerSocket server = new ServerSocket(9999); Socket socket = null; int i = 0; while(true){ i++; socket = server.accept();//這也是個阻塞的方法來的 System.out.println("有" + i + "個用戶鏈接了服務器"); new Thread(new socketTest().new ServerDoThread(socket)).start(); } } class ServerDoThread implements Runnable { Socket socket; InputStream inputStream; public ServerDoThread(Socket socket) { this.socket = socket; try { this.inputStream = socket.getInputStream(); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { byte buffer[] = new byte[1024 * 4]; int temp = 0; try { while((temp = inputStream.read(buffer)) != -1) { System.out.println(new String(buffer, 0, temp)); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } 複製代碼