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