一、什麼是socket? 當兩台電腦需要通信的時候,往往我們使用的都是TCP去實現的,但是並不會直接去操作TCP協議,通常是通過Socket進行tcp通信。Socket是操作系統提供給開發者的一個介面,通過它,就可以實現設備之間的通信。 二、TCP是如何通信的? TCP連接和斷開分別會存在3次握手 ...
一、什麼是socket? 當兩台電腦需要通信的時候,往往我們使用的都是TCP去實現的,但是並不會直接去操作TCP協議,通常是通過Socket進行tcp通信。Socket是操作系統提供給開發者的一個介面,通過它,就可以實現設備之間的通信。 二、TCP是如何通信的? TCP連接和斷開分別會存在3次握手/4此握手的過程,並且在此過程中包含了發送數據的長度(接受數據的長度),無容置疑,這個過程是複雜的,這裡我們不需要做深入的探討。如果有興趣,可以參考此文章,這裡詳細的解釋了TCP通信的過程: https://ketao1989.github.io/2017/03/29/java-server-in-action/ 三、Socket消息的收發 在Java中處理socket的方式有三種:
- 傳統的io流方式(BIO模式),阻塞型;
- NIO的方式;
- AIO的方式;
//<--------------服務端代碼-------------------->
public class SocketReadLister implements Runnable {
private final int tcpPort=9999;
private ServerSocket serverSocket;
@Override
public void run() {
try {
serverSocket = new ServerSocket(this.tcpPort);
while(true){
Socket socket = serverSocket.accept();
//socket.setSoTimeout(5*1000);//設置讀取數據超時時間為5s
new Thread(new SocketReadThread(socket)).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
new Thread(new SocketReadLister()).start();
}
}
public class SocketReadThread implements Runnable {
private Socket socket;
public SocketReadThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
byte[] data = new byte[1024];
try {
InputStream is=socket.getInputStream();
int length=0;
int num=is.available();
while((length = is.read(data)) != -1){
String result = new String(data);
System.out.println("數據available:"+num);
System.out.println("數據:"+result);
System.out.println("length:" + length);
}
System.out.print("結束數據讀取:"+length);
}catch (SocketTimeoutException socketTimeoutException){
try {
Thread.sleep(2*1000);
}catch (Exception e) {
e.printStackTrace();
}
run();
} catch (Exception e){
e.printStackTrace();
try {
socket.close();
}catch (IOException io){
io.printStackTrace();
}
}
}
}
//<---------------------客戶端代碼---------------------------->
public class SocketClient implements Runnable {
private final int tcpPort=9999;
private Socket socket;
@Override
public void run() {
String msg = "ab23567787hdhfhhfy";
byte[] byteMsg = msg.getBytes();
try {
socket = new Socket("127.0.0.1", 9999);
OutputStream out = socket.getOutputStream();
InputStream inputStream=socket.getInputStream();
out.write(byteMsg);
Thread.sleep(10*1000);
char[] chars=msg.toCharArray();
String str="";
/*out.flush();*/
for(int i=0;i<msg.length();i++) {
str=chars[i]+"-"+i;
out.write(str.getBytes());
Thread.sleep(1*1000);
}
byte[] bytes=new byte[8];
while(true) {
if(inputStream.available()>0) {
if(inputStream.read(bytes)!=-1) {
System.out.println(new String(bytes));
}
}
Thread.sleep(10*1000);
}
} catch (Exception e) {
e.printStackTrace();
try {
socket.close();
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(new SocketClient()).start();
}
}
正如代碼中所示,通常情況下我們在while迴圈中將is.read(data)) != -1作為判斷依據,判斷是否繼續讀取,這種情況下,確實可以將數據完整的讀取,但是客戶端沒有傳輸數據的時候,read()方法開始阻塞,直到有數據時才繼續執行後續代碼,使得程式掛起。
為什麼會出現這種情況呢?
在JDK中,關於read()的說明如下:當讀取到流的末尾,沒有可讀數據的時候,read()方法將返回-1,如果沒有數據,那麼read()將會發生阻塞。因此,在讀取文件流的情況下,這樣是完全正確的,但是在網路編程的情況下,socket連接不會斷開,那麼InputStream的read()將永遠不會返回-1,程式將讀完數據後,繼續迴圈讀取然後發生阻塞。
在InputStream中,提供了available();此方法是非阻塞的,通過它可以初步的判定socket流中是否有數據,並返回一個預估數據長度的值,但是請註意,這裡是預估,並不是準確的計算出數據的長度,所以在JDK說明文檔中,有提示使用該方法獲取的值去聲明 byte[]的長度,然後讀取數據,這是錯誤的做法。這樣在每次讀取數據之前,都可以先判斷一下流中是否存在數據,然後再讀取,這樣就可以避免阻塞造成程式的掛起。代碼如下:
while(true){
if(is.available()>0){
is.read(data);
}
}
說到read(),在InputStream中提供了3個read的重載方法:read()、read(byte[])、read(byte[],int offset,int len);後面兩種讀取方法都是基於 read()實現的,同樣存在阻塞的特性,那麼我們可以思考一下,假定byte[]的長度為1024,撇開while,拿read(byte[])一次性讀取來說,當另一端發送的數據不足1024個位元組時,為什麼這個read(byte[])沒有發生阻塞?
關於這個問題,網上有帖子說,這跟InputStream的flush()有關,但經過測試,我不這麼認為。我更加認同https://ketao1989.github.io/2017/03/29/java-server-in-action/中所說的那樣,TCP握手期間,會傳遞數據的長度,當讀取完數據,read()返回-1,即使此時沒有讀取到1024個位元組數據,剩下的用0填充,這樣就能很好的解釋這個問題了。
Socket既然時網路通訊用,那麼由於各種原因,必然會有網路延遲,造成socket讀取超時;socket讀取超時時,其連接任然是有效的,因此在處理該異常時不需要關閉連接。以下是代碼片段:
if (nRecv < nRecvNeed){
int nSize = 0;
wsaBuf=new byte[nRecvNeed-nRecv];
int readCount = 0; // 已經成功讀取的位元組的個數
try {
while (readCount < wsaBuf.length) {
//Thread.sleep(100);//讀取之前先將線程休眠,避免迴圈時,程式占用CPU過高
try {
availableNum=inputStream.available();
if(availableNum>0){
readCount += inputStream.read(wsaBuf, readCount, (wsaBuf.length - readCount));//避免數據讀取不完整
}
}catch (SocketTimeoutException timeOut){
System.out.println("讀取超時,線程執行休眠操作,2秒後再讀取");
Thread.sleep(2*1000);
}
}
}catch (Exception e){
System.out.println("讀取數據異常");
e.printStackTrace();
close();//關閉socket連接
break;
}
nSize=wsaBuf.length;
nRecv+=nSize;
}
另外,需要補充說明的是,socket.close()方法執行後,只能更改本端的連接狀態,不能將該狀態通知給對端,也就是說如果服務端或客戶端一方執行了close(),另一端並不知道此時連接已經斷開了。
此外,以上代碼還存在一個很嚴重的問題亟待解決,這也是在開發中容易忽視的地方——程式能正常運行,但CPU占用過高;原因如下:
當readCount < wsaBuf.length,即數據還未讀取完整時,線程會持續不斷的從socket流中讀取數據,由於這裡使用了inputStream.available()來判斷使用需要讀取數據,當沒有數據傳輸的時候,此處就變成了一個死迴圈,說到此處,原因就非常明瞭了,在電腦運行過程中無論他是單核還是多核,系統獲取電腦資源(CPU等)都是按照時間分片的方式進行的,同一時間有且只有一個線程能獲取到系統資源,所以當遇到死迴圈時,系統資源一直得不到釋放,因此CPU會越來越高,解決的辦法是在迴圈中對程式進行線程休眠一定時間。