工欲善其事,必先利其器,從互聯網誕生到如今,基本上全部的程序都是網絡程序,不多有單機版的程序了。 而網絡編程的本質是兩個設備之間的數據交換,固然,在計算機網絡中,設備主要指計算機。咱們如今進行網絡編程,基本上都是使用已經封裝好的框架,畢竟本身實現維護一套網絡編程框架,費時費力不說,作出來以後還不必定好用,有那時間多保養保養頭髮。 html
記得大學學習java的時候,並無接觸網絡編程相關的基礎知識,一直都是單機版程序,沒有見識到網絡編程的美妙,因此對socket這個東西既熟悉又陌生。熟悉的緣由是在MFC實驗課中接觸到socket知識的,沒錯,就是那門古老的開發語言,如今已經銷聲匿跡了,陌生的緣由大概也就不言而喻了。好了,說了那麼多,那socket究竟是什麼呢?java
Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口 。不少同窗會把tcp、udp協議和socket搞混,其實Socket只是一種鏈接模式,不是協議。tcp、udp是兩個最基本的協議,不少其它協議都是基於這兩個協議。android
用socket能夠建立tcp鏈接,也能夠建立udp鏈接,這意味着,用socket能夠建立任何協議的鏈接。簡單的來講,socket至關於一艘船,你把目的地(ip+端口)告訴它,把要運輸的貨物搬上去,而後它將貨物送往目的地。這個貨物具體須要怎麼運輸,運輸完成以後是否還要通知你已經到達目的地,這就是實現協議的區別了。git
首先來回顧一下socket的基本使用,建立一個服務端所需步驟github
嗯。。。Talk is cheap,Show me the code:apache
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();
}
}
}
複製代碼
建立一個客戶端所需步驟,其實和服務端代碼差很少:編程
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框架是對java的NIO包的一個封裝,簡化了NIO程序開發的難度,封裝了不少底層的細節,讓開發者把精力集中到業務邏輯上來。可能有一些同窗不知道NIO是什麼,這裏簡單介紹一下,NIO就是new IO,是jdk1.4引入的,它是同步非阻塞的,好比一個服務器多個客戶端,客戶端發送的請求都會註冊到服務器的多路複用器上,多路複用器輪詢到鏈接有I/O請求時才啓動一個線程進行處理。它適用於鏈接數目多且鏈接比較短的架構,好比聊天服務器。網絡
Mina 的API 將真正的網絡通訊與咱們的應用程序隔離開來,你只須要關心你要發送、接收的數據以及你的業務邏輯便可。若是想要使用mina,須要先去apache下載mina的jar包:mina下載地址,我使用的是2.0.16版本,下載解壓以後,只須要使用mina-core-2.0.16.jar和slf4j-android.jar這兩個包便可.session
先看看咱們實現的效果圖(因爲是視頻轉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