注:本文爲對Java socket詳解,看這一篇就夠了續的重排版,方便自己瀏覽。
常見問題:
1.arp爲什麼是網絡層協議,而不是數據鏈路層協議?
2.Socket屬於那一層?socket和TCP/UDP的關係是什麼?Socket和TCP/IP是什麼關係?
3.Socket通信的流程?
4.客戶端socket發送消息後,爲什麼服務端socket沒有收到?
5.使用while 循環實現連續輸入,是不是就是多線程模式?
6.對多線程處理機制不是很明白,希望詳細講解?
7.希望詳細講解ServerSocketChannel和SocketChannel與ServerSoket和Socket的區別?
8.希望有詳細的例子,可以直接拷貝下來運行?
一、計算機網絡
三張圖:
以上三張圖分別介紹了計算機網絡的分層、個協議之間的關係、Socket建立的過程。
我們將的socket屬於傳輸層,其中UDP是一種面向無連接的傳輸層協議。UDP不關心對端是否真正收到了傳送過去的數據。如果需要檢查對端是否收到分組數據包,或者對端是否連接到網絡,則需要在應用程序中實現。UDP常用在分組數據較少或多播、廣播通信以及視頻通信等多媒體領域。在這裏我們不進行詳細討論,這裏主要講解的是基於TCP/IP協議下的socket通信。
socket是基於應用服務與TCP/IP通信之間的一個抽象,他將TCP/IP協議裏面複雜的通信邏輯進行分裝,對用戶來說,只要通過一組簡單的API就可以實現網絡的連接。
服務端初始化ServerSocket,然後對指定的端口進行綁定,接着對端口及進行監聽,通過調用accept方法阻塞,此時,如果客戶端有一個socket連接到服務端,那麼服務端通過監聽和accept方法可以與客戶端進行連接。
二:socket通信基本示例:
在對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) {
try {
// 初始化服務端socket並且綁定9999端口
ServerSocket serverSocket =new ServerSocket(9999);
//等待客戶端的連接
Socket socket = serverSocket.accept();
//獲取輸入流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
//讀取一行數據
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();
}
}
}
啓動服務端:
發現正常,等待客戶端的的連接
啓動客戶端:
發現客戶端啓動正常後,馬上執行完後關閉。同時服務端控制檯報錯:
服務端控制檯報錯:
然後好多童鞋,就拷貝這個java.net.SocketException: Connection reset上王查異常,查詢解決方案,搞了半天都不知道怎麼回事。解決這個問題我們首先要明白,socket通信是阻塞的,他會在以下幾個地方進行阻塞。第一個是accept方法,調用這個方法後,服務端一直阻塞在哪裏,直到有客戶端連接進來。第二個是read方法,調用read方法也會進行阻塞。通過上面的示例我們可以發現,該問題發生在read方法中。有朋友說是Client沒有發送成功,其實不是的,我們可以通debug跟蹤一下,發現客戶端發送了,並且沒有問題。而是發生在服務端中,當服務端調用read方法後,他一直阻塞在哪裏,因爲客戶端沒有給他一個標識,告訴是否消息發送完成,所以服務端還在一直等待接受客戶端的數據,結果客戶端此時已經關閉了,就是在服務端報錯:java.net.SocketException: Connection reset
那麼理解上面的原理後,我們就能明白,客戶端發送完消息後,需要給服務端一個標識,告訴服務端,我已經發送完成了,服務端就可以將接受的消息打印出來。
通常大家會用以下方法進行進行結束:
socket.close() 或者調用socket.shutdownOutput();方法。調用這倆個方法,都會結束客戶端socket。但是有本質的區別。socket.close() 將socket關閉連接,那邊如果有服務端給客戶端反饋信息,此時客戶端是收不到的。而socket.shutdownOutput()是將輸出流關閉,此時,如果服務端有信息返回,則客戶端是可以正常接受的。現在我們將上面的客戶端示例修改一下啊,增加一個標識告訴流已經輸出完畢:
客戶端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();
//關閉socket的輸出流
socket.shutdownOutput();
}catch (IOException e) {
e.printStackTrace();
}
}
}
在看服務端控制檯:
服務端在接受到客戶端關閉流的信息後,知道信息輸入已經完畢,蘇哦有就能正常讀取到客戶端傳過來的數據。通過上面示例,我們可以基本瞭解socket通信原理,掌握了一些socket通信的基本api和方法,實際應用中,都是通過此處進行實現變通的。
三:while循環連續接受客戶端信息:
上面的示例中scoket客戶端和服務端固然可以通信,但是客戶端每次發送信息後socket就需要關閉,下次如果需要發送信息,需要socket從新啓動,這顯然是無法適應生產環境的需要。比如在我們是實際應用中QQ,如果每次發送一條信息,就需要重新登陸QQ,我估計這程序不是給人設計的,那麼如何讓服務可以連續給服務端發送消息?下面我們通過while循環進行簡單展示:
服務端:
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(),"UTF-8"));
//讀取一行數據
String str;
//通過while循環不斷讀取信息,
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 循環,就可以實現客戶端不間斷的通過標準輸入流讀取來的消息,發送給服務端。在這裏有個細節,大家看到沒有,我客戶端沒有寫socket.close() 或者調用socket.shutdownOutput();服務端是如何知道客戶端已經輸入完成了?服務端接受數據的時候是如何判斷客戶端已經輸入完成呢?這就是一個核心點,雙方約定一個標識,當客戶端發送一個標識給服務端時,表明客戶端端已經完成一個數據的載入。而服務端在結束數據的時候,也通過這個標識進行判斷,如果接受到這個標識,表明數據已經傳入完成,那麼服務端就可以將數據度入後顯示出來。
在上面的示例中,客戶端端在循環發送數據時候,每發送一行,添加一個換行標識“\n”標識,在告訴服務端我數據已經發送完成了。而服務端在讀取客戶數據時,通過while ((str = bufferedReader.readLine())!=null)去判斷是否讀到了流的結尾,負責服務端將會一直阻塞在哪裏,等待客戶端的輸入。
通過while方式,我們可以實現多個客戶端和服務端進行聊天。但是,下面敲黑板,劃重點。由於socket通信是阻塞式的,假設我現在有A和B倆個客戶端同時連接到服務端的上,當客戶端A發送信息給服務端後,那麼服務端將一直阻塞在A的客戶端上,不同的通過while循環從A客戶端讀取信息,此時如果B給服務端發送信息時,將進入阻塞隊列,直到A客戶端發送完畢,並且退出後,B纔可以和服務端進行通信。簡單地說,我們現在實現的功能,雖然可以讓客戶端不間斷的和服務端進行通信,與其說是一對一的功能,因爲只有當客戶端A關閉後,客戶端B纔可以真正和服務端進行通信,這顯然不是我們想要的。 下面我們通過多線程的方式給大家實現正常人類的思維。
四:多線程下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() {
//獲取輸入流,並且指定統一的編碼格式
BufferedReader bufferedReader =null;
try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//讀取一行數據
String str;
//通過while循環不斷讀取信息,
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控制檯輸入:
服務端控制檯:
通過這裏我們可以發現,客戶端A和客戶端B同時連接到服務端後,都可以和服務端進行通信,也不會出現前面講到使用while(true)時候客戶端A連接時客戶端B不能與服務端進行交互的情況。在這裏我們看到,主要是通過服務端的 new Thread(new Runnable() {}實現的,每一個客戶端連接進來後,服務端都會單獨起個一線程,與客戶端進行數據交互,這樣就保證了每個客戶端處理的數據是單獨的,不會出現相互阻塞的情況,這樣就基本是實現了QQ程序的基本聊天原理。
但是實際生產環境中,這種寫法對於客戶端連接少的的情況下是沒有問題,但是如果有大批量的客戶端連接進行,那我們服務端估計就要歇菜了。假如有上萬個socket連接進來,服務端就是新建這麼多進程,反正樓主是不敢想,而且socket 的回收機制又不是很及時,這麼多線程被new 出來,就發送一句話,然後就沒有然後了,導致服務端被大量的無用線程暫用,對性能是非常大的消耗,在實際生產過程中,我們可以通過線程池技術,保證線程的複用,下面請看改良後的服務端程序。
改良後的服務端:
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);
//創建一個線程池
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"));
//讀取一行數據
String str;
//通過while循環不斷讀取信息,
while ((str = bufferedReader.readLine()) !=null) {
//輸出打印
System.out.println("客戶端說:" + str);
}
}catch (IOException e) {
e.printStackTrace();
}
};
executorService.submit(runnable);
}
}
}
運行後服務端控制檯:
通過線程池技術,我們可以實現線程的複用。其實在這裏executorService.submit在併發時,如果要求當前執行完畢的線程有返回結果時,這裏面有一個大坑,在這裏我就不一一詳細說明,具體我在我的另一篇文章中《把多線程說個透》裏面詳細介紹。本章主要講述socket相關內容。
在實際應用中,socket發送的數據並不是按照一行一行發送的,比如我們常見的報文,那麼我們就不能要求每發送一次數據,都在增加一個“\n”標識,這是及其不專業的,在實際應用中,通過是採用數據長度+類型+數據的方式,在我們常接觸的熱Redis就是採用這種方式,
五:socket 指定長度發送數據
在實際應用中,網絡的數據在TCP/IP協議下的socket都是採用數據流的方式進行發送,那麼在發送過程中就要求我們將數據流轉出字節進行發送,讀取的過程中也是採用字節緩存的方式結束。那麼問題就來了,在socket通信時候,我們大多數發送的數據都是不定長的,所有接受方也不知道此次數據發送有多長,因此無法精確地創建一個緩衝區(字節數組)用來接收,在不定長通訊中,通常使用的方式時每次默認讀取8*1024長度的字節,若輸入流中仍有數據,則再次讀取,一直到輸入流沒有數據爲止。但是如果發送數據過大時,發送方會對數據進行分包發送,這種情況下或導致接收方判斷錯誤,誤以爲數據傳輸完成,因而接收不全。在這種情況下就會引出一些問題,諸如半包,粘包,分包等問題,爲了後續一些例子中好理解,我在這裏直接將半包,粘包,分包概念性東西在寫一下(引用度娘)
5.1 半包
接受方沒有接受到一個完整的包,只接受了部分。
原因:TCP爲提高傳輸效率,將一個包分配的足夠大,導致接受方並不能一次接受完。
影響:長連接和短連接中都會出現
5.2 粘包
發送方發送的多個包數據到接收方接收時粘成一個包,從接收緩衝區看,後一包數據的頭緊接着前一包數據的尾。
分類:一種是粘在一起的包都是完整的數據包,另一種情況是粘在一起的包有不完整的包
出現粘包現象的原因是多方面的:
1)發送方粘包:由TCP協議本身造成的,TCP爲提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一包數據。若連續幾次發送的數據都很少,通常TCP會根據優化算法把這些數據合成一包後一次發送出去,這樣接收方就收到了粘包數據。
2)接收方粘包:接收方用戶進程不及時接收數據,從而導致粘包現象。這是因爲接收方先把收到的數據放在系統接收緩衝區,用戶進程從該緩衝區取數據,若下一包數據到達時前一包數據尚未被用戶進程取走,則下一包數據放到系統接收緩衝區時就接到前一包數據之後,而用戶進程根據預先設定的緩衝區大小從系統接收緩衝區取數據,這樣就一次取到了多包數據。
5.3分包
分包(1):在出現粘包的時候,我們的接收方要進行分包處理;
分包(2):一個數據包被分成了多次接收;
原因:1. IP分片傳輸導致的;2.傳輸過程中丟失部分包導致出現的半包;3.一個包可能被分成了兩次傳輸,在取數據的時候,先取到了一部分(還可能與接收的緩衝區大小有關係)。
影響:粘包和分包在長連接中都會出現
那麼如何解決半包和粘包的問題,就涉及一個一個數據發送如何標識結束的問題,通常有以下幾種情況
固定長度:每次發送固定長度的數據;
特殊標示:以回車,換行作爲特殊標示;獲取到指定的標識時,說明包獲取完整。
字節長度:包頭+包長+包體的協議形式,當服務器端獲取到指定的包長時才說明獲取完整;
所以大部分情況下,雙方使用socket通訊時都會約定一個定長頭放在傳輸數據的最前端,用以標識數據體的長度,通常定長頭有整型int,短整型short,字符串Strinng三種形式。
下面我們通過幾個簡單的小示例,演示發送接受定長數據,前面我們講過通過特殊標識的方式,可是有什麼我們發送的數據比較大,並且數據本身就會包含我們約定的特殊標識,那麼我們在接受數據時,就會出現半包的情況,通過這種情況下,我們都是纔有包頭+包長+包體的協議模式,每次發送數據的時候,我們都會固定前4個字節爲數據長度,那到數據長度後,我們就可以非常精確的創建一個數據緩存區用來接收數據。
那麼下面就先通過包類型+包長度+消息內容定義一個socket通信對象,數據類型爲byte類型,包長度爲int類型,消息內容爲byte類型。
首先我們創建府服務端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("獲取的數據類型爲:"+b);
System.out.println("獲取的數據長度爲:"+len);
System.out.println("獲取的數據內容爲:"+str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
在服務端創建後,我們通過DataInputStream 數據流進行數據獲取,首先我們獲取數據的類型,然後在獲取數據的長度,因爲數據實際有效長度是整個數據的長度減去5,(包括前個字節爲數據類型,前二到五個字節爲數據長度)。然後根據數據的實際有效長度創建數據緩存區,用戶存放數據,這邊確保每次接接受數據的完整性,不會出現半包與粘包的情況。在數據讀取的時候,我們通過readFully()方法讀取數據。下面我們來創建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創建後,我們通過dataOutputStream輸出流中的writeByte()方法,設置數據類型,writeInt()方法設置數據長度,然後通過write()方法將數據發送到服務端進行通信,發送完畢後,爲了確保數據完全發送,通過調用flush()方法刷新緩衝區。
下面我們通過控制可以看到服務端接受數據的情況:
客戶端發送數據:
服務端接受數據:
上面服務端分別接受到數據的類型,長度和詳細內容,具體下面的錯誤異常是由於客戶端發送一次後關閉,服務端任在接受數據,就會出現連接重置的錯誤,這是一個簡單的通過數據類型+數據長度+數據內容的方法發送數據的一個小例子,讓大家瞭解socket通信數據發送的原理,在實際應用中,原理不出其左右,只是在業務邏輯上完善而已。
六:socket 建立長連接
在瞭解socket長連接和短連接之前,我們先通過一個概念性的東西,理解一下什麼叫長連接,什麼叫短連接,長連接的原理和短連接的原理,
6.1 長連接
指在一個連接上可以連續發送多個數據包,在連接保持期間,如果沒有數據包發送,需要雙方發鏈路檢測包。整個通訊過程,客戶端和服務端只用一個Socket對象,長期保持Socket的連接。
6.2 短連接
短連接服務是每次請求都建立鏈接,交互完之後關閉鏈接,
6.3 長連接與短連接的優勢
長連接多用於操作頻繁,點對點的通訊,而且連接數不能太多情況。每個TCP連接都需要三步握手,這需要時間,如果每個操作都是短連接,再操作的話那麼處理速度會降低很多,所以每個操作完後都不斷開,下次處理時直接發送數據包就OK了,不用建立TCP連接。例如:數據庫的連接用長連接,如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創建也是對資源的浪費。
而像WEB網站的http服務一般都用短鏈接,因爲長連接對於服務端來說會耗費一定的資源,而像WEB網站這麼頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源,如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都佔用一個連接的話,那可想而知吧。所以併發量大,但每個用戶無需頻繁操作情況下需用短連好。(度娘)
在這章之前,你看到所有的例子,都是短連接,每次連接完畢後,都是自動斷開,如果需要重新連接,則需要建立新的連接對象,比如像前一章我們看到的例子中,服務端有connection reset錯誤,就是短連接的一種。接下來,我們主要講解一下長連接原理,在實際應用中,長連接他並不是真正意義上的長連接,(他不像我們打電話一樣,電話通了之後一直不掛的這種連接)。他們是通過一種稱之爲心跳包或者叫做鏈路檢測包,去定時檢查socket 是否關閉,輸入/輸出流是否關閉。
在這裏有個問題,也是好多初學者比較困惑的,也是好多初學socket時候,遇到的一個問題,那就是socket是通過流的方式通信的,既然關閉流,就是關閉socket,那麼長連接不是很簡單嗎?就是我們讀取流中的信息後,不關閉流,等下次使用時,直接往流中扔數據不就行了?
針對這個問題,我做個詳細的解答,儘可能的描述清楚,首先我們socket是針對應用層與TCP/ip數據傳輸協議封裝的一套方案,那麼他的底層也是通過Tcp/Tcp/ip或則UDP通信的,所以說socket本身並不是一直通信協議,而是一套接口的封裝。而tcp/IP協議組裏面的應用層包括FTP、HTTP、TELNET、SMTP、DNS等協議,我們知道,http1.0是短連接,http1.1是長連接,我們在打開http通信協議裏面在Response headers中可以看到這麼一句Connection:keep-alive。他是幹什麼的,他就是表示長連接,但是他並不是一直保持的連接,他有一個時間段,如果我們想一直保持這個連接怎麼辦?那就是在制定的時間內讓客戶端和服務端進行一個請求,請求可以是服務端發起,也可以是客戶端發起,通常我們是在客戶端不定時的發送一個字節數據給服務端,這個就是我們稱之爲心跳包,想想心跳是怎麼跳動的,是不是爲了檢測人活着,心會定時的跳動,就是這個原理。
七:非阻塞ServerSocketChannel通信
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
八:socket服務端接受信息後反饋給客戶端
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。