相信能夠認知到socket層面的大家,至少對網(wǎng)絡的分層有一定的了解。這里便簡單的呈現(xiàn)一張經(jīng)典的網(wǎng)絡協(xié)議的層級圖方便對應理解。
在Java(Kotlin)/Android中,Socket是對TCP/UDP層協(xié)議的封裝接口,負責完成三次握手的連接建立,我們使用socket可以輕易的完成在C/S模式(即客戶端/服務器端)下數(shù)據(jù)的簡單傳輸。下面我們首先分析一下服務器端Socket的:1.監(jiān)聽端口;2.獲取客戶端發(fā)來的輸入流;3.向客戶端發(fā)送服務器端的輸出流。
object Main {
@JvmStatic fun main(args: Array<String>) {
val server = SfServer()//初始化Server,開始監(jiān)聽
server.startListen()
}
}
import java.net.ServerSocket
class SfServer {
var serverSocket: ServerSocket? = null//在服務端要使用ServerSocket而不是Socket
fun startListen() {
try {
serverSocket = ServerSocket(9000)//實例化ServerSocket的過程是一個阻塞的過程,只有綁定成功或者失敗才會繼續(xù)執(zhí)行
System.out.println("監(jiān)聽端口9000成功")
while (true) {
val socket = serverSocket!!.accept()//accept是監(jiān)聽端口,同樣是阻塞的,只有監(jiān)聽到來訪者才會繼續(xù)執(zhí)行
val dealThread = Thread(DealClientSocket(socket))//當執(zhí)行到這一行說明accept已監(jiān)聽到來訪者,這時獲取來訪者的socket,單開一個線程對其進行處理,主線程繼續(xù)監(jiān)聽阻塞。
dealThread.start()
}
} catch(e: Exception) {
e.printStackTrace()
System.out.println("綁定失敗")
}
}
}
以上就是server端的第一步:監(jiān)聽端口。接下來看在處理這一個客戶端的訪問時,如何獲取輸入流和輸出流。
import java.io.*
import java.net.Socket
import java.nio.charset.Charset
import java.util.*
class DealClientSocket(val clientSocket: Socket) : Runnable {
//將建立握手的socket傳入構(gòu)造函數(shù),交給這個類處理。
@Throws(UnsupportedOperationException::class)
override fun run() {
val input = socket.inputStream
val str = readLine(input)
System.out.println(str)
val pw = PrintWriter(socket.outputStream)
pw.println("來自server的問候")
pw.flush()
input.close()
pw.close()
socket.close()
}
@Throws(IOException::class)
private fun readLine(`is`: InputStream): String {
val lineByteList = ArrayList<Byte>()
var readByte: Byte
do {
readByte = `is`.read().toByte()
lineByteList.add(java.lang.Byte.valueOf(readByte))
} while (readByte.toInt() != 10)
val byteArr = lineByteList.toByteArray()
return String(byteArr, Charset.defaultCharset())
}
}
1.這里獲取socket中的InputStream,將數(shù)據(jù)一行一行取出。
2.使用PrintWriter包裝OutputStream,將要回復的數(shù)據(jù)發(fā)出。
3.關閉輸入輸出流和socket連接。
客戶端的代碼與server端代碼也只有一丁點的不同:
import android.util.Log
import java.io.IOException
import java.io.InputStream
import java.io.PrintWriter
import java.net.Socket
import java.nio.charset.Charset
class SimpleSocket : Runnable {
val HOST = "10.59.47.206"
val PORT = 9000
override fun run() {
var socket = Socket(HOST, PORT)//不同點:需要指定ip地址和端口號
val pw = PrintWriter(socket.getOutputStream())
val input = socket.getInputStream()
pw.println("from app")
pw.flush()
val back = readLine(input)
Log.e("sfhttp", "Server:$back")
input.close()
pw.close()
socket.close()
}
@Throws(IOException::class)
private fun readLine(`is`: InputStream): String {
val lineByteList = ArrayList<Byte>()
var readByte: Byte
do {
readByte = `is`.read().toByte()
lineByteList.add(java.lang.Byte.valueOf(readByte))
} while (readByte.toInt() != 10)
val byteArr = lineByteList.toByteArray()
return String(byteArr, Charset.defaultCharset())
}
}
值得注意的是,上述兩端有一個隱形的“坑”:當獲取輸入流的時候,沒有使用BufferedReader(InputStreamReader(socket.getInputStream))進行封裝,而是直接操作的inputStream。
雖然java提供了上述封裝,但它有個特點,一旦不滿足某些條件,就一定會阻塞住,有時候雖然數(shù)據(jù)都打印出來了,竟然還莫名其妙的阻塞著。這就非常的蛋疼了。于是我們干脆自定義readLine方法,讀取一行。這一行的結(jié)束標志就是byte.toInt=10(換行符\n).因此我們在PrintWriter調(diào)用的時候都是用pringln()方法自帶換行符。
這個特點將在上層應用和協(xié)議——HTTP協(xié)議中得到體現(xiàn)。因此不能小看,需要牢記。
總結(jié):
- Socket是TCP/UDP協(xié)議層的封裝接口
- Socket、ServerSocket的使用,互相發(fā)送和讀取信息
- Socket 需要注意換行符的使用
- HTTP的實現(xiàn)是基于Socket.