前言
socket是套接字,是一個(gè)對(duì) TCP / IP協(xié)議進(jìn)行封裝的編程調(diào)用接口普气,嗯谜疤。。现诀。比較官方規(guī)范的介紹方式夷磕,但如果你剛接觸網(wǎng)絡(luò)編程時(shí),看到這個(gè)解釋可能會(huì)想說(shuō)仔沿,誰(shuí)他媽知道套接字是個(gè)什么鬼坐桩。感覺(jué)就像用原版的英語(yǔ)詞典查一個(gè)生詞,單詞的解釋是一句英語(yǔ)封锉,有幾個(gè)詞不認(rèn)識(shí)绵跷,得遞歸查,內(nèi)心神獸奔騰而過(guò)~-~成福。
簡(jiǎn)單介紹一下socket相關(guān)知識(shí)點(diǎn):
1.協(xié)議:
可以先簡(jiǎn)單理解為約定,比如usb接口,定義了usb的各種標(biāo)準(zhǔn),包括它的基本外觀,尺寸,和其他什么參數(shù),然后所有需要usb接口的產(chǎn)品都按照約定來(lái)做,這樣子,隨便買根數(shù)據(jù)線,在哪里都能傳輸數(shù)據(jù),充電,正常工作碾局。那么就可以理解,TCP和UDP作為一種網(wǎng)絡(luò)運(yùn)輸層的協(xié)議奴艾,對(duì)于兩個(gè)網(wǎng)絡(luò)設(shè)備净当,如果都實(shí)現(xiàn)了該協(xié)議,那么在該協(xié)議的基礎(chǔ)上蕴潦,設(shè)備之間的網(wǎng)路運(yùn)輸層就可以正常交互像啼。
2.Socket和Http:
網(wǎng)絡(luò)分為5層,最上面兩層分別是應(yīng)用層和運(yùn)輸層潭苞,TCP和UDP是實(shí)現(xiàn)了運(yùn)輸層的協(xié)議忽冻,而Socket是對(duì)TCP和UDP協(xié)議進(jìn)行了封裝,方便上層調(diào)用此疹,HTTP屬于應(yīng)用層甚颂,他們不在一個(gè)層級(jí),本不具備可比性秀菱,運(yùn)輸層協(xié)議是為了約定數(shù)據(jù)傳輸?shù)姆绞秸裎埽瑧?yīng)用層協(xié)議是為了解決數(shù)據(jù)的包裝方式。
舉個(gè)栗子:
從學(xué)校前門到學(xué)校后門衍菱,我們需要送一千本書過(guò)去赶么。
- 這里書就是要傳輸?shù)臄?shù)據(jù),前門和后門相當(dāng)于需要通信的客戶端和服務(wù)端脊串。
- 出于運(yùn)輸速度和人力資源分配的考慮辫呻,我們約定書要分成最多100本一捆的格式去運(yùn)輸比較高效清钥,這一千本書要按順序分成1~100,101~200放闺。祟昭。。901~1000怖侦,這樣子依次送篡悟,這就是運(yùn)輸?shù)膮f(xié)議。
- 那么在學(xué)校后門這里匾寝,因?yàn)槲抑缽那伴T送過(guò)來(lái)的書是每捆100本按順序依次送過(guò)來(lái)搬葬,那么我把這些書按照收到的順序依次拼接起來(lái),那么就保證了些書原本的順序艳悔,即數(shù)據(jù)的完整性急凰,有序性沒(méi)有被破環(huán)。前門運(yùn)輸之前猜年,書是什么樣子抡锈,后門這里拿到后還是什么樣子。
- 而如果我們約定這些書是用紙箱裝還是用車拖乔外,學(xué)校的后門的地理位置在哪企孩,送到后門后如果沒(méi)有被及時(shí)領(lǐng)走的話,應(yīng)該保存多久袁稽,到期了是扔掉還是怎么處理勿璃。這些除數(shù)據(jù)內(nèi)容本身以外的約定,Http協(xié)議在解決的問(wèn)題推汽。
- 如果一定要把Socket和Http拿來(lái)比較补疑,差別就是在工作方式上,Http通信中只有客戶端向服務(wù)端請(qǐng)求了數(shù)據(jù)歹撒,服務(wù)端才能響應(yīng)并發(fā)數(shù)據(jù)給客戶端莲组,而Socket通信中,只要客戶端和服務(wù)端之間建立了連接暖夭,那么在客戶端沒(méi)有請(qǐng)求數(shù)據(jù)的情況下锹杈,服務(wù)端可以主動(dòng)向客戶端發(fā)送數(shù)據(jù)。
3.TCP和UDP
其實(shí)在網(wǎng)絡(luò)編程中迈着,TCP的更為常用一些竭望,這里簡(jiǎn)單聊一下區(qū)別
TCP 3次握手舉例:
Client:“Service,我要連”
Service: “好裕菠,我知道你要連咬清,同意Client連接”
Client:“哦,我知道你知道我要連”
-
TCP:面向連接、面向字節(jié)流旧烧、雙向通信影钉、 可靠
-
面向連接:TCP在客戶端和服務(wù)端數(shù)據(jù)交互之前,有一個(gè)3次握手確認(rèn)連接的過(guò)程掘剪,只有客戶端和服務(wù)端都確認(rèn)了彼此的連接狀態(tài)平委,才會(huì)開(kāi)始傳輸數(shù)據(jù)。
- 以上夺谁,就可以建立連接通道了廉赔,這樣的好處是避免網(wǎng)絡(luò)延時(shí)等情況下,Client發(fā)送連接請(qǐng)求后予权,Service沒(méi)收到,而等到Client已經(jīng)不需要和Service通信后浪册,Service才收到連接請(qǐng)求扫腺,如果直接就這樣子建立通道了,會(huì)造成資源浪費(fèi)村象。
- 即然說(shuō)到連接的3次握手笆环,那就有必要提一下TCP斷開(kāi)連接的4次回收,即任一方發(fā)送“我要斷開(kāi)連接的通知”厚者,接收方回復(fù)“已經(jīng)知曉你斷開(kāi)連接了”躁劣,4次是因?yàn)槿我环蕉伎梢园l(fā)送斷開(kāi)連接的消息。
- 面向字節(jié)流:流是字符序列库菲。對(duì)于TCP而言账忘,傳輸?shù)膱?bào)文長(zhǎng)度有最大限制,對(duì)于更大的數(shù)據(jù)而言熙宇,就必須把該數(shù)據(jù)分割成一塊塊的數(shù)據(jù)塊鳖擒,全部傳輸完成后,在拼接成原始數(shù)據(jù)烫止。
- 可靠:按順序傳輸數(shù)據(jù)蒋荚、不丟失、不重復(fù)
-
面向連接:TCP在客戶端和服務(wù)端數(shù)據(jù)交互之前,有一個(gè)3次握手確認(rèn)連接的過(guò)程掘剪,只有客戶端和服務(wù)端都確認(rèn)了彼此的連接狀態(tài)平委,才會(huì)開(kāi)始傳輸數(shù)據(jù)。
-
UDP:無(wú)連接的馆蠕、面向報(bào)文期升、不可靠
- 無(wú)連接、不可靠:不需要像TCP那樣互躬,建立連接后通訊播赁,它只需要數(shù)據(jù)要送到哪里去,就可以開(kāi)始傳輸數(shù)據(jù)吼渡,至于數(shù)據(jù)是否丟失行拢,一概不管
- 面向報(bào)文:數(shù)據(jù)有多大,UDP就一次性傳輸多大,不做切割數(shù)據(jù)的動(dòng)作
使用方式:
-
客戶端實(shí)現(xiàn)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client_tcp);
ButterKnife.bind(this);
//線程池
pools = Executors.newCachedThreadPool();
//啟動(dòng)服務(wù)端
startService(new Intent(this, ServiceTcp.class));
}
這里我們創(chuàng)建一個(gè)Activity命名為ClientTcp左為客戶端
方便起見(jiàn)舟奠,用線程池替代創(chuàng)建線程執(zhí)行任務(wù)
pools.execute(new Runnable() {
@Override
public void run() {
//在連接成功以前,會(huì)循環(huán)嘗試連接知道成功為止
while (mSocket == null || !mSocket.isConnected()) {
try {
//這里構(gòu)造socket傳入ip和端口號(hào),拿到socket對(duì)象
mSocket = new Socket("localhost", 2000);
} catch (final IOException e) {
e.printStackTrace();
//toast要切換到主線程中執(zhí)行
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ClientTcp.this, "連接失敗" + "\r\n" + "一秒后重連" + "\r\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
try {
Thread.sleep(1000); //延時(shí)1秒重連
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
try {
//注意,在上面連接成功以后代碼才會(huì)走到這里,否則在while循環(huán)那里是阻塞的狀態(tài)
br = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
os = mSocket.getOutputStream();//初始化輸入輸出流
} catch (IOException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ClientTcp.this, "連接成功", Toast.LENGTH_SHORT).show();
}
});
//死循環(huán)接受消息
acceptMessage(br);
}
});
在子線程我,我們嘗試連接Service段socket,做了連接失敗后重新連接的處理以及輸入輸出流的初始化,接下來(lái),我們看看acceptMessage(br)方法做了在接收數(shù)據(jù)時(shí)做了怎樣的處理
private void acceptMessage(BufferedReader br) {
while (mSocket.isConnected()){
try {
while (!br.ready()){} //當(dāng)流中沒(méi)有數(shù)據(jù)的時(shí)候,會(huì)一直阻塞在這里
final String response = br.readLine();
runOnUiThread(new Runnable() {
@Override
public void run() {
//將用以標(biāo)記換行的符號(hào)"~~"還原,并切換到主線程中顯示
String[] split = response.split("~~");
mShowMessage.setText(split[0] + "\n" + split[1]);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
當(dāng)socket中獲取到的輸入流中沒(méi)有數(shù)據(jù)時(shí),下面的代碼不會(huì)執(zhí)行,一旦有數(shù)據(jù),就會(huì)調(diào)用readLine()方法讀出字符,這里做了一個(gè)小處理:服務(wù)端把接受的詳細(xì)acceptMessage和要回復(fù)的消息responseMessage以acceptMessage + "\n" + responseMessage憑借換行符后發(fā)給客戶端,但是客戶端在執(zhí)行readLinea()讀取數(shù)據(jù)時(shí),讀到"\n"便會(huì)停止讀取,如果沒(méi)有讀到取"\n",便會(huì)一直阻塞 在這里,這也是為什么客戶端和服務(wù)端在要發(fā)送的消息字符串的末尾都會(huì)添加"\n"竭缝。所以客戶端接收到服務(wù)端的消息后,將臨時(shí)定義的“~~”再替換回?fù)Q行符號(hào)沼瘫,讓textview顯示的時(shí)后能方便區(qū)分發(fā)送的和接受的消息
case R.id.sendMessage:
String str = mEt.getText().toString();
if (os != null && !TextUtils.isEmpty(str)){
try {
//在字符串末尾拼接換行符,避免服務(wù)端socket讀取數(shù)據(jù)時(shí)阻塞線程
os.write((str + "\r\n").getBytes());
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
break;
發(fā)送很簡(jiǎn)單,需要注意一下flush()和close的區(qū)別,close()會(huì)關(guān)閉掉流,在關(guān)閉掉之前會(huì)刷新一次流中的數(shù)據(jù),那么這個(gè)流接下來(lái)就不能使用了,而flush刷新后,可以繼續(xù)使用
-
服務(wù)端實(shí)現(xiàn)
@Override
public void onDestroy() {
super.onDestroy();
isServideTcpDestory = true;
}
private class TcpAcceptRunnable implements Runnable {
protected ServerSocket mServerSocket;
@Override
public void run() {
try {
mServerSocket = new ServerSocket(2000);
} catch (IOException e) {
e.printStackTrace();
}
while (!isServideTcpDestory) {
try {
//這里是一個(gè)阻塞方法,如果沒(méi)有客戶端請(qǐng)求連接,線程會(huì)停在這里等待
final Socket socket = mServerSocket.accept();
mThreadPools.execute(new Runnable() {
@Override
public void run() {
try {
responseTcpClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服務(wù)端,只要在服務(wù)開(kāi)啟時(shí),執(zhí)行這個(gè)Runnable就可以了,在服務(wù)ondestory時(shí),會(huì)將isServideTcpDestory值設(shè)置為true,從而終止while死循環(huán)抬纸。值得一提的是,new ServiceSocket(2000)會(huì)使該線程監(jiān)聽(tīng)自己的2000端口,這里的mServerSocket.accept()方法會(huì)產(chǎn)生一個(gè)socket,但知道有客戶端請(qǐng)求連接2000端口位置,線程會(huì)一直阻塞在這里。拿到和特定客戶端對(duì)應(yīng)的服務(wù)端Socket對(duì)象后耿戚,我們看看在子線程中responseTcpClient(socket)方法作了什么處理
private void responseTcpClient(Socket client) throws IOException {
//操作流
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
String acceptResponse = "";
while (!isServideTcpDestory) {
while (!br.ready()) { }
acceptResponse = br.readLine();
String responseStr = mStrings[new Random().nextInt(mStrings.length)];
//因?yàn)閞eadLine()方法在讀到換行符之前會(huì)一直等待,這里用"~~"代替換行,在clientTcp中拿到數(shù)據(jù)再替換成換行符設(shè)置給textview
bw.write("sendMessage: " + acceptResponse + "~~" + "receiveResponse: " + responseStr + "\r\n");
bw.flush();
}
br.close();
bw.close();
client.close();
}
看起來(lái)湿故,和在客戶端的acceptMessage(BufferedReader br)方法中并沒(méi)有什么差別
我們來(lái)看看最后的效果