????????剛給大家講解Java socket通信后梁钾,好多童鞋私信我,有好多地方不理解募判,看不明白。特抽時間整理一下咒唆,詳細講述Java socket通信原理和實現(xiàn)案例届垫。整個過程樓主都是通過先簡單明了的示例讓大家了解整個基本原理,后慢慢接近生產(chǎn)實用示例全释,先概況后脈絡給大家梳理出來的装处,所有涉及示例都可以直接拷貝運行。樓主才疏學淺,如有部分原理錯誤請大家及時指正.
請尊重作者勞動成果妄迁,轉(zhuǎn)載請標明原文鏈接:http://www.reibang.com/p/cde27461c226
? ? ? ? 整理和總結(jié)了一下大家常遇到的問題:
? ? ? ?1.? ? 客戶端socket發(fā)送消息后寝蹈,為什么服務端socket沒有收到?
? ? ? ? 2.? ? 使用while 循環(huán)實現(xiàn)連續(xù)輸入登淘,是不是就是多線程模式箫老?
? ? ? ? 3.? ? 對多線程處理機制不是很明白,希望詳細講解黔州?
? ? ? ? 4.? ? 希望詳細講解ServerSocketChannel和SocketChannel與ServerSoket和Socket的區(qū)別耍鬓?
? ? ? ? 5.? ? 希望有詳細的例子,可以直接拷貝下來運行流妻?
針對童鞋們提出的問題牲蜀,我會在本文章中詳細一一簡答,并且給出詳細的例子绅这,下面言歸正傳涣达。
一:socket通信基本原理。
首先socket 通信是基于TCP/IP 網(wǎng)絡層上的一種傳送方式证薇,我們通常把TCP和UDP稱為傳輸層峭判。
如上圖,在七個層級關(guān)系中棕叫,我們將的socket屬于傳輸層林螃,其中UDP是一種面向無連接的傳輸層協(xié)議。UDP不關(guān)心對端是否真正收到了傳送過去的數(shù)據(jù)俺泣。如果需要檢查對端是否收到分組數(shù)據(jù)包疗认,或者對端是否連接到網(wǎng)絡,則需要在應用程序中實現(xiàn)伏钠。UDP常用在分組數(shù)據(jù)較少或多播横漏、廣播通信以及視頻通信等多媒體領(lǐng)域。在這里我們不進行詳細討論熟掂,這里主要講解的是基于TCP/IP協(xié)議下的socket通信缎浇。
socket是基于應用服務與TCP/IP通信之間的一個抽象,他將TCP/IP協(xié)議里面復雜的通信邏輯進行分裝赴肚,對用戶來說素跺,只要通過一組簡單的API就可以實現(xiàn)網(wǎng)絡的連接。借用網(wǎng)絡上一組socket通信圖給大家進行詳細講解:
首先誉券,服務端初始化ServerSocket指厌,然后對指定的端口進行綁定,接著對端口及進行監(jiān)聽踊跟,通過調(diào)用accept方法阻塞踩验,此時,如果客戶端有一個socket連接到服務端,那么服務端通過監(jiān)聽和accept方法可以與客戶端進行連接箕憾。
二:socket通信基本示例:
在對socket通信基本原理明白后牡借,那我們就寫一個最簡單的示例,展示童鞋們常遇到的第一個問題:客戶端發(fā)送消息后袭异,服務端無法收到消息蓖捶。
服務端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
// 初始化服務端socket并且綁定9999端口
? ? ? ? ? ? ServerSocket serverSocket? =new ServerSocket(9999);
? ? ? ? ? ? //等待客戶端的連接
? ? ? ? ? ? Socket socket = serverSocket.accept();
? ? ? ? ? ? //獲取輸入流
? ? ? ? ? ? BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
? ? ? ? ? ? //讀取一行數(shù)據(jù)
? ? ? ? ? ? String str = bufferedReader.readLine();
? ? ? ? ? ? //輸出打印
? ? ? ? ? ? System.out.println(str);
? ? ? ? }catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
客戶端:
package socket.socket1.socket;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
Socket socket =new Socket("127.0.0.1",9999);
? ? ? ? ? ? BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
? ? ? ? ? ? String str="你好,這是我的第一個socket";
? ? ? ? ? ? bufferedWriter.write(str);
? ? ? ? }catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
啟動服務端:
發(fā)現(xiàn)正常扁远,等待客戶端的的連接
啟動客戶端:
發(fā)現(xiàn)客戶端啟動正常后,馬上執(zhí)行完后關(guān)閉刻像。同時服務端控制臺報錯:
服務端控制臺報錯:
然后好多童鞋畅买,就拷貝這個java.net.SocketException: Connection reset上王查異常,查詢解決方案细睡,搞了半天都不知道怎么回事谷羞。解決這個問題我們首先要明白,socket通信是阻塞的溜徙,他會在以下幾個地方進行阻塞湃缎。第一個是accept方法,調(diào)用這個方法后蠢壹,服務端一直阻塞在哪里嗓违,直到有客戶端連接進來。第二個是read方法图贸,調(diào)用read方法也會進行阻塞蹂季。通過上面的示例我們可以發(fā)現(xiàn),該問題發(fā)生在read方法中疏日。有朋友說是Client沒有發(fā)送成功偿洁,其實不是的,我們可以通debug跟蹤一下沟优,發(fā)現(xiàn)客戶端發(fā)送了涕滋,并且沒有問題。而是發(fā)生在服務端中挠阁,當服務端調(diào)用read方法后宾肺,他一直阻塞在哪里,因為客戶端沒有給他一個標識侵俗,告訴是否消息發(fā)送完成爱榕,所以服務端還在一直等待接受客戶端的數(shù)據(jù),結(jié)果客戶端此時已經(jīng)關(guān)閉了坡慌,就是在服務端報錯:java.net.SocketException: Connection reset
那么理解上面的原理后黔酥,我們就能明白,客戶端發(fā)送完消息后,需要給服務端一個標識跪者,告訴服務端味滞,我已經(jīng)發(fā)送完成了,服務端就可以將接受的消息打印出來片任。
? ? ? ? 通常大家會用以下方法進行進行結(jié)束:
socket.close() 或者調(diào)用socket.shutdownOutput();方法夺克。調(diào)用這倆個方法,都會結(jié)束客戶端socket忘衍。但是有本質(zhì)的區(qū)別逾苫。socket.close() 將socket關(guān)閉連接,那邊如果有服務端給客戶端反饋信息枚钓,此時客戶端是收不到的铅搓。而socket.shutdownOutput()是將輸出流關(guān)閉,此時搀捷,如果服務端有信息返回星掰,則客戶端是可以正常接受的。現(xiàn)在我們將上面的客戶端示例修改一下啊嫩舟,增加一個標識告訴流已經(jīng)輸出完畢:
客戶端2:
package socket.socket1.socket;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
Socket socket =new Socket("127.0.0.1",9999);
? ? ? ? ? ? BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
? ? ? ? ? ? String str="你好氢烘,這是我的第一個socket";
? ? ? ? ? ? bufferedWriter.write(str);
? ? ? ? ? ? //刷新輸入流
? ? ? ? ? ? bufferedWriter.flush();
? ? ? ? ? ? //關(guān)閉socket的輸出流
? ? ? ? ? ? socket.shutdownOutput();
? ? ? ? }catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
在看服務端控制臺:
服務端在接受到客戶端關(guān)閉流的信息后,知道信息輸入已經(jīng)完畢家厌,蘇哦有就能正常讀取到客戶端傳過來的數(shù)據(jù)播玖。通過上面示例,我們可以基本了解socket通信原理饭于,掌握了一些socket通信的基本api和方法黎棠,實際應用中,都是通過此處進行實現(xiàn)變通的镰绎。
三:while循環(huán)連續(xù)接受客戶端信息:
上面的示例中scoket客戶端和服務端固然可以通信脓斩,但是客戶端每次發(fā)送信息后socket就需要關(guān)閉,下次如果需要發(fā)送信息畴栖,需要socket從新啟動随静,這顯然是無法適應生產(chǎn)環(huán)境的需要。比如在我們是實際應用中QQ吗讶,如果每次發(fā)送一條信息燎猛,就需要重新登陸QQ,我估計這程序不是給人設計的照皆,那么如何讓服務可以連續(xù)給服務端發(fā)送消息重绷?下面我們通過while循環(huán)進行簡單展示:
服務端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
// 初始化服務端socket并且綁定9999端口
? ? ? ? ? ? ServerSocket serverSocket? =new ServerSocket(9999);
? ? ? ? ? ? //等待客戶端的連接
? ? ? ? ? ? Socket socket = serverSocket.accept();
? ? ? ? ? ? //獲取輸入流,并且指定統(tǒng)一的編碼格式
? ? ? ? ? ? BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
? ? ? ? ? ? //讀取一行數(shù)據(jù)
? ? ? ? ? ? String str;
? ? ? ? ? ? //通過while循環(huán)不斷讀取信息,
? ? ? ? ? ? while ((str = bufferedReader.readLine())!=null){
//輸出打印
? ? ? ? ? ? ? ? System.out.println(str);
? ? ? ? ? ? }
}catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
客戶端:
package socket.socket1.socket;
import java.io.*;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
//初始化一個socket
? ? ? ? ? ? Socket socket =new Socket("127.0.0.1",9999);
? ? ? ? ? ? //通過socket獲取字符流
? ? ? ? ? ? BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
? ? ? ? ? ? //通過標準輸入流獲取字符流
? ? ? ? ? ? BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
? ? ? ? ? while (true){
String str = bufferedReader.readLine();
? ? ? ? ? ? ? bufferedWriter.write(str);
? ? ? ? ? ? ? bufferedWriter.write("\n");
? ? ? ? ? ? ? bufferedWriter.flush();
? ? ? ? ? }
}catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
客戶端控制中心:
服務端控制中心:
大家可以看到膜毁,通過一個while 循環(huán)昭卓,就可以實現(xiàn)客戶端不間斷的通過標準輸入流讀取來的消息愤钾,發(fā)送給服務端。在這里有個細節(jié)候醒,大家看到?jīng)]有能颁,我客戶端沒有寫socket.close() 或者調(diào)用socket.shutdownOutput();服務端是如何知道客戶端已經(jīng)輸入完成了?服務端接受數(shù)據(jù)的時候是如何判斷客戶端已經(jīng)輸入完成呢倒淫?這就是一個核心點伙菊,雙方約定一個標識,當客戶端發(fā)送一個標識給服務端時敌土,表明客戶端端已經(jīng)完成一個數(shù)據(jù)的載入镜硕。而服務端在結(jié)束數(shù)據(jù)的時候,也通過這個標識進行判斷返干,如果接受到這個標識兴枯,表明數(shù)據(jù)已經(jīng)傳入完成,那么服務端就可以將數(shù)據(jù)度入后顯示出來犬金。
? ? ? ? 在上面的示例中,客戶端端在循環(huán)發(fā)送數(shù)據(jù)時候六剥,每發(fā)送一行晚顷,添加一個換行標識“\n”標識,在告訴服務端我數(shù)據(jù)已經(jīng)發(fā)送完成了疗疟。而服務端在讀取客戶數(shù)據(jù)時该默,通過while ((str = bufferedReader.readLine())!=null)去判斷是否讀到了流的結(jié)尾,負責服務端將會一直阻塞在哪里策彤,等待客戶端的輸入栓袖。
? ? ? ? 通過while方式,我們可以實現(xiàn)多個客戶端和服務端進行聊天店诗。但是裹刮,下面敲黑板,劃重點庞瘸。由于socket通信是阻塞式的捧弃,假設我現(xiàn)在有A和B倆個客戶端同時連接到服務端的上,當客戶端A發(fā)送信息給服務端后擦囊,那么服務端將一直阻塞在A的客戶端上违霞,不同的通過while循環(huán)從A客戶端讀取信息,此時如果B給服務端發(fā)送信息時瞬场,將進入阻塞隊列买鸽,直到A客戶端發(fā)送完畢,并且退出后贯被,B才可以和服務端進行通信眼五。簡單地說妆艘,我們現(xiàn)在實現(xiàn)的功能,雖然可以讓客戶端不間斷的和服務端進行通信弹砚,與其說是一對一的功能双仍,因為只有當客戶端A關(guān)閉后,客戶端B才可以真正和服務端進行通信桌吃,這顯然不是我們想要的朱沃。?下面我們通過多線程的方式給大家實現(xiàn)正常人類的思維。
四:多線程下socket編程
服務端:
package socket.socket1.socket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args)throws IOException {
// 初始化服務端socket并且綁定9999端口
? ? ? ? ? ? ServerSocket serverSocket? =new ServerSocket(9999);
? ? ? ? ? ? while (true){
//等待客戶端的連接
? ? ? ? ? ? ? ? Socket socket = serverSocket.accept();
? ? ? ? ? ? ? ? //每當有一個客戶端連接進來后茅诱,就啟動一個單獨的線程進行處理
? ? ? ? ? ? ? ? new Thread(new Runnable() {
@Override
? ? ? ? ? ? ? ? ? ? public void run() {
//獲取輸入流,并且指定統(tǒng)一的編碼格式
? ? ? ? ? ? ? ? ? ? ? ? BufferedReader bufferedReader =null;
? ? ? ? ? ? ? ? ? ? ? ? try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
? ? ? ? ? ? ? ? ? ? ? ? ? ? //讀取一行數(shù)據(jù)
? ? ? ? ? ? ? ? ? ? ? ? ? ? String str;
? ? ? ? ? ? ? ? ? ? ? ? ? ? //通過while循環(huán)不斷讀取信息逗物,
? ? ? ? ? ? ? ? ? ? ? ? ? ? while ((str = bufferedReader.readLine())!=null){
//輸出打印
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println("客戶端說:"+str);
? ? ? ? ? ? ? ? ? ? ? ? ? ? }
}catch (IOException e) {
e.printStackTrace();
? ? ? ? ? ? ? ? ? ? ? ? }
}
}).start();
? ? ? ? ? ? }
}
}
客戶端:
package socket.socket1.socket;
import java.io.*;
import java.net.Socket;
public class ClientSocket {
public static void main(String[] args) {
try {
//初始化一個socket
? ? ? ? ? ? Socket socket =new Socket("127.0.0.1",9999);
? ? ? ? ? ? //通過socket獲取字符流
? ? ? ? ? ? BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
? ? ? ? ? ? //通過標準輸入流獲取字符流
? ? ? ? ? ? BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
? ? ? ? ? while (true){
String str = bufferedReader.readLine();
? ? ? ? ? ? ? bufferedWriter.write(str);
? ? ? ? ? ? ? bufferedWriter.write("\n");
? ? ? ? ? ? ? bufferedWriter.flush();
? ? ? ? ? }
}catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
通過客戶端A控制臺輸入:
通過客戶端B控制臺輸入:
服務端控制臺:
通過這里我們可以發(fā)現(xiàn),客戶端A和客戶端B同時連接到服務端后瑟俭,都可以和服務端進行通信翎卓,也不會出現(xiàn)前面講到使用while(true)時候客戶端A連接時客戶端B不能與服務端進行交互的情況。在這里我們看到摆寄,主要是通過服務端的?new Thread(new Runnable() {}實現(xiàn)的失暴,每一個客戶端連接進來后,服務端都會單獨起個一線程微饥,與客戶端進行數(shù)據(jù)交互逗扒,這樣就保證了每個客戶端處理的數(shù)據(jù)是單獨的,不會出現(xiàn)相互阻塞的情況欠橘,這樣就基本是實現(xiàn)了QQ程序的基本聊天原理矩肩。
? ? ? ? 但是實際生產(chǎn)環(huán)境中,這種寫法對于客戶端連接少的的情況下是沒有問題肃续,但是如果有大批量的客戶端連接進行黍檩,那我們服務端估計就要歇菜了。假如有上萬個socket連接進來始锚,服務端就是新建這么多進程刽酱,反正樓主是不敢想,而且socket 的回收機制又不是很及時瞧捌,這么多線程被new 出來肛跌,就發(fā)送一句話,然后就沒有然后了察郁,導致服務端被大量的無用線程暫用衍慎,對性能是非常大的消耗,在實際生產(chǎn)過程中皮钠,我們可以通過線程池技術(shù)稳捆,保證線程的復用,下面請看改良后的服務端程序麦轰。
改良后的服務端:
package socket.socket1.socket;
import java.beans.Encoder;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServerSocketTest {
public static void main(String[] args)throws IOException {
// 初始化服務端socket并且綁定9999端口
? ? ? ? ServerSocket serverSocket =new ServerSocket(9999);
? ? ? ? //創(chuàng)建一個線程池
? ? ? ? ExecutorService executorService = Executors.newFixedThreadPool(100);
? ? ? ? while (true) {
//等待客戶端的連接
? ? ? ? ? ? Socket socket = serverSocket.accept();
? ? ? ? ? ? Runnable runnable = () -> {
BufferedReader bufferedReader =null;
? ? ? ? ? ? ? ? try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
? ? ? ? ? ? ? ? ? ? //讀取一行數(shù)據(jù)
? ? ? ? ? ? ? ? ? ? String str;
? ? ? ? ? ? ? ? ? ? //通過while循環(huán)不斷讀取信息乔夯,
? ? ? ? ? ? ? ? ? ? while ((str = bufferedReader.readLine()) !=null) {
//輸出打印
? ? ? ? ? ? ? ? ? ? ? ? System.out.println("客戶端說:" + str);
? ? ? ? ? ? ? ? ? ? }
}catch (IOException e) {
e.printStackTrace();
? ? ? ? ? ? ? ? }
};
? ? ? ? ? ? executorService.submit(runnable);
? ? ? ? }
}
}
運行后服務端控制臺:
通過線程池技術(shù)砖织,我們可以實現(xiàn)線程的復用。其實在這里executorService.submit在并發(fā)時末荐,如果要求當前執(zhí)行完畢的線程有返回結(jié)果時侧纯,這里面有一個大坑,在這里我就不一一詳細說明甲脏,具體我在我的另一篇文章中《把多線程說個透》里面詳細介紹眶熬。本章主要講述socket相關(guān)內(nèi)容。
在實際應用中块请,socket發(fā)送的數(shù)據(jù)并不是按照一行一行發(fā)送的娜氏,比如我們常見的報文,那么我們就不能要求每發(fā)送一次數(shù)據(jù)墩新,都在增加一個“\n”標識贸弥,這是及其不專業(yè)的,在實際應用中海渊,通過是采用數(shù)據(jù)長度+類型+數(shù)據(jù)的方式绵疲,在我們常接觸的熱Redis就是采用這種方式,
五:socket 指定長度發(fā)送數(shù)據(jù)
在實際應用中臣疑,網(wǎng)絡的數(shù)據(jù)在TCP/IP協(xié)議下的socket都是采用數(shù)據(jù)流的方式進行發(fā)送盔憨,那么在發(fā)送過程中就要求我們將數(shù)據(jù)流轉(zhuǎn)出字節(jié)進行發(fā)送,讀取的過程中也是采用字節(jié)緩存的方式結(jié)束朝捆。那么問題就來了般渡,在socket通信時候懒豹,我們大多數(shù)發(fā)送的數(shù)據(jù)都是不定長的芙盘,所有接受方也不知道此次數(shù)據(jù)發(fā)送有多長,因此無法精確地創(chuàng)建一個緩沖區(qū)(字節(jié)數(shù)組)用來接收脸秽,在不定長通訊中儒老,通常使用的方式時每次默認讀取8*1024長度的字節(jié),若輸入流中仍有數(shù)據(jù)记餐,則再次讀取驮樊,一直到輸入流沒有數(shù)據(jù)為止。但是如果發(fā)送數(shù)據(jù)過大時片酝,發(fā)送方會對數(shù)據(jù)進行分包發(fā)送囚衔,這種情況下或?qū)е陆邮辗脚袛噱e誤,誤以為數(shù)據(jù)傳輸完成雕沿,因而接收不全练湿。在這種情況下就會引出一些問題,諸如半包审轮,粘包肥哎,分包等問題辽俗,為了后續(xù)一些例子中好理解,我在這里直接將半包篡诽,粘包崖飘,分包概念性東西在寫一下(引用度娘)
5.1 半包
接受方?jīng)]有接受到一個完整的包,只接受了部分杈女。
原因:TCP為提高傳輸效率朱浴,將一個包分配的足夠大,導致接受方并不能一次接受完碧信。
影響:長連接和短連接中都會出現(xiàn)
5.2 粘包
發(fā)送方發(fā)送的多個包數(shù)據(jù)到接收方接收時粘成一個包赊琳,從接收緩沖區(qū)看,后一包數(shù)據(jù)的頭緊接著前一包數(shù)據(jù)的尾砰碴。
分類:一種是粘在一起的包都是完整的數(shù)據(jù)包躏筏,另一種情況是粘在一起的包有不完整的包
出現(xiàn)粘包現(xiàn)象的原因是多方面的:
1)發(fā)送方粘包:由TCP協(xié)議本身造成的,TCP為提高傳輸效率呈枉,發(fā)送方往往要收集到足夠多的數(shù)據(jù)后才發(fā)送一包數(shù)據(jù)趁尼。若連續(xù)幾次發(fā)送的數(shù)據(jù)都很少,通常TCP會根據(jù)優(yōu)化算法把這些數(shù)據(jù)合成一包后一次發(fā)送出去猖辫,這樣接收方就收到了粘包數(shù)據(jù)酥泞。
2)接收方粘包:接收方用戶進程不及時接收數(shù)據(jù),從而導致粘包現(xiàn)象啃憎。這是因為接收方先把收到的數(shù)據(jù)放在系統(tǒng)接收緩沖區(qū)芝囤,用戶進程從該緩沖區(qū)取數(shù)據(jù),若下一包數(shù)據(jù)到達時前一包數(shù)據(jù)尚未被用戶進程取走辛萍,則下一包數(shù)據(jù)放到系統(tǒng)接收緩沖區(qū)時就接到前一包數(shù)據(jù)之后悯姊,而用戶進程根據(jù)預先設定的緩沖區(qū)大小從系統(tǒng)接收緩沖區(qū)取數(shù)據(jù),這樣就一次取到了多包數(shù)據(jù)贩毕。
5.3分包
分包(1):在出現(xiàn)粘包的時候悯许,我們的接收方要進行分包處理;
分包(2):一個數(shù)據(jù)包被分成了多次接收辉阶;
原因:1. IP分片傳輸導致的先壕;2.傳輸過程中丟失部分包導致出現(xiàn)的半包;3.一個包可能被分成了兩次傳輸谆甜,在取數(shù)據(jù)的時候垃僚,先取到了一部分(還可能與接收的緩沖區(qū)大小有關(guān)系)。
影響:粘包和分包在長連接中都會出現(xiàn)
那么如何解決半包和粘包的問題规辱,就涉及一個一個數(shù)據(jù)發(fā)送如何標識結(jié)束的問題谆棺,通常有以下幾種情況
固定長度:每次發(fā)送固定長度的數(shù)據(jù);
特殊標示:以回車按摘,換行作為特殊標示包券;獲取到指定的標識時纫谅,說明包獲取完整。
字節(jié)長度:包頭+包長+包體的協(xié)議形式溅固,當服務器端獲取到指定的包長時才說明獲取完整付秕;
所以大部分情況下,雙方使用socket通訊時都會約定一個定長頭放在傳輸數(shù)據(jù)的最前端侍郭,用以標識數(shù)據(jù)體的長度询吴,通常定長頭有整型int,短整型short亮元,字符串Strinng三種形式猛计。
下面我們通過幾個簡單的小示例,演示發(fā)送接受定長數(shù)據(jù)爆捞,前面我們講過通過特殊標識的方式奉瘤,可是有什么我們發(fā)送的數(shù)據(jù)比較大,并且數(shù)據(jù)本身就會包含我們約定的特殊標識煮甥,那么我們在接受數(shù)據(jù)時盗温,就會出現(xiàn)半包的情況,通過這種情況下成肘,我們都是才有包頭+包長+包體的協(xié)議模式卖局,每次發(fā)送數(shù)據(jù)的時候,我們都會固定前4個字節(jié)為數(shù)據(jù)長度双霍,那到數(shù)據(jù)長度后砚偶,我們就可以非常精確的創(chuàng)建一個數(shù)據(jù)緩存區(qū)用來接收數(shù)據(jù)。
那么下面就先通過包類型+包長度+消息內(nèi)容定義一個socket通信對象洒闸,數(shù)據(jù)類型為byte類型染坯,包長度為int類型,消息內(nèi)容為byte類型顷蟀。
首先我們創(chuàng)建府服務端socket酒请。
package socket.socket1.socket5;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketTest {
public static void main(String[] args) {
try {
ServerSocket serverSocket =new ServerSocket(9999);
? ? ? ? ? ? ? ? Socket client = serverSocket.accept();
? ? ? ? ? ? ? ? InputStream inputStream = client.getInputStream();
? ? ? ? ? ? ? ? DataInputStream dataInputStream =new DataInputStream(inputStream);
? ? ? ? ? ? ? ? while (true){
????????????????????byte b = dataInputStream.readByte();
? ? ? ? ? ? ? ? ? ? int len = dataInputStream.readInt();
? ? ? ? ? ? ? ? ? ? byte[] data =new byte[len -5];
? ? ? ? ? ? ? ? ? ? dataInputStream.readFully(data);
? ? ? ? ? ? ? ? ? ? String str =new String(data);
? ? ? ? ? ? ? ? ? ? System.out.println("獲取的數(shù)據(jù)類型為:"+b);
? ? ? ? ? ? ? ? ? ? System.out.println("獲取的數(shù)據(jù)長度為:"+len);
? ? ? ? ? ? ? ? ? ? System.out.println("獲取的數(shù)據(jù)內(nèi)容為:"+str);
? ? ? ? ? ? ? ? }
}catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
在服務端創(chuàng)建后骡技,我們通過DataInputStream 數(shù)據(jù)流進行數(shù)據(jù)獲取鸣个,首先我們獲取數(shù)據(jù)的類型,然后在獲取數(shù)據(jù)的長度布朦,因為數(shù)據(jù)實際有效長度是整個數(shù)據(jù)的長度減去5囤萤,(包括前個字節(jié)為數(shù)據(jù)類型,前二到五個字節(jié)為數(shù)據(jù)長度)是趴。然后根據(jù)數(shù)據(jù)的實際有效長度創(chuàng)建數(shù)據(jù)緩存區(qū)涛舍,用戶存放數(shù)據(jù),這邊確保每次接接受數(shù)據(jù)的完整性唆途,不會出現(xiàn)半包與粘包的情況富雅。在數(shù)據(jù)讀取的時候掸驱,我們通過readFully()方法讀取數(shù)據(jù)。下面我們來創(chuàng)建socket的客戶端:
package socket.socket1.socket5;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class ClientSocketTest {
public static void main(String[] args) {
try {
Socket socket =new Socket("127.0.0.1",9999);
? ? ? ? ? ? OutputStream outputStream = socket.getOutputStream();
? ? ? ? ? ? DataOutputStream dataOutputStream =new DataOutputStream(outputStream);
? ? ? ? ? ? Scanner scanner =new Scanner(System.in);
? ? ? ? ? ? if(scanner.hasNext()){
????????????String str = scanner.next();
? ? ? ? ? ? ? ? int type =1;
? ? ? ? ? ? ? ? byte[] data = str.getBytes();
? ? ? ? ? ? ? ? int len = data.length +5;
? ? ? ? ? ? ? ? dataOutputStream.writeByte(type);
? ? ? ? ? ? ? ? dataOutputStream.writeInt(len);
? ? ? ? ? ? ? ? dataOutputStream.write(data);
? ? ? ? ? ? ? ? dataOutputStream.flush();
? ? ? ? ? ? }
}catch (IOException e) {
e.printStackTrace();
? ? ? ? }
}
}
客戶端socket創(chuàng)建后没佑,我們通過dataOutputStream輸出流中的writeByte()方法毕贼,設置數(shù)據(jù)類型,writeInt()方法設置數(shù)據(jù)長度蛤奢,然后通過write()方法將數(shù)據(jù)發(fā)送到服務端進行通信鬼癣,發(fā)送完畢后,為了確保數(shù)據(jù)完全發(fā)送啤贩,通過調(diào)用flush()方法刷新緩沖區(qū)待秃。
下面我們通過控制可以看到服務端接受數(shù)據(jù)的情況:
客戶端發(fā)送數(shù)據(jù):
服務端接受數(shù)據(jù):
上面服務端分別接受到數(shù)據(jù)的類型,長度和詳細內(nèi)容痹屹,具體下面的錯誤異常是由于客戶端發(fā)送一次后關(guān)閉章郁,服務端任在接受數(shù)據(jù),就會出現(xiàn)連接重置的錯誤志衍,這是一個簡單的通過數(shù)據(jù)類型+數(shù)據(jù)長度+數(shù)據(jù)內(nèi)容的方法發(fā)送數(shù)據(jù)的一個小例子驱犹,讓大家了解socket通信數(shù)據(jù)發(fā)送的原理,在實際應用中足画,原理不出其左右雄驹,只是在業(yè)務邏輯上完善而已。
六:socket 建立長連接
在了解socket長連接和短連接之前淹辞,我們先通過一個概念性的東西医舆,理解一下什么叫長連接,什么叫短連接象缀,長連接的原理和短連接的原理蔬将,
6.1 長連接
指在一個連接上可以連續(xù)發(fā)送多個數(shù)據(jù)包,在連接保持期間央星,如果沒有數(shù)據(jù)包發(fā)送霞怀,需要雙方發(fā)鏈路檢測包。整個通訊過程莉给,客戶端和服務端只用一個Socket對象毙石,長期保持Socket的連接。
6.2 短連接
短連接服務是每次請求都建立鏈接颓遏,交互完之后關(guān)閉鏈接徐矩,
6.3 長連接與短連接的優(yōu)勢
長連接多用于操作頻繁,點對點的通訊叁幢,而且連接數(shù)不能太多情況滤灯。每個TCP連接都需要三步握手,這需要時間,如果每個操作都是短連接鳞骤,再操作的話那么處理速度會降低很多窒百,所以每個操作完后都不斷開,下次處理時直接發(fā)送數(shù)據(jù)包就OK了豫尽,不用建立TCP連接贝咙。例如:數(shù)據(jù)庫的連接用長連接,如果用短連接頻繁的通信會造成socket錯誤拂募,而且頻繁的socket 創(chuàng)建也是對資源的浪費庭猩。
而像WEB網(wǎng)站的http服務一般都用短鏈接,因為長連接對于服務端來說會耗費一定的資源陈症,而像WEB網(wǎng)站這么頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源蔼水,如果用長連接,而且同時有成千上萬的用戶录肯,如果每個用戶都占用一個連接的話趴腋,那可想而知吧。所以并發(fā)量大论咏,但每個用戶無需頻繁操作情況下需用短連好优炬。(度娘)
在這章之前,你看到所有的例子厅贪,都是短連接蠢护,每次連接完畢后,都是自動斷開养涮,如果需要重新連接葵硕,則需要建立新的連接對象,比如像前一章我們看到的例子中贯吓,服務端有connection reset錯誤懈凹,就是短連接的一種。接下來悄谐,我們主要講解一下長連接原理介评,在實際應用中,長連接他并不是真正意義上的長連接爬舰,(他不像我們打電話一樣们陆,電話通了之后一直不掛的這種連接)。他們是通過一種稱之為心跳包或者叫做鏈路檢測包洼专,去定時檢查socket 是否關(guān)閉棒掠,輸入/輸出流是否關(guān)閉孵构。
在這里有個問題屁商,也是好多初學者比較困惑的,也是好多初學socket時候,遇到的一個問題蜡镶,那就是socket是通過流的方式通信的雾袱,既然關(guān)閉流,就是關(guān)閉socket官还,那么長連接不是很簡單嗎芹橡?就是我們讀取流中的信息后,不關(guān)閉流望伦,等下次使用時林说,直接往流中扔數(shù)據(jù)不就行了?
針對這個問題屯伞,我做個詳細的解答腿箩,盡可能的描述清楚,首先我們socket是針對應用層與TCP/ip數(shù)據(jù)傳輸協(xié)議封裝的一套方案劣摇,那么他的底層也是通過Tcp/Tcp/ip或則UDP通信的,所以說socket本身并不是一直通信協(xié)議末融,而是一套接口的封裝钧惧。而tcp/IP協(xié)議組里面的應用層包括FTP、HTTP勾习、TELNET浓瞪、SMTP、DNS等協(xié)議巧婶,我們知道追逮,http1.0是短連接,http1.1是長連接粹舵,我們在打開http通信協(xié)議里面在Response headers中可以看到這么一句Connection:keep-alive钮孵。他是干什么的,他就是表示長連接眼滤,但是他并不是一直保持的連接巴席,他有一個時間段,如果我們想一直保持這個連接怎么辦诅需?那就是在制定的時間內(nèi)讓客戶端和服務端進行一個請求漾唉,請求可以是服務端發(fā)起,也可以是客戶端發(fā)起堰塌,通常我們是在客戶端不定時的發(fā)送一個字節(jié)數(shù)據(jù)給服務端赵刑,這個就是我們稱之為心跳包,想想心跳是怎么跳動的场刑,是不是為了檢測人活著般此,心會定時的跳動,就是這個原理。
七:非阻塞ServerSocketChannel通信
铐懊。邀桑。。科乎。壁畸。。茅茂。捏萍。。空闲。照弥。。进副。这揣。。影斑。给赞。。矫户。片迅。。皆辽。柑蛇。。驱闷。耻台。。空另。盆耽。。扼菠。摄杂。
八:socket服務端接受信息后反饋給客戶端
。循榆。析恢。。秧饮。映挂。泽篮。。袖肥。咪辱。振劳。椎组。。历恐。寸癌。。弱贼。蒸苇。。吮旅。溪烤。。庇勃。檬嘀。。责嚷。鸳兽。。罕拂。揍异。。爆班。
九:socket經(jīng)典小例子