【转】Java Socket编程基础及深入讲解
原文:https://www.cnblogs.com/yiwangzhibujian/p/7107785.html#q2.3.3
Socket是Java網(wǎng)絡(luò)編程的基礎(chǔ),了解還是有好處的,
這篇文章主要講解Socket的基礎(chǔ)編程。Socket用在哪呢,主要用在進(jìn)程間,網(wǎng)絡(luò)間通信。本篇比較長,特別做了個目錄:
一、Socket通信基本示例
二、消息通信優(yōu)化
-
- 2.1 雙向通信,發(fā)送消息并接受消息
- 2.2 使用場景
- 2.3 如何告知對方已發(fā)送完命令
- 2.3.1 通過Socket關(guān)閉
- 2.3.2 通過Socket關(guān)閉輸出流的方式
- 2.3.3 通過約定符號
- 2.3.4 通過指定長度
三、服務(wù)端優(yōu)化
-
- 3.1 服務(wù)端并發(fā)處理能力
- 3.2 服務(wù)端其他屬性
- 3.3 性能再次提升
四、Socket的其它知識
-
- 4.1 客戶端綁定端口
- 4.2 讀超時SO_TIMEOUT
- 4.3 設(shè)置連接超時
- 4.4 判斷Socket是否可用
- 4.4.1 自定義心跳包
- 4.4.2 通過發(fā)送緊急數(shù)據(jù)
- 4.4.3 真的需要判斷連接斷開嗎
- 4.5 設(shè)置端口重用SO_REUSEADDR?
- 4.6 設(shè)置關(guān)閉等待SO_LINGER
- 4.7 設(shè)置發(fā)送延遲策略TCP_NODELAY
- 4.8 設(shè)置輸出輸出緩沖區(qū)大小SO_RCVBUF/SO_SNDBUF
- 4.9 設(shè)置保持連接存活SO_KEEPALIVE
- 4.10 異常:java.net.SocketException: Connection reset by peer
五、關(guān)于Socket的理解
-
- 5.1 Socket和TCP/IP
- 5.2 Socket和RMI
- 5.3 DatagramSocket與Socket
- 5.4 拆包和黏包
- 5.4.1 黏包
- 5.4.2 拆包
一、Socket通信基本示例
這種模式是基礎(chǔ),必須掌握,后期對Socket的優(yōu)化都是在這個基礎(chǔ)上的,也是為以后學(xué)習(xí)NIO做鋪墊。
package yiwangzhibujian.onlysend;import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket;public class SocketServer {public static void main(String[] args) throws Exception {// 監(jiān)聽指定的端口int port = 55533;ServerSocket server = new ServerSocket(port);// server將一直等待連接的到來System.out.println("server將一直等待連接的到來");Socket socket = server.accept();// 建立好連接后,從socket中獲取輸入流,并建立緩沖區(qū)進(jìn)行讀取InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[1024];int len;StringBuilder sb = new StringBuilder();while ((len = inputStream.read(bytes)) != -1) {//注意指定編碼格式,發(fā)送方和接收方一定要統(tǒng)一,建議使用UTF-8sb.append(new String(bytes, 0, len,"UTF-8"));}System.out.println("get message from client: " + sb);inputStream.close();socket.close();server.close();} }服務(wù)端監(jiān)聽一個端口,等待連接的到來。
package yiwangzhibujian.onlysend;import java.io.OutputStream; import java.net.Socket;public class SocketClient {public static void main(String args[]) throws Exception {// 要連接的服務(wù)端IP地址和端口String host = "127.0.0.1"; int port = 55533;// 與服務(wù)端建立連接Socket socket = new Socket(host, port);// 建立連接后獲得輸出流OutputStream outputStream = socket.getOutputStream();String message="你好 yiwangzhibujian";socket.getOutputStream().write(message.getBytes("UTF-8"));outputStream.close();socket.close();} }客戶端通過ip和端口,連接到指定的server,然后通過Socket獲得輸出流,并向其輸出內(nèi)容,服務(wù)器會獲得消息。最終服務(wù)端控制臺打印如下:
server將一直等待連接的到來 get message from client: 你好 yiwangzhibujian通過這個例子應(yīng)該掌握并了解:
- Socket服務(wù)端和客戶端的基本編程
- 傳輸編碼統(tǒng)一指定,防止亂碼
這個例子做為學(xué)習(xí)的基本例子,實際開發(fā)中會有各種變形,比如客戶端在發(fā)送完消息后,需要服務(wù)端進(jìn)行處理并返回,如下。
二、消息通信優(yōu)化
2.1 雙向通信,發(fā)送消息并接受消息
這個也是做為Socket編程的基本,應(yīng)該掌握,例子如下:
package yiwangzhibujian.waitreceive;import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket;public class SocketServer {public static void main(String[] args) throws Exception {// 監(jiān)聽指定的端口int port = 55533;ServerSocket server = new ServerSocket(port);// server將一直等待連接的到來System.out.println("server將一直等待連接的到來");Socket socket = server.accept();// 建立好連接后,從socket中獲取輸入流,并建立緩沖區(qū)進(jìn)行讀取InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[1024];int len;StringBuilder sb = new StringBuilder();//只有當(dāng)客戶端關(guān)閉它的輸出流的時候,服務(wù)端才能取得結(jié)尾的-1while ((len = inputStream.read(bytes)) != -1) {// 注意指定編碼格式,發(fā)送方和接收方一定要統(tǒng)一,建議使用UTF-8sb.append(new String(bytes, 0, len, "UTF-8"));}System.out.println("get message from client: " + sb);OutputStream outputStream = socket.getOutputStream();outputStream.write("Hello Client,I get the message.".getBytes("UTF-8"));inputStream.close();outputStream.close();socket.close();server.close();} }與之前server的不同在于,當(dāng)讀取完客戶端的消息后,打開輸出流,將指定消息發(fā)送回客戶端,客戶端程序為:
package yiwangzhibujian.waitreceive;import java.io.InputStream; import java.io.OutputStream; import java.net.Socket;public class SocketClient {public static void main(String args[]) throws Exception {// 要連接的服務(wù)端IP地址和端口String host = "127.0.0.1";int port = 55533;// 與服務(wù)端建立連接Socket socket = new Socket(host, port);// 建立連接后獲得輸出流OutputStream outputStream = socket.getOutputStream();String message = "你好 yiwangzhibujian";socket.getOutputStream().write(message.getBytes("UTF-8"));//通過shutdownOutput高速服務(wù)器已經(jīng)發(fā)送完數(shù)據(jù),后續(xù)只能接受數(shù)據(jù)socket.shutdownOutput();InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[1024];int len;StringBuilder sb = new StringBuilder();while ((len = inputStream.read(bytes)) != -1) {//注意指定編碼格式,發(fā)送方和接收方一定要統(tǒng)一,建議使用UTF-8sb.append(new String(bytes, 0, len,"UTF-8"));}System.out.println("get message from server: " + sb);inputStream.close();outputStream.close();socket.close();} }客戶端也有相應(yīng)的變化,在發(fā)送完消息時,調(diào)用關(guān)閉輸出流方法,然后打開輸出流,等候服務(wù)端的消息。
2.2 使用場景
這個模式的使用場景一般用在,客戶端發(fā)送命令給服務(wù)器,然后服務(wù)器相應(yīng)指定的命令,如果只是客戶端發(fā)送消息給服務(wù)器,然后讓服務(wù)器返回收到消息的消息,這就有點過分了,這就是完全不相信Socket的傳輸安全性,要知道它的底層可是TCP,如果沒有發(fā)送到服務(wù)器端是會拋異常的,這點完全不用擔(dān)心。
2.3 如何告知對方已發(fā)送完命令
其實這個問題還是比較重要的,正常來說,客戶端打開一個輸出流,如果不做約定,也不關(guān)閉它,那么服務(wù)端永遠(yuǎn)不知道客戶端是否發(fā)送完消息,那么服務(wù)端會一直等待下去,直到讀取超時。所以怎么告知服務(wù)端已經(jīng)發(fā)送完消息就顯得特別重要。
2.3.1 通過Socket關(guān)閉
這個是第一章介紹的方式,當(dāng)Socket關(guān)閉的時候,服務(wù)端就會收到響應(yīng)的關(guān)閉信號,那么服務(wù)端也就知道流已經(jīng)關(guān)閉了,這個時候讀取操作完成,就可以繼續(xù)后續(xù)工作。
但是這種方式有一些缺點
- 客戶端Socket關(guān)閉后,將不能接受服務(wù)端發(fā)送的消息,也不能再次發(fā)送消息
- 如果客戶端想再次發(fā)送消息,需要重現(xiàn)創(chuàng)建Socket連接
2.3.2 通過Socket關(guān)閉輸出流的方式
這種方式調(diào)用的方法是:
socket.shutdownOutput();而不是(outputStream為發(fā)送消息到服務(wù)端打開的輸出流):
outputStream.close();如果關(guān)閉了輸出流,那么相應(yīng)的Socket也將關(guān)閉,和直接關(guān)閉Socket一個性質(zhì)。
調(diào)用Socket的shutdownOutput()方法,底層會告知服務(wù)端我這邊已經(jīng)寫完了,那么服務(wù)端收到消息后,就能知道已經(jīng)讀取完消息,如果服務(wù)端有要返回給客戶的消息那么就可以通過服務(wù)端的輸出流發(fā)送給客戶端,如果沒有,直接關(guān)閉Socket。
這種方式通過關(guān)閉客戶端的輸出流,告知服務(wù)端已經(jīng)寫完了,雖然可以讀到服務(wù)端發(fā)送的消息,但是還是有一點點缺點:
- 不能再次發(fā)送消息給服務(wù)端,如果再次發(fā)送,需要重新建立Socket連接
這個缺點,在訪問頻率比較高的情況下將是一個需要優(yōu)化的地方。
2.3.3 通過約定符號
這種方式的用法,就是雙方約定一個字符或者一個短語,來當(dāng)做消息發(fā)送完成的標(biāo)識,通常這么做就需要改造讀取方法。
假如約定單端的一行為end,代表發(fā)送完成,例如下面的消息,end則代表消息發(fā)送完成:
hello yiwangzhibujian end那么服務(wù)端響應(yīng)的讀取操作需要進(jìn)行如下改造:
Socket socket = server.accept(); // 建立好連接后,從socket中獲取輸入流,并建立緩沖區(qū)進(jìn)行讀取 BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8")); String line; StringBuilder sb = new StringBuilder(); while ((line = read.readLine()) != null && "end".equals(line)) {//注意指定編碼格式,發(fā)送方和接收方一定要統(tǒng)一,建議使用UTF-8sb.append(line); }可以看見,服務(wù)端不僅判斷是否讀到了流的末尾,還判斷了是否讀到了約定的末尾。
這么做的優(yōu)缺點如下:
- 優(yōu)點:不需要關(guān)閉流,當(dāng)發(fā)送完一條命令(消息)后可以再次發(fā)送新的命令(消息)
- 缺點:需要額外的約定結(jié)束標(biāo)志,太簡單的容易出現(xiàn)在要發(fā)送的消息中,誤被結(jié)束,太復(fù)雜的不好處理,還占帶寬
經(jīng)過了這么多的優(yōu)化還是有缺點,難道就沒有完美的解決方案嗎,答案是有的,看接下來的內(nèi)容。
2.3.4 通過指定長度
如果你了解一點class文件的結(jié)構(gòu)(后續(xù)會寫,敬請期待),那么你就會佩服這么設(shè)計方式,也就是說我們可以在此找靈感,就是我們可以先指定后續(xù)命令的長度,然后讀取指定長度的內(nèi)容做為客戶端發(fā)送的消息。
現(xiàn)在首要的問題就是用幾個字節(jié)指定長度呢,我們可以算一算:
- 1個字節(jié):最大256,表示256B
- 2個字節(jié):最大65536,表示64K
- 3個字節(jié):最大16777216,表示16M
- 4個字節(jié):最大4294967296,表示4G
- 依次類推
這個時候是不是很糾結(jié),最大的當(dāng)然是最保險的,但是真的有必要選擇最大的嗎,其實如果你稍微了解一點UTF-8的編碼方式(字符編碼后續(xù)會寫,敬請期待),那么你就應(yīng)該能想到為什么一定要固定表示長度字節(jié)的長度呢,我們可以使用變長方式來表示長度的表示,比如:
- 第一個字節(jié)首位為0:即0XXXXXXX,表示長度就一個字節(jié),最大128,表示128B
- 第一個字節(jié)首位為110,那么附帶后面一個字節(jié)表示長度:即110XXXXX?10XXXXXX,最大2048,表示2K
- 第一個字節(jié)首位為1110,那么附帶后面二個字節(jié)表示長度:即110XXXXX?10XXXXXX?10XXXXXX,最大131072,表示128K
- 依次類推
上面提到的這種用法適合高富帥的程序員使用,一般呢,如果用作命名發(fā)送,兩個字節(jié)就夠了,如果還不放心4個字節(jié)基本就能滿足你的所有要求,下面的例子我們將采用2個字節(jié)表示長度,目的只是給你一種思路,讓你知道有這種方式來獲取消息的結(jié)尾:
服務(wù)端程序:
package yiwangzhibujian.waitreceive2;import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket;public class SocketServer {public static void main(String[] args) throws Exception {// 監(jiān)聽指定的端口int port = 55533;ServerSocket server = new ServerSocket(port);// server將一直等待連接的到來System.out.println("server將一直等待連接的到來");Socket socket = server.accept();// 建立好連接后,從socket中獲取輸入流,并建立緩沖區(qū)進(jìn)行讀取InputStream inputStream = socket.getInputStream();byte[] bytes;// 因為可以復(fù)用Socket且能判斷長度,所以可以一個Socket用到底while (true) {// 首先讀取兩個字節(jié)表示的長度int first = inputStream.read();//如果讀取的值為-1 說明到了流的末尾,Socket已經(jīng)被關(guān)閉了,此時將不能再去讀取if(first==-1){break;}int second = inputStream.read();int length = (first << 8) + second;// 然后構(gòu)造一個指定長的byte數(shù)組bytes = new byte[length];// 然后讀取指定長度的消息即可inputStream.read(bytes);System.out.println("get message from client: " + new String(bytes, "UTF-8"));}inputStream.close();socket.close();server.close();} }此處的讀取步驟為,先讀取兩個字節(jié)的長度,然后讀取消息,客戶端為:
package yiwangzhibujian.waitreceive2;import java.io.OutputStream; import java.net.Socket;public class SocketClient {public static void main(String args[]) throws Exception {// 要連接的服務(wù)端IP地址和端口String host = "127.0.0.1";int port = 55533;// 與服務(wù)端建立連接Socket socket = new Socket(host, port);// 建立連接后獲得輸出流OutputStream outputStream = socket.getOutputStream();String message = "你好 yiwangzhibujian";//首先需要計算得知消息的長度byte[] sendBytes = message.getBytes("UTF-8");//然后將消息的長度優(yōu)先發(fā)送出去outputStream.write(sendBytes.length >>8);outputStream.write(sendBytes.length);//然后將消息再次發(fā)送出去outputStream.write(sendBytes);outputStream.flush();//==========此處重復(fù)發(fā)送一次,實際項目中為多個命名,此處只為展示用法message = "第二條消息";sendBytes = message.getBytes("UTF-8");outputStream.write(sendBytes.length >>8);outputStream.write(sendBytes.length);outputStream.write(sendBytes);outputStream.flush();//==========此處重復(fù)發(fā)送一次,實際項目中為多個命名,此處只為展示用法message = "the third message!";sendBytes = message.getBytes("UTF-8");outputStream.write(sendBytes.length >>8);outputStream.write(sendBytes.length);outputStream.write(sendBytes); outputStream.close();socket.close();} }客戶端要多做的是,在發(fā)送消息之前先把消息的長度發(fā)送過去。
這種事先約定好長度的做法解決了之前提到的種種問題,Redis的Java客戶端Jedis就是用這種方式實現(xiàn)的這種方式的缺點:
- 暫時還沒發(fā)現(xiàn)
當(dāng)然如果是需要服務(wù)器返回結(jié)果,那么也依然使用這種方式,服務(wù)端也是先發(fā)送結(jié)果的長度,然后客戶端進(jìn)行讀取。當(dāng)然現(xiàn)在流行的就是,長度+類型+數(shù)據(jù)模式的傳輸方式。
三、服務(wù)端優(yōu)化
3.1 服務(wù)端并發(fā)處理能力
在上面的例子中,服務(wù)端僅僅只是接受了一個Socket請求,并處理了它,然后就結(jié)束了,但是在實際開發(fā)中,一個Socket服務(wù)往往需要服務(wù)大量的Socket請求,那么就不能再服務(wù)完一個Socket的時候就關(guān)閉了,這時候可以采用循環(huán)接受請求并處理的邏輯:
package yiwangzhibujian.multiserver;import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket;public class SocketServer {public static void main(String args[]) throws IOException {// 監(jiān)聽指定的端口int port = 55533;ServerSocket server = new ServerSocket(port);// server將一直等待連接的到來System.out.println("server將一直等待連接的到來");while(true){Socket socket = server.accept();// 建立好連接后,從socket中獲取輸入流,并建立緩沖區(qū)進(jìn)行讀取InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[1024];int len;StringBuilder sb = new StringBuilder();while ((len = inputStream.read(bytes)) != -1) {// 注意指定編碼格式,發(fā)送方和接收方一定要統(tǒng)一,建議使用UTF-8sb.append(new String(bytes, 0, len, "UTF-8"));}System.out.println("get message from client: " + sb);inputStream.close();socket.close();}} }這種一般也是新手寫法,但是能夠循環(huán)處理多個Socket請求,不過當(dāng)一個請求的處理比較耗時的時候,后面的請求將被阻塞,所以一般都是用多線程的方式來處理Socket,即每有一個Socket請求的時候,就創(chuàng)建一個線程來處理它。
不過在實際生產(chǎn)中,創(chuàng)建的線程會交給線程池來處理,為了:
- 線程復(fù)用,創(chuàng)建線程耗時,回收線程慢
- 防止短時間內(nèi)高并發(fā),指定線程池大小,超過數(shù)量將等待,方式短時間創(chuàng)建大量線程導(dǎo)致資源耗盡,服務(wù)掛掉
使用線程池的方式,算是一種成熟的方式。可以應(yīng)用在生產(chǎn)中。
3.2 服務(wù)端其他屬性
ServerSocket有以下3個屬性。
- SO_TIMEOUT:表示等待客戶連接的超時時間。一般不設(shè)置,會持續(xù)等待。
- SO_REUSEADDR:表示是否允許重用服務(wù)器所綁定的地址。一般不設(shè)置,經(jīng)我的測試沒必要,下面會進(jìn)行詳解。
- SO_RCVBUF:表示接收數(shù)據(jù)的緩沖區(qū)的大小。一般不設(shè)置,用系統(tǒng)默認(rèn)就可以了。
具體詳細(xì)的解釋可以參照下面。
3.3 性能再次提升
當(dāng)現(xiàn)在的性能還不能滿足需求的時候,就需要考慮使用NIO,這不是本篇的內(nèi)容,后續(xù)會貼出。
四、Socket的其它知識
其實如果經(jīng)常看有關(guān)網(wǎng)絡(luò)編程的源碼的話,就會發(fā)現(xiàn)Socket還是有很多設(shè)置的,可以學(xué)著用,但是還是要有一些基本的了解比較好。下面就對Socket的Java API中涉及到的進(jìn)行簡單講解。首先呢Socket有哪些可以設(shè)置的選項,其實在SocketOptions接口中已經(jīng)都列出來了:
- int TCP_NODELAY = 0x0001:對此連接禁用 Nagle 算法。
- int SO_BINDADDR = 0x000F:此選項為 TCP 或 UDP 套接字在 IP 地址頭中設(shè)置服務(wù)類型或流量類字段。
- int SO_REUSEADDR = 0x04:設(shè)置套接字的 SO_REUSEADDR。
- int SO_BROADCAST = 0x0020:此選項啟用和禁用發(fā)送廣播消息的處理能力。
- int IP_MULTICAST_IF = 0x10:設(shè)置用于發(fā)送多播包的傳出接口。
- int IP_MULTICAST_IF2 = 0x1f:設(shè)置用于發(fā)送多播包的傳出接口。
- int IP_MULTICAST_LOOP = 0x12:此選項啟用或禁用多播數(shù)據(jù)報的本地回送。
- int IP_TOS = 0x3:此選項為 TCP 或 UDP 套接字在 IP 地址頭中設(shè)置服務(wù)類型或流量類字段。
- int SO_LINGER = 0x0080:指定關(guān)閉時逗留的超時值。
- int SO_TIMEOUT = 0x1006:設(shè)置阻塞 Socket 操作的超時值: ServerSocket.accept(); SocketInputStream.read(); DatagramSocket.receive(); 選項必須在進(jìn)入阻塞操作前設(shè)置才能生效。
- int SO_SNDBUF = 0x1001:設(shè)置傳出網(wǎng)絡(luò) I/O 的平臺所使用的基礎(chǔ)緩沖區(qū)大小的提示。
- int SO_RCVBUF = 0x1002:設(shè)置傳入網(wǎng)絡(luò) I/O 的平臺所使用基礎(chǔ)緩沖區(qū)的大小的提示。
- int SO_KEEPALIVE = 0x0008:為 TCP 套接字設(shè)置 keepalive 選項時
- int SO_OOBINLINE = 0x1003:置 OOBINLINE 選項時,在套接字上接收的所有 TCP 緊急數(shù)據(jù)都將通過套接字輸入流接收。
上面只是簡單介紹了下(來源Java API),下面有對其中的某些的詳細(xì)講解,沒講到的后續(xù)如果用到會補上。
4.1 客戶端綁定端口
服務(wù)端綁定端口是可以理解的,因為要監(jiān)聽指定的端口,但是客戶端為什么要綁定端口,說實話我覺得這么做的人有點2,或許有的網(wǎng)絡(luò)安全策略配置了端口訪出,使用戶只能使用指定的端口,那么這樣的配置也是挺2的,直接說就可以不要留面子。
當(dāng)然首先要理解的是,如果沒有指定端口的話,Socket會自動選取一個可以用的端口,不用瞎操心的。
但是你非得指定一個端口也是可以的,做法如下,這時候就不能用Socket的構(gòu)造方法了,要一步一步來:
// 要連接的服務(wù)端IP地址和端口 String host = "localhost"; int port = 55533; // 與服務(wù)端建立連接 Socket socket = new Socket(); socket.bind(new InetSocketAddress(55534)); socket.connect(new InetSocketAddress(host, port));這樣做就可以了,但是當(dāng)這個程序執(zhí)行完成以后,再次執(zhí)行就會報,端口占用異常:
java.net.BindException: Address already in use: connect明明上一個Socket已經(jīng)關(guān)閉了,為什么再次使用還會說已經(jīng)被占用了呢?如果你是用netstat 命令來查看端口的使用情況:
netstat -n|findstr "55533" TCP 127.0.0.1:55534 127.0.0.1:55533 TIME_WAIT就會發(fā)現(xiàn)端口的使用狀態(tài)為TIME_WAIT,說到這你需要有一點TCP連接的基本常識,建議看《》,這是其中的一點摘抄筆記,或許對理解有一些幫助。
簡單來說,當(dāng)連接主動關(guān)閉后,端口狀態(tài)變?yōu)門IME_WAIT,其他程序依然不能使用這個端口,防止服務(wù)端因為超時重新發(fā)送的確認(rèn)連接斷開對新連接的程序造成影響。
TIME_WAIT的時間一般有底層決定,一般是2分鐘,還有1分鐘和30秒的。
所以,客戶端不要綁定端口,不要綁定端口,不要綁定端口。
4.2?讀超時SO_TIMEOUT
讀超時這個屬性還是比較重要的,當(dāng)Socket優(yōu)化到最后的時候,往往一個Socket連接會一直用下去,那么當(dāng)一端因為異常導(dǎo)致連接沒有關(guān)閉,另一方是不應(yīng)該持續(xù)等下去的,所以應(yīng)該設(shè)置一個讀取的超時時間,當(dāng)超過指定的時間后,還沒有讀到數(shù)據(jù),就假定這個連接無用,然后拋異常,捕獲異常后關(guān)閉連接就可以了,調(diào)用方法為:
public void setSoTimeout(int timeout) throws SocketExceptiontimeout - 指定的以毫秒為單位的超時值。設(shè)置0為持續(xù)等待下去。建議根據(jù)網(wǎng)絡(luò)環(huán)境和實際生產(chǎn)環(huán)境選擇。
這個選項設(shè)置的值將對以下操作有影響:
- ServerSocket.accept()
- SocketInputStream.read()
- DatagramSocket.receive()
4.3 設(shè)置連接超時
這個連接超時和上面說的讀超時不一樣,讀超時是在建立連接以后,讀數(shù)據(jù)時使用的,而連接超時是在進(jìn)行連接的時候,等待的時間。
4.4 判斷Socket是否可用
當(dāng)需要判斷一個Socket是否可用的時候,不能簡簡單單判斷是否為null,是否關(guān)閉,下面給出一個比較全面的判斷Socket是否可用的表達(dá)式,這是根據(jù)Socket自身的一些狀態(tài)進(jìn)行判斷的,它的狀態(tài)有:
- bound:是否綁定
- closed:是否關(guān)閉
- connected:是否連接
- shutIn:是否關(guān)閉輸入流
- shutOut:是否關(guān)閉輸出流
建議如此使用,但這只是第一步,保證Socket自身的狀態(tài)是可用的,但是當(dāng)連接正常創(chuàng)建后,上面的屬性如果不調(diào)用本方相應(yīng)的方法是不會改變的,也就是說如果網(wǎng)絡(luò)斷開、服務(wù)器主動斷開,Java底層是不會檢測到連接斷開并改變Socket的狀態(tài),所以,真實的檢測連接狀態(tài)還是得通過額外的手段,有兩種方式。
4.4.1 自定義心跳包
雙方需要約定,什么樣的消息屬于心跳包,什么樣的消息屬于正常消息,假設(shè)你看了上面的章節(jié)現(xiàn)在說就容易理解了,我們定義前兩個字節(jié)為消息的長度,那么我們就可以定義第3個字節(jié)為消息的屬性,可以指定一位為消息的類型,1為心跳,0為正常消息。那么要做的有如下:
- 客戶端發(fā)送心跳包
- 服務(wù)端獲取消息判斷是否是心跳包,若是丟棄
- 當(dāng)客戶端發(fā)送心跳包失敗時,就可以斷定連接不可用
具體的編碼不再貼出,自己實現(xiàn)即可。
4.4.2 通過發(fā)送緊急數(shù)據(jù)
Socket自帶一種模式,那就是發(fā)送緊急數(shù)據(jù),這有一個前提,那就是服務(wù)端的OOBINLINE不能設(shè)置為true,它的默認(rèn)值是false。
OOBINLINE的true和false影響了什么:
- 對客戶端沒有影響
- 對服務(wù)端,如果設(shè)置為true,那么服務(wù)端將會捕獲緊急數(shù)據(jù),這會對接收數(shù)據(jù)造成混淆,需要額外判斷
發(fā)送緊急數(shù)據(jù)通過調(diào)用Socket的方法:
socket.sendUrgentData(0);發(fā)送數(shù)據(jù)任意即可,因為OOBINLINE為false的時候,服務(wù)端會丟棄掉緊急數(shù)據(jù)。
當(dāng)發(fā)送緊急數(shù)據(jù)報錯以后,我們就會知道連接不通了。
4.4.3 真的需要判斷連接斷開嗎
通過上面的兩種方式已經(jīng)可以判斷出連接是否可用,然后我們就可以進(jìn)行后續(xù)操作,可是請大家認(rèn)真考慮下面的問題:
如果你認(rèn)真考慮了上面的問題,那么你就會覺得發(fā)送心跳包完全是沒有必要的操作,通過發(fā)送心跳包來判斷連接是否可用是通過捕獲異常來判斷的。那么我們完全可以在發(fā)送消息報出IO異常的時候,在異常中重新發(fā)送一次即可,這兩種方式的編碼有什么不同呢,下面寫一寫偽代碼。
提前檢測連接是否可用:
//有一個連接中的socket Socket socket=... //要發(fā)送的數(shù)據(jù) String data=""; try{//發(fā)送心跳包或者緊急數(shù)據(jù),來檢測連接的可用性 }catch (Excetption e){//打印日志,并重連Socketsocket=new Socket(host,port); } socket.write(data);直接發(fā)送數(shù)據(jù),出異常后重新連接再次發(fā)送:
//有一個連接中的socket Socket socket=... //要發(fā)送的數(shù)據(jù) String data=""; try{socket.write(data); }catch (Excetption e){//打印日志,并重連Socketsocket=new Socket(host,port);socket.write(data); }通過比較可以發(fā)現(xiàn)兩種方式的特點,現(xiàn)在簡單介紹下:
- 兩種方式均可實現(xiàn)連接斷開重新連接并發(fā)送
- 提前檢測,再每次發(fā)送消息的時候都要檢測,影響效率,占用帶寬
希望大家認(rèn)真考慮,做出自己的選擇。
4.5 設(shè)置端口重用SO_REUSEADDR?
首先,創(chuàng)建Socket時,默認(rèn)是禁止的,設(shè)置true有什么作用呢,Java API中是這么介紹的:
關(guān)閉 TCP 連接時,該連接可能在關(guān)閉后的一段時間內(nèi)保持超時狀態(tài)(通常稱為 TIME_WAIT 狀態(tài)或 2MSL 等待狀態(tài))。對于使用已知套接字地址或端口的應(yīng)用程序而言,如果存在處于超時狀態(tài)的連接(包括地址和端口),可能不能將套接字綁定到所需的 SocketAddress 上。
使用 bind(SocketAddress) 綁定套接字前啟用 SO_REUSEADDR 允許在上一個連接處于超時狀態(tài)時綁定套接字。
一般是用在綁定端口的時候使用,但是經(jīng)過我的測試建議如下:
- 服務(wù)端綁定端口后,關(guān)閉服務(wù)端,重新啟動后不會提示端口占用
- 客戶端綁定端口后,關(guān)閉,即便設(shè)置ReuseAddress為true,即便能綁定端口,連接的時候還是會報端口占用異常
綜上所述,不建議綁定端口,也沒必要設(shè)置ReuseAddress,當(dāng)然ReuseAddress的底層還是和硬件有關(guān)系的,或許在你的機器上測試結(jié)果和我不一樣,若是如此和平臺相關(guān)性差異這么大配置更是不建議使用了。
4.6 設(shè)置關(guān)閉等待SO_LINGER
Java API的介紹是:啟用/禁用具有指定逗留時間(以秒為單位)的 SO_LINGER。最大超時值是特定于平臺的。 該設(shè)置僅影響套接字關(guān)閉。?
大家都是這么說的,當(dāng)調(diào)用Socket的close方法后,沒有發(fā)送的數(shù)據(jù)將不再發(fā)送,設(shè)置這個值的話,Socket會等待指定的時間發(fā)送完數(shù)據(jù)包。說實話,經(jīng)過我簡單的測試,對于一般數(shù)據(jù)量來說,幾十K左右,即便直接關(guān)閉Socket的連接,服務(wù)端也是可以收到數(shù)據(jù)的。
所以對于一般應(yīng)用沒必要設(shè)置這個值,當(dāng)數(shù)據(jù)量發(fā)送過大拋出異常時,再來設(shè)置這個值也不晚。那么到達(dá)逗留超時值時,套接字將通過 TCP RST 強制性 關(guān)閉。啟用超時值為零的選項將立即強制關(guān)閉。如果指定的超時值大于 65,535,則其將被減少到 65,535。?
4.7 設(shè)置發(fā)送延遲策略TCP_NODELAY
一般來說當(dāng)客戶端想服務(wù)器發(fā)送數(shù)據(jù)的時候,會根據(jù)當(dāng)前數(shù)據(jù)量來決定是否發(fā)送,如果數(shù)據(jù)量過小,那么系統(tǒng)將會根據(jù)Nagle 算法(暫時還沒研究),來決定發(fā)送包的合并,也就是說發(fā)送會有延遲,這在有時候是致命的,比如說對實時性要求很高的消息發(fā)送,在線對戰(zhàn)游戲等,即便數(shù)據(jù)量很小也要求立即發(fā)送,如果稍有延遲就會感覺到卡頓,默認(rèn)情況下Nagle 算法是開啟的,所以如果不打算有延遲,最好關(guān)閉它。這樣一旦有數(shù)據(jù)將會立即發(fā)送而不會寫入緩沖區(qū)。
但是對延遲要求不是特別高下還是可以使用的,還是可以提升網(wǎng)絡(luò)傳輸效率的。
4.8 設(shè)置輸出輸出緩沖區(qū)大小SO_RCVBUF/SO_SNDBUF
- SO_SNDBUF:發(fā)送緩沖
- SO_RCVBUF:接收緩沖
默認(rèn)都是8K,如果有需要可以修改,通過相應(yīng)的set方法。不建議修改的太小,設(shè)置太小數(shù)據(jù)傳輸將過于頻繁。太大了將會造成消息停留。
不過我對這個經(jīng)過測試后有以下結(jié)論:
- 當(dāng)數(shù)據(jù)填滿緩沖區(qū)時,一定會發(fā)送
- 當(dāng)數(shù)據(jù)沒有填滿緩沖區(qū)時也會發(fā)送,這個算法還是上面說的Nagle 算法
4.9 設(shè)置保持連接存活SO_KEEPALIVE
雖然說當(dāng)設(shè)置連接連接的讀超時為0,即無限等待時,Socket不會被主動關(guān)閉,但是總會有莫名其妙的軟件來檢測你的連接是否有數(shù)據(jù)發(fā)送,長時間沒有數(shù)據(jù)傳輸?shù)倪B接會被它們關(guān)閉掉。
因此通過設(shè)置這個選項為true,可以有如下效果:當(dāng)2個小時(具體的實現(xiàn)而不同)內(nèi)在任意方向上都沒有跨越套接字交換數(shù)據(jù),則 TCP 會自動發(fā)送一個保持存活的消息到對面。將會有以下三種響應(yīng):
所以對于構(gòu)建長時間連接的Socket還是配置上SO_KEEPALIVE比較好。
4.10 異常:java.net.SocketException: Connection reset by peer
這個異常的含義是,我正在寫數(shù)據(jù)的時候,你把連接給關(guān)閉了。這個異常在一般正常的編碼是不會出現(xiàn)這個異常的,因為用戶通常會判斷是否讀到流的末尾了,讀到末尾才會進(jìn)行關(guān)閉操作,如果出現(xiàn)這個異常,那就檢查一下判斷是否讀到流的末尾邏輯是否正確。
?
五、關(guān)于Socket的理解
5.1 Socket和TCP/IP
最近在看《TCP/IP詳解 卷1:協(xié)議》,關(guān)于TCP/IP我覺得講解的非常詳細(xì),我做了點摘抄,可以大致看看,非常建議大家閱讀下這本書。通常TCP/IP分為四層:
也就是說Socket實際上是歸屬于應(yīng)用層,使用的事運輸層的TCP,使用SocketServer監(jiān)聽的端口,也是可以被Telnet連接的。可以看下面兩行代碼:
ServerSocket server = new ServerSocket(port); Socket socket = server.accept();在什么情況獲取到這個Socket呢,通過理論加測試,結(jié)論是在三次握手操作后,系統(tǒng)才會將這個連接交給應(yīng)用層,ServerSocket 才知道有一個連接過來了。那么系統(tǒng)當(dāng)接收到一個TCP連接請求后,如果上層還沒有接受它(假如SocketServer循環(huán)處理Socket,一次一個),那么系統(tǒng)將緩存這個連接請求,既然是緩存那么就是有限度的,書上介紹的是緩存3個,但是經(jīng)過我的本機測試是50個,也就是說,系統(tǒng)將會為應(yīng)用層的Socket緩存50和TCP連接(這是和系統(tǒng)底層有關(guān)系的),當(dāng)超過指定數(shù)量后,系統(tǒng)將會拒絕連接。
假如緩存的TCP連接請求發(fā)送來數(shù)據(jù),那么系統(tǒng)也會緩存這些數(shù)據(jù),等待SocketServer獲得這個連接的時候一并交給它,這個會在后期學(xué)習(xí)NIO進(jìn)行詳解。
換句話說,系統(tǒng)接收TCP連接請求放入緩存隊列,而SocketServer從緩存隊列獲取Socket。
而上面例子中的為了讓服務(wù)端知道發(fā)送完消息的,關(guān)閉輸出流的操作:
socket.shutdownOutput();其實是對應(yīng)著四次揮手的第一次:
也就是上面說的主動關(guān)閉,FIN_WAIT_1,這樣服務(wù)端就能得知客戶端發(fā)送完消息,此時服務(wù)端可以選擇關(guān)閉連接,也可以選擇發(fā)送數(shù)據(jù)后關(guān)閉連接:
這就是TCP所說的半關(guān)閉。其實很多知識都是想通的,多學(xué)點基礎(chǔ)知識還是有必要的。
5.2 Socket和RMI
RMI基礎(chǔ)知識就不多介紹了(后續(xù)會寫,敬請期待),現(xiàn)在假定你對RMI有所了解,那么一般就會對這兩種技術(shù)有所比較。或者說在應(yīng)用的時候就會想用那種技術(shù)比較好。
RMI全稱:Remote Method Invocation-遠(yuǎn)程方法調(diào)用,通過名字其實就能對這種技術(shù)有個初步的了解。現(xiàn)在我就簡單說說我對這兩種技術(shù)的想法。
這個待寫,等我寫完RMI博客的時候補上,那時候會更細(xì)致的了解下。
5.3 DatagramSocket與Socket
這一段涉及到UDP,依然和上面一樣,后續(xù)會補上。
5.4?拆包和黏包
使用Socket通信的時候,或多或少都聽過拆包和黏包,如果沒聽過而去貿(mào)然編程那么偶爾就會碰到一些莫名其妙的問題,所有有這方面的知識還是比較重要的,至少知道怎么發(fā)生,怎么防范。
現(xiàn)在先簡單說明下拆包和黏包的原因:
- 拆包:當(dāng)一次發(fā)送(Socket)的數(shù)據(jù)量過大,而底層(TCP/IP)不支持一次發(fā)送那么大的數(shù)據(jù)量,則會發(fā)生拆包現(xiàn)象。
- 黏包:當(dāng)在短時間內(nèi)發(fā)送(Socket)很多數(shù)據(jù)量小的包時,底層(TCP/IP)會根據(jù)一定的算法(指Nagle)把一些包合作為一個包發(fā)送。
首先可以明確的是,大部分情況下我們是不希望發(fā)生拆包和黏包的(如果希望發(fā)生,什么都去做即可),那么怎么去避免呢,下面進(jìn)行詳解?
5.4.1 黏包
首先我們應(yīng)該正確看待黏包,黏包實際上是對網(wǎng)絡(luò)通信的一種優(yōu)化,假如說上層只發(fā)送一個字節(jié)數(shù)據(jù),而底層卻發(fā)送了41個字節(jié),其中20字節(jié)的I P首部、 20字節(jié)的T C P首部和1個字節(jié)的數(shù)據(jù),而且發(fā)送完后還需要確認(rèn),這么做浪費了帶寬,量大時還會造成網(wǎng)絡(luò)擁堵。當(dāng)然它還是有一定的缺點的,就是因為它會合并一些包會導(dǎo)致數(shù)據(jù)不能立即發(fā)送出去,會造成延遲,如果能接受(一般延遲為200ms),那么還是不建議關(guān)閉這種優(yōu)化,如果因為黏包會造成業(yè)務(wù)上的錯誤,那么請改正你的服務(wù)端讀取算法(協(xié)議),因為即便不發(fā)生黏包,在服務(wù)端緩存區(qū)也可能會合并起來一起提交給上層,推薦使用長度+類型+數(shù)據(jù)模式。
如果不希望發(fā)生黏包,那么通過禁用TCP_NODELAY即可,Socket中也有相應(yīng)的方法:
void setTcpNoDelay(boolean on)通過設(shè)置為true即可防止在發(fā)送的時候黏包,但是當(dāng)發(fā)送的速率大于讀取的速率時,在服務(wù)端也會發(fā)生黏包,即因服務(wù)端讀取過慢,導(dǎo)致它一次可能讀取多個包。
5.4.2 拆包
這個問題應(yīng)該引起重視,在TCP/IP詳解中說過:最大報文段長度(MSS)表示TCP傳往另一端的最大塊數(shù)據(jù)的長度。當(dāng)一個連接建立時,連接的雙方都要通告各自的 MSS。客戶端會盡量滿足服務(wù)端的要求且不能大于服務(wù)端的MSS值,當(dāng)沒有協(xié)商時,會使用值536字節(jié)。雖然看起來MSS值越大越好,但是考慮到一些其他情況,這個值還是不太好確定,具體詳見《TCP/IP詳解 卷1:協(xié)議》。
如何應(yīng)對拆包,其實在上面2.3節(jié)已經(jīng)介紹過了,那就是如何表明發(fā)送完一條消息了,對于已知數(shù)據(jù)長度的模式,可以構(gòu)造相同大小的數(shù)組,循環(huán)讀取,示例代碼如下:
int length=1024;//這個是讀取的到數(shù)據(jù)長度,現(xiàn)假定1024 byte[] data=new byte[1024]; int readLength=0; while(readLength<length){int read = inputStream.read(data, readLength, length-readLength);readLength+=read; }這樣當(dāng)循環(huán)結(jié)束后,就能讀取到完整的一條數(shù)據(jù),而不需要考慮拆包了。
?
單單關(guān)于Java的Socket編程已經(jīng)基本介紹完成了,當(dāng)然還有更深層次的知識沒有涉及到,后續(xù)如果能有接觸也會寫出來,希望我的文章能幫助到有需要的人,如果有什么不對的地方請指出,禁止轉(zhuǎn)載。
轉(zhuǎn)載于:https://www.cnblogs.com/tc310/p/10611243.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的【转】Java Socket编程基础及深入讲解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ios高级开发之多线程(一)
- 下一篇: 这里蜗牛睡眠APP如何绑定手机号