Java網(wǎng)絡(luò)編程
Java對于網(wǎng)絡(luò)通訊有著非常強(qiáng)大的支持。不僅可以獲取網(wǎng)絡(luò)資源,傳遞參數(shù)到遠(yuǎn)程服務(wù)器,還可以通過Socket對象實(shí)現(xiàn)TCP協(xié)議,通過DatagramSocket對象實(shí)現(xiàn)UDP協(xié)議。同時,對于多點(diǎn)廣播以及代理服務(wù)器也有著非常強(qiáng)大的支持。以下是本人在學(xué)習(xí)過程中的總結(jié)和歸納。
1. Java的基本網(wǎng)絡(luò)支持
1.1 InetAddress
Java中的InetAddress是一個代表IP地址的對象。IP地址可以由字節(jié)數(shù)組和字符串來分別表示,InetAddress將IP地址以對象的形式進(jìn)行封裝,可以更方便的操作和獲取其屬性。InetAddress沒有構(gòu)造方法,可以通過兩個靜態(tài)方法獲得它的對象。代碼如下:
-
- InetAddress ip = InetAddress.getByName("www.oneedu.cn");
-
- System.out.println("oneedu是否可達(dá):" + ip.isReachable(2000));
-
- System.out.println(ip.getHostAddress());
-
- InetAddress local = InetAddress.getByAddress(new byte[]
- {127,0,0,1});
- System.out.println("本機(jī)是否可達(dá):" + local.isReachable(5000));
-
- System.out.println(local.getCanonicalHostName());
|
1.2 URLDecoder和URLEncoder 這兩個類可以別用于將application/x-www-form-urlencoded MIME類型的字符串轉(zhuǎn)換為普通字符串,將普通字符串轉(zhuǎn)換為這類特殊型的字符串。使用URLDecoder類的靜態(tài)方法decode()用于解碼,URLEncoder類的靜態(tài)方法encode()用于編碼。具體使用方法如下。
-
-
- String keyWord = URLDecoder.decode(
- "%E6%9D%8E%E5%88%9A+j2ee", "UTF-8");
- System.out.println(keyWord);
-
-
- String urlStr = URLEncoder.encode(
- "ROR敏捷開發(fā)最佳指南" , "GBK");
- System.out.println(urlStr);
|
1.3 URL和URLConnection URL可以被認(rèn)為是指向互聯(lián)網(wǎng)資源的“指針”,通過URL可以獲得互聯(lián)網(wǎng)資源相關(guān)信息,包括獲得URL的InputStream對象獲取資源的信息,以及一個到URL所引用遠(yuǎn)程對象的連接URLConnection。 URLConnection對象可以向所代表的URL發(fā)送請求和讀取URL的資源。通常,創(chuàng)建一個和URL的連接,需要如下幾個步驟: a. 創(chuàng)建URL對象,并通過調(diào)用openConnection方法獲得URLConnection對象; b. 設(shè)置URLConnection參數(shù)和普通請求屬性; c. 向遠(yuǎn)程資源發(fā)送請求; d. 遠(yuǎn)程資源變?yōu)榭捎茫绦蚩梢栽L問遠(yuǎn)程資源的頭字段和通過輸入流來讀取遠(yuǎn)程資源返回的信息。 這里需要重點(diǎn)討論一下第三步:如果只是發(fā)送GET方式請求,使用connect方法建立和遠(yuǎn)程資源的連接即可;如果是需要發(fā)送POST方式的請求,則需要獲取URLConnection對象所對應(yīng)的輸出流來發(fā)送請求。這里需要注意的是,由于GET方法的參數(shù)傳遞方式是將參數(shù)顯式追加在地址后面,那么在構(gòu)造URL對象時的參數(shù)就應(yīng)當(dāng)是包含了參數(shù)的完整URL地址,而在獲得了URLConnection對象之后,就直接調(diào)用connect方法即可發(fā)送請求。而POST方法傳遞參數(shù)時僅僅需要頁面URL,而參數(shù)通過需要通過輸出流來傳遞。另外還需要設(shè)置頭字段。以下是兩種方式的代碼。
-
- String urlName = url + "?" + param;
- URL realUrl = new URL(urlName);
-
- URLConnection conn = realUrl.openConnection();
-
- conn.setRequestProperty("accept", "*/*");
- conn.setRequestProperty("connection", "Keep-Alive");
- conn.setRequestProperty("user-agent",
- "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
-
- conn.connect();
-
-
- URL realUrl = new URL(url);
-
- URLConnection conn = realUrl.openConnection();
-
- conn.setRequestProperty("accept", "*/*");
- conn.setRequestProperty("connection", "Keep-Alive");
- conn.setRequestProperty("user-agent",
- "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
-
- conn.setDoOutput(true);
- conn.setDoInput(true);
-
- out = new PrintWriter(conn.getOutputStream());
-
- out.print(param);
-
|
另外需要注意的是,如果既需要讀取又需要發(fā)送,一定要先使用輸出流,再使用輸入流。因?yàn)檫h(yuǎn)程資源不會主動向本地發(fā)送請求,必須要先請求資源。2. 基于TCP協(xié)議的網(wǎng)絡(luò)編程 TCP協(xié)議是一種可靠的通絡(luò)協(xié)議,通信兩端的Socket使得它們之間形成網(wǎng)絡(luò)虛擬鏈路,兩端的程序可以通過虛擬鏈路進(jìn)行通訊。Java使用socket對象代表兩端的通信端口,并通過socket產(chǎn)生的IO流來進(jìn)行網(wǎng)絡(luò)通信。
2.1 ServerSocket
在兩個通信端沒有建立虛擬鏈路之前,必須有一個通信實(shí)體首先主動監(jiān)聽來自另一端的請求。ServerSocket對象使用accept()方法用于監(jiān)聽來自客戶端的Socket連接,如果收到一個客戶端Socket的連接請求,該方法將返回一個與客戶端Socket對應(yīng)的Socket對象。如果沒有連接,它將一直處于等待狀態(tài)。通常情況下,服務(wù)器不應(yīng)只接受一個客戶端請求,而應(yīng)該通過循環(huán)調(diào)用accept()不斷接受來自客戶端的所有請求。
這里需要注意的是,對于多次接收客戶端數(shù)據(jù)的情況來說,一方面可以每次都在客戶端建立一個新的Socket對象然后通過輸入輸出通訊,這樣對于服務(wù)器端來說,每次循環(huán)所接收的內(nèi)容也不一樣,被認(rèn)為是不同的客戶端。另外,也可以只建立一次,然后在這個虛擬鏈路上通信,這樣在服務(wù)器端一次循環(huán)的內(nèi)容就是通信的全過程。
服務(wù)器端的示例代碼:
-
- ServerSocket ss = new ServerSocket(30000);
-
- while (true)
- {
-
- Socket s = ss.accept();
-
- PrintStream ps = new PrintStream(s.getOutputStream());
-
- ps.println("您好,您收到了服務(wù)器的新年祝福!");
-
- ps.close();
- s.close();
- }
|
2.2 Socket
使用Socket可以主動連接到服務(wù)器端,使用服務(wù)器的IP地址和端口號初始化之后,服務(wù)器端的accept便可以解除阻塞繼續(xù)向下執(zhí)行,這樣就建立了一對互相連接的Socket。
客戶端示例代碼:
- Socket socket = new Socket("127.0.0.1" , 30000);
-
- BufferedReader br = new BufferedReader(
- new InputStreamReader(socket.getInputStream()));
-
- String line = br.readLine();
- System.out.println("來自服務(wù)器的數(shù)據(jù):" + line);
-
- br.close();
- socket.close();
|
2.3 使用多線程 在復(fù)雜的通訊中,使用多線程非常必要。對于服務(wù)器來說,它需要接收來自多個客戶端的連接請求,處理多個客戶端通訊需要并發(fā)執(zhí)行,那么就需要對每一個傳過來的Socket在不同的線程中進(jìn)行處理,每條線程需要負(fù)責(zé)與一個客戶端進(jìn)行通信。以防止其中一個客戶端的處理阻塞會影響到其他的線程。對于客戶端來說,一方面要讀取來自服務(wù)器端的數(shù)據(jù),另一方面又要向服務(wù)器端輸出數(shù)據(jù),它們同樣也需要在不同的線程中分別處理。具體代碼如下,服務(wù)器端:
- public class MyServer
- {
-
- public static ArrayList<Socket> socketList = new ArrayList<Socket>();
- public static void main(String[] args)
- throws IOException
- {
- ServerSocket ss = new ServerSocket(30000);
- while(true)
- {
-
- Socket s = ss.accept();
- socketList.add(s);
-
- new Thread(new ServerThread(s)).start();
- }
- }
- }
|
客戶端:
- public class MyClient
- {
- public static void main(String[] args)
- throws IOException
- {
- Socket s = s = new Socket("127.0.0.1" , 30000);
-
- new Thread(new ClientThread(s)).start();
-
- PrintStream ps = new PrintStream(s.getOutputStream());
- String line = null;
-
- BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
- while ((line = br.readLine()) != null)
- {
-
- ps.println(line);
- }
- }
- }
|
2.4 使用協(xié)議字符
協(xié)議字符用于標(biāo)識一些字段的特定功能,用于說明傳輸內(nèi)容的特性。它可以由用戶自定義。一般情況下,可以定義一個存放這些協(xié)議字符的接口。如下:
- public interface YeekuProtocol
- {
-
- int PROTOCOL_LEN = 2;
-
-
- String MSG_ROUND = "§γ";
- String USER_ROUND = "∏∑";
- String LOGIN_SUCCESS = "1";
- String NAME_REP = "-1";
- String PRIVATE_ROUND = "★【";
- String SPLIT_SIGN = "※";
- }
|
在字段時可以加上這些字符,如下代碼:
- while(true)
- {
- String userName = JOptionPane.showInputDialog(tip + "輸入用戶名");
-
- ps.println(YeekuProtocol.USER_ROUND + userName
- + YeekuProtocol.USER_ROUND);
-
- String result = brServer.readLine();
-
- if (result.equals(YeekuProtocol.NAME_REP))
- {
- tip = "用戶名重復(fù)!請重新";
- continue;
- }
-
- if (result.equals(YeekuProtocol.LOGIN_SUCCESS))
- {
- break;
- }
- }
|
收到發(fā)送來的字段時候,也再次拆分成所需要的部分,如下代碼:
- if (line.startsWith(YeekuProtocol.PRIVATE_ROUND)
- && line.endsWith(YeekuProtocol.PRIVATE_ROUND))
- {
-
- String userAndMsg = getRealMsg(line);
-
- String user = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[0];
- String msg = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[1];
-
- Server.clients.get(user).println(
- Server.clients.getKeyByValue(ps) + "悄悄地對你說:" + msg);
- }
|
3. UDP協(xié)議的網(wǎng)絡(luò)編程 UDP協(xié)議是一種不可靠的網(wǎng)絡(luò)協(xié)議,它在通訊實(shí)例的兩端個建立一個Socket,但這兩個Socket之間并沒有虛擬鏈路,這兩個Socket只是發(fā)送和接受數(shù)據(jù)報(bào)的對象,Java提供了DatagramSocket對象作為基于UDP協(xié)議的Socket,使用DatagramPacket代表DatagramSocket發(fā)送和接收的數(shù)據(jù)報(bào)。
3.1 使用DatagramSocket發(fā)送、接收數(shù)據(jù)
DatagramSocket本身并不負(fù)責(zé)維護(hù)狀態(tài)和產(chǎn)生IO流。它僅僅負(fù)責(zé)接收和發(fā)送數(shù)據(jù)報(bào)。使用receive(DatagramPacket p)方法接收,使用send(DatagramPacket p)方法發(fā)送。
這里需要首先明確的是,DatagramPacket對象的構(gòu)造。DatagramPacket的內(nèi)部實(shí)際上采用了一個字節(jié)型數(shù)組來保存數(shù)據(jù),它的初始化方法如下:
-
- Private DatagaramSocket udpSocket=new DatagaramSocket(buf,buf.length);
-
- Private DatagaramSocket udpSocket=new DatagaramSocket(buf,buf.length,IP,PORT);
- udpSocket。setData(outBuf);
|
作為這兩個方法的參數(shù),作用和構(gòu)造不同的。作為接收方法中的參數(shù),DatagramPacket中的數(shù)組一個空的數(shù)組,用來存放接收到的DatagramPacket對象中的數(shù)組;而作為發(fā)送方法參數(shù),DatagramPacket本身含有了目的端的IP和端口,以及存儲了要發(fā)送內(nèi)容的指定了長度的字節(jié)型數(shù)組。 另外,DatagramPacket對象還提供了setData(Byte[] b)和Byte[] b= getData()方法,用于設(shè)置DatagramPacket中包含的數(shù)組內(nèi)容和獲得其中包含數(shù)組的內(nèi)容。 使用TCP和UDP通訊的編碼區(qū)別: a. 在TCP中,目標(biāo)IP和端口由Socket指定包含;UDP中,目標(biāo)IP由DatagramPacket包含指定,DatagramSocket只負(fù)責(zé)發(fā)送和接受。 b. 在TCP中,通訊是通過Socket獲得的IO流來實(shí)現(xiàn);在UDP中,則通過DatagramSocket的send和receive方法。
3.2 使用MulticastSocket實(shí)現(xiàn)多點(diǎn)廣播
MulticastSocket是DatagramSocket的子類,可以將數(shù)據(jù)報(bào)以廣播形式發(fā)送到數(shù)量不等的多個客戶端。實(shí)現(xiàn)策略就是定義一個廣播地址,使得每個MulticastSocket都加入到這個地址中。從而每次使用MulticastSocket發(fā)送數(shù)據(jù)報(bào)(包含的廣播地址)時,所有加入了這個廣播地址的MulticastSocket對象都可以收到信息。
MulticastSocket的初始化需要傳遞端口號作為參數(shù),特別對于需要接受信息的端來說,它的端口號需要與發(fā)送端數(shù)據(jù)報(bào)中包含的端口號一致。具體代碼如下:
-
-
- socket = new MulticastSocket(BROADCAST_PORT);
- broadcastAddress = InetAddress.getByName(BROADCAST_IP);
-
- socket.joinGroup(broadcastAddress);
-
- socket.setLoopbackMode(false);
-
- outPacket = new DatagramPacket(new byte[0] , 0 ,
- broadcastAddress , BROADCAST_PORT);
|
4. 使用代理服務(wù)器 Java中可以使用Proxy直接創(chuàng)建連接代理服務(wù)器,具體使用方法如下:
- public class ProxyTest
- {
- Proxy proxy;
- URL url;
- URLConnection conn;
-
- Scanner scan;
- PrintStream ps ;
-
-
- String proxyAddress = "202.128.23.32";
- int proxyPort;
-
- String urlStr = "http://www.oneedu.cn";
-
- public void init()
- {
- try
- {
- url = new URL(urlStr);
-
- proxy = new Proxy(Proxy.Type.HTTP,
- new InetSocketAddress(proxyAddress , proxyPort));
-
- conn = url.openConnection(proxy);
-
- conn.setConnectTimeout(5000);
- scan = new Scanner(conn.getInputStream());
-
- ps = new PrintStream("Index.htm");
- while (scan.hasNextLine())
- {
- String line = scan.nextLine();
-
- System.out.println(line);
-
- ps.println(line);
- }
- }
- catch(MalformedURLException ex)
- {
- System.out.println(urlStr + "不是有效的網(wǎng)站地址!");
- }
- catch(IOException ex)
- {
- ex.printStackTrace();
- }
-
- finally
- {
- if (ps != null)
- {
- ps.close();
- }
- }
- }
-
- }
|
5. 編碼中的問題總結(jié)
a. 雙方初始化套接字以后,就等于建立了鏈接,表示雙方互相可以知曉對方的狀態(tài)。服務(wù)器端可以調(diào)用接收到的客戶端套接字進(jìn)行輸入輸出流操作,客戶端可以調(diào)用自身內(nèi)部的套接字對象進(jìn)行輸入輸出操作。這樣可以保持輸入輸出的流暢性。例如,客戶端向服務(wù)器端發(fā)送消息時,可以隔一段的時間輸入一段信息,然后服務(wù)器端使用循環(huán)不斷的讀取傳過來的輸入流。
b. 對于可能出現(xiàn)阻塞的方法,例如客戶端進(jìn)行循環(huán)不斷讀取來自服務(wù)器端的響應(yīng)信息時,如果此時服務(wù)器端并沒有向客戶端進(jìn)行輸出,那么讀取的方法將處于阻塞狀態(tài),直到收到信息為止才向下執(zhí)行代碼。那么對于這樣容易產(chǎn)生阻塞的代碼,就需要將它放在一個單獨(dú)的線程中處理。
c. 有一些流是順承的。例如,服務(wù)器端在收到客戶端的消息以后,就將消息再通過輸出流向其他所有服務(wù)器發(fā)送。那么,這個來自客戶端的輸入流和發(fā)向客戶端的輸出流就是順接的關(guān)系,不必對它們分在兩個不同的線程。
d. println()方法對應(yīng)readLine()。
e. 在JFrame類中,一般不要將自己的代碼寫進(jìn)main方法中,可以將代碼寫到自定義的方法中,然后在main方法中調(diào)用。