網(wǎng)絡(luò)編程
TCP流套接字編程
TCP提供的API主要是兩個(gè)類:ServerSocket 和 Socket .
TCP不需要一個(gè)類來表示"TCP數(shù)據(jù)報(bào)"因?yàn)門CP不是以數(shù)據(jù)報(bào)為單位進(jìn)行傳輸?shù)?是以字節(jié)的方式,流式傳輸
ServerSocket API
ServerSocket 是專門給服務(wù)器使用的Socket對象.
ServerSocket 構(gòu)造方法:
ServerSocket(int port)
創(chuàng)建一個(gè)服務(wù)端流套接字Socket,并綁定到指定端口.
ServerSocket 方法:
Socketaccept()
開始監(jiān)聽指定端口(創(chuàng)建時(shí)綁定的端口),有客戶端連接后,返回一個(gè)服務(wù)端Socket對象,并基于Socket建立與客戶端的連接,否則阻塞等待.
voidclose()
關(guān)閉此套接字
Socket API
Socket是既會給客戶端使用,也會給服務(wù)器使用.
Socket 構(gòu)造方法:在服務(wù)器這邊是有accept返回的.在客戶端這邊,在代碼里構(gòu)造的時(shí)候制定一個(gè)IP和端口號.(此處的IP和端口是服務(wù)器IP和端口)有了這個(gè)信息就能和服務(wù)器進(jìn)行連接了.
Socket(String host, intport)
創(chuàng)建一個(gè)客戶端流套接字Socket,并與對應(yīng)IP的主機(jī)上,對應(yīng)端口的進(jìn)程建立連接.
Socket 方法:
進(jìn)一步通過Socket對象獲取內(nèi)部的流對象,借助流對象來進(jìn)行發(fā)送/接收.
InetAddress getInetAddress()
返回套接字所連接的地址InputStream getInputStream()
返回此套接字的輸入流OutputStream getOutputStream()
返回此套接字的輸出流
TCP中的長短連接
在TCP有連接的場景下,針對連接這個(gè)概念,有兩種典型的表現(xiàn)形式.
-
短連接:客戶端每次給服務(wù)器發(fā)消息,先建立連接,發(fā)送請求;下次再發(fā)送,則重新建立連接.
-
長連接:客戶端建立連接后,連接先不斷開,然后再次發(fā)送請求,讀取響應(yīng);再發(fā)送請求,讀取響應(yīng);若干輪之后,客戶端確實(shí)短時(shí)間之內(nèi)不再需要使用這個(gè)連接了,此時(shí)再斷開.
兩者區(qū)別:
- 建立連接、關(guān)閉連接的耗時(shí):短連接每次請求、響應(yīng)都需要建立連接,關(guān)閉連接;而長連接只需要第一次建立連接,之后的請求、響應(yīng)都可以直接傳輸。相對來說建立連接,關(guān)閉連接也是要耗時(shí)的,長連接效率更高。
- 主動(dòng)發(fā)送請求不同:短連接一般是客戶端主動(dòng)向服務(wù)端發(fā)送請求;而長連接可以是客戶端主動(dòng)發(fā)送請求,也可以是服務(wù)端主動(dòng)發(fā)。
- 兩者的使用場景有不同:短連接適用于客戶端請求頻率不高的場景,如瀏覽網(wǎng)頁等。長連接適用于客戶端與服務(wù)端通信頻繁的場景,如聊天室,實(shí)時(shí)游戲等。
拓展了解:
手寫TCP版本的回顯服務(wù)器
TCP服務(wù)端
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("啟動(dòng)服務(wù)器!");
while (true) {
//使用這個(gè)clientSocket和具體的客戶端進(jìn)行交流
Socket clinentSocket = serverSocket.accept();
processConnection(clinentSocket);
}
}
//使用這個(gè)方法處理一個(gè)連接 一個(gè)連接對應(yīng)一個(gè)客戶端
//可能涉及到多次交互
private void processConnection(Socket clinentSocket) {
//獲得客戶端IP和端口
System.out.printf("[%s:%d] 客戶端上線!\n",clinentSocket.getInetAddress().toString(),clinentSocket.getPort());
// 基于上述socket對象和客戶端進(jìn)行通信
try(InputStream inputStream = clinentSocket.getInputStream();
OutputStream outputStream = clinentSocket.getOutputStream()){
//由于要處理多個(gè)請求和響應(yīng),也是使用循環(huán)
while (true){
//1. 讀取請求
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//沒有下一個(gè)數(shù)據(jù) 說明讀完了 (客戶端關(guān)閉了連接)
System.out.printf("[%s:%d] 客戶端下線!\n",clinentSocket.getInetAddress().toString(),clinentSocket.getPort());
break;
}
//此處使用next是一直讀取到換行符/空格/其他空白符結(jié)束
//但是結(jié)果不包含上述空白符
String request = scanner.next();
//2. 根據(jù)請求響應(yīng)
String response = process(request);
//3. 返回響應(yīng)結(jié)果
//outputStream沒有write String這樣的功能 可以把String里的字節(jié)數(shù)組拿出來 進(jìn)行寫入
//也可以用字符流來轉(zhuǎn)換一下
PrintWriter printWriter = new PrintWriter(outputStream);
//此處使用println來寫入 讓結(jié)果中帶有一個(gè)\n 換行 方便對端來接收解析
printWriter.println(response);
//flush用來刷新緩沖區(qū) 保證當(dāng)前寫入的數(shù)據(jù) 確實(shí)是發(fā)送出去了
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s \n",clinentSocket.getInetAddress().toString(),clinentSocket.getPort(),
request,response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
clinentSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
TCP客戶端
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//Socket 構(gòu)造方法能夠識別 電分十進(jìn)制格式的IP地址 比DatagramPacket 更方便
//new 這個(gè)對象的同時(shí),就會進(jìn)行 TCP 連接操作
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客戶端啟動(dòng)!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while (true){
//1. 先從鍵盤上讀取用戶輸入的內(nèi)容
System.out.println("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("goodbye");
break;
}
//2. 把讀到的內(nèi)容構(gòu)造成請求 發(fā)送給服務(wù)器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
//加flush保證數(shù)據(jù)確實(shí)發(fā)送出去了
printWriter.flush();
//3. 讀取服務(wù)器響應(yīng)
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4. 把響應(yīng)顯示到界面上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
問題:
當(dāng)前代碼里使用的是println來發(fā)送數(shù)據(jù),println會在發(fā)送的數(shù)據(jù)后面自動(dòng)帶上\n換行.如果不適用println,而是使用print(不帶\n換行) 上面的代碼是否能正確運(yùn)行?
不能正確運(yùn)行,沒有\(zhòng)n是不行的.TCP協(xié)議是面向字節(jié)流的協(xié)議(字節(jié)流特性:一次讀多少個(gè)字節(jié)都行).接收方無法知道我們一次要多多少字節(jié),這就需要我們在數(shù)據(jù)傳輸中進(jìn)行明確的約定.此處代碼中,隱式約定了使用\n來作為當(dāng)前代碼的請求/響應(yīng)分割約定.
但是我們發(fā)現(xiàn)上面的代碼還是存在一些問題的:
為解決上面的問題,我們使用多線程,主線程負(fù)責(zé)進(jìn)行accept.每次接收到一個(gè)連接,創(chuàng)建新線程 ,由這個(gè)新的線程負(fù)責(zé)處理這個(gè)新的客戶端.每個(gè)線程是獨(dú)立的執(zhí)行流.每個(gè)獨(dú)立的執(zhí)行流是各自執(zhí)行各自的邏輯,彼此之間是并發(fā)關(guān)系.不會出現(xiàn)一邊阻塞而影響到另一邊執(zhí)行的情況.
如果服務(wù)器,客戶端特別多,很多客戶端頻繁建立連接就需要頻繁創(chuàng)建/銷毀線程了, 此時(shí)單純的多線程的處理方法也不行了,所以我們就得用線程池來進(jìn)行處理.
如果客戶端非常多而且客戶端連接都遲遲不斷開,就會導(dǎo)致機(jī)器上有很多線程.如果一個(gè)服務(wù)器有幾千個(gè)客戶端就得是幾千個(gè)線程.這個(gè)事情對機(jī)器來說,是一個(gè)很大的負(fù)擔(dān).這個(gè)時(shí)候?yàn)榱私鉀Q單機(jī)支持更大量客戶端的問題即C10M問題.就想辦法讓一個(gè)線程,處理多個(gè)客戶端連接.為了解決這個(gè)問題操作系統(tǒng)底層提出了IO多路復(fù)用,IO多路轉(zhuǎn)接.就是利用充分等待時(shí)間,做別的事情.我們給這個(gè)線程安排個(gè)集合,這個(gè)集合就放了一堆連接.這個(gè)線程就負(fù)責(zé)監(jiān)聽這個(gè)集合,哪個(gè)連接有數(shù)據(jù)來了,線程就來處理哪個(gè)連接,雖然連接有很多,總還是有先有后的.操作系統(tǒng)提供了一些原生API ,select,poll,epoll.在Java中,提供了一組NIO這樣類,就封裝了上述多路復(fù)用的API.
改進(jìn)后:TCP服務(wù)器代碼文章來源:http://www.zghlxwxcb.cn/news/detail-653782.html
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("啟動(dòng)服務(wù)器!");
//此處使用CachedThreadPool ,使用FixedThreadPool不太合適(線程數(shù)不太應(yīng)該是固定的 )
ExecutorService threadPool = Executors.newCachedThreadPool();
while (true) {
//使用這個(gè)clientSocket和具體的客戶端進(jìn)行交流
Socket clinentSocket = serverSocket.accept();
//此處使用多線程處理
/*Thread t = new Thread(()->{
processConnection(clinentSocket);
});*/
//使用線程池
threadPool.submit(()->{
processConnection(clinentSocket);
});
}
}
//使用這個(gè)方法處理一個(gè)連接 一個(gè)連接對應(yīng)一個(gè)客戶端
//可能涉及到多次交互
private void processConnection(Socket clinentSocket) {
//獲得客戶端IP和端口
System.out.printf("[%s:%d] 客戶端上線!\n",clinentSocket.getInetAddress().toString(),clinentSocket.getPort());
// 基于上述socket對象和客戶端進(jìn)行通信
try(InputStream inputStream = clinentSocket.getInputStream();
OutputStream outputStream = clinentSocket.getOutputStream()){
//由于要處理多個(gè)請求和響應(yīng),也是使用循環(huán)
while (true){
//1. 讀取請求
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//沒有下一個(gè)數(shù)據(jù) 說明讀完了 (客戶端關(guān)閉了連接)
System.out.printf("[%s:%d] 客戶端下線!\n",clinentSocket.getInetAddress().toString(),clinentSocket.getPort());
break;
}
//此處使用next是一直讀取到換行符/空格/其他空白符結(jié)束
//但是結(jié)果不包含上述空白符
String request = scanner.next();
//2. 根據(jù)請求響應(yīng)
String response = process(request);
//3. 返回響應(yīng)結(jié)果
//outputStream沒有write String這樣的功能 可以把String里的字節(jié)數(shù)組拿出來 進(jìn)行寫入
//也可以用字符流來轉(zhuǎn)換一下
PrintWriter printWriter = new PrintWriter(outputStream);
//此處使用println來寫入 讓結(jié)果中帶有一個(gè)\n 換行 方便對端來接收解析
printWriter.println(response);
//flush用來刷新緩沖區(qū) 保證當(dāng)前寫入的數(shù)據(jù) 確實(shí)是發(fā)送出去了
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s \n",clinentSocket.getInetAddress().toString(),clinentSocket.getPort(),
request,response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
clinentSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
文章來源地址http://www.zghlxwxcb.cn/news/detail-653782.html
到了這里,關(guān)于【網(wǎng)絡(luò)編程】(TCP流套接字編程 ServerSocket API Socket API 手寫TCP版本的回顯服務(wù)器 TCP中的長短連接)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!