工欲善其事,必先利其器,從網際網路誕生到現在,基本上所有的程式都是網路程式,很少有單機版的程式了。 而網路程式設計的本質是兩個裝置之間的資料交換,當然,在計算機網路中,裝置主要指計算機。我們現在進行網路程式設計,基本上都是使用已經封裝好的框架,畢竟自己實現維護一套網路程式設計框架,費時費力不說,做出來之後還不一定好用,有那時間多保養保養頭髮。
Socket簡介
記得大學學習java的時候,並沒有接觸網路程式設計相關的基礎知識,一直都是單機版程式,沒有見識到網路程式設計的美妙,所以對socket這個東西既熟悉又陌生。熟悉的原因是在MFC實驗課中接觸到socket知識的,沒錯,就是那門古老的開發語言,現在已經銷聲匿跡了,陌生的原因大概也就不言而喻了。好了,說了那麼多,那socket到底是什麼呢?
Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面 。很多同學會把tcp、udp協議和socket搞混,其實Socket只是一種連線模式,不是協議。tcp、udp是兩個最基本的協議,很多其它協議都是基於這兩個協議。
用socket可以建立tcp連線,也可以建立udp連線,這意味著,用socket可以建立任何協議的連線。簡單的來說,socket相當於一艘船,你把目的地(ip+埠)告訴它,把要運輸的貨物搬上去,然後它將貨物送往目的地。這個貨物具體需要怎麼運輸,運輸完成之後是否還要通知你已經到達目的地,這就是實現協議的區別了。
Socket使用
首先來回顧一下socket的基本使用,建立一個服務端所需步驟
- 建立ServerSocket物件繫結監聽埠。
- 通過accept()方法監聽客戶端的請求。
- 建立連線後,通過輸入輸出流讀取客戶端傳送的請求資訊。
- 通過輸出流向客戶端傳送請求資訊。
- 關閉相關資源。
嗯。。。Talk is cheap,Show me the code:
public class SocketServer {
public static void main(String[] args) {
//第一步
SocketServer socketServer = new SocketServer();
socketServer.startServer(2333);
}
private void startServer(int port) {
try {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("伺服器已啟動,等待客戶連線...");
//第二步 呼叫accept()方法開始監聽,等待客戶端的連線 這個方法會阻塞當前執行緒
Socket socket = serverSocket.accept();
System.out.println("客戶端連線成功");
//第三步 建立輸入輸出流讀資料
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String receivedMsg;
while ((receivedMsg = bufferedReader.readLine()) != null && !("end").equals(receivedMsg)) {
System.out.println("客戶端:" + receivedMsg);
//第四步 給客戶端傳送請求
String response = "hello client";
System.out.println("我(服務端):" + response);
bufferedWriter.write(response+ "\n");
bufferedWriter.flush();
}
//關閉相關資源
socket.close();
serverSocket.close();
bufferedWriter.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製程式碼
建立一個客戶端所需步驟,其實和服務端程式碼差不多:
- 建立Socket物件,指明需要連線的伺服器的地址和埠。
- 建立連線後,通過輸出流向伺服器傳送請求資訊。
- 通過輸入流獲取伺服器的響應資訊。
- 關閉相關資源
public class SocketClient {
public static void main(String[] args){
SocketClient socketClient = new SocketClient();
socketClient.startClient(2333);
}
void startClient(int port){
try {
Socket clientSocket = new Socket("localhost",port);
System.out.println("客戶端已啟動");
//給伺服器發訊息
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
//接收伺服器傳過來的訊息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//鍵盤輸入訊息 傳送給服務端
BufferedReader inputReader = new BufferedReader(new InputStreamReader(System.in));
String readLine = null;
while (!(readLine = inputReader.readLine()).equals("bye")){
System.out.println("我(客戶端):" + readLine);
//將鍵盤輸入的訊息傳送給伺服器
bufferedWriter.write(readLine+"\n");
bufferedWriter.flush();
String response = bufferedReader.readLine();
System.out.println("服務端: " + response);
}
bufferedWriter.close();
inputReader.close();
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
複製程式碼
以上對於socket的示例比較簡單,只實現了客戶端給伺服器傳送訊息,伺服器收到訊息並回復客戶端。現實的情況其實很複雜,如伺服器高併發的處理,客戶端怎麼監聽伺服器隨時發來的訊息,客戶端斷線重連機制,心跳包的處理,還有在對訊息的拆包、粘包的處理等等。而引入mina框架,我們不必關注複雜的網路通訊的實現,只需專注於具體的業務邏輯。
Mina框架簡介
MINA框架是對java的NIO包的一個封裝,簡化了NIO程式開發的難度,封裝了很多底層的細節,讓開發者把精力集中到業務邏輯上來。可能有一些同學不知道NIO是什麼,這裡簡單介紹一下,NIO就是new IO,是jdk1.4引入的,它是同步非阻塞的,比如一個伺服器多個客戶端,客戶端傳送的請求都會註冊到伺服器的多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。它適用於連線數目多且連線比較短的架構,比如聊天伺服器。
mina簡單使用
Mina 的API 將真正的網路通訊與我們的應用程式隔離開來,你只需要關心你要傳送、接收的資料以及你的業務邏輯即可。如果想要使用mina,需要先去apache下載mina的jar包:mina下載地址,我使用的是2.0.16版本,下載解壓之後,只需要使用mina-core-2.0.16.jar和slf4j-android.jar這兩個包即可.
先看看我們實現的效果圖(由於是視訊轉gif,可能不太好看出來,就是一個簡單的文字通訊):
建立服務端
前面說mina將網路通訊與我們的應用程式隔離開來,那我們看看怎麼樣實現一個TCP的服務端,最近在學習kotlin,以後的程式碼應該都用kotlin展示了:
//建立一個非阻塞的service端的socket
val acceptor = NioSocketAcceptor()
//設定編解碼器 ProtocolCodecFilter攔截器 網路傳輸需要將物件轉換為位元組流
acceptor.filterChain.addLast("codec",ProtocolCodecFilter(TextLineCodecFactory()))
//設定讀取資料的緩衝區大小
acceptor.sessionConfig.readBufferSize = 2048
//讀寫通道10秒內無操作進入空閒狀態
acceptor.sessionConfig.setIdleTime(IdleStatus.BOTH_IDLE, 10)
//繫結埠
acceptor.bind(InetSocketAddress(8080))
複製程式碼
這段程式碼我們就初始化了一個TCP服務端,其中,編解碼器使用的是mina自帶的換行符編解碼器工廠,設定編解碼器是因為在網路上傳輸資料時,傳送端傳送資料需要將物件轉換為位元組流進行傳輸,接收端收到資料後再將位元組流轉換回來。相當於雙方約定一套規則,具體規則可以自己定,也可以用現成的。我這裡只需要傳送文字,就用內建的啦。
網路通訊已經實現,那傳送、接收資料呢?我們具體的業務邏輯都在IoHandler這個類中進行處理,編寫一個類繼承IoHandlerAdapter ,並重寫它的幾個方法,記得在bind埠之前呼叫acceptor.setHandler(MyIoHandlerAdapter()),不然無法監聽到具體事件 :
/**
* 向客戶端傳送訊息後會呼叫此方法
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageSent(session: IoSession?, message: Any?) {
super.messageSent(session, message)
LogUtils.i("伺服器傳送訊息成功")
}
/**
* 從埠接受訊息,會響應此方法來對訊息進行處理
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageReceived(session: IoSession?, message: Any?) {
super.messageReceived(session, message)
val msg = message!!.toString()
LogUtils.i("伺服器接收訊息成功:$msg")
}
/**
* 伺服器與客戶端建立連線
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionCreated(session: IoSession?) {
super.sessionCreated(session)
LogUtils.i("伺服器與客戶端建立連線")
}
/**
* 伺服器與客戶端連線開啟
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionOpened(session: IoSession?) {
super.sessionOpened(session)
LogUtils.i("伺服器與客戶端連線開啟")
}
/**
* 關閉與客戶端的連線時會呼叫此方法
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionClosed(session: IoSession?) {
super.sessionClosed(session)
LogUtils.i("關閉與客戶端的連線時會呼叫此方法")
}
/**
* 伺服器進入空閒狀態
* @param session
* @param status
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
super.sessionIdle(session, status)
LogUtils.i("伺服器進入空閒狀態")
}
/**
* 異常
* @param session
* @param cause
* @throws Exception
*/
@Throws(Exception::class)
override fun exceptionCaught(session: IoSession?, cause: Throwable) {
super.exceptionCaught(session, cause)
LogUtils.i("伺服器異常$cause")
}
複製程式碼
大家應該注意到IoSession
這個東西了,每一個方法引數裡都有它,那它具體是幹什麼的呢?
IoSession
是一個介面,這個介面用於表示Server 端與Client 端的連線,IoAcceptor.accept()
的時候返回例項。這個介面有如下常用的方法:
WriteFuture write(Object message):
這個方法用於寫資料,該操作是非同步的。CloseFuture close(boolean immediately):
這個方法用於關閉IoSession,該操作也是非同步的,引數指定true 表示立即關閉,否則就在所有的寫操作都flush 之後再關閉。Object setAttribute(Object key,Object value):
這個方法用於給我們向會話中新增一些屬性,這樣可以在會話過程中都可以使用,類似於HttpSession 的setAttrbute()方法。IoSession 內部使用同步的HashMap 儲存你新增的自定義屬性。SocketAddress getRemoteAddress():
這個方法獲取遠端連線的套接字地址。void suspendWrite():
這個方法用於掛起寫操作,那麼有void resumeWrite()方法與之配對。對於read()方法同樣適用。ReadFuture read():
這個方法用於讀取資料, 但預設是不能使用的, 你需要呼叫IoSessionConfig 的setUseReadOperation(true)才可以使用這個非同步讀取的方法。一般我們不會用到這個方法,因為這個方法的內部實現是將資料儲存到一個BlockingQueue,假如是Server 端,因為大量的Client 端傳送的資料在Server 端都這麼讀取,那麼可能會導致記憶體洩漏,但對於Client,可能有的時候會比較便利。IoService getService():
這個方法返回與當前會話物件關聯的IoService 例項。
換言之,拿到IoSession
就能夠進行兩端打call了,什麼?你問我怎麼打call?
fun sendText(message: String,client IoSession){
var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
ioBuffer.put(message.toByteArray())
ioBuffer.flip()
client.write(ioBuffer)
}
複製程式碼
建立客戶端
無論是Server 端還是Client 端,在Mina中的執行流程都是一樣的。唯一不同的就是IoService 的Client 端實現是IoConnector。
val connector = NioSocketConnector()
// 設定連結超時時間
connector.connectTimeoutMillis = 15000
// 新增過濾器
connector.filterChain.addLast("codec",ProtocolCodecFilter(TextLineCodecFactory()))
connector.setDefaultRemoteAddress(InetSocketAddress(ip, port))
val future = connector.connect()
future.awaitUninterruptibly()// 等待連線建立完成
var session = future.session// 獲得IoSession
複製程式碼
IoHandlerAdapter 和服務端一樣,這裡不做過多介紹。
最後貼上服務端程式碼:
class MinaServer : IoHandlerAdapter(){
private val acceptor: NioSocketAcceptor
private var isConnected = false
private var handler: Handler by Delegates.notNull()
init {
//建立一個非阻塞的service端的socket
acceptor = NioSocketAcceptor()
//設定編解碼器 ProtocolCodecFilter攔截器 網路傳輸需要將物件轉換為位元組流
acceptor.filterChain.addLast("codec",
ProtocolCodecFilter(TextLineCodecFactory()))
//設定讀取資料的緩衝區大小
acceptor.sessionConfig.readBufferSize = 2048
//讀寫通道10秒內無操作進入空閒狀態
acceptor.sessionConfig.setIdleTime(IdleStatus.BOTH_IDLE, 10)
handler = Handler()
}
fun connect(port: Int): MinaServer {
if (isConnected)
return this
thread {
try {
//註冊回撥 監聽和客戶端之間的訊息
acceptor.handler = this
acceptor.isReuseAddress = true
//繫結埠
acceptor.bind(InetSocketAddress(port))
isConnected = true
handler.post {
connectCallback?.onOpened()
}
} catch (e: Exception) {
e.printStackTrace()
handler.post {
connectCallback?.onError(e)
}
LogUtils.i("伺服器連線異常")
isConnected = false
}
}
return this
}
fun sendText(message: String){
for (client in acceptor.managedSessions.values){
var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
ioBuffer.put(message.toByteArray())
ioBuffer.flip()
client.write(ioBuffer)
}
}
/**
* 向客戶端傳送訊息後會呼叫此方法
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageSent(session: IoSession?, message: Any?) {
super.messageSent(session, message)
LogUtils.i("伺服器傳送訊息成功")
connectCallback?.onSendSuccess()
}
/**
* 從埠接受訊息,會響應此方法來對訊息進行處理
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageReceived(session: IoSession?, message: Any?) {
super.messageReceived(session, message)
handler.post {
connectCallback?.onGetMessage(message)
}
val msg = message!!.toString()
LogUtils.i("伺服器接收訊息成功:$msg")
}
/**
* 伺服器與客戶端建立連線
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionCreated(session: IoSession?) {
super.sessionCreated(session)
handler.post {
connectCallback?.onConnected()
}
LogUtils.i("伺服器與客戶端建立連線")
}
/**
* 伺服器與客戶端連線開啟
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionOpened(session: IoSession?) {
super.sessionOpened(session)
LogUtils.i("伺服器與客戶端連線開啟")
}
/**
* 關閉與客戶端的連線時會呼叫此方法
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionClosed(session: IoSession?) {
super.sessionClosed(session)
handler.post {
connectCallback?.onDisConnected()
}
LogUtils.i("關閉與客戶端的連線時會呼叫此方法")
}
/**
* 伺服器進入空閒狀態
* @param session
* @param status
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
super.sessionIdle(session, status)
LogUtils.i("伺服器進入空閒狀態")
}
/**
* 異常
* @param session
* @param cause
* @throws Exception
*/
@Throws(Exception::class)
override fun exceptionCaught(session: IoSession?, cause: Throwable) {
super.exceptionCaught(session, cause)
handler.post {
connectCallback?.onError(cause)
}
LogUtils.i("伺服器異常$cause")
}
private var connectCallback:ConnectCallback? = null
fun setConnectCallback(callback:ConnectCallback){
this.connectCallback = callback
}
interface ConnectCallback{
fun onSendSuccess()
fun onGetMessage(message: Any?)
fun onOpened()
fun onConnected()
fun onDisConnected()
fun onError(cause: Throwable)
}
}
複製程式碼
在服務端中的activity中使用:
var mServer = MinaServer()
mServer
.connect(2333)
.setConnectCallback(object : MinaServer.ConnectCallback {
override fun onSendSuccess() {
//傳送訊息成功
}
override fun onGetMessage(message: Any?) {
//接收訊息成功
val msg = message.toString()
}
override fun onOpened() {
}
override fun onConnected() {
}
override fun onDisConnected() {
}
override fun onError(cause: Throwable) {
Toast.makeText(applicationContext, "伺服器異常" + cause.toString(), Toast.LENGTH_SHORT).show()
}
})
複製程式碼
再看客戶端程式碼:
lass MinaClient : IoHandlerAdapter(){
private val connector: NioSocketConnector
private var session: IoSession? = null
var isConnected = false
private var handler:Handler by Delegates.notNull()
init {
connector = NioSocketConnector()
// 設定連結超時時間
connector.connectTimeoutMillis = 15000
// 新增過濾器
connector.filterChain.addLast("codec",
ProtocolCodecFilter(TextLineCodecFactory()))
handler = Handler()
}
fun connect(ip: String, port: Int): MinaClient {
if (isConnected)
return this
thread {
connector.handler = this
connector.setDefaultRemoteAddress(InetSocketAddress(ip, port))
//開始連線
try {
val future = connector.connect()
future.awaitUninterruptibly()// 等待連線建立完成
session = future.session// 獲得session
isConnected = session != null && session!!.isConnected
} catch (e: Exception) {
e.printStackTrace()
handler.post {
connectCallback?.onError(e)
}
println("客戶端連結異常...")
}
}
return this
}
fun disConnect(){
if (isConnected){
session?.closeOnFlush()
connector.dispose()
}else{
connectCallback?.onDisConnected()
}
}
fun sendText(message: String){
var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
ioBuffer.put(message.toByteArray())
ioBuffer.flip()
session?.write(ioBuffer)
}
/**
* 向服務端端傳送訊息後會呼叫此方法
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageSent(session: IoSession?, message: Any?) {
super.messageSent(session, message)
LogUtils.i("客戶端傳送訊息成功")
handler.post {
connectCallback?.onSendSuccess()
}
}
/**
* 從埠接受訊息,會響應此方法來對訊息進行處理
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageReceived(session: IoSession?, message: Any?) {
super.messageReceived(session, message)
LogUtils.i("客戶端接收訊息成功:")
handler.post {
connectCallback?.onGetMessage(message)
}
}
/**
* 伺服器與客戶端建立連線
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionCreated(session: IoSession?) {
super.sessionCreated(session)
LogUtils.i("伺服器與客戶端建立連線")
handler.post {
connectCallback?.onConnected()
}
}
/**
* 伺服器與客戶端連線開啟
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionOpened(session: IoSession?) {
super.sessionOpened(session)
LogUtils.i("伺服器與客戶端連線開啟")
}
/**
* 關閉與客戶端的連線時會呼叫此方法
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionClosed(session: IoSession?) {
super.sessionClosed(session)
LogUtils.i("關閉與客戶端的連線時會呼叫此方法")
isConnected = false
handler.post {
connectCallback?.onDisConnected()
}
}
/**
* 客戶端進入空閒狀態
* @param session
* @param status
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
super.sessionIdle(session, status)
LogUtils.i("客戶端進入空閒狀態")
}
/**
* 異常
* @param session
* @param cause
* @throws Exception
*/
@Throws(Exception::class)
override fun exceptionCaught(session: IoSession?, cause: Throwable) {
super.exceptionCaught(session, cause)
LogUtils.i("客戶端異常$cause")
handler.post {
connectCallback?.onError(cause)
}
}
private var connectCallback:ConnectCallback? = null
fun setConnectCallback(callback:ConnectCallback){
this.connectCallback = callback
}
interface ConnectCallback{
fun onSendSuccess()
fun onGetMessage(message: Any?)
fun onConnected()
fun onDisConnected()
fun onError(cause: Throwable)
}
}
複製程式碼
客戶端的activity中使用:
var mClient = MinaClient()
mClient
.connect("192.168.0.108", 2333)
.setConnectCallback(object : MinaClient.ConnectCallback {
override fun onGetMessage(message: Any?) {
val msg = message.toString()
}
override fun onConnected() {
}
override fun onDisConnected() {
Toast.makeText(applicationContext, "斷開連線成功", Toast.LENGTH_SHORT).show()
}
override fun onError(cause: Throwable) {
Toast.makeText(applicationContext, "伺服器異常" + cause.toString(), Toast.LENGTH_SHORT).show()
}
override fun onSendSuccess() {
}
})
複製程式碼
介面佈局比較簡單,就是一個recyclerview+幾個button,如果覺得我講得不夠清楚T^T,可以到github上檢視原始碼:minaSimple