大家好,我是徐愛卿。博客地址:flutterall.com
引言
Android網(wǎng)絡(luò)編程一直都是我想記錄的一篇文章,由于種種原因,一直推遲,終于在在今天開始寫了。這是一個好的開始,O(∩_∩)O哈哈~。
網(wǎng)絡(luò)上有很多關(guān)于Android網(wǎng)絡(luò)編程的文章,我感覺沒有一個適當?shù)目偨Y(jié)合適我的。所以,今天我決定將Android網(wǎng)絡(luò)編程的系列文章做一個總結(jié),在這里與大家分享。
這幾篇系列文章總的分為兩大模塊:Socket編程與HTTP編程(關(guān)于在Android中的)。今天我們先來看看通過Socket編程實現(xiàn)的服務(wù)器與客戶端(我們這里是手機端)之間的通信。
這篇文章你能學到什么?
- 了解網(wǎng)絡(luò)通信的基本原理
- 學會最基礎(chǔ)的Socket通信原理(萬丈高樓平地起)
- 明白TCP協(xié)議與UDP協(xié)議的區(qū)別與適用場景
網(wǎng)絡(luò)編程基礎(chǔ)
TCP/IP協(xié)議
我們先看看從宏觀上來看兩臺機器是如何通信的。
我們通過QQ和服務(wù)器進行通信,都需要哪些東西呢?
兩臺主機進行通信,需要知道雙方電腦的的地址(也就是IP地址);知道兩個電腦的地址之后,我們還需要知道我發(fā)送到目的電腦的目的軟件(使用端口標記)。這樣兩臺電腦連接成功之后就可以進行通信了。
那么這些東西例如:目的地如何規(guī)定,發(fā)送的數(shù)據(jù)如何包裝,放到哪里?這中間就需要有各種協(xié)議。大家都使用這個協(xié)議,統(tǒng)一成一個規(guī)范,這樣符合這個規(guī)范的各種設(shè)備之間能夠進行兼容性的通信。
最為廣泛的的協(xié)議就是OSI協(xié)議和TCP/IP協(xié)議了,但是OSI協(xié)議較為繁瑣,未推廣(想了解的自己Google)。反而TCP/IP(transfer control protocol/internet protocol,傳輸控制協(xié)議/網(wǎng)際協(xié)議)協(xié)議簡單明了,得到現(xiàn)今的廣泛使用。
TCP/IP準確的說是一組協(xié)議,是很多協(xié)議的集合,是一組協(xié)議集合的簡稱。來看看:
| 名稱 | 協(xié)議 | 功能 | |
|---|---|---|---|
| 應(yīng)用層 | HTTP、Telnet、FTP、TFTP | 提供應(yīng)用程序網(wǎng)絡(luò)接口 | |
| 傳輸層 | TCP、UDP | 建立端到端的連接 | |
| 網(wǎng)絡(luò)層 | IP | 尋址和路由 | |
| 數(shù)據(jù)鏈路層 | Ethernet、802.3、PPP | 物理介質(zhì)訪問 | |
| 物理層 | 接口和電纜 | 二進制數(shù)據(jù)流傳輸 |
下面以QQ的數(shù)據(jù)傳輸為例子:

