如何使用Socket在客戶端實現(xiàn)長連接?
長連接貌似是一個很高深莫測的知識,但是只要你做直播、IM、游戲、彈幕里面的任何一種,或者是你的app想要實時的接收某些消息,你就會要接觸到長連接技術(shù)。本文主要教你如何在客戶端如何使用Socket實現(xiàn)長連接。
Socket背景知識
要做長連接的話,是不能用http協(xié)議來做的,因為http協(xié)議已經(jīng)是應(yīng)用層協(xié)議了,并且http協(xié)議是無狀態(tài)的,而我們要做長連接,肯定是需要在應(yīng)用層封裝自己的業(yè)務(wù),所以就需要基于TCP協(xié)議來做,而基于TCP協(xié)議的話,就要用到Socket了。
Socket是java針對tcp層通信封裝的一套網(wǎng)絡(luò)方案
TCP協(xié)議我們知道,是基于ip(或者域名)和端口對指定機(jī)器進(jìn)行的點對點訪問,他的連接成功有兩個條件,就是對方ip可以到達(dá)和端口是開放的
Socket能幫完成TCP三次握手,而應(yīng)用層的頭部信息需要自己去解析,也就是說,自己要制定好協(xié)議,并且要去解析byte
http也有長連接。在http1.0的時候,使用的是短連接,也就是說,每次請求一次數(shù)據(jù),都要重新建立連接。但是從http1.1之后,我們看到頭部會有一個
Connection:keep-alive
這個表示tcp連接建立之后不會馬上銷毀,而是保存一段時間,在這段時間內(nèi)如果需要請求改網(wǎng)站的其他數(shù)據(jù),都是使用這個連接來完成傳輸?shù)摹?/p>
Socket使用方式
Socket看上去不是很好用,因為他是基于java.io來實現(xiàn)的,你要直接跟InputStream和OutputStream打交道,也就是直接跟byte[]打交道,所以用起來并不是這么友好。
下面通過一個簡單的例子,往一臺服務(wù)器發(fā)\01 \00 \00 \00 \00這一串字節(jié),服務(wù)器也返回相同的字節(jié)流,上代碼:
@Test
public void testSocket() throws Exception {
logger.debug("start");
Socket socket = new Socket();
socket.connect(address);
byte[] output = new byte[]{(byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0};
socket.getOutputStream().write(output);
byte[] input = new byte[64];
int readByte = socket.getInputStream().read(input);
logger.debug("readByte " + readByte);
for (int i = 0; i < readByte; i++) {
logger.debug("read [" + i + "]:" + input[i]);
}
socket.close();
}
輸出:
11:40:40.326 [main] DEBUG com.roy.test.SocketTest - start
11:40:40.345 [main] DEBUG com.roy.test.SocketTest - readByte 5
11:40:40.345 [main] DEBUG com.roy.test.SocketTest - read 1
11:40:40.345 [main] DEBUG com.roy.test.SocketTest - read 0
11:40:40.345 [main] DEBUG com.roy.test.SocketTest - read 0
11:40:40.345 [main] DEBUG com.roy.test.SocketTest - read 0
11:40:40.345 [main] DEBUG com.roy.test.SocketTest - read 0
看出來寫起來還是比較麻煩的,主要就是InputStream, OutputStream 和byte[]使用起來太不方便了。
SocketChannel blocking
Socket為了優(yōu)化自己的封裝和并發(fā)性能,推出了nio包下面的SocketChannel,這個相比于Socket的好處就是并發(fā)性能的提高和封裝的優(yōu)化了。
SocketChannel有兩種方式——阻塞和非阻塞的,阻塞的用法和Socket差不多,都是在read和write的時候會阻塞線程,下面用一段代碼來實現(xiàn)相同的功能。
@Test
public void testSocketChannelBlock() throws Exception {
final SocketChannel channel = SocketChannel.open(address);
ByteBuffer output = ByteBuffer.allocate(5);
output.put((byte) 1);
output.putInt(0);
output.flip();
channel.write(output);
logger.debug("write complete, start read");
ByteBuffer input = ByteBuffer.allocate(5);
int readByte = channel.read(input);
logger.debug("readByte " + readByte);
input.flip();
if (readByte == -1) {
logger.debug("readByte == -1, return!");
return;
}
for (int i = 0; i < readByte; i++) {
logger.debug("read [" + i + "]:" + input.get());
}
}
log 輸出:
23:24:34.684 [main] DEBUG com.dz.test.SocketTest - write complete, start read
23:24:34.901 [main] DEBUG com.dz.test.SocketTest - readByte 5
23:24:34.901 [main] DEBUG com.dz.test.SocketTest - read [0]:1
23:24:34.901 [main] DEBUG com.dz.test.SocketTest - read [1]:0
23:24:34.901 [main] DEBUG com.dz.test.SocketTest - read [2]:0
23:24:34.901 [main] DEBUG com.dz.test.SocketTest - read [3]:0
23:24:34.901 [main] DEBUG com.dz.test.SocketTest - read [4]:0
從上面的。封裝優(yōu)化主要體現(xiàn)在ByteBuffer,IntBuffer這一系列類的封裝——因為是網(wǎng)絡(luò)相關(guān)的,所以這里用到的主要是ByteBuffer。
ByteBuffer和byte[]最大的區(qū)別,就是ByteBuffer可以很方便的讀取int, long等數(shù)據(jù)類型,他提供了getInt(), getInt(int offset)這樣的方法,這種方法主要用在識別頭部數(shù)據(jù)部分,因為頭部數(shù)據(jù)一般都是由多種數(shù)據(jù)類型組成,比方說表示數(shù)據(jù)格式的contentType:String,表示長度的length:int等等,這些就是getInt()這樣的方法主要的應(yīng)用場景,而byte[]如果要取int,String相對來說就要復(fù)雜一些了,這是java.nio相比于java.io優(yōu)勢的一點。
這里需要說明一個比較坑的點,就是ByteBuffer.flip()這個方法,這個方法的作用主要是重置索引,在write()之前和read()之后調(diào)用,否則會因為索引不對,導(dǎo)致你的數(shù)據(jù)寫不進(jìn)去,讀不出來。
ByteBuffer是一個功能強(qiáng)大的類,因為本文主要是講Socket和SocketChannel,所以在這里就不做過多描述。具體ByteBuffer的詳細(xì)介紹,可以參考:Java NIO系列教程(三) Buffer
而nio相比于io最大的優(yōu)勢還是在于并發(fā)性能,因為nio里面的n代表的就是non-blocking的意思,上面那個讀取數(shù)據(jù)的代碼也相對老舊,一般我們?nèi)绻肧ocketChannel,都是用non-blocking的方式來實現(xiàn)的,而如果要用non-blocking模式,首先要介紹的就是Selector。
Selector
我們知道,傳統(tǒng)io是阻塞的,也就是說,一個線程只能處理一個io流,也就是一個Socket。有了Selector之后,一個線程就能處理多個SocketChannel。
Selector的原