博客主頁java
TCP(Transmission Control Protocol)是傳輸控制協議,一種面向鏈接的,可靠的,基於字節流的傳輸層通訊協議。算法
TCP 通訊同UDP通訊同樣,都可以實現兩臺計算機之間的通訊,通訊的兩端都須要建立Socket對象。segmentfault
區別在於:數組
在JDK中,提供了兩個類用於實現TCP通訊程序緩存
JDK中java.net包中提供ServerSocket類,該類的實例對象能夠實現一個服務器段的程序。服務器
ServerSocket(int port) 建立綁定到指定端口的服務器套接字。
使用該構造方法在建立ServerSocket對象時,就能夠將其綁定到一個指定的端口號上(參數port就是端口號)網絡
Socket accept() 偵聽要鏈接到此套接字並接受它。該方法將阻塞直到創建鏈接。 InetAddress getInetAddress() 返回此服務器套接字的本地地址。若是套接字被綁定在closed以前,則該方法將在套接字關閉後繼續返回本地地址。
ServerSocket對象負責監聽某臺計算機的某個端口號,在建立ServerSocket對象後,須要繼續調用該對象的accept()方法,接收來自客戶端的請求。當執行了accept()方法以後,服務器端程序會發生阻塞,直到客戶端發出鏈接請求,accept()方法纔會返回一個Scoket對象用於和客戶端實現通訊,程序才能繼續向下執行.socket
JDK提供了一個Socket類,用於實現TCP客戶端程序。tcp
Socket(String host, int port) 建立流套接字並將其鏈接到指定主機上的指定端口號。
使用該構造方法在建立Socket對象時,會根據參數去鏈接在指定地址和端口上運行的服務器程序,其中參數host接收的是一個字符串類型的IP地址。函數
Socket(InetAddress address, int port) 建立流套接字並將其鏈接到指定IP地址的指定端口號。 若是指定的主機是null ,則至關於指定地址爲InetAddress.getByName (null) 。 換句話說,它至關於指定回送接口的地址。
該方法在使用上與第二個構造方法相似,參數address用於接收一個InetAddress類型的對象,該對象用於封裝一個IP地址。
int getPort() 返回此套接字鏈接到的遠程端口號。 InetAddress getLocalAddress() 獲取套接字所綁定的本地地址。 void close() 關閉此套接字。任何線程當前被阻塞在這個套接字上的I / O操做將會拋出一個SocketException 。 關閉此socket也將關閉socket的InputStream和OutputStream 。 InputStream getInputStream() 返回此套接字的輸入流。關閉返回的InputStream將關閉相關的套接字。 OutputStream getOutputStream() 返回此套接字的輸出流。關閉返回的OutputStream將關閉相關的套接字。 void shutdownOutput() 禁用此套接字的輸出流。 任何先前寫入的數據將被髮送,隨後是TCP的正常鏈接終止序列。 若是在套接字上調用shutdownOutput()以後寫入套接字輸出流,則流將拋出IOException。
在Socket類的經常使用方法中,getInputStream()和getOutStream()方法分別用於獲取輸入流和輸出流。當客戶端和服務端創建鏈接後,數據是以IO流的形式進行交互的,從而實現通訊。
瞭解了Socket 和 ServerSocket這兩個類的基本用法,經過下面簡單的TCP加深理解。
注意:若是先啓動客戶端,拋出java.net.ConnectException: Connection refused (Connection refused)異常
服務端實現:
public class TcpServer { public static void main(String[] args) throws IOException { System.out.println("Server->啓動"); // 建立ServerSocket對象,並綁定到指定端口爲20000 ServerSocket serverSocket = new ServerSocket(20000); // 偵聽鏈接,獲取Socket對象 // accept方法將阻塞直到創建鏈接。 Socket socket = serverSocket.accept(); // 經過socket獲取網絡輸入流 InputStream is = socket.getInputStream(); // 從輸入流中讀取字節數據到buffer中 byte[] buffer = new byte[1024]; int len= is.read(buffer); String msg = new String(buffer, 0, len); // 打印接收到的數據 System.out.println("Server-> receive msg:" + msg); // 關閉資源 socket.close(); System.out.println("Server->關閉"); } }
客戶端實現:
public class TcpClient { public static void main(String[] args) throws IOException { System.out.println("Client->啓動"); // 建立Socket對象,並鏈接到指定主機上的指定端口號。 Socket socket = new Socket("127.0.0.1", 20000); // 經過Socket獲取網絡輸出流 OutputStream os = socket.getOutputStream(); // 經過輸出流寫入數據 os.write("hello tcp!".getBytes()); //關閉資源 os.close(); System.out.println("Client->關閉"); } }
服務端實現:
public class TcpServer { public static void main(String[] args) throws IOException { //... // ====================回寫數據==================== // 經過socket獲取網絡輸出流 OutputStream os = socket.getOutputStream(); // 經過網絡輸出流回寫數據 os.write("hello, 我收到了.".getBytes()); // ... } }
客戶端實現:
public class TcpClient { public static void main(String[] args) throws IOException { // ... // ====================接收服務端回寫數據==================== // 經過Socket獲取網絡輸入流 InputStream is = socket.getInputStream(); // 從網絡輸入流中讀取數據 byte[] buffer = new byte[1024]; int len = is.read(buffer); System.out.println("Client-> receive msg: " + new String(buffer, 0, len)); // ... } }
在實際項目實操中,建立客戶端Socket時,使用無參數的Socket構造,或者經過Socket(Proxy proxy)構造,這樣Socket對象建立成功後,是一個未鏈接Socket,就能夠經過Socket對象進行初始化配置。
private static final int PORT = 20001; private static final int LOCAL_PORT = 30001; private static Socket createSocket() throws IOException { // 建立一個未鏈接的Socket對象 Socket socket = new Socket(); // 或者使用無代理(忽略任何其餘代理配置)的構造函數,等效於空構造函數 //Socket socket = new Socket(Proxy.NO_PROXY); // 將Socket綁定到本地IP地址和端口號 socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT)); return socket; }
也能夠在建立Socket時,指定應該使用什麼樣的代理轉發數據。
// 建立一個經過指定的HTTP代理服務器鏈接的Socket,數據經過指定的代理轉發 Proxy proxy = new Proxy( Proxy.Type.HTTP, new InetSocketAddress("www.baidu.com", 1080) ); Socket socket = new Socket(proxy);
下面幾種方式建立Socket對象時,在建立時就鏈接到指定的服務器上,不能作一些初始化配置。
// 建立Socket,並將其鏈接到指定主機上和指定端口號的服務器上 Socket socket = new Socket("localhost", PORT); //Socket(InetAddress address, int port) //建立流套接字並將其鏈接到指定IP地址的指定端口號。 Socket socket = new Socket(Inet4Address.getLocalHost(), PORT); //Socket(InetAddress address, int port, InetAddress localAddr, int localPort) //建立套接字並將其鏈接到指定的遠程端口上指定的遠程地址。 Socket socket = new Socket( Inet4Address.getLocalHost(), PORT, Inet4Address.getLocalHost(), LOCAL_PORT ); //Socket(String host, int port, InetAddress localAddr, int localPort) //建立套接字並將其鏈接到指定遠程端口上的指定遠程主機。 Socket socket = new Socket( "localhost", PORT, Inet4Address.getLocalHost(), LOCAL_PORT );
在設置Socket一些初始化配置時,須要注意,在Socket鏈接後配置將不起做用,必須在鏈接以前調用。
private static void configSocket(Socket socket) throws SocketException { // 設置讀取超時時間,單位:毫秒。timeout=0時,無限超時;timeout>0時,與此Socket相關聯的InputStream上的read()調用將僅阻止此時間. // 若是超時超時,則引起java.net.SocketTimeoutException socket.setSoTimeout(2000); //Nagle的算法,true啓用TCP_NODELAY, false禁用。 socket.setTcpNoDelay(true); // 是否須要在長時無數據響應時發送確認數據(相似心跳包),時間大約爲2小時 socket.setKeepAlive(true); // 設置逗留時間(以秒爲單位),最大超時值是平臺特定的,該設置僅影響關Socket關閉。默認爲false,0 // false, 0: 默認狀況,關閉時當即返回,底層系統接管輸出流,將緩衝區的數據發送完成 // true, 0: 當即關閉返回,緩存區數據拋棄,直接發送RST結束命令到對方,並沒有需通過2MSL等待 // true, 2: 關閉時最長堵塞2秒,隨後按照第二種狀況處理 socket.setSoLinger(true, 2); // 是否接收TCP緊急數據,默認爲false,禁止接收,在Socket接收的TCP緊急數據被靜默地丟棄。 socket.setOOBInline(true); // 設置接收緩衝區區大小 // 增長接收緩衝區大小能夠提升大容量鏈接的網絡I / O的性能,同時能夠幫助減小輸入數據的積壓 // 須要注意:1.對於客戶端Socket,在將Socket鏈接到服務器以前,必須調用setReceiveBufferSize() // 2. 對於ServerSocket接受的Socket,必須經過在ServerSocket綁定到本地地址以前調用ServerSocket.setReceiveBufferSize(int)來完成。 socket.setReceiveBufferSize(64 * 1024 * 1024); // 設置發送緩衝區大小的大小,該值必須大於0 socket.setSendBufferSize(64 * 1024 * 1024); // 注意:在此連Socket鏈接後調用此方法將不起做用,必須在鏈接以前調用 // 設置此Socket的性能參數: // connectionTime :一個 int表達短鏈接時間的相對重要性 // latency :一個 int表達低延遲的相對重要性 // bandwidth :一個 int表達高帶寬的相對重要性 // 這三個值只是簡單的比較,哪一個參數設置的值大偏向誰 socket.setPerformancePreferences(1, 1, 0); }
最後在建立Socket並配置後,將此Socket鏈接到具備指定的服務器。
public static void main(String[] args) throws IOException { Socket socket = createSocket(); configSocket(socket); //connect(SocketAddress endpoint, int timeout) //將此Socket鏈接到具備指定超時值的服務器 socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000); }
一般在建立ServerSocket對象時,使用空參數的構造函數,這樣後續能夠給ServerSocket設置一些配置。
private static ServerSocket createServerSocket() throws IOException { // 建立未綁定的服務器套接字 ServerSocket server = new ServerSocket(); return server; }
下面幾種方式建立ServerSocket對象時,在建立時就bind到指定的端口,不能作一些初始化配置
// 建立綁定到指定端口的服務器套接字 // 等待鏈接的最大隊列長度設置爲50 ,若是鏈接在隊列已滿時到達,則鏈接被拒絕 ServerSocket server = new ServerSocket(PORT); // 建立服務器套接字並將其綁定到指定的本地端口號,同時並指定了積壓 // 等待鏈接的最大隊列長度設置爲backlog ,若是鏈接在隊列已滿時到達,則鏈接被拒絕 ServerSocket server = new ServerSocket(PORT, 50); // 建立一個具備指定端口的服務器,偵聽backlog和本地IP地址綁定 ServerSocket server = new ServerSocket(PORT, 50, Inet4Address.getLocalHost());
最後在建立ServerSocket並設置配置後,bind指定的端口
private static final int PORT = 20001; public static void main(String[] args) throws IOException { ServerSocket server = createServerSocket(); configServerSocket(server); //將 ServerSocket綁定到特定地址(IP地址和端口號) server.bind(new InetSocketAddress(Inet4Address.getLocalHost(), PORT)); }
在設置ServerSocket一些初始化配置時,須要在bind以前纔能有效。
private static void configServerSocket(ServerSocket server) throws SocketException { // 當TCP鏈接關閉時,鏈接可能會在鏈接關閉後一段時間內保持在超時狀態(一般稱爲TIME_WAIT狀態或2MSL等待狀態) // 若是在套接字地址或端口的超時狀態中存在鏈接,則可能沒法將套接字綁定到所需的SocketAddress // 設置爲true,套接字bind(SocketAddress)容許在上一個鏈接處於超時狀態時綁定套接字 server.setReuseAddress(true); //設置套接字接收緩衝區的大小 // 注意:在ServerSocket在綁定到本地地址以前調用 // 也就是意味着必須使用無參數構造函數建立ServerSocket,而後調用setReceiveBufferSize() server.setReceiveBufferSize(64 * 1024 * 1024); // 設置讀取超時時間,單位:毫秒。timeout=0時,無限超時;timeout>0時,與此ServerSocket的accept()調用將僅阻止此時間. // 若是超時超時,則引起java.net.SocketTimeoutException //server.setSoTimeout(2000); //設置性能參數:短連接,延遲,帶寬的相對重要性 server.setPerformancePreferences(1, 1, 0); }
在使用Socket的輸出流,傳輸基本數據類型時,如int類型。
咱們先來看使用Socket傳輸int類型數據,例如:傳輸int類型10
// 客戶端 private static void todo_client(Socket socket) throws IOException { OutputStream os = socket.getOutputStream(); InputStream is = socket.getInputStream(); os.write(10); // 釋放資源 socket.close(); } // 服務器 private void todo_server(Socket socket) throws IOException { // 獲取網絡輸入流 InputStream is = socket.getInputStream(); byte[] buffer = new byte[1024]; int len = is.read(buffer); System.out.println("Server-> len: " + len + " data: " + new String(buffer, 0, len)); }
咱們發現打印輸出的log不是咱們指望的,數字10沒有輸出
> Task :TcpServer.main() Server-> len: 1 data:
當咱們在傳輸int類型10時,調用下面方法,客戶端將int類型轉爲byte數組。
public static byte[] intToByteArray(int a) { return new byte[]{ (byte) ((a >> 24) & 0xFF), (byte) ((a >> 16) & 0xFF), (byte) ((a >> 8) & 0xFF), (byte) (a & 0xFF) }; }
在服務端接收時,調用下面方法,客戶端將byte數組轉爲int類型。
public int byteArrayToInt(byte[] b) { return b[3] & 0xFF | (b[2] & 0xFF) << 8 | (b[1] & 0xFF) << 16 | (b[0] & 0xFF) << 24; }
打印輸出的log輸出咱們指望的值
> Task :TcpServer.main() Server-> len: 4 data: 10
在JDK java.nio包中,爲咱們提供了更方面的類ByteBuffer,一個字節緩存區。緩衝區的索引不是以字節爲單位,而是根據其值的類型特定大小,如int類型大小爲4,long類型大小爲8。更重要是緩衝區更高效。
客戶端:使用ByteBuffer的wrap方法將字節數組封裝到緩衝區中,而後put到緩存區中。如int類型,將int值的四個字節寫入當前位置的緩衝區
private static void todo_client(Socket socket) throws IOException { OutputStream os = socket.getOutputStream(); InputStream is = socket.getInputStream(); byte[] buffer = new byte[256]; // 將一個字節數組包裝到緩衝區中。 ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); // byte byte b = 126; byteBuffer.put(b); // char char c = 'a'; byteBuffer.putChar(c); // int int i = 1223344; byteBuffer.putInt(i); //bool boolean bool = true; byteBuffer.put(bool ? (byte) 1 : (byte) 0); // long long l = 1287655778990L; byteBuffer.putLong(l); //float float f = 3.1345f; byteBuffer.putFloat(f); // double double d = 12223.0232199761; byteBuffer.putDouble(d); // String String str = "hello, 你好啊!"; byteBuffer.put(str.getBytes()); os.write(buffer, 0, byteBuffer.position() + 1); // 釋放資源 socket.close(); }
服務端:使用ByteBuffer的wrap方法將字節數組封裝到緩衝區中,而後從緩存區中讀取值,如int類型值,在該緩衝區的當前位置讀取接下來的四個字節
private void todo_server(Socket socket) throws IOException { // 獲取網絡輸入流 InputStream is = socket.getInputStream(); byte[] buffer = new byte[256]; int len = is.read(buffer); ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, len); // byte byte b = byteBuffer.get(); // char char c = byteBuffer.getChar(); // int int i = byteBuffer.getInt(); // boolean boolean bool = byteBuffer.get() == 1; //long long l = byteBuffer.getLong(); // float float f = byteBuffer.getFloat(); // double double d = byteBuffer.getDouble(); int pos = byteBuffer.position(); String str = new String(buffer, pos, len - pos - 1); System.out.println("Server-> len: " + len + "\n" + " b: " + b + "\n" + " c: " + c + "\n" + " i: " + i + "\n" + " bool: " + bool + "\n" + " l: " + l + "\n" + " f: " + f + "\n" + " d: " + d + "\n" + " str: " + str + "\n" ); }
從輸出的log能夠看出,全部基本數據類型的輸出都正確:
Server-> len: 46 b: 126 c: a i: 1223344 bool: true l: 1287655778990 f: 3.1345 d: 12223.0232199761 str: hello, 你好啊!
若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)