IP地址、端口
在上節(jié)中我們知道端到端的連接提到了幾個關(guān)鍵的字眼:IP地址、端口;
IP地址用來標記唯一的計算機位置,端口號用來標記一臺電腦中的不同應(yīng)用程序。
其中IP地址是32為二進制,例如:192.168.0.0.1等等,這個組合方式是一種協(xié)議拼起來的,詳情Google。
端口號范圍是065536,其中01023是系統(tǒng)專用,例如:
| 協(xié)議名稱 | 協(xié)議功能 | 默認端口號 |
|---|---|---|
| HTTP(HypertextTransfer Protocol)超文本傳輸協(xié)議 | 瀏覽網(wǎng)頁 | 80 |
| FTP(File TransferProtocol) 文件傳輸協(xié)議 | 用于網(wǎng)絡(luò)上傳輸文件 | 21 |
| TELNET | 遠程終端訪問 | 23 |
| POP3(Post OfficeProtocol) | 郵局協(xié)議版本 | 110 |
IP地址和端口號組成了我們的Socket,也就是“套接字”,Socket只是一個API。
Socket原理機制:
通信的兩端都有Socket
網(wǎng)絡(luò)通信其實就是Socket間的通信
數(shù)據(jù)在兩個Socket間通過IO傳輸
單獨的Socke是沒用任何作用的,基于一定的協(xié)議(比如:TCP、UDP協(xié)議)下的socket編程才能使得數(shù)據(jù)暢通傳輸,下面我們就開始吧。
基于TCP(傳輸控制協(xié)議)協(xié)議的Socket編程
以下將“基于TCP(傳輸控制協(xié)議)協(xié)議的Socket編程”簡稱為TCP編程
既然基于TCP,那么就有著它的一套代碼邏輯體系。我們只需要在Socket API的幫助下,使用TCP協(xié)議,就可以進行一個完整的TCP編程了。
主要API:
Socket,客戶端相關(guān)
- 構(gòu)造方法
public Socket(String host, int port) throws UnknownHostException, IOException
釋義:創(chuàng)建一個流套接字并將其連接到指定主機上的指定端口號(就是用來連接到host主機的port端口的) - 方法
|方法名稱 | 方法功能|
| ------------- :|-------------:|
|getInputStream()) | 拿到此套接字的輸入流,收到的數(shù)據(jù)就在這里 |
|getOutputStream()| 返回此套接字的輸出流。 要發(fā)送的數(shù)據(jù)放到這里|
ServerSocket,服務(wù)器相關(guān)
- 構(gòu)造方法
ServerSocket(int port)
釋義:創(chuàng)建服務(wù)端的監(jiān)聽port端口的套接字 - 方法
Socket accept() throws IOException偵聽并接受到此套接字的連接。此方法在連接傳入之前一直阻塞。服務(wù)端通過這個方法拿到與客戶端建立端到端的連接的socket。
總體流程圖示:
- TCP編程的服務(wù)端流程:
1.創(chuàng)建ServerSocket類對象-serverSocket
2.使用serverSocket開始一直阻塞監(jiān)聽,等待客戶端發(fā)送來數(shù)據(jù)并且得到socket
3.根據(jù)socket的輸入流讀取客戶端的數(shù)據(jù),根據(jù)socket的輸出流返回給客戶端數(shù)據(jù)
4.關(guān)閉socket以及IO流 - TCP編程的客戶端對象
1.創(chuàng)建客戶端的socket對象
2.使用客戶端的socket對象的輸出流發(fā)送給服務(wù)器數(shù)據(jù),使用客戶端的socket對象的輸入流得到服務(wù)端的數(shù)據(jù)
TCP編程
下面我們使用上面的TCP編程的流程來實現(xiàn):手機發(fā)送信息到服務(wù)器,服務(wù)器返回給我們數(shù)據(jù)。
服務(wù)端的話,這里使用eclipse。使用Eclipse新建一個Server.java來處理服務(wù)器端的邏輯??蛻舳说脑捠褂肁S來新建一個Client.java文件。然后運行服務(wù)器,在運行手機上的程序,從手機上發(fā)送一段內(nèi)容到服務(wù)器端接收。大概就是這里流程。

