java網絡編程之簡單客戶端服務器
用Java開發(fā)網絡軟件非常方便和強大,Java的這種力量來源于他獨有的一套強大的用于網絡的 API,這些API是一系列的類和接口,均位于包和中。在這篇文章中我們將介紹套接字(Socket)慨念,同時以實例說明如何使用Network API操縱套接字,在完成本文后,你就可以編寫網絡低端通訊軟件。
什么是套接字(Socket)?
Network API是典型的用于基于TCP/IP網絡Java程序與其他程序通訊,Network API依靠Socket進行通訊。Socket可以看成在兩個程序進行通訊連接中的一個端點,一個程序將一段信息寫入Socket中,該Socket將這段信息發(fā)送給另外一個Socket中,使這段信息能傳送到其他程序中。如圖1
我們來分析一下圖1,Host A上的程序A將一段信息寫入Socket中,Socket的內容被Host A的網絡管理軟件訪問,并將這段信息通過Host A的網絡接口卡發(fā)送到Host B,Host B的網絡接口卡接收到這段信息后,傳送給Host B的網絡管理軟件,網絡管理軟件將這段信息保存在Host B的Socket中,然后程序B才能在Socket中閱讀這段信息。
假設在圖1的網絡中添加第三個主機Host C,那么Host A怎么知道信息被正確傳送到Host B而不是被傳送到Host C中了呢?基于TCP/IP網絡中的每一個主機均被賦予了一個唯一的IP地址,IP地址是一個32位的無符號整數(shù),由于沒有轉變成二進制,因此通常以小數(shù)點分隔,如:198.163.227.6,正如所見IP地址均由四個部分組成,每個部分的范圍都是0-255,以表示8位地址。
值得注意的是IP地址都是32位地址,這是IP協(xié)議版本4(簡稱Ipv4)規(guī)定的,目前由于IPv4地址已近耗盡,所以IPv6地址正逐漸代替Ipv4地址,Ipv6地址則是128位無符號整數(shù)。
假設第二個程序被加入圖1的網絡的Host B中,那么由Host A傳來的信息如何能被正確的傳給程序B而不是傳給新加入的程序呢?這是因為每一個基于TCP/IP網絡通訊的程序都被賦予了唯一的端口和端口號,端口是一個信息緩沖區(qū),用于保留Socket中的輸入/輸出信息,端口號是一個16位無符號整數(shù),范圍是0-65535,以區(qū)別主機上的每一個程序(端口號就像房屋中的房間號),低于256的短口號保留給標準應用程序,比如pop3的端口號就是110,每一個套接字都組合進了IP地址、端口、端口號,這樣形成的整體就可以區(qū)別每一個套接字t,下面我們就來談談兩種套接字:流套接字和自尋址數(shù)據(jù)套接字。
流套接字(Stream Socket)
無論何時,在兩個網絡應用程序之間發(fā)送和接收信息時都需要建立一個可靠的連接,流套接字依靠TCP協(xié)議來保證信息正確到達目的地,實際上,IP包有可能在網絡中丟失或者在傳送過程中發(fā)生錯誤,任何一種情況發(fā)生,作為接受方的 TCP將聯(lián)系發(fā)送方TCP重新發(fā)送這個IP包。這就是所謂的在兩個流套接字之間建立可靠的連接。
流套接字在C/S程序中扮演一個必需的角色,客戶機程序(需要訪問某些服務的網絡應用程序)創(chuàng)建一個扮演服務器程序的主機的IP地址和服務器程序(為客戶端應用程序提供服務的網絡應用程序)的端口號的流套接字對象。
客戶端流套接字的初始化代碼將IP地址和端口號傳遞給客戶端主機的網絡管理軟件,管理軟件將IP地址和端口號通過NIC傳遞給服務器端主機;服務器端主機讀到經過NIC傳遞來的數(shù)據(jù),然后查看服務器程序是否處于監(jiān)聽狀態(tài),這種監(jiān)聽依然是通過套接字和端口來進行的;如果服務器程序處于監(jiān)聽狀態(tài),那么服務器端網絡管理軟件就向客戶機網絡管理軟件發(fā)出一個積極的響應信號,接收到響應信號后,客戶端流套接字初始化代碼就給客戶程序建立一個端口號,并將這個端口號傳遞給服務器程序的套接字(服務器程序將使用這個端口號識別傳來的信息是否是屬于客戶程序)同時完成流套接字的初始化。
如果服務器程序沒有處于監(jiān)聽狀態(tài),那么服務器端網絡管理軟件將給客戶端傳遞一個消極信號,收到這個消極信號后,客戶程序的流套接字初始化代碼將拋出一個異常對象并且不建立通訊連接,也不創(chuàng)建流套接字對象。這種情形就像打電話一樣,當有人的時候通訊建立,否則電話將被掛起。
這部分的工作包括了相關聯(lián)的三個類:InetAddress, Socket, 和 ServerSocket。 InetAddress對象描繪了32位或128位IP地址,Socket對象代表了客戶程序流套接字,ServerSocket代表了服務程序流套接字,所有這三個類均位于包中。
InetAddress類
InetAddress類在網絡API套接字編程中扮演了一個重要角色。參數(shù)傳遞給流套接字類和自尋址套接字類構造器或非構造器方法。InetAddress描述了32位或64位IP地址,要完成這個功能,InetAddress類主要依靠兩個支持類Inet4Address 和 Inet6Address,這三個類是繼承關系,InetAddrress是父類,Inet4Address 和 Inet6Address是子類。
由于InetAddress類只有一個構造函數(shù),而且不能傳遞參數(shù),所以不能直接創(chuàng)建InetAddress對象,比如下面的做法就是錯誤的:
InetAddress ia = new InetAddress ();
但我們可以通過下面的5個工廠方法創(chuàng)建來創(chuàng)建一個InetAddress對象或InetAddress數(shù)組:
. getAllByName(String host)方法返回一個InetAddress對象的引用,每個對象包含一個表示相應主機名的單獨的IP地址,這個IP地址是通過host參數(shù)傳遞的,對于指定的主機如果沒有IP地址存在那么這個方法將拋出一個UnknownHostException 異常對象。
. getByAddress(byte [] addr)方法返回一個InetAddress對象的引用,這個對象包含了一個Ipv4地址或Ipv6地址,Ipv4地址是一個4字節(jié)數(shù)組,Ipv6地址是一個16字節(jié)地址數(shù)組,如果返回的數(shù)組既不是4字節(jié)的也不是16字節(jié)的,那么方法將會拋出一個UnknownHostException異常對象。
. getByAddress(String host, byte [] addr)方法返回一個InetAddress對象的引用,這個InetAddress對象包含了一個由host和4字節(jié)的addr數(shù)組指定的IP地址,或者是host和16字節(jié)的addr數(shù)組指定的IP地址,如果這個數(shù)組既不是4字節(jié)的也不是16位字節(jié)的,那么該方法將拋出一個UnknownHostException異常對象。
. getByName(String host)方法返回一個InetAddress對象,該對象包含了一個與host參數(shù)指定的主機相對應的IP地址,對于指定的主機如果沒有IP地址存在,那么方法將拋出一個UnknownHostException異常對象。
. getLocalHost()方法返回一個InetAddress對象,這個對象包含了本地機的IP地址,考慮到本地主機既是客戶程序主機又是服務器程序主機,為避免混亂,我們將客戶程序主機稱為客戶主機,將服務器程序主機稱為服務器主機。
上面講到的方法均提到返回一個或多個InetAddress對象的引用,實際上每一個方法都要返回一個或多個Inet4Address/Inet6Address對象的引用,調用者不需要知道引用的子類型,相反調用者可以使用返回的引用調用InetAddress對象的非靜態(tài)方法,包括子類型的多態(tài)以確保重載方法被調用。
InetAddress和它的子類型對象處理主機名到主機IPv4或IPv6地址的轉換,要完成這個轉換需要使用域名系統(tǒng),下面的代碼示范了如何通過調用getByName(String host)方法獲得InetAddress子類對象的方法,這個對象包含了與host參數(shù)相對應的IP地址:
InetAddress ia = InetAddress.getByName (""));
一但獲得了InetAddress子類對象的引用就可以調用InetAddress的各種方法來獲得InetAddress子類對象中的IP地址信息,比如,可以通過調用getCanonicalHostName()從域名服務中獲得標準的主機名;getHostAddress()獲得IP地址,getHostName()獲得主機名,isLoopbackAddress()判斷IP地址是否是一個loopback地址。
Socket類
當客戶程序需要與服務器程序通訊的時候,客戶程序在客戶機創(chuàng)建一個socket對象,Socket類有幾個構造函數(shù)。兩個常用的構造函數(shù)是 Socket(InetAddress addr, int port) 和 Socket(String host, int port),兩個構造函數(shù)都創(chuàng)建了一個基于Socket的連接服務器端流套接字的流套接字。對于第一個InetAddress子類對象通過addr參數(shù)獲得服務器主機的IP地址,對于第二個函數(shù)host參數(shù)包被分配到InetAddress對象中,如果沒有IP地址與host參數(shù)相一致,那么將拋出UnknownHostException異常對象。兩個函數(shù)都通過參數(shù)port獲得服務器的端口號。假設已經建立連接了,網絡API將在客戶端基于Socket的流套接字中捆綁客戶程序的IP地址和任意一個端口號,否則兩個函數(shù)都會拋出一個IOException對象。
如果創(chuàng)建了一個Socket對象,那么它可能通過調用Socket的 getInputStream()方法從服務程序獲得輸入流讀傳送來的信息,也可能通過調用Socket的 getOutputStream()方法獲得輸出流來發(fā)送消息。在讀寫活動完成之后,客戶程序調用close()方法關閉流和流套接字,下面的代碼創(chuàng)建了一個服務程序主機地址為198.163.227.6,端口號為13的Socket對象,然后從這個新創(chuàng)建的Socket對象中讀取輸入流,然后再關閉流和Socket對象。
Socket s = new Socket ("198.163.227.6", 13);
InputStream is = s.getInputStream ();
// Read from the stream.
is.close ();
s.close ();
ServerSocket類
由于SSClient使用了流套接字,所以服務程序也要使用流套接字。這就要創(chuàng)建一個ServerSocket對象,ServerSocket有幾個構造函數(shù),最簡單的是ServerSocket(int port),當使用ServerSocket(int port)創(chuàng)建一個ServerSocket對象,port參數(shù)傳遞端口號,這個端口就是服務器監(jiān)聽連接請求的端口,如果在這時出現(xiàn)錯誤將拋出IOException異常對象,否則將創(chuàng)建ServerSocket對象并開始準備接收連接請求。
接下來服務程序進入無限循環(huán)之中,無限循環(huán)從調用ServerSocket的accept()方法開始,在調用開始后accept()方法將導致調用線程阻塞直到連接建立。在建立連接后accept()返回一個最近創(chuàng)建的Socket對象,該Socket對象綁定了客戶程序的IP地址或端口號。
由于存在單個服務程序與多個客戶程序通訊的可能,所以服務程序響應客戶程序不應該花很多時間,否則客戶程序在得到服務前有可能花很多時間來等待通訊的建立,然而服務程序和客戶程序的會話有可能是很長的(這與電話類似),因此為加快對客戶程序連接請求的響應,典型的方法是服務器主機運行一個后臺線程,這個后臺線程處理服務程序和客戶程序的通訊。
下面給出一個客戶端和服務器的程序,客戶端向服務器發(fā)送數(shù)據(jù),服務器接收數(shù)據(jù)并用它生成一個結果,然后將結果返回給客戶端,并在控制臺上顯示。
服務器代碼
[java]
1. package Server;
2.
3. import java.io.*;
4. import .*;
5. import java.util.*;
6. import java.awt.*;
7. import javax.swing.*;
8.
9. public class Server extends JFrame {
10. // Text area for displaying contents
11. private JTextArea jta = new JTextArea();
12.
13. public static void main(String[] args) {
14. new Server();
15. }
16.
17. public Server() {
18. // Place text area on the frame
19. getContentPane().setLayout(new BorderLayout());
20. getContentPane().add(new JScrollPane(jta), BorderLayout.CENTER);
21.
22. setTitle("Server");
23. setSize(500, 300);
24. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
25. setVisible(true); // It is necessary to show the frame here!
26.
27. try {
28. // Create a server socket
29. ServerSocket serverSocket = new ServerSocket(8000);
30. jta.append("Server started at " + new Date() + '\n');
31.
32. // Listen for a connection request
33. Socket socket = serverSocket.accept();
34.
35. // Create data input and output streams
36. DataInputStream inputFromClient = new DataInputStream(
37. socket.getInputStream());
38. DataOutputStream outputToClient = new DataOutputStream(
39. socket.getOutputStream());
40.
41. while (true) {
42. // Receive radius from the client
43. double radius = inputFromClient.readDouble();
44.
45. // Compute area
46. double area = radius * radius * Math.PI;
47.
48. // Send area back to the client
49. outputToClient.writeDouble(area);
50.
51. jta.append("Radius received from client: " + radius + '\n');
52. jta.append("Area found: " + area + '\n');
53. }
54. }
55. catch(IOException ex) {
56. System.err.println(ex);
57. }
58. }
59. }
客戶端程序
[java]
1. package Client;
2.
3. import java.io.*;
4. import .*;
5. import java.awt.*;
6. import java.awt.event.*;
7. import javax.swing.*;
8.
9. public class Client extends JFrame implements ActionListener {
10. // Text field for receiving radius
11. private JTextField jtf = new JTextField();
12.
13. // Text area to display contents
14. private JTextArea jta = new JTextArea();
15.
16. // IO streams
17. private DataOutputStream outputToServer;
18. private DataInputStream inputFromServer;
19.
20. public static void main(String[] args) {
21. new Client();
22. }
23.
24. public Client() {
25. // Panel p to hold the label and text field
26. JPanel p = new JPanel();
27. p.setLayout(new BorderLayout());
28. p.add(new JLabel("Enter radius"), BorderLayout.WEST);
29. p.add(jtf, BorderLayout.CENTER);
30. jtf.setHorizontalAlignment(JTextField.RIGHT);
31.
32. getContentPane().setLayout(new BorderLayout());
33. getContentPane().add(p, BorderLayout.NORTH);
34. getContentPane().add(new JScrollPane(jta), BorderLayout.CENTER);
35.
36. jtf.addActionListener(this); // Register listener
37.
38. setTitle("Client");
39. setSize(500, 300);
40. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
41. setVisible(true); // It is necessary to show the frame here!
42.
43. try {
44. // Create a socket to connect to the server
45. Socket socket = new Socket("localhost", 8000);
46. // Socket socket = new Socket("130.254.204.36", 8000);
47. // Socket socket = new Socket("drake.Armstrong.edu", 8000);
48.
49. // Create an input stream to receive data from the server
50. inputFromServer = new DataInputStream(
51. socket.getInputStream());
52.
53. // Create an output stream to send data to the server
54. outputToServer =
55. new DataOutputStream(socket.getOutputStream());
56. }
57. catch (IOException ex) {
58. jta.append(ex.toString() + '\n');
59. }
60. }
61.
62. public void actionPerformed(ActionEvent e) {
63. String actionCommand = e.getActionCommand();
64. if (e.getSource() instanceof JTextField) {
65. try {
66. // Get the radius from the text field
67. double radius = Double.parseDouble(jtf.getText().trim());
68.
69. // Send the radius to the server
70. outputToServer.writeDouble(radius);
71. outputToServer.flush();
72.
73. // Get area from the server
74. double area = inputFromServer.readDouble();
75.
76. // Display to the text area
77. jta.append("Radius is " + radius + "\n");
78. jta.append("Area received from the server is "
79. + area + '\n');
80. }
81. catch (IOException ex) {
82. System.err.println(ex);
83. }
84. }
85. }
86. }
運行結果如下如所
在做一個服務器服務多個客戶端時,可以簡單的為買個客戶端創(chuàng)建一個線程。相互獨立的線程和指定的客戶端進行通信,每個線程創(chuàng)建數(shù)據(jù)輸入輸出流向客戶端發(fā)送接收數(shù)據(jù)。
多線程服務器端的代碼如下:
[java]
1. package MultiThreadServer;
2.
3. import java.io.*;
4. import .*;
5. import java.util.*;
6. import java.awt.*;
7. import javax.swing.*;
8.
9. public class MultiThreadServer extends JFrame {
10. // Text area for displaying contents
11. private JTextArea jta = new JTextArea();
12.
13. public static void main(String[] args) {
14. new MultiThreadServer();
15. }
16.
17. public MultiThreadServer() {
18. // Place text area on the frame
19. getContentPane().setLayout(new BorderLayout());
20. getContentPane().add(new JScrollPane(jta), BorderLayout.CENTER);
21.
22. setTitle("MultiThreadServer");
23. setSize(500, 300);
24. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
25. setVisible(true); // It is necessary to show the frame here!
26.
27. try {
28. // Create a server socket
29. ServerSocket serverSocket = new ServerSocket(8000);
30. jta.append("MultiThreadServer started at " + new Date() + '\n');
31.
32. // Number a client
33. int clientNo = 1;
34.
35. while (true) {
36. // Listen for a new connection request
37. Socket socket = serverSocket.accept();
38.
39. // Display the client number
40. jta.append("Starting thread for client " + clientNo +
41. " at " + new Date() + '\n');
42.
43. // Find the client's host name, and IP address
44. InetAddress inetAddress = socket.getInetAddress();
45. jta.append("Client " + clientNo + "'s host name is "
46. + inetAddress.getHostName() + "\n");
47. jta.append("Client " + clientNo + "'s IP Address is "
48. + inetAddress.getHostAddress() + "\n");
49.
50. // Create a new thread for the connection
51. HandleAClient thread = new HandleAClient(socket);
52.
53. // Start the new thread
54. thread.start();
55.
56. // Increment clientNo
57. clientNo++;
58. }
59. }
60. catch(IOException ex) {
61. System.err.println(ex);
62. }
63. }
64.
65. // Inner class
66. // Define the thread class for handling new connection
67. class HandleAClient extends Thread {
68. private Socket socket; // A connected socket
69.
70. /** Construct a thread */
71. public HandleAClient(Socket socket) {
72. this.socket = socket;
73. }
74.
75. /** Run a thread */
76. public void run() {
77. try {
78. // Create data input and output streams
79. DataInputStream inputFromClient = new DataInputStream(
80. socket.getInputStream());
81. DataOutputStream outputToClient = new DataOutputStream(
82. socket.getOutputStream());
83.
84. // Continuously serve the client
85. while (true) {
86. // Receive radius from the client
87. double radius = inputFromClient.readDouble();
88.
89. // Compute area
90. double area = radius * radius * Math.PI;
91.
92. // Send area back to the client
93. outputToClient.writeDouble(area);
94.
95. jta.append("radius received from client: " +
96. radius + '\n');
97. jta.append("Area found: " + area + '\n');
98. }
99. }
100. catch(IOException e) {
101. System.err.println(e);
102. }
103. }
104. }
105. }
運行結果如下: