1、網(wǎng)絡(luò)協(xié)議分層
網(wǎng)絡(luò)層次可劃分為五層因特網(wǎng)協(xié)議棧和七層因特網(wǎng)協(xié)議棧缀棍。
1.1 五層因特網(wǎng)協(xié)議棧
因特網(wǎng)協(xié)議棧共有五層:應(yīng)用層宅此、傳輸層、網(wǎng)絡(luò)層爬范、鏈路層和物理層父腕。不同于OSI七層模型這也是實(shí)際使用中使用的分層方式。
(1)應(yīng)用層
支持網(wǎng)絡(luò)應(yīng)用青瀑,應(yīng)用協(xié)議僅僅是網(wǎng)絡(luò)應(yīng)用的一個組成部分璧亮,運(yùn)行在不同主機(jī)上的進(jìn)程則使用應(yīng)用層協(xié)議進(jìn)行通信痢法。主要的協(xié)議有:http、ftp杜顺、telnet、smtp蘸炸、pop3等躬络。
(2)傳輸層
負(fù)責(zé)為信源和信宿提供應(yīng)用程序進(jìn)程間的數(shù)據(jù)傳輸服務(wù),這一層上主要定義了兩個傳輸協(xié)議搭儒,傳輸控制協(xié)議即TCP和用戶數(shù)據(jù)報(bào)協(xié)議UDP穷当。
(3)網(wǎng)絡(luò)層
負(fù)責(zé)將數(shù)據(jù)報(bào)獨(dú)立地從信源發(fā)送到信宿,主要解決路由選擇淹禾、擁塞控制和網(wǎng)絡(luò)互聯(lián)等問題馁菜。
(4)數(shù)據(jù)鏈路層
負(fù)責(zé)將IP數(shù)據(jù)報(bào)封裝成合適在物理網(wǎng)絡(luò)上傳輸?shù)膸袷讲鬏敚驅(qū)奈锢砭W(wǎng)絡(luò)接收到的幀解封铃岔,取出IP數(shù)據(jù)報(bào)交給網(wǎng)絡(luò)層汪疮。
(5)物理層
負(fù)責(zé)將比特流在結(jié)點(diǎn)間傳輸,即負(fù)責(zé)物理傳輸毁习。該層的協(xié)議既與鏈路有關(guān)也與傳輸介質(zhì)有關(guān)智嚷。
1.2 七層因特網(wǎng)協(xié)議棧
ISO提出的OSI(Open System Interconnection)模型將網(wǎng)絡(luò)分為七層,即物理層( Physical )纺且、數(shù)據(jù)鏈路層(Data Link)盏道、網(wǎng)絡(luò)層(Network)、傳輸層(Transport)载碌、會話層(Session)猜嘱、表示層(Presentation)和應(yīng)用層(Application)。
OSI模型共分七層:從上至下依次是 應(yīng)用層指網(wǎng)絡(luò)操作系統(tǒng)和具體的應(yīng)用程序嫁艇,對應(yīng)WWW服務(wù)器朗伶、FTP服務(wù)器等應(yīng)用軟件 表示層數(shù)據(jù)語法的轉(zhuǎn)換、數(shù)據(jù)的傳送等 會話層 建立起兩端之間的會話關(guān)系步咪,并負(fù)責(zé)數(shù)據(jù)的傳送 傳輸層 負(fù)責(zé)錯誤的檢查與修復(fù)腕让,以確保傳送的質(zhì)量,是TCP工作的地方歧斟。(報(bào)文) 網(wǎng)絡(luò)層 提供了編址方案,IP協(xié)議工作的地方(數(shù)據(jù)包) 數(shù)據(jù)鏈路層將由物理層傳來的未經(jīng)處理的位數(shù)據(jù)包裝成數(shù)據(jù)幀 物理層 對應(yīng)網(wǎng)線纯丸、網(wǎng)卡、接口等物理設(shè)備(位)静袖。
(1)物理層
物理層(Physical layer)是參考模型的最低層觉鼻。該層是網(wǎng)絡(luò)通信的數(shù)據(jù)傳輸介質(zhì),由連接不同結(jié)點(diǎn)的電纜與設(shè)備共同構(gòu)成队橙。主要功能是:利用傳輸介質(zhì)為數(shù)據(jù)鏈路層提供物理連接坠陈,負(fù)責(zé)處理數(shù)據(jù)傳輸并監(jiān)控?cái)?shù)據(jù)出錯率萨惑,以便數(shù)據(jù)流的透明傳輸。
(2)數(shù)據(jù)鏈路層
數(shù)據(jù)鏈路層(Data link layer)是參考模型的第2層仇矾。 主要功能是:在物理層提供的服務(wù)基礎(chǔ)上庸蔼,在通信的實(shí)體間建立數(shù)據(jù)鏈路連接,傳輸以“幀”為單位的數(shù)據(jù)包贮匕,并采用差錯控制與流量控制方法姐仅,使有差錯的物理線路變成無差錯的數(shù)據(jù)鏈路。
(3)網(wǎng)絡(luò)層
網(wǎng)絡(luò)層(Network layer)是參考模型的第3層刻盐。主要功能是:為數(shù)據(jù)在結(jié)點(diǎn)之間傳輸創(chuàng)建邏輯鏈路掏膏,通過路由選擇算法為分組通過通信子網(wǎng)選擇最適當(dāng)?shù)穆窂剑约皩?shí)現(xiàn)擁塞控制敦锌、網(wǎng)絡(luò)互聯(lián)等功能馒疹。
(4)傳輸層
傳輸層(Transport layer)是參考模型的第4層。主要功能是向用戶提供可靠的端到端(End-to-End)服務(wù)乙墙,處理數(shù)據(jù)包錯誤颖变、數(shù)據(jù)包次序,以及其他一些關(guān)鍵傳輸問題听想。傳輸層向高層屏蔽了下層數(shù)據(jù)通信的細(xì)節(jié)悼做,因此,它是計(jì)算機(jī)通信體系結(jié)構(gòu)中關(guān)鍵的一層哗魂。
(5)會話層
會話層(Session layer)是參考模型的第5層肛走。主要功能是:負(fù)責(zé)維護(hù)兩個結(jié)點(diǎn)之間的傳輸鏈接,以便確保點(diǎn)到點(diǎn)傳輸不中斷录别,以及管理數(shù)據(jù)交換等功能朽色。
(6)表示層
表示層(Presentation layer)是參考模型的第6層。主要功能是:用于處理在兩個通信系統(tǒng)中交換信息的表示方式组题,主要包括數(shù)據(jù)格式變換葫男、數(shù)據(jù)加密與解密、數(shù)據(jù)壓縮與恢復(fù)等功能崔列。
(7)應(yīng)用層
應(yīng)用層(Application layer)是參考模型的最高層梢褐。主要功能是:為應(yīng)用軟件提供了很多服務(wù),例如文件服務(wù)器赵讯、數(shù)據(jù)庫服務(wù)盈咳、電子郵件與其他網(wǎng)絡(luò)軟件服務(wù)。
1.3 對應(yīng)關(guān)系圖
2边翼、TCP/IP協(xié)議中的IP地址和端口號
上圖是一個TCP/IP數(shù)據(jù)包頭部的結(jié)構(gòu)概要鱼响。
當(dāng)互聯(lián)網(wǎng)中的一個節(jié)點(diǎn)要想另一個節(jié)點(diǎn)發(fā)送數(shù)據(jù)的時候,它需要兩個信息:
- 數(shù)據(jù)發(fā)送目標(biāo)節(jié)點(diǎn)的IP地址组底,用來標(biāo)識數(shù)據(jù)要送往哪個節(jié)點(diǎn)丈积。這里的IP地址信息筐骇,在上圖的IPv4協(xié)議包頭里面,這里沒有展開江滨。
- 數(shù)據(jù)發(fā)送目標(biāo)節(jié)點(diǎn)的端口號铛纬,用來標(biāo)識這些發(fā)送的數(shù)據(jù)將由目標(biāo)節(jié)點(diǎn)上的什么服務(wù)接收處理。因?yàn)槟繕?biāo)節(jié)點(diǎn)上可能啟著好多服務(wù)唬滑,比如http告唆、ftp、telnet等间雀。每個服務(wù)會監(jiān)聽一個專門的端口號,這樣镊屎,當(dāng)發(fā)送方制定了目的端口號之后惹挟,也就指定了目標(biāo)節(jié)點(diǎn)上接收處理該數(shù)據(jù)包的服務(wù)。從上圖可以看出缝驳,端口號出現(xiàn)在TCP協(xié)議頭部分连锯,分為目標(biāo)端口號和源端口號。
注:這里的目的端口號是8080用狱,看起來有些奇怪运怖,這個應(yīng)該是一個代理服務(wù)器的端口也就是說,數(shù)據(jù)包先發(fā)到代理服務(wù)器的8080端口夏伊,然后再由代理服務(wù)器將數(shù)據(jù)包轉(zhuǎn)發(fā)出去摇展。
2.0 端口號分類
第一類公認(rèn)端口(Well Known Ports):從0到1023,它們緊密綁定(binding)于一些服務(wù)溺忧。通常這些端口的通訊明確表明了某種服務(wù)的協(xié)議咏连,必須要有Root權(quán)限才能綁定。例如:80端口實(shí)際上總是HTTP通訊鲁森。
第二類注冊端口(Registered Ports):從1024到49151祟滴。它們松散地綁定于一些服務(wù)。也就是說有許多服務(wù)綁定于這些端口歌溉,這些端口同樣用于許多其它目的垄懂。例如:許多系統(tǒng)處理動態(tài)端口從1024左右開始。
第三類動態(tài)和/或私有端口(Dynamic, private or ephemeral ports):從49152到65535痛垛。理論上草慧,不應(yīng)為服務(wù)分配這些端口。
實(shí)際上匙头,機(jī)器通常從1024起分配動態(tài)端口冠蒋。但也有例外:SUN的RPC端口從32768開始。
Well-known ports
The port numbers in the range from 0 to 1023 (0 to 2^10 ? 1) are the well-known ports or system ports. They are used by system processes that provide widely used types of network services. On Unix-like operating systems, a process must execute with superuser privileges to be able to bind a network socket to an IP address using one of the well-known ports.
Registered ports
The range of port numbers from 1024 to 49151 (2^10 to 2^14 + 2^15 ? 1) are the registered ports. They are assigned by IANA for specific service upon application by a requesting entity. On most systems, registered ports can be used without superuser privileges.
Dynamic, private or ephemeral ports
The range 49152–65535 (2^15 + 2^14 to 2^16 ? 1) contains dynamic or private ports that cannot be registered with IANA. This range is used for private or customized services, for temporary purposes, and for automatic allocation of ephemeral ports.
From Wikipedia.
2.1 目的端口號
目的端口號用于指定數(shù)據(jù)包傳到目標(biāo)服務(wù)器之后乾胶,會被哪個程序接收處理抖剿。如下面兩個圖朽寞。
如果發(fā)送的目的端口號是80,那么數(shù)據(jù)包會被目標(biāo)服務(wù)器上的http服務(wù)器端處理程序接受處理:
如果發(fā)送的目的端口號是21斩郎,那么數(shù)據(jù)包會被目標(biāo)服務(wù)器上的ftp服務(wù)器端處理程序接受處理:
不難理解脑融,同樣地,如果發(fā)送的目的端口號是23缩宜,那么數(shù)據(jù)包會被目標(biāo)服務(wù)器上的telnet服務(wù)器端處理程序接受處理肘迎。
2.2 源端口號
將數(shù)據(jù)包發(fā)送到目標(biāo)節(jié)點(diǎn)時,提供源端口號是為了讓目標(biāo)節(jié)點(diǎn)上接受處理數(shù)據(jù)包的服務(wù)器程序能夠?qū)⒎祷財(cái)?shù)據(jù)包發(fā)送到客戶端正確的會話上锻煌。
實(shí)際上妓布,當(dāng)服務(wù)器端程序收到數(shù)據(jù)包之后,會將數(shù)據(jù)包的源端口和目的端口反轉(zhuǎn)宋梧,這樣就能夠?qū)?shù)據(jù)發(fā)送到正確的客戶端程序上了匣沼。
As Host A receives the Internet Server's reply, the Transport layer will notice the reversed ports and recognise it as a response to the previous packet it sent (the one with the green arrow).
The Transport and Session layers keep track of all new connections, established connections and connections that are in the process of being torn down, which explains how Host A remembers that it's expecting a reply from the Internet Server.
這個源端口號就是從上面提到的端口分類中的第三類端口中,任意分配的一個端口號捂龄。至于這個端口選擇的范圍释涛,視具體的操作系統(tǒng)而定,有的操作系統(tǒng)上還能夠提供命令來修改這個范圍倦沧。
3唇撬、Java中的Socket編程
套接字(socket)是一個抽象層,應(yīng)用程序可以通過它發(fā)送或接收數(shù)據(jù)展融,可對其進(jìn)行像對文件一樣的打開窖认、讀寫和關(guān)閉等操作。套接字允許應(yīng)用程序?qū)/O插入到網(wǎng)絡(luò)中告希,并與網(wǎng)絡(luò)中的其他應(yīng)用程序進(jìn)行通信耀态。網(wǎng)絡(luò)套接字是IP地址與端口的組合。
傳輸層實(shí)現(xiàn)端到端的通信暂雹,因此首装,每一個傳輸層連接有兩個端點(diǎn)。那么杭跪,傳輸層連接的端點(diǎn)是什么呢仙逻?不是主機(jī),不是主機(jī)的IP地址涧尿,不是應(yīng)用進(jìn)程系奉,也不是傳輸層的協(xié)議端口。傳輸層連接的端點(diǎn)叫做套接字(socket)姑廉。根據(jù)RFC793的定義:端口號拼接到IP地址就構(gòu)成了套接字缺亮。所謂套接字,實(shí)際上是一個通信端點(diǎn)桥言,每個套接字都有一個套接字序號萌踱,包括主機(jī)的IP地址與一個16位的主機(jī)端口號葵礼,即形如(主機(jī)IP地址:端口號
)。例如并鸵,如果IP地址是210.37.145.1鸳粉,而端口號是23,那么得到套接字就是(210.37.145.1:23
)园担。
套接字可以看成是兩個網(wǎng)絡(luò)應(yīng)用程序進(jìn)行通信時届谈,各自通信連接中的一個端點(diǎn)。通信時弯汰,其中的一個網(wǎng)絡(luò)應(yīng)用程序?qū)⒁獋鬏數(shù)囊欢涡畔懭胨谥鳈C(jī)的Socket中艰山,該Socket通過網(wǎng)絡(luò)接口卡的傳輸介質(zhì)將這段信息發(fā)送給另一臺主機(jī)的Socket中,使這段信息能傳送到其他程序中咏闪。因此曙搬,兩個應(yīng)用程序之間的數(shù)據(jù)傳輸要通過套接字來完成。
在網(wǎng)絡(luò)應(yīng)用程序設(shè)計(jì)時汤踏,由于TCP/IP的核心內(nèi)容被封裝在操作系統(tǒng)中织鲸,如果應(yīng)用程序要使用TCP/IP舔腾,可以通過系統(tǒng)提供的TCP/IP的編程接口來實(shí)現(xiàn)溪胶。在Windows環(huán)境下,網(wǎng)絡(luò)應(yīng)用程序編程接口稱作Windows Socket稳诚。為了支持用戶開發(fā)面向應(yīng)用的通信程序哗脖,大部分系統(tǒng)都提供了一組基于TCP或者UDP的應(yīng)用程序編程接口(API),該接口通常以一組函數(shù)的形式出現(xiàn)扳还,也稱為套接字(Socket)才避。
3.1 簡單例子:單次收發(fā)
創(chuàng)建一個Maven模塊simpletest-parent
作為父模塊,位于目錄/Users/chengxia/Developer/Java/simpletest-parent
氨距,pom.xml配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lfqy.socket</groupId>
<artifactId>simpletest-parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>simpletest-server</module>
<module>simpletest-client</module>
</modules>
</project>
創(chuàng)建一個子模塊simpletest-server
作為Socket服務(wù)器端桑逝,位于/Users/chengxia/Developer/Java/simpletest-parent/simpletest-server
,pom.xml配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simpletest-parent</artifactId>
<groupId>com.lfqy.socket</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.lfqy.socket</groupId>
<artifactId>simpletest-server</artifactId>
</project>
創(chuàng)建一個子模塊simpletest-client
作為Socket客戶端俏让,位于/Users/chengxia/Developer/Java/simpletest-parent/simpletest-client
楞遏,pom.xml配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simpletest-parent</artifactId>
<groupId>com.lfqy.socket</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.lfqy.socket</groupId>
<artifactId>simpletest-client</artifactId>
</project>
整個項(xiàng)目的目錄結(jié)構(gòu)如下圖:
相關(guān)的代碼如下。
com.lfqy.socket.client.ClientSocket0Test:
package com.lfqy.socket.client;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
/**
* Created by chengxia on 2019/9/23.
*/
public class ClientSocket0Test {
public static void main(String []args){
try {
Socket socket =new Socket("127.0.0.1",9999);
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String str="Hello, Server! I am Client.";
bufferedWriter.write(str);
//刷新輸入流
bufferedWriter.flush();
//關(guān)閉socket的輸出流
socket.shutdownOutput();
}catch (IOException e) {
e.printStackTrace();
}
}
}
com.lfqy.socket.server.ServerSocket0Test:
package com.lfqy.socket.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by chengxia on 2019/9/23.
*/
public class ServerSocket0Test {
public static void main(String []args){
try {
// 初始化服務(wù)端socket并且綁定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客戶端的連接
Socket socket = serverSocket.accept();
//獲取輸入流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
//讀取一行數(shù)據(jù)
String str = bufferedReader.readLine();
//輸出打印
System.out.println(str);
}catch (IOException e) {
e.printStackTrace();
}
}
}
在啟動時首昔,先運(yùn)行com.lfqy.socket.server.ServerSocket0Test
啟動服務(wù)器端寡喝,運(yùn)行成功之后,控制臺沒有任何輸出勒奇,程序在等客戶端連接预鬓。然后,運(yùn)行com.lfqy.socket.client.ClientSocket0Test
啟動客戶端赊颠,連接并向服務(wù)器端發(fā)送數(shù)據(jù)格二,運(yùn)行成功之后劈彪,服務(wù)器端程序的輸出如下,顯示發(fā)送成功:
Hello, Server! I am Client.
Process finished with exit code 0
3.2 連續(xù)收發(fā)
基于前面的例子蟋定,在和com.lfqy.socket.client.ClientSocket0Test
相同包下粉臊,創(chuàng)建com.lfqy.socket.client.ClientSocket1Test
作為客戶端,如下驶兜。
package com.lfqy.socket.client;
import java.io.*;
import java.net.Socket;
/**
* Created by chengxia on 2019/9/23.
*/
public class ClientSocket1Test {
public static void main(String []args){
try {
Socket socket =new Socket("127.0.0.1",9999);
//通過socket獲取字符流
BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//通過標(biāo)準(zhǔn)輸入流獲取字符流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
while (true){
String str = bufferedReader.readLine();
bufferedWriter.write(str);
bufferedWriter.write("\n");
bufferedWriter.flush();
System.out.println("Sent: " + str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
基于前面的例子扼仲,在和com.lfqy.socket.server.ServerSocket0Test
相同包下,創(chuàng)建com.lfqy.socket.server.ServerSocket1Test
作為客戶端抄淑,如下屠凶。
package com.lfqy.socket.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by chengxia on 2019/9/23.
*/
public class ServerSocket1Test {
public static void main(String []args){
try {
// 初始化服務(wù)端socket并且綁定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客戶端的連接
Socket socket = serverSocket.accept();
//獲取輸入流,并且指定統(tǒng)一的編碼格式
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//通過循環(huán),不斷地讀取客戶端發(fā)過來的數(shù)據(jù)肆资,并輸出到控制臺
String str = null;
while ((str = bufferedReader.readLine()) != null){
//輸出打印
System.out.println("Received: " + str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
運(yùn)行服務(wù)器端的程序之后矗愧,運(yùn)行客戶端,客戶端控制臺輸入如下:
Hi, Server!
Sent: Hi, Server!
I am a Client.
Sent: I am a Client.
對應(yīng)服務(wù)器端的控制臺輸出如下:
Received: Hi, Server!
Received: I am a Client.
3.3 同一個客戶端處理多個客戶端的連接請求_多線程實(shí)現(xiàn)
前面的例子中郑原,一個服務(wù)器程序?qū)?yīng)一個客戶端唉韭,同時,只能有一個客戶端連接犯犁。下面的例子中属愤,我們引入多線程,讓一個服務(wù)器端可以接受多個客戶端的連接酸役。
原理上比較簡單住诸,就是服務(wù)器端程序一直在等待連接,每當(dāng)有客戶端連接時涣澡,就新建一個線程處理該客戶端連接贱呐。然后,繼續(xù)等待下一個客戶端程序連接入桂。代碼如下奄薇。
package com.lfqy.socket.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by chengxia on 2019/9/23.
*/
public class ServerSocket11Test {
//由于在內(nèi)部類中用到了這個變量,所以這個變量不能是局部變量抗愁,否則編譯異常
private static Socket socket;
public static void main(String []args){
try {
// 初始化服務(wù)端socket并且綁定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
while(true){
//等待客戶端的連接
socket = serverSocket.accept();
//每當(dāng)有一個客戶端連接進(jìn)來后馁蒂,就啟動一個單獨(dú)的線程進(jìn)行處理
new Thread(new Runnable() {
public void run() {
//獲取輸入流,并且指定統(tǒng)一的編碼格式
BufferedReader bufferedReader =null;
try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//讀取一行數(shù)據(jù)
String str;
//通過while循環(huán)不斷讀取信息,
while ((str = bufferedReader.readLine())!=null){
//輸出打印
System.out.println("Received: "+str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
這樣驹愚,運(yùn)行com.lfqy.socket.server.ServerSocket11Test
啟動服務(wù)器程序之后远搪,分別運(yùn)行兩次客戶端連接程序,并在控制臺做如下輸入逢捺。
第一次運(yùn)行com.lfqy.socket.client.ClientSocket1Test
:
Hi, server! I am client 1.
Sent: Hi, server! I am client 1.
第二次運(yùn)行com.lfqy.socket.client.ClientSocket1Test
:
Hi, server! I am client 2.
Sent: Hi, server! I am client 2.
這時候谁鳍,查看服務(wù)器程序的控制臺輸出,發(fā)現(xiàn)兩個客戶端發(fā)送的數(shù)據(jù)都已經(jīng)收到了。如下倘潜。
Received: Hi, server! I am client 1.
Received: Hi, server! I am client 2.
3.4 線程池實(shí)現(xiàn)服務(wù)器端同時處理多個客戶端的連接
上面的例子中绷柒,每一個連到服務(wù)器端的客戶端獨(dú)占一個線程。如果有大量的客戶端連接到服務(wù)器端涮因,就會有大量的線程被新建出來废睦,而很多線程可能只進(jìn)行特別少的通信就被閑置了,這樣會導(dǎo)致非常大的性能開銷养泡,Java的內(nèi)存回收機(jī)制可能不能及時回收這些資源嗜湃,導(dǎo)致性能浪費(fèi)。
為此澜掩,我們可以使用線程池技術(shù)购披,來復(fù)用線程。在下面的例子中肩榕,我們創(chuàng)建了一個大小為100的線程池刚陡,線程的創(chuàng)建和回收由線程池管理,這樣實(shí)現(xiàn)了線程的復(fù)用株汉。代碼如下筐乳。
com.lfqy.socket.server.ServerSocket12Test
:
package com.lfqy.socket.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by chengxia on 2019/9/23.
*/
public class ServerSocket12Test {
//由于在內(nèi)部類中用到了這個變量,所以這個變量不能是局部變量乔妈,否則編譯異常
private static Socket socket;
public static void main(String []args){
try {
// 初始化服務(wù)端socket并且綁定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//創(chuàng)建一個線程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
while(true){
//等待客戶端的連接
socket = serverSocket.accept();
//每當(dāng)有一個客戶端連接進(jìn)來后蝙云,就啟動一個單獨(dú)的線程進(jìn)行處理
Runnable r = new Runnable() {
public void run() {
//獲取輸入流,并且指定統(tǒng)一的編碼格式
BufferedReader bufferedReader =null;
try {
bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
//讀取一行數(shù)據(jù)
String str;
//通過while循環(huán)不斷讀取信息,
while ((str = bufferedReader.readLine())!=null){
//輸出打印
System.out.println("Received: "+str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
};
executorService.submit(r);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
這樣褒翰,運(yùn)行com.lfqy.socket.server.ServerSocket12Test
啟動服務(wù)器端程序贮懈。第一次啟動com.lfqy.socket.client.ClientSocket1Test
匀泊,在控制臺輸入一行發(fā)送數(shù)據(jù)优训,之后,再運(yùn)行com.lfqy.socket.client.ClientSocket1Test
啟動另一個客戶端程序各聘,在控制臺輸入一行數(shù)據(jù)之后揣非,關(guān)掉第一個客戶端程序,再在第二個客戶端程序控制臺輸入另外一行數(shù)據(jù)躲因。最后三個控制臺輸出如下早敬。
客戶端控制臺1:
Hi, Server! This is Client 1.
Sent: Hi, Server! This is Client 1.
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
客戶端控制臺2:
Hi, Server! This is Client 2.
Sent: Hi, Server! This is Client 2.
Hi, Client 1 is over. I am Client 2.
Sent: Hi, Client 1 is over. I am Client 2.
服務(wù)器端控制臺:
Received: Hi, Server! This is Client 1.
Received: Hi, Server! This is Client 2.
Received: Hi, Client 1 is over. I am Client 2.
3.5 Socket發(fā)送指定長度的信息
前面的例子中,Socket客戶端和服務(wù)器端的通信都是以數(shù)據(jù)行為單位的大脉,每次發(fā)送和接收都是一行數(shù)據(jù)搞监,發(fā)送的數(shù)據(jù)行之間必須有行分隔符。在實(shí)際的Socket編程應(yīng)用中镰矿,這樣非常不方便琐驴,而且有場景限制。通常,我們會指定發(fā)送數(shù)據(jù)的長度绝淡,并將數(shù)據(jù)類型寫入到數(shù)據(jù)的頭部信息中宙刘,這樣再讀取時,就可以更加靈活牢酵。代碼如下悬包。
com.lfqy.socket.server.ServerSocket2Test
:
package com.lfqy.socket.server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by chengxia on 2019/9/23.
*/
public class ServerSocket2Test {
public static void main(String []args){
try {
// 初始化服務(wù)端socket并且綁定9999端口
ServerSocket serverSocket = new ServerSocket(9999);
//等待客戶端的連接
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
DataInputStream dataInputStream =new DataInputStream(inputStream);
while (true){
//讀取數(shù)據(jù)類型
byte b = dataInputStream.readByte();
//讀取長度
int len = dataInputStream.readInt();
//因?yàn)榍拔鍌€字節(jié)是包頭,所以在讀取內(nèi)容的時候馍乙,應(yīng)該去掉前五個字節(jié)
byte[] data =new byte[len -5];
dataInputStream.readFully(data);
String str =new String(data);
System.out.println("Received type:"+b);
System.out.println("Received length:"+len);
System.out.println("Received content:"+str);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
com.lfqy.socket.client.ClientSocket2Test
:
package com.lfqy.socket.client;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* Created by chengxia on 2019/9/23.
*/
public class ClientSocket2Test {
public static void main(String []args){
try {
Socket socket =new Socket("127.0.0.1",9999);
//獲得向socket寫入數(shù)據(jù)的輸出流
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dataOutputStream =new DataOutputStream(outputStream);
//從控制臺接受輸入布近,發(fā)送到服務(wù)器端
//通過標(biāo)準(zhǔn)輸入流獲取字符流
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
while (true){
String str = bufferedReader.readLine();
//在向服務(wù)器發(fā)送之前,先加一個包頭丝格,包含:類型+長度吊输,共5個字節(jié)加匈。
byte type =1;
byte[] data = str.getBytes();
int len = data.length +5;
dataOutputStream.writeByte(type);
dataOutputStream.writeInt(len);
dataOutputStream.write(data);
dataOutputStream.flush();
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
在這個例子中裁奇,客戶端在向服務(wù)器端發(fā)送數(shù)據(jù)的時候,向數(shù)據(jù)包中寫入了一個長度為5的頭部:第一個字節(jié)標(biāo)識數(shù)據(jù)類型乒躺,后面是一個四個字節(jié)的int數(shù)據(jù)標(biāo)識數(shù)據(jù)包的長度琅束。在服務(wù)器端讀取的時候扭屁,只需要先讀一個字節(jié),拿到數(shù)據(jù)類型涩禀,然后料滥,再讀一個int拿到數(shù)據(jù)包的長度len,這樣艾船,后面數(shù)據(jù)正文部分的長度應(yīng)該是len-5
葵腹,直接將這部分全部督讀到一個長度為len-5
的byte數(shù)組中即可。
運(yùn)行com.lfqy.socket.server.ServerSocket2Test
啟動服務(wù)器端程序屿岂,運(yùn)行com.lfqy.socket.client.ClientSocket2Test
啟動客戶端程序践宴,在客戶端程序的控制臺輸入如下:
Hello!
這時候,服務(wù)器端的程序輸出如下:
Received type:1
Received length:11
Received content:Hello!
上面輸出的長度11實(shí)際上就是Hello!
的長度6爷怀,加上頭部信息長度5的和阻肩。
Structure
上面的所有例子都做完之后,項(xiàng)目的結(jié)構(gòu)如下:
4运授、Socket長連接和短連接
長連接指在一個連接上可以連續(xù)發(fā)送多個數(shù)據(jù)包烤惊,在連接保持期間,如果沒有數(shù)據(jù)包發(fā)送吁朦,需要雙方發(fā)鏈路檢測包柒室。整個通訊過程,客戶端和服務(wù)端只用一個Socket對象逗宜,長期保持Socket的連接雄右。
短連接是每次請求都建立鏈接剥啤,交互完之后關(guān)閉鏈接。
長連接多用于操作頻繁不脯,點(diǎn)對點(diǎn)的通訊府怯,而且連接數(shù)不能太多情況。每個TCP連接都需要三步握手防楷,這需要時間牺丙,如果每個操作都是短連接,再操作的話那么處理速度會降低很多复局,所以每個操作完后都不斷開冲簿,下次處理時直接發(fā)送數(shù)據(jù)包就OK了,不用建立TCP連接亿昏。例如:數(shù)據(jù)庫的連接用長連接峦剔,如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創(chuàng)建也是對資源的浪費(fèi)角钩。
而像WEB網(wǎng)站的http服務(wù)一般都用短鏈接吝沫,因?yàn)殚L連接對于服務(wù)端來說會耗費(fèi)一定的資源,而像WEB網(wǎng)站這么頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源递礼,如果用長連接惨险,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話脊髓,那可想而知吧辫愉。所以并發(fā)量大,但每個用戶無需頻繁操作情況下需用短連好将硝。
前面的例子都是短連接恭朗,每次連接完畢后,都是自動斷開依疼,如果需要重新連接痰腮,則需要建立新的連接對象。在實(shí)際應(yīng)用中涛贯,長連接他并不是真正意義上的長連接诽嘉,(他不像我們打電話一樣蔚出,電話通了之后一直不掛的這種連接)弟翘。他們是通過一種稱之為心跳包或者叫做鏈路檢測包,去定時檢查socket是否關(guān)閉骄酗,輸入輸出流是否關(guān)閉稀余。
socket是通過流的方式通信的,既然關(guān)閉流趋翻,就是關(guān)閉socket睛琳,那么長連接是不是我們讀取流中的信息后,不關(guān)閉流,等下次使用時师骗,直接往流中扔數(shù)據(jù)就历等?
不是這樣的,socket是針對應(yīng)用層與TCP/ip數(shù)據(jù)傳輸協(xié)議封裝的一套方案辟癌,那么他的底層也是通過Tcp/Tcp/ip或則UDP通信的寒屯,所以說socket本身并不是一直通信協(xié)議,而是一套接口的封裝黍少。而tcp/IP協(xié)議組里面的應(yīng)用層包括FTP寡夹、HTTP、TELNET厂置、SMTP菩掏、DNS等協(xié)議,我們知道昵济,http1.0是短連接智绸,http1.1是長連接,我們在打開http通信協(xié)議里面在Response headers中可以看到這么一句Connection:keep-alive访忿,用來表示表示長連接传于。但是長連接并非一直保持的連接,它在制定的時間內(nèi)讓客戶端和服務(wù)端進(jìn)行一個請求來保持該連接醉顽,請求可以是服務(wù)端發(fā)起沼溜,也可以是客戶端發(fā)起。通常在客戶端不定時的發(fā)送一個字節(jié)數(shù)據(jù)給服務(wù)端游添,稱之為心跳包系草。