服務(wù)器端:
Code:
package com.hui;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try {
// 為了看流程,我就把所有的代碼都放在main函數(shù)里了,也沒有捕捉異常,直接拋出去了。實際開發(fā)中不可取。
// 1.新建ServerSocket對象,創(chuàng)建指定端口的連接
ServerSocket serverSocket = new ServerSocket(12306);
System.out.println("服務(wù)端監(jiān)聽開始了~~~~");
// 2.進行監(jiān)聽
Socket socket = serverSocket.accept();// 開始監(jiān)聽9999端口,并接收到此套接字的連接。
// 3.拿到輸入流(客戶端發(fā)送的信息就在這里)
InputStream is = socket.getInputStream();
// 4.解析數(shù)據(jù)
InputStreamReader reader = new InputStreamReader(is);
BufferedReader bufReader = new BufferedReader(reader);
String s = null;
StringBuffer sb = new StringBuffer();
while ((s = bufReader.readLine()) != null) {
sb.append(s);
}
System.out.println("服務(wù)器:" + sb.toString());
// 關(guān)閉輸入流
socket.shutdownInput();
OutputStream os = socket.getOutputStream();
os.write(("我是服務(wù)端,客戶端發(fā)給我的數(shù)據(jù)就是:"+sb.toString()).getBytes());
os.flush();
// 關(guān)閉輸出流
socket.shutdownOutput();
os.close();
// 關(guān)閉IO資源
bufReader.close();
reader.close();
is.close();
socket.close();// 關(guān)閉socket
serverSocket.close();// 關(guān)閉ServerSocket
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:
在使用TCP編程的時候,最后需要釋放資源,關(guān)閉socket(socket.close());關(guān)閉socket輸入輸出流(socket.shutdownInput()以及socket.shutdownOutput());關(guān)閉IO流(is.close() os.close())。需要注意的是:關(guān)閉socket的輸入輸出流需要放在關(guān)閉io流之前。因為, <u>**關(guān)閉IO流會同時關(guān)閉socket,一旦關(guān)閉了socket的,就不能再進行socket的相關(guān)操作了。而,只關(guān)閉socket輸入輸出流(socket.shutdownInput()以及socket.shutdownOutput())不會完全關(guān)閉socket,此時任然可以進行socket方面的操作。 **</u>所以要先調(diào)用socket.shutdownXXX,然后再調(diào)用io.close();
客戶端:
頁面文件沒什么好看的。然后就是點擊button的時候發(fā)送數(shù)據(jù),收到數(shù)據(jù)展示出來。我們這里主要看點擊按鈕時做的事情。
public void onClick(View view){
new Thread(){
@Override
public void run() {
super.run();
try {
//1.創(chuàng)建監(jiān)聽指定服務(wù)器地址以及指定服務(wù)器監(jiān)聽的端口號
Socket socket = new Socket("111.111.11.11", 12306);//111.111.11.11為我這個本機的IP地址,端口號為12306.
//2.拿到客戶端的socket對象的輸出流發(fā)送給服務(wù)器數(shù)據(jù)
OutputStream os = socket.getOutputStream();
//寫入要發(fā)送給服務(wù)器的數(shù)據(jù)
os.write(et.getText().toString().getBytes());
os.flush();
socket.shutdownOutput();
//拿到socket的輸入流,這里存儲的是服務(wù)器返回的數(shù)據(jù)
InputStream is = socket.getInputStream();
//解析服務(wù)器返回的數(shù)據(jù)
InputStreamReader reader = new InputStreamReader(is);
BufferedReader bufReader = new BufferedReader(reader);
String s = null;
final StringBuffer sb = new StringBuffer();
while((s = bufReader.readLine()) != null){
sb.append(s);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(sb.toString());
}
});
//3、關(guān)閉IO資源(注:實際開發(fā)中需要放到finally中)
bufReader.close();
reader.close();
is.close();
os.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
注意!
實際開發(fā)中的關(guān)閉IO資源需要放到finally中。這里主要是為了先理解TCP編程的socket通信。還有,上面講過的io.close()需要放到socket.showdownXX()后面。
關(guān)于new Socket("111.111.11.11", 12306),如何查看本機地址,自己百度哦~~~
整體運行結(jié)果如下:

在上圖中,我們手機端發(fā)送完一個請求后,服務(wù)端(Server)拿到數(shù)據(jù),解析數(shù)據(jù),返回給客戶端數(shù)據(jù),關(guān)閉所有資源,也就是服務(wù)器關(guān)閉了。這時,如果另一個客戶端再想跟服務(wù)器進行通信時,發(fā)現(xiàn)服務(wù)器已經(jīng)關(guān)閉了,無法與服務(wù)器再次進行通信。換句話說,只能跟服務(wù)器通信一次,服務(wù)端 只能支持單線程數(shù)據(jù)處理。也就是說,上面的服務(wù)器的代碼無法實現(xiàn)多線程編程,只能進行一次通信。
那么如果我們想實現(xiàn)server的多線程數(shù)據(jù)處理,使得server處理完我這個請求后不會關(guān)閉,任然可以處理其他客戶端的請求,怎么辦呢?
TCP的多線程編程
思路:
在上面例子中,我們執(zhí)行serversocket.accept()等待客戶端去連接,與客戶建立完連接后,拿到對應(yīng)的socket,然后進行相應(yīng)的處理。那么多個客戶端的請求,我們就一直不關(guān)閉ServerSocket,一直等待客戶端連接,一旦建立連接拿到socket,就可以吧這個socket放到單獨的線程中,從而實現(xiàn)這個建立連接的端到端通信的socket在自己單獨的線程中處理。這樣就能實現(xiàn)Socket的多線程處理。
- step1:
創(chuàng)建ServerThread,單獨處理拿到的socket,使得客戶端到服務(wù)器端的這個socket會話在一個單獨的線程中。
package com.hui;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
public class ServerThread extends Thread{
private Socket socket;
//在構(gòu)造中得到要單獨會話的socket
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
super.run();
InputStreamReader reader = null;
BufferedReader bufReader = null;
OutputStream os = null;
try {
reader = new InputStreamReader(socket.getInputStream());
bufReader = new BufferedReader(reader);
String s = null;
StringBuffer sb = new StringBuffer();
while((s = bufReader.readLine()) != null){
sb.append(s);
}
System.out.println("服務(wù)器:"+sb.toString());
//關(guān)閉輸入流
socket.shutdownInput();
//返回給客戶端數(shù)據(jù)
os = socket.getOutputStream();
os.write(("我是服務(wù)端,客戶端發(fā)給我的數(shù)據(jù)就是:"+sb.toString()).getBytes());
os.flush();
socket.shutdownOutput();
} catch (IOException e2) {
e2.printStackTrace();
} finally{//關(guān)閉IO資源
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bufReader != null){
try {
bufReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- step2:
創(chuàng)建MultiThreadServer
package com.hui;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class MultiThreadServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(12306);
//死循環(huán)
while(true){
System.out.println("MultiThreadServer~~~監(jiān)聽~~~");
//accept方法會阻塞,直到有客戶端與之建立連接
Socket socket = serverSocket.accept();
ServerThread serverThread = new ServerThread(socket);
serverThread.start();
}
} catch (IOException e) {
e.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}
}
}
下面我使用兩個手機,多次進行與服務(wù)器的連接,演示如下:
總體結(jié)果:


重要的事情說三遍!萬丈高樓平地起!萬丈高樓平地起?。∪f丈高樓平地起?。?!只有當我們明白了最底層的,知識才是最牢固的。上面的講解的是基于TCP協(xié)議的socket編程。而我們后來將要講的HTTP相關(guān)的大都是基于TCP/IP協(xié)議的。一個TCP/IP協(xié)議我們又不能直接使用,Socket可以說是TCP/IP協(xié)議的抽象與包裝,然后我們就可以做相對于TCP/IP的網(wǎng)絡(luò)通信與信息傳輸了。
UDP編程
上面我們講解了基于TCP協(xié)議的Socket編程,現(xiàn)在開始我們就開始講解基于UDP協(xié)議的Socket編程了。
UDP,是User Datagram Protocol,也就是用戶數(shù)據(jù)包協(xié)議。關(guān)鍵點在于“數(shù)據(jù)包”。主要就是把數(shù)據(jù)進行打包然后丟給目標,而不管目標是否接收到數(shù)據(jù)。主要的流程就是:<u>發(fā)送者打包數(shù)據(jù)(DatagramPacket)然后通過DatagramSocket發(fā)送,接收者收到數(shù)據(jù)包解開數(shù)據(jù)。</u>
主要API:
DatagramPacket,用來包裝發(fā)送的數(shù)據(jù)
構(gòu)造方法
發(fā)送數(shù)據(jù)的構(gòu)造
DatagramPacket(byte[] buf, int length,SocketAddress address)
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
用來將長度為 length 的包發(fā)送到指定主機上的指定端口號。length 參數(shù)必須小于等于 buf.length。接收數(shù)據(jù)的構(gòu)造:
public DatagramPacket(byte[] buf, int length)
用來接收長度為 length 的數(shù)據(jù)包。
DatagramSocket:
構(gòu)造方法
DatagramSocket()
構(gòu)造數(shù)據(jù)報套接字并將其綁定到 <u>本地主機上任何可用的端口 </u>。套接字將被綁定到通配符地址,IP 地址由內(nèi)核來選擇。
DatagramSocket(int port)
創(chuàng)建數(shù)據(jù)報套接字并將其綁定到<u>本地主機上的指定端口</u>。套接字將被綁定到通配符地址,IP 地址由內(nèi)核來選擇。
發(fā)送數(shù)據(jù)
send(DatagramPacket p)
從此套接字發(fā)送數(shù)據(jù)報包。DatagramPacket 包含的信息指示:將要發(fā)送的數(shù)據(jù)、其長度、遠程主機的 IP 地址和遠程主機的端口號。
接收數(shù)據(jù)
receive(DatagramPacket p)
從此套接字接收數(shù)據(jù)報包。當此方法返回時,DatagramPacket 的緩沖區(qū)填充了接收的數(shù)據(jù)。數(shù)據(jù)報包也包含發(fā)送方的 IP 地址和發(fā)送方機器上的端口號。
下面開始代碼了
客戶端
主要頁面與上面的tcp一致,只不過是通訊時的方法改了。如下:
private void udp() {
byte[] bytes = et.getText().toString().getBytes();
try {
/*******************發(fā)送數(shù)據(jù)***********************/
InetAddress address = InetAddress.getByName("192.168.232.2");
//1.構(gòu)造數(shù)據(jù)包
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, 12306);
//2.創(chuàng)建數(shù)據(jù)報套接字并將其綁定到本地主機上的指定端口。
DatagramSocket socket = new DatagramSocket();
//3.從此套接字發(fā)送數(shù)據(jù)報包。
socket.send(packet);
/*******************接收數(shù)據(jù)***********************/
//1.構(gòu)造 DatagramPacket,用來接收長度為 length 的數(shù)據(jù)包。
final byte[] bytes1 = new byte[1024];
DatagramPacket receiverPacket = new DatagramPacket(bytes1, bytes1.length);
socket.receive(receiverPacket);
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(new String(bytes1, 0, bytes1.length));
}
});
// socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
服務(wù)端
UDPServer
package com.hui;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
public class UDPServer {
public static void main(String[] args) throws IOException {
byte[] buf = new byte[1024];
// 一:接受數(shù)據(jù)
// 1.創(chuàng)建接受數(shù)據(jù)的數(shù)據(jù)包
DatagramPacket packet = new DatagramPacket(buf, buf.length);
// 2.創(chuàng)建UPD 的 socket
DatagramSocket socket = new DatagramSocket(12306);
// 3.接收數(shù)據(jù)
System.out.println("服務(wù)端開始監(jiān)聽!~~~~");
socket.receive(packet);
// 4.處理數(shù)據(jù)
System.out.println("服務(wù)端:" + new String(buf, 0, buf.length));
// 二:返回數(shù)據(jù)
DatagramPacket p = new DatagramPacket(buf, buf.length, packet.getAddress(), packet.getPort());
socket.send(p);
socket.close();
}
}

TCP與UDP區(qū)別與使用場景
至此,基于TCP、UDP協(xié)議的Socket通信已經(jīng)講完了基礎(chǔ)部分。那么這兩個協(xié)議在實際中有什么區(qū)別,分別適用于什么場景呢?
TCP
對于TCP的數(shù)據(jù)傳輸而言,傳輸數(shù)據(jù)之前需要進行三次握手建立穩(wěn)定的連接。建立連接通道后,數(shù)據(jù)包會在這個通道中以字節(jié)流的形式進行數(shù)據(jù)的傳輸。由于建立穩(wěn)定連接后才開始傳輸數(shù)據(jù),而同時還是以字節(jié)流的形式發(fā)送數(shù)據(jù),所以發(fā)送數(shù)據(jù)速度較慢,但是不會造成數(shù)據(jù)包丟失。即使數(shù)據(jù)包丟失了,會進行數(shù)據(jù)重發(fā)。同時,如果收到的數(shù)據(jù)包順序錯亂,會進行排序糾正。
三次握手??
這個網(wǎng)絡(luò)上的解釋太多了,想詳細了解的自行去百度上Google一下。<u>簡單理解</u>的就是這樣的:我家是農(nóng)村的,記得小時后爺爺在田里種地。到了晌午時間,奶奶快燒好飯后我都要去喊爺爺吃飯,因為干農(nóng)活的地離家里不遠不近的,我就跑到隔壁家里的平頂房上喊爺爺吃飯。我先大喊一聲“爺爺,回家吃飯啦”。爺爺如果聽到我說的話就會給我一個應(yīng)答“好的!知道了,馬上就回去,你們先吃吧!”我只有聽到了這句話,才知道爺爺這個時候能聽到我說的話,我然后就再次回答爺爺:“好的!那你快點!”這三句話說完,就確定了我能聽到爺爺?shù)膽?yīng)答,爺爺也能聽到我的回復(fù)。這樣我就確定我跟爺爺之間的喊話通道是正常的,如果還想對爺爺說什么話,直接說就好了。最后,爺爺聽到了我說的話,就不再回復(fù)我的話了,然后,拿起鋤頭回來了。
總結(jié)下來,就是面向連接、數(shù)據(jù)可靠,速度慢,有序的。
<u>適用于需要安全穩(wěn)定傳輸數(shù)據(jù)的場景。例如后面要講解的HTTP、HTTPS網(wǎng)絡(luò)協(xié)議,F(xiàn)TP文件傳輸協(xié)議以及POP、SMTP郵件傳輸協(xié)議?;蛘唛_發(fā)交易類、支付類等軟件時,都需要基于TCP協(xié)議的Socket連接進行安全可靠的數(shù)據(jù)傳輸?shù)鹊?lt;/u>
UDP
對于UDP的數(shù)據(jù)傳輸而言,UDP不會去建立連接。它不管目的地是否存在,直接將數(shù)據(jù)發(fā)送給目的地,同時不會過問發(fā)送的數(shù)據(jù)是否丟失,到達的數(shù)據(jù)是否順序錯亂。如果你想處理這些問題的話,需要自己在應(yīng)用層自行處理。
總結(jié)下來,不面向連接、數(shù)據(jù)不可靠、速度快、無序的。
<u>適用于需要實時性較高不較為關(guān)注數(shù)據(jù)結(jié)果的場景,例如:打電話、視頻會議、廣播電臺,等。</u>
_,最后的最后,歡迎拍磚。家里要蓋房子了,上海的房價傷不起~~~~
