# 15.2 套接字
“套接字”或者“插座”(`Socket`)也是一種軟件形式的抽象,用于表達兩臺機器間一個連接的“終端”。針對一個特定的連接,每臺機器上都有一個“套接字”,可以想象它們之間有一條虛擬的“線纜”。線纜的每一端都插入一個“套接字”或者“插座”里。當然,機器之間的物理性硬件以及電纜連接都是完全未知的。抽象的基本宗旨是讓我們盡可能不必知道那些細節。
在Java中,我們創建一個套接字,用它建立與其他機器的連接。從套接字得到的結果是一個`InputStream`以及`OutputStream`(若使用恰當的轉換器,則分別是`Reader`和`Writer`),以便將連接作為一個IO流對象對待。有兩個基于數據流的套接字類:`ServerSocket`,服務器用它“監聽”進入的連接;以及`Socket`,客戶用它初始一次連接。一旦客戶(程序)申請建立一個套接字連接,`ServerSocket`就會返回(通過`accept()`方法)一個對應的服務器端套接字,以便進行直接通信。從此時起,我們就得到了真正的“套接字-套接字”連接,可以用同樣的方式對待連接的兩端,因為它們本來就是相同的!此時可以利用`getInputStream()`以及`getOutputStream()`從每個套接字產生對應的`InputStream`和`OutputStream`對象。這些數據流必須封裝到緩沖區內。可按第10章介紹的方法對類進行格式化,就象對待其他任何流對象那樣。
對于Java庫的命名機制,`ServerSocket`(服務器套接字)的使用無疑是容易產生混淆的又一個例證。大家可能認為`ServerSocket`最好叫作`ServerConnector`(服務器連接器),或者其他什么名字,只是不要在其中安插一個`Socket`。也可能以為`ServerSocket`和`Socket`都應從一些通用的基類繼承。事實上,這兩種類確實包含了幾個通用的方法,但還不夠資格把它們賦給一個通用的基類。相反,`ServerSocket`的主要任務是在那里耐心地等候其他機器同它連接,再返回一個實際的`Socket`。這正是`ServerSocket`這個命名不恰當的地方,因為它的目標不是真的成為一個`Socket`,而是在其他人同它連接的時候產生一個`Socket`對象。
然而,`ServerSocket`確實會在主機上創建一個物理性的“服務器”或者監聽用的套接字。這個套接字會監聽進入的連接,然后利用`accept()`方法返回一個“已建立”套接字(本地和遠程端點均已定義)。容易混淆的地方是這兩個套接字(監聽和已建立)都與相同的服務器套接字關聯在一起。監聽套接字只能接收新的連接請求,不能接收實際的數據包。所以盡管`ServerSocket`對于編程并無太大的意義,但它確實是“物理性”的。
創建一個`ServerSocket`時,只需為其賦予一個端口編號。不必把一個IP地址分配它,因為它已經在自己代表的那臺機器上了。但在創建一個`Socket`時,卻必須同時賦予IP地址以及要連接的端口編號(另一方面,從`ServerSocket.accept()`返回的`Socket`已經包含了所有這些信息)。
## 15.2.1 一個簡單的服務器和客戶端程序
這個例子將以最簡單的方式運用套接字對服務器和客戶端進行操作。服務器的全部工作就是等候建立一個連接,然后用那個連接產生的`Socket`創建一個`InputStream`以及一個`OutputStream`。在這之后,它從`InputStream`讀入的所有東西都會反饋給`OutputStream`,直到接收到行中止(END)為止,最后關閉連接。
客戶端連接與服務器的連接,然后創建一個`OutputStream`。文本行通過`OutputStream`發送。客戶端也會創建一個`InputStream`,用它收聽服務器說些什么(本例只不過是反饋回來的同樣的字句)。
服務器與客戶端(程序)都使用同樣的端口號,而且客戶端利用本地主機地址連接位于同一臺機器中的服務器(程序),所以不必在一個物理性的網絡里完成測試(在某些配置環境中,可能需要同真正的網絡建立連接,否則程序不能工作——盡管實際并不通過那個網絡通信)。
下面是服務器程序:
```
//: JabberServer.java
// Very simple server that just
// echoes whatever the client sends.
import java.io.*;
import java.net.*;
public class JabberServer {
// Choose a port outside of the range 1-1024:
public static final int PORT = 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Started: " + s);
try {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
System.out.println(
"Connection accepted: "+ socket);
BufferedReader in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by PrintWriter:
PrintWriter out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())),true);
while (true) {
String str = in.readLine();
if (str.equals("END")) break;
System.out.println("Echoing: " + str);
out.println(str);
}
// Always close the two sockets...
} finally {
System.out.println("closing...");
socket.close();
}
} finally {
s.close();
}
}
} ///:~
```
可以看到,`ServerSocket`需要的只是一個端口編號,不需要IP地址(因為它就在這臺機器上運行)。調用`accept()`時,方法會暫時陷入停頓狀態(堵塞),直到某個客戶嘗試同它建立連接。換言之,盡管它在那里等候連接,但其他進程仍能正常運行(參考第14章)。建好一個連接以后,`accept()`就會返回一個`Socket`對象,它是那個連接的代表。
清除套接字的責任在這里得到了很藝術的處理。假如`ServerSocket`構造器失敗,則程序簡單地退出(注意必須保證`ServerSocket`的構造器在失敗之后不會留下任何打開的網絡套接字)。針對這種情況,`main()`會“拋”出一個`IOException`異常,所以不必使用一個`try`塊。若`ServerSocket`構造器成功執行,則其他所有方法調用都必須到一個`try-finally`代碼塊里尋求保護,以確保無論塊以什么方式留下,`ServerSocket`都能正確地關閉。
同樣的道理也適用于由`accept()`返回的`Socket`。若`accept()`失敗,那么我們必須保證`Socket`不再存在或者含有任何資源,以便不必清除它們。但假若執行成功,則后續的語句必須進入一個`try-finally`塊內,以保障在它們失敗的情況下,`Socket`仍能得到正確的清除。由于套接字使用了重要的非內存資源,所以在這里必須特別謹慎,必須自己動手將它們清除(Java中沒有提供“析構器”來幫助我們做這件事情)。
無論`ServerSocket`還是由`accept()`產生的`Socket`都打印到`System.out`里。這意味著它們的`toString`方法會得到自動調用。這樣便產生了:
```
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]
```
大家不久就會看到它們如何與客戶程序做的事情配合。
程序的下一部分看來似乎僅僅是打開文件,以便讀取和寫入,只是`InputStream`和`OutputStream`是從`Socket`對象創建的。利用兩個“轉換器”類I`nputStreamReader`和`OutputStreamWriter`,`InputStream`和`OutputStream`對象已經分別轉換成為Java 1.1的`Reader`和`Writer`對象。也可以直接使用Java1.0的`InputStream`和`OutputStream`類,但對輸出來說,使用`Writer`方式具有明顯的優勢。這一優勢是通過`PrintWriter`表現出來的,它有一個重載的構造器,能獲取第二個參數——一個布爾值標志,指向是否在每一次`println()`結束的時候自動刷新輸出(但不適用于`print()`語句)。每次寫入了輸出內容后(寫進`out`),它的緩沖區必須刷新,使信息能正式通過網絡傳遞出去。對目前這個例子來說,刷新顯得尤為重要,因為客戶和服務器在采取下一步操作之前都要等待一行文本內容的到達。若刷新沒有發生,那么信息不會進入網絡,除非緩沖區滿(溢出),這會為本例帶來許多問題。
編寫網絡應用程序時,需要特別注意自動刷新機制的使用。每次刷新緩沖區時,必須創建和發出一個數據包(數據封)。就目前的情況來說,這正是我們所希望的,因為假如包內包含了還沒有發出的文本行,服務器和客戶端之間的相互“握手”就會停止。換句話說,一行的末尾就是一條消息的末尾。但在其他許多情況下,消息并不是用行分隔的,所以不如不用自動刷新機制,而用內建的緩沖區判決機制來決定何時發送一個數據包。這樣一來,我們可以發出較大的數據包,而且處理進程也能加快。
注意和我們打開的幾乎所有數據流一樣,它們都要進行緩沖處理。本章末尾有一個練習,清楚展現了假如我們不對數據流進行緩沖,那么會得到什么樣的后果(速度會變慢)。
無限`while`循環從`BufferedReader in`內讀取文本行,并將信息寫入`System.out`,然后寫入`PrintWriter.out`。注意這可以是任何數據流,它們只是在表面上同網絡連接。
客戶程序發出包含了`"END"`的行后,程序會中止循環,并關閉`Socket`。
下面是客戶程序的源碼:
```
//: JabberClient.java
// Very simple client that just sends
// lines to the server and reads lines
// that the server sends.
import java.net.*;
import java.io.*;
public class JabberClient {
public static void main(String[] args)
throws IOException {
// Passing null to getByName() produces the
// special "Local Loopback" IP address, for
// testing on one machine w/o a network:
InetAddress addr =
InetAddress.getByName(null);
// Alternatively, you can use
// the address or name:
// InetAddress addr =
// InetAddress.getByName("127.0.0.1");
// InetAddress addr =
// InetAddress.getByName("localhost");
System.out.println("addr = " + addr);
Socket socket =
new Socket(addr, JabberServer.PORT);
// Guard everything in a try-finally to make
// sure that the socket is closed:
try {
System.out.println("socket = " + socket);
BufferedReader in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by PrintWriter:
PrintWriter out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())),true);
for(int i = 0; i < 10; i ++) {
out.println("howdy " + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} finally {
System.out.println("closing...");
socket.close();
}
}
} ///:~
```
在`main()`中,大家可看到獲得本地主機IP地址的`InetAddress`的三種途徑:使用`null`,使用`localhost`,或者直接使用保留地址`127.0.0.1`。當然,如果想通過網絡同一臺遠程主機連接,也可以換用那臺機器的IP地址。打印出`InetAddress addr`后(通過對`toString()`方法的自動調用),結果如下:
```
localhost/127.0.0.1
```
通過向`getByName()`傳遞一個`null`,它會默認尋找`localhost`,并生成特殊的保留地址`127.0.0.1`。注意在名為`socket`的套接字創建時,同時使用了`InetAddress`以及端口號。打印這樣的某個`Socket`對象時,為了真正理解它的含義,請記住一次獨一無二的因特網連接是用下述四種數據標識的:`clientHost`(客戶主機)、`clientPortNumber`(客戶端口號)、`serverHost`(服務主機)以及`serverPortNumber`(服務端口號)。服務程序啟動后,會在本地主機(`127.0.0.1`)上建立為它分配的端口(8080)。一旦客戶程序發出請求,機器上下一個可用的端口就會分配給它(這種情況下是1077),這一行動也在與服務程序相同的機器(`127.0.0.1`)上進行。現在,為了使數據能在客戶及服務程序之間來回傳送,每一端都需要知道把數據發到哪里。所以在同一個“已知”服務程序連接的時候,客戶會發出一個“返回地址”,使服務器程序知道將自己的數據發到哪兒。我們在服務器端的示范輸出中可以體會到這一情況:
```
Socket[addr=127.0.0.1,port=1077,localport=8080]
```
這意味著服務器剛才已接受了來自`127.0.0.1`這臺機器的端口1077的連接,同時監聽自己的本地端口(8080)。而在客戶端:
```
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
```
這意味著客戶已用自己的本地端口1077與`127.0.0.1`機器上的端口8080建立了 連接。
大家會注意到每次重新啟動客戶程序的時候,本地端口的編號都會增加。這個編號從1025(剛好在系統保留的1-1024之外)開始,并會一直增加下去,除非我們重啟機器。若重新啟動機器,端口號仍然會從1025開始自增(在Unix機器中,一旦超過保留的套按字范圍,數字就會再次從最小的可用數字開始)。
創建好`Socket`對象后,將其轉換成`BufferedReader`和`PrintWriter的`過程便與在服務器中相同(同樣地,兩種情況下都要從一個`Socket`開始)。在這里,客戶通過發出字符串`"howdy"`,并在后面跟隨一個數字,從而初始化通信。注意緩沖區必須再次刷新(這是自動發生的,通過傳遞給`PrintWriter`構造器的第二個參數)。若緩沖區沒有刷新,那么整個會話(通信)都會被掛起,因為用于初始化的`"howdy"`永遠不會發送出去(緩沖區不夠滿,不足以造成發送動作的自動進行)。從服務器返回的每一行都會寫入`System.out`,以驗證一切都在正常運轉。為中止會話,需要發出一個`"END"`。若客戶程序簡單地掛起,那么服務器會“拋”出一個異常。
大家在這里可以看到我們采用了同樣的措施來確保由`Socket`代表的網絡資源得到正確的清除,這是用一個`try-finally`塊實現的。
套接字建立了一個“專用”連接,它會一直持續到明確斷開連接為止(專用連接也可能間接性地斷開,前提是某一端或者中間的某條鏈路出現故障而崩潰)。這意味著參與連接的雙方都被鎖定在通信中,而且無論是否有數據傳遞,連接都會連續處于開放狀態。從表面看,這似乎是一種合理的連網方式。然而,它也為網絡帶來了額外的開銷。本章后面會介紹進行連網的另一種方式。采用那種方式,連接的建立只是暫時的。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物